mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-28 07:25:42 -04:00
feat(permissions): migrate all call sites off Accompanist + add denial recovery
Migrate intro, both map flavors, and the barcode scanner to the native rememberXxxPermissionState() helpers; remove accompanist-permissions from the version catalog, feature convention plugin, and module build files. Add user-facing denial recovery where it was previously silent: barcode camera shows a PermissionRecoveryCard, USB permission denial surfaces an error message, and the map location button routes permanent denial to app settings. Convert the Bluetooth bonding error to a string resource. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -265,7 +265,6 @@ dependencies {
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.koin.androidx.workmanager)
|
||||
implementation(libs.koin.annotations)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.Manifest
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -68,8 +67,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import org.meshtastic.core.ui.util.PermissionStatus
|
||||
import org.meshtastic.core.ui.util.rememberLocationPermissionState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
@@ -208,7 +207,6 @@ private fun cacheManagerCallback(onTaskComplete: () -> Unit, onTaskFailed: (Int)
|
||||
* @param mapViewModel The [MapViewModel] providing data and state for the map.
|
||||
* @param navigateToNodeDetails Callback to navigate to the details screen of a selected node.
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class) // Added for Accompanist
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
@Composable
|
||||
fun MapView(
|
||||
@@ -246,9 +244,8 @@ fun MapView(
|
||||
val unknownText = stringResource(Res.string.unknown)
|
||||
val nowText = stringResource(Res.string.now)
|
||||
|
||||
// Accompanist permissions state for location
|
||||
val locationPermissionsState =
|
||||
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
// Location permission state (native; recomputed on resume).
|
||||
val locationPermission = rememberLocationPermissionState()
|
||||
var triggerLocationToggleAfterPermission by remember { mutableStateOf(false) }
|
||||
|
||||
fun loadOnlineTileSourceBase(): ITileSource {
|
||||
@@ -309,8 +306,8 @@ fun MapView(
|
||||
}
|
||||
|
||||
// Effect to toggle MyLocation after permission is granted
|
||||
LaunchedEffect(locationPermissionsState.allPermissionsGranted) {
|
||||
if (locationPermissionsState.allPermissionsGranted && triggerLocationToggleAfterPermission) {
|
||||
LaunchedEffect(locationPermission.isGranted) {
|
||||
if (locationPermission.isGranted && triggerLocationToggleAfterPermission) {
|
||||
map.toggleMyLocation()
|
||||
triggerLocationToggleAfterPermission = false
|
||||
}
|
||||
@@ -637,11 +634,15 @@ fun MapView(
|
||||
},
|
||||
isLocationTrackingEnabled = myLocationOverlay != null,
|
||||
onToggleLocationTracking = {
|
||||
if (locationPermissionsState.allPermissionsGranted) {
|
||||
map.toggleMyLocation()
|
||||
} else {
|
||||
triggerLocationToggleAfterPermission = true
|
||||
locationPermissionsState.launchMultiplePermissionRequest()
|
||||
when {
|
||||
locationPermission.isGranted -> map.toggleMyLocation()
|
||||
// Permanently denied: the system won't prompt again, so send the user to settings.
|
||||
locationPermission.status == PermissionStatus.PERMANENTLY_DENIED ->
|
||||
locationPermission.openAppSettings()
|
||||
else -> {
|
||||
triggerLocationToggleAfterPermission = true
|
||||
locationPermission.request()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -57,8 +56,8 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import org.meshtastic.core.ui.util.PermissionStatus
|
||||
import org.meshtastic.core.ui.util.rememberLocationPermissionState
|
||||
import com.google.android.gms.location.LocationCallback
|
||||
import com.google.android.gms.location.LocationRequest
|
||||
import com.google.android.gms.location.LocationResult
|
||||
@@ -177,7 +176,7 @@ private const val TRACEROUTE_OFFSET_METERS = 100.0
|
||||
private const val TRACEROUTE_BOUNDS_PADDING_PX = 120
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
@OptIn(MapsComposeExperimentalApi::class, ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
|
||||
@OptIn(MapsComposeExperimentalApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MapView(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -190,16 +189,15 @@ fun MapView(
|
||||
val mapLayers by mapViewModel.mapLayers.collectAsStateWithLifecycle()
|
||||
|
||||
// --- Location permissions ---
|
||||
val locationPermissionsState =
|
||||
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
val locationPermission = rememberLocationPermissionState()
|
||||
var triggerLocationToggleAfterPermission by remember { mutableStateOf(false) }
|
||||
|
||||
// --- Location tracking ---
|
||||
var isLocationTrackingEnabled by remember { mutableStateOf(false) }
|
||||
var followPhoneBearing by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(locationPermissionsState.allPermissionsGranted) {
|
||||
if (locationPermissionsState.allPermissionsGranted && triggerLocationToggleAfterPermission) {
|
||||
LaunchedEffect(locationPermission.isGranted) {
|
||||
if (locationPermission.isGranted && triggerLocationToggleAfterPermission) {
|
||||
isLocationTrackingEnabled = true
|
||||
triggerLocationToggleAfterPermission = false
|
||||
}
|
||||
@@ -280,8 +278,8 @@ fun MapView(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isLocationTrackingEnabled, locationPermissionsState.allPermissionsGranted) {
|
||||
if (isLocationTrackingEnabled && locationPermissionsState.allPermissionsGranted) {
|
||||
LaunchedEffect(isLocationTrackingEnabled, locationPermission.isGranted) {
|
||||
if (isLocationTrackingEnabled && locationPermission.isGranted) {
|
||||
val locationRequest =
|
||||
LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000L)
|
||||
.setMinUpdateIntervalMillis(2000L)
|
||||
@@ -529,7 +527,7 @@ fun MapView(
|
||||
properties =
|
||||
MapProperties(
|
||||
mapType = effectiveGoogleMapType,
|
||||
isMyLocationEnabled = isLocationTrackingEnabled && locationPermissionsState.allPermissionsGranted,
|
||||
isMyLocationEnabled = isLocationTrackingEnabled && locationPermission.isGranted,
|
||||
),
|
||||
onMapLongClick = { latLng ->
|
||||
if (isMainMode && isConnected) {
|
||||
@@ -695,14 +693,19 @@ fun MapView(
|
||||
},
|
||||
isLocationTrackingEnabled = isLocationTrackingEnabled,
|
||||
onToggleLocationTracking = {
|
||||
if (locationPermissionsState.allPermissionsGranted) {
|
||||
isLocationTrackingEnabled = !isLocationTrackingEnabled
|
||||
if (!isLocationTrackingEnabled) {
|
||||
followPhoneBearing = false
|
||||
when {
|
||||
locationPermission.isGranted -> {
|
||||
isLocationTrackingEnabled = !isLocationTrackingEnabled
|
||||
if (!isLocationTrackingEnabled) {
|
||||
followPhoneBearing = false
|
||||
}
|
||||
}
|
||||
// Permanently denied: the system won't prompt again, so send the user to settings to recover.
|
||||
locationPermission.status == PermissionStatus.PERMANENTLY_DENIED -> locationPermission.openAppSettings()
|
||||
else -> {
|
||||
triggerLocationToggleAfterPermission = true
|
||||
locationPermission.request()
|
||||
}
|
||||
} else {
|
||||
triggerLocationToggleAfterPermission = true
|
||||
locationPermissionsState.launchMultiplePermissionRequest()
|
||||
}
|
||||
},
|
||||
bearing = cameraPositionState.position.bearing,
|
||||
|
||||
@@ -65,7 +65,6 @@ class KmpFeatureConventionPlugin : Plugin<Project> {
|
||||
}
|
||||
|
||||
sourceSets.getByName("androidMain").dependencies {
|
||||
implementation(libs.library("accompanist-permissions"))
|
||||
implementation(libs.library("androidx-activity-compose"))
|
||||
|
||||
implementation(libs.library("compose-multiplatform-ui"))
|
||||
|
||||
@@ -36,7 +36,6 @@ dependencies {
|
||||
implementation(libs.compose.multiplatform.material3)
|
||||
implementation(libs.compose.multiplatform.runtime)
|
||||
implementation(libs.compose.multiplatform.ui)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.kermit)
|
||||
|
||||
// ML Kit is used for the Google flavor, while ZXing is used for F-Droid to avoid GMS dependencies.
|
||||
|
||||
@@ -14,11 +14,8 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:OptIn(ExperimentalPermissionsApi::class)
|
||||
|
||||
package org.meshtastic.core.barcode
|
||||
|
||||
import android.Manifest
|
||||
import androidx.camera.compose.CameraXViewfinder
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
@@ -31,11 +28,14 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
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.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -52,29 +52,42 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.camera_permission_rationale
|
||||
import org.meshtastic.core.resources.close
|
||||
import org.meshtastic.core.ui.component.PermissionRecoveryCard
|
||||
import org.meshtastic.core.ui.icon.Close
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.util.BarcodeScanner
|
||||
import org.meshtastic.core.ui.util.PermissionStatus
|
||||
import org.meshtastic.core.ui.util.rememberCameraPermissionState
|
||||
|
||||
@Composable
|
||||
fun rememberBarcodeScanner(onResult: (String?) -> Unit): BarcodeScanner {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var pendingScan by remember { mutableStateOf(false) }
|
||||
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
|
||||
var showPermissionRecovery by remember { mutableStateOf(false) }
|
||||
val cameraPermission = rememberCameraPermissionState()
|
||||
val currentStatus = rememberUpdatedState(cameraPermission.status)
|
||||
|
||||
LaunchedEffect(cameraPermissionState.status.isGranted) {
|
||||
if (cameraPermissionState.status.isGranted && pendingScan) {
|
||||
showDialog = true
|
||||
pendingScan = false
|
||||
LaunchedEffect(cameraPermission.status) {
|
||||
when {
|
||||
!pendingScan -> Unit
|
||||
cameraPermission.isGranted -> {
|
||||
showDialog = true
|
||||
pendingScan = false
|
||||
}
|
||||
// The request completed without a grant — surface a recovery card instead of failing silently.
|
||||
cameraPermission.status != PermissionStatus.NOT_REQUESTED -> {
|
||||
showPermissionRecovery = true
|
||||
pendingScan = false
|
||||
}
|
||||
}
|
||||
// Dismiss the recovery card once the permission is granted (e.g. user returned from settings).
|
||||
if (cameraPermission.isGranted) showPermissionRecovery = false
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
@@ -90,14 +103,28 @@ fun rememberBarcodeScanner(onResult: (String?) -> Unit): BarcodeScanner {
|
||||
)
|
||||
}
|
||||
|
||||
if (showPermissionRecovery) {
|
||||
Dialog(onDismissRequest = { showPermissionRecovery = false }) {
|
||||
Surface(shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surface) {
|
||||
PermissionRecoveryCard(
|
||||
state = cameraPermission,
|
||||
rationale = stringResource(Res.string.camera_permission_rationale),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return remember {
|
||||
object : BarcodeScanner {
|
||||
override fun startScan() {
|
||||
if (cameraPermissionState.status.isGranted) {
|
||||
showDialog = true
|
||||
} else {
|
||||
pendingScan = true
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
when (currentStatus.value) {
|
||||
PermissionStatus.GRANTED -> showDialog = true
|
||||
PermissionStatus.PERMANENTLY_DENIED -> showPermissionRecovery = true
|
||||
else -> {
|
||||
pendingScan = true
|
||||
cameraPermission.request()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,11 @@ import co.touchlab.kermit.Severity
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.bonding_failed_permissions
|
||||
import org.meshtastic.core.resources.usb_permission_denied
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
@@ -75,7 +79,7 @@ class AndroidScannerViewModel(
|
||||
} catch (ex: SecurityException) {
|
||||
Logger.w(ex) { "Bonding failed for ${entry.device.address.anonymize} Permissions not granted" }
|
||||
serviceRepository.setErrorMessage(
|
||||
text = "Bonding failed: ${ex.message} Permissions not granted",
|
||||
text = getString(Res.string.bonding_failed_permissions),
|
||||
severity = Severity.Warn,
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
@@ -102,6 +106,10 @@ class AndroidScannerViewModel(
|
||||
changeDeviceAddress(entry.fullAddress)
|
||||
} else {
|
||||
Logger.e { "USB permission denied for device ${entry.address}" }
|
||||
serviceRepository.setErrorMessage(
|
||||
text = getString(Res.string.usb_permission_denied),
|
||||
severity = Severity.Warn,
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
@@ -16,40 +16,36 @@
|
||||
*/
|
||||
package org.meshtastic.feature.intro
|
||||
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.MultiplePermissionsState
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import org.meshtastic.core.ui.util.PermissionUiState
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
internal class AndroidIntroPermissions(
|
||||
private val bluetoothState: MultiplePermissionsState,
|
||||
private val locationState: MultiplePermissionsState,
|
||||
private val notificationState: PermissionState?,
|
||||
private val bluetoothState: PermissionUiState,
|
||||
private val locationState: PermissionUiState,
|
||||
private val notificationState: PermissionUiState?,
|
||||
) : IntroPermissions {
|
||||
override val bluetooth: IntroPermissionState =
|
||||
object : IntroPermissionState {
|
||||
override val isGranted: Boolean
|
||||
get() = bluetoothState.allPermissionsGranted
|
||||
get() = bluetoothState.isGranted
|
||||
|
||||
override fun launchRequest() = bluetoothState.launchMultiplePermissionRequest()
|
||||
override fun launchRequest() = bluetoothState.request()
|
||||
}
|
||||
|
||||
override val location: IntroPermissionState =
|
||||
object : IntroPermissionState {
|
||||
override val isGranted: Boolean
|
||||
get() = locationState.allPermissionsGranted
|
||||
get() = locationState.isGranted
|
||||
|
||||
override fun launchRequest() = locationState.launchMultiplePermissionRequest()
|
||||
override fun launchRequest() = locationState.request()
|
||||
}
|
||||
|
||||
override val notification: IntroPermissionState? =
|
||||
notificationState?.let { state ->
|
||||
object : IntroPermissionState {
|
||||
override val isGranted: Boolean
|
||||
get() = state.status.isGranted
|
||||
get() = state.isGranted
|
||||
|
||||
override fun launchRequest() = state.launchPermissionRequest()
|
||||
override fun launchRequest() = state.request()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.feature.intro
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -24,11 +23,10 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import org.meshtastic.core.ui.component.MeshtasticNavDisplay
|
||||
import org.meshtastic.core.ui.util.rememberBluetoothPermissionState
|
||||
import org.meshtastic.core.ui.util.rememberLocationPermissionState
|
||||
import org.meshtastic.core.ui.util.rememberNotificationPermissionState
|
||||
|
||||
/**
|
||||
* Main application introduction screen. This Composable hosts the navigation flow and hoists the permission states.
|
||||
@@ -36,29 +34,18 @@ import org.meshtastic.core.ui.component.MeshtasticNavDisplay
|
||||
* @param onDone Callback invoked when the introduction flow is completed.
|
||||
* @param viewModel ViewModel for tracking the introduction flow state.
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun AppIntroductionScreen(onDone: () -> Unit, viewModel: IntroViewModel) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val notificationPermissionState: PermissionState? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
// Pre-Android 13 has no runtime notification permission, so there is nothing to configure — keep it null so the
|
||||
// intro flow can skip the notification screen entirely. SDK_INT is constant per process, so the conditional call
|
||||
// is recomposition-safe.
|
||||
val notificationPermissionState =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) rememberNotificationPermissionState() else null
|
||||
|
||||
val locationPermissions =
|
||||
listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
val locationPermissionState = rememberMultiplePermissionsState(permissions = locationPermissions)
|
||||
|
||||
val bluetoothPermissions =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
listOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val bluetoothPermissionState = rememberMultiplePermissionsState(permissions = bluetoothPermissions)
|
||||
val locationPermissionState = rememberLocationPermissionState()
|
||||
val bluetoothPermissionState = rememberBluetoothPermissionState()
|
||||
|
||||
val permissions =
|
||||
remember(notificationPermissionState, locationPermissionState, bluetoothPermissionState) {
|
||||
|
||||
@@ -4,7 +4,6 @@ xmlutil = "0.91.3"
|
||||
# Android
|
||||
agp = "9.2.1"
|
||||
appcompat = "1.7.1"
|
||||
accompanist = "0.37.3"
|
||||
car-app = "1.9.0-alpha01"
|
||||
appfunctions = "1.0.0-alpha09"
|
||||
|
||||
@@ -245,7 +244,6 @@ turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
|
||||
# Other
|
||||
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" }
|
||||
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||
coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||
coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
|
||||
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
|
||||
|
||||
Reference in New Issue
Block a user