mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Customize display of library grid pages (#368)
## Details Adds options to customize how grid pages are displayed. Every grid page can be individually customized. The options are saved per user on the device and restored when navigating back to the page. The pages that support it have a new button next to the sort/filter buttons and changes are updated live. The options are: * Primary or Thumb image * Aspect ratio * Show details header w/ backdrop (similar to the one on the home page) * Number of columns * Spacing between cards * Card image content scale (eg Fit, Fill, Crop, etc) Note: the Genre tabs' grids cannot be customized yet ### Other changes - Nav drawer now opens over top of content instead of pushing it - Images for most cards are fetched with actual fill size ### Possible future work * Customize Genre grids * Add more image types, banners? * Add similar customization options to pages with rows, eg Home page and Recommended tabs ## Example For example, this is a movie library using thumb images with 8 columns and reduced spacing. It also shows the details header ## Issues Closes #186 Related to #72
This commit is contained in:
parent
b98fb4a1d8
commit
384401a72c
47 changed files with 1658 additions and 628 deletions
|
|
@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.data.model.JellyfinServer
|
|||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptions
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
|
|
@ -22,7 +23,7 @@ import java.util.UUID
|
|||
|
||||
@Database(
|
||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
|
||||
version = 8,
|
||||
version = 9,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(3, 4),
|
||||
|
|
@ -30,6 +31,7 @@ import java.util.UUID
|
|||
AutoMigration(5, 6),
|
||||
AutoMigration(6, 7),
|
||||
AutoMigration(7, 8),
|
||||
AutoMigration(8, 9),
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
@ -73,6 +75,18 @@ class Converters {
|
|||
Timber.e(ex, "Error parsing filter")
|
||||
GetItemsFilter()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun convertViewOptions(viewOptions: ViewOptions?): String? = viewOptions?.let { Json.encodeToString(viewOptions) }
|
||||
|
||||
@TypeConverter
|
||||
fun convertViewOptions(viewOptions: String?): ViewOptions? =
|
||||
try {
|
||||
viewOptions?.let { Json.decodeFromString(viewOptions) }
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error parsing view options")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
object Migrations {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ sealed interface ExtrasItem {
|
|||
val type: ExtraType
|
||||
val destination: Destination
|
||||
val title: String?
|
||||
val imageUrl: String?
|
||||
|
||||
data class Group(
|
||||
override val parentId: UUID,
|
||||
|
|
@ -24,7 +23,6 @@ sealed interface ExtrasItem {
|
|||
Destination.ItemGrid(null, type.stringRes, items.map { it.id })
|
||||
|
||||
override val title: String? = null
|
||||
override val imageUrl: String? = items.random().imageUrl
|
||||
}
|
||||
|
||||
data class Single(
|
||||
|
|
@ -37,7 +35,6 @@ sealed interface ExtrasItem {
|
|||
item = item,
|
||||
)
|
||||
override val title: String? get() = item.title
|
||||
override val imageUrl: String? get() = item.imageUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,20 +8,15 @@ import com.github.damontecres.wholphin.ui.seasonEpisodePadded
|
|||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Serializable
|
||||
data class BaseItem(
|
||||
val data: BaseItemDto,
|
||||
val imageUrl: String?,
|
||||
val backdropImageUrl: String? = null,
|
||||
val logoImageUrl: String? = null,
|
||||
val useSeriesForPrimary: Boolean,
|
||||
) {
|
||||
val id get() = data.id
|
||||
|
||||
|
|
@ -95,72 +90,15 @@ data class BaseItem(
|
|||
}
|
||||
|
||||
companion object {
|
||||
var primaryMaxWidth: Int? = null
|
||||
set(value) {
|
||||
Timber.v("primaryMaxWidth=$value")
|
||||
field = value
|
||||
}
|
||||
var primaryMaxHeight: Int? = null
|
||||
set(value) {
|
||||
Timber.v("primaryMaxHeight=$value")
|
||||
field = value
|
||||
}
|
||||
|
||||
fun from(
|
||||
dto: BaseItemDto,
|
||||
api: ApiClient,
|
||||
useSeriesForPrimary: Boolean = false,
|
||||
): BaseItem {
|
||||
val backdropImageUrl =
|
||||
if (dto.type == BaseItemKind.EPISODE) {
|
||||
val seriesId = dto.seriesId
|
||||
if (seriesId != null) {
|
||||
api.imageApi.getItemImageUrl(seriesId, ImageType.BACKDROP)
|
||||
} else {
|
||||
api.imageApi.getItemImageUrl(dto.id, ImageType.BACKDROP)
|
||||
}
|
||||
} else {
|
||||
api.imageApi.getItemImageUrl(dto.id, ImageType.BACKDROP)
|
||||
}
|
||||
val primaryImageUrl =
|
||||
if (useSeriesForPrimary && dto.type == BaseItemKind.EPISODE && dto.seriesId != null) {
|
||||
api.imageApi.getItemImageUrl(
|
||||
dto.seriesId!!,
|
||||
ImageType.PRIMARY,
|
||||
maxHeight = primaryMaxHeight,
|
||||
maxWidth = primaryMaxWidth,
|
||||
quality = 96,
|
||||
)
|
||||
} else if (dto.imageTags == null || dto.imageTags!![ImageType.PRIMARY] == null) {
|
||||
// TODO is this a bad assumption?
|
||||
null
|
||||
} else {
|
||||
api.imageApi.getItemImageUrl(
|
||||
dto.id,
|
||||
ImageType.PRIMARY,
|
||||
maxHeight = primaryMaxHeight,
|
||||
maxWidth = primaryMaxWidth,
|
||||
quality = 96,
|
||||
)
|
||||
}
|
||||
val logoImageUrl =
|
||||
if (dto.type == BaseItemKind.EPISODE || dto.type == BaseItemKind.SEASON) {
|
||||
val seriesId = dto.seriesId
|
||||
if (seriesId != null) {
|
||||
api.imageApi.getItemImageUrl(seriesId, ImageType.LOGO)
|
||||
} else {
|
||||
api.imageApi.getItemImageUrl(dto.id, ImageType.LOGO)
|
||||
}
|
||||
} else {
|
||||
api.imageApi.getItemImageUrl(dto.id, ImageType.LOGO)
|
||||
}
|
||||
return BaseItem(
|
||||
): BaseItem =
|
||||
BaseItem(
|
||||
dto,
|
||||
primaryImageUrl,
|
||||
backdropImageUrl,
|
||||
logoImageUrl,
|
||||
useSeriesForPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.room.Entity
|
|||
import androidx.room.ForeignKey
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptions
|
||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
|
|
@ -36,6 +37,7 @@ data class LibraryDisplayInfo(
|
|||
val direction: SortOrder,
|
||||
@ColumnInfo(defaultValue = "{}")
|
||||
val filter: GetItemsFilter,
|
||||
val viewOptions: ViewOptions?,
|
||||
) {
|
||||
@Ignore @Transient
|
||||
val sortAndDirection = SortAndDirection(sort, direction)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue