This commit is contained in:
Ax333l 2024-08-17 21:25:13 +02:00
parent e992a99783
commit 0930b9fda7
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
13 changed files with 227 additions and 91 deletions

View File

@ -1,9 +1,14 @@
package app.revanced.manager
import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.util.Log
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
import coil.Coil
import coil.ImageLoader
@ -23,6 +28,8 @@ class ManagerApplication : Application() {
private val scope = MainScope()
private val prefs: PreferencesManager by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
private val fs: Filesystem by inject()
override fun onCreate() {
super.onCreate()
@ -65,5 +72,34 @@ class ManagerApplication : Application() {
updateCheck()
}
}
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
private var firstActivityCreated = false
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (firstActivityCreated) return
firstActivityCreated = true
// We do not want to call onFreshProcessStart() if there is state to restore.
// This can happen on system-initiated process death.
if (savedInstanceState == null) {
Log.d(tag, "Fresh process created")
onFreshProcessStart()
} else Log.d(tag, "System-initiated process death detected")
}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
})
}
private fun onFreshProcessStart() {
fs.uiTempDir.apply {
deleteRecursively()
mkdirs()
}
}
}

View File

@ -9,6 +9,8 @@ import android.os.Environment
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import app.revanced.manager.util.RequestManageStorageContract
import java.io.File
import java.nio.file.Path
class Filesystem(private val app: Application) {
val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here.
@ -17,21 +19,35 @@ class Filesystem(private val app: Application) {
* A directory that gets cleared when the app restarts.
* Do not store paths to this directory in a parcel.
*/
val tempDir = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
deleteRecursively()
mkdirs()
}
fun externalFilesDir() = Environment.getExternalStorageDirectory().toPath()
/**
* A directory for storing temporary files related to UI.
* This is the same as [tempDir], but does not get cleared on system-initiated process death.
* Paths to this directory can be safely stored in parcels.
*/
val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE).apply {
mkdirs()
}
fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath()
private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
private val storagePermissionName = if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
private val storagePermissionName =
if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
fun permissionContract(): Pair<ActivityResultContract<String, Boolean>, String> {
val contract = if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
val contract =
if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
return contract to storagePermissionName
}
fun hasStoragePermission() = if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(storagePermissionName) == PackageManager.PERMISSION_GRANTED
fun hasStoragePermission() =
if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(
storagePermissionName
) == PackageManager.PERMISSION_GRANTED
}

View File

@ -26,7 +26,7 @@ class Session(
private val androidContext: Context,
private val logger: Logger,
private val input: File,
private val onPatchCompleted: () -> Unit,
private val onPatchCompleted: suspend () -> Unit,
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
) : Closeable {
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =

View File

@ -20,7 +20,7 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection,
options: Options,
logger: Logger,
onPatchCompleted: () -> Unit,
onPatchCompleted: suspend () -> Unit,
onProgress: ProgressEventHandler,
) {
val bundles = bundles()

View File

@ -66,7 +66,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection,
options: Options,
logger: Logger,
onPatchCompleted: () -> Unit,
onPatchCompleted: suspend () -> Unit,
onProgress: ProgressEventHandler,
) = coroutineScope {
// Get the location of our own Apk.
@ -123,7 +123,9 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
val eventHandler = object : IPatcherEvents.Stub() {
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
override fun patchSucceeded() = onPatchCompleted()
override fun patchSucceeded() {
launch { onPatchCompleted() }
}
override fun progress(name: String?, state: String?, msg: String?) =
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)

View File

@ -35,7 +35,7 @@ sealed class Runtime(context: Context) : KoinComponent {
selectedPatches: PatchSelection,
options: Options,
logger: Logger,
onPatchCompleted: () -> Unit,
onPatchCompleted: suspend () -> Unit,
onProgress: ProgressEventHandler,
)
}

View File

