mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-29 09:33:04 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70f420dedb | ||
|
|
23193ceb39 | ||
|
|
cb246d1287 | ||
|
|
95cd677da9 | ||
|
|
0d049d93fb | ||
|
|
469d948061 | ||
|
|
f5d92768e2 | ||
|
|
8cb2951bc6 | ||
|
|
bb578dab23 | ||
|
|
b1b47048a3 | ||
|
|
bf09300dfe | ||
|
|
70165232c0 | ||
|
|
aabbc902cb | ||
|
|
0ee7056600 |
171
.github/workflows/ci-android.yml
vendored
171
.github/workflows/ci-android.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Build APK and root module (and create nightly release)
|
name: Android CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,95 +6,138 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
paths:
|
paths:
|
||||||
- 'android/**'
|
- 'android/**'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'android/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
release:
|
branch:
|
||||||
description: 'Create a nightly release'
|
description: Branch to build
|
||||||
required: true
|
required: true
|
||||||
type: boolean
|
default: main
|
||||||
default: false
|
|
||||||
custom_notes:
|
|
||||||
description: 'Custom updates to add to What''s Changed section'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-debug-apk:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
short_sha: ${{ steps.vars.outputs.short_sha }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || github.ref }}
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: zulu
|
||||||
java-version: 21
|
java-version: 21
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
- name: Build debug APK
|
- name: Decode keystore
|
||||||
run: ./gradlew assembleDebug
|
run: echo "${{ secrets.RELEASE_KEYSTORE_FILE }}" | base64 --decode > android/release.keystore
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
- name: Accept Licenses
|
||||||
|
run: yes | sdkmanager --licenses
|
||||||
|
- name: Install NDK
|
||||||
|
run: sdkmanager "ndk;30.0.14904198"
|
||||||
|
- name: Create local.properties
|
||||||
|
run: |
|
||||||
|
cat <<EOF > android/local.properties
|
||||||
|
RELEASE_STORE_FILE=../release.keystore
|
||||||
|
RELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }}
|
||||||
|
RELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }}
|
||||||
|
RELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew packageReleaseArtifacts
|
||||||
working-directory: android
|
working-directory: android
|
||||||
- name: Upload artifact
|
- id: vars
|
||||||
uses: actions/upload-artifact@v4
|
run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Debug APK
|
name: apk-release
|
||||||
path: android/app/build/outputs/apk/**/*.apk
|
path: release/*release.apk
|
||||||
nightly-release:
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apk-debug
|
||||||
|
path: release/*debug.apk
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: root-module-release
|
||||||
|
path: release/*release.zip
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: root-module-debug
|
||||||
|
path: release/*debug.zip
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-bundle
|
||||||
|
path: release/*.aab
|
||||||
|
|
||||||
|
release:
|
||||||
|
if: github.event_name == 'push'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/release-nightly' || github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'true'
|
needs: build
|
||||||
needs: build-debug-apk
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
- name: Export APK_NAME for later use
|
with:
|
||||||
run: echo "APK_NAME=LibrePods-$(echo ${{ github.sha }} | cut -c1-7).apk" >> $GITHUB_ENV
|
name: apk-release
|
||||||
- name: Rename .apk file
|
path: artifacts/apk-release
|
||||||
run: mv "./Debug APK/debug/"*.apk "./$APK_NAME"
|
|
||||||
- name: Decode keystore file
|
|
||||||
run: echo "${{ secrets.DEBUG_KEYSTORE_FILE }}" | base64 --decode > debug.keystore
|
|
||||||
- name: Install apksigner
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y apksigner
|
|
||||||
- name: Sign APK
|
|
||||||
run: |
|
|
||||||
apksigner sign --ks debug.keystore --ks-key-alias androiddebugkey --ks-pass pass:android --key-pass pass:android "./$APK_NAME"
|
|
||||||
- name: Verify APK
|
|
||||||
run: apksigner verify "./$APK_NAME"
|
|
||||||
- name: Fetch the latest non-nightly release tag
|
|
||||||
id: fetch-tag
|
|
||||||
run: echo "::set-output name=tag::$(git describe --tags $(git rev-list --tags --max-count=1))"
|
|
||||||
- name: Retrieve commits since the last release
|
|
||||||
id: get-commits
|
|
||||||
run: |
|
|
||||||
COMMITS=$(git log ${{ steps.fetch-tag.outputs.tag }}..HEAD --pretty=format:"- %s (%h)" --abbrev-commit)
|
|
||||||
echo "::set-output name=commits::${COMMITS}"
|
|
||||||
- name: Prepare release notes
|
|
||||||
id: release-notes
|
|
||||||
run: |
|
|
||||||
# Create a temporary file for release notes
|
|
||||||
NOTES_FILE=$(mktemp)
|
|
||||||
|
|
||||||
# Process custom notes if they exist
|
- uses: actions/download-artifact@v4
|
||||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ github.event.inputs.custom_notes }}" ]; then
|
with:
|
||||||
CUSTOM_NOTES="${{ github.event.inputs.custom_notes }}"
|
name: apk-debug
|
||||||
|
path: artifacts/apk-debug
|
||||||
|
|
||||||
# Check if custom notes already have bullet points or GitHub-style formatting
|
- uses: actions/download-artifact@v4
|
||||||
if echo "$CUSTOM_NOTES" | grep -q "^\*\|^- \|http.*commit\|in #[0-9]\+"; then
|
with:
|
||||||
# Already formatted, use as is
|
name: root-module-release
|
||||||
echo "$CUSTOM_NOTES" > "$NOTES_FILE"
|
path: artifacts/root-module-release
|
||||||
else
|
|
||||||
# Add bullet point formatting
|
|
||||||
echo "- $CUSTOM_NOTES" > "$NOTES_FILE"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "notes_file=$NOTES_FILE" >> $GITHUB_OUTPUT
|
- uses: actions/download-artifact@v4
|
||||||
- name: Zip root-module directory
|
with:
|
||||||
run: sh ./build-magisk-module.sh
|
name: root-module-debug
|
||||||
- name: Delete release if exist then create release
|
path: artifacts/root-module-debug
|
||||||
|
- id: prev
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
gh release view "nightly" && gh release delete "nightly" -y --cleanup-tag
|
TAG=$(gh release list \
|
||||||
gh release create "nightly" "./$APK_NAME" "./btl2capfix.zip" -p -t "Nightly Release" --notes-file "${{ steps.release-notes.outputs.notes_file }}" --generate-notes
|
--limit 1 \
|
||||||
|
--json tagName \
|
||||||
|
--jq '.[0].tagName')
|
||||||
|
|
||||||
|
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||||
|
- id: changelog
|
||||||
|
run: |
|
||||||
|
if [ -z "${{ steps.prev.outputs.tag }}" ]; then
|
||||||
|
NOTES=$(git log --pretty=format:"- %s (%h)")
|
||||||
|
else
|
||||||
|
NOTES=$(git log ${{ steps.prev.outputs.tag }}..HEAD --pretty=format:"- %s (%h)")
|
||||||
|
fi
|
||||||
|
echo "notes<<EOF" >> $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/**/* \
|
||||||
|
-t "Nightly ${{ needs.build.outputs.short_sha }}" \
|
||||||
|
--notes "${{ steps.changelog.outputs.notes }}" \
|
||||||
|
--prerelease
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
val versionName = "0.2.3"
|
val appVersionName = "0.2.5"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
@@ -30,8 +30,8 @@ android {
|
|||||||
applicationId = "me.kavishdevar.librepods"
|
applicationId = "me.kavishdevar.librepods"
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 38
|
versionCode = 42
|
||||||
versionName = versionName
|
versionName = appVersionName
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
@@ -177,7 +177,7 @@ fun registerRootModuleZipTask(
|
|||||||
rename { "LibrePods.apk" }
|
rename { "LibrePods.apk" }
|
||||||
}
|
}
|
||||||
|
|
||||||
archiveFileName.set("LibrePods-FOSS-v$versionName-$buildType.zip")
|
archiveFileName.set("LibrePods-FOSS-v$appVersionName-$buildType.zip")
|
||||||
destinationDirectory.set(layout.buildDirectory.dir("outputs/rootModuleZips"))
|
destinationDirectory.set(layout.buildDirectory.dir("outputs/rootModuleZips"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,12 +205,12 @@ val collect = tasks.register<Copy>("collectReleaseArtifacts") {
|
|||||||
|
|
||||||
from(layout.buildDirectory.dir("outputs/apk/xposed/release")) {
|
from(layout.buildDirectory.dir("outputs/apk/xposed/release")) {
|
||||||
include("*.apk")
|
include("*.apk")
|
||||||
rename(".*", "LibrePods-FOSS-v$versionName-release.apk")
|
rename(".*", "LibrePods-FOSS-v$appVersionName-release.apk")
|
||||||
}
|
}
|
||||||
|
|
||||||
from(layout.buildDirectory.dir("outputs/apk/xposed/debug")) {
|
from(layout.buildDirectory.dir("outputs/apk/xposed/debug")) {
|
||||||
include("*.apk")
|
include("*.apk")
|
||||||
rename(".*", "LibrePods-FOSS-v$versionName-debug.apk")
|
rename(".*", "LibrePods-FOSS-v$appVersionName-debug.apk")
|
||||||
}
|
}
|
||||||
|
|
||||||
from(layout.buildDirectory.dir("outputs/bundle/xposedPlayRelease")) {
|
from(layout.buildDirectory.dir("outputs/bundle/xposedPlayRelease")) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -118,11 +121,15 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
import dev.chrisbanes.haze.rememberHazeState
|
import dev.chrisbanes.haze.rememberHazeState
|
||||||
import me.kavishdevar.librepods.data.AirPodsNotifications
|
import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||||
import me.kavishdevar.librepods.data.ControlCommandRepository
|
import me.kavishdevar.librepods.data.ControlCommandRepository
|
||||||
|
import me.kavishdevar.librepods.presentation.components.AppInfoCard
|
||||||
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 +153,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 +165,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,7 +224,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
fun Main() {
|
fun Main() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
if (!isSupported(sharedPreferences)) {
|
if (!isSupported(sharedPreferences) && !XposedState.bluetoothScopeEnabled) {
|
||||||
val showDialog = remember { mutableStateOf(false) }
|
val showDialog = remember { mutableStateOf(false) }
|
||||||
val showPlayBypassVisible = remember { mutableStateOf(false) }
|
val showPlayBypassVisible = remember { mutableStateOf(false) }
|
||||||
val hazeState = rememberHazeState()
|
val hazeState = rememberHazeState()
|
||||||
@@ -242,27 +250,25 @@ fun Main() {
|
|||||||
verticalArrangement = Arrangement
|
verticalArrangement = Arrangement
|
||||||
.spacedBy(16.dp)
|
.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
val innerBackdrop = rememberLayerBackdrop()
|
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.layerBackdrop(innerBackdrop),
|
modifier = Modifier,
|
||||||
verticalArrangement = Arrangement
|
verticalArrangement = Arrangement
|
||||||
.spacedBy(16.dp)
|
.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.not_supported),
|
text = stringResource(R.string.not_supported),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 20.sp,
|
fontSize = 28.sp,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
),
|
),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
DeviceInfoCard()
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -274,7 +280,7 @@ fun Main() {
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
color = if (isDarkTheme) Color.White else Color.Black,
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -282,23 +288,28 @@ fun Main() {
|
|||||||
.padding(horizontal = 12.dp, vertical = 16.dp)
|
.padding(horizontal = 12.dp, vertical = 16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
StyledButton(
|
StyledButton(
|
||||||
onClick = { showDialog.value = true },
|
onClick = { showDialog.value = true },
|
||||||
backdrop = innerBackdrop,
|
backdrop = rememberLayerBackdrop(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
|
isInteractive = false,
|
||||||
|
surfaceColor = if (isDarkTheme) Color(0xFF862424) else Color(0xFFC94646)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.bypass_compatibility_check),
|
text = stringResource(R.string.bypass_compatibility_check),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
color = Color.White,
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
DeviceInfoCard()
|
||||||
|
AppInfoCard()
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,26 +327,105 @@ fun Main() {
|
|||||||
} else {
|
} else {
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putBoolean("bypass_device_check.v2", true)
|
putBoolean("bypass_device_check.v2", true)
|
||||||
|
}
|
||||||
val intent = Intent(context, MainActivity::class.java)
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
},
|
},
|
||||||
hazeState = hazeState
|
backdrop = backdrop
|
||||||
|
// hazeState = hazeState
|
||||||
)
|
)
|
||||||
|
|
||||||
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 = {
|
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
|
showPlayBypassVisible.value = false
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putBoolean("bypass_device_check.v2", true)
|
putBoolean("bypass_device_check.v2", true)
|
||||||
@@ -344,8 +434,25 @@ fun Main() {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backdrop = backdrop
|
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
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
/*
|
||||||
|
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
Copyright (C) 2025 LibrePods contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppInfoCard() {
|
||||||
|
val rowHeight = remember { mutableStateOf(0.dp) }
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
|
||||||
|
.padding(start = 16.dp, bottom = 8.dp, end = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.about), style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(28.dp))
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(28.dp))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
rowHeight.value = with(density) { coordinates.size.height.toDp() }
|
||||||
|
},
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.version), style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.VERSION_NAME, style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
||||||
|
alpha = 0.8f
|
||||||
|
),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.version_code), style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.VERSION_CODE.toString(), style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
||||||
|
alpha = 0.8f
|
||||||
|
),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.flavor), style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.FLAVOR, style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
||||||
|
alpha = 0.8f
|
||||||
|
),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.build_type), style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.BUILD_TYPE,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
||||||
|
alpha = 0.8f
|
||||||
|
),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,37 +18,32 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredWidthIn
|
import androidx.compose.foundation.layout.requiredWidthIn
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
||||||
import androidx.compose.ui.input.pointer.PointerEventType
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
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
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
@@ -56,13 +51,13 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import com.kyant.backdrop.backdrops.LayerBackdrop
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.HazeState
|
import com.kyant.backdrop.drawBackdrop
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import com.kyant.backdrop.effects.blur
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
import com.kyant.backdrop.effects.lens
|
||||||
|
import com.kyant.backdrop.effects.vibrancy
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@@ -75,36 +70,47 @@ fun ConfirmationDialog(
|
|||||||
dismissText: String = "Cancel",
|
dismissText: String = "Cancel",
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onDismiss: () -> Unit = { showDialog.value = false },
|
onDismiss: () -> Unit = { showDialog.value = false },
|
||||||
hazeState: HazeState,
|
backdrop: LayerBackdrop,
|
||||||
) {
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
val accentColor = if (isDarkTheme) Color(0xFF0091FF) else Color(0xFF0088FF)
|
||||||
|
|
||||||
val haptics = LocalHapticFeedback.current
|
AnimatedVisibility(
|
||||||
val scope = rememberCoroutineScope()
|
visible = showDialog.value,
|
||||||
|
enter = scaleIn(initialScale = 1.05f) + fadeIn(),
|
||||||
if (showDialog.value) {
|
exit = scaleOut(targetScale = 1.05f) + fadeOut()
|
||||||
Dialog(
|
) {
|
||||||
onDismissRequest = { showDialog.value = false },
|
Box(
|
||||||
properties = DialogProperties(
|
modifier = Modifier.fillMaxSize(),
|
||||||
dismissOnBackPress = false,
|
contentAlignment = Alignment.Center
|
||||||
dismissOnClickOutside = false
|
) {
|
||||||
)
|
val innerBackdrop = rememberLayerBackdrop()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.4f))
|
||||||
|
.clickable(enabled = false, onClick = {}),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// .fillMaxWidth(0.75f)
|
|
||||||
.requiredWidthIn(min = 200.dp, max = 360.dp)
|
.requiredWidthIn(min = 200.dp, max = 360.dp)
|
||||||
.background(Color.Transparent, RoundedCornerShape(14.dp))
|
.clip(RoundedCornerShape(48.dp))
|
||||||
.clip(RoundedCornerShape(14.dp))
|
.drawBackdrop(
|
||||||
.hazeEffect(
|
backdrop = backdrop,
|
||||||
hazeState,
|
exportedBackdrop = innerBackdrop,
|
||||||
style = CupertinoMaterials.regular(
|
shape = { RoundedCornerShape(48.dp) },
|
||||||
containerColor = if (isDarkTheme) Color(0xFF1C1C1E).copy(alpha = 0.95f) else Color.White.copy(alpha = 0.95f)
|
effects = {
|
||||||
|
vibrancy()
|
||||||
|
blur(4f.dp.toPx())
|
||||||
|
lens(12f.dp.toPx(), 48f.dp.toPx(), true)
|
||||||
|
},
|
||||||
|
onDrawSurface = {
|
||||||
|
drawRect(
|
||||||
|
if (isDarkTheme) Color(0xFF1F1F1F).copy(alpha = 0.35f) else Color(0xFFE0E0E0).copy(alpha = 0.7f)
|
||||||
)
|
)
|
||||||
)
|
})) {
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -130,108 +136,42 @@ fun ConfirmationDialog(
|
|||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = Color(0x40888888),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
var leftPressed by remember { mutableStateOf(false) }
|
|
||||||
var rightPressed by remember { mutableStateOf(false) }
|
|
||||||
val pressedColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
.fillMaxWidth()
|
horizontalArrangement = Arrangement.spacedBy(24.dp)
|
||||||
.height(48.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
awaitPointerEventScope {
|
|
||||||
while (true) {
|
|
||||||
val event = awaitPointerEvent()
|
|
||||||
val position = event.changes.first().position
|
|
||||||
val width = size.width.toFloat()
|
|
||||||
val height = size.height.toFloat()
|
|
||||||
val isWithinBounds = position.y >= 0 && position.y <= height
|
|
||||||
val isLeft = position.x < width / 2
|
|
||||||
event.changes.first().consume()
|
|
||||||
when (event.type) {
|
|
||||||
PointerEventType.Press -> {
|
|
||||||
if (isWithinBounds) {
|
|
||||||
leftPressed = isLeft
|
|
||||||
rightPressed = !isLeft
|
|
||||||
} else {
|
|
||||||
leftPressed = false
|
|
||||||
rightPressed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PointerEventType.Move -> {
|
|
||||||
if (isWithinBounds) {
|
|
||||||
if (leftPressed != isLeft) scope.launch { haptics.performHapticFeedback(
|
|
||||||
HapticFeedbackType.SegmentTick) }
|
|
||||||
leftPressed = isLeft
|
|
||||||
rightPressed = !isLeft
|
|
||||||
} else {
|
|
||||||
leftPressed = false
|
|
||||||
rightPressed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PointerEventType.Release -> {
|
|
||||||
if (isWithinBounds) {
|
|
||||||
if (leftPressed) {
|
|
||||||
scope.launch { haptics.performHapticFeedback(
|
|
||||||
HapticFeedbackType.Reject) }
|
|
||||||
onDismiss()
|
|
||||||
} else if (rightPressed) {
|
|
||||||
scope.launch { haptics.performHapticFeedback(
|
|
||||||
HapticFeedbackType.Confirm) }
|
|
||||||
onConfirm()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
leftPressed = false
|
|
||||||
rightPressed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
horizontalArrangement = Arrangement.Start,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
Box(
|
StyledButton(
|
||||||
modifier = Modifier
|
onClick = onDismiss,
|
||||||
.weight(1f)
|
backdrop = innerBackdrop,
|
||||||
.fillMaxHeight()
|
modifier = Modifier.weight(1f),
|
||||||
.background(if (leftPressed) pressedColor else Color.Transparent),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = dismissText,
|
text = dismissText, style = TextStyle(
|
||||||
style = TextStyle(
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
color = accentColor,
|
fontWeight = FontWeight.Medium,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontSize = 14.sp,
|
||||||
|
color = textColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
StyledButton(
|
||||||
modifier = Modifier
|
onClick = onConfirm,
|
||||||
.width(1.dp)
|
backdrop = innerBackdrop,
|
||||||
.fillMaxHeight()
|
modifier = Modifier.weight(1f),
|
||||||
.background(Color(0x40888888))
|
surfaceColor = accentColor
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.fillMaxHeight()
|
|
||||||
.background(if (rightPressed) pressedColor else Color.Transparent),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = confirmText,
|
text = confirmText, style = TextStyle(
|
||||||
style = TextStyle(
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
color = accentColor,
|
fontWeight = FontWeight.Medium,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontSize = 14.sp,
|
||||||
|
color = Color.White
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
@@ -40,6 +42,11 @@ fun DeviceInfoCard() {
|
|||||||
|
|
||||||
Column (
|
Column (
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
|
||||||
|
.padding(start = 16.dp, top = 24.dp, end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.device_info), style = TextStyle(
|
text = stringResource(R.string.device_info), style = TextStyle(
|
||||||
@@ -47,8 +54,9 @@ fun DeviceInfoCard() {
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f),
|
color = textColor.copy(alpha = 0.6f),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
), modifier = Modifier.padding(start = 16.dp)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
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 = {
|
||||||
|
if (enabled) {
|
||||||
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
|
||||||
onClick()
|
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,25 +259,30 @@ half4 main(float2 coord) {
|
|||||||
)
|
)
|
||||||
.pointerInput(scope) {
|
.pointerInput(scope) {
|
||||||
val onDragStop: () -> Unit = {
|
val onDragStop: () -> Unit = {
|
||||||
|
if (enabled) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
|
launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
|
||||||
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
||||||
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
|
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
inspectDragGestures(
|
inspectDragGestures(
|
||||||
onDragStart = { down ->
|
onDragStart = { down ->
|
||||||
|
if (enabled) {
|
||||||
pressStartPosition = down.position
|
pressStartPosition = down.position
|
||||||
scope.launch {
|
scope.launch {
|
||||||
launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
|
launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
|
||||||
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
||||||
launch { offsetAnimation.snapTo(Offset.Zero) }
|
launch { offsetAnimation.snapTo(Offset.Zero) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDragEnd = { onDragStop() },
|
onDragEnd = { onDragStop() },
|
||||||
onDragCancel = onDragStop
|
onDragCancel = onDragStop
|
||||||
) { _, dragAmount ->
|
) { _, dragAmount ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
if (enabled) {
|
||||||
if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
|
if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
|
||||||
HapticFeedbackType.SegmentFrequentTick
|
HapticFeedbackType.SegmentFrequentTick
|
||||||
)
|
)
|
||||||
@@ -267,14 +290,15 @@ half4 main(float2 coord) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.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,12 +53,12 @@ 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.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
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
|
||||||
@@ -62,7 +66,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
|
||||||
@@ -72,14 +78,20 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
|||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import me.kavishdevar.librepods.BuildConfig
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.presentation.components.AppInfoCard
|
||||||
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
|
import me.kavishdevar.librepods.presentation.components.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 +102,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,7 +385,14 @@ fun AppSettingsScreen(
|
|||||||
independent = true,
|
independent = true,
|
||||||
enabled = state.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
} else {
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(top = 16.dp, bottom = 2.dp)
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.customizations_unavailable),
|
text = stringResource(R.string.customizations_unavailable),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
@@ -377,14 +402,11 @@ fun AppSettingsScreen(
|
|||||||
color = textColor.copy(alpha = 0.6f),
|
color = textColor.copy(alpha = 0.6f),
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(top = 16.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +439,20 @@ fun AppSettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.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(
|
||||||
text = stringResource(R.string.contact), style = TextStyle(
|
text = stringResource(R.string.contact), style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f),
|
color = textColor.copy(alpha = 0.6f),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Column(
|
Column(
|
||||||
@@ -439,29 +467,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
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -506,139 +512,8 @@ fun AppSettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
DeviceInfoCard()
|
DeviceInfoCard()
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
AppInfoCard()
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
||||||
val rowHeight = remember { mutableStateOf(0.dp) }
|
|
||||||
val density = LocalDensity.current
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(28.dp))
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(backgroundColor, RoundedCornerShape(28.dp))
|
|
||||||
.padding(top = 2.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
.onGloballyPositioned { coordinates ->
|
|
||||||
rowHeight.value = with(density) { coordinates.size.height.toDp() }
|
|
||||||
},
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.version), style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = BuildConfig.VERSION_NAME, style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
|
||||||
alpha = 0.8f
|
|
||||||
),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = Color(0x40888888),
|
|
||||||
modifier = Modifier.padding(horizontal = 12.dp)
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.version_code), style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = BuildConfig.VERSION_CODE.toString(), style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
|
||||||
alpha = 0.8f
|
|
||||||
),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = Color(0x40888888),
|
|
||||||
modifier = Modifier.padding(horizontal = 12.dp)
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.flavor), style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = BuildConfig.FLAVOR, style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
|
||||||
alpha = 0.8f
|
|
||||||
),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = Color(0x40888888),
|
|
||||||
modifier = Modifier.padding(horizontal = 12.dp)
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.build_type), style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = BuildConfig.BUILD_TYPE,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
|
||||||
alpha = 0.8f
|
|
||||||
),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
@@ -719,5 +594,94 @@ 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,
|
||||||
|
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||||
|
)
|
||||||
|
)
|
||||||
|
StyledIconButton(
|
||||||
|
icon = "\uDBC0\uDE1F",
|
||||||
|
backdrop = innerBackdrop,
|
||||||
|
surfaceColor = if (isSystemInDarkTheme()) Color(0xFF0091FF) else Color(0xFF0088FF),
|
||||||
|
iconTint = if (subjectState.text.isNotEmpty() && descriptionState.text.isNotEmpty()) Color.White else Color.Gray,
|
||||||
|
enabled = subjectState.text.isNotEmpty() && descriptionState.text.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
contactBottomSheet.value = false
|
||||||
|
val intent = Intent(Intent.ACTION_SENDTO).apply {
|
||||||
|
data = "mailto:".toUri()
|
||||||
|
putExtra(Intent.EXTRA_EMAIL, arrayOf("contact@kavish.xyz"))
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, "LibrePods: ${subjectState.text}")
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
"${descriptionState.text}" +
|
||||||
|
"\n\n----------" +
|
||||||
|
"\nPhone details:" +
|
||||||
|
"\nMANUFACTURER: ${Build.MANUFACTURER}" +
|
||||||
|
"\nMODEL: ${Build.MODEL} (${Build.PRODUCT})" +
|
||||||
|
"\nDISPLAY_VERSION: ${Build.DISPLAY}" +
|
||||||
|
"\nID: ${Build.ID} (SDK ${Build.VERSION.SDK_INT_FULL})" +
|
||||||
|
"\nXposed enabled/active: ${XposedState.isAvailable}/${XposedState.bluetoothScopeEnabled}" +
|
||||||
|
"\n\nApp details:" +
|
||||||
|
"\nVERSION: ${BuildConfig.VERSION_NAME}" +
|
||||||
|
"\nVERSION_CODE: ${BuildConfig.VERSION_CODE}" +
|
||||||
|
"\nFLAVOR: ${BuildConfig.FLAVOR}" +
|
||||||
|
"\nBUILD_TYPE: ${BuildConfig.BUILD_TYPE}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
subjectState.clearText()
|
||||||
|
descriptionState.clearText()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
StyledInputField(
|
||||||
|
inputState = subjectState,
|
||||||
|
focusRequester = subjectFocusRequester,
|
||||||
|
placeholder = stringResource(R.string.subject),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
StyledInputField(
|
||||||
|
inputState = descriptionState,
|
||||||
|
focusRequester = descriptionFocusRequester,
|
||||||
|
placeholder = stringResource(R.string.describe_your_issue),
|
||||||
|
singleLine = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
|
|||||||
hearingAidEnabled.value = false
|
hearingAidEnabled.value = false
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
},
|
},
|
||||||
hazeState = hazeStateS.value,
|
// hazeState = hazeStateS.value,
|
||||||
// backdrop = backdrop
|
backdrop = backdrop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
StyledInputField(
|
||||||
name.value = TextFieldValue("")
|
textFieldState,
|
||||||
}
|
focusRequester
|
||||||
) {
|
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import android.os.Build
|
|||||||
fun isSupported(sharedPreferences: SharedPreferences): Boolean {
|
fun isSupported(sharedPreferences: SharedPreferences): Boolean {
|
||||||
val isPixel = Build.MANUFACTURER.lowercase() == "google"
|
val isPixel = Build.MANUFACTURER.lowercase() == "google"
|
||||||
val isOppoOrOnePlus = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo")
|
val isOppoOrOnePlus = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo")
|
||||||
|
val isBypassFlagActive = sharedPreferences.getBoolean("bypass_device_check.v2", false)
|
||||||
|
|
||||||
|
if (isBypassFlagActive) return true
|
||||||
|
|
||||||
if (isPixel) {
|
if (isPixel) {
|
||||||
when (Build.VERSION.SDK_INT) {
|
when (Build.VERSION.SDK_INT) {
|
||||||
@@ -38,5 +41,5 @@ fun isSupported(sharedPreferences: SharedPreferences): Boolean {
|
|||||||
} else if (isOppoOrOnePlus) {
|
} else if (isOppoOrOnePlus) {
|
||||||
return Build.VERSION.SDK_INT >= 36
|
return Build.VERSION.SDK_INT >= 36
|
||||||
}
|
}
|
||||||
return sharedPreferences.getBoolean("bypass_device_check.v2", false)
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class KotlinModule: XposedModule() {
|
|||||||
log(Log.INFO, TAG, "framework: $frameworkName($frameworkVersionCode) API $apiVersion")
|
log(Log.INFO, TAG, "framework: $frameworkName($frameworkVersionCode) API $apiVersion")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||||
override fun onPackageLoaded(param: PackageLoadedParam) {
|
override fun onPackageLoaded(param: PackageLoadedParam) {
|
||||||
log(Log.INFO, TAG, "onPackageLoaded :: ${param.packageName}")
|
log(Log.INFO, TAG, "onPackageLoaded :: ${param.packageName}")
|
||||||
|
|
||||||
@@ -27,8 +28,36 @@ class KotlinModule: XposedModule() {
|
|||||||
log(Log.INFO, TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
|
log(Log.INFO, TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
|
||||||
try {
|
try {
|
||||||
if (param.isFirstPackage) {
|
if (param.isFirstPackage) {
|
||||||
log(Log.INFO, TAG, "Loading native library for Bluetooth hook")
|
val abi = android.os.Build.SUPPORTED_ABIS.first()
|
||||||
System.loadLibrary("l2c_fcr_hook")
|
val soName = "libl2c_fcr_hook.so"
|
||||||
|
|
||||||
|
val candidates = buildList {
|
||||||
|
add("${moduleApplicationInfo.sourceDir}!/lib/$abi/$soName")
|
||||||
|
|
||||||
|
moduleApplicationInfo.splitSourceDirs?.forEach { split ->
|
||||||
|
add("$split!/lib/$abi/$soName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var loaded = false
|
||||||
|
|
||||||
|
for (path in candidates) {
|
||||||
|
try {
|
||||||
|
log(Log.INFO, TAG, "Trying to load native lib from $path")
|
||||||
|
System.load(path)
|
||||||
|
log(Log.INFO, TAG, "Loaded native lib from $path")
|
||||||
|
loaded = true
|
||||||
|
break
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
log(Log.WARN, TAG, "Failed to load from $path: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
log(Log.ERROR, TAG, "Could not load $soName from base or splits")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val remotePrefValue = getRemotePreferences("me.kavishdevar.librepods").getBoolean("vendor_id_hook", false)
|
val remotePrefValue = getRemotePreferences("me.kavishdevar.librepods").getBoolean("vendor_id_hook", false)
|
||||||
log(Log.INFO, TAG, "sdp hook enabled (remote pref): $remotePrefValue")
|
log(Log.INFO, TAG, "sdp hook enabled (remote pref): $remotePrefValue")
|
||||||
NativeBridge.setSdpHook(remotePrefValue)
|
NativeBridge.setSdpHook(remotePrefValue)
|
||||||
|
|||||||
Reference in New Issue
Block a user