mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-29 17:44:36 +00:00
android: add xposed check and email form
too many emails with absolutely no content
This commit is contained in:
29
.github/workflows/ci-android.yml
vendored
29
.github/workflows/ci-android.yml
vendored
@@ -84,16 +84,35 @@ jobs:
|
|||||||
needs: build
|
needs: build
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-artifacts
|
name: apk-release
|
||||||
path: artifacts
|
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
|
- id: prev
|
||||||
run: |
|
run: |
|
||||||
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- id: changelog
|
- id: changelog
|
||||||
run: |
|
run: |
|
||||||
if [ -z "${{ steps.prev.outputs.tag }}" ]; then
|
if [ -z "${{ steps.prev.outputs.tag }}" ]; then
|
||||||
@@ -104,13 +123,15 @@ jobs:
|
|||||||
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
||||||
echo "$NOTES" >> $GITHUB_OUTPUT
|
echo "$NOTES" >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- id: tag
|
- id: tag
|
||||||
run: echo "tag=nightly-${{ needs.build.outputs.short_sha }}" >> $GITHUB_OUTPUT
|
run: echo "tag=nightly-${{ needs.build.outputs.short_sha }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- env:
|
- env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
gh release create "${{ steps.tag.outputs.tag }}" \
|
gh release create "${{ steps.tag.outputs.tag }}" \
|
||||||
artifacts/* \
|
artifacts/**/* \
|
||||||
-t "Nightly ${{ needs.build.outputs.short_sha }}" \
|
-t "Nightly ${{ needs.build.outputs.short_sha }}" \
|
||||||
--notes "${{ steps.changelog.outputs.notes }}" \
|
--notes "${{ steps.changelog.outputs.notes }}" \
|
||||||
--prerelease
|
--prerelease
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
val appVersionName = "0.2.3"
|
val appVersionName = "0.2.4"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
@@ -30,7 +30,7 @@ android {
|
|||||||
applicationId = "me.kavishdevar.librepods"
|
applicationId = "me.kavishdevar.librepods"
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 38
|
versionCode = 40
|
||||||
versionName = appVersionName
|
versionName = appVersionName
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Notifications
|
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.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.rotate
|
import androidx.compose.ui.graphics.drawscope.rotate
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.platform.LocalWindowInfo
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
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.data.ControlCommandRepository
|
||||||
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
|
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
|
||||||
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
|
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.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
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.AccessibilitySettingsScreen
|
||||||
import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen
|
import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen
|
||||||
import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen
|
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.AppSettingsViewModel
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
import me.kavishdevar.librepods.utils.XposedState
|
||||||
import me.kavishdevar.librepods.utils.isSupported
|
import me.kavishdevar.librepods.utils.isSupported
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@@ -157,7 +164,7 @@ lateinit var connectionStatusReceiver: BroadcastReceiver
|
|||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
init {
|
init {
|
||||||
if (BuildConfig.FLAVOR == "xposed") {
|
if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) {
|
||||||
System.loadLibrary("l2c_fcr_hook")
|
System.loadLibrary("l2c_fcr_hook")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,23 +336,118 @@ fun Main() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (BuildConfig.PLAY_BUILD) {
|
if (BuildConfig.PLAY_BUILD) {
|
||||||
PlayBypassSheet(
|
StyledBottomSheet(
|
||||||
visible = showPlayBypassVisible.value,
|
visible = showPlayBypassVisible.value,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
showPlayBypassVisible.value = false
|
showPlayBypassVisible.value = false
|
||||||
showDialog.value = true
|
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
|
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
|
return
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.Build
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.utils.XposedState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceInfoCard() {
|
fun DeviceInfoCard() {
|
||||||
@@ -41,14 +43,20 @@ fun DeviceInfoCard() {
|
|||||||
Column (
|
Column (
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Box(
|
||||||
text = stringResource(R.string.device_info), style = TextStyle(
|
modifier = Modifier
|
||||||
fontSize = 14.sp,
|
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
|
||||||
fontWeight = FontWeight.Bold,
|
.padding(start = 16.dp, bottom = 2.dp, top = 24.dp, end = 4.dp)
|
||||||
color = textColor.copy(alpha = 0.6f),
|
) {
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
Text(
|
||||||
), modifier = Modifier.padding(start = 16.dp)
|
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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(28.dp))
|
.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))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ import androidx.compose.ui.graphics.rememberGraphicsLayer
|
|||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
@@ -81,9 +82,11 @@ import kotlin.math.tanh
|
|||||||
fun StyledIconButton(
|
fun StyledIconButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
icon: String,
|
icon: String,
|
||||||
tint: Color = Color.Unspecified,
|
iconTint: Color = Color.Unspecified,
|
||||||
|
surfaceColor: Color = Color.Unspecified,
|
||||||
backdrop: LayerBackdrop = rememberLayerBackdrop(),
|
backdrop: LayerBackdrop = rememberLayerBackdrop(),
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit,
|
||||||
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
val haptics = LocalHapticFeedback.current
|
val haptics = LocalHapticFeedback.current
|
||||||
val darkMode = isSystemInDarkTheme()
|
val darkMode = isSystemInDarkTheme()
|
||||||
@@ -96,6 +99,7 @@ fun StyledIconButton(
|
|||||||
val innerShadowLayer = rememberGraphicsLayer().apply {
|
val innerShadowLayer = rememberGraphicsLayer().apply {
|
||||||
compositingStrategy = CompositingStrategy.Offscreen
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
}
|
}
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val interactiveHighlightShader = remember {
|
val interactiveHighlightShader = remember {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
@@ -120,8 +124,10 @@ half4 main(float2 coord) {
|
|||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
|
if (enabled) {
|
||||||
onClick()
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(56.dp),
|
shape = RoundedCornerShape(56.dp),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -137,6 +143,7 @@ half4 main(float2 coord) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
layerBlock = {
|
layerBlock = {
|
||||||
|
if (!enabled) return@drawBackdrop
|
||||||
val width = size.width
|
val width = size.width
|
||||||
val height = size.height
|
val height = size.height
|
||||||
|
|
||||||
@@ -161,6 +168,12 @@ half4 main(float2 coord) {
|
|||||||
(height / width).fastCoerceAtMost(1f)
|
(height / width).fastCoerceAtMost(1f)
|
||||||
},
|
},
|
||||||
onDrawSurface = {
|
onDrawSurface = {
|
||||||
|
if (!enabled) {
|
||||||
|
drawRect(
|
||||||
|
(if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy(0.5f)
|
||||||
|
)
|
||||||
|
return@drawBackdrop
|
||||||
|
}
|
||||||
val progress = progressAnimation.value.coerceIn(0f, 1f)
|
val progress = progressAnimation.value.coerceIn(0f, 1f)
|
||||||
|
|
||||||
val shape = RoundedCornerShape(56.dp)
|
val shape = RoundedCornerShape(56.dp)
|
||||||
@@ -187,6 +200,10 @@ half4 main(float2 coord) {
|
|||||||
}
|
}
|
||||||
drawLayer(innerShadowLayer)
|
drawLayer(innerShadowLayer)
|
||||||
|
|
||||||
|
if (surfaceColor.isSpecified) {
|
||||||
|
drawRect(surfaceColor)
|
||||||
|
}
|
||||||
|
|
||||||
drawRect(
|
drawRect(
|
||||||
(if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy(
|
(if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy(
|
||||||
progress.coerceIn(
|
progress.coerceIn(
|
||||||
@@ -197,6 +214,7 @@ half4 main(float2 coord) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDrawFront = {
|
onDrawFront = {
|
||||||
|
if (!enabled) return@drawBackdrop
|
||||||
val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
|
val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
|
||||||
if (progress > 0f) {
|
if (progress > 0f) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && interactiveHighlightShader != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && interactiveHighlightShader != null) {
|
||||||
@@ -241,40 +259,46 @@ half4 main(float2 coord) {
|
|||||||
)
|
)
|
||||||
.pointerInput(scope) {
|
.pointerInput(scope) {
|
||||||
val onDragStop: () -> Unit = {
|
val onDragStop: () -> Unit = {
|
||||||
scope.launch {
|
if (enabled) {
|
||||||
launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
|
scope.launch {
|
||||||
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
|
||||||
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
|
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
||||||
|
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inspectDragGestures(
|
inspectDragGestures(
|
||||||
onDragStart = { down ->
|
onDragStart = { down ->
|
||||||
pressStartPosition = down.position
|
if (enabled) {
|
||||||
scope.launch {
|
pressStartPosition = down.position
|
||||||
launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
|
scope.launch {
|
||||||
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
|
||||||
launch { offsetAnimation.snapTo(Offset.Zero) }
|
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
||||||
|
launch { offsetAnimation.snapTo(Offset.Zero) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDragEnd = { onDragStop() },
|
onDragEnd = { onDragStop() },
|
||||||
onDragCancel = onDragStop
|
onDragCancel = onDragStop
|
||||||
) { _, dragAmount ->
|
) { _, dragAmount ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
|
if (enabled) {
|
||||||
HapticFeedbackType.SegmentFrequentTick
|
if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
|
||||||
)
|
HapticFeedbackType.SegmentFrequentTick
|
||||||
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
)
|
||||||
|
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.size(48.dp),
|
.size(with(density) { 48.sp.toDp() }),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = icon,
|
text = icon,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 20.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
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))
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.padding
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -98,7 +98,7 @@ fun StyledSelectList(
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(if (hasIcon) 72.dp else 55.dp)
|
.heightIn(min = if (hasIcon) 72.dp else 55.dp)
|
||||||
.background(animatedBackgroundColor, shape)
|
.background(animatedBackgroundColor, shape)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
@@ -128,7 +128,7 @@ fun StyledSelectList(
|
|||||||
contentDescription = "Icon",
|
contentDescription = "Icon",
|
||||||
tint = Color(0xFF007AFF),
|
tint = Color(0xFF007AFF),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(48.dp)
|
.heightIn(min = 48.dp)
|
||||||
.wrapContentWidth()
|
.wrapContentWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (!hearingAidEnabled && BuildConfig.FLAVOR == "xposed") {
|
// if (!hearingAidEnabled && XposedState.isAvailable) {
|
||||||
// Text(
|
// Text(
|
||||||
// text = stringResource(R.string.apply_eq_to), style = TextStyle(
|
// text = stringResource(R.string.apply_eq_to), style = TextStyle(
|
||||||
// fontSize = 14.sp,
|
// fontSize = 14.sp,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.widget.Toast
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -35,8 +36,11 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
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.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -49,8 +53,10 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.lerp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -74,12 +82,17 @@ import me.kavishdevar.librepods.BuildConfig
|
|||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
|
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
|
||||||
import me.kavishdevar.librepods.presentation.components.NavigationButton
|
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.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.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
||||||
|
import me.kavishdevar.librepods.utils.XposedState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSettingsScreen(
|
fun AppSettingsScreen(
|
||||||
navController: NavController, viewModel: AppSettingsViewModel = viewModel()
|
navController: NavController, viewModel: AppSettingsViewModel = viewModel()
|
||||||
@@ -90,6 +103,12 @@ fun AppSettingsScreen(
|
|||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
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(
|
StyledScaffold(
|
||||||
title = stringResource(R.string.settings)
|
title = stringResource(R.string.settings)
|
||||||
) { topPadding, hazeState, bottomPadding ->
|
) { topPadding, hazeState, bottomPadding ->
|
||||||
@@ -367,24 +386,28 @@ fun AppSettingsScreen(
|
|||||||
independent = true,
|
independent = true,
|
||||||
enabled = state.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Box(
|
||||||
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
|
modifier = Modifier
|
||||||
|
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
|
||||||
.padding(horizontal = 16.dp)
|
.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 (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) {
|
||||||
if (BuildConfig.FLAVOR == "xposed") {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
val restartBluetoothText =
|
val restartBluetoothText =
|
||||||
stringResource(R.string.found_offset_restart_bluetooth)
|
stringResource(R.string.found_offset_restart_bluetooth)
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
@@ -417,14 +440,20 @@ fun AppSettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
Box(
|
||||||
text = stringResource(R.string.contact), style = TextStyle(
|
modifier = Modifier
|
||||||
fontSize = 14.sp,
|
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
|
||||||
fontWeight = FontWeight.Bold,
|
.padding(start = 16.dp, bottom = 2.dp, top = 24.dp, end = 4.dp)
|
||||||
color = textColor.copy(alpha = 0.6f),
|
) {
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
Text(
|
||||||
), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
|
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))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Column(
|
Column(
|
||||||
@@ -439,29 +468,7 @@ fun AppSettingsScreen(
|
|||||||
to = "",
|
to = "",
|
||||||
name = stringResource(R.string.email),
|
name = stringResource(R.string.email),
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onClick = {
|
onClick = { contactBottomSheet.value = true },
|
||||||
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,
|
|
||||||
"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)
|
|
||||||
},
|
|
||||||
independent = false
|
independent = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -507,14 +514,20 @@ fun AppSettingsScreen(
|
|||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
DeviceInfoCard()
|
DeviceInfoCard()
|
||||||
|
|
||||||
Text(
|
Box(
|
||||||
text = stringResource(R.string.about), style = TextStyle(
|
modifier = Modifier
|
||||||
fontSize = 14.sp,
|
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
|
||||||
fontWeight = FontWeight.Bold,
|
.padding(start = 16.dp, bottom = 2.dp, top = 24.dp, end = 4.dp)
|
||||||
color = textColor.copy(alpha = 0.6f),
|
) {
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
Text(
|
||||||
), modifier = Modifier.padding(start = 16.dp, bottom = 2.dp, top = 24.dp)
|
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 rowHeight = remember { mutableStateOf(0.dp) }
|
||||||
val density = LocalDensity.current
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,43 +21,26 @@
|
|||||||
package me.kavishdevar.librepods.presentation.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import android.content.Context
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.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.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
@@ -67,14 +50,12 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
|||||||
@Composable
|
@Composable
|
||||||
fun RenameScreen(viewModel: AirPodsViewModel) {
|
fun RenameScreen(viewModel: AirPodsViewModel) {
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val name = remember { mutableStateOf(TextFieldValue(sharedPreferences.getString("name", "") ?: "")) }
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
keyboardController?.show()
|
keyboardController?.show()
|
||||||
name.value = name.value.copy(selection = TextRange(name.value.text.length))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
@@ -86,67 +67,18 @@ fun RenameScreen(viewModel: AirPodsViewModel) {
|
|||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(spacerHeight))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val textFieldState = rememberTextFieldState()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
textFieldState.edit { sharedPreferences.getString("name", "") ?: "" }
|
||||||
val cursorColor = if (isDarkTheme) Color.White else Color.Black
|
LaunchedEffect(textFieldState.text) {
|
||||||
Row(
|
sharedPreferences.edit {putString("name", textFieldState.text as String?)}
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
viewModel.setName(textFieldState.text.toString())
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StyledInputField(
|
||||||
|
textFieldState,
|
||||||
|
focusRequester
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.BuildConfig
|
|
||||||
import me.kavishdevar.librepods.billing.BillingManager
|
import me.kavishdevar.librepods.billing.BillingManager
|
||||||
import me.kavishdevar.librepods.data.XposedRemotePrefProvider
|
import me.kavishdevar.librepods.data.XposedRemotePrefProvider
|
||||||
import me.kavishdevar.librepods.utils.NativeBridge
|
import me.kavishdevar.librepods.utils.NativeBridge
|
||||||
|
import me.kavishdevar.librepods.utils.XposedState
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
data class AppSettingsUiState(
|
data class AppSettingsUiState(
|
||||||
@@ -91,7 +91,7 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
|
connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (BuildConfig.FLAVOR == "xposed") {
|
if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) {
|
||||||
NativeBridge.setSdpHook(_uiState.value.vendorIdHook)
|
NativeBridge.setSdpHook(_uiState.value.vendorIdHook)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
|
object XposedState {
|
||||||
|
var isAvailable: Boolean = false
|
||||||
|
var bluetoothScopeEnabled: Boolean = false
|
||||||
|
}
|
||||||
@@ -263,4 +263,8 @@
|
|||||||
<string name="digital_assistant_on_long_press">Digital Assistant on Long Press</string>
|
<string name="digital_assistant_on_long_press">Digital Assistant on Long Press</string>
|
||||||
<string name="digital_assistant_on_long_press_description">Invoke Digital Assistant when long pressing the AirPods Pro stem.</string>
|
<string name="digital_assistant_on_long_press_description">Invoke Digital Assistant when long pressing the AirPods Pro stem.</string>
|
||||||
<string name="customizations_unavailable">Customizations unavailable. Connect your AirPods at least once to access.</string>
|
<string name="customizations_unavailable">Customizations unavailable. Connect your AirPods at least once to access.</string>
|
||||||
|
<string name="xposed_available">Xposed available</string>
|
||||||
|
<string name="app_enabled_in_xposed">App enabled in Xposed</string>
|
||||||
|
<string name="subject">Subject</string>
|
||||||
|
<string name="describe_your_issue">Describe your issue</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import io.github.libxposed.service.XposedServiceHelper
|
|||||||
import me.kavishdevar.librepods.billing.BillingManager
|
import me.kavishdevar.librepods.billing.BillingManager
|
||||||
import me.kavishdevar.librepods.billing.BillingProviderFactory
|
import me.kavishdevar.librepods.billing.BillingProviderFactory
|
||||||
import me.kavishdevar.librepods.utils.XposedServiceHolder
|
import me.kavishdevar.librepods.utils.XposedServiceHolder
|
||||||
|
import me.kavishdevar.librepods.utils.XposedState
|
||||||
|
|
||||||
class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver {
|
class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -22,13 +23,18 @@ class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener
|
|||||||
|
|
||||||
override fun onResume(owner: LifecycleOwner) {
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
BillingManager.provider.queryPurchases()
|
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) {
|
override fun onServiceBind(service: XposedService) {
|
||||||
XposedServiceHolder.service = p0
|
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) {
|
override fun onServiceDied(p0: XposedService) {
|
||||||
XposedServiceHolder.service = null
|
XposedServiceHolder.service = null
|
||||||
|
XposedState.isAvailable = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user