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:
parent
4f5cd3e7b1
commit
b1f9c32a19
10 changed files with 415 additions and 78 deletions
151
Intents.md
Normal file
151
Intents.md
Normal 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"
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()}), " +
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue