mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-08-26 14:05:02 +02:00
android: refactor... stuff (2, see description)
implement buddy (head tracking and heartrate, finally!), added swipe anywhere to go back, and cleaned up app settings a bit.
This commit is contained in:
@@ -9,6 +9,7 @@ plugins {
|
|||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
alias(libs.plugins.room3)
|
alias(libs.plugins.room3)
|
||||||
|
alias(libs.plugins.protobuf)
|
||||||
// alias(libs.plugins.hilt)
|
// alias(libs.plugins.hilt)
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
@@ -31,13 +32,30 @@ room3 {
|
|||||||
schemaDirectory("$projectDir/schemas")
|
schemaDirectory("$projectDir/schemas")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = libs.protobuf.protoc.get().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
generateProtoTasks {
|
||||||
|
all().forEach { task ->
|
||||||
|
task.builtins {
|
||||||
|
create("java")
|
||||||
|
create("kotlin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
optIn.addAll(
|
optIn.addAll(
|
||||||
"androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
"androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||||
"kotlin.uuid.ExperimentalUuidApi",
|
"kotlin.uuid.ExperimentalUuidApi",
|
||||||
"kotlinx.coroutines.FlowPreview",
|
"kotlinx.coroutines.FlowPreview",
|
||||||
"kotlinx.serialization.ExperimentalSerializationApi"
|
"kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
"kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,6 +154,7 @@ dependencies {
|
|||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.foundation.layout)
|
||||||
implementation(libs.androidx.lifecycle.process)
|
implementation(libs.androidx.lifecycle.process)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
@@ -169,8 +188,9 @@ dependencies {
|
|||||||
implementation(libs.androidx.room3.runtime)
|
implementation(libs.androidx.room3.runtime)
|
||||||
ksp(libs.androidx.room3.compiler)
|
ksp(libs.androidx.room3.compiler)
|
||||||
implementation(libs.kotlinx.serialization.cbor)
|
implementation(libs.kotlinx.serialization.cbor)
|
||||||
|
implementation(libs.protobuf.kotlin)
|
||||||
// compileOnly(files("../../../framework-classes.jar"))
|
// compileOnly(files("../../../framework-classes.jar"))
|
||||||
|
implementation(libs.androidx.healthconnect.client)
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutLibraries {
|
aboutLibraries {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "ae2e348e0198b8d871e9e3ed6959ab30",
|
"identityHash": "7a502eac34ab57e1c12ba8f849e172cf",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "AppleEntity",
|
"tableName": "AppleEntity",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "AppSettingsEntity",
|
"tableName": "AppSettingsEntity",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `nightMode` TEXT NOT NULL, `designSystem` TEXT NOT NULL, `debugMode` INTEGER NOT NULL, `bleScanMode` INTEGER NOT NULL, `bleReportDelay` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `nightMode` TEXT NOT NULL, `designSystem` TEXT NOT NULL, `useHighestRefreshRate` INTEGER NOT NULL, `debugMode` INTEGER NOT NULL, `bleScanMode` INTEGER NOT NULL, `bleReportDelay` INTEGER NOT NULL, `swipeAnywhereForBack` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
@@ -62,6 +62,12 @@
|
|||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "useHighestRefreshRate",
|
||||||
|
"columnName": "useHighestRefreshRate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "debugMode",
|
"fieldPath": "debugMode",
|
||||||
"columnName": "debugMode",
|
"columnName": "debugMode",
|
||||||
@@ -79,6 +85,12 @@
|
|||||||
"columnName": "bleReportDelay",
|
"columnName": "bleReportDelay",
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "swipeAnywhereForBack",
|
||||||
|
"columnName": "swipeAnywhereForBack",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
@@ -163,11 +175,41 @@
|
|||||||
"appWidgetId"
|
"appWidgetId"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "HeartRateSampleEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `bpm` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bpm",
|
||||||
|
"columnName": "bpm",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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, 'ae2e348e0198b8d871e9e3ed6959ab30')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7a502eac34ab57e1c12ba8f849e172cf')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,8 @@
|
|||||||
android:name="android.permission.INTERACT_ACROSS_USERS"
|
android:name="android.permission.INTERACT_ACROSS_USERS"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.MODIFY_AUDIO_ROUTING"/>
|
android:name="android.permission.MODIFY_AUDIO_ROUTING"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@
|
|||||||
<!-- android:maxSdkVersion="30" />-->
|
<!-- android:maxSdkVersion="30" />-->
|
||||||
<uses-permission android:name="com.android.vending.BILLING" />
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".LibrePodsApplication"
|
android:name=".LibrePodsApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -154,6 +157,28 @@
|
|||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths" />
|
android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".presentation.activities.PrivacyPolicyActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name="ViewPermissionUsageActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:targetActivity=".presentation.activities.PrivacyPolicyActivity"
|
||||||
|
android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
|
||||||
|
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
<queries>
|
||||||
|
<package android:name="com.google.android.apps.healthdata" />
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package me.kavishdevar.librepods
|
package me.kavishdevar.librepods
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import androidx.health.connect.client.HealthConnectClient
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
@@ -12,7 +13,10 @@ import me.kavishdevar.librepods.billing.BillingProviderFactory
|
|||||||
import me.kavishdevar.librepods.database.LibrePodsDatabase
|
import me.kavishdevar.librepods.database.LibrePodsDatabase
|
||||||
import me.kavishdevar.librepods.repository.AppDataRepository
|
import me.kavishdevar.librepods.repository.AppDataRepository
|
||||||
import me.kavishdevar.librepods.repository.AppleRepository
|
import me.kavishdevar.librepods.repository.AppleRepository
|
||||||
|
import me.kavishdevar.librepods.repository.HeartRateRepository
|
||||||
|
import me.kavishdevar.librepods.repository.RecordingRepository
|
||||||
import me.kavishdevar.librepods.repository.WidgetConfigRepository
|
import me.kavishdevar.librepods.repository.WidgetConfigRepository
|
||||||
|
import me.kavishdevar.librepods.utils.GestureFeedback
|
||||||
import me.kavishdevar.librepods.utils.XposedServiceHolder
|
import me.kavishdevar.librepods.utils.XposedServiceHolder
|
||||||
import me.kavishdevar.librepods.utils.XposedState
|
import me.kavishdevar.librepods.utils.XposedState
|
||||||
|
|
||||||
@@ -24,6 +28,18 @@ class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener
|
|||||||
val appDataRepository by lazy { AppDataRepository(database.appSettingsDao(), database.appStateDao()) }
|
val appDataRepository by lazy { AppDataRepository(database.appSettingsDao(), database.appStateDao()) }
|
||||||
val widgetConfigRepository by lazy { WidgetConfigRepository(database.widgetConfigDao()) }
|
val widgetConfigRepository by lazy { WidgetConfigRepository(database.widgetConfigDao()) }
|
||||||
|
|
||||||
|
val recordingRepository by lazy { RecordingRepository(applicationContext) }
|
||||||
|
val heartRateRepository by lazy { HeartRateRepository(database.heartRateDao()) }
|
||||||
|
|
||||||
|
val healthConnectClient: HealthConnectClient? by lazy {
|
||||||
|
val status = HealthConnectClient.getSdkStatus(this)
|
||||||
|
if (status == HealthConnectClient.SDK_AVAILABLE) {
|
||||||
|
HealthConnectClient.getOrCreate(this)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
System.loadLibrary("hiddenapi")
|
System.loadLibrary("hiddenapi")
|
||||||
|
|
||||||
@@ -37,6 +53,8 @@ class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener
|
|||||||
BillingManager.provider = BillingProviderFactory.create(this)
|
BillingManager.provider = BillingProviderFactory.create(this)
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
|
||||||
|
GestureFeedback.init(this)
|
||||||
|
|
||||||
super<Application>.onCreate()
|
super<Application>.onCreate()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-51
@@ -37,8 +37,11 @@ import me.kavishdevar.librepods.bluetooth.aacp.packet.CustomEqPacket
|
|||||||
import me.kavishdevar.librepods.bluetooth.aacp.packet.EarDetectionResponsePacket
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.EarDetectionResponsePacket
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.packet.InformationPacket
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.InformationPacket
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.packet.MagicKeyResponsePacket
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.MagicKeyResponsePacket
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.RTBuddyPacket
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.packet.RenamePacket
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.RenamePacket
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.packet.StemPressPacket
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.StemPressPacket
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.rtbuddy.proto.SensorDataWX
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.rtbuddy.proto.SensorServiceType
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.AppleEvent
|
import me.kavishdevar.librepods.bluetooth.aacp.types.AppleEvent
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.Capability
|
import me.kavishdevar.librepods.bluetooth.aacp.types.Capability
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.CapabilityEntry
|
import me.kavishdevar.librepods.bluetooth.aacp.types.CapabilityEntry
|
||||||
@@ -48,12 +51,18 @@ import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
|||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.CustomEq
|
import me.kavishdevar.librepods.bluetooth.aacp.types.CustomEq
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.MagicKeyType
|
import me.kavishdevar.librepods.bluetooth.aacp.types.MagicKeyType
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.MessageOpcode
|
import me.kavishdevar.librepods.bluetooth.aacp.types.MessageOpcode
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.RTBuddyDescriptor
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.SensorDataWxBuddyPayload
|
||||||
import me.kavishdevar.librepods.data.audio.MicrophoneFrame
|
import me.kavishdevar.librepods.data.audio.MicrophoneFrame
|
||||||
|
import me.kavishdevar.librepods.data.heartrate.HeartRateSample
|
||||||
import me.kavishdevar.librepods.devices.AppleDevice
|
import me.kavishdevar.librepods.devices.AppleDevice
|
||||||
import me.kavishdevar.librepods.devices.BatteryStatus
|
import me.kavishdevar.librepods.devices.BatteryStatus
|
||||||
|
import me.kavishdevar.librepods.utils.HeadTracking
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class AACPManager(private val device: AppleDevice) {
|
class AACPManager(private val device: AppleDevice) {
|
||||||
@@ -62,6 +71,8 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
|
|
||||||
private var socket: BluetoothSocket? = null
|
private var socket: BluetoothSocket? = null
|
||||||
|
|
||||||
|
private val rtBuddyManager = RTBuddyManager(::sendPacket)
|
||||||
|
|
||||||
fun connect(): Boolean {
|
fun connect(): Boolean {
|
||||||
if (socket != null && socket!!.isConnected) {
|
if (socket != null && socket!!.isConnected) {
|
||||||
Log.i(TAG, "Already connected")
|
Log.i(TAG, "Already connected")
|
||||||
@@ -192,7 +203,7 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.d(TAG, "received packet: ${packet.toHexString()}")
|
// Log.d(TAG, "received packet: ${packet.toHexString()}")
|
||||||
val opcode = packet[4]
|
val opcode = packet[4]
|
||||||
when (MessageOpcode.fromByte(opcode)) {
|
when (MessageOpcode.fromByte(opcode)) {
|
||||||
MessageOpcode.BUD_ROLE -> {
|
MessageOpcode.BUD_ROLE -> {
|
||||||
@@ -314,7 +325,28 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MessageOpcode.BUDDY_COMMAND -> {
|
MessageOpcode.BUDDY_COMMAND -> {
|
||||||
Log.w(TAG, "BUDDY not implemented")
|
val packet = RTBuddyPacket.parse(packet)
|
||||||
|
|
||||||
|
val rtBuddyPayload = packet.rtBuddyPayload
|
||||||
|
|
||||||
|
rtBuddyManager.handlePacket(packet)
|
||||||
|
|
||||||
|
when (rtBuddyPayload.descriptor) {
|
||||||
|
RTBuddyDescriptor.SENSOR_DATA_WX -> {
|
||||||
|
val sensorDataWxBuddyPayload = rtBuddyPayload as SensorDataWxBuddyPayload
|
||||||
|
val data = sensorDataWxBuddyPayload.data
|
||||||
|
|
||||||
|
handleSensorData(data)
|
||||||
|
} else -> {
|
||||||
|
Log.d(TAG, "Unhandled descriptor: ${rtBuddyPayload.descriptor}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// device.updateState {
|
||||||
|
// it.copy(
|
||||||
|
// aacpPackets = it.aacpPackets + packet
|
||||||
|
// )
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageOpcode.MAGIC_KEYS_RESPONSE -> {
|
MessageOpcode.MAGIC_KEYS_RESPONSE -> {
|
||||||
@@ -625,55 +657,7 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
return sendPacket(packet)
|
return sendPacket(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendStartHeadTracking(): Boolean {
|
fun setSensorServiceReportInterval(sensorServiceType: SensorServiceType, interval: Duration): Boolean = rtBuddyManager.setSensorServiceReportInterval(sensorServiceType, interval)
|
||||||
val payload = byteArrayOf(
|
|
||||||
0x00, 0x00, 0x10, 0x00,
|
|
||||||
0x10, 0x00, 0x08, 0xA1.toByte(), 0x02, 0x42, 0x0B, 0x08, 0x0E, 0x10, 0x02, 0x1A, 0x05, 0x01, 0x40, 0x9C.toByte(), 0x00, 0x00
|
|
||||||
)
|
|
||||||
val packet = AACPPacket.createUnknownPacket(
|
|
||||||
MessageOpcode.BUDDY_COMMAND,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
return sendPacket(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendStartAlternateHeadTracking(): Boolean {
|
|
||||||
val payload = byteArrayOf(
|
|
||||||
0x00, 0x00, 0x10, 0x00,
|
|
||||||
0x0F, 0x00, 0x08, 0x73, 0x42, 0x0B, 0x08, 0x10, 0x10, 0x02, 0x1A, 0x05, 0x01, 0x40, 0x9C.toByte(), 0x00, 0x00
|
|
||||||
)
|
|
||||||
val packet = AACPPacket.createUnknownPacket(
|
|
||||||
MessageOpcode.BUDDY_COMMAND,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
return sendPacket(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendStopHeadTracking(): Boolean {
|
|
||||||
val payload = byteArrayOf(
|
|
||||||
0x00, 0x00, 0x10, 0x00,
|
|
||||||
0x11, 0x00, 0x08, 0x7E, 0x10, 0x02, 0x42, 0x0B, 0x08, 0x4E, 0x10, 0x02, 0x1A, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00
|
|
||||||
)
|
|
||||||
val packet = AACPPacket.createUnknownPacket(
|
|
||||||
MessageOpcode.BUDDY_COMMAND,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
return sendPacket(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendStopAlternateHeadTracking(): Boolean {
|
|
||||||
val payload = byteArrayOf(
|
|
||||||
0x00, 0x00, 0x10, 0x00,
|
|
||||||
0x0F, 0x00, 0x08, 0x75, 0x42, 0x0B, 0x08, 0x10, 0x10, 0x02, 0x1A, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00
|
|
||||||
)
|
|
||||||
|
|
||||||
val packet = AACPPacket.createUnknownPacket(
|
|
||||||
opcode = MessageOpcode.BUDDY_COMMAND,
|
|
||||||
payload = payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
return sendPacket(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendRename(name: String): Boolean {
|
fun sendRename(name: String): Boolean {
|
||||||
val packet = RenamePacket.create(name)
|
val packet = RenamePacket.create(name)
|
||||||
@@ -1045,6 +1029,8 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
connectedDevices = emptyList(),
|
connectedDevices = emptyList(),
|
||||||
microphoneFrames = emptyList(),
|
microphoneFrames = emptyList(),
|
||||||
controlStates = emptyMap(),
|
controlStates = emptyMap(),
|
||||||
|
aacpPackets = emptyList(),
|
||||||
|
currentHeartRate = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1174,4 +1160,93 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
|
|
||||||
return capabilities
|
return capabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSensorData(data: SensorDataWX) {
|
||||||
|
if (data.hasCommand()) {
|
||||||
|
when (data.command.service) {
|
||||||
|
SensorServiceType.ACTIVITY, SensorServiceType.DEVMOTION6 -> {
|
||||||
|
val payload = data.command.payload.toByteArray()
|
||||||
|
|
||||||
|
if (payload.size != 58) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Unexpected payload size for ACTIVITY/DEVMOTION6: ${payload.size}, payload: ${payload.toHexString()}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fun i16(offset: Int): Int =
|
||||||
|
(payload[offset].toInt() and 0xFF) or
|
||||||
|
((payload[offset + 1].toInt() and 0xFF) shl 8)
|
||||||
|
.let { value ->
|
||||||
|
if (value and 0x8000 != 0) value - 0x10000 else value
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadTracking.addAccel(i16(device.settings.value.headGesturesVerticalOffset).toFloat(), i16(device.settings.value.headGesturesHorizontalOffset).toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
SensorServiceType.HEARTRATE -> {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Received sensor data for service: ${data.command.service}, payload: ${data.command.payload.toByteArray().toHexString()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SensorServiceType.HEARTRATEv2 -> {
|
||||||
|
val payload = data.command.payload.toByteArray()
|
||||||
|
val timestamp = Clock.System.now()
|
||||||
|
if (payload.size == 18) {
|
||||||
|
val heartRate = payload[1].toInt()
|
||||||
|
|
||||||
|
// same as healthconnect's datatype. 300 isn't possible anyway, but whatever
|
||||||
|
if (heartRate !in 1..300) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Invalid heart rate value: $heartRate"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!device.state.value.hrmActive) {
|
||||||
|
device.updateState {
|
||||||
|
it.copy(
|
||||||
|
hrmActive = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"hr: $heartRate bpm"
|
||||||
|
)
|
||||||
|
|
||||||
|
val heartRateSample = HeartRateSample(
|
||||||
|
bpm = heartRate,
|
||||||
|
timestamp = timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
device.updateState {
|
||||||
|
it.copy(
|
||||||
|
currentHeartRate = heartRateSample
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Unexpected payload size for HEARTRATEv2: ${payload.size}, payload: ${payload.toHexString()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val payload = data.command.payload.toByteArray()
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Unhandled sensor command service: ${data.command.service}, payload: ${payload.toHexString()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
package me.kavishdevar.librepods.bluetooth.aacp
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.AACPPacket
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.RTBuddyPacket
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.rtbuddy.proto.SensorDataWX
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.rtbuddy.proto.SensorServiceSetting
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.rtbuddy.proto.SensorServiceType
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.SensorDataWxBuddyPayload
|
||||||
|
import me.kavishdevar.librepods.devices.PacketDestination
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
class RTBuddyManager(
|
||||||
|
private val sendPacket: (AACPPacket) -> Boolean,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "RTBuddyManager"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sequence = 0
|
||||||
|
|
||||||
|
private fun nextSequence(): Int {
|
||||||
|
val next = sequence
|
||||||
|
sequence = (sequence + 1) and 0x7FFFFFFF
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeSequence(data: SensorDataWX) {
|
||||||
|
Log.d(TAG, "Received SensorDataWX seq=${data.seq}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handlePacket(packet: RTBuddyPacket) {
|
||||||
|
when (val payload = packet.rtBuddyPayload) {
|
||||||
|
is SensorDataWxBuddyPayload -> {
|
||||||
|
val data = payload.data
|
||||||
|
observeSequence(data)
|
||||||
|
|
||||||
|
// handleSensorData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Unhandled RTBuddy payload: ${payload.descriptor}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private fun handleSensorData(data: SensorDataWX) {
|
||||||
|
// if (data.hasServiceSettings()) {
|
||||||
|
// when (data.serviceSettings.service) {
|
||||||
|
// SensorServiceType.ACTIVITY,
|
||||||
|
// SensorServiceType.DEVMOTION6 -> {
|
||||||
|
// // Head tracking data/configuration
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// SensorServiceType.HEARTRATE -> {
|
||||||
|
// // Heart rate
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// else -> {
|
||||||
|
// Log.d(
|
||||||
|
// TAG,
|
||||||
|
// "Unhandled sensor service: ${data.serviceSettings.service}"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun sendSensorData(
|
||||||
|
data: SensorDataWX,
|
||||||
|
): Boolean {
|
||||||
|
val dataWithSequence = data.toBuilder()
|
||||||
|
.setSeq(nextSequence())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val packet = RTBuddyPacket.create(
|
||||||
|
rtBuddyPayload = SensorDataWxBuddyPayload(
|
||||||
|
data = dataWithSequence,
|
||||||
|
),
|
||||||
|
destination = PacketDestination.DEVICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
return sendPacket(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendSensorServiceSetting(
|
||||||
|
service: SensorServiceType,
|
||||||
|
configuration: ByteArray,
|
||||||
|
): Boolean {
|
||||||
|
val data = SensorDataWX.newBuilder()
|
||||||
|
.setServiceSettings(
|
||||||
|
SensorServiceSetting.newBuilder()
|
||||||
|
.setService(service)
|
||||||
|
.setSetting(2)
|
||||||
|
.setConfiguration(
|
||||||
|
ByteString.copyFrom(configuration)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return sendSensorData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSensorServiceReportInterval(sensorServiceType: SensorServiceType, interval: Duration): Boolean = sendSensorServiceSetting(
|
||||||
|
service = sensorServiceType,
|
||||||
|
configuration = byteArrayOf(0x01) + interval.toMicrosUINT32LE()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Duration.toMicrosUINT32LE(): ByteArray {
|
||||||
|
val microseconds = this.inWholeMicroseconds
|
||||||
|
return byteArrayOf(
|
||||||
|
microseconds.toByte(),
|
||||||
|
(microseconds shr 8).toByte(),
|
||||||
|
(microseconds shr 16).toByte(),
|
||||||
|
(microseconds shr 24).toByte()
|
||||||
|
)
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package me.kavishdevar.librepods.bluetooth.aacp.packet
|
||||||
|
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.rtbuddy.proto.SensorDataWX
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.MessageOpcode
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.Opcode
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.RTBuddyDescriptor
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.RTBuddyPayload
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.SensorDataWxBuddyPayload
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.UnknownBuddyPayload
|
||||||
|
import me.kavishdevar.librepods.devices.PacketDestination
|
||||||
|
|
||||||
|
data class RTBuddyPacket(
|
||||||
|
val rtBuddyPayload: RTBuddyPayload,
|
||||||
|
|
||||||
|
override val payload: ByteArray,
|
||||||
|
override val destination: PacketDestination,
|
||||||
|
) : AACPPacket {
|
||||||
|
|
||||||
|
override val type: AACPPacketType = AACPPacketType.MESSAGE
|
||||||
|
override val service: Byte = 0x04
|
||||||
|
override val opcode: Opcode = MessageOpcode.BUDDY_COMMAND
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(
|
||||||
|
data: ByteArray,
|
||||||
|
destination: PacketDestination = PacketDestination.HOST,
|
||||||
|
): RTBuddyPacket {
|
||||||
|
val payload = if (
|
||||||
|
data.size >= 6 &&
|
||||||
|
data[0] == AACPPacketType.MESSAGE.value &&
|
||||||
|
data[4] == MessageOpcode.BUDDY_COMMAND.value
|
||||||
|
) {
|
||||||
|
data.copyOfRange(6, data.size)
|
||||||
|
} else {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
require(payload.size >= 6) {
|
||||||
|
"RTBuddy packet is too short: ${payload.size}"
|
||||||
|
}
|
||||||
|
|
||||||
|
val descriptorValue =
|
||||||
|
(payload[0].toUInt() and 0xFFu) or
|
||||||
|
((payload[1].toUInt() and 0xFFu) shl 8) or
|
||||||
|
((payload[2].toUInt() and 0xFFu) shl 16) or
|
||||||
|
((payload[3].toUInt() and 0xFFu) shl 24)
|
||||||
|
|
||||||
|
val length =
|
||||||
|
(payload[4].toInt() and 0xFF) or
|
||||||
|
((payload[5].toInt() and 0xFF) shl 8)
|
||||||
|
|
||||||
|
require(payload.size >= 6 + length) {
|
||||||
|
"RTBuddy payload truncated: expected $length bytes, got ${payload.size - 6}"
|
||||||
|
}
|
||||||
|
|
||||||
|
val descriptor = RTBuddyDescriptor.fromValue(descriptorValue)
|
||||||
|
val data = payload.copyOfRange(6, 6 + length)
|
||||||
|
|
||||||
|
val rtBuddyPayload: RTBuddyPayload = when (descriptor) {
|
||||||
|
RTBuddyDescriptor.SENSOR_DATA_WX -> {
|
||||||
|
SensorDataWxBuddyPayload(
|
||||||
|
data = SensorDataWX.parseFrom(data),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
UnknownBuddyPayload(
|
||||||
|
descriptor = descriptor,
|
||||||
|
descriptorValue = descriptorValue,
|
||||||
|
data = data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RTBuddyPacket(
|
||||||
|
rtBuddyPayload = rtBuddyPayload,
|
||||||
|
payload = payload,
|
||||||
|
destination = destination,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fun create(
|
||||||
|
rtBuddyPayload: RTBuddyPayload,
|
||||||
|
destination: PacketDestination = PacketDestination.DEVICE,
|
||||||
|
): RTBuddyPacket {
|
||||||
|
val data = when (rtBuddyPayload) {
|
||||||
|
is SensorDataWxBuddyPayload ->
|
||||||
|
rtBuddyPayload.data.toByteArray()
|
||||||
|
|
||||||
|
is UnknownBuddyPayload ->
|
||||||
|
rtBuddyPayload.data
|
||||||
|
}
|
||||||
|
|
||||||
|
val descriptorValue = when (rtBuddyPayload) {
|
||||||
|
is SensorDataWxBuddyPayload ->
|
||||||
|
rtBuddyPayload.descriptor.value
|
||||||
|
|
||||||
|
is UnknownBuddyPayload ->
|
||||||
|
rtBuddyPayload.descriptorValue
|
||||||
|
}
|
||||||
|
|
||||||
|
require(data.size <= 0xFFFF) {
|
||||||
|
"RTBuddy payload too large: ${data.size}"
|
||||||
|
}
|
||||||
|
|
||||||
|
val payload = byteArrayOf(
|
||||||
|
(descriptorValue and 0xFFu).toByte(),
|
||||||
|
((descriptorValue shr 8) and 0xFFu).toByte(),
|
||||||
|
((descriptorValue shr 16) and 0xFFu).toByte(),
|
||||||
|
((descriptorValue shr 24) and 0xFFu).toByte(),
|
||||||
|
(data.size and 0xFF).toByte(),
|
||||||
|
((data.size shr 8) and 0xFF).toByte(),
|
||||||
|
) + data
|
||||||
|
|
||||||
|
return RTBuddyPacket(
|
||||||
|
rtBuddyPayload = rtBuddyPayload,
|
||||||
|
payload = payload,
|
||||||
|
destination = destination,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package me.kavishdevar.librepods.bluetooth.aacp.types
|
||||||
|
|
||||||
|
enum class RTBuddyDescriptor(val value: UInt) {
|
||||||
|
ACOUSTIC(0x00000001u),
|
||||||
|
SCP(0x00000002u),
|
||||||
|
BUDDY(0x00000004u),
|
||||||
|
VIRTUAL_CLI_PRIMARY(0x00000008u),
|
||||||
|
VIRTUAL_CLI_SECONDARY(0x00000010u),
|
||||||
|
APP_DIAGNOSTICS(0x00000020u),
|
||||||
|
LOGGING_TRIGGER(0x00000040u),
|
||||||
|
DEBUG_DATA(0x00000080u),
|
||||||
|
TOUCH(0x00000100u),
|
||||||
|
LOG_CONFIG(0x00000200u),
|
||||||
|
LOG_MSG(0x00000400u),
|
||||||
|
SENSOR(0x00000800u),
|
||||||
|
SWITCH_CONTROL(0x00001000u),
|
||||||
|
MISMATCHED_BUDS(0x00002000u),
|
||||||
|
UNKNOWN_4000(0x00004000u),
|
||||||
|
B2P(0x00008000u),
|
||||||
|
CONTINUITY(0x00010000u),
|
||||||
|
BATTERY_HEALTH(0x00020000u),
|
||||||
|
SENSOR_V2(0x00040000u),
|
||||||
|
OBC_V2(0x00080000u),
|
||||||
|
SENSOR_DATA_WX(0x00100000u),
|
||||||
|
UNKNOWN_00200000(0x00200000u),
|
||||||
|
UNKNOWN_00400000(0x00400000u),
|
||||||
|
DIGITAL_ENGRAVING_INFO(0x00800000u),
|
||||||
|
ACTIVE_MODE_DATA(0x01000000u),
|
||||||
|
|
||||||
|
UNKNOWN(0xFFFFFFFFu); // better way?
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(value: UInt): RTBuddyDescriptor = entries.firstOrNull { it.value == value } ?: UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package me.kavishdevar.librepods.bluetooth.aacp.types
|
||||||
|
|
||||||
|
sealed interface RTBuddyPayload {
|
||||||
|
val descriptor: RTBuddyDescriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UnknownBuddyPayload(
|
||||||
|
override val descriptor: RTBuddyDescriptor,
|
||||||
|
val descriptorValue: UInt,
|
||||||
|
val data: ByteArray,
|
||||||
|
) : RTBuddyPayload
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package me.kavishdevar.librepods.bluetooth.aacp.types
|
||||||
|
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.rtbuddy.proto.SensorDataWX
|
||||||
|
|
||||||
|
data class SensorDataWxBuddyPayload(
|
||||||
|
val data: SensorDataWX,
|
||||||
|
): RTBuddyPayload {
|
||||||
|
override val descriptor: RTBuddyDescriptor = RTBuddyDescriptor.SENSOR_DATA_WX
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package me.kavishdevar.librepods.data.heartrate
|
||||||
|
|
||||||
|
import androidx.room3.Dao
|
||||||
|
import androidx.room3.Insert
|
||||||
|
import androidx.room3.Query
|
||||||
|
import me.kavishdevar.librepods.database.heartrate.HeartRateSampleEntity
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface HeartRateDao {
|
||||||
|
@Insert
|
||||||
|
suspend fun insert(sample: HeartRateSampleEntity)
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT * FROM HeartRateSampleEntity
|
||||||
|
WHERE timestamp >= :start
|
||||||
|
AND timestamp < :end
|
||||||
|
ORDER BY timestamp ASC
|
||||||
|
""")
|
||||||
|
suspend fun get(
|
||||||
|
start: Instant,
|
||||||
|
end: Instant,
|
||||||
|
): List<HeartRateSampleEntity>
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package me.kavishdevar.librepods.data.heartrate
|
||||||
|
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
data class HeartRateSample (
|
||||||
|
val bpm: Int,
|
||||||
|
val timestamp: Instant
|
||||||
|
)
|
||||||
|
// TODO: implement records (don't have a better place to put this)
|
||||||
@@ -2,7 +2,7 @@ package me.kavishdevar.librepods.data.updates
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.AirPodsSettingsScreenPreviewMaterial
|
import me.kavishdevar.librepods.presentation.screens.apple.AppleSettingsScreenPreviewMaterial
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.EqualizerScreenPreviewApple
|
import me.kavishdevar.librepods.presentation.screens.apple.EqualizerScreenPreviewApple
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.EqualizerScreenPreviewMaterial
|
import me.kavishdevar.librepods.presentation.screens.apple.EqualizerScreenPreviewMaterial
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||||
@@ -13,7 +13,7 @@ val update1_0_0 = listOf(
|
|||||||
titleRes = R.string.material3e,
|
titleRes = R.string.material3e,
|
||||||
descriptionRes = R.string.update_m3e_description,
|
descriptionRes = R.string.update_m3e_description,
|
||||||
demoComposeable = @Composable {
|
demoComposeable = @Composable {
|
||||||
AirPodsSettingsScreenPreviewMaterial()
|
AppleSettingsScreenPreviewMaterial()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
UpdateItem(
|
UpdateItem(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import me.kavishdevar.librepods.bluetooth.MacAddress
|
|||||||
import me.kavishdevar.librepods.data.apple.AppleCache
|
import me.kavishdevar.librepods.data.apple.AppleCache
|
||||||
import me.kavishdevar.librepods.devices.AppleMetadata
|
import me.kavishdevar.librepods.devices.AppleMetadata
|
||||||
import me.kavishdevar.librepods.devices.AppleSettings
|
import me.kavishdevar.librepods.devices.AppleSettings
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
object Converters {
|
object Converters {
|
||||||
@ColumnTypeConverter
|
@ColumnTypeConverter
|
||||||
@@ -39,4 +40,12 @@ object Converters {
|
|||||||
@ColumnTypeConverter
|
@ColumnTypeConverter
|
||||||
fun bytesToAppleCache(bytes: ByteArray): AppleCache =
|
fun bytesToAppleCache(bytes: ByteArray): AppleCache =
|
||||||
Cbor.decodeFromByteArray(bytes)
|
Cbor.decodeFromByteArray(bytes)
|
||||||
|
|
||||||
|
@ColumnTypeConverter
|
||||||
|
fun kotlinInstantToLong(instant: Instant): Long =
|
||||||
|
instant.toEpochMilliseconds()
|
||||||
|
|
||||||
|
@ColumnTypeConverter
|
||||||
|
fun longToKotlinInstant(millis: Long): Instant =
|
||||||
|
Instant.fromEpochMilliseconds(millis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package me.kavishdevar.librepods.database
|
|||||||
import androidx.room3.ColumnTypeConverters
|
import androidx.room3.ColumnTypeConverters
|
||||||
import androidx.room3.Database
|
import androidx.room3.Database
|
||||||
import androidx.room3.RoomDatabase
|
import androidx.room3.RoomDatabase
|
||||||
|
import me.kavishdevar.librepods.data.heartrate.HeartRateDao
|
||||||
import me.kavishdevar.librepods.database.app.AppSettingsDao
|
import me.kavishdevar.librepods.database.app.AppSettingsDao
|
||||||
import me.kavishdevar.librepods.database.app.AppSettingsEntity
|
import me.kavishdevar.librepods.database.app.AppSettingsEntity
|
||||||
import me.kavishdevar.librepods.database.app.AppStateDao
|
import me.kavishdevar.librepods.database.app.AppStateDao
|
||||||
import me.kavishdevar.librepods.database.app.AppStateEntity
|
import me.kavishdevar.librepods.database.app.AppStateEntity
|
||||||
import me.kavishdevar.librepods.database.apple.AppleDao
|
import me.kavishdevar.librepods.database.apple.AppleDao
|
||||||
import me.kavishdevar.librepods.database.apple.AppleEntity
|
import me.kavishdevar.librepods.database.apple.AppleEntity
|
||||||
|
import me.kavishdevar.librepods.database.heartrate.HeartRateSampleEntity
|
||||||
import me.kavishdevar.librepods.database.widget.WidgetConfigDao
|
import me.kavishdevar.librepods.database.widget.WidgetConfigDao
|
||||||
import me.kavishdevar.librepods.database.widget.WidgetConfigEntity
|
import me.kavishdevar.librepods.database.widget.WidgetConfigEntity
|
||||||
|
|
||||||
@@ -19,6 +21,7 @@ import me.kavishdevar.librepods.database.widget.WidgetConfigEntity
|
|||||||
AppSettingsEntity::class,
|
AppSettingsEntity::class,
|
||||||
AppStateEntity::class,
|
AppStateEntity::class,
|
||||||
WidgetConfigEntity::class,
|
WidgetConfigEntity::class,
|
||||||
|
HeartRateSampleEntity::class
|
||||||
],
|
],
|
||||||
version = 1,
|
version = 1,
|
||||||
)
|
)
|
||||||
@@ -29,4 +32,6 @@ abstract class LibrePodsDatabase: RoomDatabase() {
|
|||||||
abstract fun appStateDao(): AppStateDao
|
abstract fun appStateDao(): AppStateDao
|
||||||
|
|
||||||
abstract fun widgetConfigDao(): WidgetConfigDao
|
abstract fun widgetConfigDao(): WidgetConfigDao
|
||||||
|
|
||||||
|
abstract fun heartRateDao(): HeartRateDao
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -13,12 +13,12 @@ data class AppSettingsEntity(
|
|||||||
|
|
||||||
val nightMode: NightTheme = NightTheme.System,
|
val nightMode: NightTheme = NightTheme.System,
|
||||||
val designSystem: DesignSystem = DesignSystem.Material,
|
val designSystem: DesignSystem = DesignSystem.Material,
|
||||||
|
val useHighestRefreshRate: Boolean = false,
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently only shows the button for Debug screen.
|
|
||||||
*/
|
|
||||||
val debugMode: Boolean = false,
|
val debugMode: Boolean = false,
|
||||||
|
|
||||||
val bleScanMode: Int = ScanSettings.SCAN_MODE_BALANCED,
|
val bleScanMode: Int = ScanSettings.SCAN_MODE_BALANCED,
|
||||||
val bleReportDelay: Long = 0,
|
val bleReportDelay: Long = 0,
|
||||||
|
|
||||||
|
val swipeAnywhereForBack: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package me.kavishdevar.librepods.database.heartrate
|
||||||
|
|
||||||
|
import androidx.room3.Entity
|
||||||
|
import androidx.room3.PrimaryKey
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class HeartRateSampleEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
val id: Long = 0L,
|
||||||
|
|
||||||
|
val timestamp: Instant,
|
||||||
|
val bpm: Int,
|
||||||
|
)
|
||||||
@@ -18,12 +18,17 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.bluetooth.MacAddress
|
import me.kavishdevar.librepods.bluetooth.MacAddress
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.AACPManager
|
import me.kavishdevar.librepods.bluetooth.aacp.AACPManager
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.rtbuddy.proto.SensorServiceType
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.AppleEvent
|
import me.kavishdevar.librepods.bluetooth.aacp.types.AppleEvent
|
||||||
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.bluetooth.att.ATTManager
|
import me.kavishdevar.librepods.bluetooth.att.ATTManager
|
||||||
import me.kavishdevar.librepods.data.StemAction
|
import me.kavishdevar.librepods.data.StemAction
|
||||||
|
import me.kavishdevar.librepods.utils.GestureDetector
|
||||||
|
import me.kavishdevar.librepods.utils.HeadTracking
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
private const val TAG = "AppleDevice"
|
private const val TAG = "AppleDevice"
|
||||||
|
|
||||||
@@ -77,6 +82,12 @@ class AppleDevice(
|
|||||||
val aacp = AACPManager(this)
|
val aacp = AACPManager(this)
|
||||||
val att = ATTManager(this)
|
val att = ATTManager(this)
|
||||||
|
|
||||||
|
|
||||||
|
// ik, probably wrong place. TODO
|
||||||
|
private val gestureDetector by lazy {
|
||||||
|
GestureDetector()
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
updateMetadata {
|
updateMetadata {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -205,25 +216,35 @@ class AppleDevice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startHeadTracking() {
|
fun startHeadTracking() {
|
||||||
if (settings.value.alternateHeadTrackingPackets) {
|
aacp.setSensorServiceReportInterval(
|
||||||
aacp.sendStartAlternateHeadTracking()
|
sensorServiceType = if (metadata.value.version3.first().digitToInt() >= 8) SensorServiceType.DEVMOTION6 else SensorServiceType.ACTIVITY,
|
||||||
} else {
|
interval = _settings.value.headTrackingInterval
|
||||||
aacp.sendStartHeadTracking()
|
)
|
||||||
}
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(headTrackingActive = true)
|
it.copy(headTrackingActive = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopHeadTracking() {
|
fun stopHeadTracking() {
|
||||||
if (settings.value.alternateHeadTrackingPackets) {
|
aacp.setSensorServiceReportInterval(
|
||||||
aacp.sendStopAlternateHeadTracking()
|
sensorServiceType = if (metadata.value.version3.first().digitToInt() >= 8) SensorServiceType.DEVMOTION6 else SensorServiceType.ACTIVITY,
|
||||||
} else {
|
interval = Duration.ZERO
|
||||||
aacp.sendStopHeadTracking()
|
)
|
||||||
}
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(headTrackingActive = false)
|
it.copy(headTrackingActive = false)
|
||||||
}
|
}
|
||||||
|
gestureDetector.stopDetection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun detectHeadGestures(callback: (Boolean) -> Unit) {
|
||||||
|
if (!state.value.headTrackingActive) startHeadTracking()
|
||||||
|
if (!state.value.headTrackingActive) {
|
||||||
|
aacp.setSensorServiceReportInterval(
|
||||||
|
sensorServiceType = if (metadata.value.version3.first().digitToInt() >= 8) SensorServiceType.DEVMOTION6 else SensorServiceType.ACTIVITY,
|
||||||
|
interval = 40.milliseconds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
gestureDetector.startDetection(HeadTracking.acceleration, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setHeadGesturesEnabled(enabled: Boolean) {
|
fun setHeadGesturesEnabled(enabled: Boolean) {
|
||||||
@@ -232,6 +253,25 @@ class AppleDevice(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setHeadGesturesVerticalOffset(offset: Int) {
|
||||||
|
_settings.update {
|
||||||
|
it.copy(headGesturesVerticalOffset = offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHeadGesturesHorizontalOffset(offset: Int) {
|
||||||
|
_settings.update {
|
||||||
|
it.copy(headGesturesHorizontalOffset = offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHeadTrackingInterval(interval: Duration) {
|
||||||
|
startHeadTracking()
|
||||||
|
_settings.update {
|
||||||
|
it.copy(headTrackingInterval = interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setCustomEqEnabled(enabled: Boolean) {
|
fun setCustomEqEnabled(enabled: Boolean) {
|
||||||
aacp.setCustomEq(_state.value.customEq.copy(state = if(enabled) 2 else 1))
|
aacp.setCustomEq(_state.value.customEq.copy(state = if(enabled) 2 else 1))
|
||||||
}
|
}
|
||||||
@@ -243,17 +283,24 @@ class AppleDevice(
|
|||||||
aacp.setCustomEq(_state.value.customEq.copy(low = low, mid = mid, high = high))
|
aacp.setCustomEq(_state.value.customEq.copy(low = low, mid = mid, high = high))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testHeadGestures() {
|
// AACPManager sets hrmActive true when a valid reading is received
|
||||||
if (settings.value.alternateHeadTrackingPackets) {
|
fun startHr(): Boolean = aacp.setSensorServiceReportInterval(
|
||||||
aacp.sendStartAlternateHeadTracking()
|
sensorServiceType = if (metadata.value.version3.first().digitToInt() >= 8) SensorServiceType.HEARTRATE_COMMAND else SensorServiceType.HEARTRATE,
|
||||||
} else {
|
interval = 1.seconds
|
||||||
aacp.sendStartHeadTracking()
|
)
|
||||||
}
|
|
||||||
_state.update {
|
fun stopHr(): Boolean {
|
||||||
|
val success = aacp.setSensorServiceReportInterval(
|
||||||
|
sensorServiceType = if (metadata.value.version3.first().digitToInt() >= 8) SensorServiceType.HEARTRATE_COMMAND else SensorServiceType.HEARTRATE,
|
||||||
|
interval = Duration.ZERO
|
||||||
|
)
|
||||||
|
if (state.value.hrmActive && success) _state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
detectHeadGestures = true
|
hrmActive = false,
|
||||||
|
currentHeartRate = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendRawPacket(data: ByteArray): Boolean = aacp.sendRawPacket(data)
|
fun sendRawPacket(data: ByteArray): Boolean = aacp.sendRawPacket(data)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package me.kavishdevar.librepods.devices
|
|||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import me.kavishdevar.librepods.data.StemAction
|
import me.kavishdevar.librepods.data.StemAction
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AppleSettings(
|
data class AppleSettings(
|
||||||
@@ -10,6 +12,9 @@ data class AppleSettings(
|
|||||||
val cacheDisconnectedComponentBattery: Boolean = true,
|
val cacheDisconnectedComponentBattery: Boolean = true,
|
||||||
|
|
||||||
val headGesturesEnabled: Boolean = true, // head_gestures_enabled
|
val headGesturesEnabled: Boolean = true, // head_gestures_enabled
|
||||||
|
val headGesturesVerticalOffset: Int = 30,
|
||||||
|
val headGesturesHorizontalOffset: Int = 28,
|
||||||
|
val headTrackingInterval: Duration = 40.milliseconds,
|
||||||
|
|
||||||
val leftLongPressAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES, // left_long_press_action
|
val leftLongPressAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES, // left_long_press_action
|
||||||
val rightLongPressAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES, // right_long_press_action
|
val rightLongPressAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES, // right_long_press_action
|
||||||
@@ -17,8 +22,6 @@ data class AppleSettings(
|
|||||||
val showIslandPopup: Boolean = true, // show_island_popup
|
val showIslandPopup: Boolean = true, // show_island_popup
|
||||||
val showBottomSheetPopup: Boolean = true, // show_bottom_sheet_popup
|
val showBottomSheetPopup: Boolean = true, // show_bottom_sheet_popup
|
||||||
|
|
||||||
val alternateHeadTrackingPackets: Boolean = true, // use_alternate_head_tracking_packets
|
|
||||||
|
|
||||||
val takeoverWhenDisconnected: Boolean = true, // takeover_when_disconnected
|
val takeoverWhenDisconnected: Boolean = true, // takeover_when_disconnected
|
||||||
val takeoverWhenIdle: Boolean = true, // takeover_when_idle
|
val takeoverWhenIdle: Boolean = true, // takeover_when_idle
|
||||||
val takeoverWhenMusic: Boolean = true, // takeover_when_music
|
val takeoverWhenMusic: Boolean = true, // takeover_when_music
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import me.kavishdevar.librepods.bluetooth.aacp.types.CustomEq
|
|||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.MagicKeyType
|
import me.kavishdevar.librepods.bluetooth.aacp.types.MagicKeyType
|
||||||
import me.kavishdevar.librepods.data.audio.MicrophoneFrame
|
import me.kavishdevar.librepods.data.audio.MicrophoneFrame
|
||||||
import me.kavishdevar.librepods.data.audio.MicrophoneState
|
import me.kavishdevar.librepods.data.audio.MicrophoneState
|
||||||
|
import me.kavishdevar.librepods.data.heartrate.HeartRateSample
|
||||||
import me.kavishdevar.librepods.data.recording.RecordingState
|
import me.kavishdevar.librepods.data.recording.RecordingState
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
data class AppleState(
|
data class AppleState(
|
||||||
val isLocallyConnected: Boolean = false,
|
val isLocallyConnected: Boolean = false,
|
||||||
@@ -51,5 +54,9 @@ data class AppleState(
|
|||||||
val headphoneAccomodationEnabledForMedia: Boolean = false,
|
val headphoneAccomodationEnabledForMedia: Boolean = false,
|
||||||
val headphoneAccomodationEnabledForPhone: Boolean = false,
|
val headphoneAccomodationEnabledForPhone: Boolean = false,
|
||||||
|
|
||||||
|
val currentHeartRate: HeartRateSample? = null,
|
||||||
|
val hrmActive: Boolean = false,
|
||||||
|
val heartRateInterval: Duration = 1.seconds,
|
||||||
|
|
||||||
val aacpPackets: List<AACPPacket> = emptyList(),
|
val aacpPackets: List<AACPPacket> = emptyList(),
|
||||||
): DeviceState
|
): DeviceState
|
||||||
|
|||||||
+20
-2
@@ -28,9 +28,11 @@ import android.content.Context
|
|||||||
import android.content.Context.MODE_PRIVATE
|
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.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.Display
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
@@ -58,8 +60,8 @@ 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
|
||||||
|
|
||||||
lateinit var serviceConnection: ServiceConnection
|
private lateinit var serviceConnection: ServiceConnection
|
||||||
lateinit var connectionStatusReceiver: BroadcastReceiver
|
private lateinit var connectionStatusReceiver: BroadcastReceiver
|
||||||
//lateinit var testReviewReceiver: BroadcastReceiver
|
//lateinit var testReviewReceiver: BroadcastReceiver
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -95,6 +97,22 @@ class MainActivity : ComponentActivity() {
|
|||||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(settings.swipeAnywhereForBack) {
|
||||||
|
if (settings.useHighestRefreshRate) {
|
||||||
|
val display = getSystemService(DisplayManager::class.java)
|
||||||
|
.getDisplay(Display.DEFAULT_DISPLAY)
|
||||||
|
|
||||||
|
val highest = display.supportedModes
|
||||||
|
.maxByOrNull { it.refreshRate }
|
||||||
|
|
||||||
|
highest?.let {
|
||||||
|
window.attributes = window.attributes.apply {
|
||||||
|
preferredRefreshRate = it.refreshRate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LibrePodsTheme(
|
LibrePodsTheme(
|
||||||
designSystem = settings.designSystem,
|
designSystem = settings.designSystem,
|
||||||
darkTheme = darkTheme
|
darkTheme = darkTheme
|
||||||
|
|||||||
+7
-3
@@ -7,7 +7,6 @@ import android.content.Intent
|
|||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -15,6 +14,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -34,9 +34,9 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.kavishdevar.librepods.LibrePodsApplication
|
import me.kavishdevar.librepods.LibrePodsApplication
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
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.bluetooth.MacAddress
|
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
@@ -164,13 +164,15 @@ private fun WidgetDevicePickerContent(
|
|||||||
) {
|
) {
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
title = stringResource(R.string.configure_widget),
|
title = stringResource(R.string.configure_widget),
|
||||||
) {
|
navigateBack = null
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.size(topPadding))
|
||||||
if (devices.isEmpty()) {
|
if (devices.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = "No devices found. Please ensure a compatible device is paired with your phone and try again.",
|
text = "No devices found. Please ensure a compatible device is paired with your phone and try again.",
|
||||||
@@ -231,6 +233,8 @@ private fun WidgetDevicePickerContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(bottomPadding))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
package me.kavishdevar.librepods.presentation.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import me.kavishdevar.librepods.LibrePodsApplication
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.onboarding.PrivacyPolicyPage
|
||||||
|
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||||
|
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
||||||
|
|
||||||
|
class PrivacyPolicyActivity : ComponentActivity() {
|
||||||
|
val appDataRepository by lazy { (application as LibrePodsApplication).appDataRepository }
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
val settings by appDataRepository.settings.collectAsState()
|
||||||
|
|
||||||
|
val systemDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
val darkTheme = when (settings.nightMode) {
|
||||||
|
NightTheme.Dark -> true
|
||||||
|
NightTheme.Light -> false
|
||||||
|
NightTheme.System -> systemDarkTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
val view = LocalView.current
|
||||||
|
val window = (view.context as Activity).window
|
||||||
|
|
||||||
|
LaunchedEffect(darkTheme) {
|
||||||
|
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
LibrePodsTheme(
|
||||||
|
designSystem = settings.designSystem,
|
||||||
|
darkTheme = darkTheme
|
||||||
|
) {
|
||||||
|
// For demo screenshots
|
||||||
|
// val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
|
||||||
|
// windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
// windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.privacy_policy),
|
||||||
|
navigateBack = null
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
|
Column {
|
||||||
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(52.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(52.dp))
|
||||||
|
.padding(16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {
|
||||||
|
PrivacyPolicyPage { finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-3
@@ -122,12 +122,13 @@ fun StyledListDemo() {
|
|||||||
darkTheme = false
|
darkTheme = false
|
||||||
) {
|
) {
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
title = "StyledListTest"
|
title = "StyledListTest",
|
||||||
) {
|
navigateBack = null
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column (
|
Column (
|
||||||
modifier = Modifier.padding(horizontal = 12.dp)
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(56.dp))
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
StyledList(
|
StyledList(
|
||||||
title = "hello"
|
title = "hello"
|
||||||
) {
|
) {
|
||||||
@@ -145,6 +146,7 @@ fun StyledListDemo() {
|
|||||||
onCheckedChange = { checked.value = it },
|
onCheckedChange = { checked.value = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -668,7 +668,7 @@ fun StyledListScope.StyledListItem(
|
|||||||
Text(
|
Text(
|
||||||
text = supportingText,
|
text = supportingText,
|
||||||
style = if (LocalDesignSystem.current == DesignSystem.Apple && orientation == StyledListItemOrientation.Horizontal) MaterialTheme.typography.bodyMedium else MaterialTheme.typography.bodySmall,
|
style = if (LocalDesignSystem.current == DesignSystem.Apple && orientation == StyledListItemOrientation.Horizontal) MaterialTheme.typography.bodyMedium else MaterialTheme.typography.bodySmall,
|
||||||
color = if (selected == true) MaterialTheme.colorScheme.onPrimaryContainer.copy(0.7f) else MaterialTheme.colorScheme.onSurface.copy(0.7f), // TODO: move to color scheme
|
color = if (selected == true && LocalDesignSystem.current == DesignSystem.Material) MaterialTheme.colorScheme.onPrimaryContainer.copy(0.7f) else MaterialTheme.colorScheme.onSurface.copy(0.7f), // TODO: move to color scheme
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
@@ -805,6 +805,7 @@ private fun StyledListItemContent(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
|
if (onClick != null) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = {
|
onPress = {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@@ -820,11 +821,12 @@ private fun StyledListItemContent(
|
|||||||
HapticFeedbackType.ContextClick
|
HapticFeedbackType.ContextClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClick?.invoke()
|
onClick.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.heightIn(min = height)
|
.heightIn(min = height)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
+111
-25
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
|
import android.graphics.RenderEffect
|
||||||
|
import android.graphics.Shader
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -27,6 +29,7 @@ import androidx.compose.animation.scaleIn
|
|||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -61,10 +64,17 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asComposeRenderEffect
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import androidx.compose.ui.input.pointer.consumePositionChange
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import com.kyant.backdrop.backdrops.LayerBackdrop
|
import com.kyant.backdrop.backdrops.LayerBackdrop
|
||||||
@@ -75,6 +85,9 @@ import dev.chrisbanes.haze.hazeEffect
|
|||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.chrisbanes.haze.rememberHazeState
|
import dev.chrisbanes.haze.rememberHazeState
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
|
import me.kavishdevar.librepods.presentation.navigation.LocalIsCurrentEntry
|
||||||
|
import me.kavishdevar.librepods.presentation.navigation.LocalSharedTransitionScope
|
||||||
|
import me.kavishdevar.librepods.presentation.navigation.LocalTransitionProgress
|
||||||
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
|
||||||
|
|
||||||
@@ -84,11 +97,10 @@ fun StyledScaffold(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
visible: Boolean = true,
|
visible: Boolean = true,
|
||||||
title: String,
|
title: String,
|
||||||
showBackButton: Boolean = false,
|
navigateBack: (() -> Unit)?,
|
||||||
onNavigateBack: () -> Unit = {},
|
|
||||||
actionButtons: List<@Composable (backdrop: LayerBackdrop) -> Unit> = emptyList(),
|
actionButtons: List<@Composable (backdrop: LayerBackdrop) -> Unit> = emptyList(),
|
||||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||||
content: @Composable () -> Unit
|
content: @Composable (topPadding: Dp, bottomPadding: Dp) -> Unit
|
||||||
) {
|
) {
|
||||||
val hazeState = rememberHazeState(blurEnabled = true)
|
val hazeState = rememberHazeState(blurEnabled = true)
|
||||||
|
|
||||||
@@ -105,14 +117,18 @@ fun StyledScaffold(
|
|||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (showBackButton) {
|
if (navigateBack != null) {
|
||||||
Row {
|
Row {
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
FilledTonalIconButton(
|
FilledTonalIconButton(
|
||||||
onClick = onNavigateBack,
|
onClick = navigateBack,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.minimumInteractiveComponentSize()
|
.minimumInteractiveComponentSize()
|
||||||
.size(IconButtonDefaults.mediumContainerSize(IconButtonDefaults.IconButtonWidthOption.Narrow)),
|
.size(
|
||||||
|
IconButtonDefaults.mediumContainerSize(
|
||||||
|
IconButtonDefaults.IconButtonWidthOption.Narrow
|
||||||
|
)
|
||||||
|
),
|
||||||
shape = IconButtonDefaults.mediumRoundShape
|
shape = IconButtonDefaults.mediumRoundShape
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -130,7 +146,7 @@ fun StyledScaffold(
|
|||||||
text = it,
|
text = it,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.padding(start = if (showBackButton) 8.dp else 12.dp, end = 12.dp),
|
modifier = Modifier.padding(start = if (navigateBack != null) 8.dp else 12.dp, end = 12.dp),
|
||||||
style = MaterialTheme.typography.titleSmall
|
style = MaterialTheme.typography.titleSmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -146,15 +162,20 @@ fun StyledScaffold(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.then(if (visible) Modifier.padding(start = paddingValues.calculateStartPadding(LocalLayoutDirection.current), end = paddingValues.calculateEndPadding(LocalLayoutDirection.current)) else Modifier)
|
.then(
|
||||||
|
if (visible) Modifier.padding(
|
||||||
|
start = paddingValues.calculateStartPadding(
|
||||||
|
LocalLayoutDirection.current
|
||||||
|
),
|
||||||
|
end = paddingValues.calculateEndPadding(LocalLayoutDirection.current)
|
||||||
|
) else Modifier
|
||||||
|
)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.hazeSource(hazeState)
|
.hazeSource(hazeState)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding()))
|
content(paddingValues.calculateTopPadding(), paddingValues.calculateBottomPadding())
|
||||||
content()
|
|
||||||
Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,6 +194,7 @@ fun StyledScaffold(
|
|||||||
)
|
)
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
val topPadding = paddingValues.calculateTopPadding()
|
val topPadding = paddingValues.calculateTopPadding()
|
||||||
|
val bottomPadding = paddingValues.calculateBottomPadding()
|
||||||
val startPadding = paddingValues.calculateLeftPadding(LocalLayoutDirection.current)
|
val startPadding = paddingValues.calculateLeftPadding(LocalLayoutDirection.current)
|
||||||
val endPadding = paddingValues.calculateRightPadding(LocalLayoutDirection.current)
|
val endPadding = paddingValues.calculateRightPadding(LocalLayoutDirection.current)
|
||||||
|
|
||||||
@@ -184,24 +206,71 @@ fun StyledScaffold(
|
|||||||
) {
|
) {
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
val bgColor = MaterialTheme.colorScheme.surfaceContainer
|
val bgColor = MaterialTheme.colorScheme.surfaceContainer
|
||||||
AnimatedVisibility(
|
|
||||||
visible = showBackButton,
|
val density = LocalDensity.current
|
||||||
enter = fadeIn() + scaleIn(
|
val screenWidthPx = with(density) {
|
||||||
initialScale = 0f,
|
LocalWindowInfo.current.containerDpSize.width.toPx()
|
||||||
animationSpec = tween()
|
}
|
||||||
),
|
val isCurrentEntry = LocalIsCurrentEntry.current
|
||||||
exit = fadeOut() + scaleOut(
|
val transitionProgress = LocalTransitionProgress.current
|
||||||
targetScale = 0.5f,
|
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||||
animationSpec = tween(100)
|
|
||||||
),
|
val showBackButton = if (transitionProgress == 0f) navigateBack != null else !isCurrentEntry
|
||||||
|
|
||||||
|
if (showBackButton) {
|
||||||
|
with(sharedTransitionScope) {
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.zIndex(3f)
|
.zIndex(3f)
|
||||||
.padding(top = topPadding, start = 8.dp)
|
.padding(top = topPadding, start = 8.dp)
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
awaitFirstDown(requireUnconsumed = false)
|
||||||
|
|
||||||
|
do {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
|
||||||
|
event.changes.forEach { change ->
|
||||||
|
change.consumePositionChange()
|
||||||
|
}
|
||||||
|
} while (event.changes.any { it.pressed })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.renderInSharedTransitionScopeOverlay(
|
||||||
|
zIndexInOverlay = 3f,
|
||||||
|
renderInOverlay = {
|
||||||
|
!isCurrentEntry && transitionProgress != 0f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.graphicsLayer { // AI generated
|
||||||
|
if (!isCurrentEntry && navigateBack == null && transitionProgress < 0f) {
|
||||||
|
val progress = (-transitionProgress).coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
val eased = progress * progress * (3f - 2f * progress)
|
||||||
|
|
||||||
|
val scale = 1f - 0.18f * eased
|
||||||
|
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
|
||||||
|
alpha = 1f - 0.28f * eased
|
||||||
|
|
||||||
|
val blur = 8f * progress
|
||||||
|
|
||||||
|
renderEffect = RenderEffect.createBlurEffect(
|
||||||
|
blur,
|
||||||
|
blur,
|
||||||
|
Shader.TileMode.DECAL
|
||||||
|
).asComposeRenderEffect()
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
StyledIconButton(
|
StyledIconButton(
|
||||||
onClick = onNavigateBack,
|
onClick = { navigateBack?.invoke() },
|
||||||
backdrop = backdrop
|
backdrop = backdrop // i know, this doesn't capture what's actually beneath it. but it's going to matter just in the transition.
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = LocalIcons.current.ArrowBack,
|
imageVector = LocalIcons.current.ArrowBack,
|
||||||
@@ -211,6 +280,8 @@ fun StyledScaffold(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
@@ -270,6 +341,21 @@ fun StyledScaffold(
|
|||||||
.zIndex(3f)
|
.zIndex(3f)
|
||||||
.padding(top = topPadding, end = 8.dp)
|
.padding(top = topPadding, end = 8.dp)
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
awaitFirstDown(requireUnconsumed = false)
|
||||||
|
|
||||||
|
do {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
|
||||||
|
event.changes.forEach { change ->
|
||||||
|
change.consumePositionChange()
|
||||||
|
}
|
||||||
|
} while (event.changes.any { it.pressed })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Row{
|
Row{
|
||||||
actionButtons.forEach { actionButton ->
|
actionButtons.forEach { actionButton ->
|
||||||
@@ -283,7 +369,7 @@ fun StyledScaffold(
|
|||||||
.hazeSource(hazeState)
|
.hazeSource(hazeState)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
content()
|
content(topPadding + 64.dp, bottomPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -759,12 +759,13 @@ fun StyledSliderPreview() {
|
|||||||
) {
|
) {
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
title = "test",
|
title = "test",
|
||||||
) {
|
navigateBack = null
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(72.dp))
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
StyledSlider(
|
StyledSlider(
|
||||||
value = a.floatValue,
|
value = a.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
@@ -803,6 +804,7 @@ fun StyledSliderPreview() {
|
|||||||
independent = true,
|
independent = true,
|
||||||
description = stringResource(R.string.adaptive_audio_description),
|
description = stringResource(R.string.adaptive_audio_description),
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1248,4 +1248,54 @@ object AppleIcons: IconSet {
|
|||||||
|
|
||||||
override val CircleDotted: ImageVector
|
override val CircleDotted: ImageVector
|
||||||
get() = CommonIcons.CircleDotted
|
get() = CommonIcons.CircleDotted
|
||||||
|
|
||||||
|
override val VitalSigns: ImageVector
|
||||||
|
get() = WaveFormPathEcg
|
||||||
|
val WaveFormPathEcg: ImageVector
|
||||||
|
get() {
|
||||||
|
val current = _waveformPathEcg
|
||||||
|
if (current != null) return current
|
||||||
|
|
||||||
|
return ImageVector.Builder(
|
||||||
|
name = "WaveformPathEcg",
|
||||||
|
defaultWidth = 62.78099822998047.dp,
|
||||||
|
defaultHeight = 67.31999969482422.dp,
|
||||||
|
viewportWidth = 62.781f,
|
||||||
|
viewportHeight = 67.32f,
|
||||||
|
).apply {
|
||||||
|
path(
|
||||||
|
fill = SolidColor(Color(0xFFFFFFFF)),
|
||||||
|
fillAlpha = 0.85f,
|
||||||
|
) {
|
||||||
|
moveTo(x = 2.25f, y = 39.7f)
|
||||||
|
horizontalLineToRelative(dx = 13.84f)
|
||||||
|
quadToRelative(dx1 = 2.1f, dy1 = 0.0f, dx2 = 2.57f, dy2 = -1.87f)
|
||||||
|
lineToRelative(dx = 4.84f, dy = -22.25f)
|
||||||
|
horizontalLineToRelative(dx = -0.28f)
|
||||||
|
lineToRelative(dx = 8.34f, dy = 50.06f)
|
||||||
|
curveToRelative(dx1 = 0.38f, dy1 = 2.25f, dx2 = 3.85f, dy2 = 2.22f, dx3 = 4.28f, dy3 = 0.0f)
|
||||||
|
lineToRelative(dx = 8.07f, dy = -37.75f)
|
||||||
|
lineToRelative(dx = -0.25f, dy = -0.03f)
|
||||||
|
lineToRelative(dx = 2.0f, dy = 9.72f)
|
||||||
|
curveToRelative(dx1 = 0.28f, dy1 = 1.44f, dx2 = 1.09f, dy2 = 2.13f, dx3 = 2.56f, dy3 = 2.13f)
|
||||||
|
horizontalLineToRelative(dx = 12.12f)
|
||||||
|
arcToRelative(a = 2.2f, b = 2.2f, theta = 0.0f, isMoreThanHalf = true, isPositiveArc = false, dx1 = 0.0f, dy1 = -4.4f)
|
||||||
|
horizontalLineTo(x = 48.66f)
|
||||||
|
lineToRelative(dx = 0.75f, dy = 0.46f)
|
||||||
|
lineToRelative(dx = -3.72f, dy = -16.38f)
|
||||||
|
curveToRelative(dx1 = -0.47f, dy1 = -2.18f, dx2 = -3.69f, dy2 = -2.15f, dx3 = -4.22f, dy3 = 0.1f)
|
||||||
|
lineTo(x = 33.4f, y = 55.43f)
|
||||||
|
horizontalLineToRelative(dx = 0.37f)
|
||||||
|
lineToRelative(dx = -8.37f, dy = -50.7f)
|
||||||
|
curveToRelative(dx1 = -0.35f, dy1 = -2.18f, dx2 = -3.53f, dy2 = -2.24f, dx3 = -4.03f, dy3 = 0.0f)
|
||||||
|
lineToRelative(dx = -6.72f, dy = 31.04f)
|
||||||
|
lineToRelative(dx = 0.4f, dy = -0.47f)
|
||||||
|
horizontalLineTo(x = 2.25f)
|
||||||
|
curveToRelative(dx1 = -1.28f, dy1 = 0.0f, dx2 = -2.25f, dy2 = 1.0f, dx3 = -2.25f, dy3 = 2.22f)
|
||||||
|
arcToRelative(a = 2.2f, b = 2.2f, theta = 0.0f, isMoreThanHalf = false, isPositiveArc = false, dx1 = 2.25f, dy1 = 2.19f)
|
||||||
|
}
|
||||||
|
}.build().also { _waveformPathEcg = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _waveformPathEcg: ImageVector? = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ interface IconSet {
|
|||||||
|
|
||||||
val CircleDotted: ImageVector
|
val CircleDotted: ImageVector
|
||||||
|
|
||||||
|
val VitalSigns: ImageVector
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* AirPods Icons
|
* AirPods Icons
|
||||||
*/
|
*/
|
||||||
@@ -155,6 +157,7 @@ interface IconSet {
|
|||||||
"BoltCircle" to BoltCircle,
|
"BoltCircle" to BoltCircle,
|
||||||
"Circle" to Circle,
|
"Circle" to Circle,
|
||||||
"CircleDotted" to CircleDotted,
|
"CircleDotted" to CircleDotted,
|
||||||
|
"VitalSign" to VitalSigns,
|
||||||
|
|
||||||
"AirPods1" to AirPods1,
|
"AirPods1" to AirPods1,
|
||||||
"AirPods1Case" to AirPods1Case,
|
"AirPods1Case" to AirPods1Case,
|
||||||
|
|||||||
+65
@@ -1658,4 +1658,69 @@ object MaterialIcons: IconSet {
|
|||||||
|
|
||||||
override val CircleDotted: ImageVector
|
override val CircleDotted: ImageVector
|
||||||
get() = CommonIcons.CircleDotted
|
get() = CommonIcons.CircleDotted
|
||||||
|
|
||||||
|
override val VitalSigns: ImageVector
|
||||||
|
get() {
|
||||||
|
if (_vital_signs != null) {
|
||||||
|
return _vital_signs!!
|
||||||
|
}
|
||||||
|
_vital_signs =
|
||||||
|
ImageVector.Builder(
|
||||||
|
name = "vital_signs",
|
||||||
|
defaultWidth = 24.dp,
|
||||||
|
defaultHeight = 24.dp,
|
||||||
|
viewportWidth = 24f,
|
||||||
|
viewportHeight = 24f,
|
||||||
|
)
|
||||||
|
.apply {
|
||||||
|
path(
|
||||||
|
fill = SolidColor(Color.Black),
|
||||||
|
fillAlpha = 1f,
|
||||||
|
stroke = null,
|
||||||
|
strokeAlpha = 1f,
|
||||||
|
strokeLineWidth = 1f,
|
||||||
|
strokeLineCap = StrokeCap.Butt,
|
||||||
|
strokeLineJoin = StrokeJoin.Bevel,
|
||||||
|
strokeLineMiter = 1f,
|
||||||
|
pathFillType = PathFillType.Companion.NonZero,
|
||||||
|
) {
|
||||||
|
moveTo(8.15f, 19.73f)
|
||||||
|
quadTo(7.78f, 19.45f, 7.6f, 19.02f)
|
||||||
|
lineTo(5.3f, 13f)
|
||||||
|
horizontalLineTo(2f)
|
||||||
|
quadTo(1.58f, 13f, 1.29f, 12.71f)
|
||||||
|
quadTo(1f, 12.43f, 1f, 12f)
|
||||||
|
reflectiveQuadTo(1.29f, 11.29f)
|
||||||
|
reflectiveQuadTo(2f, 11f)
|
||||||
|
horizontalLineTo(6f)
|
||||||
|
quadToRelative(0.33f, 0f, 0.56f, 0.17f)
|
||||||
|
reflectiveQuadToRelative(0.36f, 0.47f)
|
||||||
|
lineTo(9f, 17.1f)
|
||||||
|
lineTo(13.6f, 4.97f)
|
||||||
|
quadToRelative(0.17f, -0.43f, 0.55f, -0.7f)
|
||||||
|
reflectiveQuadTo(15f, 4f)
|
||||||
|
reflectiveQuadToRelative(0.85f, 0.27f)
|
||||||
|
reflectiveQuadToRelative(0.55f, 0.7f)
|
||||||
|
lineTo(18.7f, 11f)
|
||||||
|
horizontalLineTo(22f)
|
||||||
|
quadToRelative(0.43f, 0f, 0.71f, 0.29f)
|
||||||
|
reflectiveQuadTo(23f, 12f)
|
||||||
|
reflectiveQuadToRelative(-0.29f, 0.71f)
|
||||||
|
reflectiveQuadTo(22f, 13f)
|
||||||
|
horizontalLineTo(18f)
|
||||||
|
quadToRelative(-0.32f, 0f, -0.56f, -0.18f)
|
||||||
|
reflectiveQuadTo(17.08f, 12.35f)
|
||||||
|
lineTo(15f, 6.9f)
|
||||||
|
lineTo(10.4f, 19.02f)
|
||||||
|
quadToRelative(-0.17f, 0.43f, -0.55f, 0.7f)
|
||||||
|
reflectiveQuadTo(9f, 20f)
|
||||||
|
reflectiveQuadTo(8.15f, 19.73f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
return _vital_signs!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _vital_signs: ImageVector? = null
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-597
@@ -1,656 +1,112 @@
|
|||||||
package me.kavishdevar.librepods.presentation.navigation
|
package me.kavishdevar.librepods.presentation.navigation
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.activity.BackEventCompat.Companion.EDGE_LEFT
|
import androidx.compose.animation.EnterTransition
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
import androidx.compose.animation.SharedTransitionLayout
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.scaleOut
|
|
||||||
import androidx.compose.animation.slideInHorizontally
|
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
|
||||||
import androidx.navigation3.runtime.NavEntry
|
import androidx.navigation3.runtime.NavEntry
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
import me.kavishdevar.librepods.LibrePodsApplication
|
import me.kavishdevar.librepods.LibrePodsApplication
|
||||||
import me.kavishdevar.librepods.bluetooth.MacAddress
|
import me.kavishdevar.librepods.bluetooth.MacAddress
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
|
||||||
import me.kavishdevar.librepods.data.updates.updates
|
|
||||||
import me.kavishdevar.librepods.devices.AppleDevice
|
import me.kavishdevar.librepods.devices.AppleDevice
|
||||||
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.screens.AppSettingsScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.BLESettingsScreenRoute
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.DeviceListRoute
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.OpenSourceLicensesScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.PurchaseScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.ReleaseNotesScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.TroubleshootingScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.AccessibilitySettingsScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.AdaptiveStrengthScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.AirPodsSettingsRoute
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.CallControlScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.DebugRoute
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.EqualizerRoute
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.HeadTrackingScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.HearingAidAdjustmentsScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.HearingAidScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.HearingProtectionScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.LongPress
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.MicrophoneSettingsRoute
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.RecordingScreenRoute
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.RenameScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.TransparencySettingsScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.UpdateHearingTestRoute
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.VersionScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.screens.onboarding.OnboardingScreen
|
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
|
||||||
import me.kavishdevar.librepods.repository.RecordingRepository
|
|
||||||
|
|
||||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavGraph(
|
fun AppNavGraph(
|
||||||
|
backStack: NavBackStack<Screen>,
|
||||||
|
devicesState: State<Map<MacAddress, Device<*, *, *>>>,
|
||||||
showReleaseNotes: Boolean = false,
|
showReleaseNotes: Boolean = false,
|
||||||
updatesShown: () -> Unit = {},
|
updatesShown: () -> Unit = {},
|
||||||
onboardingComplete: () -> Unit = {},
|
onboardingComplete: () -> Unit = {},
|
||||||
backStack: SnapshotStateList<Screen>,
|
|
||||||
devicesState: State<Map<MacAddress, Device<*, *, *>>>
|
|
||||||
) {
|
) {
|
||||||
|
val backRequests = remember { Channel<CompletableDeferred<Unit>>(Channel.BUFFERED) }
|
||||||
|
|
||||||
val devices by devicesState
|
val devices by devicesState
|
||||||
|
|
||||||
val navigate: (Screen) -> Unit = { screen ->
|
|
||||||
backStack.add(screen)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun navigateToPurchase() {
|
|
||||||
navigate(Screen.Purchase)
|
|
||||||
}
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val appDataRepository by lazy { (context.applicationContext as LibrePodsApplication).appDataRepository }
|
val appDataRepository by lazy { (context.applicationContext as LibrePodsApplication).appDataRepository }
|
||||||
val recordingRepository = RecordingRepository(context)
|
val recordingRepository by lazy { (context.applicationContext as LibrePodsApplication).recordingRepository }
|
||||||
|
val heartRateRepository by lazy { (context.applicationContext as LibrePodsApplication).heartRateRepository }
|
||||||
|
|
||||||
val currentDevice: Device<*, *, *>? =
|
val currentDevice: Device<*, *, *>? = devices[backStack.lastOrNull()?.let { (it as? DeviceScreen)?.macAddress }]
|
||||||
devices[backStack.lastOrNull()?.let { (it as? DeviceScreen)?.macAddress }]
|
|
||||||
|
|
||||||
@SuppressLint("UnrememberedMutableState")
|
@SuppressLint("UnrememberedMutableState")
|
||||||
val currentConnectionState by (currentDevice as? AppleDevice)?.connectionState?.collectAsState()
|
val currentConnectionState by (currentDevice as? AppleDevice)?.connectionState?.collectAsState()?: mutableStateOf(ConnectionState.DISCONNECTED)
|
||||||
?: mutableStateOf(ConnectionState.DISCONNECTED)
|
|
||||||
|
|
||||||
|
LaunchedEffect(currentConnectionState) {
|
||||||
if (currentConnectionState == ConnectionState.DISCONNECTED) {
|
if (currentConnectionState == ConnectionState.DISCONNECTED) {
|
||||||
// not sure how we will be able to navigate to another device from one device, but just in case we had two different devices in the backstack, we will remove DeviceScreens only of the disconnected device from the backstack
|
while (
|
||||||
while (backStack.isNotEmpty() && backStack.last() is DeviceScreen && (backStack.last() as DeviceScreen).macAddress == currentDevice?.macAddress) {
|
backStack.size > 1 &&
|
||||||
|
backStack.lastOrNull() is DeviceScreen
|
||||||
|
) {
|
||||||
|
val completed = CompletableDeferred<Unit>()
|
||||||
|
backRequests.send(completed)
|
||||||
|
completed.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val appSettings by appDataRepository.settings.collectAsState()
|
||||||
|
|
||||||
|
val sceneStrategy = remember(appSettings.swipeAnywhereForBack) {
|
||||||
|
SwipeBackSceneStrategy<Screen>(
|
||||||
|
enabled = appSettings.swipeAnywhereForBack,
|
||||||
|
backRequests = backRequests,
|
||||||
|
onDismiss = {
|
||||||
|
if (backStack.size > 1) {
|
||||||
backStack.removeAt(backStack.lastIndex)
|
backStack.removeAt(backStack.lastIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
}
|
||||||
|
|
||||||
SharedTransitionLayout {
|
SharedTransitionLayout {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalSharedTransitionScope provides this,
|
||||||
|
) {
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
sharedTransitionScope = this,
|
sharedTransitionScope = this,
|
||||||
backStack = backStack,
|
backStack = backStack,
|
||||||
|
sceneStrategies = listOf(sceneStrategy),
|
||||||
onBack = {
|
onBack = {
|
||||||
if (backStack.size > 1) {
|
if (backStack.size > 1) {
|
||||||
backStack.removeAt(backStack.lastIndex)
|
backStack.removeAt(backStack.lastIndex)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
entryProvider = { screen ->
|
entryProvider = { screen ->
|
||||||
when (screen) {
|
|
||||||
Screen.Onboarding ->
|
|
||||||
NavEntry(screen) {
|
NavEntry(screen) {
|
||||||
OnboardingScreen {
|
RenderScreenContent(
|
||||||
onboardingComplete()
|
screen = screen,
|
||||||
if (showReleaseNotes) navigate(Screen.ReleaseNotes) else navigate(
|
backStack = backStack,
|
||||||
Screen.DeviceList
|
backRequests = backRequests,
|
||||||
)
|
|
||||||
backStack.remove(screen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Screen.DeviceList ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
DeviceListRoute(
|
|
||||||
devices = devices,
|
devices = devices,
|
||||||
navigateToDevice = { macAddress ->
|
|
||||||
when (devices[macAddress]) {
|
|
||||||
is AppleDevice -> navigate(Screen.AppleScreen(macAddress))
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.AppleScreen ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
AirPodsSettingsRoute(
|
|
||||||
viewModel = appleViewModel,
|
|
||||||
navigateToRename = { navigate(Screen.Rename(screen.macAddress)) },
|
|
||||||
navigateToHearingProtection = {
|
|
||||||
navigate(
|
|
||||||
Screen.HearingProtection(
|
|
||||||
screen.macAddress
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigateToHearingAid = { navigate(Screen.HearingAid(screen.macAddress)) },
|
|
||||||
navigateToLeftLongPress = {
|
|
||||||
navigate(
|
|
||||||
Screen.LongPress(screen.macAddress, "Left")
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigateToRightLongPress = {
|
|
||||||
navigate(
|
|
||||||
Screen.LongPress(screen.macAddress, "Right")
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigateToPurchase = { navigate(Screen.Purchase) },
|
|
||||||
navigateToAdaptiveStrength = {
|
|
||||||
navigate(
|
|
||||||
Screen.AdaptiveStrength(
|
|
||||||
screen.macAddress
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigateToEqualizer = { navigate(Screen.Equalizer(screen.macAddress)) },
|
|
||||||
navigateToHeadTracking = { navigate(Screen.HeadTracking(screen.macAddress)) },
|
|
||||||
navigateToAccessibility = { navigate(Screen.Accessibility(screen.macAddress)) },
|
|
||||||
navigateToVersion = { navigate(Screen.VersionInfo(screen.macAddress)) },
|
|
||||||
navigateToCallControlScreen = {
|
|
||||||
navigate(
|
|
||||||
Screen.CallControl(
|
|
||||||
screen.macAddress,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigateToMicrophoneSettings = {
|
|
||||||
navigate(
|
|
||||||
Screen.MicrophoneSettings(
|
|
||||||
screen.macAddress
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigateToRecordingScreen = { navigate(Screen.Recording(screen.macAddress)) },
|
|
||||||
navigateToDebugScreen = { navigate(Screen.Debug(screen.macAddress)) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.Rename ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
RenameScreen(appleViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
Screen.AppSettings ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppSettingsViewModel(
|
|
||||||
appDataRepository = appDataRepository,
|
appDataRepository = appDataRepository,
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val appSettingsViewModel: AppSettingsViewModel = viewModel(factory = factory)
|
|
||||||
|
|
||||||
AppSettingsScreen(
|
|
||||||
viewModel = appSettingsViewModel,
|
|
||||||
navigateToPurchase = ::navigateToPurchase,
|
|
||||||
navigateToTroubleshooting = { navigate(Screen.Troubleshooting) },
|
|
||||||
navigateToOpenSourceLicenses = { navigate(Screen.OpenSourceLicenses) },
|
|
||||||
navigateToReleaseNotesScreen = { navigate(Screen.ReleaseNotes) },
|
|
||||||
navigateToBleSettingsScreen = { navigate(Screen.BLESettings) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Screen.Troubleshooting ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
TroubleshootingScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.HeadTracking ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
recordingRepository = recordingRepository,
|
||||||
|
heartRateRepository = heartRateRepository,
|
||||||
|
showReleaseNotes = showReleaseNotes,
|
||||||
|
updatesShown = updatesShown,
|
||||||
|
onboardingComplete = onboardingComplete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
HeadTrackingScreen(appleViewModel, ::navigateToPurchase)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.Accessibility ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
AccessibilitySettingsScreen(
|
|
||||||
viewModel = appleViewModel,
|
|
||||||
navigateToPurchase = ::navigateToPurchase,
|
|
||||||
navigateToTransparencyCustomization = {
|
|
||||||
navigate(
|
|
||||||
Screen.TransparencyCustomization(
|
|
||||||
screen.macAddress
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.TransparencyCustomization ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
TransparencySettingsScreen(appleViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.HearingAid ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
HearingAidScreen(
|
|
||||||
viewModel = appleViewModel,
|
|
||||||
onNavigateHearingAidAdjustments = {
|
|
||||||
navigate(
|
|
||||||
Screen.HearingAidAdjustments(
|
|
||||||
screen.macAddress
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onNavigateHearingTest = { navigate(Screen.UpdateHearingTest(screen.macAddress)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.HearingAidAdjustments ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
HearingAidAdjustmentsScreen(appleViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.AdaptiveStrength ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
AdaptiveStrengthScreen(appleViewModel, ::navigateToPurchase)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Screen.CameraControl ->
|
|
||||||
// NavEntry(screen) {
|
|
||||||
// CameraControlScreen(AppleViewModel)
|
|
||||||
// }
|
|
||||||
|
|
||||||
Screen.OpenSourceLicenses ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
OpenSourceLicensesScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.UpdateHearingTest ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
UpdateHearingTestRoute(appleViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.VersionInfo ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
VersionScreen(appleViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.HearingProtection ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
HearingProtectionScreen(
|
|
||||||
viewModel = appleViewModel,
|
|
||||||
navigateToPurchase = ::navigateToPurchase
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.Purchase ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val vm: PurchaseViewModel = viewModel()
|
|
||||||
PurchaseScreen(vm, backStack)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.Equalizer ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
EqualizerRoute(appleViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.LongPress ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
LongPress(
|
|
||||||
viewModel = appleViewModel,
|
|
||||||
name = screen.bud,
|
|
||||||
navigateToPurchase = ::navigateToPurchase
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.CallControl ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
CallControlScreen(
|
|
||||||
viewModel = appleViewModel,
|
|
||||||
action = screen.action,
|
|
||||||
onCallControlValueChanged = { flipped ->
|
|
||||||
device.setControlCommand(
|
|
||||||
ControlCommandIdentifier.CALL_MANAGEMENT_CONFIG,
|
|
||||||
if (flipped) byteArrayOf(0x00, 0x02) else byteArrayOf(
|
|
||||||
0x00,
|
|
||||||
0x03
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.MicrophoneSettings ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
MicrophoneSettingsRoute(viewModel = appleViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.ReleaseNotes ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
ReleaseNotesScreen(
|
|
||||||
updates = updates,
|
|
||||||
releaseNotesShown = {
|
|
||||||
if (showReleaseNotes) {
|
|
||||||
navigate(Screen.DeviceList)
|
|
||||||
backStack.remove(screen)
|
|
||||||
updatesShown()
|
|
||||||
} else {
|
|
||||||
backStack.removeAt(backStack.lastIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.Recording ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
RecordingScreenRoute(appleViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.Debug ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val device = devices[screen.macAddress] as? AppleDevice ?: return@NavEntry
|
|
||||||
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppleViewModel(
|
|
||||||
device = device,
|
|
||||||
recordingRepository = recordingRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appleViewModel: AppleViewModel = viewModel(key = "${screen.macAddress.value}:${device.connectionNumber}", factory = factory)
|
|
||||||
|
|
||||||
DebugRoute(appleViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Screen.BLESettings ->
|
|
||||||
NavEntry(screen) {
|
|
||||||
val factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
AppSettingsViewModel(
|
|
||||||
appDataRepository = appDataRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val appSettingsViewModel: AppSettingsViewModel = viewModel(factory = factory)
|
|
||||||
|
|
||||||
BLESettingsScreenRoute(
|
|
||||||
viewModel = appSettingsViewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
transitionSpec = {
|
transitionSpec = { EnterTransition.None togetherWith ExitTransition.None },
|
||||||
slideInHorizontally { it } togetherWith slideOutHorizontally { -it / 4 }
|
popTransitionSpec = { EnterTransition.None togetherWith ExitTransition.None },
|
||||||
},
|
predictivePopTransitionSpec = { EnterTransition.None togetherWith ExitTransition.None }
|
||||||
popTransitionSpec = {
|
|
||||||
slideInHorizontally { -it / 4 } togetherWith slideOutHorizontally { it }
|
|
||||||
},
|
|
||||||
predictivePopTransitionSpec = { swipeEdge ->
|
|
||||||
if (m3eEnabled) {
|
|
||||||
val enterOffset: (Int) -> Int =
|
|
||||||
if (swipeEdge == EDGE_LEFT) {
|
|
||||||
{ -it / 6 }
|
|
||||||
} else {
|
|
||||||
{ it / 6 }
|
|
||||||
}
|
|
||||||
|
|
||||||
val exitOffset: (Int) -> Int =
|
|
||||||
if (swipeEdge == EDGE_LEFT) {
|
|
||||||
{ it / 8 }
|
|
||||||
} else {
|
|
||||||
{ -it / 8 }
|
|
||||||
}
|
|
||||||
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(250)
|
|
||||||
) +
|
|
||||||
slideInHorizontally(
|
|
||||||
initialOffsetX = enterOffset,
|
|
||||||
animationSpec = tween(250)
|
|
||||||
) togetherWith
|
|
||||||
fadeOut(
|
|
||||||
targetAlpha = 0.75f,
|
|
||||||
animationSpec = tween(250)
|
|
||||||
) +
|
|
||||||
scaleOut(
|
|
||||||
targetScale = 0.85f,
|
|
||||||
animationSpec = tween(250)
|
|
||||||
) +
|
|
||||||
slideOutHorizontally(
|
|
||||||
targetOffsetX = exitOffset,
|
|
||||||
animationSpec = tween(250)
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
slideInHorizontally { -it / 4 } togetherWith slideOutHorizontally { it }
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-136
@@ -1,35 +1,26 @@
|
|||||||
package me.kavishdevar.librepods.presentation.navigation
|
package me.kavishdevar.librepods.presentation.navigation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.material3.FilledTonalIconButton
|
|
||||||
import androidx.compose.material3.FilledTonalIconToggleButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.runtime.saveable.rememberSerializable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
import androidx.compose.ui.unit.dp
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import com.kyant.backdrop.backdrops.LayerBackdrop
|
import kotlinx.serialization.serializer
|
||||||
import me.kavishdevar.librepods.R
|
|
||||||
import me.kavishdevar.librepods.bluetooth.MacAddress
|
import me.kavishdevar.librepods.bluetooth.MacAddress
|
||||||
import me.kavishdevar.librepods.devices.AppleDevice
|
import me.kavishdevar.librepods.devices.AppleDevice
|
||||||
|
import me.kavishdevar.librepods.devices.ConnectionState
|
||||||
import me.kavishdevar.librepods.devices.Device
|
import me.kavishdevar.librepods.devices.Device
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
@Composable
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
fun rememberScreenNavBackStack(vararg elements: Screen): NavBackStack<Screen> {
|
||||||
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
return rememberSerializable(serializer = serializer()) {
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
NavBackStack(*elements)
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationRoot(
|
fun NavigationRoot(
|
||||||
@@ -41,133 +32,33 @@ fun NavigationRoot(
|
|||||||
) {
|
) {
|
||||||
val devices by devicesState
|
val devices by devicesState
|
||||||
|
|
||||||
val backStack = remember {
|
val backStack = rememberScreenNavBackStack(
|
||||||
mutableStateListOf(
|
|
||||||
when {
|
when {
|
||||||
showOnboarding -> Screen.Onboarding
|
showOnboarding -> Screen.Onboarding
|
||||||
showReleaseNotes -> Screen.ReleaseNotes
|
showReleaseNotes -> Screen.ReleaseNotes
|
||||||
else -> Screen.DeviceList
|
else -> Screen.DeviceList
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val connectedDevice = devices.values.firstOrNull { it.connectionState.collectAsState().value == ConnectionState.CONNECTED }
|
||||||
|
|
||||||
|
LaunchedEffect(connectedDevice) {
|
||||||
|
if (connectedDevice != null) {
|
||||||
|
val targetScreen = when (connectedDevice) {
|
||||||
|
is AppleDevice -> Screen.AppleScreen(connectedDevice.macAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentScreen = backStack.last()
|
if (targetScreen !in backStack) {
|
||||||
|
backStack.add(targetScreen)
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
}
|
||||||
|
}
|
||||||
val title = when (currentScreen) {
|
|
||||||
Screen.Onboarding -> ""
|
|
||||||
Screen.DeviceList -> stringResource(R.string.app_name)
|
|
||||||
is Screen.AppleScreen -> devices[currentScreen.macAddress]?.metadata?.collectAsState()?.value?.name ?: currentScreen.macAddress.value
|
|
||||||
is Screen.Accessibility -> stringResource(R.string.accessibility)
|
|
||||||
is Screen.AdaptiveStrength -> stringResource(R.string.customize_adaptive_audio)
|
|
||||||
Screen.AppSettings -> stringResource(R.string.settings)
|
|
||||||
// Screen.CameraControl -> stringResource(R.string.camera_control)
|
|
||||||
is Screen.Equalizer -> stringResource(R.string.equalizer)
|
|
||||||
is Screen.HeadTracking -> stringResource(R.string.head_tracking)
|
|
||||||
is Screen.HearingAid -> stringResource(R.string.hearing_aid)
|
|
||||||
is Screen.HearingAidAdjustments -> stringResource(R.string.adjustments)
|
|
||||||
is Screen.HearingProtection -> stringResource(R.string.hearing_protection)
|
|
||||||
is Screen.LongPress -> currentScreen.bud
|
|
||||||
Screen.OpenSourceLicenses -> stringResource(R.string.open_source_licenses)
|
|
||||||
Screen.Purchase -> stringResource(R.string.unlock_advanced_features)
|
|
||||||
is Screen.Rename -> stringResource(R.string.name)
|
|
||||||
is Screen.TransparencyCustomization -> stringResource(R.string.customize_transparency_mode)
|
|
||||||
Screen.Troubleshooting -> stringResource(R.string.troubleshooting)
|
|
||||||
is Screen.UpdateHearingTest -> stringResource(R.string.update_hearing_test)
|
|
||||||
is Screen.VersionInfo -> stringResource(R.string.version)
|
|
||||||
is Screen.CallControl -> currentScreen.action
|
|
||||||
is Screen.MicrophoneSettings -> stringResource(R.string.microphone_mode)
|
|
||||||
Screen.ReleaseNotes -> ""
|
|
||||||
is Screen.Recording -> stringResource(R.string.recorder)
|
|
||||||
is Screen.Debug -> "debug"
|
|
||||||
is Screen.BLESettings -> stringResource(R.string.ble_settings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// is this a bad idea? probably. I can't think of a better way without having to pass around a shouldShowBackButton to each screen to pass to each scaffold
|
|
||||||
val actionButtons = when (currentScreen) {
|
|
||||||
is Screen.AppleScreen, is Screen.DeviceList -> listOf<@Composable (backdrop: LayerBackdrop) -> Unit>(
|
|
||||||
{ scaffoldBackdrop ->
|
|
||||||
if (m3eEnabled) {
|
|
||||||
FilledTonalIconButton(
|
|
||||||
onClick = { backStack.add(Screen.AppSettings) },
|
|
||||||
modifier = Modifier
|
|
||||||
.minimumInteractiveComponentSize()
|
|
||||||
.size(IconButtonDefaults.mediumContainerSize(IconButtonDefaults.IconButtonWidthOption.Uniform)),
|
|
||||||
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Settings,
|
|
||||||
contentDescription = "settings",
|
|
||||||
modifier = Modifier.size(IconButtonDefaults.mediumIconSize),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
StyledIconButton(
|
|
||||||
onClick = { backStack.add(Screen.AppSettings) },
|
|
||||||
backdrop = scaffoldBackdrop
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = LocalIcons.current.Settings,
|
|
||||||
contentDescription = "Settings",
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
is Screen.HeadTracking -> listOf<@Composable (backdrop: LayerBackdrop) -> Unit>(
|
|
||||||
{ scaffoldBackdrop ->
|
|
||||||
val device = devices[currentScreen.macAddress] as? AppleDevice? ?: return@listOf
|
|
||||||
|
|
||||||
val state by device.state.collectAsState()
|
|
||||||
|
|
||||||
if (m3eEnabled) {
|
|
||||||
FilledTonalIconToggleButton(
|
|
||||||
checked = state.headTrackingActive,
|
|
||||||
onCheckedChange = { if (it) device.startHeadTracking() else device.stopHeadTracking() },
|
|
||||||
modifier = Modifier
|
|
||||||
.minimumInteractiveComponentSize()
|
|
||||||
.size(IconButtonDefaults.mediumContainerSize(IconButtonDefaults.IconButtonWidthOption.Uniform)),
|
|
||||||
shape = IconButtonDefaults.mediumRoundShape
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (state.headTrackingActive) MaterialIcons.Pause else Icons.Default.PlayArrow,
|
|
||||||
contentDescription = "Play/Pause",
|
|
||||||
modifier = Modifier.size(IconButtonDefaults.mediumIconSize),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
StyledIconButton(
|
|
||||||
onClick = if (!state.headTrackingActive) device::startHeadTracking else device::stopHeadTracking,
|
|
||||||
backdrop = scaffoldBackdrop
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (state.headTrackingActive) LocalIcons.current.Pause else LocalIcons.current.Play,
|
|
||||||
contentDescription = "Play/Pause",
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else -> listOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledScaffold(
|
|
||||||
visible = currentScreen.showTopBar,
|
|
||||||
title = title,
|
|
||||||
showBackButton = backStack.size > 1,
|
|
||||||
onNavigateBack = { backStack.removeAt(backStack.lastIndex) },
|
|
||||||
actionButtons = actionButtons
|
|
||||||
) {
|
|
||||||
AppNavGraph(
|
AppNavGraph(
|
||||||
|
backStack = backStack,
|
||||||
|
devicesState = devicesState,
|
||||||
showReleaseNotes = showReleaseNotes,
|
showReleaseNotes = showReleaseNotes,
|
||||||
updatesShown = updatesShown,
|
updatesShown = updatesShown,
|
||||||
onboardingComplete = onboardingComplete,
|
onboardingComplete = onboardingComplete,
|
||||||
backStack = backStack,
|
|
||||||
devicesState = devicesState,
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+438
@@ -0,0 +1,438 @@
|
|||||||
|
package me.kavishdevar.librepods.presentation.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.bluetooth.MacAddress
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||||
|
import me.kavishdevar.librepods.data.updates.updates
|
||||||
|
import me.kavishdevar.librepods.devices.AppleDevice
|
||||||
|
import me.kavishdevar.librepods.devices.Device
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.AppSettingsScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.BLESettingsScreenRoute
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.DeviceListScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.OpenSourceLicensesScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.PurchaseScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.ReleaseNotesScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.AccessibilitySettingsScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.AdaptiveStrengthScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.AppleSettingsRoute
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.CallControlScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.DebugRoute
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.EqualizerRoute
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.HeadTrackingScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.HearingAidAdjustmentsScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.HearingAidScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.HearingProtectionScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.HeartRateRoute
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.LongPress
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.MicrophoneSettingsRoute
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.RecordingScreenRoute
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.RenameScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.TransparencySettingsScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.UpdateHearingTestRoute
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.apple.VersionScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.onboarding.OnboardingScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
||||||
|
import me.kavishdevar.librepods.repository.AppDataRepository
|
||||||
|
import me.kavishdevar.librepods.repository.HeartRateRepository
|
||||||
|
import me.kavishdevar.librepods.repository.RecordingRepository
|
||||||
|
|
||||||
|
|
||||||
|
private fun createAppleViewModelFactory(
|
||||||
|
device: AppleDevice,
|
||||||
|
appDataRepository: AppDataRepository,
|
||||||
|
recordingRepository: RecordingRepository,
|
||||||
|
heartRateRepository: HeartRateRepository
|
||||||
|
): ViewModelProvider.Factory = viewModelFactory {
|
||||||
|
initializer {
|
||||||
|
AppleViewModel(
|
||||||
|
device = device,
|
||||||
|
appDataRepository = appDataRepository,
|
||||||
|
recordingRepository = recordingRepository,
|
||||||
|
heartRateRepository = heartRateRepository
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderScreenContent(
|
||||||
|
screen: Screen,
|
||||||
|
backStack: NavBackStack<Screen>,
|
||||||
|
backRequests: Channel<CompletableDeferred<Unit>>,
|
||||||
|
devices: Map<MacAddress, Device<*, *, *>>,
|
||||||
|
appDataRepository: AppDataRepository,
|
||||||
|
recordingRepository: RecordingRepository,
|
||||||
|
heartRateRepository: HeartRateRepository,
|
||||||
|
showReleaseNotes: Boolean,
|
||||||
|
updatesShown: () -> Unit,
|
||||||
|
onboardingComplete: () -> Unit
|
||||||
|
) {
|
||||||
|
val navigate: (Screen) -> Unit = { target -> backStack.add(target) }
|
||||||
|
fun navigateToPurchase() = navigate(Screen.Purchase)
|
||||||
|
|
||||||
|
val navigateBack: (() -> Unit)? = if (backStack.size > 1) {
|
||||||
|
{
|
||||||
|
val completed = CompletableDeferred<Unit>()
|
||||||
|
backRequests.trySend(completed)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
|
||||||
|
when (screen) {
|
||||||
|
Screen.Onboarding -> {
|
||||||
|
OnboardingScreen {
|
||||||
|
onboardingComplete()
|
||||||
|
if (showReleaseNotes) navigate(Screen.ReleaseNotes) else navigate(Screen.DeviceList)
|
||||||
|
backStack.remove(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Screen.DeviceList -> {
|
||||||
|
DeviceListScreen(
|
||||||
|
devices = devices,
|
||||||
|
navigateToAppSettings = { navigate(Screen.AppSettings) },
|
||||||
|
navigateToDevice = { macAddress ->
|
||||||
|
when (devices[macAddress]) {
|
||||||
|
is AppleDevice -> navigate(Screen.AppleScreen(macAddress))
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.AppleScreen -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
val left = stringResource(R.string.left)
|
||||||
|
val right = stringResource(R.string.right)
|
||||||
|
|
||||||
|
AppleSettingsRoute(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
navigateToRename = { navigate(Screen.Rename(screen.macAddress)) },
|
||||||
|
navigateToHearingProtection = { navigate(Screen.HearingProtection(screen.macAddress)) },
|
||||||
|
navigateToHearingAid = { navigate(Screen.HearingAid(screen.macAddress)) },
|
||||||
|
navigateToLeftLongPress = { navigate(Screen.LongPress(screen.macAddress, left)) },
|
||||||
|
navigateToRightLongPress = { navigate(Screen.LongPress(screen.macAddress, right)) },
|
||||||
|
navigateToPurchase = ::navigateToPurchase,
|
||||||
|
navigateToAdaptiveStrength = { navigate(Screen.AdaptiveStrength(screen.macAddress)) },
|
||||||
|
navigateToEqualizer = { navigate(Screen.Equalizer(screen.macAddress)) },
|
||||||
|
navigateToHeadTracking = { navigate(Screen.HeadTracking(screen.macAddress)) },
|
||||||
|
navigateToAccessibility = { navigate(Screen.Accessibility(screen.macAddress)) },
|
||||||
|
navigateToVersion = { navigate(Screen.VersionInfo(screen.macAddress)) },
|
||||||
|
navigateToCallControlScreen = { navigate(Screen.CallControl(screen.macAddress, it)) },
|
||||||
|
navigateToMicrophoneSettings = { navigate(Screen.MicrophoneSettings(screen.macAddress)) },
|
||||||
|
navigateToRecordingScreen = { navigate(Screen.Recording(screen.macAddress)) },
|
||||||
|
navigateToHeartRateScreen = { navigate(Screen.HeartRate(screen.macAddress)) },
|
||||||
|
navigateToDebugScreen = { navigate(Screen.Debug(screen.macAddress)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.Rename -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
RenameScreen(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Screen.AppSettings -> {
|
||||||
|
val factory = viewModelFactory {
|
||||||
|
initializer { AppSettingsViewModel(appDataRepository = appDataRepository) }
|
||||||
|
}
|
||||||
|
val appSettingsViewModel: AppSettingsViewModel = viewModel(factory = factory)
|
||||||
|
|
||||||
|
AppSettingsScreen(
|
||||||
|
viewModel = appSettingsViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
navigateToPurchase = ::navigateToPurchase,
|
||||||
|
navigateToOpenSourceLicenses = { navigate(Screen.OpenSourceLicenses) },
|
||||||
|
navigateToReleaseNotesScreen = { navigate(Screen.ReleaseNotes) },
|
||||||
|
navigateToBleSettingsScreen = { navigate(Screen.BLESettings) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.HeadTracking -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
HeadTrackingScreen(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
navigateToPurchase = ::navigateToPurchase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.Accessibility -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
AccessibilitySettingsScreen(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
navigateToPurchase = ::navigateToPurchase,
|
||||||
|
navigateToTransparencyCustomization = {
|
||||||
|
navigate(Screen.TransparencyCustomization(screen.macAddress))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.TransparencyCustomization -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
TransparencySettingsScreen(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.HearingAid -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
HearingAidScreen(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
navigateToHearingAidAdjustments = { navigate(Screen.HearingAidAdjustments(screen.macAddress)) },
|
||||||
|
navigateToHearingTest = { navigate(Screen.UpdateHearingTest(screen.macAddress)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.HearingAidAdjustments -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
HearingAidAdjustmentsScreen(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.AdaptiveStrength -> {
|
||||||
|
val device = devices[screen.macAddress] as? AppleDevice ?: return
|
||||||
|
val factory = createAppleViewModelFactory(device, appDataRepository, recordingRepository, heartRateRepository)
|
||||||
|
val appleViewModel: AppleViewModel = viewModel(
|
||||||
|
key = "${screen.macAddress.value}:${device.connectionNumber}",
|
||||||
|
factory = factory
|
||||||
|
)
|
||||||
|
AdaptiveStrengthScreen(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
navigateToPurchase = ::navigateToPurchase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Screen.OpenSourceLicenses -> OpenSourceLicensesScreen(navigateBack)
|
||||||
|
|
||||||
|
is Screen.UpdateHearingTest -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
UpdateHearingTestRoute(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.VersionInfo -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
VersionScreen(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.HearingProtection -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
HearingProtectionScreen(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
navigateToPurchase = ::navigateToPurchase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.Purchase -> {
|
||||||
|
val viewModel: PurchaseViewModel = viewModel()
|
||||||
|
PurchaseScreen(
|
||||||
|
viewModel = viewModel,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.Equalizer -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
EqualizerRoute(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.LongPress -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
LongPress(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
name = screen.bud,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
navigateToPurchase = ::navigateToPurchase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.CallControl -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
CallControlScreen(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
action = screen.action,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
onCallControlValueChanged = { flipped ->
|
||||||
|
device.setControlCommand(
|
||||||
|
ControlCommandIdentifier.CALL_MANAGEMENT_CONFIG,
|
||||||
|
if (flipped) byteArrayOf(0x00, 0x02) else byteArrayOf(0x00, 0x03)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.MicrophoneSettings -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
MicrophoneSettingsRoute(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.ReleaseNotes -> {
|
||||||
|
ReleaseNotesScreen(
|
||||||
|
updates = updates,
|
||||||
|
releaseNotesShown = {
|
||||||
|
if (showReleaseNotes) {
|
||||||
|
navigate(Screen.DeviceList)
|
||||||
|
backStack.remove(screen)
|
||||||
|
updatesShown()
|
||||||
|
} else {
|
||||||
|
backStack.removeAt(backStack.lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.Recording -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
RecordingScreenRoute(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.Debug -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
DebugRoute(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.BLESettings -> {
|
||||||
|
val factory = viewModelFactory {
|
||||||
|
initializer { AppSettingsViewModel(appDataRepository = appDataRepository) }
|
||||||
|
}
|
||||||
|
val appSettingsViewModel: AppSettingsViewModel = viewModel(factory = factory)
|
||||||
|
BLESettingsScreenRoute(
|
||||||
|
viewModel = appSettingsViewModel,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Screen.HeartRate -> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
HeartRateRoute(
|
||||||
|
viewModel = appleViewModel,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-3
@@ -30,9 +30,6 @@ sealed interface Screen: NavKey {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data object AppSettings: Screen
|
data object AppSettings: Screen
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data object Troubleshooting: Screen
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HeadTracking(
|
data class HeadTracking(
|
||||||
override val macAddress: MacAddress
|
override val macAddress: MacAddress
|
||||||
@@ -126,6 +123,11 @@ sealed interface Screen: NavKey {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object BLESettings: Screen
|
data object BLESettings: Screen
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HeartRate(
|
||||||
|
override val macAddress: MacAddress
|
||||||
|
): DeviceScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
+297
@@ -0,0 +1,297 @@
|
|||||||
|
package me.kavishdevar.librepods.presentation.navigation
|
||||||
|
|
||||||
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.draggable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
|
import androidx.navigation3.runtime.NavEntry
|
||||||
|
import androidx.navigation3.scene.Scene
|
||||||
|
import androidx.navigation3.scene.SceneStrategy
|
||||||
|
import androidx.navigation3.scene.SceneStrategyScope
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
// mostly AI generated
|
||||||
|
|
||||||
|
private enum class Direction {
|
||||||
|
Forward,
|
||||||
|
Back,
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
class SwipeBackSceneStrategy<T : Any>(
|
||||||
|
private val enabled: Boolean,
|
||||||
|
private val backRequests: Channel<CompletableDeferred<Unit>>,
|
||||||
|
private val onDismiss: () -> Unit
|
||||||
|
) : SceneStrategy<T> {
|
||||||
|
|
||||||
|
private var previousEntries: List<NavEntry<T>> = emptyList()
|
||||||
|
|
||||||
|
override fun SceneStrategyScope<T>.calculateScene(
|
||||||
|
entries: List<NavEntry<T>>
|
||||||
|
): Scene<T>? {
|
||||||
|
if (entries.isEmpty()) return null
|
||||||
|
|
||||||
|
val currentEntry = entries.last()
|
||||||
|
val previousEntry = entries.getOrNull(entries.lastIndex - 1)
|
||||||
|
|
||||||
|
val direction = when {
|
||||||
|
previousEntries.isEmpty() -> Direction.None
|
||||||
|
currentEntry in previousEntries -> Direction.Back
|
||||||
|
else -> Direction.Forward
|
||||||
|
}
|
||||||
|
|
||||||
|
previousEntries = entries
|
||||||
|
//
|
||||||
|
// if (previousEntry == null) {
|
||||||
|
// return object : Scene<T> {
|
||||||
|
// override val key: Any
|
||||||
|
// get() = "${currentEntry.contentKey}_${currentEntry.hashCode()}"
|
||||||
|
//
|
||||||
|
// override val entries: List<NavEntry<T>>
|
||||||
|
// get() = listOf(currentEntry)
|
||||||
|
//
|
||||||
|
// override val previousEntries: List<NavEntry<T>>
|
||||||
|
// get() = emptyList()
|
||||||
|
//
|
||||||
|
// override val content: @Composable () -> Unit
|
||||||
|
// get() = { currentEntry.Content() }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return object : Scene<T> {
|
||||||
|
override val key: Any
|
||||||
|
get() = "${currentEntry.contentKey}_${currentEntry.hashCode()}"
|
||||||
|
|
||||||
|
override val entries: List<NavEntry<T>>
|
||||||
|
get() = listOfNotNull(previousEntry, currentEntry)
|
||||||
|
|
||||||
|
override val previousEntries: List<NavEntry<T>>
|
||||||
|
get() = listOfNotNull(previousEntry)
|
||||||
|
|
||||||
|
override val content: @Composable () -> Unit
|
||||||
|
get() = {
|
||||||
|
SwipeBackSceneContent(
|
||||||
|
previousEntry = previousEntry,
|
||||||
|
currentEntry = currentEntry,
|
||||||
|
direction = direction,
|
||||||
|
swipeEnabled = enabled,
|
||||||
|
backRequests = backRequests,
|
||||||
|
onDismiss = onDismiss
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun <T : Any> SwipeBackSceneContent(
|
||||||
|
previousEntry: NavEntry<T>?,
|
||||||
|
currentEntry: NavEntry<T>,
|
||||||
|
direction: Direction,
|
||||||
|
swipeEnabled: Boolean,
|
||||||
|
backRequests: Channel<CompletableDeferred<Unit>>,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val screenWidthPx = with(density) {
|
||||||
|
LocalWindowInfo.current.containerDpSize.width.toPx()
|
||||||
|
}
|
||||||
|
|
||||||
|
val animatedOffset = remember(
|
||||||
|
"${currentEntry.contentKey}_${currentEntry.hashCode()}"
|
||||||
|
) {
|
||||||
|
Animatable(
|
||||||
|
if (direction == Direction.Forward) {
|
||||||
|
screenWidthPx
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var transitionProgress by remember {
|
||||||
|
mutableFloatStateOf(
|
||||||
|
if (direction == Direction.Forward) 1f else 0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(
|
||||||
|
"${currentEntry.contentKey}_${currentEntry.hashCode()}",
|
||||||
|
direction
|
||||||
|
) {
|
||||||
|
if (direction == Direction.Forward) {
|
||||||
|
transitionProgress = 1f
|
||||||
|
|
||||||
|
animatedOffset.animateTo(
|
||||||
|
0f,
|
||||||
|
tween(220)
|
||||||
|
)
|
||||||
|
|
||||||
|
transitionProgress = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
for (completed in backRequests) {
|
||||||
|
animatedOffset.animateTo(
|
||||||
|
screenWidthPx,
|
||||||
|
tween(150)
|
||||||
|
)
|
||||||
|
|
||||||
|
transitionProgress = -1f
|
||||||
|
onDismiss()
|
||||||
|
completed.complete(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PredictiveBackHandler { progressFlow ->
|
||||||
|
try {
|
||||||
|
progressFlow.collect { backEvent ->
|
||||||
|
val progress = backEvent.progress
|
||||||
|
|
||||||
|
transitionProgress = -progress
|
||||||
|
|
||||||
|
animatedOffset.snapTo(
|
||||||
|
progress * screenWidthPx
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
animatedOffset.animateTo(
|
||||||
|
screenWidthPx,
|
||||||
|
tween(150)
|
||||||
|
)
|
||||||
|
|
||||||
|
transitionProgress = -1f
|
||||||
|
onDismiss()
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
animatedOffset.animateTo(
|
||||||
|
0f,
|
||||||
|
tween(150)
|
||||||
|
)
|
||||||
|
|
||||||
|
transitionProgress = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val draggableState = rememberDraggableState { delta ->
|
||||||
|
scope.launch {
|
||||||
|
val offset =
|
||||||
|
(animatedOffset.value + delta)
|
||||||
|
.coerceAtLeast(0f)
|
||||||
|
|
||||||
|
animatedOffset.snapTo(offset)
|
||||||
|
|
||||||
|
transitionProgress =
|
||||||
|
-(offset / screenWidthPx)
|
||||||
|
.coerceIn(0f, 1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalTransitionProgress provides transitionProgress
|
||||||
|
) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalIsCurrentEntry provides false
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
translationX =
|
||||||
|
if (direction == Direction.Forward) {
|
||||||
|
0f
|
||||||
|
} else {
|
||||||
|
(-screenWidthPx / 3f) +
|
||||||
|
(animatedOffset.value / 3f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
previousEntry?.Content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalIsCurrentEntry provides true
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
translationX = animatedOffset.value
|
||||||
|
}
|
||||||
|
.draggable(
|
||||||
|
enabled = swipeEnabled && previousEntry != null,
|
||||||
|
state = draggableState,
|
||||||
|
orientation = Orientation.Horizontal,
|
||||||
|
onDragStopped = { velocity ->
|
||||||
|
val currentOffset = animatedOffset.value
|
||||||
|
val currentProgress =
|
||||||
|
(currentOffset / screenWidthPx).coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
val shouldDismiss =
|
||||||
|
currentOffset > screenWidthPx * 0.35f ||
|
||||||
|
velocity > 1000f
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
if (shouldDismiss) {
|
||||||
|
animate(
|
||||||
|
initialValue = currentProgress,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = tween(150)
|
||||||
|
) { value, _ ->
|
||||||
|
transitionProgress = -value
|
||||||
|
scope.launch {
|
||||||
|
animatedOffset.snapTo(value * screenWidthPx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionProgress = -1f
|
||||||
|
onDismiss()
|
||||||
|
} else {
|
||||||
|
animate(
|
||||||
|
initialValue = currentProgress,
|
||||||
|
targetValue = 0f,
|
||||||
|
animationSpec = tween(150)
|
||||||
|
) { value, _ ->
|
||||||
|
transitionProgress = -value
|
||||||
|
scope.launch {
|
||||||
|
animatedOffset.snapTo(value * screenWidthPx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionProgress = 0f
|
||||||
|
animatedOffset.snapTo(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
currentEntry.Content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package me.kavishdevar.librepods.presentation.navigation
|
||||||
|
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
|
||||||
|
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope> {
|
||||||
|
error("LocalSharedTransitionScope not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalTransitionProgress = compositionLocalOf { 0f }
|
||||||
|
val LocalIsCurrentEntry = compositionLocalOf { false }
|
||||||
+50
-27
@@ -29,14 +29,10 @@ import androidx.compose.foundation.layout.Box
|
|||||||
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.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.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.input.TextFieldState
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
@@ -76,10 +72,10 @@ import me.kavishdevar.librepods.presentation.components.StyledInputField
|
|||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.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.NightTheme
|
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
||||||
import me.kavishdevar.librepods.utils.XposedState
|
import me.kavishdevar.librepods.utils.XposedState
|
||||||
@@ -89,8 +85,8 @@ import java.util.concurrent.TimeUnit
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppSettingsScreen(
|
fun AppSettingsScreen(
|
||||||
viewModel: AppSettingsViewModel = viewModel(),
|
viewModel: AppSettingsViewModel = viewModel(),
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
navigateToPurchase: () -> Unit,
|
navigateToPurchase: () -> Unit,
|
||||||
navigateToTroubleshooting: () -> Unit,
|
|
||||||
navigateToOpenSourceLicenses: () -> Unit,
|
navigateToOpenSourceLicenses: () -> Unit,
|
||||||
navigateToReleaseNotesScreen: () -> Unit,
|
navigateToReleaseNotesScreen: () -> Unit,
|
||||||
navigateToBleSettingsScreen: () -> Unit
|
navigateToBleSettingsScreen: () -> Unit
|
||||||
@@ -107,10 +103,10 @@ fun AppSettingsScreen(
|
|||||||
val subjectFocusRequester = remember { FocusRequester() }
|
val subjectFocusRequester = remember { FocusRequester() }
|
||||||
val descriptionFocusRequester = remember { FocusRequester() }
|
val descriptionFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
StyledScaffold(
|
||||||
val topPadding = if (m3eEnabled) 16.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
title = stringResource(R.string.settings),
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -137,6 +133,7 @@ fun AppSettingsScreen(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.state.timeUntilFOSSPremiumExpiry > 0L) {
|
if (state.state.timeUntilFOSSPremiumExpiry > 0L) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -208,10 +205,41 @@ fun AppSettingsScreen(
|
|||||||
enabled = state.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
StyledList(title = stringResource(R.string.interaction)) {
|
||||||
|
StyledToggle(
|
||||||
|
label = stringResource(R.string.swipe_anywhere_to_go_back),
|
||||||
|
checked = state.settings.swipeAnywhereForBack,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
viewModel.updateSettings {
|
||||||
|
it.copy(swipeAnywhereForBack = checked)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
StyledToggle(
|
||||||
|
label = stringResource(R.string.use_highest_refresh_rate),
|
||||||
|
checked = state.settings.useHighestRefreshRate,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
viewModel.updateSettings {
|
||||||
|
it.copy(useHighestRefreshRate = checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
StyledList(
|
||||||
|
title = stringResource(R.string.advanced_options),
|
||||||
|
description = stringResource(R.string.do_not_change)
|
||||||
|
) {
|
||||||
if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) {
|
if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) {
|
||||||
val restartBluetoothText = stringResource(R.string.found_offset_restart_bluetooth)
|
val restartBluetoothText =
|
||||||
|
stringResource(R.string.found_offset_restart_bluetooth)
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.act_as_an_apple_device) + " (${
|
label = stringResource(R.string.act_as_an_apple_device) + " (${
|
||||||
stringResource(
|
stringResource(
|
||||||
@@ -227,15 +255,9 @@ fun AppSettingsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledListItem(
|
|
||||||
contentText = stringResource(R.string.ble_settings),
|
|
||||||
supportingText = stringResource(R.string.do_not_change),
|
|
||||||
orientation = StyledListItemOrientation.Vertical,
|
|
||||||
onClick = navigateToBleSettingsScreen
|
|
||||||
)
|
|
||||||
|
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.enable_debug_mode),
|
label = stringResource(R.string.enable_debug_mode),
|
||||||
|
description = stringResource(R.string.debug_mode_description),
|
||||||
checked = state.settings.debugMode,
|
checked = state.settings.debugMode,
|
||||||
onCheckedChange = { checked ->
|
onCheckedChange = { checked ->
|
||||||
viewModel.updateSettings {
|
viewModel.updateSettings {
|
||||||
@@ -244,17 +266,14 @@ fun AppSettingsScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!BuildConfig.PLAY_BUILD) {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
StyledList {
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.troubleshooting),
|
contentText = stringResource(R.string.ble_settings),
|
||||||
onClick = navigateToTroubleshooting,
|
orientation = StyledListItemOrientation.Vertical,
|
||||||
|
onClick = navigateToBleSettingsScreen
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
StyledList(title = stringResource(R.string.contact)) {
|
StyledList(title = stringResource(R.string.contact)) {
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
@@ -325,9 +344,12 @@ fun AppSettingsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
|
||||||
DeviceInfoCard()
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
DeviceInfoCard()
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
AppInfoCard(navigateToReleaseNotesScreen)
|
AppInfoCard(navigateToReleaseNotesScreen)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -404,6 +426,7 @@ fun AppSettingsScreen(
|
|||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StyledBottomSheet(
|
StyledBottomSheet(
|
||||||
visible = contactBottomSheet.value,
|
visible = contactBottomSheet.value,
|
||||||
|
|||||||
+13
-27
@@ -1,16 +1,9 @@
|
|||||||
package me.kavishdevar.librepods.presentation.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
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.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
|
||||||
@@ -24,38 +17,27 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
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.unit.Dp
|
|
||||||
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.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
|
||||||
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
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BLESettingsScreenRoute(
|
fun BLESettingsScreenRoute(
|
||||||
viewModel: AppSettingsViewModel
|
viewModel: AppSettingsViewModel,
|
||||||
|
navigateBack: (() -> Unit)?
|
||||||
) {
|
) {
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val settings = uiState.settings
|
val settings = uiState.settings
|
||||||
|
|
||||||
Box (
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
BLESettingsScreen(
|
BLESettingsScreen(
|
||||||
topPadding = topPadding,
|
navigateBack = navigateBack,
|
||||||
bottomPadding = bottomPadding,
|
|
||||||
scanMode = settings.bleScanMode,
|
scanMode = settings.bleScanMode,
|
||||||
onScanModeChanged = { scanMode ->
|
onScanModeChanged = { scanMode ->
|
||||||
viewModel.updateSettings {
|
viewModel.updateSettings {
|
||||||
@@ -69,13 +51,11 @@ fun BLESettingsScreenRoute(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BLESettingsScreen(
|
fun BLESettingsScreen(
|
||||||
topPadding: Dp = 16.dp,
|
navigateBack: (() -> Unit)? = null,
|
||||||
bottomPadding: Dp = 16.dp,
|
|
||||||
scanMode: Int,
|
scanMode: Int,
|
||||||
onScanModeChanged: (Int) -> Unit,
|
onScanModeChanged: (Int) -> Unit,
|
||||||
reportDelay: Long,
|
reportDelay: Long,
|
||||||
@@ -83,6 +63,10 @@ fun BLESettingsScreen(
|
|||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.ble_settings),
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@@ -131,14 +115,16 @@ fun BLESettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledList(title = stringResource(R.string.ble_report_delay)) {
|
|
||||||
StyledSlider(
|
StyledSlider(
|
||||||
|
label = stringResource(R.string.ble_report_delay),
|
||||||
value = sliderValue.floatValue,
|
value = sliderValue.floatValue,
|
||||||
onValueChange = { sliderValue.floatValue = it },
|
onValueChange = { sliderValue.floatValue = it },
|
||||||
valueRange = 0f..1000f,
|
valueRange = 0f..1000f,
|
||||||
description = sliderValue.floatValue.toString() + "ms",
|
description = sliderValue.floatValue.toString() + "ms",
|
||||||
|
independent = true // i thought I got rid of all this lol
|
||||||
)
|
)
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.padding(bottom = bottomPadding))
|
Spacer(modifier = Modifier.padding(bottom = bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-32
@@ -9,26 +9,25 @@ import androidx.compose.animation.core.Spring
|
|||||||
import androidx.compose.animation.core.infiniteRepeatable
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
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.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
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.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.MaterialShapes
|
import androidx.compose.material3.MaterialShapes
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.contentColorFor
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
import androidx.compose.material3.toPath
|
import androidx.compose.material3.toPath
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -48,7 +47,6 @@ import androidx.compose.ui.graphics.Path
|
|||||||
import androidx.compose.ui.graphics.drawscope.rotate
|
import androidx.compose.ui.graphics.drawscope.rotate
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
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 androidx.compose.ui.unit.dp
|
||||||
import androidx.graphics.shapes.Morph
|
import androidx.graphics.shapes.Morph
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -69,9 +67,11 @@ 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.NoiseControlSettings
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||||
|
import me.kavishdevar.librepods.presentation.components.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
|
||||||
@@ -79,38 +79,48 @@ import me.kavishdevar.librepods.presentation.utils.createAirPodsBatteryRichText
|
|||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DeviceListRoute(
|
|
||||||
devices: Map<MacAddress, Device<*, *, *>>,
|
|
||||||
navigateToDevice: (MacAddress) -> Unit,
|
|
||||||
) {
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
Box (
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
DeviceListScreen(
|
|
||||||
devices = devices,
|
|
||||||
navigateToDevice = navigateToDevice,
|
|
||||||
topPadding = topPadding,
|
|
||||||
bottomPadding = bottomPadding
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceListScreen(
|
fun DeviceListScreen(
|
||||||
devices: Map<MacAddress, Device<*, *, *>>,
|
devices: Map<MacAddress, Device<*, *, *>>,
|
||||||
|
navigateToAppSettings: () -> Unit,
|
||||||
navigateToDevice: (MacAddress) -> Unit,
|
navigateToDevice: (MacAddress) -> Unit,
|
||||||
topPadding: Dp = 16.dp,
|
|
||||||
bottomPadding: Dp = 16.dp
|
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.app_name),
|
||||||
|
navigateBack = null,
|
||||||
|
actionButtons = listOf(
|
||||||
|
{ scaffoldBackdrop ->
|
||||||
|
if (LocalDesignSystem.current == DesignSystem.Material) {
|
||||||
|
FilledTonalIconButton(
|
||||||
|
onClick = navigateToAppSettings,
|
||||||
|
modifier = Modifier
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.size(IconButtonDefaults.mediumContainerSize(IconButtonDefaults.IconButtonWidthOption.Uniform)),
|
||||||
|
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Settings,
|
||||||
|
contentDescription = "settings",
|
||||||
|
modifier = Modifier.size(IconButtonDefaults.mediumIconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StyledIconButton(
|
||||||
|
onClick = navigateToAppSettings,
|
||||||
|
backdrop = scaffoldBackdrop
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = LocalIcons.current.Settings,
|
||||||
|
contentDescription = "Settings",
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@@ -463,4 +473,5 @@ fun DeviceListScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.padding(top = bottomPadding))
|
Spacer(modifier = Modifier.padding(top = bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
package me.kavishdevar.librepods.presentation.screens
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material3.CircularWavyProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LoadingScreen() {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularWavyProgressIndicator(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(120.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun LoadingScreenPreview() {
|
|
||||||
LibrePodsTheme {
|
|
||||||
LoadingScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-5
@@ -43,6 +43,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
|
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
||||||
@@ -57,15 +58,18 @@ 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.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OpenSourceLicensesScreen() {
|
fun OpenSourceLicensesScreen(
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
navigateBack: (() -> Unit)?
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
) {
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.open_source_licenses),
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -184,4 +188,5 @@ fun OpenSourceLicensesScreen() {
|
|||||||
|
|
||||||
Spacer(Modifier.height(bottomPadding))
|
Spacer(Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-11
@@ -36,7 +36,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
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.snapshots.SnapshotStateList
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -45,12 +44,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.StyledListItemOrientation
|
|
||||||
import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle
|
import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.navigation.Screen
|
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
||||||
@@ -59,7 +58,7 @@ import me.kavishdevar.librepods.utils.XposedState
|
|||||||
@Composable
|
@Composable
|
||||||
fun PurchaseScreen(
|
fun PurchaseScreen(
|
||||||
viewModel: PurchaseViewModel = viewModel(),
|
viewModel: PurchaseViewModel = viewModel(),
|
||||||
backStack: SnapshotStateList<Screen>
|
navigateBack: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
@@ -68,10 +67,10 @@ fun PurchaseScreen(
|
|||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
StyledScaffold(
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
title = stringResource(R.string.unlock_advanced_features),
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layerBackdrop(backdrop)
|
.layerBackdrop(backdrop)
|
||||||
@@ -83,9 +82,7 @@ fun PurchaseScreen(
|
|||||||
|
|
||||||
LaunchedEffect(state.isPremium) {
|
LaunchedEffect(state.isPremium) {
|
||||||
if (state.isPremium) {
|
if (state.isPremium) {
|
||||||
if (backStack.size > 1) {
|
navigateBack?.invoke()
|
||||||
backStack.removeAt(backStack.lastIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!state.isPremium) {
|
if (!state.isPremium) {
|
||||||
@@ -214,4 +211,5 @@ fun PurchaseScreen(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-922
@@ -1,922 +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
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
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.clickable
|
|
||||||
import androidx.compose.foundation.horizontalScroll
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
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.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.filled.Share
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.Font
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import me.kavishdevar.librepods.R
|
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
|
||||||
import me.kavishdevar.librepods.utils.LogCollector
|
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CustomIconButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.padding(8.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
|
||||||
@Composable
|
|
||||||
fun TroubleshootingScreen() {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val logCollector = remember { LogCollector(context) }
|
|
||||||
val savedLogs = remember { mutableStateListOf<File>() }
|
|
||||||
|
|
||||||
var isCollectingLogs by remember { mutableStateOf(false) }
|
|
||||||
var showTroubleshootingSteps by remember { mutableStateOf(false) }
|
|
||||||
var currentStep by remember { mutableIntStateOf(0) }
|
|
||||||
var logContent by remember { mutableStateOf("") }
|
|
||||||
var selectedLogFile by remember { mutableStateOf<File?>(null) }
|
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
|
||||||
var showDeleteAllDialog by remember { mutableStateOf(false) }
|
|
||||||
var isLoadingLogContent by remember { mutableStateOf(false) }
|
|
||||||
var logContentLoaded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(isCollectingLogs) {
|
|
||||||
while (isCollectingLogs) {
|
|
||||||
delay(250)
|
|
||||||
delay(250)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
val textColor = if (isSystemInDarkTheme()) Color.White else Color.Black
|
|
||||||
val accentColor = if (isSystemInDarkTheme()) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
|
||||||
val buttonBgColor = if (isSystemInDarkTheme()) Color(0xFF333333) else Color(0xFFDDDDDD)
|
|
||||||
|
|
||||||
var instructionText by remember { mutableStateOf("") }
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val logsDir = File(context.filesDir, "logs")
|
|
||||||
if (logsDir.exists()) {
|
|
||||||
savedLogs.clear()
|
|
||||||
savedLogs.addAll(logsDir.listFiles()?.filter { it.name.endsWith(".txt") }
|
|
||||||
?.sortedByDescending { it.lastModified() } ?: emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val saveLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.CreateDocument("text/plain")
|
|
||||||
) { uri ->
|
|
||||||
if (uri != null) {
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
|
||||||
outputStream.write(logContent.toByteArray())
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Toast.makeText(context, "Log saved successfully", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Failed to save log: ${e.localizedMessage}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(currentStep) {
|
|
||||||
instructionText = when (currentStep) {
|
|
||||||
0 -> "First, let's ensure Xposed module is properly configured. Tap the button below to check Xposed scope settings."
|
|
||||||
1 -> "Please put your AirPods in the case and close it, so they disconnect completely."
|
|
||||||
2 -> "Preparing to collect logs... Please wait."
|
|
||||||
3 -> "Now, open the AirPods case and connect your AirPods. Logs are being collected. Connection will be detected automatically, or you can manually stop logging when you're done."
|
|
||||||
4 -> "Log collection complete! You can now save or share the logs."
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openLogBottomSheet(file: File) {
|
|
||||||
selectedLogFile = file
|
|
||||||
logContent = ""
|
|
||||||
isLoadingLogContent = false
|
|
||||||
logContentLoaded = false
|
|
||||||
showBottomSheet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
val topPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.layerBackdrop(backdrop)
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.height(topPadding))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.saved_logs),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = textColor.copy(alpha = 0.6f),
|
|
||||||
fontFamily = FontFamily(Font(R.font.inter))
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(16.dp, bottom = 4.dp, top = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
|
||||||
|
|
||||||
if (savedLogs.isEmpty()) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
backgroundColor,
|
|
||||||
RoundedCornerShape(28.dp)
|
|
||||||
)
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.no_logs_found),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
backgroundColor,
|
|
||||||
RoundedCornerShape(28.dp)
|
|
||||||
)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Total Logs: ${savedLogs.size}",
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
|
|
||||||
if (savedLogs.size > 1) {
|
|
||||||
TextButton(
|
|
||||||
onClick = { showDeleteAllDialog = true },
|
|
||||||
colors = ButtonDefaults.textButtonColors(
|
|
||||||
contentColor = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text("Delete All")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
savedLogs.forEach { logFile ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.clickable {
|
|
||||||
openLogBottomSheet(logFile)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = logFile.name,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.US)
|
|
||||||
.format(Date(logFile.lastModified())),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = textColor.copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomIconButton(
|
|
||||||
onClick = {
|
|
||||||
selectedLogFile = logFile
|
|
||||||
showDeleteDialog = true
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Delete,
|
|
||||||
contentDescription = "Delete",
|
|
||||||
tint = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = !showTroubleshootingSteps,
|
|
||||||
enter = fadeIn(animationSpec = tween(300)),
|
|
||||||
exit = fadeOut(animationSpec = tween(300))
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = { showTroubleshootingSteps = true },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = buttonBgColor,
|
|
||||||
contentColor = textColor
|
|
||||||
),
|
|
||||||
enabled = !isCollectingLogs
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.collect_logs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = showTroubleshootingSteps,
|
|
||||||
enter = fadeIn(animationSpec = tween(300)) +
|
|
||||||
slideInVertically(animationSpec = tween(300)) { it / 2 },
|
|
||||||
exit = fadeOut(animationSpec = tween(300)) +
|
|
||||||
slideOutVertically(animationSpec = tween(300)) { it / 2 }
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.troubleshooting_steps),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = textColor.copy(alpha = 0.6f),
|
|
||||||
fontFamily = FontFamily(Font(R.font.inter))
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
backgroundColor,
|
|
||||||
RoundedCornerShape(28.dp)
|
|
||||||
)
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
val textAlpha = animateFloatAsState(
|
|
||||||
targetValue = 1f,
|
|
||||||
animationSpec = tween(durationMillis = 300),
|
|
||||||
label = "textAlpha"
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = instructionText,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor.copy(alpha = textAlpha.value),
|
|
||||||
lineHeight = 22.sp
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
when (currentStep) {
|
|
||||||
0 -> {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
logCollector.openXposedSettings(context)
|
|
||||||
delay(2000)
|
|
||||||
currentStep = 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = buttonBgColor,
|
|
||||||
contentColor = textColor
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text("Open Xposed Settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
currentStep = 2
|
|
||||||
isCollectingLogs = true
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
try {
|
|
||||||
logCollector.clearLogs()
|
|
||||||
|
|
||||||
logCollector.addLogMarker(LogCollector.LogMarkerType.START)
|
|
||||||
|
|
||||||
logCollector.killBluetoothService()
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
delay(500)
|
|
||||||
currentStep = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
val timestamp = SimpleDateFormat(
|
|
||||||
"yyyyMMdd_HHmmss",
|
|
||||||
Locale.US
|
|
||||||
).format(Date())
|
|
||||||
|
|
||||||
logContent =
|
|
||||||
logCollector.startLogCollection(
|
|
||||||
listener = { /* Removed live log display */ },
|
|
||||||
connectionDetectedCallback = {
|
|
||||||
launch {
|
|
||||||
delay(5000)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (isCollectingLogs) {
|
|
||||||
logCollector.stopLogCollection()
|
|
||||||
currentStep = 4
|
|
||||||
isCollectingLogs =
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val logFile =
|
|
||||||
logCollector.saveLogToInternalStorage(
|
|
||||||
"airpods_log_$timestamp.txt",
|
|
||||||
logContent
|
|
||||||
)
|
|
||||||
logFile?.let {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
savedLogs.add(0, it)
|
|
||||||
selectedLogFile = it
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Log saved: ${it.name}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Error collecting logs: ${e.message}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
isCollectingLogs = false
|
|
||||||
currentStep = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = buttonBgColor,
|
|
||||||
contentColor = textColor
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text("Continue")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
2, 3 -> {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
color = accentColor
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = if (currentStep == 2) "Preparing..." else "Collecting logs...",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
|
|
||||||
if (currentStep == 3) {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
logCollector.addLogMarker(
|
|
||||||
LogCollector.LogMarkerType.CUSTOM,
|
|
||||||
"Manual stop requested by user"
|
|
||||||
)
|
|
||||||
delay(1000)
|
|
||||||
logCollector.stopLogCollection()
|
|
||||||
delay(500)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
currentStep = 4
|
|
||||||
isCollectingLogs = false
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Log collection stopped",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = buttonBgColor,
|
|
||||||
contentColor = textColor
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text("Stop Collection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
4 -> {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
selectedLogFile?.let { file ->
|
|
||||||
val fileUri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${context.packageName}.provider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
val shareIntent =
|
|
||||||
Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = "text/plain"
|
|
||||||
putExtra(
|
|
||||||
Intent.EXTRA_STREAM,
|
|
||||||
fileUri
|
|
||||||
)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
shareIntent,
|
|
||||||
"Share log file"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = buttonBgColor,
|
|
||||||
contentColor = textColor
|
|
||||||
),
|
|
||||||
modifier = Modifier.width(150.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Share,
|
|
||||||
contentDescription = "Share"
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("Share")
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
selectedLogFile?.let { file ->
|
|
||||||
saveLauncher.launch(
|
|
||||||
file.absolutePath
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = buttonBgColor,
|
|
||||||
contentColor = textColor
|
|
||||||
),
|
|
||||||
modifier = Modifier.width(150.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = LocalIcons.current.Save,
|
|
||||||
contentDescription = "Save"
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("Save")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
currentStep = 0
|
|
||||||
showTroubleshootingSteps = false
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = buttonBgColor,
|
|
||||||
contentColor = textColor
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text("Done")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDeleteDialog && selectedLogFile != null) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showDeleteDialog = false },
|
|
||||||
title = { Text("Delete Log File") },
|
|
||||||
text = {
|
|
||||||
Text("Are you sure you want to delete this log file? This action cannot be undone.")
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
selectedLogFile?.let { file ->
|
|
||||||
if (file.delete()) {
|
|
||||||
savedLogs.remove(file)
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Log file deleted",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
)
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Failed to delete log file",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showDeleteDialog = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text("Delete", color = MaterialTheme.colorScheme.error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showDeleteDialog = false }) {
|
|
||||||
Text("Cancel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDeleteAllDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showDeleteAllDialog = false },
|
|
||||||
title = { Text("Delete All Logs") },
|
|
||||||
text = {
|
|
||||||
Text("Are you sure you want to delete all log files? This action cannot be undone and will remove ${savedLogs.size} log files.")
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
var deletedCount = 0
|
|
||||||
savedLogs.forEach { file ->
|
|
||||||
if (file.delete()) {
|
|
||||||
deletedCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (deletedCount > 0) {
|
|
||||||
savedLogs.clear()
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Deleted $deletedCount log files",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Failed to delete log files",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showDeleteAllDialog = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text("Delete All", color = MaterialTheme.colorScheme.error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showDeleteAllDialog = false }) {
|
|
||||||
Text("Cancel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showBottomSheet) {
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = { showBottomSheet = false },
|
|
||||||
sheetState = sheetState,
|
|
||||||
containerColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7),
|
|
||||||
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
|
|
||||||
tonalElevation = 8.dp
|
|
||||||
) {
|
|
||||||
LaunchedEffect(selectedLogFile) {
|
|
||||||
if (!logContentLoaded) {
|
|
||||||
delay(300)
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
isLoadingLogContent = true
|
|
||||||
logContent = try {
|
|
||||||
selectedLogFile?.readText() ?: ""
|
|
||||||
} catch (e: Exception) {
|
|
||||||
"Error loading log content: ${e.message}"
|
|
||||||
}
|
|
||||||
isLoadingLogContent = false
|
|
||||||
logContentLoaded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
|
||||||
.padding(bottom = 32.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 12.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = selectedLogFile?.name ?: "Log Content",
|
|
||||||
style = TextStyle(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.inter))
|
|
||||||
),
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.US)
|
|
||||||
.format(Date(selectedLogFile?.lastModified() ?: 0)),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = textColor.copy(alpha = 0.7f),
|
|
||||||
fontFamily = FontFamily(Font(R.font.inter))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoadingLogContent) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(300.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(color = accentColor)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(300.dp)
|
|
||||||
.background(
|
|
||||||
color = Color.Black,
|
|
||||||
shape = RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
val horizontalScrollState = rememberScrollState()
|
|
||||||
val verticalScrollState = rememberScrollState()
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(8.dp)
|
|
||||||
.horizontalScroll(horizontalScrollState)
|
|
||||||
.verticalScroll(verticalScrollState)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = logContent,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = Color.LightGray,
|
|
||||||
lineHeight = 20.sp,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
softWrap = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
selectedLogFile?.let { file ->
|
|
||||||
val fileUri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${context.packageName}.provider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = "text/plain"
|
|
||||||
putExtra(Intent.EXTRA_STREAM, fileUri)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
shareIntent,
|
|
||||||
"Share log file"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = buttonBgColor,
|
|
||||||
contentColor = textColor
|
|
||||||
),
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Share,
|
|
||||||
contentDescription = "Share"
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("Share")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
selectedLogFile?.let { file ->
|
|
||||||
saveLauncher.launch(file.absolutePath)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = buttonBgColor,
|
|
||||||
contentColor = textColor
|
|
||||||
),
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = LocalIcons.current.Save,
|
|
||||||
contentDescription = "Save"
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("Save")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
logCollector.stopLogCollection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+12
-7
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.screens.apple
|
package me.kavishdevar.librepods.presentation.screens.apple
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
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
|
||||||
@@ -56,6 +55,7 @@ import me.kavishdevar.librepods.devices.BaseCapability
|
|||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
@@ -67,7 +67,12 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
//private var phoneMediaDebounceJob: Job? = null
|
//private var phoneMediaDebounceJob: Job? = null
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AccessibilitySettingsScreen(viewModel: AppleViewModel, navigateToPurchase: () -> Unit, navigateToTransparencyCustomization: () -> Unit) {
|
fun AccessibilitySettingsScreen(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
|
navigateToPurchase: () -> Unit,
|
||||||
|
navigateToTransparencyCustomization: () -> Unit
|
||||||
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
@@ -82,14 +87,13 @@ fun AccessibilitySettingsScreen(viewModel: AppleViewModel, navigateToPurchase: (
|
|||||||
)?.toInt() == 1
|
)?.toInt() == 1
|
||||||
|
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
StyledScaffold(
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
title = stringResource(R.string.accessibility),
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
@@ -572,4 +576,5 @@ fun AccessibilitySettingsScreen(viewModel: AppleViewModel, navigateToPurchase: (
|
|||||||
// }
|
// }
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.screens.apple
|
package me.kavishdevar.librepods.presentation.screens.apple
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
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
|
||||||
@@ -48,6 +47,7 @@ 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.presentation.components.StyledButton
|
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.components.StyledSlider
|
||||||
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
|
||||||
@@ -56,21 +56,24 @@ import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AdaptiveStrengthScreen(viewModel: AppleViewModel, navigateToPurchase: () -> Unit) {
|
fun AdaptiveStrengthScreen(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
|
navigateToPurchase: () -> Unit
|
||||||
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
StyledScaffold(
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
title = stringResource(R.string.customize_adaptive_audio),
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.layerBackdrop(backdrop)
|
.layerBackdrop(backdrop)
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
@@ -107,7 +110,6 @@ fun AdaptiveStrengthScreen(viewModel: AppleViewModel, navigateToPurchase: () ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.customize_adaptive_audio),
|
|
||||||
value = sliderValue.floatValue,
|
value = sliderValue.floatValue,
|
||||||
onValueChange = { sliderValue.floatValue = it },
|
onValueChange = { sliderValue.floatValue = it },
|
||||||
valueRange = 0f..100f,
|
valueRange = 0f..100f,
|
||||||
@@ -120,4 +122,5 @@ fun AdaptiveStrengthScreen(viewModel: AppleViewModel, navigateToPurchase: () ->
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-43
@@ -16,25 +16,17 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
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.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
|
||||||
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
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.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
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
|
||||||
@@ -44,10 +36,8 @@ 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 androidx.compose.ui.unit.dp
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|
||||||
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
|
||||||
@@ -59,22 +49,22 @@ import me.kavishdevar.librepods.presentation.components.BatteryView
|
|||||||
import me.kavishdevar.librepods.presentation.components.CallControlSettings
|
import me.kavishdevar.librepods.presentation.components.CallControlSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.ConnectionSettings
|
import me.kavishdevar.librepods.presentation.components.ConnectionSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.HearingHealthSettings
|
import me.kavishdevar.librepods.presentation.components.HearingHealthSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
|
||||||
import me.kavishdevar.librepods.presentation.components.NoiseControlSettings
|
import me.kavishdevar.librepods.presentation.components.NoiseControlSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.PressAndHoldSettings
|
import me.kavishdevar.librepods.presentation.components.PressAndHoldSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
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.theme.LocalDesignSystem
|
|
||||||
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.io.encoding.ExperimentalEncodingApi
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsRoute(
|
fun AppleSettingsRoute(
|
||||||
viewModel: AppleViewModel,
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
navigateToRename: () -> Unit,
|
navigateToRename: () -> Unit,
|
||||||
navigateToHearingProtection: () -> Unit,
|
navigateToHearingProtection: () -> Unit,
|
||||||
navigateToHearingAid: () -> Unit,
|
navigateToHearingAid: () -> Unit,
|
||||||
@@ -89,27 +79,14 @@ fun AirPodsSettingsRoute(
|
|||||||
navigateToCallControlScreen: (action: String) -> Unit,
|
navigateToCallControlScreen: (action: String) -> Unit,
|
||||||
navigateToMicrophoneSettings: () -> Unit,
|
navigateToMicrophoneSettings: () -> Unit,
|
||||||
navigateToRecordingScreen: () -> Unit,
|
navigateToRecordingScreen: () -> Unit,
|
||||||
navigateToDebugScreen: () -> Unit
|
navigateToDebugScreen: () -> Unit,
|
||||||
|
navigateToHeartRateScreen: () -> Unit
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
AppleSettingsScreen(
|
||||||
val topPadding = WindowInsets.statusBars.asPaddingValues()
|
|
||||||
.calculateTopPadding() + if (m3eEnabled) 0.dp else 84.dp
|
|
||||||
val bottomPadding =
|
|
||||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
AirPodsSettingsScreen(
|
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
|
|
||||||
topPadding = topPadding,
|
|
||||||
bottomPadding = bottomPadding,
|
|
||||||
|
|
||||||
setControlCommandInt = { id, value -> viewModel.setControlCommand(id, value) },
|
setControlCommandInt = { id, value -> viewModel.setControlCommand(id, value) },
|
||||||
setControlCommandBoolean = { id, value -> viewModel.setControlCommand(id, value) },
|
setControlCommandBoolean = { id, value -> viewModel.setControlCommand(id, value) },
|
||||||
// setControlCommandByte = { id, value -> viewModel.setControlCommand(id, value) },
|
// setControlCommandByte = { id, value -> viewModel.setControlCommand(id, value) },
|
||||||
@@ -121,6 +98,7 @@ fun AirPodsSettingsRoute(
|
|||||||
// onAutomaticConnectionChanged = viewModel::setAutomaticConnectionEnabled,
|
// onAutomaticConnectionChanged = viewModel::setAutomaticConnectionEnabled,
|
||||||
disconnect = viewModel::disconnect,
|
disconnect = viewModel::disconnect,
|
||||||
|
|
||||||
|
navigateBack = navigateBack,
|
||||||
navigateToRename = navigateToRename,
|
navigateToRename = navigateToRename,
|
||||||
navigateToHearingProtection = navigateToHearingProtection,
|
navigateToHearingProtection = navigateToHearingProtection,
|
||||||
navigateToHearingAid = navigateToHearingAid,
|
navigateToHearingAid = navigateToHearingAid,
|
||||||
@@ -135,20 +113,16 @@ fun AirPodsSettingsRoute(
|
|||||||
navigateToCallControlScreen = navigateToCallControlScreen,
|
navigateToCallControlScreen = navigateToCallControlScreen,
|
||||||
navigateToMicrophoneSettings = navigateToMicrophoneSettings,
|
navigateToMicrophoneSettings = navigateToMicrophoneSettings,
|
||||||
navigateToRecordingScreen = navigateToRecordingScreen,
|
navigateToRecordingScreen = navigateToRecordingScreen,
|
||||||
|
navigateToHeartRateScreen = navigateToHeartRateScreen,
|
||||||
navigateToDebugScreen = navigateToDebugScreen
|
navigateToDebugScreen = navigateToDebugScreen
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
|
||||||
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreen(
|
fun AppleSettingsScreen(
|
||||||
uiState: AppleUiState,
|
uiState: AppleUiState,
|
||||||
|
|
||||||
topPadding: Dp = 16.dp,
|
|
||||||
bottomPadding: Dp = 16.dp,
|
|
||||||
|
|
||||||
setControlCommandInt: (ControlCommandIdentifier, Int) -> Unit,
|
setControlCommandInt: (ControlCommandIdentifier, Int) -> Unit,
|
||||||
setControlCommandBoolean: (ControlCommandIdentifier, Boolean) -> Unit,
|
setControlCommandBoolean: (ControlCommandIdentifier, Boolean) -> Unit,
|
||||||
// setControlCommandByte: (ControlCommandIdentifier, Byte) -> Unit,
|
// setControlCommandByte: (ControlCommandIdentifier, Byte) -> Unit,
|
||||||
@@ -161,6 +135,7 @@ fun AirPodsSettingsScreen(
|
|||||||
|
|
||||||
disconnect: () -> Unit,
|
disconnect: () -> Unit,
|
||||||
|
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
navigateToRename: () -> Unit,
|
navigateToRename: () -> Unit,
|
||||||
navigateToHearingProtection: () -> Unit,
|
navigateToHearingProtection: () -> Unit,
|
||||||
navigateToHearingAid: () -> Unit,
|
navigateToHearingAid: () -> Unit,
|
||||||
@@ -175,6 +150,7 @@ fun AirPodsSettingsScreen(
|
|||||||
navigateToCallControlScreen: (action: String) -> Unit,
|
navigateToCallControlScreen: (action: String) -> Unit,
|
||||||
navigateToMicrophoneSettings: () -> Unit,
|
navigateToMicrophoneSettings: () -> Unit,
|
||||||
navigateToRecordingScreen: () -> Unit,
|
navigateToRecordingScreen: () -> Unit,
|
||||||
|
navigateToHeartRateScreen: () -> Unit,
|
||||||
navigateToDebugScreen: () -> Unit
|
navigateToDebugScreen: () -> Unit
|
||||||
) {
|
) {
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
@@ -185,10 +161,12 @@ fun AirPodsSettingsScreen(
|
|||||||
|
|
||||||
val baseCapabilities = spec.baseCapabilities
|
val baseCapabilities = spec.baseCapabilities
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = uiState.metadata.name,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
) {
|
) {
|
||||||
item(key = "top_padding") { Spacer(modifier = Modifier.height(topPadding)) }
|
item(key = "top_padding") { Spacer(modifier = Modifier.height(topPadding)) }
|
||||||
|
|
||||||
@@ -262,6 +240,19 @@ fun AirPodsSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (baseCapabilities.contains(BaseCapability.HRM)) {
|
||||||
|
item(key = "spacer_heart_rate") {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
item(key = "heart_rate") {
|
||||||
|
StyledListItem(
|
||||||
|
contentText = stringResource(R.string.heart_rate),
|
||||||
|
onClick = navigateToHeartRateScreen,
|
||||||
|
supportingText = state.currentHeartRate?.let { "${it.bpm} bpm" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (baseCapabilities.contains(BaseCapability.STEM_CONFIG)) {
|
if (baseCapabilities.contains(BaseCapability.STEM_CONFIG)) {
|
||||||
item(key = "spacer_press_hold") {
|
item(key = "spacer_press_hold") {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -494,20 +485,24 @@ fun AirPodsSettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
|
|
||||||
|
if (uiState.appSettings.debugMode) {
|
||||||
item(key = "debug") {
|
item(key = "debug") {
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = "debug",
|
contentText = "debug",
|
||||||
onClick = navigateToDebugScreen
|
onClick = navigateToDebugScreen
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
item(key = "bottom_padding") { Spacer(modifier = Modifier.height(bottomPadding)) }
|
item(key = "bottom_padding") { Spacer(modifier = Modifier.height(bottomPadding)) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(name = "Apple")
|
@Preview(name = "Apple")
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreenPreviewApple() {
|
fun AppleSettingsScreenPreviewApple() {
|
||||||
LibrePodsTheme(
|
LibrePodsTheme(
|
||||||
designSystem = DesignSystem.Apple
|
designSystem = DesignSystem.Apple
|
||||||
) {
|
) {
|
||||||
@@ -515,7 +510,7 @@ fun AirPodsSettingsScreenPreviewApple() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
) {
|
) {
|
||||||
AirPodsSettingsScreen(
|
AppleSettingsScreen(
|
||||||
uiState = AppleUiState(),
|
uiState = AppleUiState(),
|
||||||
|
|
||||||
setControlCommandInt = { _, _ -> },
|
setControlCommandInt = { _, _ -> },
|
||||||
@@ -524,6 +519,7 @@ fun AirPodsSettingsScreenPreviewApple() {
|
|||||||
|
|
||||||
disconnect = {},
|
disconnect = {},
|
||||||
|
|
||||||
|
navigateBack = null,
|
||||||
navigateToRename = {},
|
navigateToRename = {},
|
||||||
navigateToHearingProtection = {},
|
navigateToHearingProtection = {},
|
||||||
navigateToHearingAid = {},
|
navigateToHearingAid = {},
|
||||||
@@ -538,6 +534,7 @@ fun AirPodsSettingsScreenPreviewApple() {
|
|||||||
navigateToCallControlScreen = {},
|
navigateToCallControlScreen = {},
|
||||||
navigateToMicrophoneSettings = {},
|
navigateToMicrophoneSettings = {},
|
||||||
navigateToRecordingScreen = {},
|
navigateToRecordingScreen = {},
|
||||||
|
navigateToHeartRateScreen = {},
|
||||||
navigateToDebugScreen = {}
|
navigateToDebugScreen = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -547,7 +544,7 @@ fun AirPodsSettingsScreenPreviewApple() {
|
|||||||
|
|
||||||
@Preview(name = "Material")
|
@Preview(name = "Material")
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreenPreviewMaterial() {
|
fun AppleSettingsScreenPreviewMaterial() {
|
||||||
LibrePodsTheme(
|
LibrePodsTheme(
|
||||||
designSystem = DesignSystem.Material
|
designSystem = DesignSystem.Material
|
||||||
) {
|
) {
|
||||||
@@ -555,7 +552,7 @@ fun AirPodsSettingsScreenPreviewMaterial() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
) {
|
) {
|
||||||
AirPodsSettingsScreen(
|
AppleSettingsScreen(
|
||||||
uiState = AppleUiState(),
|
uiState = AppleUiState(),
|
||||||
|
|
||||||
setControlCommandInt = { _, _ -> },
|
setControlCommandInt = { _, _ -> },
|
||||||
@@ -564,6 +561,7 @@ fun AirPodsSettingsScreenPreviewMaterial() {
|
|||||||
|
|
||||||
disconnect = {},
|
disconnect = {},
|
||||||
|
|
||||||
|
navigateBack = null,
|
||||||
navigateToRename = {},
|
navigateToRename = {},
|
||||||
navigateToHearingProtection = {},
|
navigateToHearingProtection = {},
|
||||||
navigateToHearingAid = {},
|
navigateToHearingAid = {},
|
||||||
@@ -578,6 +576,7 @@ fun AirPodsSettingsScreenPreviewMaterial() {
|
|||||||
navigateToCallControlScreen = {},
|
navigateToCallControlScreen = {},
|
||||||
navigateToMicrophoneSettings = {},
|
navigateToMicrophoneSettings = {},
|
||||||
navigateToRecordingScreen = {},
|
navigateToRecordingScreen = {},
|
||||||
|
navigateToHeartRateScreen = {},
|
||||||
navigateToDebugScreen = {}
|
navigateToDebugScreen = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-20
@@ -1,19 +1,12 @@
|
|||||||
package me.kavishdevar.librepods.presentation.screens.apple
|
package me.kavishdevar.librepods.presentation.screens.apple
|
||||||
|
|
||||||
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.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.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@@ -28,27 +21,23 @@ 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.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CallControlScreen(viewModel: AppleViewModel, action: String, onCallControlValueChanged: (Boolean) -> Unit) {
|
fun CallControlScreen(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
action: String,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
|
onCallControlValueChanged: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
val bytes =
|
val bytes = state.controlStates[ControlCommandIdentifier.CALL_MANAGEMENT_CONFIG]?.take(2)?.toByteArray() ?: byteArrayOf(0x00, 0x00)
|
||||||
state.controlStates[ControlCommandIdentifier.CALL_MANAGEMENT_CONFIG]?.take(
|
|
||||||
2
|
|
||||||
)?.toByteArray() ?: byteArrayOf(0x00, 0x00)
|
|
||||||
val flipped = try {
|
val flipped = try {
|
||||||
bytes[1] == 0x02.toByte()
|
bytes[1] == 0x02.toByte()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -64,10 +53,13 @@ fun CallControlScreen(viewModel: AppleViewModel, action: String, onCallControlVa
|
|||||||
val pressOnceIsAction by remember { derivedStateOf { singlePressAction == pressOnceText } }
|
val pressOnceIsAction by remember { derivedStateOf { singlePressAction == pressOnceText } }
|
||||||
val flippedValue = action != muteUnmuteText
|
val flippedValue = action != muteUnmuteText
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = action,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@@ -96,4 +88,5 @@ fun CallControlScreen(viewModel: AppleViewModel, action: String, onCallControlVa
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-27
@@ -1,18 +1,12 @@
|
|||||||
package me.kavishdevar.librepods.presentation.screens.apple
|
package me.kavishdevar.librepods.presentation.screens.apple
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
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.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.text.input.TextFieldState
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.foundation.text.input.delete
|
import androidx.compose.foundation.text.input.delete
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -26,61 +20,59 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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 me.kavishdevar.librepods.bluetooth.aacp.packet.AACPPacketType
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.AACPPacketType
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.packet.BatteryInfoPacket
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.BatteryInfoPacket
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.packet.ControlCommandPacket
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.ControlCommandPacket
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.packet.EarDetectionResponsePacket
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.EarDetectionResponsePacket
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.packet.MagicKeyResponsePacket
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.MagicKeyResponsePacket
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.RTBuddyPacket
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.packet.RenamePacket
|
import me.kavishdevar.librepods.bluetooth.aacp.packet.RenamePacket
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.rtbuddy.proto.SensorServiceType
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.MessageOpcode
|
import me.kavishdevar.librepods.bluetooth.aacp.types.MessageOpcode
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.RTBuddyDescriptor
|
||||||
|
import me.kavishdevar.librepods.bluetooth.aacp.types.SensorDataWxBuddyPayload
|
||||||
import me.kavishdevar.librepods.devices.DeviceComponent
|
import me.kavishdevar.librepods.devices.DeviceComponent
|
||||||
import me.kavishdevar.librepods.devices.PacketDestination
|
import me.kavishdevar.librepods.devices.PacketDestination
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.icons.richText
|
import me.kavishdevar.librepods.presentation.icons.richText
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
|
||||||
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 me.kavishdevar.librepods.utils.nonScaledSp
|
import me.kavishdevar.librepods.utils.nonScaledSp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DebugRoute(viewModel: AppleViewModel) {
|
fun DebugRoute(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?
|
||||||
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
Box (
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
DebugScreen(
|
DebugScreen(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
topPadding = topPadding,
|
navigateBack = navigateBack,
|
||||||
bottomPadding = bottomPadding,
|
sendPacket = viewModel::sendRawPacket,
|
||||||
sendPacket = viewModel::sendRawPacket
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DebugScreen(
|
fun DebugScreen(
|
||||||
uiState: AppleUiState,
|
uiState: AppleUiState,
|
||||||
topPadding: Dp = 16.dp,
|
navigateBack: (() -> Unit)?,
|
||||||
bottomPadding: Dp = 16.dp,
|
sendPacket: (ByteArray) -> Boolean,
|
||||||
sendPacket: (ByteArray) -> Boolean = { false }
|
|
||||||
) {
|
) {
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
|
|
||||||
Log.d("DebugScreen", "Screen ${state.aacpPackets.size}")
|
// Log.d("DebugScreen", "Screen ${state.aacpPackets.size}")
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = "debug",
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@@ -203,6 +195,62 @@ fun DebugScreen(
|
|||||||
"Magic Keys (${packet.magicKeys.size})"
|
"Magic Keys (${packet.magicKeys.size})"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is RTBuddyPacket -> {
|
||||||
|
val rtBuddyPayload = packet.rtBuddyPayload
|
||||||
|
when (rtBuddyPayload.descriptor) {
|
||||||
|
RTBuddyDescriptor.SENSOR_DATA_WX -> {
|
||||||
|
val sensorDataWxBuddyPayload =
|
||||||
|
rtBuddyPayload as SensorDataWxBuddyPayload
|
||||||
|
val data = sensorDataWxBuddyPayload.data
|
||||||
|
|
||||||
|
if (data.hasCommand()) {
|
||||||
|
when (data.command.service) {
|
||||||
|
SensorServiceType.ACTIVITY, SensorServiceType.DEVMOTION6 -> {
|
||||||
|
val payload = data.command.payload.toByteArray()
|
||||||
|
|
||||||
|
fun i16(offset: Int): Int =
|
||||||
|
(payload[offset].toInt() and 0xFF) or
|
||||||
|
((payload[offset + 1].toInt() and 0xFF) shl 8)
|
||||||
|
.let { value ->
|
||||||
|
if (value and 0x8000 != 0) value - 0x10000 else value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun vec3(offset: Int): String =
|
||||||
|
"(${i16(offset)}, ${i16(offset + 2)}, ${i16(offset + 4)})"
|
||||||
|
|
||||||
|
"""
|
||||||
|
DEVMOTION
|
||||||
|
v0: ${vec3(20)}
|
||||||
|
v1: ${vec3(26)}
|
||||||
|
v2: ${vec3(32)}
|
||||||
|
v3: ${vec3(38)}
|
||||||
|
v4: ${vec3(44)}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
SensorServiceType.HEARTRATEv2 -> {
|
||||||
|
val payload = data.command.payload.toByteArray()
|
||||||
|
|
||||||
|
if (payload.size >= 11) {
|
||||||
|
val heartRate = payload[1].toInt() and 0xFF
|
||||||
|
|
||||||
|
"Possible HR: $heartRate, payload=${payload.toHexString()}"
|
||||||
|
} else "???"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
"Unhandled sensor command service: ${data.command.service}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else "?????"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
"Unhandled descriptor: ${rtBuddyPayload.descriptor}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> packet.opcode.toString()
|
else -> packet.opcode.toString()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -269,4 +317,5 @@ fun DebugScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.padding(top = bottomPadding))
|
Spacer(modifier = Modifier.padding(top = bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-32
@@ -30,20 +30,15 @@ import androidx.compose.foundation.layout.Box
|
|||||||
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.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
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
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.layout.visible
|
import androidx.compose.foundation.layout.visible
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -72,7 +67,6 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
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.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.lerp
|
import androidx.compose.ui.util.lerp
|
||||||
@@ -86,9 +80,9 @@ import me.kavishdevar.librepods.R
|
|||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
|
import me.kavishdevar.librepods.presentation.components.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.theme.LocalDesignSystem
|
|
||||||
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.math.abs
|
import kotlin.math.abs
|
||||||
@@ -96,32 +90,23 @@ import kotlin.math.roundToInt
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EqualizerRoute(viewModel: AppleViewModel) {
|
fun EqualizerRoute(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?
|
||||||
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
Box (
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
EqualizerScreen(
|
EqualizerScreen(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
topPadding = topPadding,
|
navigateBack = navigateBack,
|
||||||
bottomPadding = bottomPadding,
|
|
||||||
setCustomEqEnabled = viewModel::setCustomEqEnabled,
|
setCustomEqEnabled = viewModel::setCustomEqEnabled,
|
||||||
setCustomEq = viewModel::setCustomEq
|
setCustomEq = viewModel::setCustomEq
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@Composable
|
@Composable
|
||||||
fun EqualizerScreen(
|
fun EqualizerScreen(
|
||||||
uiState: AppleUiState,
|
uiState: AppleUiState,
|
||||||
topPadding: Dp = 16.dp,
|
navigateBack: (() -> Unit)?,
|
||||||
bottomPadding: Dp = 16.dp,
|
|
||||||
setCustomEqEnabled: (Boolean) -> Unit,
|
setCustomEqEnabled: (Boolean) -> Unit,
|
||||||
setCustomEq: (Int, Int, Int) -> Unit
|
setCustomEq: (Int, Int, Int) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -131,6 +116,10 @@ fun EqualizerScreen(
|
|||||||
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.equalizer),
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@@ -228,6 +217,7 @@ fun EqualizerScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -698,16 +688,13 @@ fun EqualizerScreenPreviewApple() {
|
|||||||
) {
|
) {
|
||||||
val state = remember { mutableStateOf(AppleUiState()) }
|
val state = remember { mutableStateOf(AppleUiState()) }
|
||||||
|
|
||||||
Box (
|
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
EqualizerScreen(
|
EqualizerScreen(
|
||||||
uiState = state.value,
|
uiState = state.value,
|
||||||
|
navigateBack = null,
|
||||||
setCustomEqEnabled = { state.value = state.value.copy(state = state.value.state.copy(customEq = state.value.state.customEq.copy(state = if (it) 2 else 1))) },
|
setCustomEqEnabled = { state.value = state.value.copy(state = state.value.state.copy(customEq = state.value.state.customEq.copy(state = if (it) 2 else 1))) },
|
||||||
setCustomEq = {low, mid, high -> state.value = state.value.copy(state = state.value.state.copy(customEq = state.value.state.customEq.copy(low = low, mid = mid, high = high)))}
|
setCustomEq = {low, mid, high -> state.value = state.value.copy(state = state.value.state.copy(customEq = state.value.state.customEq.copy(low = low, mid = mid, high = high)))}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(name = "Material")
|
@Preview(name = "Material")
|
||||||
@@ -717,16 +704,12 @@ fun EqualizerScreenPreviewMaterial() {
|
|||||||
designSystem = DesignSystem.Material
|
designSystem = DesignSystem.Material
|
||||||
) {
|
) {
|
||||||
val state = remember { mutableStateOf(AppleUiState()) }
|
val state = remember { mutableStateOf(AppleUiState()) }
|
||||||
Box (
|
|
||||||
modifier = Modifier
|
|
||||||
.wrapContentHeight()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
EqualizerScreen(
|
EqualizerScreen(
|
||||||
uiState = state.value,
|
uiState = state.value,
|
||||||
|
navigateBack = null,
|
||||||
setCustomEqEnabled = { state.value = state.value.copy(state = state.value.state.copy(customEq = state.value.state.customEq.copy(state = if (it) 2 else 1))) },
|
setCustomEqEnabled = { state.value = state.value.copy(state = state.value.state.copy(customEq = state.value.state.customEq.copy(state = if (it) 2 else 1))) },
|
||||||
setCustomEq = {low, mid, high -> state.value = state.value.copy(state = state.value.state.copy(customEq = state.value.state.customEq.copy(low = low, mid = mid, high = high)))}
|
setCustomEq = {low, mid, high -> state.value = state.value.copy(state = state.value.state.copy(customEq = state.value.state.customEq.copy(low = low, mid = mid, high = high)))}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-26
@@ -32,26 +32,28 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
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.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledTonalIconToggleButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -64,6 +66,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
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.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@@ -78,14 +81,17 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
|||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.AppleEvent
|
|
||||||
import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle
|
import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
|
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
||||||
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
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
@@ -98,7 +104,11 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HeadTrackingScreen(viewModel: AppleViewModel, navigateToPurchase: () -> Unit) {
|
fun HeadTrackingScreen(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
|
navigateToPurchase: () -> Unit
|
||||||
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val settings = uiState.settings
|
val settings = uiState.settings
|
||||||
@@ -112,10 +122,6 @@ fun HeadTrackingScreen(viewModel: AppleViewModel, navigateToPurchase: () -> Unit
|
|||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
var gestureText by remember { mutableStateOf("") }
|
var gestureText by remember { mutableStateOf("") }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -124,10 +130,46 @@ fun HeadTrackingScreen(viewModel: AppleViewModel, navigateToPurchase: () -> Unit
|
|||||||
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.head_gestures),
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
actionButtons = listOf(
|
||||||
|
{ scaffoldBackdrop ->
|
||||||
|
if (LocalDesignSystem.current == DesignSystem.Material) {
|
||||||
|
FilledTonalIconToggleButton(
|
||||||
|
checked = uiState.state.headTrackingActive,
|
||||||
|
onCheckedChange = { if (it) viewModel.startHeadTracking() else viewModel.stopHeadTracking() },
|
||||||
|
modifier = Modifier
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.size(IconButtonDefaults.mediumContainerSize(IconButtonDefaults.IconButtonWidthOption.Uniform)),
|
||||||
|
shape = IconButtonDefaults.mediumRoundShape
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (uiState.state.headTrackingActive) MaterialIcons.Pause else Icons.Default.PlayArrow,
|
||||||
|
contentDescription = "Play/Pause",
|
||||||
|
modifier = Modifier.size(IconButtonDefaults.mediumIconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StyledIconButton(
|
||||||
|
onClick = if (!uiState.state.headTrackingActive) viewModel::startHeadTracking else viewModel::stopHeadTracking,
|
||||||
|
backdrop = scaffoldBackdrop
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (uiState.state.headTrackingActive) LocalIcons.current.Pause else LocalIcons.current.Play,
|
||||||
|
contentDescription = "Play/Pause",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
.padding(horizontal = 16.dp)
|
||||||
.verticalScroll(scrollState),
|
.verticalScroll(scrollState),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
@@ -138,7 +180,6 @@ fun HeadTrackingScreen(viewModel: AppleViewModel, navigateToPurchase: () -> Unit
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.layerBackdrop(backdrop)
|
.layerBackdrop(backdrop)
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if (!uiState.isPremium) {
|
if (!uiState.isPremium) {
|
||||||
@@ -167,14 +208,13 @@ fun HeadTrackingScreen(viewModel: AppleViewModel, navigateToPurchase: () -> Unit
|
|||||||
header = true
|
header = true
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
"Velocity",
|
"Velocity",
|
||||||
style = MaterialTheme.typography.labelSmallEmphasized,
|
style = MaterialTheme.typography.labelSmallEmphasized,
|
||||||
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
|
modifier = Modifier.padding(vertical = 8.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Plot()
|
Plot()
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -194,18 +234,12 @@ fun HeadTrackingScreen(viewModel: AppleViewModel, navigateToPurchase: () -> Unit
|
|||||||
onClick = {
|
onClick = {
|
||||||
gestureText = gestureTextValue
|
gestureText = gestureTextValue
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
viewModel.testHeadGestures()
|
viewModel.detectHeadGestures { gestureText = if (it) "\"Yes\" gesture detected." else "\"No\" gesture detected." }
|
||||||
val accepted = viewModel.events
|
|
||||||
.filterIsInstance<AppleEvent.HeadGesturesResult>()
|
|
||||||
.first()
|
|
||||||
.yes
|
|
||||||
gestureText = if (accepted) "\"Yes\" gesture detected." else "\"No\" gesture detected."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backdrop = backdrop,
|
backdrop = backdrop,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
maxScale = 0.05f,
|
maxScale = 0.05f,
|
||||||
materialButtonStyle = MaterialButtonStyle.Outlined
|
materialButtonStyle = MaterialButtonStyle.Outlined
|
||||||
) {
|
) {
|
||||||
@@ -254,8 +288,43 @@ fun HeadTrackingScreen(viewModel: AppleViewModel, navigateToPurchase: () -> Unit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uiState.appSettings.debugMode) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
StyledToggle(
|
||||||
|
label = "[debug] alternate horizontal byte offset",
|
||||||
|
checked = settings.headGesturesHorizontalOffset == 26,
|
||||||
|
onCheckedChange = { viewModel.setHeadGesturesHorizontalOffset(if (it) 26 else 28) }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
val sliderValue = remember {
|
||||||
|
mutableFloatStateOf(settings.headTrackingInterval.inWholeMilliseconds.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(sliderValue) {
|
||||||
|
snapshotFlow { sliderValue.floatValue }
|
||||||
|
.debounce(100.milliseconds)
|
||||||
|
.collect { value ->
|
||||||
|
viewModel.setHeadTrackingInterval(value.toInt().milliseconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledSlider(
|
||||||
|
label = "[debug] head tracking interval",
|
||||||
|
value = sliderValue.floatValue,
|
||||||
|
onValueChange = { sliderValue.floatValue = it },
|
||||||
|
valueRange = 10f..200f,
|
||||||
|
snapPoints = listOf(40f),
|
||||||
|
independent = true,
|
||||||
|
description = "how often airpods report sensor information",
|
||||||
|
enabled = uiState.isPremium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+10
-11
@@ -23,13 +23,9 @@ import androidx.compose.foundation.background
|
|||||||
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.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,17 +48,19 @@ 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.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.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
|
||||||
|
|
||||||
private const val TAG = "HearingAidAdjustments"
|
private const val TAG = "HearingAidAdjustments"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HearingAidAdjustmentsScreen(viewModel: AppleViewModel) {
|
fun HearingAidAdjustmentsScreen(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?
|
||||||
|
) {
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
@@ -149,10 +147,10 @@ fun HearingAidAdjustmentsScreen(viewModel: AppleViewModel) {
|
|||||||
sendHearingAidSettings(state.hearingAidData, hearingAidSettings.value, debounceJob, viewModel::writeATTCharacteristic)
|
sendHearingAidSettings(state.hearingAidData, hearingAidSettings.value, debounceJob, viewModel::writeATTCharacteristic)
|
||||||
}
|
}
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
StyledScaffold(
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
title = stringResource(R.string.adjustments),
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -228,4 +226,5 @@ fun HearingAidAdjustmentsScreen(viewModel: AppleViewModel) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-19
@@ -24,16 +24,11 @@ import androidx.compose.foundation.background
|
|||||||
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.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.ExperimentalMaterial3Api
|
|
||||||
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
|
||||||
@@ -48,7 +43,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
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 dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -59,17 +53,20 @@ import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings
|
|||||||
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
|
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
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.io.encoding.ExperimentalEncodingApi
|
|
||||||
|
|
||||||
private const val TAG = "AccessibilitySettings"
|
private const val TAG = "HearingAidScreen"
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
@Composable
|
@Composable
|
||||||
fun HearingAidScreen(viewModel: AppleViewModel, onNavigateHearingAidAdjustments: () -> Unit, onNavigateHearingTest: () -> Unit) {
|
fun HearingAidScreen(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
|
navigateToHearingAidAdjustments: () -> Unit,
|
||||||
|
navigateToHearingTest: () -> Unit
|
||||||
|
) {
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
@@ -86,10 +83,10 @@ fun HearingAidScreen(viewModel: AppleViewModel, onNavigateHearingAidAdjustments:
|
|||||||
mutableStateOf((aidStatus?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.getOrNull(0) == 0x01.toByte()))
|
mutableStateOf((aidStatus?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.getOrNull(0) == 0x01.toByte()))
|
||||||
}
|
}
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
StyledScaffold(
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
title = stringResource(R.string.hearing_aid),
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layerBackdrop(backdrop)
|
.layerBackdrop(backdrop)
|
||||||
@@ -132,7 +129,7 @@ fun HearingAidScreen(viewModel: AppleViewModel, onNavigateHearingAidAdjustments:
|
|||||||
)
|
)
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.adjustments),
|
contentText = stringResource(R.string.adjustments),
|
||||||
onClick = onNavigateHearingAidAdjustments,
|
onClick = navigateToHearingAidAdjustments,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +143,7 @@ fun HearingAidScreen(viewModel: AppleViewModel, onNavigateHearingAidAdjustments:
|
|||||||
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.update_hearing_test),
|
contentText = stringResource(R.string.update_hearing_test),
|
||||||
onClick = onNavigateHearingTest,
|
onClick = navigateToHearingTest,
|
||||||
)
|
)
|
||||||
|
|
||||||
// not implemented yet
|
// not implemented yet
|
||||||
@@ -188,6 +185,7 @@ fun HearingAidScreen(viewModel: AppleViewModel, onNavigateHearingAidAdjustments:
|
|||||||
// }
|
// }
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
showDialog = showDialog,
|
showDialog = showDialog,
|
||||||
@@ -213,13 +211,13 @@ fun HearingAidScreen(viewModel: AppleViewModel, onNavigateHearingAidAdjustments:
|
|||||||
}
|
}
|
||||||
val parsed = parseTransparencySettingsResponse(state.hearingAidData)
|
val parsed = parseTransparencySettingsResponse(state.hearingAidData)
|
||||||
if (parsed == null) {
|
if (parsed == null) {
|
||||||
Log.w(TAG, "transparency parse failed")
|
Log.w(TAG, "hearingaid parse failed")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val disabledSettings = parsed.copy(enabled = false)
|
val disabledSettings = parsed.copy(enabled = false)
|
||||||
sendTransparencySettings(viewModel::writeATTCharacteristic, disabledSettings)
|
sendTransparencySettings(viewModel::writeATTCharacteristic, disabledSettings)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error disabling transparency: ${e.message}")
|
Log.e(TAG, "Error disabling hearingaid: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-12
@@ -21,14 +21,10 @@ package me.kavishdevar.librepods.presentation.screens.apple
|
|||||||
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.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.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
|
||||||
@@ -40,25 +36,28 @@ import androidx.compose.ui.unit.dp
|
|||||||
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.bluetooth.att.ATTHandle
|
|
||||||
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.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
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
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HearingProtectionScreen(viewModel: AppleViewModel, navigateToPurchase: () -> Unit) {
|
fun HearingProtectionScreen(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
|
navigateToPurchase: () -> Unit
|
||||||
|
) {
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
StyledScaffold(
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
title = stringResource(R.string.hearing_protection),
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -117,4 +116,5 @@ fun HearingProtectionScreen(viewModel: AppleViewModel, navigateToPurchase: () ->
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+253
@@ -0,0 +1,253 @@
|
|||||||
|
package me.kavishdevar.librepods.presentation.screens.apple
|
||||||
|
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material3.FilledTonalIconToggleButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialShapes
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
|
import androidx.compose.material3.toShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalLocale
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.health.connect.client.permission.HealthPermission
|
||||||
|
import androidx.health.connect.client.records.HeartRateRecord
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.isGranted
|
||||||
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
|
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
||||||
|
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||||
|
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HeartRateRoute(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
HeartRateScreen(
|
||||||
|
uiState = uiState,
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
startHr = viewModel::startHr,
|
||||||
|
stopHr = viewModel::stopHr,
|
||||||
|
setHrRange = viewModel::setHrRange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun HeartRateScreen(
|
||||||
|
uiState: AppleUiState,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
|
startHr: () -> Unit,
|
||||||
|
stopHr: () -> Unit,
|
||||||
|
setHrRange: (ClosedRange<Long>) -> Unit
|
||||||
|
) {
|
||||||
|
val state = uiState.state
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.heart_rate),
|
||||||
|
navigateBack = navigateBack,
|
||||||
|
actionButtons = listOf(
|
||||||
|
{ scaffoldBackdrop ->
|
||||||
|
if (LocalDesignSystem.current == DesignSystem.Material) {
|
||||||
|
FilledTonalIconToggleButton(
|
||||||
|
checked = state.hrmActive,
|
||||||
|
onCheckedChange = { if (it) startHr() else stopHr() },
|
||||||
|
modifier = Modifier
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.size(IconButtonDefaults.mediumContainerSize(IconButtonDefaults.IconButtonWidthOption.Uniform)),
|
||||||
|
shape = IconButtonDefaults.mediumRoundShape
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (state.hrmActive) MaterialIcons.Pause else Icons.Default.PlayArrow,
|
||||||
|
contentDescription = "Start/Stop",
|
||||||
|
modifier = Modifier.size(IconButtonDefaults.mediumIconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StyledIconButton(
|
||||||
|
onClick = { if (!state.hrmActive) startHr() else stopHr() },
|
||||||
|
backdrop = scaffoldBackdrop
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (state.hrmActive) LocalIcons.current.Pause else LocalIcons.current.Play,
|
||||||
|
contentDescription = "Start/Stop",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.verticalScroll(scrollState),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
|
|
||||||
|
val sliderValue = remember {
|
||||||
|
mutableFloatStateOf(state.heartRateInterval.inWholeMilliseconds.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
val healthPermissions = rememberPermissionState(
|
||||||
|
HealthPermission.getWritePermission(HeartRateRecord::class)
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = !healthPermissions.status.isGranted) {
|
||||||
|
if (healthPermissions.status.isGranted) return@AnimatedVisibility
|
||||||
|
StyledListItem(
|
||||||
|
contentText = stringResource(R.string.permission_healthconnect),
|
||||||
|
onClick = { healthPermissions.launchPermissionRequest() },
|
||||||
|
supportingText = stringResource(R.string.permission_description_healthconnect),
|
||||||
|
leadingContent = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
MaterialShapes.SoftBurst.normalized()
|
||||||
|
.toShape()
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = LocalIcons.current.VitalSigns,
|
||||||
|
contentDescription = "vital signs",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orientation = StyledListItemOrientation.Vertical
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saw this while using one of the forks. 1 minute doesn't seem to work for me
|
||||||
|
// StyledList(
|
||||||
|
// title = stringResource(R.string.interval),
|
||||||
|
// description = stringResource(R.string.heart_rate_interval_description),
|
||||||
|
// ) {
|
||||||
|
// StyledListItem(
|
||||||
|
// onClick = { setHrInterval(1.seconds) },
|
||||||
|
// contentText = stringResource(R.string.one_second),
|
||||||
|
// selected = state.heartRateInterval == 1.seconds
|
||||||
|
// )
|
||||||
|
// StyledListItem(
|
||||||
|
// onClick = { setHrInterval(1.minutes) },
|
||||||
|
// contentText = stringResource(R.string.one_minute),
|
||||||
|
// selected = state.heartRateInterval == 1.minutes
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
AnimatedVisibility(state.currentHeartRate != null) {
|
||||||
|
if (state.currentHeartRate == null) return@AnimatedVisibility
|
||||||
|
StyledListItem(
|
||||||
|
onClick = null,
|
||||||
|
content = {
|
||||||
|
Text(
|
||||||
|
text = "${state.currentHeartRate.bpm} bpm",
|
||||||
|
style = MaterialTheme.typography.bodyLargeEmphasized,
|
||||||
|
modifier = Modifier.fillMaxHeight()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
val locale = LocalLocale.current.platformLocale
|
||||||
|
val timePattern = DateFormat.getBestDateTimePattern(
|
||||||
|
locale,
|
||||||
|
"jms"
|
||||||
|
)
|
||||||
|
val formatter = DateTimeFormatter.ofPattern(timePattern)
|
||||||
|
|
||||||
|
val timeString = formatter.format(
|
||||||
|
Instant.ofEpochMilli(state.currentHeartRate.timestamp.toEpochMilliseconds())
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = timeString,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
MaterialShapes.SoftBurst.normalized()
|
||||||
|
.toShape()
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = LocalIcons.current.VitalSigns,
|
||||||
|
contentDescription = "vital signs",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orientation = StyledListItemOrientation.Vertical
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 350.dp)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
RoundedCornerShape(28.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// TODO: graph or something
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-23
@@ -1,16 +1,11 @@
|
|||||||
package me.kavishdevar.librepods.presentation.screens.apple
|
package me.kavishdevar.librepods.presentation.screens.apple
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
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.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
|
||||||
@@ -19,55 +14,46 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
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.unit.Dp
|
|
||||||
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.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MicrophoneSettingsRoute(
|
fun MicrophoneSettingsRoute(
|
||||||
viewModel: AppleViewModel
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
val id = ControlCommandIdentifier.MIC_MODE
|
val id = ControlCommandIdentifier.MIC_MODE
|
||||||
|
|
||||||
Box (
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
MicrophoneSettingsScreen(
|
MicrophoneSettingsScreen(
|
||||||
|
navigateBack = navigateBack,
|
||||||
selectedMode = state.controlStates[id]?.getOrNull(0)?.toInt() ?: 0,
|
selectedMode = state.controlStates[id]?.getOrNull(0)?.toInt() ?: 0,
|
||||||
topPadding = topPadding,
|
|
||||||
bottomPadding = bottomPadding,
|
|
||||||
onMicrophoneSettingsChanged = {
|
onMicrophoneSettingsChanged = {
|
||||||
viewModel.setControlCommand(id, it)
|
viewModel.setControlCommand(id, it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MicrophoneSettingsScreen(
|
fun MicrophoneSettingsScreen(
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
selectedMode: Int,
|
selectedMode: Int,
|
||||||
topPadding: Dp = 16.dp,
|
|
||||||
bottomPadding: Dp = 16.dp,
|
|
||||||
onMicrophoneSettingsChanged: (Int) -> Unit
|
onMicrophoneSettingsChanged: (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.microphone_mode),
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -100,4 +86,5 @@ fun MicrophoneSettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-17
@@ -24,18 +24,13 @@ import android.util.Log
|
|||||||
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.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.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
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
|
||||||
@@ -47,24 +42,25 @@ import androidx.compose.ui.res.painterResource
|
|||||||
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 com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|
||||||
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.StyledListItemOrientation
|
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
import me.kavishdevar.librepods.presentation.components.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
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LongPress(viewModel: AppleViewModel, name: String, navigateToPurchase: () -> Unit) {
|
fun LongPress(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
name: String,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
|
navigateToPurchase: () -> Unit
|
||||||
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
@@ -78,14 +74,14 @@ fun LongPress(viewModel: AppleViewModel, name: String, navigateToPurchase: () ->
|
|||||||
Log.d("PressAndHoldSettingsScreen", "Noise Cancellation mode: ${(modesByte and 0x02) != 0.toByte()}")
|
Log.d("PressAndHoldSettingsScreen", "Noise Cancellation mode: ${(modesByte and 0x02) != 0.toByte()}")
|
||||||
Log.d("PressAndHoldSettingsScreen", "Adaptive mode: ${(modesByte and 0x08) != 0.toByte()}")
|
Log.d("PressAndHoldSettingsScreen", "Adaptive mode: ${(modesByte and 0x08) != 0.toByte()}")
|
||||||
|
|
||||||
val longPressAction = if (name.lowercase() == "left") settings.leftLongPressAction else settings.rightLongPressAction
|
val longPressAction = if (name == stringResource(R.string.left)) settings.leftLongPressAction else settings.rightLongPressAction
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = name,
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column (
|
Column (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -233,4 +229,5 @@ fun LongPress(viewModel: AppleViewModel, name: String, navigateToPurchase: () ->
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-21
@@ -8,14 +8,10 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
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.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -45,12 +41,11 @@ 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.StyledListItemOrientation
|
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
import me.kavishdevar.librepods.presentation.components.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
|
||||||
@@ -60,27 +55,17 @@ import java.time.format.DateTimeFormatter
|
|||||||
@Composable
|
@Composable
|
||||||
fun RecordingScreenRoute(
|
fun RecordingScreenRoute(
|
||||||
viewModel: AppleViewModel,
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
Box (
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
RecordingScreen(
|
RecordingScreen(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
|
navigateBack = navigateBack,
|
||||||
recordings = viewModel.recordings(),
|
recordings = viewModel.recordings(),
|
||||||
startRecording = viewModel::startRecording,
|
startRecording = viewModel::startRecording,
|
||||||
stopRecording = viewModel::stopRecording,
|
stopRecording = viewModel::stopRecording,
|
||||||
topPadding = topPadding,
|
|
||||||
bottomPadding = bottomPadding
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
@@ -92,11 +77,10 @@ fun RecordingScreenRoute(
|
|||||||
@Composable
|
@Composable
|
||||||
fun RecordingScreen(
|
fun RecordingScreen(
|
||||||
uiState: AppleUiState,
|
uiState: AppleUiState,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
recordings: List<Recording>,
|
recordings: List<Recording>,
|
||||||
startRecording: () -> Unit,
|
startRecording: () -> Unit,
|
||||||
stopRecording: () -> Unit,
|
stopRecording: () -> Unit,
|
||||||
topPadding: Dp = 16.dp,
|
|
||||||
bottomPadding: Dp = 16.dp
|
|
||||||
) {
|
) {
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
|
|
||||||
@@ -139,6 +123,10 @@ fun RecordingScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.recorder),
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -285,4 +273,5 @@ fun RecordingScreen(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.padding(bottom = bottomPadding))
|
Spacer(modifier = Modifier.padding(bottom = bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-18
@@ -20,20 +20,13 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.screens.apple
|
package me.kavishdevar.librepods.presentation.screens.apple
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
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.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.text.input.rememberTextFieldState
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -41,21 +34,22 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.edit
|
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RenameScreen(viewModel: AppleViewModel) {
|
fun RenameScreen(
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?
|
||||||
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
@@ -64,17 +58,16 @@ fun RenameScreen(viewModel: AppleViewModel) {
|
|||||||
keyboardController?.show()
|
keyboardController?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val metadata = uiState.metadata
|
val metadata = uiState.metadata
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.name),
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(topPadding))
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
@@ -93,4 +86,5 @@ fun RenameScreen(viewModel: AppleViewModel) {
|
|||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-15
@@ -26,20 +26,15 @@ import androidx.compose.foundation.layout.Box
|
|||||||
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.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.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
@@ -61,11 +56,11 @@ import androidx.compose.ui.graphics.luminance
|
|||||||
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 androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|
||||||
import me.kavishdevar.librepods.R
|
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.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
@@ -73,15 +68,15 @@ import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
|||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.sectionHeader
|
import me.kavishdevar.librepods.presentation.theme.sectionHeader
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
||||||
|
|
||||||
private const val TAG = "TransparencySettings"
|
private const val TAG = "TransparencySettings"
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TransparencySettingsScreen(viewModel: AppleViewModel) {
|
fun TransparencySettingsScreen(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?,
|
||||||
|
) {
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5f
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5f
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
@@ -94,10 +89,10 @@ fun TransparencySettingsScreen(viewModel: AppleViewModel) {
|
|||||||
|
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
StyledScaffold(
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
title = stringResource(R.string.customize_transparency_mode),
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -263,7 +258,7 @@ fun TransparencySettingsScreen(viewModel: AppleViewModel) {
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.equalizer),
|
text = stringResource(R.string.equalizer),
|
||||||
style = MaterialTheme.typography.labelSmallEmphasized,
|
style = MaterialTheme.typography.labelSmallEmphasized,
|
||||||
color = if (m3eEnabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.sectionHeader,
|
color = if (LocalDesignSystem.current == DesignSystem.Material) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.sectionHeader,
|
||||||
modifier = Modifier.padding(16.dp, bottom = 4.dp)
|
modifier = Modifier.padding(16.dp, bottom = 4.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -358,4 +353,5 @@ fun TransparencySettingsScreen(viewModel: AppleViewModel) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-22
@@ -25,14 +25,9 @@ import androidx.compose.foundation.layout.Box
|
|||||||
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.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.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.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -56,7 +51,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
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 androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
@@ -64,47 +58,42 @@ 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.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.theme.LocalDesignSystem
|
|
||||||
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
|
||||||
|
|
||||||
private const val TAG = "UpdateHearingTestScreen"
|
private const val TAG = "UpdateHearingTestScreen"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateHearingTestRoute(viewModel: AppleViewModel) {
|
fun UpdateHearingTestRoute(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?
|
||||||
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
|
||||||
UpdateHearingTestScreen(
|
UpdateHearingTestScreen(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
topPadding = topPadding,
|
navigateBack = navigateBack,
|
||||||
bottomPadding = bottomPadding,
|
|
||||||
setATTCharacteristicValue = viewModel::writeATTCharacteristic
|
setATTCharacteristicValue = viewModel::writeATTCharacteristic
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateHearingTestScreen(
|
fun UpdateHearingTestScreen(
|
||||||
uiState: AppleUiState,
|
uiState: AppleUiState,
|
||||||
topPadding: Dp = 16.dp,
|
navigateBack: (() -> Unit)?,
|
||||||
bottomPadding: Dp = 16.dp,
|
|
||||||
setATTCharacteristicValue: (ATTHandle, ByteArray) -> Unit
|
setATTCharacteristicValue: (ATTHandle, ByteArray) -> Unit
|
||||||
) {
|
) {
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
|
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.update_hearing_test),
|
||||||
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(verticalScrollState)
|
.verticalScroll(verticalScrollState)
|
||||||
@@ -282,6 +271,7 @@ fun UpdateHearingTestScreen(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(name = "Apple")
|
@Preview(name = "Apple")
|
||||||
@@ -295,6 +285,7 @@ fun UpdateHearingTestScreenPreviewApple() {
|
|||||||
) {
|
) {
|
||||||
UpdateHearingTestScreen(
|
UpdateHearingTestScreen(
|
||||||
uiState = AppleUiState(),
|
uiState = AppleUiState(),
|
||||||
|
navigateBack = null,
|
||||||
setATTCharacteristicValue = { _, _ -> }
|
setATTCharacteristicValue = { _, _ -> }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -314,6 +305,7 @@ fun UpdateHearingTestScreenPreviewMaterial() {
|
|||||||
) {
|
) {
|
||||||
UpdateHearingTestScreen(
|
UpdateHearingTestScreen(
|
||||||
uiState = AppleUiState(),
|
uiState = AppleUiState(),
|
||||||
|
navigateBack = null,
|
||||||
setATTCharacteristicValue = { _, _ -> }
|
setATTCharacteristicValue = { _, _ -> }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-14
@@ -18,17 +18,11 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.screens.apple
|
package me.kavishdevar.librepods.presentation.screens.apple
|
||||||
|
|
||||||
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.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.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -38,24 +32,25 @@ 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.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VersionScreen(viewModel: AppleViewModel) {
|
fun VersionScreen(
|
||||||
|
viewModel: AppleViewModel,
|
||||||
|
navigateBack: (() -> Unit)?
|
||||||
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val metadata = uiState.metadata
|
val metadata = uiState.metadata
|
||||||
|
|
||||||
val m3eEnabled = LocalDesignSystem.current == DesignSystem.Material
|
StyledScaffold(
|
||||||
val topPadding = if (m3eEnabled) 0.dp else WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 84.dp
|
title = stringResource(R.string.version),
|
||||||
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 12.dp
|
navigateBack = navigateBack
|
||||||
|
) { topPadding, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(topPadding))
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
@@ -80,4 +75,5 @@ fun VersionScreen(viewModel: AppleViewModel) {
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -93,6 +93,7 @@ fun OnboardingScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
HorizontalUncontainedCarousel(
|
HorizontalUncontainedCarousel(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
+74
-26
@@ -37,8 +37,11 @@ import androidx.compose.runtime.remember
|
|||||||
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.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.health.connect.client.permission.HealthPermission
|
||||||
|
import androidx.health.connect.client.records.HeartRateRecord
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
@@ -46,9 +49,10 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|||||||
import com.google.accompanist.permissions.isGranted
|
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.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@@ -63,11 +67,8 @@ fun PermissionsPage(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val canDrawOverlays = remember { mutableStateOf(Settings.canDrawOverlays(context)) }
|
val canDrawOverlays = remember { mutableStateOf(Settings.canDrawOverlays(context)) }
|
||||||
|
|
||||||
val phonePermissionState = rememberMultiplePermissionsState(
|
val healthPermissions = rememberPermissionState(
|
||||||
listOf(
|
HealthPermission.getWritePermission(HeartRateRecord::class)
|
||||||
"android.permission.READ_PHONE_STATE",
|
|
||||||
"android.permission.ANSWER_PHONE_CALLS"
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
if (grantingAll) {
|
if (grantingAll) {
|
||||||
if (!canDrawOverlays.value) {
|
if (!canDrawOverlays.value) {
|
||||||
@@ -80,10 +81,23 @@ fun PermissionsPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val phonePermissionState = rememberMultiplePermissionsState(
|
||||||
|
listOf(
|
||||||
|
"android.permission.READ_PHONE_STATE",
|
||||||
|
"android.permission.ANSWER_PHONE_CALLS"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (grantingAll) {
|
||||||
|
if (!healthPermissions.status.isGranted) healthPermissions.launchPermissionRequest()
|
||||||
|
else if (!canDrawOverlays.value) canDrawOverlays.value = Settings.canDrawOverlays(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val notificationPermissionState = rememberPermissionState("android.permission.POST_NOTIFICATIONS") {
|
val notificationPermissionState = rememberPermissionState("android.permission.POST_NOTIFICATIONS") {
|
||||||
if (grantingAll) {
|
if (grantingAll) {
|
||||||
if (!phonePermissionState.allPermissionsGranted) phonePermissionState.launchMultiplePermissionRequest()
|
if (!phonePermissionState.allPermissionsGranted) phonePermissionState.launchMultiplePermissionRequest()
|
||||||
|
else if (!healthPermissions.status.isGranted) healthPermissions.launchPermissionRequest()
|
||||||
else if (!canDrawOverlays.value) canDrawOverlays.value = Settings.canDrawOverlays(context)
|
else if (!canDrawOverlays.value) canDrawOverlays.value = Settings.canDrawOverlays(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,6 +114,7 @@ fun PermissionsPage(
|
|||||||
if (grantingAll) {
|
if (grantingAll) {
|
||||||
if (!notificationPermissionState.status.isGranted) notificationPermissionState.launchPermissionRequest()
|
if (!notificationPermissionState.status.isGranted) notificationPermissionState.launchPermissionRequest()
|
||||||
else if (!phonePermissionState.allPermissionsGranted) phonePermissionState.launchMultiplePermissionRequest()
|
else if (!phonePermissionState.allPermissionsGranted) phonePermissionState.launchMultiplePermissionRequest()
|
||||||
|
else if (!healthPermissions.status.isGranted) healthPermissions.launchPermissionRequest()
|
||||||
else if (!canDrawOverlays.value) canDrawOverlays.value = Settings.canDrawOverlays(context)
|
else if (!canDrawOverlays.value) canDrawOverlays.value = Settings.canDrawOverlays(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,21 +150,21 @@ fun PermissionsPage(
|
|||||||
.verticalScroll(scrollState),
|
.verticalScroll(scrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
StyledList(title = "Required Permissions") {
|
StyledList(title = stringResource(R.string.required_permissions)) {
|
||||||
val animatedBluetoothIconColor by animateColorAsState(if (bluetoothPermissionsState.allPermissionsGranted) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface)
|
val animatedBluetoothIconColor by animateColorAsState(if (bluetoothPermissionsState.allPermissionsGranted) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
val animatedBluetoothContainerColor by animateColorAsState(
|
val animatedBluetoothContainerColor by animateColorAsState(
|
||||||
if (bluetoothPermissionsState.allPermissionsGranted) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest
|
if (bluetoothPermissionsState.allPermissionsGranted) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = "Bluetooth",
|
contentText = stringResource(R.string.bluetooth),
|
||||||
onClick = if (!bluetoothPermissionsState.allPermissionsGranted) {
|
onClick = if (!bluetoothPermissionsState.allPermissionsGranted) {
|
||||||
{
|
{
|
||||||
grantingAll = false
|
grantingAll = false
|
||||||
bluetoothPermissionsState.launchMultiplePermissionRequest()
|
bluetoothPermissionsState.launchMultiplePermissionRequest()
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
supportingText = "Required to communicate with AirPods",
|
supportingText = stringResource(R.string.permission_description_bluetooth),
|
||||||
orientation = StyledListItemOrientation.Vertical,
|
orientation = StyledListItemOrientation.Vertical,
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Box(
|
Box(
|
||||||
@@ -172,27 +187,27 @@ fun PermissionsPage(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
StyledList(title = "Optional Permissions") {
|
StyledList(title = stringResource(R.string.optional_permissions)) {
|
||||||
val animatedNotificationsIconColor by animateColorAsState(
|
val animatedNotificationsIconColor by animateColorAsState(
|
||||||
if (notificationPermissionState.status.isGranted) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
|
if (notificationPermissionState.status.isGranted) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
val animatedNotificationsContainerColor by animateColorAsState(
|
val animatedNotificationsContainerColor by animateColorAsState(
|
||||||
if (notificationPermissionState.status.isGranted) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest
|
if (notificationPermissionState.status.isGranted) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
val animatedPhoneIconColor by animateColorAsState(if (phonePermissionState.allPermissionsGranted) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface)
|
val animatedPhoneIconColor by animateColorAsState(if (phonePermissionState.allPermissionsGranted) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
val animatedPhoneContainerColor by animateColorAsState(
|
val animatedPhoneContainerColor by animateColorAsState(
|
||||||
if (phonePermissionState.allPermissionsGranted) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest
|
if (phonePermissionState.allPermissionsGranted) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = "Notifications",
|
contentText = stringResource(R.string.notifications),
|
||||||
onClick = if (!notificationPermissionState.status.isGranted) {
|
onClick = if (!notificationPermissionState.status.isGranted) {
|
||||||
{
|
{
|
||||||
grantingAll = false
|
grantingAll = false
|
||||||
notificationPermissionState.launchPermissionRequest()
|
notificationPermissionState.launchPermissionRequest()
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
supportingText = "Show battery status",
|
supportingText = stringResource(R.string.permission_description_notification),
|
||||||
orientation = StyledListItemOrientation.Vertical,
|
orientation = StyledListItemOrientation.Vertical,
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Box(
|
Box(
|
||||||
@@ -215,14 +230,14 @@ fun PermissionsPage(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = "Phone",
|
contentText = stringResource(R.string.phone),
|
||||||
onClick = if (!phonePermissionState.allPermissionsGranted) {
|
onClick = if (!phonePermissionState.allPermissionsGranted) {
|
||||||
{
|
{
|
||||||
grantingAll = false
|
grantingAll = false
|
||||||
phonePermissionState.launchMultiplePermissionRequest()
|
phonePermissionState.launchMultiplePermissionRequest()
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
supportingText = "Respond to phone calls with head gestures",
|
supportingText = stringResource(R.string.permission_description_phone),
|
||||||
orientation = StyledListItemOrientation.Vertical,
|
orientation = StyledListItemOrientation.Vertical,
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Box(
|
Box(
|
||||||
@@ -237,7 +252,7 @@ fun PermissionsPage(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = MaterialIcons.Call,
|
imageVector = MaterialIcons.Call,
|
||||||
contentDescription = "bluetooth",
|
contentDescription = "call",
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
tint = animatedPhoneIconColor
|
tint = animatedPhoneIconColor
|
||||||
)
|
)
|
||||||
@@ -246,11 +261,44 @@ fun PermissionsPage(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val animatedOverlayIconColor by animateColorAsState(if (canDrawOverlays.value) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface)
|
val animatedHealthConnectIconColor by animateColorAsState(if (healthPermissions.status.isGranted) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
val animatedOverlayContainerColor by animateColorAsState(if (canDrawOverlays.value) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest)
|
val animatedHealthConnectContainerColor by animateColorAsState(if (healthPermissions.status.isGranted) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = "Display over other apps",
|
contentText = stringResource(R.string.permission_healthconnect),
|
||||||
|
onClick = if (!healthPermissions.status.isGranted) {
|
||||||
|
{
|
||||||
|
grantingAll = false
|
||||||
|
healthPermissions.launchPermissionRequest()
|
||||||
|
}
|
||||||
|
} else null,
|
||||||
|
supportingText = stringResource(R.string.permission_description_healthconnect),
|
||||||
|
leadingContent = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(
|
||||||
|
animatedHealthConnectContainerColor,
|
||||||
|
MaterialShapes.SoftBurst.normalized()
|
||||||
|
.toShape()
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MaterialIcons.VitalSigns,
|
||||||
|
contentDescription = "vital signs",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = animatedHealthConnectIconColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val animatedOverlayIconColor by animateColorAsState(if (canDrawOverlays.value) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
val animatedOverlayContainerColor by animateColorAsState(if (canDrawOverlays.value) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
|
||||||
|
StyledListItem(
|
||||||
|
contentText = stringResource(R.string.permission_overlay),
|
||||||
onClick = if (!canDrawOverlays.value) {
|
onClick = if (!canDrawOverlays.value) {
|
||||||
{
|
{
|
||||||
grantingAll = false
|
grantingAll = false
|
||||||
@@ -261,7 +309,7 @@ fun PermissionsPage(
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
supportingText = "Show popups when AirPods are nearby or audio switches to them.",
|
supportingText = stringResource(R.string.permission_description_overlay),
|
||||||
orientation = StyledListItemOrientation.Vertical,
|
orientation = StyledListItemOrientation.Vertical,
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Box(
|
Box(
|
||||||
@@ -276,7 +324,7 @@ fun PermissionsPage(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = MaterialIcons.Overlay,
|
imageVector = MaterialIcons.Overlay,
|
||||||
contentDescription = "bluetooth",
|
contentDescription = "overlay",
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
tint = animatedOverlayIconColor
|
tint = animatedOverlayIconColor
|
||||||
)
|
)
|
||||||
|
|||||||
+57
-27
@@ -48,42 +48,50 @@ fun PrivacyPolicyPage(
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Overview",
|
text = "Overview",
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "LibrePods does not collect, store, sell, or share personal information for advertising, analytics, tracking, or profiling purposes. The app does not include analytics, crash reporting, telemetry, advertising SDKs, or tracking services.",
|
text = "LibrePods does not collect, store, sell, or share personal information for advertising, analytics, tracking, or profiling purposes. The app does not include analytics, crash reporting, telemetry, advertising SDKs, or tracking services.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "All information remains on your device unless you explicitly choose to contact me, create a GitHub issue from the app, or make a purchase or sponsorship through a third-party platform.",
|
text = "All information remains on your device unless you explicitly choose to contact me, create a GitHub issue from the app, or make a purchase or sponsorship through a third-party platform.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Third Party Services",
|
text = "Third Party Services",
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "LibrePods provides several ways to contact me, including email, Discord, and GitHub Issues.",
|
text = "LibrePods provides several ways to contact me, including email, Discord, and GitHub Issues.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Email",
|
text = "Email",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "If you contact me by email, I receive your email address and any information you choose to include in your message. When using the contact form within LibrePods, your email client will open with a pre-filled email address, the subject line and body that you fill out. The body will also include LibrePods version information and device information to help with troubleshooting.",
|
text = "If you contact me by email, I receive your email address and any information you choose to include in your message. When using the contact form within LibrePods, your email client will open with a pre-filled email address, the subject line and body that you fill out. The body will also include LibrePods version information and device information to help with troubleshooting.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "You can edit or remove any of this information before sending the email.",
|
text = "You can edit or remove any of this information before sending the email.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -93,22 +101,26 @@ fun PrivacyPolicyPage(
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "The app provides a link to the LibrePods Discord server. If you choose to join the Discord server, you will be subject to Discord's privacy policy.",
|
text = "The app provides a link to the LibrePods Discord server. If you choose to join the Discord server, you will be subject to Discord's privacy policy.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "I do not receive any information about you from Discord other than what is publicly visible in the Discord server, such as your username, joining date, common servers, and any messages or content you post in the server, unless you choose to share it with me in the Discord server.",
|
text = "I do not receive any information about you from Discord other than what is publicly visible in the Discord server, such as your username, joining date, common servers, and any messages or content you post in the server, unless you choose to share it with me in the Discord server.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "GitHub Issues",
|
text = "GitHub Issues",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "When creating a GitHub issue through LibrePods, the app will pre-fill the issue form with:",
|
text = "When creating a GitHub issue through LibrePods, the app will pre-fill the issue form with:",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -117,68 +129,86 @@ fun PrivacyPolicyPage(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"• LibrePods version name and version code",
|
"• LibrePods version name and version code",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "• Device manufacturer and model",
|
text = "• Device manufacturer and model",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "• Android build information",
|
text = "• Android build information",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "• Installation source (Google Play or GitHub)",
|
text = "• Installation source (Google Play or GitHub)",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "This information helps diagnose bugs and provide support. No information is sent automatically. The information is only submitted if you choose to create the GitHub issue.",
|
text = "This information helps diagnose bugs and provide support. No information is sent automatically. The information is only submitted if you choose to create the GitHub issue.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Payments", style = MaterialTheme.typography.titleLarge
|
text = "Payments",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
if (BuildConfig.PLAY_BUILD) {
|
if (BuildConfig.PLAY_BUILD) {
|
||||||
Text(
|
Text(
|
||||||
text = "Google Play", style = MaterialTheme.typography.titleMedium
|
text = "Google Play",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "When using the version available on Google Play, purchases are processed by Google Play.",
|
text = "When using the version available on Google Play, purchases are processed by Google Play.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "LibrePods verifies the purchase with Google Play on-device, not with a remote server that I control. I do not receive any information about you or your purchase from Google Play. Payment processing is handled entirely by Google Play, and I do not have access to any of your payment information.",
|
text = "LibrePods verifies the purchase with Google Play on-device, not with a remote server that I control. I do not receive any information about you or your purchase from Google Play. Payment processing is handled entirely by Google Play, and I do not have access to any of your payment information.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = "GitHub Sponsors", style = MaterialTheme.typography.titleMedium
|
text = "GitHub Sponsors",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "When using the FOSS version available on GitHub, the upgrade button links to GitHub Sponsors. If you choose to sponsor LibrePods, your sponsorship is processed by GitHub.",
|
text = "When using the FOSS version available on GitHub, the upgrade button links to GitHub Sponsors. If you choose to sponsor LibrePods, your sponsorship is processed by GitHub.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Your username and country/region are shared with me when you sponsor LibrePods. Depending on your GitHub Sponsors privacy settings, I may also receive your email address.",
|
text = "Your username and country/region are shared with me when you sponsor LibrePods. Depending on your GitHub Sponsors privacy settings, I may also receive your email address.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Contact", style = MaterialTheme.typography.titleLarge
|
text = "Contact",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "If you have questions about this privacy policy, please contact me via email at privacy@kavish.xyz.",
|
text = "If you have questions about this privacy policy, please contact me via email at privacy@kavish.xyz.",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
@@ -187,7 +217,7 @@ fun PrivacyPolicyPage(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.i_agree),
|
text = stringResource(R.string.i_agree),
|
||||||
style = MaterialTheme.typography.labelMediumEmphasized
|
style = MaterialTheme.typography.labelMediumEmphasized,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+68
-5
@@ -21,35 +21,54 @@ package me.kavishdevar.librepods.presentation.viewmodel
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.billing.BillingManager
|
import me.kavishdevar.librepods.billing.BillingManager
|
||||||
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.data.StemAction
|
import me.kavishdevar.librepods.data.StemAction
|
||||||
|
import me.kavishdevar.librepods.data.heartrate.HeartRateSample
|
||||||
import me.kavishdevar.librepods.data.recording.Recording
|
import me.kavishdevar.librepods.data.recording.Recording
|
||||||
import me.kavishdevar.librepods.data.xposed.XposedRemotePrefProvider
|
import me.kavishdevar.librepods.data.xposed.XposedRemotePrefProvider
|
||||||
|
import me.kavishdevar.librepods.database.app.AppSettingsEntity
|
||||||
|
import me.kavishdevar.librepods.database.app.AppStateEntity
|
||||||
import me.kavishdevar.librepods.devices.AppleDevice
|
import me.kavishdevar.librepods.devices.AppleDevice
|
||||||
import me.kavishdevar.librepods.devices.AppleMetadata
|
import me.kavishdevar.librepods.devices.AppleMetadata
|
||||||
import me.kavishdevar.librepods.devices.AppleSettings
|
import me.kavishdevar.librepods.devices.AppleSettings
|
||||||
import me.kavishdevar.librepods.devices.AppleState
|
import me.kavishdevar.librepods.devices.AppleState
|
||||||
|
import me.kavishdevar.librepods.repository.AppDataRepository
|
||||||
|
import me.kavishdevar.librepods.repository.HeartRateRepository
|
||||||
import me.kavishdevar.librepods.repository.RecordingRepository
|
import me.kavishdevar.librepods.repository.RecordingRepository
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
data class AppleUiState(
|
data class AppleUiState(
|
||||||
val state: AppleState = AppleState(),
|
val state: AppleState = AppleState(),
|
||||||
val settings: AppleSettings = AppleSettings(),
|
val settings: AppleSettings = AppleSettings(),
|
||||||
val metadata: AppleMetadata = AppleMetadata(),
|
val metadata: AppleMetadata = AppleMetadata(),
|
||||||
|
|
||||||
|
val appState: AppStateEntity = AppStateEntity(),
|
||||||
|
val appSettings: AppSettingsEntity = AppSettingsEntity(),
|
||||||
|
|
||||||
val isPremium: Boolean = false,
|
val isPremium: Boolean = false,
|
||||||
val vendorIdHook: Boolean = false,
|
val vendorIdHook: Boolean = false,
|
||||||
val recordings: List<Recording> = emptyList(),
|
val recordings: List<Recording> = emptyList(),
|
||||||
|
|
||||||
|
val heartRateSamples: List<HeartRateSample> = emptyList(),
|
||||||
|
|
||||||
|
val heartRateRange: ClosedRange<Long>? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class AppleViewModel(
|
class AppleViewModel(
|
||||||
private val device: AppleDevice,
|
private val device: AppleDevice,
|
||||||
|
private val appDataRepository: AppDataRepository,
|
||||||
private val recordingRepository: RecordingRepository,
|
private val recordingRepository: RecordingRepository,
|
||||||
|
private val heartRateRepository: HeartRateRepository
|
||||||
) : ViewModel(), DeviceViewModel {
|
) : ViewModel(), DeviceViewModel {
|
||||||
val billingManager = BillingManager
|
val billingManager = BillingManager
|
||||||
|
|
||||||
@@ -57,12 +76,42 @@ class AppleViewModel(
|
|||||||
|
|
||||||
private var attObserveJob: Job? = null
|
private var attObserveJob: Job? = null
|
||||||
|
|
||||||
val uiState = combine(
|
private val _heartRateRange = MutableStateFlow<ClosedRange<Long>?>(null)
|
||||||
|
|
||||||
|
private val heartRateSamples: Flow<List<HeartRateSample>> = _heartRateRange
|
||||||
|
.transformLatest { range ->
|
||||||
|
if (range == null) {
|
||||||
|
emit(emptyList())
|
||||||
|
} else {
|
||||||
|
val startInstant = Instant.fromEpochMilliseconds(range.start)
|
||||||
|
val endInstant = Instant.fromEpochMilliseconds(range.endInclusive)
|
||||||
|
val samples = heartRateRepository.get(startInstant, endInstant)
|
||||||
|
emit(samples)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val deviceDetails = combine(
|
||||||
device.state,
|
device.state,
|
||||||
device.settings,
|
device.settings,
|
||||||
device.metadata,
|
device.metadata
|
||||||
|
) { state, settings, metadata ->
|
||||||
|
Triple(state, settings, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val appData = combine(
|
||||||
|
appDataRepository.state,
|
||||||
|
appDataRepository.settings
|
||||||
|
) { appState, appSettings ->
|
||||||
|
Pair(appState, appSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
val uiState = combine(
|
||||||
|
deviceDetails,
|
||||||
|
appData,
|
||||||
billingManager.provider.isPremium,
|
billingManager.provider.isPremium,
|
||||||
) { state, settings, metadata, isPremium ->
|
_heartRateRange,
|
||||||
|
heartRateSamples
|
||||||
|
) { (state, settings, metadata), (appState, appSettings), isPremium, heartRateRange, heartRateSamples ->
|
||||||
AppleUiState(
|
AppleUiState(
|
||||||
state = state,
|
state = state,
|
||||||
settings = settings,
|
settings = settings,
|
||||||
@@ -71,7 +120,11 @@ class AppleViewModel(
|
|||||||
vendorIdHook = XposedRemotePrefProvider.create().getBoolean(
|
vendorIdHook = XposedRemotePrefProvider.create().getBoolean(
|
||||||
"vendor_id_hook",
|
"vendor_id_hook",
|
||||||
false
|
false
|
||||||
) // TODO: make this a Flow, even if it means polling every few seconds
|
), // TODO: make this a Flow (even if it means polling every few seconds?)
|
||||||
|
heartRateRange = heartRateRange,
|
||||||
|
heartRateSamples = heartRateSamples,
|
||||||
|
appState = appState,
|
||||||
|
appSettings = appSettings
|
||||||
)
|
)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
@@ -133,7 +186,17 @@ class AppleViewModel(
|
|||||||
fun setCustomEqEnabled(enabled: Boolean) = device.setCustomEqEnabled(enabled)
|
fun setCustomEqEnabled(enabled: Boolean) = device.setCustomEqEnabled(enabled)
|
||||||
fun setCustomEq(low: Int, mid: Int, high: Int) = device.setCustomEq(low, mid, high)
|
fun setCustomEq(low: Int, mid: Int, high: Int) = device.setCustomEq(low, mid, high)
|
||||||
|
|
||||||
fun testHeadGestures() = device.testHeadGestures()
|
fun detectHeadGestures(callback: (Boolean) -> Unit) = device.detectHeadGestures(callback)
|
||||||
|
|
||||||
|
fun setHeadGesturesVerticalOffset(offset: Int) = device.setHeadGesturesVerticalOffset(offset)
|
||||||
|
fun setHeadGesturesHorizontalOffset(offset: Int) = device.setHeadGesturesHorizontalOffset(offset)
|
||||||
|
fun setHeadTrackingInterval(interval: Duration) = device.setHeadTrackingInterval(interval)
|
||||||
|
|
||||||
|
fun startHr() = device.startHr()
|
||||||
|
fun stopHr() = device.stopHr()
|
||||||
|
fun setHrRange(range: ClosedRange<Long>) {
|
||||||
|
_heartRateRange.value = range
|
||||||
|
}
|
||||||
|
|
||||||
fun sendRawPacket(data: ByteArray): Boolean = device.sendRawPacket(data)
|
fun sendRawPacket(data: ByteArray): Boolean = device.sendRawPacket(data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class AppleRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveCacheFromState(macAddress: MacAddress, state: AppleState) {
|
suspend fun saveCacheFromState(macAddress: MacAddress, state: AppleState) {
|
||||||
Log.d(TAG, "Saving AppleCache from AppleState for ${macAddress.toRedactedString()}: $state")
|
Log.d(TAG, "Saving AppleCache from AppleState for ${macAddress.toRedactedString()}")
|
||||||
val cache = try {
|
val cache = try {
|
||||||
AppleCache(
|
AppleCache(
|
||||||
capabilities = state.capabilities,
|
capabilities = state.capabilities,
|
||||||
@@ -48,7 +48,7 @@ class AppleRepository(
|
|||||||
customEq = state.customEq
|
customEq = state.customEq
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to create AppleCache from AppleState for ${macAddress.toRedactedString()}: $state", e)
|
Log.e(TAG, "Failed to create AppleCache from AppleState for ${macAddress.toRedactedString()}", e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package me.kavishdevar.librepods.repository
|
||||||
|
|
||||||
|
import me.kavishdevar.librepods.data.heartrate.HeartRateDao
|
||||||
|
import me.kavishdevar.librepods.data.heartrate.HeartRateSample
|
||||||
|
import me.kavishdevar.librepods.database.heartrate.HeartRateSampleEntity
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
class HeartRateRepository(
|
||||||
|
private val dao: HeartRateDao,
|
||||||
|
) {
|
||||||
|
suspend fun insert(sample: HeartRateSample) {
|
||||||
|
dao.insert(sample.toEntity())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(
|
||||||
|
start: Instant,
|
||||||
|
end: Instant,
|
||||||
|
): List<HeartRateSample> = dao.get(start, end).map { it.toSample() }
|
||||||
|
|
||||||
|
private fun HeartRateSample.toEntity() = HeartRateSampleEntity(
|
||||||
|
timestamp = timestamp,
|
||||||
|
bpm = bpm
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun HeartRateSampleEntity.toSample() = HeartRateSample(
|
||||||
|
bpm = bpm,
|
||||||
|
timestamp = timestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,16 +18,21 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.BatteryManager
|
import android.os.BatteryManager
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
|
import android.os.ext.SdkExtensions
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
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 androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.health.connect.client.permission.HealthPermission
|
||||||
|
import androidx.health.connect.client.records.HeartRateRecord
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -42,6 +47,7 @@ import me.kavishdevar.librepods.bluetooth.MacAddress
|
|||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.MagicKeyType
|
import me.kavishdevar.librepods.bluetooth.aacp.types.MagicKeyType
|
||||||
import me.kavishdevar.librepods.bluetooth.verifyRPA
|
import me.kavishdevar.librepods.bluetooth.verifyRPA
|
||||||
|
import me.kavishdevar.librepods.data.heartrate.HeartRateSample
|
||||||
import me.kavishdevar.librepods.database.app.AppSettingsEntity
|
import me.kavishdevar.librepods.database.app.AppSettingsEntity
|
||||||
import me.kavishdevar.librepods.devices.AppleDevice
|
import me.kavishdevar.librepods.devices.AppleDevice
|
||||||
import me.kavishdevar.librepods.devices.AppleSettings
|
import me.kavishdevar.librepods.devices.AppleSettings
|
||||||
@@ -58,7 +64,10 @@ import me.kavishdevar.librepods.presentation.overlays.IslandWindow
|
|||||||
import me.kavishdevar.librepods.presentation.widgets.BatteryWidget
|
import me.kavishdevar.librepods.presentation.widgets.BatteryWidget
|
||||||
import me.kavishdevar.librepods.utils.MediaController
|
import me.kavishdevar.librepods.utils.MediaController
|
||||||
import me.kavishdevar.librepods.utils.redactMac
|
import me.kavishdevar.librepods.utils.redactMac
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlin.time.toJavaInstant
|
||||||
|
|
||||||
private const val TAG = "LibrePodsService"
|
private const val TAG = "LibrePodsService"
|
||||||
|
|
||||||
@@ -94,6 +103,14 @@ class LibrePodsService : Service() {
|
|||||||
(application as LibrePodsApplication).widgetConfigRepository
|
(application as LibrePodsApplication).widgetConfigRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val heartRateRepository by lazy {
|
||||||
|
(application as LibrePodsApplication).heartRateRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
private val healthConnectClient by lazy {
|
||||||
|
(application as LibrePodsApplication).healthConnectClient
|
||||||
|
}
|
||||||
|
|
||||||
private val hasConnectedToAACP by lazy {
|
private val hasConnectedToAACP by lazy {
|
||||||
appDataRepository.state.value.hasConnectedToAACP
|
appDataRepository.state.value.hasConnectedToAACP
|
||||||
}
|
}
|
||||||
@@ -684,6 +701,24 @@ class LibrePodsService : Service() {
|
|||||||
6, 8, 9 -> MediaController.stopSpeaking()
|
6, 8, 9 -> MediaController.stopSpeaking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.currentHeartRate?.timestamp != previousState.currentHeartRate?.timestamp -> {
|
||||||
|
state.currentHeartRate?.let { heartRateSample ->
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"current heart rate changed from device ${device.macAddress.toRedactedString()}"
|
||||||
|
)
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"current heart rate: ${heartRateSample.bpm} bpm, timestamp: ${heartRateSample.timestamp.toJavaInstant()}. processing."
|
||||||
|
)
|
||||||
|
|
||||||
|
processHeartRateSample(
|
||||||
|
heartRateSample = heartRateSample,
|
||||||
|
interval = state.heartRateInterval
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
previousState = state
|
previousState = state
|
||||||
}
|
}
|
||||||
@@ -1066,6 +1101,13 @@ class LibrePodsService : Service() {
|
|||||||
device.disableAudio(this)
|
device.disableAudio(this)
|
||||||
device.disconnectAudio(this)
|
device.disconnectAudio(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not the right place because the stream doesn't switch to the remaining bud when the active one is removed
|
||||||
|
if (device is AppleDevice && device.state.value.hrmActive) {
|
||||||
|
device.updateState {
|
||||||
|
it.copy(hrmActive = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1089,6 +1131,44 @@ class LibrePodsService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun processHeartRateSample(heartRateSample: HeartRateSample, interval: Duration) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
Log.d(TAG, "inserting to local db")
|
||||||
|
heartRateRepository.insert(heartRateSample)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 7) {
|
||||||
|
if (checkSelfPermission(HealthPermission.getWritePermission(HeartRateRecord::class)) != PackageManager.PERMISSION_GRANTED) return
|
||||||
|
val healthConnectHeartRateSample = HeartRateRecord.Sample(
|
||||||
|
time = heartRateSample.timestamp.toJavaInstant(),
|
||||||
|
beatsPerMinute = heartRateSample.bpm.toLong()
|
||||||
|
)
|
||||||
|
|
||||||
|
val zoneOffset = ZoneOffset.systemDefault().rules.getOffset(heartRateSample.timestamp.toJavaInstant())
|
||||||
|
|
||||||
|
val heartRateRecord = HeartRateRecord(
|
||||||
|
startTime = (heartRateSample.timestamp - interval).toJavaInstant(),
|
||||||
|
endTime = heartRateSample.timestamp.toJavaInstant(),
|
||||||
|
startZoneOffset = zoneOffset,
|
||||||
|
endZoneOffset = zoneOffset,
|
||||||
|
samples = listOf(healthConnectHeartRateSample),
|
||||||
|
metadata = androidx.health.connect.client.records.metadata.Metadata.autoRecorded(
|
||||||
|
device = androidx.health.connect.client.records.metadata.Device(type = androidx.health.connect.client.records.metadata.Device.TYPE_HEARABLE)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (healthConnectClient != null) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
healthConnectClient!!.insertRecords(listOf(heartRateRecord))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Health Connect client not available")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "U SDK Extension <7")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Shizuku
|
// TODO: Shizuku
|
||||||
private fun setAppleBluetoothMetadata(
|
private fun setAppleBluetoothMetadata(
|
||||||
@Suppress("unused") device: AppleDevice
|
@Suppress("unused") device: AppleDevice
|
||||||
|
|||||||
@@ -16,20 +16,16 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.kavishdevar.librepods.services.LibrePodsService
|
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
@@ -37,10 +33,10 @@ import kotlin.math.abs
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class GestureDetector(
|
// TODO: rewrite
|
||||||
private val librepodsService: LibrePodsService
|
class GestureDetector {
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "GestureDetector"
|
private const val TAG = "GestureDetector"
|
||||||
|
|
||||||
@@ -54,8 +50,6 @@ class GestureDetector(
|
|||||||
private const val MAX_VALID_ORIENTATION_VALUE = 6000
|
private const val MAX_VALID_ORIENTATION_VALUE = 6000
|
||||||
}
|
}
|
||||||
|
|
||||||
// val audio = GestureFeedback(ServiceManager.getService()?.baseContext!!)
|
|
||||||
|
|
||||||
private val horizontalBuffer = Collections.synchronizedList(ArrayList<Double>())
|
private val horizontalBuffer = Collections.synchronizedList(ArrayList<Double>())
|
||||||
private val verticalBuffer = Collections.synchronizedList(ArrayList<Double>())
|
private val verticalBuffer = Collections.synchronizedList(ArrayList<Double>())
|
||||||
|
|
||||||
@@ -86,7 +80,7 @@ class GestureDetector(
|
|||||||
|
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
private var detectionJob: Job? = null
|
private var detectionJob: Job? = null
|
||||||
private var gestureDetectedCallback: ((Boolean) -> Unit)? = null
|
private var processingJob: Job? = null
|
||||||
|
|
||||||
private var significantMotion = false
|
private var significantMotion = false
|
||||||
private var lastSignificantMotionTime = 0L
|
private var lastSignificantMotionTime = 0L
|
||||||
@@ -96,48 +90,51 @@ class GestureDetector(
|
|||||||
while (verticalAvgBuffer.size < 3) verticalAvgBuffer.add(0.0)
|
while (verticalAvgBuffer.size < 3) verticalAvgBuffer.add(0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDetection(doNotStop: Boolean = false, onGestureDetected: (Boolean) -> Unit) {
|
fun startDetection(acceleration: StateFlow<Acceleration>, callback: (Boolean) -> Unit) {
|
||||||
if (isRunning) return
|
if (isRunning) return
|
||||||
|
|
||||||
Log.d(TAG, "Starting gesture detection...")
|
Log.d(TAG, "Starting gesture detection...")
|
||||||
isRunning = true
|
isRunning = true
|
||||||
gestureDetectedCallback = onGestureDetected
|
|
||||||
|
|
||||||
// Log.d(TAG, "started: ${airPodsService.startHeadTracking()}")
|
|
||||||
|
|
||||||
clearData()
|
clearData()
|
||||||
|
|
||||||
prevHorizontal = 0.0
|
prevHorizontal = 0.0
|
||||||
prevVertical = 0.0
|
prevVertical = 0.0
|
||||||
|
|
||||||
|
processingJob = CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
acceleration.collect {
|
||||||
|
processHeadOrientation(it.horizontal.toInt(), it.vertical.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
detectionJob = CoroutineScope(Dispatchers.Default).launch {
|
detectionJob = CoroutineScope(Dispatchers.Default).launch {
|
||||||
while (isRunning) {
|
while (isRunning) {
|
||||||
delay(50)
|
delay(50.milliseconds)
|
||||||
|
|
||||||
val gesture = detectGestures()
|
val gesture = detectGestures()
|
||||||
if (gesture != null) {
|
if (gesture != null) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
// audio.playConfirmation(gesture)
|
GestureFeedback.playConfirmation(gesture)
|
||||||
|
|
||||||
gestureDetectedCallback?.invoke(gesture)
|
callback.invoke(gesture)
|
||||||
stopDetection(doNotStop)
|
stopDetection()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun stopDetection(doNotStop: Boolean = false) {
|
fun stopDetection() {
|
||||||
if (!isRunning) return
|
if (!isRunning) return
|
||||||
|
|
||||||
Log.d(TAG, "Stopping gesture detection")
|
Log.d(TAG, "Stopping gesture detection")
|
||||||
isRunning = false
|
isRunning = false
|
||||||
|
|
||||||
// if (!doNotStop) airPodsService.stopHeadTracking()
|
|
||||||
|
|
||||||
detectionJob?.cancel()
|
detectionJob?.cancel()
|
||||||
detectionJob = null
|
detectionJob = null
|
||||||
gestureDetectedCallback = null
|
|
||||||
|
processingJob?.cancel()
|
||||||
|
processingJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processHeadOrientation(horizontal: Int, vertical: Int) {
|
fun processHeadOrientation(horizontal: Int, vertical: Int) {
|
||||||
@@ -156,7 +153,7 @@ fun startDetection(doNotStop: Boolean = false, onGestureDetected: (Boolean) -> U
|
|||||||
|
|
||||||
if (significantHorizontal && (!significantVertical || abs(horizontalDelta) > abs(verticalDelta))) {
|
if (significantHorizontal && (!significantVertical || abs(horizontalDelta) > abs(verticalDelta))) {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
// audio.playDirectional(isVertical = false, value = horizontalDelta)
|
GestureFeedback.playDirectional(isVertical = false, value = horizontalDelta)
|
||||||
}
|
}
|
||||||
significantMotion = true
|
significantMotion = true
|
||||||
lastSignificantMotionTime = System.currentTimeMillis()
|
lastSignificantMotionTime = System.currentTimeMillis()
|
||||||
@@ -164,7 +161,7 @@ fun startDetection(doNotStop: Boolean = false, onGestureDetected: (Boolean) -> U
|
|||||||
}
|
}
|
||||||
else if (significantVertical) {
|
else if (significantVertical) {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
// audio.playDirectional(isVertical = true, value = verticalDelta)
|
GestureFeedback.playDirectional(isVertical = true, value = verticalDelta)
|
||||||
}
|
}
|
||||||
significantMotion = true
|
significantMotion = true
|
||||||
lastSignificantMotionTime = System.currentTimeMillis()
|
lastSignificantMotionTime = System.currentTimeMillis()
|
||||||
@@ -363,7 +360,7 @@ fun startDetection(doNotStop: Boolean = false, onGestureDetected: (Boolean) -> U
|
|||||||
|
|
||||||
private fun detectGestures(): Boolean? {
|
private fun detectGestures(): Boolean? {
|
||||||
val requiredExtremes = getRequiredExtremes()
|
val requiredExtremes = getRequiredExtremes()
|
||||||
Log.d(TAG, "Current required extremes: $requiredExtremes")
|
// Log.d(TAG, "Current required extremes: $requiredExtremes")
|
||||||
|
|
||||||
if (verticalPeaks.size + verticalTroughs.size >= requiredExtremes) {
|
if (verticalPeaks.size + verticalTroughs.size >= requiredExtremes) {
|
||||||
val allExtremes = (verticalPeaks + verticalTroughs).sortedBy { it.first }
|
val allExtremes = (verticalPeaks + verticalTroughs).sortedBy { it.first }
|
||||||
|
|||||||
@@ -23,14 +23,12 @@ package me.kavishdevar.librepods.utils
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.SoundPool
|
import android.media.SoundPool
|
||||||
import android.os.Build
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class GestureFeedback(context: Context) {
|
object GestureFeedback {
|
||||||
|
|
||||||
private val TAG = "GestureFeedback"
|
private val TAG = "GestureFeedback"
|
||||||
|
|
||||||
@@ -71,7 +69,7 @@ class GestureFeedback(context: Context) {
|
|||||||
private val RIGHT_VOLUME = Pair(0.0f, 1.0f)
|
private val RIGHT_VOLUME = Pair(0.0f, 1.0f)
|
||||||
private val VERTICAL_VOLUME = Pair(1.0f, 1.0f)
|
private val VERTICAL_VOLUME = Pair(1.0f, 1.0f)
|
||||||
|
|
||||||
init {
|
fun init (context: Context) {
|
||||||
soundId = soundPool.load(context, R.raw.blip_no, 1)
|
soundId = soundPool.load(context, R.raw.blip_no, 1)
|
||||||
confirmYesId = soundPool.load(context, R.raw.confirm_yes, 1)
|
confirmYesId = soundPool.load(context, R.raw.confirm_yes, 1)
|
||||||
confirmNoId = soundPool.load(context, R.raw.confirm_no, 1)
|
confirmNoId = soundPool.load(context, R.raw.confirm_no, 1)
|
||||||
@@ -84,7 +82,6 @@ class GestureFeedback(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
|
||||||
fun playDirectional(isVertical: Boolean, value: Double) {
|
fun playDirectional(isVertical: Boolean, value: Double) {
|
||||||
if (!soundsLoaded.get()) {
|
if (!soundsLoaded.get()) {
|
||||||
Log.d(TAG, "Sounds not yet loaded, skipping playback")
|
Log.d(TAG, "Sounds not yet loaded, skipping playback")
|
||||||
|
|||||||
@@ -20,82 +20,19 @@ package me.kavishdevar.librepods.utils
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
data class Orientation(val pitch: Float = 0f, val yaw: Float = 0f)
|
|
||||||
data class Acceleration(val vertical: Float = 0f, val horizontal: Float = 0f)
|
data class Acceleration(val vertical: Float = 0f, val horizontal: Float = 0f)
|
||||||
|
|
||||||
|
// TODO: remove
|
||||||
object HeadTracking {
|
object HeadTracking {
|
||||||
private val _orientation = MutableStateFlow(Orientation())
|
|
||||||
val orientation = _orientation.asStateFlow()
|
|
||||||
|
|
||||||
private val _acceleration = MutableStateFlow(Acceleration())
|
private val _acceleration = MutableStateFlow(Acceleration())
|
||||||
val acceleration = _acceleration.asStateFlow()
|
val acceleration = _acceleration.asStateFlow()
|
||||||
|
|
||||||
private val calibrationSamples = mutableListOf<Triple<Int, Int, Int>>()
|
fun addAccel(vert: Float, horiz: Float) {
|
||||||
private var isCalibrated = false
|
_acceleration.value = Acceleration(vert, horiz)
|
||||||
private var o1Neutral = 19000
|
|
||||||
private var o2Neutral = 0
|
|
||||||
private var o3Neutral = 0
|
|
||||||
|
|
||||||
private const val CALIBRATION_SAMPLE_COUNT = 10
|
|
||||||
private const val ORIENTATION_OFFSET = 5500
|
|
||||||
|
|
||||||
fun processPacket(packet: ByteArray) {
|
|
||||||
val o1 = bytesToInt(packet[43], packet[44])
|
|
||||||
val o2 = bytesToInt(packet[45], packet[46])
|
|
||||||
val o3 = bytesToInt(packet[47], packet[48])
|
|
||||||
|
|
||||||
val horizontalAccel = bytesToInt(packet[51], packet[52]).toFloat()
|
|
||||||
val verticalAccel = bytesToInt(packet[53], packet[54]).toFloat()
|
|
||||||
|
|
||||||
if (!isCalibrated) {
|
|
||||||
calibrationSamples.add(Triple(o1, o2, o3))
|
|
||||||
if (calibrationSamples.size >= CALIBRATION_SAMPLE_COUNT) {
|
|
||||||
calibrate()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val orientation = calculateOrientation(o1, o2, o3)
|
|
||||||
_orientation.value = orientation
|
|
||||||
|
|
||||||
_acceleration.value = Acceleration(verticalAccel, horizontalAccel)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calibrate() {
|
|
||||||
if (calibrationSamples.size < 3) return
|
|
||||||
|
|
||||||
// Add offset during calibration
|
|
||||||
o1Neutral = calibrationSamples.map { it.first + ORIENTATION_OFFSET }.average().roundToInt()
|
|
||||||
o2Neutral = calibrationSamples.map { it.second + ORIENTATION_OFFSET }.average().roundToInt()
|
|
||||||
o3Neutral = calibrationSamples.map { it.third + ORIENTATION_OFFSET }.average().roundToInt()
|
|
||||||
|
|
||||||
isCalibrated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UnusedVariable")
|
|
||||||
private fun calculateOrientation(o1: Int, o2: Int, o3: Int): Orientation {
|
|
||||||
if (!isCalibrated) return Orientation()
|
|
||||||
|
|
||||||
val o1Norm = (o1 + ORIENTATION_OFFSET) - o1Neutral
|
|
||||||
val o2Norm = (o2 + ORIENTATION_OFFSET) - o2Neutral
|
|
||||||
val o3Norm = (o3 + ORIENTATION_OFFSET) - o3Neutral
|
|
||||||
|
|
||||||
val pitch = (o2Norm + o3Norm) / 2f / 32000f * 180f
|
|
||||||
val yaw = (o2Norm - o3Norm) / 2f / 32000f * 180f
|
|
||||||
|
|
||||||
return Orientation(pitch, yaw)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bytesToInt(b1: Byte, b2: Byte): Int {
|
|
||||||
return (b2.toInt() shl 8) or (b1.toInt() and 0xFF)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
calibrationSamples.clear()
|
|
||||||
isCalibrated = false
|
|
||||||
_orientation.value = Orientation()
|
|
||||||
_acceleration.value = Acceleration()
|
_acceleration.value = Acceleration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import android.util.Log
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
// TODO: fragile play/pause, don't refactor unless needed. userPlayedMedia and iPaused etc. probably should not be here
|
||||||
object MediaController {
|
object MediaController {
|
||||||
private var initialVolume: Int? = null
|
private var initialVolume: Int? = null
|
||||||
private lateinit var audioManager: AudioManager
|
private lateinit var audioManager: AudioManager
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package rtbuddy;
|
||||||
|
|
||||||
|
option java_package = "me.kavishdevar.librepods.bluetooth.aacp.rtbuddy.proto";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
enum SensorServiceType {
|
||||||
|
SENSOR_SERVICE_UNKNOWN = 0;
|
||||||
|
DRVCOMM = 1;
|
||||||
|
TTR = 2;
|
||||||
|
ANALYTICS = 3;
|
||||||
|
ACTIN = 4;
|
||||||
|
KADABRA = 5;
|
||||||
|
MANDO = 6;
|
||||||
|
IED = 7;
|
||||||
|
NEO = 8;
|
||||||
|
NEORELAY = 9;
|
||||||
|
COMM = 10;
|
||||||
|
ACCEL = 11;
|
||||||
|
GYRO = 12;
|
||||||
|
PDR = 13;
|
||||||
|
ACTIVITY = 14;
|
||||||
|
CMA = 15;
|
||||||
|
DEVMOTION6 = 16;
|
||||||
|
SPL0 = 17;
|
||||||
|
HOSTLIBHID = 18;
|
||||||
|
HEARTRATE = 19;
|
||||||
|
HEARTRATEv2 = 20;
|
||||||
|
|
||||||
|
SENSOR_SERVICE_UNKNOWN_82 = 82;
|
||||||
|
HEARTRATE_COMMAND = 84;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SensorDataWX {
|
||||||
|
int32 seq = 1;
|
||||||
|
int32 log_type = 2;
|
||||||
|
bytes another_sensor_stream = 3;
|
||||||
|
RequestAllDescriptors request_all_descriptors = 4;
|
||||||
|
SensorDescriptor sensor_descriptor = 5;
|
||||||
|
|
||||||
|
SensorCommand command = 7;
|
||||||
|
SensorServiceSetting service_settings = 8;
|
||||||
|
SensorTypeAck start_ack = 9;
|
||||||
|
|
||||||
|
SensorTypeAck command_ack = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RequestAllDescriptors {
|
||||||
|
}
|
||||||
|
|
||||||
|
message SensorCommand {
|
||||||
|
SensorServiceType service = 1;
|
||||||
|
bytes payload = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SensorDescriptor {
|
||||||
|
SensorServiceType service = 1;
|
||||||
|
bytes sensor_descriptor = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SensorTypeAck {
|
||||||
|
SensorServiceType service = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SensorServiceSetting {
|
||||||
|
SensorServiceType service = 1;
|
||||||
|
int32 setting = 2;
|
||||||
|
bytes configuration = 3;
|
||||||
|
}
|
||||||
@@ -320,4 +320,24 @@
|
|||||||
<string name="enable_debug_mode">Enable debug mode</string>
|
<string name="enable_debug_mode">Enable debug mode</string>
|
||||||
<string name="ble_report_delay_description">Any value greater than 0 enables batching.</string>
|
<string name="ble_report_delay_description">Any value greater than 0 enables batching.</string>
|
||||||
<string name="ble_report_delay">Report delay (in ms)</string>
|
<string name="ble_report_delay">Report delay (in ms)</string>
|
||||||
|
<string name="interval">Interval</string>
|
||||||
|
<string name="heart_rate_interval_description">Interval between heart rate measurements</string>
|
||||||
|
<string name="no_heart_rate_data">No heart rate data</string>
|
||||||
|
<string name="one_second">1 second</string>
|
||||||
|
<string name="one_minute">1 minute</string>
|
||||||
|
<string name="permission_healthconnect">Write heart rate to Health Connect</string>
|
||||||
|
<string name="permission_description_healthconnect">Allows the app to write heart rate data to Android\'s Health Connect platform.</string>
|
||||||
|
<string name="required_permissions">Required Permissions</string>
|
||||||
|
<string name="bluetooth">Bluetooth</string>
|
||||||
|
<string name="permission_description_bluetooth">Required to communicate with AirPods.</string>
|
||||||
|
<string name="optional_permissions">Optional Permissions</string>
|
||||||
|
<string name="notifications">Notifications</string>
|
||||||
|
<string name="permission_description_notification">Show battery status when connected or available nearby.</string>
|
||||||
|
<string name="permission_description_phone">Respond to phone calls with head gestures.</string>
|
||||||
|
<string name="permission_overlay">Display over other apps</string>
|
||||||
|
<string name="permission_description_overlay">Show popups when AirPods are nearby or audio switches to them.</string>
|
||||||
|
<string name="swipe_anywhere_to_go_back">Swipe anywhere to go back</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="interaction">Interaction</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ plugins {
|
|||||||
alias(libs.plugins.ksp) apply false
|
alias(libs.plugins.ksp) apply false
|
||||||
alias(libs.plugins.room3) apply false
|
alias(libs.plugins.room3) apply false
|
||||||
alias(libs.plugins.android.library) apply false
|
alias(libs.plugins.android.library) apply false
|
||||||
|
alias (libs.plugins.protobuf) apply false
|
||||||
// alias(libs.plugins.hilt) apply false
|
// alias(libs.plugins.hilt) apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,10 @@ navevent = "1.1.2"
|
|||||||
m3 = "1.5.0-alpha23"
|
m3 = "1.5.0-alpha23"
|
||||||
ksp = "2.3.10"
|
ksp = "2.3.10"
|
||||||
room = "3.0.0"
|
room = "3.0.0"
|
||||||
ktxSerialization = "1.11.0"
|
protobufPlugin = "0.10.0"
|
||||||
junit = "4.13.2"
|
protobuf = "4.35.1"
|
||||||
junitVersion = "1.1.5"
|
healthConnect = "1.2.0-alpha05"
|
||||||
espressoCore = "3.5.1"
|
foundationLayout = "1.12.0"
|
||||||
appcompat = "1.6.1"
|
|
||||||
material = "1.10.0"
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||||
@@ -71,11 +69,10 @@ androidx-navigationevent = { module = "androidx.navigationevent:navigationevent"
|
|||||||
androidx-room3-runtime = { group = "androidx.room3", name = "room3-runtime", version.ref = "room" }
|
androidx-room3-runtime = { group = "androidx.room3", name = "room3-runtime", version.ref = "room" }
|
||||||
androidx-room3-compiler = { group = "androidx.room3", name = "room3-compiler", version.ref = "room" }
|
androidx-room3-compiler = { group = "androidx.room3", name = "room3-compiler", version.ref = "room" }
|
||||||
kotlinx-serialization-cbor = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-cbor", version = "ktxSerialization" }
|
kotlinx-serialization-cbor = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-cbor", version = "ktxSerialization" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
protobuf-kotlin = { group = "com.google.protobuf", name = "protobuf-kotlin", version.ref = "protobuf" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
androidx-healthconnect-client = { group = "androidx.health.connect", name = "connect-client", version.ref = "healthConnect" }
|
||||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
androidx-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
|
||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
@@ -86,3 +83,4 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
|||||||
room3 = { id = "androidx.room3", version.ref = "room" }
|
room3 = { id = "androidx.room3", version.ref = "room" }
|
||||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
|
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user