From 5b6d05f51c68973c071ef9bcf64607a341ae0ae3 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 17:28:58 +0530 Subject: [PATCH 01/11] feat: New device icons --- app/src/main/res/drawable/imac_gen2.xml | 25 +++++++++++++ app/src/main/res/drawable/imac_gen3.xml | 28 ++++++++++++++ .../main/res/drawable/macbook_air_gen2.xml | 34 +++++++++++++++++ .../main/res/drawable/macbook_air_gen3.xml | 37 +++++++++++++++++++ app/src/main/res/drawable/macbook_neo.xml | 34 +++++++++++++++++ .../main/res/drawable/macbook_pro_gen2.xml | 30 +++++++++++++++ .../main/res/drawable/macbook_pro_gen3.xml | 33 +++++++++++++++++ app/src/main/res/drawable/macmini_gen1.xml | 21 +++++++++++ app/src/main/res/drawable/macmini_gen3.xml | 18 +++++++++ app/src/main/res/drawable/macpro_gen3.xml | 37 +++++++++++++++++++ app/src/main/res/drawable/macstudio_gen1.xml | 15 ++++++++ 11 files changed, 312 insertions(+) create mode 100644 app/src/main/res/drawable/imac_gen2.xml create mode 100644 app/src/main/res/drawable/imac_gen3.xml create mode 100644 app/src/main/res/drawable/macbook_air_gen2.xml create mode 100644 app/src/main/res/drawable/macbook_air_gen3.xml create mode 100644 app/src/main/res/drawable/macbook_neo.xml create mode 100644 app/src/main/res/drawable/macbook_pro_gen2.xml create mode 100644 app/src/main/res/drawable/macbook_pro_gen3.xml create mode 100644 app/src/main/res/drawable/macmini_gen1.xml create mode 100644 app/src/main/res/drawable/macmini_gen3.xml create mode 100644 app/src/main/res/drawable/macpro_gen3.xml create mode 100644 app/src/main/res/drawable/macstudio_gen1.xml diff --git a/app/src/main/res/drawable/imac_gen2.xml b/app/src/main/res/drawable/imac_gen2.xml new file mode 100644 index 0000000..f9835cb --- /dev/null +++ b/app/src/main/res/drawable/imac_gen2.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/imac_gen3.xml b/app/src/main/res/drawable/imac_gen3.xml new file mode 100644 index 0000000..9dd6101 --- /dev/null +++ b/app/src/main/res/drawable/imac_gen3.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/macbook_air_gen2.xml b/app/src/main/res/drawable/macbook_air_gen2.xml new file mode 100644 index 0000000..33642db --- /dev/null +++ b/app/src/main/res/drawable/macbook_air_gen2.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/macbook_air_gen3.xml b/app/src/main/res/drawable/macbook_air_gen3.xml new file mode 100644 index 0000000..ea0c2d0 --- /dev/null +++ b/app/src/main/res/drawable/macbook_air_gen3.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/macbook_neo.xml b/app/src/main/res/drawable/macbook_neo.xml new file mode 100644 index 0000000..c781de2 --- /dev/null +++ b/app/src/main/res/drawable/macbook_neo.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/macbook_pro_gen2.xml b/app/src/main/res/drawable/macbook_pro_gen2.xml new file mode 100644 index 0000000..f295f39 --- /dev/null +++ b/app/src/main/res/drawable/macbook_pro_gen2.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/macbook_pro_gen3.xml b/app/src/main/res/drawable/macbook_pro_gen3.xml new file mode 100644 index 0000000..fde2572 --- /dev/null +++ b/app/src/main/res/drawable/macbook_pro_gen3.xml @@ -0,0 +1,33 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/macmini_gen1.xml b/app/src/main/res/drawable/macmini_gen1.xml new file mode 100644 index 0000000..9670973 --- /dev/null +++ b/app/src/main/res/drawable/macmini_gen1.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/macmini_gen3.xml b/app/src/main/res/drawable/macmini_gen3.xml new file mode 100644 index 0000000..82ef14a --- /dev/null +++ b/app/src/main/res/drawable/macmini_gen3.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/macpro_gen3.xml b/app/src/main/res/drawable/macpro_gen3.xml new file mode 100644 index 0000000..da7b81c --- /dev/null +++ b/app/src/main/res/drawable/macpro_gen3.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/macstudio_gen1.xml b/app/src/main/res/drawable/macstudio_gen1.xml new file mode 100644 index 0000000..067c267 --- /dev/null +++ b/app/src/main/res/drawable/macstudio_gen1.xml @@ -0,0 +1,15 @@ + + + + + From 7b9144e0ff8c068ccf3998e8fb6785d71e14a04a Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 17:29:24 +0530 Subject: [PATCH 02/11] feat: Mapping new icons with device identifiers --- .../airsync/utils/DeviceIconResolver.kt | 28 +------ .../airsync/utils/DevicePreviewResolver.kt | 17 +--- .../sameerasw/airsync/utils/MacModelMapper.kt | 81 +++++++++++++++++++ 3 files changed, 85 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt diff --git a/app/src/main/java/com/sameerasw/airsync/utils/DeviceIconResolver.kt b/app/src/main/java/com/sameerasw/airsync/utils/DeviceIconResolver.kt index 135925d..8709f52 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/DeviceIconResolver.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/DeviceIconResolver.kt @@ -25,34 +25,12 @@ object DeviceIconResolver { @DrawableRes fun getIconRes(device: ConnectedDevice?): Int { - if (device == null) return R.drawable.ic_laptop_24 - - val name = device.name - val model = device.model - val type = device.deviceType - - return getIconResForName(name, model, type) + return MacModelMapper.getIconRes(device) } @DrawableRes fun getIconResForName(name: String?, model: String?, deviceType: String?): Int { - val hay = buildString { - if (!name.isNullOrBlank()) append(name).append(' ') - if (!model.isNullOrBlank()) append(model).append(' ') - if (!deviceType.isNullOrBlank()) append(deviceType) - }.trim().lowercase() - - // Check top categories by substring - return when { - hay.contains("imac") -> R.drawable.ic_desktop_24 - hay.contains("mac mini") || hay.contains("macmini") -> R.drawable.ic_mac_mini_24 - hay.contains("mac studio") -> R.drawable.ic_mac_studio_24 - // Ensure "mac pro" (desktop) is checked before generic "pro" in MacBook Pro names - hay.contains("mac pro") -> R.drawable.ic_mac_pro_24 - // MacBook covers both Air and Pro; we keep existing laptop icon for both - hay.contains("macbook") -> R.drawable.ic_laptop_24 - else -> R.drawable.ic_laptop_24 - } + return MacModelMapper.getIconRes(name ?: "", model, deviceType) } /** Convenience for places without direct device: read last device once. */ @@ -63,7 +41,7 @@ object DeviceIconResolver { val last = ds.getLastConnectedDevice().first() getIconRes(last) } catch (_: Exception) { - R.drawable.ic_laptop_24 + R.drawable.macbook_air_gen2 } } } diff --git a/app/src/main/java/com/sameerasw/airsync/utils/DevicePreviewResolver.kt b/app/src/main/java/com/sameerasw/airsync/utils/DevicePreviewResolver.kt index 61d618a..81fb4aa 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/DevicePreviewResolver.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/DevicePreviewResolver.kt @@ -18,21 +18,6 @@ object DevicePreviewResolver { @DrawableRes fun getPreviewRes(device: ConnectedDevice?): Int { - if (device == null) return R.drawable.ic_device_macbook - val hay = buildString { - if (!device.name.isNullOrBlank()) append(device.name).append(' ') - device.model?.let { if (it.isNotBlank()) append(it).append(' ') } - device.deviceType?.let { if (it.isNotBlank()) append(it) } - }.trim().lowercase() - - return when { - hay.contains("imac") -> R.drawable.ic_device_imac - hay.contains("mac mini") || hay.contains("macmini") -> R.drawable.ic_device_macmini - hay.contains("mac studio") -> R.drawable.ic_device_macstudio - // Ensure "mac pro" (desktop) before generic "pro" in MacBook Pro names - hay.contains("mac pro") -> R.drawable.ic_device_macpro - hay.contains("macbook") -> R.drawable.ic_device_macbook - else -> R.drawable.ic_device_macbook - } + return MacModelMapper.getPreviewRes(device) } } diff --git a/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt b/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt new file mode 100644 index 0000000..e6c4838 --- /dev/null +++ b/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt @@ -0,0 +1,81 @@ +package com.sameerasw.airsync.utils + +import androidx.annotation.DrawableRes +import com.sameerasw.airsync.R +import com.sameerasw.airsync.domain.model.ConnectedDevice + +object MacModelMapper { + + @DrawableRes + fun getPreviewRes(device: ConnectedDevice?): Int { + if (device == null) return R.drawable.macbook_air_gen2 + return getPreviewRes(device.name, device.model, device.deviceType) + } + + @DrawableRes + fun getIconRes(device: ConnectedDevice?): Int { + if (device == null) return R.drawable.macbook_air_gen2 + return getIconRes(device.name, device.model, device.deviceType) + } + + @DrawableRes + fun getPreviewRes(name: String, model: String?, deviceType: String?): Int { + val modelStr = model?.replace(" ", "") ?: "" + val nameStr = name.replace(" ", "").lowercase() + val typeStr = deviceType?.replace(" ", "")?.lowercase() ?: "" + val hay = "$nameStr$modelStr$typeStr".lowercase() + return resolveDrawable(modelStr, hay) + } + + @DrawableRes + fun getIconRes(name: String, model: String?, deviceType: String?): Int { + // For now, same logic as preview as per user request to use these vectors everywhere + return getPreviewRes(name, model, deviceType) + } + + @DrawableRes + private fun resolveDrawable(model: String, hay: String): Int { + // 1) Explicit Model Matching based on user mapping + return when { + // MacBook Air Gen 3 + isMatch(model, listOf("Mac17,4", "Mac17,3", "Mac16,13", "Mac16,12", "Mac15,13", "Mac15,12", "Mac14,15", "Mac14,2")) -> R.drawable.macbook_air_gen3 + + // MacBook Air Gen 2 + isMatch(model, listOf("MacBookAir10,1", "MacBookAir9,1", "MacBookAir8,2", "MacBookAir8,1")) -> R.drawable.macbook_air_gen2 + + // MacBook Pro Gen 3 + isMatch(model, listOf("Mac17,7", "Mac17,9", "Mac17,6", "Mac17,8", "Mac17,2", "Mac16,1", "Mac16,6", "Mac16,8", "Mac16,7", "Mac16,5", "Mac15,3", "Mac15,6", "Mac15,8", "Mac15,10", "Mac15,7", "Mac15,9", "Mac15,11", "Mac14,5", "Mac14,9", "Mac14,6", "Mac14,10", "MacBookPro18,3", "MacBookPro18,4", "MacBookPro18,1", "MacBookPro18,2")) -> R.drawable.macbook_pro_gen3 + + // MacBook Pro Gen 2 + isMatch(model, listOf("Mac14,7", "MacBookPro17,1", "MacBookPro16,3", "MacBookPro16,2", "MacBookPro16,1", "MacBookPro16,4", "MacBookPro15,4", "MacBookPro15,1", "MacBookPro15,3", "MacBookPro15,2")) -> R.drawable.macbook_pro_gen2 + + // Mac mini Gen 3 + isMatch(model, listOf("Mac16,11", "Mac16,10")) -> R.drawable.macmini_gen3 + + // iMac Gen 3 + isMatch(model, listOf("Mac16,3", "Mac16,2", "Mac15,5", "Mac15,4", "iMac21,1", "iMac21,2")) -> R.drawable.imac_gen3 + + // iMac Gen 2 + isMatch(model, listOf("iMac20,1", "iMac20,2", "iMac19,1", "iMac19,2", "iMacPro1,1")) -> R.drawable.imac_gen2 + + // MacBook Neo + model.contains("Mac17,5", ignoreCase = true) -> R.drawable.macbook_neo + + // 2) Category-based fallbacks if no specific model match + hay.contains("macbookair") -> R.drawable.macbook_air_gen2 + hay.contains("macbookpro") -> R.drawable.macbook_pro_gen3 + hay.contains("macmini") -> R.drawable.macmini_gen1 + hay.contains("imac") -> R.drawable.imac_gen3 + hay.contains("macstudio") -> R.drawable.macstudio_gen1 + hay.contains("macpro") -> R.drawable.macpro_gen3 + hay.contains("macbookneo") -> R.drawable.macbook_neo + + // 3) Final Absolute Fallback + else -> R.drawable.macbook_air_gen2 + } + } + + private fun isMatch(model: String, list: List): Boolean { + return list.any { model.contains(it, ignoreCase = true) } + } +} From 06364004197b701b08fb66785d105bce34da0f20 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 17:39:59 +0530 Subject: [PATCH 03/11] feat: Material you device icons --- app/src/main/java/com/sameerasw/airsync/MainActivity.kt | 8 +++++++- .../ui/components/cards/ConnectionStatusCard.kt | 3 ++- .../ui/components/cards/LastConnectedDeviceCard.kt | 3 ++- .../com/sameerasw/airsync/widget/AirSyncWidgetProvider.kt | 5 +++++ app/src/main/res/values-night-v31/colors_app.xml | 1 + app/src/main/res/values-night/colors_app.xml | 1 + app/src/main/res/values-v31/colors_app.xml | 1 + app/src/main/res/values/colors_app.xml | 1 + 8 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sameerasw/airsync/MainActivity.kt b/app/src/main/java/com/sameerasw/airsync/MainActivity.kt index b9ab9e5..8da1a07 100644 --- a/app/src/main/java/com/sameerasw/airsync/MainActivity.kt +++ b/app/src/main/java/com/sameerasw/airsync/MainActivity.kt @@ -228,7 +228,13 @@ class MainActivity : ComponentActivity() { // Switch to device icon - with null check for OEM device compatibility try { splashIcon.setImageResource(deviceIconRes) - Log.d("MainActivity", "Switched to device icon") + // Apply Material You primary color tint + val colorPrimary = androidx.core.content.ContextCompat.getColor( + this@MainActivity, + R.color.material_primary + ) + splashIcon.imageTintList = android.content.res.ColorStateList.valueOf(colorPrimary) + Log.d("MainActivity", "Switched to device icon with primary tint") // Fade in the new device icon val fadeInIcon = diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt index 8d4f47e..99ea3b0 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt @@ -86,7 +86,8 @@ fun ConnectionStatusCard( contentDescription = "Connected Mac preview", modifier = Modifier .fillMaxWidth(0.75f), - contentScale = ContentScale.Fit + contentScale = ContentScale.Fit, + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(MaterialTheme.colorScheme.primary) ) } diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt index 65e0b58..b21e053 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt @@ -59,7 +59,8 @@ fun LastConnectedDeviceCard( contentDescription = "Connected Mac preview", modifier = Modifier .fillMaxWidth(0.45f), - contentScale = ContentScale.Fit + contentScale = ContentScale.Fit, + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(MaterialTheme.colorScheme.primary) ) } diff --git a/app/src/main/java/com/sameerasw/airsync/widget/AirSyncWidgetProvider.kt b/app/src/main/java/com/sameerasw/airsync/widget/AirSyncWidgetProvider.kt index 4fec2bf..29042e5 100644 --- a/app/src/main/java/com/sameerasw/airsync/widget/AirSyncWidgetProvider.kt +++ b/app/src/main/java/com/sameerasw/airsync/widget/AirSyncWidgetProvider.kt @@ -113,6 +113,11 @@ class AirSyncWidgetProvider : AppWidgetProvider() { // Device image (large preview) and name val previewRes = DevicePreviewResolver.getPreviewRes(lastDevice) views.setImageViewResource(R.id.widget_device_image, previewRes) + + // Apply primary accent tint + val accentColor = androidx.core.content.ContextCompat.getColor(context, R.color.material_primary) + views.setInt(R.id.widget_device_image, "setColorFilter", accentColor) + // Dim the device image when not connected (including while connecting) val alphaFloat = if (isConnected) 1.0f else 0.6f val alphaInt = if (isConnected) 255 else 153 // 0.6 * 255 ≈ 153 diff --git a/app/src/main/res/values-night-v31/colors_app.xml b/app/src/main/res/values-night-v31/colors_app.xml index ec76dbc..ad3a28c 100644 --- a/app/src/main/res/values-night-v31/colors_app.xml +++ b/app/src/main/res/values-night-v31/colors_app.xml @@ -1,4 +1,5 @@ @android:color/system_accent1_700 + @android:color/system_accent1_200 diff --git a/app/src/main/res/values-night/colors_app.xml b/app/src/main/res/values-night/colors_app.xml index b56d03e..5c1830c 100644 --- a/app/src/main/res/values-night/colors_app.xml +++ b/app/src/main/res/values-night/colors_app.xml @@ -3,4 +3,5 @@ @color/widget_background #FFD0BCFF + #FFD0BCFF diff --git a/app/src/main/res/values-v31/colors_app.xml b/app/src/main/res/values-v31/colors_app.xml index 0e27cd0..c60ed9b 100644 --- a/app/src/main/res/values-v31/colors_app.xml +++ b/app/src/main/res/values-v31/colors_app.xml @@ -1,4 +1,5 @@ @android:color/system_accent1_100 + @android:color/system_accent1_600 diff --git a/app/src/main/res/values/colors_app.xml b/app/src/main/res/values/colors_app.xml index c9d438c..b85ff39 100644 --- a/app/src/main/res/values/colors_app.xml +++ b/app/src/main/res/values/colors_app.xml @@ -3,4 +3,5 @@ @color/widget_background #FF6650a4 + #FF6650a4 From 24267466b2b3976f9affeb99a9aac207028c8fdd Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 17:47:22 +0530 Subject: [PATCH 04/11] feat: Revert tile icon to old method --- .../airsync/service/AirSyncTileService.kt | 5 ++-- .../airsync/utils/DeviceIconResolver.kt | 5 ++++ .../sameerasw/airsync/utils/MacModelMapper.kt | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sameerasw/airsync/service/AirSyncTileService.kt b/app/src/main/java/com/sameerasw/airsync/service/AirSyncTileService.kt index d5917d9..e7fe404 100644 --- a/app/src/main/java/com/sameerasw/airsync/service/AirSyncTileService.kt +++ b/app/src/main/java/com/sameerasw/airsync/service/AirSyncTileService.kt @@ -184,7 +184,7 @@ class AirSyncTileService : TileService() { qsTile?.apply { val dynamicIcon = - com.sameerasw.airsync.utils.DeviceIconResolver.getIconRes(lastDevice) + com.sameerasw.airsync.utils.DeviceIconResolver.getTileIconRes(lastDevice) icon = Icon.createWithResource(this@AirSyncTileService, dynamicIcon) if (isConnected && lastDevice != null) { @@ -229,7 +229,8 @@ class AirSyncTileService : TileService() { state = Tile.STATE_INACTIVE label = "AirSync" subtitle = "Error" - val dynamicIcon = com.sameerasw.airsync.utils.DeviceIconResolver.getIconRes(null) + val dynamicIcon = + com.sameerasw.airsync.utils.DeviceIconResolver.getTileIconRes(null) icon = Icon.createWithResource(this@AirSyncTileService, dynamicIcon) updateTile() } diff --git a/app/src/main/java/com/sameerasw/airsync/utils/DeviceIconResolver.kt b/app/src/main/java/com/sameerasw/airsync/utils/DeviceIconResolver.kt index 8709f52..380ae07 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/DeviceIconResolver.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/DeviceIconResolver.kt @@ -28,6 +28,11 @@ object DeviceIconResolver { return MacModelMapper.getIconRes(device) } + @DrawableRes + fun getTileIconRes(device: ConnectedDevice?): Int { + return MacModelMapper.getTileIconRes(device) + } + @DrawableRes fun getIconResForName(name: String?, model: String?, deviceType: String?): Int { return MacModelMapper.getIconRes(name ?: "", model, deviceType) diff --git a/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt b/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt index e6c4838..499f482 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt @@ -18,6 +18,12 @@ object MacModelMapper { return getIconRes(device.name, device.model, device.deviceType) } + @DrawableRes + fun getTileIconRes(device: ConnectedDevice?): Int { + if (device == null) return R.drawable.rounded_laptop_mac_24 + return getTileIconRes(device.name, device.model, device.deviceType) + } + @DrawableRes fun getPreviewRes(name: String, model: String?, deviceType: String?): Int { val modelStr = model?.replace(" ", "") ?: "" @@ -33,6 +39,25 @@ object MacModelMapper { return getPreviewRes(name, model, deviceType) } + @DrawableRes + fun getTileIconRes(name: String, model: String?, deviceType: String?): Int { + val modelStr = model?.replace(" ", "") ?: "" + val nameStr = name.replace(" ", "").lowercase() + val typeStr = deviceType?.replace(" ", "")?.lowercase() ?: "" + val hay = "$nameStr$modelStr$typeStr".lowercase() + + return when { + hay.contains("macbookair") -> R.drawable.rounded_laptop_mac_24 + hay.contains("macbookpro") -> R.drawable.rounded_laptop_mac_24 + hay.contains("macmini") -> R.drawable.ic_mac_mini_24 + hay.contains("imac") -> R.drawable.ic_desktop_24 + hay.contains("macstudio") -> R.drawable.ic_mac_studio_24 + hay.contains("macpro") -> R.drawable.ic_mac_pro_24 + hay.contains("macbookneo") -> R.drawable.rounded_laptop_mac_24 + else -> R.drawable.rounded_laptop_mac_24 + } + } + @DrawableRes private fun resolveDrawable(model: String, hay: String): Int { // 1) Explicit Model Matching based on user mapping From 59f59d64f12a6f4fcbf3603a8045df2860472058 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 18:43:49 +0530 Subject: [PATCH 05/11] feat: Main tab organization --- .../ui/components/SlowlyRotatingAppIcon.kt | 40 +++++++++ .../components/cards/ConnectionStatusCard.kt | 15 +++- .../ui/components/cards/DefaultTabCard.kt | 2 +- .../cards/LastConnectedDeviceCard.kt | 81 +++++++++---------- .../ui/components/cards/MediaPlayerCard.kt | 20 ++--- .../components/cards/RemoteFunctionsCard.kt | 10 +-- .../ui/screens/AirSyncMainScreen.kt | 20 +++-- 7 files changed, 122 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SlowlyRotatingAppIcon.kt diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SlowlyRotatingAppIcon.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SlowlyRotatingAppIcon.kt new file mode 100644 index 0000000..5adf822 --- /dev/null +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SlowlyRotatingAppIcon.kt @@ -0,0 +1,40 @@ +package com.sameerasw.airsync.presentation.ui.components + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import com.sameerasw.airsync.R + +@Composable +fun SlowlyRotatingAppIcon( + modifier: Modifier = Modifier +) { + val infiniteTransition = rememberInfiniteTransition(label = "SlowRotation") + val rotation by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 8000, easing = LinearEasing) + ), + label = "Rotation" + ) + + Image( + painter = painterResource(id = R.drawable.ic_launcher_monochrome), + contentDescription = null, + modifier = modifier + .graphicsLayer { + rotationZ = rotation + }, + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(MaterialTheme.colorScheme.primary) + ) +} diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt index 99ea3b0..924f9c4 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt @@ -3,6 +3,7 @@ package com.sameerasw.airsync.presentation.ui.components.cards import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow @@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -32,6 +34,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.sameerasw.airsync.domain.model.ConnectedDevice import com.sameerasw.airsync.domain.model.UiState +import com.sameerasw.airsync.presentation.ui.components.RotatingAppIcon +import com.sameerasw.airsync.presentation.ui.components.SlowlyRotatingAppIcon import com.sameerasw.airsync.utils.DevicePreviewResolver import com.sameerasw.airsync.utils.HapticUtil @@ -100,8 +104,8 @@ fun ConnectionStatusCard( ) { Text( "${connectedDevice.name}", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.weight(1f) ) @@ -169,7 +173,12 @@ fun ConnectionStatusCard( } if (isConnected) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { LoadingIndicator() } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + SlowlyRotatingAppIcon( + modifier = Modifier + .size(54.dp) + ) + } // Icon( // painter = painterResource(id = com.sameerasw.airsync.R.drawable.rounded_devices_24), // contentDescription = "Connected", diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DefaultTabCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DefaultTabCard.kt index 74141df..386b63f 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DefaultTabCard.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DefaultTabCard.kt @@ -66,7 +66,7 @@ fun DefaultTabCard( ) { TabOption( title = "Connect", - icon = Icons.Filled.Phonelink, + iconRes = R.drawable.ic_launcher_monochrome, isSelected = currentDefaultTab == "connect", onClick = { HapticUtil.performClick(haptics) diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt index b21e053..7f21642 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt @@ -69,11 +69,29 @@ fun LastConnectedDeviceCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text( - "${device.name}", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(bottom = 10.dp) - ) + Column{ + + Text( + "${device.name}", + style = MaterialTheme.typography.headlineSmall + ) + + val lastConnectedTime = remember(device.lastConnected) { + val currentTime = System.currentTimeMillis() + val diffMinutes = (currentTime - device.lastConnected) / (1000 * 60) + when { + diffMinutes < 1 -> "Just now" + diffMinutes < 60 -> "${diffMinutes}m ago" + diffMinutes < 1440 -> "${diffMinutes / 60}h ago" + else -> "${diffMinutes / 1440}d ago" + } + } + Text( + "Last seen $lastConnectedTime", + style = MaterialTheme.typography.bodyMedium + ) + + } // Display status badge - PLUS or FREE Card( @@ -97,46 +115,11 @@ fun LastConnectedDeviceCard( } } - - // Display device model and type if available - device.model?.let { model -> - Text( - "Model: $model", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } // device.deviceType?.let { type -> // Text("Type: $type", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) // } - val lastConnectedTime = remember(device.lastConnected) { - val currentTime = System.currentTimeMillis() - val diffMinutes = (currentTime - device.lastConnected) / (1000 * 60) - when { - diffMinutes < 1 -> "Just now" - diffMinutes < 60 -> "${diffMinutes}m ago" - diffMinutes < 1440 -> "${diffMinutes / 60}h ago" - else -> "${diffMinutes / 1440}d ago" - } - } - Text("Last seen $lastConnectedTime", style = MaterialTheme.typography.bodyMedium) - // Auto-reconnect toggle - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text("Auto reconnect", style = MaterialTheme.typography.bodyMedium) - Switch(checked = isAutoReconnectEnabled, onCheckedChange = { enabled -> - if (enabled) HapticUtil.performToggleOn(haptics) else HapticUtil.performToggleOff( - haptics - ) - onToggleAutoReconnect(enabled) - }) - } Button( onClick = { @@ -151,12 +134,28 @@ fun LastConnectedDeviceCard( Icon( painter = painterResource(id = com.sameerasw.airsync.R.drawable.rounded_sync_desktop_24), contentDescription = "Quick connect", - modifier = Modifier.padding(end = 8.dp), + modifier = Modifier.padding(end = 12.dp), // tint = MaterialTheme.colorScheme.primary ) Text("Quick Connect") } + // Auto-reconnect toggle + Row( + modifier = Modifier + .fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text("Auto reconnect", style = MaterialTheme.typography.bodyMedium) + Switch(checked = isAutoReconnectEnabled, onCheckedChange = { enabled -> + if (enabled) HapticUtil.performToggleOn(haptics) else HapticUtil.performToggleOff( + haptics + ) + onToggleAutoReconnect(enabled) + }) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/MediaPlayerCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/MediaPlayerCard.kt index fe96167..03a6bf5 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/MediaPlayerCard.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/MediaPlayerCard.kt @@ -60,7 +60,9 @@ fun MediaPlayerCard( Card( modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(4.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceBright) + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest + ) ) { Box( modifier = Modifier.fillMaxWidth() @@ -79,7 +81,7 @@ fun MediaPlayerCard( Box( modifier = Modifier .matchParentSize() - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.6f)) + .background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)) ) } Column( @@ -102,15 +104,13 @@ fun MediaPlayerCard( fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = if (albumArtBitmap != null) MaterialTheme.colorScheme.onBackground else MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface ) Text( text = musicInfo?.artist?.takeIf { it.isNotEmpty() } ?: "from your Mac", style = MaterialTheme.typography.titleMedium, - color = if (albumArtBitmap != null) MaterialTheme.colorScheme.onBackground.copy( - alpha = 0.7f - ) else MaterialTheme.colorScheme.onSurfaceVariant, + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -184,7 +184,7 @@ fun MediaPlayerCard( Icon( imageVector = if (isMuted) Icons.AutoMirrored.Filled.VolumeOff else Icons.AutoMirrored.Filled.VolumeUp, contentDescription = "Mute", - tint = if (albumArtBitmap != null) Color.White else MaterialTheme.colorScheme.onSurface + tint = MaterialTheme.colorScheme.primary ) } @@ -194,9 +194,9 @@ fun MediaPlayerCard( valueRange = 0f..100f, modifier = Modifier.weight(1f), colors = SliderDefaults.colors( - thumbColor = if (albumArtBitmap != null) Color.White else MaterialTheme.colorScheme.primary, - activeTrackColor = if (albumArtBitmap != null) Color.White else MaterialTheme.colorScheme.primary, - inactiveTrackColor = if (albumArtBitmap != null) Color.White.copy(alpha = 0.3f) else MaterialTheme.colorScheme.surfaceVariant + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant ) ) } diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/RemoteFunctionsCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/RemoteFunctionsCard.kt index 2fa7b94..bb85d7b 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/RemoteFunctionsCard.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/RemoteFunctionsCard.kt @@ -44,7 +44,7 @@ fun RemoteFunctionsCard( modifier = modifier .fillMaxWidth(), shape = MaterialTheme.shapes.extraSmall, - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceBright) + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHighest) ) { Row( modifier = Modifier @@ -60,7 +60,7 @@ fun RemoteFunctionsCard( onRemoteAction("lock_screen") }, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, + containerColor = MaterialTheme.colorScheme.surfaceBright, contentColor = MaterialTheme.colorScheme.onSecondaryContainer ), contentPadding = PaddingValues(horizontal = 8.dp), @@ -88,7 +88,7 @@ fun RemoteFunctionsCard( onRemoteAction("screensaver") }, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, + containerColor = MaterialTheme.colorScheme.surfaceBright, contentColor = MaterialTheme.colorScheme.onSecondaryContainer ), contentPadding = PaddingValues(horizontal = 8.dp), @@ -116,7 +116,7 @@ fun RemoteFunctionsCard( onRemoteAction("brightness_down") }, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, + containerColor = MaterialTheme.colorScheme.surfaceBright, contentColor = MaterialTheme.colorScheme.onSecondaryContainer ), contentPadding = PaddingValues(0.dp), @@ -138,7 +138,7 @@ fun RemoteFunctionsCard( onRemoteAction("brightness_up") }, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, + containerColor = MaterialTheme.colorScheme.surfaceBright, contentColor = MaterialTheme.colorScheme.onSecondaryContainer ), contentPadding = PaddingValues(0.dp), diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt index ac450ed..e67711a 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt @@ -43,8 +43,12 @@ import androidx.compose.material.icons.filled.Phonelink import androidx.compose.material.icons.filled.QrCodeScanner import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.outlined.Phonelink +import androidx.compose.material.icons.rounded.ContentPaste import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Gamepad import androidx.compose.material.icons.rounded.Keyboard +import androidx.compose.material.icons.rounded.Phonelink +import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.CircularWavyProgressIndicator @@ -648,15 +652,15 @@ fun AirSyncMainScreen( val tabs = remember(uiState.isConnected) { if (uiState.isConnected) { listOf( - AirSyncTab(R.string.tab_connect, Icons.Outlined.Phonelink, 0), - AirSyncTab(R.string.tab_remote, Icons.Filled.Gamepad, 1), - AirSyncTab(R.string.tab_clipboard, Icons.Filled.ContentPaste, 2), - AirSyncTab(R.string.tab_settings, Icons.Filled.Settings, 3) + AirSyncTab(R.string.tab_connect, Icons.Rounded.Phonelink, 0), + AirSyncTab(R.string.tab_remote, Icons.Rounded.Gamepad, 1), + AirSyncTab(R.string.tab_clipboard, Icons.Rounded.ContentPaste, 2), + AirSyncTab(R.string.tab_settings, Icons.Rounded.Settings, 3) ) } else { listOf( - AirSyncTab(R.string.tab_connect, Icons.Filled.Phonelink, 0), - AirSyncTab(R.string.tab_settings, Icons.Filled.Settings, 1) + AirSyncTab(R.string.tab_connect, Icons.Rounded.Phonelink, 0), + AirSyncTab(R.string.tab_settings, Icons.Rounded.Settings, 1) ) } } @@ -751,6 +755,7 @@ fun AirSyncMainScreen( ) } + // Connection Status Card ConnectionStatusCard( isConnected = uiState.isConnected, @@ -772,6 +777,7 @@ fun AirSyncMainScreen( ) } + // Media Player Card AnimatedVisibility( visible = uiState.isConnected, @@ -796,6 +802,8 @@ fun AirSyncMainScreen( } } + + RoundedCardContainer { // Nearby Devices (UDP Discovery) val discoveredDevices by viewModel.discoveredDevices.collectAsState() From c7bcd80aadd9724f07b594d13735e78fab299085 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 19:09:55 +0530 Subject: [PATCH 06/11] feat: Floating player --- .../ui/components/FloatingMediaPlayer.kt | 275 ++++++++++++++++++ .../ui/components/SettingsView.kt | 2 +- .../components/cards/ConnectionStatusCard.kt | 20 +- .../ui/components/cards/MediaPlayerCard.kt | 206 ------------- .../ui/screens/AirSyncMainScreen.kt | 61 ++-- 5 files changed, 317 insertions(+), 247 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt delete mode 100644 app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/MediaPlayerCard.kt diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt new file mode 100644 index 0000000..d3887fd --- /dev/null +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt @@ -0,0 +1,275 @@ +package com.sameerasw.airsync.presentation.ui.components + +import android.graphics.Bitmap +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeOff +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material.icons.rounded.Pause +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material.icons.rounded.SkipNext +import androidx.compose.material.icons.rounded.SkipPrevious +import androidx.compose.material3.ButtonGroup +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.sameerasw.airsync.domain.model.MacMusicInfo + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun FloatingMediaPlayer( + musicInfo: MacMusicInfo?, + albumArtBitmap: Bitmap?, + volume: Float, + isMuted: Boolean, + onVolumeChange: (Float) -> Unit, + onToggleMute: () -> Unit, + onMediaAction: (String) -> Unit, + modifier: Modifier = Modifier +) { + var isExpanded by remember { mutableStateOf(false) } + + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .animateContentSize(), + shape = if (isExpanded) RoundedCornerShape(24.dp) else RoundedCornerShape(64.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest + ) + ) { + Box(modifier = Modifier.fillMaxWidth()) { + // Background Image (Album Art) + if (albumArtBitmap != null) { + Image( + bitmap = albumArtBitmap.asImageBitmap(), + contentDescription = null, + modifier = Modifier + .matchParentSize() + .blur(16.dp), + contentScale = ContentScale.Crop + ) + // Scrim + Box( + modifier = Modifier + .matchParentSize() + .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)) + ) + } + + if (!isExpanded) { + // Mini Player Layout + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Expand Button + IconButton( + onClick = { isExpanded = true }, + modifier = Modifier.size(40.dp) + ) { + Icon( + imageVector = Icons.Rounded.KeyboardArrowUp, + contentDescription = "Expand", + tint = MaterialTheme.colorScheme.onSurface + ) + } + + // Metadata + Column( + modifier = Modifier + .weight(1f) + ) { + Text( + text = musicInfo?.title?.takeIf { it.isNotEmpty() } ?: "Nothing Playing", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = musicInfo?.artist?.takeIf { it.isNotEmpty() } ?: "from your Mac", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + // Play/Pause Button + FilledIconButton( + onClick = { onMediaAction("media_play_pause") }, + ) { + Icon( + imageVector = if (musicInfo?.isPlaying == true) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + contentDescription = if (musicInfo?.isPlaying == true) "Pause" else "Play", + modifier = Modifier.size(48.dp) + ) + } + } + } else { + // Expanded Player Layout + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + // Header with collapse button + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + IconButton(onClick = { isExpanded = false }) { + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = "Collapse", + tint = MaterialTheme.colorScheme.onSurface + ) + } + + // Metadata (Centered) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.weight(1f) + ) { + Text( + text = musicInfo?.title?.takeIf { it.isNotEmpty() } ?: "Nothing Playing", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = musicInfo?.artist?.takeIf { it.isNotEmpty() } ?: "from your Mac", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Spacer(modifier = Modifier.size(48.dp)) // To balance the chevron + } + + // Media Controls + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + ButtonGroup( + modifier = Modifier + .fillMaxWidth() + .height(60.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + content = { + FilledTonalIconButton( + onClick = { onMediaAction("media_prev") }, + modifier = Modifier.weight(0.7f).fillMaxHeight() + ) { + Icon(Icons.Rounded.SkipPrevious, contentDescription = "Previous", modifier = Modifier.size(36.dp)) + } + + FilledIconButton( + onClick = { onMediaAction("media_play_pause") }, + modifier = Modifier.weight(1.5f).fillMaxHeight() + ) { + Icon( + imageVector = if (musicInfo?.isPlaying == true) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + contentDescription = if (musicInfo?.isPlaying == true) "Pause" else "Play", + modifier = Modifier.size(48.dp) + ) + } + + FilledTonalIconButton( + onClick = { onMediaAction("media_next") }, + modifier = Modifier.weight(0.7f).fillMaxHeight() + ) { + Icon(Icons.Rounded.SkipNext, contentDescription = "Next", modifier = Modifier.size(36.dp)) + } + } + ) + } + + // Volume Control + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + IconButton(onClick = onToggleMute) { + Icon( + imageVector = if (isMuted) Icons.AutoMirrored.Filled.VolumeOff else Icons.AutoMirrored.Filled.VolumeUp, + contentDescription = "Mute", + tint = MaterialTheme.colorScheme.primary + ) + } + + Slider( + value = volume, + onValueChange = onVolumeChange, + valueRange = 0f..100f, + modifier = Modifier.weight(1f), + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt index f6651ec..993a4ad 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt @@ -471,7 +471,7 @@ fun SettingsView( onAvatarLongClick = onToggleDeveloperMode ) - Spacer(modifier = Modifier.height(100.dp)) + Spacer(modifier = Modifier.height(180.dp)) } } diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt index 924f9c4..63dbfad 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt @@ -68,16 +68,16 @@ fun ConnectionStatusCard( Column( modifier = Modifier .fillMaxSize() - .background( - brush = Brush.linearGradient( - colors = listOf( - gradientColor.copy(alpha = 0.3f), - Color.Transparent - ), - start = Offset(0f, 1f), - end = Offset.Infinite - ) - ) +// .background( +// brush = Brush.linearGradient( +// colors = listOf( +// gradientColor.copy(alpha = 0.3f), +// Color.Transparent +// ), +// start = Offset(0f, 1f), +// end = Offset.Infinite +// ) +// ) .padding(horizontal = 16.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/MediaPlayerCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/MediaPlayerCard.kt deleted file mode 100644 index 03a6bf5..0000000 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/MediaPlayerCard.kt +++ /dev/null @@ -1,206 +0,0 @@ -package com.sameerasw.airsync.presentation.ui.components.cards - -import android.graphics.Bitmap -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.VolumeOff -import androidx.compose.material.icons.automirrored.filled.VolumeUp -import androidx.compose.material.icons.rounded.Pause -import androidx.compose.material.icons.rounded.PlayArrow -import androidx.compose.material.icons.rounded.SkipNext -import androidx.compose.material.icons.rounded.SkipPrevious -import androidx.compose.material3.ButtonGroup -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledIconButton -import androidx.compose.material3.FilledTonalIconButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.SliderDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.blur -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.sameerasw.airsync.domain.model.MacMusicInfo - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun MediaPlayerCard( - musicInfo: MacMusicInfo?, - albumArtBitmap: Bitmap?, - volume: Float, - isMuted: Boolean, - onVolumeChange: (Float) -> Unit, - onToggleMute: () -> Unit, - onMediaAction: (String) -> Unit, - modifier: Modifier = Modifier -) { - - Card( - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest - ) - ) { - Box( - modifier = Modifier.fillMaxWidth() - ) { - // Background Image (Album Art) - if (albumArtBitmap != null) { - Image( - bitmap = albumArtBitmap.asImageBitmap(), - contentDescription = null, - modifier = Modifier - .matchParentSize() - .blur(8.dp), - contentScale = ContentScale.Crop - ) - // Dark scrim for readability - Box( - modifier = Modifier - .matchParentSize() - .background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)) - ) - } - Column( - modifier = Modifier.padding( - horizontal = 24.dp, - vertical = 32.dp - ), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(32.dp) - ) { - // Metadata - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = musicInfo?.title?.takeIf { it.isNotEmpty() } - ?: "Nothing Playing", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = musicInfo?.artist?.takeIf { it.isNotEmpty() } - ?: "from your Mac", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - ButtonGroup( - modifier = Modifier - .weight(1f) - .height(60.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), - content = { - // Previous Button - FilledTonalIconButton( - onClick = { onMediaAction("media_prev") }, - modifier = Modifier - .weight(0.7f) - .fillMaxHeight(), - ) { - Icon( - Icons.Rounded.SkipPrevious, - contentDescription = "Previous", - modifier = Modifier.size(36.dp) - ) - } - - // Play/Pause Button - FilledIconButton( - onClick = { onMediaAction("media_play_pause") }, - modifier = Modifier - .weight(1.5f) - .fillMaxHeight() - ) { - Icon( - imageVector = if (musicInfo?.isPlaying == true) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, - contentDescription = if (musicInfo?.isPlaying == true) "Pause" else "Play", - modifier = Modifier.size(48.dp) - ) - } - - // Next Button - FilledTonalIconButton( - onClick = { onMediaAction("media_next") }, - modifier = Modifier - .weight(0.7f) - .fillMaxHeight(), - ) { - Icon( - Icons.Rounded.SkipNext, - contentDescription = "Next", - modifier = Modifier.size(36.dp) - ) - } - } - ) - } - - // Volume Control - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - IconButton(onClick = onToggleMute) { - Icon( - imageVector = if (isMuted) Icons.AutoMirrored.Filled.VolumeOff else Icons.AutoMirrored.Filled.VolumeUp, - contentDescription = "Mute", - tint = MaterialTheme.colorScheme.primary - ) - } - - Slider( - value = volume, - onValueChange = onVolumeChange, - valueRange = 0f..100f, - modifier = Modifier.weight(1f), - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - } - } - } - } -} diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt index e67711a..2e5ccd7 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt @@ -100,10 +100,10 @@ import com.sameerasw.airsync.presentation.ui.components.RoundedCardContainer import com.sameerasw.airsync.presentation.ui.components.SettingsView import com.sameerasw.airsync.presentation.ui.modifiers.BlurDirection import com.sameerasw.airsync.presentation.ui.modifiers.progressiveBlur +import com.sameerasw.airsync.presentation.ui.components.FloatingMediaPlayer import com.sameerasw.airsync.presentation.ui.components.cards.ConnectionStatusCard import com.sameerasw.airsync.presentation.ui.components.cards.LastConnectedDeviceCard import com.sameerasw.airsync.presentation.ui.components.cards.ManualConnectionCard -import com.sameerasw.airsync.presentation.ui.components.cards.MediaPlayerCard import com.sameerasw.airsync.presentation.ui.components.cards.RemoteFunctionsCard import com.sameerasw.airsync.presentation.ui.components.cards.RateAppCard import com.sameerasw.airsync.presentation.ui.components.dialogs.ConnectionDialog @@ -696,7 +696,7 @@ fun AirSyncMainScreen( val statusBarHeightPx = with(density) { WindowInsets.statusBars.asPaddingValues().calculateTopPadding().toPx() } - val bottomBlurHeightPx = with(density) { 130.dp.toPx() } + val bottomBlurHeightPx = with(density) { 180.dp.toPx() } Box( modifier = Modifier @@ -776,34 +776,8 @@ fun AirSyncMainScreen( onRemoteAction = { sendRemoteAction(it) } ) } - - - // Media Player Card - AnimatedVisibility( - visible = uiState.isConnected, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - MediaPlayerCard( - musicInfo = macStatus?.music, - albumArtBitmap = albumArtBitmap, - volume = volume, - isMuted = isMuted, - onVolumeChange = { - volume = it - sendRemoteAction("vol_set", it.toInt()) - }, - onToggleMute = { - sendRemoteAction("vol_mute") - isMuted = !isMuted - }, - onMediaAction = { sendRemoteAction(it) } - ) - } } - - RoundedCardContainer { // Nearby Devices (UDP Discovery) val discoveredDevices by viewModel.discoveredDevices.collectAsState() @@ -1046,7 +1020,7 @@ fun AirSyncMainScreen( RemoteControlScreen( modifier = Modifier .fillMaxSize() - .padding(top = statusBarHeight, bottom = 100.dp), + .padding(top = statusBarHeight, bottom = 180.dp), showKeyboard = showKeyboard, onDismissKeyboard = { showKeyboard = false } ) @@ -1091,7 +1065,7 @@ fun AirSyncMainScreen( onHistoryToggle = { viewModel.setClipboardHistoryEnabled(it) }, modifier = Modifier .fillMaxSize() - .padding(top = topSpacing, bottom = 100.dp), + .padding(top = topSpacing, bottom = 180.dp), ) } else { Box(Modifier.fillMaxSize()) @@ -1193,6 +1167,33 @@ fun AirSyncMainScreen( } } ) + + // Floating Media Player + AnimatedVisibility( + visible = uiState.isConnected, + enter = fadeIn() + expandVertically(expandFrom = Alignment.Bottom), + exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 108.dp) // Positioned above the floating toolbar + .zIndex(2f) + ) { + FloatingMediaPlayer( + musicInfo = macStatus?.music, + albumArtBitmap = albumArtBitmap, + volume = volume, + isMuted = isMuted, + onVolumeChange = { + volume = it + sendRemoteAction("vol_set", it.toInt()) + }, + onToggleMute = { + sendRemoteAction("vol_mute") + isMuted = !isMuted + }, + onMediaAction = { sendRemoteAction(it) } + ) + } } } From e681c821a397a84bad4568d6cd3e3e1472f65105 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 19:28:38 +0530 Subject: [PATCH 07/11] feat: Floating player gestrue and landscape toolbar layout --- .../ui/components/AirSyncFloatingToolbar.kt | 8 +- .../ui/components/FloatingMediaPlayer.kt | 92 ++++++- .../components/cards/ConnectionStatusCard.kt | 38 ++- .../ui/screens/AirSyncMainScreen.kt | 254 ++++++++++++------ 4 files changed, 284 insertions(+), 108 deletions(-) diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AirSyncFloatingToolbar.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AirSyncFloatingToolbar.kt index cad76e9..c10fdac 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AirSyncFloatingToolbar.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AirSyncFloatingToolbar.kt @@ -48,10 +48,10 @@ fun AirSyncFloatingToolbar( var expanded by remember { mutableStateOf(true) } HorizontalFloatingToolbar( - modifier = modifier - .windowInsetsPadding( - androidx.compose.foundation.layout.WindowInsets.navigationBars - ), +// modifier = modifier +// .windowInsetsPadding( +// androidx.compose.foundation.layout.WindowInsets.navigationBars +// ), expanded = expanded, floatingActionButton = floatingActionButton, scrollBehavior = scrollBehavior, diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt index d3887fd..95f0d32 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt @@ -55,9 +55,32 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.animation.core.exponentialDecay +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.gestures.animateTo +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.offset +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset import com.sameerasw.airsync.domain.model.MacMusicInfo +import kotlinx.coroutines.launch +import kotlin.math.roundToInt -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +enum class DragValue { Collapsed, Expanded } + +@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class) @Composable fun FloatingMediaPlayer( musicInfo: MacMusicInfo?, @@ -69,19 +92,50 @@ fun FloatingMediaPlayer( onMediaAction: (String) -> Unit, modifier: Modifier = Modifier ) { - var isExpanded by remember { mutableStateOf(false) } + val density = LocalDensity.current + val config = LocalConfiguration.current + val screenHeight = config.screenHeightDp.dp + val scope = rememberCoroutineScope() + + val collapsedHeight = 72.dp + val expandedHeight = 280.dp + + val collapsedPx = with(density) { collapsedHeight.toPx() } + val expandedPx = with(density) { expandedHeight.toPx() } + + val anchoredDraggableState = remember { + AnchoredDraggableState( + initialValue = DragValue.Collapsed, + anchors = DraggableAnchors { + DragValue.Collapsed at 0f + DragValue.Expanded at -(expandedPx - collapsedPx) + }, + positionalThreshold = { distance: Float -> distance * 0.5f }, + velocityThreshold = { with(density) { 100.dp.toPx() } }, + snapAnimationSpec = spring(), + decayAnimationSpec = exponentialDecay() + ) + } + + // Sync state with anchoredDraggableState + val currentOffset = anchoredDraggableState.requireOffset() + val isExpanded = anchoredDraggableState.currentValue == DragValue.Expanded + val progress = if (currentOffset.isNaN()) 0f else { + (currentOffset / -(expandedPx - collapsedPx)).coerceIn(0f, 1f) + } Card( modifier = modifier .fillMaxWidth() .padding(horizontal = 16.dp) - .animateContentSize(), - shape = if (isExpanded) RoundedCornerShape(24.dp) else RoundedCornerShape(64.dp), + .height(collapsedHeight + (expandedHeight - collapsedHeight) * progress) + .anchoredDraggable(anchoredDraggableState, Orientation.Vertical), + shape = RoundedCornerShape(lerp(64f, 24f, progress).dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceContainerHighest ) ) { - Box(modifier = Modifier.fillMaxWidth()) { + Box(modifier = Modifier.fillMaxSize()) { // Background Image (Album Art) if (albumArtBitmap != null) { Image( @@ -100,18 +154,23 @@ fun FloatingMediaPlayer( ) } - if (!isExpanded) { - // Mini Player Layout + // Adaptive content based on progress + if (progress < 0.5f) { + // Mini Player Layout (Fading out) Row( modifier = Modifier .fillMaxWidth() - .padding(8.dp), + .height(collapsedHeight) + .padding(8.dp) + .graphicsLayer(alpha = 1f - progress * 2), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) ) { // Expand Button IconButton( - onClick = { isExpanded = true }, + onClick = { + scope.launch { anchoredDraggableState.animateTo(DragValue.Expanded) } + }, modifier = Modifier.size(40.dp) ) { Icon( @@ -155,11 +214,12 @@ fun FloatingMediaPlayer( } } } else { - // Expanded Player Layout + // Expanded Player Layout (Fading in) Column( modifier = Modifier .fillMaxWidth() - .padding(24.dp), + .padding(24.dp) + .graphicsLayer(alpha = (progress - 0.5f) * 2), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(24.dp) ) { @@ -169,7 +229,9 @@ fun FloatingMediaPlayer( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - IconButton(onClick = { isExpanded = false }) { + IconButton(onClick = { + scope.launch { anchoredDraggableState.animateTo(DragValue.Collapsed) } + }) { Icon( imageVector = Icons.Rounded.KeyboardArrowDown, contentDescription = "Collapse", @@ -242,6 +304,8 @@ fun FloatingMediaPlayer( ) } + Spacer(modifier = Modifier.weight(1f)) + // Volume Control Row( modifier = Modifier.fillMaxWidth(), @@ -273,3 +337,7 @@ fun FloatingMediaPlayer( } } } + +fun lerp(start: Float, stop: Float, fraction: Float): Float { + return (1 - fraction) * start + fraction * stop +} diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt index 63dbfad..50b01b4 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt @@ -7,13 +7,18 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -31,7 +36,9 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.sameerasw.airsync.R import com.sameerasw.airsync.domain.model.ConnectedDevice import com.sameerasw.airsync.domain.model.UiState import com.sameerasw.airsync.presentation.ui.components.RotatingAppIcon @@ -202,11 +209,32 @@ fun ConnectionStatusCard( ) if (isConnected) { - OutlinedButton(onClick = { - HapticUtil.performClick(haptics) - onDisconnect() - }) { - Text("Disconnect") + + // Screensaver Button + Button( + onClick = { + HapticUtil.performClick(haptics) + onDisconnect() + }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceBright, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ), + contentPadding = PaddingValues(horizontal = 8.dp), + modifier = Modifier + .height(48.dp) + ) { + Icon( + painter = painterResource(id = com.sameerasw.airsync.R.drawable.rounded_devices_off_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.size(6.dp)) + Text( + text = "Disconnect", + style = MaterialTheme.typography.labelLarge, + maxLines = 1 + ) } } } diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt index 2e5ccd7..d932e29 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt @@ -7,9 +7,11 @@ import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandHorizontally import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable @@ -23,6 +25,7 @@ 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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -681,7 +684,12 @@ fun AirSyncMainScreen( modifier = Modifier.fillMaxSize(), containerColor = MaterialTheme.colorScheme.surfaceContainer, ) { innerPadding -> + val density = androidx.compose.ui.platform.LocalDensity.current + val configuration = androidx.compose.ui.platform.LocalConfiguration.current + val isLandscape = configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE + val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + val statusBarHeightPx = with(density) { statusBarHeight.toPx() } val topSpacing = (statusBarHeight - 24.dp).coerceAtLeast(0.dp) // Track page changes for haptic feedback on swipe @@ -692,11 +700,9 @@ fun AirSyncMainScreen( } // Blur heights - val density = androidx.compose.ui.platform.LocalDensity.current - val statusBarHeightPx = with(density) { - WindowInsets.statusBars.asPaddingValues().calculateTopPadding().toPx() + val bottomBlurHeightPx = with(density) { + if (isLandscape) 100.dp.toPx() else 180.dp.toPx() } - val bottomBlurHeightPx = with(density) { 180.dp.toPx() } Box( modifier = Modifier @@ -1020,7 +1026,7 @@ fun AirSyncMainScreen( RemoteControlScreen( modifier = Modifier .fillMaxSize() - .padding(top = statusBarHeight, bottom = 180.dp), + .padding(top = statusBarHeight, bottom = if (isLandscape) 100.dp else 180.dp), showKeyboard = showKeyboard, onDismissKeyboard = { showKeyboard = false } ) @@ -1065,7 +1071,7 @@ fun AirSyncMainScreen( onHistoryToggle = { viewModel.setClipboardHistoryEnabled(it) }, modifier = Modifier .fillMaxSize() - .padding(top = topSpacing, bottom = 180.dp), + .padding(top = topSpacing, bottom = if (isLandscape) 100.dp else 180.dp), ) } else { Box(Modifier.fillMaxSize()) @@ -1098,101 +1104,136 @@ fun AirSyncMainScreen( } } - AirSyncFloatingToolbar( + // Adaptive Bottom Bars Container + Box( modifier = Modifier .align(Alignment.BottomCenter) - // .offset(y = -ScreenOffset) - .zIndex(1f), - currentPage = pagerState.currentPage, - tabs = tabs, - onTabSelected = { index -> - scope.launch { - val distance = kotlin.math.abs(index - pagerState.currentPage) - if (distance == 1) { - pagerState.animateScrollToPage(index) - } else { - pagerState.scrollToPage(index) + .fillMaxWidth() + .padding(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) +// .padding(bottom = 16.dp) + .zIndex(2f) + ) { + if (isLandscape) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.Bottom + ) { + AnimatedVisibility( + visible = uiState.isConnected, + enter = fadeIn() + expandHorizontally(), + exit = fadeOut() + shrinkHorizontally(), + modifier = Modifier.weight(1f) + ) { + FloatingMediaPlayer( + musicInfo = macStatus?.music, + albumArtBitmap = albumArtBitmap, + volume = volume, + isMuted = isMuted, + onVolumeChange = { + volume = it + sendRemoteAction("vol_set", it.toInt()) + }, + onToggleMute = { + sendRemoteAction("vol_mute") + isMuted = !isMuted + }, + onMediaAction = { sendRemoteAction(it) } + ) } - } - }, - floatingActionButton = @Composable { - val currentTab = tabs.getOrNull(pagerState.currentPage) - FloatingToolbarDefaults.StandardFloatingActionButton( - onClick = { - HapticUtil.performClick(haptics) - val titleStr = currentTab?.let { context.getString(it.title) } - when (titleStr) { - "Remote" -> { - showKeyboard = !showKeyboard - } - - "Clipboard" -> { - viewModel.clearClipboardHistory() - } - else -> { // Connect or Settings - if (uiState.isConnected) { - disconnect() + AirSyncFloatingToolbar( + modifier = Modifier.zIndex(1f), + currentPage = pagerState.currentPage, + tabs = tabs, + onTabSelected = { index -> + scope.launch { + val distance = kotlin.math.abs(index - pagerState.currentPage) + if (distance == 1) { + pagerState.animateScrollToPage(index) } else { - launchScanner(context) + pagerState.scrollToPage(index) } } + }, + floatingActionButton = { + MainFAB( + currentTab = tabs.getOrNull(pagerState.currentPage), + isConnected = uiState.isConnected, + onAction = { action -> + when (action) { + "keyboard" -> showKeyboard = !showKeyboard + "clear_history" -> viewModel.clearClipboardHistory() + "disconnect" -> disconnect() + "scan" -> launchScanner(context) + } + } + ) } - } + ) + } + } else { + // Portrait: Stacked + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp) ) { - val titleStr = currentTab?.let { context.getString(it.title) } - when (titleStr) { - "Remote" -> { - Icon(Icons.Rounded.Keyboard, contentDescription = "Keyboard") - } - - "Clipboard" -> { - Icon(Icons.Rounded.Delete, contentDescription = "Clear History") - } + AnimatedVisibility( + visible = uiState.isConnected, + enter = fadeIn() + expandVertically(expandFrom = Alignment.Bottom), + exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom), + ) { + FloatingMediaPlayer( + musicInfo = macStatus?.music, + albumArtBitmap = albumArtBitmap, + volume = volume, + isMuted = isMuted, + onVolumeChange = { + volume = it + sendRemoteAction("vol_set", it.toInt()) + }, + onToggleMute = { + sendRemoteAction("vol_mute") + isMuted = !isMuted + }, + onMediaAction = { sendRemoteAction(it) } + ) + } - else -> { // Connect or Settings - if (uiState.isConnected) { - Icon( - imageVector = Icons.Filled.LinkOff, - contentDescription = "Disconnect" - ) - } else { - Icon( - imageVector = Icons.Filled.QrCodeScanner, - contentDescription = "Scan QR" - ) + AirSyncFloatingToolbar( + modifier = Modifier.zIndex(1f), + currentPage = pagerState.currentPage, + tabs = tabs, + onTabSelected = { index -> + scope.launch { + val distance = kotlin.math.abs(index - pagerState.currentPage) + if (distance == 1) { + pagerState.animateScrollToPage(index) + } else { + pagerState.scrollToPage(index) + } } + }, + floatingActionButton = { + MainFAB( + currentTab = tabs.getOrNull(pagerState.currentPage), + isConnected = uiState.isConnected, + onAction = { action -> + when (action) { + "keyboard" -> showKeyboard = !showKeyboard + "clear_history" -> viewModel.clearClipboardHistory() + "disconnect" -> disconnect() + "scan" -> launchScanner(context) + } + } + ) } - } + ) } } - ) - - // Floating Media Player - AnimatedVisibility( - visible = uiState.isConnected, - enter = fadeIn() + expandVertically(expandFrom = Alignment.Bottom), - exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom), - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = 108.dp) // Positioned above the floating toolbar - .zIndex(2f) - ) { - FloatingMediaPlayer( - musicInfo = macStatus?.music, - albumArtBitmap = albumArtBitmap, - volume = volume, - isMuted = isMuted, - onVolumeChange = { - volume = it - sendRemoteAction("vol_set", it.toInt()) - }, - onToggleMute = { - sendRemoteAction("vol_mute") - isMuted = !isMuted - }, - onMediaAction = { sendRemoteAction(it) } - ) } } } @@ -1239,3 +1280,42 @@ fun AirSyncMainScreen( } } } + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun MainFAB( + currentTab: AirSyncTab?, + isConnected: Boolean, + onAction: (String) -> Unit +) { + val haptics = LocalHapticFeedback.current + + FloatingToolbarDefaults.StandardFloatingActionButton( + onClick = { + HapticUtil.performClick(haptics) + when (currentTab?.title) { + R.string.tab_remote -> onAction("keyboard") + R.string.tab_clipboard -> onAction("clear_history") + else -> { + if (isConnected) onAction("disconnect") else onAction("scan") + } + } + } + ) { + when (currentTab?.title) { + R.string.tab_remote -> { + Icon(Icons.Rounded.Keyboard, contentDescription = "Keyboard") + } + R.string.tab_clipboard -> { + Icon(Icons.Rounded.Delete, contentDescription = "Clear History") + } + else -> { + if (isConnected) { + Icon(imageVector = Icons.Filled.LinkOff, contentDescription = "Disconnect") + } else { + Icon(imageVector = Icons.Filled.QrCodeScanner, contentDescription = "Scan QR") + } + } + } + } +} From de4f67f98ee9c92a7440d2eccdc9fb3f18de3cd9 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 19:32:53 +0530 Subject: [PATCH 08/11] feat: PPlayer bar haptics --- .../presentation/ui/components/FloatingMediaPlayer.kt | 9 +++++++++ .../ui/components/cards/ConnectionStatusCard.kt | 2 -- .../airsync/presentation/ui/screens/ClipboardScreen.kt | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt index 95f0d32..8f390c4 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt @@ -69,12 +69,15 @@ import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.IntOffset import com.sameerasw.airsync.domain.model.MacMusicInfo +import com.sameerasw.airsync.utils.HapticUtil import kotlinx.coroutines.launch import kotlin.math.roundToInt @@ -96,6 +99,7 @@ fun FloatingMediaPlayer( val config = LocalConfiguration.current val screenHeight = config.screenHeightDp.dp val scope = rememberCoroutineScope() + val haptics = LocalHapticFeedback.current val collapsedHeight = 72.dp val expandedHeight = 280.dp @@ -117,6 +121,11 @@ fun FloatingMediaPlayer( ) } + // Trigger haptic feedback on expansion state change + LaunchedEffect(anchoredDraggableState.currentValue) { + HapticUtil.performLightTick(haptics) + } + // Sync state with anchoredDraggableState val currentOffset = anchoredDraggableState.requireOffset() val isExpanded = anchoredDraggableState.currentValue == DragValue.Expanded diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt index 50b01b4..54f03e3 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt @@ -210,7 +210,6 @@ fun ConnectionStatusCard( if (isConnected) { - // Screensaver Button Button( onClick = { HapticUtil.performClick(haptics) @@ -220,7 +219,6 @@ fun ConnectionStatusCard( containerColor = MaterialTheme.colorScheme.surfaceBright, contentColor = MaterialTheme.colorScheme.onSecondaryContainer ), - contentPadding = PaddingValues(horizontal = 8.dp), modifier = Modifier .height(48.dp) ) { diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/ClipboardScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/ClipboardScreen.kt index 598ddc2..c158c64 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/ClipboardScreen.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/ClipboardScreen.kt @@ -310,7 +310,7 @@ fun ClipboardScreen( Row( modifier = Modifier .fillMaxWidth() - .padding(8.dp), + .padding(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { From 24baabb20dd8d5a4c33cc9d91e5f9848cf57c21b Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 21:02:01 +0530 Subject: [PATCH 09/11] fix: Media volume level --- .../airsync/presentation/ui/screens/AirSyncMainScreen.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt index d932e29..06d5409 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt @@ -181,6 +181,14 @@ fun AirSyncMainScreen( val macStatus by MacDeviceStatusManager.macDeviceStatus.collectAsState() val albumArtBitmap by MacDeviceStatusManager.albumArt.collectAsState() + // Sync volume and mute state with Mac status updates + LaunchedEffect(macStatus?.music) { + macStatus?.music?.let { music -> + volume = music.volume.toFloat() + isMuted = music.isMuted + } + } + // Volume updates from Mac DisposableEffect(Unit) { val callback = { newVolume: Int -> From a8744350f70fc732d1848053ba5619fede504b2d Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 21:03:17 +0530 Subject: [PATCH 10/11] version: Up build --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3977bbd..45ed1bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { applicationId = "com.sameerasw.airsync" minSdk = 30 targetSdk = 36 - versionCode = 25 + versionCode = 26 versionName = "3.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From 3e9941e934a07e11f1b3c9ce415a4eac9f85181c Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sat, 14 Mar 2026 21:05:49 +0530 Subject: [PATCH 11/11] feat: Toasts only if app active --- .../java/com/sameerasw/airsync/AirSyncApp.kt | 25 ++++++++++++++++++ .../airsync/utils/WebSocketMessageHandler.kt | 14 +++++----- .../sameerasw/airsync/utils/WebSocketUtil.kt | 26 +++++++++++-------- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt b/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt index 4843db3..085c026 100644 --- a/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt +++ b/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt @@ -1,17 +1,42 @@ package com.sameerasw.airsync +import android.app.Activity import android.app.Application +import android.os.Bundle import com.sameerasw.airsync.data.local.DataStoreManager import io.sentry.android.core.SentryAndroid import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking class AirSyncApp : Application() { + private var activityCount = 0 + + companion object { + private var instance: AirSyncApp? = null + fun isAppForeground(): Boolean = instance?.isForeground() ?: false + } + override fun onCreate() { super.onCreate() + instance = this initSentry() + registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + override fun onActivityStarted(activity: Activity) { + activityCount++ + } + override fun onActivityResumed(activity: Activity) {} + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) { + activityCount-- + } + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + }) } + private fun isForeground(): Boolean = activityCount > 0 + private fun initSentry() { val dataStoreManager = DataStoreManager.getInstance(this) val isEnabled = runBlocking { dataStoreManager.getSentryReportingEnabled().first() } diff --git a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt index 4b59577..12dca51 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt @@ -523,12 +523,14 @@ object WebSocketMessageHandler { // Version compatibility check val minVersion = BuildConfig.MIN_MAC_APP_VERSION if (isVersionOutdated(macVersion, minVersion)) { - launch(Dispatchers.Main) { - Toast.makeText( - context, - "Mac app is outdated ($macVersion < $minVersion). Please update the mac app and reconnect.", - Toast.LENGTH_LONG - ).show() + if (com.sameerasw.airsync.AirSyncApp.isAppForeground()) { + launch(Dispatchers.Main) { + Toast.makeText( + context, + "Mac app is outdated ($macVersion < $minVersion). Please update the mac app and reconnect.", + Toast.LENGTH_LONG + ).show() + } } } diff --git a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketUtil.kt index c28394e..1e59e68 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketUtil.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketUtil.kt @@ -347,9 +347,11 @@ object WebSocketUtil { ) { if (webSocket == WebSocketUtil.webSocket) { if (code != 1000) { - CoroutineScope(Dispatchers.Main).launch { - val msg = reason.ifEmpty { "Unknown Server Disconnect" } - android.widget.Toast.makeText(context, "Disconnected: $msg", android.widget.Toast.LENGTH_SHORT).show() + if (com.sameerasw.airsync.AirSyncApp.isAppForeground()) { + CoroutineScope(Dispatchers.Main).launch { + val msg = reason.ifEmpty { "Unknown Server Disconnect" } + android.widget.Toast.makeText(context, "Disconnected: $msg", android.widget.Toast.LENGTH_SHORT).show() + } } } isConnected.set(false) @@ -391,15 +393,17 @@ object WebSocketUtil { if (wasActive || isFinalManualAttempt) { if (manualAttempt || isSocketOpen.get()) { - CoroutineScope(Dispatchers.Main).launch { - val msg = when (t) { - is java.net.ConnectException -> "Connection Refused (Is AirSync Mac running?)" - is java.net.SocketTimeoutException -> "Could not discover your mac" - is java.net.UnknownHostException -> "Could not reach your mac" - is java.io.EOFException, is java.net.SocketException -> "Lost connection to your mac" - else -> t.message ?: "Unknown connection error" + if (com.sameerasw.airsync.AirSyncApp.isAppForeground()) { + CoroutineScope(Dispatchers.Main).launch { + val msg = when (t) { + is java.net.ConnectException -> "Connection Refused (Is AirSync Mac running?)" + is java.net.SocketTimeoutException -> "Could not discover your mac" + is java.net.UnknownHostException -> "Could not reach your mac" + is java.io.EOFException, is java.net.SocketException -> "Lost connection to your mac" + else -> t.message ?: "Unknown connection error" + } + android.widget.Toast.makeText(context, "AirSync: $msg", android.widget.Toast.LENGTH_LONG).show() } - android.widget.Toast.makeText(context, "AirSync: $msg", android.widget.Toast.LENGTH_LONG).show() } } isConnected.set(false)