Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit b3927bca authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

feat: sync install action state in search results

Unify search result buttons with live install/download state so users see accurate actions while downloads and installs progress.

Introduce a status stream + reconciler and map that into Compose UI state to keep button rendering and behaviors consistent across sources.
parent f1632493
Loading
Loading
Loading
Loading
+51 −22
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * 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
 * (at your option) 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 foundation.e.apps.data.install
package foundation.e.apps.data.install


import android.content.Context
import android.content.Context
@@ -115,30 +133,38 @@ class AppManagerWrapper @Inject constructor(
            val appDownload = getDownloadList()
            val appDownload = getDownloadList()
                .singleOrNull { it.id.contentEquals(app._id) && it.packageName.contentEquals(app.package_name) }
                .singleOrNull { it.id.contentEquals(app._id) && it.packageName.contentEquals(app.package_name) }
                ?: return 0
                ?: return 0

            return calculateProgress(appDownload, progress)
            if (!appDownload.id.contentEquals(app._id) || !appDownload.packageName.contentEquals(app.package_name)) {
                return@let
        }
        }

        return 0
            if (!isProgressValidForApp(application, progress)) {
                return -1
    }
    }


            val downloadingMap = progress.totalSizeBytes.filter { item ->
    suspend fun calculateProgress(
                appDownload.downloadIdMap.keys.contains(item.key) && item.value > 0
        appInstall: AppInstall,
        progress: DownloadProgress
    ): Int {
        val downloadIds = appInstall.downloadIdMap.keys
        if (downloadIds.isEmpty()) {
            // Download request exists but ids not yet populated; show 0% instead of dropping percent.
            return 0
        }
        }


            if (appDownload.downloadIdMap.size > downloadingMap.size) { // All files for download are not ready yet
        val totalSizeBytes = progress.totalSizeBytes
            .filterKeys { downloadIds.contains(it) }
            .values
            .sum()
        if (totalSizeBytes <= 0) {
            return 0
            return 0
        }
        }


            val totalSizeBytes = downloadingMap.values.sum()
        val downloadedSoFar = progress.bytesDownloadedSoFar
            val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item ->
            .filterKeys { downloadIds.contains(it) }
                appDownload.downloadIdMap.keys.contains(item.key)
            .values
            }.values.sum()
            .sum()
            return ((downloadedSoFar / totalSizeBytes.toDouble()) * PERCENTAGE_MULTIPLIER).toInt()

        }
        val percent = ((downloadedSoFar / totalSizeBytes.toDouble()) * 100)
        return 0
            .toInt()
            .coerceIn(0, 100)
        return percent
    }
    }


    private suspend fun isProgressValidForApp(
    private suspend fun isProgressValidForApp(
@@ -195,7 +221,10 @@ class AppManagerWrapper @Inject constructor(
        return Pair(1, 0)
        return Pair(1, 0)
    }
    }


    fun getDownloadingItemStatus(application: Application?, downloadList: List<AppInstall>): Status? {
    fun getDownloadingItemStatus(
        application: Application?,
        downloadList: List<AppInstall>
    ): Status? {
        application?.let { app ->
        application?.let { app ->
            val downloadingItem =
            val downloadingItem =
                downloadList.find { it.packageName == app.package_name || it.id == app.package_name }
                downloadList.find { it.packageName == app.package_name || it.id == app.package_name }
+26 −0
Original line number Original line Diff line number Diff line
@@ -114,6 +114,32 @@ class PwaManager @Inject constructor(
        context.startActivity(launchIntent)
        context.startActivity(launchIntent)
    }
    }


    /**
     * Return all installed PWA URLs from PWA Player.
     * Used for periodic status polling in Compose search to mirror legacy status detection.
     */
    fun getInstalledPwaUrls(): Set<String> {
        val installed = mutableSetOf<String>()
        context.contentResolver.query(
            PWA_PLAYER.toUri(),
            arrayOf("url"),
            null,
            null,
            null
        )?.use { cursor ->
            val urlIndex = cursor.getColumnIndex("url")
            if (urlIndex != -1) {
                while (cursor.moveToNext()) {
                    val url = cursor.getString(urlIndex)
                    if (!url.isNullOrBlank()) {
                        installed.add(url)
                    }
                }
            }
        }
        return installed
    }

    suspend fun installPWAApp(appInstall: AppInstall) {
    suspend fun installPWAApp(appInstall: AppInstall) {
        // Update status
        // Update status
        appInstall.status = Status.DOWNLOADING
        appInstall.status = Status.DOWNLOADING
+30 −16
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@


package foundation.e.apps.ui.compose.components
package foundation.e.apps.ui.compose.components


import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.clickable
@@ -44,6 +45,7 @@ import androidx.compose.runtime.remember
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.platform.testTag
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.stringResource
@@ -54,6 +56,7 @@ import androidx.compose.ui.unit.dp
import coil.compose.rememberImagePainter
import coil.compose.rememberImagePainter
import foundation.e.apps.R
import foundation.e.apps.R
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.ui.compose.state.InstallButtonAction
import foundation.e.apps.ui.compose.theme.AppTheme
import foundation.e.apps.ui.compose.theme.AppTheme


@Composable
@Composable
@@ -271,8 +274,16 @@ private fun PrimaryActionArea(
        // render the primary action button
        // render the primary action button
    }
    }


    val accentColor = MaterialTheme.colorScheme.tertiary

    val labelTextColor = when {
        uiState.isFilledStyle -> MaterialTheme.colorScheme.onPrimary
        else -> accentColor
    }

    val buttonContent: @Composable () -> Unit = {
    val buttonContent: @Composable () -> Unit = {
        if (uiState.isInProgress) {
        val showSpinner = uiState.isInProgress && uiState.label.isBlank()
        if (showSpinner) {
            val indicatorColor =
            val indicatorColor =
                if (uiState.isFilledStyle) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onPrimary
                if (uiState.isFilledStyle) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onPrimary
            CircularProgressIndicator(
            CircularProgressIndicator(
@@ -280,7 +291,7 @@ private fun PrimaryActionArea(
                    .size(16.dp)
                    .size(16.dp)
                    .testTag(SearchResultListItemTestTags.PRIMARY_PROGRESS),
                    .testTag(SearchResultListItemTestTags.PRIMARY_PROGRESS),
                strokeWidth = 2.dp,
                strokeWidth = 2.dp,
                color = indicatorColor,
                color = labelTextColor,
            )
            )
        } else {
        } else {
            val textColor =
            val textColor =
@@ -289,21 +300,16 @@ private fun PrimaryActionArea(
                text = uiState.label,
                text = uiState.label,
                maxLines = 1,
                maxLines = 1,
                overflow = TextOverflow.Clip,
                overflow = TextOverflow.Clip,
                color = textColor,
                color = labelTextColor,
            )
            )
        }
        }
    }
    }


    Column(horizontalAlignment = Alignment.End) {
    Column(horizontalAlignment = Alignment.End) {
        val containerColor = if (uiState.isFilledStyle) {
        val borderColor = when {
            MaterialTheme.colorScheme.primary
            uiState.isFilledStyle -> Color.Transparent
        } else {
            uiState.enabled -> accentColor
            MaterialTheme.colorScheme.secondaryContainer
            else -> accentColor.copy(alpha = 0.38f)
        }
        val contentColor = if (uiState.isFilledStyle) {
            MaterialTheme.colorScheme.onPrimary
        } else {
            MaterialTheme.colorScheme.onSecondaryContainer
        }
        }
        Button(
        Button(
            onClick = onPrimaryClick,
            onClick = onPrimaryClick,
@@ -313,11 +319,18 @@ private fun PrimaryActionArea(
                .testTag(SearchResultListItemTestTags.PRIMARY_BUTTON),
                .testTag(SearchResultListItemTestTags.PRIMARY_BUTTON),
            shape = RoundedCornerShape(4.dp),
            shape = RoundedCornerShape(4.dp),
            colors = ButtonDefaults.buttonColors(
            colors = ButtonDefaults.buttonColors(
                containerColor = containerColor,
                containerColor = when {
                contentColor = contentColor,
                    uiState.isFilledStyle -> accentColor
                disabledContainerColor = containerColor.copy(alpha = 0.38f),
                    else -> Color.Transparent
                disabledContentColor = contentColor.copy(alpha = 0.38f),
                },
                contentColor = labelTextColor,
                disabledContainerColor = when {
                    uiState.isFilledStyle -> accentColor.copy(alpha = 0.12f)
                    else -> Color.Transparent
                },
                disabledContentColor = labelTextColor.copy(alpha = 0.38f),
            ),
            ),
            border = BorderStroke(1.dp, borderColor),
            contentPadding = ButtonDefaults.ContentPadding,
            contentPadding = ButtonDefaults.ContentPadding,
        ) {
        ) {
            buttonContent()
            buttonContent()
@@ -369,6 +382,7 @@ data class PrimaryActionUiState(
    val isInProgress: Boolean,
    val isInProgress: Boolean,
    val isFilledStyle: Boolean,
    val isFilledStyle: Boolean,
    val showMore: Boolean = false,
    val showMore: Boolean = false,
    val actionIntent: InstallButtonAction = InstallButtonAction.NoOp,
)
)


internal object SearchResultListItemTestTags {
internal object SearchResultListItemTestTags {
+50 −20
Original line number Original line Diff line number Diff line
@@ -39,21 +39,21 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.LazyPagingItems
import com.aurora.gplayapi.data.models.App
import foundation.e.apps.R
import foundation.e.apps.R
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.utils.toApplication
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.enums.Status
import foundation.e.apps.ui.compose.components.search.SearchErrorState
import foundation.e.apps.ui.compose.components.search.SearchErrorState
import foundation.e.apps.ui.compose.components.search.SearchResultListItemPlaceholder
import foundation.e.apps.ui.compose.components.search.SearchResultListItemPlaceholder
import foundation.e.apps.ui.compose.components.search.SearchShimmerList
import foundation.e.apps.ui.compose.components.search.SearchShimmerList
import foundation.e.apps.ui.compose.state.InstallButtonAction
import foundation.e.apps.ui.compose.state.InstallButtonState
import foundation.e.apps.ui.compose.state.InstallButtonStyle
import foundation.e.apps.ui.search.v2.ScrollPosition
import foundation.e.apps.ui.search.v2.ScrollPosition
import foundation.e.apps.ui.search.v2.SearchTabType
import foundation.e.apps.ui.search.v2.SearchTabType
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.collectLatest
@@ -66,16 +66,17 @@ fun SearchResultsContent(
    selectedTab: SearchTabType,
    selectedTab: SearchTabType,
    fossItems: LazyPagingItems<Application>?,
    fossItems: LazyPagingItems<Application>?,
    pwaItems: LazyPagingItems<Application>?,
    pwaItems: LazyPagingItems<Application>?,
    playStoreItems: LazyPagingItems<Application>?,
    searchVersion: Int,
    searchVersion: Int,
    getScrollPosition: (SearchTabType) -> ScrollPosition?,
    getScrollPosition: (SearchTabType) -> ScrollPosition?,
    onScrollPositionChange: (SearchTabType, Int, Int) -> Unit,
    onScrollPositionChange: (SearchTabType, Int, Int) -> Unit,
    onTabSelect: (SearchTabType) -> Unit,
    onTabSelect: (SearchTabType) -> Unit,
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
    playStoreItems: LazyPagingItems<App>? = null,
    onResultClick: (Application) -> Unit = {},
    onResultClick: (Application) -> Unit = {},
    onPrimaryActionClick: (Application) -> Unit = {},
    onPrimaryActionClick: (Application, InstallButtonAction) -> Unit = { _, _ -> },
    onShowMoreClick: (Application) -> Unit = {},
    onShowMoreClick: (Application) -> Unit = {},
    onPrivacyClick: (Application) -> Unit = {},
    onPrivacyClick: (Application) -> Unit = {},
    installButtonStateProvider: (Application) -> InstallButtonState,
) {
) {
    if (tabs.isEmpty() || selectedTab !in tabs) {
    if (tabs.isEmpty() || selectedTab !in tabs) {
        return
        return
@@ -138,6 +139,7 @@ fun SearchResultsContent(
                        onPrimaryActionClick = onPrimaryActionClick,
                        onPrimaryActionClick = onPrimaryActionClick,
                        onShowMoreClick = onShowMoreClick,
                        onShowMoreClick = onShowMoreClick,
                        onPrivacyClick = onPrivacyClick,
                        onPrivacyClick = onPrivacyClick,
                        installButtonStateProvider = installButtonStateProvider,
                        modifier = Modifier.fillMaxSize(),
                        modifier = Modifier.fillMaxSize(),
                    )
                    )
                }
                }
@@ -153,6 +155,7 @@ fun SearchResultsContent(
                        onPrimaryActionClick = onPrimaryActionClick,
                        onPrimaryActionClick = onPrimaryActionClick,
                        onShowMoreClick = onShowMoreClick,
                        onShowMoreClick = onShowMoreClick,
                        onPrivacyClick = onPrivacyClick,
                        onPrivacyClick = onPrivacyClick,
                        installButtonStateProvider = installButtonStateProvider,
                        modifier = Modifier.fillMaxSize(),
                        modifier = Modifier.fillMaxSize(),
                    )
                    )
                }
                }
@@ -167,6 +170,7 @@ fun SearchResultsContent(
                        onPrimaryActionClick = onPrimaryActionClick,
                        onPrimaryActionClick = onPrimaryActionClick,
                        onShowMoreClick = onShowMoreClick,
                        onShowMoreClick = onShowMoreClick,
                        onPrivacyClick = onPrivacyClick,
                        onPrivacyClick = onPrivacyClick,
                        installButtonStateProvider = installButtonStateProvider,
                        modifier = Modifier.fillMaxSize(),
                        modifier = Modifier.fillMaxSize(),
                    )
                    )
                }
                }
@@ -177,17 +181,17 @@ fun SearchResultsContent(


@Composable
@Composable
private fun PagingPlayStoreResultList(
private fun PagingPlayStoreResultList(
    items: LazyPagingItems<App>?,
    items: LazyPagingItems<Application>?,
    searchVersion: Int,
    searchVersion: Int,
    getScrollPosition: (SearchTabType) -> ScrollPosition?,
    getScrollPosition: (SearchTabType) -> ScrollPosition?,
    onScrollPositionChange: (SearchTabType, Int, Int) -> Unit,
    onScrollPositionChange: (SearchTabType, Int, Int) -> Unit,
    onItemClick: (Application) -> Unit,
    onItemClick: (Application) -> Unit,
    onPrimaryActionClick: (Application) -> Unit,
    onPrimaryActionClick: (Application, InstallButtonAction) -> Unit,
    onShowMoreClick: (Application) -> Unit,
    onShowMoreClick: (Application) -> Unit,
    onPrivacyClick: (Application) -> Unit,
    onPrivacyClick: (Application) -> Unit,
    installButtonStateProvider: (Application) -> InstallButtonState,
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
) {
) {
    val context = LocalContext.current
    val lazyItems = items ?: return
    val lazyItems = items ?: return
    val saved = getScrollPosition(SearchTabType.COMMON_APPS)
    val saved = getScrollPosition(SearchTabType.COMMON_APPS)
    val listState = rememberSaveable(
    val listState = rememberSaveable(
@@ -251,18 +255,25 @@ private fun PagingPlayStoreResultList(
                        count = lazyItems.itemCount,
                        count = lazyItems.itemCount,
                        key = { index ->
                        key = { index ->
                            val item = lazyItems.peek(index)
                            val item = lazyItems.peek(index)
                            item?.packageName.takeIf { !it.isNullOrBlank() }
                            item?.package_name.takeIf { !it.isNullOrBlank() }
                                ?: item?.id.toString()
                                ?: item?._id.toString()
                        },
                        },
                    ) { index ->
                    ) { index ->
                        val app = lazyItems[index]
                        val application = lazyItems[index]
                        if (app != null) {
                        if (application != null) {
                            val application = app.toApplication(context)
                            val uiState = application.toSearchResultUiState(
                                installButtonStateProvider(application)
                            )
                            SearchResultListItem(
                            SearchResultListItem(
                                application = application,
                                application = application,
                                uiState = application.toSearchResultUiState(),
                                uiState = uiState,
                                onItemClick = onItemClick,
                                onItemClick = onItemClick,
                                onPrimaryActionClick = onPrimaryActionClick,
                                onPrimaryActionClick = {
                                    onPrimaryActionClick(
                                        application,
                                        uiState.primaryAction.actionIntent
                                    )
                                },
                                onShowMoreClick = onShowMoreClick,
                                onShowMoreClick = onShowMoreClick,
                                onPrivacyClick = onPrivacyClick,
                                onPrivacyClick = onPrivacyClick,
                                modifier = Modifier.fillMaxWidth(),
                                modifier = Modifier.fillMaxWidth(),
@@ -298,9 +309,10 @@ private fun PagingSearchResultList(
    getScrollPosition: (SearchTabType) -> ScrollPosition?,
    getScrollPosition: (SearchTabType) -> ScrollPosition?,
    onScrollPositionChange: (SearchTabType, Int, Int) -> Unit,
    onScrollPositionChange: (SearchTabType, Int, Int) -> Unit,
    onItemClick: (Application) -> Unit,
    onItemClick: (Application) -> Unit,
    onPrimaryActionClick: (Application) -> Unit,
    onPrimaryActionClick: (Application, InstallButtonAction) -> Unit,
    onShowMoreClick: (Application) -> Unit,
    onShowMoreClick: (Application) -> Unit,
    onPrivacyClick: (Application) -> Unit,
    onPrivacyClick: (Application) -> Unit,
    installButtonStateProvider: (Application) -> InstallButtonState,
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
) {
) {
    val lazyItems = items ?: return
    val lazyItems = items ?: return
@@ -371,11 +383,19 @@ private fun PagingSearchResultList(
                    ) { index ->
                    ) { index ->
                        val application = lazyItems[index]
                        val application = lazyItems[index]
                        if (application != null) {
                        if (application != null) {
                            val uiState = application.toSearchResultUiState(
                                installButtonStateProvider(application)
                            )
                            SearchResultListItem(
                            SearchResultListItem(
                                application = application,
                                application = application,
                                uiState = application.toSearchResultUiState(),
                                uiState = uiState,
                                onItemClick = onItemClick,
                                onItemClick = onItemClick,
                                onPrimaryActionClick = onPrimaryActionClick,
                                onPrimaryActionClick = {
                                    onPrimaryActionClick(
                                        application,
                                        uiState.primaryAction.actionIntent
                                    )
                                },
                                onShowMoreClick = onShowMoreClick,
                                onShowMoreClick = onShowMoreClick,
                                onPrivacyClick = onPrivacyClick,
                                onPrivacyClick = onPrivacyClick,
                                modifier = Modifier.fillMaxWidth(),
                                modifier = Modifier.fillMaxWidth(),
@@ -408,7 +428,7 @@ private fun PagingSearchResultList(
}
}


@Composable
@Composable
private fun Application.toSearchResultUiState(): SearchResultListItemState {
private fun Application.toSearchResultUiState(buttonState: InstallButtonState): SearchResultListItemState {
    if (isPlaceHolder) {
    if (isPlaceHolder) {
        return SearchResultListItemState(
        return SearchResultListItemState(
            author = "",
            author = "",
@@ -453,7 +473,17 @@ private fun Application.toSearchResultUiState(): SearchResultListItemState {
        privacyScore = "",
        privacyScore = "",
        showPrivacyScore = false, // Privacy scores are disabled on Search per functional spec.
        showPrivacyScore = false, // Privacy scores are disabled on Search per functional spec.
        isPrivacyLoading = false,
        isPrivacyLoading = false,
        primaryAction = resolvePrimaryActionState(this),
        primaryAction = PrimaryActionUiState(
            label = buttonState.label.text
               ?: buttonState.progressPercentText
                ?: buttonState.label.resId?.let { stringResource(id = it) }
                ?: "",
            enabled = buttonState.enabled,
            isInProgress = buttonState.isInProgress(),
            isFilledStyle = buttonState.style == InstallButtonStyle.AccentFill,
            showMore = false,
            actionIntent = buttonState.actionIntent,
        ),
        iconUrl = iconUrl,
        iconUrl = iconUrl,
        placeholderResId = null,
        placeholderResId = null,
        isPlaceholder = false,
        isPlaceholder = false,
+8 −4
Original line number Original line Diff line number Diff line
@@ -41,10 +41,11 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.paging.PagingData
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import com.aurora.gplayapi.data.models.App
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.ui.compose.components.SearchInitialState
import foundation.e.apps.ui.compose.components.SearchInitialState
import foundation.e.apps.ui.compose.components.SearchResultsContent
import foundation.e.apps.ui.compose.components.SearchResultsContent
import foundation.e.apps.ui.compose.state.InstallButtonAction
import foundation.e.apps.ui.compose.state.InstallButtonState
import foundation.e.apps.ui.search.v2.ScrollPosition
import foundation.e.apps.ui.search.v2.ScrollPosition
import foundation.e.apps.ui.search.v2.SearchTabType
import foundation.e.apps.ui.search.v2.SearchTabType
import foundation.e.apps.ui.search.v2.SearchUiState
import foundation.e.apps.ui.search.v2.SearchUiState
@@ -62,7 +63,7 @@ fun SearchScreen(
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
    fossPaging: Flow<PagingData<Application>>? = null,
    fossPaging: Flow<PagingData<Application>>? = null,
    pwaPaging: Flow<PagingData<Application>>? = null,
    pwaPaging: Flow<PagingData<Application>>? = null,
    playStorePaging: Flow<PagingData<App>>? = null,
    playStorePaging: Flow<PagingData<Application>>? = null,
    searchVersion: Int = 0,
    searchVersion: Int = 0,
    getScrollPosition: (SearchTabType) -> ScrollPosition? = { null },
    getScrollPosition: (SearchTabType) -> ScrollPosition? = { null },
    onScrollPositionChange: (SearchTabType, Int, Int) -> Unit = { _, _, _ -> },
    onScrollPositionChange: (SearchTabType, Int, Int) -> Unit = { _, _, _ -> },
@@ -70,6 +71,8 @@ fun SearchScreen(
    onPrimaryActionClick: (Application) -> Unit = {},
    onPrimaryActionClick: (Application) -> Unit = {},
    onShowMoreClick: (Application) -> Unit = {},
    onShowMoreClick: (Application) -> Unit = {},
    onPrivacyClick: (Application) -> Unit = {},
    onPrivacyClick: (Application) -> Unit = {},
    onPrimaryAction: (Application, InstallButtonAction) -> Unit = { _, _ -> },
    installButtonStateProvider: (Application) -> InstallButtonState,
) {
) {
    val focusManager = LocalFocusManager.current
    val focusManager = LocalFocusManager.current
    val keyboardController = LocalSoftwareKeyboardController.current
    val keyboardController = LocalSoftwareKeyboardController.current
@@ -146,6 +149,7 @@ fun SearchScreen(
                    selectedTab = uiState.selectedTab!!,
                    selectedTab = uiState.selectedTab!!,
                    fossItems = fossItems,
                    fossItems = fossItems,
                    pwaItems = pwaItems,
                    pwaItems = pwaItems,
                    playStoreItems = playStoreItems,
                    searchVersion = searchVersion,
                    searchVersion = searchVersion,
                    getScrollPosition = getScrollPosition,
                    getScrollPosition = getScrollPosition,
                    onScrollPositionChange = onScrollPositionChange,
                    onScrollPositionChange = onScrollPositionChange,
@@ -153,11 +157,11 @@ fun SearchScreen(
                    modifier = Modifier
                    modifier = Modifier
                        .fillMaxWidth()
                        .fillMaxWidth()
                        .padding(top = 8.dp),
                        .padding(top = 8.dp),
                    playStoreItems = playStoreItems,
                    onResultClick = onResultClick,
                    onResultClick = onResultClick,
                    onPrimaryActionClick = onPrimaryActionClick,
                    onPrimaryActionClick = onPrimaryAction,
                    onShowMoreClick = onShowMoreClick,
                    onShowMoreClick = onShowMoreClick,
                    onPrivacyClick = onPrivacyClick,
                    onPrivacyClick = onPrivacyClick,
                    installButtonStateProvider = installButtonStateProvider,
                )
                )
            } else {
            } else {
                SearchInitialState(
                SearchInitialState(
Loading