mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Add ability to order navigation drawer items (#886)
## Description Allows for reordering the items in the navigation drawer This does not change the home page row order since #803 decouples the nav drawer and home page rows. The initial order will be the "Library Order" settings on the web under Profile->Home. Making any changes locally in Wholphin will set the order. If any new libraries are added or if Seerr integration is enabled, these will added to the end of the "pinned" list. ### Related issues Closes #822 Related to #399 & #803 ### Testing Tested on emulator - Re-ordering - Granting/Revoking user permission to a new library ## Screenshots N/A, nav drawer is the same, just sorted differently ## AI or LLM usage None
This commit is contained in:
parent
fcba1c7444
commit
17b0eef5c5
9 changed files with 1214 additions and 163 deletions
|
|
@ -40,7 +40,7 @@ import java.util.UUID
|
|||
SeerrUser::class,
|
||||
|
||||
],
|
||||
version = 30,
|
||||
version = 31,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(3, 4),
|
||||
|
|
@ -54,6 +54,7 @@ import java.util.UUID
|
|||
AutoMigration(11, 12),
|
||||
AutoMigration(12, 20),
|
||||
AutoMigration(20, 30),
|
||||
AutoMigration(30, 31),
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
|
|
@ -24,4 +25,5 @@ data class NavDrawerPinnedItem(
|
|||
val userId: Int,
|
||||
val itemId: String,
|
||||
val type: NavPinType,
|
||||
@ColumnInfo(defaultValue = "-1") val order: Int,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.asFlow
|
||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
|
||||
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.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.UserDto
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NavDrawerService
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
@param:DefaultCoroutineScope private val coroutineScope: CoroutineScope,
|
||||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val serverPreferencesDao: ServerPreferencesDao,
|
||||
private val seerrServerRepository: SeerrServerRepository,
|
||||
) {
|
||||
private val _state = MutableStateFlow(NavDrawerItemState.EMPTY)
|
||||
val state: StateFlow<NavDrawerItemState> = _state
|
||||
|
||||
init {
|
||||
serverRepository.currentUser
|
||||
.asFlow()
|
||||
.combine(serverRepository.currentUserDto.asFlow()) { user, userDto ->
|
||||
Pair(user, userDto)
|
||||
}.onEach { (user, userDto) ->
|
||||
Timber.d("User updated: user=%s, userDto=%s", user?.id, userDto?.id)
|
||||
_state.update {
|
||||
it.copy(
|
||||
items = emptyList(),
|
||||
moreItems = emptyList(),
|
||||
)
|
||||
}
|
||||
if (user != null && userDto != null && user.id == userDto.id) {
|
||||
updateNavDrawer(user, userDto)
|
||||
}
|
||||
}.launchIn(coroutineScope)
|
||||
seerrServerRepository.active
|
||||
.onEach { discoverActive ->
|
||||
_state.update { it.copy(discoverEnabled = discoverActive) }
|
||||
}.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
suspend fun updateNavDrawer(
|
||||
user: JellyfinUser,
|
||||
userDto: UserDto,
|
||||
) {
|
||||
val tvAccess = userDto.policy?.enableLiveTvAccess ?: false
|
||||
val userViews =
|
||||
api.userViewsApi
|
||||
.getUserViews(userId = user.id)
|
||||
.content.items
|
||||
val recordingFolders =
|
||||
if (tvAccess) {
|
||||
api.liveTvApi
|
||||
.getRecordingFolders(userId = user.id)
|
||||
.content.items
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
} else {
|
||||
setOf()
|
||||
}
|
||||
|
||||
val builtins = listOf(NavDrawerItem.Favorites, NavDrawerItem.Discover)
|
||||
|
||||
val libraries =
|
||||
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 allItems = builtins + libraries
|
||||
|
||||
val navDrawerPins =
|
||||
serverPreferencesDao.getNavDrawerPinnedItems(user).associateBy { it.itemId }
|
||||
|
||||
val items = mutableListOf<NavDrawerItem>()
|
||||
val moreItems = mutableListOf<NavDrawerItem>()
|
||||
allItems
|
||||
// Sort by order if non-default, existing items before customize will have -1 value
|
||||
// New items from the server will get Int.MAX_VALUE
|
||||
// Items the user doesn't have access to anymore will be skipped
|
||||
.sortedBy { navDrawerPins[it.id]?.order?.takeIf { it >= 0 } ?: Int.MAX_VALUE }
|
||||
.forEach {
|
||||
// Assume pinned if unknown
|
||||
val pinned = navDrawerPins[it.id]?.type ?: NavPinType.PINNED
|
||||
if (pinned == NavPinType.PINNED) {
|
||||
items.add(it)
|
||||
} else {
|
||||
moreItems.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
items = items,
|
||||
moreItems = moreItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NavDrawerItemState(
|
||||
val items: List<NavDrawerItem>,
|
||||
val moreItems: List<NavDrawerItem>,
|
||||
val discoverEnabled: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = NavDrawerItemState(emptyList(), emptyList(), false)
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,10 @@ annotation class StandardOkHttpClient
|
|||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class IoCoroutineScope
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class DefaultCoroutineScope
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppModule {
|
||||
|
|
@ -177,6 +181,11 @@ object AppModule {
|
|||
@IoCoroutineScope
|
||||
fun ioCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@DefaultCoroutineScope
|
||||
fun defaultCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun workManager(
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
|||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -54,7 +54,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.findViewTreeViewModelStoreOwner
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.tv.material3.DrawerState
|
||||
import androidx.tv.material3.DrawerValue
|
||||
import androidx.tv.material3.Icon
|
||||
|
|
@ -72,6 +71,7 @@ import com.github.damontecres.wholphin.data.model.JellyfinUser
|
|||
import com.github.damontecres.wholphin.preferences.AppThemeColors
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.NavDrawerService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||
import com.github.damontecres.wholphin.services.SetupDestination
|
||||
|
|
@ -79,23 +79,16 @@ import com.github.damontecres.wholphin.services.SetupNavigationManager
|
|||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.components.TimeDisplay
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.setup.UserIconCardImage
|
||||
import com.github.damontecres.wholphin.ui.spacedByWithFooter
|
||||
import com.github.damontecres.wholphin.ui.theme.LocalTheme
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -104,80 +97,17 @@ class NavDrawerViewModel
|
|||
@Inject
|
||||
constructor(
|
||||
private val api: ApiClient,
|
||||
private val navDrawerService: NavDrawerService,
|
||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
val navigationManager: NavigationManager,
|
||||
val setupNavigationManager: SetupNavigationManager,
|
||||
val backdropService: BackdropService,
|
||||
private val seerrServerRepository: SeerrServerRepository,
|
||||
) : ViewModel() {
|
||||
val moreLibraries = MutableLiveData<List<NavDrawerItem>>(null)
|
||||
val libraries = MutableLiveData<List<NavDrawerItem>>(listOf())
|
||||
val state = navDrawerService.state
|
||||
|
||||
val selectedIndex = MutableLiveData(-1)
|
||||
val showMore = MutableLiveData(false)
|
||||
|
||||
init {
|
||||
seerrServerRepository.active
|
||||
.onEach {
|
||||
init()
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun init() {
|
||||
viewModelScope.launchIO {
|
||||
val all = navDrawerItemRepository.getNavDrawerItems()
|
||||
val libraries = navDrawerItemRepository.getFilteredNavDrawerItems(all)
|
||||
val moreLibraries = all.toMutableList().apply { removeAll(libraries) }
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
this@NavDrawerViewModel.moreLibraries.value = moreLibraries
|
||||
this@NavDrawerViewModel.libraries.value = libraries
|
||||
}
|
||||
val asDestinations =
|
||||
(
|
||||
libraries +
|
||||
listOf(
|
||||
NavDrawerItem.More,
|
||||
NavDrawerItem.Discover,
|
||||
) + moreLibraries
|
||||
).map {
|
||||
if (it is ServerNavDrawerItem) {
|
||||
it.destination
|
||||
} else if (it is NavDrawerItem.Favorites) {
|
||||
Destination.Favorites
|
||||
} else if (it is NavDrawerItem.Discover) {
|
||||
Destination.Discover
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val backstack = navigationManager.backStack.toList().reversed()
|
||||
for (i in 0..<backstack.size) {
|
||||
val key = backstack[i]
|
||||
if (key is Destination) {
|
||||
val index =
|
||||
if (key is Destination.Home) {
|
||||
-1
|
||||
} else if (key is Destination.Search) {
|
||||
-2
|
||||
} else {
|
||||
val idx = asDestinations.indexOf(key)
|
||||
if (idx >= 0) {
|
||||
idx
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
Timber.v("Found $index => $key")
|
||||
if (index != null) {
|
||||
selectedIndex.setValueOnMain(index)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val moreExpanded = MutableLiveData(false)
|
||||
|
||||
fun onClickDrawerItem(
|
||||
index: Int,
|
||||
|
|
@ -193,7 +123,7 @@ class NavDrawerViewModel
|
|||
}
|
||||
|
||||
NavDrawerItem.More -> {
|
||||
setShowMore(!showMore.value!!)
|
||||
setShowMore(!moreExpanded.value!!)
|
||||
}
|
||||
|
||||
NavDrawerItem.Discover -> {
|
||||
|
|
@ -215,7 +145,7 @@ class NavDrawerViewModel
|
|||
}
|
||||
|
||||
fun setShowMore(value: Boolean) {
|
||||
showMore.value = value
|
||||
moreExpanded.value = value
|
||||
}
|
||||
|
||||
fun getUserImage(user: JellyfinUser): String = api.imageApi.getUserImageUrl(user.id)
|
||||
|
|
@ -289,15 +219,12 @@ fun NavDrawer(
|
|||
drawerState.setValue(DrawerValue.Open)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
val moreLibraries by viewModel.moreLibraries.observeAsState(listOf())
|
||||
val libraries by viewModel.libraries.observeAsState(listOf())
|
||||
LaunchedEffect(Unit) { viewModel.init() }
|
||||
|
||||
val showMore by viewModel.showMore.observeAsState(false)
|
||||
// A negative index is a built in page, >=0 is a library
|
||||
val state by viewModel.state.collectAsState()
|
||||
val moreExpanded by viewModel.moreExpanded.observeAsState(false)
|
||||
// A negative index is a built-in page, >=0 is a library
|
||||
val selectedIndex by viewModel.selectedIndex.observeAsState(-1)
|
||||
|
||||
BackHandler(enabled = showMore && drawerState.currentValue == DrawerValue.Open) {
|
||||
BackHandler(enabled = moreExpanded && drawerState.currentValue == DrawerValue.Open) {
|
||||
viewModel.setShowMore(false)
|
||||
}
|
||||
|
||||
|
|
@ -412,51 +339,83 @@ fun NavDrawer(
|
|||
),
|
||||
)
|
||||
}
|
||||
itemsIndexed(libraries) { index, it ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
NavItem(
|
||||
library = it,
|
||||
selected = selectedIndex == index,
|
||||
moreExpanded = showMore,
|
||||
drawerOpen = isOpen,
|
||||
interactionSource = interactionSource,
|
||||
onClick = {
|
||||
viewModel.onClickDrawerItem(index, it)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
selectedIndex == index,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (showMore) {
|
||||
itemsIndexed(moreLibraries) { index, it ->
|
||||
val adjustedIndex = (index + libraries.size + 1)
|
||||
itemsIndexed(state.items) { index, it ->
|
||||
if (it !is NavDrawerItem.Discover || state.discoverEnabled) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
NavItem(
|
||||
library = it,
|
||||
selected = selectedIndex == adjustedIndex,
|
||||
moreExpanded = showMore,
|
||||
selected = selectedIndex == index,
|
||||
moreExpanded = moreExpanded,
|
||||
drawerOpen = isOpen,
|
||||
onClick = { viewModel.onClickDrawerItem(adjustedIndex, it) },
|
||||
containerColor =
|
||||
if (isOpen) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
||||
} else {
|
||||
Color.Unspecified
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
onClick = {
|
||||
viewModel.onClickDrawerItem(index, it)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
selectedIndex == adjustedIndex,
|
||||
selectedIndex == index,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.moreItems.isNotEmpty()) {
|
||||
item {
|
||||
val index = state.items.size
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
NavItem(
|
||||
library = NavDrawerItem.More,
|
||||
selected = selectedIndex == index,
|
||||
moreExpanded = moreExpanded,
|
||||
drawerOpen = isOpen,
|
||||
interactionSource = interactionSource,
|
||||
onClick = {
|
||||
viewModel.onClickDrawerItem(index, NavDrawerItem.More)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
selectedIndex == index,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (moreExpanded) {
|
||||
itemsIndexed(state.moreItems) { index, it ->
|
||||
val adjustedIndex =
|
||||
remember(state) { (index + state.items.size + 1) }
|
||||
if (it !is NavDrawerItem.Discover || state.discoverEnabled) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
NavItem(
|
||||
library = it,
|
||||
selected = selectedIndex == adjustedIndex,
|
||||
moreExpanded = moreExpanded,
|
||||
drawerOpen = isOpen,
|
||||
onClick = {
|
||||
viewModel.onClickDrawerItem(
|
||||
adjustedIndex,
|
||||
it,
|
||||
)
|
||||
},
|
||||
containerColor =
|
||||
if (isOpen) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
||||
} else {
|
||||
Color.Unspecified
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
selectedIndex == adjustedIndex,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
IconNavItem(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
package com.github.damontecres.wholphin.ui.preferences
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.ListItem
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Switch
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.Button
|
||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
|
||||
data class NavDrawerPin(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val pinned: Boolean,
|
||||
val item: NavDrawerItem,
|
||||
) {
|
||||
companion object {
|
||||
fun create(
|
||||
context: Context,
|
||||
items: Map<NavDrawerItem, Boolean>,
|
||||
) {
|
||||
items.map { (item, pinned) ->
|
||||
NavDrawerPin(item.id, item.name(context), pinned, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class MoveDirection {
|
||||
UP,
|
||||
DOWN,
|
||||
}
|
||||
|
||||
private fun <T> List<T>.move(
|
||||
direction: MoveDirection,
|
||||
index: Int,
|
||||
): List<T> =
|
||||
toMutableList().apply {
|
||||
if (direction == MoveDirection.DOWN) {
|
||||
val down = this[index]
|
||||
val up = this[index + 1]
|
||||
set(index, up)
|
||||
set(index + 1, down)
|
||||
} else {
|
||||
val up = this[index]
|
||||
val down = this[index - 1]
|
||||
set(index - 1, up)
|
||||
set(index, down)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavDrawerPreference(
|
||||
title: String,
|
||||
summary: String?,
|
||||
items: List<NavDrawerPin>,
|
||||
onSave: (List<NavDrawerPin>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
ClickPreference(
|
||||
title = title,
|
||||
summary = summary,
|
||||
onClick = { showDialog = true },
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
)
|
||||
if (showDialog) {
|
||||
NavDrawerPreferenceDialog(
|
||||
items = items,
|
||||
onDismissRequest = { showDialog = false },
|
||||
onClick = { index ->
|
||||
val newItems =
|
||||
items.toMutableList().apply {
|
||||
set(index, items[index].let { it.copy(pinned = !it.pinned) })
|
||||
}
|
||||
onSave.invoke(newItems)
|
||||
},
|
||||
onMoveUp = { index ->
|
||||
onSave(items.move(MoveDirection.UP, index))
|
||||
},
|
||||
onMoveDown = { index ->
|
||||
onSave(items.move(MoveDirection.DOWN, index))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavDrawerPreferenceDialog(
|
||||
items: List<NavDrawerPin>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onClick: (Int) -> Unit,
|
||||
onMoveUp: (Int) -> Unit,
|
||||
onMoveDown: (Int) -> Unit,
|
||||
) {
|
||||
BasicDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.nav_drawer_pins),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
) {
|
||||
itemsIndexed(items, key = { _, item -> item.id }) { index, item ->
|
||||
NavDrawerPreferenceListItem(
|
||||
title = item.title,
|
||||
pinned = item.pinned,
|
||||
moveUpAllowed = index > 0,
|
||||
moveDownAllowed = index < items.lastIndex,
|
||||
onClick = { onClick.invoke(index) },
|
||||
onMoveUp = { onMoveUp.invoke(index) },
|
||||
onMoveDown = { onMoveDown.invoke(index) },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavDrawerPreferenceListItem(
|
||||
title: String,
|
||||
pinned: Boolean,
|
||||
moveUpAllowed: Boolean,
|
||||
moveDownAllowed: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onMoveUp: () -> Unit,
|
||||
onMoveDown: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 40.dp, max = 88.dp),
|
||||
) {
|
||||
ListItem(
|
||||
selected = false,
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = title,
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = pinned,
|
||||
onCheckedChange = {
|
||||
onClick.invoke()
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = onClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
) {
|
||||
MoveButton(R.string.fa_caret_up, moveUpAllowed, onMoveUp)
|
||||
MoveButton(R.string.fa_caret_down, moveDownAllowed, onMoveDown)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MoveButton(
|
||||
@StringRes icon: Int,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) = Button(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = Modifier.size(32.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(icon),
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontAwesome,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewTvSpec
|
||||
@Composable
|
||||
fun NavDrawerPreferenceListItemPreview() {
|
||||
WholphinTheme {
|
||||
NavDrawerPreferenceListItem(
|
||||
title = "Movies",
|
||||
pinned = true,
|
||||
moveUpAllowed = true,
|
||||
moveDownAllowed = true,
|
||||
onClick = {},
|
||||
onMoveUp = {},
|
||||
onMoveDown = { },
|
||||
modifier = Modifier.width(360.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ fun PreferencesContent(
|
|||
val currentServer by seerrVm.currentSeerrServer.collectAsState(null)
|
||||
var showPinFlow by remember { mutableStateOf(false) }
|
||||
|
||||
val navDrawerPins by viewModel.navDrawerPins.observeAsState(mapOf())
|
||||
val navDrawerPins by viewModel.navDrawerPins.collectAsState(emptyList())
|
||||
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
|
||||
val seerrIntegrationEnabled by viewModel.seerrEnabled.collectAsState(false)
|
||||
var seerrDialogMode by remember { mutableStateOf<SeerrDialogMode>(SeerrDialogMode.None) }
|
||||
|
|
@ -335,21 +335,16 @@ fun PreferencesContent(
|
|||
}
|
||||
|
||||
AppPreference.UserPinnedNavDrawerItems -> {
|
||||
val selectedItems =
|
||||
navDrawerPins.keys.mapNotNull {
|
||||
if (navDrawerPins[it] ?: false) it else null
|
||||
}
|
||||
MultiChoicePreference(
|
||||
NavDrawerPreference(
|
||||
title = stringResource(pref.title),
|
||||
summary = pref.summary(context, null),
|
||||
possibleValues = navDrawerPins.keys,
|
||||
selectedValues = selectedItems.toSet(),
|
||||
onValueChange = { newSelectedItems ->
|
||||
viewModel.updatePins(newSelectedItems)
|
||||
items = navDrawerPins,
|
||||
onSave = {
|
||||
viewModel.updatePins(it)
|
||||
},
|
||||
) {
|
||||
Text(it.name(context))
|
||||
}
|
||||
modifier = Modifier,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.SendAppLogs -> {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@ package com.github.damontecres.wholphin.ui.preferences
|
|||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.isPinned
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
||||
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||
|
|
@ -17,23 +15,28 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
|
|||
import com.github.damontecres.wholphin.preferences.resetSubtitles
|
||||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.NavDrawerService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import com.github.damontecres.wholphin.util.RememberTabManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.ClientInfo
|
||||
import org.jellyfin.sdk.model.DeviceInfo
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -48,6 +51,7 @@ class PreferencesViewModel
|
|||
private val rememberTabManager: RememberTabManager,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
private val navDrawerService: NavDrawerService,
|
||||
private val serverPreferencesDao: ServerPreferencesDao,
|
||||
private val seerrServerRepository: SeerrServerRepository,
|
||||
private val deviceInfo: DeviceInfo,
|
||||
|
|
@ -55,7 +59,46 @@ class PreferencesViewModel
|
|||
) : ViewModel(),
|
||||
RememberTabManager by rememberTabManager {
|
||||
private lateinit var allNavDrawerItems: List<NavDrawerItem>
|
||||
val navDrawerPins = MutableLiveData<Map<NavDrawerItem, Boolean>>(mapOf())
|
||||
// val navDrawerPins = MutableLiveData<List<NavDrawerPin>>(emptyList())
|
||||
|
||||
val navDrawerPins =
|
||||
navDrawerService.state
|
||||
.combine(
|
||||
serverRepository.currentUser.asFlow(),
|
||||
) { state, user ->
|
||||
Pair(state, user)
|
||||
}.combine(seerrServerRepository.active) { (state, user), seerr ->
|
||||
Triple(state, user, seerr)
|
||||
}.map { (state, user, seerr) ->
|
||||
withContext(Dispatchers.IO) {
|
||||
val navDrawerPins =
|
||||
serverPreferencesDao
|
||||
.getNavDrawerPinnedItems(user!!)
|
||||
.associateBy { it.itemId }
|
||||
|
||||
val allItems = state.let { it.items + it.moreItems }
|
||||
val pins =
|
||||
allItems
|
||||
.sortedBy {
|
||||
navDrawerPins[it.id]?.order?.takeIf { it >= 0 } ?: Int.MAX_VALUE
|
||||
}.mapNotNull {
|
||||
if (!seerr && it is NavDrawerItem.Discover) {
|
||||
null
|
||||
} else {
|
||||
// Assume pinned if unknown
|
||||
val pinned = navDrawerPins[it.id]?.type ?: NavPinType.PINNED
|
||||
NavDrawerPin(
|
||||
it.id,
|
||||
it.name(context),
|
||||
pinned == NavPinType.PINNED,
|
||||
it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pins
|
||||
}
|
||||
}
|
||||
|
||||
val currentUser get() = serverRepository.currentUser
|
||||
|
||||
|
|
@ -70,42 +113,50 @@ class PreferencesViewModel
|
|||
init {
|
||||
viewModelScope.launchIO {
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
allNavDrawerItems = navDrawerItemRepository.getNavDrawerItems()
|
||||
val pins = serverPreferencesDao.getNavDrawerPinnedItems(user)
|
||||
val navDrawerPins = allNavDrawerItems.associateWith { pins.isPinned(it.id) }
|
||||
this@PreferencesViewModel.navDrawerPins.setValueOnMain(navDrawerPins)
|
||||
// fetchNavDrawerPins(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePins(newSelectedItems: List<NavDrawerItem>) {
|
||||
private suspend fun fetchNavDrawerPins(user: JellyfinUser) {
|
||||
navDrawerService.state.map {
|
||||
val navDrawerPins =
|
||||
serverPreferencesDao.getNavDrawerPinnedItems(user).associateBy { it.itemId }
|
||||
|
||||
val allItems = navDrawerService.state.first().let { it.items + it.moreItems }
|
||||
val pins =
|
||||
allItems
|
||||
.sortedBy { navDrawerPins[it.id]?.order?.takeIf { it >= 0 } ?: Int.MAX_VALUE }
|
||||
.map {
|
||||
// Assume pinned if unknown
|
||||
val pinned = navDrawerPins[it.id]?.type ?: NavPinType.PINNED
|
||||
NavDrawerPin(it.id, it.name(context), pinned == NavPinType.PINNED, it)
|
||||
}
|
||||
pins
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePins(items: List<NavDrawerPin>) {
|
||||
viewModelScope.launchIO(ExceptionHandler(true)) {
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
val disabledItems =
|
||||
mutableListOf<NavDrawerItem>().apply {
|
||||
addAll(allNavDrawerItems)
|
||||
removeAll(newSelectedItems)
|
||||
serverRepository.currentUserDto.value?.let { userDto ->
|
||||
if (user.id == userDto.id) {
|
||||
Timber.v("Updating pins")
|
||||
val toSave =
|
||||
items.mapIndexed { index, item ->
|
||||
NavDrawerPinnedItem(
|
||||
user.rowId,
|
||||
item.id,
|
||||
if (item.pinned) NavPinType.PINNED else NavPinType.UNPINNED,
|
||||
index,
|
||||
)
|
||||
}
|
||||
serverPreferencesDao.saveNavDrawerPinnedItems(*toSave.toTypedArray())
|
||||
navDrawerService.updateNavDrawer(user, userDto)
|
||||
} else {
|
||||
throw IllegalStateException("User IDs do not match")
|
||||
}
|
||||
val enabledItems = newSelectedItems.toSet()
|
||||
val toSave =
|
||||
disabledItems.map {
|
||||
NavDrawerPinnedItem(
|
||||
user.rowId,
|
||||
it.id,
|
||||
NavPinType.UNPINNED,
|
||||
)
|
||||
} +
|
||||
enabledItems.map {
|
||||
NavDrawerPinnedItem(
|
||||
user.rowId,
|
||||
it.id,
|
||||
NavPinType.PINNED,
|
||||
)
|
||||
}
|
||||
serverPreferencesDao.saveNavDrawerPinnedItems(*toSave.toTypedArray())
|
||||
val pins = serverPreferencesDao.getNavDrawerPinnedItems(user)
|
||||
val navDrawerPins = allNavDrawerItems.associateWith { pins.isPinned(it.id) }
|
||||
this@PreferencesViewModel.navDrawerPins.setValueOnMain(navDrawerPins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue