feature(AppPicker): Add installation time sorting option
This commit is contained in:
parent
9d0f163181
commit
d17bff1749
@ -18,6 +18,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -35,6 +36,7 @@ import com.artemchep.keyguard.feature.home.vault.component.SearchTextField
|
|||||||
import com.artemchep.keyguard.feature.home.vault.component.surfaceColorAtElevation
|
import com.artemchep.keyguard.feature.home.vault.component.surfaceColorAtElevation
|
||||||
import com.artemchep.keyguard.feature.navigation.NavigationIcon
|
import com.artemchep.keyguard.feature.navigation.NavigationIcon
|
||||||
import com.artemchep.keyguard.feature.navigation.RouteResultTransmitter
|
import com.artemchep.keyguard.feature.navigation.RouteResultTransmitter
|
||||||
|
import com.artemchep.keyguard.feature.search.sort.SortButton
|
||||||
import com.artemchep.keyguard.res.Res
|
import com.artemchep.keyguard.res.Res
|
||||||
import com.artemchep.keyguard.res.*
|
import com.artemchep.keyguard.res.*
|
||||||
import com.artemchep.keyguard.ui.DefaultProgressBar
|
import com.artemchep.keyguard.ui.DefaultProgressBar
|
||||||
@ -88,6 +90,13 @@ fun ChangePasswordScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(listRevision) {
|
LaunchedEffect(listRevision) {
|
||||||
|
// Scroll to the start of the list if the list has
|
||||||
|
// no real content.
|
||||||
|
if (listRevision == null) {
|
||||||
|
listState.scrollToItem(0, 0)
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: How do you wait till the layout state start to represent
|
// TODO: How do you wait till the layout state start to represent
|
||||||
// the actual data?
|
// the actual data?
|
||||||
val listSize =
|
val listSize =
|
||||||
@ -136,6 +145,14 @@ fun ChangePasswordScreen(
|
|||||||
icon = {
|
icon = {
|
||||||
NavigationIcon()
|
NavigationIcon()
|
||||||
},
|
},
|
||||||
|
actions = {
|
||||||
|
val content = loadableState.getOrNull()
|
||||||
|
?: return@CustomToolbarContent
|
||||||
|
val sort by content.sort.collectAsState()
|
||||||
|
AppPickerSortButton(
|
||||||
|
state = sort,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
val query = filterState.value?.query
|
val query = filterState.value?.query
|
||||||
@ -227,6 +244,20 @@ fun ChangePasswordScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppPickerSortButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
state: AppPickerState.Sort,
|
||||||
|
) {
|
||||||
|
val filters = state.sort
|
||||||
|
val clearFilters = state.clearSort
|
||||||
|
SortButton(
|
||||||
|
modifier = modifier,
|
||||||
|
items = filters,
|
||||||
|
onClear = clearFilters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NoItemsPlaceholder(
|
private fun NoItemsPlaceholder(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -5,12 +5,16 @@ import androidx.compose.ui.text.AnnotatedString
|
|||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.optics.optics
|
import arrow.optics.optics
|
||||||
import com.artemchep.keyguard.common.model.Loadable
|
import com.artemchep.keyguard.common.model.Loadable
|
||||||
|
import com.artemchep.keyguard.feature.apppicker.model.AppPickerSortItem
|
||||||
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
||||||
import com.artemchep.keyguard.feature.favicon.AppIconUrl
|
import com.artemchep.keyguard.feature.favicon.AppIconUrl
|
||||||
|
import com.artemchep.keyguard.feature.home.vault.model.SortItem
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
data class AppPickerState(
|
data class AppPickerState(
|
||||||
val filter: StateFlow<Filter>,
|
val filter: StateFlow<Filter>,
|
||||||
|
val sort: StateFlow<Sort>,
|
||||||
val content: Loadable<Either<Throwable, Content>>,
|
val content: Loadable<Either<Throwable, Content>>,
|
||||||
) {
|
) {
|
||||||
@Immutable
|
@Immutable
|
||||||
@ -21,6 +25,14 @@ data class AppPickerState(
|
|||||||
companion object
|
companion object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class Sort(
|
||||||
|
val sort: ImmutableList<AppPickerSortItem>,
|
||||||
|
val clearSort: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@optics
|
@optics
|
||||||
data class Content(
|
data class Content(
|
||||||
|
@ -4,15 +4,22 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.CalendarMonth
|
||||||
|
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import arrow.core.partially1
|
import arrow.core.partially1
|
||||||
import com.artemchep.keyguard.android.util.broadcastFlow
|
import com.artemchep.keyguard.android.util.broadcastFlow
|
||||||
import com.artemchep.keyguard.common.model.Loadable
|
import com.artemchep.keyguard.common.model.Loadable
|
||||||
import com.artemchep.keyguard.common.usecase.UnlockUseCase
|
import com.artemchep.keyguard.common.usecase.UnlockUseCase
|
||||||
|
import com.artemchep.keyguard.feature.apppicker.model.AppPickerSortItem
|
||||||
import com.artemchep.keyguard.feature.crashlytics.crashlyticsAttempt
|
import com.artemchep.keyguard.feature.crashlytics.crashlyticsAttempt
|
||||||
import com.artemchep.keyguard.feature.favicon.AppIconUrl
|
import com.artemchep.keyguard.feature.favicon.AppIconUrl
|
||||||
import com.artemchep.keyguard.feature.home.vault.search.IndexedText
|
import com.artemchep.keyguard.feature.home.vault.search.IndexedText
|
||||||
|
import com.artemchep.keyguard.feature.localization.TextHolder
|
||||||
import com.artemchep.keyguard.feature.navigation.RouteResultTransmitter
|
import com.artemchep.keyguard.feature.navigation.RouteResultTransmitter
|
||||||
import com.artemchep.keyguard.feature.navigation.state.navigatePopSelf
|
import com.artemchep.keyguard.feature.navigation.state.navigatePopSelf
|
||||||
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||||
@ -20,14 +27,95 @@ import com.artemchep.keyguard.feature.search.search.IndexedModel
|
|||||||
import com.artemchep.keyguard.feature.search.search.mapSearch
|
import com.artemchep.keyguard.feature.search.search.mapSearch
|
||||||
import com.artemchep.keyguard.feature.search.search.searchFilter
|
import com.artemchep.keyguard.feature.search.search.searchFilter
|
||||||
import com.artemchep.keyguard.feature.search.search.searchQueryHandle
|
import com.artemchep.keyguard.feature.search.search.searchQueryHandle
|
||||||
|
import com.artemchep.keyguard.platform.parcelize.LeIgnoredOnParcel
|
||||||
|
import com.artemchep.keyguard.platform.parcelize.LeParcelable
|
||||||
|
import com.artemchep.keyguard.platform.parcelize.LeParcelize
|
||||||
|
import com.artemchep.keyguard.res.Res
|
||||||
|
import com.artemchep.keyguard.res.*
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import org.jetbrains.compose.resources.StringResource
|
||||||
import org.kodein.di.compose.localDI
|
import org.kodein.di.compose.localDI
|
||||||
import org.kodein.di.direct
|
import org.kodein.di.direct
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
|
||||||
|
@LeParcelize
|
||||||
|
@Serializable
|
||||||
|
data class AppPickerComparatorHolder(
|
||||||
|
val comparator: AppPickerSort,
|
||||||
|
val reversed: Boolean = false,
|
||||||
|
) : LeParcelable {
|
||||||
|
companion object {
|
||||||
|
fun of(map: Map<String, Any?>): AppPickerComparatorHolder {
|
||||||
|
return AppPickerComparatorHolder(
|
||||||
|
comparator = AppPickerSort.valueOf(map["comparator"].toString())
|
||||||
|
?: AppPickerAlphabeticalSort,
|
||||||
|
reversed = map["reversed"].toString() == "true",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toMap() = mapOf(
|
||||||
|
"comparator" to comparator.id,
|
||||||
|
"reversed" to reversed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppPickerSort : Comparator<AppInfo>, Parcelable {
|
||||||
|
val id: String
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun valueOf(
|
||||||
|
name: String,
|
||||||
|
): AppPickerSort? = when (name) {
|
||||||
|
AppPickerAlphabeticalSort.id -> AppPickerAlphabeticalSort
|
||||||
|
AppPickerInstallTimeSort.id -> AppPickerInstallTimeSort
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LeParcelize
|
||||||
|
@Serializable
|
||||||
|
data object AppPickerAlphabeticalSort : AppPickerSort {
|
||||||
|
@LeIgnoredOnParcel
|
||||||
|
@Transient
|
||||||
|
override val id: String = "alphabetical"
|
||||||
|
|
||||||
|
override fun compare(
|
||||||
|
a: AppInfo,
|
||||||
|
b: AppInfo,
|
||||||
|
): Int = kotlin.run {
|
||||||
|
val aTitle = a.label
|
||||||
|
val bTitle = b.label
|
||||||
|
aTitle.compareTo(bTitle, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LeParcelize
|
||||||
|
@Serializable
|
||||||
|
data object AppPickerInstallTimeSort : AppPickerSort {
|
||||||
|
@LeIgnoredOnParcel
|
||||||
|
@Transient
|
||||||
|
override val id: String = "install_time"
|
||||||
|
|
||||||
|
override fun compare(
|
||||||
|
a: AppInfo,
|
||||||
|
b: AppInfo,
|
||||||
|
): Int = kotlin.run {
|
||||||
|
val aInstallTime = a.installTime
|
||||||
|
val bInstallTime = b.installTime
|
||||||
|
-aInstallTime.compareTo(bInstallTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class AppPickerUiException(
|
private class AppPickerUiException(
|
||||||
msg: String,
|
msg: String,
|
||||||
cause: Throwable,
|
cause: Throwable,
|
||||||
@ -54,7 +142,26 @@ fun produceAppPickerState(
|
|||||||
unlockUseCase,
|
unlockUseCase,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val queryHandle = searchQueryHandle("query")
|
val sortDefault = AppPickerComparatorHolder(
|
||||||
|
comparator = AppPickerAlphabeticalSort,
|
||||||
|
)
|
||||||
|
val sortSink = mutablePersistedFlow(
|
||||||
|
key = "sort",
|
||||||
|
serialize = { _, value ->
|
||||||
|
value.toMap()
|
||||||
|
},
|
||||||
|
deserialize = { _, value ->
|
||||||
|
AppPickerComparatorHolder.of(value)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
sortDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
val queryHandle = searchQueryHandle(
|
||||||
|
key = "query",
|
||||||
|
revisionFlow = sortSink
|
||||||
|
.map { it.hashCode() },
|
||||||
|
)
|
||||||
val queryFlow = searchFilter(queryHandle) { model, revision ->
|
val queryFlow = searchFilter(queryHandle) { model, revision ->
|
||||||
AppPickerState.Filter(
|
AppPickerState.Filter(
|
||||||
revision = revision,
|
revision = revision,
|
||||||
@ -62,10 +169,121 @@ fun produceAppPickerState(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val appsComparator = Comparator { a: AppInfo, b: AppInfo ->
|
fun onClearSort() {
|
||||||
a.label.compareTo(b.label, ignoreCase = true)
|
sortSink.value = sortDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createComparatorAction(
|
||||||
|
id: String,
|
||||||
|
title: StringResource,
|
||||||
|
icon: ImageVector? = null,
|
||||||
|
config: AppPickerComparatorHolder,
|
||||||
|
) = AppPickerSortItem.Item(
|
||||||
|
id = id,
|
||||||
|
config = config,
|
||||||
|
title = TextHolder.Res(title),
|
||||||
|
icon = icon,
|
||||||
|
onClick = {
|
||||||
|
sortSink.value = config
|
||||||
|
},
|
||||||
|
checked = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AppPickerComparatorSortGroup(
|
||||||
|
val item: AppPickerSortItem.Item,
|
||||||
|
val subItems: List<AppPickerSortItem.Item>,
|
||||||
|
)
|
||||||
|
|
||||||
|
val cam = mapOf(
|
||||||
|
AppPickerAlphabeticalSort to AppPickerComparatorSortGroup(
|
||||||
|
item = createComparatorAction(
|
||||||
|
id = "title",
|
||||||
|
icon = Icons.Outlined.SortByAlpha,
|
||||||
|
title = Res.string.sortby_title_title,
|
||||||
|
config = AppPickerComparatorHolder(
|
||||||
|
comparator = AppPickerAlphabeticalSort,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subItems = listOf(
|
||||||
|
createComparatorAction(
|
||||||
|
id = "title_normal",
|
||||||
|
title = Res.string.sortby_title_normal_mode,
|
||||||
|
config = AppPickerComparatorHolder(
|
||||||
|
comparator = AppPickerAlphabeticalSort,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
createComparatorAction(
|
||||||
|
id = "title_rev",
|
||||||
|
title = Res.string.sortby_title_reverse_mode,
|
||||||
|
config = AppPickerComparatorHolder(
|
||||||
|
comparator = AppPickerAlphabeticalSort,
|
||||||
|
reversed = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AppPickerInstallTimeSort to AppPickerComparatorSortGroup(
|
||||||
|
item = createComparatorAction(
|
||||||
|
id = "modify_date",
|
||||||
|
icon = Icons.Outlined.CalendarMonth,
|
||||||
|
title = Res.string.sortby_installation_date_title,
|
||||||
|
config = AppPickerComparatorHolder(
|
||||||
|
comparator = AppPickerInstallTimeSort,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subItems = listOf(
|
||||||
|
createComparatorAction(
|
||||||
|
id = "modify_date_normal",
|
||||||
|
title = Res.string.sortby_installation_date_normal_mode,
|
||||||
|
config = AppPickerComparatorHolder(
|
||||||
|
comparator = AppPickerInstallTimeSort,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
createComparatorAction(
|
||||||
|
id = "modify_date_rev",
|
||||||
|
title = Res.string.sortby_installation_date_reverse_mode,
|
||||||
|
config = AppPickerComparatorHolder(
|
||||||
|
comparator = AppPickerInstallTimeSort,
|
||||||
|
reversed = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val sortFlow = sortSink
|
||||||
|
.map { orderConfig ->
|
||||||
|
val mainItems = cam.values
|
||||||
|
.map { it.item }
|
||||||
|
.map { item ->
|
||||||
|
val checked = item.config.comparator == orderConfig.comparator
|
||||||
|
item.copy(checked = checked)
|
||||||
|
}
|
||||||
|
val subItems = cam[orderConfig.comparator]?.subItems.orEmpty()
|
||||||
|
.map { item ->
|
||||||
|
val checked = item.config == orderConfig
|
||||||
|
item.copy(checked = checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
val out = mutableListOf<AppPickerSortItem>()
|
||||||
|
out += mainItems
|
||||||
|
if (subItems.isNotEmpty()) {
|
||||||
|
out += AppPickerSortItem.Section(
|
||||||
|
id = "sub_items_section",
|
||||||
|
text = TextHolder.Res(Res.string.options),
|
||||||
|
)
|
||||||
|
out += subItems
|
||||||
|
}
|
||||||
|
|
||||||
|
AppPickerState.Sort(
|
||||||
|
sort = out.toPersistentList(),
|
||||||
|
clearSort = if (orderConfig != sortDefault) {
|
||||||
|
::onClearSort
|
||||||
|
} else null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.stateIn(screenScope)
|
||||||
|
|
||||||
fun onClick(appInfo: AppInfo) {
|
fun onClick(appInfo: AppInfo) {
|
||||||
val uri = "androidapp://${appInfo.packageName}"
|
val uri = "androidapp://${appInfo.packageName}"
|
||||||
val result = AppPickerResult.Confirm(uri)
|
val result = AppPickerResult.Confirm(uri)
|
||||||
@ -76,7 +294,6 @@ fun produceAppPickerState(
|
|||||||
fun List<AppInfo>.toItems(): List<AppPickerState.Item> {
|
fun List<AppInfo>.toItems(): List<AppPickerState.Item> {
|
||||||
val packageNameCollisions = mutableMapOf<String, Int>()
|
val packageNameCollisions = mutableMapOf<String, Int>()
|
||||||
return this
|
return this
|
||||||
.sortedWith(appsComparator)
|
|
||||||
.map { appInfo ->
|
.map { appInfo ->
|
||||||
val key = kotlin.run {
|
val key = kotlin.run {
|
||||||
val newPackageNameCollisionCounter = packageNameCollisions
|
val newPackageNameCollisionCounter = packageNameCollisions
|
||||||
@ -100,8 +317,13 @@ fun produceAppPickerState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val itemsFlow = getAppsFlow(context.context)
|
val itemsFlow = getAppsFlow(context.context)
|
||||||
.map { apps ->
|
.combine(sortSink) { items, sort ->
|
||||||
apps
|
val comparator = Comparator<AppInfo> { a, b ->
|
||||||
|
val result = sort.comparator.compare(a, b)
|
||||||
|
if (sort.reversed) -result else result
|
||||||
|
}
|
||||||
|
val sortedItems = items
|
||||||
|
.sortedWith(comparator)
|
||||||
.toItems()
|
.toItems()
|
||||||
// Index for the search.
|
// Index for the search.
|
||||||
.map { item ->
|
.map { item ->
|
||||||
@ -110,6 +332,7 @@ fun produceAppPickerState(
|
|||||||
indexedText = IndexedText.invoke(item.name.text),
|
indexedText = IndexedText.invoke(item.name.text),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
sortedItems
|
||||||
}
|
}
|
||||||
.mapSearch(
|
.mapSearch(
|
||||||
handle = queryHandle,
|
handle = queryHandle,
|
||||||
@ -140,6 +363,7 @@ fun produceAppPickerState(
|
|||||||
.map { content ->
|
.map { content ->
|
||||||
val state = AppPickerState(
|
val state = AppPickerState(
|
||||||
filter = queryFlow,
|
filter = queryFlow,
|
||||||
|
sort = sortFlow,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
Loadable.Ok(state)
|
Loadable.Ok(state)
|
||||||
@ -178,17 +402,20 @@ private fun getApps(
|
|||||||
val apps = pm.queryIntentActivities(intent, 0)
|
val apps = pm.queryIntentActivities(intent, 0)
|
||||||
apps
|
apps
|
||||||
.map { info ->
|
.map { info ->
|
||||||
|
val packageInfo = pm.getPackageInfo(info.activityInfo.packageName, 0)
|
||||||
|
val applicationInfo = pm.getApplicationInfo(info.activityInfo.packageName, 0)
|
||||||
val system = run {
|
val system = run {
|
||||||
val ai = pm.getApplicationInfo(info.activityInfo.packageName, 0)
|
|
||||||
val mask =
|
val mask =
|
||||||
ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
|
ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
|
||||||
ai.flags.and(mask) != 0
|
applicationInfo.flags.and(mask) != 0
|
||||||
}
|
}
|
||||||
val label = info.loadLabel(pm)?.toString().orEmpty()
|
val label = info.loadLabel(pm)?.toString().orEmpty()
|
||||||
|
val installTime = packageInfo.firstInstallTime
|
||||||
AppInfo(
|
AppInfo(
|
||||||
packageName = info.activityInfo.packageName,
|
packageName = info.activityInfo.packageName,
|
||||||
label = label,
|
label = label,
|
||||||
system = system,
|
system = system,
|
||||||
|
installTime = installTime,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,4 +424,5 @@ data class AppInfo(
|
|||||||
val packageName: String,
|
val packageName: String,
|
||||||
val label: String,
|
val label: String,
|
||||||
val system: Boolean,
|
val system: Boolean,
|
||||||
|
val installTime: Long,
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.artemchep.keyguard.feature.apppicker.model
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import arrow.optics.optics
|
||||||
|
import com.artemchep.keyguard.feature.apppicker.AppPickerComparatorHolder
|
||||||
|
import com.artemchep.keyguard.feature.localization.TextHolder
|
||||||
|
import com.artemchep.keyguard.feature.search.sort.model.SortItemModel
|
||||||
|
|
||||||
|
@optics
|
||||||
|
sealed interface AppPickerSortItem : SortItemModel {
|
||||||
|
companion object;
|
||||||
|
|
||||||
|
data class Section(
|
||||||
|
override val id: String,
|
||||||
|
override val text: TextHolder? = null,
|
||||||
|
) : AppPickerSortItem, SortItemModel.Section {
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Item(
|
||||||
|
override val id: String,
|
||||||
|
val config: AppPickerComparatorHolder,
|
||||||
|
override val icon: ImageVector? = null,
|
||||||
|
override val title: TextHolder,
|
||||||
|
override val checked: Boolean,
|
||||||
|
override val onClick: (() -> Unit)? = null,
|
||||||
|
) : AppPickerSortItem, SortItemModel.Item
|
||||||
|
}
|
@ -763,6 +763,9 @@
|
|||||||
<string name="sortby_modification_date_title">Modification date</string>
|
<string name="sortby_modification_date_title">Modification date</string>
|
||||||
<string name="sortby_modification_date_normal_mode">Newest First</string>
|
<string name="sortby_modification_date_normal_mode">Newest First</string>
|
||||||
<string name="sortby_modification_date_reverse_mode">Oldest First</string>
|
<string name="sortby_modification_date_reverse_mode">Oldest First</string>
|
||||||
|
<string name="sortby_installation_date_title">Installation date</string>
|
||||||
|
<string name="sortby_installation_date_normal_mode">Most Recent First</string>
|
||||||
|
<string name="sortby_installation_date_reverse_mode">Oldest First</string>
|
||||||
<string name="sortby_expiration_date_title">Expiration date</string>
|
<string name="sortby_expiration_date_title">Expiration date</string>
|
||||||
<string name="sortby_expiration_date_normal_mode">Newest First</string>
|
<string name="sortby_expiration_date_normal_mode">Newest First</string>
|
||||||
<string name="sortby_expiration_date_reverse_mode">Oldest First</string>
|
<string name="sortby_expiration_date_reverse_mode">Oldest First</string>
|
||||||
|
@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@ -21,11 +22,13 @@ class SearchQueryHandle(
|
|||||||
val querySink: MutableStateFlow<String>,
|
val querySink: MutableStateFlow<String>,
|
||||||
val queryState: MutableState<String>,
|
val queryState: MutableState<String>,
|
||||||
val queryIndexed: Flow<IndexedText?>,
|
val queryIndexed: Flow<IndexedText?>,
|
||||||
|
val revisionFlow: Flow<Int>,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RememberStateFlowScope.searchQueryHandle(
|
fun RememberStateFlowScope.searchQueryHandle(
|
||||||
key: String,
|
key: String,
|
||||||
|
revisionFlow: Flow<Int> = flowOf(0),
|
||||||
): SearchQueryHandle {
|
): SearchQueryHandle {
|
||||||
val querySink = mutablePersistedFlow(key) { "" }
|
val querySink = mutablePersistedFlow(key) { "" }
|
||||||
val queryState = mutableComposeState(querySink)
|
val queryState = mutableComposeState(querySink)
|
||||||
@ -45,35 +48,43 @@ fun RememberStateFlowScope.searchQueryHandle(
|
|||||||
querySink = querySink,
|
querySink = querySink,
|
||||||
queryState = queryState,
|
queryState = queryState,
|
||||||
queryIndexed = queryIndexedFlow,
|
queryIndexed = queryIndexedFlow,
|
||||||
|
revisionFlow = revisionFlow,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> RememberStateFlowScope.searchFilter(
|
suspend fun <T> RememberStateFlowScope.searchFilter(
|
||||||
handle: SearchQueryHandle,
|
handle: SearchQueryHandle,
|
||||||
transform: (TextFieldModel2, Int) -> T,
|
transform: (TextFieldModel2, Int) -> T,
|
||||||
) = handle.querySink
|
) = combine(
|
||||||
.map { query ->
|
handle.querySink,
|
||||||
val revision = query.trim().hashCode()
|
handle.revisionFlow,
|
||||||
val model = TextFieldModel2(
|
) { query, rev ->
|
||||||
state = handle.queryState,
|
val revision = rev xor query.trim().hashCode()
|
||||||
text = query,
|
val model = TextFieldModel2(
|
||||||
onChange = handle.queryState::value::set,
|
state = handle.queryState,
|
||||||
)
|
text = query,
|
||||||
transform(
|
onChange = handle.queryState::value::set,
|
||||||
model,
|
)
|
||||||
revision,
|
transform(
|
||||||
)
|
model,
|
||||||
}
|
revision,
|
||||||
|
)
|
||||||
|
}
|
||||||
.stateIn(screenScope)
|
.stateIn(screenScope)
|
||||||
|
|
||||||
fun <T> Flow<List<IndexedModel<T>>>.mapSearch(
|
fun <T> Flow<List<IndexedModel<T>>>.mapSearch(
|
||||||
handle: SearchQueryHandle,
|
handle: SearchQueryHandle,
|
||||||
transform: (T, IndexedText.FindResult) -> T,
|
transform: (T, IndexedText.FindResult) -> T,
|
||||||
) = this
|
) = combine(
|
||||||
.combine(handle.queryIndexed) { items, query -> items to query }
|
this,
|
||||||
.mapLatest { (items, query) ->
|
handle.queryIndexed,
|
||||||
|
handle.revisionFlow,
|
||||||
|
) { items, query, rev ->
|
||||||
|
Triple(items, query, rev)
|
||||||
|
}
|
||||||
|
.mapLatest { (items, query, rev) ->
|
||||||
if (query == null) {
|
if (query == null) {
|
||||||
return@mapLatest items.map { it.model } to 0
|
return@mapLatest items.map { it.model } to rev
|
||||||
}
|
}
|
||||||
|
|
||||||
val filteredItems = items
|
val filteredItems = items
|
||||||
@ -83,6 +94,7 @@ fun <T> Flow<List<IndexedModel<T>>>.mapSearch(
|
|||||||
highlightContentColor = handle.scope.colorScheme.searchHighlightContentColor,
|
highlightContentColor = handle.scope.colorScheme.searchHighlightContentColor,
|
||||||
transform = transform,
|
transform = transform,
|
||||||
)
|
)
|
||||||
filteredItems to query.text.hashCode()
|
val revision = rev xor query.text.hashCode()
|
||||||
|
filteredItems to revision
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user