Parse intents to search, view, or play media (#1688)

## Description
Adds parsing for intents to search, view, or play media based on the
action and parameters. This allows for remotely opening Wholphin and
starting playback.

This is considered experimental, so the syntax or behaviors may change!

See the `Intents.md` file in this PR for syntax and examples.

### Related issues
Replaces https://github.com/damontecres/Wholphin/pull/738
Closes https://github.com/damontecres/Wholphin/discussions/1095

### Testing
Emulator with the test commands in the docs

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-07-20 10:04:34 -04:00 • committed by GitHub
parent 4f5cd3e7b1
commit b1f9c32a19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 415 additions and 78 deletions

151
Intents.md Normal file
View file

@ -0,0 +1,151 @@
# Intents
> [!WARNING]
> This feature is only available in the develop builds until the next release!
> [!IMPORTANT]
> This feature is experimental and the exact behaviors and/or parameters may change at any time
Wholphin accepts [intents](https://developer.android.com/guide/components/intents-filters) to load or play specified media.
Parameters can be specified as "extras" (eg `--es query value`) or as query parameters to the data URI (eg `-d wholphin://search?query=value`).
UUID parameters can be specified with or without hyphens.
If using data URI query parameters on the command line, you need to escape ampersands: `itemId=...\&userId=...`
## Server/user switching
You can choose a server and user to switch to with the `serverId` and `userId` UUID parameters. If the profile is not protected, the app will switch to it.
If not specified, the app will use the current user if auto sign in is enabled.
## Search
Action: `android.intent.action.SEARCH`
Shorthand action: `search`
Parameters:
- `query` - Optional, prepopulate the search field
### Examples
Full format
```bash
adb shell am start \
-a android.intent.action.SEARCH \
-n 'com.github.damontecres.wholphin/.MainActivity' \
--es query "Avengers"
```
URI based
```bash
adb shell am start -d 'wholphin://search?query=Avengers'
```
Multi-word query:
```bash
# Whole shell command is wrapped in double quotes
adb shell "am start \
-a android.intent.action.SEARCH \
-n 'com.github.damontecres.wholphin/.MainActivity' \
--es query 'Avengers Endgame'"
adb shell am start -d 'wholphin://search?query=Avengers+Endgame'
adb shell am start -d 'wholphin://search?query=Avengers%20Endgame'
```
## View
This opens the app to the specified item such as an episode or movie
Action: `android.intent.action.VIEW`
Shorthand action: `view`
Parameters:
- `itemId` - Required, the UUID of the media item
### Examples
Full format
```bash
adb shell am start \
-a android.intent.action.VIEW \
-n 'com.github.damontecres.wholphin/.MainActivity' \
--es itemId "5cf8f8e7-2a5f-4aa9-8c12-ddf63d42ee6d"
```
URI based
```bash
adb shell am start -d 'wholphin://view?itemId=5cf8f8e7-2a5f-4aa9-8c12-ddf63d42ee6d'
```
## Playback
Action: `com.github.damontecres.wholphin.PLAYBACK`
Shorthand action: `play`
Parameters:
- `itemId` - Required, the UUID of the media item
- `position` - Optional, the start position for playback in milliseconds
### Examples
Full format
```bash
adb shell am start \
-a com.github.damontecres.wholphin.PLAYBACK \
-n 'com.github.damontecres.wholphin/.MainActivity' \
--es itemId "5cf8f8e7-2a5f-4aa9-8c12-ddf63d42ee6d"
```
URI based
```bash
adb shell am start -d 'wholphin://play?itemId=5cf8f8e7-2a5f-4aa9-8c12-ddf63d42ee6d'
```
Start at 4m30s:
```bash
adb shell am start \
-a com.github.damontecres.wholphin.PLAYBACK \
-n 'com.github.damontecres.wholphin/.MainActivity' \
--es itemId "5cf8f8e7-2a5f-4aa9-8c12-ddf63d42ee6d" \
--el position 270000
adb shell am start -d 'wholphin://play?itemId=5cf8f8e7-2a5f-4aa9-8c12-ddf63d42ee6d\&position=270000'
```
## More examples
### Switch user
Full format
```bash
adb shell am start \
-a android.intent.action.VIEW \
-n 'com.github.damontecres.wholphin/.MainActivity' \
--es itemId "5cf8f8e7-2a5f-4aa9-8c12-ddf63d42ee6d" \
--es serverId "341880f9-ca88-4038-8718-f6b2407950b1" \
--es userId "5c8575e0-468d-44fb-8c70-00aa79160587"
```
URI based
```bash
adb shell am start -d 'wholphin://view?itemId=66c80aaac9c17d3761782d2205520229\&serverId=341880f9-ca88-4038-8718-f6b2407950b1\&userId=5c8575e0-468d-44fb-8c70-00aa79160587'
```
### Debug build
Debug builds have a different package, so you must specify the component by its full name:
```bash
adb shell am start \
-a android.intent.action.VIEW \
-n 'com.github.damontecres.wholphin.debug/com.github.damontecres.wholphin.MainActivity' \
--es itemId "5cf8f8e7-2a5f-4aa9-8c12-ddf63d42ee6d"
```

View file

@ -58,6 +58,13 @@
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="com.github.damontecres.wholphin.PLAYBACK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="wholphin" />
</intent-filter>
</activity>
<provider

View file

@ -36,6 +36,8 @@ import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.DatePlayedInvalidationService
import com.github.damontecres.wholphin.services.DeviceProfileService
import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.IntentResult
import com.github.damontecres.wholphin.services.IntentService
import com.github.damontecres.wholphin.services.LatestNextUpSchedulerService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver
@ -54,7 +56,6 @@ import com.github.damontecres.wholphin.ui.CoilConfig
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.collectLatestIn
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.playback.PlayExternalViewModel
@ -80,7 +81,6 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import javax.inject.Inject
@ -145,6 +145,9 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var screensaverService: ScreensaverService
@Inject
lateinit var intentService: IntentService
private var signInAuto = true
private var playerBackend: PlayerBackend? = null
@ -187,8 +190,7 @@ class MainActivity : AppCompatActivity() {
}
navigationManager.backStack = NavBackStack(*backStack.toTypedArray())
} else {
val startDestination = intent?.let(::extractDestination) ?: Destination.Home()
navigationManager.backStack = NavBackStack(startDestination)
navigationManager.backStack = NavBackStack(Destination.Home())
}
viewModel.serverRepository.currentUserFlow
@ -223,7 +225,7 @@ class MainActivity : AppCompatActivity() {
playerBackend = prefs.playbackPreferences.playerBackend
}
viewModel.appStart()
viewModel.appStart(intent)
setContent {
Surface(
Modifier
@ -307,7 +309,7 @@ class MainActivity : AppCompatActivity() {
override fun onRestart() {
super.onRestart()
Timber.d("onRestart")
viewModel.appStart()
viewModel.appStart(null)
if (!playExternalViewModel.launched.value) {
// If restarting during playback that is not external, go back a page
val lastDest = navigationManager.backStack.lastOrNull()
@ -384,42 +386,9 @@ class MainActivity : AppCompatActivity() {
super.onNewIntent(intent)
Timber.v("onNewIntent")
setIntent(intent)
extractDestination(intent)?.let {
navigationManager.replace(it)
}
viewModel.appStart(intent)
}
private fun extractDestination(intent: Intent): Destination? =
intent.let {
val itemId =
it.getStringExtra(INTENT_ITEM_ID)?.toUUIDOrNull()
val type =
it.getStringExtra(INTENT_ITEM_TYPE)?.let(BaseItemKind::fromNameOrNull)
if (itemId != null && type != null) {
val seriesId = it.getStringExtra(INTENT_SERIES_ID)?.toUUIDOrNull()
val seasonId = it.getStringExtra(INTENT_SEASON_ID)?.toUUIDOrNull()
val episodeNumber = it.getIntExtra(INTENT_EPISODE_NUMBER, -1)
val seasonNumber = it.getIntExtra(INTENT_SEASON_NUMBER, -1)
if (seriesId != null && seasonId != null && episodeNumber >= 0 && seasonNumber >= 0) {
Destination.SeriesOverview(
itemId = seriesId,
type = BaseItemKind.SERIES,
seasonEpisode =
SeasonEpisodeIds(
seasonId = seasonId,
seasonNumber = seasonNumber,
episodeId = itemId,
episodeNumber = episodeNumber,
),
)
} else {
Destination.MediaItem(itemId, type)
}
} else {
null
}
}
fun changeDisplayMode(modeId: Int) {
lifecycleScope.launch(WholphinDispatchers.Main + ExceptionHandler(autoToast = true)) {
val attrs = window.attributes
@ -431,13 +400,6 @@ class MainActivity : AppCompatActivity() {
}
companion object {
const val INTENT_ITEM_ID = "itemId"
const val INTENT_ITEM_TYPE = "itemType"
const val INTENT_SERIES_ID = "seriesId"
const val INTENT_EPISODE_NUMBER = "epNum"
const val INTENT_SEASON_NUMBER = "seaNum"
const val INTENT_SEASON_ID = "seaId"
private const val KEY_BACK_STACK = "backStack"
private const val KEY_EXTERNAL_PLAYER = "extPlayer"
@ -453,16 +415,55 @@ class MainActivityViewModel
@param:ApplicationContext private val context: Context,
private val preferences: DataStore<AppPreferences>,
val serverRepository: ServerRepository,
private val navigationManager: SetupNavigationManager,
private val setupNavigationManager: SetupNavigationManager,
private val navigationManager: NavigationManager,
private val deviceProfileService: DeviceProfileService,
private val backdropService: BackdropService,
private val appUpgradeHandler: AppUpgradeHandler,
private val intentService: IntentService,
) : ViewModel() {
private val mutex = Mutex()
fun appStart() {
fun appStart(intent: Intent?) {
viewModelScope.launchDefault {
mutex.withLock {
try {
val result = intent?.let { intentService.parseIntent(intent) }
when (result) {
is IntentResult.Error -> {
setupNavigationManager.navigateTo(SetupDestination.ServerList)
Timber.e("Error parsing intent: %s", result.message)
showToast(context, "Invalid intent: ${result.message}")
return@withLock
}
is IntentResult.Target -> {
val current = serverRepository.current.value
if (current != null) {
Timber.i("Received valid intent, switching to AppContent")
navigationManager.replace(result.destination)
setupNavigationManager.navigateTo(
SetupDestination.AppContent(current),
)
} else {
// This should never happen, but just reset app state if it does
setupNavigationManager.navigateTo(SetupDestination.ServerList)
Timber.e("Error parsing intent, no user is active")
showToast(context, "An error occurred parsing the intent")
}
return@withLock
}
IntentResult.NoOp,
null,
-> {
// No-op, proceed below
}
}
} catch (ex: Exception) {
Timber.e(ex, "Error parsing intent")
}
try {
val needUpgrade = appUpgradeHandler.needUpgrade()
if (needUpgrade) {
@ -479,9 +480,9 @@ class MainActivityViewModel
val prefs =
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
val profileProtected =
serverRepository.current.value?.user?.let {
it.hasPin || it.requireLogin
} == true
serverRepository.current.value
?.user
?.isProtected == true
if (prefs.signInAutomatically && !profileProtected) {
val current =
serverRepository.restoreSession(
@ -490,38 +491,46 @@ class MainActivityViewModel
)
if (current != null) {
if (current.user.hasPin || current.user.requireLogin) {
navigationManager.navigateTo(SetupDestination.UserList(current.server))
setupNavigationManager.navigateTo(
SetupDestination.UserList(
current.server,
),
)
} else {
// Restored
navigationManager.navigateTo(SetupDestination.AppContent(current))
setupNavigationManager.navigateTo(
SetupDestination.AppContent(
current,
),
)
}
} else {
// Did not restore
navigationManager.navigateTo(SetupDestination.ServerList)
setupNavigationManager.navigateTo(SetupDestination.ServerList)
}
} else {
navigationManager.navigateTo(SetupDestination.Loading)
setupNavigationManager.navigateTo(SetupDestination.Loading)
backdropService.clearBackdrop()
val currentServerId = prefs.currentServerId?.toUUIDOrNull()
if (currentServerId != null) {
val currentServer =
serverRepository.serverDao.getServer(currentServerId)?.server
if (currentServer != null) {
navigationManager.navigateTo(
setupNavigationManager.navigateTo(
SetupDestination.UserList(
currentServer,
),
)
} else {
navigationManager.navigateTo(SetupDestination.ServerList)
setupNavigationManager.navigateTo(SetupDestination.ServerList)
}
} else {
navigationManager.navigateTo(SetupDestination.ServerList)
setupNavigationManager.navigateTo(SetupDestination.ServerList)
}
}
} catch (ex: Exception) {
Timber.e(ex, "Error during appStart")
navigationManager.navigateTo(SetupDestination.ServerList)
setupNavigationManager.navigateTo(SetupDestination.ServerList)
}
}
}

View file

@ -71,6 +71,8 @@ data class JellyfinUser(
) {
val hasPin: Boolean get() = pin.isNotNullOrBlank()
val isProtected: Boolean get() = hasPin || requireLogin
override fun toString(): String =
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, lastUsed=$lastUsed, " +
"accessToken?=${accessToken.isNotNullOrBlank()}, pin?=${pin.isNotNullOrBlank()}), " +

View file

@ -0,0 +1,164 @@
package com.github.damontecres.wholphin.services
import android.app.SearchManager
import android.content.Intent
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.nav.Destination
import kotlinx.coroutines.flow.first
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class IntentService
@Inject
constructor(
private val api: ApiClient,
private val serverRepository: ServerRepository,
private val userPreferencesService: UserPreferencesService,
) {
suspend fun parseIntent(intent: Intent): IntentResult {
Timber.v("Parsing intent %s", intent)
Timber.v("Intent extras: %s", intent.extras)
Timber.v("Intent data: %s", intent.data)
if (intent.getStringParam("type") != null) {
return getDestinationFromChannel(intent)?.let { IntentResult.Target(it) }
?: IntentResult.Error("Invalid parameters")
}
val action = intent.action ?: intent.data?.host
if (action == Intent.ACTION_MAIN || action.isNullOrBlank()) {
return IntentResult.NoOp
}
val userId = intent.getStringParam("userId")?.toUUIDOrNull()
val serverId = intent.getStringParam("serverId")?.toUUIDOrNull()
if (userId != null && serverId != null) {
Timber.v("Intent switches user")
val user =
serverRepository.serverDao.getUser(serverId, userId)
?: return IntentResult.Error("User not found")
if (!user.isProtected) {
serverRepository.restoreSession(serverId, userId)
} else {
return IntentResult.Error("Cannot switch to specified user")
}
} else if (serverRepository.currentUser == null) {
Timber.v("No current user, so restoring last")
val appPrefs = userPreferencesService.flow.first().appPreferences
if (appPrefs.signInAutomatically) {
val userId = appPrefs.currentUserId.toUUIDOrNull()
val serverId = appPrefs.currentServerId.toUUIDOrNull()
if (userId != null && serverId != null) {
serverRepository.restoreSession(serverId, userId)
?: return IntentResult.Error("Error restoring user")
} else {
return IntentResult.Error("Could not auto sign-in, specify a server & user")
}
} else {
return IntentResult.Error("Auto sign-in not enabled, specify a server & user")
}
}
if (action == Intent.ACTION_SEARCH || action == "search") {
val query = intent.getStringParam(SearchManager.QUERY)
return IntentResult.Target(Destination.Search(query ?: ""))
}
val itemId =
intent.getStringParam("itemId")?.toUUIDOrNull()
?: return IntentResult.Error("No item id provided")
val item =
try {
api.userLibraryApi
.getItem(itemId)
.content
.let { BaseItem(it) }
} catch (ex: Exception) {
Timber.w(ex, "Error fetching item %s", itemId)
return IntentResult.Error("Could not fetch item $itemId")
}
val destination =
when (action) {
Intent.ACTION_VIEW, "view" -> {
item.destination()
}
"com.github.damontecres.wholphin.PLAYBACK", "play" -> {
val position = intent.getLongParam("position")?.coerceAtLeast(0)
Destination.Playback(
itemId = itemId,
positionMs = position ?: 0L,
)
}
else -> {
return IntentResult.Error("Invalid action: ${intent.action}")
}
}
return IntentResult.Target(destination)
}
private fun Intent.getStringParam(key: String) = getStringExtra(key) ?: data?.getQueryParameter(key)
private fun Intent.getLongParam(key: String) =
getLongExtra(key, -1).takeIf { it >= 0 } ?: data?.getQueryParameter(key)?.toLongOrNull()
private fun getDestinationFromChannel(intent: Intent): Destination? =
intent.let {
val itemId =
it.getStringExtra(INTENT_ITEM_ID)?.toUUIDOrNull()
val type =
it.getStringExtra(INTENT_ITEM_TYPE)?.let(BaseItemKind::fromNameOrNull)
if (itemId != null && type != null) {
val seriesId = it.getStringExtra(INTENT_SERIES_ID)?.toUUIDOrNull()
val seasonId = it.getStringExtra(INTENT_SEASON_ID)?.toUUIDOrNull()
val episodeNumber = it.getIntExtra(INTENT_EPISODE_NUMBER, -1)
val seasonNumber = it.getIntExtra(INTENT_SEASON_NUMBER, -1)
if (seriesId != null && seasonId != null && episodeNumber >= 0 && seasonNumber >= 0) {
Destination.SeriesOverview(
itemId = seriesId,
type = BaseItemKind.SERIES,
seasonEpisode =
SeasonEpisodeIds(
seasonId = seasonId,
seasonNumber = seasonNumber,
episodeId = itemId,
episodeNumber = episodeNumber,
),
)
} else {
Destination.MediaItem(itemId, type)
}
} else {
null
}
}
companion object {
const val INTENT_ITEM_ID = "itemId"
const val INTENT_ITEM_TYPE = "itemType"
const val INTENT_SERIES_ID = "seriesId"
const val INTENT_EPISODE_NUMBER = "epNum"
const val INTENT_SEASON_NUMBER = "seaNum"
const val INTENT_SEASON_ID = "seaId"
}
}
sealed interface IntentResult {
data class Error(
val message: String,
) : IntentResult
data object NoOp : IntentResult
data class Target(
val destination: Destination,
) : IntentResult
}

View file

@ -24,6 +24,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.IntentService
import com.github.damontecres.wholphin.services.LatestNextUpService
import com.github.damontecres.wholphin.ui.SlimItemFields
import dagger.assisted.Assisted
@ -226,27 +227,27 @@ class TvProviderWorker
setIntent(
Intent(context, MainActivity::class.java)
.putExtra(MainActivity.INTENT_ITEM_ID, item.id.toString())
.putExtra(MainActivity.INTENT_ITEM_TYPE, item.type.serialName)
.putExtra(IntentService.INTENT_ITEM_ID, item.id.toString())
.putExtra(IntentService.INTENT_ITEM_TYPE, item.type.serialName)
.apply {
if (item.type == BaseItemKind.EPISODE) {
putExtra(
MainActivity.INTENT_SERIES_ID,
IntentService.INTENT_SERIES_ID,
dto.seriesId?.toString(),
)
putExtra(
MainActivity.INTENT_SEASON_ID,
IntentService.INTENT_SEASON_ID,
dto.seasonId?.toString(),
)
dto.parentIndexNumber?.let {
putExtra(
MainActivity.INTENT_SEASON_NUMBER,
IntentService.INTENT_SEASON_NUMBER,
it,
)
}
dto.indexNumber?.let {
putExtra(
MainActivity.INTENT_EPISODE_NUMBER,
IntentService.INTENT_EPISODE_NUMBER,
it,
)
}
@ -363,27 +364,27 @@ class TvProviderWorker
setIntent(
Intent(context, MainActivity::class.java)
.putExtra(MainActivity.INTENT_ITEM_ID, item.id.toString())
.putExtra(MainActivity.INTENT_ITEM_TYPE, item.type.serialName)
.putExtra(IntentService.INTENT_ITEM_ID, item.id.toString())
.putExtra(IntentService.INTENT_ITEM_TYPE, item.type.serialName)
.apply {
if (item.type == BaseItemKind.EPISODE) {
putExtra(
MainActivity.INTENT_SERIES_ID,
IntentService.INTENT_SERIES_ID,
dto.seriesId?.toString(),
)
putExtra(
MainActivity.INTENT_SEASON_ID,
IntentService.INTENT_SEASON_ID,
dto.seasonId?.toString(),
)
dto.parentIndexNumber?.let {
putExtra(
MainActivity.INTENT_SEASON_NUMBER,
IntentService.INTENT_SEASON_NUMBER,
it,
)
}
dto.indexNumber?.let {
putExtra(
MainActivity.INTENT_EPISODE_NUMBER,
IntentService.INTENT_EPISODE_NUMBER,
it,
)
}

View file

@ -496,6 +496,7 @@ private const val COMBINED_ROW = TAB_ROW + 1
@Composable
fun SearchPage(
initialQuery: String,
userPreferences: UserPreferences,
modifier: Modifier = Modifier,
viewModel: SearchViewModel = hiltViewModel(),
@ -513,7 +514,7 @@ fun SearchPage(
val voiceSearchButtonVisible = prefs.showVoiceSearchButton
// val query = rememberTextFieldState()
var query by rememberSaveable { mutableStateOf("") }
var query by rememberSaveable { mutableStateOf(initialQuery) }
val focusRequesters = remember { List(SEERR_ROW + 1) { FocusRequester() } }
val seerrActive by viewModel.seerrActive.collectAsState(initial = false)

View file

@ -54,10 +54,11 @@ sealed class Destination(
) : Destination(true)
@Serializable
data object UserAppPreferences : Destination(true)
data class Search(
val query: String = "",
) : Destination()
@Serializable
data object Search : Destination()
data object UserAppPreferences : Destination(true)
@Serializable
data class SeriesOverview(

View file

@ -351,9 +351,10 @@ fun DestinationContent(
LicenseInfo(modifier)
}
Destination.Search -> {
is Destination.Search -> {
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
SearchPage(
initialQuery = destination.query,
userPreferences = preferences,
modifier = modifier,
)

View file

@ -409,7 +409,7 @@ fun NavDrawer(
interactionSource = interactionSource,
onClick = {
viewModel.setIndex(SEARCH_INDEX)
viewModel.navigationManager.navigateToFromDrawer(Destination.Search)
viewModel.navigationManager.navigateToFromDrawer(Destination.Search())
},
modifier =
Modifier