@ -61,9 +61,9 @@ class PatcherWorker(
val selectedPatches: PatchSelection,
val options: Options,
val logger: Logger,
val downloadProgress: MutableStateFlow<Pair<Float, Float>?>,
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
val setInputFile: (File) -> Unit,
val onDownloadProgress: suspend (Pair<Float, Float>?) -> Unit,
val onPatchCompleted: suspend () -> Unit,
val setInputFile: suspend (File) -> Unit,
val onProgress: ProgressEventHandler
) {
val packageName get() = input.packageName
@ -146,7 +146,7 @@ class PatcherWorker(
downloadedAppRepository.download(
selectedApp.app,
prefs.preferSplits.get(),
onDownload = { args.downloadProgress.emit(it) }
onDownload = args.onDownloadProgress
).also {
args.setInputFile(it)
updateProgress(state = State.COMPLETED) // Download APK
@ -170,11 +170,13 @@ class PatcherWorker(
args.selectedPatches,
args.options,
args.logger,
/*
onPatchCompleted = {
args.patchesProgress.update { (completed, total) ->
completed + 1 to total
}
},
},*/
args.onPatchCompleted,
args.onProgress
)

View File

@ -36,13 +36,14 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.ui.component.ArrowButton
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.model.ProgressKey
import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider
import kotlin.math.floor
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
@ -51,6 +52,7 @@ fun Steps(
category: StepCategory,
steps: List<Step>,
stepCount: Pair<Int, Int>? = null,
stepProgressProvider: StepProgressProvider
) {
var expanded by rememberSaveable { mutableStateOf(true) }
@ -115,13 +117,17 @@ fun Steps(
modifier = Modifier.fillMaxWidth()
) {
steps.forEach { step ->
val downloadProgress = step.downloadProgress?.collectAsStateWithLifecycle()
val (progress, progressText) = when (step.progressKey) {
null -> null
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) -> downloaded / total to "$downloaded/$total MB" }
} ?: (null to null)
SubStep(
name = step.name,
state = step.state,
message = step.message,
downloadProgress = downloadProgress?.value
progress = progress,
progressText = progressText
)
}
}
@ -134,7 +140,8 @@ fun SubStep(
name: String,
state: State,
message: String? = null,
downloadProgress: Pair<Float, Float>? = null
progress: Float? = null,
progressText: String? = null
) {
var messageExpanded by rememberSaveable { mutableStateOf(true) }
@ -155,7 +162,7 @@ fun SubStep(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center
) {
StepIcon(state, downloadProgress, size = 20.dp)
StepIcon(state, progress, size = 20.dp)
}
Text(
@ -166,8 +173,8 @@ fun SubStep(
modifier = Modifier.weight(1f, true),
)
if (message != null) {
Box(
when {
message != null -> Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center
) {
@ -177,13 +184,11 @@ fun SubStep(
onClick = null
)
}
} else {
downloadProgress?.let { (current, total) ->
Text(
"$current/$total MB",
style = MaterialTheme.typography.labelSmall
)
}
progressText != null -> Text(
progressText,
style = MaterialTheme.typography.labelSmall
)
}
}
@ -199,7 +204,7 @@ fun SubStep(
}
@Composable
fun StepIcon(state: State, progress: Pair<Float, Float>? = null, size: Dp) {
fun StepIcon(state: State, progress: Float? = null, size: Dp) {
val strokeWidth = Dp(floor(size.value / 10) + 1)
when (state) {
@ -233,7 +238,7 @@ fun StepIcon(state: State, progress: Pair<Float, Float>? = null, size: Dp) {
contentDescription = description
}
},
progress = { progress?.let { (current, total) -> current / total } },
progress = { progress },
strokeWidth = strokeWidth
)
}

View File

@ -1,8 +1,10 @@
package app.revanced.manager.ui.model
import android.os.Parcelable
import androidx.annotation.StringRes
import app.revanced.manager.R
import kotlinx.coroutines.flow.StateFlow
import kotlinx.parcelize.Parcelize
enum class StepCategory(@StringRes val displayName: Int) {
PREPARING(R.string.patcher_step_group_preparing),
@ -14,10 +16,19 @@ enum class State {
WAITING, RUNNING, FAILED, COMPLETED
}
enum class ProgressKey {
DOWNLOAD
}
interface StepProgressProvider {
val downloadProgress: Pair<Float, Float>?
}
@Parcelize
data class Step(
val name: String,
val category: StepCategory,
val state: State = State.WAITING,
val message: String? = null,
val downloadProgress: StateFlow<Pair<Float, Float>?>? = null
)
val progressKey: ProgressKey? = null
) : Parcelable

View File

@ -36,13 +36,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
import app.revanced.manager.ui.component.patcher.Steps
import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.viewmodel.PatcherViewModel
import app.revanced.manager.util.APK_MIMETYPE
@ -69,22 +67,6 @@ fun PatcherScreen(
}
}
val patchesProgress by vm.patchesProgress.collectAsStateWithLifecycle()
val progress by remember {
derivedStateOf {
val (patchesCompleted, patchesTotal) = patchesProgress
val current = vm.steps.count {
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
} + patchesCompleted
val total = vm.steps.size - 1 + patchesTotal
current.toFloat() / total.toFloat()
}
}
if (showInstallPicker)
InstallPickerDialog(
onDismiss = { showInstallPicker = false },
@ -150,7 +132,7 @@ fun PatcherScreen(
.fillMaxSize()
) {
LinearProgressIndicator(
progress = { progress },
progress = { vm.progress },
modifier = Modifier.fillMaxWidth()
)
@ -166,7 +148,8 @@ fun PatcherScreen(
Steps(
category = category,
steps = steps,
stepCount = if (category == StepCategory.PATCHING) patchesProgress else null
stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null,
stepProgressProvider = vm
)
}
}

View File

@ -9,6 +9,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.PM
@ -23,11 +24,10 @@ import java.nio.file.Files
class AppSelectorViewModel(
private val app: Application,
private val pm: PM,
fs: Filesystem,
private val patchBundleRepository: PatchBundleRepository
) : ViewModel() {
private val inputFile = File(app.filesDir, "input.apk").also {
it.delete()
}
private val inputFile = File(fs.uiTempDir, "input.apk").also(File::delete)
val appList = pm.appList
var onStorageClick: (SelectedApp.Local) -> Unit = {}

View File

@ -9,14 +9,19 @@ import android.content.pm.PackageInstaller
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.core.content.ContextCompat
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable
import androidx.work.WorkInfo
import androidx.work.WorkManager
import app.revanced.manager.R
@ -31,11 +36,15 @@ import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.service.InstallService
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.model.ProgressKey
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider
import app.revanced.manager.util.PM
import app.revanced.manager.util.saveableVar
import app.revanced.manager.util.saver.snapshotStateListSaver
import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
@ -43,8 +52,6 @@ import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout
import kotlinx.coroutines.withContext
@ -55,32 +62,47 @@ import java.nio.file.Files
import java.time.Duration
import java.util.UUID
// @SuppressLint("AutoboxingStateCreation")
@Stable
@OptIn(SavedStateHandleSaveableApi::class)
class PatcherViewModel(
private val input: Destination.Patcher
) : ViewModel(), KoinComponent {
) : ViewModel(), KoinComponent, StepProgressProvider {
private val app: Application by inject()
private val fs: Filesystem by inject()
private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject()
private val savedStateHandle: SavedStateHandle by inject()
private var installedApp: InstalledApp? = null
val packageName: String = input.selectedApp.packageName
var installedPackageName by mutableStateOf<String?>(null)
val packageName = input.selectedApp.packageName
var installedPackageName by savedStateHandle.saveable(
key = "installedPackageName",
// Force Kotlin to select the correct overload.
stateSaver = autoSaver()
) {
mutableStateOf<String?>(null)
}
private set
var isInstalling by mutableStateOf(false)
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
var isInstalling by mutableStateOf(ongoingPmSession)
private set
private val tempDir = fs.tempDir.resolve("installer").also {
it.deleteRecursively()
it.mkdirs()
private val tempDir = savedStateHandle.saveable(key = "tempDir") {
fs.uiTempDir.resolve("installer").also {
it.deleteRecursively()
it.mkdirs()
}
}
private var inputFile: File? = null
private var inputFile: File? by savedStateHandle.saveableVar()
private val outputFile = tempDir.resolve("output.apk")
private val logs = mutableListOf<Pair<LogLevel, String>>()
private val logs by savedStateHandle.saveable<MutableList<Pair<LogLevel, String>>> { mutableListOf() }
private val logger = object : Logger() {
override fun log(level: LogLevel, message: String) {
level.androidLog(message)
@ -92,18 +114,43 @@ class PatcherViewModel(
}
}
val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size }))
private val downloadProgress = MutableStateFlow<Pair<Float, Float>?>(null)
val steps = generateSteps(
app,
input.selectedApp,
downloadProgress
).toMutableStateList()
private val patchCount = input.selectedPatches.values.sumOf { it.size }
private var completedPatchCount by savedStateHandle.saveable {
// SavedStateHandle.saveable only supports the boxed version.
@Suppress("AutoboxingStateCreation") mutableStateOf(
0
)
}
val patchesProgress get() = completedPatchCount to patchCount
override var downloadProgress by savedStateHandle.saveable(
key = "downloadProgress",
stateSaver = autoSaver()
) {
viewModelScope
mutableStateOf<Pair<Float, Float>?>(null)
}
private set
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
generateSteps(
app,
input.selectedApp
).toMutableStateList()
}
private var currentStepIndex = 0
val progress by derivedStateOf {
val current = steps.count {
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
} + completedPatchCount
val total = steps.size - 1 + patchCount
current.toFloat() / total.toFloat()
}
private val workManager = WorkManager.getInstance(app)
private val patcherWorkerId: UUID =
private val patcherWorkerId by savedStateHandle.saveable<UUID> {
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
"patching", PatcherWorker.Args(
input.selectedApp,
@ -111,9 +158,9 @@ class PatcherViewModel(
input.selectedPatches,
input.options,
logger,
downloadProgress,
patchesProgress,
setInputFile = { inputFile = it },
onDownloadProgress = { withContext(Dispatchers.Main) { downloadProgress = it } },
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
onProgress = { name, state, message ->
viewModelScope.launch {
steps[currentStepIndex] = steps[currentStepIndex].run {
@ -134,6 +181,7 @@ class PatcherViewModel(
}
)
)
}
val patcherSucceeded =
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->
@ -172,7 +220,8 @@ class PatcherViewModel(
}
}
init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it.
init {
// TODO: detect system-initiated process death during the patching process.
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
addAction(InstallService.APP_INSTALL_ACTION)
}, ContextCompat.RECEIVER_NOT_EXPORTED)
@ -278,8 +327,8 @@ class PatcherViewModel(
}
}
companion object {
private const val TAG = "ReVanced Patcher"
private companion object {
const val TAG = "ReVanced Patcher"
fun LogLevel.androidLog(msg: String) = when (this) {
LogLevel.TRACE -> Log.v(TAG, msg)
@ -288,11 +337,7 @@ class PatcherViewModel(
LogLevel.ERROR -> Log.e(TAG, msg)
}
fun generateSteps(
context: Context,
selectedApp: SelectedApp,
downloadProgress: StateFlow<Pair<Float, Float>?>? = null
): List<Step> {
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
val needsDownload = selectedApp is SelectedApp.Download
return listOfNotNull(
@ -300,7 +345,7 @@ class PatcherViewModel(
context.getString(R.string.download_apk),
StepCategory.PREPARING,
state = State.RUNNING,
downloadProgress = downloadProgress,
progressKey = ProgressKey.DOWNLOAD,
).takeIf { needsDownload },
Step(
context.getString(R.string.patcher_step_load_patches),

View File

@ -10,6 +10,7 @@ import android.icu.text.CompactDecimalFormat
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.MainThread
import androidx.annotation.StringRes
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
@ -24,6 +25,7 @@ import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import app.revanced.manager.R
@ -42,6 +44,9 @@ import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.Locale
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
typealias PatchSelection = Map<Int, Set<String>>
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
@ -156,9 +161,21 @@ fun String.relativeTime(context: Context): String {
return when {
duration.toMinutes() < 1 -> context.getString(R.string.just_now)
duration.toMinutes() < 60 -> context.getString(R.string.minutes_ago, duration.toMinutes().toString())
duration.toHours() < 24 -> context.getString(R.string.hours_ago, duration.toHours().toString())
duration.toDays() < 30 -> context.getString(R.string.days_ago, duration.toDays().toString())
duration.toMinutes() < 60 -> context.getString(
R.string.minutes_ago,
duration.toMinutes().toString()
)
duration.toHours() < 24 -> context.getString(
R.string.hours_ago,
duration.toHours().toString()
)
duration.toDays() < 30 -> context.getString(
R.string.days_ago,
duration.toDays().toString()
)
else -> {
val formatter = DateTimeFormatter.ofPattern("MMM d")
val formattedDate = inputDateTime.format(formatter)
@ -218,4 +235,23 @@ fun ScrollState.isScrollingUp(): State<Boolean> {
}
val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
@MainThread
fun <T : Any> SavedStateHandle.saveableVar(init: () -> T): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> =
PropertyDelegateProvider { _: Any?, property ->
val name = property.name
if (name !in this) this[name] = init()
object : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = get(name)!!
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) =
set(name, value)
}
}
fun <T : Any> SavedStateHandle.saveableVar(): ReadWriteProperty<Any?, T?> =
object : ReadWriteProperty<Any?, T?> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T? = get(property.name)
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) =
set(property.name, value)
}