Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
400218a
Add WholphinPluginService for Jellyfin plugin API interaction
kamilkosek Jan 2, 2026
dfbfc62
Implement Wholphin plugin background login support in SwitchUserViewM…
kamilkosek Jan 2, 2026
9696b81
Add home configuration endpoint and data classes to WholphinPluginSer…
kamilkosek Jan 2, 2026
7c08275
Implement plugin-driven home configuration loading in HomeViewModel
kamilkosek Jan 2, 2026
76a1fb8
Add plugin settings retrieval to WholphinPluginService
kamilkosek Jan 3, 2026
a063c0a
Refactor filter parsing in HomeViewModel to use serialName lookup for…
kamilkosek Jan 3, 2026
052b9fe
Merge branch 'main' into develop/server-plugin-support
kamilkosek Jan 5, 2026
c92b8e3
Merge branch 'main' into develop/server-plugin-support
kamilkosek Jan 7, 2026
81219cc
Add navigation drawer configuration support to WholphinPluginService
kamilkosek Jan 7, 2026
1fd6ecc
Enhance navigation drawer with plugin support for custom items and icons
kamilkosek Jan 7, 2026
9195c4a
Add RecommendedCollection and CollectionFolderCollection components
kamilkosek Jan 7, 2026
b1c4dde
Merge branch 'main' into develop/server-plugin-support
kamilkosek Jan 9, 2026
6008f3b
Refactor plugin capabilities checks in SwitchServerViewModel and Swit…
kamilkosek Jan 15, 2026
26da6da
Merge remote-tracking branch 'damontecres/main' into develop/server-p…
kamilkosek Jan 15, 2026
7bc2c58
Add focus requesters for tab navigation and update GenreCardGrid item…
kamilkosek Jan 15, 2026
219b9ed
Add plugin Seerr URL handling in Preferences and related components t…
kamilkosek Jan 15, 2026
b9d4ece
cleanup unused
kamilkosek Jan 16, 2026
6bd1187
Load sections in parallel and update ui individually
kamilkosek Jan 16, 2026
3b42417
add advanced boxset view mode
kamilkosek Jan 16, 2026
c275e4c
enable seerr network (streaming provider) discovery based on boxset´s
kamilkosek Jan 17, 2026
4e248aa
Merge branch 'damontecres:main' into develop/server-plugin-support
kamilkosek Jan 17, 2026
14c7887
Merge branch 'damontecres:main' into develop/server-plugin-support
kamilkosek Jan 19, 2026
627af2e
Merge branch 'damontecres:main' into develop/server-plugin-support
kamilkosek Jan 21, 2026
7ed23e2
Update default box set view mode preference to advanced view
kamilkosek Jan 21, 2026
7ba263a
client side filtering recommendations, to only show items included in…
kamilkosek Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,26 @@ import android.content.Context
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
import com.github.damontecres.wholphin.data.model.NavPinType
import com.github.damontecres.wholphin.services.WholphinPluginService
import com.github.damontecres.wholphin.services.NavDrawerItemType
import com.github.damontecres.wholphin.services.SeerrServerRepository
import com.github.damontecres.wholphin.ui.nav.CustomNavDrawerItem
import com.github.damontecres.wholphin.ui.nav.CustomNavDrawerItemType
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
import com.github.damontecres.wholphin.util.supportedCollectionTypes
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.first
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.liveTvApi
import org.jellyfin.sdk.api.client.extensions.userViewsApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.serializer.toUUID
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -26,9 +35,74 @@ class NavDrawerItemRepository
private val api: ApiClient,
private val serverRepository: ServerRepository,
private val serverPreferencesDao: ServerPreferencesDao,
private val pluginService: WholphinPluginService,
private val seerrServerRepository: SeerrServerRepository,
) {
suspend fun getNavDrawerItems(): List<NavDrawerItem> {
val user = serverRepository.currentUser.value
val server = serverRepository.currentServer.value

// Fetch all available Jellyfin libraries
val jellyfinLibraries = fetchJellyfinLibraries()

// Try to fetch plugin configuration
val pluginConfig = server?.url?.let { serverUrl ->
try {
pluginService.getNavDrawerConfiguration(serverUrl)
} catch (e: Exception) {
Timber.w(e, "Error fetching nav drawer configuration")
null
}
}

val builtins =
if (seerrServerRepository.active.first()) {
listOf(NavDrawerItem.Favorites, NavDrawerItem.Discover)
} else {
listOf(NavDrawerItem.Favorites)
}

// If plugin config is available and not empty, use it to control middle section
return if (pluginConfig != null && pluginConfig.items.isNotEmpty()) {
val visibleItems = applyPluginConfiguration(jellyfinLibraries, pluginConfig, visibleOnly = true)
builtins + visibleItems
} else {
// Fallback to existing behavior (plugin not configured or empty config)
builtins + jellyfinLibraries
}
}

/**
* Get items that should be hidden behind the "More" button
* Returns hidden items when plugin config is active, or unpinned items when using local pins
*/
suspend fun getHiddenNavDrawerItems(): List<NavDrawerItem> {
val server = serverRepository.currentServer.value

// Try to fetch plugin configuration
val pluginConfig = server?.url?.let { serverUrl ->
try {
pluginService.getNavDrawerConfiguration(serverUrl)
} catch (e: Exception) {
null
}
}

// If plugin config is active and not empty, return hidden items
if (pluginConfig != null && pluginConfig.items.isNotEmpty()) {
val jellyfinLibraries = fetchJellyfinLibraries()
return applyPluginConfiguration(jellyfinLibraries, pluginConfig, visibleOnly = false)
}

// Fallback: use pin-based logic (plugin not configured or empty config)
val allItems = getNavDrawerItems()
return getFilteredNavDrawerItems(allItems)
}

/**
* Fetch all available Jellyfin libraries and recordings
*/
private suspend fun fetchJellyfinLibraries(): List<ServerNavDrawerItem> {
val user = serverRepository.currentUser.value
val tvAccess =
serverRepository.currentUserDto.value
Expand All @@ -49,31 +123,132 @@ class NavDrawerItemRepository
setOf()
}

val builtins =
if (seerrServerRepository.active.first()) {
listOf(NavDrawerItem.Favorites, NavDrawerItem.Discover)
} else {
listOf(NavDrawerItem.Favorites)
return userViews
.filter { it.collectionType in supportedCollectionTypes || it.id in recordingFolders }
.map {
val destination =
if (it.id in recordingFolders) {
Destination.Recordings(it.id)
} else {
BaseItem.from(it, api).destination()
}
ServerNavDrawerItem(
itemId = it.id,
name = it.name ?: it.id.toString(),
destination = destination,
type = it.collectionType ?: CollectionType.UNKNOWN,
)
}
}

val libraries =
userViews
.filter { it.collectionType in supportedCollectionTypes || it.id in recordingFolders }
.map {
val destination =
if (it.id in recordingFolders) {
Destination.Recordings(it.id)
/**
* Apply plugin configuration to control nav drawer items
*
* Merges Jellyfin libraries with plugin configuration, creating custom items
* for collections/playlists and respecting the order and visibility defined by the plugin.
*
* @param visibleOnly If true, returns only visible items; if false, returns only hidden items
*/
private suspend fun applyPluginConfiguration(
jellyfinLibraries: List<ServerNavDrawerItem>,
pluginConfig: com.github.damontecres.wholphin.services.NavDrawerConfiguration,
visibleOnly: Boolean,
): List<NavDrawerItem> {
val user = serverRepository.currentUser.value

// Map Jellyfin libraries by ID for quick lookup
val librariesById = jellyfinLibraries.associateBy { it.itemId }

// Process plugin configuration items
val allItems = pluginConfig.items
.sortedBy { it.order }
.mapNotNull { config ->
val itemId = try {
config.id.toUUID()
} catch (e: Exception) {
Timber.w("Invalid UUID in nav drawer config: ${config.id}")
return@mapNotNull null
}

val item = when (config.type) {
NavDrawerItemType.LIBRARY -> {
// Use existing library item or create a basic one if not found
val existing = librariesById[itemId]
if (existing != null) {
// Override name if provided in config
if (config.name != null) {
existing.copy(name = config.name)
} else {
existing
}
} else {
BaseItem.from(it, api).destination()
// Library not found in Jellyfin, skip it
Timber.w("Library $itemId from plugin config not found in Jellyfin")
null
}
ServerNavDrawerItem(
itemId = it.id,
name = it.name ?: it.id.toString(),
destination = destination,
type = it.collectionType ?: CollectionType.UNKNOWN,
)
}

NavDrawerItemType.COLLECTION, NavDrawerItemType.PLAYLIST -> {
// Fetch the collection/playlist item from Jellyfin
val item = fetchItemById(itemId, user?.id)
if (item != null) {
val destination = BaseItem.from(item, api).destination()
val displayName = config.name ?: item.name ?: itemId.toString()
val imageUrl = config.imageUrl

CustomNavDrawerItem(
itemId = itemId,
itemName = displayName,
destination = destination,
itemType = when (config.type) {
NavDrawerItemType.COLLECTION -> CustomNavDrawerItemType.COLLECTION
NavDrawerItemType.PLAYLIST -> CustomNavDrawerItemType.PLAYLIST
else -> CustomNavDrawerItemType.COLLECTION
},
imageUrl = imageUrl,
)
} else {
Timber.w("Collection/Playlist $itemId from plugin config not found in Jellyfin")
null
}
}
}
return builtins + libraries

// Return item with its visibility flag
item?.let { Pair(it, config.visible) }
}

// Filter by visibility
val filteredItems = allItems
.filter { (_, visible) -> visible == visibleOnly }
.map { (item, _) -> item }

// Add "More" button to visible items if there are hidden items
return if (visibleOnly) {
val hasHiddenItems = allItems.any { (_, visible) -> !visible }
if (hasHiddenItems) {
filteredItems + listOf(NavDrawerItem.More)
} else {
filteredItems
}
} else {
filteredItems
}
}

/**
* Fetch a single item (collection or playlist) by ID
*/
private suspend fun fetchItemById(itemId: UUID, userId: UUID?): BaseItemDto? {
return try {
api.itemsApi.getItems(
userId = userId,
ids = listOf(itemId),
).content.items.firstOrNull()
} catch (e: Exception) {
Timber.w(e, "Error fetching item $itemId")
null
}
}

suspend fun getFilteredNavDrawerItems(items: List<NavDrawerItem>): List<NavDrawerItem> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,19 @@ sealed interface AppPreference<Pref, T> {
valueToIndex = { if (it != AppThemeColors.UNRECOGNIZED) it.number else 0 },
)

val BoxSetViewModePref =
AppChoicePreference<AppPreferences, BoxSetViewMode>(
title = R.string.boxset_view_mode,
defaultValue = BoxSetViewMode.ADVANCED_VIEW,
getter = { it.interfacePreferences.boxsetViewMode },
setter = { prefs, value ->
prefs.updateInterfacePreferences { boxsetViewMode = value }
},
displayValues = R.array.boxset_view_modes,
indexToValue = { BoxSetViewMode.forNumber(it) },
valueToIndex = { if (it != BoxSetViewMode.UNRECOGNIZED) it.number else 0 },
)

val InstalledVersion =
AppClickablePreference<AppPreferences>(
title = R.string.installed_version,
Expand Down Expand Up @@ -907,6 +920,7 @@ val basicPreferences =
AppPreference.RememberSelectedTab,
AppPreference.SubtitleStyle,
AppPreference.ThemeColors,
AppPreference.BoxSetViewModePref,
),
),
PreferenceGroup(
Expand Down
Loading