From 2b32321b277313e7f6861e892c70e154c3e1afef Mon Sep 17 00:00:00 2001 From: uc <73456732+ohuc@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:26:08 +0530 Subject: [PATCH 1/2] checkpoint: mirror uncommitted working-copy state (M3 Expressive redesign WIP) Snapshot of the main checkout's uncommitted changes so the head-gesture rework in the next commit is a clean, cherry-pickable delta on top of the exact state the app is currently built from. Not authored in this branch. Co-Authored-By: Claude Fable 5 --- android/app/build.gradle.kts | 2 + android/app/src/main/AndroidManifest.xml | 1 - .../me/kavishdevar/librepods/MainActivity.kt | 459 ++----- .../librepods/QuickSettingsDialogActivity.kt | 60 +- .../presentation/components/AboutCard.kt | 236 ++-- .../presentation/components/AppInfoCard.kt | 221 ++-- .../presentation/components/AudioSettings.kt | 166 ++- .../components/BatteryIndicator.kt | 70 +- .../presentation/components/BatteryView.kt | 16 +- .../components/CallControlSettings.kt | 112 +- .../components/ConfirmationDialog.kt | 170 +-- .../components/ConnectionSettings.kt | 58 +- .../components/ControlCenterButton.kt | 106 -- ...ontrolCenterNoiseControlSegmentedButton.kt | 92 +- .../presentation/components/DeviceInfoCard.kt | 271 ++--- .../components/ExpressiveModifiers.kt | 57 + .../components/HearingHealthSettings.kt | 86 +- .../presentation/components/IconMappings.kt | 63 + .../components/MicrophoneSettings.kt | 348 +++--- .../components/NavigationButton.kt | 179 ++- .../components/NoiseControlSettings.kt | 319 ++--- .../components/PressAndHoldSettings.kt | 90 +- .../components/StyledBottomSheet.kt | 52 +- .../presentation/components/StyledButton.kt | 323 +---- .../presentation/components/StyledDropdown.kt | 97 +- .../components/StyledIconButton.kt | 290 +---- .../components/StyledInputField.kt | 149 ++- .../presentation/components/StyledScaffold.kt | 119 +- .../components/StyledSelectList.kt | 209 ++-- .../presentation/components/StyledSlider.kt | 639 ++-------- .../presentation/components/StyledSwitch.kt | 306 +---- .../presentation/components/StyledToggle.kt | 213 +--- .../components/VerticalVolumeSlider.kt | 6 +- .../presentation/m3/AppDestinations.kt | 70 ++ .../librepods/presentation/m3/AppHaptics.kt | 64 + .../librepods/presentation/m3/AppNavBar.kt | 164 +++ .../librepods/presentation/m3/AppShell.kt | 210 ++++ .../presentation/m3/ExpressiveControls.kt | 158 +++ .../presentation/m3/ExpressiveGroup.kt | 77 ++ .../presentation/m3/ExpressiveInteractions.kt | 51 + .../presentation/m3/ExpressiveListItem.kt | 230 ++++ .../presentation/m3/ExpressiveNoiseControl.kt | 342 ++++++ .../presentation/m3/ExpressiveProgress.kt | 147 +++ .../m3/ExpressiveReconnectHero.kt | 231 ++++ .../presentation/m3/ExpressiveScaffold.kt | 115 ++ .../presentation/m3/ExpressiveSearch.kt | 185 +++ .../presentation/m3/ExpressiveSlider.kt | 85 ++ .../presentation/m3/screens/ControlsScreen.kt | 307 +++++ .../presentation/m3/screens/HomeScreen.kt | 262 ++++ .../presentation/m3/screens/SettingsScreen.kt | 137 +++ .../librepods/presentation/m3/viz/AncViz.kt | 305 +++++ .../presentation/m3/viz/AncVizPrefs.kt | 51 + .../presentation/overlays/IslandWindow.kt | 758 ------------ .../presentation/overlays/PopupWindow.kt | 290 ----- .../screens/AccessibilitySettingsScreen.kt | 724 ++++------- .../screens/AdaptiveStrengthScreen.kt | 122 +- .../screens/AirPodsSettingsScreen.kt | 554 --------- .../presentation/screens/AppSettingsScreen.kt | 1077 ++++++++--------- .../screens/CameraControlScreen.kt | 98 +- .../presentation/screens/DebugScreen.kt | 282 ++--- .../screens/HeadTrackingScreen.kt | 525 ++++---- .../screens/HearingAidAdjustmentsScreen.kt | 235 ++-- .../presentation/screens/HearingAidScreen.kt | 320 ++--- .../screens/HearingProtectionScreen.kt | 112 +- .../screens/OpenSourceLicensesScreen.kt | 69 +- .../screens/PressAndHoldSettingsScreen.kt | 254 ++-- .../presentation/screens/RenameScreen.kt | 78 +- .../screens/TransparencySettingsScreen.kt | 526 ++++---- .../screens/TroubleshootingScreen.kt | 1029 ++++++---------- .../screens/UpdateHearingTestScreen.kt | 394 +++--- .../presentation/screens/VersionInfoScreen.kt | 173 +-- .../librepods/presentation/theme/Color.kt | 164 ++- .../librepods/presentation/theme/Shape.kt | 36 + .../librepods/presentation/theme/Theme.kt | 37 +- .../librepods/presentation/theme/Type.kt | 86 +- .../viewmodel/AppSettingsViewModel.kt | 15 +- .../librepods/services/AirPodsService.kt | 221 +--- .../librepods/utils/GestureDetector.kt | 59 +- .../res/drawable/progress_bar_background.xml | 2 +- .../main/res/drawable/widget_background.xml | 2 +- .../widget_button_checked_shape_end.xml | 11 +- .../widget_button_checked_shape_middle.xml | 11 +- .../widget_button_checked_shape_start.xml | 11 +- .../res/drawable/widget_button_shape_end.xml | 8 +- .../drawable/widget_button_shape_middle.xml | 8 +- .../drawable/widget_button_shape_start.xml | 8 +- android/app/src/main/res/font/montserrat.ttf | Bin 0 -> 744936 bytes android/app/src/main/res/font/roboto_flex.ttf | Bin 0 -> 1775480 bytes .../src/main/res/layout/battery_widget.xml | 27 +- .../app/src/main/res/layout/island_window.xml | 124 -- .../main/res/layout/noise_control_widget.xml | 67 +- .../app/src/main/res/layout/popup_window.xml | 109 -- .../app/src/main/res/values-night/colors.xml | 15 + android/app/src/main/res/values/colors.xml | 19 + android/gradle/libs.versions.toml | 6 +- android/wear/build.gradle.kts | 22 +- android/wear/src/main/AndroidManifest.xml | 37 + .../wear/WatchDataListenerService.kt | 40 +- .../librepods/wear/widget/LibrePodsWidget.kt | 290 +++++ .../wear/widget/WidgetActionReceiver.kt | 74 ++ android/wear/src/main/res/values/strings.xml | 2 + .../res/xml/librepods_anc_widget_info.xml | 18 + 102 files changed, 8485 insertions(+), 9440 deletions(-) delete mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterButton.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ExpressiveModifiers.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/components/IconMappings.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppDestinations.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppHaptics.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppNavBar.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppShell.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveControls.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveGroup.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveInteractions.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveListItem.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveNoiseControl.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveProgress.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveReconnectHero.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveScaffold.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveSearch.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveSlider.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/ControlsScreen.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/HomeScreen.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/SettingsScreen.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/viz/AncViz.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/viz/AncVizPrefs.kt delete mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/IslandWindow.kt delete mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/PopupWindow.kt delete mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Shape.kt create mode 100644 android/app/src/main/res/font/montserrat.ttf create mode 100644 android/app/src/main/res/font/roboto_flex.ttf delete mode 100644 android/app/src/main/res/layout/island_window.xml delete mode 100644 android/app/src/main/res/layout/popup_window.xml create mode 100644 android/wear/src/main/java/me/kavishdevar/librepods/wear/widget/LibrePodsWidget.kt create mode 100644 android/wear/src/main/java/me/kavishdevar/librepods/wear/widget/WidgetActionReceiver.kt create mode 100644 android/wear/src/main/res/xml/librepods_anc_widget_info.xml diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 8b1fd3b4b..c1da71cb2 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -134,6 +134,8 @@ dependencies { implementation(libs.androidx.dynamicanimation) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.material.icons.core) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.graphics.shapes) debugImplementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.foundation.layout) implementation(libs.aboutlibraries) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d87434108..1a17c63ec 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,7 +13,6 @@ - diff --git a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt index 49f28615e..028893abf 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt @@ -41,9 +41,11 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -68,6 +70,9 @@ 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.CheckCircle +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Phone import androidx.compose.material3.Button @@ -75,8 +80,12 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -124,7 +133,6 @@ import me.kavishdevar.librepods.presentation.components.DeviceInfoCard import me.kavishdevar.librepods.presentation.components.StyledIconButton import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen -import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen import me.kavishdevar.librepods.presentation.screens.AppSettingsScreen import me.kavishdevar.librepods.presentation.screens.CameraControlScreen import me.kavishdevar.librepods.presentation.screens.DebugScreen @@ -139,6 +147,7 @@ import me.kavishdevar.librepods.presentation.screens.TransparencySettingsScreen import me.kavishdevar.librepods.presentation.screens.TroubleshootingScreen import me.kavishdevar.librepods.presentation.screens.UpdateHearingTestScreen import me.kavishdevar.librepods.presentation.screens.VersionScreen +import me.kavishdevar.librepods.presentation.m3.AppShell import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel @@ -216,83 +225,53 @@ fun Main() { val context = LocalContext.current val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) if (!isSupported(sharedPreferences) && !XposedState.bluetoothScopeEnabled) { - val hazeState = rememberHazeState() - val backdrop = rememberLayerBackdrop() - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - val scrollState = rememberScrollState() Box( modifier = Modifier .fillMaxSize() - .hazeSource(hazeState) - .layerBackdrop(backdrop) - .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)), + .background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center ) { Column( modifier = Modifier .padding(horizontal = 16.dp) .verticalScroll(scrollState), - verticalArrangement = Arrangement - .spacedBy(16.dp) + verticalArrangement = Arrangement.spacedBy(16.dp) ) { Spacer(modifier = Modifier.height(48.dp)) - Column( - modifier = Modifier, - verticalArrangement = Arrangement - .spacedBy(16.dp) - ) { - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.not_supported), - style = TextStyle( - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.SemiBold, - color = textColor, - fontSize = 28.sp, - textAlign = TextAlign.Center - ), - modifier = Modifier.fillMaxWidth() - ) + Text( + text = stringResource(R.string.not_supported), + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) - Box( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .clip(RoundedCornerShape(28.dp)) - ) { - Text( - text = stringResource(R.string.check_the_repository_for_more_info), - style = TextStyle( - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Medium, - color = if (isDarkTheme) Color.White else Color.Black, - fontSize = 16.sp - ), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 16.dp) - ) - } - Spacer(modifier = Modifier.height(4.dp)) + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = MaterialTheme.shapes.large, + modifier = Modifier.fillMaxWidth() + ) { Text( - text = stringResource(R.string.enable_app_in_xposed_or_update_device), - style = TextStyle( - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Light, - color = if (isDarkTheme) Color.White else Color.Black, - fontSize = 14.sp - ), + text = stringResource(R.string.check_the_repository_for_more_info), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 16.dp) ) - DeviceInfoCard() - AppInfoCard() } + Text( + text = stringResource(R.string.enable_app_in_xposed_or_update_device), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) + DeviceInfoCard() + AppInfoCard() Spacer(modifier = Modifier.height(48.dp)) } } @@ -301,14 +280,6 @@ fun Main() { val isConnected = remember { mutableStateOf(false) } - var canDrawOverlays by remember { mutableStateOf(Settings.canDrawOverlays(context)) } - val overlaySkipped = remember { - mutableStateOf( - context.getSharedPreferences("settings", MODE_PRIVATE) - .getBoolean("overlay_permission_skipped", false) - ) - } - val bluetoothPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { listOf( "android.permission.BLUETOOTH_CONNECT", @@ -348,138 +319,10 @@ fun Main() { } } - LaunchedEffect(Unit) { - canDrawOverlays = Settings.canDrawOverlays(context) - } - - if (permissionState.allPermissionsGranted && (canDrawOverlays || overlaySkipped.value)) { - - val navController = rememberNavController() - - Box( - modifier = Modifier.fillMaxSize() - ) { - val backButtonBackdrop = rememberLayerBackdrop() - Box( - modifier = Modifier - .fillMaxSize() - .background(if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7)) - .layerBackdrop(backButtonBackdrop) - ) { - NavHost( - navController = navController, - startDestination = "settings", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, animationSpec = tween(durationMillis = 300) - ) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { -it / 4 }, animationSpec = tween(durationMillis = 300) - ) - }, - popEnterTransition = { - slideInHorizontally( - initialOffsetX = { -it / 4 }, - animationSpec = tween(durationMillis = 300) - ) - }, - popExitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, animationSpec = tween(durationMillis = 300) - ) - }) { - composable("settings") { - if (airPodsViewModel != null) AirPodsSettingsScreen(airPodsViewModel, navController) - } - composable("debug") { - DebugScreen(navController = navController) - } - composable("long_press/{bud}") { navBackStackEntry -> - if (airPodsViewModel != null) LongPress( - viewModel = airPodsViewModel, - name = navBackStackEntry.arguments?.getString("bud")!!, - navController = navController - ) - } - composable("rename") { - if (airPodsViewModel != null) RenameScreen(airPodsViewModel) - } - composable("app_settings") { - val appSettingsViewModel: AppSettingsViewModel = viewModel() - AppSettingsScreen(navController, appSettingsViewModel) - } - composable("troubleshooting") { - TroubleshootingScreen(navController) - } - composable("head_tracking") { - if (airPodsViewModel != null) HeadTrackingScreen(airPodsViewModel, navController) - } - composable("accessibility") { - if (airPodsViewModel != null) AccessibilitySettingsScreen(airPodsViewModel, navController) - } - composable("transparency_customization") { - if (airPodsViewModel != null) TransparencySettingsScreen(airPodsViewModel) - } - composable("hearing_aid") { - if (airPodsViewModel != null) HearingAidScreen(airPodsViewModel, navController) - } - composable("hearing_aid_adjustments") { - if (airPodsViewModel != null) HearingAidAdjustmentsScreen(airPodsViewModel) - } - composable("adaptive_strength") { - if (airPodsViewModel != null) AdaptiveStrengthScreen(airPodsViewModel, navController) - } - composable("camera_control") { - if (airPodsViewModel != null) CameraControlScreen(airPodsViewModel) - } - composable("open_source_licenses") { - OpenSourceLicensesScreen(navController) - } - composable("update_hearing_test") { - if (airPodsViewModel != null) UpdateHearingTestScreen(airPodsViewModel) - } - composable("version_info") { - if (airPodsViewModel != null) VersionScreen(airPodsViewModel) - } - composable("hearing_protection") { - if (airPodsViewModel != null) HearingProtectionScreen(airPodsViewModel, navController) - } - } - } - - val showBackButton = remember { mutableStateOf(false) } + if (permissionState.allPermissionsGranted) { - LaunchedEffect(navController) { - navController.addOnDestinationChangedListener { _, destination, _ -> - showBackButton.value = - destination.route != "settings" // && destination.route != "onboarding" - } - } - - AnimatedVisibility( - visible = showBackButton.value, - enter = fadeIn(animationSpec = tween()) + scaleIn( - initialScale = 0f, - animationSpec = tween() - ), - exit = fadeOut(animationSpec = tween()) + scaleOut( - targetScale = 0.5f, - animationSpec = tween(100) - ), - modifier = Modifier - .align(Alignment.TopStart) - .padding( - start = 8.dp, top = (LocalWindowInfo.current.containerSize.width * 0.05f).dp - ) - ) { - StyledIconButton( - onClick = { navController.popBackStack() }, - icon = "􀯶", - backdrop = backButtonBackdrop - ) - } + if (airPodsViewModel != null) { + AppShell(airPodsViewModel) } context.startForegroundService(Intent(context, AirPodsService::class.java)) @@ -514,24 +357,20 @@ fun Main() { } } else { PermissionsScreen( - permissionState = permissionState, - canDrawOverlays = canDrawOverlays, - onOverlaySettingsReturn = { canDrawOverlays = Settings.canDrawOverlays(context) }) + permissionState = permissionState + ) } } @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @Composable fun PermissionsScreen( - permissionState: MultiplePermissionsState, - canDrawOverlays: Boolean, - onOverlaySettingsReturn: () -> Unit + permissionState: MultiplePermissionsState ) { val context = LocalContext.current - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White - val textColor = if (isDarkTheme) Color.White else Color.Black - val accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5) + val backgroundColor = MaterialTheme.colorScheme.surfaceContainer + val textColor = MaterialTheme.colorScheme.onSurface + val accentColor = MaterialTheme.colorScheme.primary val scrollState = rememberScrollState() @@ -547,7 +386,7 @@ fun PermissionsScreen( Column( modifier = Modifier .fillMaxSize() - .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)) + .background(MaterialTheme.colorScheme.background) .padding(16.dp) .verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -556,62 +395,41 @@ fun PermissionsScreen( .fillMaxWidth() .height(180.dp), contentAlignment = Alignment.Center ) { - Text( - text = "\uDBC2\uDEB7", style = TextStyle( - fontSize = 48.sp, - fontWeight = FontWeight.Bold, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor, - textAlign = TextAlign.Center - ) - ) - Canvas( + Box( modifier = Modifier .size(120.dp) .scale(pulseScale) + .clip(RoundedCornerShape(40.dp)) + .background(accentColor.copy(alpha = 0.15f)), + contentAlignment = Alignment.Center ) { - val radius = size.minDimension / 2.2f - val centerX = size.width / 2 - val centerY = size.height / 2 - - rotate(degrees = 45f) { - drawCircle( - color = accentColor.copy(alpha = 0.1f), - radius = radius * 1.3f, - center = Offset(centerX, centerY) - ) - - drawCircle( - color = accentColor.copy(alpha = 0.2f), - radius = radius * 1.1f, - center = Offset(centerX, centerY) - ) - } + Icon( + imageVector = Icons.Filled.Lock, + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(56.dp) + ) } } Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Permission Required", style = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor, - textAlign = TextAlign.Center - ), modifier = Modifier.fillMaxWidth() + text = "Permission Required", + style = MaterialTheme.typography.headlineSmall, + color = textColor, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(R.string.permissions_required), style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor.copy(alpha = 0.7f), - textAlign = TextAlign.Center - ), modifier = Modifier.fillMaxWidth() + text = stringResource(R.string.permissions_required), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(32.dp)) @@ -652,102 +470,21 @@ fun PermissionsScreen( accentColor = accentColor ) - PermissionCard( - title = "Display Over Other Apps", - description = "For popup animations when AirPods connect", - icon = ImageVector.vectorResource(id = R.drawable.ic_layers), - isGranted = canDrawOverlays, - backgroundColor = backgroundColor, - textColor = textColor, - accentColor = accentColor - ) - Spacer(modifier = Modifier.height(24.dp)) Button( onClick = { permissionState.launchMultiplePermissionRequest() }, modifier = Modifier .fillMaxWidth() - .height(55.dp), - colors = ButtonDefaults.buttonColors( - containerColor = accentColor - ), - shape = RoundedCornerShape(8.dp) + .height(56.dp), + shape = MaterialTheme.shapes.large ) { Text( "Ask for regular permissions", - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = Color.White - ), - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - - Button( - onClick = { - val intent = Intent( - Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - "package:${context.packageName}".toUri() - ) - context.startActivity(intent) - onOverlaySettingsReturn() - }, - modifier = Modifier - .fillMaxWidth() - .height(55.dp), - colors = ButtonDefaults.buttonColors( - containerColor = if (canDrawOverlays) Color.Gray else accentColor - ), - enabled = !canDrawOverlays, - shape = RoundedCornerShape(8.dp) - ) { - Text( - if (canDrawOverlays) "Overlay Permission Granted" else "Grant Overlay Permission", - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = Color.White - ), + style = MaterialTheme.typography.labelLarge ) } - if (!canDrawOverlays && basicPermissionsGranted) { - Spacer(modifier = Modifier.height(12.dp)) - - Button( - onClick = { - context.getSharedPreferences("settings", MODE_PRIVATE).edit { - putBoolean("overlay_permission_skipped", true) - } - - val intent = Intent(context, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - context.startActivity(intent) - }, - modifier = Modifier - .fillMaxWidth() - .height(55.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color(0xFF757575) - ), - shape = RoundedCornerShape(8.dp) - ) { - Text( - "Continue without overlay", - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = Color.White - ), - ) - } - } } } @@ -761,14 +498,12 @@ fun PermissionCard( textColor: Color, accentColor: Color ) { - Card( + Surface( modifier = Modifier .fillMaxWidth() .padding(vertical = 6.dp), - colors = CardDefaults.cardColors( - containerColor = backgroundColor - ), - shape = RoundedCornerShape(12.dp) + color = backgroundColor, + shape = MaterialTheme.shapes.large ) { Row( modifier = Modifier @@ -778,18 +513,17 @@ fun PermissionCard( ) { Box( modifier = Modifier - .size(40.dp) - .clip(RoundedCornerShape(8.dp)) + .size(44.dp) + .clip(RoundedCornerShape(14.dp)) .background( - if (isGranted) accentColor.copy(alpha = 0.15f) else Color.Gray.copy( - alpha = 0.15f - ) + if (isGranted) accentColor.copy(alpha = 0.15f) + else MaterialTheme.colorScheme.surfaceContainerHighest ), contentAlignment = Alignment.Center ) { Icon( imageVector = icon, contentDescription = title, - tint = if (isGranted) accentColor else Color.Gray, + tint = if (isGranted) accentColor else MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp) ) } @@ -800,37 +534,24 @@ fun PermissionCard( .padding(start = 16.dp) ) { Text( - text = title, style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor - ) + text = title, + style = MaterialTheme.typography.bodyLarge, + color = textColor ) Text( - text = description, style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor.copy(alpha = 0.6f) - ) + text = description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } - Box( - modifier = Modifier - .size(24.dp) - .clip(RoundedCornerShape(12.dp)) - .background(if (isGranted) Color(0xFF4CAF50) else Color.Gray), - contentAlignment = Alignment.Center - ) { - Text( - text = if (isGranted) "✓" else "!", style = TextStyle( - fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Color.White - ) - ) - } + Icon( + imageVector = if (isGranted) Icons.Filled.CheckCircle else Icons.Filled.Error, + contentDescription = null, + tint = if (isGranted) accentColor else MaterialTheme.colorScheme.outline, + modifier = Modifier.size(24.dp) + ) } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt index 2aa9158c3..9db8b329b 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt @@ -61,6 +61,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -241,7 +243,7 @@ fun DraggableDismissBox( Box( modifier = Modifier .fillMaxSize() - .background(Color.Black.copy(alpha = 0.5f * backgroundAlpha)) + .background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.5f * backgroundAlpha)) .pointerInput(Unit) { detectVerticalDragGestures( onDragStart = { isDragging = true }, @@ -305,7 +307,7 @@ fun NewControlCenterDialogContent( ) { val context = LocalContext.current val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val textColor = Color.White + val colorScheme = MaterialTheme.colorScheme var currentAncMode by remember { mutableStateOf(NoiseControlMode.TRANSPARENCY) } var isConvAwarenessEnabled by remember { mutableStateOf(false) } @@ -419,17 +421,26 @@ fun NewControlCenterDialogContent( if (service != null) { Spacer(modifier = Modifier.weight(1f)) - Column( + Surface( modifier = Modifier .weight(2f) .fillMaxWidth(), + shape = MaterialTheme.shapes.extraLarge, + color = colorScheme.surfaceContainerHigh, + tonalElevation = 3.dp, + shadowElevation = 6.dp + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Icon( painter = painterResource(id = R.drawable.airpods), contentDescription = "Device Icon", - tint = textColor.copy(alpha = 0.8f), + tint = colorScheme.onSurfaceVariant, modifier = Modifier.size(48.dp) ) @@ -437,8 +448,8 @@ fun NewControlCenterDialogContent( Text( text = deviceName, - color = textColor, - fontSize = 16.sp, + color = colorScheme.onSurface, + style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium ) @@ -469,13 +480,23 @@ fun NewControlCenterDialogContent( .padding(vertical = 8.dp) ) } + } Spacer(modifier = Modifier.weight(1f)) + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 72.dp), + shape = MaterialTheme.shapes.extraLarge, + color = colorScheme.surfaceContainerHigh, + tonalElevation = 3.dp, + shadowElevation = 6.dp + ) { Box( modifier = Modifier .fillMaxWidth() - .padding(bottom = 72.dp) + .padding(vertical = 20.dp) .animateContentSize( animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, @@ -525,7 +546,7 @@ fun NewControlCenterDialogContent( .clip(CircleShape) .background( brush = noiseControlButtonBrush ?: - Brush.linearGradient(colors = listOf(Color(0xFF0A84FF), Color(0xFF0A84FF))) + Brush.linearGradient(colors = listOf(colorScheme.primary, colorScheme.primary)) ) .clickable( onClick = { onNoiseControlExpandedChange(true) }, @@ -537,7 +558,7 @@ fun NewControlCenterDialogContent( Icon( painter = painterResource(id = getModeIconRes(currentAncMode)), contentDescription = getModeLabel(currentAncMode), - tint = Color.White, + tint = colorScheme.onPrimary, modifier = Modifier.size(32.dp) ) } @@ -546,8 +567,8 @@ fun NewControlCenterDialogContent( Text( text = getModeLabel(currentAncMode), - color = Color.White, - fontSize = 12.sp, + color = colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Medium, textAlign = androidx.compose.ui.text.style.TextAlign.Center ) @@ -567,8 +588,8 @@ fun NewControlCenterDialogContent( .background( Brush.linearGradient( colors = listOf( - if (isConvAwarenessEnabled) Color(0xFF0A84FF) else Color(0x593C3C3E), - if (isConvAwarenessEnabled) Color(0xFF0A84FF) else Color(0x593C3C3E) + if (isConvAwarenessEnabled) colorScheme.primary else colorScheme.surfaceContainerHighest, + if (isConvAwarenessEnabled) colorScheme.primary else colorScheme.surfaceContainerHighest ) ) ) @@ -589,7 +610,7 @@ fun NewControlCenterDialogContent( Icon( painter = painterResource(id = R.drawable.airpods), contentDescription = "Conversational Awareness", - tint = Color.White, + tint = if (isConvAwarenessEnabled) colorScheme.onPrimary else colorScheme.onSurfaceVariant, modifier = Modifier.size(32.dp) ) } @@ -598,8 +619,8 @@ fun NewControlCenterDialogContent( Text( text = "Conversational\nAwareness", - color = Color.White, - fontSize = 12.sp, + color = colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Medium, textAlign = androidx.compose.ui.text.style.TextAlign.Center, lineHeight = 14.sp @@ -609,11 +630,16 @@ fun NewControlCenterDialogContent( } } } + } } else { Spacer(modifier = Modifier.weight(1f)) Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - Text("Loading...", color = textColor) + Text( + "Loading...", + color = colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyLarge + ) } Spacer(modifier = Modifier.weight(1f)) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AboutCard.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AboutCard.kt index f0669bada..7c267bfe6 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AboutCard.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AboutCard.kt @@ -23,32 +23,30 @@ package me.kavishdevar.librepods.presentation.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -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.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity 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.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController import me.kavishdevar.librepods.R import kotlin.io.encoding.ExperimentalEncodingApi @@ -61,146 +59,134 @@ fun AboutCard( serialNumbers: List, version: String? ) { - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - Box( modifier = Modifier - .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)) - .padding(horizontal = 16.dp, vertical = 4.dp) - ){ + .background(MaterialTheme.colorScheme.background) + .padding(start = 20.dp, end = 16.dp, top = 8.dp, bottom = 6.dp) + ) { Text( text = stringResource(R.string.about), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } val rowHeight = remember { mutableStateOf(0.dp) } val density = LocalDensity.current - Column( - modifier = Modifier - .clip(RoundedCornerShape(28.dp)) - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .padding(top = 2.dp) + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier.fillMaxWidth(), ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .onGloballyPositioned { coordinates -> - rowHeight.value = with(density) { coordinates.size.height.toDp() } - }, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.model_name), - style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp) + .onGloballyPositioned { coordinates -> + rowHeight.value = with(density) { coordinates.size.height.toDp() } + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.model_name), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, ) - ) - Text( - text = modelName, - style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) + Text( + text = modelName, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) + } + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 16.dp) ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier - .padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.model_name), - style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.model_name), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, ) - ) - Text( - text = actualModel, - style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) + Text( + text = actualModel, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) + } + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 16.dp) ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier - .padding(horizontal = 12.dp) - ) - val serialNumbers = listOf( - serialNumbers[0], - "􀀛 ${serialNumbers[1]}", - "􀀧 ${serialNumbers[2]}" - ) - val serialNumber = remember { mutableIntStateOf(0) } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.serial_number), - style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), + val serials = listOf( + serialNumbers[0] to null, + serialNumbers[1] to "􀀛", + serialNumbers[2] to "􀀧", ) - Text( - text = serialNumbers[serialNumber.intValue], - style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), + val serialNumber = remember { mutableIntStateOf(0) } + Row( modifier = Modifier - .clickable( + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.serial_number), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { - serialNumber.intValue = (serialNumber.intValue + 1) % serialNumbers.size + serialNumber.intValue = (serialNumber.intValue + 1) % serials.size } + ) { + val (serialText, glyph) = serials[serialNumber.intValue] + val icon = glyph?.let { sfSymbolToIcon(it) } + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(6.dp)) + } + Text( + text = serialText, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 16.dp) + ) + NavigationButton( + to = "version_info", + navController = navController, + name = stringResource(R.string.version), + currentState = version, + independent = false, + height = rowHeight.value + 32.dp ) } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier - .padding(horizontal = 12.dp) - ) - NavigationButton( - to = "version_info", - navController = navController, - name = stringResource(R.string.version), - currentState = version, - independent = false, - height = rowHeight.value + 32.dp - ) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AppInfoCard.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AppInfoCard.kt index bc4d08712..b9114733f 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AppInfoCard.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AppInfoCard.kt @@ -18,32 +18,23 @@ package me.kavishdevar.librepods.presentation.components -import androidx.compose.foundation.background -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.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity 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.unit.dp -import androidx.compose.ui.unit.sp import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.R @@ -51,142 +42,108 @@ import me.kavishdevar.librepods.R fun AppInfoCard() { val rowHeight = remember { mutableStateOf(0.dp) } val density = LocalDensity.current - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color.Black Column { - Box( - modifier = Modifier - .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)) - .padding(start = 16.dp, bottom = 8.dp, end = 4.dp) - ) { - Text( - text = stringResource(R.string.about), style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } + Text( + text = stringResource(R.string.about), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 8.dp, bottom = 6.dp) + ) - Column( - modifier = Modifier - .clip(RoundedCornerShape(28.dp)) - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth() ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .onGloballyPositioned { coordinates -> - rowHeight.value = with(density) { coordinates.size.height.toDp() } - }, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.version), style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp) + .onGloballyPositioned { coordinates -> + rowHeight.value = with(density) { coordinates.size.height.toDp() } + }, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.version), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, ) - ) - Text( - text = BuildConfig.VERSION_NAME, style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.8f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) + Text( + text = BuildConfig.VERSION_NAME, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) + } + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 20.dp) ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.version_code), style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.version_code), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, ) - ) - Text( - text = BuildConfig.VERSION_CODE.toString(), style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.8f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) + Text( + text = BuildConfig.VERSION_CODE.toString(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) + } + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 20.dp) ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.flavor), style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.flavor), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, ) - ) - Text( - text = BuildConfig.FLAVOR, style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.8f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) + Text( + text = BuildConfig.FLAVOR, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) + } + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 20.dp) ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.build_type), style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.build_type), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, ) - ) - Text( - text = BuildConfig.BUILD_TYPE, - style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.8f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) + Text( + text = BuildConfig.BUILD_TYPE, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - ) + } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AudioSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AudioSettings.kt index c7836d05c..957491744 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AudioSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AudioSettings.kt @@ -20,27 +20,18 @@ package me.kavishdevar.librepods.presentation.components -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color 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.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import me.kavishdevar.librepods.R @@ -66,97 +57,80 @@ fun AudioSettings( vendorIdHook: Boolean, isPremium: Boolean ) { - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - if (!adaptiveVolumeCapability && !conversationalAwarenessCapability && !loudSoundReductionCapability && !adaptiveAudioCapability) { return } - Box( - modifier = Modifier - .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)) - .padding(horizontal = 16.dp, vertical = 4.dp) - ){ - Text( - text = stringResource(R.string.audio), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) + Text( + text = stringResource(R.string.audio), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 8.dp, bottom = 6.dp) + ) - Column( - modifier = Modifier - .clip(RoundedCornerShape(28.dp)) - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .padding(top = 2.dp) + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier.fillMaxWidth() ) { - - if (adaptiveVolumeCapability) { - StyledToggle( - label = stringResource(R.string.personalized_volume), - description = stringResource(R.string.personalized_volume_description), - independent = false, - checked = adaptiveVolumeChecked, - onCheckedChange = onAdaptiveVolumeCheckedChange, - enabled = isPremium - ) - - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier - .padding(horizontal = 12.dp) - ) - } - - if (conversationalAwarenessCapability) { - StyledToggle( - label = stringResource(R.string.conversational_awareness), - description = stringResource(R.string.conversational_awareness_description), - independent = false, - checked = conversationalAwarenessChecked, - onCheckedChange = onConversationalAwarenessCheckedChange, - enabled = isPremium - ) - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier - .padding(horizontal = 12.dp) - ) - } - - if (loudSoundReductionCapability && vendorIdHook){ - StyledToggle( - label = stringResource(R.string.loud_sound_reduction), - description = stringResource(R.string.loud_sound_reduction_description), - independent = false, - checked = loudSoundReductionChecked, - onCheckedChange = onLoudSoundReductionCheckedChange, - enabled = isPremium - ) - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier - .padding(horizontal = 12.dp) - ) - } - - if (adaptiveAudioCapability) { - NavigationButton( - to = "adaptive_strength", - name = stringResource(R.string.adaptive_audio), - navController = navController, - independent = false - ) + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 4.dp) + ) { + + if (adaptiveVolumeCapability) { + StyledToggle( + label = stringResource(R.string.personalized_volume), + description = stringResource(R.string.personalized_volume_description), + independent = false, + checked = adaptiveVolumeChecked, + onCheckedChange = onAdaptiveVolumeCheckedChange, + enabled = isPremium + ) + + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant + ) + } + + if (conversationalAwarenessCapability) { + StyledToggle( + label = stringResource(R.string.conversational_awareness), + description = stringResource(R.string.conversational_awareness_description), + independent = false, + checked = conversationalAwarenessChecked, + onCheckedChange = onConversationalAwarenessCheckedChange, + enabled = isPremium + ) + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant + ) + } + + if (loudSoundReductionCapability && vendorIdHook){ + StyledToggle( + label = stringResource(R.string.loud_sound_reduction), + description = stringResource(R.string.loud_sound_reduction_description), + independent = false, + checked = loudSoundReductionChecked, + onCheckedChange = onLoudSoundReductionCheckedChange, + enabled = isPremium + ) + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant + ) + } + + if (adaptiveAudioCapability) { + NavigationButton( + to = "adaptive_strength", + name = stringResource(R.string.adaptive_audio), + navController = navController, + independent = false + ) + } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryIndicator.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryIndicator.kt index a2a5804e0..1b2eca773 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryIndicator.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryIndicator.kt @@ -18,7 +18,6 @@ package me.kavishdevar.librepods.presentation.components - import android.content.res.Configuration import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween @@ -31,6 +30,10 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -39,25 +42,25 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalDensity -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.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import me.kavishdevar.librepods.R import me.kavishdevar.librepods.data.BatteryStatus import kotlin.math.cos import kotlin.math.min import kotlin.math.sin import kotlin.math.sqrt +/** + * Material 3 Expressive battery ring: a circular progress arc, a Material charging-bolt that springs + * in when charging, and a percentage label. Healthy levels use a green fill, low levels the theme + * error color; the track uses a tonal surface color. + */ @Composable fun BatteryIndicator( batteryPercentage: Int, @@ -65,12 +68,10 @@ fun BatteryIndicator( prefix: String = "", previousCharging: Boolean = false, ) { - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color.Black else Color(0xFFF2F2F7) - val batteryTextColor = if (isDarkTheme) Color.White else Color.Black - val batteryFillColor = - if (batteryPercentage > 25) if (isDarkTheme) Color(0xFF2ED158) else Color(0xFF35C759) - else if (isDarkTheme) Color(0xFFFC4244) else Color(0xFFfe373C) + val colorScheme = MaterialTheme.colorScheme + val healthy = if (isSystemInDarkTheme()) Color(0xFF2ED158) else Color(0xFF1FA94B) + val batteryFillColor = if (batteryPercentage > 25) healthy else colorScheme.error + val trackColor = colorScheme.surfaceContainerHighest val initialScale = if (previousCharging) 1f else 0f val scaleAnim = remember { Animatable(initialScale) } @@ -82,7 +83,7 @@ fun BatteryIndicator( } Column( - modifier = Modifier.background(backgroundColor).padding(4.dp), // just for haze to work + modifier = Modifier.padding(4.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Box( @@ -91,20 +92,12 @@ fun BatteryIndicator( val strokeWidthPx = with(LocalDensity.current) { 4.dp.toPx() } val gapFromCenterPx = with(LocalDensity.current) { 8.sp.toPx() } - val trackColor = if (isDarkTheme) Color(0xFF272728) else Color(0xFFE3E3E8) val optimizedLimit = 0.8f val progress = batteryPercentage / 100f - Canvas(modifier = Modifier.size(34.dp)) { + Canvas(modifier = Modifier.size(38.dp)) { val startAngle = -90f val stroke = Stroke(width = strokeWidthPx, cap = StrokeCap.Round) - val inset = strokeWidthPx / 2 - Rect( - left = inset, - top = inset, - right = size.width - inset, - bottom = size.height - inset - ) val radius = size.minDimension / 2 if (status == BatteryStatus.OPTIMIZED_CHARGING) { @@ -125,7 +118,6 @@ fun BatteryIndicator( style = stroke ) - // ---- PILL MARKER AT 80% ---- val angleDeg = startAngle + 360f * optimizedLimit val angleRad = Math.toRadians(angleDeg.toDouble()) @@ -173,26 +165,23 @@ fun BatteryIndicator( } } - Text( - text = "\uDBC0\uDEE6", style = TextStyle( - fontSize = 14.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = batteryFillColor, - textAlign = TextAlign.Center - ), modifier = Modifier.scale(scaleAnim.value) + Icon( + imageVector = Icons.Filled.Bolt, + contentDescription = null, + tint = batteryFillColor, + modifier = Modifier + .size(16.dp) + .scale(scaleAnim.value) ) } Spacer(modifier = Modifier.height(4.dp)) Text( - text = "$prefix $batteryPercentage%", - color = batteryTextColor, - style = TextStyle( - fontSize = 14.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - textAlign = TextAlign.Center - ), + text = if (prefix.isBlank()) "$batteryPercentage%" else "$prefix $batteryPercentage%", + color = colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, ) } } @@ -200,14 +189,15 @@ fun BatteryIndicator( @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable fun BatteryIndicatorPreview() { - val bg = if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7) Box( - modifier = Modifier.background(bg) + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(16.dp) ) { BatteryIndicator( batteryPercentage = 50, status = BatteryStatus.OPTIMIZED_CHARGING, - prefix = "\uDBC6\uDCE5", + prefix = "L", previousCharging = false ) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryView.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryView.kt index 7accabee7..835c11e0f 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryView.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryView.kt @@ -23,7 +23,6 @@ package me.kavishdevar.librepods.presentation.components import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,12 +32,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.stringResource @@ -64,9 +63,6 @@ fun BatteryView( val rightLevel = right?.level ?: 0 val caseLevel = case?.level ?: 0 - val caseCharging = case?.status == BatteryStatus.CHARGING || - case?.status == BatteryStatus.OPTIMIZED_CHARGING - val singleDisplayed = remember { mutableStateOf(false) } Box( @@ -109,7 +105,7 @@ fun BatteryView( BatteryIndicator( leftLevel, left?.status ?: BatteryStatus.NOT_CHARGING, - "\uDBC6\uDCE5" + "L" ) } @@ -121,7 +117,7 @@ fun BatteryView( BatteryIndicator( rightLevel, right?.status ?: BatteryStatus.NOT_CHARGING, - "\uDBC6\uDCE8" + "R" ) } } @@ -144,7 +140,7 @@ fun BatteryView( BatteryIndicator( caseLevel, case?.status ?: BatteryStatus.NOT_CHARGING, - prefix = if (!singleDisplayed.value) "\uDBC3\uDE6C" else "" + prefix = if (!singleDisplayed.value) stringResource(R.string.case_alt) else "" ) } } @@ -161,11 +157,9 @@ fun BatteryViewPreview() { Battery(BatteryComponent.CASE, 60, BatteryStatus.NOT_CHARGING) ) - val bg = if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7) - Box( modifier = Modifier - .background(bg) + .background(MaterialTheme.colorScheme.background) .padding(16.dp) ) { BatteryView( diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/CallControlSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/CallControlSettings.kt index 2b00c06ed..856db8429 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/CallControlSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/CallControlSettings.kt @@ -21,10 +21,8 @@ package me.kavishdevar.librepods.presentation.components import android.util.Log -import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -32,8 +30,13 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -46,7 +49,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned @@ -54,12 +56,7 @@ import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback 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.unit.dp -import androidx.compose.ui.unit.sp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import kotlinx.coroutines.launch @@ -73,31 +70,27 @@ fun CallControlSettings( flipped: Boolean, onCallControlValueChanged: (Boolean) -> Unit ) { - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) Box( modifier = Modifier - .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)) - .padding(horizontal = 16.dp, vertical = 4.dp) + .padding(start = 20.dp, end = 16.dp, top = 8.dp, bottom = 6.dp) ){ Text( text = stringResource(R.string.call_controls), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } - Column( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .padding(top = 2.dp) + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier.fillMaxWidth() ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 2.dp) + ) { val scope = rememberCoroutineScope() val haptics = LocalHapticFeedback.current @@ -138,34 +131,34 @@ fun CallControlSettings( Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .padding(horizontal = 20.dp) .height(58.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = stringResource(R.string.answer_call), - fontSize = 16.sp, - color = textColor, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 4.dp) ) Text( text = stringResource(R.string.press_once), - fontSize = 16.sp, - color = textColor.copy(alpha = 0.6f) + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } HorizontalDivider( thickness = 1.dp, - color = Color(0x40888888), + color = MaterialTheme.colorScheme.outlineVariant, modifier = Modifier - .padding(horizontal = 12.dp) + .padding(horizontal = 16.dp) ) Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .padding(horizontal = 20.dp) .height(58.dp) .pointerInput(Unit) { detectTapGestures { offset -> @@ -235,8 +228,8 @@ fun CallControlSettings( ) { Text( text = stringResource(R.string.mute_unmute), - fontSize = 16.sp, - color = textColor, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 4.dp) ) Box( @@ -249,21 +242,16 @@ fun CallControlSettings( ) { Text( text = singlePressAction, - style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(alpha = 0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - Text( - text = "􀆏", - style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier .padding(start = 6.dp) + .size(20.dp) ) } @@ -293,15 +281,15 @@ fun CallControlSettings( } HorizontalDivider( thickness = 1.dp, - color = Color(0x40888888), + color = MaterialTheme.colorScheme.outlineVariant, modifier = Modifier - .padding(horizontal = 12.dp) + .padding(horizontal = 16.dp) ) Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .padding(horizontal = 20.dp) .height(58.dp) .pointerInput(Unit) { detectTapGestures { offset -> @@ -371,8 +359,8 @@ fun CallControlSettings( ) { Text( text = stringResource(R.string.hang_up), - fontSize = 16.sp, - color = textColor, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 4.dp) ) Box( @@ -385,21 +373,16 @@ fun CallControlSettings( ) { Text( text = doublePressAction, - style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(alpha = 0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - Text( - text = "􀆏", - style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier .padding(start = 6.dp) + .size(20.dp) ) } @@ -428,5 +411,6 @@ fun CallControlSettings( } } } + } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConfirmationDialog.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConfirmationDialog.kt index 35e538ad9..243311bf6 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConfirmationDialog.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConfirmationDialog.kt @@ -25,7 +25,6 @@ import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -36,30 +35,23 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredWidthIn -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.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.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.kyant.backdrop.backdrops.LayerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.effects.blur -import com.kyant.backdrop.effects.lens -import com.kyant.backdrop.effects.vibrancy import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import me.kavishdevar.librepods.R +/** + * Material 3 Expressive confirmation dialog. A scrim-backed [Surface] card (extra-large rounded + * corners, `surfaceContainerHigh` tonal fill) that scales in with the expressive motion scheme. + * Signature preserved: [backdrop] is retained for source compatibility but is no longer used. + */ @ExperimentalHazeMaterialsApi @Composable fun ConfirmationDialog( @@ -70,108 +62,78 @@ fun ConfirmationDialog( dismissText: String = "Cancel", onConfirm: () -> Unit, onDismiss: () -> Unit = { showDialog.value = false }, - backdrop: LayerBackdrop, + @Suppress("UNUSED_PARAMETER") backdrop: LayerBackdrop, ) { - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val accentColor = if (isDarkTheme) Color(0xFF0091FF) else Color(0xFF0088FF) - AnimatedVisibility( visible = showDialog.value, - enter = scaleIn(initialScale = 1.05f) + fadeIn(), - exit = scaleOut(targetScale = 1.05f) + fadeOut() + enter = scaleIn(initialScale = 0.9f) + fadeIn(), + exit = scaleOut(targetScale = 0.9f) + fadeOut() ) { Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.4f)) + .clickable(enabled = false, onClick = {}), contentAlignment = Alignment.Center ) { - val innerBackdrop = rememberLayerBackdrop() - Box( + Surface( + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor = MaterialTheme.colorScheme.onSurface, modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.4f)) - .clickable(enabled = false, onClick = {}), - contentAlignment = Alignment.Center + .padding(24.dp) + .requiredWidthIn(min = 200.dp, max = 360.dp) ) { - Box( - modifier = Modifier - .requiredWidthIn(min = 200.dp, max = 360.dp) - .clip(RoundedCornerShape(48.dp)) - .drawBackdrop( + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(24.dp)) + Text( + title, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 24.dp) + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 24.dp) + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + StyledButton( + onClick = onDismiss, + backdrop = backdrop, + modifier = Modifier.weight(1f), + surfaceColor = MaterialTheme.colorScheme.surfaceContainerHighest, + tint = MaterialTheme.colorScheme.onSurface, + ) { + Text( + text = dismissText, + style = MaterialTheme.typography.labelLarge, + ) + } + StyledButton( + onClick = onConfirm, backdrop = backdrop, - exportedBackdrop = innerBackdrop, - shape = { RoundedCornerShape(48.dp) }, - effects = { - vibrancy() - blur(4f.dp.toPx()) - lens(12f.dp.toPx(), 48f.dp.toPx(), true) - }, - onDrawSurface = { - drawRect( - if (isDarkTheme) Color(0xFF1F1F1F).copy(alpha = 0.35f) else Color(0xFFE0E0E0).copy(alpha = 0.7f) - ) - })) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Spacer(modifier = Modifier.height(24.dp)) - Text( - title, - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 16.dp) - ) - Spacer(modifier = Modifier.height(12.dp)) - Text( - message, - style = TextStyle( - fontSize = 14.sp, - color = textColor.copy(alpha = 0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 16.dp) - ) - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(0.9f), - horizontalArrangement = Arrangement.spacedBy(24.dp) + modifier = Modifier.weight(1f), + surfaceColor = MaterialTheme.colorScheme.primary, + tint = MaterialTheme.colorScheme.onPrimary, ) { - StyledButton( - onClick = onDismiss, - backdrop = innerBackdrop, - modifier = Modifier.weight(1f), - ) { - Text( - text = dismissText, style = TextStyle( - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = textColor - ) - ) - } - StyledButton( - onClick = onConfirm, - backdrop = innerBackdrop, - modifier = Modifier.weight(1f), - surfaceColor = accentColor - ) { - Text( - text = confirmText, style = TextStyle( - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = Color.White - ) - ) - } + Text( + text = confirmText, + style = MaterialTheme.typography.labelLarge, + ) } - Spacer(modifier = Modifier.height(24.dp)) } + Spacer(modifier = Modifier.height(24.dp)) } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConnectionSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConnectionSettings.kt index ba5481852..617d8352f 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConnectionSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConnectionSettings.kt @@ -20,16 +20,14 @@ package me.kavishdevar.librepods.presentation.components -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.kavishdevar.librepods.R @@ -42,34 +40,34 @@ fun ConnectionSettings( automaticConnectionEnabled: Boolean, onAutomaticConnectionChanged: (Boolean) -> Unit, ) { - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - - Column( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .padding(top = 2.dp) + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier.fillMaxWidth() ) { - StyledToggle( - label = stringResource(R.string.ear_detection), - independent = false, - checked = automaticEarDetectionEnabled, - onCheckedChange = onAutomaticEarDetectionChanged - ) - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), + Column( modifier = Modifier - .padding(horizontal = 12.dp) - ) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + ) { + StyledToggle( + label = stringResource(R.string.ear_detection), + independent = false, + checked = automaticEarDetectionEnabled, + onCheckedChange = onAutomaticEarDetectionChanged + ) + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + ) - StyledToggle( - label = stringResource(R.string.automatically_connect), - description = stringResource(R.string.automatically_connect_description), - independent = false, - checked = automaticConnectionEnabled, - onCheckedChange = onAutomaticConnectionChanged - ) + StyledToggle( + label = stringResource(R.string.automatically_connect), + description = stringResource(R.string.automatically_connect_description), + independent = false, + checked = automaticConnectionEnabled, + onCheckedChange = onAutomaticConnectionChanged + ) + } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterButton.kt deleted file mode 100644 index 5340b0cae..000000000 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterButton.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - LibrePods - AirPods liberated from Apple’s ecosystem - Copyright (C) 2025 LibrePods contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -@file:Suppress("unused") - -package me.kavishdevar.librepods.presentation.components - -import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp - -private val SelectedColorBlue = Color(0xFF0A84FF) -private val UnselectedColor = Color(0x593C3C3E) -private val TextColor = Color.White -private val IconTint = Color.White - -@Composable -fun ControlCenterButton( - label: String, - icon: Painter, - onClick: () -> Unit, - modifier: Modifier = Modifier, - iconAreaSize: Dp, - isSelected: Boolean, - backgroundBrush: Brush? = null -) { - val targetBackgroundColor = if (isSelected) SelectedColorBlue else UnselectedColor - val backgroundColor by animateColorAsState( - targetValue = targetBackgroundColor, - label = "ButtonBackground" - ) - - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Box( - modifier = Modifier - .size(iconAreaSize) - .clip(CircleShape) - .background(backgroundBrush ?: Brush.linearGradient(colors=listOf(backgroundColor, backgroundColor))) - .clickable( - onClick = onClick, - indication = null, - interactionSource = remember { MutableInteractionSource() } - ), - contentAlignment = Alignment.Center - ) { - Icon( - painter = icon, - contentDescription = null, - tint = IconTint, - modifier = Modifier.size(32.dp) - ) - } - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = label, - color = TextColor, - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center, - maxLines = 2 - ) - } -} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt index ca0d1f4c8..c7f633c91 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt @@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -58,27 +59,25 @@ import androidx.compose.ui.unit.sp import me.kavishdevar.librepods.R import me.kavishdevar.librepods.data.NoiseControlMode -private val ContainerColor = Color(0x593C3C3E) -private val SelectedIndicatorColorGray = Color(0xFF6C6C6E) -private val SelectedIndicatorColorBlue = Color(0xFF0A84FF) -private val TextColor = Color.White -private val IconTintUnselected = Color.White -private val IconTintSelected = Color.White - -internal val AdaptiveRainbowBrush = Brush.sweepGradient( - colors = listOf( - Color(0xFFB03A2F), Color(0xFFB07A2F), Color(0xFFB0A22F), Color(0xFF6AB02F), - Color(0xFF2FAAB0), Color(0xFF2F5EB0), Color(0xFF7D2FB0), Color(0xFFB02F7D), - Color(0xFFB03A2F) - ) -) - internal val IconAreaSize = 72.dp private val IconSize = 42.dp private val IconRowHeight = IconAreaSize + 12.dp private val TextRowHeight = 24.dp private val TextSize = 12.sp +/** Vibrant rainbow sweep used to represent the "Adaptive" noise-control mode. */ +internal val AdaptiveRainbowBrush = Brush.sweepGradient( + colors = listOf( + Color(0xFFFF5252), Color(0xFFFFB300), Color(0xFF66BB6A), Color(0xFF26C6DA), + Color(0xFF5C6BC0), Color(0xFFAB47BC), Color(0xFFFF5252) + ) +) + +/** + * Material 3 Expressive segmented noise-control selector for the Quick-Settings control center. + * A vibrant primary "pill" slides between modes with a low-damping (bouncy) spring; the adaptive + * mode shows a playful theme-colored rainbow. Signature & behaviour preserved. + */ @Composable fun ControlCenterNoiseControlSegmentedButton( modifier: Modifier = Modifier, @@ -86,28 +85,36 @@ fun ControlCenterNoiseControlSegmentedButton( selectedMode: NoiseControlMode, onModeSelected: (NoiseControlMode) -> Unit ) { + val colorScheme = MaterialTheme.colorScheme + val containerColor = colorScheme.surfaceContainerHighest + val selectedColor = colorScheme.primary + val onSelectedColor = colorScheme.onPrimary + val unselectedColor = colorScheme.onSurfaceVariant + + val adaptiveBrush = remember(colorScheme) { + Brush.sweepGradient( + listOf( + colorScheme.primary, colorScheme.tertiary, colorScheme.secondary, + colorScheme.primary + ) + ) + } + val selectedIndex = availableModes.indexOf(selectedMode).coerceAtLeast(0) val density = LocalDensity.current var iconRowWidthPx by remember { mutableFloatStateOf(0f) } val itemCount = availableModes.size val itemSlotWidthPx = remember(iconRowWidthPx, itemCount) { - if (itemCount > 0 && iconRowWidthPx > 0) { - iconRowWidthPx / itemCount - } else { - 0f - } + if (itemCount > 0 && iconRowWidthPx > 0) iconRowWidthPx / itemCount else 0f } val itemSlotWidthDp = remember(itemSlotWidthPx) { with(density) { itemSlotWidthPx.toDp() } } val iconAreaSizePx = remember { with(density) { IconAreaSize.toPx() } } val targetIndicatorStartPx = remember(selectedIndex, itemSlotWidthPx, iconAreaSizePx) { if (itemSlotWidthPx > 0) { - val slotCenterPx = (selectedIndex + 0.5f) * itemSlotWidthPx - slotCenterPx - (iconAreaSizePx / 2f) - } else { - 0f - } + (selectedIndex + 0.5f) * itemSlotWidthPx - (iconAreaSizePx / 2f) + } else 0f } val indicatorOffset: Dp by animateDpAsState( @@ -119,15 +126,6 @@ fun ControlCenterNoiseControlSegmentedButton( label = "IndicatorOffset" ) - val indicatorBackground = remember(selectedMode) { - when (selectedMode) { - NoiseControlMode.ADAPTIVE -> AdaptiveRainbowBrush - NoiseControlMode.OFF -> Brush.linearGradient(colors=listOf(SelectedIndicatorColorGray, SelectedIndicatorColorGray)) - NoiseControlMode.TRANSPARENCY, - NoiseControlMode.NOISE_CANCELLATION -> Brush.linearGradient(colors=listOf(SelectedIndicatorColorBlue, SelectedIndicatorColorBlue)) - } - } - Column( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally @@ -137,7 +135,7 @@ fun ControlCenterNoiseControlSegmentedButton( .fillMaxWidth() .height(IconRowHeight) .clip(CircleShape) - .background(ContainerColor) + .background(containerColor) .onSizeChanged { iconRowWidthPx = it.width.toFloat() }, contentAlignment = Alignment.Center ) { @@ -147,11 +145,19 @@ fun ControlCenterNoiseControlSegmentedButton( .offset(x = indicatorOffset) .size(IconAreaSize) .clip(CircleShape) - .background(indicatorBackground) + .then( + if (selectedMode == NoiseControlMode.ADAPTIVE) { + Modifier.background(adaptiveBrush) + } else { + Modifier.background(selectedColor) + } + ) ) Row( - modifier = Modifier.fillMaxWidth().align(Alignment.Center), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceAround ) { @@ -160,7 +166,7 @@ fun ControlCenterNoiseControlSegmentedButton( NoiseControlIconItem( modifier = Modifier.size(IconAreaSize), mode = mode, - isSelected = isSelected, + tint = if (isSelected) onSelectedColor else unselectedColor, onClick = { onModeSelected(mode) } ) } @@ -180,7 +186,7 @@ fun ControlCenterNoiseControlSegmentedButton( val isSelected = selectedMode == mode Text( text = getModeLabel(mode), - color = TextColor, + color = if (isSelected) colorScheme.onSurface else colorScheme.onSurfaceVariant, fontSize = TextSize, fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, textAlign = TextAlign.Center, @@ -195,13 +201,11 @@ fun ControlCenterNoiseControlSegmentedButton( private fun NoiseControlIconItem( modifier: Modifier = Modifier, mode: NoiseControlMode, - isSelected: Boolean, + tint: Color, onClick: () -> Unit ) { val iconRes = remember(mode) { getModeIconRes(mode) } - val tint = IconTintUnselected - Box( modifier = modifier .clip(CircleShape) @@ -215,13 +219,12 @@ private fun NoiseControlIconItem( Icon( painter = painterResource(id = iconRes), contentDescription = getModeLabel(mode), - tint = if (isSelected && mode == NoiseControlMode.ADAPTIVE) IconTintSelected else tint, + tint = tint, modifier = Modifier.size(IconSize) ) } } - private fun getModeIconRes(mode: NoiseControlMode): Int { return when (mode) { NoiseControlMode.OFF -> R.drawable.noise_cancellation @@ -239,4 +242,3 @@ private fun getModeLabel(mode: NoiseControlMode): String { NoiseControlMode.NOISE_CANCELLATION -> "Noise Cancellation" } } - diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/DeviceInfoCard.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/DeviceInfoCard.kt index 18ab494d3..01747e32b 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/DeviceInfoCard.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/DeviceInfoCard.kt @@ -1,235 +1,100 @@ package me.kavishdevar.librepods.presentation.components import android.os.Build -import androidx.compose.foundation.background -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.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity 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.unit.dp -import androidx.compose.ui.unit.sp import me.kavishdevar.librepods.R import me.kavishdevar.librepods.utils.XposedState @Composable fun DeviceInfoCard() { - val isDarkTheme = isSystemInDarkTheme() - - val textColor = if (isDarkTheme) Color.White else Color.Black - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - - val rowHeight = remember { mutableStateOf(0.dp) } - val density = LocalDensity.current - - Column ( + Column( verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Box( - modifier = Modifier - .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)) - .padding(start = 16.dp, top = 24.dp, end = 4.dp) + Text( + text = stringResource(R.string.device_info), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 8.dp, bottom = 6.dp) + ) + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth() ) { - Text( - text = stringResource(R.string.device_info), style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) + Column(modifier = Modifier.fillMaxWidth()) { + DeviceInfoRow( + label = stringResource(R.string.manufacturer), + value = Build.MANUFACTURER ) - ) - } - Column( - modifier = Modifier - .clip(RoundedCornerShape(28.dp)) - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .onGloballyPositioned { coordinates -> - rowHeight.value = with(density) { coordinates.size.height.toDp() } - }, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.manufacturer), style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + DeviceInfoDivider() + DeviceInfoRow( + label = stringResource(R.string.model_number), + value = Build.MODEL ) - Text( - text = Build.MANUFACTURER, style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.8f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + DeviceInfoDivider() + DeviceInfoRow( + label = stringResource(R.string.build_id), + value = Build.DISPLAY ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.model_number), style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + DeviceInfoDivider() + DeviceInfoRow( + label = stringResource(R.string.version), + value = Build.ID + " (${Build.VERSION.SDK_INT_FULL})" ) - Text( - text = Build.MODEL, style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.8f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + DeviceInfoDivider() + DeviceInfoRow( + label = stringResource(R.string.xposed_available), + value = if (XposedState.isAvailable) stringResource(R.string.yes) else stringResource(R.string.no) ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.build_id), style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - Text( - text = Build.DISPLAY, style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.8f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.version), style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - Text( - text = Build.ID + " (${Build.VERSION.SDK_INT_FULL})", - style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.8f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.xposed_available), style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - Text( - text = if (XposedState.isAvailable) stringResource(R.string.yes) else stringResource(R.string.no), style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.8f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.app_enabled_in_xposed), style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - Text( - text = if (XposedState.bluetoothScopeEnabled) stringResource(R.string.yes) else stringResource(R.string.no), style = TextStyle( - fontSize = 16.sp, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.8f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + DeviceInfoDivider() + DeviceInfoRow( + label = stringResource(R.string.app_enabled_in_xposed), + value = if (XposedState.bluetoothScopeEnabled) stringResource(R.string.yes) else stringResource(R.string.no) ) } } } } + +@Composable +private fun DeviceInfoRow(label: String, value: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Composable +private fun DeviceInfoDivider() { + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 12.dp) + ) +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ExpressiveModifiers.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ExpressiveModifiers.kt new file mode 100644 index 000000000..79e2f4290 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ExpressiveModifiers.kt @@ -0,0 +1,57 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.components + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue + +/** + * Material 3 Expressive's signature feel is physics-based, slightly-overshooting motion. This helper + * gives any clickable surface a springy "squish" on press: scale dips toward [pressedScale] while + * held, then bounces back with a low-damping spring. Drive it from a [androidx.compose.foundation.interaction.MutableInteractionSource] + * shared with the component's `clickable`/`Surface`/`Button`, e.g.: + * + * ``` + * val interaction = remember { MutableInteractionSource() } + * val scale by rememberBounceScale(interaction) + * Surface(onClick = …, interactionSource = interaction, + * modifier = Modifier.graphicsLayer { scaleX = scale; scaleY = scale }) { … } + * ``` + */ +@Composable +fun rememberBounceScale( + interactionSource: InteractionSource, + pressedScale: Float = 0.96f, +): State { + val pressed by interactionSource.collectIsPressedAsState() + return animateFloatAsState( + targetValue = if (pressed) pressedScale else 1f, + animationSpec = spring( + dampingRatio = 0.42f, + stiffness = Spring.StiffnessMedium, + ), + label = "bounceScale", + ) +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/HearingHealthSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/HearingHealthSettings.kt index a444ddae8..8819a4a8a 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/HearingHealthSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/HearingHealthSettings.kt @@ -20,26 +20,17 @@ package me.kavishdevar.librepods.presentation.components -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color 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.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController import me.kavishdevar.librepods.R import kotlin.io.encoding.ExperimentalEncodingApi @@ -51,55 +42,42 @@ fun HearingHealthSettings( hasHearingAidCapability: Boolean, vendorIdHook: Boolean ) { - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) val shouldShowHearingAid = hasHearingAidCapability && vendorIdHook if (hasPPECapability && shouldShowHearingAid) { - Box( - modifier = Modifier - .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)) - .padding(horizontal = 16.dp, vertical = 4.dp) - ){ - Text( - text = stringResource(R.string.hearing_health), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } - Column( - modifier = Modifier - .clip(RoundedCornerShape(28.dp)) - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .padding(top = 2.dp) + Text( + text = stringResource(R.string.hearing_health), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 8.dp, bottom = 6.dp) + ) + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth() ) { - NavigationButton( - to = "hearing_protection", - name = stringResource(R.string.hearing_protection), - navController = navController, - independent = false - ) - - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier - .padding(horizontal = 12.dp) - ) + Column { + NavigationButton( + to = "hearing_protection", + name = stringResource(R.string.hearing_protection), + navController = navController, + independent = false + ) + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 20.dp) + ) - NavigationButton( - to = "hearing_aid", - name = stringResource(R.string.hearing_aid), - navController = navController, - independent = false - ) + NavigationButton( + to = "hearing_aid", + name = stringResource(R.string.hearing_aid), + navController = navController, + independent = false + ) + } } } else if (shouldShowHearingAid) { NavigationButton( diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/IconMappings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/IconMappings.kt new file mode 100644 index 000000000..59dd32c38 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/IconMappings.kt @@ -0,0 +1,63 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.VolumeDown +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Numbers +import androidx.compose.material.icons.filled.Sensors +import androidx.compose.material.icons.filled.SensorsOff +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Tag +import androidx.compose.ui.graphics.vector.ImageVector + +/* + * The old UI rendered Apple SF Symbols as text glyphs (only renderable with the bundled SF Pro + * font). The Material 3 redesign uses real Material vector icons instead. This map lets the shared + * components and screens convert the legacy glyph strings to [ImageVector]s with zero call-site + * churn — `StyledIconButton(icon = "􀍟")` keeps compiling but now draws a Material Settings icon. + * + * Returns null for an unrecognised glyph so callers can fall back gracefully. + */ +fun sfSymbolToIcon(glyph: String): ImageVector? = when (glyph) { + "􀯶" -> Icons.AutoMirrored.Filled.ArrowBack // U+100BF6 chevron.backward (back) + "􀯻" -> Icons.AutoMirrored.Filled.KeyboardArrowRight // U+100BFB chevron.forward (disclosure) + "􀍟" -> Icons.Filled.Settings // U+10035F gearshape + "􀈑" -> Icons.Filled.Delete // U+100211 trash + "􀆉" -> Icons.Filled.KeyboardArrowUp // U+100189 chevron.up (sent) + "􀆊" -> Icons.Filled.KeyboardArrowDown // U+10018A chevron.down (received) + "􀆏" -> Icons.Filled.KeyboardArrowDown // U+10018F chevron.down (disclosure) + "􀊥" -> Icons.AutoMirrored.Filled.VolumeDown // U+1002A5 speaker.wave.1 + "􀊩" -> Icons.AutoMirrored.Filled.VolumeUp // U+1002A9 speaker.wave.3 + "􀊅" -> Icons.Filled.Sensors // U+100285 head tracking active + "􀊃" -> Icons.Filled.SensorsOff // U+100283 head tracking inactive + "􀁡" -> Icons.Filled.Cancel // U+100061 xmark.circle.fill (clear) + "􀆅" -> Icons.Filled.Check // U+100185 checkmark + "􀀛" -> Icons.Filled.Tag // U+10001B left bud badge + "􀀧" -> Icons.Filled.Numbers // U+100027 right bud badge + else -> null +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/MicrophoneSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/MicrophoneSettings.kt index 5f693e9b9..deec60533 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/MicrophoneSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/MicrophoneSettings.kt @@ -20,10 +20,8 @@ package me.kavishdevar.librepods.presentation.components -import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,7 +29,12 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -43,7 +46,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned @@ -51,11 +53,7 @@ import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback 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.unit.dp -import androidx.compose.ui.unit.sp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import kotlinx.coroutines.launch @@ -69,194 +67,188 @@ fun MicrophoneSettings( micModeValue: Byte, onMicModeValueChanged: (Byte) -> Unit ) { - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - - Column( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .padding(top = 2.dp) + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface ) { - var selectedMode by remember { - mutableStateOf( - when (micModeValue) { - 0x00.toByte() -> "Automatic" - 0x01.toByte() -> "Always Right" - 0x02.toByte() -> "Always Left" - else -> "Automatic" - } - ) - } - var showDropdown by remember { mutableStateOf(false) } - var touchOffset by remember { mutableStateOf(null) } - var boxPosition by remember { mutableStateOf(Offset.Zero) } - var lastDismissTime by remember { mutableLongStateOf(0L) } - val reopenThresholdMs = 250L - - val density = LocalDensity.current - val itemHeightPx = with(density) { 48.dp.toPx() } - var parentHoveredIndex by remember { mutableStateOf(null) } - var parentDragActive by remember { mutableStateOf(false) } - var previousIdx by remember { mutableStateOf(null) } - val haptics = LocalHapticFeedback.current - val scope = rememberCoroutineScope() - val microphoneAutomaticText = stringResource(R.string.microphone_automatic) - val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right) - val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left) - Row( + Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) - .height(58.dp) - .pointerInput(Unit) { - detectTapGestures { offset -> - val now = System.currentTimeMillis() - if (showDropdown) { - showDropdown = false - lastDismissTime = now - } else { - if (now - lastDismissTime > reopenThresholdMs) { - touchOffset = offset - showDropdown = true - } - } + .padding(top = 2.dp) + ) { + var selectedMode by remember { + mutableStateOf( + when (micModeValue) { + 0x00.toByte() -> "Automatic" + 0x01.toByte() -> "Always Right" + 0x02.toByte() -> "Always Left" + else -> "Automatic" } - } - .pointerInput(Unit) { - detectDragGesturesAfterLongPress( - onDragStart = { offset -> + ) + } + var showDropdown by remember { mutableStateOf(false) } + var touchOffset by remember { mutableStateOf(null) } + var boxPosition by remember { mutableStateOf(Offset.Zero) } + var lastDismissTime by remember { mutableLongStateOf(0L) } + val reopenThresholdMs = 250L + + val density = LocalDensity.current + val itemHeightPx = with(density) { 48.dp.toPx() } + var parentHoveredIndex by remember { mutableStateOf(null) } + var parentDragActive by remember { mutableStateOf(false) } + var previousIdx by remember { mutableStateOf(null) } + val haptics = LocalHapticFeedback.current + val scope = rememberCoroutineScope() + val microphoneAutomaticText = stringResource(R.string.microphone_automatic) + val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right) + val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(58.dp) + .pointerInput(Unit) { + detectTapGestures { offset -> val now = System.currentTimeMillis() - touchOffset = offset - if (!showDropdown && now - lastDismissTime > reopenThresholdMs) { - showDropdown = true - } - lastDismissTime = now - parentDragActive = true - parentHoveredIndex = 0 - }, - onDrag = { change, _ -> - val current = change.position - val touch = touchOffset ?: current - val posInPopupY = current.y - touch.y - val idx = (posInPopupY / itemHeightPx).toInt() - if (idx != previousIdx) { - scope.launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) } + if (showDropdown) { + showDropdown = false + lastDismissTime = now + } else { + if (now - lastDismissTime > reopenThresholdMs) { + touchOffset = offset + showDropdown = true + } } - parentHoveredIndex = idx - previousIdx = idx - }, - onDragEnd = { - parentDragActive = false - parentHoveredIndex?.let { idx -> - val options = listOf( - microphoneAutomaticText, - microphoneAlwaysRightText, - microphoneAlwaysLeftText - ) - if (idx in options.indices) { - val option = options[idx] - selectedMode = option - showDropdown = false - lastDismissTime = System.currentTimeMillis() - val byteValue = when (option) { - options[0] -> 0x00 - options[1] -> 0x01 - options[2] -> 0x02 - else -> 0x00 - } + } + } + .pointerInput(Unit) { + detectDragGesturesAfterLongPress( + onDragStart = { offset -> + val now = System.currentTimeMillis() + touchOffset = offset + if (!showDropdown && now - lastDismissTime > reopenThresholdMs) { + showDropdown = true + } + lastDismissTime = now + parentDragActive = true + parentHoveredIndex = 0 + }, + onDrag = { change, _ -> + val current = change.position + val touch = touchOffset ?: current + val posInPopupY = current.y - touch.y + val idx = (posInPopupY / itemHeightPx).toInt() + if (idx != previousIdx) { + scope.launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) } + } + parentHoveredIndex = idx + previousIdx = idx + }, + onDragEnd = { + parentDragActive = false + parentHoveredIndex?.let { idx -> + val options = listOf( + microphoneAutomaticText, + microphoneAlwaysRightText, + microphoneAlwaysLeftText + ) + if (idx in options.indices) { + val option = options[idx] + selectedMode = option + showDropdown = false + lastDismissTime = System.currentTimeMillis() + val byteValue = when (option) { + options[0] -> 0x00 + options[1] -> 0x01 + options[2] -> 0x02 + else -> 0x00 + } // service.aacpManager.sendControlCommand( // AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE.value, // byteArrayOf(byteValue.toByte()) // ) - onMicModeValueChanged(byteValue.toByte()) + onMicModeValueChanged(byteValue.toByte()) + } } + if (parentHoveredIndex != null && parentHoveredIndex in 0..2) { + scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) } + } + parentHoveredIndex = null + }, + onDragCancel = { + parentDragActive = false + parentHoveredIndex = null } - if (parentHoveredIndex != null && parentHoveredIndex in 0..2) { - scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) } - } - parentHoveredIndex = null - }, - onDragCancel = { - parentDragActive = false - parentHoveredIndex = null - } - ) - }, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.microphone_mode), - style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier.padding(bottom = 4.dp) - ) - Box( - modifier = Modifier.onGloballyPositioned { coordinates -> - boxPosition = coordinates.positionInParent() - } + ) + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Row( - verticalAlignment = Alignment.CenterVertically + Text( + text = stringResource(R.string.microphone_mode), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 4.dp) + ) + Box( + modifier = Modifier.onGloballyPositioned { coordinates -> + boxPosition = coordinates.positionInParent() + } ) { - Text( - text = selectedMode, - style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(alpha = 0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = selectedMode, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - ) - Text( - text = "􀆏", - style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .padding(start = 6.dp) + .size(20.dp) + ) + } + + val microphoneAutomaticText = stringResource(R.string.microphone_automatic) + val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right) + val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left) + + StyledDropdown( + expanded = showDropdown, + onDismissRequest = { + showDropdown = false + lastDismissTime = System.currentTimeMillis() + }, + options = listOf( + microphoneAutomaticText, + microphoneAlwaysRightText, + microphoneAlwaysLeftText ), - modifier = Modifier - .padding(start = 6.dp) + selectedOption = selectedMode, + touchOffset = touchOffset, + boxPosition = boxPosition, + externalHoveredIndex = parentHoveredIndex, + externalDragActive = parentDragActive, + onOptionSelected = { option -> + selectedMode = option + showDropdown = false + val byteValue = when (option) { + microphoneAutomaticText -> 0x00 + microphoneAlwaysRightText -> 0x01 + microphoneAlwaysLeftText -> 0x02 + else -> 0x00 + } + onMicModeValueChanged(byteValue.toByte()) + }, + hazeState = hazeState ) } - - val microphoneAutomaticText = stringResource(R.string.microphone_automatic) - val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right) - val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left) - - StyledDropdown( - expanded = showDropdown, - onDismissRequest = { - showDropdown = false - lastDismissTime = System.currentTimeMillis() - }, - options = listOf( - microphoneAutomaticText, - microphoneAlwaysRightText, - microphoneAlwaysLeftText - ), - selectedOption = selectedMode, - touchOffset = touchOffset, - boxPosition = boxPosition, - externalHoveredIndex = parentHoveredIndex, - externalDragActive = parentDragActive, - onOptionSelected = { option -> - selectedMode = option - showDropdown = false - val byteValue = when (option) { - microphoneAutomaticText -> 0x00 - microphoneAlwaysRightText -> 0x01 - microphoneAlwaysLeftText -> 0x02 - else -> 0x00 - } - onMicModeValueChanged(byteValue.toByte()) - }, - hazeState = hazeState - ) } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NavigationButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NavigationButton.kt index ceff731a2..0adefee46 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NavigationButton.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NavigationButton.kt @@ -18,49 +18,47 @@ package me.kavishdevar.librepods.presentation.components -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface 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.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.RectangleShape +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback -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.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import kotlinx.coroutines.launch -import me.kavishdevar.librepods.R +/** + * Material 3 Expressive settings row. A tonal [Surface] with a leading label, optional trailing + * value and a chevron, that springs on press. `independent = true` rounds it into a standalone + * card; `false` makes it a flush row for grouping inside a section card. + */ @Composable fun NavigationButton( to: String, name: String, - navController: NavController, onClick: (() -> Unit)? = null, + navController: NavController, + onClick: (() -> Unit)? = null, independent: Boolean = true, title: String? = null, description: String? = null, @@ -68,106 +66,73 @@ fun NavigationButton( height: Dp = 58.dp, enabled: Boolean = true ) { - val isDarkTheme = isSystemInDarkTheme() - var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) } - val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500)) val haptics = LocalHapticFeedback.current - val scope = rememberCoroutineScope() + val interaction = remember { MutableInteractionSource() } + val scale by rememberBounceScale(interaction) Column { if (title != null) { - Box( - modifier = Modifier - .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)) - .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 4.dp) - ){ - Text( - text = title, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } + Text( + text = title, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 8.dp, bottom = 6.dp) + ) } - Row( + Surface( + onClick = { + if (enabled) { + haptics.performHapticFeedback(HapticFeedbackType.ContextClick) + if (onClick != null) onClick() else navController.navigate(to) + } + }, + enabled = enabled, + interactionSource = interaction, + shape = if (independent) MaterialTheme.shapes.large else RectangleShape, + color = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface, modifier = Modifier - .background( - animatedBackgroundColor, - RoundedCornerShape(if (independent) 28.dp else 0.dp) - ) - .height(height) - .pointerInput(Unit) { - detectTapGestures( - onPress = { - if (enabled) { - backgroundColor = - if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9) - tryAwaitRelease() - backgroundColor = - if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - } - }, - onTap = { - if (enabled) { - scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) } - if (onClick != null) onClick() else navController.navigate(to) - } - } - ) + .fillMaxWidth() + .graphicsLayer { + scaleX = scale + scaleY = scale } - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = name, - style = TextStyle( - fontSize = 16.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = if (isDarkTheme) Color.White else Color.Black, - ) - ) - Spacer(modifier = Modifier.weight(1f)) - if (currentState != null) { + Row( + modifier = Modifier + .heightIn(min = height) + .padding(horizontal = 20.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { Text( - text = currentState, - style = TextStyle( - fontSize = 16.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f), + text = name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.weight(1f)) + if (currentState != null) { + Text( + text = currentState, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) + Spacer(modifier = Modifier.width(6.dp)) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), ) } - Text( - text = "􀯻", - style = TextStyle( - fontSize = 16.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f) - ), - modifier = Modifier - .padding(start = if (currentState != null) 6.dp else 0.dp) - ) } if (description != null) { - Box( - modifier = Modifier - .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)) // because blur effect doesn't work for some reason - .padding(horizontal = 16.dp, vertical = 4.dp), - ) { - Text( - text = description, - style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Light, - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - // modifier = Modifier.padding(horizontal = 16.dp) - ) - } + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 6.dp) + ) } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt index 453c35c76..7383a3bd6 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt @@ -21,15 +21,13 @@ package me.kavishdevar.librepods.presentation.components import android.annotation.SuppressLint -import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column @@ -44,225 +42,129 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.imageResource 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.TextAlign import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import me.kavishdevar.librepods.R import me.kavishdevar.librepods.data.NoiseControlMode import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.roundToInt -@SuppressLint("UnspecifiedRegisterReceiverFlag", "UnusedBoxWithConstraintsScope") +private fun modeIconRes(mode: NoiseControlMode): Int = when (mode) { + NoiseControlMode.OFF -> R.drawable.noise_cancellation + NoiseControlMode.TRANSPARENCY -> R.drawable.transparency + NoiseControlMode.ADAPTIVE -> R.drawable.adaptive + NoiseControlMode.NOISE_CANCELLATION -> R.drawable.noise_cancellation +} + +/** + * Material 3 Expressive segmented noise-control selector: a vibrant primary "pill" that slides + * between modes with a low-damping (bouncy) spring and can be dragged. The adaptive mode shows a + * playful rainbow gradient pill. Signature & behaviour preserved from the original. + */ +@SuppressLint("UnusedBoxWithConstraintsScope") @Composable fun NoiseControlSettings( showOffListeningMode: Boolean, noiseControlModeValue: Int, onNoiseControlModeChanged: (Int) -> Unit ) { - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFE3E3E8) - val textColor = if (isDarkTheme) Color.White else Color.Black - val textColorSelected = if (isDarkTheme) Color.White else Color.Black - val selectedBackground = if (isDarkTheme) Color(0xBF5C5A5F) else Color(0xFFFFFFFF) - - - + val colorScheme = MaterialTheme.colorScheme val noiseControlMode = remember { mutableStateOf(NoiseControlMode.OFF) } - val d1a = remember { mutableFloatStateOf(0f) } - val d2a = remember { mutableFloatStateOf(0f) } - val d3a = remember { mutableFloatStateOf(0f) } - - // this function exists solely for the dividers, should get rid of it fun onModeSelected(mode: NoiseControlMode, received: Boolean = false) { val previousMode = noiseControlMode.value - val targetMode = if (!showOffListeningMode && mode == NoiseControlMode.OFF) { - NoiseControlMode.TRANSPARENCY + NoiseControlMode.TRANSPARENCY } else { mode } - noiseControlMode.value = targetMode - if (!received && targetMode != previousMode) onNoiseControlModeChanged(targetMode.ordinal + 1) - - - when (noiseControlMode.value) { - NoiseControlMode.NOISE_CANCELLATION -> { - d1a.floatValue = 1f - d2a.floatValue = 1f - d3a.floatValue = 0f - } - NoiseControlMode.OFF -> { - d1a.floatValue = 0f - d2a.floatValue = 1f - d3a.floatValue = 1f - } - NoiseControlMode.ADAPTIVE -> { - d1a.floatValue = 1f - d2a.floatValue = 0f - d3a.floatValue = 0f - } - NoiseControlMode.TRANSPARENCY -> { - d1a.floatValue = 0f - d2a.floatValue = 0f - d3a.floatValue = 1f - } - } } - val index = (noiseControlModeValue - 1).coerceIn(0, NoiseControlMode.entries.size - 1) noiseControlMode.value = NoiseControlMode.entries[index] - onModeSelected(noiseControlMode.value, received = true) - Box( - modifier = Modifier - .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)) - .padding(horizontal = 16.dp, vertical = 4.dp) - ){ - Text( - text = stringResource(R.string.noise_control), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) + val modes = remember(showOffListeningMode) { + buildList { + if (showOffListeningMode) add(NoiseControlMode.OFF) + add(NoiseControlMode.TRANSPARENCY) + add(NoiseControlMode.ADAPTIVE) + add(NoiseControlMode.NOISE_CANCELLATION) + } + } + + val adaptiveBrush = remember { + Brush.sweepGradient( + listOf( + colorScheme.primary, colorScheme.tertiary, colorScheme.secondary, + colorScheme.primary ) ) } + Text( + text = stringResource(R.string.noise_control), + style = MaterialTheme.typography.labelLarge, + color = colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 8.dp, bottom = 6.dp) + ) + BoxWithConstraints( modifier = Modifier .fillMaxWidth() - .padding(vertical = 8.dp) + .padding(vertical = 4.dp) ) { val density = LocalDensity.current - val buttonCount = if (showOffListeningMode) 4 else 3 + val buttonCount = modes.size val buttonWidth = maxWidth / buttonCount + val selectedSlot = modes.indexOf(noiseControlMode.value).coerceAtLeast(0) + val isDragging = remember { mutableStateOf(false) } var dragOffset by remember { - mutableFloatStateOf( - with(density) { - when(noiseControlMode.value) { - NoiseControlMode.OFF -> if (showOffListeningMode) 0f else buttonWidth.toPx() - NoiseControlMode.TRANSPARENCY -> if (showOffListeningMode) buttonWidth.toPx() else 0f - NoiseControlMode.ADAPTIVE -> if (showOffListeningMode) (buttonWidth * 2).toPx() else buttonWidth.toPx() - NoiseControlMode.NOISE_CANCELLATION -> if (showOffListeningMode) (buttonWidth * 3).toPx() else (buttonWidth * 2).toPx() - } - } - ) - } - - val animationSpec: AnimationSpec = SpringSpec( - dampingRatio = Spring.DampingRatioLowBouncy, - stiffness = Spring.StiffnessMediumLow, - visibilityThreshold = 0.01f - ) - - val targetOffset = buttonWidth * when(noiseControlMode.value) { - NoiseControlMode.OFF -> if (showOffListeningMode) 0 else 1 - NoiseControlMode.TRANSPARENCY -> if (showOffListeningMode) 1 else 0 - NoiseControlMode.ADAPTIVE -> if (showOffListeningMode) 2 else 1 - NoiseControlMode.NOISE_CANCELLATION -> if (showOffListeningMode) 3 else 2 + mutableFloatStateOf(with(density) { (buttonWidth * selectedSlot).toPx() }) } val animatedOffset by animateFloatAsState( targetValue = with(density) { - if (isDragging.value) dragOffset else targetOffset.toPx() + if (isDragging.value) dragOffset else (buttonWidth * selectedSlot).toPx() }, - animationSpec = animationSpec, + animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = 0.01f + ), label = "selector" ) - Column( - modifier = Modifier.fillMaxWidth() - ) { + Column(modifier = Modifier.fillMaxWidth()) { Box( modifier = Modifier .fillMaxWidth() - .height(60.dp) - .background(backgroundColor, RoundedCornerShape(28.dp)) + .height(64.dp) + .clip(RoundedCornerShape(32.dp)) + .background(colorScheme.surfaceContainerHighest) ) { - Row( - modifier = Modifier.fillMaxWidth() - ) { - if (showOffListeningMode) { - NoiseControlButton( - icon = ImageBitmap.imageResource(R.drawable.noise_cancellation), - onClick = { onModeSelected(NoiseControlMode.OFF) }, - textColor = if (noiseControlMode.value == NoiseControlMode.OFF) textColorSelected else textColor, - modifier = Modifier.weight(1f), - usePadding = false - ) - VerticalDivider( - thickness = 1.dp, - modifier = Modifier - .padding(vertical = 10.dp) - .alpha(d1a.floatValue), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) - ) - } - NoiseControlButton( - icon = ImageBitmap.imageResource(R.drawable.transparency), - onClick = { onModeSelected(NoiseControlMode.TRANSPARENCY) }, - textColor = if (noiseControlMode.value == NoiseControlMode.TRANSPARENCY) textColorSelected else textColor, - modifier = Modifier.weight(1f), - usePadding = false - ) - VerticalDivider( - thickness = 1.dp, - modifier = Modifier - .padding(vertical = 10.dp) - .alpha(d2a.floatValue), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) - ) - NoiseControlButton( - icon = ImageBitmap.imageResource(R.drawable.adaptive), - onClick = { onModeSelected(NoiseControlMode.ADAPTIVE) }, - textColor = if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) textColorSelected else textColor, - modifier = Modifier.weight(1f), - usePadding = false - ) - VerticalDivider( - thickness = 1.dp, - modifier = Modifier - .padding(vertical = 10.dp) - .alpha(d3a.floatValue), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) - ) - NoiseControlButton( - icon = ImageBitmap.imageResource(R.drawable.noise_cancellation), - onClick = { onModeSelected(NoiseControlMode.NOISE_CANCELLATION) }, - textColor = if (noiseControlMode.value == NoiseControlMode.NOISE_CANCELLATION) textColorSelected else textColor, - modifier = Modifier.weight(1f), - usePadding = false - ) - } - + // Sliding selection pill Box( modifier = Modifier .width(buttonWidth) @@ -281,23 +183,23 @@ fun NoiseControlSettings( onDragStopped = { isDragging.value = false val position = dragOffset / with(density) { buttonWidth.toPx() } - val newIndex = position.roundToInt() - val newMode = when(newIndex) { - 0 -> if (showOffListeningMode) NoiseControlMode.OFF else NoiseControlMode.TRANSPARENCY - 1 -> if (showOffListeningMode) NoiseControlMode.TRANSPARENCY else NoiseControlMode.ADAPTIVE - 2 -> if (showOffListeningMode) NoiseControlMode.ADAPTIVE else NoiseControlMode.NOISE_CANCELLATION - 3 -> NoiseControlMode.NOISE_CANCELLATION - else -> noiseControlMode.value // Keep current if index is invalid - } - onModeSelected(newMode) + val newSlot = position.roundToInt().coerceIn(0, buttonCount - 1) + onModeSelected(modes[newSlot]) } ) ) { Box( modifier = Modifier .fillMaxSize() - .padding(3.dp) - .background(selectedBackground, RoundedCornerShape(26.dp)) + .padding(4.dp) + .clip(RoundedCornerShape(28.dp)) + .then( + if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) { + Modifier.background(adaptiveBrush) + } else { + Modifier.background(colorScheme.primary) + } + ) ) } @@ -306,92 +208,43 @@ fun NoiseControlSettings( .fillMaxWidth() .zIndex(1f) ) { - if (showOffListeningMode) { + modes.forEach { mode -> + val selected = noiseControlMode.value == mode NoiseControlButton( - icon = ImageBitmap.imageResource(R.drawable.noise_cancellation), - onClick = { onModeSelected(NoiseControlMode.OFF) }, - textColor = if (noiseControlMode.value == NoiseControlMode.OFF) textColorSelected else textColor, + icon = ImageBitmap.imageResource(modeIconRes(mode)), + onClick = { onModeSelected(mode) }, + textColor = if (selected) colorScheme.onPrimary else colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f), usePadding = false ) - VerticalDivider( - thickness = 1.dp, - modifier = Modifier - .padding(vertical = 10.dp) - .alpha(d1a.floatValue), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) - ) } - NoiseControlButton( - icon = ImageBitmap.imageResource(R.drawable.transparency), - onClick = { onModeSelected(NoiseControlMode.TRANSPARENCY) }, - textColor = if (noiseControlMode.value == NoiseControlMode.TRANSPARENCY) textColorSelected else textColor, - modifier = Modifier.weight(1f), - usePadding = false - ) - VerticalDivider( - thickness = 1.dp, - modifier = Modifier - .padding(vertical = 10.dp) - .alpha(d2a.floatValue), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) - ) - NoiseControlButton( - icon = ImageBitmap.imageResource(R.drawable.adaptive), - onClick = { onModeSelected(NoiseControlMode.ADAPTIVE) }, - textColor = if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) textColorSelected else textColor, - modifier = Modifier.weight(1f), - usePadding = false - ) - VerticalDivider( - thickness = 1.dp, - modifier = Modifier - .padding(vertical = 10.dp) - .alpha(d3a.floatValue), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) - ) - NoiseControlButton( - icon = ImageBitmap.imageResource(R.drawable.noise_cancellation), - onClick = { onModeSelected(NoiseControlMode.NOISE_CANCELLATION) }, - textColor = if (noiseControlMode.value == NoiseControlMode.NOISE_CANCELLATION) textColorSelected else textColor, - modifier = Modifier.weight(1f), - usePadding = false - ) } } Row( modifier = Modifier .fillMaxWidth() - .padding(top = 4.dp) + .padding(top = 6.dp) ) { - if (showOffListeningMode) { + modes.forEach { mode -> + val selected = noiseControlMode.value == mode Text( - text = stringResource(R.string.off), - style = TextStyle(fontSize = 12.sp, color = textColor), + text = stringResource(modeLabelRes(mode)), + style = MaterialTheme.typography.labelMedium, + color = if (selected) colorScheme.onSurface else colorScheme.onSurfaceVariant, + fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal, textAlign = TextAlign.Center, modifier = Modifier.weight(1f) ) } - Text( - text = stringResource(R.string.transparency), - style = TextStyle(fontSize = 12.sp, color = textColor), - textAlign = TextAlign.Center, - modifier = Modifier.weight(1f) - ) - Text( - text = stringResource(R.string.adaptive), - style = TextStyle(fontSize = 12.sp, color = textColor), - textAlign = TextAlign.Center, - modifier = Modifier.weight(1f) - ) - Text( - text = stringResource(R.string.noise_cancellation), - style = TextStyle(fontSize = 12.sp, color = textColor), - textAlign = TextAlign.Center, - modifier = Modifier.weight(1f) - ) } } } } + +private fun modeLabelRes(mode: NoiseControlMode): Int = when (mode) { + NoiseControlMode.OFF -> R.string.off + NoiseControlMode.TRANSPARENCY -> R.string.transparency + NoiseControlMode.ADAPTIVE -> R.string.adaptive + NoiseControlMode.NOISE_CANCELLATION -> R.string.noise_cancellation +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt index a29ea725c..1cc07b979 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt @@ -18,26 +18,17 @@ package me.kavishdevar.librepods.presentation.components -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color 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.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController import me.kavishdevar.librepods.R import me.kavishdevar.librepods.data.StemAction @@ -48,10 +39,6 @@ fun PressAndHoldSettings( leftAction: StemAction, rightAction: StemAction ) { - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val dividerColor = Color(0x40888888) - val leftActionText = when (leftAction) { StemAction.CYCLE_NOISE_CONTROL_MODES -> stringResource(R.string.noise_control) StemAction.DIGITAL_ASSISTANT -> "Digital Assistant" @@ -63,46 +50,39 @@ fun PressAndHoldSettings( StemAction.DIGITAL_ASSISTANT -> "Digital Assistant" else -> "INVALID!!" } - Box( - modifier = Modifier - .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)) - .padding(horizontal = 16.dp, vertical = 4.dp) - ){ - Text( - text = stringResource(R.string.press_and_hold_airpods), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } - Column( - modifier = Modifier - .fillMaxWidth() - .background(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF), RoundedCornerShape(28.dp)) - .clip(RoundedCornerShape(28.dp)) + + Text( + text = stringResource(R.string.press_and_hold_airpods), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 8.dp, bottom = 6.dp) + ) + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier.fillMaxWidth() ) { - NavigationButton( - to = "long_press/Left", - name = stringResource(R.string.left), - navController = navController, - independent = false, - currentState = leftActionText, - ) - HorizontalDivider( - thickness = 1.dp, - color = dividerColor, - modifier = Modifier - .padding(horizontal = 16.dp) - ) - NavigationButton( - to = "long_press/Right", - name = stringResource(R.string.right), - navController = navController, - independent = false, - currentState = rightActionText, - ) + Column { + NavigationButton( + to = "long_press/Left", + name = stringResource(R.string.left), + navController = navController, + independent = false, + currentState = leftActionText, + ) + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier + .padding(horizontal = 20.dp) + ) + NavigationButton( + to = "long_press/Right", + name = stringResource(R.string.right), + navController = navController, + independent = false, + currentState = rightActionText, + ) + } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledBottomSheet.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledBottomSheet.kt index b98139810..e6f253e94 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledBottomSheet.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledBottomSheet.kt @@ -1,28 +1,23 @@ package me.kavishdevar.librepods.presentation.components import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetValue +import androidx.compose.material3.Surface import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import com.kyant.backdrop.backdrops.LayerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.effects.blur -import com.kyant.backdrop.effects.lens -import com.kyant.backdrop.effects.vibrancy @OptIn(ExperimentalMaterial3Api::class) @@ -35,10 +30,9 @@ fun StyledBottomSheet( ) { if (!visible) return - val isDarkTheme = isSystemInDarkTheme() val sheetState = rememberModalBottomSheetState(false) // move this to parent composable - val isExpanded = sheetState.targetValue == SheetValue.Expanded + val isExpanded = sheetState.targetValue == SheetValue.Expanded val progress by animateFloatAsState( targetValue = if (isExpanded) 1f else 0f, @@ -52,36 +46,24 @@ fun StyledBottomSheet( sheetState = sheetState, containerColor = Color.Transparent, dragHandle = { }, - shape = RoundedCornerShape(animatedCorner), - scrimColor = Color.Transparent, + shape = MaterialTheme.shapes.extraLarge, modifier = Modifier.padding(4.dp) ) { val innerBackdrop = rememberLayerBackdrop() - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(animatedCorner)) - .drawBackdrop( - backdrop = backdrop, - exportedBackdrop = innerBackdrop, - shape = { RoundedCornerShape(animatedCorner) }, - effects = { - vibrancy() - blur(4f.dp.toPx()) - lens(12f.dp.toPx(), 48f.dp.toPx(), true) - }, - onDrawSurface = { - drawRect( - if (isDarkTheme) Color.DarkGray.copy(alpha = 0.3f) else Color( - 0xFFE0E0E0 - ).copy(alpha = 0.45f) - ) - } - ) - .padding(top = 24.dp) - .padding(horizontal = 16.dp) + Surface( + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth() ) { - content(innerBackdrop, progress) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp) + .padding(horizontal = 16.dp) + ) { + content(innerBackdrop, progress) + } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt index 6c60148fc..49ed6a3b2 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt @@ -18,312 +18,69 @@ package me.kavishdevar.librepods.presentation.components -import android.graphics.RuntimeShader -import android.os.Build -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.VectorConverter -import androidx.compose.animation.core.VisibilityThreshold -import androidx.compose.animation.core.spring -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.heightIn +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -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.geometry.Offset -import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.isSpecified -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastCoerceAtMost -import androidx.compose.ui.util.fastCoerceIn -import androidx.compose.ui.util.lerp import com.kyant.backdrop.Backdrop -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.effects.blur -import com.kyant.backdrop.effects.lens -import com.kyant.backdrop.effects.vibrancy -import com.kyant.backdrop.highlight.Highlight -import kotlinx.coroutines.launch -import me.kavishdevar.librepods.utils.inspectDragGestures -import kotlin.math.abs -import kotlin.math.atan2 -import kotlin.math.cos -import kotlin.math.sin -import kotlin.math.tanh +/** + * Material 3 Expressive filled button with a springy press. Signature preserved from the old + * liquid-glass button; [backdrop], [isInteractive] and [maxScale] are kept for source compatibility + * but are no longer used. [surfaceColor] overrides the container; [tint] overrides the content color. + */ @Composable fun StyledButton( onClick: () -> Unit, - backdrop: Backdrop, + @Suppress("UNUSED_PARAMETER") backdrop: Backdrop, modifier: Modifier = Modifier, - isInteractive: Boolean = true, + @Suppress("UNUSED_PARAMETER") isInteractive: Boolean = true, tint: Color = Color.Unspecified, surfaceColor: Color = Color.Unspecified, - maxScale: Float = 0.1f, + @Suppress("UNUSED_PARAMETER") maxScale: Float = 0.1f, enabled: Boolean = true, content: @Composable RowScope.() -> Unit, ) { - val isInteractive = enabled && isInteractive - val scope = rememberCoroutineScope() val haptics = LocalHapticFeedback.current - val progressAnimation = remember { Animatable(0f) } - var pressStartPosition by remember { mutableStateOf(Offset.Zero) } - val offsetAnimation = remember { Animatable(Offset.Zero, Offset.VectorConverter) } - var isPressed by remember { mutableStateOf(false) } + val interaction = remember { MutableInteractionSource() } + val scale by rememberBounceScale(interaction) - val interactiveHighlightShader = remember { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - RuntimeShader( - """ -uniform float2 size; -layout(color) uniform half4 color; -uniform float radius; -uniform float2 offset; + val container = if (surfaceColor.isSpecified) surfaceColor else MaterialTheme.colorScheme.secondaryContainer + val onContainer = if (tint.isSpecified) tint else contentColorFor(container) -half4 main(float2 coord) { - float2 center = offset; - float dist = distance(coord, center); - float intensity = smoothstep(radius, radius * 0.5, dist); - return color * intensity; -}""" - ) - } else { - null - } - } - - Row( - modifier - .then( - if (!isInteractive) { - Modifier.drawBackdrop( - backdrop = backdrop, - shape = { RoundedCornerShape(28f.dp) }, - effects = { - blur(16f.dp.toPx()) - }, - layerBlock = null, - onDrawSurface = { - if (tint.isSpecified) { - drawRect(tint, blendMode = BlendMode.Hue) - drawRect(tint.copy(alpha = 0.75f)) - } else { - drawRect(Color.White.copy(0.1f)) - } - if (surfaceColor.isSpecified && enabled) { - val color = if (isPressed) { - Color( - red = surfaceColor.red * 0.5f, - green = surfaceColor.green * 0.5f, - blue = surfaceColor.blue * 0.5f, - alpha = surfaceColor.alpha - ) - } else { - surfaceColor - } - drawRect(color) - } else { - if (isPressed) { - drawRect(Color.Black.copy(alpha = 0.4f)) - drawRect(Color.White.copy(alpha = 0.2f)) - } - } - }, - onDrawFront = null, - highlight = { Highlight.Ambient.copy(alpha = 0f) } - ) - } else { - Modifier.drawBackdrop( - backdrop = backdrop, - shape = { RoundedCornerShape(28f.dp) }, - effects = { - vibrancy() - blur(2f.dp.toPx()) - lens( - refractionHeight = 12f.dp.toPx(), - refractionAmount = 24f.dp.toPx(), - depthEffect = true, - chromaticAberration = true - ) - }, - layerBlock = { - val width = size.width - val height = size.height - - val progress = progressAnimation.value - val scale = lerp(1f, 1f + maxScale, progress) - - val maxOffset = size.minDimension - val initialDerivative = 0.05f - val offset = offsetAnimation.value - translationX = - maxOffset * tanh(initialDerivative * offset.x / maxOffset) - translationY = - maxOffset * tanh(initialDerivative * offset.y / maxOffset) - - val maxDragScale = 0.1f - val offsetAngle = atan2(offset.y, offset.x) - scaleX = - scale + - maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) * - (width / height).fastCoerceAtMost(1f) - scaleY = - scale + - maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) * - (height / width).fastCoerceAtMost(1f) - }, - onDrawSurface = { - if (tint.isSpecified) { - drawRect(tint, blendMode = BlendMode.Hue) - drawRect(tint.copy(alpha = 0.75f)) - } else { - drawRect(Color.White.copy(0.1f)) - } - if (surfaceColor.isSpecified) { - val color = if (!isInteractive && isPressed) { - Color( - red = surfaceColor.red * 0.5f, - green = surfaceColor.green * 0.5f, - blue = surfaceColor.blue * 0.5f, - alpha = surfaceColor.alpha - ) - } else { - surfaceColor - } - drawRect(color) - } - }, - onDrawFront = { - val progress = progressAnimation.value.fastCoerceIn(0f, 1f) - if (progress > 0f) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && interactiveHighlightShader != null) { - drawRect( - Color.White.copy(0.1f * progress), - blendMode = BlendMode.Plus - ) - interactiveHighlightShader.apply { - val offset = pressStartPosition + offsetAnimation.value - setFloatUniform("size", size.width, size.height) - setColorUniform( - "color", - Color.White.copy(0.15f * progress).toArgb() - ) - setFloatUniform("radius", size.maxDimension) - setFloatUniform( - "offset", - offset.x.fastCoerceIn(0f, size.width), - offset.y.fastCoerceIn(0f, size.height) - ) - } - drawRect( - ShaderBrush(interactiveHighlightShader), - blendMode = BlendMode.Plus - ) - } else { - drawRect( - Color.White.copy(0.25f * progress), - blendMode = BlendMode.Plus - ) - } - } - } - ) - } - ) - .clickable( - interactionSource = null, - indication = null, - role = Role.Button, - onClick = { - if (enabled) { - haptics.performHapticFeedback(HapticFeedbackType.ContextClick) - onClick() - } - } - ) - .then( - if (isInteractive) { - Modifier.pointerInput(scope) { - val progressAnimationSpec = spring(0.5f, 300f, 0.001f) - val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold) - val onDragStop: () -> Unit = { - scope.launch { - launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) } - launch { progressAnimation.animateTo(0f, progressAnimationSpec) } - launch { - offsetAnimation.animateTo( - Offset.Zero, - offsetAnimationSpec - ) - } - } - } - inspectDragGestures( - onDragStart = { down -> - pressStartPosition = down.position - scope.launch { - launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) } - launch { - progressAnimation.animateTo( - 1f, - progressAnimationSpec - ) - } - launch { offsetAnimation.snapTo(Offset.Zero) } - } - }, - onDragEnd = { - onDragStop() - }, - onDragCancel = onDragStop - ) { _, dragAmount -> - scope.launch { - if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback( - HapticFeedbackType.SegmentFrequentTick - ) - offsetAnimation.snapTo(offsetAnimation.value + dragAmount) - } - } - } - } else { - Modifier.pointerInput(Unit) { - detectTapGestures( - onPress = { - isPressed = true - tryAwaitRelease() - isPressed = false - }, - onTap = { - if (enabled) { - haptics.performHapticFeedback(HapticFeedbackType.ContextClick) - onClick() - } - } - ) - } - } - ) - .height(48f.dp) - .padding(horizontal = 16f.dp), - horizontalArrangement = Arrangement.spacedBy(8f.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, - content = content + Button( + onClick = { + if (enabled) { + haptics.performHapticFeedback(HapticFeedbackType.ContextClick) + onClick() + } + }, + enabled = enabled, + interactionSource = interaction, + shape = MaterialTheme.shapes.large, + colors = ButtonDefaults.buttonColors( + containerColor = container, + contentColor = onContainer, + ), + modifier = modifier + .heightIn(min = 56.dp) + .graphicsLayer { + scaleX = scale + scaleY = scale + }, + content = content, ) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledDropdown.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledDropdown.kt index 4446f0856..3f12367ea 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledDropdown.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledDropdown.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -36,13 +35,13 @@ import androidx.compose.foundation.layout.Row 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.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -60,22 +59,13 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup -import dev.chrisbanes.haze.HazeEffectScope import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.HazeTint -import dev.chrisbanes.haze.hazeEffect -import dev.chrisbanes.haze.materials.CupertinoMaterials import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import kotlinx.coroutines.launch -import me.kavishdevar.librepods.R @ExperimentalHazeMaterialsApi @Composable @@ -89,7 +79,7 @@ fun StyledDropdown( onOptionSelected: (String) -> Unit, externalHoveredIndex: Int? = null, externalDragActive: Boolean = false, - hazeState: HazeState, + @Suppress("UNUSED_PARAMETER") hazeState: HazeState, @SuppressLint("ModifierParameter") modifier: Modifier = Modifier ) { if (expanded) { @@ -103,13 +93,15 @@ fun StyledDropdown( enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(), exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut() ) { - Card( + Surface( modifier = modifier .padding(8.dp) .width(300.dp) - .background(Color.Transparent) - .clip(RoundedCornerShape(8.dp)), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + .clip(MaterialTheme.shapes.large), + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + tonalElevation = 3.dp, + shadowElevation = 4.dp ) { var hoveredIndex by remember { mutableStateOf(null) } val itemHeight = 48.dp @@ -176,13 +168,17 @@ fun StyledDropdown( } else { index == hoveredIndex } - val isSystemInDarkTheme = isSystemInDarkTheme() + val isSelected = text == selectedOption Box( modifier = Modifier .fillMaxWidth() .height(itemHeight) .background( - Color.Transparent + if (isHovered) { + MaterialTheme.colorScheme.surfaceContainerHighest + } else { + Color.Transparent + } ) .clickable( interactionSource = remember { MutableInteractionSource() }, @@ -192,23 +188,7 @@ fun StyledDropdown( onOptionSelected(text) onDismissRequest() } - .hazeEffect( - state = hazeState, - style = CupertinoMaterials.regular(), - block = fun HazeEffectScope.() { - alpha = 1f - backgroundColor = if (isSystemInDarkTheme) { - Color(0xB02C2C2E) - } else { - Color(0xB0FFFFFF) - } - tints = if (isHovered) listOf( - HazeTint( - color = if (isSystemInDarkTheme) Color(0x338A8A8A) else Color(0x40D9D9D9) - ) - ) else listOf() - }) - .padding(horizontal = 12.dp), + .padding(horizontal = 16.dp), contentAlignment = Alignment.CenterStart ) { Row( @@ -217,37 +197,26 @@ fun StyledDropdown( verticalAlignment = Alignment.CenterVertically ) { Text( - text, - style = TextStyle( - fontSize = 16.sp, - color = if (isSystemInDarkTheme()) Color.White else Color.Black.copy(alpha = 0.75f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + text = text, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface ) - Checkbox( - checked = text == selectedOption, - onCheckedChange = { onOptionSelected(text) }, - colors = CheckboxDefaults.colors().copy( - checkedCheckmarkColor = Color(0xFF007AFF), - uncheckedCheckmarkColor = Color.Transparent, - checkedBoxColor = Color.Transparent, - uncheckedBoxColor = Color.Transparent, - checkedBorderColor = Color.Transparent, - uncheckedBorderColor = Color.Transparent, - disabledBorderColor = Color.Transparent, - disabledCheckedBoxColor = Color.Transparent, - disabledUncheckedBoxColor = Color.Transparent, - disabledUncheckedBorderColor = Color.Transparent + if (isSelected) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) ) - ) + } } } if (index != options.lastIndex) { - HorizontalDivider( + androidx.compose.material3.HorizontalDivider( thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(start = 12.dp, end = 0.dp) + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(start = 16.dp, end = 0.dp) ) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledIconButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledIconButton.kt index 6937587a2..1dbcce842 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledIconButton.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledIconButton.kt @@ -18,289 +18,71 @@ package me.kavishdevar.librepods.presentation.components -import android.graphics.RuntimeShader -import android.os.Build -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.VectorConverter -import androidx.compose.animation.core.VisibilityThreshold -import androidx.compose.animation.core.spring -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.BlurEffect import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ShaderBrush -import androidx.compose.ui.graphics.TileMode -import androidx.compose.ui.graphics.drawOutline -import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.isSpecified -import androidx.compose.ui.graphics.layer.CompositingStrategy -import androidx.compose.ui.graphics.layer.drawLayer -import androidx.compose.ui.graphics.rememberGraphicsLayer -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback -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.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.util.fastCoerceAtMost -import androidx.compose.ui.util.fastCoerceIn -import androidx.compose.ui.util.lerp import com.kyant.backdrop.backdrops.LayerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.effects.lens -import com.kyant.backdrop.highlight.Highlight -import com.kyant.backdrop.shadow.Shadow -import kotlinx.coroutines.launch -import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.utils.inspectDragGestures -import kotlin.math.abs -import kotlin.math.atan2 -import kotlin.math.cos -import kotlin.math.sin -import kotlin.math.tanh +/** + * Material 3 Expressive tonal icon button. Replaces the old SF-Pro glyph icon button: the [icon] + * string (an SF Symbol glyph) is converted to a Material [androidx.compose.ui.graphics.vector.ImageVector] + * via [sfSymbolToIcon]. The [backdrop] parameter is retained for source compatibility but unused. + */ @Composable fun StyledIconButton( modifier: Modifier = Modifier, icon: String, iconTint: Color = Color.Unspecified, surfaceColor: Color = Color.Unspecified, - backdrop: LayerBackdrop = rememberLayerBackdrop(), + @Suppress("UNUSED_PARAMETER") backdrop: LayerBackdrop = rememberLayerBackdrop(), onClick: () -> Unit, enabled: Boolean = true ) { val haptics = LocalHapticFeedback.current - val darkMode = isSystemInDarkTheme() - val scope = rememberCoroutineScope() - val progressAnimationSpec = spring(0.5f, 300f, 0.001f) - val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold) - val progressAnimation = remember { Animatable(0f) } - val offsetAnimation = remember { Animatable(Offset.Zero, Offset.VectorConverter) } - var pressStartPosition by remember { mutableStateOf(Offset.Zero) } - val innerShadowLayer = rememberGraphicsLayer().apply { - compositingStrategy = CompositingStrategy.Offscreen - } - val density = LocalDensity.current + val interaction = remember { MutableInteractionSource() } + val scale by rememberBounceScale(interaction) + val vector = sfSymbolToIcon(icon) - val interactiveHighlightShader = remember { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - RuntimeShader( - """ -uniform float2 size; -layout(color) uniform half4 color; -uniform float radius; -uniform float2 offset; + val containerColor = + if (surfaceColor.isSpecified) surfaceColor else MaterialTheme.colorScheme.surfaceContainerHighest + val contentColor = + if (iconTint.isSpecified) iconTint else MaterialTheme.colorScheme.onSurface -half4 main(float2 coord) { - float2 center = offset; - float dist = distance(coord, center); - float intensity = smoothstep(radius, radius * 0.5, dist); - return color * intensity; -}""" - ) - } else { - null - } - } - val isDarkTheme = isSystemInDarkTheme() - TextButton( + FilledTonalIconButton( onClick = { if (enabled) { - scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) } + haptics.performHapticFeedback(HapticFeedbackType.ContextClick) onClick() } }, - shape = RoundedCornerShape(56.dp), - modifier = modifier - .padding(horizontal = 12.dp) - .drawBackdrop( - backdrop = backdrop, - shape = { RoundedCornerShape(56.dp) }, - highlight = { Highlight.Ambient.copy(alpha = if (isDarkTheme) 1f else 0f) }, - shadow = { - Shadow( - radius = 12f.dp, - color = Color.Black.copy(if (isDarkTheme) 0.08f else 0.2f) - ) - }, - layerBlock = { - if (!enabled) return@drawBackdrop - val width = size.width - val height = size.height - - val progress = progressAnimation.value - val scale = lerp(1f, 1.5f, progress) - - val maxOffset = size.minDimension - val initialDerivative = 0.05f - val offset = offsetAnimation.value - translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset) - translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset) - - val maxDragScale = 0.1f - val offsetAngle = atan2(offset.y, offset.x) - scaleX = - scale + - maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) * - (width / height).fastCoerceAtMost(1f) - scaleY = - scale + - maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) * - (height / width).fastCoerceAtMost(1f) - }, - onDrawSurface = { - if (!enabled) { - drawRect( - (if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy(0.5f) - ) - return@drawBackdrop - } - val progress = progressAnimation.value.coerceIn(0f, 1f) - - val shape = RoundedCornerShape(56.dp) - val outline = shape.createOutline(size, layoutDirection, this) - val innerShadowOffset = 4f.dp.toPx() - val innerShadowBlurRadius = 4f.dp.toPx() - - innerShadowLayer.alpha = progress - innerShadowLayer.renderEffect = - BlurEffect( - innerShadowBlurRadius, - innerShadowBlurRadius, - TileMode.Decal - ) - innerShadowLayer.record { - drawOutline(outline, Color.Black.copy(0.2f)) - translate(0f, innerShadowOffset) { - drawOutline( - outline, - Color.Transparent, - blendMode = BlendMode.Clear - ) - } - } - drawLayer(innerShadowLayer) - - if (surfaceColor.isSpecified) { - drawRect(surfaceColor) - } - - drawRect( - (if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy( - progress.coerceIn( - 0.15f, - 0.35f - ) - ) - ) - }, - onDrawFront = { - if (!enabled) return@drawBackdrop - val progress = progressAnimation.value.fastCoerceIn(0f, 1f) - if (progress > 0f) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && interactiveHighlightShader != null) { - drawRect( - Color.White.copy(0.1f * progress), - blendMode = BlendMode.Plus - ) - interactiveHighlightShader.apply { - val offset = pressStartPosition + offsetAnimation.value - setFloatUniform("size", size.width, size.height) - setColorUniform( - "color", - Color.White.copy(0.15f * progress).toArgb() - ) - setFloatUniform("radius", size.maxDimension) - setFloatUniform( - "offset", - offset.x.fastCoerceIn(0f, size.width), - offset.y.fastCoerceIn(0f, size.height) - ) - } - drawRect( - ShaderBrush(interactiveHighlightShader), - blendMode = BlendMode.Plus - ) - } else { - drawRect( - Color.White.copy(0.25f * progress), - blendMode = BlendMode.Plus - ) - } - } - }, - effects = { - lens( - refractionHeight = 6f.dp.toPx(), - refractionAmount = size.height / 2f, - depthEffect = true, - chromaticAberration = true - ) - }, - ) - .pointerInput(scope) { - val onDragStop: () -> Unit = { - if (enabled) { - scope.launch { - launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) } - launch { progressAnimation.animateTo(0f, progressAnimationSpec) } - launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) } - } - } - } - inspectDragGestures( - onDragStart = { down -> - if (enabled) { - pressStartPosition = down.position - scope.launch { - launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) } - launch { progressAnimation.animateTo(1f, progressAnimationSpec) } - launch { offsetAnimation.snapTo(Offset.Zero) } - } - } - }, - onDragEnd = { onDragStop() }, - onDragCancel = onDragStop - ) { _, dragAmount -> - scope.launch { - if (enabled) { - if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback( - HapticFeedbackType.SegmentFrequentTick - ) - offsetAnimation.snapTo(offsetAnimation.value + dragAmount) - } - } - } - } - .size(with(density) { 48.sp.toDp() }), + enabled = enabled, + interactionSource = interaction, + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = containerColor, + contentColor = contentColor, + ), + modifier = modifier.graphicsLayer { + scaleX = scale + scaleY = scale + }, ) { - Text( - text = icon, - style = TextStyle( - fontSize = 20.sp, - fontWeight = FontWeight.Normal, - color = if (iconTint.isSpecified) iconTint else if (darkMode) Color.White else Color.Black, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) + if (vector != null) { + Icon(imageVector = vector, contentDescription = null) + } else { + Text(text = icon) + } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledInputField.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledInputField.kt index 86eae6a68..a7c0670f1 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledInputField.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledInputField.kt @@ -1,12 +1,9 @@ package me.kavishdevar.librepods.presentation.components -import android.R.attr.singleLine import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.updateTransition -import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -15,12 +12,17 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -28,17 +30,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity -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.unit.dp import androidx.compose.ui.unit.sp -import me.kavishdevar.librepods.R @Composable @@ -48,13 +44,11 @@ fun StyledInputField( placeholder: String = "", singleLine: Boolean = true ){ - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color.Black val minHeight = if (singleLine) 58.dp else 120.dp val verticalAlignment = if (singleLine) Alignment.CenterVertically else Alignment.Top val hasText = inputState.text.isNotEmpty() val density = LocalDensity.current + val cursorColor = MaterialTheme.colorScheme.primary val spacerHeight by animateDpAsState( targetValue = if (hasText) with(density) { 32.sp.toDp() } else 0.dp, label = "labelSpacer" @@ -70,85 +64,78 @@ fun StyledInputField( Box( modifier = Modifier.fillMaxWidth() ) { - Row( - verticalAlignment = verticalAlignment, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = minHeight) - .background( - backgroundColor, - RoundedCornerShape(28.dp) - ) - .padding(horizontal = 16.dp, vertical = 8.dp) - .pointerInput(Unit) { - detectTapGestures { - focusRequester.requestFocus() - } - } + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth() ) { - BasicTextField( - state = inputState, - lineLimits = if (singleLine) TextFieldLineLimits.SingleLine else TextFieldLineLimits.Default, - textStyle = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - cursorBrush = SolidColor(textColor), - decorator = { innerTextField -> - Row( - modifier = Modifier.padding(top = if (singleLine) 0.dp else 16.dp), - verticalAlignment = verticalAlignment, - ) { + Row( + verticalAlignment = verticalAlignment, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = minHeight) + .padding(horizontal = 16.dp, vertical = 8.dp) + .pointerInput(Unit) { + detectTapGestures { + focusRequester.requestFocus() + } + } + ) { + BasicTextField( + state = inputState, + lineLimits = if (singleLine) TextFieldLineLimits.SingleLine else TextFieldLineLimits.Default, + textStyle = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface + ), + cursorBrush = SolidColor(cursorColor), + decorator = { innerTextField -> Row( - modifier = Modifier - .weight(1f) + modifier = Modifier.padding(top = if (singleLine) 0.dp else 16.dp), + verticalAlignment = verticalAlignment, ) { - Box( + Row( modifier = Modifier - .weight(1f), - contentAlignment = if (singleLine) Alignment.CenterStart else Alignment.TopStart + .weight(1f) ) { - Text( - text = placeholder, - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Light, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor.copy(alpha = 0.8f) - ), + Box( modifier = Modifier - .offset(y = yOffset) - ) + .weight(1f), + contentAlignment = if (singleLine) Alignment.CenterStart else Alignment.TopStart + ) { + Text( + text = placeholder, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .offset(y = yOffset) + ) - innerTextField() + innerTextField() + } } - } - if (singleLine && !inputState.text.isEmpty()) { - IconButton( - onClick = { - inputState.clearText() + if (singleLine && !inputState.text.isEmpty()) { + IconButton( + onClick = { + inputState.clearText() + } + ) { + Icon( + imageVector = Icons.Filled.Cancel, + contentDescription = "Clear text", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) } - ) { - Text( - text = "􀁡", - style = TextStyle( - fontSize = 16.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy( - alpha = 0.6f - ) - ), - ) } } - } - }, - modifier = Modifier - .fillMaxWidth() - .padding(start = 8.dp) - .focusRequester(focusRequester) - ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp) + .focusRequester(focusRequester) + ) + } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledScaffold.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledScaffold.kt index 6376188ca..e2b3eef96 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledScaffold.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledScaffold.kt @@ -18,47 +18,37 @@ package me.kavishdevar.librepods.presentation.components -import androidx.compose.foundation.isSystemInDarkTheme 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.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalLayoutDirection -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.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex import com.kyant.backdrop.backdrops.LayerBackdrop -import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import dev.chrisbanes.haze.HazeProgressive import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.HazeTint -import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.rememberHazeState -import me.kavishdevar.librepods.R +/** + * Material 3 Expressive screen scaffold: an edge-to-edge [Scaffold] with a [CenterAlignedTopAppBar]. + * + * The content lambda still receives `(spacerValue, hazeState, bottomPadding)` so existing screens + * compile unchanged. Because the M3 top app bar reserves its own space, `spacerValue` is now `0.dp` + * (the leading spacer screens add becomes a no-op). The [HazeState] and the action-button + * [LayerBackdrop] are still provided for source compatibility, though the new design doesn't blur. + */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun StyledScaffold( title: String, @@ -66,70 +56,43 @@ fun StyledScaffold( snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, content: @Composable (spacerValue: Dp, hazeState: HazeState, bottomPadding: Dp) -> Unit ) { - val isDarkTheme = isSystemInDarkTheme() - val hazeState = rememberHazeState(blurEnabled = true) + val hazeState = rememberHazeState() + val backdrop = rememberLayerBackdrop() Scaffold( - containerColor = if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7), + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, snackbarHost = { SnackbarHost(snackbarHostState) }, - modifier = Modifier - .then(if (!isDarkTheme) Modifier.shadow(elevation = 36.dp, shape = RoundedCornerShape(52.dp), ambientColor = Color.Black, spotColor = Color.Black) else Modifier) - .clip(RoundedCornerShape(52.dp)) - ) { paddingValues -> - val topPadding = paddingValues.calculateTopPadding() - val bottomPadding = paddingValues.calculateBottomPadding() + 16.dp - val startPadding = paddingValues.calculateLeftPadding(LocalLayoutDirection.current) - val endPadding = paddingValues.calculateRightPadding(LocalLayoutDirection.current) - + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + actions = { actionButtons.forEach { it(backdrop) } }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + scrolledContainerColor = MaterialTheme.colorScheme.background, + titleContentColor = MaterialTheme.colorScheme.onBackground, + actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + }, + ) { innerPadding -> Box( modifier = Modifier .fillMaxSize() - .padding(start = startPadding, end = endPadding) + .padding(top = innerPadding.calculateTopPadding()) ) { - val backdrop = rememberLayerBackdrop() - Box( - modifier = Modifier - .zIndex(2f) - .height(64.dp + topPadding) - .fillMaxWidth() - .layerBackdrop(backdrop) - .hazeEffect(state = hazeState) { - tints = listOf(HazeTint(color = if (isDarkTheme) Color.Black else Color.White)) - progressive = HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) - } - ) { - Column(modifier = Modifier.fillMaxSize()) { - Spacer(modifier = Modifier.height(topPadding + 12.dp)) - Text( - text = title, - style = TextStyle( - fontSize = 20.sp, - fontWeight = FontWeight.Medium, - color = if (isDarkTheme) Color.White else Color.Black, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - } - } - Row( - modifier = Modifier - .zIndex(3f) - .padding(top = topPadding, end = 8.dp) - .align(Alignment.TopEnd) - ) { - actionButtons.forEach { actionButton -> - actionButton(backdrop) - } - } - - content(topPadding + 64.dp, hazeState, bottomPadding + 12.dp) + content(0.dp, hazeState, innerPadding.calculateBottomPadding() + 16.dp) } } } - @Composable fun StyledScaffold( title: String, @@ -141,7 +104,7 @@ fun StyledScaffold( title = title, actionButtons = actionButtons, snackbarHostState = snackbarHostState, - ) { _, _, _-> + ) { _, _, _ -> content() } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt index b9f6c2f70..3e5cb051b 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt @@ -18,42 +18,36 @@ package me.kavishdevar.librepods.presentation.components -import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface 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.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import me.kavishdevar.librepods.R data class SelectItem( val name: String, @@ -65,121 +59,108 @@ data class SelectItem( val enabled: Boolean = true ) +/** + * Material 3 Expressive single-select list. A tonal [Surface] card whose rows spring on press and + * mark the current choice with a Material check icon that fades in. Each row is flush (square) and + * the whole group is rounded into one [MaterialTheme.shapes.large] card. + */ @Composable fun StyledSelectList( items: List, modifier: Modifier = Modifier ) { - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color.Black - val haptics = LocalHapticFeedback.current - Column( - modifier = modifier - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)), - horizontalAlignment = Alignment.CenterHorizontally + Surface( + modifier = modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, ) { - val visibleItems = items.filter { it.visible } - visibleItems.forEachIndexed { index, item -> - val isFirst = index == 0 - val isLast = index == visibleItems.size - 1 - val hasIcon = item.iconRes != null + Column(horizontalAlignment = Alignment.CenterHorizontally) { + val visibleItems = items.filter { it.visible } + visibleItems.forEachIndexed { index, item -> + val isLast = index == visibleItems.size - 1 + val hasIcon = item.iconRes != null - val shape = when { - isFirst && isLast -> RoundedCornerShape(28.dp) - isFirst -> RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp) - isLast -> RoundedCornerShape(bottomStart = 28.dp, bottomEnd = 28.dp) - else -> RoundedCornerShape(0.dp) - } - var itemBackgroundColor by remember { mutableStateOf(if (item.enabled) backgroundColor else if (isDarkTheme) Color(0x40050505) else Color(0x40D9D9D9)) } - val animatedBackgroundColor by animateColorAsState(targetValue = itemBackgroundColor, animationSpec = tween(durationMillis = 500)) + val interaction = remember(item) { MutableInteractionSource() } + val scale by rememberBounceScale(interaction) - Row( - modifier = Modifier - .heightIn(min = if (hasIcon) 72.dp else 55.dp) - .background(animatedBackgroundColor, shape) - .pointerInput(Unit) { - detectTapGestures( - onPress = { - if (item.enabled) { - itemBackgroundColor = - if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9) - tryAwaitRelease() - itemBackgroundColor = backgroundColor - } - }, - onTap = { - if (item.enabled) { - haptics.performHapticFeedback(HapticFeedbackType.ContextClick) - item.onClick() - } - } - ) - } - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - if (hasIcon) { - Icon( - painter = painterResource(item.iconRes), - contentDescription = "Icon", - tint = Color(0xFF007AFF), - modifier = Modifier - .height(48.dp) - .wrapContentWidth() - ) - } - Column( + Surface( + onClick = { + if (item.enabled) { + haptics.performHapticFeedback(HapticFeedbackType.ContextClick) + item.onClick() + } + }, + enabled = item.enabled, + interactionSource = interaction, + shape = RectangleShape, + color = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface, modifier = Modifier - .weight(1f) - .padding(vertical = 2.dp) - .padding(start = if (hasIcon) 8.dp else 4.dp) + .fillMaxWidth() + .graphicsLayer { + scaleX = scale + scaleY = scale + } ) { - Text( - item.name, - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)), - ) - item.description?.let { - Text( - it, - fontSize = 14.sp, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)), + Row( + modifier = Modifier + .heightIn(min = if (hasIcon) 72.dp else 56.dp) + .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (hasIcon) { + Icon( + painter = painterResource(item.iconRes!!), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .size(40.dp) + .wrapContentWidth() + ) + } + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = 8.dp) + .padding(start = if (hasIcon) 12.dp else 0.dp) + ) { + Text( + text = item.name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + item.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + val floatAnimateState by animateFloatAsState( + targetValue = if (item.selected) 1f else 0f, + label = "checkAlpha" + ) + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary.copy(alpha = floatAnimateState), + modifier = Modifier.size(24.dp) ) } } - val floatAnimateState by animateFloatAsState( - targetValue = if (item.selected) 1f else 0f, - animationSpec = tween(durationMillis = 300) - ) - Text( - text = "􀆅", - style = TextStyle( - fontSize = 20.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = Color(0xFF007AFF).copy(alpha = floatAnimateState), - ), - modifier = Modifier.padding(end = 4.dp) - ) - } - if (!isLast) { - if (hasIcon) { + if (!isLast) { HorizontalDivider( thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(start = 72.dp, end = 20.dp) - ) - } else { - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(start = 20.dp, end = 20.dp) + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding( + start = if (hasIcon) 72.dp else 20.dp, + end = 20.dp + ) ) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSlider.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSlider.kt index 58c9b571d..7ce7ed503 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSlider.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSlider.kt @@ -18,203 +18,42 @@ package me.kavishdevar.librepods.presentation.components -import android.annotation.SuppressLint -import android.content.res.Configuration -import android.util.Log -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FiniteAnimationSpec -import androidx.compose.animation.core.spring -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState -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.fillMaxSize 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.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -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.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.util.VelocityTracker -import androidx.compose.ui.input.pointer.util.addPointerInputChange -import androidx.compose.ui.layout.layout -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.layout.positionInParent -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalHapticFeedback -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.tooling.preview.Preview -import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.util.fastCoerceIn -import androidx.compose.ui.util.fastRoundToInt -import androidx.compose.ui.util.lerp import com.kyant.backdrop.Backdrop -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberCombinedBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.effects.blur -import com.kyant.backdrop.effects.lens -import com.kyant.backdrop.highlight.Highlight -import com.kyant.backdrop.shadow.InnerShadow -import com.kyant.backdrop.shadow.Shadow -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.utils.inspectDragGestures -import kotlin.math.abs -import kotlin.math.roundToInt -@Composable -fun rememberMomentumAnimation( - maxScale: Float, - progressAnimationSpec: FiniteAnimationSpec = - spring(1f, 1000f, 0.01f), - velocityAnimationSpec: FiniteAnimationSpec = - spring(0.5f, 250f, 5f), - scaleXAnimationSpec: FiniteAnimationSpec = - spring(0.4f, 400f, 0.01f), - scaleYAnimationSpec: FiniteAnimationSpec = - spring(0.6f, 400f, 0.01f) -): MomentumAnimation { - val animationScope = rememberCoroutineScope() - return remember( - maxScale, - animationScope, - progressAnimationSpec, - velocityAnimationSpec, - scaleXAnimationSpec, - scaleYAnimationSpec - ) { - MomentumAnimation( - maxScale = maxScale, - animationScope = animationScope, - progressAnimationSpec = progressAnimationSpec, - velocityAnimationSpec = velocityAnimationSpec, - scaleXAnimationSpec = scaleXAnimationSpec, - scaleYAnimationSpec = scaleYAnimationSpec - ) - } -} - -class MomentumAnimation( - val maxScale: Float, - private val animationScope: CoroutineScope, - private val progressAnimationSpec: FiniteAnimationSpec, - private val velocityAnimationSpec: FiniteAnimationSpec, - private val scaleXAnimationSpec: FiniteAnimationSpec, - private val scaleYAnimationSpec: FiniteAnimationSpec -) { - - private val velocityTracker = VelocityTracker() - - private val progressAnimation = Animatable(0f) - private val velocityAnimation = Animatable(0f) - private val scaleXAnimation = Animatable(1f) - private val scaleYAnimation = Animatable(1f) - - val progress: Float get() = progressAnimation.value - val velocity: Float get() = velocityAnimation.value - val scaleX: Float get() = scaleXAnimation.value - val scaleY: Float get() = scaleYAnimation.value - - var isDragging: Boolean by mutableStateOf(false) - private set - - val modifier: Modifier = Modifier.pointerInput(Unit) { - inspectDragGestures( - onDragStart = { - isDragging = true - velocityTracker.resetTracking() - startPressingAnimation() - }, - onDragEnd = { change -> - isDragging = false - val velocity = velocityTracker.calculateVelocity() - updateVelocity(velocity) - velocityTracker.addPointerInputChange(change) - velocityTracker.resetTracking() - endPressingAnimation() - settleVelocity() - }, - onDragCancel = { - isDragging = false - velocityTracker.resetTracking() - endPressingAnimation() - settleVelocity() - } - ) { change, _ -> - isDragging = true - velocityTracker.addPointerInputChange(change) - val velocity = velocityTracker.calculateVelocity() - updateVelocity(velocity) - } - } - - private fun updateVelocity(velocity: Velocity) { - animationScope.launch { velocityAnimation.animateTo(velocity.x, velocityAnimationSpec) } - } - - private fun settleVelocity() { - animationScope.launch { velocityAnimation.animateTo(0f, velocityAnimationSpec) } - } - - fun startPressingAnimation() { - animationScope.launch { - launch { progressAnimation.animateTo(1f, progressAnimationSpec) } - launch { scaleXAnimation.animateTo(maxScale, scaleXAnimationSpec) } - launch { scaleYAnimation.animateTo(maxScale, scaleYAnimationSpec) } - } - } - - fun endPressingAnimation() { - animationScope.launch { - launch { progressAnimation.animateTo(0f, progressAnimationSpec) } - launch { scaleXAnimation.animateTo(1f, scaleXAnimationSpec) } - launch { scaleYAnimation.animateTo(1f, scaleYAnimationSpec) } - } - } -} - -@SuppressLint("UnrememberedMutableState") +/** + * Material 3 Expressive slider. Signature preserved; the old liquid-glass track is replaced with an + * M3 [Slider] (springy thumb + expressive motion). [startIcon]/[endIcon] are SF-Symbol glyph strings + * converted to Material icons via [sfSymbolToIcon]. [backdrop], [snapPoints] and [snapThreshold] are + * retained for compatibility but unused. + */ @Composable fun StyledSlider( label: String? = null, value: Float, onValueChange: (Float) -> Unit, valueRange: ClosedFloatingPointRange, - backdrop: Backdrop = rememberLayerBackdrop(), - snapPoints: List = emptyList(), - snapThreshold: Float = 0.05f, + @Suppress("UNUSED_PARAMETER") backdrop: Backdrop = rememberLayerBackdrop(), + @Suppress("UNUSED_PARAMETER") snapPoints: List = emptyList(), + @Suppress("UNUSED_PARAMETER") snapThreshold: Float = 0.05f, startIcon: String? = null, endIcon: String? = null, startLabel: String? = null, @@ -223,409 +62,89 @@ fun StyledSlider( description: String? = null, enabled: Boolean = true ) { - val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - val isLightTheme = !isSystemInDarkTheme() - val trackColor = - if (isLightTheme) Color(0xFF787878).copy(0.2f) - else Color(0xFF787880).copy(0.36f) - val accentColor = - if (enabled) { - if (isLightTheme) Color(0xFF0088FF) - else Color(0xFF0091FF) - } else { - trackColor - } - val labelTextColor = if (isLightTheme) Color.Black else Color.White - - val fraction by derivedStateOf { - ((value - valueRange.start) / (valueRange.endInclusive - valueRange.start)) - .fastCoerceIn(0f, 1f) - } - - val sliderBackdrop = rememberLayerBackdrop() - val trackWidthState = remember { mutableFloatStateOf(0f) } - val trackPositionState = remember { mutableFloatStateOf(0f) } - val startIconWidthState = remember { mutableFloatStateOf(0f) } - val endIconWidthState = remember { mutableFloatStateOf(0f) } - val density = LocalDensity.current - val haptics = LocalHapticFeedback.current - var lastDragValue by remember { mutableFloatStateOf(value) } - - val momentumAnimation = rememberMomentumAnimation(maxScale = 1.5f) - - val content = @Composable { - Box( - Modifier - .fillMaxWidth(if (startIcon == null && endIcon == null) 0.95f else 1f) - ) { - Box( - Modifier - .padding(vertical = 4.dp) - .layerBackdrop(sliderBackdrop) - .fillMaxWidth() - ) { - Column( - modifier = Modifier - .fillMaxWidth(1f) - .padding(vertical = 12.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (startLabel != null || endLabel != null) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = startLabel ?: "", - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = labelTextColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - Text( - text = endLabel ?: "", - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = labelTextColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } - Spacer(modifier = Modifier.height(12.dp)) - } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .then(if (startIcon == null && endIcon == null) Modifier.padding(horizontal = 8.dp) else Modifier), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(0.dp) - ) { - if (startIcon != null) { - Text( - text = startIcon, - style = TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.Normal, - color = accentColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier - .padding(horizontal = 12.dp) - .onGloballyPositioned { - startIconWidthState.floatValue = it.size.width.toFloat() - } - ) - } - Box( - Modifier - .weight(1f) - .onSizeChanged { trackWidthState.floatValue = it.width.toFloat() } - .onGloballyPositioned { - trackPositionState.floatValue = - it.positionInParent().y + it.size.height / 2f - } - ) { - Box( - Modifier - .clip(RoundedCornerShape(28.dp)) - .background(trackColor) - .height(6f.dp) - .fillMaxWidth() - ) - - Box( - Modifier - .clip(RoundedCornerShape(28.dp)) - .background(accentColor) - .height(6f.dp) - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - val fraction = fraction - val width = - (fraction * constraints.maxWidth).fastRoundToInt() - layout(width, placeable.height) { - placeable.place(0, 0) - } - } - ) - } - if (endIcon != null) { - Text( - text = endIcon, - style = TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.Normal, - color = accentColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier - .padding(horizontal = 12.dp) - .onGloballyPositioned { - endIconWidthState.floatValue = it.size.width.toFloat() - } - ) - } - } - if (snapPoints.isNotEmpty() && startLabel != null && endLabel != null) Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - if (snapPoints.isNotEmpty()) { - val trackWidth = if (startIcon != null && endIcon != null) trackWidthState.floatValue - with(density) { 6.dp.toPx() } * 2 else trackWidthState.floatValue- with(density) { 22.dp.toPx() } - val startOffset = - if (startIcon != null) startIconWidthState.floatValue + with( - density - ) { 34.dp.toPx() } else with(density) { 14.dp.toPx() } - Box( - Modifier - .fillMaxWidth() - ) { - snapPoints.forEach { point -> - val pointFraction = - ((point - valueRange.start) / (valueRange.endInclusive - valueRange.start)) - .fastCoerceIn(0f, 1f) - Box( - Modifier - .graphicsLayer { - translationX = - startOffset + pointFraction * trackWidth - 4.dp.toPx() - } - .size(2.dp) - .background( - trackColor, - CircleShape - ) - ) - } - } - } - } - } - } - } - - Box( - Modifier - .graphicsLayer { -// val startOffset = -// if (startIcon != null) startIconWidthState.floatValue + with(density) { 24.dp.toPx() } else with(density) { 12.dp.toPx() } -// translationX = -// startOffset + fraction * trackWidthState.floatValue - size.width / 2f - val startOffset = - if (startIcon != null) - startIconWidthState.floatValue + with(density) { 24.dp.toPx() } - else - with(density) { 8.dp.toPx() } - - translationX = - (startOffset + fraction * trackWidthState.floatValue - size.width / 2f) - .fastCoerceIn( - startOffset - size.width / 4f, - startOffset + trackWidthState.floatValue - size.width * 3f / 4f - ) - translationY = if (startLabel != null || endLabel != null) trackPositionState.floatValue + with(density) { 26.dp.toPx() } + size.height / 2f else trackPositionState.floatValue + with(density) { 8.dp.toPx() } - } - .then( - if (enabled) { - Modifier - .draggable( - rememberDraggableState { delta -> - val trackWidth = trackWidthState.floatValue - if (trackWidth > 0f) { - val targetFraction = fraction + delta / trackWidth - val targetValue = - lerp( - valueRange.start, - valueRange.endInclusive, - targetFraction - ) - .fastCoerceIn( - valueRange.start, - valueRange.endInclusive - ) - snapPoints.forEach { snap -> - if ((lastDragValue < snap && targetValue >= snap) || - (snap in targetValue.. Unit = { + Column( modifier = Modifier - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(2.dp) + .fillMaxWidth() + .padding(vertical = 8.dp) ) { if (label != null) { Text( text = label, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = labelTextColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier.padding(horizontal = 18.dp, vertical = 4.dp) + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 8.dp) ) } - - Box( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .padding(horizontal = 8.dp, vertical = 0.dp) - .heightIn(min = 58.dp), - contentAlignment = Alignment.Center + if (startLabel != null || endLabel != null) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = startLabel ?: "", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = endLabel ?: "", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + } + Spacer(modifier = Modifier.height(8.dp)) + } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - content() + startIcon?.let { sfSymbolToIcon(it) }?.let { vector -> + Icon( + imageVector = vector, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + } + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + enabled = enabled, + modifier = Modifier.weight(1f), + ) + endIcon?.let { sfSymbolToIcon(it) }?.let { vector -> + Icon( + imageVector = vector, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + } } - if (description != null) { Text( text = description, - style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Light, - color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier - .padding(horizontal = 18.dp, vertical = 4.dp) + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp) ) } } - } else { - if (label != null) Log.w("StyledSlider", "Label is ignored when independent is false") - if (description != null) Log.w("StyledSlider", "Description is ignored when independent is false") - content() } -} - -private fun snapIfClose(value: Float, points: List, threshold: Float = 0.05f): Float { - val nearest = points.minByOrNull { abs(it - value) } ?: value - return if (abs(nearest - value) <= threshold) nearest else value -} -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun StyledSliderPreview() { - val a = remember { mutableFloatStateOf(0.5f) } - Box( - Modifier - .background(if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF0F0F0)) - .padding(16.dp) - .fillMaxSize() - ) { - Column ( - Modifier.align(Alignment.Center), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) - { - StyledSlider( - value = a.floatValue, - onValueChange = { - a.floatValue = it - }, - valueRange = 0f..2f, - snapPoints = listOf(1f), - snapThreshold = 0.1f, - independent = true, - startIcon = "A", - endIcon = "B", - ) - StyledSlider( - value = a.floatValue, - onValueChange = { - a.floatValue = it - }, - valueRange = 0f..2f, - snapPoints = listOf(1f), - snapThreshold = 0.1f, - independent = true, - startIcon = "A", - endIcon = "B", - enabled = false - ) + if (independent) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier.fillMaxWidth() + ) { + Box(modifier = Modifier.padding(horizontal = 20.dp, vertical = 4.dp)) { + inner() + } } + } else { + inner() } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSwitch.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSwitch.kt index d479da0d2..3651d8dbc 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSwitch.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSwitch.kt @@ -18,300 +18,44 @@ package me.kavishdevar.librepods.presentation.components -import android.content.res.Configuration -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.BlurEffect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Paint -import androidx.compose.ui.graphics.TileMode -import androidx.compose.ui.graphics.drawOutline -import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.graphics.drawscope.scale -import androidx.compose.ui.graphics.drawscope.translate -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.layer.CompositingStrategy -import androidx.compose.ui.graphics.layer.drawLayer -import androidx.compose.ui.graphics.rememberGraphicsLayer -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastCoerceIn -import androidx.compose.ui.util.lerp -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberCombinedBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.effects.lens -import com.kyant.backdrop.highlight.Highlight -import com.kyant.backdrop.shadow.Shadow -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlin.math.abs +/** + * Material 3 Expressive [Switch]. The thumb already morphs/grows on press and animates with the + * theme's expressive motion scheme; we add a check-mark thumb when on for extra clarity. + */ @Composable fun StyledSwitch( checked: Boolean, onCheckedChange: (Boolean) -> Unit, enabled: Boolean = true, ) { - val isDarkTheme = isSystemInDarkTheme() - val haptics = LocalHapticFeedback.current - - val onColor = if (enabled) Color(0xFF34C759) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6) - val offColor = if (enabled) if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6) else if (isDarkTheme) Color( - 0x805B5B5E - ) else Color(0xFFD1D1D6) - - val trackWidth = 64.dp - val trackHeight = 28.dp - val thumbHeight = 24.dp - val thumbWidth = 39.dp - - val backdrop = rememberLayerBackdrop() - val switchBackdrop = rememberLayerBackdrop() - val fraction by remember { - derivedStateOf { if (checked) 1f else 0f } - } - val animatedFraction = remember { Animatable(fraction) } - val trackWidthPx = remember { mutableFloatStateOf(0f) } - val density = LocalDensity.current - val scope = rememberCoroutineScope() - val progressAnimationSpec = spring(0.5f, 300f, 0.001f) - val progressAnimation = remember { Animatable(0f) } - val innerShadowLayer = rememberGraphicsLayer().apply { - compositingStrategy = CompositingStrategy.Offscreen - } - val targetColor = if (checked) onColor else offColor - val animatedTrackColor by animateColorAsState(targetColor) - val totalDrag = remember { mutableFloatStateOf(0f) } - val tapThreshold = 10f - val isFirstComposition = remember { mutableStateOf(true) } - LaunchedEffect(checked) { - if (!isFirstComposition.value) { - if (checked) { - haptics.performHapticFeedback(HapticFeedbackType.ToggleOn) - } else { - haptics.performHapticFeedback(HapticFeedbackType.ToggleOff) - } - coroutineScope { - launch { - val targetFrac = if (checked) 1f else 0f - animatedFraction.animateTo(targetFrac, progressAnimationSpec) - } - if (progressAnimation.value > 0f) return@coroutineScope - launch { - progressAnimation.animateTo(1f, tween(175, easing = FastOutSlowInEasing)) - progressAnimation.animateTo(0f, tween(175, easing = FastOutSlowInEasing)) - } - } - } - isFirstComposition.value = false - } - - Box( - modifier = Modifier - .width(trackWidth) - .height(trackHeight), - contentAlignment = Alignment.CenterStart - ) { - Box( - modifier = Modifier - .layerBackdrop(switchBackdrop) - .clip(RoundedCornerShape(trackHeight / 2)) - .background(animatedTrackColor) - .width(trackWidth) - .height(trackHeight) - .onSizeChanged { trackWidthPx.floatValue = it.width.toFloat() } - ) - Box( - modifier = Modifier - .padding(horizontal = 2.dp) - .graphicsLayer { - translationX = animatedFraction.value * (trackWidthPx.floatValue - with(density) { thumbWidth.toPx() + 4.dp.toPx() }) - } - .then(if (enabled) Modifier.draggable( - rememberDraggableState { delta -> - if (trackWidthPx.floatValue > 0f) { - val oldFraction = animatedFraction.value - val newFraction = (animatedFraction.value + delta / trackWidthPx.floatValue).fastCoerceIn(-0.3f, 1.3f) - scope.launch { - animatedFraction.snapTo(newFraction) - } - totalDrag.floatValue += abs(delta) - val newChecked = newFraction >= 0.5f - if (newChecked != checked) { - onCheckedChange(newChecked) - } - if ((oldFraction < 0.5f && newFraction >= 0.5f) || (oldFraction >= 0.5f && newFraction < 0.5f)) { - haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) - } - } - }, - Orientation.Horizontal, - startDragImmediately = true, - onDragStarted = { - totalDrag.floatValue = 0f - scope.launch { - progressAnimation.animateTo(1f, progressAnimationSpec) - } - }, - onDragStopped = { - scope.launch { - if (totalDrag.floatValue < tapThreshold) { - val newChecked = !checked - onCheckedChange(newChecked) - val snappedFraction = if (newChecked) 1f else 0f - coroutineScope { - launch { progressAnimation.animateTo(0f, progressAnimationSpec) } - launch { animatedFraction.animateTo(snappedFraction, progressAnimationSpec) } - } - } else { - val snappedFraction = if (animatedFraction.value >= 0.5f) 1f else 0f - onCheckedChange(snappedFraction >= 0.5f) - coroutineScope { - launch { progressAnimation.animateTo(0f, progressAnimationSpec) } - launch { animatedFraction.animateTo(snappedFraction, progressAnimationSpec) } - } - } - } - } - ) else Modifier) - .drawBackdrop( - rememberCombinedBackdrop(backdrop, switchBackdrop), - { RoundedCornerShape(thumbHeight / 2) }, - highlight = { - val progress = progressAnimation.value - Highlight.Ambient.copy( - alpha = progress - ) - }, - shadow = { - Shadow( - radius = 4f.dp, - color = Color.Black.copy(0.05f) - ) - }, - layerBlock = { - val progress = progressAnimation.value - val scale = lerp(1f, 1.5f, progress) - scaleX = scale - scaleY = scale - }, - onDrawBackdrop = { drawScope -> - drawIntoCanvas { canvas -> - canvas.save() - canvas.drawRect( - left = 0f, - top = 0f, - right = size.width, - bottom = size.height, - paint = Paint().apply { - color = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7) - } - ) - scale(0.7f) { - drawScope() - } - } - }, - onDrawSurface = { - val progress = progressAnimation.value.fastCoerceIn(0f, 1f) - - val shape = RoundedCornerShape(thumbHeight / 2) - val outline = shape.createOutline(size, layoutDirection, this) - val innerShadowOffset = 4f.dp.toPx() - val innerShadowBlurRadius = 4f.dp.toPx() - - innerShadowLayer.alpha = progress - innerShadowLayer.renderEffect = - BlurEffect( - innerShadowBlurRadius, - innerShadowBlurRadius, - TileMode.Decal - ) - innerShadowLayer.record { - drawOutline(outline, Color.Black.copy(0.2f)) - translate(0f, innerShadowOffset) { - drawOutline( - outline, - Color.Transparent, - blendMode = BlendMode.Clear - ) - } - } - drawLayer(innerShadowLayer) - - drawRect(Color.White.copy(1f - progress)) - }, - effects = { - lens( - refractionHeight = 6f.dp.toPx(), - refractionAmount = size.height / 2f, - depthEffect = true, - chromaticAberration = true - ) - } + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + thumbContent = if (checked) { + { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), ) - .width(thumbWidth) - .height(thumbHeight) - ) - } + } + } else null, + ) } -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview @Composable fun StyledSwitchPreview() { - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7) - Box( - modifier = Modifier - .background(backgroundColor) - .width(100.dp) - .height(150.dp), - contentAlignment = Alignment.Center - ) { - val checked = remember { mutableStateOf(true) } - StyledSwitch( - checked = checked.value, - onCheckedChange = { - checked.value = it - }, - enabled = true, - ) -// LaunchedEffect(Unit) { -// delay(1000) -// checked.value = false -// delay(1000) -// checked.value = true -// } - } + StyledSwitch(checked = true, onCheckedChange = {}) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledToggle.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledToggle.kt index 4028dc4a2..deca526b5 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledToggle.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledToggle.kt @@ -16,50 +16,34 @@ along with this program. If not, see . */ -@file:OptIn(ExperimentalEncodingApi::class) - package me.kavishdevar.librepods.presentation.components -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface 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.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState -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.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalHapticFeedback -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.graphics.RectangleShape +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import kotlinx.coroutines.launch -import me.kavishdevar.librepods.R -import kotlin.io.encoding.ExperimentalEncodingApi +/** + * Material 3 Expressive labelled toggle row. `independent = true` renders a rounded tonal card + * whose whole surface springs and toggles on tap; `false` is a flush row for use inside a section. + */ @Composable fun StyledToggle( title: String? = null, @@ -70,191 +54,86 @@ fun StyledToggle( enabled: Boolean = true, onCheckedChange: (Boolean) -> Unit, ) { - val currentChecked by rememberUpdatedState(checked) - - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - - val haptics = LocalHapticFeedback.current - val scope = rememberCoroutineScope() - - var backgroundColor by remember { - mutableStateOf( - if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - ) - } - - val animatedBackgroundColor by animateColorAsState( - targetValue = backgroundColor, - animationSpec = tween(durationMillis = 500) - ) - if (independent) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { + val interaction = remember { MutableInteractionSource() } + val scale by rememberBounceScale(interaction) + Column(modifier = Modifier.padding(vertical = 4.dp)) { if (title != null) { Text( text = title, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - top = 8.dp, - bottom = 4.dp - ) + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 8.dp, bottom = 6.dp) ) } - - Box( + Surface( + onClick = { if (enabled) onCheckedChange(!checked) }, + enabled = enabled, + interactionSource = interaction, + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, modifier = Modifier - .background(animatedBackgroundColor, RoundedCornerShape(28.dp)) - .padding(4.dp) - .pointerInput(Unit) { - detectTapGestures( - onPress = { - if (enabled) { - backgroundColor = - if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9) - tryAwaitRelease() - backgroundColor = - if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - } - }, - onTap = { - if (enabled) { - scope.launch { haptics.performHapticFeedback(if (!currentChecked) HapticFeedbackType.ToggleOn else HapticFeedbackType.ToggleOff) } - onCheckedChange(!currentChecked) - } - } - ) + .fillMaxWidth() + .graphicsLayer { + scaleX = scale + scaleY = scale } ) { Row( modifier = Modifier - .fillMaxWidth() - .height(55.dp) - .padding(horizontal = 12.dp), + .heightIn(min = 56.dp) + .padding(horizontal = 20.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = label, modifier = Modifier.weight(1f), - style = TextStyle( - fontSize = 16.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Normal, - color = textColor - ) + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface ) - + Spacer(modifier = Modifier.width(12.dp)) StyledSwitch( checked = checked, enabled = enabled, - onCheckedChange = { - if (enabled) { - scope.launch { haptics.performHapticFeedback(if (it) HapticFeedbackType.ToggleOn else HapticFeedbackType.ToggleOff) } - onCheckedChange(it) - } - } + onCheckedChange = { if (enabled) onCheckedChange(it) } ) } } - if (description != null) { - Spacer(modifier = Modifier.height(8.dp)) - - Box( - modifier = Modifier - .padding(horizontal = 16.dp) - .background( - if (isDarkTheme) Color(0xFF000000) - else Color(0xFFF2F2F7) - ) - ) { - Text( - text = description, - style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Light, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 6.dp) + ) } } } else { - val isPressed = remember { mutableStateOf(false) } - Row( modifier = Modifier .fillMaxWidth() - .background( - shape = RoundedCornerShape(28.dp), - color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent - ) - .padding(16.dp) - .pointerInput(Unit) { - detectTapGestures( - onPress = { - isPressed.value = true - tryAwaitRelease() - isPressed.value = false - } - ) - } - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - if (enabled) { - scope.launch { haptics.performHapticFeedback(if (!currentChecked) HapticFeedbackType.ToggleOn else HapticFeedbackType.ToggleOff) } - onCheckedChange(!currentChecked) - } - }, + .padding(horizontal = 4.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { - Column( - modifier = Modifier - .weight(1f) - .padding(end = 4.dp) - ) { + Column(modifier = Modifier.weight(1f)) { Text( text = label, - style = TextStyle( - fontSize = 16.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Normal, - color = textColor - ) + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface ) - - Spacer(modifier = Modifier.height(4.dp)) - if (description != null) { Text( text = description, - style = TextStyle( - fontSize = 12.sp, - color = textColor.copy(0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)), - ) + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } - + Spacer(modifier = Modifier.width(12.dp)) StyledSwitch( checked = checked, enabled = enabled, - onCheckedChange = { - if (enabled) { - onCheckedChange(it) - } - } + onCheckedChange = { if (enabled) onCheckedChange(it) } ) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/VerticalVolumeSlider.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/VerticalVolumeSlider.kt index 37bddd8e6..fd2f81556 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/VerticalVolumeSlider.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/VerticalVolumeSlider.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -42,7 +43,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp @@ -71,8 +71,8 @@ fun VerticalVolumeSlider( cornerRadiusChangeFactor: Float = 0.2f, directionalStretchRatio: Float = 0.75f ) { - val trackColor = Color(0x593C3C3E) - val progressColor = Color.White + val trackColor = MaterialTheme.colorScheme.surfaceVariant + val progressColor = MaterialTheme.colorScheme.primary var dragFraction by remember { mutableFloatStateOf(initialFraction) } var isDragging by remember { mutableStateOf(false) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppDestinations.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppDestinations.kt new file mode 100644 index 000000000..f0daad9e6 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppDestinations.kt @@ -0,0 +1,70 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Headphones +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.outlined.Headphones +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.Tune +import androidx.compose.ui.graphics.vector.ImageVector + +/** + * Top-level destinations shown in the floating-toolbar bottom navigation (Caffeine-style). + * `route` is the navigation-compose route string for the tab's start screen. + */ +enum class AppDestinations( + val route: String, + val label: String, + val iconOutlined: ImageVector, + val iconFilled: ImageVector, +) { + HOME("home", "Device", Icons.Outlined.Headphones, Icons.Filled.Headphones), + CONTROLS("controls", "Controls", Icons.Outlined.Tune, Icons.Filled.Tune), + SETTINGS("settings_tab", "Settings", Icons.Outlined.Settings, Icons.Filled.Settings), +} + +val topLevelDestinations = listOf( + AppDestinations.HOME, + AppDestinations.CONTROLS, + AppDestinations.SETTINGS, +) + +/** + * Resolve the toolbar tab that should appear selected for an arbitrary current route — including the + * pushed detail screens, which map back to the parent tab they were opened from (Caffeine-style), so + * the nav-bar pill never jumps to "Device" while you're inside a Controls/Settings subpage. + */ +fun destinationForRoute(route: String?): AppDestinations = when (route) { + AppDestinations.CONTROLS.route -> AppDestinations.CONTROLS + AppDestinations.SETTINGS.route -> AppDestinations.SETTINGS + + // Detail screens opened from the Controls tab. + "transparency_customization", "adaptive_strength", "head_tracking", "accessibility", + "hearing_aid", "hearing_aid_adjustments", "camera_control", "hearing_protection", + "update_hearing_test", "long_press/{bud}" -> AppDestinations.CONTROLS + + // Detail screens opened from the Settings tab. + "app_settings", "troubleshooting", "version_info", "open_source_licenses", "debug" -> AppDestinations.SETTINGS + + // "rename" is reached from the Device (Home) tab; everything else falls back to Home. + else -> AppDestinations.HOME +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppHaptics.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppHaptics.kt new file mode 100644 index 000000000..d37ad5628 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppHaptics.kt @@ -0,0 +1,64 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.m3 + +import android.view.HapticFeedbackConstants +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalView +import kotlinx.coroutines.delay + +/** + * Centralised, view-backed haptics for the Material 3 Expressive UI. Using the platform + * [HapticFeedbackConstants] gives richer, OS-tuned feedback than Compose's generic haptics — fired + * on the key structural interactions (navigation, toggles, confirmations). + */ +@Stable +class AppHaptics( + private val view: View, +) { + /** Selecting a navigation destination / pushing a screen. */ + fun navigation() = view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) + + /** Flipping a switch / toggle. */ + fun toggle() = view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) + + /** A committed/positive action (primary button, apply). */ + fun confirm() = view.performHapticFeedback(HapticFeedbackConstants.CONFIRM) + + /** A light tick — segmented selection, slider detents. */ + fun tick() = view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) + + /** A short celebratory burst. */ + suspend fun celebration() { + confirm() + delay(150) + tick() + delay(100) + tick() + } +} + +@Composable +fun rememberAppHaptics(): AppHaptics { + val view = LocalView.current + return remember(view) { AppHaptics(view) } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppNavBar.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppNavBar.kt new file mode 100644 index 000000000..06e61c372 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppNavBar.kt @@ -0,0 +1,164 @@ +/* + 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 . +*/ + +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FloatingToolbarDefaults +import androidx.compose.material3.HorizontalFloatingToolbar +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalRippleConfiguration +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.ToggleButton +import androidx.compose.material3.ToggleButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.boundsInParent +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +/** + * The floating bottom navigation bar — lifted from Caffeine Health: an M3 Expressive + * [HorizontalFloatingToolbar] of [ToggleButton]s with a bouncy sliding "pill" behind the selected + * tab (low-damping spring) and a label that expands open only on the active destination. + */ +@Composable +fun AppNavBar( + current: AppDestinations, + onSelect: (AppDestinations) -> Unit, + modifier: Modifier = Modifier, +) { + val haptics = rememberAppHaptics() + val buttonBounds = remember { mutableStateMapOf() } + val currentIndex = topLevelDestinations.indexOf(current).coerceAtLeast(0) + val targetRect = buttonBounds[currentIndex] + val button0Rect = buttonBounds[0] + val pillRelativeX = (targetRect?.left ?: 0f) - (button0Rect?.left ?: 0f) + + val pillAnimatedX by animateFloatAsState( + targetValue = pillRelativeX, + animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow), + label = "pillX", + ) + val pillAnimatedWidth by animateFloatAsState( + targetValue = targetRect?.width ?: 0f, + animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow), + label = "pillWidth", + ) + val pillColor = MaterialTheme.colorScheme.primary + + HorizontalFloatingToolbar( + expanded = true, + colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors( + toolbarContainerColor = MaterialTheme.colorScheme.primaryContainer, + toolbarContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + modifier = modifier, + ) { + CompositionLocalProvider(LocalRippleConfiguration provides null) { + topLevelDestinations.forEachIndexed { index, destination -> + val selected = current == destination + ToggleButton( + checked = selected, + onCheckedChange = { + if (!selected) { + haptics.navigation() + onSelect(destination) + } + }, + modifier = Modifier + .height(56.dp) + .onGloballyPositioned { coords -> buttonBounds[index] = coords.boundsInParent() } + .then( + if (index == 0) { + Modifier.drawWithContent { + if (pillAnimatedWidth > 0f) { + drawRoundRect( + color = pillColor, + topLeft = Offset(pillAnimatedX, 0f), + size = Size(pillAnimatedWidth, size.height), + cornerRadius = CornerRadius(size.height / 2f), + ) + } + drawContent() + } + } else Modifier + ), + colors = ToggleButtonDefaults.toggleButtonColors( + checkedContainerColor = Color.Transparent, + checkedContentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + shapes = ToggleButtonDefaults.shapes( + shape = CircleShape, + pressedShape = CircleShape, + checkedShape = CircleShape, + ), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = if (selected) destination.iconFilled else destination.iconOutlined, + contentDescription = destination.label, + modifier = Modifier.size(24.dp), + ) + AnimatedVisibility( + visible = selected, + enter = expandHorizontally(animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec()), + exit = shrinkHorizontally(animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec()), + ) { + Text( + text = destination.label, + modifier = Modifier.padding(start = ButtonDefaults.IconSpacing), + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold), + ) + } + } + } + } + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppShell.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppShell.kt new file mode 100644 index 000000000..18737c572 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/AppShell.kt @@ -0,0 +1,210 @@ +/* + 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 . +*/ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import me.kavishdevar.librepods.presentation.m3.screens.ControlsScreen +import me.kavishdevar.librepods.presentation.m3.screens.HomeScreen +import me.kavishdevar.librepods.presentation.m3.screens.SettingsScreen +import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen +import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen +import me.kavishdevar.librepods.presentation.screens.AppSettingsScreen +import me.kavishdevar.librepods.presentation.screens.CameraControlScreen +import me.kavishdevar.librepods.presentation.screens.DebugScreen +import me.kavishdevar.librepods.presentation.screens.HeadTrackingScreen +import me.kavishdevar.librepods.presentation.screens.HearingAidAdjustmentsScreen +import me.kavishdevar.librepods.presentation.screens.HearingAidScreen +import me.kavishdevar.librepods.presentation.screens.HearingProtectionScreen +import me.kavishdevar.librepods.presentation.screens.LongPress +import me.kavishdevar.librepods.presentation.screens.OpenSourceLicensesScreen +import me.kavishdevar.librepods.presentation.screens.RenameScreen +import me.kavishdevar.librepods.presentation.screens.TransparencySettingsScreen +import me.kavishdevar.librepods.presentation.screens.TroubleshootingScreen +import me.kavishdevar.librepods.presentation.screens.UpdateHearingTestScreen +import me.kavishdevar.librepods.presentation.screens.VersionScreen +import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel +import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel + +/** + * The single-activity shell: a [Scaffold] whose bottom bar is the Caffeine-style floating toolbar + * navigation, hosting three top-level tabs (Device / Controls / Settings) plus the pushed detail + * screens. Top-level switches are state-preserving; detail screens slide up and the nav bar tucks away. + */ +@Composable +fun AppShell(viewModel: AirPodsViewModel) { + val navController = rememberNavController() + val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route + val current = destinationForRoute(currentRoute) + + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background, + contentWindowInsets = WindowInsets(0.dp), + bottomBar = { + // The floating nav bar stays put across subpages (Caffeine-style) and pads only for the + // navigation-bar inset — NOT safeDrawing — so the keyboard (IME) never pushes it up. + Box( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom)) + .padding(bottom = 16.dp), + contentAlignment = Alignment.BottomCenter, + ) { + AppNavBar( + current = current, + onSelect = { destination -> + navController.navigate(destination.route) { + popUpTo(navController.graph.startDestinationId) { saveState = true } + launchSingleTop = true + restoreState = true + } + }, + ) + } + }, + ) { _ -> + NavHost( + navController = navController, + startDestination = AppDestinations.HOME.route, + // Opaque backdrop so the aggressive scale-down pop never flashes the raw window behind it. + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + // Tab switches cross-fade; pushing/popping a detail screen slides horizontally (Caffeine-style). + enterTransition = { + if (isTabSwitch()) { + fadeIn(tween(TAB_FADE_MS)) + } else { + slideInHorizontally(initialOffsetX = { it }) + fadeIn(tween(DETAIL_FADE_MS)) + } + }, + exitTransition = { + if (isTabSwitch()) { + fadeOut(tween(TAB_FADE_MS)) + } else { + slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut(tween(DETAIL_FADE_MS)) + } + }, + // Predictive back (manifest enableOnBackInvokedCallback=true) seeks these pop specs with the + // gesture on API 34+ (on 33 the pop just plays on back-press). Intentionally more aggressive + // than Caffeine: the dismissed page slides off AND scales down, while the revealed page eases + // back in from a slight inset. + popEnterTransition = { + if (isTabSwitch()) { + fadeIn(tween(TAB_FADE_MS)) + } else { + slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn(tween(DETAIL_FADE_MS)) + + scaleIn(initialScale = 0.94f) + } + }, + popExitTransition = { + if (isTabSwitch()) { + fadeOut(tween(TAB_FADE_MS)) + } else { + slideOutHorizontally(targetOffsetX = { it }) + fadeOut(tween(DETAIL_FADE_MS)) + + scaleOut(targetScale = 0.85f) + } + }, + ) { + // --- Top-level tabs --- + composable(AppDestinations.HOME.route) { HomeScreen(viewModel, navController) } + composable(AppDestinations.CONTROLS.route) { ControlsScreen(viewModel, navController) } + composable(AppDestinations.SETTINGS.route) { SettingsScreen(viewModel, navController) } + + // --- Detail screens (pushed) --- + detailRoutes(navController, viewModel) + } + } +} + +/** Detail destinations pushed on top of the tabs. */ +private fun androidx.navigation.NavGraphBuilder.detailRoutes( + navController: NavHostController, + airPodsVm: AirPodsViewModel, +) { + composable("debug") { DebugScreen(navController = navController) } + composable("long_press/{bud}") { entry -> + LongPress( + viewModel = airPodsVm, + name = entry.arguments?.getString("bud") ?: "", + navController = navController, + ) + } + composable("rename") { RenameScreen(airPodsVm) } + composable("app_settings") { + val appSettingsViewModel: AppSettingsViewModel = viewModel() + AppSettingsScreen(navController, appSettingsViewModel) + } + composable("troubleshooting") { TroubleshootingScreen(navController) } + composable("head_tracking") { HeadTrackingScreen(airPodsVm, navController) } + composable("accessibility") { AccessibilitySettingsScreen(airPodsVm, navController) } + composable("transparency_customization") { TransparencySettingsScreen(airPodsVm) } + composable("hearing_aid") { HearingAidScreen(airPodsVm, navController) } + composable("hearing_aid_adjustments") { HearingAidAdjustmentsScreen(airPodsVm) } + composable("adaptive_strength") { AdaptiveStrengthScreen(airPodsVm, navController) } + composable("camera_control") { CameraControlScreen(airPodsVm) } + composable("open_source_licenses") { OpenSourceLicensesScreen(navController) } + composable("update_hearing_test") { UpdateHearingTestScreen(airPodsVm) } + composable("version_info") { VersionScreen(airPodsVm) } + composable("hearing_protection") { HearingProtectionScreen(airPodsVm, navController) } +} + +private const val TAB_FADE_MS = 200 +private const val DETAIL_FADE_MS = 120 + +/** True when navigating between two top-level tabs — those cross-fade instead of sliding. */ +private fun AnimatedContentTransitionScope.isTabSwitch(): Boolean = + initialState.destination.route.isTopLevelRoute() && + targetState.destination.route.isTopLevelRoute() + +private fun String?.isTopLevelRoute(): Boolean = + topLevelDestinations.any { it.route == this } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveControls.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveControls.kt new file mode 100644 index 000000000..50edf4879 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveControls.kt @@ -0,0 +1,158 @@ +/* + 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 . +*/ + +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.ButtonGroup +import androidx.compose.material3.ButtonGroupDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.ToggleButton +import androidx.compose.material3.ToggleButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +/** M3 Expressive [Switch] — the thumb grows on press and shows a check when on. */ +@Composable +fun ExpressiveSwitch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + enabled: Boolean = true, +) { + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + thumbContent = if (checked) { + { Icon(Icons.Filled.Check, contentDescription = null, modifier = Modifier.size(SwitchDefaults.IconSize)) } + } else null, + ) +} + +/** + * An equal-width **connected button group** of M3 Expressive [ToggleButton]s — each segment takes an + * equal `weight`, the group uses [ButtonGroupDefaults] leading/middle/trailing shapes so it reads as + * one connected control, and the selected segment fills + morphs rounder. For a few fixed options + * (e.g. press-speed); use [ExpressiveSegmentedFilter] for many, scrollable options. + */ +@Composable +fun ExpressiveSegmentedGroup( + options: List, + selected: T, + onSelect: (T) -> Unit, + label: (T) -> String, + modifier: Modifier = Modifier, + icon: ((T) -> androidx.compose.ui.graphics.vector.ImageVector?)? = null, +) { + val haptics = rememberAppHaptics() + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + ) { + options.forEachIndexed { index, option -> + val isSelected = option == selected + ToggleButton( + checked = isSelected, + onCheckedChange = { + if (!isSelected) { + haptics.tick() + onSelect(option) + } + }, + modifier = Modifier + .weight(1f) + .height(56.dp), + shapes = when { + options.size == 1 -> ButtonGroupDefaults.connectedMiddleButtonShapes() + index == 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() + index == options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() + else -> ButtonGroupDefaults.connectedMiddleButtonShapes() + }, + ) { + val vector = icon?.invoke(option) + if (vector != null) { + Icon(vector, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(ToggleButtonDefaults.IconSpacing)) + } + Text( + text = label(option), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +/** A single action for [ExpressiveActionGroup]. */ +data class ExpressiveAction( + val label: String, + val onClick: () -> Unit, +) + +/** + * Replaces stacks of standalone buttons with an M3 Expressive [ButtonGroup]: pressing a button + * expands it while neighbours compress, with overflow handled automatically. + */ +@Composable +fun ExpressiveActionGroup( + actions: List, + modifier: Modifier = Modifier, +) { + val haptics = rememberAppHaptics() + ButtonGroup( + // ≤3 actions never overflow, so the indicator is never shown; ignore the menu state. + overflowIndicator = { + FilledIconButton(onClick = {}) { + Icon(Icons.Filled.MoreVert, contentDescription = "More actions") + } + }, + modifier = modifier, + ) { + actions.forEach { action -> + clickableItem( + onClick = { + haptics.confirm() + action.onClick() + }, + label = action.label, + ) + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveGroup.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveGroup.kt new file mode 100644 index 000000000..6d1ef273e --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveGroup.kt @@ -0,0 +1,77 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * A small-caps-feeling section header that sits above an [ExpressiveGroup]. The whole design is + * container-first: titled groups of rows on tonal cards, the structural backbone of every screen. + */ +@Composable +fun ExpressiveSectionHeader( + text: String, + modifier: Modifier = Modifier, +) { + Text( + text = text.uppercase(), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + modifier = modifier.padding(start = 4.dp, end = 20.dp, top = 24.dp, bottom = 10.dp), + ) +} + +/** + * The container that defines the redesign: a large, soft, tonal card grouping a column of rows. + * Use [ExpressiveDivider] between rows. + */ +@Composable +fun ExpressiveGroup( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Surface( + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = modifier.fillMaxWidth(), + ) { + Column(content = content) + } +} + +/** Inset hairline divider between rows inside an [ExpressiveGroup]. */ +@Composable +fun ExpressiveDivider(startInset: Boolean = true) { + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f), + modifier = Modifier.padding(start = if (startInset) 68.dp else 20.dp, end = 20.dp), + ) +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveInteractions.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveInteractions.kt new file mode 100644 index 000000000..beac4f1eb --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveInteractions.kt @@ -0,0 +1,51 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer + +/** + * The signature M3 Expressive "squish": a clickable surface dips toward [pressedScale] while held + * and springs back with a low-damping spring. Drive it from the same + * [androidx.compose.foundation.interaction.MutableInteractionSource] the surface uses for clicks. + */ +@Composable +fun Modifier.pressBounce( + interactionSource: InteractionSource, + pressedScale: Float = 0.95f, +): Modifier { + val pressed by interactionSource.collectIsPressedAsState() + val scale by animateFloatAsState( + targetValue = if (pressed) pressedScale else 1f, + animationSpec = spring(dampingRatio = 0.42f, stiffness = Spring.StiffnessMedium), + label = "pressBounce", + ) + return this.graphicsLayer { + scaleX = scale + scaleY = scale + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveListItem.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveListItem.kt new file mode 100644 index 000000000..3dc56d8c1 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveListItem.kt @@ -0,0 +1,230 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +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.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +/** Leading icon in a soft tonal rounded square — the recurring visual motif of the redesign. */ +@Composable +fun ExpressiveLeadingIcon( + icon: ImageVector, + containerColor: Color = MaterialTheme.colorScheme.primaryContainer, + contentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier: Modifier = Modifier, +) { + Surface( + shape = RoundedCornerShape(14.dp), + color = containerColor, + contentColor = contentColor, + modifier = modifier.size(40.dp), + ) { + Box(contentAlignment = Alignment.Center) { + Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(22.dp)) + } + } +} + +/** + * A clickable row for an [ExpressiveGroup]: leading tonal icon, title + optional supporting text, + * an optional trailing value and/or chevron, with a springy press and haptic feedback. + */ +@Composable +fun ExpressiveListItem( + title: String, + modifier: Modifier = Modifier, + subtitle: String? = null, + leadingIcon: ImageVector? = null, + iconContainerColor: Color = MaterialTheme.colorScheme.primaryContainer, + iconContentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer, + value: String? = null, + showChevron: Boolean = false, + enabled: Boolean = true, + trailing: (@Composable () -> Unit)? = null, + onClick: (() -> Unit)? = null, +) { + val haptics = rememberAppHaptics() + val interaction = remember { MutableInteractionSource() } + + val rowContent: @Composable () -> Unit = { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 64.dp) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (leadingIcon != null) { + ExpressiveLeadingIcon( + icon = leadingIcon, + containerColor = iconContainerColor, + contentColor = iconContentColor, + ) + Spacer(Modifier.width(16.dp)) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = if (enabled) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + ) + if (subtitle != null) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + if (value != null) { + Spacer(Modifier.width(12.dp)) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (trailing != null) { + Spacer(Modifier.width(12.dp)) + trailing() + } + if (showChevron) { + Spacer(Modifier.width(4.dp)) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + } + } + } + + if (onClick != null) { + Surface( + onClick = { + if (enabled) { + haptics.navigation() + onClick() + } + }, + enabled = enabled, + interactionSource = interaction, + color = Color.Transparent, + modifier = modifier.pressBounce(interaction), + ) { rowContent() } + } else { + Surface(color = Color.Transparent, modifier = modifier) { rowContent() } + } +} + +/** + * A labelled row hosting a trailing M3 [androidx.compose.material3.Switch]; tapping anywhere on the + * row flips it (with a toggle haptic). + */ +@Composable +fun ExpressiveSwitchRow( + title: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + subtitle: String? = null, + leadingIcon: ImageVector? = null, + enabled: Boolean = true, +) { + val haptics = rememberAppHaptics() + val interaction = remember { MutableInteractionSource() } + + Surface( + onClick = { + if (enabled) { + haptics.toggle() + onCheckedChange(!checked) + } + }, + enabled = enabled, + interactionSource = interaction, + color = Color.Transparent, + modifier = modifier.pressBounce(interaction), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 64.dp) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (leadingIcon != null) { + ExpressiveLeadingIcon(icon = leadingIcon) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + if (subtitle != null) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + ExpressiveSwitch( + checked = checked, + onCheckedChange = { + if (enabled) { + haptics.toggle() + onCheckedChange(it) + } + }, + enabled = enabled, + ) + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveNoiseControl.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveNoiseControl.kt new file mode 100644 index 000000000..65f0a0842 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveNoiseControl.kt @@ -0,0 +1,342 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Block +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import me.kavishdevar.librepods.R +import me.kavishdevar.librepods.presentation.m3.viz.AncVisualizer +import me.kavishdevar.librepods.presentation.m3.viz.AncVizStyle + +/** Protocol listening-mode values (see `ControlCommandIdentifiers.LISTENING_MODE`). */ +private const val MODE_OFF = 1 +private const val MODE_NOISE_CANCELLATION = 2 +private const val MODE_TRANSPARENCY = 3 +private const val MODE_ADAPTIVE = 4 + +private val TrackHeight = 58.dp +private val CapsuleInset = 5.dp +private val SegIconSize = 28.dp + +/** + * The noise-control switcher. The **active mode reads first** (a leading icon that swaps with a bouncy + * scale, the mode name, and a left-aligned description that reveals word-by-word) sitting *above* a + * refined **icon-only segmented control** whose tonal capsule springs to the active cell. Flat tonal, + * no gradients; motion reserved for interaction. Per-mode haptics mirror what the mode does. [modes] + * are the protocol listening-mode values in display order. + */ +@Composable +fun ExpressiveNoiseControlSelector( + modes: List, + selected: Int, + onSelect: (Int) -> Unit, + modifier: Modifier = Modifier, + vizStyle: AncVizStyle = AncVizStyle.DEFAULT, +) { + if (modes.isEmpty()) return + val haptics = rememberAppHaptics() + val cs = MaterialTheme.colorScheme + + // Per-mode haptic that mirrors what the mode does (skipped on first composition / state hydration). + var hapticPrimed by remember { mutableStateOf(false) } + LaunchedEffect(selected) { + if (hapticPrimed) { + when (selected) { + MODE_NOISE_CANCELLATION -> haptics.confirm() // a solid "seal shut" + MODE_TRANSPARENCY -> haptics.toggle() // a light "open up" + MODE_ADAPTIVE -> haptics.celebration() // a little magic + else -> Unit // Off: stay quiet + } + } else { + hapticPrimed = true + } + } + + Column(modifier = modifier.fillMaxWidth()) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(210.dp) + .clip(RoundedCornerShape(28.dp)) + .background(cs.surfaceContainerLow), + ) { + AncVisualizer( + modeValue = selected, + style = vizStyle, + modifier = Modifier.fillMaxSize(), + ) + } + + Spacer(Modifier.height(18.dp)) + + ActiveModeDetail(selected) + + Spacer(Modifier.height(8.dp)) + + SegmentedControl( + modes = modes, + selected = selected, + onSelect = { mode -> + haptics.tick() + onSelect(mode) + }, + ) + } +} + +/** + * The active-mode header: a leading icon tile (icon swaps with a bouncy scale), the mode name (a calm + * crossfade), and the description spanning the full width flush-left, revealed word-by-word. + */ +@Composable +private fun ActiveModeDetail(selected: Int) { + val cs = MaterialTheme.colorScheme + Column(modifier = Modifier.fillMaxWidth()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(44.dp) + .clip(RoundedCornerShape(14.dp)) + .background(cs.primaryContainer), + contentAlignment = Alignment.Center, + ) { + AnimatedContent( + targetState = selected, + transitionSpec = { + (scaleIn(initialScale = 0.5f, animationSpec = spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessLow)) + fadeIn(tween(200))) togetherWith + (scaleOut(targetScale = 0.5f, animationSpec = tween(150)) + fadeOut(tween(150))) + }, + label = "activeIcon", + ) { mode -> + ModeGlyph(mode = mode, tint = cs.onPrimaryContainer, iconSize = 24.dp) + } + } + Spacer(Modifier.width(14.dp)) + AnimatedContent( + targetState = selected, + transitionSpec = { fadeIn(tween(180)) togetherWith fadeOut(tween(120)) }, + label = "activeName", + ) { mode -> + Text( + text = modeLabel(mode), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = cs.onSurface, + ) + } + } + + Spacer(Modifier.height(12.dp)) + + TypographyCaption(text = captionFor(selected)) + } +} + +/** A word-by-word fade-and-rise reveal — restarts whenever the active mode (and thus the text) changes. */ +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun TypographyCaption(text: String) { + val cs = MaterialTheme.colorScheme + val words = remember(text) { text.split(" ") } + val progress = remember(text) { Animatable(0f) } + LaunchedEffect(text) { + progress.animateTo(1f, tween(durationMillis = 220 + words.size * 55, easing = FastOutSlowInEasing)) + } + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + val n = words.size.toFloat() + words.forEachIndexed { i, word -> + val t = ((progress.value * n) - i).coerceIn(0f, 1f) + Text( + text = word, + style = MaterialTheme.typography.bodyMedium, + color = cs.onSurfaceVariant.copy(alpha = t), + modifier = Modifier.graphicsLayer { + translationY = (1f - t) * 10.dp.toPx() + }, + ) + } + } +} + +/** Icon-only segmented control with a single spring-sliding capsule marking the active mode. */ +@Composable +private fun SegmentedControl( + modes: List, + selected: Int, + onSelect: (Int) -> Unit, +) { + val cs = MaterialTheme.colorScheme + val density = LocalDensity.current + val selectedIndex = modes.indexOf(selected).coerceAtLeast(0) + + var rowWidthPx by remember { mutableFloatStateOf(0f) } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(TrackHeight) + .clip(CircleShape) + .background(cs.surfaceContainerHighest) + .onSizeChanged { rowWidthPx = it.width.toFloat() }, + contentAlignment = Alignment.CenterStart, + ) { + if (rowWidthPx > 0f) { + val slotPx = rowWidthPx / modes.size + val insetPx = with(density) { CapsuleInset.toPx() } + val capStartPx = selectedIndex * slotPx + insetPx + val capWidth = with(density) { (slotPx - insetPx * 2f).toDp() } + val xOffset by animateDpAsState( + targetValue = with(density) { capStartPx.toDp() }, + animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow), + label = "capsuleX", + ) + Box( + modifier = Modifier + .offset(x = xOffset) + .height(TrackHeight - CapsuleInset * 2) + .width(capWidth) + .clip(CircleShape) + .background(cs.primary), + ) + } + + Row(modifier = Modifier.fillMaxSize()) { + modes.forEach { mode -> + val isSelected = mode == selected + val tint by animateColorAsState( + targetValue = if (isSelected) cs.onPrimary else cs.onSurfaceVariant, + animationSpec = tween(220), + label = "cellTint", + ) + val scale by animateFloatAsState( + targetValue = if (isSelected) 1.08f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow), + label = "glyphScale", + ) + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clip(CircleShape) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { if (mode != selected) onSelect(mode) }, + contentAlignment = Alignment.Center, + ) { + ModeGlyph( + mode = mode, + tint = tint, + iconSize = SegIconSize, + modifier = Modifier.graphicsLayer { + scaleX = scale + scaleY = scale + }, + ) + } + } + } + } +} + +@Composable +private fun ModeGlyph(mode: Int, tint: Color, iconSize: Dp, modifier: Modifier = Modifier) { + val m = modifier.size(iconSize) + when (mode) { + MODE_TRANSPARENCY -> Icon(painterResource(R.drawable.transparency), modeLabel(mode), m, tint) + MODE_ADAPTIVE -> Icon(painterResource(R.drawable.adaptive), modeLabel(mode), m, tint) + MODE_NOISE_CANCELLATION -> Icon(painterResource(R.drawable.noise_cancellation), modeLabel(mode), m, tint) + else -> Icon(Icons.Filled.Block, modeLabel(mode), m, tint) // Off + } +} + +private fun modeLabel(mode: Int): String = when (mode) { + MODE_OFF -> "Off" + MODE_NOISE_CANCELLATION -> "Noise Cancellation" + MODE_TRANSPARENCY -> "Transparency" + MODE_ADAPTIVE -> "Adaptive" + else -> "" +} + +/** The one live line — what the active mode actually does. */ +private fun captionFor(mode: Int): String = when (mode) { + MODE_OFF -> "Noise control is off." + MODE_NOISE_CANCELLATION -> "Blocks outside noise so you hear only your audio." + MODE_TRANSPARENCY -> "Lets the world in while you listen." + MODE_ADAPTIVE -> "Automatically tunes noise control to your surroundings." + else -> "" +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveProgress.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveProgress.kt new file mode 100644 index 000000000..8412105f3 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveProgress.kt @@ -0,0 +1,147 @@ +/* + 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 . +*/ + +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material3.CircularWavyProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearWavyProgressIndicator +import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** Battery accent colour — green when healthy, error when low. */ +@Composable +fun batteryColor(level: Int): Color { + val healthy = if (isSystemInDarkTheme()) Color(0xFF2ED158) else Color(0xFF1FA94B) + return if (level > 25) healthy else MaterialTheme.colorScheme.error +} + +/** + * Battery ring — an M3 Expressive [CircularWavyProgressIndicator] with a medium, thick squiggle on by + * default: the AirPods component silhouette ([iconRes]) tinted in the centre, a charging-bolt badge + * when powered, and the numeric level rendered by the caller beneath so it stays visible while charging. + */ +@Composable +fun WavyBatteryRing( + level: Int, + charging: Boolean, + iconRes: Int, + modifier: Modifier = Modifier, + diameter: Dp = 86.dp, +) { + val animatedProgress by animateFloatAsState( + targetValue = (level / 100f).coerceIn(0f, 1f), + animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow), + label = "batteryProgress", + ) + + val color = batteryColor(level) + val density = LocalDensity.current + val ringStroke = remember(density) { + Stroke(width = with(density) { 6.dp.toPx() }, cap = StrokeCap.Round) + } + + Box( + modifier = modifier.size(diameter), + contentAlignment = Alignment.Center, + ) { + CircularWavyProgressIndicator( + progress = { animatedProgress }, + modifier = Modifier.size(diameter), + color = color, + trackColor = MaterialTheme.colorScheme.surfaceContainerHighest, + stroke = ringStroke, + trackStroke = ringStroke, + amplitude = { 0.4f }, + wavelength = 34.dp, + ) + Icon( + painter = painterResource(iconRes), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(diameter * 0.38f), + ) + if (charging) { + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(24.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Filled.Bolt, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(15.dp), + ) + } + } + } +} + +/** A squiggly horizontal level meter (e.g. EQ band / strength visualisation). */ +@Composable +fun WavyLevel( + fraction: Float, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.primary, +) { + LinearWavyProgressIndicator( + progress = { fraction.coerceIn(0f, 1f) }, + modifier = modifier, + color = color, + trackColor = MaterialTheme.colorScheme.surfaceContainerHighest, + ) +} + +/** A shape-morphing loading indicator (for connecting / busy states). */ +@Composable +fun ExpressiveLoading( + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.primary, +) { + LoadingIndicator(modifier = modifier, color = color) +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveReconnectHero.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveReconnectHero.kt new file mode 100644 index 000000000..2e9c68502 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveReconnectHero.kt @@ -0,0 +1,231 @@ +/* + 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 . +*/ + +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.BluetoothSearching +import androidx.compose.material.icons.rounded.HeadsetOff +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialShapes +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.material3.toPath +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.drawWithContent +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.graphics.shapes.Morph +import kotlinx.coroutines.delay +import kotlin.math.min +import kotlin.time.Duration.Companion.seconds + +/** + * "AirPods not connected" hero: a large tonal disc whose inner shape morphs through + * [MaterialShapes] polygons, adapted from upstream LibrePods' M3 Expressive reconnect screen. + * The disc itself is the reconnect button — the shape squishes with a bouncy spring while + * pressed, and while a reconnect attempt is in flight it morphs continuously shape-to-shape, + * swaps to a searching icon, and animates from secondary to tertiary container colour. Flat + * tonal colours only — shape + motion carry the expression. + * + * The in-flight state auto-resets after five seconds so a failed attempt can be retried; on + * success the caller's screen leaves the not-connected state entirely, unmounting this. + * + * @param canReconnect whether a previously-connected device is known — gates the tap-to-reconnect + * affordance; when false the hero is a passive status display. + */ +@Composable +fun ExpressiveReconnectHero( + canReconnect: Boolean, + onReconnect: () -> Unit, + modifier: Modifier = Modifier, +) { + val haptics = rememberAppHaptics() + var reconnecting by remember { mutableStateOf(false) } + + LaunchedEffect(reconnecting) { + if (reconnecting) { + delay(5.seconds) + reconnecting = false + } + } + + val morphs = remember { + val polygons = listOf( + MaterialShapes.Cookie9Sided, + MaterialShapes.Clover4Leaf, + MaterialShapes.SoftBurst, + MaterialShapes.Sunny, + MaterialShapes.Pentagon, + MaterialShapes.Cookie4Sided, + MaterialShapes.Oval, + ).map { it.normalized() } + polygons.indices.map { i -> Morph(polygons[i], polygons[(i + 1) % polygons.size]) } + } + var morphIndex by remember { mutableIntStateOf(0) } + val morphProgress = remember { Animatable(0f) } + + LaunchedEffect(reconnecting) { + if (!reconnecting) { + morphIndex = 0 + morphProgress.snapTo(0f) + return@LaunchedEffect + } + while (true) { + morphProgress.snapTo(0f) + morphProgress.animateTo( + targetValue = 1f, + animationSpec = tween(durationMillis = 650, easing = FastOutSlowInEasing), + ) + morphIndex = (morphIndex + 1) % morphs.size + } + } + + val shapeColor by animateColorAsState( + targetValue = if (reconnecting) { + MaterialTheme.colorScheme.tertiaryContainer + } else { + MaterialTheme.colorScheme.secondaryContainer + }, + label = "reconnectShapeColor", + ) + val pressSpring = remember { + spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow) + } + val path = remember { Path() } + val scaleMatrix = remember { Matrix() } + + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .size(240.dp) + .background(MaterialTheme.colorScheme.surfaceContainerHigh, CircleShape) + .clickable( + interactionSource = null, + indication = ripple(bounded = false, radius = 120.dp), + enabled = canReconnect && !reconnecting, + onClick = {}, + ) + .pointerInput(canReconnect) { + if (!canReconnect) return@pointerInput + detectTapGestures( + onTap = { + if (!reconnecting) { + haptics.confirm() + // The press already morphed toward shape 1, so start the + // searching loop from there for a seamless hand-off. + morphIndex = 1 + reconnecting = true + onReconnect() + } + }, + onPress = { + if (!reconnecting) { + morphProgress.animateTo(1f, pressSpring) + tryAwaitRelease() + if (!reconnecting) { + morphProgress.animateTo(0f, pressSpring) + } + } + }, + ) + } + .drawWithContent { + val shapePath = morphs[morphIndex].toPath( + progress = morphProgress.value, + path = path, + ) + val bounds = shapePath.getBounds() + val scale = min(size.width / bounds.width, size.height / bounds.height) * 0.8f + scaleMatrix.reset() + scaleMatrix.scale(x = scale, y = scale) + shapePath.transform(scaleMatrix) + shapePath.translate(size.center - shapePath.getBounds().center) + drawPath(path = shapePath, color = shapeColor) + drawContent() + }, + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = if (reconnecting) { + Icons.AutoMirrored.Rounded.BluetoothSearching + } else { + Icons.Rounded.HeadsetOff + }, + contentDescription = null, + modifier = Modifier.size(84.dp), + tint = if (reconnecting) { + MaterialTheme.colorScheme.onTertiaryContainer + } else { + MaterialTheme.colorScheme.onSecondaryContainer + }, + ) + } + + Spacer(Modifier.height(32.dp)) + + Text( + text = if (reconnecting) "Reconnecting…" else "AirPods not connected", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(8.dp)) + Text( + text = when { + reconnecting -> "Connecting to your last device…" + canReconnect -> "Pop in your AirPods, or tap the shape to reconnect." + else -> "Pop in your AirPods and they'll connect automatically." + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveScaffold.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveScaffold.kt new file mode 100644 index 000000000..2abac5850 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveScaffold.kt @@ -0,0 +1,115 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +/** + * Per-screen scaffold for the new shell, modelled on Caffeine's page header: a fixed, **top-left + * aligned headline** sitting flush at the top of the content, with an optional tonal back button and + * trailing [actions]. Deliberately *not* a collapsing [androidx.compose.material3.LargeTopAppBar] — + * that pushed the title into the vertical middle of a tall bar and read as misaligned. The shell owns + * the bottom nav, so this only lays out the header + scrolling content beneath it. + * + * The [content] receives a zero [PaddingValues]: the header is laid out above the content (not as an + * inset), so callers can keep their existing `top = padding.calculateTopPadding()` usage as a no-op. + */ +@Composable +fun ExpressiveScreenScaffold( + title: String, + modifier: Modifier = Modifier, + onBack: (() -> Unit)? = null, + large: Boolean = onBack == null, + actions: @Composable RowScope.() -> Unit = {}, + content: @Composable (PaddingValues) -> Unit, +) { + val haptics = rememberAppHaptics() + + Column( + modifier = modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (onBack != null) { + FilledTonalIconButton( + onClick = { + haptics.navigation() + onBack() + }, + ) { + Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") + } + Spacer(Modifier.width(8.dp)) + } + Text( + text = title, + style = if (large) { + MaterialTheme.typography.headlineMedium + } else { + MaterialTheme.typography.headlineSmall + }, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Row( + verticalAlignment = Alignment.CenterVertically, + content = actions, + ) + } + + Box(modifier = Modifier + .weight(1f) + .fillMaxWidth()) { + content(PaddingValues(0.dp)) + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveSearch.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveSearch.kt new file mode 100644 index 000000000..c6e87e86c --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveSearch.kt @@ -0,0 +1,185 @@ +/* + 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 . +*/ + +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.ButtonGroupDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.ToggleButton +import androidx.compose.material3.ToggleButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +/** + * A pill-shaped M3 search field (tonal container, leading search icon, clear button). Paired with + * [ExpressiveSegmentedFilter] this gives the "search + category filter" pattern. + */ +@Composable +fun ExpressiveSearchField( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier = Modifier, + placeholder: String = "Search", +) { + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.padding(horizontal = 18.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { + if (query.isEmpty()) { + Text( + text = placeholder, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + BasicTextField( + value = query, + onValueChange = onQueryChange, + singleLine = true, + textStyle = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + modifier = Modifier.fillMaxWidth(), + ) + } + if (query.isNotEmpty()) { + val haptics = rememberAppHaptics() + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .size(22.dp) + .clickableNoRipple { + haptics.tick() + onQueryChange("") + }, + ) + } + } + } +} + +/** + * A horizontally-scrolling **connected button group** (M3 Expressive [ToggleButton]s with + * [ButtonGroupDefaults] leading/middle/trailing shapes) for category filters — an icon + label per + * segment, lifted from Caffeine's add-consumption category filter. The selected segment fills with the + * checked color and morphs to a rounder shape; overflow scrolls horizontally. + */ +@Composable +fun ExpressiveSegmentedFilter( + options: List, + selected: T, + onSelect: (T) -> Unit, + label: (T) -> String, + icon: (T) -> ImageVector, + modifier: Modifier = Modifier, +) { + val haptics = rememberAppHaptics() + Row( + modifier = modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + ) { + options.forEachIndexed { index, option -> + val isSelected = option == selected + ToggleButton( + checked = isSelected, + onCheckedChange = { + if (!isSelected) { + haptics.tick() + onSelect(option) + } + }, + shapes = when { + options.size == 1 -> ButtonGroupDefaults.connectedMiddleButtonShapes() + index == 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() + index == options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() + else -> ButtonGroupDefaults.connectedMiddleButtonShapes() + }, + ) { + Icon( + imageVector = icon(option), + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.size(ToggleButtonDefaults.IconSpacing)) + Text( + text = label(option), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +@Composable +private fun Modifier.clickableNoRipple(onClick: () -> Unit): Modifier { + val interaction = remember { androidx.compose.foundation.interaction.MutableInteractionSource() } + return this.clickable( + interactionSource = interaction, + indication = null, + onClick = onClick, + ) +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveSlider.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveSlider.kt new file mode 100644 index 000000000..56bbef829 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/ExpressiveSlider.kt @@ -0,0 +1,85 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.m3 + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +/** + * M3 Expressive [Slider] (thick track + large springy thumb) flanked by optional icons, with a tick + * haptic when the user releases. + */ +@Composable +fun ExpressiveSlider( + value: Float, + onValueChange: (Float) -> Unit, + valueRange: ClosedFloatingPointRange, + modifier: Modifier = Modifier, + enabled: Boolean = true, + steps: Int = 0, + startIcon: ImageVector? = null, + endIcon: ImageVector? = null, + onValueChangeFinished: (() -> Unit)? = null, +) { + val haptics = rememberAppHaptics() + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (startIcon != null) { + Icon( + imageVector = startIcon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + } + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + steps = steps, + enabled = enabled, + modifier = Modifier.weight(1f), + onValueChangeFinished = { + haptics.tick() + onValueChangeFinished?.invoke() + }, + ) + if (endIcon != null) { + Icon( + imageVector = endIcon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/ControlsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/ControlsScreen.kt new file mode 100644 index 000000000..dd49343da --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/ControlsScreen.kt @@ -0,0 +1,307 @@ +/* + 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 . +*/ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package me.kavishdevar.librepods.presentation.m3.screens + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.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.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Accessibility +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.Block +import androidx.compose.material.icons.filled.GraphicEq +import androidx.compose.material.icons.filled.Hearing +import androidx.compose.material.icons.filled.HearingDisabled +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.RecordVoiceOver +import androidx.compose.material.icons.filled.Sensors +import androidx.compose.material.icons.filled.TouchApp +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.filled.VolumeUp +import androidx.compose.material.icons.rounded.SentimentDissatisfied +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.ControlCommandIdentifiers +import me.kavishdevar.librepods.bluetooth.ATTHandles +import me.kavishdevar.librepods.data.Capability +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSearchField +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSegmentedFilter +import me.kavishdevar.librepods.presentation.m3.ExpressiveSwitchRow +import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel + +private enum class ControlCategory(val label: String, val icon: ImageVector) { + ALL("All", Icons.Filled.Apps), + AUDIO("Audio", Icons.Filled.GraphicEq), + GESTURES("Gestures", Icons.Filled.TouchApp), + HEARING("Hearing", Icons.Filled.Hearing), + CONNECTION("Connection", Icons.Filled.Sensors), +} + +private sealed interface ControlKind { + data class Toggle(val checked: Boolean, val onChange: (Boolean) -> Unit) : ControlKind + data class Nav(val route: String) : ControlKind +} + +private data class ControlEntry( + val title: String, + val subtitle: String?, + val icon: ImageVector, + val category: ControlCategory, + val kind: ControlKind, +) + +@Composable +fun ControlsScreen(viewModel: AirPodsViewModel, navController: NavController) { + val state by viewModel.uiState.collectAsState() + var query by remember { mutableStateOf("") } + var category by remember { mutableStateOf(ControlCategory.ALL) } + + val caps = state.capabilities + + fun controlBool(id: ControlCommandIdentifiers): Boolean = + state.controlStates[id]?.getOrNull(0) == 0x01.toByte() + + val entries = buildList { + if (caps.contains(Capability.LOUD_SOUND_REDUCTION)) { + add( + ControlEntry( + "Off listening mode", "Include an Off option in noise control", Icons.Filled.Block, + ControlCategory.AUDIO, + ControlKind.Toggle(controlBool(ControlCommandIdentifiers.ALLOW_OFF_OPTION)) { + viewModel.setOffListeningMode(it) + }, + ) + ) + } + if (caps.contains(Capability.ADAPTIVE_VOLUME)) { + add( + ControlEntry( + "Adaptive volume", "Auto-adjust volume to your surroundings", Icons.Filled.VolumeUp, + ControlCategory.AUDIO, + ControlKind.Toggle(controlBool(ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG)) { + viewModel.setControlCommandBoolean(ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG, it) + }, + ) + ) + } + if (caps.contains(Capability.CONVERSATION_AWARENESS)) { + add( + ControlEntry( + "Conversational awareness", "Lower volume when you start speaking", Icons.Filled.RecordVoiceOver, + ControlCategory.AUDIO, + ControlKind.Toggle(controlBool(ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG)) { + viewModel.setControlCommandBoolean(ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG, it) + }, + ) + ) + } + add(ControlEntry("Transparency tuning", "Customize ambient sound", Icons.Filled.Tune, ControlCategory.AUDIO, ControlKind.Nav("transparency_customization"))) + add(ControlEntry("Adaptive audio strength", "How aggressively noise is managed", Icons.Filled.GraphicEq, ControlCategory.AUDIO, ControlKind.Nav("adaptive_strength"))) + add(ControlEntry("Microphone", "Choose the active microphone", Icons.Filled.Mic, ControlCategory.AUDIO, ControlKind.Nav("accessibility"))) + + if (caps.contains(Capability.HEAD_GESTURES)) { + add(ControlEntry("Head gestures", "Nod or shake to answer calls", Icons.Filled.Sensors, ControlCategory.GESTURES, ControlKind.Nav("head_tracking"))) + } + add(ControlEntry("Press & hold", "Assign stem press-and-hold actions", Icons.Filled.TouchApp, ControlCategory.GESTURES, ControlKind.Nav("accessibility"))) + + if (caps.contains(Capability.HEARING_AID)) { + add(ControlEntry("Hearing aid", "Use your AirPods as a hearing aid", Icons.Filled.Hearing, ControlCategory.HEARING, ControlKind.Nav("hearing_aid"))) + } + if (caps.contains(Capability.PPE)) { + add(ControlEntry("Hearing protection", "Reduce loud environmental noise", Icons.Filled.HearingDisabled, ControlCategory.HEARING, ControlKind.Nav("hearing_protection"))) + } + if (caps.contains(Capability.LOUD_SOUND_REDUCTION)) { + add( + ControlEntry( + "Loud sound reduction", "Soften sudden loud noises", Icons.Filled.HearingDisabled, + ControlCategory.HEARING, + ControlKind.Toggle(state.loudSoundReductionEnabled) { + viewModel.setATTCharacteristicValue( + ATTHandles.LOUD_SOUND_REDUCTION, + byteArrayOf(if (it) 0x01 else 0x00), + ) + }, + ) + ) + } + + add( + ControlEntry( + "Automatic ear detection", "Pause audio when removed", Icons.Filled.Hearing, + ControlCategory.CONNECTION, + ControlKind.Toggle(state.automaticEarDetectionEnabled) { viewModel.setAutomaticEarDetectionEnabled(it) }, + ) + ) + add( + ControlEntry( + "Automatic connection", "Connect automatically when in range", Icons.Filled.Sensors, + ControlCategory.CONNECTION, + ControlKind.Toggle(state.automaticConnectionEnabled) { viewModel.setAutomaticConnectionEnabled(it) }, + ) + ) + add(ControlEntry("Accessibility", "Tone, balance and more", Icons.Filled.Accessibility, ControlCategory.CONNECTION, ControlKind.Nav("accessibility"))) + } + + val filtered = entries.filter { + (category == ControlCategory.ALL || it.category == category) && + (query.isBlank() || it.title.contains(query, ignoreCase = true)) + } + val grouped = filtered.groupBy { it.category } + + ExpressiveScreenScaffold(title = "Controls") { padding -> + // No live socket means nothing here can actually reach the AirPods, so hide the controls + // entirely rather than letting the user toggle things into the void. + if (!state.isLocallyConnected) { + ControlsNotConnected(padding) + return@ExpressiveScreenScaffold + } + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 140.dp, + start = 16.dp, + end = 16.dp, + ), + ) { + item(key = "search") { + ExpressiveSearchField(query = query, onQueryChange = { query = it }, placeholder = "Search controls") + Spacer(Modifier.height(12.dp)) + ExpressiveSegmentedFilter( + options = ControlCategory.entries.toList(), + selected = category, + onSelect = { category = it }, + label = { it.label }, + icon = { it.icon }, + ) + Spacer(Modifier.height(4.dp)) + } + + if (grouped.isEmpty()) { + item(key = "empty") { + Column(Modifier.animateItem()) { + Spacer(Modifier.height(48.dp)) + Text( + text = "No controls match \"$query\"", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + } + } + } + + grouped.forEach { (cat, items) -> + item(key = cat.name) { + // animateItem(): groups slide/fade as filtering changes which categories appear; + // animateContentSize(): the card resizes smoothly as rows are added/removed by search. + Column(Modifier.animateItem()) { + ExpressiveSectionHeader(cat.label) + ExpressiveGroup(modifier = Modifier.animateContentSize()) { + items.forEachIndexed { index, entry -> + if (index > 0) ExpressiveDivider() + when (val kind = entry.kind) { + is ControlKind.Toggle -> ExpressiveSwitchRow( + title = entry.title, + subtitle = entry.subtitle, + leadingIcon = entry.icon, + checked = kind.checked, + onCheckedChange = kind.onChange, + ) + is ControlKind.Nav -> ExpressiveListItem( + title = entry.title, + subtitle = entry.subtitle, + leadingIcon = entry.icon, + showChevron = true, + onClick = { navController.navigate(kind.route) }, + ) + } + } + } + } + } + } + } + } +} + +/** Sad-face empty state shown on the Controls tab while the AirPods aren't connected. */ +@Composable +private fun ControlsNotConnected(padding: PaddingValues) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Rounded.SentimentDissatisfied, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(96.dp), + ) + Spacer(Modifier.height(24.dp)) + Text( + text = "AirPods not connected", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(8.dp)) + Text( + text = "Connect your AirPods to adjust their controls.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/HomeScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/HomeScreen.kt new file mode 100644 index 000000000..ec75173fb --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/HomeScreen.kt @@ -0,0 +1,262 @@ +/* + 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 . +*/ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package me.kavishdevar.librepods.presentation.m3.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BatteryChargingFull +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Hearing +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import me.kavishdevar.librepods.R +import me.kavishdevar.librepods.bluetooth.AACPManager +import me.kavishdevar.librepods.data.Battery +import me.kavishdevar.librepods.data.BatteryComponent +import me.kavishdevar.librepods.data.BatteryStatus +import me.kavishdevar.librepods.data.Capability +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveNoiseControlSelector +import me.kavishdevar.librepods.presentation.m3.ExpressiveReconnectHero +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSwitchRow +import me.kavishdevar.librepods.presentation.m3.WavyBatteryRing +import me.kavishdevar.librepods.presentation.m3.viz.AncVizPrefs +import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel + +@Composable +fun HomeScreen(viewModel: AirPodsViewModel, navController: NavController) { + val state by viewModel.uiState.collectAsState() + + ExpressiveScreenScaffold(title = state.deviceName) { padding -> + if (!state.isLocallyConnected) { + NotConnected(padding, viewModel, navController) + return@ExpressiveScreenScaffold + } + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 140.dp, + start = 16.dp, + end = 16.dp, + ), + ) { + // Battery hero — squiggly wavy rings + item { + ExpressiveSectionHeader("Battery") + BatteryHero(state.battery) + } + + // Noise control — expressive segmented selector + if (state.capabilities.contains(Capability.LISTENING_MODE)) { + item { + ExpressiveSectionHeader("Noise control") + NoiseControlCard(state.offListeningMode, currentAncValue(state), onSelect = { + viewModel.setControlCommandInt( + AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE, it, + ) + }) + } + } + + // Quick toggles + item { + ExpressiveSectionHeader("Quick settings") + ExpressiveGroup { + ExpressiveListItem( + title = "Name", + value = state.deviceName, + leadingIcon = Icons.Filled.Edit, + showChevron = true, + onClick = { navController.navigate("rename") }, + ) + ExpressiveDivider() + ExpressiveSwitchRow( + title = "Automatic ear detection", + leadingIcon = Icons.Filled.Hearing, + checked = state.automaticEarDetectionEnabled, + onCheckedChange = viewModel::setAutomaticEarDetectionEnabled, + ) + ExpressiveDivider() + ExpressiveSwitchRow( + title = "Optimized charging", + subtitle = "Delay charging past 80% to extend battery life", + leadingIcon = Icons.Filled.BatteryChargingFull, + checked = state.dynamicEndOfCharge, + onCheckedChange = viewModel::setDynamicEndOfCharge, + ) + } + } + + item { + Spacer(Modifier.height(24.dp)) + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + TextButton(onClick = viewModel::disconnect) { + Text( + text = "Disconnect", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.error, + ) + } + } + } + } + } +} + +private fun currentAncValue(state: me.kavishdevar.librepods.presentation.viewmodel.AirPodsUiState): Int = + state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE] + ?.getOrNull(0)?.toInt() ?: 3 + +@Composable +private fun BatteryHero(battery: List) { + val present = battery.filter { it.status != BatteryStatus.DISCONNECTED && it.level > 0 } + ExpressiveGroup { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 20.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + if (present.isEmpty()) { + Text( + text = "No battery data", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp), + ) + } + present.forEach { component -> + Column(horizontalAlignment = Alignment.CenterHorizontally) { + WavyBatteryRing( + level = component.level, + charging = component.status == BatteryStatus.CHARGING || + component.status == BatteryStatus.OPTIMIZED_CHARGING, + iconRes = batteryIconRes(component.component), + ) + Spacer(Modifier.height(10.dp)) + Text( + text = batteryLabel(component.component), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = "${component.level}%", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + } + } +} + +private fun batteryLabel(component: Int): String = when (component) { + BatteryComponent.LEFT -> "Left" + BatteryComponent.RIGHT -> "Right" + BatteryComponent.CASE -> "Case" + else -> "" +} + +private fun batteryIconRes(component: Int): Int = when (component) { + BatteryComponent.LEFT -> R.drawable.airpods_pro_left_notification + BatteryComponent.RIGHT -> R.drawable.airpods_pro_right_notification + else -> R.drawable.airpods_pro_case_notification +} + +@Composable +private fun NoiseControlCard(showOff: Boolean, currentValue: Int, onSelect: (Int) -> Unit) { + val context = LocalContext.current + LaunchedEffect(Unit) { AncVizPrefs.loadIfNeeded(context) } + val vizStyle by AncVizPrefs.style.collectAsState() + // Protocol listening-mode values, in display order. + val modes = buildList { + if (showOff) add(1) // Off + add(3) // Transparency + add(4) // Adaptive + add(2) // Noise Cancellation + } + ExpressiveGroup { + ExpressiveNoiseControlSelector( + modes = modes, + selected = currentValue, + onSelect = onSelect, + vizStyle = vizStyle, + modifier = Modifier.padding(20.dp), + ) + } +} + +@Composable +private fun NotConnected( + padding: PaddingValues, + viewModel: AirPodsViewModel, + navController: NavController, +) { + val state by viewModel.uiState.collectAsState() + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + ExpressiveReconnectHero( + canReconnect = state.connectionSuccessful, + onReconnect = viewModel::reconnectFromSavedMac, + ) + Spacer(Modifier.height(28.dp)) + OutlinedButton(onClick = { navController.navigate("troubleshooting") }) { + Text( + text = "Troubleshoot", + style = MaterialTheme.typography.labelLarge, + ) + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/SettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/SettingsScreen.kt new file mode 100644 index 000000000..afc337b57 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/screens/SettingsScreen.kt @@ -0,0 +1,137 @@ +/* + 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 . +*/ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package me.kavishdevar.librepods.presentation.m3.screens + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Article +import androidx.compose.material.icons.filled.Accessibility +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import me.kavishdevar.librepods.BuildConfig +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel + +@Composable +fun SettingsScreen(viewModel: AirPodsViewModel, navController: NavController) { + ExpressiveScreenScaffold(title = "Settings") { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 140.dp, + start = 16.dp, + end = 16.dp, + ), + ) { + item { + ExpressiveSectionHeader("App") + ExpressiveGroup { + ExpressiveListItem( + title = "App settings", + subtitle = "Widget, takeover, conversational awareness", + leadingIcon = Icons.Filled.Tune, + showChevron = true, + onClick = { navController.navigate("app_settings") }, + ) + ExpressiveDivider() + ExpressiveListItem( + title = "Accessibility", + leadingIcon = Icons.Filled.Accessibility, + showChevron = true, + onClick = { navController.navigate("accessibility") }, + ) + } + } + + item { + ExpressiveSectionHeader("Device") + ExpressiveGroup { + ExpressiveListItem( + title = "Rename", + leadingIcon = Icons.Filled.Edit, + showChevron = true, + onClick = { navController.navigate("rename") }, + ) + ExpressiveDivider() + ExpressiveListItem( + title = "Firmware & version", + leadingIcon = Icons.Filled.Info, + showChevron = true, + onClick = { navController.navigate("version_info") }, + ) + } + } + + item { + ExpressiveSectionHeader("Support") + ExpressiveGroup { + ExpressiveListItem( + title = "Troubleshooting", + leadingIcon = Icons.Filled.Settings, + showChevron = true, + onClick = { navController.navigate("troubleshooting") }, + ) + if (!BuildConfig.PLAY_BUILD) { + ExpressiveDivider() + ExpressiveListItem( + title = "Debug", + subtitle = "Raw AACP packet log", + leadingIcon = Icons.Filled.BugReport, + showChevron = true, + onClick = { navController.navigate("debug") }, + ) + } + } + } + + item { + ExpressiveSectionHeader("About") + ExpressiveGroup { + ExpressiveListItem( + title = "Open-source licenses", + subtitle = "LibrePods v${BuildConfig.VERSION_NAME}", + leadingIcon = Icons.AutoMirrored.Filled.Article, + showChevron = true, + onClick = { navController.navigate("open_source_licenses") }, + ) + } + Spacer(Modifier.height(8.dp)) + } + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/viz/AncViz.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/viz/AncViz.kt new file mode 100644 index 000000000..b6d1cb8de --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/viz/AncViz.kt @@ -0,0 +1,305 @@ +/* + 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 . +*/ + +// Noise-control centerpiece visualizations. Three interchangeable flat-tonal scenes (Orb / Waveform / +// Radial EQ) painted on a Compose Canvas, driven entirely by four per-mode scalars (energy, ripple, +// pulse, dome) that ease toward the active mode's targets each frame — the scenes never branch on +// "which mode", so mode changes morph smoothly. +// +// Design language (Material 3 Expressive, flat tonal): NO gradients, NO glow, NO white-gloss specular, +// NO dark vignette. Solid theme-colour shapes only; every stroke/size is dp-scaled (so weights stay +// consistent across densities instead of shimmering sub-pixel hairlines); motion is choreographed sine +// (deterministic, eased) rather than random jitter. Per-mode colour is pulled from the live Material 3 +// scheme (primary / secondary / tertiary + their containers) so it tracks dynamic colour and never +// clashes with the app accent. The Orb (default) uses an aperture/iris metaphor — its pupil closes when +// sound is sealed out (Noise Cancellation) and opens wide when the world is let in (Transparency). + +package me.kavishdevar.librepods.presentation.m3.viz + +import androidx.compose.foundation.Canvas +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.withFrameNanos +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.unit.dp +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.sin + +/** The interchangeable centerpiece visualizations (user-switchable in settings). */ +enum class AncVizStyle(val key: String, val label: String) { + ORB("orb", "Orb"), + WAVEFORM("waveform", "Waveform"), + RADIAL("radial", "Radial EQ"); + + companion object { + val DEFAULT = ORB + // Unknown / retired keys (e.g. the dropped "blob"/"particles") fall back to the default. + fun fromKey(key: String?): AncVizStyle = entries.firstOrNull { it.key == key } ?: DEFAULT + } +} + +private const val TAU = (2.0 * PI).toFloat() +private const val PIf = PI.toFloat() + +private fun lerpF(a: Float, b: Float, t: Float) = a + (b - a) * t + +// ── Mode model: each mode's four scene scalars (0..1). ── +private class Scene(val energy: Float, val ripple: Float, val pulse: Float, val dome: Float) + +private fun sceneFor(mode: Int): Scene = when (mode) { + 2 -> Scene(energy = 0.12f, ripple = 0.0f, pulse = 0.06f, dome = 1.0f) // Noise Cancellation + 3 -> Scene(energy = 1.0f, ripple = 1.0f, pulse = 0.22f, dome = 0.0f) // Transparency + 4 -> Scene(energy = 0.52f, ripple = 0.32f, pulse = 1.0f, dome = 0.16f) // Adaptive + else -> Scene(energy = 0.9f, ripple = 0.10f, pulse = 0.16f, dome = 0.0f) // Off +} + +// ── Per-mode colour, pulled from the live M3 scheme so it tracks dynamic colour and never clashes with +// the app accent. `accent` is the vivid tone (discs/strokes); `soft` its muted container companion (the +// low-energy tone the scenes lerp up from, and the Orb's "pupil"). The scheme already encodes light/dark. ── +private fun modeAccent(mode: Int, scheme: ColorScheme): Color = when (mode) { + 2 -> scheme.primary // Noise Cancellation — the headline mode + 3 -> scheme.tertiary // Transparency — the most hue-distinct role + 4 -> scheme.secondary // Adaptive + else -> scheme.onSurfaceVariant // Off — neutral +} + +private fun modeSoft(mode: Int, scheme: ColorScheme): Color = when (mode) { + 2 -> scheme.primaryContainer + 3 -> scheme.tertiaryContainer + 4 -> scheme.secondaryContainer + else -> scheme.surfaceVariant +} + +/** Per-frame environment handed to each renderer (geometry + eased scalars + colour helpers). */ +private class VizEnv( + val w: Float, val h: Float, val cx: Float, val cy: Float, val minWH: Float, + val dt: Float, val t: Float, + val energy: Float, val ripple: Float, val dome: Float, val pulse: Float, + val accent: Color, val soft: Color, +) { + fun a(alpha: Float) = accent.copy(alpha = alpha.coerceIn(0f, 1f)) +} + +/** Holds the eased live scalars/colours + animation clock between frames. */ +private class VizHolder { + var energy = 0f; var ripple = 0f; var dome = 0f; var pulse = 0f + var accent = Color.Transparent; var soft = Color.Transparent + private var tEnergy = 0f; private var tRipple = 0f; private var tDome = 0f; private var tPulse = 0f + private var tAccent = Color.Transparent; private var tSoft = Color.Transparent + var t = 0f; var dt = 0f + private var primed = false + + fun setTarget(s: Scene, a: Color, sf: Color) { + tEnergy = s.energy; tRipple = s.ripple; tDome = s.dome; tPulse = s.pulse; tAccent = a; tSoft = sf + if (!primed) { + energy = s.energy; ripple = s.ripple; dome = s.dome; pulse = s.pulse; accent = a; soft = sf + primed = true + } + } + + fun advance(frameDt: Float) { + dt = frameDt + val k = (1.0 - 0.0008.pow(frameDt.toDouble())).toFloat() + energy = lerpF(energy, tEnergy, k); ripple = lerpF(ripple, tRipple, k) + dome = lerpF(dome, tDome, k); pulse = lerpF(pulse, tPulse, k) + accent = lerp(accent, tAccent, k); soft = lerp(soft, tSoft, k) + t += frameDt + } +} + +/** + * The animated centerpiece. Runs a [withFrameNanos] clock, eases the four scalars (+ accent/soft + * colours) toward the active mode's targets, and paints the chosen [style] onto a Canvas every frame. + * Crash-guarded (a bad frame can never take down the UI). + */ +@Composable +fun AncVisualizer( + modeValue: Int, + style: AncVizStyle, + modifier: Modifier = Modifier, +) { + val scheme = MaterialTheme.colorScheme + val holder = remember { VizHolder() } + + // Re-target whenever the mode or the resolved scheme colours change (mode switch, dark-mode toggle, + // or a dynamic-colour palette change); the holder eases between them each frame. + val accent = modeAccent(modeValue, scheme) + val soft = modeSoft(modeValue, scheme) + LaunchedEffect(modeValue, accent, soft) { + holder.setTarget(sceneFor(modeValue), accent, soft) + } + + val tick = remember { mutableLongStateOf(0L) } + LaunchedEffect(Unit) { + var last = 0L + while (true) { + withFrameNanos { now -> + val dt = if (last == 0L) 0f else ((now - last) / 1_000_000_000f).coerceAtMost(0.05f) + last = now + holder.advance(dt) + tick.longValue = now + } + } + } + + Canvas(modifier) { + tick.longValue // subscribe to the frame clock so we repaint each frame + val w = size.width + val h = size.height + if (w < 2f || h < 2f) return@Canvas + val env = VizEnv( + w = w, h = h, cx = w / 2f, cy = h / 2f, minWH = max(1f, min(w, h)), + dt = holder.dt, t = holder.t, + energy = holder.energy, ripple = holder.ripple, dome = holder.dome, pulse = holder.pulse, + accent = holder.accent, soft = holder.soft, + ) + try { + when (style) { + AncVizStyle.ORB -> renderOrb(env) + AncVizStyle.WAVEFORM -> renderWaveform(env) + AncVizStyle.RADIAL -> renderRadialEq(env) + } + } catch (_: Throwable) { + // never let a single bad frame crash the UI + } + } +} + +// ── shared scene piece ─────────────────────────────────────────────────────── +/** + * Crisp concentric rings that grow outward and fade — the "letting sound through" motif (driven by + * [VizEnv.ripple], so they only appear in Transparency/Adaptive). Flat strokes only: clean rings whose + * alpha steps down as they expand, never a radial-gradient haze. + */ +private fun DrawScope.expandingRings(env: VizEnv, count: Int, maxRFrac: Float) { + if (env.ripple <= 0.01f) return + val maxR = env.minWH * maxRFrac + val stroke = 2.dp.toPx() + for (i in 0 until count) { + val ph = ((env.t * 0.3f) + i.toFloat() / count) % 1f + drawCircle( + color = env.a(env.ripple * (1f - ph) * 0.5f), + radius = max(0f, ph * maxR), center = Offset(env.cx, env.cy), style = Stroke(stroke), + ) + } +} + +// ── 1 · Orb (default) — an aperture/iris ───────────────────────────────────── +private fun DrawScope.renderOrb(env: VizEnv) { + val center = Offset(env.cx, env.cy) + val breathe = 0.5f + 0.5f * sin(env.t * 1.7f) + + // Aperture radius: grows with energy, contracts a touch when sealed (dome), gently breathes (pulse). + val r = max(2f, env.minWH * (0.20f + 0.12f * env.energy) * + (1f - 0.18f * env.dome) * (1f + 0.05f * breathe * (0.4f + env.pulse))) + + expandingRings(env, count = 3, maxRFrac = 0.46f) + + // Outer halo — a single crisp stroke ring that pulses (no glow). + drawCircle( + color = env.a(0.20f + 0.30f * env.pulse * breathe), + radius = r * (1.30f + 0.12f * breathe * env.pulse), center = center, style = Stroke(2.dp.toPx()), + ) + + // Main disc: solid accent. + drawCircle(color = env.accent, radius = r, center = center) + + // Inner "pupil": solid soft/container disc whose size = how OPEN the aperture is. + // Closed for Noise Cancellation (dome high) → reads sealed; wide for Transparency/Off → reads open. + val openness = ((0.30f + 0.50f * env.energy) * (1f - 0.90f * env.dome)).coerceIn(0.06f, 0.78f) + drawCircle(color = env.soft, radius = r * openness, center = center) + + // Tiny accent centre — keeps the iris legible when the pupil is wide. + drawCircle(color = env.accent, radius = r * openness * 0.34f, center = center) +} + +// ── 2 · Waveform — solid tonal bars ────────────────────────────────────────── +private fun DrawScope.renderWaveform(env: VizEnv) { + val n = 33 + val gap = 4.dp.toPx() + val bw = max(3.dp.toPx(), (env.w - gap * (n - 1)) / n) + val totalW = n * bw + (n - 1) * gap + var x = env.cx - totalW / 2f + val maxH = env.h * 0.40f + val minBar = 3.dp.toPx() + val stillness = 1f - env.dome * 0.80f + + for (i in 0 until n) { + val frac = i.toFloat() / (n - 1) + // Deterministic, choreographed height — two detuned sines per bar, windowed into a smooth arch + // (tall in the middle). No randomness, so the motion reads as designed rather than jittery. + val window = 0.40f + 0.60f * sin(frac * PIf) + val phase = frac * 7f + val wave = 0.5f + 0.5f * (0.65f * sin(env.t * 2.2f + phase) + 0.35f * sin(env.t * 3.7f + phase * 1.7f)) + val amp = maxH * env.energy * window * (0.35f + 0.65f * wave * stillness) + minBar + val f = min(1f, amp / maxH) + drawRoundRect( + color = lerp(env.soft, env.accent, f), + topLeft = Offset(x, env.cy - amp), size = Size(bw, amp * 2f), + cornerRadius = CornerRadius(bw / 2f), + ) + x += bw + gap + } +} + +// ── 3 · Radial EQ — solid tonal spokes around a flat core ──────────────────── +private fun DrawScope.renderRadialEq(env: VizEnv) { + val center = Offset(env.cx, env.cy) + val n = 56 + val baseR = env.minWH * 0.20f + val maxLen = env.minWH * 0.20f + val rot = env.t * 0.10f + val stillness = 1f - env.dome * 0.80f + val stroke = 3.dp.toPx() + val minLen = 1.5.dp.toPx() + + // Flat core: a solid container disc with one crisp accent ring (no gradient core). + drawCircle(color = env.soft, radius = baseR, center = center) + drawCircle(color = env.a(0.50f), radius = baseR, center = center, style = Stroke(1.5.dp.toPx())) + + for (i in 0 until n) { + val frac = i.toFloat() / n + val ang = frac * TAU + rot + val phase = frac * TAU * 3f + val wave = 0.5f + 0.5f * (0.60f * sin(env.t * 2.0f + phase) + 0.40f * sin(env.t * 3.3f - phase * 1.3f)) + val len = max(minLen, maxLen * env.energy * (0.30f + 0.70f * wave * stillness)) + val ix = env.cx + cos(ang) * baseR; val iy = env.cy + sin(ang) * baseR + val ox = env.cx + cos(ang) * (baseR + len); val oy = env.cy + sin(ang) * (baseR + len) + val f = min(1f, len / maxLen) + drawLine( + color = lerp(env.soft, env.accent, f), + start = Offset(ix, iy), end = Offset(ox, oy), strokeWidth = stroke, cap = StrokeCap.Round, + ) + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/viz/AncVizPrefs.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/viz/AncVizPrefs.kt new file mode 100644 index 000000000..79a4bc303 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/m3/viz/AncVizPrefs.kt @@ -0,0 +1,51 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.m3.viz + +import android.content.Context +import androidx.core.content.edit +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * The user's chosen noise-control centerpiece [AncVizStyle], persisted in the app's `settings` + * SharedPreferences and exposed as a [StateFlow] so the home card and the settings screen stay in sync. + */ +object AncVizPrefs { + private const val PREFS = "settings" + private const val KEY = "anc_viz_style" + + private val _style = MutableStateFlow(AncVizStyle.DEFAULT) + val style: StateFlow = _style.asStateFlow() + + @Volatile private var loaded = false + + fun loadIfNeeded(context: Context) { + if (loaded) return + loaded = true + val key = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY, null) + _style.value = AncVizStyle.fromKey(key) + } + + fun set(context: Context, style: AncVizStyle) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit { putString(KEY, style.key) } + _style.value = style + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/IslandWindow.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/IslandWindow.kt deleted file mode 100644 index bf5eff89e..000000000 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/IslandWindow.kt +++ /dev/null @@ -1,758 +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 . -*/ - -@file:OptIn(ExperimentalEncodingApi::class) - -package me.kavishdevar.librepods.presentation.overlays - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.ObjectAnimator -import android.animation.PropertyValuesHolder -import android.animation.ValueAnimator -import android.annotation.SuppressLint -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.res.Resources -import android.graphics.PixelFormat -import android.graphics.drawable.GradientDrawable -import android.media.AudioManager -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.util.Log.e -import android.view.Gravity -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.VelocityTracker -import android.view.View -import android.view.WindowManager -import android.view.animation.AccelerateInterpolator -import android.view.animation.AnticipateOvershootInterpolator -import android.view.animation.DecelerateInterpolator -import android.view.animation.OvershootInterpolator -import android.widget.FrameLayout -import android.widget.ImageButton -import android.widget.LinearLayout -import android.widget.ProgressBar -import android.widget.TextView -import android.widget.VideoView -import androidx.core.net.toUri -import androidx.dynamicanimation.animation.DynamicAnimation -import androidx.dynamicanimation.animation.SpringAnimation -import androidx.dynamicanimation.animation.SpringForce -import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.data.AirPodsNotifications -import me.kavishdevar.librepods.data.Battery -import me.kavishdevar.librepods.data.BatteryComponent -import me.kavishdevar.librepods.data.BatteryStatus -import me.kavishdevar.librepods.services.ServiceManager -import kotlin.io.encoding.ExperimentalEncodingApi -import kotlin.math.abs - -enum class IslandType { - CONNECTED, - TAKING_OVER, - MOVED_TO_REMOTE, - MOVED_TO_OTHER_DEVICE, -} - -class IslandWindow(private val context: Context) { - private val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - @SuppressLint("InflateParams") - private val islandView: View = LayoutInflater.from(context).inflate(R.layout.island_window, null) - private var isClosing = false - private var params: WindowManager.LayoutParams? = null - - private var initialY = 0f - private var initialTouchY = 0f - private var lastTouchY = 0f - private var velocityTracker: VelocityTracker? = null - private var isBeingDragged = false - private var autoCloseHandler: Handler? = null - private var autoCloseRunnable: Runnable? = null - private var initialHeight = 0 - private var screenHeight = 0 - private var isDraggingDown = false - private var lastMoveTime = 0L - private var yMovement = 0f - private var dragDistance = 0f - - private var initialConnectedTextY = 0f - private var initialDeviceTextY = 0f - private var initialBatteryViewY = 0f - private var initialVideoViewY = 0f - private var initialTextSeparation = 0f - - private val containerView = FrameLayout(context) - - private lateinit var springAnimation: SpringAnimation - private val flingAnimator = ValueAnimator() - - private val batteryReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == AirPodsNotifications.BATTERY_DATA) { - val batteryList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableArrayListExtra("data", Battery::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableArrayListExtra("data") - } - updateBatteryDisplay(batteryList) - } else if (intent?.action == AirPodsNotifications.DISCONNECT_RECEIVERS) { - try { - context?.unregisterReceiver(this) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - } - - val isVisible: Boolean - get() = containerView.parent != null && containerView.visibility == View.VISIBLE - - @SuppressLint("SetTextI18n") - private fun updateBatteryDisplay(batteryList: ArrayList?) { - if (batteryList == null || batteryList.isEmpty()) return - - val leftBattery = batteryList.find { it.component == BatteryComponent.LEFT } - val rightBattery = batteryList.find { it.component == BatteryComponent.RIGHT } - - val leftLevel = leftBattery?.level ?: 0 - val rightLevel = rightBattery?.level ?: 0 - leftBattery?.status ?: BatteryStatus.DISCONNECTED - rightBattery?.status ?: BatteryStatus.DISCONNECTED - - val batteryText = islandView.findViewById(R.id.island_battery_text) - val batteryProgressBar = islandView.findViewById(R.id.island_battery_progress) - - val displayBatteryLevel = when { - leftLevel > 0 && rightLevel > 0 -> minOf(leftLevel, rightLevel) - leftLevel > 0 -> leftLevel - rightLevel > 0 -> rightLevel - else -> null - } - - if (displayBatteryLevel != null) { - batteryText.text = "$displayBatteryLevel%" - batteryProgressBar.progress = displayBatteryLevel - batteryProgressBar.isIndeterminate = false - } else { - batteryText.text = "?" - batteryProgressBar.progress = 0 - batteryProgressBar.isIndeterminate = false - } - } - - @SuppressLint("SetTextI18s", "ClickableViewAccessibility", "UnspecifiedRegisterReceiverFlag", - "SetTextI18n" - ) - fun show(name: String, batteryPercentage: Int, context: Context, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false, otherDeviceName: String? = null) { - if (ServiceManager.getService()?.islandOpen == true) return - else ServiceManager.getService()?.islandOpen = true - - val displayMetrics = Resources.getSystem().displayMetrics - val width = (displayMetrics.widthPixels * 0.95).toInt() - screenHeight = displayMetrics.heightPixels - - val batteryList = ServiceManager.getService()?.getBattery() - val batteryText = islandView.findViewById(R.id.island_battery_text) - val batteryProgressBar = islandView.findViewById(R.id.island_battery_progress) - - val displayBatteryLevel = if (batteryList != null) { - val leftBattery = batteryList.find { it.component == BatteryComponent.LEFT } - val rightBattery = batteryList.find { it.component == BatteryComponent.RIGHT } - - when { - (leftBattery?.level ?: 0) > 0 && (rightBattery?.level ?: 0) > 0 -> - minOf(leftBattery!!.level, rightBattery!!.level) - (leftBattery?.level ?: 0) > 0 -> leftBattery!!.level - (rightBattery?.level ?: 0) > 0 -> rightBattery!!.level - batteryPercentage > 0 -> batteryPercentage - else -> null - } - } else if (batteryPercentage > 0) { - batteryPercentage - } else { - null - } - - if (displayBatteryLevel != null) { - batteryText.text = "$displayBatteryLevel%" - batteryProgressBar.progress = displayBatteryLevel - } else { - batteryText.text = "?" - batteryProgressBar.progress = 0 - } - - batteryProgressBar.isIndeterminate = false - islandView.findViewById(R.id.island_device_name).text = name - - val actionButton = islandView.findViewById(R.id.island_action_button) - val batteryBg = islandView.findViewById(R.id.island_battery_bg) - if (type == IslandType.MOVED_TO_OTHER_DEVICE && !reversed) { - actionButton.visibility = View.VISIBLE - actionButton.setOnClickListener { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - ServiceManager.getService()?.takeOver("reverse") - } - close() - } - batteryText.visibility = View.GONE - batteryProgressBar.visibility = View.GONE - batteryBg.visibility = View.GONE - } else { - actionButton.visibility = View.GONE - batteryText.visibility = View.VISIBLE - batteryProgressBar.visibility = View.VISIBLE - batteryBg.visibility = View.VISIBLE - } - - val batteryIntentFilter = IntentFilter(AirPodsNotifications.BATTERY_DATA) - batteryIntentFilter.addAction(AirPodsNotifications.DISCONNECT_RECEIVERS) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver(batteryReceiver, batteryIntentFilter, Context.RECEIVER_EXPORTED) - } else { - context.registerReceiver(batteryReceiver, batteryIntentFilter) - } - - ServiceManager.getService()?.sendBatteryBroadcast() - - containerView.removeAllViews() - val containerParams = FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT - ) - - containerView.addView(islandView, containerParams) - - params = WindowManager.LayoutParams( - width, - WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, - PixelFormat.TRANSLUCENT - ).apply { - gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL - } - - islandView.visibility = View.VISIBLE - containerView.visibility = View.VISIBLE - - containerView.setOnTouchListener { _, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return@setOnTouchListener false) - flingAnimator.cancel() - - velocityTracker?.recycle() - velocityTracker = VelocityTracker.obtain() - velocityTracker?.addMovement(event) - - initialY = containerView.translationY - initialTouchY = event.rawY - lastTouchY = event.rawY - initialHeight = islandView.height - isBeingDragged = false - isDraggingDown = false - lastMoveTime = System.currentTimeMillis() - dragDistance = 0f - - captureInitialPositions() - - true - } - MotionEvent.ACTION_MOVE -> { - velocityTracker?.addMovement(event) - val deltaY = event.rawY - initialTouchY - val moveDelta = event.rawY - lastTouchY - dragDistance += abs(moveDelta) - - isDraggingDown = moveDelta > 0 - - val currentTime = System.currentTimeMillis() - val timeDelta = currentTime - lastMoveTime - if (timeDelta > 0) { - yMovement = moveDelta / timeDelta * 10 - } - lastMoveTime = currentTime - - if (abs(deltaY) > 5 || isBeingDragged) { - isBeingDragged = true - - // Cancel auto close timer when dragging starts - autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return@setOnTouchListener false) - - val dampedDeltaY = if (deltaY > 0) { - initialY + (deltaY * 0.6f) - } else { - initialY + (deltaY * 0.9f) - } - containerView.translationY = dampedDeltaY - - if (isDraggingDown && deltaY > 0) { - val stretchAmount = (deltaY * 0.5f).coerceAtMost(200f) - applyCustomStretchEffect(stretchAmount) - } - } - - lastTouchY = event.rawY - true - } - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - velocityTracker?.addMovement(event) - velocityTracker?.computeCurrentVelocity(1000) - val yVelocity = velocityTracker?.yVelocity ?: 0f - - if (isBeingDragged) { - val currentTranslationY = containerView.translationY - abs(yVelocity) > 800 - val significantDrag = abs(dragDistance) > 80 - - when { - yVelocity < -1200 || (currentTranslationY < -80 && !isDraggingDown) -> { - animateDismissWithInertia(yVelocity) - } - yVelocity > 1200 || (isDraggingDown && significantDrag) -> { - animateExpandWithStretch(yVelocity) - } - else -> { - springBackWithInertia(yVelocity) - } - } - } else if (dragDistance < 10) { - resetAutoCloseTimer() - } - - velocityTracker?.recycle() - velocityTracker = null - isBeingDragged = false - true - } - else -> false - } - } - - when (type) { - IslandType.CONNECTED -> { - islandView.findViewById(R.id.island_connected_text).text = context.getString(R.string.island_connected_text) - } - IslandType.TAKING_OVER -> { - islandView.findViewById(R.id.island_connected_text).text = context.getString(R.string.island_taking_over_text) - } - IslandType.MOVED_TO_REMOTE -> { - islandView.findViewById(R.id.island_connected_text).text = context.getString(R.string.island_moved_to_remote_text) - } - IslandType.MOVED_TO_OTHER_DEVICE -> { - if (otherDeviceName == null || otherDeviceName.isEmpty()) { - e("IslandWindow", "Other device name is null or empty for MOVED_TO_OTHER_DEVICE type") - } - if (reversed) { - islandView.findViewById(R.id.island_connected_text).text = context.getString(R.string.island_moved_to_other_device_reversed_text) - } else { - islandView.findViewById(R.id.island_connected_text).text = context.getString(R.string.island_moved_to_other_device_text, otherDeviceName) - } - } - } - - val videoView = islandView.findViewById(R.id.island_video_view) - val videoUri = "android.resource://me.kavishdevar.librepods/${R.raw.island}".toUri() - videoView.setAudioFocusRequest(AudioManager.AUDIOFOCUS_NONE) - videoView.setVideoURI(videoUri) - videoView.setOnPreparedListener { mediaPlayer -> - mediaPlayer.isLooping = true - videoView.start() - } - - try { - windowManager.addView(containerView, params) - } catch (e: Exception) { - e.printStackTrace() - } - - islandView.post { - initialHeight = islandView.height - captureInitialPositions() - } - - springAnimation = SpringAnimation(containerView, DynamicAnimation.TRANSLATION_Y, 0f).apply { - spring = SpringForce(0f) - .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) - .setStiffness(SpringForce.STIFFNESS_MEDIUM) - } - - val scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 0.5f, 1f) - val scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 0.5f, 1f) - val translationY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, -200f, 0f) - ObjectAnimator.ofPropertyValuesHolder(containerView, scaleX, scaleY, translationY).apply { - duration = 700 - interpolator = AnticipateOvershootInterpolator() - start() - } - - resetAutoCloseTimer() - } - - private fun captureInitialPositions() { - val connectedText = islandView.findViewById(R.id.island_connected_text) - val deviceText = islandView.findViewById(R.id.island_device_name) - val batteryView = islandView.findViewById(R.id.island_battery_container) - val videoView = islandView.findViewById(R.id.island_video_view) - - connectedText.post { - initialConnectedTextY = connectedText.y - initialDeviceTextY = deviceText.y - initialTextSeparation = deviceText.y - (connectedText.y + connectedText.height) - - if (batteryView != null) initialBatteryViewY = batteryView.y - initialVideoViewY = videoView.y - } - } - - private fun applyCustomStretchEffect(stretchAmount: Float) { - try { - val mainLayout = islandView.findViewById(R.id.island_window_layout) - islandView.findViewById(R.id.island_connected_text) - val deviceText = islandView.findViewById(R.id.island_device_name) - islandView.findViewById(R.id.island_battery_container) - islandView.findViewById(R.id.island_video_view) - - val stretchFactor = 1f + (stretchAmount / 300f).coerceAtMost(4.0f) - val newMinHeight = (initialHeight * stretchFactor).toInt() - mainLayout.minimumHeight = newMinHeight - - val textMarginIncrease = (stretchAmount * 0.8f).toInt() - - val deviceTextParams = deviceText.layoutParams as LinearLayout.LayoutParams - deviceTextParams.topMargin = textMarginIncrease - deviceText.layoutParams = deviceTextParams - - val background = mainLayout.background - if (background is GradientDrawable) { - val cornerRadius = 56f - background.cornerRadius = cornerRadius - } - - if (params != null) { - params!!.height = screenHeight - - val containerParams = containerView.layoutParams - containerParams.height = screenHeight - containerView.layoutParams = containerParams - - try { - windowManager.updateViewLayout(containerView, params) - } catch (e: Exception) { - e.printStackTrace() - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun resetAutoCloseTimer() { - autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return) - autoCloseHandler = Handler(Looper.getMainLooper()) - autoCloseRunnable = Runnable { close() } - autoCloseHandler?.postDelayed(autoCloseRunnable!!, 4500) - } - - private fun springBackWithInertia(velocity: Float) { - springAnimation.cancel() - flingAnimator.cancel() - - springAnimation.setStartVelocity(velocity) - - val baseStiffness = SpringForce.STIFFNESS_MEDIUM - val dynamicStiffness = baseStiffness * (1f + (abs(velocity) / 3000f)) - springAnimation.spring = SpringForce(0f) - .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) - .setStiffness(dynamicStiffness) - - resetStretchEffects() - - if (params != null) { - params!!.height = WindowManager.LayoutParams.WRAP_CONTENT - try { - windowManager.updateViewLayout(containerView, params) - } catch (e: Exception) { - e.printStackTrace() - } - } - - springAnimation.start() - } - - private fun resetStretchEffects() { - try { - val mainLayout = islandView.findViewById(R.id.island_window_layout) - val deviceText = islandView.findViewById(R.id.island_device_name) - - val heightAnimator = ValueAnimator.ofInt(mainLayout.minimumHeight, initialHeight) - heightAnimator.duration = 300 - heightAnimator.interpolator = OvershootInterpolator(1.5f) - heightAnimator.addUpdateListener { animation -> - mainLayout.minimumHeight = animation.animatedValue as Int - } - - val deviceTextParams = deviceText.layoutParams as LinearLayout.LayoutParams - val textMarginAnimator = ValueAnimator.ofInt(deviceTextParams.topMargin, 0) - textMarginAnimator.duration = 300 - textMarginAnimator.interpolator = OvershootInterpolator(1.5f) - textMarginAnimator.addUpdateListener { animation -> - deviceTextParams.topMargin = animation.animatedValue as Int - deviceText.layoutParams = deviceTextParams - } - - heightAnimator.start() - textMarginAnimator.start() - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun animateDismissWithInertia(velocity: Float) { - springAnimation.cancel() - flingAnimator.cancel() - - val baseDistance = -screenHeight - val velocityFactor = (abs(velocity) / 2000f).coerceIn(0.5f, 2.0f) - val targetDistance = baseDistance * velocityFactor - - val baseDuration = 400L - val velocityDurationFactor = (1500f / (abs(velocity) + 1500f)) - val duration = (baseDuration * velocityDurationFactor).toLong().coerceIn(200L, 500L) - - flingAnimator.setFloatValues(containerView.translationY, targetDistance) - flingAnimator.duration = duration - flingAnimator.addUpdateListener { animation -> - containerView.translationY = animation.animatedValue as Float - - val progress = animation.animatedFraction - containerView.scaleX = 1f - (progress * 0.5f) - containerView.scaleY = 1f - (progress * 0.5f) - - containerView.alpha = 1f - progress - } - flingAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - forceClose() - } - }) - - flingAnimator.interpolator = DecelerateInterpolator(1.2f) - flingAnimator.start() - } - - private fun animateExpandWithStretch(velocity: Float) { - springAnimation.cancel() - flingAnimator.cancel() - - val baseDuration = 600L - val velocityFactor = (1800f / (abs(velocity) + 1800f)).coerceIn(0.5f, 1.5f) - val expandDuration = (baseDuration * velocityFactor).toLong().coerceIn(300L, 700L) - - if (params != null) { - params!!.height = screenHeight - try { - windowManager.updateViewLayout(containerView, params) - } catch (e: Exception) { - e.printStackTrace() - } - } - - val containerAnimator = ValueAnimator.ofFloat(containerView.translationY, screenHeight * 0.6f) - containerAnimator.duration = expandDuration - containerAnimator.interpolator = DecelerateInterpolator(0.8f) - containerAnimator.addUpdateListener { animation -> - containerView.translationY = animation.animatedValue as Float - } - - val stretchAnimator = ValueAnimator.ofFloat(0f, 1f) - stretchAnimator.duration = expandDuration - stretchAnimator.interpolator = OvershootInterpolator(0.5f) - stretchAnimator.addUpdateListener { animation -> - val progress = animation.animatedValue as Float - animateCustomStretch(progress) - } - - val normalizeAnimator = ValueAnimator.ofFloat(1.0f, 0.0f) - normalizeAnimator.duration = 300 - normalizeAnimator.startDelay = expandDuration - 150 - normalizeAnimator.interpolator = AccelerateInterpolator(1.2f) - normalizeAnimator.addUpdateListener { animation -> - val progress = animation.animatedValue as Float - containerView.alpha = progress - - if (progress < 0.7f) { - islandView.findViewById(R.id.island_video_view).visibility = View.GONE - } - } - normalizeAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - ServiceManager.getService()?.startMainActivity() - forceClose() - } - }) - - containerAnimator.start() - stretchAnimator.start() - normalizeAnimator.start() - } - - private fun animateCustomStretch(progress: Float) { - try { - val mainLayout = islandView.findViewById(R.id.island_window_layout) - val connectedText = islandView.findViewById(R.id.island_connected_text) - val deviceText = islandView.findViewById(R.id.island_device_name) - - val targetHeight = (screenHeight * 0.7f).toInt() - val currentHeight = initialHeight + ((targetHeight - initialHeight) * progress) - mainLayout.minimumHeight = currentHeight.toInt() - - val mainLayoutParams = mainLayout.layoutParams - mainLayoutParams.height = LinearLayout.LayoutParams.MATCH_PARENT - mainLayout.layoutParams = mainLayoutParams - - val targetMargin = (400 * progress).toInt() - val deviceTextParams = deviceText.layoutParams as LinearLayout.LayoutParams - deviceTextParams.topMargin = targetMargin - deviceText.layoutParams = deviceTextParams - - val baseTextSize = 24f - deviceText.textSize = baseTextSize + (progress * 8f) - - val baseSubTextSize = 16f - connectedText.textSize = baseSubTextSize + (progress * 4f) - } catch (e: Exception) { - e.printStackTrace() - } - } - - fun close() { - if (Looper.myLooper() != Looper.getMainLooper()) { - Handler(Looper.getMainLooper()).post { close() } - return - } - try { - if (isClosing) return - isClosing = true - - try { - context.unregisterReceiver(batteryReceiver) - } catch (e: Exception) { -// e.printStackTrace() - } - - ServiceManager.getService()?.islandOpen = false - autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return) - - resetStretchEffects() - - val videoView = islandView.findViewById(R.id.island_video_view) - try { - videoView.stopPlayback() - } catch (e: Exception) { - e.printStackTrace() - } - - val scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, containerView.scaleX, 0.5f) - val scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, containerView.scaleY, 0.5f) - val translationY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, containerView.translationY, -200f) - ObjectAnimator.ofPropertyValuesHolder(containerView, scaleX, scaleY, translationY).apply { - duration = 700 - interpolator = AnticipateOvershootInterpolator() - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - cleanupAndRemoveView() - } - }) - start() - } - } catch (e: Exception) { - e.printStackTrace() - // Even if animation fails, ensure we cleanup - cleanupAndRemoveView() - } - } - - private fun cleanupAndRemoveView() { - if (Looper.myLooper() != Looper.getMainLooper()) { - Handler(Looper.getMainLooper()).post { cleanupAndRemoveView() } - return - } - try { - containerView.visibility = View.GONE - } catch (e: Exception) { - e("IslandWindow", "Error setting visibility: $e") - } - try { - if (containerView.parent != null) { - windowManager.removeView(containerView) - } - } catch (e: Exception) { - e("IslandWindow", "Error removing view: $e") - } - isClosing = false - // Make sure all animations are canceled - try { - springAnimation.cancel() - } catch (e: Exception) { - e("IslandWindow", "Error cancelling spring animation $e") - } - try { - flingAnimator.cancel() - } catch (e: Exception) { - e("IslandWindow", "Error cancelling fling animation $e") - } - } - - fun forceClose() { - if (Looper.myLooper() != Looper.getMainLooper()) { - Handler(Looper.getMainLooper()).post { forceClose() } - return - } - try { - if (isClosing) return - isClosing = true - - try { - context.unregisterReceiver(batteryReceiver) - } catch (e: Exception) { - e.printStackTrace() - } - - ServiceManager.getService()?.islandOpen = false - autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return) - - // Cancel all ongoing animations - springAnimation.cancel() - flingAnimator.cancel() - - // Immediately remove the view without animations - cleanupAndRemoveView() - } catch (e: Exception) { - e.printStackTrace() - isClosing = false - } - } -} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/PopupWindow.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/PopupWindow.kt deleted file mode 100644 index 4247ea47a..000000000 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/PopupWindow.kt +++ /dev/null @@ -1,290 +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 . -*/ - - -package me.kavishdevar.librepods.presentation.overlays - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.ObjectAnimator -import android.animation.PropertyValuesHolder -import android.annotation.SuppressLint -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.graphics.PixelFormat -import android.media.AudioManager -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.util.Log -import android.view.Gravity -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View -import android.view.WindowManager -import android.view.animation.AccelerateInterpolator -import android.view.animation.DecelerateInterpolator -import android.widget.ImageButton -import android.widget.LinearLayout -import android.widget.TextView -import android.widget.VideoView -import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.data.AirPodsNotifications -import me.kavishdevar.librepods.data.Battery -import me.kavishdevar.librepods.data.BatteryComponent -import me.kavishdevar.librepods.data.BatteryStatus - -@SuppressLint("InflateParams", "ClickableViewAccessibility") -class PopupWindow( - private val context: Context, - private val onCloseCallback: () -> Unit = {} -) { - private val mView: View - private var isClosing = false - private var autoCloseHandler = Handler(Looper.getMainLooper()) - private var autoCloseRunnable: Runnable? = null - private var batteryUpdateReceiver: BroadcastReceiver? = null - - @Suppress("DEPRECATION") - private val mParams: WindowManager.LayoutParams = WindowManager.LayoutParams().apply { - height = WindowManager.LayoutParams.WRAP_CONTENT - val displayMetrics = context.resources.displayMetrics - val screenWidthDp = displayMetrics.widthPixels / displayMetrics.density - width = if (screenWidthDp >= 600) { - (400 * displayMetrics.density).toInt() - } else { - WindowManager.LayoutParams.MATCH_PARENT - } - type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY - format = PixelFormat.TRANSLUCENT - gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL - dimAmount = 0.3f - flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or - WindowManager.LayoutParams.FLAG_FULLSCREEN or - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or - WindowManager.LayoutParams.FLAG_DIM_BEHIND or - WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH - } - - private val mWindowManager: WindowManager - - init { - val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - mView = layoutInflater.inflate(R.layout.popup_window, null) - mParams.x = 0 - mParams.y = 0 - - mView.setOnClickListener { - close() - } - - mView.findViewById(R.id.close_button).setOnClickListener { - close() - } - - val ll = mView.findViewById(R.id.linear_layout) - ll.setOnClickListener { - close() - } - - @Suppress("DEPRECATION") - mView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_FULLSCREEN or - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - - mView.setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - val touchY = event.rawY - val popupTop = mView.top - if (touchY < popupTop) { - close() - true - } else { - false - } - } else { - false - } - } - mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - } - - @SuppressLint("InlinedApi", "SetTextI18s") - fun open(name: String = "AirPods Pro", batteryNotification: AirPodsNotifications.BatteryNotification) { - try { - if (mView.windowToken == null && mView.parent == null && !isClosing) { - mView.findViewById(R.id.name).text = name - - updateBatteryStatus(batteryNotification) - - val vid = mView.findViewById(R.id.video) - vid.setAudioFocusRequest(AudioManager.AUDIOFOCUS_NONE) - vid.setVideoPath("android.resource://me.kavishdevar.librepods/" + R.raw.connected) - vid.resolveAdjustedSize(vid.width, vid.height) - vid.start() - vid.setOnCompletionListener { - vid.start() - } - - try { - mWindowManager.addView(mView, mParams) - } catch (e: Exception) { - e.printStackTrace() - } - - val displayMetrics = mView.context.resources.displayMetrics - val screenHeight = displayMetrics.heightPixels - - mView.translationY = screenHeight.toFloat() - mView.alpha = 1f - - val translationY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, screenHeight.toFloat(), 0f) - - ObjectAnimator.ofPropertyValuesHolder(mView, translationY).apply { - duration = 500 - interpolator = DecelerateInterpolator() - start() - } - - registerBatteryUpdateReceiver() - - autoCloseRunnable = Runnable { close() } - autoCloseHandler.postDelayed(autoCloseRunnable!!, 12000) - } - } catch (e: Exception) { - Log.e("PopupWindow", "Error opening popup: ${e.message}") - onCloseCallback() - } - } - - @SuppressLint("UnspecifiedRegisterReceiverFlag") - private fun registerBatteryUpdateReceiver() { - batteryUpdateReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == AirPodsNotifications.BATTERY_DATA) { - val batteryList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableArrayListExtra("data", Battery::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableArrayListExtra("data") - } - if (batteryList != null) { - updateBatteryStatusFromList(batteryList) - } - } - } - } - - val filter = IntentFilter(AirPodsNotifications.BATTERY_DATA) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver(batteryUpdateReceiver, filter, Context.RECEIVER_EXPORTED) - } else { - context.registerReceiver(batteryUpdateReceiver, filter) - } - } - - private fun unregisterBatteryUpdateReceiver() { - batteryUpdateReceiver?.let { - try { - context.unregisterReceiver(it) - batteryUpdateReceiver = null - } catch (e: Exception) { - Log.e("PopupWindow", "Error unregistering battery receiver: ${e.message}") - } - } - } - - private fun updateBatteryStatusFromList(batteryList: List) { - val batteryLeftText = mView.findViewById(R.id.left_battery) - val batteryRightText = mView.findViewById(R.id.right_battery) - val batteryCaseText = mView.findViewById(R.id.case_battery) - - batteryLeftText.text = batteryList.find { it.component == BatteryComponent.LEFT }?.let { - if (it.status != BatteryStatus.DISCONNECTED) { - "\uDBC3\uDC8E ${it.level}%" - } else { - "" - } - } ?: "" - - batteryRightText.text = batteryList.find { it.component == BatteryComponent.RIGHT }?.let { - if (it.status != BatteryStatus.DISCONNECTED) { - "\uDBC3\uDC8D ${it.level}%" - } else { - "" - } - } ?: "" - - batteryCaseText.text = batteryList.find { it.component == BatteryComponent.CASE }?.let { - if (it.status != BatteryStatus.DISCONNECTED) { - "\uDBC3\uDE6C ${it.level}%" - } else { - "" - } - } ?: "" - } - - @SuppressLint("SetTextI18s") - fun updateBatteryStatus(batteryNotification: AirPodsNotifications.BatteryNotification) { - val batteryStatus = batteryNotification.getBattery() - updateBatteryStatusFromList(batteryStatus) - } - - fun close() { - try { - if (isClosing) return - isClosing = true - - autoCloseRunnable?.let { autoCloseHandler.removeCallbacks(it) } - unregisterBatteryUpdateReceiver() - - val vid = mView.findViewById(R.id.video) - vid.stopPlayback() - - ObjectAnimator.ofFloat(mView, "translationY", mView.height.toFloat()).apply { - duration = 500 - interpolator = AccelerateInterpolator() - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - try { - mView.visibility = View.GONE - if (mView.parent != null) { - mWindowManager.removeView(mView) - } - } catch (e: Exception) { - Log.e("PopupWindow", "Error removing view: ${e.message}") - } finally { - isClosing = false - onCloseCallback() - } - } - }) - start() - } - } catch (e: Exception) { - Log.e("PopupWindow", "Error closing popup: ${e.message}") - isClosing = false - onCloseCallback() - } - } -} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt index 86d341939..6491b58b6 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt @@ -21,58 +21,32 @@ package me.kavishdevar.librepods.presentation.screens // import me.kavishdevar.librepods.utils.RadareOffsetFinder import android.annotation.SuppressLint import android.util.Log -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress -import androidx.compose.foundation.gestures.detectTapGestures -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.PaddingValues import androidx.compose.foundation.layout.Spacer 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.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeMute +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.HearingDisabled +import androidx.compose.material.icons.filled.NotificationsActive +import androidx.compose.material.icons.filled.SwipeVertical +import androidx.compose.material.icons.filled.Timer +import androidx.compose.material.icons.filled.TouchApp +import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf 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.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInParent -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalHapticFeedback 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.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.hazeSource -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -82,12 +56,13 @@ import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.AACPManager import me.kavishdevar.librepods.bluetooth.ATTHandles import me.kavishdevar.librepods.data.Capability -import me.kavishdevar.librepods.presentation.components.NavigationButton -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledDropdown -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.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSegmentedGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveSlider +import me.kavishdevar.librepods.presentation.m3.ExpressiveSwitchRow import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import kotlin.io.encoding.ExperimentalEncodingApi @@ -96,15 +71,11 @@ private var toneVolumeDebounceJob: Job? = null //private const val TAG = "AccessibilitySettings" @SuppressLint("DefaultLocale") -@ExperimentalHazeMaterialsApi @OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class) @Composable fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavController) { val state by viewModel.uiState.collectAsState() - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val hearingAidEnabled = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull( 1 @@ -113,270 +84,245 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC 0 )?.toInt() == 1 - val backdrop = rememberLayerBackdrop() - - StyledScaffold( - title = stringResource(R.string.accessibility) - ) { topPadding, hazeState, bottomPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .hazeSource(hazeState) - .layerBackdrop(backdrop) - .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Spacer(modifier = Modifier.height(topPadding)) + // Press-speed / press-and-hold options share a Byte -> label mapping. + val pressSpeedOptions = listOf( + 0.toByte() to stringResource(R.string.default_option), + 1.toByte() to stringResource(R.string.slower), + 2.toByte() to stringResource(R.string.slowest), + ) + val pressAndHoldDurationOptions = listOf( + 0.toByte() to stringResource(R.string.default_option), + 1.toByte() to stringResource(R.string.slower), + 2.toByte() to stringResource(R.string.slowest), + ) + val volumeSwipeSpeedOptions = listOf( + 1.toByte() to stringResource(R.string.default_option), + 2.toByte() to stringResource(R.string.longer), + 3.toByte() to stringResource(R.string.longest), + ) -// val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) } -// val phoneEQEnabled = remember { mutableStateOf(false) } -// val mediaEQEnabled = remember { mutableStateOf(false) } + val selectedPressSpeed = + state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL]?.getOrNull(0) + ?: 0.toByte() + val selectedPressAndHoldDuration = + state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL]?.getOrNull(0) + ?: 0.toByte() + val selectedVolumeSwipeSpeed = + state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL]?.getOrNull(0) + ?: 1.toByte() - val pressSpeedOptions = mapOf( - 0.toByte() to stringResource(R.string.default_option), - 1.toByte() to stringResource(R.string.slower), - 2.toByte() to stringResource(R.string.slowest) - ) + val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) } + val phoneEQEnabled = remember { mutableStateOf(false) } + val mediaEQEnabled = remember { mutableStateOf(false) } - val selectedPressSpeedValue = - state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL]?.getOrNull( - 0 + LaunchedEffect(phoneMediaEQ.value, phoneEQEnabled.value, mediaEQEnabled.value) { + phoneMediaDebounceJob?.cancel() + phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch { + delay(150) + try { + val phoneByte = if (phoneEQEnabled.value) 0x01.toByte() else 0x02.toByte() + val mediaByte = if (mediaEQEnabled.value) 0x01.toByte() else 0x02.toByte() + Log.d( + "AccessibilitySettingsScreen", + "Sending phone/media EQ (phoneEnabled=${phoneEQEnabled.value}, mediaEnabled=${mediaEQEnabled.value})" ) - var selectedPressSpeed by remember { - mutableStateOf( - pressSpeedOptions[selectedPressSpeedValue] ?: pressSpeedOptions[0] + viewModel.sendPhoneMediaEQ(phoneMediaEQ.value, phoneByte, mediaByte) + } catch (e: Exception) { + Log.w( + "AccessibilitySettingsScreen", + "Error sending phone/media EQ: ${e.message}" ) } + } + } - val pressAndHoldDurationOptions = mapOf( - 0.toByte() to stringResource(R.string.default_option), - 1.toByte() to stringResource(R.string.slower), - 2.toByte() to stringResource(R.string.slowest) - ) + val toneVolumeValue = + state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME]?.getOrNull(0) + ?.toFloat() ?: 75f - val selectedPressAndHoldDurationValue = - state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL]?.getOrNull( - 0 - ) - var selectedPressAndHoldDuration by remember { - mutableStateOf( - pressAndHoldDurationOptions[selectedPressAndHoldDurationValue] - ?: pressAndHoldDurationOptions[0] - ) - } + val oneBudAncChecked = + state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE]?.getOrNull(0) == + 0x01.toByte() - val volumeSwipeSpeedOptions = mapOf( - 1.toByte() to stringResource(R.string.default_option), - 2.toByte() to stringResource(R.string.longer), - 3.toByte() to stringResource(R.string.longest) - ) - val selectedVolumeSwipeSpeedValue = - state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL]?.getOrNull( - 0 - ) - var selectedVolumeSwipeSpeed by remember { - mutableStateOf( - volumeSwipeSpeedOptions[selectedVolumeSwipeSpeedValue] - ?: volumeSwipeSpeedOptions[1] - ) + val showLoudSoundReduction = + state.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) && state.vendorIdHook + val showTransparencyCustomization = !hearingAidEnabled && state.vendorIdHook + val showVolumeSwipe = state.capabilities.contains(Capability.SWIPE_FOR_VOLUME) + val volumeSwipeEnabled = + state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE]?.getOrNull(0) + ?.toInt() == 0x01 + + ExpressiveScreenScaffold( + title = stringResource(R.string.accessibility), + onBack = { navController.popBackStack() }, + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), + ) { + // --- Press behaviour --- + item(key = "press") { + ExpressiveSectionHeader(stringResource(R.string.press_speed)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.press_speed), + subtitle = stringResource(R.string.press_speed_description), + leadingIcon = Icons.Filled.TouchApp, + enabled = state.isPremium, + ) + ExpressiveSegmentedGroup( + options = pressSpeedOptions.map { it.first }, + selected = selectedPressSpeed, + onSelect = { value -> + viewModel.setControlCommandByte( + identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL, + value = value, + ) + }, + label = { byte -> pressSpeedOptions.first { it.first == byte }.second }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + ) + } } - val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) } - val phoneEQEnabled = remember { mutableStateOf(false) } - val mediaEQEnabled = remember { mutableStateOf(false) } + item(key = "press_hold") { + ExpressiveSectionHeader(stringResource(R.string.press_and_hold_duration)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.press_and_hold_duration), + subtitle = stringResource(R.string.press_and_hold_duration_description), + leadingIcon = Icons.Filled.Timer, + enabled = state.isPremium, + ) + ExpressiveSegmentedGroup( + options = pressAndHoldDurationOptions.map { it.first }, + selected = selectedPressAndHoldDuration, + onSelect = { value -> + viewModel.setControlCommandByte( + identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL, + value = value, + ) + }, + label = { byte -> pressAndHoldDurationOptions.first { it.first == byte }.second }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + ) + } + } - LaunchedEffect(phoneMediaEQ.value, phoneEQEnabled.value, mediaEQEnabled.value) { - phoneMediaDebounceJob?.cancel() - phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch { - delay(150) - try { - val phoneByte = if (phoneEQEnabled.value) 0x01.toByte() else 0x02.toByte() - val mediaByte = if (mediaEQEnabled.value) 0x01.toByte() else 0x02.toByte() - Log.d( - "AccessibilitySettingsScreen", - "Sending phone/media EQ (phoneEnabled=${phoneEQEnabled.value}, mediaEnabled=${mediaEQEnabled.value})" + // --- Noise control & transparency --- + item(key = "noise") { + ExpressiveSectionHeader(stringResource(R.string.noise_control)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.noise_cancellation_single_airpod), + subtitle = stringResource(R.string.noise_cancellation_single_airpod_description), + leadingIcon = Icons.Filled.HearingDisabled, + checked = oneBudAncChecked, + onCheckedChange = { + viewModel.setControlCommandBoolean( + AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, it + ) + }, + enabled = state.isPremium, + ) + if (showLoudSoundReduction) { + me.kavishdevar.librepods.presentation.m3.ExpressiveDivider() + ExpressiveSwitchRow( + title = stringResource(R.string.loud_sound_reduction), + subtitle = stringResource(R.string.loud_sound_reduction_description), + leadingIcon = Icons.Filled.HearingDisabled, + checked = state.loudSoundReductionEnabled, + onCheckedChange = { + viewModel.setATTCharacteristicValue( + ATTHandles.LOUD_SOUND_REDUCTION, + if (it) byteArrayOf(0x01) else byteArrayOf(0x00) + ) + }, + enabled = state.isPremium, ) - viewModel.sendPhoneMediaEQ(phoneMediaEQ.value, phoneByte, mediaByte) - } catch (e: Exception) { - Log.w( - "AccessibilitySettingsScreen", - "Error sending phone/media EQ: ${e.message}" + } + if (showTransparencyCustomization) { + me.kavishdevar.librepods.presentation.m3.ExpressiveDivider() + ExpressiveListItem( + title = stringResource(R.string.customize_transparency_mode), + leadingIcon = Icons.Filled.Tune, + showChevron = true, + enabled = state.isPremium, + onClick = { navController.navigate("transparency_customization") }, ) } } } - Box( - modifier = Modifier.then( - if (!state.isPremium) { - Modifier.pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent(PointerEventPass.Initial) - event.changes.forEach { it.consume() } - } - } - } - } else Modifier)) { - DropdownMenuComponent( - label = stringResource(R.string.press_speed), - description = stringResource(R.string.press_speed_description), - options = pressSpeedOptions.values.toList(), - selectedOption = selectedPressSpeed ?: stringResource(R.string.default_option), - onOptionSelected = { newValue -> - selectedPressSpeed = newValue - viewModel.setControlCommandByte( - identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL, - value = pressSpeedOptions.filterValues { it == newValue }.keys.firstOrNull() - ?: 0.toByte() - ) - }, - textColor = textColor, - hazeState = hazeState, - independent = true - ) - } - Box( - modifier = Modifier.then( - if (!state.isPremium) { - Modifier.pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent(PointerEventPass.Initial) - event.changes.forEach { it.consume() } - } - } + // --- Tone volume --- + item(key = "tone") { + ExpressiveSectionHeader(stringResource(R.string.tone_volume)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.tone_volume), + subtitle = stringResource(R.string.tone_volume_description), + leadingIcon = Icons.Filled.NotificationsActive, + enabled = state.isPremium, + ) + ExpressiveSlider( + value = toneVolumeValue, + onValueChange = { + viewModel.setControlCommandValue( + AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME, + byteArrayOf(it.toInt().toByte(), 0x50) + ) + }, + valueRange = 0f..100f, + startIcon = Icons.AutoMirrored.Filled.VolumeMute, + endIcon = Icons.AutoMirrored.Filled.VolumeUp, + enabled = state.isPremium, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) } - } else Modifier)) { - DropdownMenuComponent( - label = stringResource(R.string.press_and_hold_duration), - description = stringResource(R.string.press_and_hold_duration_description), - options = pressAndHoldDurationOptions.values.toList(), - selectedOption = selectedPressAndHoldDuration - ?: stringResource(R.string.default_option), - onOptionSelected = { newValue -> - selectedPressAndHoldDuration = newValue - viewModel.setControlCommandByte( - identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL, - value = pressAndHoldDurationOptions.filterValues { it == newValue }.keys.firstOrNull() - ?: 0.toByte() - ) - }, - textColor = textColor, - hazeState = hazeState, - independent = true - ) } - StyledToggle( - title = stringResource(R.string.noise_control), - label = stringResource(R.string.noise_cancellation_single_airpod), - description = stringResource(R.string.noise_cancellation_single_airpod_description), - independent = true, - checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE]?.getOrNull( - 0 - ) == 0x01.toByte(), - onCheckedChange = { - viewModel.setControlCommandBoolean( - AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, it - ) - }, - enabled = state.isPremium - ) - if (state.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) && state.vendorIdHook) { - StyledToggle( - label = stringResource(R.string.loud_sound_reduction), - description = stringResource(R.string.loud_sound_reduction_description), - checked = state.loudSoundReductionEnabled, - onCheckedChange = { - viewModel.setATTCharacteristicValue( - ATTHandles.LOUD_SOUND_REDUCTION, - if (it) byteArrayOf(0x01) else byteArrayOf(0x00) + // --- Volume swipe --- + if (showVolumeSwipe) { + item(key = "volume_swipe") { + ExpressiveSectionHeader(stringResource(R.string.volume_control)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.volume_control), + subtitle = stringResource(R.string.volume_control_description), + leadingIcon = Icons.Filled.SwipeVertical, + checked = volumeSwipeEnabled, + onCheckedChange = { + viewModel.setControlCommandBoolean( + AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, it + ) + }, + enabled = state.isPremium, ) - }, - enabled = state.isPremium - ) - } - - if (!hearingAidEnabled && state.vendorIdHook) { - NavigationButton( - to = "transparency_customization", - name = stringResource(R.string.customize_transparency_mode), - navController = navController, - enabled = state.isPremium - ) - } - - val toneVolumeValue = - state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME]?.getOrNull( - 0 - )?.toFloat() ?: 75f - StyledSlider( - label = stringResource(R.string.tone_volume), - description = stringResource(R.string.tone_volume_description), - value = toneVolumeValue, - onValueChange = { - viewModel.setControlCommandValue( - AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME, - byteArrayOf(it.toInt().toByte(), 0x50) - ) - }, - valueRange = 0f..100f, - snapPoints = listOf(75f), - startIcon = "\uDBC0\uDEA1", - endIcon = "\uDBC0\uDEA9", - independent = true, - enabled = state.isPremium - ) - - if (state.capabilities.contains(Capability.SWIPE_FOR_VOLUME)) { - val volumeSwipeEnabled = - state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE]?.getOrNull( - 0 - )?.toInt() == 0x01 - StyledToggle( - label = stringResource(R.string.volume_control), - description = stringResource(R.string.volume_control_description), - checked = volumeSwipeEnabled, - onCheckedChange = { - viewModel.setControlCommandBoolean( - AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, it + me.kavishdevar.librepods.presentation.m3.ExpressiveDivider() + ExpressiveListItem( + title = stringResource(R.string.volume_swipe_speed), + subtitle = stringResource(R.string.volume_swipe_speed_description), + leadingIcon = Icons.Filled.Timer, + enabled = state.isPremium, + ) + ExpressiveSegmentedGroup( + options = volumeSwipeSpeedOptions.map { it.first }, + selected = selectedVolumeSwipeSpeed, + onSelect = { value -> + viewModel.setControlCommandByte( + identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL, + value = value, + ) + }, + label = { byte -> volumeSwipeSpeedOptions.first { it.first == byte }.second }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), ) - }, - enabled = state.isPremium - ) - - Box( - modifier = Modifier.then( - if (!state.isPremium) { - Modifier.pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent(PointerEventPass.Initial) - event.changes.forEach { it.consume() } - } - } } - } else Modifier)) { - DropdownMenuComponent( - label = stringResource(R.string.volume_swipe_speed), - description = stringResource(R.string.volume_swipe_speed_description), - options = volumeSwipeSpeedOptions.values.toList(), - selectedOption = selectedVolumeSwipeSpeed - ?: stringResource(R.string.default_option), - onOptionSelected = { newValue -> - selectedVolumeSwipeSpeed = newValue - viewModel.setControlCommandByte( - identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL, - value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull() - ?: 1.toByte() - ) - }, - textColor = textColor, - hazeState = hazeState, - independent = true - ) } } @@ -613,210 +559,6 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC // } // } // } - Spacer(modifier = Modifier.height(bottomPadding)) - } - } -} - -@ExperimentalHazeMaterialsApi -@Composable -private fun DropdownMenuComponent( - label: String, - options: List, - selectedOption: String, - onOptionSelected: (String) -> Unit, - textColor: Color, - hazeState: HazeState, - description: String? = null, - independent: Boolean = true -) { - val density = LocalDensity.current - val itemHeightPx = with(density) { 48.dp.toPx() } - - var expanded by remember { mutableStateOf(false) } - var touchOffset by remember { mutableStateOf(null) } - var boxPosition by remember { mutableStateOf(Offset.Zero) } - var lastDismissTime by remember { mutableLongStateOf(0L) } - var parentHoveredIndex by remember { mutableStateOf(null) } - var parentDragActive by remember { mutableStateOf(false) } - var previousIdx by remember { mutableStateOf(null) } - val haptics = LocalHapticFeedback.current - val scope = rememberCoroutineScope() - - Column(modifier = Modifier.fillMaxWidth()) { - Column( - modifier = Modifier - .fillMaxWidth() - .then( - if (independent) { - if (description != null) { - Modifier.padding(top = 8.dp, bottom = 4.dp) - } else { - Modifier.padding(vertical = 8.dp) - } - } else Modifier - ) - .background( - if (independent) (if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color( - 0xFFFFFFFF - )) else Color.Transparent, - if (independent) RoundedCornerShape(28.dp) else RoundedCornerShape(0.dp) - ) then (if (independent) Modifier.padding(horizontal = 4.dp) else Modifier).clip( - if (independent) RoundedCornerShape(28.dp) else RoundedCornerShape(0.dp) - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 12.dp, end = 12.dp) - .height(58.dp) - .pointerInput(Unit) { - detectTapGestures { offset -> - val now = System.currentTimeMillis() - if (expanded) { - expanded = false - lastDismissTime = now - } else { - if (now - lastDismissTime > 250L) { - touchOffset = offset - expanded = true - } - } - } - } - .pointerInput(Unit) { - detectDragGesturesAfterLongPress(onDragStart = { offset -> - val now = System.currentTimeMillis() - touchOffset = offset - if (!expanded && now - lastDismissTime > 250L) { - expanded = true - } - lastDismissTime = now - parentDragActive = true - parentHoveredIndex = 0 - }, onDrag = { change, _ -> - val current = change.position - val touch = touchOffset ?: current - val posInPopupY = current.y - touch.y - val idx = (posInPopupY / itemHeightPx).toInt() - if (idx != previousIdx) { - scope.launch { - haptics.performHapticFeedback( - HapticFeedbackType.SegmentTick - ) - } - } - parentHoveredIndex = idx - previousIdx = idx - }, onDragEnd = { - parentDragActive = false - parentHoveredIndex?.let { idx -> - if (idx in options.indices) { - onOptionSelected(options[idx]) - expanded = false - lastDismissTime = System.currentTimeMillis() - } - } - if (parentHoveredIndex != null && parentHoveredIndex in options.indices) { - scope.launch { - haptics.performHapticFeedback( - HapticFeedbackType.GestureEnd - ) - } - } - parentHoveredIndex = null - }, onDragCancel = { - parentDragActive = false - parentHoveredIndex = null - }) - }, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = label, - fontSize = 16.sp, - color = textColor, - modifier = Modifier.padding(bottom = 4.dp) - ) - if (!independent && description != null) { - Text( - text = description, style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Light, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), modifier = Modifier.padding(16.dp, top = 0.dp, bottom = 2.dp) - ) - } - } - Box( - modifier = Modifier.onGloballyPositioned { coordinates -> - boxPosition = coordinates.positionInParent() - }) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = selectedOption, style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(alpha = 0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - Text( - text = "􀆏", style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), modifier = Modifier.padding(start = 6.dp) - ) - } - - StyledDropdown( - expanded = expanded, - onDismissRequest = { - expanded = false - lastDismissTime = System.currentTimeMillis() - }, - options = options, - selectedOption = selectedOption, - touchOffset = touchOffset, - boxPosition = boxPosition, - externalHoveredIndex = parentHoveredIndex, - externalDragActive = parentDragActive, - onOptionSelected = { option -> - onOptionSelected(option) - expanded = false - }, - hazeState = hazeState - ) - } - } - } - if (independent && description != null) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .background( - if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7) - ) - ) { - Text( - text = description, style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Light, - color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy( - alpha = 0.6f - ), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt index 53a7370a1..a04662668 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt @@ -18,14 +18,20 @@ package me.kavishdevar.librepods.presentation.screens -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer 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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.VolumeMute +import androidx.compose.material.icons.filled.VolumeUp +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -36,70 +42,98 @@ 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.Color 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.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.AACPManager -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.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSlider import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel @Composable fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel, navController: NavController) { val state by viewModel.uiState.collectAsState() - val backdrop = rememberLayerBackdrop() - StyledScaffold( - title = stringResource(R.string.customize_adaptive_audio) - ) { spacerHeight -> + val sliderValue = remember { + mutableFloatStateOf( + 100f - (state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH] + ?.getOrNull(0)?.toFloat() ?: 50f) + ) + } + var job by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + + ExpressiveScreenScaffold( + title = stringResource(R.string.customize_adaptive_audio), + onBack = { navController.popBackStack() }, + ) { padding -> Column( modifier = Modifier .fillMaxSize() - .layerBackdrop(backdrop) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + .verticalScroll(rememberScrollState()) + .padding( + top = padding.calculateTopPadding(), + start = 16.dp, + end = 16.dp, + ), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Spacer(modifier = Modifier.height(spacerHeight)) - val sliderValue = remember { - mutableFloatStateOf(100f - (state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH]?.getOrNull(0)?.toFloat() ?: 50f)) - } - var job by remember { mutableStateOf(null) } - val scope = rememberCoroutineScope() - StyledSlider( - label = stringResource(R.string.customize_adaptive_audio), - value = sliderValue.floatValue, - onValueChange = { - sliderValue.floatValue = it - job?.cancel() - job = scope.launch { - delay(150) - viewModel.setControlCommandValue( - AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH, - byteArrayOf((100 - it).toInt().toByte()) + ExpressiveSectionHeader(stringResource(R.string.customize_adaptive_audio)) + ExpressiveGroup { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + ExpressiveSlider( + value = sliderValue.floatValue, + onValueChange = { + sliderValue.floatValue = it + job?.cancel() + job = scope.launch { + delay(150) + viewModel.setControlCommandValue( + AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH, + byteArrayOf((100 - it).toInt().toByte()) + ) + } + }, + valueRange = 0f..100f, + startIcon = Icons.Filled.VolumeMute, + endIcon = Icons.Filled.VolumeUp, + enabled = state.isPremium, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "More noise", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = "Less noise", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - }, - valueRange = 0f..100f, - snapPoints = listOf(0f, 50f, 100f), - startIcon = "􀊥", - endIcon = "􀊩", - independent = true, - description = stringResource(R.string.adaptive_audio_description), - enabled = state.isPremium + } + } + Text( + text = stringResource(R.string.adaptive_audio_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 8.dp), ) + Spacer(Modifier.height(120.dp)) } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt deleted file mode 100644 index 0c124c7f5..000000000 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt +++ /dev/null @@ -1,554 +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 . -*/ - -@file:OptIn(ExperimentalEncodingApi::class) - -package me.kavishdevar.librepods.presentation.screens - -// import me.kavishdevar.librepods.utils.RadareOffsetFinder -import android.annotation.SuppressLint -import android.content.Context.MODE_PRIVATE -import android.content.Intent -import android.content.SharedPreferences -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures -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.Spacer -import androidx.compose.foundation.layout.fillMaxSize -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.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext -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.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.net.toUri -import androidx.navigation.NavController -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.highlight.Highlight -import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.hazeSource -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import kotlinx.coroutines.delay -import me.kavishdevar.librepods.BuildConfig -import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.bluetooth.AACPManager -import me.kavishdevar.librepods.bluetooth.ATTHandles -import me.kavishdevar.librepods.data.AirPodsPro3 -import me.kavishdevar.librepods.data.Capability -import me.kavishdevar.librepods.presentation.components.AboutCard -import me.kavishdevar.librepods.presentation.components.AudioSettings -import me.kavishdevar.librepods.presentation.components.BatteryView -import me.kavishdevar.librepods.presentation.components.CallControlSettings -import me.kavishdevar.librepods.presentation.components.ConnectionSettings -import me.kavishdevar.librepods.presentation.components.HearingHealthSettings -import me.kavishdevar.librepods.presentation.components.MicrophoneSettings -import me.kavishdevar.librepods.presentation.components.NavigationButton -import me.kavishdevar.librepods.presentation.components.NoiseControlSettings -import me.kavishdevar.librepods.presentation.components.PressAndHoldSettings -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledIconButton -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledToggle -import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel -import java.util.concurrent.TimeUnit -import kotlin.io.encoding.ExperimentalEncodingApi - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) -@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag") -@Composable -fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavController) { - val state by viewModel.uiState.collectAsState() - val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE) - var deviceName by remember { - mutableStateOf( - TextFieldValue( - sharedPreferences.getString("name", state.deviceName).toString() - ) - ) - } - - val nameChangeListener = remember { - SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - if (key == "name") { - deviceName = - TextFieldValue(sharedPreferences.getString("name", "AirPods Pro").toString()) - } - } - } - - DisposableEffect(Unit) { - sharedPreferences.registerOnSharedPreferenceChangeListener(nameChangeListener) - onDispose { - sharedPreferences.unregisterOnSharedPreferenceChangeListener(nameChangeListener) - } - } - - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(Unit) { - viewModel.refreshInitialData() - } - - val hazeStateS = remember { mutableStateOf(HazeState()) } - - StyledScaffold( - title = deviceName.text, actionButtons = listOf( - { scaffoldBackdrop -> - StyledIconButton( - onClick = { navController.navigate("app_settings") }, - icon = "􀍟", - backdrop = scaffoldBackdrop - ) - }), snackbarHostState = snackbarHostState - ) { topPadding, hazeState, bottomPadding -> - hazeStateS.value = hazeState - var blockTouches by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - viewModel.demoActivated.collect { - blockTouches = true - delay(1000) - blockTouches = false - } - } - - if (state.isLocallyConnected) { - val capabilities = state.capabilities - LazyColumn( - modifier = Modifier - .fillMaxSize() - .hazeSource(hazeState) - .padding(horizontal = 16.dp) - .then(if (blockTouches) Modifier.pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent(PointerEventPass.Initial) - event.changes.forEach { it.consume() } - } - } - } else Modifier)) { - item(key = "spacer_top") { Spacer(modifier = Modifier.height(topPadding)) } - - item(key = "battery") { - BatteryView( - batteryList = state.battery, - budsRes = state.instance?.model?.budsRes ?: R.drawable.airpods_pro_2_case, - caseRes = state.instance?.model?.caseRes ?: R.drawable.airpods_pro_2_case - ) - } - item(key = "spacer_battery") { Spacer(modifier = Modifier.height(32.dp)) } - - item(key = "name") { - NavigationButton( - to = "rename", - name = stringResource(R.string.name), - currentState = deviceName.text, - navController = navController, - independent = true - ) - } - - val hasHearingAidCapability = - state.instance?.model?.capabilities?.contains(Capability.HEARING_AID) == true - val hasPPECapability = - state.instance?.model?.capabilities?.contains(Capability.PPE) == true - - if (hasHearingAidCapability || hasPPECapability) { - if (hasPPECapability || (state.vendorIdHook && hasHearingAidCapability)) item( - key = "spacer_hearing_health" - ) { Spacer(modifier = Modifier.height(24.dp)) } - item(key = "hearing_health") { - HearingHealthSettings( - navController = navController, - hasPPECapability = hasPPECapability, - hasHearingAidCapability = hasHearingAidCapability, - vendorIdHook = state.vendorIdHook - ) - } - } - - if (capabilities.contains(Capability.LISTENING_MODE)) { - item(key = "spacer_noise") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "noise_control") { - NoiseControlSettings( - showOffListeningMode = state.offListeningMode, - noiseControlModeValue = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE]?.getOrNull( - 0 - )?.toInt() ?: 3, - onNoiseControlModeChanged = { - viewModel.setControlCommandInt( - AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE, - it - ) - }, - ) - } - } - - if (capabilities.contains(Capability.STEM_CONFIG)) { - item(key = "spacer_press_hold") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "press_hold") { - PressAndHoldSettings( - navController = navController, - leftAction = state.leftAction, - rightAction = state.rightAction - ) - } - } - - item(key = "spacer_call") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "call_control") { - val bytes = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG]?.take(2)?.toByteArray() ?: byteArrayOf(0x00, 0x00) - val flipped = try { bytes[1] == 0x02.toByte() } catch (e: Exception) { false } - CallControlSettings( - hazeState = hazeState, - flipped = flipped, - onCallControlValueChanged = { - viewModel.setControlCommandValue( - AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG, - if (it) byteArrayOf(0x00, 0x02) else byteArrayOf(0x00, 0x03) - ) - }) - } - -// if (capabilities.contains(Capability.STEM_CONFIG) && !BuildConfig.PLAY_BUILD) { -// item(key = "spacer_camera") { Spacer(modifier = Modifier.height(16.dp)) } -// item(key = "camera_control") { -// NavigationButton( -// to = "camera_control", -// name = stringResource(R.string.camera_remote), -// description = stringResource(R.string.camera_control_description), -// title = stringResource(R.string.camera_control), -// navController = navController -// ) -// } -// } - - item(key = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "audio") { - val model = state.instance?.model ?: AirPodsPro3() - val adaptiveVolumeCapability = - model.capabilities.contains(Capability.ADAPTIVE_VOLUME) - val conversationalAwarenessCapability = - model.capabilities.contains(Capability.CONVERSATION_AWARENESS) - val loudSoundReductionCapability = - model.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) - val adaptiveAudioCapability = - model.capabilities.contains(Capability.ADAPTIVE_VOLUME) - - val adaptiveVolumeChecked = - state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG]?.getOrNull( - 0 - ) == 0x01.toByte() - val conversationalAwarenessChecked = - state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG]?.getOrNull( - 0 - ) == 0x01.toByte() - - AudioSettings( - navController = navController, - adaptiveVolumeCapability = adaptiveVolumeCapability, - conversationalAwarenessCapability = conversationalAwarenessCapability, - loudSoundReductionCapability = loudSoundReductionCapability, - adaptiveAudioCapability = adaptiveAudioCapability, - adaptiveVolumeChecked = adaptiveVolumeChecked, - onAdaptiveVolumeCheckedChange = { checked -> - viewModel.setControlCommandBoolean( - AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG, - checked - ) - }, - conversationalAwarenessChecked = conversationalAwarenessChecked && state.isPremium, - onConversationalAwarenessCheckedChange = { checked -> - viewModel.setControlCommandBoolean( - AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG, - checked - ) - }, - loudSoundReductionChecked = state.loudSoundReductionEnabled, - onLoudSoundReductionCheckedChange = { - viewModel.setATTCharacteristicValue( - ATTHandles.LOUD_SOUND_REDUCTION, - byteArrayOf(if (it) 0x01.toByte() else 0x00.toByte()) - ) - }, - vendorIdHook = state.vendorIdHook, - isPremium = state.isPremium - ) - } - - item(key = "spacer_connection") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "connection") { - ConnectionSettings( - automaticEarDetectionEnabled = state.automaticEarDetectionEnabled, - onAutomaticEarDetectionChanged = { - viewModel.setAutomaticEarDetectionEnabled(it) - }, - automaticConnectionEnabled = state.automaticConnectionEnabled, - onAutomaticConnectionChanged = { viewModel.setAutomaticConnectionEnabled(it) }) - } - - item(key = "spacer_microphone") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "microphone") { - val id = AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE - MicrophoneSettings( - hazeState = hazeState, - micModeValue = state.controlStates[id]?.getOrNull(0) ?: 0x00.toByte(), - onMicModeValueChanged = { viewModel.setControlCommandByte(id, it) }) - } - - if (capabilities.contains(Capability.SLEEP_DETECTION)) { - item(key = "spacer_sleep") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "sleep_detection") { - val id = - AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG - StyledToggle( - label = stringResource(R.string.sleep_detection), - checked = state.controlStates[id]?.getOrNull(0) == 0x01.toByte(), - onCheckedChange = { - viewModel.setControlCommandBoolean(id, it) - }, - enabled = state.isPremium - ) - } - } - - if (capabilities.contains(Capability.HEAD_GESTURES)) { - item(key = "spacer_head_tracking") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "head_tracking") { - NavigationButton( - to = "head_tracking", - name = stringResource(R.string.head_gestures), - navController = navController, - currentState = if (sharedPreferences.getBoolean( - "head_gestures", false - ) - ) stringResource(R.string.on) else stringResource(R.string.off) - ) - } - } - - item(key = "spacer_dynamic_end_of_charge") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "dynamic_end_of_charge") { - StyledToggle( - label = stringResource(R.string.optimized_charging), - description = stringResource(R.string.optimized_charging_description), - checked = state.dynamicEndOfCharge, - onCheckedChange = viewModel::setDynamicEndOfCharge - ) - } - - item(key = "spacer_accessibility") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "accessibility") { - NavigationButton( - to = "accessibility", - name = stringResource(R.string.accessibility), - navController = navController - ) - } - - if (capabilities.contains(Capability.LOUD_SOUND_REDUCTION)) { - item(key = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) } - item(key = "off_listening") { - val id = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION - StyledToggle( - label = stringResource(R.string.off_listening_mode), - description = stringResource(R.string.off_listening_mode_description), - checked = state.controlStates[id]?.getOrNull(0) == 0x01.toByte(), - onCheckedChange = viewModel::setOffListeningMode - ) - } - } - - item(key = "spacer_about") { Spacer(modifier = Modifier.height(32.dp)) } - item(key = "about") { - AboutCard( - navController = navController, - modelName = state.modelName, - actualModel = state.actualModel, - serialNumbers = state.serialNumbers, - version = state.version3, - ) - } - - item(key = "spacer_disconnect") { Spacer(modifier = Modifier.height(28.dp)) } - item(key = "disconnect_button") { - StyledButton( - onClick = viewModel::disconnect, - backdrop = rememberLayerBackdrop(), - isInteractive = false, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 56.dp) - ) { - Text( - text = stringResource(R.string.disconnect), - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = if (isSystemInDarkTheme()) Color(0xFF0091FF) else Color(0xFF0088FF), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - textAlign = TextAlign.Start, - modifier = Modifier.fillMaxWidth() - ) - } - } - -// item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) } -// item(key = "debug") { NavigationButton("debug", "Debug", navController) } - item(key = "spacer_bottom") { Spacer(Modifier.height(bottomPadding)) } - } - } else { - val backdrop = rememberLayerBackdrop() - Column( - modifier = Modifier - .fillMaxSize() - .drawBackdrop( - backdrop = rememberLayerBackdrop(), - exportedBackdrop = backdrop, - shape = { RoundedCornerShape(0.dp) }, - highlight = { - Highlight.Ambient.copy(alpha = 0f) - }, - effects = {}) - .hazeSource(hazeState) - .padding(horizontal = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - val tapCount = remember { mutableIntStateOf(0) } - val lastTapTime = remember { mutableLongStateOf(0L) } - Column( - modifier = Modifier - .fillMaxWidth() - .pointerInput(Unit) { - detectTapGestures( - onTap = { - val now = System.currentTimeMillis() - - if (now - lastTapTime.longValue > 400) { - tapCount.intValue = 0 - } - - tapCount.intValue++ - lastTapTime.longValue = now - - if (tapCount.intValue >= 5) { - tapCount.intValue = 0 - viewModel.activateDemoMode() - } - }) - }) { - Text( - text = stringResource(R.string.airpods_not_connected), style = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Medium, - color = if (isSystemInDarkTheme()) Color.White else Color.Black, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() - ) - Spacer(Modifier.height(24.dp)) - Text( - text = stringResource(R.string.airpods_not_connected_description), - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Light, - color = if (isSystemInDarkTheme()) Color.White else Color.Black, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) - } - - Spacer(Modifier.height(32.dp)) - if (!BuildConfig.PLAY_BUILD) { - StyledButton( - onClick = { navController.navigate("troubleshooting") }, - backdrop = backdrop, - modifier = Modifier - .fillMaxWidth(0.9f) - ) { - Text( - text = stringResource(R.string.troubleshooting), - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = if (isSystemInDarkTheme()) Color.White else Color.Black - ) - ) - } - Spacer(Modifier.height(16.dp)) - } - if (state.connectionSuccessful) { - StyledButton( - onClick = { - viewModel.reconnectFromSavedMac() - }, backdrop = backdrop, modifier = Modifier.fillMaxWidth(0.9f) - ) { - Text( - text = stringResource(R.string.reconnect_to_last_device), - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = if (isSystemInDarkTheme()) Color.White else Color.Black - ) - ) - } - } - } - } - } -} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt index 80fb78232..47475f2c5 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt @@ -16,6 +16,8 @@ along with this program. If not, see . */ +@file:OptIn(ExperimentalMaterial3Api::class) + package me.kavishdevar.librepods.presentation.screens import android.content.Intent @@ -24,75 +26,72 @@ import android.net.Uri import android.os.Build import android.widget.Toast import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -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.PaddingValues import androidx.compose.foundation.layout.Spacer 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.rememberScrollState +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.clearText -import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Chat +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Description +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Hearing +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.RecordVoiceOver +import androidx.compose.material.icons.filled.SwapHoriz +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.filled.VolumeUp +import androidx.compose.material.icons.filled.Widgets import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext 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.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.lerp -import androidx.compose.ui.unit.sp import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import dev.chrisbanes.haze.hazeSource import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.AppInfoCard -import me.kavishdevar.librepods.presentation.components.DeviceInfoCard -import me.kavishdevar.librepods.presentation.components.NavigationButton -import me.kavishdevar.librepods.presentation.components.StyledBottomSheet -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledIconButton -import me.kavishdevar.librepods.presentation.components.StyledInputField -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledSlider -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.m3.ExpressiveActionGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveAction +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSlider +import me.kavishdevar.librepods.presentation.m3.ExpressiveSwitchRow +import me.kavishdevar.librepods.presentation.m3.viz.AncVisualizer +import me.kavishdevar.librepods.presentation.m3.viz.AncVizPrefs +import me.kavishdevar.librepods.presentation.m3.viz.AncVizStyle import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel import me.kavishdevar.librepods.utils.XposedState -import java.util.concurrent.TimeUnit @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -100,624 +99,516 @@ fun AppSettingsScreen( navController: NavController, viewModel: AppSettingsViewModel = viewModel() ) { val context = LocalContext.current - val scrollState = rememberScrollState() val state by viewModel.uiState.collectAsState() - val backdrop = rememberLayerBackdrop() - - val contactBottomSheet = remember { mutableStateOf(false) } - val subjectState = remember { TextFieldState() } - val descriptionState = remember { TextFieldState() } - val subjectFocusRequester = remember { FocusRequester() } - val descriptionFocusRequester = remember { FocusRequester() } - - StyledScaffold( - title = stringResource(R.string.settings) - ) { topPadding, hazeState, bottomPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .layerBackdrop(backdrop) - .hazeSource(state = hazeState) - .verticalScroll(scrollState) - .padding(horizontal = 16.dp) + var contactSheetVisible by remember { mutableStateOf(false) } + val contactSheetState = rememberModalBottomSheetState() + + ExpressiveScreenScaffold( + title = stringResource(R.string.settings), + onBack = { navController.popBackStack() }, + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 140.dp, + start = 16.dp, + end = 16.dp, + ), ) { - Spacer(modifier = Modifier.height(topPadding)) - - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color.Black - - if (state.connectionSuccessful) { - StyledToggle( - title = stringResource(R.string.widget), - label = stringResource(R.string.show_phone_battery_in_widget), - description = stringResource(R.string.show_phone_battery_in_widget_description), - checked = state.showPhoneBatteryInWidget, - onCheckedChange = viewModel::setShowPhoneBatteryInWidget, - enabled = state.isPremium - ) - - Text( - text = stringResource(R.string.popup_animations), style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp) - ) - - Spacer(modifier = Modifier.height(2.dp)) - - Column( + item { + LaunchedEffect(Unit) { AncVizPrefs.loadIfNeeded(context) } + val vizStyle by AncVizPrefs.style.collectAsState() + ExpressiveSectionHeader("Noise control visualization") + Box( modifier = Modifier .fillMaxWidth() - .background( - backgroundColor, RoundedCornerShape(28.dp) - ) - .padding(vertical = 4.dp) + .height(150.dp) + .clip(RoundedCornerShape(28.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerLow), ) { - StyledToggle( - label = stringResource(R.string.show_bottom_sheet_popup), - description = stringResource(R.string.show_bottom_sheet_popup_description), - checked = state.showBottomSheetPopup, - onCheckedChange = viewModel::setShowBottomSheetPopup, - independent = false - ) - - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) - - StyledToggle( - label = stringResource(R.string.show_island_popup), - description = stringResource(R.string.show_island_popup_description), - checked = state.showIslandPopup, - onCheckedChange = viewModel::setShowIslandPopup, - independent = false - ) + AncVisualizer(modeValue = 4, style = vizStyle, modifier = Modifier.fillMaxSize()) } + Spacer(Modifier.height(12.dp)) + ExpressiveGroup { + AncVizStyle.entries.forEachIndexed { i, st -> + if (i > 0) ExpressiveDivider() + ExpressiveListItem( + title = st.label, + onClick = { AncVizPrefs.set(context, st) }, + trailing = if (st == vizStyle) { + { Icon(Icons.Filled.Check, contentDescription = "Selected", tint = MaterialTheme.colorScheme.primary) } + } else null, + ) + } + } + } - Text( - text = stringResource(R.string.conversational_awareness), style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp) - ) - - Spacer(modifier = Modifier.height(2.dp)) + if (state.connectionSuccessful) { + item { + ExpressiveSectionHeader(stringResource(R.string.widget)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.show_phone_battery_in_widget), + subtitle = stringResource(R.string.show_phone_battery_in_widget_description), + leadingIcon = Icons.Filled.Widgets, + checked = state.showPhoneBatteryInWidget, + onCheckedChange = viewModel::setShowPhoneBatteryInWidget, + enabled = state.isPremium, + ) + } + } - Column( - modifier = Modifier - .fillMaxWidth() - .background( - backgroundColor, RoundedCornerShape(28.dp) + item { + ExpressiveSectionHeader(stringResource(R.string.conversational_awareness)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.conversational_awareness_pause_music), + subtitle = stringResource(R.string.conversational_awareness_pause_music_description), + leadingIcon = Icons.Filled.RecordVoiceOver, + checked = state.conversationalAwarenessPauseMusicEnabled, + onCheckedChange = viewModel::setConversationalAwarenessPauseMusicEnabled, + enabled = state.isPremium, ) - .padding(vertical = 4.dp) - ) { - StyledToggle( - label = stringResource(R.string.conversational_awareness_pause_music), - description = stringResource(R.string.conversational_awareness_pause_music_description), - checked = state.conversationalAwarenessPauseMusicEnabled, - onCheckedChange = viewModel::setConversationalAwarenessPauseMusicEnabled, - independent = false, - enabled = state.isPremium - ) + ExpressiveDivider() + ExpressiveSwitchRow( + title = stringResource(R.string.relative_conversational_awareness_volume), + subtitle = stringResource(R.string.relative_conversational_awareness_volume_description), + leadingIcon = Icons.Filled.VolumeUp, + checked = state.relativeConversationalAwarenessVolumeEnabled, + onCheckedChange = viewModel::setRelativeConversationalAwarenessVolumeEnabled, + enabled = state.isPremium, + ) + } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) + val conversationalAwarenessVolume = state.conversationalAwarenessVolume + LaunchedEffect(conversationalAwarenessVolume) { + viewModel.setConversationalAwarenessVolume(conversationalAwarenessVolume) + } - StyledToggle( - label = stringResource(R.string.relative_conversational_awareness_volume), - description = stringResource(R.string.relative_conversational_awareness_volume_description), - checked = state.relativeConversationalAwarenessVolumeEnabled, - onCheckedChange = viewModel::setRelativeConversationalAwarenessVolumeEnabled, - independent = false, + Spacer(Modifier.height(12.dp)) + Text( + text = stringResource(R.string.conversational_awareness_volume), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + ) + ExpressiveSlider( + value = conversationalAwarenessVolume, + onValueChange = { newValue -> + viewModel.setConversationalAwarenessVolume(newValue) + }, + valueRange = 10f..85f, enabled = state.isPremium, + startIcon = Icons.Filled.VolumeUp, + endIcon = Icons.Filled.VolumeUp, + modifier = Modifier.padding(horizontal = 8.dp), ) } - Spacer(modifier = Modifier.height(16.dp)) + if (context.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) { + item { + ExpressiveSectionHeader(stringResource(R.string.ear_detection)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.disconnect_when_not_wearing), + subtitle = stringResource(R.string.disconnect_when_not_wearing_description), + leadingIcon = Icons.Filled.Hearing, + checked = state.disconnectWhenNotWearing, + onCheckedChange = viewModel::setDisconnectWhenNotWearing, + enabled = state.isPremium, + ) + } + } + } - val conversationalAwarenessVolume = state.conversationalAwarenessVolume - LaunchedEffect(conversationalAwarenessVolume) { - viewModel.setConversationalAwarenessVolume(conversationalAwarenessVolume) + item { + ExpressiveSectionHeader(stringResource(R.string.takeover_airpods_state)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.takeover_disconnected), + subtitle = stringResource(R.string.takeover_disconnected_desc), + checked = state.takeoverWhenDisconnected, + onCheckedChange = viewModel::setTakeoverWhenDisconnected, + enabled = state.isPremium, + ) + ExpressiveDivider(startInset = false) + ExpressiveSwitchRow( + title = stringResource(R.string.takeover_idle), + subtitle = stringResource(R.string.takeover_idle_desc), + checked = state.takeoverWhenIdle, + onCheckedChange = viewModel::setTakeoverWhenIdle, + enabled = state.isPremium, + ) + ExpressiveDivider(startInset = false) + ExpressiveSwitchRow( + title = stringResource(R.string.takeover_music), + subtitle = stringResource(R.string.takeover_music_desc), + checked = state.takeoverWhenMusic, + onCheckedChange = viewModel::setTakeoverWhenMusic, + enabled = state.isPremium, + ) + ExpressiveDivider(startInset = false) + ExpressiveSwitchRow( + title = stringResource(R.string.takeover_call), + subtitle = stringResource(R.string.takeover_call_desc), + checked = state.takeoverWhenCall, + onCheckedChange = viewModel::setTakeoverWhenCall, + enabled = state.isPremium, + ) + } } - StyledSlider( - label = stringResource(R.string.conversational_awareness_volume), - value = conversationalAwarenessVolume, - valueRange = 10f..85f, - snapPoints = listOf(44f), - startLabel = "10%", - endLabel = "85%", - onValueChange = { newValue -> - viewModel.setConversationalAwarenessVolume( - newValue + item { + ExpressiveSectionHeader(stringResource(R.string.takeover_phone_state)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.takeover_ringing_call), + subtitle = stringResource(R.string.takeover_ringing_call_desc), + checked = state.takeoverWhenRingingCall, + onCheckedChange = viewModel::setTakeoverWhenRingingCall, + enabled = state.isPremium, ) - }, - independent = true, - enabled = state.isPremium - ) + ExpressiveDivider(startInset = false) + ExpressiveSwitchRow( + title = stringResource(R.string.takeover_media_start), + subtitle = stringResource(R.string.takeover_media_start_desc), + checked = state.takeoverWhenMediaStart, + onCheckedChange = viewModel::setTakeoverWhenMediaStart, + enabled = state.isPremium, + ) + } + } -// if (!BuildConfig.PLAY_BUILD) { -// Spacer(modifier = Modifier.height(16.dp)) -// -// NavigationButton( -// to = "", -// title = stringResource(R.string.camera_control), -// name = stringResource(R.string.set_custom_camera_package), -// navController = navController, -// onClick = { -// if (state.isPremium) viewModel.setShowCameraDialog(true) -// }, -// independent = true, -// description = stringResource(R.string.camera_control_app_description) -// ) -// } - - Spacer(modifier = Modifier.height(16.dp)) - if (context.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) { - StyledToggle( - title = stringResource(R.string.ear_detection), - label = stringResource(R.string.disconnect_when_not_wearing), - description = stringResource(R.string.disconnect_when_not_wearing_description), - checked = state.disconnectWhenNotWearing, - onCheckedChange = viewModel::setDisconnectWhenNotWearing, - enabled = state.isPremium + item { + ExpressiveSectionHeader(stringResource(R.string.advanced_options)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.use_alternate_head_tracking_packets), + subtitle = stringResource(R.string.use_alternate_head_tracking_packets_description), + leadingIcon = Icons.Filled.Tune, + checked = state.useAlternateHeadTrackingPackets, + onCheckedChange = viewModel::setUseAlternateHeadTrackingPackets, + enabled = state.isPremium, + ) + } + } + } else { + item { + Spacer(Modifier.height(16.dp)) + Text( + text = stringResource(R.string.customizations_unavailable), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp), ) } + } - Text( - text = stringResource(R.string.takeover_airpods_state), style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp) - ) - - Spacer(modifier = Modifier.height(4.dp)) + if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) { + item { + val restartBluetoothText = stringResource(R.string.found_offset_restart_bluetooth) + ExpressiveSectionHeader(stringResource(R.string.requires_xposed)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.act_as_an_apple_device), + subtitle = stringResource(R.string.act_as_an_apple_device_description), + leadingIcon = Icons.Filled.SwapHoriz, + checked = state.vendorIdHook, + onCheckedChange = { enabled -> + Toast.makeText(context, restartBluetoothText, Toast.LENGTH_SHORT).show() + viewModel.setVendorIdHook(enabled) + }, + enabled = state.isPremium, + ) + } + } + } - Column( - modifier = Modifier - .fillMaxWidth() - .background( - backgroundColor, RoundedCornerShape(28.dp) + if (!BuildConfig.PLAY_BUILD) { + item { + ExpressiveSectionHeader(stringResource(R.string.troubleshooting)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.troubleshooting), + subtitle = stringResource(R.string.troubleshooting_description), + leadingIcon = Icons.Filled.BugReport, + showChevron = true, + onClick = { navController.navigate("troubleshooting") }, ) - .padding(vertical = 4.dp) - ) { - StyledToggle( - label = stringResource(R.string.takeover_disconnected), - description = stringResource(R.string.takeover_disconnected_desc), - checked = state.takeoverWhenDisconnected, - onCheckedChange = viewModel::setTakeoverWhenDisconnected, - independent = false, - enabled = state.isPremium + } + } + } + + item { + ExpressiveSectionHeader(stringResource(R.string.contact)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.email), + leadingIcon = Icons.Filled.Email, + showChevron = true, + onClick = { contactSheetVisible = true }, ) - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) + ExpressiveDivider() + ExpressiveListItem( + title = stringResource(R.string.discord), + leadingIcon = Icons.AutoMirrored.Filled.Chat, + showChevron = true, + onClick = { + val intent = + Intent(Intent.ACTION_VIEW, "https://discord.gg/Ts4wupXcmc".toUri()) + context.startActivity(intent) + }, ) + ExpressiveDivider() + ExpressiveListItem( + title = stringResource(R.string.github_issues), + leadingIcon = Icons.Filled.BugReport, + showChevron = true, + onClick = { + val appVersion = Uri.encode("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") + val device = Uri.encode("${Build.MANUFACTURER} ${Build.MODEL}") + val androidVersion = Uri.encode("${Build.ID} (${Build.DISPLAY})") + val appSource = Uri.encode( + when { + BuildConfig.PLAY_BUILD -> "Play" + else -> "GitHub" + } + ) + val url = "https://github.com/kavishdevar/librepods/issues/new" + + "?template=01-bug-report-android.yml" + + "&app-source=$appSource" + + "&app-version=$appVersion" + + "&device=$device" + + "&android-version=$androidVersion" + + val intent = Intent(Intent.ACTION_VIEW, url.toUri()) + context.startActivity(intent) + }, + ) + } + } - StyledToggle( - label = stringResource(R.string.takeover_idle), - description = stringResource(R.string.takeover_idle_desc), - checked = state.takeoverWhenIdle, - onCheckedChange = viewModel::setTakeoverWhenIdle, - independent = false, - enabled = state.isPremium + item { + ExpressiveSectionHeader(stringResource(R.string.device_info)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.manufacturer), + value = Build.MANUFACTURER, ) - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) + ExpressiveDivider(startInset = false) + ExpressiveListItem( + title = stringResource(R.string.model_number), + value = Build.MODEL, ) - - StyledToggle( - label = stringResource(R.string.takeover_music), - description = stringResource(R.string.takeover_music_desc), - checked = state.takeoverWhenMusic, - onCheckedChange = viewModel::setTakeoverWhenMusic, - independent = false, - enabled = state.isPremium + ExpressiveDivider(startInset = false) + ExpressiveListItem( + title = stringResource(R.string.build_id), + value = Build.DISPLAY, ) - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) + ExpressiveDivider(startInset = false) + ExpressiveListItem( + title = stringResource(R.string.version), + value = Build.ID + " (${Build.VERSION.SDK_INT_FULL})", ) - - StyledToggle( - label = stringResource(R.string.takeover_call), - description = stringResource(R.string.takeover_call_desc), - checked = state.takeoverWhenCall, - onCheckedChange = viewModel::setTakeoverWhenCall, - independent = false, - enabled = state.isPremium + ExpressiveDivider(startInset = false) + ExpressiveListItem( + title = stringResource(R.string.xposed_available), + value = if (XposedState.isAvailable) stringResource(R.string.yes) else stringResource(R.string.no), + ) + ExpressiveDivider(startInset = false) + ExpressiveListItem( + title = stringResource(R.string.app_enabled_in_xposed), + value = if (XposedState.bluetoothScopeEnabled) stringResource(R.string.yes) else stringResource(R.string.no), ) } + } - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(R.string.takeover_phone_state), style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), modifier = Modifier.padding(horizontal = 16.dp) - ) - Spacer(modifier = Modifier.height(4.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .background( - backgroundColor, RoundedCornerShape(28.dp) - ) - .padding(vertical = 4.dp) - ) { - StyledToggle( - label = stringResource(R.string.takeover_ringing_call), - description = stringResource(R.string.takeover_ringing_call_desc), - checked = state.takeoverWhenRingingCall, - onCheckedChange = viewModel::setTakeoverWhenRingingCall, - independent = false, - enabled = state.isPremium + item { + ExpressiveSectionHeader(stringResource(R.string.about)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.version), + leadingIcon = Icons.Filled.Info, + value = BuildConfig.VERSION_NAME, ) - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) + ExpressiveDivider(startInset = false) + ExpressiveListItem( + title = stringResource(R.string.version_code), + value = BuildConfig.VERSION_CODE.toString(), ) - - StyledToggle( - label = stringResource(R.string.takeover_media_start), - description = stringResource(R.string.takeover_media_start_desc), - checked = state.takeoverWhenMediaStart, - onCheckedChange = viewModel::setTakeoverWhenMediaStart, - independent = false, - enabled = state.isPremium + ExpressiveDivider(startInset = false) + ExpressiveListItem( + title = stringResource(R.string.flavor), + value = BuildConfig.FLAVOR, + ) + ExpressiveDivider(startInset = false) + ExpressiveListItem( + title = stringResource(R.string.build_type), + value = BuildConfig.BUILD_TYPE, + ) + ExpressiveDivider() + ExpressiveListItem( + title = stringResource(R.string.open_source_licenses), + leadingIcon = Icons.Filled.Description, + showChevron = true, + onClick = { navController.navigate("open_source_licenses") }, ) } + } + } + if (state.showCameraDialog) { + AlertDialog(onDismissRequest = { viewModel.setShowCameraDialog(false) }, title = { Text( - text = stringResource(R.string.advanced_options), style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp) + stringResource(R.string.set_custom_camera_package), + style = MaterialTheme.typography.titleLarge ) - - Spacer(modifier = Modifier.height(2.dp)) - - StyledToggle( - label = stringResource(R.string.use_alternate_head_tracking_packets), - description = stringResource(R.string.use_alternate_head_tracking_packets_description), - checked = state.useAlternateHeadTrackingPackets, - onCheckedChange = viewModel::setUseAlternateHeadTrackingPackets, - independent = true, - enabled = state.isPremium - ) - Spacer(modifier = Modifier.height(16.dp)) - } else { - Box( - modifier = Modifier - .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)) - .padding(horizontal = 16.dp) - .padding(top = 16.dp, bottom = 2.dp) - ) { + }, text = { + Column { Text( - text = stringResource(R.string.customizations_unavailable), - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor.copy(alpha = 0.6f), + stringResource(R.string.enter_custom_camera_package), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 8.dp) + ) + + OutlinedTextField( + value = state.cameraPackageValue, + onValueChange = { + viewModel.setCameraPackageValue(it) + viewModel.setCameraPackageError(null) + }, + modifier = Modifier.fillMaxWidth(), + isError = state.cameraPackageError != null, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Ascii, + capitalization = KeyboardCapitalization.None ), - modifier = Modifier + supportingText = { + if (state.cameraPackageError != null) { + Text( + state.cameraPackageError ?: "", + color = MaterialTheme.colorScheme.error + ) + } + }, + label = { Text(stringResource(R.string.custom_camera_package)) }) + } + }, confirmButton = { + val successText = stringResource(R.string.custom_camera_package_set_success) + TextButton( + onClick = { + viewModel.saveCameraPackage() + Toast.makeText(context, successText, Toast.LENGTH_SHORT).show() + }) { + Text( + "Save", + style = MaterialTheme.typography.labelLarge ) } - } + }, dismissButton = { + TextButton( + onClick = { viewModel.setShowCameraDialog(false) }) { + Text( + "Cancel", + style = MaterialTheme.typography.labelLarge + ) + } + }) + } - if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) { - val restartBluetoothText = - stringResource(R.string.found_offset_restart_bluetooth) - StyledToggle( - label = stringResource(R.string.act_as_an_apple_device) + " (${ - stringResource( - R.string.requires_xposed + if (contactSheetVisible) { + ContactBottomSheet( + sheetState = contactSheetState, + onDismiss = { contactSheetVisible = false }, + onSend = { subject, description -> + contactSheetVisible = false + val intent = Intent(Intent.ACTION_SENDTO).apply { + data = "mailto:".toUri() + putExtra(Intent.EXTRA_EMAIL, arrayOf("contact@kavish.xyz")) + putExtra(Intent.EXTRA_SUBJECT, "LibrePods: $subject") + putExtra( + Intent.EXTRA_TEXT, + description + + "\n\n----------" + + "\nPhone details:" + + "\nMANUFACTURER: ${Build.MANUFACTURER}" + + "\nMODEL: ${Build.MODEL} (${Build.PRODUCT})" + + "\nDISPLAY_VERSION: ${Build.DISPLAY}" + + "\nID: ${Build.ID} (SDK ${Build.VERSION.SDK_INT_FULL})" + + "\nXposed enabled/active: ${XposedState.isAvailable}/${XposedState.bluetoothScopeEnabled}" + + "\n\nApp details:" + + "\nVERSION: ${BuildConfig.VERSION_NAME}" + + "\nVERSION_CODE: ${BuildConfig.VERSION_CODE}" + + "\nFLAVOR: ${BuildConfig.FLAVOR}" + + "\nBUILD_TYPE: ${BuildConfig.BUILD_TYPE}" ) - })", - description = stringResource(R.string.act_as_an_apple_device_description), - checked = state.vendorIdHook, - onCheckedChange = { enabled -> - Toast.makeText(context, restartBluetoothText, Toast.LENGTH_SHORT).show() - viewModel.setVendorIdHook(enabled) - }, - independent = true, - enabled = state.isPremium - ) - } + } + context.startActivity(intent) + }, + ) + } + } +} - if (!BuildConfig.PLAY_BUILD) { - Spacer(modifier = Modifier.height(16.dp)) - NavigationButton( - to = "troubleshooting", - name = stringResource(R.string.troubleshooting), - navController = navController, - independent = true, - description = stringResource(R.string.troubleshooting_description) - ) - } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ContactBottomSheet( + sheetState: androidx.compose.material3.SheetState, + onDismiss: () -> Unit, + onSend: (subject: String, description: String) -> Unit, +) { + var subject by remember { mutableStateOf("") } + var description by remember { mutableStateOf("") } + val canSend = subject.isNotEmpty() && description.isNotEmpty() + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 32.dp), + ) { + Text( + text = stringResource(R.string.describe_your_issue), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 16.dp), + ) - Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = subject, + onValueChange = { subject = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text(stringResource(R.string.subject)) }, + ) - Box( - modifier = Modifier - .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)) - .padding(start = 16.dp, bottom = 2.dp, top = 24.dp, end = 4.dp) - ) { - Text( - text = stringResource(R.string.contact), style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } + Spacer(Modifier.height(12.dp)) - Spacer(modifier = Modifier.height(4.dp)) - Column( + OutlinedTextField( + value = description, + onValueChange = { description = it }, modifier = Modifier .fillMaxWidth() - .background( - backgroundColor, RoundedCornerShape(28.dp) - ) - .clip(RoundedCornerShape(28.dp)) - ) { - NavigationButton( - to = "", - name = stringResource(R.string.email), - navController = navController, - onClick = { contactBottomSheet.value = true }, - independent = false - ) - - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) - - NavigationButton( - to = "", - name = stringResource(R.string.discord), - navController = navController, - onClick = { - val intent = - Intent(Intent.ACTION_VIEW, "https://discord.gg/Ts4wupXcmc".toUri()) - context.startActivity(intent) - }, - independent = false - ) + .height(160.dp), + label = { Text(stringResource(R.string.describe_your_issue)) }, + ) - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier.padding(horizontal = 12.dp) - ) + Spacer(Modifier.height(20.dp)) - NavigationButton( - to = "", - name = stringResource(R.string.github_issues), - navController = navController, - onClick = { - val appVersion = Uri.encode("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") - val device = Uri.encode("${Build.MANUFACTURER} ${Build.MODEL}") - val androidVersion = Uri.encode("${Build.ID} (${Build.DISPLAY})") - val appSource = Uri.encode( - when { - BuildConfig.PLAY_BUILD -> "Play" - else -> "GitHub" - } - ) - val url = "https://github.com/kavishdevar/librepods/issues/new" + - "?template=01-bug-report-android.yml" + - "&app-source=$appSource" + - "&app-version=$appVersion" + - "&device=$device" + - "&android-version=$androidVersion" - - val intent = Intent(Intent.ACTION_VIEW, url.toUri()) - context.startActivity(intent) + ExpressiveActionGroup( + actions = listOf( + ExpressiveAction(label = "Send") { + if (canSend) onSend(subject, description) }, - independent = false - ) - } - - Spacer(modifier = Modifier.height(20.dp)) - DeviceInfoCard() - Spacer(modifier = Modifier.height(16.dp)) - AppInfoCard() - - Spacer(modifier = Modifier.height(16.dp)) - - NavigationButton( - to = "open_source_licenses", - name = stringResource(R.string.open_source_licenses), - navController = navController, - independent = true + ), ) - - Spacer(modifier = Modifier.height(bottomPadding)) - - if (state.showCameraDialog) { - AlertDialog(onDismissRequest = { viewModel.setShowCameraDialog(false) }, title = { - Text( - stringResource(R.string.set_custom_camera_package), - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Medium - ) - }, text = { - Column { - Text( - stringResource(R.string.enter_custom_camera_package), - fontFamily = FontFamily(Font(R.font.sf_pro)), - modifier = Modifier.padding(bottom = 8.dp) - ) - - OutlinedTextField( - value = state.cameraPackageValue, - onValueChange = { - viewModel.setCameraPackageValue(it) - viewModel.setCameraPackageError(null) - }, - modifier = Modifier.fillMaxWidth(), - isError = state.cameraPackageError != null, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Ascii, - capitalization = KeyboardCapitalization.None - ), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = if (isDarkTheme) Color(0xFF007AFF) else Color( - 0xFF3C6DF5 - ), - unfocusedBorderColor = if (isDarkTheme) Color.Gray else Color.LightGray - ), - supportingText = { - if (state.cameraPackageError != null) { - Text( - state.cameraPackageError ?: "", - color = MaterialTheme.colorScheme.error - ) - } - }, - label = { Text(stringResource(R.string.custom_camera_package)) }) - } - }, confirmButton = { - val successText = stringResource(R.string.custom_camera_package_set_success) - TextButton( - onClick = { - viewModel.saveCameraPackage() - Toast.makeText(context, successText, Toast.LENGTH_SHORT).show() - }) { - Text( - "Save", - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Medium - ) - } - }, dismissButton = { - TextButton( - onClick = { viewModel.setShowCameraDialog(false) }) { - Text( - "Cancel", - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Medium - ) - } - }) - } - } - StyledBottomSheet( - visible = contactBottomSheet.value, - onDismiss = { contactBottomSheet.value = false }, - backdrop = backdrop - ) { innerBackdrop, progress -> - val animatedPadding = lerp(16.dp, 2.dp, progress) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = animatedPadding) - .padding(bottom = 16.dp), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - StyledIconButton( - icon = "\uDBC0\uDD84", - backdrop = innerBackdrop, - onClick = { contactBottomSheet.value = false } - ) - Text ( - text = stringResource(R.string.describe_your_issue), - style = TextStyle( - fontSize = 18.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - color = if (isSystemInDarkTheme()) Color.White else Color.Black - ) - ) - StyledIconButton( - icon = "\uDBC0\uDE1F", - backdrop = innerBackdrop, - surfaceColor = if (isSystemInDarkTheme()) Color(0xFF0091FF) else Color(0xFF0088FF), - iconTint = if (subjectState.text.isNotEmpty() && descriptionState.text.isNotEmpty()) Color.White else Color.Gray, - enabled = subjectState.text.isNotEmpty() && descriptionState.text.isNotEmpty(), - onClick = { - contactBottomSheet.value = false - val intent = Intent(Intent.ACTION_SENDTO).apply { - data = "mailto:".toUri() - putExtra(Intent.EXTRA_EMAIL, arrayOf("contact@kavish.xyz")) - putExtra(Intent.EXTRA_SUBJECT, "LibrePods: ${subjectState.text}") - putExtra( - Intent.EXTRA_TEXT, - "${descriptionState.text}" + - "\n\n----------" + - "\nPhone details:" + - "\nMANUFACTURER: ${Build.MANUFACTURER}" + - "\nMODEL: ${Build.MODEL} (${Build.PRODUCT})" + - "\nDISPLAY_VERSION: ${Build.DISPLAY}" + - "\nID: ${Build.ID} (SDK ${Build.VERSION.SDK_INT_FULL})" + - "\nXposed enabled/active: ${XposedState.isAvailable}/${XposedState.bluetoothScopeEnabled}" + - "\n\nApp details:" + - "\nVERSION: ${BuildConfig.VERSION_NAME}" + - "\nVERSION_CODE: ${BuildConfig.VERSION_CODE}" + - "\nFLAVOR: ${BuildConfig.FLAVOR}" + - "\nBUILD_TYPE: ${BuildConfig.BUILD_TYPE}" - ) - } - context.startActivity(intent) - subjectState.clearText() - descriptionState.clearText() - } - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - StyledInputField( - inputState = subjectState, - focusRequester = subjectFocusRequester, - placeholder = stringResource(R.string.subject), - ) - - Spacer(modifier = Modifier.height(12.dp)) - - StyledInputField( - inputState = descriptionState, - focusRequester = descriptionFocusRequester, - placeholder = stringResource(R.string.describe_your_issue), - singleLine = false - ) - } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/CameraControlScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/CameraControlScreen.kt index 433e7b324..635fbb45d 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/CameraControlScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/CameraControlScreen.kt @@ -24,28 +24,33 @@ import android.content.Context import android.content.Intent import android.provider.Settings import android.view.accessibility.AccessibilityManager -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Block +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.PhotoCamera +import androidx.compose.material.icons.filled.TouchApp +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.SelectItem -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledSelectList -import me.kavishdevar.librepods.services.AppListenerService import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.StemPressType +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.services.AppListenerService import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel @Composable @@ -72,40 +77,53 @@ fun CameraControlScreen(viewModel: AirPodsViewModel) { } } - val cameraOptions = remember(currentCameraAction) { + data class CameraOption( + val name: String, + val icon: ImageVector, + val action: StemPressType?, + ) + + val cameraOptions = remember { listOf( - SelectItem( - name = "Off", - selected = currentCameraAction == null, - onClick = { handleSelection(null) } - ), - SelectItem( - name = "Press once", - selected = currentCameraAction == StemPressType.SINGLE_PRESS, - onClick = { handleSelection(StemPressType.SINGLE_PRESS) } - ), - SelectItem( - name = "Press and hold AirPods", - selected = currentCameraAction == StemPressType.LONG_PRESS, - onClick = { handleSelection(StemPressType.LONG_PRESS) } - ) + CameraOption("Off", Icons.Filled.Block, null), + CameraOption("Press once", Icons.Filled.PhotoCamera, StemPressType.SINGLE_PRESS), + CameraOption("Press and hold AirPods", Icons.Filled.TouchApp, StemPressType.LONG_PRESS), ) } - val backdrop = rememberLayerBackdrop() - - StyledScaffold( - title = stringResource(R.string.camera_control) - ) { spacerHeight -> - Column( - modifier = Modifier - .fillMaxSize() - .layerBackdrop(backdrop) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + ExpressiveScreenScaffold(title = stringResource(R.string.camera_control)) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), ) { - Spacer(modifier = Modifier.height(spacerHeight)) - StyledSelectList(items = cameraOptions) + item { + ExpressiveSectionHeader("Shutter action") + ExpressiveGroup { + cameraOptions.forEachIndexed { index, option -> + if (index > 0) ExpressiveDivider() + val selected = currentCameraAction == option.action + ExpressiveListItem( + title = option.name, + leadingIcon = option.icon, + onClick = { handleSelection(option.action) }, + trailing = if (selected) { + { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + } else null, + ) + } + } + } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/DebugScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/DebugScreen.kt index 75c4bc2d6..088f09d1e 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/DebugScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/DebugScreen.kt @@ -16,7 +16,7 @@ along with this program. If not, see . */ -@file:OptIn(ExperimentalHazeMaterialsApi::class, ExperimentalEncodingApi::class) +@file:OptIn(ExperimentalEncodingApi::class) package me.kavishdevar.librepods.presentation.screens @@ -28,36 +28,43 @@ import android.os.Build import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Send -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.DeleteSweep +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -67,25 +74,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager -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.input.TextFieldValue import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import dev.chrisbanes.haze.hazeSource -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledIconButton -import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.data.BatteryStatus import me.kavishdevar.librepods.data.isHeadTrackingData +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.pressBounce import me.kavishdevar.librepods.services.ServiceManager import kotlin.io.encoding.ExperimentalEncodingApi @@ -302,13 +302,14 @@ fun DebugScreen(navController: NavController) { val refreshTrigger = remember { mutableIntStateOf(0) } LaunchedEffect(refreshTrigger.intValue) { - while(true) { + while (true) { delay(1000) refreshTrigger.intValue += 1 } } val expandedItems = remember { mutableStateOf(setOf()) } + val packet = remember { mutableStateOf(TextFieldValue("")) } fun copyToClipboard(text: String) { val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager @@ -323,49 +324,97 @@ fun DebugScreen(navController: NavController) { } } - val isDarkTheme = isSystemInDarkTheme() - val backdrop = rememberLayerBackdrop() - StyledScaffold( + fun sendPacket() { + if (packet.value.text.isNotBlank()) { + airPodsService?.aacpManager?.sendPacket( + packet.value.text + .split(" ") + .map { it.toInt(16).toByte() } + .toByteArray() + ) + packet.value = TextFieldValue("") + focusManager.clearFocus() + + if (packetLogs.isNotEmpty()) { + coroutineScope.launch { + try { + delay(100) + listState.animateScrollToItem( + index = (packetLogs.size - 1).coerceAtLeast(0), + scrollOffset = 0 + ) + } catch (e: Exception) { + e.printStackTrace() + listState.scrollToItem( + index = (packetLogs.size - 1).coerceAtLeast(0) + ) + } + } + } + } + } + + ExpressiveScreenScaffold( title = "Debug", - actionButtons = listOf( - {scaffoldBackdrop -> - StyledIconButton( - onClick = { - airPodsService?.clearLogs() - expandedItems.value = emptySet() - }, - icon = "􀈑", - backdrop = scaffoldBackdrop + onBack = { navController.popBackStack() }, + actions = { + IconButton( + onClick = { + airPodsService?.clearLogs() + expandedItems.value = emptySet() + } + ) { + Icon( + imageVector = Icons.Filled.DeleteSweep, + contentDescription = "Clear logs", ) } - ), - ) { topPadding, hazeState, bottomPadding -> + }, + ) { padding -> Column( modifier = Modifier .fillMaxSize() - .hazeSource(hazeState) - .navigationBarsPadding() - .layerBackdrop(backdrop) + .padding(top = padding.calculateTopPadding()) .padding(horizontal = 16.dp) ) { - Spacer(modifier = Modifier.height(topPadding)) LazyColumn( state = listState, modifier = Modifier .fillMaxWidth() .weight(1f), - content = { - items(packetLogs.size) { index -> - val message = packetLogs.elementAt(index) - val isSent = message.startsWith("Sent") - val isExpanded = expandedItems.value.contains(index) - val packetInfo = parsePacket(message) - - Card( + contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (packetLogs.isEmpty()) { + item { + Spacer(Modifier.height(48.dp)) + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + text = "No packets captured yet", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + + items(packetLogs.size) { index -> + val message = packetLogs.elementAt(index) + val isSent = message.startsWith("Sent") + val isExpanded = expandedItems.value.contains(index) + val packetInfo = parsePacket(message) + + val interaction = remember { MutableInteractionSource() } + + ExpressiveGroup( + modifier = Modifier.pressBounce(interaction), + ) { + Box( modifier = Modifier .fillMaxWidth() - .padding(vertical = 2.dp) .combinedClickable( + interactionSource = interaction, + indication = null, onClick = { expandedItems.value = if (isExpanded) { expandedItems.value - index @@ -377,23 +426,19 @@ fun DebugScreen(navController: NavController) { copyToClipboard(packetInfo.rawData) } ), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - shape = RoundedCornerShape(4.dp), - colors = CardDefaults.cardColors( - containerColor = if (isSystemInDarkTheme()) Color(0xFF1C1B20) else Color(0xFFF2F2F7), - ) ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = if (isSent) "􀆉" else "􀆊", - style = TextStyle( - fontSize = 16.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = if (isSent) Color(0xFF4CD964) else Color(0xFFFF3B30) - ), + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = if (isSent) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown, + contentDescription = if (isSent) "Sent" else "Received", + tint = if (isSent) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp), ) - Spacer(modifier = Modifier.width(4.dp)) - Column { + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { Text( text = if (packetInfo.isUnknown) { val shortenedData = packetInfo.rawData.take(60) + @@ -402,47 +447,38 @@ fun DebugScreen(navController: NavController) { } else { "${packetInfo.type}: ${packetInfo.description}" }, - style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - fontFamily = FontFamily(Font(R.font.hack)) - ) + style = MaterialTheme.typography.bodyMedium, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurface, ) if (isExpanded) { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(6.dp)) if (packetInfo.parsedData.isNotEmpty()) { packetInfo.parsedData.forEach { (key, value) -> Row { Text( text = "$key: ", - style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - fontFamily = FontFamily(Font(R.font.hack)) - ), - color = Color.Gray + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( text = value, - style = TextStyle( - fontSize = 12.sp, - fontFamily = FontFamily(Font(R.font.hack)) - ), - color = Color.Gray + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(6.dp)) } Text( text = "Raw: ${packetInfo.rawData}", - style = TextStyle( - fontSize = 12.sp, - fontFamily = FontFamily(Font(R.font.hack)) - ), - color = Color.Gray + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -450,73 +486,45 @@ fun DebugScreen(navController: NavController) { } } } - ) + } + + ExpressiveDivider(startInset = false) Spacer(modifier = Modifier.height(8.dp)) - val airPodsService = ServiceManager.getService()?.let { mutableStateOf(it) } - HorizontalDivider() Row( modifier = Modifier .fillMaxWidth() - .background(if (isSystemInDarkTheme()) Color(0xFF1C1B20) else Color(0xFFF2F2F7)), - verticalAlignment = Alignment.CenterVertically + .navigationBarsPadding() + .imePadding(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - val packet = remember { mutableStateOf(TextFieldValue("")) } TextField( value = packet.value, onValueChange = { packet.value = it }, - label = { Text("Packet") }, + label = { Text("Packet (hex)") }, + singleLine = true, modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .padding(bottom = 5.dp), - trailingIcon = { - IconButton( - onClick = { - if (packet.value.text.isNotBlank()) { - airPodsService?.value?.aacpManager?.sendPacket( - packet.value.text - .split(" ") - .map { it.toInt(16).toByte() } - .toByteArray() - ) - packet.value = TextFieldValue("") - focusManager.clearFocus() - - if (packetLogs.isNotEmpty()) { - coroutineScope.launch { - try { - delay(100) - listState.animateScrollToItem( - index = (packetLogs.size - 1).coerceAtLeast(0), - scrollOffset = 0 - ) - } catch (e: Exception) { - e.printStackTrace() - listState.scrollToItem( - index = (packetLogs.size - 1).coerceAtLeast(0) - ) - } - } - } - } - } - ) { - @Suppress("DEPRECATION") - Icon(Icons.Filled.Send, contentDescription = "Send") - } - }, + .weight(1f) + .padding(vertical = 8.dp), + shape = RoundedCornerShape(20.dp), colors = TextFieldDefaults.colors( - focusedContainerColor = if (isSystemInDarkTheme()) Color(0xFF1C1B20) else Color(0xFFF2F2F7), - unfocusedContainerColor = if (isSystemInDarkTheme()) Color(0xFF1C1B20) else Color(0xFFF2F2F7), + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, - focusedTextColor = if (isSystemInDarkTheme()) Color.White else Color.Black, - unfocusedTextColor = if (isSystemInDarkTheme()) Color.White else Color.Black.copy(alpha = 0.6f), - focusedLabelColor = if (isSystemInDarkTheme()) Color.White.copy(alpha = 0.6f) else Color.Black, - unfocusedLabelColor = if (isSystemInDarkTheme()) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f), + disabledIndicatorColor = Color.Transparent, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + unfocusedTextColor = MaterialTheme.colorScheme.onSurface, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, ), - shape = RoundedCornerShape(12.dp) ) + FilledIconButton( + onClick = { sendPacket() }, + modifier = Modifier.size(56.dp), + ) { + Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send") + } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt index d440e6d60..c5d52c834 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt @@ -19,38 +19,42 @@ // this is absolutely unnecessary, why did I make this. a simple toggle would've sufficed -@file:OptIn(ExperimentalEncodingApi::class) +@file:OptIn(ExperimentalEncodingApi::class, ExperimentalMaterial3Api::class) package me.kavishdevar.librepods.presentation.screens import android.graphics.Paint import android.graphics.RadialGradient import android.graphics.Shader -import android.graphics.Typeface import android.util.Log import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring 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.togetherWith import androidx.compose.foundation.Canvas -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio 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.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -69,7 +73,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.asAndroidPath @@ -79,26 +82,22 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.drawText -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.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import dev.chrisbanes.haze.hazeSource -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.kavishdevar.librepods.R -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.StyledToggle +import me.kavishdevar.librepods.presentation.m3.ExpressiveAction +import me.kavishdevar.librepods.presentation.m3.ExpressiveActionGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSwitchRow +import me.kavishdevar.librepods.presentation.m3.rememberAppHaptics import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.utils.HeadTracking @@ -108,185 +107,156 @@ import kotlin.math.cos import kotlin.math.sin import kotlin.random.Random -@ExperimentalHazeMaterialsApi -@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) +@OptIn(ExperimentalAnimationApi::class) @Composable fun HeadTrackingScreen(viewModel: AirPodsViewModel, navController: NavController) { val state by viewModel.uiState.collectAsState() + val haptics = rememberAppHaptics() + DisposableEffect(Unit) { viewModel.startHeadTracking() onDispose { viewModel.stopHeadTracking() } } - val isDarkTheme = isSystemInDarkTheme() - if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color.Black - val scrollState = rememberScrollState() - val backdrop = rememberLayerBackdrop() - StyledScaffold( - title = stringResource(R.string.head_tracking), - actionButtons = listOf( - { scaffoldBackdrop -> - StyledIconButton( - onClick = { - if (!state.headTrackingActive) { - viewModel.startHeadTracking() - Log.d("HeadTrackingScreen", "Head tracking started") - } else { - viewModel.stopHeadTracking() - Log.d("HeadTrackingScreen", "Head tracking stopped") - } - }, - icon = if (state.headTrackingActive) "􀊅" else "􀊃", - backdrop = scaffoldBackdrop - ) - } - ), - ) { topPadding, hazeState, _ -> + var gestureText by remember { mutableStateOf("") } + val coroutineScope = rememberCoroutineScope() - var gestureText by remember { mutableStateOf("") } - val coroutineScope = rememberCoroutineScope() + var lastClickTime by remember { mutableLongStateOf(0L) } + var shouldExplode by remember { mutableStateOf(false) } - var lastClickTime by remember { mutableLongStateOf(0L) } - var shouldExplode by remember { mutableStateOf(false) } + LaunchedEffect(gestureText) { + if (gestureText.isNotEmpty()) { + lastClickTime = System.currentTimeMillis() + delay(3000) + if (System.currentTimeMillis() - lastClickTime >= 3000) { + shouldExplode = true + } + } + } - val scrollState = rememberScrollState() + val gestureTextValue = stringResource(R.string.shake_your_head_or_nod) + val particleColor = MaterialTheme.colorScheme.onSurface - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(scrollState), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Column ( - modifier = Modifier - .fillMaxWidth() - .hazeSource(state = hazeState) - .layerBackdrop(backdrop) - .padding(top = 8.dp) - .padding(horizontal = 16.dp) - ) { - Spacer(modifier = Modifier.height(topPadding)) - - StyledToggle( - label = "Head Gestures", - checked = state.headGesturesEnabled, - onCheckedChange = { viewModel.setHeadGesturesEnabled(it) }, - enabled = state.isPremium || state.headGesturesEnabled, - description = stringResource(R.string.head_gestures_details) + ExpressiveScreenScaffold( + title = stringResource(R.string.head_tracking), + onBack = { navController.popBackStack() }, + actions = { + IconButton(onClick = { + haptics.toggle() + if (!state.headTrackingActive) { + viewModel.startHeadTracking() + Log.d("HeadTrackingScreen", "Head tracking started") + } else { + viewModel.stopHeadTracking() + Log.d("HeadTrackingScreen", "Head tracking stopped") + } + }) { + Icon( + imageVector = if (state.headTrackingActive) Icons.Filled.Pause else Icons.Filled.PlayArrow, + contentDescription = if (state.headTrackingActive) "Stop head tracking" else "Start head tracking", ) + } + }, + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { + ExpressiveGroup { + ExpressiveSwitchRow( + title = "Head Gestures", + subtitle = stringResource(R.string.head_gestures_details), + checked = state.headGesturesEnabled, + onCheckedChange = { viewModel.setHeadGesturesEnabled(it) }, + enabled = state.isPremium || state.headGesturesEnabled, + ) + } + } - Spacer(modifier = Modifier.height(16.dp)) - Text( - "Head Orientation", - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, top = 8.dp) - ) + item { + ExpressiveSectionHeader("Head Orientation") HeadVisualization() + } - Spacer(modifier = Modifier.height(16.dp)) - Text( - "Velocity", - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, top = 8.dp) - ) + item { + ExpressiveSectionHeader("Velocity") AccelerationPlot() - - Spacer(modifier = Modifier.height(16.dp)) - - LaunchedEffect(gestureText) { - if (gestureText.isNotEmpty()) { - lastClickTime = System.currentTimeMillis() - delay(3000) - if (System.currentTimeMillis() - lastClickTime >= 3000) { - shouldExplode = true - } - } - } } - val gestureTextValue = stringResource(R.string.shake_your_head_or_nod) - StyledButton( - onClick = { - gestureText = gestureTextValue - coroutineScope.launch { - val accepted = ServiceManager.getService()?.testHeadGestures() ?: false - gestureText = if (accepted) "\"Yes\" gesture detected." else "\"No\" gesture detected." - } - }, - backdrop = backdrop, - modifier = Modifier.fillMaxWidth(0.75f), - maxScale = 0.05f - ) { - Text( - "Test Head Gestures", - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor + + item { + Spacer(Modifier.height(24.dp)) + ExpressiveActionGroup( + actions = listOf( + ExpressiveAction( + label = "Test Head Gestures", + onClick = { + gestureText = gestureTextValue + coroutineScope.launch { + val accepted = ServiceManager.getService()?.testHeadGestures() ?: false + gestureText = if (accepted) "\"Yes\" gesture detected." else "\"No\" gesture detected." + } + }, + ) ), + modifier = Modifier.fillMaxWidth(), ) } - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.padding(top = 12.dp, bottom = 24.dp) - ) { - AnimatedContent( - targetState = gestureText, - transitionSpec = { - (fadeIn( - animationSpec = tween(300) - ) + slideInVertically( - initialOffsetY = { 40 }, - animationSpec = tween(300) - )).togetherWith(fadeOut(animationSpec = tween(150))) - } - ) { text -> - if (shouldExplode) { - LaunchedEffect(Unit) { - CoroutineScope(coroutineScope.coroutineContext).launch { - delay(750) - gestureText = "" + + item { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 24.dp), + ) { + AnimatedContent( + targetState = gestureText, + transitionSpec = { + (fadeIn( + animationSpec = tween(300) + ) + slideInVertically( + initialOffsetY = { 40 }, + animationSpec = tween(300) + )).togetherWith(fadeOut(animationSpec = tween(150))) + } + ) { text -> + if (shouldExplode) { + LaunchedEffect(Unit) { + CoroutineScope(coroutineScope.coroutineContext).launch { + delay(750) + gestureText = "" + } } + ParticleText( + text = text, + style = TextStyle( + fontSize = 20.sp, + color = particleColor, + textAlign = TextAlign.Center + ), + onAnimationComplete = { + shouldExplode = false + }, + ) + } else { + Text( + text = text, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + ) } - ParticleText( - text = text, - style = TextStyle( - fontSize = 20.sp, - fontWeight = FontWeight.Medium, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor, - textAlign = TextAlign.Center - ), - onAnimationComplete = { - shouldExplode = false - }, - ) - } else { - Text( - text = text, - style = TextStyle( - fontSize = 20.sp, - fontWeight = FontWeight.Medium, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor, - textAlign = TextAlign.Center - ), - modifier = Modifier - .fillMaxWidth() - ) } } } @@ -366,9 +336,36 @@ private fun ParticleText( @Composable private fun HeadVisualization() { val orientation by HeadTracking.orientation.collectAsState() - val darkTheme = isSystemInDarkTheme() - val backgroundColor = if (darkTheme) Color(0xFF1C1C1E) else Color.White - val strokeColor = if (darkTheme) Color.White else Color.Black + val backgroundColor = MaterialTheme.colorScheme.surfaceContainer + val strokeColor = MaterialTheme.colorScheme.onSurface + val labelColor = MaterialTheme.colorScheme.onSurfaceVariant + + // Decouple rendering from the sparse, noisy sensor stream: spring toward each + // new orientation so the face glides at the display's refresh rate (120 fps + // where available) instead of snapping between raw samples. A lightly-damped, + // medium-stiffness spring doubles as a low-pass filter for the sensor jitter + // that made the head look "janky", while still tracking quickly enough to feel + // live. + val animatedPitch by animateFloatAsState( + targetValue = orientation.pitch, + animationSpec = spring(dampingRatio = 0.9f, stiffness = 350f), + label = "headPitch" + ) + val animatedYaw by animateFloatAsState( + targetValue = orientation.yaw, + animationSpec = spring(dampingRatio = 0.9f, stiffness = 350f), + label = "headYaw" + ) + + // Reusable draw objects — re-allocating native Paint/Path on every frame at + // 120 fps churns the heap and reintroduces the very stutter we're removing. + val spherePath = remember { Path() } + val smilePath = remember { Path() } + val facePaint = remember { Paint().apply { style = Paint.Style.FILL } } + val highlightPaint = remember { Paint().apply { style = Paint.Style.FILL } } + val secondaryHighlightPaint = remember { Paint().apply { style = Paint.Style.FILL } } + val shadowPaint = remember { Paint().apply { style = Paint.Style.FILL } } + val labelPaint = remember { Paint().apply { isAntiAlias = true } } Card( modifier = Modifier @@ -377,7 +374,7 @@ private fun HeadVisualization() { colors = CardDefaults.cardColors( containerColor = backgroundColor ), - shape = RoundedCornerShape(28.dp) + shape = MaterialTheme.shapes.large ) { Box( modifier = Modifier.fillMaxSize(), @@ -393,8 +390,8 @@ private fun HeadVisualization() { val center = Offset(width / 2, height / 2) val faceRadius = height * 0.35f - val pitch = Math.toRadians(orientation.pitch.toDouble()) - val yaw = Math.toRadians(orientation.yaw.toDouble()) + val pitch = Math.toRadians(animatedPitch.toDouble()) + val yaw = Math.toRadians(animatedYaw.toDouble()) val cosY = cos(yaw).toFloat() val sinY = sin(yaw).toFloat() @@ -436,7 +433,7 @@ private fun HeadVisualization() { ) } - val spherePath = Path() + spherePath.rewind() val firstPoint = project(rotate3D(Triple(faceRadius, 0f, 0f))) spherePath.moveTo(firstPoint.first, firstPoint.second) @@ -452,75 +449,63 @@ private fun HeadVisualization() { spherePath.close() drawContext.canvas.nativeCanvas.apply { - val paint = Paint().apply { - style = Paint.Style.FILL - shader = RadialGradient( - center.x + sinY * faceRadius * 0.3f, - center.y - sinP * faceRadius * 0.3f, - faceRadius * 1.4f, - intArrayOf( - backgroundColor.copy(alpha = 1f).toArgb(), - backgroundColor.copy(alpha = 0.95f).toArgb(), - backgroundColor.copy(alpha = 0.9f).toArgb(), - backgroundColor.copy(alpha = 0.8f).toArgb(), - backgroundColor.copy(alpha = 0.7f).toArgb() - ), - floatArrayOf(0.3f, 0.5f, 0.7f, 0.8f, 1f), - Shader.TileMode.CLAMP - ) - } - drawPath(spherePath.asAndroidPath(), paint) - - val highlightPaint = Paint().apply { - style = Paint.Style.FILL - shader = RadialGradient( - center.x - faceRadius * 0.4f - sinY * faceRadius * 0.5f, - center.y - faceRadius * 0.4f - sinP * faceRadius * 0.5f, - faceRadius * 0.9f, - intArrayOf( - android.graphics.Color.WHITE, - android.graphics.Color.argb(100, 255, 255, 255), - android.graphics.Color.TRANSPARENT - ), - floatArrayOf(0f, 0.3f, 1f), - Shader.TileMode.CLAMP - ) - alpha = if (darkTheme) 30 else 60 - } + facePaint.shader = RadialGradient( + center.x + sinY * faceRadius * 0.3f, + center.y - sinP * faceRadius * 0.3f, + faceRadius * 1.4f, + intArrayOf( + backgroundColor.copy(alpha = 1f).toArgb(), + backgroundColor.copy(alpha = 0.95f).toArgb(), + backgroundColor.copy(alpha = 0.9f).toArgb(), + backgroundColor.copy(alpha = 0.8f).toArgb(), + backgroundColor.copy(alpha = 0.7f).toArgb() + ), + floatArrayOf(0.3f, 0.5f, 0.7f, 0.8f, 1f), + Shader.TileMode.CLAMP + ) + drawPath(spherePath.asAndroidPath(), facePaint) + + highlightPaint.shader = RadialGradient( + center.x - faceRadius * 0.4f - sinY * faceRadius * 0.5f, + center.y - faceRadius * 0.4f - sinP * faceRadius * 0.5f, + faceRadius * 0.9f, + intArrayOf( + strokeColor.copy(alpha = 1f).toArgb(), + strokeColor.copy(alpha = 0.4f).toArgb(), + strokeColor.copy(alpha = 0f).toArgb() + ), + floatArrayOf(0f, 0.3f, 1f), + Shader.TileMode.CLAMP + ) + highlightPaint.alpha = 25 drawPath(spherePath.asAndroidPath(), highlightPaint) - val secondaryHighlightPaint = Paint().apply { - style = Paint.Style.FILL - shader = RadialGradient( - center.x + faceRadius * 0.3f + sinY * faceRadius * 0.3f, - center.y + faceRadius * 0.3f - sinP * faceRadius * 0.3f, - faceRadius * 0.7f, - intArrayOf( - android.graphics.Color.WHITE, - android.graphics.Color.TRANSPARENT - ), - floatArrayOf(0f, 1f), - Shader.TileMode.CLAMP - ) - alpha = if (darkTheme) 15 else 30 - } + secondaryHighlightPaint.shader = RadialGradient( + center.x + faceRadius * 0.3f + sinY * faceRadius * 0.3f, + center.y + faceRadius * 0.3f - sinP * faceRadius * 0.3f, + faceRadius * 0.7f, + intArrayOf( + strokeColor.copy(alpha = 1f).toArgb(), + strokeColor.copy(alpha = 0f).toArgb() + ), + floatArrayOf(0f, 1f), + Shader.TileMode.CLAMP + ) + secondaryHighlightPaint.alpha = 15 drawPath(spherePath.asAndroidPath(), secondaryHighlightPaint) - val shadowPaint = Paint().apply { - style = Paint.Style.FILL - shader = RadialGradient( - center.x + sinY * faceRadius * 0.5f, - center.y - sinP * faceRadius * 0.5f, - faceRadius * 1.1f, - intArrayOf( - android.graphics.Color.TRANSPARENT, - android.graphics.Color.BLACK - ), - floatArrayOf(0.7f, 1f), - Shader.TileMode.CLAMP - ) - alpha = if (darkTheme) 40 else 20 - } + shadowPaint.shader = RadialGradient( + center.x + sinY * faceRadius * 0.5f, + center.y - sinP * faceRadius * 0.5f, + faceRadius * 1.1f, + intArrayOf( + android.graphics.Color.TRANSPARENT, + android.graphics.Color.BLACK + ), + floatArrayOf(0.7f, 1f), + Shader.TileMode.CLAMP + ) + shadowPaint.alpha = 25 drawPath(spherePath.asAndroidPath(), shadowPaint) } @@ -535,7 +520,7 @@ private fun HeadVisualization() { val smileSweepAngle = 140f val smileOffsetY = faceRadius * 0.1f - val smilePath = Path() + smilePath.rewind() for (i in 0..32) { val angle = Math.toRadians(smileStartAngle + (smileSweepAngle * i / 32.0)) val x = cos(angle.toFloat()) * smileRadius @@ -577,25 +562,17 @@ private fun HeadVisualization() { } drawContext.canvas.nativeCanvas.apply { - val paint = Paint().apply { - color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK - textSize = 12.sp.toPx() - textAlign = Paint.Align.RIGHT - typeface = Typeface.create( - "SF Pro", - Typeface.NORMAL - ) - } + labelPaint.color = labelColor.toArgb() + labelPaint.textSize = 12.sp.toPx() + labelPaint.textAlign = Paint.Align.RIGHT - val pitch = orientation.pitch.toInt() - val yaw = orientation.yaw.toInt() - val text = "Pitch: ${pitch}° Yaw: ${yaw}°" + val text = "Pitch: ${animatedPitch.toInt()}° Yaw: ${animatedYaw.toInt()}°" drawText( text, width - 8.dp.toPx(), height - 8.dp.toPx(), - paint + labelPaint ) } } @@ -608,10 +585,16 @@ private fun AccelerationPlot() { val acceleration by HeadTracking.acceleration.collectAsState() val maxPoints = 100 val points = remember { mutableStateListOf>() } - val darkTheme = isSystemInDarkTheme() var maxAbs by remember { mutableFloatStateOf(1000f) } + val containerColor = MaterialTheme.colorScheme.surfaceContainer + val gridColor = MaterialTheme.colorScheme.outlineVariant + val axisColor = MaterialTheme.colorScheme.outline + val labelColor = MaterialTheme.colorScheme.onSurfaceVariant + val horizontalColor = MaterialTheme.colorScheme.primary + val verticalColor = MaterialTheme.colorScheme.error + LaunchedEffect(acceleration) { points.add(Pair(acceleration.horizontal, acceleration.vertical)) if (points.size > maxPoints) { @@ -627,9 +610,9 @@ private fun AccelerationPlot() { .fillMaxWidth() .height(300.dp), colors = CardDefaults.cardColors( - containerColor = if (darkTheme) Color(0xFF1C1C1E) else Color.White + containerColor = containerColor ), - shape = RoundedCornerShape(28.dp) + shape = MaterialTheme.shapes.large ) { Box( modifier = Modifier @@ -645,8 +628,6 @@ private fun AccelerationPlot() { val yScale = (height - 40.dp.toPx()) / (maxAbs * 2) val zeroY = height / 2 - val gridColor = if (darkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.1f) - for (i in 0..maxPoints step 10) { val x = i * xScale drawLine( @@ -669,7 +650,7 @@ private fun AccelerationPlot() { } drawLine( - color = if (darkTheme) Color.White.copy(alpha = 0.3f) else Color.Black.copy(alpha = 0.3f), + color = axisColor, start = Offset(0f, zeroY), end = Offset(width, zeroY), strokeWidth = 1.5f.dp.toPx() @@ -681,14 +662,14 @@ private fun AccelerationPlot() { val x2 = (i + 1) * xScale drawLine( - color = Color(0xFF007AFF), + color = horizontalColor, start = Offset(x1, zeroY - points[i].first * yScale), end = Offset(x2, zeroY - points[i + 1].first * yScale), strokeWidth = 2.dp.toPx() ) drawLine( - color = Color(0xFFFF3B30), + color = verticalColor, start = Offset(x1, zeroY - points[i].second * yScale), end = Offset(x2, zeroY - points[i + 1].second * yScale), strokeWidth = 2.dp.toPx() @@ -698,7 +679,7 @@ private fun AccelerationPlot() { drawContext.canvas.nativeCanvas.apply { val paint = Paint().apply { - color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK + color = labelColor.toArgb() textSize = 12.sp.toPx() textAlign = Paint.Align.RIGHT } @@ -711,20 +692,20 @@ private fun AccelerationPlot() { val legendY = 15.dp.toPx() val textOffsetY = legendY + 5.dp.toPx() / 2 - drawCircle(Color(0xFF007AFF), 5.dp.toPx(), Offset(width - 150.dp.toPx(), legendY)) + drawCircle(horizontalColor, 5.dp.toPx(), Offset(width - 150.dp.toPx(), legendY)) drawContext.canvas.nativeCanvas.apply { val paint = Paint().apply { - color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK + color = labelColor.toArgb() textSize = 12.sp.toPx() textAlign = Paint.Align.LEFT } drawText("Horizontal", width - 140.dp.toPx(), textOffsetY, paint) } - drawCircle(Color(0xFFFF3B30), 5.dp.toPx(), Offset(width - 70.dp.toPx(), legendY)) + drawCircle(verticalColor, 5.dp.toPx(), Offset(width - 70.dp.toPx(), legendY)) drawContext.canvas.nativeCanvas.apply { val paint = Paint().apply { - color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK + color = labelColor.toArgb() textSize = 12.sp.toPx() textAlign = Paint.Align.LEFT } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt index 108e19c0d..d3ebd9919 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt @@ -16,20 +16,26 @@ along with this program. If not, see . */ +@file:OptIn(ExperimentalMaterial3Api::class) + package me.kavishdevar.librepods.presentation.screens import android.annotation.SuppressLint import android.util.Log -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeMute +import androidx.compose.material.icons.automirrored.filled.VolumeUp import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -41,35 +47,27 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.hazeSource -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import kotlinx.coroutines.Job import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.AACPManager import me.kavishdevar.librepods.data.HearingAidSettings import me.kavishdevar.librepods.data.parseHearingAidSettingsResponse import me.kavishdevar.librepods.data.sendHearingAidSettings -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledSlider -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSlider +import me.kavishdevar.librepods.presentation.m3.ExpressiveSwitchRow import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import kotlin.io.encoding.ExperimentalEncodingApi private const val TAG = "HearingAidAdjustments" @SuppressLint("DefaultLocale") -@ExperimentalHazeMaterialsApi @OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class) @Composable fun HearingAidAdjustmentsScreen(viewModel: AirPodsViewModel) { - isSystemInDarkTheme() - val verticalScrollState = rememberScrollState() - val hazeState = remember { HazeState() } val state by viewModel.uiState.collectAsState() - val backdrop = rememberLayerBackdrop() val debounceJob = remember { mutableStateOf(null) } @@ -144,81 +142,134 @@ fun HearingAidAdjustmentsScreen(viewModel: AirPodsViewModel) { sendHearingAidSettings(state.hearingAidData, hearingAidSettings.value, debounceJob, viewModel::setATTCharacteristicValue) } - StyledScaffold(title = stringResource(R.string.adjustments)) { spacerHeight -> - Column( - modifier = Modifier - .hazeSource(hazeState) - .fillMaxSize() - .layerBackdrop(backdrop) - .verticalScroll(verticalScrollState) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + ExpressiveScreenScaffold(title = stringResource(R.string.adjustments)) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), ) { - Spacer(modifier = Modifier.height(spacerHeight)) - - StyledSlider( - label = stringResource(R.string.amplification), - valueRange = -1f..1f, - value = amplificationSliderValue.floatValue, - onValueChange = { - amplificationSliderValue.floatValue = it - }, - startIcon = "􀊥", - endIcon = "􀊩", - independent = true, - ) - - StyledToggle( - label = stringResource(R.string.swipe_to_control_amplification), - checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE]?.getOrNull(0) == 0x01.toByte(), - onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE, it) }, - description = stringResource(R.string.swipe_amplification_description) - ) - - StyledSlider( - label = stringResource(R.string.balance), - valueRange = -1f..1f, - value = balanceSliderValue.floatValue, - onValueChange = { - balanceSliderValue.floatValue = it - }, - snapPoints = listOf(-1f, 0f, 1f), - startLabel = stringResource(R.string.left), - endLabel = stringResource(R.string.right), - independent = true, - ) - - StyledSlider( - label = stringResource(R.string.tone), - valueRange = -1f..1f, - value = toneSliderValue.floatValue, - onValueChange = { - toneSliderValue.floatValue = it - }, - startLabel = stringResource(R.string.darker), - endLabel = stringResource(R.string.brighter), - independent = true, - ) - - StyledSlider( - label = stringResource(R.string.ambient_noise_reduction), - valueRange = 0f..1f, - value = ambientNoiseReductionSliderValue.floatValue, - onValueChange = { - ambientNoiseReductionSliderValue.floatValue = it - }, - startLabel = stringResource(R.string.less), - endLabel = stringResource(R.string.more), - independent = true, - ) - - StyledToggle( - label = stringResource(R.string.conversation_boost), - checked = conversationBoostEnabled.value, - onCheckedChange = { conversationBoostEnabled.value = it }, - independent = true, - description = stringResource(R.string.conversation_boost_description) - ) + item { + ExpressiveSectionHeader(stringResource(R.string.amplification)) + ExpressiveGroup { + SliderRow { + ExpressiveSlider( + value = amplificationSliderValue.floatValue, + onValueChange = { amplificationSliderValue.floatValue = it }, + valueRange = -1f..1f, + startIcon = Icons.AutoMirrored.Filled.VolumeMute, + endIcon = Icons.AutoMirrored.Filled.VolumeUp, + ) + } + } + } + + item { + ExpressiveGroup(modifier = Modifier.padding(top = 12.dp)) { + ExpressiveSwitchRow( + title = stringResource(R.string.swipe_to_control_amplification), + subtitle = stringResource(R.string.swipe_amplification_description), + checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE]?.getOrNull(0) == 0x01.toByte(), + onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE, it) }, + ) + } + } + + item { + ExpressiveSectionHeader(stringResource(R.string.balance)) + ExpressiveGroup { + SliderRow( + startLabel = stringResource(R.string.left), + endLabel = stringResource(R.string.right), + ) { + ExpressiveSlider( + value = balanceSliderValue.floatValue, + onValueChange = { balanceSliderValue.floatValue = it }, + valueRange = -1f..1f, + ) + } + } + } + + item { + ExpressiveSectionHeader(stringResource(R.string.tone)) + ExpressiveGroup { + SliderRow( + startLabel = stringResource(R.string.darker), + endLabel = stringResource(R.string.brighter), + ) { + ExpressiveSlider( + value = toneSliderValue.floatValue, + onValueChange = { toneSliderValue.floatValue = it }, + valueRange = -1f..1f, + ) + } + } + } + + item { + ExpressiveSectionHeader(stringResource(R.string.ambient_noise_reduction)) + ExpressiveGroup { + SliderRow( + startLabel = stringResource(R.string.less), + endLabel = stringResource(R.string.more), + ) { + ExpressiveSlider( + value = ambientNoiseReductionSliderValue.floatValue, + onValueChange = { ambientNoiseReductionSliderValue.floatValue = it }, + valueRange = 0f..1f, + ) + } + } + } + + item { + ExpressiveGroup(modifier = Modifier.padding(top = 12.dp)) { + ExpressiveSwitchRow( + title = stringResource(R.string.conversation_boost), + subtitle = stringResource(R.string.conversation_boost_description), + checked = conversationBoostEnabled.value, + onCheckedChange = { conversationBoostEnabled.value = it }, + ) + } + } + } + } +} + +/** A slider hosted inside an [ExpressiveGroup] with optional left/right captions beneath it. */ +@Composable +private fun SliderRow( + startLabel: String? = null, + endLabel: String? = null, + slider: @Composable () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + slider() + if (startLabel != null || endLabel != null) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = startLabel.orEmpty(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = endLabel.orEmpty(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt index 3cb72ce40..2aca15172 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt @@ -16,26 +16,27 @@ along with this program. If not, see . */ +@file:OptIn(ExperimentalMaterial3Api::class) + package me.kavishdevar.librepods.presentation.screens import android.annotation.SuppressLint import android.util.Log -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Hearing +import androidx.compose.material.icons.filled.MonitorHeart +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -43,21 +44,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color 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.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.hazeSource -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -65,27 +54,22 @@ import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.AACPManager import me.kavishdevar.librepods.data.parseTransparencySettingsResponse import me.kavishdevar.librepods.data.sendTransparencySettings -import me.kavishdevar.librepods.presentation.components.ConfirmationDialog -import me.kavishdevar.librepods.presentation.components.NavigationButton -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSwitchRow import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import kotlin.io.encoding.ExperimentalEncodingApi private const val TAG = "AccessibilitySettings" @SuppressLint("DefaultLocale") -@ExperimentalHazeMaterialsApi -@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class) +@OptIn(ExperimentalEncodingApi::class) @Composable fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController) { - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val verticalScrollState = rememberScrollState() - val snackbarHostState = remember { SnackbarHostState() } - val showDialog = remember { mutableStateOf(false) } - val backdrop = rememberLayerBackdrop() val initialLoad = remember { mutableStateOf(true) } val state by viewModel.uiState.collectAsState() @@ -96,185 +80,117 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController) mutableStateOf((aidStatus?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.getOrNull(0) == 0x01.toByte())) } - val hazeStateS = remember { mutableStateOf(HazeState()) } // dont question this. i could possibly use something other than initializing it with an empty state and then replacing it with the the one provided by the scaffold + LaunchedEffect(hearingAidEnabled.value) { + if (hearingAidEnabled.value && !initialLoad.value) { + showDialog.value = true + } else if (!hearingAidEnabled.value && !initialLoad.value) { + viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, byteArrayOf(0x01, 0x02)) + viewModel.setControlCommandByte(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, 0x02.toByte()) + hearingAidEnabled.value = false + } + initialLoad.value = false + } - StyledScaffold( + ExpressiveScreenScaffold( title = stringResource(R.string.hearing_aid), - snackbarHostState = snackbarHostState, - ) { topPadding, hazeState, bottomPadding -> - Column( - modifier = Modifier - .layerBackdrop(backdrop) - .hazeSource(hazeState) - .fillMaxSize() - .verticalScroll(verticalScrollState) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + onBack = { navController.popBackStack() }, + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), ) { - hazeStateS.value = hazeState - Spacer(modifier = Modifier.height(topPadding)) - -// val mediaAssistEnabled = remember { mutableStateOf(false) } -// val adjustMediaEnabled = remember { mutableStateOf(false) } -// val adjustPhoneEnabled = remember { mutableStateOf(false) } - - LaunchedEffect(hearingAidEnabled.value) { - if (hearingAidEnabled.value && !initialLoad.value) { - showDialog.value = true - } else if (!hearingAidEnabled.value && !initialLoad.value) { - viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, byteArrayOf(0x01, 0x02)) - viewModel.setControlCommandByte(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, 0x02.toByte()) - hearingAidEnabled.value = false + item { + ExpressiveSectionHeader(stringResource(R.string.hearing_aid)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.hearing_aid), + leadingIcon = Icons.Filled.Hearing, + checked = hearingAidEnabled.value, + onCheckedChange = { hearingAidEnabled.value = it }, + ) + ExpressiveDivider() + ExpressiveListItem( + title = stringResource(R.string.adjustments), + leadingIcon = Icons.Filled.Tune, + showChevron = true, + onClick = { navController.navigate("hearing_aid_adjustments") }, + ) } - initialLoad.value = false + Text( + text = stringResource(R.string.hearing_aid_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp), + ) } -// fun onAdjustPhoneChange(value: Boolean) { -// // TODO -// } - -// fun onAdjustMediaChange(value: Boolean) { -// // TODO -// } - - Text( - text = stringResource(R.string.hearing_aid), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier.padding(16.dp, bottom = 2.dp) - ) - - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - - Column( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .clip( - RoundedCornerShape(28.dp) + item { + Spacer(Modifier.height(8.dp)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.update_hearing_test), + leadingIcon = Icons.Filled.MonitorHeart, + showChevron = true, + onClick = { navController.navigate("update_hearing_test") }, ) - ) { - StyledToggle( - label = stringResource(R.string.hearing_aid), - checked = hearingAidEnabled.value, - onCheckedChange = { hearingAidEnabled.value = it }, - independent = false - ) - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier - .padding(horizontal = 12.dp) - ) - NavigationButton( - to = "hearing_aid_adjustments", - name = stringResource(R.string.adjustments), - navController = navController, - independent = false - ) + } } - Text( - text = stringResource(R.string.hearing_aid_description), - style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Light, - color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier.padding(horizontal = 16.dp) - ) - Spacer(modifier = Modifier.height(16.dp)) - - NavigationButton( - to = "update_hearing_test", - name = stringResource(R.string.update_hearing_test), - navController = navController, - independent = true - ) - - // not implemented yet - - // StyledToggle( - // title = stringResource(R.string.media_assist), - // label = stringResource(R.string.media_assist), - // checkedState = mediaAssistEnabled, - // independent = true, - // description = stringResource(R.string.media_assist_description) - // ) - - // Spacer(modifier = Modifier.height(8.dp)) - - // Column ( - // modifier = Modifier - // .fillMaxWidth() - // .background(backgroundColor, RoundedCornerShape(28.dp)) - // ) { - // StyledToggle( - // label = stringResource(R.string.adjust_media), - // checkedState = adjustMediaEnabled, - // onCheckedChange = { onAdjustMediaChange(it) }, - // independent = false - // ) - // HorizontalDivider( - // thickness = 1.dp, - // color = Color(0x40888888), - // modifier = Modifier - // .padding(horizontal = 12.dp) - // ) - - // StyledToggle( - // label = stringResource(R.string.adjust_calls), - // checkedState = adjustPhoneEnabled, - // onCheckedChange = { onAdjustPhoneChange(it) }, - // independent = false - // ) - // } - Spacer(modifier = Modifier.height(bottomPadding)) } } - ConfirmationDialog( - showDialog = showDialog, - title = "Enable Hearing Aid", - message = "Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.", - confirmText = "Enable", - dismissText = "Cancel", - onConfirm = { - showDialog.value = false - val enrolled = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(0) == 0x01.toByte() - if (!enrolled) { - viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, byteArrayOf(0x01, 0x01)) - } else { - viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, byteArrayOf(0x01, 0x01)) - } - viewModel.setControlCommandByte(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, 0x01.toByte()) - hearingAidEnabled.value = true - CoroutineScope(Dispatchers.IO).launch { - try { - if (state.hearingAidData.isEmpty()) { - Log.w(TAG, "read failed") - return@launch + + if (showDialog.value) { + AlertDialog( + onDismissRequest = { + hearingAidEnabled.value = false + showDialog.value = false + }, + title = { Text("Enable Hearing Aid") }, + text = { Text("Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.") }, + confirmButton = { + TextButton(onClick = { + showDialog.value = false + val enrolled = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(0) == 0x01.toByte() + if (!enrolled) { + viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, byteArrayOf(0x01, 0x01)) + } else { + viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, byteArrayOf(0x01, 0x01)) } - val parsed = parseTransparencySettingsResponse(state.hearingAidData) - if (parsed == null) { - Log.w(TAG, "transparency parse failed") - return@launch + viewModel.setControlCommandByte(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, 0x01.toByte()) + hearingAidEnabled.value = true + CoroutineScope(Dispatchers.IO).launch { + try { + if (state.hearingAidData.isEmpty()) { + Log.w(TAG, "read failed") + return@launch + } + val parsed = parseTransparencySettingsResponse(state.hearingAidData) + if (parsed == null) { + Log.w(TAG, "transparency parse failed") + return@launch + } + val disabledSettings = parsed.copy(enabled = false) + sendTransparencySettings(viewModel::setATTCharacteristicValue, disabledSettings) + } catch (e: Exception) { + Log.e(TAG, "Error disabling transparency: ${e.message}") + } } - val disabledSettings = parsed.copy(enabled = false) - sendTransparencySettings(viewModel::setATTCharacteristicValue, disabledSettings) - } catch (e: Exception) { - Log.e(TAG, "Error disabling transparency: ${e.message}") + }) { + Text("Enable") } - } - }, - onDismiss = { - hearingAidEnabled.value = false - showDialog.value = false - }, -// hazeState = hazeStateS.value, - backdrop = backdrop - ) + }, + dismissButton = { + TextButton(onClick = { + hearingAidEnabled.value = false + showDialog.value = false + }) { + Text("Cancel") + } + }, + ) + } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt index 42b04660b..b048dc04a 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt @@ -18,82 +18,86 @@ package me.kavishdevar.librepods.presentation.screens -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues 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.material3.Text +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HearingDisabled +import androidx.compose.material.icons.filled.VolumeOff import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color 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.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.AACPManager import me.kavishdevar.librepods.bluetooth.ATTHandles -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.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSwitchRow import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel @Composable fun HearingProtectionScreen(viewModel: AirPodsViewModel, navController: NavController) { - val backdrop = rememberLayerBackdrop() val state by viewModel.uiState.collectAsState() - StyledScaffold( + + val ppeChecked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG] + ?.getOrNull(0)?.toInt() == 1 + + ExpressiveScreenScaffold( title = stringResource(R.string.hearing_protection), - ) { spacerHeight -> - Column( - modifier = Modifier - .fillMaxSize() - .layerBackdrop(backdrop) - .padding(horizontal = 16.dp) + onBack = { navController.popBackStack() }, + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), ) { - Spacer(modifier = Modifier.height(spacerHeight)) if (state.vendorIdHook) { - StyledToggle( - title = stringResource(R.string.environmental_noise), - label = stringResource(R.string.loud_sound_reduction), - description = stringResource(R.string.loud_sound_reduction_description), - checked = state.loudSoundReductionEnabled, - onCheckedChange = { - viewModel.setATTCharacteristicValue( - ATTHandles.LOUD_SOUND_REDUCTION, - byteArrayOf(if (it) 1.toByte() else 0.toByte()) + item(key = "environmental_noise") { + ExpressiveSectionHeader(stringResource(R.string.environmental_noise)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.loud_sound_reduction), + subtitle = stringResource(R.string.loud_sound_reduction_description), + leadingIcon = Icons.Filled.VolumeOff, + checked = state.loudSoundReductionEnabled, + onCheckedChange = { + viewModel.setATTCharacteristicValue( + ATTHandles.LOUD_SOUND_REDUCTION, + byteArrayOf(if (it) 1.toByte() else 0.toByte()) + ) + }, + enabled = state.isPremium, ) - }, - enabled = state.isPremium - ) - - Spacer(modifier = Modifier.height(12.dp)) + } + } } - StyledToggle( - title = stringResource(R.string.workspace_use), - label = stringResource(R.string.ppe), - description = stringResource(R.string.workspace_use_description), - checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG]?.getOrNull( - 0 - )?.toInt() == 1, - onCheckedChange = { - viewModel.setControlCommandBoolean( - AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG, it + + item(key = "workspace_use") { + ExpressiveSectionHeader(stringResource(R.string.workspace_use)) + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.ppe), + subtitle = stringResource(R.string.workspace_use_description), + leadingIcon = Icons.Filled.HearingDisabled, + checked = ppeChecked, + onCheckedChange = { + viewModel.setControlCommandBoolean( + AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG, it + ) + }, + enabled = state.isPremium, ) - }, - enabled = state.isPremium - ) + } + } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt index 8f57cdb56..9f47aabfc 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt @@ -16,15 +16,12 @@ along with this program. If not, see . */ +@file:OptIn(ExperimentalMaterial3Api::class) + package me.kavishdevar.librepods.presentation.screens -import android.annotation.SuppressLint -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable @@ -34,49 +31,35 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer import com.mikepenz.aboutlibraries.ui.compose.produceLibraries -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import kotlinx.coroutines.Job import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import kotlin.io.encoding.ExperimentalEncodingApi - -private var debounceJob: Job? = null +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold -@SuppressLint("DefaultLocale") -@ExperimentalHazeMaterialsApi -@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class) @Composable fun OpenSourceLicensesScreen(navController: NavController) { - val isDarkTheme = isSystemInDarkTheme() - val backdrop = rememberLayerBackdrop() + val context = LocalContext.current + val libraries by produceLibraries { + context.resources.openRawResource(R.raw.aboutlibraries) + .bufferedReader() + .use { it.readText() } + } - StyledScaffold( - title = stringResource(R.string.open_source_licenses) - ) { spacerHeight -> - Column( - modifier = Modifier - .fillMaxSize() - .layerBackdrop(backdrop) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Spacer(modifier = Modifier.height(spacerHeight)) - val context = LocalContext.current - val libraries by produceLibraries { - context.resources.openRawResource(R.raw.aboutlibraries) - .bufferedReader() - .use { it.readText() } - } - LibrariesContainer( - libraries = libraries, - modifier = Modifier - .padding(0.dp) - .fillMaxSize() - ) - } + ExpressiveScreenScaffold( + title = stringResource(R.string.open_source_licenses), + onBack = { navController.popBackStack() }, + ) { padding -> + // LibrariesContainer scrolls internally, so apply the scaffold top inset as content + // padding and reserve bottom space so the last row clears the floating nav bar. + LibrariesContainer( + libraries = libraries, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), + ) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt index c09df1206..d291405bf 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt @@ -16,43 +16,46 @@ along with this program. If not, see . */ -@file:OptIn(ExperimentalStdlibApi::class, ExperimentalEncodingApi::class) +@file:OptIn(ExperimentalStdlibApi::class, ExperimentalEncodingApi::class, ExperimentalMaterial3Api::class) package me.kavishdevar.librepods.presentation.screens import android.util.Log -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer 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.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.GraphicEq +import androidx.compose.material.icons.filled.Hearing +import androidx.compose.material.icons.filled.HearingDisabled +import androidx.compose.material.icons.filled.NoiseAware +import androidx.compose.material.icons.filled.RecordVoiceOver import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector 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.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.AACPManager import me.kavishdevar.librepods.data.StemAction -import me.kavishdevar.librepods.presentation.components.SelectItem -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledSelectList +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSwitchRow import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import kotlin.experimental.and import kotlin.io.encoding.ExperimentalEncodingApi @@ -61,9 +64,6 @@ import kotlin.io.encoding.ExperimentalEncodingApi @OptIn(ExperimentalMaterial3Api::class) @Composable fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavController) { - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val state by viewModel.uiState.collectAsState() val modesByte = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]?.get(0) ?: 0 @@ -75,113 +75,131 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr Log.d("PressAndHoldSettingsScreen", "Adaptive mode: ${(modesByte and 0x08) != 0.toByte()}") val longPressAction = if (name.lowercase() == "left") state.leftAction else state.rightAction - val backdrop = rememberLayerBackdrop() - StyledScaffold( - title = name - ) { spacerHeight -> - Column ( - modifier = Modifier - .layerBackdrop(backdrop) - .fillMaxSize() - .padding(top = 8.dp) - .padding(horizontal = 16.dp) + val cycleSelected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES + + ExpressiveScreenScaffold( + title = name, + onBack = { navController.popBackStack() }, + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), ) { - Spacer(modifier = Modifier.height(spacerHeight)) - val actionItems = listOf( - SelectItem( - name = stringResource(R.string.noise_control), - selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES, - onClick = { - viewModel.setLongPressAction(name, StemAction.CYCLE_NOISE_CONTROL_MODES) - } - ), - SelectItem( - name = stringResource(R.string.digital_assistant), - selected = longPressAction == StemAction.DIGITAL_ASSISTANT, - onClick = { - viewModel.setLongPressAction(name, StemAction.DIGITAL_ASSISTANT) - }, - enabled = state.isPremium - ) - ) - StyledSelectList(items = actionItems) - - if (longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES) { - Spacer(modifier = Modifier.height(32.dp)) - Text( - text = stringResource(R.string.noise_control), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - fontFamily = FontFamily(Font(R.font.sf_pro)), - modifier = Modifier - .padding(horizontal = 18.dp) - ) - - Spacer(modifier = Modifier.height(8.dp)) + item { + ExpressiveSectionHeader("Press & hold") + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.noise_control), + leadingIcon = Icons.Filled.NoiseAware, + trailing = if (cycleSelected) { + { SelectedCheck() } + } else null, + onClick = { + viewModel.setLongPressAction(name, StemAction.CYCLE_NOISE_CONTROL_MODES) + }, + ) + ExpressiveDivider() + ExpressiveListItem( + title = stringResource(R.string.digital_assistant), + leadingIcon = Icons.Filled.RecordVoiceOver, + enabled = state.isPremium, + trailing = if (longPressAction == StemAction.DIGITAL_ASSISTANT) { + { SelectedCheck() } + } else null, + onClick = { + viewModel.setLongPressAction(name, StemAction.DIGITAL_ASSISTANT) + }, + ) + } + } + if (cycleSelected) { val currentByte = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]?.get(0)?.toInt() ?: 0 - val listeningModeItems = mutableListOf() - if (state.offListeningMode) { - listeningModeItems.add( - SelectItem( - name = stringResource(R.string.off), - description = stringResource(R.string.listening_mode_off_description), - iconRes = R.drawable.noise_cancellation, - selected = (currentByte and 0x01) != 0, - onClick = { - viewModel.toggleListeningMode(0x01) - } + item { + ExpressiveSectionHeader(stringResource(R.string.noise_control)) + ExpressiveGroup { + var first = true + + if (state.offListeningMode) { + ListeningModeToggle( + title = stringResource(R.string.off), + subtitle = stringResource(R.string.listening_mode_off_description), + icon = Icons.Filled.HearingDisabled, + checked = (currentByte and 0x01) != 0, + onCheckedChange = { viewModel.toggleListeningMode(0x01) }, + ) + first = false + } + + if (!first) ExpressiveDivider() + ListeningModeToggle( + title = stringResource(R.string.transparency), + subtitle = stringResource(R.string.listening_mode_transparency_description), + icon = Icons.Filled.Hearing, + checked = (currentByte and 0x04) != 0, + onCheckedChange = { viewModel.toggleListeningMode(0x04) }, + ) + + ExpressiveDivider() + ListeningModeToggle( + title = stringResource(R.string.adaptive), + subtitle = stringResource(R.string.listening_mode_adaptive_description), + icon = Icons.Filled.GraphicEq, + checked = (currentByte and 0x08) != 0, + onCheckedChange = { viewModel.toggleListeningMode(0x08) }, + ) + + ExpressiveDivider() + ListeningModeToggle( + title = stringResource(R.string.noise_cancellation), + subtitle = stringResource(R.string.listening_mode_noise_cancellation_description), + icon = Icons.Filled.NoiseAware, + checked = (currentByte and 0x02) != 0, + onCheckedChange = { viewModel.toggleListeningMode(0x02) }, ) + } + + Spacer(Modifier.height(12.dp)) + Text( + text = stringResource(R.string.press_and_hold_noise_control_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 12.dp), ) } - listeningModeItems.addAll(listOf( - SelectItem( - name = stringResource(R.string.transparency), - description = stringResource(R.string.listening_mode_transparency_description), - iconRes = R.drawable.transparency, - selected = (currentByte and 0x04) != 0, - onClick = { - viewModel.toggleListeningMode(0x04) - } - ), - SelectItem( - name = stringResource(R.string.adaptive), - description = stringResource(R.string.listening_mode_adaptive_description), - iconRes = R.drawable.adaptive, - selected = (currentByte and 0x08) != 0, - onClick = { - viewModel.toggleListeningMode(0x08) - } - ), - SelectItem( - name = stringResource(R.string.noise_cancellation), - description = stringResource(R.string.listening_mode_noise_cancellation_description), - iconRes = R.drawable.noise_cancellation, - selected = (currentByte and 0x02) != 0, - onClick = { - viewModel.toggleListeningMode(0x02) - } - ) - )) - StyledSelectList(items = listeningModeItems) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(R.string.press_and_hold_noise_control_description), - style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Light, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier - .padding(horizontal = 18.dp) - ) } } } } + +@Composable +private fun SelectedCheck() { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) +} + +@Composable +private fun ListeningModeToggle( + title: String, + subtitle: String, + icon: ImageVector, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + ExpressiveSwitchRow( + title = title, + subtitle = subtitle, + leadingIcon = icon, + checked = checked, + onCheckedChange = onCheckedChange, + ) +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt index d06177b6d..815db9d7a 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt @@ -16,37 +16,43 @@ along with this program. If not, see . */ -@file:OptIn(ExperimentalEncodingApi::class) +@file:OptIn(ExperimentalEncodingApi::class, ExperimentalMaterial3Api::class) package me.kavishdevar.librepods.presentation.screens import android.content.Context -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DriveFileRenameOutline import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember 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.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.content.edit -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledInputField -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import kotlin.io.encoding.ExperimentalEncodingApi -@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RenameScreen(viewModel: AirPodsViewModel) { val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) @@ -58,28 +64,42 @@ fun RenameScreen(viewModel: AirPodsViewModel) { keyboardController?.show() } - StyledScaffold( - title = stringResource(R.string.name), - ) { spacerHeight -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) - ) { - Spacer(modifier = Modifier.height(spacerHeight)) + val name = sharedPreferences.getString("name", "") ?: "" + val textFieldState = rememberTextFieldState(initialText = name) - val name = sharedPreferences.getString("name", "")?: "" - val textFieldState = rememberTextFieldState(initialText = name) - - LaunchedEffect(textFieldState.text) { - sharedPreferences.edit {putString("name", textFieldState.text as String?)} - viewModel.setName(textFieldState.text.toString()) - } + LaunchedEffect(textFieldState.text) { + sharedPreferences.edit { putString("name", textFieldState.text as String?) } + viewModel.setName(textFieldState.text.toString()) + } - StyledInputField( - textFieldState, - focusRequester - ) + ExpressiveScreenScaffold(title = stringResource(R.string.name)) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), + ) { + item { + ExpressiveSectionHeader(stringResource(R.string.name)) + OutlinedTextField( + state = textFieldState, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + label = { Text(stringResource(R.string.name)) }, + leadingIcon = { + Icon( + imageVector = Icons.Filled.DriveFileRenameOutline, + contentDescription = null, + ) + }, + lineLimits = TextFieldLineLimits.SingleLine, + textStyle = MaterialTheme.typography.bodyLarge, + ) + } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TransparencySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TransparencySettingsScreen.kt index 526771212..cf60005eb 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TransparencySettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TransparencySettingsScreen.kt @@ -21,25 +21,23 @@ package me.kavishdevar.librepods.presentation.screens // import me.kavishdevar.librepods.utils.RadareOffsetFinder import android.annotation.SuppressLint import android.util.Log -import androidx.compose.foundation.background -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.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer 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.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeMute +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.Equalizer +import androidx.compose.material.icons.filled.GraphicEq +import androidx.compose.material.icons.filled.Hearing +import androidx.compose.material.icons.filled.RecordVoiceOver +import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Slider -import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -50,254 +48,267 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color 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.unit.dp -import androidx.compose.ui.unit.sp -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import dev.chrisbanes.haze.hazeSource -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import me.kavishdevar.librepods.R import me.kavishdevar.librepods.data.TransparencySettings import me.kavishdevar.librepods.data.parseTransparencySettingsResponse import me.kavishdevar.librepods.data.sendTransparencySettings -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledSlider -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader +import me.kavishdevar.librepods.presentation.m3.ExpressiveSlider +import me.kavishdevar.librepods.presentation.m3.ExpressiveSwitchRow import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import kotlin.io.encoding.ExperimentalEncodingApi private const val TAG = "TransparencySettings" +/** A small left/right caption pair shown under a slider, replacing the legacy slider's startLabel/endLabel. */ +@Composable +private fun SliderEndLabels(start: String, end: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = start, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = end, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + @SuppressLint("DefaultLocale") -@ExperimentalHazeMaterialsApi @OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class) @Composable fun TransparencySettingsScreen(viewModel: AirPodsViewModel) { - val isDarkTheme = isSystemInDarkTheme() - val textColor = if (isDarkTheme) Color.White else Color.Black - val verticalScrollState = rememberScrollState() - - val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491) - val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5) - val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF) - - val backdrop = rememberLayerBackdrop() - - val state by viewModel.uiState.collectAsState() - StyledScaffold( - title = stringResource(R.string.customize_transparency_mode) - ){ topPadding, hazeState, bottomPadding -> - Column( - modifier = Modifier - .hazeSource(hazeState) - .layerBackdrop(backdrop) - .fillMaxSize() - .verticalScroll(verticalScrollState) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Spacer(modifier = Modifier.height(topPadding)) - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - - val enabled = rememberSaveable { mutableStateOf(false) } - val amplificationSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) } - val balanceSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) } - val toneSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) } - val ambientNoiseReductionSliderValue = rememberSaveable { mutableFloatStateOf(0.0f) } - val conversationBoostEnabled = rememberSaveable { mutableStateOf(false) } - val eq = rememberSaveable( - saver = Saver( - save = { it.value.toList() }, - restore = { mutableStateOf(it.toFloatArray()) } - ) - ) { mutableStateOf(FloatArray(8)) } - val phoneMediaEQ = rememberSaveable( - saver = Saver( - save = { it.value.toList() }, - restore = { mutableStateOf(it.toFloatArray()) } - ) - ) { mutableStateOf(FloatArray(8) { 0.5f }) } + val enabled = rememberSaveable { mutableStateOf(false) } + val amplificationSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) } + val balanceSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) } + val toneSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) } + val ambientNoiseReductionSliderValue = rememberSaveable { mutableFloatStateOf(0.0f) } + val conversationBoostEnabled = rememberSaveable { mutableStateOf(false) } + val eq = rememberSaveable( + saver = Saver( + save = { it.value.toList() }, + restore = { mutableStateOf(it.toFloatArray()) } + ) + ) { mutableStateOf(FloatArray(8)) } + val phoneMediaEQ = rememberSaveable( + saver = Saver( + save = { it.value.toList() }, + restore = { mutableStateOf(it.toFloatArray()) } + ) + ) { mutableStateOf(FloatArray(8) { 0.5f }) } - val initialized = rememberSaveable { mutableStateOf(false) } + val initialized = rememberSaveable { mutableStateOf(false) } - val transparencySettings = remember { - mutableStateOf( - TransparencySettings( - enabled = enabled.value, - leftEQ = eq.value, - rightEQ = eq.value, - leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2, - rightAmplification = amplificationSliderValue.floatValue + (balanceSliderValue.floatValue - 0.5f) * amplificationSliderValue.floatValue * 2, - leftTone = toneSliderValue.floatValue, - rightTone = toneSliderValue.floatValue, - leftConversationBoost = conversationBoostEnabled.value, - rightConversationBoost = conversationBoostEnabled.value, - leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, - rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, - netAmplification = amplificationSliderValue.floatValue, - balance = balanceSliderValue.floatValue - ) - ) - } + val transparencySettings = remember { + mutableStateOf( + TransparencySettings( + enabled = enabled.value, + leftEQ = eq.value, + rightEQ = eq.value, + leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2, + rightAmplification = amplificationSliderValue.floatValue + (balanceSliderValue.floatValue - 0.5f) * amplificationSliderValue.floatValue * 2, + leftTone = toneSliderValue.floatValue, + rightTone = toneSliderValue.floatValue, + leftConversationBoost = conversationBoostEnabled.value, + rightConversationBoost = conversationBoostEnabled.value, + leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, + rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, + netAmplification = amplificationSliderValue.floatValue, + balance = balanceSliderValue.floatValue + ) + ) + } - LaunchedEffect( - enabled.value, - amplificationSliderValue.floatValue, - balanceSliderValue.floatValue, - toneSliderValue.floatValue, - conversationBoostEnabled.value, - ambientNoiseReductionSliderValue.floatValue, - eq.value - ) { - if (!initialized.value) return@LaunchedEffect - transparencySettings.value = TransparencySettings( - enabled = enabled.value, - leftEQ = eq.value, - rightEQ = eq.value, - leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f, - rightAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue > 0) balanceSliderValue.floatValue else 0f, - leftTone = toneSliderValue.floatValue, - rightTone = toneSliderValue.floatValue, - leftConversationBoost = conversationBoostEnabled.value, - rightConversationBoost = conversationBoostEnabled.value, - leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, - rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, - netAmplification = amplificationSliderValue.floatValue, - balance = balanceSliderValue.floatValue - ) - Log.d("TransparencySettings", "Updated settings: ${transparencySettings.value}") - sendTransparencySettings(viewModel::setATTCharacteristicValue, transparencySettings.value) - } + LaunchedEffect( + enabled.value, + amplificationSliderValue.floatValue, + balanceSliderValue.floatValue, + toneSliderValue.floatValue, + conversationBoostEnabled.value, + ambientNoiseReductionSliderValue.floatValue, + eq.value + ) { + if (!initialized.value) return@LaunchedEffect + transparencySettings.value = TransparencySettings( + enabled = enabled.value, + leftEQ = eq.value, + rightEQ = eq.value, + leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f, + rightAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue > 0) balanceSliderValue.floatValue else 0f, + leftTone = toneSliderValue.floatValue, + rightTone = toneSliderValue.floatValue, + leftConversationBoost = conversationBoostEnabled.value, + rightConversationBoost = conversationBoostEnabled.value, + leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, + rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, + netAmplification = amplificationSliderValue.floatValue, + balance = balanceSliderValue.floatValue + ) + Log.d("TransparencySettings", "Updated settings: ${transparencySettings.value}") + sendTransparencySettings(viewModel::setATTCharacteristicValue, transparencySettings.value) + } - LaunchedEffect(state.transparencyData) { - val parsedSettings = parseTransparencySettingsResponse(data = state.transparencyData) ?: return@LaunchedEffect - Log.d(TAG, "Initial transparency settings: $parsedSettings") - enabled.value = parsedSettings.enabled - amplificationSliderValue.floatValue = parsedSettings.netAmplification - balanceSliderValue.floatValue = parsedSettings.balance - toneSliderValue.floatValue = parsedSettings.leftTone - ambientNoiseReductionSliderValue.floatValue = - parsedSettings.leftAmbientNoiseReduction - conversationBoostEnabled.value = parsedSettings.leftConversationBoost - if (!eq.value.contentEquals(parsedSettings.leftEQ)) { - eq.value = parsedSettings.leftEQ.copyOf() - } - initialized.value = true - } + LaunchedEffect(state.transparencyData) { + val parsedSettings = parseTransparencySettingsResponse(data = state.transparencyData) ?: return@LaunchedEffect + Log.d(TAG, "Initial transparency settings: $parsedSettings") + enabled.value = parsedSettings.enabled + amplificationSliderValue.floatValue = parsedSettings.netAmplification + balanceSliderValue.floatValue = parsedSettings.balance + toneSliderValue.floatValue = parsedSettings.leftTone + ambientNoiseReductionSliderValue.floatValue = + parsedSettings.leftAmbientNoiseReduction + conversationBoostEnabled.value = parsedSettings.leftConversationBoost + if (!eq.value.contentEquals(parsedSettings.leftEQ)) { + eq.value = parsedSettings.leftEQ.copyOf() + } + initialized.value = true + } + ExpressiveScreenScaffold( + title = stringResource(R.string.customize_transparency_mode), + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), + ) { if (state.vendorIdHook) { - StyledToggle( - label = stringResource(R.string.transparency_mode), - checked = enabled.value, - independent = true, - description = stringResource(R.string.customize_transparency_mode_description), - onCheckedChange = { enabled.value = it } - ) - Spacer(modifier = Modifier.height(4.dp)) - StyledSlider( - label = stringResource(R.string.amplification), - valueRange = -1f..1f, - value = amplificationSliderValue.floatValue, - onValueChange = { - amplificationSliderValue.floatValue = it - }, - startIcon = "􀊥", - endIcon = "􀊩", - independent = true - ) - - StyledSlider( - label = stringResource(R.string.balance), - valueRange = -1f..1f, - value = balanceSliderValue.floatValue, - onValueChange = { - balanceSliderValue.floatValue = it - }, - snapPoints = listOf(-1f, 0f, 1f), - startLabel = stringResource(R.string.left), - endLabel = stringResource(R.string.right), - independent = true, - ) + // --- Master toggle --- + item(key = "toggle") { + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.transparency_mode), + subtitle = stringResource(R.string.customize_transparency_mode_description), + leadingIcon = Icons.Filled.Hearing, + checked = enabled.value, + onCheckedChange = { enabled.value = it }, + ) + } + } - StyledSlider( - label = stringResource(R.string.tone), - valueRange = -1f..1f, - value = toneSliderValue.floatValue, - onValueChange = { - toneSliderValue.floatValue = it - }, - startLabel = stringResource(R.string.darker), - endLabel = stringResource(R.string.brighter), - independent = true, - ) + // --- Amplification & balance --- + item(key = "amplification") { + ExpressiveSectionHeader(stringResource(R.string.amplification)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.amplification), + leadingIcon = Icons.AutoMirrored.Filled.VolumeUp, + ) + ExpressiveSlider( + value = amplificationSliderValue.floatValue, + onValueChange = { amplificationSliderValue.floatValue = it }, + valueRange = -1f..1f, + startIcon = Icons.AutoMirrored.Filled.VolumeMute, + endIcon = Icons.AutoMirrored.Filled.VolumeUp, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + ExpressiveDivider(startInset = false) + ExpressiveListItem( + title = stringResource(R.string.balance), + leadingIcon = Icons.Filled.Tune, + ) + ExpressiveSlider( + value = balanceSliderValue.floatValue, + onValueChange = { balanceSliderValue.floatValue = it }, + valueRange = -1f..1f, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + SliderEndLabels( + start = stringResource(R.string.left), + end = stringResource(R.string.right), + ) + } + } - StyledSlider( - label = stringResource(R.string.ambient_noise_reduction), - valueRange = 0f..1f, - value = ambientNoiseReductionSliderValue.floatValue, - onValueChange = { - ambientNoiseReductionSliderValue.floatValue = it - }, - startLabel = stringResource(R.string.less), - endLabel = stringResource(R.string.more), - independent = true, - ) + // --- Tone --- + item(key = "tone") { + ExpressiveSectionHeader(stringResource(R.string.tone)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.tone), + leadingIcon = Icons.Filled.Tune, + ) + ExpressiveSlider( + value = toneSliderValue.floatValue, + onValueChange = { toneSliderValue.floatValue = it }, + valueRange = -1f..1f, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + SliderEndLabels( + start = stringResource(R.string.darker), + end = stringResource(R.string.brighter), + ) + } + } - StyledToggle( - label = stringResource(R.string.conversation_boost), - checked = conversationBoostEnabled.value, - independent = true, - description = stringResource(R.string.conversation_boost_description), - onCheckedChange = { conversationBoostEnabled.value = it } - ) + // --- Ambient noise reduction --- + item(key = "anr") { + ExpressiveSectionHeader(stringResource(R.string.ambient_noise_reduction)) + ExpressiveGroup { + ExpressiveListItem( + title = stringResource(R.string.ambient_noise_reduction), + leadingIcon = Icons.Filled.GraphicEq, + ) + ExpressiveSlider( + value = ambientNoiseReductionSliderValue.floatValue, + onValueChange = { ambientNoiseReductionSliderValue.floatValue = it }, + valueRange = 0f..1f, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + SliderEndLabels( + start = stringResource(R.string.less), + end = stringResource(R.string.more), + ) + } + } - Text( - text = stringResource(R.string.equalizer), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - modifier = Modifier.padding(16.dp, bottom = 4.dp) - ) + // --- Conversation boost --- + item(key = "conversation_boost") { + ExpressiveGroup { + ExpressiveSwitchRow( + title = stringResource(R.string.conversation_boost), + subtitle = stringResource(R.string.conversation_boost_description), + leadingIcon = Icons.Filled.RecordVoiceOver, + checked = conversationBoostEnabled.value, + onCheckedChange = { conversationBoostEnabled.value = it }, + ) + } + } - Column( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween - ) { - for (i in 0 until 8) { - val eqValue = remember(eq.value[i]) { mutableFloatStateOf(eq.value[i]) } - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(38.dp) - ) { - Text( - text = String.format("%.2f", eqValue.floatValue), - fontSize = 12.sp, - color = textColor, - modifier = Modifier.padding(bottom = 4.dp) + // --- Equalizer (8 bands) --- + item(key = "equalizer") { + ExpressiveSectionHeader(stringResource(R.string.equalizer)) + ExpressiveGroup { + for (i in 0 until 8) { + val eqValue = remember(eq.value[i]) { mutableFloatStateOf(eq.value[i]) } + if (i > 0) ExpressiveDivider(startInset = false) + ExpressiveListItem( + title = stringResource(R.string.band_label, i + 1), + leadingIcon = Icons.Filled.Equalizer, + value = String.format("%.2f", eqValue.floatValue), ) - - Slider( + ExpressiveSlider( value = eqValue.floatValue, onValueChange = { newVal -> eqValue.floatValue = newVal @@ -306,63 +317,12 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) { eq.value = newEQ }, valueRange = 0f..100f, - modifier = Modifier - .fillMaxWidth(0.9f) - .height(36.dp), - colors = SliderDefaults.colors( - thumbColor = thumbColor, - activeTrackColor = activeTrackColor, - inactiveTrackColor = trackColor - ), - thumb = { - Box( - modifier = Modifier - .size(24.dp) - .shadow(4.dp, CircleShape) - .background(thumbColor, CircleShape) - ) - }, - track = { - Box( - modifier = Modifier - .fillMaxWidth() - .height(12.dp), - contentAlignment = Alignment.CenterStart - ) - { - Box( - modifier = Modifier - .fillMaxWidth() - .height(4.dp) - .background(trackColor, RoundedCornerShape(4.dp)) - ) - Box( - modifier = Modifier - .fillMaxWidth(eqValue.floatValue / 100f) - .height(4.dp) - .background( - activeTrackColor, - RoundedCornerShape(4.dp) - ) - ) - } - } - ) - - Text( - text = stringResource(R.string.band_label, i + 1), - fontSize = 12.sp, - color = textColor, - modifier = Modifier.padding(top = 4.dp) + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) } } } - - Spacer(modifier = Modifier.height(16.dp)) } - - Spacer(modifier = Modifier.height(bottomPadding)) } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt index d9195718e..22736fbb7 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt @@ -16,6 +16,8 @@ along with this program. If not, see . */ +@file:OptIn(ExperimentalMaterial3Api::class) + package me.kavishdevar.librepods.presentation.screens import android.content.Intent @@ -23,38 +25,33 @@ 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.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer 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.width +import androidx.compose.foundation.lazy.LazyColumn 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.material.icons.filled.Description 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.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text @@ -72,55 +69,34 @@ 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.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 androidx.navigation.NavController -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import dev.chrisbanes.haze.hazeSource -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.components.StyledScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveAction +import me.kavishdevar.librepods.presentation.m3.ExpressiveActionGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveLoading +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader 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(navController: NavController) { val context = LocalContext.current - val scrollState = rememberScrollState() val coroutineScope = rememberCoroutineScope() val logCollector = remember { LogCollector(context) } @@ -146,13 +122,7 @@ fun TroubleshootingScreen(navController: NavController) { 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) { @@ -209,155 +179,102 @@ fun TroubleshootingScreen(navController: NavController) { showBottomSheet = true } - val backdrop = rememberLayerBackdrop() + fun shareLog(file: 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")) + } - Box( - modifier = Modifier.fillMaxSize() - ) { - StyledScaffold( - title = stringResource(R.string.troubleshooting) - ){ topPadding, hazeState, bottomPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .layerBackdrop(backdrop) - .hazeSource(state = hazeState) - .verticalScroll(scrollState) - .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.sf_pro)) - ), - 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 + ExpressiveScreenScaffold( + title = stringResource(R.string.troubleshooting), + onBack = { navController.popBackStack() }, + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), + ) { + item(key = "saved_logs") { + ExpressiveSectionHeader(stringResource(R.string.saved_logs)) + ExpressiveGroup { + if (savedLogs.isEmpty()) { + ExpressiveListItem( + title = stringResource(R.string.no_logs_found), + leadingIcon = Icons.Default.Description, ) - } - } 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") + } else { + ExpressiveListItem( + title = "Total logs", + value = savedLogs.size.toString(), + trailing = if (savedLogs.size > 1) { + { + TextButton(onClick = { showDeleteAllDialog = true }) { + Text( + "Delete all", + color = MaterialTheme.colorScheme.error, + ) + } } - } - } - + } else null, + ) 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 = { + ExpressiveDivider() + ExpressiveListItem( + title = logFile.name, + subtitle = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.US) + .format(Date(logFile.lastModified())), + leadingIcon = Icons.Default.Description, + onClick = { openLogBottomSheet(logFile) }, + trailing = { + IconButton(onClick = { selectedLogFile = logFile showDeleteDialog = true + }) { + Icon( + Icons.Default.Delete, + contentDescription = "Delete", + tint = MaterialTheme.colorScheme.error, + ) } - ) { - Icon( - Icons.Default.Delete, - contentDescription = "Delete", - tint = MaterialTheme.colorScheme.error - ) - } - } + }, + ) } } } + } + item(key = "collect_action") { 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)) + if (!isCollectingLogs) { + ExpressiveActionGroup( + actions = listOf( + ExpressiveAction(stringResource(R.string.collect_logs)) { + showTroubleshootingSteps = true + } + ) + ) } } + } + item(key = "wizard") { AnimatedVisibility( visible = showTroubleshootingSteps, enter = fadeIn(animationSpec = tween(300)) + @@ -367,544 +284,388 @@ fun TroubleshootingScreen(navController: NavController) { ) { 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.sf_pro)) - ), - 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) + ExpressiveSectionHeader(stringResource(R.string.troubleshooting_steps)) + ExpressiveGroup { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + Text( + text = instructionText, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface ) - .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)) + 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 + when (currentStep) { + 0 -> { + ExpressiveActionGroup( + actions = listOf( + ExpressiveAction("Open Xposed Settings") { + coroutineScope.launch { + logCollector.openXposedSettings(context) + delay(2000) + currentStep = 1 + } + } + ) ) - ) { - Text("Open Xposed Settings") } - } - 1 -> { - Button( - onClick = { - currentStep = 2 - isCollectingLogs = true + 1 -> { + ExpressiveActionGroup( + actions = listOf( + ExpressiveAction("Continue") { + currentStep = 2 + isCollectingLogs = true - coroutineScope.launch { - try { - logCollector.clearLogs() + coroutineScope.launch { + try { + logCollector.clearLogs() - logCollector.addLogMarker(LogCollector.LogMarkerType.START) + logCollector.addLogMarker(LogCollector.LogMarkerType.START) - logCollector.killBluetoothService() + logCollector.killBluetoothService() - withContext(Dispatchers.Main) { - delay(500) - currentStep = 3 - } + 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 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() } } - ) - - 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 + } } } - } 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)) + 2, 3 -> { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + ExpressiveLoading() - Text( - text = if (currentStep == 2) "Preparing..." else "Collecting logs...", - fontSize = 14.sp, - color = textColor - ) + Spacer(modifier = Modifier.height(8.dp)) - if (currentStep == 3) { - Spacer(modifier = Modifier.height(16.dp)) + Text( + text = if (currentStep == 2) "Preparing..." else "Collecting logs...", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) - 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() + if (currentStep == 3) { + Spacer(modifier = Modifier.height(16.dp)) + + ExpressiveActionGroup( + actions = listOf( + ExpressiveAction("Stop Collection") { + 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" + 4 -> { + ExpressiveActionGroup( + actions = listOf( + ExpressiveAction("Share") { + selectedLogFile?.let { shareLog(it) } + }, + ExpressiveAction("Save") { + selectedLogFile?.let { file -> + saveLauncher.launch(file.absolutePath) + } + }, ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Share") - } + ) - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { - selectedLogFile?.let { file -> - saveLauncher.launch( - file.absolutePath - ) + ExpressiveActionGroup( + actions = listOf( + ExpressiveAction("Done") { + currentStep = 0 + showTroubleshootingSteps = false } - }, - shape = RoundedCornerShape(10.dp), - colors = ButtonDefaults.buttonColors( - containerColor = buttonBgColor, - contentColor = textColor - ), - modifier = Modifier.width(150.dp) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_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 (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 + 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++ } - ) { - Text("Delete All", color = MaterialTheme.colorScheme.error) } - }, - dismissButton = { - TextButton(onClick = { showDeleteAllDialog = false }) { - Text("Cancel") + 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") } } - } + ) + } - 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 + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { showBottomSheet = false }, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = MaterialTheme.shapes.extraLarge, + 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(horizontal = 16.dp, vertical = 12.dp) - .padding(bottom = 32.dp) + .padding(bottom = 12.dp), ) { - Column( + Text( + text = selectedLogFile?.name ?: "Log Content", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.US) + .format(Date(selectedLogFile?.lastModified() ?: 0)), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + if (isLoadingLogContent) { + Box( modifier = Modifier .fillMaxWidth() - .padding(bottom = 12.dp), + .height(300.dp), + contentAlignment = Alignment.Center ) { - Text( - text = selectedLogFile?.name ?: "Log Content", - style = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - 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.sf_pro)) - ) + ExpressiveLoading() } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .height(300.dp) + .background( + color = MaterialTheme.colorScheme.surfaceContainerHighest, + shape = MaterialTheme.shapes.medium + ) + ) { + val horizontalScrollState = rememberScrollState() + val verticalScrollState = rememberScrollState() - if (isLoadingLogContent) { Box( modifier = Modifier - .fillMaxWidth() - .height(300.dp), - contentAlignment = Alignment.Center + .fillMaxSize() + .padding(8.dp) + .horizontalScroll(horizontalScrollState) + .verticalScroll(verticalScrollState) ) { - 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 - ) - } + Text( + text = logContent, + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + 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" - ) - ) - } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ExpressiveActionGroup( + actions = listOf( + ExpressiveAction("Share") { + selectedLogFile?.let { shareLog(it) } }, - 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 = { + ExpressiveAction("Save") { selectedLogFile?.let { file -> saveLauncher.launch(file.absolutePath) } }, - shape = RoundedCornerShape(10.dp), - colors = ButtonDefaults.buttonColors( - containerColor = buttonBgColor, - contentColor = textColor - ), - modifier = Modifier.weight(1f) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_save), - contentDescription = "Save" - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Save") - } - } + ) + ) } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt index 175a830cc..ee88dfe89 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt @@ -19,9 +19,9 @@ package me.kavishdevar.librepods.presentation.screens import android.util.Log -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -29,9 +29,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -45,241 +45,221 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color 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.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import dev.chrisbanes.haze.hazeSource import kotlinx.coroutines.Job import me.kavishdevar.librepods.R import me.kavishdevar.librepods.data.HearingAidSettings import me.kavishdevar.librepods.data.parseHearingAidSettingsResponse import me.kavishdevar.librepods.data.sendHearingAidSettings -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel private const val TAG = "HearingAidAdjustments" @Composable fun UpdateHearingTestScreen(viewModel: AirPodsViewModel) { - val verticalScrollState = rememberScrollState() val state by viewModel.uiState.collectAsState() - val backdrop = rememberLayerBackdrop() - StyledScaffold( - title = stringResource(R.string.hearing_test) - ) { topPadding, hazeState, bottomPadding -> - Column( - modifier = Modifier - .hazeSource(hazeState) - .fillMaxSize() - .layerBackdrop(backdrop) - .verticalScroll(verticalScrollState) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - val textColor = if (isSystemInDarkTheme()) Color.White else Color.Black - - Spacer(modifier = Modifier.height(topPadding)) - Text( - text = stringResource(R.string.hearing_test_value_instruction), - modifier = Modifier.fillMaxWidth(), - style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - textAlign = TextAlign.Center, - ) - val tone = rememberSaveable { mutableFloatStateOf(0.5f) } - val ambientNoiseReduction = rememberSaveable { mutableFloatStateOf(0.0f) } - val ownVoiceAmplification = rememberSaveable { mutableFloatStateOf(0.5f) } - val leftAmplification = rememberSaveable { mutableFloatStateOf(0.5f) } - val rightAmplification = rememberSaveable { mutableFloatStateOf(0.5f) } - val conversationBoostEnabled = rememberSaveable { mutableStateOf(false) } - val leftEQ = rememberSaveable( - saver = Saver( - save = { it.value.toList() }, - restore = { mutableStateOf(it.toFloatArray()) } - ) - ) { - mutableStateOf(FloatArray(8)) - } - val rightEQ = rememberSaveable( - saver = Saver( - save = { it.value.toList() }, - restore = { mutableStateOf(it.toFloatArray()) } - ) - ) { - mutableStateOf(FloatArray(8)) - } + val tone = rememberSaveable { mutableFloatStateOf(0.5f) } + val ambientNoiseReduction = rememberSaveable { mutableFloatStateOf(0.0f) } + val ownVoiceAmplification = rememberSaveable { mutableFloatStateOf(0.5f) } + val leftAmplification = rememberSaveable { mutableFloatStateOf(0.5f) } + val rightAmplification = rememberSaveable { mutableFloatStateOf(0.5f) } + val conversationBoostEnabled = rememberSaveable { mutableStateOf(false) } + val leftEQ = rememberSaveable( + saver = Saver( + save = { it.value.toList() }, + restore = { mutableStateOf(it.toFloatArray()) } + ) + ) { + mutableStateOf(FloatArray(8)) + } + val rightEQ = rememberSaveable( + saver = Saver( + save = { it.value.toList() }, + restore = { mutableStateOf(it.toFloatArray()) } + ) + ) { + mutableStateOf(FloatArray(8)) + } - val debounceJob = remember { mutableStateOf(null) } - val initialized = rememberSaveable { mutableStateOf(false) } + val debounceJob = remember { mutableStateOf(null) } + val initialized = rememberSaveable { mutableStateOf(false) } - val hearingAidSettings = remember { - mutableStateOf( - HearingAidSettings( - leftEQ = leftEQ.value, - rightEQ = rightEQ.value, - leftAmplification = leftAmplification.floatValue, - rightAmplification = rightAmplification.floatValue, - leftTone = tone.floatValue, - rightTone = tone.floatValue, - leftConversationBoost = conversationBoostEnabled.value, - rightConversationBoost = conversationBoostEnabled.value, - leftAmbientNoiseReduction = ambientNoiseReduction.floatValue, - rightAmbientNoiseReduction = ambientNoiseReduction.floatValue, - netAmplification = leftAmplification.floatValue + rightAmplification.floatValue / 2, - balance = 0.5f + (rightAmplification.floatValue - leftAmplification.floatValue) / 2, - ownVoiceAmplification = ownVoiceAmplification.floatValue - ) - ) - } + val hearingAidSettings = remember { + mutableStateOf( + HearingAidSettings( + leftEQ = leftEQ.value, + rightEQ = rightEQ.value, + leftAmplification = leftAmplification.floatValue, + rightAmplification = rightAmplification.floatValue, + leftTone = tone.floatValue, + rightTone = tone.floatValue, + leftConversationBoost = conversationBoostEnabled.value, + rightConversationBoost = conversationBoostEnabled.value, + leftAmbientNoiseReduction = ambientNoiseReduction.floatValue, + rightAmbientNoiseReduction = ambientNoiseReduction.floatValue, + netAmplification = leftAmplification.floatValue + rightAmplification.floatValue / 2, + balance = 0.5f + (rightAmplification.floatValue - leftAmplification.floatValue) / 2, + ownVoiceAmplification = ownVoiceAmplification.floatValue + ) + ) + } - LaunchedEffect(state.hearingAidData) { - val parsed = parseHearingAidSettingsResponse(state.hearingAidData) - if (parsed != null) { - leftEQ.value = parsed.leftEQ.copyOf() - rightEQ.value = parsed.rightEQ.copyOf() - conversationBoostEnabled.value = parsed.leftConversationBoost - tone.floatValue = parsed.leftTone - ambientNoiseReduction.floatValue = parsed.leftAmbientNoiseReduction - ownVoiceAmplification.floatValue = parsed.ownVoiceAmplification - leftAmplification.floatValue = parsed.leftAmplification - rightAmplification.floatValue = parsed.rightAmplification - initialized.value = true - Log.d(TAG, "Updated hearing aid settings from notification") - } else { - Log.w(TAG, "Failed to parse hearing aid settings from notification") - } - } + LaunchedEffect(state.hearingAidData) { + val parsed = parseHearingAidSettingsResponse(state.hearingAidData) + if (parsed != null) { + leftEQ.value = parsed.leftEQ.copyOf() + rightEQ.value = parsed.rightEQ.copyOf() + conversationBoostEnabled.value = parsed.leftConversationBoost + tone.floatValue = parsed.leftTone + ambientNoiseReduction.floatValue = parsed.leftAmbientNoiseReduction + ownVoiceAmplification.floatValue = parsed.ownVoiceAmplification + leftAmplification.floatValue = parsed.leftAmplification + rightAmplification.floatValue = parsed.rightAmplification + initialized.value = true + Log.d(TAG, "Updated hearing aid settings from notification") + } else { + Log.w(TAG, "Failed to parse hearing aid settings from notification") + } + } - LaunchedEffect( - leftEQ.value, - rightEQ.value, - conversationBoostEnabled.value, - leftAmplification.floatValue, - rightAmplification.floatValue, - tone.floatValue, - ambientNoiseReduction.floatValue, - ownVoiceAmplification.floatValue - ) { - if (!initialized.value) return@LaunchedEffect - hearingAidSettings.value = HearingAidSettings( - leftEQ = leftEQ.value, - rightEQ = rightEQ.value, - leftAmplification = leftAmplification.floatValue, - rightAmplification = rightAmplification.floatValue, - leftTone = tone.floatValue, - rightTone = tone.floatValue, - leftConversationBoost = conversationBoostEnabled.value, - rightConversationBoost = conversationBoostEnabled.value, - leftAmbientNoiseReduction = ambientNoiseReduction.floatValue, - rightAmbientNoiseReduction = ambientNoiseReduction.floatValue, - netAmplification = leftAmplification.floatValue + rightAmplification.floatValue / 2, - balance = 0.5f + (rightAmplification.floatValue - leftAmplification.floatValue) / 2, - ownVoiceAmplification = ownVoiceAmplification.floatValue - ) - Log.d(TAG, "Updated settings: ${hearingAidSettings.value}") - sendHearingAidSettings(state.hearingAidData, hearingAidSettings.value, debounceJob, viewModel::setATTCharacteristicValue) - } + LaunchedEffect( + leftEQ.value, + rightEQ.value, + conversationBoostEnabled.value, + leftAmplification.floatValue, + rightAmplification.floatValue, + tone.floatValue, + ambientNoiseReduction.floatValue, + ownVoiceAmplification.floatValue + ) { + if (!initialized.value) return@LaunchedEffect + hearingAidSettings.value = HearingAidSettings( + leftEQ = leftEQ.value, + rightEQ = rightEQ.value, + leftAmplification = leftAmplification.floatValue, + rightAmplification = rightAmplification.floatValue, + leftTone = tone.floatValue, + rightTone = tone.floatValue, + leftConversationBoost = conversationBoostEnabled.value, + rightConversationBoost = conversationBoostEnabled.value, + leftAmbientNoiseReduction = ambientNoiseReduction.floatValue, + rightAmbientNoiseReduction = ambientNoiseReduction.floatValue, + netAmplification = leftAmplification.floatValue + rightAmplification.floatValue / 2, + balance = 0.5f + (rightAmplification.floatValue - leftAmplification.floatValue) / 2, + ownVoiceAmplification = ownVoiceAmplification.floatValue + ) + Log.d(TAG, "Updated settings: ${hearingAidSettings.value}") + sendHearingAidSettings(state.hearingAidData, hearingAidSettings.value, debounceJob, viewModel::setATTCharacteristicValue) + } - val frequencies = - listOf("250Hz", "500Hz", "1kHz", "2kHz", "3kHz", "4kHz", "6kHz", "8kHz") + val frequencies = + listOf("250Hz", "500Hz", "1kHz", "2kHz", "3kHz", "4kHz", "6kHz", "8kHz") - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Spacer(modifier = Modifier.width(60.dp)) - Text( - text = stringResource(R.string.left), - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - style = TextStyle( - fontSize = 18.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor - ) - ) + ExpressiveScreenScaffold(title = stringResource(R.string.hearing_test)) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), + ) { + item { Text( - text = stringResource(R.string.right), - modifier = Modifier.weight(1f), + text = stringResource(R.string.hearing_test_value_instruction), + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, - style = TextStyle( - fontSize = 18.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = textColor - ) ) } - frequencies.forEachIndexed { index, freq -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = freq, + item { + ExpressiveSectionHeader(text = stringResource(R.string.hearing_test)) + ExpressiveGroup { + Row( modifier = Modifier - .width(60.dp) - .align(Alignment.CenterVertically), - textAlign = TextAlign.End, - style = TextStyle( - color = textColor, - fontSize = 16.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - ) - OutlinedTextField( - value = leftEQ.value[index].toString(), - onValueChange = { newValue -> - val parsed = newValue.toFloatOrNull() - if (parsed != null) { - val newArray = leftEQ.value.copyOf() - newArray[index] = parsed - leftEQ.value = newArray - Log.d(TAG, "Left EQ updated at index $index to $parsed") - } - }, -// label = { Text("Value", fontSize = 14.sp, fontFamily = FontFamily(Font(R.font.sf_pro))) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - textStyle = TextStyle( - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontSize = 14.sp - ), - modifier = Modifier.weight(1f) - ) - OutlinedTextField( - value = rightEQ.value[index].toString(), - onValueChange = { newValue -> - val parsed = newValue.toFloatOrNull() - if (parsed != null) { - val newArray = rightEQ.value.copyOf() - newArray[index] = parsed - rightEQ.value = newArray - Log.d(TAG, "Right EQ updated at index $index to $parsed") - } - }, -// label = { Text("Value", fontSize = 14.sp, fontFamily = FontFamily(Font(R.font.sf_pro))) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - textStyle = TextStyle( - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontSize = 14.sp - ), - modifier = Modifier.weight(1f) - ) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Spacer(modifier = Modifier.width(60.dp)) + Text( + text = stringResource(R.string.left), + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = stringResource(R.string.right), + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + frequencies.forEachIndexed { index, freq -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = freq, + modifier = Modifier.width(60.dp), + textAlign = TextAlign.End, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + OutlinedTextField( + value = leftEQ.value[index].toString(), + onValueChange = { newValue -> + val parsed = newValue.toFloatOrNull() + if (parsed != null) { + val newArray = leftEQ.value.copyOf() + newArray[index] = parsed + leftEQ.value = newArray + Log.d(TAG, "Left EQ updated at index $index to $parsed") + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + textStyle = MaterialTheme.typography.bodyLarge, + shape = MaterialTheme.shapes.medium, + modifier = Modifier.weight(1f), + ) + OutlinedTextField( + value = rightEQ.value[index].toString(), + onValueChange = { newValue -> + val parsed = newValue.toFloatOrNull() + if (parsed != null) { + val newArray = rightEQ.value.copyOf() + newArray[index] = parsed + rightEQ.value = newArray + Log.d(TAG, "Right EQ updated at index $index to $parsed") + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + textStyle = MaterialTheme.typography.bodyLarge, + shape = MaterialTheme.shapes.medium, + modifier = Modifier.weight(1f), + ) + } + } + Spacer(modifier = Modifier.height(6.dp)) } } - Spacer(modifier = Modifier.height(bottomPadding)) } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/VersionInfoScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/VersionInfoScreen.kt index 266958762..19d5e7455 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/VersionInfoScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/VersionInfoScreen.kt @@ -18,160 +18,59 @@ package me.kavishdevar.librepods.presentation.screens -import androidx.compose.foundation.background -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.PaddingValues 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.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Text +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Numbers import androidx.compose.runtime.Composable 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.graphics.Color 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.unit.dp -import androidx.compose.ui.unit.sp -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveDivider +import me.kavishdevar.librepods.presentation.m3.ExpressiveGroup +import me.kavishdevar.librepods.presentation.m3.ExpressiveListItem +import me.kavishdevar.librepods.presentation.m3.ExpressiveScreenScaffold +import me.kavishdevar.librepods.presentation.m3.ExpressiveSectionHeader import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel @Composable fun VersionScreen(viewModel: AirPodsViewModel) { val state by viewModel.uiState.collectAsState() - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color.Black + val versionLabel = stringResource(R.string.version) - val backdrop = rememberLayerBackdrop() - - StyledScaffold( - title = stringResource(R.string.version) - ) { spacerHeight -> - Column( - modifier = Modifier - .fillMaxSize() - .layerBackdrop(backdrop) - .padding(horizontal = 16.dp) + ExpressiveScreenScaffold(title = versionLabel) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = padding.calculateTopPadding(), + bottom = 120.dp, + start = 16.dp, + end = 16.dp, + ), ) { - Spacer(modifier = Modifier.height(spacerHeight)) - Box( - modifier = Modifier - .background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)) - .padding(horizontal = 16.dp, vertical = 4.dp) - ){ - Text( - text = stringResource(R.string.version), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = textColor.copy(alpha = 0.6f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } - - Column( - modifier = Modifier - .clip(RoundedCornerShape(28.dp)) - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(28.dp)) - .padding(top = 2.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.version) + " 1", - style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - Text( - text = state.version1, - style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + item { + ExpressiveSectionHeader(versionLabel) + ExpressiveGroup { + ExpressiveListItem( + title = "$versionLabel 1", + leadingIcon = Icons.Filled.Numbers, + value = state.version1, ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier - .padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.version) + " 2", - style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - Text( - text = state.version2, - style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) - ) - } - HorizontalDivider( - thickness = 1.dp, - color = Color(0x40888888), - modifier = Modifier - .padding(horizontal = 12.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.version) + " 3", - style = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + ExpressiveDivider() + ExpressiveListItem( + title = "$versionLabel 2", + leadingIcon = Icons.Filled.Numbers, + value = state.version2, ) - Text( - text = state.version3, - style = TextStyle( - fontSize = 16.sp, - color = textColor.copy(0.8f), - fontFamily = FontFamily(Font(R.font.sf_pro)) - ) + ExpressiveDivider() + ExpressiveListItem( + title = "$versionLabel 3", + leadingIcon = Icons.Filled.Numbers, + value = state.version3, ) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Color.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Color.kt index f2e44c1d3..b6dc86f21 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Color.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Color.kt @@ -16,15 +16,175 @@ along with this program. If not, see . */ - package me.kavishdevar.librepods.presentation.theme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.ui.graphics.Color +/* + * Material 3 Expressive palette for LibrePods. + * + * Vibrant violet primary, electric teal secondary and warm coral tertiary — a bold, energetic + * scheme that matches the "expressive" motion. Used as the fallback whenever dynamic color + * (Material You, Android 12+) is unavailable. Tonal roles are filled out completely so every + * M3 component (cards, sliders, switches, button groups, wavy indicators…) renders correctly. + */ + +// region Light +private val PrimaryLight = Color(0xFF5E35F2) +private val OnPrimaryLight = Color(0xFFFFFFFF) +private val PrimaryContainerLight = Color(0xFFE7DEFF) +private val OnPrimaryContainerLight = Color(0xFF1B0066) + +private val SecondaryLight = Color(0xFF00696E) +private val OnSecondaryLight = Color(0xFFFFFFFF) +private val SecondaryContainerLight = Color(0xFF8FF2F8) +private val OnSecondaryContainerLight = Color(0xFF00201F) + +private val TertiaryLight = Color(0xFF9B4500) +private val OnTertiaryLight = Color(0xFFFFFFFF) +private val TertiaryContainerLight = Color(0xFFFFDBCA) +private val OnTertiaryContainerLight = Color(0xFF351000) + +private val ErrorLight = Color(0xFFBA1A1A) +private val OnErrorLight = Color(0xFFFFFFFF) +private val ErrorContainerLight = Color(0xFFFFDAD6) +private val OnErrorContainerLight = Color(0xFF410002) + +private val BackgroundLight = Color(0xFFFDF7FF) +private val OnBackgroundLight = Color(0xFF1C1B20) +private val SurfaceLight = Color(0xFFFDF7FF) +private val OnSurfaceLight = Color(0xFF1C1B20) +private val SurfaceVariantLight = Color(0xFFE6E0EC) +private val OnSurfaceVariantLight = Color(0xFF48454E) +private val OutlineLight = Color(0xFF79757F) +private val OutlineVariantLight = Color(0xFFCAC4CF) +private val SurfaceContainerLowestLight = Color(0xFFFFFFFF) +private val SurfaceContainerLowLight = Color(0xFFF7F1FB) +private val SurfaceContainerLight = Color(0xFFF1EBF5) +private val SurfaceContainerHighLight = Color(0xFFEBE6EF) +private val SurfaceContainerHighestLight = Color(0xFFE5E0E9) +private val InverseSurfaceLight = Color(0xFF313035) +private val InverseOnSurfaceLight = Color(0xFFF4EFF7) +private val InversePrimaryLight = Color(0xFFCFBDFF) +// endregion + +// region Dark +private val PrimaryDark = Color(0xFFCFBDFF) +private val OnPrimaryDark = Color(0xFF330A92) +private val PrimaryContainerDark = Color(0xFF491CC9) +private val OnPrimaryContainerDark = Color(0xFFE7DEFF) + +private val SecondaryDark = Color(0xFF4FD8E0) +private val OnSecondaryDark = Color(0xFF00363A) +private val SecondaryContainerDark = Color(0xFF004F54) +private val OnSecondaryContainerDark = Color(0xFF8FF2F8) + +private val TertiaryDark = Color(0xFFFFB68A) +private val OnTertiaryDark = Color(0xFF532200) +private val TertiaryContainerDark = Color(0xFF763300) +private val OnTertiaryContainerDark = Color(0xFFFFDBCA) + +private val ErrorDark = Color(0xFFFFB4AB) +private val OnErrorDark = Color(0xFF690005) +private val ErrorContainerDark = Color(0xFF93000A) +private val OnErrorContainerDark = Color(0xFFFFDAD6) + +private val BackgroundDark = Color(0xFF141218) +private val OnBackgroundDark = Color(0xFFE6E1E9) +private val SurfaceDark = Color(0xFF141218) +private val OnSurfaceDark = Color(0xFFE6E1E9) +private val SurfaceVariantDark = Color(0xFF48454E) +private val OnSurfaceVariantDark = Color(0xFFCAC4CF) +private val OutlineDark = Color(0xFF948F99) +private val OutlineVariantDark = Color(0xFF48454E) +private val SurfaceContainerLowestDark = Color(0xFF0E0D13) +private val SurfaceContainerLowDark = Color(0xFF1D1B20) +private val SurfaceContainerDark = Color(0xFF211F26) +private val SurfaceContainerHighDark = Color(0xFF2C2A30) +private val SurfaceContainerHighestDark = Color(0xFF36343B) +private val InverseSurfaceDark = Color(0xFFE6E1E9) +private val InverseOnSurfaceDark = Color(0xFF313035) +private val InversePrimaryDark = Color(0xFF5E35F2) +// endregion + +val LightColors = lightColorScheme( + primary = PrimaryLight, + onPrimary = OnPrimaryLight, + primaryContainer = PrimaryContainerLight, + onPrimaryContainer = OnPrimaryContainerLight, + secondary = SecondaryLight, + onSecondary = OnSecondaryLight, + secondaryContainer = SecondaryContainerLight, + onSecondaryContainer = OnSecondaryContainerLight, + tertiary = TertiaryLight, + onTertiary = OnTertiaryLight, + tertiaryContainer = TertiaryContainerLight, + onTertiaryContainer = OnTertiaryContainerLight, + error = ErrorLight, + onError = OnErrorLight, + errorContainer = ErrorContainerLight, + onErrorContainer = OnErrorContainerLight, + background = BackgroundLight, + onBackground = OnBackgroundLight, + surface = SurfaceLight, + onSurface = OnSurfaceLight, + surfaceVariant = SurfaceVariantLight, + onSurfaceVariant = OnSurfaceVariantLight, + outline = OutlineLight, + outlineVariant = OutlineVariantLight, + surfaceContainerLowest = SurfaceContainerLowestLight, + surfaceContainerLow = SurfaceContainerLowLight, + surfaceContainer = SurfaceContainerLight, + surfaceContainerHigh = SurfaceContainerHighLight, + surfaceContainerHighest = SurfaceContainerHighestLight, + inverseSurface = InverseSurfaceLight, + inverseOnSurface = InverseOnSurfaceLight, + inversePrimary = InversePrimaryLight, + surfaceTint = PrimaryLight, +) + +val DarkColors = darkColorScheme( + primary = PrimaryDark, + onPrimary = OnPrimaryDark, + primaryContainer = PrimaryContainerDark, + onPrimaryContainer = OnPrimaryContainerDark, + secondary = SecondaryDark, + onSecondary = OnSecondaryDark, + secondaryContainer = SecondaryContainerDark, + onSecondaryContainer = OnSecondaryContainerDark, + tertiary = TertiaryDark, + onTertiary = OnTertiaryDark, + tertiaryContainer = TertiaryContainerDark, + onTertiaryContainer = OnTertiaryContainerDark, + error = ErrorDark, + onError = OnErrorDark, + errorContainer = ErrorContainerDark, + onErrorContainer = OnErrorContainerDark, + background = BackgroundDark, + onBackground = OnBackgroundDark, + surface = SurfaceDark, + onSurface = OnSurfaceDark, + surfaceVariant = SurfaceVariantDark, + onSurfaceVariant = OnSurfaceVariantDark, + outline = OutlineDark, + outlineVariant = OutlineVariantDark, + surfaceContainerLowest = SurfaceContainerLowestDark, + surfaceContainerLow = SurfaceContainerLowDark, + surfaceContainer = SurfaceContainerDark, + surfaceContainerHigh = SurfaceContainerHighDark, + surfaceContainerHighest = SurfaceContainerHighestDark, + inverseSurface = InverseSurfaceDark, + inverseOnSurface = InverseOnSurfaceDark, + inversePrimary = InversePrimaryDark, + surfaceTint = PrimaryDark, +) + +// Legacy seed colors kept so any stray references still resolve; no longer used by the theme. val Purple80 = Color(0xFFD0BCFF) val PurpleGrey80 = Color(0xFFCCC2DC) val Pink80 = Color(0xFFEFB8C8) - val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Shape.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Shape.kt new file mode 100644 index 000000000..8f63a8bdd --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Shape.kt @@ -0,0 +1,36 @@ +/* + 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 . +*/ + +package me.kavishdevar.librepods.presentation.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +/* + * Material 3 Expressive leans on large, generous corner radii. These feed every M3 component's + * default shape (cards, dialogs, buttons, text fields, menus, sheets…) so the whole app gets a + * soft, rounded, expressive silhouette. + */ +val LibrePodsShapes = Shapes( + extraSmall = RoundedCornerShape(8.dp), + small = RoundedCornerShape(14.dp), + medium = RoundedCornerShape(20.dp), + large = RoundedCornerShape(28.dp), + extraLarge = RoundedCornerShape(36.dp), +) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Theme.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Theme.kt index 04225ccdd..c3d6fbc11 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Theme.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Theme.kt @@ -20,26 +20,23 @@ package me.kavishdevar.librepods.presentation.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme +import androidx.compose.material3.MotionScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 -) - +/** + * Material 3 Expressive theme for LibrePods. + * + * [MaterialExpressiveTheme] wires in [MotionScheme.expressive] — springs with low damping that give + * every Material component a lively, slightly-overshooting "bouncy" feel out of the box. On Android + * 12+ we use dynamic color (Material You); otherwise we fall back to the vibrant violet→teal scheme + * defined in `Color.kt`. Typography is the Roboto Flex variable scale; shapes are the expressive set. + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun LibrePodsTheme( darkTheme: Boolean = isSystemInDarkTheme(), @@ -52,13 +49,15 @@ fun LibrePodsTheme( if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> DarkColorScheme - else -> LightColorScheme + darkTheme -> DarkColors + else -> LightColors } - MaterialTheme( + MaterialExpressiveTheme( colorScheme = colorScheme, - typography = Typography, + motionScheme = MotionScheme.expressive(), + shapes = LibrePodsShapes, + typography = LibrePodsTypography, content = content ) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Type.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Type.kt index 72a44242a..e7415eff0 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Type.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Type.kt @@ -20,33 +20,69 @@ package me.kavishdevar.librepods.presentation.theme import androidx.compose.material3.Typography 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.FontVariation import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp +import me.kavishdevar.librepods.R -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ +/* + * Typography for the Material 3 Expressive redesign — the clean, geometric **Montserrat** variable + * font (bundled, OFL — no Google Play Services / downloadable-font dependency, FOSS-friendly). + * Each registered weight maps to the single variable font driven through its `wght` axis. + */ +private fun montserrat(weight: Int): Font = Font( + resId = R.font.montserrat, + weight = FontWeight(weight), + variationSettings = FontVariation.Settings(FontVariation.weight(weight)), +) + +val MontserratFamily = FontFamily( + montserrat(300), + montserrat(400), + montserrat(500), + montserrat(600), + montserrat(700), + montserrat(800), +) + +private val BaseTypography = Typography( + displayLarge = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.Bold, fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp), + displayMedium = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.Bold, fontSize = 45.sp, lineHeight = 52.sp), + displaySmall = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.Bold, fontSize = 36.sp, lineHeight = 44.sp), + headlineLarge = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.SemiBold, fontSize = 32.sp, lineHeight = 40.sp), + headlineMedium = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.SemiBold, fontSize = 28.sp, lineHeight = 36.sp), + headlineSmall = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.SemiBold, fontSize = 24.sp, lineHeight = 32.sp), + titleLarge = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.SemiBold, fontSize = 22.sp, lineHeight = 28.sp), + titleMedium = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.Medium, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp), + titleSmall = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp), + bodyLarge = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp), + bodyMedium = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.25.sp), + bodySmall = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.4.sp), + labelLarge = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp), + labelMedium = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.SemiBold, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp), + labelSmall = TextStyle(fontFamily = MontserratFamily, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp), +) + +/* + * M3 Expressive "emphasized" type roles — same metrics, heavier Montserrat. Without these the + * library defaults silently fall back to Roboto for emphasized text in expressive components. + */ +val LibrePodsTypography = BaseTypography.copy( + displayLargeEmphasized = BaseTypography.displayLarge, + displayMediumEmphasized = BaseTypography.displayMedium, + displaySmallEmphasized = BaseTypography.displaySmall, + headlineLargeEmphasized = BaseTypography.headlineLarge.copy(fontWeight = FontWeight.Bold), + headlineMediumEmphasized = BaseTypography.headlineMedium.copy(fontWeight = FontWeight.Bold), + headlineSmallEmphasized = BaseTypography.headlineSmall.copy(fontWeight = FontWeight.Bold), + titleLargeEmphasized = BaseTypography.titleLarge.copy(fontWeight = FontWeight.Bold), + titleMediumEmphasized = BaseTypography.titleMedium.copy(fontWeight = FontWeight.Bold), + titleSmallEmphasized = BaseTypography.titleSmall.copy(fontWeight = FontWeight.SemiBold), + bodyLargeEmphasized = BaseTypography.bodyLarge.copy(fontWeight = FontWeight.SemiBold), + bodyMediumEmphasized = BaseTypography.bodyMedium.copy(fontWeight = FontWeight.SemiBold), + bodySmallEmphasized = BaseTypography.bodySmall.copy(fontWeight = FontWeight.SemiBold), + labelLargeEmphasized = BaseTypography.labelLarge.copy(fontWeight = FontWeight.Bold), + labelMediumEmphasized = BaseTypography.labelMedium.copy(fontWeight = FontWeight.Bold), + labelSmallEmphasized = BaseTypography.labelSmall.copy(fontWeight = FontWeight.Bold), ) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt index 7e0b126bb..839c46d66 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt @@ -33,8 +33,6 @@ data class AppSettingsUiState( // All features are unlocked; billing was removed from this FOSS fork. val isPremium: Boolean = true, val connectionSuccessful: Boolean = false, - val showBottomSheetPopup: Boolean = true, - val showIslandPopup: Boolean = true, ) class AppSettingsViewModel(application: Application) : AndroidViewModel(application) { @@ -79,9 +77,7 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat(), cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: "", vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false), - connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false), - showBottomSheetPopup = sharedPreferences.getBoolean("show_bottom_sheet_popup", true), - showIslandPopup = sharedPreferences.getBoolean("show_island_popup", true) + connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false) ) } } @@ -172,13 +168,4 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat _uiState.update { it.copy(vendorIdHook = enabled) } } - fun setShowBottomSheetPopup(enabled: Boolean) { - sharedPreferences.edit { putBoolean("show_bottom_sheet_popup", enabled) } - _uiState.update { it.copy(showBottomSheetPopup = enabled) } - } - - fun setShowIslandPopup(enabled: Boolean) { - sharedPreferences.edit { putBoolean("show_island_popup", enabled) } - _uiState.update { it.copy(showIslandPopup = enabled) } - } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index f7f37372b..16cd4e42b 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -101,9 +101,6 @@ import me.kavishdevar.librepods.data.BatteryStatus import me.kavishdevar.librepods.data.StemAction import me.kavishdevar.librepods.data.XposedRemotePrefProvider import me.kavishdevar.librepods.data.isHeadTrackingData -import me.kavishdevar.librepods.presentation.overlays.IslandType -import me.kavishdevar.librepods.presentation.overlays.IslandWindow -import me.kavishdevar.librepods.presentation.overlays.PopupWindow import me.kavishdevar.librepods.presentation.widgets.BatteryWidget import me.kavishdevar.librepods.presentation.widgets.NoiseControlWidget import me.kavishdevar.librepods.utils.GestureDetector @@ -308,11 +305,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList ) { if (lidOpen) { Log.d(TAG, "Lid opened") - showPopup( - this@AirPodsService, - getSharedPreferences("settings", MODE_PRIVATE).getString("name", "AirPods Pro") - ?: "AirPods" - ) if (socket.isConnected) return val leftLevel = bleManager.getMostRecentStatus()?.leftBattery ?: 0 val rightLevel = bleManager.getMostRecentStatus()?.rightBattery ?: 0 @@ -590,6 +582,25 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList enabled ) } + } else if (intent?.action == "me.kavishdevar.librepods.GESTURE_CAPTURE") { + when (intent.getStringExtra("action")) { + "start" -> { + gestureCaptureLabel = intent.getStringExtra("label") ?: "idle" + gestureCaptureStart = System.currentTimeMillis() + gestureCaptureActive = true + startHeadTracking() + Log.d("GCAP", "GCAP_START label=$gestureCaptureLabel cols=ms,h,v,o1,o2,o3,label") + } + "stop" -> { + gestureCaptureActive = false + stopHeadTracking() + Log.d("GCAP", "GCAP_STOP") + } + "label" -> { + gestureCaptureLabel = intent.getStringExtra("label") ?: "idle" + Log.d("GCAP", "GCAP_LABEL ${gestureCaptureLabel} t=${System.currentTimeMillis() - gestureCaptureStart}") + } + } } } } @@ -716,7 +727,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } else if (intent?.action == AirPodsNotifications.AIRPODS_DISCONNECTED) { device = null // isConnectedLocally = false - popupShown = false updateNotificationContent(false) aacpManager.disconnected() attManager.disconnected() @@ -725,40 +735,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } } } - val showIslandReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == "me.kavishdevar.librepods.cross_device_island") { - showIsland( - this@AirPodsService, - batteryNotification.getBattery() - .find { it.component == BatteryComponent.LEFT }?.level!!.coerceAtMost( - batteryNotification.getBattery() - .find { it.component == BatteryComponent.RIGHT }?.level!! - ) - ) - } else if (intent?.action == AirPodsNotifications.DISCONNECT_RECEIVERS) { - try { - context?.unregisterReceiver(this) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - } - - val showIslandIntentFilter = IntentFilter().apply { - addAction("me.kavishdevar.librepods.cross_device_island") - addAction(AirPodsNotifications.DISCONNECT_RECEIVERS) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver(showIslandReceiver, showIslandIntentFilter, RECEIVER_EXPORTED) - } else { - @Suppress("UnspecifiedRegisterReceiverFlag") registerReceiver( - showIslandReceiver, showIslandIntentFilter - ) - } - val deviceIntentFilter = IntentFilter().apply { addAction(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED) addAction(AirPodsNotifications.AIRPODS_DISCONNECTED) @@ -995,50 +971,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList Log.d(TAG, "reverse tapped, disconnecting audio") disconnectedBecauseReversed = true disconnectAudio(this@AirPodsService, device) - showIsland( - this@AirPodsService, - (batteryNotification.getBattery() - .find { it.component == BatteryComponent.LEFT }?.level - ?: 0).coerceAtMost( - batteryNotification.getBattery() - .find { it.component == BatteryComponent.RIGHT }?.level ?: 0 - ), - IslandType.MOVED_TO_OTHER_DEVICE, - reversed = true, - otherDeviceName = senderName - ) - } - if (!aacpManager.owns) { - showIsland( - this@AirPodsService, - (batteryNotification.getBattery() - .find { it.component == BatteryComponent.LEFT }?.level - ?: 0).coerceAtMost( - batteryNotification.getBattery() - .find { it.component == BatteryComponent.RIGHT }?.level ?: 0 - ), - IslandType.MOVED_TO_OTHER_DEVICE, - reversed = reasonReverseTapped, - otherDeviceName = senderName - ) } MediaController.sendPause() } override fun onShowNearbyUI(sender: String) { - val senderName = - aacpManager.connectedDevices.find { it.mac == sender }?.type ?: "Other device" - showIsland( - this@AirPodsService, - (batteryNotification.getBattery() - .find { it.component == BatteryComponent.LEFT }?.level ?: 0).coerceAtMost( - batteryNotification.getBattery() - .find { it.component == BatteryComponent.RIGHT }?.level ?: 0 - ), - IslandType.MOVED_TO_OTHER_DEVICE, - reversed = false, - otherDeviceName = senderName - ) } override fun onDeviceInformationReceived(deviceInformation: AACPManager.Companion.AirPodsInformation) { @@ -1261,24 +1198,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList data[0] == 0x00.toByte(), data[1] == 0x00.toByte() ) - if (inEarData.sorted() == listOf(false, false) && newInEarData.sorted() != listOf( - false, false - ) && islandWindow?.isVisible != true - ) { - showIsland( - this@AirPodsService, - (batteryNotification.getBattery() - .find { it.component == BatteryComponent.LEFT }?.level ?: 0).coerceAtMost( - batteryNotification.getBattery() - .find { it.component == BatteryComponent.RIGHT }?.level ?: 0 - ) - ) - } - - if (newInEarData == listOf(false, false) && islandWindow?.isVisible == true) { - islandWindow?.close() - } - if (newInEarData.contains(true) && inEarData == listOf(false, false)) { connectAudio(this@AirPodsService, device) justEnabledA2dp = true @@ -1663,6 +1582,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList private var isInCall = false private var callNumber: String? = null + // --- Head-gesture raw-data capture (dev tuning only) --------------------- + // Decoupled from GestureDetector: dumps every head-tracking sample to logcat + // under tag "GCAP" as CSV so the nod/shake signal can be analysed offline. + // Driven entirely over adb via the GESTURE_CAPTURE broadcast; no UI. + private var gestureCaptureActive = false + private var gestureCaptureLabel = "idle" + private var gestureCaptureStart = 0L + private fun initGestureDetector() { if (gestureDetector == null) { gestureDetector = GestureDetector(this) @@ -1670,55 +1597,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } - var popupShown = false - fun showPopup(service: Service, name: String) { - if (!sharedPreferences.getBoolean("show_bottom_sheet_popup", true)) { - return - } - if (!Settings.canDrawOverlays(service)) { - Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW") - return - } - if (popupShown) { - return - } - val popupWindow = PopupWindow(service.applicationContext) - popupWindow.open(name, batteryNotification) - popupShown = true - } - - var islandOpen = false - var islandWindow: IslandWindow? = null - - @SuppressLint("MissingPermission") - fun showIsland( - service: Service, - batteryPercentage: Int, - type: IslandType = IslandType.CONNECTED, - reversed: Boolean = false, - otherDeviceName: String? = null - ) { - Log.d(TAG, "Showing island window") - if (!sharedPreferences.getBoolean("show_island_popup", true)) { - return - } - if (!Settings.canDrawOverlays(service)) { - Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW") - return - } - CoroutineScope(Dispatchers.Main).launch { - islandWindow = IslandWindow(service.applicationContext) - islandWindow!!.show( - sharedPreferences.getString("name", "AirPods Pro").toString(), - batteryPercentage, - this@AirPodsService, - type, - reversed, - otherDeviceName - ) - } - } - @OptIn(ExperimentalMaterial3Api::class) fun startMainActivity() { val intent = Intent(this, MainActivity::class.java) @@ -2174,8 +2052,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } catch (e: Exception) { e.printStackTrace() sendToast("Failed to answer call: ${e.message}") - } finally { - islandWindow?.close() } } @@ -2200,8 +2076,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } catch (e: Exception) { e.printStackTrace() sendToast("Failed to reject call: ${e.message}") - } finally { - islandWindow?.close() } } @@ -2215,6 +2089,16 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList fun processHeadTrackingData(data: ByteArray) { val horizontal = ByteBuffer.wrap(data, 51, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt() val vertical = ByteBuffer.wrap(data, 53, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt() + if (gestureCaptureActive && data.size >= 55) { + // Raw orientation triple (bytes 43-48) logged alongside the accel pair + // the detector currently uses, so we can compare which signal best + // separates nod vs shake. CSV: ms,h,v,o1,o2,o3,label + val o1 = ByteBuffer.wrap(data, 43, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt() + val o2 = ByteBuffer.wrap(data, 45, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt() + val o3 = ByteBuffer.wrap(data, 47, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt() + val t = System.currentTimeMillis() - gestureCaptureStart + Log.d("GCAP", "GCAP,$t,$horizontal,$vertical,$o1,$o2,$o3,$gestureCaptureLabel") + } try { gestureDetector?.processHeadOrientation(horizontal, vertical) } catch (e: Exception) { @@ -2449,6 +2333,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList val externalBroadcastFilter = IntentFilter().apply { addAction("me.kavishdevar.librepods.SET_ANC_MODE") addAction("me.kavishdevar.librepods.CONVO_DETECT") + addAction("me.kavishdevar.librepods.GESTURE_CAPTURE") } var externalBroadcastReceiver: BroadcastReceiver? = null @@ -2541,15 +2426,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList ) otherDeviceTookOver = false connectAudio(this, device) - showIsland( - this, - batteryNotification.getBattery() - .find { it.component == BatteryComponent.LEFT }?.level!!.coerceAtMost( - batteryNotification.getBattery() - .find { it.component == BatteryComponent.RIGHT }?.level!! - ), - IslandType.CONNECTED - ) CoroutineScope(Dispatchers.IO).launch { delay(500) // a2dp takes time, and so does taking control + AirPods pause it for no reason after connecting @@ -2652,16 +2528,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList // isConnectedLocally = true } } - showIsland( - this, - batteryNotification.getBattery() - .find { it.component == BatteryComponent.LEFT }?.level!!.coerceAtMost( - batteryNotification.getBattery() - .find { it.component == BatteryComponent.RIGHT }?.level!! - ), - IslandType.TAKING_OVER - ) - // CrossDevice.isAvailable = false } @@ -2964,16 +2830,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList if (!this::socket.isInitialized) return socket.close() MediaController.pausedWhileTakingOver = false - Log.d(TAG, "Disconnected from AirPods, showing island.") - showIsland( - this, - batteryNotification.getBattery() - .find { it.component == BatteryComponent.LEFT }?.level!!.coerceAtMost( - batteryNotification.getBattery() - .find { it.component == BatteryComponent.RIGHT }?.level!! - ), - IslandType.MOVED_TO_REMOTE - ) + Log.d(TAG, "Disconnected from AirPods.") val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter bluetoothAdapter.getProfileProxy(this, object : BluetoothProfile.ServiceListener { override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/GestureDetector.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/GestureDetector.kt index 9892096bc..d9442ddbf 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/GestureDetector.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/GestureDetector.kt @@ -367,33 +367,44 @@ fun startDetection(doNotStop: Boolean = false, onGestureDetected: (Boolean) -> U val requiredExtremes = getRequiredExtremes() Log.d(TAG, "Current required extremes: $requiredExtremes") - if (verticalPeaks.size + verticalTroughs.size >= requiredExtremes) { - val allExtremes = (verticalPeaks + verticalTroughs).sortedBy { it.first } - - val confidence = calculateConfidenceScore(allExtremes, isVertical = true) - - Log.d(TAG, "Vertical motion confidence: $confidence (need $minConfidenceThreshold)") - - if (confidence >= minConfidenceThreshold) { - Log.d(TAG, "\"Yes\" Gesture Detected (confidence: $confidence, extremes: ${allExtremes.size}/$requiredExtremes)") - return true + // Score both axes, but FIRE as soon as one clears the threshold — exactly + // the original responsiveness. (An earlier attempt required one axis to + // out-score the other by a margin; because real nods/shakes always bleed a + // little into the off-axis, that margin was frequently unmet and detection + // stalled forever — the gesture never fired.) The only change from the + // original is the tie-break: when BOTH axes clear in the same tick we pick + // the stronger one instead of always preferring vertical, which is all that + // was needed to remove the old "vertical-checked-first = always yes" bias. + val verticalConfidence = if (verticalPeaks.size + verticalTroughs.size >= requiredExtremes) { + calculateConfidenceScore((verticalPeaks + verticalTroughs).sortedBy { it.first }, isVertical = true) + } else 0.0 + + val horizontalConfidence = if (horizontalPeaks.size + horizontalTroughs.size >= requiredExtremes) { + calculateConfidenceScore((horizontalPeaks + horizontalTroughs).sortedBy { it.first }, isVertical = false) + } else 0.0 + + val verticalOk = verticalConfidence >= minConfidenceThreshold + val horizontalOk = horizontalConfidence >= minConfidenceThreshold + + Log.d(TAG, "Confidence — vertical(yes)=$verticalConfidence (ok=$verticalOk), horizontal(no)=$horizontalConfidence (ok=$horizontalOk), need $minConfidenceThreshold") + + return when { + verticalOk && horizontalOk -> { + // Both cleared — pick the stronger axis (ties fall to "yes", which is rare and fine). + val yes = verticalConfidence >= horizontalConfidence + Log.d(TAG, "Both axes cleared → ${if (yes) "\"Yes\"" else "\"No\""} (v=$verticalConfidence, h=$horizontalConfidence)") + yes } - } - - if (horizontalPeaks.size + horizontalTroughs.size >= requiredExtremes) { - val allExtremes = (horizontalPeaks + horizontalTroughs).sortedBy { it.first } - - val confidence = calculateConfidenceScore(allExtremes, isVertical = false) - - Log.d(TAG, "Horizontal motion confidence: $confidence (need $minConfidenceThreshold)") - - if (confidence >= minConfidenceThreshold) { - Log.d(TAG, "\"No\" Gesture Detected (confidence: $confidence, extremes: ${allExtremes.size}/$requiredExtremes)") - return false + verticalOk -> { + Log.d(TAG, "\"Yes\" Gesture Detected (confidence=$verticalConfidence)") + true } + horizontalOk -> { + Log.d(TAG, "\"No\" Gesture Detected (confidence=$horizontalConfidence)") + false + } + else -> null } - - return null } private fun clearData() { diff --git a/android/app/src/main/res/drawable/progress_bar_background.xml b/android/app/src/main/res/drawable/progress_bar_background.xml index bcf1c41f6..5fdf3e5e8 100644 --- a/android/app/src/main/res/drawable/progress_bar_background.xml +++ b/android/app/src/main/res/drawable/progress_bar_background.xml @@ -8,4 +8,4 @@ - \ No newline at end of file + diff --git a/android/app/src/main/res/drawable/widget_background.xml b/android/app/src/main/res/drawable/widget_background.xml index b9414e08a..613a03511 100644 --- a/android/app/src/main/res/drawable/widget_background.xml +++ b/android/app/src/main/res/drawable/widget_background.xml @@ -2,7 +2,7 @@ - + diff --git a/android/app/src/main/res/drawable/widget_button_checked_shape_end.xml b/android/app/src/main/res/drawable/widget_button_checked_shape_end.xml index 89e4b9b72..1b11a0cec 100644 --- a/android/app/src/main/res/drawable/widget_button_checked_shape_end.xml +++ b/android/app/src/main/res/drawable/widget_button_checked_shape_end.xml @@ -1,8 +1,15 @@ + + + + + + + - - + + diff --git a/android/app/src/main/res/drawable/widget_button_checked_shape_middle.xml b/android/app/src/main/res/drawable/widget_button_checked_shape_middle.xml index 9e108c3eb..28f709fe7 100644 --- a/android/app/src/main/res/drawable/widget_button_checked_shape_middle.xml +++ b/android/app/src/main/res/drawable/widget_button_checked_shape_middle.xml @@ -1,8 +1,15 @@ + + + + + + + - - + + diff --git a/android/app/src/main/res/drawable/widget_button_checked_shape_start.xml b/android/app/src/main/res/drawable/widget_button_checked_shape_start.xml index efc3f2dec..1aa700dfa 100644 --- a/android/app/src/main/res/drawable/widget_button_checked_shape_start.xml +++ b/android/app/src/main/res/drawable/widget_button_checked_shape_start.xml @@ -1,8 +1,15 @@ + + + + + + + - - + + diff --git a/android/app/src/main/res/drawable/widget_button_shape_end.xml b/android/app/src/main/res/drawable/widget_button_shape_end.xml index 83818448b..659ed8bdd 100644 --- a/android/app/src/main/res/drawable/widget_button_shape_end.xml +++ b/android/app/src/main/res/drawable/widget_button_shape_end.xml @@ -1,15 +1,15 @@ - - + + - - + + diff --git a/android/app/src/main/res/drawable/widget_button_shape_middle.xml b/android/app/src/main/res/drawable/widget_button_shape_middle.xml index f81adc42d..a2996276b 100644 --- a/android/app/src/main/res/drawable/widget_button_shape_middle.xml +++ b/android/app/src/main/res/drawable/widget_button_shape_middle.xml @@ -1,15 +1,15 @@ - - + + - - + + diff --git a/android/app/src/main/res/drawable/widget_button_shape_start.xml b/android/app/src/main/res/drawable/widget_button_shape_start.xml index f3958bafd..d95db60b5 100644 --- a/android/app/src/main/res/drawable/widget_button_shape_start.xml +++ b/android/app/src/main/res/drawable/widget_button_shape_start.xml @@ -1,15 +1,15 @@ - - + + - - + + diff --git a/android/app/src/main/res/font/montserrat.ttf b/android/app/src/main/res/font/montserrat.ttf new file mode 100644 index 0000000000000000000000000000000000000000..92b1284e8fe4134480ba0bd034693f5883edbd59 GIT binary patch literal 744936 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

If1C)4Jw+|b4TYt=WdVIKH^jT_tI(FaSQKr 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�t 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

WcT`P$D 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 T4{$%x?ld|Gy+F1j{AB+>;v8Rv literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/font/roboto_flex.ttf b/android/app/src/main/res/font/roboto_flex.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0abe2ee29292f1b39f59103d069feda87cde585e GIT binary patch literal 1775480 zcmcG1d0*5B=_9kXXe~{bEDtB@Av)Vhq>q8InR0KnP;BondfY2du4cT(}f4^(H_GemWPTKZLRm=_hkI` z%^N?tV0OmnUj%`=2!idoaj%UWKDRD=v>-US34(U)_~8W;#LtASfL{Z6&-~%zU-{6r z)lY(OZlEC8I8AtM(&SGzv>7J|S0)HTV2Z$aCkjIF&(h~Zo5ws6T3Mk&5Kca@3u4!w zb`5%0*`eQQ?a*W+el`&(tHmEYryZx*2!i!MWrwoX+JR$I@9|Huiq+sVNze#`1*;eC zOK4S+rjcSn24B|6AwcP&Ud6A4QLGtLQ-W|+ur_20UUN|C#gM1=?wu;s2!cVc($Hih z9q1{LP3)fJChx_?Q;@_WA(zwx?_;&laY8lyTgcT|3GtF3IBFw=Bs8808gl7TVJ3>N z6d^?6UL;Kt^6BS7I#R?PAmLS~|D&*e6BsoW@kd~l~#qT-zy-%p1?!q1^LvWQIp}wzBp;2(JikC^! zUgPyT?wu5Jq&0$zoF%LWK0)gMT2A4)GeUD|yO4l>6f>G}&6HN5p0z6wXW-1i?-HrK zkd14$6fb0hR#*OgqEG@pEg+8#nYHvx-!(8=IS7#whxh>wI!x&$~U3QNS@ zf}pV$T*dQ(1HM~}SA`V7#M4aSJg)9&y947pt_di<(gMLo5)41${+H4nVJhm5Xr>7M zVu`Q}-=jdPz<%TVDXJId;TlQ*5W1nnp${Xa-G+%*iJ>$QzPl-?$90lr0Zo1RZ}6XufvIr*uL91^ z;1%Qbe<+%JsAv2K?=1^>Z-??S`2Qu!eklj{O}uCPw=7Jq*l+OPvM~O$abj04YiR!; zSJrp5&w22lu1qd5&aD6J%K5`+WqkS{u1tpcbN_aQd`lAzpUL|KjWkKH1CI;P&r)1( z<647jF|L)k-oW)98w<@?VUKu6SYXx($feW?G7sH=d=55VCm63A*i}3zuqz>t44<&^ zV?2EEsRJ3yGE9gcoO-AK}r-p=fr;V*MvjBPdfRj6nZB~(~V6tb<#U{_MW zr(41XYeA^c?!@m=;D>44OnhKA1MS2Bv&uW1;%d`#xKw~p1aJ?8S$!o zG<;>`XI~>9Mw|Fy=BJT=FFtSjZRSTjY|%iWQgjgB$8|k)^BZG1B`U%Pun|QlutP+L z`VF`?L2*VIX@o#8_c43cCh~4}|V0J;fG6p5}~DZaq*)qRTO7yTcbK62_qnApMmc;xr)! zb4(a;I-vMMzy8*&6n1FgYvOlv6gK`E6gI!PH)#c(8U&qZ^H#Cp1mt4^zIR~1QM#bS z(xZk4uqgy{x+}MBJp@u%ncVVGue2M)qyy#$3l!(<(8 z5Q6W($J=-xzYEzM%nIrpIX&=Q_%%rwCnX63q(azFIHt@dp&0Oe2+An>3;aTzkZ83A z{f!3?;9JqF@FA`UEu~h1o%pBWPuTe=X@Za`PBiFoKM&tynC@`>fe$5x!XL5~S~EG9 z3k^@1Ed-w`G!=%YOlBd+dqgKTr(-UC1o%rBB%?niO@`kG7)^J;!X7f;iajqeTMhZr z1Ond~p@rsa@DzTP=1t7;rvRUidf?*UGvR-+xfJsNYYU}08%ynD!CHG4bWFs3;5O0B z>1OR1Y3KCUV7@l`AMC184EIb0a9L=FwZ#kX18aMyV9oo1zOp{>V(bH_hxOqiXnCQS z`oi&z6NVWZ*{OzN}oX(#Loxp8;Q102dnz_^f8Xj9wv-rMeKwz zVLlKmQ-z_BwIy_*VS(|wQ_|s?ZkvjfDhz^uTVgH@7b|)2 z$FAeb`eC9k58oMm@WI3-f;+QeFBVQ8{Ay_);8KNInk7PsW*fer6x`+gxVrG?lQpTh zzKniOgsw4s@Jo!g!hz`{WtvbI@U_|9NWo{pX^J>5OM zJ^ej9d4_mKc_w-G^_=YauIEAs}6&qcq=&Hd7Ox_G*Idh&h+dq$f3wZ(Hg`nAjR1J5eYL!L*C{VHzM zuiSt;Pi&vKJn?=K_@sTEp!Q3gZ+L9DVHjaJPChi$kgbM~kcUb}Pc_O+YWezJQ18OnrfuU*R*glot= z;dAIU?bZ5g6Rx{n%fH&}YSXJ$KVSa&lb>rY$J$P{eckp|+cCDIwSm%y;@%gsJSq_< z|BV)*>R<0{#vN8Fh4aYX1frm)WI3sT93R7oUnEz_O>&PsB9AfhP_6&|54j9XR|s-q z|NS2tDm)M#3XkYZWICB4?j|$E*Tpx*LUA^IL!3mX(^+&bT~15r6FQkrp;KuAoknNS znRGUtLksCVI-kBp7tn=tF zb#y)5KsVAdx`|fN59r6VoW4u9(rt7*-AUi0m2?l?OZU+a>3({E9;An8H9Z2G@DV*G z+QJ&xi#pL!bP`=fH?g(oE_#aIq7N*DpBMlu(MD`5wiBJj7|}uOD#nU&V!W6jb`ulD zB+*Os5ZjB%^eZuio)c5)*J5{iUQDCkh&|{9F`a%ZX3&dbPx_tM3tHNnelKRy%VHn; zgV>i|5&OX+WznC+Z2B{OUu-G%r<=v*bc@)6?h;+-RdE2_Ew-YEMSprt97t=#4)nMf zL{Eqv>2+}sb)m!Q2=NX2ia3S3(w2OD{RQ=JBC?Qkgfqew;zzoWY%+zcBfH57@)h}o zJffboGaU@B9iVs7&o1ID@qqY)XprnBKPgh0B3+dwIZA$AUM=sIPssHeoyJ2GqZz0f zttr=>)+kyp?NIGv?WfvH+P|!vtom6^wOV7f+v>R0Ijb91e>IVs=$iCv@>Y`%o7}c` zv5vKV)B0WOkF0-fDmS%jn%Xq4={rqpo7UU3vdOlYZnMFr+U9Aqmd(IG%O<-tng6ACAwRWTzHRU7gaLvYduGjdgm%X|7Y1)5lI@M*CDQBTqnDhxNdUY z<$A#NgzH(??_Ga!edt=>QrlA3(!FKdmZ2>ZT4uBy*fO`}gqAZ~E^PUB%d(agE% z+A6(O|5n3WjcZlVYF?}5t=6{M+GQ&n)L zTAyrvuJx7HcUnJo7u;>!UEF=$JGsZWcX#jSKGgkH_ZjX>+)LdzyYF^C?Eb0yIrl5> zx7{DP>piSIbRHfa?L5Lg5>B$3l-)9veO0^QiKu@%X~yqQ^Cldmevz zYCWAWD}{O{duDkK^&IQD(X-O?gy&D554@pQQXy>5H` z;q{l7!CULy(L3Bb&O6n6gm=Dok@ptw)80RL-|+s;N8_XO@$hNq6Yi7f)7$4Ip94N8 ze7^Mg-sh&z1D`ryt*^bWyKfucP~UjpOy8G$NBd6no$b5W_Z{DI-%8(uzMuJ?_x;iL zj_;qoieD2y2R{$Lwtk_0@qX!k{r!gfjq{uC_m0&{yY5-`hVts(f^kJlK>K66W|iy6VNdrDj+4G zPr%Coc>$9H<^(JaC=GZwpfccKz$XD`1HKQq8SqEIUx8AfeV|8Rm%y~ZA%SlOZVRjp z{H={+n|5uY+w^WTq|F;`mbBT}rn1csZJxHZYn$G7N!!|X4(K!r9p-gd-C;|IJspm8_@cuP9exY44e|;~3d#+d9ke27ebA1e zgF)W}T@QNLQR>*SV@$^Z9rHVubUfPeddELHkxp)%0z0L4%Iq|#)2p2pb*kv}VW*>= zzUXwT)3eSVox?gOcOKt)dgpDOt2!U;d@|TBxOH%w;LzYf!PA4!hcpWb3z-t~aTlvD zE?v5H>Dy&Um!d8kyVQniLcK%#hpq@M5B)XFJFIP3SXgXWN?2algs@p*Z-p%jTNn0S z*w112!XAg!hr5J(h6jds3-1v=Abe=}=uzZ?E2!YksXh`fl!5i283 zL>MBqkfs3(e0wM zqlZV2k6sqNHu_lfspzxO7o)F7KaQ@Ckz#COqGM8G`os*5$&1+@vpeQg%-ybHSFf&V zU9-DR>$<<|(^&7=F0pa3vtt*>R>giFTOa2V=NlIjmlQWXZhhRzxIf}`@vY(q#+Ss` z#Q&DylHi#Tmynv!H{qRxcN4C66T78%o8PUt+v#riy8WFPoY*TdCvjBb#KdKZ)rr3+ zK1qsBN=)jVG%e}9q-)8J$q~r|l1C(uPo9>1DEUf?oRXN*J7q#jLCU<8XYv7-P?5U(!F2zA>9kRAL;&0_aD07>i#T^q%}!%OY=?Z zm==|moR*okGHq?z2Wj7?-Roi1BdAAIkHI}w^*Gbx@ANL|dFd0<3(~ixzn^|9!y&^h zBP1gwqi@EljExzmGVb-X?b)tpSkGQPU+Y=g^V6R9d%5=V>eZ%K=U%V$TG{KX-qyYC zd&l*j*?WHP&Alsn@9SON`&#c`Gd(j0XD-NmKl4uJpM9eHB=$+~)347k-tR2*Bkcati9_Ct!MJ!?<*55=yD>N0F!_V@v^I}CCLVUbP z2oVGIG4!NmE^>p%oR`35XV9xdAzqg?I{f zJ`Mt-txH&Vc$kszjA#s@xibzLtyZJSe13&UWLTwAUsbqaRZ;n{$u;ZOpPx9E4*p5s zcSPoh?ChMF%<|GJWagf0q?q_C%GD+L`I`sk#*`Nn?yWA$Jyxi96T24_rljNzNr}k~ z&K+_c6k!ZWdp?H75u8=(TB{Bl?#@&)=o;jsn1qG;Q@hX>_Tix#!OpRTK-bLt@yD5G zl)WTV$*9U8Z;)sgl14^nR1H%)D+SidV#N~}e1Rce#h?olfItXvbg~Bms`WjCNB5p# zXq1d$h%=^IlSSmKVO1H*TIHaN@~N^k0~n%cM^OO=TkuRK&N^K<@e7c3K03cB@|N=V zx-Y(1r~FOcidI(nAM;mMNnQh$ixbE|W#|iH#6$U|r8x`0 z4y;*wBIY;)d>mUk?V>+JJu7ji92&J%xXHA_QlH8b!^#S(*-@H{Wnwa)PWh6IwB8u@4Eh54Gz;eY~lo&Q29`J1*Gye_y%${Q2c8 zf1g-TRJ4Eob^?JqysA~{fhi@Okcx4n$fvXnRF^DC}7i;uEWc8%-C$*m|e;nIZP_S@p zp8oIc*dzg z`BSD86f9V`yr^jY`~qvV9gH4jfzyy9)@R~~ZZTVF=SVM9RqFdo-Ugwkq9I3Yw`s&+&%AINOm4* zasy4Sf}ha&KaUxVm7_-EY(b>X;(H#^)z%Wb$A%z{74QRR+UaN0W)zbdz{s%0819N=VKJIw))o9LdX72OG~Sq{RP)dN$3I->;}17R&~<_7baEB};Ej2CTTy0F zaNzFAwb0WR$^>PC{s6Tlc}2%kx}7LeE+`X_zRkklg1tqVSuO=R!KFFJl<$unS{9h; zp4;m6l+0{WTy-TkGc%Xu5pSr|E&XO?F#fg-YQMoZj4VKP9#NE0%A;G#%^?LeD7(K}L(}rL*%=Z74B)sUXVt zL|c@fGrj(b;(3&GD=jK2RgMwg()sgCuhbS*dP<(91+~@B?^Eldf`a9AJ2^WgKYu19 zWfE*b9mc_f+b3{7JlqE(j`@hW1tzbH56L7l2)>F$>>WrAvE5r)b^FeT2diq7@0EJ| z-9amqy~-luj>%zBHSr}_I(8zq2bWq~pC+x9Mao`qi1q9?de%hHv3{uv?S&p;8gz8T z0J8+83wc9Xs@zdBYaX0`_Ut^FNFoN42r}{ceQC0C_vzrL%3WqN7_JoH!hFo;GCO0b z5I5+3=@k7EdcHotjM6f4q09)~iMK4kQ#8TW(2@F$^i=(HI@bhv)2E__{!~+=_pYJk`eE{3{c<{y^?MIKUIR2d z6z22psj0ya)Vmt4iyz=CWY1`OfW|b_x*ERx(onNv)~ppZ5WZVa&P&?5XGNlkJV;qhT{Mli(0L`0++vU|Io_!g@(G8ZFd0fcR7#ppuow#fFq^CuKG+8Y_ub z|E*-&I9gd*Ioc?he@iXh)7|l}?pDJ$d2iie_XKzR!@`)6kJe}r7TvN%#s(*6V>?F) z(b{@D(6?*x&C15dK|j7$yaU9pG4Ayed;}NuyND$L%A`R~fHbJynrepFWR0|kqKlMEU&y=IN( z?!`YSCgDW;?^KI33YCqs{u|o$gA0jnmeFce8+)LR04z8Q*c$qv$`omNS5fXN{xzf} zJgDtt*hOXjG1~XRi4%AAvDAOVvOQuCBCH2>hXKVk zj{Ci4jkEJvPeS0Ph147_Ej?UAE!^CAnDP0E09 zgL zHPytmNi!IkW=)7|H5qm6zLk~KMA9}L&n00x;wP17bQJ&2V2gLGr0TA-dN$5$)@*mq`nX%Ic;AwpjU`j^kAG zxQ>lzF}VWQ%}4Q7J};NImxHU8ahr(#XIfsNo zFJNX7#56>6j`+o#_z*SZ$~j+JxXn*=wym;v`1;tfpAU~NC>U}7q*7g6ba`KXejW)c zugFaZ%yezheo_LtT6N&aELlz%Idy3FeWf{LS7)yoh4xF3?dXcm*$bX*{F#fxg4j?N z#A>v5J~}%!dQzJ#*=eN-+?HSa;6ZKv`k?A+kD^@Woa7cQP8dwu-}1ag+V@VN!|EbP zWlH|#a?kS1`QS}9=Ca%9BeP)+h?5s08%465U3hJa=ay@Q*|J=8LpgsR^k;*Aiiv(6fn!2)W^*ocEUBZj>iIhJ zIp04UbZ-mPSnaG1T8~~FW2AfVP7vjma$#`7VvV3LPw1^&xaG-+Pf@ba4>rE64-YK+ z5FWstRd9+&myVDi&NS8s`AO;5!37#&q%E#wq^@yC7!A{oh-8{D*+eOoqi$M>? z&z;WY=4zx;E2{&$m7kU~t|%Fzf_-D|LVNI>4XJOaoe#4lj3(^OM>-u%&(Xn-F)~sWN%RULZ?|GjDLEyJs z<0^1%3ow_!c6J4A+y?1F>>8OM(Ds52Qgzp?$z~g*45Mxdy^UrFs;qajutH2HhH;(H z@n}F*fzHftHL1lvO$AJxNfBN&6}=PmH|3*74R}%NX&Lwgw;6sbA53Rhb7oFCpWSEY zv~2HS-N`AZkXa&nGi8V*RZ7+)yDfxFAp0h6#?#D4f+xg8_kT~CXI??7<;&~ulor|Btg*I*;-@m-0YW?B)Q^f)23D$t7f+MHvH188T;1s1Z#xu49pC~Km9;7mk}5oh<~)I z;Tt%J{))%Yu2!{F+<7v4Q4Z%x*QR9VdF8vN%6u}@igf)}nP8=SM{d#xr7gLruT|V= z3b_dStzoxW-;vSfRy!=*l5&D19wyCzV7J)7f^ETM*WNqLoSSH@oNS*}kwsB?Demzt zm0{M_8;YczATPz;9ek;(`<3h&W6jQ-rGUzEU~lAMrHDED`E*GA8JeZvx`rCwTBFCN zJ0V`FrDbvzW)Q3@AW;(@<{uCi77*Yc782@Bot>RDK90doSoE-`WhF~n2Rn2hF`~Oe zaOaR$i{+xR~1{ zE{(`RD;=mTo)@uO85~!2mCqMO*~e-X(kI#u9p@+XemO2WyGO#zjvZ$u^vI5m8{9i# zen;vMli1e7Js>LNig(lRx?bDVh4V$n@Mf zy8D&!%B@;!@;9?B5^hPp;TnPG4WY z9@C$I^GDKj*%zF0<}LXM0gPk39iHw8U>>8I&lmgxw9W*@FFZg?SdkCSD?E6xaNePV zB=GcU5~y4{eOkFB*-ig^y1sha=hMXLGd`c8--mCscEyPkD^{L3vHBviYXKL(V z|Cf;cd0h4pnIcJJr}ME3A@dI459TUu{WEQO{WHK#RYd7E`2xlv4tZUkN-!^@YD2=9 zh&BYx;tLV9maXAfOd|Bd50R^Sa;SF3^rC6=$To8I_~+NYKfZs*+Wd8!4Z^*{pD*9} z#@JI^iQOlkDEIb$v})#SZyngO`$$y$z?VjjSe$#}6d{NAR;`bTo&55EnY3_6>BduC z6W@Gg+-tMS>Fu-M9R9F6Atg7n@2jkbN2HTtvb>s)t|^bjwLvybZI~91M1+P#M1+M# zkUimHp^=fHVc~N7c3r!+>ku2;!Fa`J_#4J+GUY!oCIJFFA!|&AsiG31>X>oH^iz{M z{1c~IypS&8pOWU~k*`k8e|6;O11FChx%%_2t-sYNWEyEgtjIKFq4Kx#m$I+~yWmnt zGC8MoQjRFqN@walXz1eUv!^W?G;rOKKW+xxxKpuf8Sx~Glmg|da!r{`iin4@S#iTA z%pqhLxvF@wzAvLyVhj0G$Te2dh$)uyF~$qVB$kfFbVjOS&R9r<4$&cLiK*%9E9WfS zWMjL~*`f8uoSgXh__yqx=Djj?&W`uQAtb9uN?3z~Yc~Cp|Y|{PJw2ti} z2l-FvJZ$Eu{Ap-Zh$01Ix#-1mdW40zA!2#`I$E_)^xCS_VXfc9Q>J}w*y z`Q1+!5-K9Mr!E7sBnsvy=I=IhIf>R%$0u}866pZG`u$AAc@AX!oJV8{jUnHZ%nqLbyp zYZ&L}i8XWuod0kq0o2}N+kz4f-#p1eYTN!=bu=jeG{1wYRv1iPy zEimFn+or{d-Tum&l>>*ZS+Kl}>?ElP(Om+Ql&kb`$?TppGRMs=o0r}rxRckQt{5vK z#L?Y!Jun6Wqe**=;{g$BSb@aSnt3}#+_tw0_jXH)iQ8T{b;ri^$mFp2?paxB8S%;8 zGQ~AhCQr-nHrUx|aLD*cg|D;=4sPEmVmb*9?c61Vi7a;RpQOKQ;yI6Dcge`1q}^J% zWq+a@gY3#hc5zLq*|hG?-N1-=#pIb8Qhxmi81b$U?l{#1?q&NJ1;q3Z|b z4R)O`xjj!JpSSWsTw&q)Q6a*RFZszIB5R8EU)jz})eP8Nh#UP;`QwwXzy4$Z>0Z4` z)9v!+t(WQ#Xp)~D(IkU6d+8LiQ_RJPH}D3EWnS^4znhlk2+3B$=>>Rfn3?^wDyN)P z0~t;ZwaJR^Jze|x?%9!U4j*X!I)`?A6=p8OdMF#y8T2rDg8U^+wl17S4)Tmko7*e< zq$54`ABc|Jrq7@siUE*4W0b@lc;h~o#!=FY>avXo50z~^yk$^MPR^h~g9k~vb*E0P z>$C2&&(;l^F=J4lm!?mD3Hw!M7`~OS$k?wUK&AP9l_j`pslDi_Pc`1Z*yw%`yDv-> z+wuEx=I1B<^Lc2?5ap0qg!4Ci)zH#cA#78HYLS@u@;w&nRKq3|y2#|k5b2+*hEgjd z!|T)kW!Vsvc^Yw<1Fi~ ztj+ZatR8uThae)1&-`r3gKIi6gAI#OB#8c~U#JYdLC=aM`t~$mzlwfdA4Z1n(Lazb z*0nXRPl3`&`Z~4_hZg*SSxb8er+5$88cPcAF|-&b8KG`~R%;owS{pf?_4Y;BD2rOw zClxl1)w1(2H$GGfWos?tduQHiw189@>pfd%;n*>w@NvD&sZr}0HEMn2zraT|s%Q9B z_{R7dm1=z)gNG#d6@C}DNSUx#@TbCIuw<-?9HZCA>D}q&sroikf2WUL*EgFmgWj2` z(m72|&_tN=1d(?Qw?IE%3E=cc80oK2xye=tI5*Y$NH#|Q0UqbeE$SJaDtw%=9%CSV z#PP%czlKJRV$`$o9tP>NVNp2NP_byFF1Xkcb%EH+uHrCp`mMhHH{~vIDqp_5T)E5k z3X-TBpMH9i_$f)n^XD(6KCo0Qt2n%FWtyXWI+Ygo0}(7e!)CPVKt9rOdj2L zyiaS-ewE!LN51;X&@o#+-MZuQtG)WX747L4d}`}w=MG6p?<~(56y7he{cAm5S{xZP zG&yTPW>%Ay@4WNDtT*3@j(w$5>kR*PuMJ%F{vTh|9R;6wu3Hlaek8GelLc)U_te^W zbM3;0T3K(dU1U-FmAQ7YMeS;H?Xrei&17@!3Zzt6zWusp1M(}@LZZqWV=_H}^MY?$ z2y2|$b=I0vyBe*UiZyS6V$K_JFs9q9&zG*LZ4-89RYUG~@mp^aqJNuZe^>2vu&&(N zkfbN^Y6?E3v|{SS83q4L3eWG=^R0%2J@|xqi)GQ#pA^o$HPMWFJTJ|8+Fj*or-oX_ zQ?+(sLoMg2TD!=imT^z5UB+v%3P!duyg2Ox9VQEu=Uk>=M*VFKFdOR^s`Z91!Q8eC zx3PYav7T9!I2qWD^~+d2#sPVINya*i7j`Wn*MaSdrn!bin9j_dr;cjqhb3A+Ie<7{ zRLIoQxy#m;uUb)@eEYrQ24a2wb7HN$r0k*V`sr`?lb3ZhF7g!Y*EWNtky0|Rz1rd@GQ4-FBNg7x@$&t6)blx`LTJZFfO)hLPFQr zIMY=*$XrgTO-OQbNN7q*{fUs2l#ni|see~;nRo}8+C|3pw6qgR7h0g6XXkkT2CDtz zy;f^k|J2%r4Yj;~YV9J6TGkV_b{VhbSt}+JI90^-df~5Zgn3q1V_aFn&SQ9`|I^CS zVQL*%`X5%7kkxgJ>G^MqOXwAEok^In^<}@ZQNthj7XGO9#aAtjG0y$m+Q@@ZuiBs` z+*YX2#b)TPjiB$Fp_jJhxRvJebh+9LolXaE-N;dTv9&xSw3e(T*}R_ZLaF2S-k;MF zDV*c=d|iz5P1WdyT%%R!gTSIfM_EGi5mBKRSwOcpL&r9P-ivw_y2t{$#0;I-2%1@2 z6?!FyW;4=lErV8P{lqD%KHQVfXav-z^4<&(K_@uQe1togzW_~CA3=v&rsp{p&>`d} zypPG`-LUZq1QrdA;oc3mW_)y1TeO7RB1T7wmW_@I+tmV=jg1Psv@@eZg=PFuVPh>| z88=keA{8&j(OYJ<3|33i3FL;+jB&(TpAj14C~W8TDsILP6?dWqZpJ$m_ezeNjiSPR zh_)Pel<dyp zcd|WDX6Q5q4IhcG5o!wg8euTA!BEftc9oC>i0~Hvo3%pn{gyeOJNy1uONO$Ss4S#& zi;UwaTXFs&2a2(Y6252rGknG@xAq!(lg{dqgn3MEf`c09dnF%B2Ct#Pf(Esi#l&B6 z3($~BEe$a|ru~7X00yd>d#bo#` zHNr13!n4htE4aj}@Li4Ym8j2FrHJfVuWl&KX&X+x3ZHHO(Wt+t^kOTS=xqk? z?OKI%L0FIZVxE>sfC?XL^wami^ZttKAg@Pu8~J@t%! za4sX@XIv)zd3}X`C)+~?{4AE#(6ng`x&)Gh^-t7dS2ARGHR1q~(IUko4OuTGg-)~wS&$PmqBuHOkhdELd;TEC0zdunoz^WTTC_SwxN+lU^G2@*?vA21P z>5;_MO2(}ZoQ8Js&kU{nRYnq0`7lSRP*&S&H&SUMcAr``tN(@V_zSSo*1fK2LcAFT zpi67V*Ty43ftr#wIxOX`$bPam|!EF^bO@-C(Wb50gWw2VZ z+Au_wAC8$xp$eVIp(g|L*IWiw%qux&z7vpB5vEcxg0;ujzBv`JLX3)MtX73({*VeA z)d0)!s<4YxSma%>!_W-dLxn};gf(^4s<3M;@G^Q;yovu2?@A8q0=(Ek2-t8=Z;uQ{ znSe~<2-y?05JS+rW;v^M0UzdaZB$_sRai)_KexaPmRX{?oNFA~j9E(sf<2=QH`e6e zkUgQL95)SlwhFasW`bozA~=?4X0uSweRv+v3`D(#sPT*n%`{Ynj%ftVd|(xNNhHIe zLNg6jp}RJMW*VwOFO4!o^BkHAonQ&gqiz*?g#~UlE-G|43+TVOKC95hDzws^?Zh-g zr;~WKm+;H}l-!JzF#mcEujjib`79R2xe`T|@OoERx`$jwa#%eMuF1T~DXphhc`-IkTl}L1)j;DTeyV zSdld^-L% z&a<=g5_6T$2BdV%3g{E~X8(=*A3j_)%{IBYty`$a)3YQ}ocMUeFn5oY?xEFt9xvA~ z{dKce^NzI^D{*f04aPSHhIF07GZ_QCIEvRpbDHeXmO#T;bfYCva-yl;>ba~p*GJ?1pK81Jtb(~0z~deg zjhs2FuP|J<61mUCeGI(c4s@}4B}1;^8n4#(G}do`FE)(h>CM`OFZRfUr^zcE51++_ z4sw+VkJg*xQR{me>q~(DDzE4BxX{x$kE^i+lcBZ@KTT{D9WW{CfqD&DsYVBgfv_ju z%;j`4hPR3si0$?=b9t?Bh}Yv$S-?An)wnu&;u7wn%b~W93;nt~uLSJy}TR^ZH2!WN113Y8Bq>H6X*m;SZu7 zn#O$$d_~NEz(|{xCL5#p0c?*g&wDVMH013zYS&jYPR^v(z%P_03NfxjSuL`@0!5Sb#v=zszdh*G}NC8h$TZY*f zDZKqJkwuabeiz`=HNb9!U!}quy);I5Fz;ounS;!eV!2pIE91$3^wN?VK{MJ_=v5q= z@uZs5uFm+$Y{oZy1xogF%<7E)w(3i8SjH0-Hi>%&z(yZ2s<|%#UUOJ=CSo+Jnw)H$ zEk?85WgIipWYBCew=m2qbdtr~@)O6cLa*Y`Iq+#LJ%luM)`wqh=^?Cs#w5fAu$Hra znmUui|G{oKw8v*=vYOBO+*(;_QOo!oK>G-n4LET?v%$e@z@>Vw-{C&vpg&nLbtiNzuqGJwt`} z#e4KLzpGX6hcK9s=~G@q0UQpXKjI(2v|fBd&f=ZH=p&mWjSI0z^=mZi+l|>YEIO`F zW@__1$-i^((dj*h^^A&6N0N)+J$5I>PM8B=rm+DpB-}ZKMof6J=AF`YvR?UY(Ng@FQ-kwr-DNv2@8b*ZR9lX26KJtAYC#!q;_-q*#P9OOeWc8lv zlh=0t>$CUx$oIPr9T^!qaqW(gScfl?&SP%_KO1M6B_>6tEz5F(X_K-*@vh#J^txuU zy8FQr-g5T?aW5P;Wd8gi!wT~gLP8Q)k=MO6W5!E4bLZv+b?+V&l$?r|bxJQf8G9hG zgWHd9GGbwsF?zx45UAs#>96&P?&)gVyo*;mpI74}<2Gi#Jfd~;=~lWxuY~cPSf4k5 z2P@z;z#CKj6enlPChIT zm`-6}B={_E+S`ENh)^(6#?ho2GK>E=g3Es?NatpcT=?N9yF}T z%VCvuv+kWFx7V83WDkrV?d6*m)CI9Jq*)n5CmT}PUJXm>Y>;N6>yWBsMxMBrZDKcty)oVY$~D)ZI|dI&hp!<;h9? zE|E-A^Nyz7AsDv;&R3%bfUom7AKz3ui*pTcGCsOZP}&S$^dR{bi&}I>hh9}zoJL{} zKMXw+)yD5^`L~7SeMvER*&A)vs@cLFv!{(26`L}o#~U|y&0jKR(X20idr79U$cud{$YY=+*}04*KSzHY|7 z-U9k-GxYWbXz{LgoEdt93e9-Jqg-F~CIA`qxwvX+Bi``n_+8#xOWv?(pFO1&H#KTw zpScZ7=sGiW`M*GyHqhTB#ti+gCA5{T8TaZ2Xshqd(3>rxwUf-yYb<)pyg*;j9YFaC z-amTF`gOvhw-w45cIq6ps;8c+u5dwW8rFXxHNBXO@d$uRjS3rY;ng#l>CW*kw}=38 z&3L05V0o5Cg9AN#k<}Nn_|TzONHHTf)yWdxl&;_sIZJ@T&@_bKaH9WU7;47<1+Q0%4NhNEuhy~K(i52p|@E;f5*p2#l7AFnvIbPz1;#jl#h}My+MU$JmI6npqciA zZmthT`WbKdNPWnAYrz{HZ>vx7oC}^Ztyb{d3y-^5TLEhB1-w$@Zyv>1v}t-e0&TN7 zO?}!Fb2dD^z@mT5DmQN1a(r@_wD9q1c)DSn68MT<^mXg$vb82W?FB=g_7-XFRLLMuK3bW+Pw$ja<1AIs&*E=T&HM-Uz**0h&=_gtnkW!Y(N@ zv?V35X+~&^+%xiPSTi-^Hs+qeQJ#%6LR;`e&Bhs_Ri3CZD9iDyF=)gnMzy-HgvpB< zgD&`$akV+Pjg#hP*wlu2mGhdxYRNmuqjP8$-EwZLnByCuxh$!em#dh0B+RU~3LD)3 z%OyvJUCd!kZx+)CEzll3V(K6X_@699^#mO||4kisGJjX14w`!P4&PHM$$vk$Qtt^4 zbRvCSbTRpb4)kUH7CLb|@C}walSw!Y+ys6YdwUOyx*P`|3=SI8?NvLVOY**XCQv})g=4YmThsj5-rh!#SImHjvC%qK zJX!0A{2JPqU!;U=eUYMRX3{95j9%xI0ZRjI|7@FVeUaL^mehc5dY#kF;!rc?4c|<_Uz59 zi1NhB%88RJ+1@U^w@%YuxQXmE`hmF24tYz+JN`F=MO@zTc0+3i`Kra+4gX|t#1b^df6HO6IGJW>oVny7a?YJcUO6tF%%l;oLX`2w!O7x}{4UF}7^7BLSNgh0x8)_taWhfp?WJSBlZo0f6*WA;UxPDk~o{U^59& zzTZsfTJm}{nV?J*h_b`*E&T|6fG#1XccvfBsvkW|UOeD=#sJjC7`_p^^7m8n^WDy% z&hlKiWlY(1Cr#VA?4{Jymsn8}P2Xrc`R8`e55^B49-lCLxRhx8!g?Nta~ntHOW-IQ z4K}*rILL`T4PEIR#pTGxnaTp%a^psELLrf^>lI4tui+^NM8w#O3w=Rch*Q6Kjuj>t zRW-D={sx^=|9Lq*u8*U5b=m&1x&roQD0r&o6LvmAqce8S#y28pEY8>w(>c2?%|dKm zA2m2%%MRMr;-FnW9JCYk8v0zXQ4ibA*!YU&VLP&C(zPrPTqt4sS0Sb-;8LHXHaP>2Tf%trOD@=McOy zN;xtx4icV_b(Tl>{cG^ii`_ZCs;gmXD_YC8S=8d9gFj;t{f{U%q{frD9kS?5If=3PV4G($<<$!hd2t4_f1tr zCMtJ-vazY!nUkUvr{wPwd#zgaeBu(M4=A(p;zQ6N;yonBGfw8yUI77k8wm!*xCz}6 z=>TV)_(grFsQaR#=kX!c9}JG9J%6ijPl9&6yZ&S4>g&fI4-vPoultyEEiQ9*+59gfHLNf2hmaWzf_;RICjR{Wj1Xg< z^uo?|yo$>XHu=tl@9rF+i)YVWd_Y-QE|xqbqN0CTKTa&!ynFZN`f-||XXhCfwsj~aSP4B(Ec8k)2UcmW~3v}ew+5>+;EbYt3 z6s+919eHDkd3#p-4;B1ppvzXuf2ZzC(@>y=g$4|@Z`w{r|@pgsZ*v+d!tNrt-mck!ke&KMSIlMbd z`c^Edh0>{3udi)Yt0=YNoNe3YxUvs@6U<5!|3}|GO_jVImJmHRy*CMG->{Px=Z)DJ zIQ(@XHkHIiA0tMsw>i1!tj#;SlzJbC;>QYotUt=8DuVwDtcUm+ z{R=X{C5@k=qdne-pL)VDlpfUR*d2DxH+C7#TQ;SgW^p@sQ4^?+7K_3E$Qn-Q6BpNq z6}maHfB(p+{@CI{g$|^J_>K4&S+NkbqUJl*q%(U5A&$_i=eoszy%Y3rC%(zQ-Jmrw zN$OGXPh*k-`BCtw5dAvS`S4Q~=fl~l!a>;k{X2Tooar8BFfIN%>%#HkT#PLHD^8ey z&GsvY^7*H=7hUwT*miHe<$GhMGRaq2>!jSxB!gYZq}GbhmOJb%8h8(?cml6Z)8K4_ z`JIyt(MtV(nqGgl9eZ5=#m_$PM> zmy5~E$AA7=iF^-F5yJ!J96cicz;zf}iaELk&6%KoLTz5xZ>KKxhqA?Ru}%N_V;F4$ z*fT0DX6jJv2qdr59R2nQwCUykVz)QN?%DOH;nyI4DJk$c{e?8{&!kaAL2A8t%A!8Q zqJFnUeeZ^Pjk~!%K;U>F!x{LW&5Zaon*VUWxAFcSoDZ3*JqtQm?jbeO|oP&=|BF1~%6@V0S*h27^*I~F)*%$VF5_crNWVoP$_QU)xwMC3}T z2;aC*M8e@L;Ptl75G{GL^_{x(TYvl^epTOD5*O3Oi|4$-`qvu&qsvUuoAr;$vlmY5 zlVs(Fa=IxkUbTvi!vfsNXHlFn_fy1jIe2m-J_hjJq|A@{){r_z3>$U&5{kc^CsGxt1ad$J^#}Rg|@h1iGuYM zD}7q_2Hk&ImC<};xhjL!bF||D&E^KvF|z-CRYp_yk5w6=9`p)UF8}k&*pz>+jA`Qk z!^#*|2JJASe^^hJzyLfF`A;z<@>;oWE?jB}82&F!Z~OGTRa zhJiqv^MLvPvG*p>Q50L;cvn?V0s|z341pjFfv}9SWo9}H7!d*)6;Z+>YFvS^2?Pvb z5fBg*Q4mED5F?;qL`02>`W%%PH6oxd$|A%KG@|%0%Azk~lIeWEs=kv<7T)ve`=9ea z-}weQT~*zEZ>{&a>=bM(k=JS9(JOwsm8jBKH;C$9+GsOGN?*4)^_&{a2Efwk(E z)v1e5uDZ$fNo&=OV^_1Lox1>d^3B6oS$hL1mm*~-FkFT8v3shoP@~pbAG_+s`wqu- zmFr_q@)eTyv*bV}{{izIj59ht-M!}Y^a~e`N}oS9eBaj#hP8KmCQnayXUi8(yz0h? zJ5d{7nUNwM{22Vs>M5wSMxXf?j?5-`&SZ9XR=xIg8EWt8X86$;3XrlM{N~ zJlT6=0rGs8*%Pa83y^2*kNd3*DJACTSXrBm6zFWuWgv&W!_xWw@JFpN+qFCEJ%SC- z4SOYf$5z>HYr=+zY2mXQ869EWm)>~IlJ42Pi{{<;_pafy`;ELdX<*j9k6f!-yAN7> zWc`u8?dj*93Lj}6y>A6ZI@j{UuEwZv|M#Qf?+r%9e>EN%V_}_narh!t=H#y7eDT%h z#wlT)bN{(TmbnT>dfpE&`1d1h(xX#u%D!#L@{S2j$ECP0it9PZ2y4~De#5n+#>Gd? z35O$}F73mcd8FmU^V6Q^!|o8-;Yn?q8Jj+RvoPo~%W5srk z;_!9i_~tKpqDX}23rtD=m!2qE>E)j*PZW}8(V0oi0>I1Y%%nP%alO*Q_p)e?bt*&g zILWWNaqE3`_7eRL`WNw&(yBoFKpGfZ14Rt`TVqO@i)*wqTxw@TNe~X{XD|`2qo#SS zzP`rH2`X+-*Vj}p)u+LCsN@UCBj{oq5gsn3q~z`)VP@IRmnV>=G+tvv(Xs#wuea}R zY#m+{5xvO~_Iiuc2)`Z?hw!^C0)J!PQ$Mn1Z?50+)b6*Q+VaF}=D&VCX&wmcyuwkj zrQLliV{IMV6s>GiskJM8@B&*#>#76hC*}(*@(owg9WPZMt2*=Bb923(cLL^4b06{> zgH@bxoAeJ6ZtZGqEwzE2x>^>NK$T?ftc7oDrz?*VU8>HCsH#&UDzNp(&*#2-$;8wJ z$q%esxwPLMNwWsLHRH3B;)~7sn=UecG>;6wGB^3+!ym9|AAWJ^#Y-<6Y955qE4}(R z$huuIr1d{?utAtd6R##FHrF&|FFbtlKX;LriYom*#Tv?c{qO& zW}EL>eajmBAn;%mXrfZhQadV}QLx~N6SUGyOO0OTyqccC(;H^Y*igNH zA7Av!2Z2ZT?c>*e@k-#XeXtOe@cp>}Cb5oZ{r=bTc%R5f_|l7wHLq#g7J(TVKX_LwEDS}lp(9ErVKpDvm|^T7=(mWSXa7x(td2)}8d@i-T}Zrjl;y4feCxCA%~!W*vD&uT&W0_Rz4o5@ z53Qd1E^GSbXJ4E>`_Ro6%;wGcTQASdyyA+?%**xWn~RDzPf~v-W#;B)%0H4e#+pN$v}$Cwb0#<& z!X9FO>^WjGRRwFaKZqHGv$=uYg@KNX?z34VBhi*?o2WnzTc2{v38zaQ8yCJ&#|L zcJOP`jct;CP4<{S)%Z2ZW3M;v*CYy-6%PU`XlkwX>6)EbwN`e~GwS*^iDJ#A7Z1y? z$*yz$RoVZoUz6%zP@+>9pAlA>nN&2AUz6$ZYqCI8NxvpN*`v+t`Zei?JeT9UILhB#1vacrXYb?OG{CLVbHfM1i0zwFnfI)ECPh?1cfc)?;o@IEbK#Z_zhR=72R z-1|TAYtjmSOovqgt{E#lQ7ylG@O(i35I3^3Lnm>bi*w+*$*9vOl zMUVibHX>XsHGfj+&?!nE6L_y{On3xI=dvs7#H3}~1-)hXnjG+p|c97&DcS+M>EmBsww-)4Byy>r@MGkn@_Pp!Ku+2MzT8V6CgnoJ0Y|Ok zhsm{etG>fxRee9eZmg3ttgq|4plb*NDoGb6V-0B%-fRusyj(+yZyg_};do^yFV@WD z5`qEuLTzX{37+JlEE87Z{Wsd1@0qV*`Sp_aZ?tEZupw|F{a*X_l?M-2w*OS!ZAI^x zUj+i@mxm9t_&|VlS@-eVf8QgbtA3JqUm!=98G z|V`RLnkKT26w($Tzf)7t*k`%~bbu*>TgeM4Ce+zGIE!xOg zLOd%eWIt4{XN7!zllMP>r;S6iUcT|B=cj&^H-2IhxNz85@$-)_v?(lY9kVf}^#hAr zPM8l2=>N(ej!>KpT-T-8^)6Gkn5q?w4RB^-J(3v!20_t$hey=W}z1Ray$xE`(Vv61c; z853H&+IC23`|gUC7qQc% z6!jZ9>LR7~5o6Bi7SdUmob_OE*t!+|U>=?H&ip%HAO6m^inFZH>Oc3E9gB+H*skT= zm&NiL-yhp?020&>^D(WHTp+YAujtA?E(|0UvX5a}LcUy)VIGyWj9){(9R3J)4uMaD zsxH>N!26LwRqt3QER+DGmlNb%J(aozB^gM4CUPqK30n0jd=;)% zO=WxGV$y217gn^@@}FM5qhityeL&R!qs7Ip4`V$M7xH0+=22E?4q&jOQuWQAbH22* zhxj5?mDJYNlNhWxD0w%2>0Av|01Cz6Tj7sr-NfSAl&aaP%TSH2kRQqIPkk` z>i9_PBAHQ*fv}Ld1Yh*}&O+v{C}SO@YcKO(@M2?D6h6Oc)5CYK-3MFh8<#!ytleI9 zvh1+(@b%cdLm!yG?X@?5eD&fvGZy8ooV!%|d_{g&*?`(eV*jbOWDxzvIQRSoqbuV?)NpR=DyrY5`k&d5(``jZA8sS$MEz zkS^;+rK49L$8+V;OnI0hiyxg}=OfrAQSo}avcQ1Nd8`ZH@mb)Qxv#KL*8E1%Qr0{& zZnd~AO!dK66u!RN+p2Css}zTcMnyNER8W!BqfC52VIzbn*z+35QXvv(eYwbrqi1%< z2>l$xLCH%rmLy$3eJC1+F@e#vudwnp-tAAG@_v)joGCRrYZ!_0o`3 zNg-q^{MBenasU65+=Wp9YPAh@C|9e%IfE2iuiCLw(rbN$q)L`=r-}?vx0RrYvh!K1 z?Rec01M7;kzeriE8Cv))OTP1sWmyO2?A{_~7Z&PG*f#Tq<0qnHHnw)m<(Yt^Mqe0c zo5UMUOhPh??O}rp11A1ULPMMa+(Oa|6WK2b!M^=Z)Mm~wEN7SJ^$h*f!)u{ox4TCk+ktk&_*R~$$ebim6JzhX;SM{GnWgJ z$5v7L{6H)}F=p=pNgBID=xT!VSFf9A%-_u;h3~xR{}Qqm|HVA=&N7>ApLvW;W^=D& zY3z3M%%K)7R_p`;{Y}v$$T*3>D|r_*7;>ay|6M$^%(Y1Cd3ZyKa!fGQ{cwluz`rYi zwhaGXfA>hydnG0B6&>N5H-bl0C_%5zG_rxaH z7(?5urxcpc^?Kt281VeO(EL7s$&!3?!zXO&UOvL!?)dgPF4Hy>{b?U|poabcuH@!B z0{v{Qz;=}^;;{HV#;cs`E8DEFeN~2jXW6oqT4u>Vp#y-dq&TSDvVrV?cP0`qmE69T zk5>7)V?$u8SoNWqq8KRgRd({m9p*XWrh)e0qg8uFf5}R*BT}(e*@@B9M!{YRxFqrf zxfgjFG9Q^#qqaY42?hkf(+3t@{V+bsr|T4OHS^78{<^ZJh304EkX`W+B$XhrO? z>k88!-YEF3Q+B@?ZmQltox7tl?mQ0UHg1%8-q^s894Iv?`)pNvkZ@RiN^R97tkc$^ z)(cUkd+O5b%H@jwXCSvh+Aqs5VMv4tH?-JxFRrh21vCX`WJsspIfqW&YV0_f-ozPlRW78{0LuG4q zmyT3Bb4f%)>6u(rle6Pihr~G$&;xtlWtmLiU#9s2$mc1u6n|ZDO~2ZleBRMY6{8<$mmvbWW6r^t5#)jV@}{kaYvyT@k3y3 zVPNf#;u^SG4K(M!E~{Fm*73aj9|2rd18byWHEw%@W-?qFJW47(qMYy4=2|Zce7z&^ zbrD-eJ|)=_o6KgC!BJNCd}+V+wDJgef2WQs9amg+ z)yNi2*N2Dqy;8*7H{d4n7Pnq`2Aq1ibgn1!CeLO)73KrC_$LY-E;iDfR9cW7j@7Bw zQf~~oMCJ|Zy>8v17ouIU40}0i4kp*E+pO{Teq!3&EaJ7tCao;_aQE-*;Mi3cF1}}3 z_QF-;#=i5|TIne)Z5V6v%HQC1**p0GK!ylZri=L&snnS@il9(2NrE$ zKCJvPfrcuoDd)qIYC`|T=K0yYT5nW4**tiF*(OSB9a2>}0YR3n6UxvByakzutWJVK zt?eZ1EH#u|@`-iSN7{>P#odQk`}HT+4w^nS<(|Ma49K=;p_^pmh~S)w*)0 zo^0I}xI?3CZWY&I`KObtTh*ZO@g#E)%_N8G4!@!Dm^rqaqiXn+?QzaYd3W5IIpS?L zB+!Nr3Nyd>>H4kPmX5llqq)_$^wP}9@V}IG)wM5TC*{XatZxhF-a9`WyW*(};~dEs z8(9(~`cu#c9$Hg*jKSu9smHYRjH0){u75K0zsr<^GB^QKZuMkuRZvuU&V=+EF{U^;4V(>&owUh_waSMao)rnv7m07KQ^@Ul@#FuyHZMb{cx;25{ zd1b)nglVpv<8U(C&)jXUKW?rvcd!4Rg|Xf&3?B6=%mZe{_uP%e2kCFEly|dsJ<8E* zZElv@07nd4R^zPgJ}3EU6K$o_HfTMT;k(-;HQ>LhGCW~oMxdw!;Hk1%=^LvD$#m>Q zE#)3%#apGb2jsewtWnUY&cNSX$vTqpgHl1*S>wNsA?F?0uFuUyqy22Z*@wrmD$~dO z_zFMq4r&TBV0WmKHf0Hfl)i1+>?vy*(=4ocOUKbYX5z(O5t4H^X;dfq? z;r0$l2`tr*Keftkf4oWfoM-w|3+pS9AM`1r7Y#`oSv8QEjGukW<;%K+MNA*pB{@2( zi=$U>|Ee2@F6-ilW2TcACSKQn=%^a<6}aO1<-;x!%~4A-|M1cV+Il^L7~|;4x1%Rx zjMweS-r%6>DtojXOE^1W4XI$iz7PF+^5d60a{uOMckW!f>MPba(1KmFX(eBj`_$4m zPAAlK@2OoPI**4QX#HJNRy-8Fetpv(Tkg7jLqglc_vhrjk=!gg1)Usulk`TOc@C4O zWP@?lq5EWXQQU1)C{ResU9J<;6zTHUZiJNKAi9OixJ%}Zq@9fh?EQc zbH&~3E05#+M;la)t!dgwxWenYZjR(8%HlV;Nu-=>Q2moDS>9SwY9cQf&!EuK0qggt zKdWGS;e++bxoelKBnOU3(vNIs^Z0M(#BATmkG6lZIc_r)PgXrkKeAX439aWKRO}uC zuacy*hH5NQRKwW3VDrOsXYc&*$SJe>@ZQimZQk|iN4vlBf%GW5{Dish`>vm#>TT5} zamU9{M@k;Ds5f{I#;#QHIpmm@>`F7%{D*lMJ}}pBc>CMY*&Cd! z+lh{k{=H9&_2!=A9Z#4)tUtK1UQzBee5Vhz4b^kkRQSHnJC6e=7Ns&~D|MReU zv)%msQ}#$x^VS3G(kQm*fO$F!-=y(*Rc8Yu_?v(sr|R>|>U@O%)%O0%E5u+@c6X21SX_Vv3a-7uxtc+4|a+?K335n6Z7!Yg<{u?bn$l@0q`2 z{43n5t(u{Y&{k{$?x5UQ&1;K1MZgLoq!~Fu#VigxNpIqB!YSq3if)67FABVi^}HLn zgH=lEU3na|{7Taa$jVNhqBMgQrLipQo6zGga!L(qXexS;jAIS=0SLET@ybh4$1P-? z)U%f>{iG%(b;YVfJftn1Th@G)=)^h%?c=gr@7rTEi+Y)9M;>Xm)t0!!eEk;|b`nGI z47Wf2>8Fp!$v@n6*Npdm@7!VjT^&INEY^3R7{YPJRIoO2al_Cweur3pgy6%*?#?OA0*09Bj)pAtHyrjw9 zodQ%qp6G|rDQ7Wfb$PC#&MbyWMPer@jLO9s@#411SGMs$;1jmgA8^Jr=^gjp1?|Id z*n5*XhDC@KRkPq`%Y18Rk4v9YIz22{bY1}^GGgU_nWt1Nyg6D>pZXHfLLdSGcXtJGtOF(;rGcF zk$vH;DmyqSsPQ7@+d!pCsZFRmxB_;0em|x;Faf3Qiil{Zu32wJVwr1|lETL_L zttl5T{cirFZ3~zsk7$eio%~>;$Kgoy272?4`y%AMIAEaWE==jwwr#I|{8O28Sx3?X zF{qsyWwIHdcSrpn_WR9?WS5iDC_EN}npu6np}b^`g>*^9AVm?>Lx75PpdeI0{0L{0 zYsoJqx0Y-f zEDqc6!R%sUq)w|o`)1Lb-hY3SSR(%hN*i!))8fhY1AAmZvgSa0bK!ZII7;R~HsCcG z^>c$&%b<1Bt+r#b*<{1v5+7`P+;19w;guxKs;SE3ppAFp`&aOO$f}OCPiW18l%6ly zUr@AQ%{A9Obp8Bsd;#QG8*5|!bJv?4q92ZFm&G;!4E54-3orKB5LV0&Y-9cA1o>ZY zFIz`m@@?~M*lLAU?#c!_^x&}M=3FYjs-U>Iaz16WB1|1 zyU&_kc=4U%$KQEYqTRJ|qn}MO!)IK3-AtxV&!LywIssNUk%F2FhxLVhxGN9!;!C8f zl^oQ7#n0kvw?nb3{eQCUL%f} z)klsT+d2QQ^ z-KJJD zX)XM+1UeNd4!r%ygdHh<%Psn#ROb*!>^Rr56?!So=oblg=+zN}`GgA1(n3^yn&FEG zTiY}$U$(jA=5kilQI29&bL6X0F*r-1L)Ij;*2$Q@T*Jh-2R`39d&ye-mOztp;9E0r zE01F4S@W=Lt812g_T+#^KeO7(JbZTW>eU#nO%P@4LHLfsqB2&oG%%nYP zd8Pq-enL+L%61L4(cwsfFU%yvn{=OlNHA#2r1OR5Bhs%s>rYW|e0jFh!gQ7D<={?X z7o!EOJxx*$3gJ!Is?wm>i{}GF>oBWU?3#MRAP~a1*pt?n3l0i3uEHh*quR}6aeyEa zMK5YbFnZ@fg6ubYiA6PojJF8<^7@7iuds#Y61FgKX3Fpp*-UvM#C%Qps0q`)4^lxP zc?~$2qAx^|*07=5*icCaL0KX!ibR|#&2~{OXNs4+?pHWTk(>z<;91Enx&sHi*MnZb z)f(u97aYC|RtZm-XKkC1u4_#&6Oe0{;88f;%fglLU@gyC%R4nohPwZ5^VzvK@7$S5 zavjUKWLstHda_+M`(_Rw487S{C2g2}R!f4ox~`0;T@TL;kl7&7v`5+DK|wC0N(!s@ zP`Q$xV9~Esj;t*vGGEq$ljy(L92P3tu+!<=Zf7S$-vJnwUOC9=Ak*Xf{gQ=TGsG;L%javR(B-#{qT;_D&x#k2EvyW(%6IOIDcvCgu@V zHenk(&QmaNJsj9$wx0s01&6CuFsyIG7VRAvE%LOT()A%!yJA;|b0!n@w{#3UMdzwT&`V*GY@UTC z`RuOXY#M`fbygjYY2Wn47n=1STv$z`U(x`+U<{ zwofbO{nVV%%KV1yUya#aNCR;nI}B)dO3np6a5Tmg^Z+#+BRKyCOYRyJjf^A_Jj>R( z+nry3y62~Lb8;qq_GV!zdusZ$duDz#ckZVvA6>Ly!6Nq12PaN^`OI7GboU3h{q4}_ zZFxQgQsQzlAE#Y(1ksD~wmd!97`~^S1 z>!3NpZ;m*~@8X*m%LV$yfm_)v{B5&?_@JsU`!+C0j__D>gxH|>m$TtE!Snec^%_#V znpHDvLL7VQ>hab9Q?!!ABJHP*a@5Yxh^oKg!FX7vie~_g)ZLv`bQN+PoiT?* zM>0Ac=8V6e)_5Mr5bw79`boF@*(|llV^P~c+o88kTgab9Ytj4y*u55XoIJ=tNst%t zk089-fJIhl2^EOLGW^}{8a-|*{r%h4Nt@HcTpMm1vneI42{?2RRn7hSL$8jy`Q}lh zE|WbJWeNKc*uWBLHuMKD4w8jBFV4s77O^qBn5TfM6aa68Jnc|xKLKJ@>rT;$ovZ=b z8CD>_J^1N&UwmkuRxFOSTsk>t)yf&{mKQ$$_=WB7v#VDJ4jo;1>$tn_0tJSguj_!> zTW zgdtGiXQ^fpGwTq)E#G{zB~Jd)d4&ez1qC;HgVL`{os4ClQ!GL^uvX9$RbJf4Nga9{ zAIul0C11%L;?an5LWMPo)}SlW0ch?7nN@rzU+&xYN}#h}Ro?F7>{|6KRR^+@?7+EgAv=K}jSy(h*t zzQflAqQ8oV%d_{IJRt+klssvM-Q9DMBC5;L@=dXyr3Y5WFp6kYCU*x>mGlNAw}$41 z!^DlX5DN_ip39R>vhl7yarotv-CI1@qV?*Wr7N$`Dth{nL%W~)vZwu7`vrI3lDl9+ z&dZx0x=xR-{7GKy>KET$IJHP0zl%WN!QZ9WEO&_2$bLrcW93 zK7(-3XSHlUPzw0SlRJE+#z@ZRfl=;VY|EAxvgK6?;uCp^Jyj`U&qg+6HEL!czByo< zr+Qxxw7OIzs+Mo#Rl6mivBs$q)z&z5-5qn~a99yY#=Q-XfAF!#9-Mi1lY%D4_WV#$ z@x$(8_JSsNzq8hcdDlwYS{rM(_sxsm+`Az6?GMc#%&Lp9$MJ`^UKl^k{Chjq;v=~{jE|%E*gljTT{n77<7eyHvirF@et#SX z@%8)l7U}netE2fc-t0csf@huojD4VAxH^gdua#{sh;3c*mP?({uUO&m+1o&yd=9+@ z`?8f}W|c!y);g941&)qO1BEzK4jB_tX1%yrSJx2X*yzH-s)E-dBVQAj)C95ny{;`n zv45U2SGdSF&u+dyPgyLy+ddD6I%@0$wnks>3jUdh%5KyqU`^+MU7C2>>!h`?Z>t4P%rD_&!*FG`K~V5En! zP~Xt{2yukjMIOL^Dx(lFL*au7|ER^FrkR_VAg& z49Tc&J+L-PF2PO@$<{HDq`pz@R;q_O(EGf8j6$l6=?z^Pap{3b&kt`6iwI!v|ytQYcv$T_Or&SlWUv8(!{Mz_6K{o!t!r=co0os zx@%x{6cZ?Ebrdru%j#&@)M#aOw8>P5vN{?uI9b^o?KXIr{w^3yLtCPu z5Tq?p`xJ+=C2E`6!m=eAWAsv%L?aX8l^xOTu!b0=PsiCEt!uKJo53DtYtg0iC*tv{ ztON0ZWxw}kNjq3lf_W1dCX-eR_5QYr^8HRY9wHApw&}`y(i`S2cw6M{@66`~mUZ5b z_e0=EcLL$VB1Ckj?IQ0$y!A>|7Qe;Zm%z4o*pt0sy{jGu*4+lpF1p5K58IMp?%QGR z>&@qN-Y@d1vi3_$q&EgW4L{OkCghL~*pJXvX&j-XhrXb828!)8mccAcKcgWZMkjh( z3tTsfx5VG82F}?Rd*PsQ+tw_4iGRp0?Vi#%(d8Soci$qY%K1`h1GUP{k9zxG&~?f1 z#p@QXOmYwOdarKhzTV8+zCCccuG=^0IwYcf*xFDQwgh?7Sa$*JyiEBa3tJ@2d!-*T zv{&f7`M;VqcmJ!ZNiJg~OohPoZ!)X5v>DiQIN^tbX&n`r8QwqC>*WnI6r8m!w&{@OjfwH zW`m?_g<~{3>rCM`S~J#@!fmxEmQ3MTtvS1#!tH=-6ooI)TChwCV`YL(q;Lnoksc^z zxtv-{nBcX-9RX)Gg>jOSJ%N7Abc`+SHTD99nHIydD9p78K99meGx%}}>p1=S7KLqE zJNVVG;K0HVucC0cHbBHtxQW(W+)Cl5T2C>R!p$^?SWn^RA>kG*PVAuc2rWsMw)K!t z6r!yS){|A3Y0YizD2yF$wr&&_2q#cjN4Ot_ZCY>JU<$)Nk1dD7;aY-C`caVZn`lYu z)RYW2)so=p&`P&!MpzI|Gla7#y}6bU_7sI%Xiad&(t023jbWctI8w8R{Y>GOwe^Xe zP0hp3QF<$_Wq1%*wALhCYR+UH99pYzxnd&2tu=diZwkj~j_?!;x6xw4hf=t$);WAE zg=4k0;dfKGoz^@&kHQyd?ZUTFxV`2Kf0x1?0OxB8JGHp*ate3U+BS)yaGch;N$+7Z zawm@)Gc|7H)p{Wanm#$r&?kd~VL?Q3St7@sq4%*^p|mn(E3kA_a4bG}CdXO0`6 zods|WJ{mke_-OF>8M%`}!1*RmoRt$qgS=;_&&|ylGgOvQ{TVTS;;gA-a;_SmHF?&w zk>jULPMbb8OJ3?d6`49_+!QM&LlQ>Z6}e-kP99eaiV_q=3R|yOa49JWo}v;s1#6H6 zA4yDgo`od9Q|WYVhIWUR10SLjwMp<)8i%=P0_;I0BQy#r<8dE>x5nbi#TBRdU=4J< zHW#4`d^cUY4Y6_R%YoV~+$SL|N1KV~E(I|c7-qsZWFP#S2t4wQS%B9YUrpDhA$1gT zFauDo#+3^_ul#-r(nIm~s=<3DAN&_dF@y4i@I<2t0{3P zzFN4l{GpIXBPPo?8St}FN-s*1p#Ft^AFQQb^}Y^$Ck`-VJ&nPY3z%cHEWn z)4`+S>Xf8+omiU6$yB^IM?skYC^B!s7L(yzV37F?t&d>M%l5T$;8X9&)*q>`%2trI zC|lopV=C_B5YGm@@yKT!YC2oxSK^axD&HLm+xkOsAEw$<)=n+tp>-hi#IyP}=|)Y; z%j$WJ;SJ_yj5Ztjo2=S;tZL&pG zOkk9)ITd%=^CqI_4MV<%)_fD}y?^X0S@vAO8wcD|fq5jt^H4k2;tGzk%Yesf?Q4;i zh4&}nJ=t@!5zfF}KA(m!WWSKGf^{+uBSXTJ>C+J&|8L)xE%2Y;l~h2sX0Z3v=Be?V zT!RwI@tUK`DEmVk_IcNquJNyi0?VBL`m2Fx8>`>Ru_61~f5tQzZ`S`xrpb1hbe{V1 zskWH~4E0N6jgo9F9W>7xrLrem)Uq~o6~4$oQ4ulMQZR~3n7@2rrKP# zY3MWA4`f;pa~wuhF4C+qSo>xz_PTGJFFh1WXnyN{T|f7Ap9XVYH?=lTjX$p~=lQ=2 zmb&g6n5jSl#Sd2F^Q}mz?`fji({RzTQr_^VO?1_>_Ky}3)w}i2kXgt;Xq^`?4Wcr z54H&zm@g*5sAUT4i{q0Qvs4)P>(4G_1K2=V3>m}*!-tTMUB)hl!H*&AN;Z^T1usIw zSOy!;MzE1=6m|rTX4kUoaHKtx-N0^SH?f=9E$mh{hK*(8SQZ=4Ca{S(l{uN+#-_l- z*)*2Trn4FBc9z3tvRpQc&1Q3Ovi=S>kIjdj&pX*dwg`IhyVw%8l-=CvG9t+p9b!`nF-dmFZe|IP~8JM3Nd z9xGz+v)$|iwukLy``CxXD8SX?4Rr;`;nEgpV-gr6s*IYVQ1Md>{s?LR?dE7=h*M8f>p9A z7GNf;<{D?5a~yBuHfXuSc@y50+j%qIoVVZ+upQWvNAXth?c?CBc?@sE+wxf6j$gpr z^A6m}JMuW*iFfAlybJFN-FkQK;urFZcn{u__u{>IAD+P7+{3-x;E6nmC-W5Em-pir z^HhEb@6Rvg1NcCm#s~4iJe~XaW&Cn}1s}q%C1Ru#q@oV^Kel5R_ zU(Yjf-swhu6Tg|?!f)kc_*g!UXYuiT0-wky@yYx)K7~)^(|9(Y&S&u3;lptz&*iiD zY(9t2<#+ITd_G^m@8k>lBEA^gbe8a?{BC{^U&im{%i$aCei*S{$sgdW_=E71vYJ24 zAK`2GqkJu2$Jg`6_y+zs&x2|0jW8~~nLo*&;#>Gu{xr|$&%j3XvwS;$j_=^l^B4Gw zynw&NU*@myo%~h)8h@St4gTx={7wEAf1B^(f9Hk#9sVwVj~DUx`ELFJ-^2Ivef&fI z5&s7-<{$G<_BDcF{~U7cE4Dh!ibFlxQWQg+sI!F`|uVD`G`Eae-(rItZueDB?sX(OJZcE~2aG zCb|okxKLaqdWfE)m*_3}hy>vl9{6E2M50K7+o2TESM(DXi&Sxm=r1l61H?d)CI*SY zB3<~zW#V#ig%~2P6hp;T;%YHWWQgHngcvDC!8pTcajm#cTrV=k4dO;|lek&jB5oC9 z#8@#-WQp-&f|w{KiOJ$NF-1%j(?qtIE@p_^MUI#We~GiiY%xd76?cevV!l`)?i35f zBC%N9C6Q6%0MyTu1$kJu~ri4Vm`;vb?|e2kq9`^5qH$~q)I z6^F$U*#G}bd@jBaUy859QE^OsExr+@;#+ZCd?&saC&UlppW>wWQIx^sz|Z28I4#bI zv*H)=tN52F7r%*f;&)LYDn*qD2vbz+8Z2pYU10Z*4O+5ry@}pbx9iRH=6VY~LXXs2 z>QQh1Ik^!9oO-KlrfmJ>!8#u+2q$lesdSAVtezBg4lQ{kLOZ5TzKs`+#qz~59b)SBjez|^y zK19D#AF5x4J)y(&41KshLLaG*(y!4+W2ek@`t^FIeuI9aev^K)ev5vqK1Ls_kJGdC z@%jXPqCQEVtly?j(WmOuaDIBaK107<&(UY0;kj;&{ydX>JRCw^@sIG^fmgU`dWRRzFvP!-=IIP z=jl)A8}&{4X8lQ6=-8rf)t}b$^=I^L`m_3W{W*Py{=EKz{vvj2y`;aazoPF9pOrm1 zAt7O~bsv~u-95p(o8lh2d+Bb_J(2E7bWf&x3f=qCeIVV_=st+RY{E37=k?8%X#D z625_iZy@0tNcaX4zJY{qAmJNGcm@)lfk8Y}&NM1#8sSYNylI3tjqs)s-Za9SMtIW* zZyMoEBfM#ZH;wS75#BVyJBaWOBD{kL?;yfEi0}>~yn_huAi_I{@D3upg9z^+s^39` ze-PmxMED00{=tNQFyS9e_y-gI!GwP>;U7%+2NV9mgnuyMA58cM6aK-3e=y-6O!x;A z{&d2hPWaOae>&k$C;aJzKb`QW6aI93|rxSi3;r9`KAK~{A zejnlY5q=-x_Yr;{;r9`KAK~{AejnlY5q=-x_gVPe2^M~Lf`#9mVBvQsSoqxu7Jhev zh2NcE;dduk_}vK>es_X}-<@FLcPCi*-3bVV- z{2s#ZA^aY~?;-re-`pO;PyEeIe9cXK%}spGP5jJF{LD@K%uW2vP5jJF{LD@K%uW2v zP5jJF{LD@K%uW2vP5jJF{LD@K%uW2vP5jJF{LD@K%uW2vP5jJF{LD@K%uW2vP5jJF z{LD@K%uW2vP5jJF{LD@K%uW2vP5jJF{LD@K%uW2vP5jJF{LD@K%uW2vP5jJF{LD@K z%uW2vP5jJF{LD@K%uW2vP5jJF{LD@K%uW2vP5jJF{LD@K%uW2vP5jJF{LD@K%uW2v zP5jJF{LD@K%uW2vP5jJF{LD@K%uW2vP5jJF{LD@K%uW2vP5jKALjA2T^>32z+{EYH z1L-;OAve)|H_?4J(S0}3eK*m4H_?4J(S0}3eK*m4H_=Ns(MvbcM>o+&H_u7ao!WJR}ErNDlB&J9?-cJ=A_4;-enoqaNa;9^#`O;-eno zqh4wUFSUc0+QCch;H7r(QvG|W{=HQHUaEgD)xVeO-%IuHrTX?#J$tEsyhNA1M3=Fh zPRV^ zEO`)d`p%LE5m))aUGWFp6@S28@dw-$f52Vw2iz5Zz+LeN+=F=4c*HOv{FYpZIN`VC zLc|HbB^M%2_$|2*al&u)2gC_~8sWG41=0z>B_ASA_$~R+oo>m8xD$R$K17`GTXX_( z!f*92#0kIE&k!g47M(zx%5TYsh*SA3`Ouwi$%nX8yIJxf;?!=Ie26%;nuh*SA3`4Dj`za<~K(=GWBcPhUnA0kfWx8y^_sr(kbL!8QQ$%lwj z`7Qc~IF;X$4-u#GTk@eh-I5P+r}A6!A>veiOFl%L%5TYsh*SA3`4DkcevnPopKw>- zqMuHvro>No;zk7 z_B-S%Lg)s;n|zx>)Z{iinmT^w%;s`t42s=*95zx=#2p&(ghsrf5hFB`6dFkmjiiJ| z`i4dZhDOptBZERCzL1DJG;i+EZ#F=tGD z*li|FpFU;G*y;7t9n;2)%bA{C4?ynZsaf?h*UW0-LA}RJ&5cxwkhJ99Q>TxcI%wc1 zN4dyxCSA+VeLH*F6{%27CGDtCMP=7T@!D*0U)1Zl@L5fU+6qW`lDh-;L7^HwS zXaZu8Le21{D=L6??LBs44uz9K!UIFXX*J=5q`tM`^g%LgjbBo%dMTe?%9ocEtX@*E zdP%|RB}J{5^6e!>tCz~>rE+*_qUEJ}^wLDyOA}%*mD6j{%LFe?#Jr@C^^!u?OA1*p zO`N?{FJ9`eUQ*0@NiplC3A&di=w7OCFHOL`q;U1pgwIP8J}*t&y)^Ojl0w!?3RQz9 zmbry=FdwSJ8z0=I1g#CAVASfEiJ{8f(Ck&>?R{p3JZ-Sg+}^@MAAZEOA7^% zmZ)B{vXYXh$_k}W?{RnJw3PBAg_Z0``3(v>hRTmO#gfO>L#!jqyD!n=Zn!Hc#g}Mt zcigRMQliCm5x1sMi59m-T=i04qQxz7x292v7B@s(A;(=wQMl9h7PmuuaD)=!Yh-&X zS|fV5Qcvqh-PRj5vUPN=xL*S#B&`Ngo7(s4fD3uI24+-nrjR>5d%Bt~G_46)Q;r%` zA=8i=R4T1a!<>h_TZ2lag-luM0uFgl7kc~il%_tox_D5o+9|E-N1-XxYkC%D*OofK zZBZ%2t?9noqEJV+!{59JU6#$*x66<8j(DO^f*w*9q8Kf_rq_rwAk)FTt8~s1wUz8d)RYmKOGmIwf{oXI^fJ$ zhmp7zXHLY_WTq`s!9`;z`Z^ShZ7hrT5ifb{hLY&rHt7+YEmcARl=?+}o-_|sj zp=mu+aUIaKUdTr;ybDlTADk8MgKrW>;MxHH4A|f4o~UUal*5Db@7|u8X23$5bf{tM z)wINcxKLv$`!uaD&Mo(a9j3lGrGCj&T<>aHf7n;)f19RVnx|<4VETIi@C?B7fho8i z!gW&9(vZKwfH@fW2LGyQ=|gaB#0BdhKESwawx(S<7#Grp-mYm^-KS|+UxaJ9rVYb) z!+>iT-pfFFGVspuTX7X>+DNqQ$ZAa+b+4vf!3p)OA2jU_SO{6v z6c^g~UYy&%??p|!|1wQmgEL;*;6klX`(OSBXaoM2-~j9YM&CXqD@KRk|RY z|5YEBaDp)ZdK+4~`rqWOVXhjc{P7$&_U&KEXZ>#*ga5~KS09>&RvGZeKdp852 zP#1E2ynifS3r+hAz1PCkFh31b{#dT8w?gwKQ~nyXTFe%l{}nW=-u^5!tBi4fnr_sV zN~<$M|7?D0;aL!ZaO$^3?R$STBq7v=`A5HO?7doO+F$OsK?tER8^ifW(UAA@AQRoe=GJ#SWB1+oA6yoKf@G#rHd-`ztYSmC<) z{O_Py-!=yGzlCGHUt3H6tGqSL;h)P{u$F3b7)<$d@Fbidgix4&23lzSTi-WK`QvaK z`}WW0uwgqk2LF%dt|2t54uk1`6i)EH`sMm7d21{u_2CEc{*k-<)`Drh{nzsr3e5`t z$$FEza(EUgg#lKV3if#TW&c}69~FPHzvPQAOOJo|L)lM1pE`Z!>@VfNo%_9_vdR>? z&F+YC#!Ca1__Sw9sl(~=_%brHHI5Vxlb#pBbd?Or%c=En}=`H@an zey=iTelL-k->Z3Mfh!~9jdrnZ{I=(Oep|86A6w@0$G?UP;dmLY^u5=tRqLYi;?yGh z+0-IOMQRc1w#fMzu2NT#li{-4aUtFk_tezl%2H3U?U<)HGQv|F>&I1EjtlqGxXN+a z5qIM5@q7|tYgJMl?kcgDI7%G*TqTZAa2>_<9j-E5<+$vnC64B}N8@t2N<0UACBBo{ zrI8{D#2BX%Z1D5vg4jw;`5v+M&~Xn$|xx)P~%AU5%dt*pE5Ga z?Y}!k09h|DcSfB$=5mxBE_0L}!}Tq~zj$I#oUo(Aan+G~Xcw9! zRYO1rFbT3}^#X2|ptmt}(+kn(WMAy5g}qeaNYLyh7)*Fltwow}gMpu<0n*eT^;q=I z;xf-!*YaYAC)tzFdzptl1!XQrX-0u?1^PG(BAsXQU4BPo+1I}J?Cr|STNjm?j^@pa zP?hEJMU_rlWNLAFrTy2~Vq0uwv8Ncd_m#aERs5_|{tb7?zYg@X*fS^X#jRq0{=gys zej;;*x{`m(%GF;Lu0Ss#!j`FkW?>SdVg!AkvS1IyPVL$GOC_CQ%q@!8c2Ql*R z{uuP0@@;4-d)X}Xs0tL>*B+S-3DKl+2SJknm~f{QGn#wOYV zPBL0*r?t~_tJ}eA*Y`84dus=@1Cy)0+CQ{^Osu|D+pKNQsJ=~GtF67VI#*k%t@MGs zEzy zT0ssZe_5o%^u?Zd^+b!<(wPcZQ-w>mc@SGqPceQcJRXnFpXc{GeZEwMFZ9g|LFH9{ zElSNOD%hV|>Rc`}m|yBFEy93s22x!mviG|jAQO29i+g$6W@Y8!mxUkjGMEgNCM82l z*|U1IEN8FMQb+Z|)S}`dn*^ZT3)!>|FGXzlIn97`Tw!g z@3VV+mBs#t^NLbSU8z@=6cy$bdkUPM^3vjRd$FfTh0c{CD5K;DBQOhz%fy0qnR#uEUEpeb5l|AM9xz)*5t(yP*vprC5 z|DuJx!!P9lgi7t`%H_rA_*B_){94tXQ4`8kAq;K16^chF6>0YJNL(Hjibcp}$Di^u z5=7*gGnHrB*dlF_2#GVl2O_t)ec9~Zo!bV1A5S@vdSn&wG;HStgPJOC;^$aLLjFUlBCT_b7ZLy5*!{C zav>zs%FD{}zI?B|%xQP1Z!60j;KmMTX-R2mxzpLsg)UKAdPeq)V8|gODy>aT$eF6r zV$t~72$hwdaiAw7R9xz|W3Z4v=B8skju~+?Bld)xCR9B_~H+iO|=3 zMO^Z^nh-|_zweTh!!-G(9LBZaP438%>AHRc{@|RzfpI@GtnVOoJZ_m$vH8Zn3LxuL%gub#u zj{N+h%*-;2g`lHN5-{aXtcIn& zsmY$|+a%G8^rZH3RgaeWJ(HC++*3V#^14^owZC$5$~DM`gdLhADOpNNydAxswGSy$=uXOob+S-hQ z{DOk`jErzFy8`?wWw|$J=9lK@pUKR0DnUdR*UD5a6X_v2qby}4da>k-GHgpt9Z@|_ zyIVqv&d9hB#R0lMS z_wB)VhjHPHFbD*pbtFr+vN~7(lVV3{zP776P5#q%X}d}bLSx4Z@TIjMiYe@25KQby zvzTdHDViV`Jt~{o!dm>&v|M-nv!cvx|GKIPELoQ~Ij1{MRh^D+av~5e^d{$Q6+HG# z)vq1HR@aM{nL6jT|3lfA07O-N|KB&v05j|`qM(xEhKi=RC%K1ZR%B*<$t8=*Hu_dp zRBEP1R`#u^uUXMIx5(5KbIHhEELT)S!(9;tWY}R?W_b7iIp@s`oBis4A9LQ}y!-CE z>pAD1d+vKSyJ-HdZ;Mhq?b2q{)iwLyq^na@De*EP zovg)6voi|szB6G3nKM;lO`wk%bcvs|?So<_69x6D2I zVomYY3I4~M5)>jhOS&8-eG)~rHMqOEdDNk(ZK@iS>QFER%SmR{HxRo>QiPIez7KZg8xaS&d-D)m0@9)9IsRc?_t>Iw&YUsPO;=v%AS2I#9Kl^kuRDjs&(VARz&514G*akDB$lZ#;0D{eK4(aS~fA;QfH1^V@^N<_nyK27WBkRnpMQPoxJQE7E< zCZm&3!vw{{MW|7=f6ih4_Lm-Yu>LKRE@XAODs+2tuBd^IpS0|NRJlde6UxclQeRbZqRM zM#rf0;p|XRsi3`yrKY9Dj%6_z8@uzv4_Z;uNS=a84=0I=a9|mkND!h2u&9oXJ_c#m zlCrH8C4=Prp+iIZ4$(r^#zUm%XJ$tABE13O%r@vn6e7YQNGM#&NN+`fibElc)HMqE z*=emPds|U3Z=qlgAe|ARG}ltXAS3+2~KNUtp|4e*6(OQ9maw1R`Lx33neiYh8O z_;~qfp}M%Tii5YwTMO2bD$=~6ReM6crchgfIX(qzt#-xg>FbDHW!3JkG6i~T4=t_G za#89V5};+Qq+H8siC_D`AP&W4#ai(15Tq5yQmU2D5)hW&ijvWa!kRO5=f=Ct!5nTD z-_$}-=is&+3X2Q15ES0F9ftx-ffmeN+O_9!z3{pg+D5eR$l+SSH7&I3-BD|%+0=7Cvr|`A!0oCC{xHhqxI0tUK7$=QB18UVXY`op($h>(?&zafH+Gl3e+$PP2xn4 zz-$7}~2Q=C1!_)01?2cHk z-tJ~s6rWmEsq@$8wxU=YDgGQssf9ejUGxr4Q@yI#echT+{FtIvLd2+cJvg*tb=N8S ziUv*6;1I~b9xpnz8n+&W^@`Sf-4)stJ}Nx zAM2#(Kv6&`Rh$%Gs022s1jRj^lrFbbHI!9{+Ajtz-No}Df{cdwoE-1BX5T^9@woaM5#>*xR4JPU761oRKF@e$$=8WT{C@#u5 zN1VzYS{$8v%xIU!1`jFQ6NPlJ-=Kqav6*xo`7WXy@UzP>o5>dFq6|QsDky44j4ZDI zx_w=`jff-#Be2A$!hOiq+PpEo@|0}an$(zvwl0c81M+UCNLGQA1}F@MYKd0|-=cp~ zk7}+Uu=&YZMuS}{4IVnYkk+XI*ffLtdY4ypyI03Mjmk@oG2|69x_(8pK_P!Px} zhc8Uw1KL7tkB7Anefw^FuN-PX2=EMwi(nSs4qbu%lV^nj+iMewIDSGbyuo@s#s&u@ zyby?Hy7h-j(vLN9=OnHV-hWPWpfUI=z*5;&K8IC-%=Fy!#Hg6Xn6{~xK*dbhwi~*yT=3agI9xtc45MQ)JI{4Dw7_k6wp4bg_1?q;(kSQV>FoR|-_!BY>hRj&N zwCFU<{WGGY8!3rtix=lEPE6zxPNT4Kgtyjav~k{BJ4xB} zkYKx?z9`a3vPdW!CcB^XG&@NV6srNTF6P{#n>8<76A{9b)*|0B4`t-Vnky6i( zhl4lQ)Lgl5SWoMNwc|WZZEpJR%=Y`yntvsB?0~AM`u03KzERyKrw-HOF1|PYY@TE! z@vPg{v$$SY+|Q#ycDKc{-DK0Q!qxScEVti%SKpXcq=}8BN{%%GuWqs>@Qt5e$VjT+ zWV!vtC%ea^W!*e$-RpF1D;ku_4jy>>y2H#;r?joGD^+10cpbab^72Ky3j2_%Fn4)d zTtgHaXO90_`-i=#Dy)02AA2{9;I#Jbz4=?i&B_h~hn9^|!`UQr={wswwN0(LeaC)J zVkL?+0(lzJzMaiWFD+x8BK4dXRDuQ%D3?BHQ;OBF2r}D;%oh5&PSQ${0*Pdw(UjDW zNclrrk!-G5zu`s$#X3*Dmy^%-t(R?W-crfG%SrM_tU8KSY?NVSiqPej6kd@f4IW_~ zeS>6QzA@|cg%k3o&Db}x+ekTxNk@*KKPdBUUi}BuM>$E?{yZoz+Pnr0=n~eHWFb=L zFgc8G{NwP+1M<4fG;GKnL*!H@tzNTnh0L~jO^urr=QP&@!CYe{drg%^!RMJ_xU$vM zK>;^Z+dSOIDFI9>HRe_*`G%^1Al0h$W73JE2TmykhU$J%cf=_DnRNWa@Oi_Q&ICN0C=2q8l_~7BnSdO*x*R(y#MMKrtyT-=2Na=eNi=k>5-h-vP!q%}) zq2AqezMjQb^2+nveAtjwaUi>1Z}iUfZ8Llr1|rGrmga0MbZS}xYYJGuq+_?kh{!4g znH816LvX)4^o^#LNOrGq=^ssOk}(&_WDz5_$cc1_En*9;dc*00*$~-C|1UA`oHg_= z!~EP@;_GMf^YaaL_g0E+`hxI5eQzHUl2dT>LQYP@&XbqDUTfpiH+t}p5#2K{9yoa{ zvn>6PCFC`e={^pdcW*{mwR!W;qhIwmBd?md_V1sL8HGy#C-T(Jvh%|wIc$s}YfJ)L zV=M=jEz4XMLQ zEEsL2Sb}rWXyFg*=A~Fr&@3b_JB=BJn$az2VW1_rXx-ShVy(zOZz<#8@8hopb7f)K z=oU5|GmJX%nxzIdoG3QkSZXUWb2O7rWp(9VzQf=Z1%I{Wz3Y zmVa4JA;hOG7R@=p;-V>}7X!jpWwxO82?#UCxM&$%ax-XJ$;n!i!!%O>7ChTUu+pwK6{N5?o9EsJTU;2z;- z7wrPm48c7@O&vJo73AsiD3rJBr8e4XQn{tWLL%d^Oh*CM8qHp32#E~A3M2(sUNozo zAM|dl6FF#Du1BzjV@<==cx-V5oKq37Axf3Ax4WAVq#v_WXCCNO>{9r(#^^w0HotnCU(dlZb z#THAx1(r8Me#E#5ci(;Q==4L|cBH2t{CWF;H}(7EKTeeuh7&0Av6KBH@cz7hy$3{{ z-GtAtw5DYf|IlH=@_%c9oT6CRLtb^h(! z`aYzNVcM?!N@lv6TjxJ!#J~}d!=!t`zT^#SlFe5qYiLgIp!qYcL6#45(t;>nEyXJ| zwWi&?1+CO1+j4q0&r0hJdPdtIzX&;kXv+TIHM{SK;RA*%HnwBw+bFf3x>|jO zO|7`eld0yi#)X^Vp$7H5>LRZM}oUH!%;##y&D~>*003 zZryrrd72Vw?1OX#7HFNdODlg(Yq;eUAQEng2od2HERMqo^x~zAsOYFRZp5mKN*eYS z6jEj4+S^(xb1|9+~_Q~AS2JUG=kND@cfZVS8ZR@t3t70d{ zJQN%I#;^&>kH4BeQ?6AOmsFo`*U`6qyLKb-g{~ZL)1hX>7$&-%RPt)e%InVthZ%ZuUAlgU7Hp>9g^0dk9H9uy20oTAl8!~1p;I;v=$I;Al?Xb zEs$gv6ciT}n9N;l$IY&G`t0h}V^^<6g!VNnu}#|vk}n1866xDrO_GwC3DI|o0-4M=S^*Mk3e5ud*+lE*&8&YpE5}fj)<&>VsvwgT; zZ?t+GIH4%TJMGuY!-o#Y8bAhUJwD}=rzqxH)vwN9RjzbL$~xHH|B8!+u_uO;kLg5i zn_3xJk$t^wQ4y@UZ6e$BS1*bBhiVEREG=ApuCP$)(4p=5@gtEYz5hk?%xF4(Dc8-$ zEqG&!SnITn!&v?>jOAdC{}rp65GR4iIi)dsD?ZbF)Y6y@ZD|%XPfWB(_m$C(j zJ8-u7N8Es2cja7{_E%JNqo~Lb*vU{4C8xXezneF&-@IuG>TKxts=U|LC$C<;aP_J! zw69@eJp6w+@JbY{v4;l4nWN*H4gb{i^h3jEnPW!Zk{Tx;(!x*aKmGL6S3mu9DCTYR z$T;R}ZEjgZ-Yr&rw358Kg2Rm183?b=m|;k>e2O_@mtp;*qT8J*JhzPN{{ zJT`vg{vS4N+?biNRvF|uOo5LNUbb@!PTVrbnm-WQHqA$&wWCEl5M0={^p4@TdC4{qEUWjMR3wq`?3Q|ch5T5As1WM^b$ojl#A zTlBC#eQqDwx7)?dRh1{@pMB1szm(mh>nTf*i0j9@<>&nQP;BVKF1Z9JLCB?il1t{8 zoG^_g`$}`r0~KC(X7%!wgWhma3z+Io z)YASpZ8TE}4l?u!4mKEq^NKGA1e_XXY0&tR+aR;q&CPtdG&dmNh=(Ocqo%`dir=Zy z+_<=yxDrV#=7D}no4`Ot2`nkU9uQE}t#oirtyN!)6;~| z-^XtE_s0+OL9AI<^zzHVlt(rnYz?g#Km=vs2M7yq1@noaF4-IzFR4? zbhu+s&OP;En!P;ty1`(%qA#m)>(aSR=Y~M60@-x>k_w&9R;1L{8O?zn0eW~gks;ky z+t5(1s<5Q%@R>8y!BQ>@l?saAuh@4c~uMtjwf zIIOPmSXEtZ@~Ac%{fkPvbn>}Q0cm?I;sQx~s8a3c-Wo1A>Wm~VH z++5F4{|cHsmsR@KmzDYYmSKLIn|r_%>_NQdW^vtjH*ebOZr&EuxukqhNN8W0Mc%lX zg?Z(LtQ*$g9wu{WL zQ8hCAy#LDVY`cGUNVm~B8HTi&Ol?jV1qVWNRQizNvr-qA<;2!%Yv7(6($YLV(|U}3 zea)IlBu#XzSv6|Kje1*iE*$|24SWDu{c(A?oPzO$Y*aKh8og6aUCPL0E*<<2xim{b zIolGe^;x)fuY(*1Jwi-shl$$#? z?v~^UacrUs{)g;ZE7Du3R&DvqWQGc1iZg79ip`9{su=uWX;sV;9T#V^#N}^^O-PLw z3&Mx>>p#5hpEnO)`16eKw&RM2A3o$MlKKoDcE{K*$F3aNb?jKfj^A?weX8?=#J7Vc zjveh60cj!Hjt{Hyd|FC6?OKdR=2PbZM?)ySBD z|C+h+7bVIdhe*g*6D^k|ud-$)C#PDIEz>M1eKSA~)W{_z$;tQ~p0;}RzSSU`HL?Y3 zYdMx=>$LP#b0+(8X`=q`yHTWAE8mh+w6#CyvS7}ZG;HGdLS!{x&Koj00Sl#Hh+WOe zeJ01j5A_SNv^hCsaugEzS!5-0Sp3am4t<`A<6hpTnbzdqWNt#<9*y%q_^?W|IDAiG z!LvH`+{NsCRcTiqa&f5JfL7!RCPU*?AQ8!a`S-R<%_(?6npu;Hmdw~#lO@)$^TU{U zvsj+NhREmvxu)po+*~q4!fRSAATM?rU16{Z;n(Lb*}oL0yw)pPDp^(sYA_0uuk!Jb*E`SFNh^65!xbijyXQwniFqXpO6_VUl2=EqBMG@DL&qk z33_5qTr+wY({tA+X3$0#dNQrhw{EqTe2?2qj!`c~TS*PfIA+Q~17wsBhy#O_6I;?P zOh+qdWxLfATIx#L6Ydvl7h9;$CO&00CniRl6V2a5$7jg5#1`_ZCw^f~kBh6b#szGd z8Q%%?T`qc9T%6e&XWbIR@t3;zi;4M*3H-$(l2m&f*buz+w^))>Q*$h-=@T>4{}9`y zaZWp0Az!qtpkUD={4!^yq?955Zd^z3hWy)F(tN+|Vzt6U%BrWq3-Hgk73(->zkU!L0wT*Votthnk8G_MCGa8Ib^khBkG~UwR zra46y*27t%KQh-*F2#WEEbVr^!K>OM@2ssu*HWLL2ZbB9clih(S=-=Ngr zQmJbg09T7-b|GJmyAh)C`odFiAxVHI2z)|RysePVn(qlYppbDxBj6wW0e3j0P%O1x zE+ltVEs)cyV+WbKQ7%)S*U0cufzPnD4STENj`7NK+Jz1xSD=&uyw|TuQuI9hR6Or_ zeB(P@3`GrS0ohTG3-zqrDOSoKk)BgSj|m}P*qVQweeJZuNk&WxP-X< zl6g;^s&DX^>}J|D!QEICsMxA>CGdN{$Yt*LrZ219CKm<1tsI~(IW@Gch5$} z>rrTpGxu&)HB|9bDNQS6r1(|(i9I6e<3i<|-ce@k{tCbS(#fjOuTQS{t4lxN5^ch1 z|2N8zvjLn1=cdLue!p);L8yPep{{Y5)kp(8BFb=L9Y^XX zWaHDQhTAc>(sq+Lt*DXO?itq(8tkHx;bQ5$xG?L2#KqApi(UIhXzz&ZCziY(6;b|S ze5$6ubYM%&Uv>9emyM+TNVQ5F9X|hh&mDJNDep6;I$x~`vMat}x&8KZn(4k(EfWVv zE-LJ^=bj5CJx7#ZtFQE{RRY7W56I~HlxZvXrw-^({wUMfTXb@5C~N+*&erVEcG=D(Gm+$G)hOwD>t(39ckC`}Ac`Qref+$OoO=K=bkr#z-a3X|^M7B_N2Z&@>#-N3IDwO6a(n&k19Q7*Wmpw0YPlcIj(+%Qh#pL|zo9zedU0!oq}YGjnN ze#R^`~yck>Ymc#RQDZEt2qeozk zM12b@TZgeZe~B_!*PWsyh{qevj z_Lgythe$=sr#3y%JB+ygc;kT=0lZHy186)jVglh;ySed@_o3h3*YpryDkfZqV~q2F zqs%PLUB(H;GQ}HLa~cmBAt&J1e8IC_2het10qHAup!@^ zR+@`NvFMl>%`1DUoTnBbr8r0PGdWX;W^u54;c3LV;EWRkj#(~_Wmg8a(xI>dTIUJ{&wPcWWT)#BhdSQ$?M_9{V+*Crc^DvJ|NJ#Yn zcDEiB$H09Ds0X4Gw7Dgg7g92g4v4eAYRFB9go00dM~`7&+W_@=Oq^!g%1BR7OFue% z)|E#t=EnCzKKxs)yWUEAXuLYIQP3kF)tW&bAHKWE~ z?mVXR!@aw8b9XnGJlxk`-nV?s$;I2<)FdTYP2giitEe6v_0s<3Yfm65dU=58fLyvc zxD3swU28LJ%^_WlWP|2~=nwhX4gGn57zmj`Lof)3 z0%Ngw6_Ly3$8syqe}Ha4m+C?(L-fQT?O!PY$}0ujK{>?CA3TXEB%RVBz{ZGJJa-q- zoROC!QZA{AIMa|1!?8rfp=&;lbQiedah{!#Az;(k2YLOr3l&OGb7aURBI zJfbY6$ZTc6c{vv_og;sV|0x?`s0|Y3RKOdc(T)=|4(xP@Y{5m!Gi``5A$MUE6?sM` z6!1#U^FN7U=+?jH=5QP>m=ubx}&0`Ubvmf#-!G8$)r9*XkKkBua=(>Q#u`m0J)A;FSa> z@eC*vF~=e9dXVmq&{|M-5+90h#2C3oc>?yGb@E|3S9Dh?Ls~FL@9;b(%=IL5#f#vm z;TXZ%UV3eAPi76LG|_YmJ#;&5uV4`KnK02=ba8N*UfB0MRE)%!z#*PO=HJJU#>xFC z`#cc^qoW|JIi8LoCy+M;^aL$>Jh~!vbwmdab3Hy}DPi0;xDo$@0V??iJUXsM$wpr7CdN<9jVvXrAEeNb5e*vZc^IWiN6=C!(_ z&y%!vEwE%sk&npp-O zmm_vTZ4r1M9VlA#15}Z+L{`c*`0&o1m>id(8HE^yY=H;vzHnI#Xft2{C>&7wCehz) z5&c|~=wKZ@w)h6xW;Q@8J#in5nrPG$Ey&|vvV=lyML0$n3TQ(!MPqb)Eep}>@EI6n z=5Bf9U|zoH<^zgh~}E%-Ug-Cgb5-&SiodCP41KB z1wj`@fSo89C=^IaK)MZ-M=%Eb#dmTQaxcU_%p&Ha*P7cpUgDue-$*y1-f@5>&~${S zB=R~@#d55f3)*syNEpGQ$?FBk-wRrRtOca4FDAKHcvRHaBZnrZtyUE1=%z5yRbD^^ zdWje1UbL&gCzU#&okfMOzktn@0_Xh@qa7jOE@Fo2C7y+`n%K{L(O#~TveE}zo368s znIj%)7Ja>V+7&%a`pD@`qNhrFr@EI{Ww~m%<6~BCZn(;s(QbtLiB#Qim^kN@Iy_IwYL8U3H_CBkQmUl5 zxFdbNTqW9JWHo@&26kUnELL?$*v8dyJ4LMJn;g-R9e-zZBv6Ue8>3TfNAw6s>TiQH zW^w8vqK_=b$`3_9OY0U|&D4YgLs4y9AEDfR5kpqM_-PU0vY|of+_->v7T$EjHM0$v z0nIc`{v_^1Ly^V04qB~F93Ll)-oiOAGT1r#VY(CjB};sd>>Nev8Dgybg!m3p(nc~z z8AyCsVgn>94mm_-=xbmCOt1kM%Bl(lyM{R~0Co;jK(T@lr5JsjV6pI!bD>3Gu}28+ zI(Hh5xTU_{hbz7d-!l`u;6uhJGd<;J;Ku&niL!i+Mz0b<2@(FyEh-?+mZA}gSn%}gjVgnX&aI#944gp?@Ps3$GQ_)jZ@p1Kt-s3=Q4r|yNKqm&AI z7j5CkMUJW62X|<`td~`YOes>&go1*V3iKID3WEf}jS9^{kG5?~iWHiA%~UQS@3gK8*;0{^^EFbm!akB`PEtOv*E;KfCiTR3%{m?Bq-hxx)d z6d5eW~kxxT&+a#8$r^O0zn2vH7?2R|%HShWp zXNHZtuS=&sk;2$%<^GLXs9747xalI^IMbgn1U=rL`-|w|CuR$)wOnIfATw{$B0*!q zI-p!DZ-U+vQ>ay9xxe8#G338x6Db9R4c&=T$|dqQF&+7S8%Bd?h%SGF&`gqL=DHXW zROfuTmBoS$5C~AN!LmzX8?w*=N*HcAjSGlLb{bH?0=4)32>CNqwSl6O$VZQ#4;dFB zkH|e}>%DD=ad7i<#fzhqy4)$xh+*hnSb8L#29yd~f)olCAt618qJ=8%mm!dPA+kt5 z2Gu)X(qQX`J`^Ed4G9Sm`3TPFZ3vNT(Ig*0$0$%Yh<7kIp9_VeH&$1l?Rf2JahLl> zU$OBQ#Ct&hBuRAnv|KOf1Anr6V^pWxCC7(!N zv4hBHu)X3rM8AxR?m!eRaIRoL-+_-6dWkos2Tc43G8?ndB~o_Iy$4_QbHv3dpNcA% zVm0)Bbm9flQ+!Pq(Z1)zOG0$6>P7#$LKUz=`4t4gB=qntQU`4S0Yjvep3~V5Mswa3 zTa+O4H?-&|#~x`w4Z+72qxKp~lHb`rplNis%V1eIW-5lq1}qHqLu4ri$P-X_l&;;;O#8Kz36|jZGt3%$anXdq z+($f3WQh9B;&qviRdp6gAW^P)0&?CJ9ZT<8bVxod!OOfR(J3}ZKa7bArS1z4f}Ub7 zlvx;1pkL3EmTndIqRtIyIuAs)RaTbagn9HT@d&iv`+18B9t@$dOPrMwn^iP!K_x}F zf#2a)(S&+5W6Or08V9owQJRvKT zAoWv)hYwnHmpCs!>!J7yjAH~6N8r_;a*-j}^FW37dlF2>73-jxjzwU0n6cf}TY$l`VR5&HX+kVWsv2q?`MJ=g=E zj;?3zWsf0VPl3ylOV}-?5)Z8`P#p-8=fB3aaz#5EqPlaP9?&_{JaW>EjmkdIbfh<~Cm`#l50I0hpI zHS~9RUe-xCR7b&ffp?!fLNCJPyYfr%xvUTs>J{-a{+Gg6{s6A24DUZ463H3aE@Ts{P7Za=p z<(Ck~TS0jVvJd(J96iNWxPO9TqKG3|$hRjHwt;~XZO-9s)YV)OII(oHuqkC!LWGI} ze>%y}7@ik{u@iy=#?qtWQ*qaQm&b{UfKsQ%kr&XFqJQIv9tl+_7UUdez^%^AO*z~;(w4Hc)93(4^I&rf4I02$`%ZTJ*=WR;{=d<%Ao7ev^kNt48estrUr zMKSM6C-|fogr&XTt^iyRIu<0L$os>H_-4$q6=M;>`Z{CM)m9}%4urF*Cv z(1LNWvu=S3o&hC-(n=zus$2};0zvOQrAQPeW)MVVjt+yWOx2=8-$;-ajt*}C#7U-z ztYyKRhkzMPokV|ya?%ZAav(ZEkE!TP=d9uiUB?7*=y^Ihq92imIXY3U!B+F7m@*X; zhjOSv6pPKZe2zfP#LM+CB&NZpnF87u(8%IM1pHS;N=^zuiEa zHOLrsU_ICiZlwt-AP0Atb+Tx>$kJVp^OHoEo5UzS7lTbAI+$cWG5sa|X^+0H1<|G>(QAhAs`u z2$7Q`<|C)`wL{K_ZlZ(FOrOhC_*9VV1sEA@^$V~buooje2fc0q$LCud zH#lyg=&l3TaSKO{a^8TCWy7ND@Fv0t{12q17tU|WL||`1y?F)os({C?CO)eL>Q9aX zir*SS}Gf_Fwo%M>gHL40Y! z5uI-0Tc%*?1}(2{BZ#ulEz8lxXVJQc77l`MIo7Xv^*U4f^ZIoP)T*KAAhe2|0;aM9 z&d82%3hml-EqlK-@M`vF5b}MXhR~We-SVL87QE1|R=Ha{)a|N8da74&-j3G|dIF{^ zW+k5&A6{VnfS;p-Zb90`U-NY5!gGs7XSHMFpb6PU*hgWhBN2}inK7b`_m2(1i zBHUIghp}BYLfND2*4?HHQ?4s{8auI1w@*_?s3UC+HnrhuL$0f1a>c2Y>W{E?-V`q> zV=%$UQ+6x8lyM?lnF&?nEX*f(cc6Mz{aSrbU8z>9=g~i|Kf!l8f$S`E*qubd6U!7m z%v_hnC5$}@lcgKRJaX4?r`i$mdZTaM4%woVI&bDPCfjDm!d#+w3W|XVJuVSFoJ=&l zn9XPCl30DiN-3H>fv2n*84$~)5m1b9;QBPM1`G2ogQJ5$sA{X1P!gtM&@Y7r=7viv z;8eh_XyF|P1o33Pk5m?$BL$2)ZA3$fc>yUb!NNp5X)TQbcVd9KQpf9~LU6Lnr(N_)swuoCWnESHMUSjzwU1xe?er zw!^zJ0sfE;tJOKoMqVns9ECV6ix`*TS-XsY1mlep@GfEJWIi{?O8b;Exs69 z*S|!secG)K$by1|MJfczMEv6M(+*9z(5e|;ppQQO7N1k3HMMyPYh({OihOS{*1Cx& z;2N_QeQFHg0-S$9`zg2|%oTd-a?BCN%O68iVq*hfdnaQ8QpJiduG6w-j2I4vMXn?< z%(&=s41UbL`b7GXY2E1?6#IQfn|j`ILaMC15!XFOvCVd#0I zpm{EpMarVfe8~yWd*et@;v9ido(euomd!Mt4`?fSA)LY`^wLC>ELz0#WW$*NR>0AF z%)E$*X#qYkG>k6=Vo)Q{f+9RY3EPN;#qAK4cga6_eojOVuwZ=&WrP;nV0(qwTZ}%a zVA1_Sx=agiat~vtq2nZ?bq9(Q-b8Q(nB(cC`u@CPXp&WGG4T{EtX-!ltZ6M_tdJMk zFjZCBX>CAM3W$%XJ5&@>vvl%TSduXkpkr-!TosZPZ)kG{alg0=T-L+`g)Tb+xnVFj zAFV#!klSG4Nz(7~cPHP+Q_jot$~($Cx<`?ZJ9LjW=d`%K&6yUpywpb2+w!J7;Gz$g zt8pa*Ds?c%aIkhm@NjV;4zFn9Sa-8_S&Pv<7(CMs+E9#T1jgwbS6DwD;3WvMzy=_6)f`T5`A7z)fb=iv;hQ@?{qe_CcuYrLDV zelix%eIHiNPthu$irr{gtNK6j5T?2TPy_0bLKW5-&&bnq4(bD@jOoo&h((_wt`v)Y zxD!2sCWUF}W2~4^Eo%lw=<2UCH@$%`eKb$uZM6B@bjbHGc0A-gIKKy#mm3}R<``M9 zfc=8B=ZTl)+P9mh;0bx`l*Za88X6s=oiC_xY6|35uelJjOF#0ZhgFK?H93-ziwS)y z1MVg8wlPB?;sY*)hm~;f9ekxr!~iR!49LMMNeFTgUaTG#2T8Y5HgML!+jFVxE-luf z$Bt2#X?m=>9wngwO;+)RH9Hz|GE3&F6)=`|8_WFqPrKLkvRSlwxv zMzT|DSF@X>u|RA@S+YP+8>UP|EYnzn)TGcWR|CJ~+zTyA#&vI zm4V-4q?e;Ma&YRI7es{eN($y_uwpsa^IfTd!$JGIWopStt1Gxb%hZ-AXI!ZfFMyW7 zI?P1$Ntpl8J1;Pm}CBcccyn)9dwN$NdG^PeQ5C`6sgITkHt7BuB%Q0`bQe)FH-Z^kohM;%!7o|vf zuV}yx325Qv$kD&BrZ@%KExZSS`{&|LhlIzOu9YGcjAFFt7_?~< zs|+q;_J_L2#1cJTG==}y(H&?>99ZB?gajpLSSt}7b{0=)#boU%NA1nidNf_-02xN3 z$b`GCbdNUb1IT+0x~UnON|j6kPMm@kq{vO&3!0!iR=hPemy)S>9E2lyLnc_NU=Rl$ zB(Rjw0V|M$p+TKR?N5ep7=50rAJz{-Zn7C4T*>lKqCQT0pk>~xwN7A1!@6xhA(^}uGV|EP~?Mp3x*N7uo@L=jGvFN*hiY z*qe4hR;%ZfyX7S{nR-2TI2L1`un>(j6)z#QiQnMOASCsuEW9g&?)opEJBo)pzb>ZJ z#Af3eES1shh~`R{pr?+(*v{1~0UAWgq$4cG^@)H>DIHg!-V3`uK6i)j+JVZgAQyo% zl4qQNTUr%4>%b!ct&OupsXO8q2IFaCXaHZJ!x8%9Y(;;g4ykKg-ARQ8gTBr77somw z=DV~ov=)X%8ar9Q8UuN5_f;1`Cg9VBAag4~X1|M+_hGJn4Qm_zTsxttG5BYpwtTR$ zV;1!Nv5mNX5+s^V2yz! z*wc-J?g1 zYIJ!I^ojP;T7e3qLytFbsQ{pNkO$SHuzy15z#Q{a{9@tA7%P{gUoXH`oEdeGKY@rt zhqept+lg+&IP(_9UkG&tN{ z`-)nGn7EV_ERG$Azf=;weq9PTC9Dy^x9khZ=dZ=M3oE??pLrH=rB$Xq0f5DU6;>ekcxp_M~$T0&{AVY z)0z1SAuYmn`d>YX#m4R;TIr+K!(E~yq)9pIZUUSsiiRg%wP-l*7#teWlW)YEBKWL9d#l89 zi6Tehvz{M8JJo~Zrowk<8|)*rSLQMJ%~ibt�c>k3(LI&YcGv0yrrE4`DdiEIl{p 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%0DRYs{CPg#xcVlY- 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