diff --git a/.github/workflows/ci-android.yml b/.github/workflows/ci-android.yml index cfe4049..cad3e39 100644 --- a/.github/workflows/ci-android.yml +++ b/.github/workflows/ci-android.yml @@ -84,16 +84,35 @@ jobs: needs: build permissions: contents: write + steps: - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 with: - name: release-artifacts - path: artifacts + name: apk-release + path: artifacts/apk-release + + - uses: actions/download-artifact@v4 + with: + name: apk-debug + path: artifacts/apk-debug + + - uses: actions/download-artifact@v4 + with: + name: root-module-release + path: artifacts/root-module-release + + - uses: actions/download-artifact@v4 + with: + name: root-module-debug + path: artifacts/root-module-debug + - id: prev run: | TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") echo "tag=$TAG" >> $GITHUB_OUTPUT + - id: changelog run: | if [ -z "${{ steps.prev.outputs.tag }}" ]; then @@ -104,13 +123,15 @@ jobs: echo "notes<> $GITHUB_OUTPUT echo "$NOTES" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT + - id: tag run: echo "tag=nightly-${{ needs.build.outputs.short_sha }}" >> $GITHUB_OUTPUT + - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create "${{ steps.tag.outputs.tag }}" \ - artifacts/* \ + artifacts/**/* \ -t "Nightly ${{ needs.build.outputs.short_sha }}" \ --notes "${{ steps.changelog.outputs.notes }}" \ - --prerelease + --prerelease \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c091b02..33503d9 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,6 +1,6 @@ import java.util.Properties -val appVersionName = "0.2.3" +val appVersionName = "0.2.4" plugins { alias(libs.plugins.android.application) @@ -30,7 +30,7 @@ android { applicationId = "me.kavishdevar.librepods" minSdk = 33 targetSdk = 37 - versionCode = 38 + versionCode = 40 versionName = appVersionName } buildTypes { 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 f343fe2..36ac3b8 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt @@ -65,6 +65,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Notifications @@ -86,11 +87,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -120,9 +123,12 @@ import me.kavishdevar.librepods.data.AirPodsNotifications import me.kavishdevar.librepods.data.ControlCommandRepository import me.kavishdevar.librepods.presentation.components.ConfirmationDialog import me.kavishdevar.librepods.presentation.components.DeviceInfoCard -import me.kavishdevar.librepods.presentation.components.PlayBypassSheet +import me.kavishdevar.librepods.presentation.components.SelectItem +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.StyledSelectList import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen @@ -146,6 +152,7 @@ import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel import me.kavishdevar.librepods.services.AirPodsService +import me.kavishdevar.librepods.utils.XposedState import me.kavishdevar.librepods.utils.isSupported import kotlin.io.encoding.ExperimentalEncodingApi @@ -157,7 +164,7 @@ lateinit var connectionStatusReceiver: BroadcastReceiver class MainActivity : ComponentActivity() { companion object { init { - if (BuildConfig.FLAVOR == "xposed") { + if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) { System.loadLibrary("l2c_fcr_hook") } } @@ -329,23 +336,118 @@ fun Main() { ) if (BuildConfig.PLAY_BUILD) { - PlayBypassSheet( + StyledBottomSheet( visible = showPlayBypassVisible.value, onDismiss = { showPlayBypassVisible.value = false showDialog.value = true }, - onConfirm = { - showPlayBypassVisible.value = false - sharedPreferences.edit { - putBoolean("bypass_device_check.v2", true) - val intent = Intent(context, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - context.startActivity(intent) - } - }, backdrop = backdrop - ) + ) { innerBackdrop, _ -> + val contentColor = if (isDarkTheme) Color.White else Color.Black + + var acknowledged by remember { mutableStateOf(false) } + val inputState = rememberTextFieldState("") + + val isValid = acknowledged && inputState.text.trim() == "OK" + + val sfPro = FontFamily(Font(R.font.sf_pro)) + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = stringResource(R.string.bypass_compatibility_check), + style = TextStyle( + fontFamily = sfPro, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + color = contentColor + ), + modifier = Modifier.padding(horizontal = 12.dp) + ) + + Text( + text = stringResource(R.string.compatibility_play_dialog_confirmation), + style = TextStyle( + fontFamily = sfPro, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = contentColor + ), + modifier = Modifier.padding(horizontal = 12.dp) + ) + + StyledSelectList( + items = listOf( + SelectItem( + name = stringResource(R.string.read_compatibility_requirements), + selected = acknowledged, + onClick = { acknowledged = !acknowledged } + ) + ) + ) + + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + keyboardController?.show() + } + + StyledInputField( + inputState = inputState, + focusRequester = focusRequester, + placeholder = stringResource(R.string.type_ok_to_continue, "OK") + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(24.dp) + ) { + StyledButton( + onClick = { showPlayBypassVisible.value = false }, + backdrop = innerBackdrop, + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(R.string.no), + style = TextStyle( + fontFamily = sfPro, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = contentColor + ) + ) + } + StyledButton( + onClick = { + showPlayBypassVisible.value = false + sharedPreferences.edit { + putBoolean("bypass_device_check.v2", true) + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + context.startActivity(intent) + } + }, + backdrop = innerBackdrop, + isInteractive = isValid, + modifier = Modifier.weight(1f), + enabled = isValid, + surfaceColor = if (isDarkTheme) Color(0xFF0091FF) else Color(0xFF0088FF) + ) { + Text( + text = stringResource(R.string.proceed), + style = TextStyle( + fontFamily = sfPro, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = if (isValid) contentColor else contentColor.copy(alpha = 0.4f) + ) + ) + } + } + } + } } return 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 ea4f768..074907a 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 @@ -4,6 +4,7 @@ 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 @@ -27,6 +28,7 @@ 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() { @@ -41,14 +43,20 @@ fun DeviceInfoCard() { Column ( verticalArrangement = Arrangement.spacedBy(8.dp) ) { - 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)) - ), modifier = Modifier.padding(start = 16.dp) - ) + 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.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 .clip(RoundedCornerShape(28.dp)) @@ -166,6 +174,62 @@ fun DeviceInfoCard() { ) ) } + 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)) + ) + ) + } } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PlayBypassConfirmation.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PlayBypassConfirmation.kt deleted file mode 100644 index 3ccac3b..0000000 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PlayBypassConfirmation.kt +++ /dev/null @@ -1,254 +0,0 @@ -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.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.input.clearText -import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.IconButton -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -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.platform.LocalSoftwareKeyboardController -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 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 - - -@ExperimentalHazeMaterialsApi -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PlayBypassSheet( - visible: Boolean, - onDismiss: () -> Unit, - onConfirm: () -> Unit, - backdrop: LayerBackdrop -) { - if (!visible) return - - val dark = isSystemInDarkTheme() - val contentColor = if (dark) Color.White else Color.Black - - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - var acknowledged by remember { mutableStateOf(false) } - val inputState = rememberTextFieldState("") - - val isValid = acknowledged && inputState.text.trim() == "OK" - - val sfPro = FontFamily(Font(R.font.sf_pro)) - - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState, - containerColor = Color.Transparent, - dragHandle = { }, - shape = RoundedCornerShape(48.dp), - scrimColor = Color.Transparent, - modifier = Modifier.padding(16.dp) - ) { - val innerBackdrop = rememberLayerBackdrop() - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(48.dp)) - .drawBackdrop( - backdrop = backdrop, - exportedBackdrop = innerBackdrop, - shape = { RoundedCornerShape(48.dp) }, - effects = { - vibrancy() - blur(6f.dp.toPx()) - lens(12f.dp.toPx(), 48f.dp.toPx(), true) - }, - onDrawSurface = { - drawRect( - if (dark) Color.DarkGray.copy(alpha = 0.3f) else Color.White.copy(alpha = 0.6f) - ) - } - ) - .padding(24.dp) - ) { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text( - text = stringResource(R.string.bypass_compatibility_check), - style = TextStyle( - fontFamily = sfPro, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - color = contentColor - ), - modifier = Modifier.padding(horizontal = 12.dp) - ) - - Text( - text = stringResource(R.string.compatibility_play_dialog_confirmation), - style = TextStyle( - fontFamily = sfPro, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = contentColor - ), - modifier = Modifier.padding(horizontal = 12.dp) - ) - - StyledSelectList( - items = listOf( - SelectItem( - name = stringResource(R.string.read_compatibility_requirements), - selected = acknowledged, - onClick = { acknowledged = !acknowledged } - ) - ) - ) - - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - - LaunchedEffect(Unit) { - focusRequester.requestFocus() - keyboardController?.show() - } - val backgroundColor = if (dark) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - val textColor = if (dark) Color.White else Color.Black - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(58.dp) - .background( - backgroundColor, - RoundedCornerShape(28.dp) - ) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - BasicTextField( - state = inputState, - textStyle = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - cursorBrush = SolidColor(textColor), - decorator = { innerTextField -> - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Row( - modifier = Modifier - .weight(1f) - ) { - Box { - if (inputState.text == "") { - Text( - text = stringResource(R.string.type_ok_to_continue, "OK"), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Light, - fontFamily = sfPro, - color = textColor.copy(alpha = 0.8f) - ) - ) - } - innerTextField() - } - } - IconButton( - onClick = { - inputState.clearText() - } - ) { - Text( - text = "􀁡", - style = TextStyle( - fontSize = 16.sp, - fontFamily = FontFamily(Font(R.font.sf_pro)), - color = if (dark) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f) - ), - ) - } - } - }, - modifier = Modifier - .fillMaxWidth() - .padding(start = 8.dp) - .focusRequester(focusRequester) - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(24.dp) - ) { - StyledButton( - onClick = onDismiss, - backdrop = innerBackdrop, - modifier = Modifier.weight(1f), - ) { - Text( - text = stringResource(R.string.no), - style = TextStyle( - fontFamily = sfPro, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = contentColor - ) - ) - } - StyledButton( - onClick = onConfirm, - backdrop = innerBackdrop, - isInteractive = isValid, - modifier = Modifier.weight(1f), - enabled = isValid, - surfaceColor = if (dark) Color(0xFF0091FF) else Color(0xFF0088FF) - ) { - Text( - text = stringResource(R.string.proceed), - style = TextStyle( - fontFamily = sfPro, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = if (isValid) contentColor else contentColor.copy(alpha = 0.4f) - ) - ) - } - } - } - } - } -} 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 new file mode 100644 index 0000000..b981398 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledBottomSheet.kt @@ -0,0 +1,87 @@ +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.ModalBottomSheet +import androidx.compose.material3.SheetValue +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) +@Composable +fun StyledBottomSheet( + visible: Boolean, + onDismiss: () -> Unit, + backdrop: LayerBackdrop, + content: @Composable (innerBackdrop: LayerBackdrop, progress: Float) -> Unit +) { + if (!visible) return + + val isDarkTheme = isSystemInDarkTheme() + val sheetState = rememberModalBottomSheetState(false) // move this to parent composable + + val isExpanded = sheetState.targetValue == SheetValue.Expanded + + val progress by animateFloatAsState( + targetValue = if (isExpanded) 1f else 0f, + label = "sheetProgress" + ) + + val animatedCorner = lerp(48.dp, 42.dp, progress) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = Color.Transparent, + dragHandle = { }, + shape = RoundedCornerShape(animatedCorner), + scrimColor = Color.Transparent, + 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) + ) { + content(innerBackdrop, progress) + } + } +} 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 baed724..6937587 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 @@ -52,6 +52,7 @@ 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 @@ -81,9 +82,11 @@ import kotlin.math.tanh fun StyledIconButton( modifier: Modifier = Modifier, icon: String, - tint: Color = Color.Unspecified, + iconTint: Color = Color.Unspecified, + surfaceColor: Color = Color.Unspecified, backdrop: LayerBackdrop = rememberLayerBackdrop(), - onClick: () -> Unit + onClick: () -> Unit, + enabled: Boolean = true ) { val haptics = LocalHapticFeedback.current val darkMode = isSystemInDarkTheme() @@ -96,6 +99,7 @@ fun StyledIconButton( val innerShadowLayer = rememberGraphicsLayer().apply { compositingStrategy = CompositingStrategy.Offscreen } + val density = LocalDensity.current val interactiveHighlightShader = remember { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -120,8 +124,10 @@ half4 main(float2 coord) { val isDarkTheme = isSystemInDarkTheme() TextButton( onClick = { - scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) } - onClick() + if (enabled) { + scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) } + onClick() + } }, shape = RoundedCornerShape(56.dp), modifier = modifier @@ -137,6 +143,7 @@ half4 main(float2 coord) { ) }, layerBlock = { + if (!enabled) return@drawBackdrop val width = size.width val height = size.height @@ -161,6 +168,12 @@ half4 main(float2 coord) { (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) @@ -187,6 +200,10 @@ half4 main(float2 coord) { } drawLayer(innerShadowLayer) + if (surfaceColor.isSpecified) { + drawRect(surfaceColor) + } + drawRect( (if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy( progress.coerceIn( @@ -197,6 +214,7 @@ half4 main(float2 coord) { ) }, 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) { @@ -241,40 +259,46 @@ half4 main(float2 coord) { ) .pointerInput(scope) { val onDragStop: () -> Unit = { - scope.launch { - launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) } - launch { progressAnimation.animateTo(0f, progressAnimationSpec) } - launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) } + if (enabled) { + 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) } + 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 (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback( - HapticFeedbackType.SegmentFrequentTick - ) - offsetAnimation.snapTo(offsetAnimation.value + dragAmount) + if (enabled) { + if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback( + HapticFeedbackType.SegmentFrequentTick + ) + offsetAnimation.snapTo(offsetAnimation.value + dragAmount) + } } } } - .size(48.dp), + .size(with(density) { 48.sp.toDp() }), ) { Text( text = icon, style = TextStyle( - fontSize = 16.sp, + fontSize = 20.sp, fontWeight = FontWeight.Normal, - color = if (tint.isSpecified) tint else if (darkMode) Color.White else Color.Black, + color = if (iconTint.isSpecified) iconTint else if (darkMode) Color.White else Color.Black, fontFamily = FontFamily(Font(R.font.sf_pro)) ) ) 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 new file mode 100644 index 0000000..86eae6a --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledInputField.kt @@ -0,0 +1,154 @@ +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 +import androidx.compose.foundation.layout.fillMaxWidth +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.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.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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 +fun StyledInputField( + inputState: TextFieldState, + focusRequester: FocusRequester, + 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 spacerHeight by animateDpAsState( + targetValue = if (hasText) with(density) { 32.sp.toDp() } else 0.dp, + label = "labelSpacer" + ) + + val transition = updateTransition(hasText, label = "floating") + val yOffset by transition.animateDp(label = "y") { + if (it) with (density) { (-48).sp.toDp() } else 0.dp + } + + Spacer(modifier = Modifier.height(spacerHeight)) + + 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() + } + } + ) { + 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( + modifier = Modifier + .weight(1f) + ) { + Box( + modifier = Modifier + .weight(1f), + contentAlignment = if (singleLine) Alignment.CenterStart else Alignment.TopStart + ) { + Text( + text = placeholder, + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Light, + fontFamily = FontFamily(Font(R.font.sf_pro)), + color = textColor.copy(alpha = 0.8f) + ), + modifier = Modifier + .offset(y = yOffset) + ) + + innerTextField() + } + } + if (singleLine && !inputState.text.isEmpty()) { + IconButton( + onClick = { + inputState.clearText() + } + ) { + 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) + ) + } + } +} 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 b9a2246..a4fd200 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 @@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row 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.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape @@ -98,7 +98,7 @@ fun StyledSelectList( Row( modifier = Modifier - .height(if (hasIcon) 72.dp else 55.dp) + .heightIn(min = if (hasIcon) 72.dp else 55.dp) .background(animatedBackgroundColor, shape) .pointerInput(Unit) { detectTapGestures( @@ -128,7 +128,7 @@ fun StyledSelectList( contentDescription = "Icon", tint = Color(0xFF007AFF), modifier = Modifier - .height(48.dp) + .heightIn(min = 48.dp) .wrapContentWidth() ) } 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 22f80f4..7988d91 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 @@ -402,7 +402,7 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC } } -// if (!hearingAidEnabled && BuildConfig.FLAVOR == "xposed") { +// if (!hearingAidEnabled && XposedState.isAvailable) { // Text( // text = stringResource(R.string.apply_eq_to), style = TextStyle( // fontSize = 14.sp, 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 d4c195c..f27d732 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 @@ -25,6 +25,7 @@ import android.widget.Toast 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 @@ -35,8 +36,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState 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.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -49,8 +53,10 @@ 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.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext @@ -62,7 +68,9 @@ 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 @@ -74,12 +82,17 @@ import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.R 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.viewmodel.AppSettingsViewModel +import me.kavishdevar.librepods.utils.XposedState +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AppSettingsScreen( navController: NavController, viewModel: AppSettingsViewModel = viewModel() @@ -90,6 +103,12 @@ fun AppSettingsScreen( 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 -> @@ -367,24 +386,28 @@ fun AppSettingsScreen( independent = true, enabled = state.isPremium ) + Spacer(modifier = Modifier.height(16.dp)) } else { - 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), - ), + Box( modifier = Modifier + .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)) .padding(horizontal = 16.dp) - .padding(top = 16.dp) - ) + .padding(top = 16.dp, bottom = 2.dp) + ) { + 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), + ), + modifier = Modifier + ) + } } - - if (BuildConfig.FLAVOR == "xposed") { - Spacer(modifier = Modifier.height(16.dp)) + if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) { val restartBluetoothText = stringResource(R.string.found_offset_restart_bluetooth) StyledToggle( @@ -417,14 +440,20 @@ fun AppSettingsScreen( Spacer(modifier = Modifier.height(8.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)) - ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp) - ) + 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 = Modifier.height(4.dp)) Column( @@ -439,29 +468,7 @@ fun AppSettingsScreen( to = "", name = stringResource(R.string.email), navController = navController, - onClick = { - val intent = Intent(Intent.ACTION_SENDTO).apply { - data = "mailto:".toUri() - putExtra(Intent.EXTRA_EMAIL, arrayOf("contact@kavish.xyz")) - putExtra(Intent.EXTRA_SUBJECT, "LibrePods: ") - putExtra( - Intent.EXTRA_TEXT, - "Describe your issue here:" + - "\n\n\n\n----------" + - "\nPhone details:" + - "\nMANUFACTURER: ${Build.MANUFACTURER}" + - "\nMODEL: ${Build.MODEL} (${Build.PRODUCT})" + - "\nDISPLAY_VERSION: ${Build.DISPLAY} (${Build.PRODUCT})" + - "\nID: ${Build.ID} (SDK ${Build.VERSION.SDK_INT_FULL})" + - "\n\nApp details:" + - "\nVERSION: ${BuildConfig.VERSION_NAME}" + - "\nVERSION_CODE: ${BuildConfig.VERSION_CODE}" + - "\nFLAVOR: ${BuildConfig.FLAVOR}" + - "\nBUILD_TYPE: ${BuildConfig.BUILD_TYPE}" - ) - } - context.startActivity(intent) - }, + onClick = { contactBottomSheet.value = true }, independent = false ) @@ -507,14 +514,20 @@ fun AppSettingsScreen( Spacer(modifier = Modifier.height(20.dp)) DeviceInfoCard() - 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)) - ), modifier = Modifier.padding(start = 16.dp, bottom = 2.dp, top = 24.dp) - ) + 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.about), style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = textColor.copy(alpha = 0.6f), + fontFamily = FontFamily(Font(R.font.sf_pro)) + ) + ) + } val rowHeight = remember { mutableStateOf(0.dp) } val density = LocalDensity.current @@ -719,5 +732,93 @@ fun AppSettingsScreen( }) } } + 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 + ) + ) + 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/RenameScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt index 95bb80c..c1fb2e0 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 @@ -21,43 +21,26 @@ package me.kavishdevar.librepods.presentation.screens import android.content.Context -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme 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.foundation.text.BasicTextField +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -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.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextRange -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.TextFieldValue import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp 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.viewmodel.AirPodsViewModel import kotlin.io.encoding.ExperimentalEncodingApi @@ -67,14 +50,12 @@ import kotlin.io.encoding.ExperimentalEncodingApi @Composable fun RenameScreen(viewModel: AirPodsViewModel) { val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) - val name = remember { mutableStateOf(TextFieldValue(sharedPreferences.getString("name", "") ?: "")) } val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current LaunchedEffect(Unit) { focusRequester.requestFocus() keyboardController?.show() - name.value = name.value.copy(selection = TextRange(name.value.text.length)) } StyledScaffold( @@ -86,67 +67,18 @@ fun RenameScreen(viewModel: AirPodsViewModel) { .padding(horizontal = 16.dp) ) { Spacer(modifier = Modifier.height(spacerHeight)) - val isDarkTheme = isSystemInDarkTheme() - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color.Black - val cursorColor = if (isDarkTheme) Color.White else Color.Black - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(58.dp) - .background( - backgroundColor, - RoundedCornerShape(28.dp) - ) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - BasicTextField( - value = name.value, - onValueChange = { - name.value = it - sharedPreferences.edit {putString("name", it.text)} - viewModel.setName(it.text) - }, - textStyle = TextStyle( - fontSize = 16.sp, - color = textColor, - fontFamily = FontFamily(Font(R.font.sf_pro)) - ), - singleLine = true, - cursorBrush = SolidColor(cursorColor), - decorationBox = { innerTextField -> - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Row( - modifier = Modifier - .weight(1f) - ) { - innerTextField() - } - IconButton( - onClick = { - name.value = TextFieldValue("") - } - ) { - 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) - ) + + val textFieldState = rememberTextFieldState() + textFieldState.edit { sharedPreferences.getString("name", "") ?: "" } + LaunchedEffect(textFieldState.text) { + sharedPreferences.edit {putString("name", textFieldState.text as String?)} + viewModel.setName(textFieldState.text.toString()) } + + StyledInputField( + textFieldState, + focusRequester + ) } } } 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 4879fba..f12b254 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 @@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.billing.BillingManager import me.kavishdevar.librepods.data.XposedRemotePrefProvider import me.kavishdevar.librepods.utils.NativeBridge +import me.kavishdevar.librepods.utils.XposedState import kotlin.math.roundToInt data class AppSettingsUiState( @@ -91,7 +91,7 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false) ) } - if (BuildConfig.FLAVOR == "xposed") { + if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) { NativeBridge.setSdpHook(_uiState.value.vendorIdHook) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/XposedState.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/XposedState.kt new file mode 100644 index 0000000..284e5e6 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/XposedState.kt @@ -0,0 +1,6 @@ +package me.kavishdevar.librepods.utils + +object XposedState { + var isAvailable: Boolean = false + var bluetoothScopeEnabled: Boolean = false +} diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index d3f7f1b..3a1db0b 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -263,4 +263,8 @@ Digital Assistant on Long Press Invoke Digital Assistant when long pressing the AirPods Pro stem. Customizations unavailable. Connect your AirPods at least once to access. + Xposed available + App enabled in Xposed + Subject + Describe your issue diff --git a/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt b/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt index 8923d46..50ee829 100644 --- a/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt +++ b/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt @@ -9,6 +9,7 @@ import io.github.libxposed.service.XposedServiceHelper import me.kavishdevar.librepods.billing.BillingManager import me.kavishdevar.librepods.billing.BillingProviderFactory import me.kavishdevar.librepods.utils.XposedServiceHolder +import me.kavishdevar.librepods.utils.XposedState class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver { override fun onCreate() { @@ -22,13 +23,18 @@ class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener override fun onResume(owner: LifecycleOwner) { BillingManager.provider.queryPurchases() + XposedState.isAvailable = true + XposedState.bluetoothScopeEnabled = XposedServiceHolder.service?.scope?.contains("com.google.android.bluetooth") == true || XposedServiceHolder.service?.scope?.contains("com.android.bluetooth") == true } - override fun onServiceBind(p0: XposedService) { - XposedServiceHolder.service = p0 + override fun onServiceBind(service: XposedService) { + XposedServiceHolder.service = service + XposedState.isAvailable = true + XposedState.bluetoothScopeEnabled = XposedServiceHolder.service?.scope?.contains("com.google.android.bluetooth") == true || XposedServiceHolder.service?.scope?.contains("com.android.bluetooth") == true } override fun onServiceDied(p0: XposedService) { XposedServiceHolder.service = null + XposedState.isAvailable = false } }