mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +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
|
|
@ -0,0 +1,320 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 9,
|
||||
"identityHash": "4255556f160da7bb269b114400da58da",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "servers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "users",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "rowId",
|
||||
"columnName": "rowId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "serverId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"rowId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_users_id_serverId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"serverId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_users_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_users_serverId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"serverId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "servers",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serverId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "ItemPlayback",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "rowId",
|
||||
"columnName": "rowId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "sourceId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "audioIndex",
|
||||
"columnName": "audioIndex",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subtitleIndex",
|
||||
"columnName": "subtitleIndex",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"rowId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ItemPlayback_userId_itemId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "NavDrawerPinnedItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
]
|
||||
},
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LibraryDisplayInfo",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sort",
|
||||
"columnName": "sort",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "direction",
|
||||
"columnName": "direction",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filter",
|
||||
"columnName": "filter",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'{}'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "viewOptions",
|
||||
"columnName": "viewOptions",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_LibraryDisplayInfo_userId_itemId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4255556f160da7bb269b114400da58da')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -19,7 +20,6 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -28,19 +28,20 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Surface
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
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.preferences.DefaultUserConfiguration
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.AppUpgradeHandler
|
||||
import com.github.damontecres.wholphin.services.DeviceProfileService
|
||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver
|
||||
import com.github.damontecres.wholphin.services.ServerEventListener
|
||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||
import com.github.damontecres.wholphin.ui.CoilConfig
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
|
|
@ -84,6 +85,9 @@ class MainActivity : AppCompatActivity() {
|
|||
@Inject
|
||||
lateinit var deviceProfileService: DeviceProfileService
|
||||
|
||||
@Inject
|
||||
lateinit var imageUrlService: ImageUrlService
|
||||
|
||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -93,16 +97,6 @@ class MainActivity : AppCompatActivity() {
|
|||
appUpgradeHandler.copySubfont(false)
|
||||
}
|
||||
setContent {
|
||||
val density = LocalDensity.current
|
||||
LaunchedEffect(density) {
|
||||
with(density) {
|
||||
// Cards are never taller than 200 (most are around 120)
|
||||
BaseItem.primaryMaxHeight = 200.dp.roundToPx()
|
||||
// This width covers up to 2.35:1 aspect ratio images
|
||||
BaseItem.primaryMaxWidth = 480.dp.roundToPx()
|
||||
}
|
||||
}
|
||||
|
||||
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
|
||||
appPreferences?.let { appPreferences ->
|
||||
CoilConfig(
|
||||
|
|
@ -121,81 +115,83 @@ class MainActivity : AppCompatActivity() {
|
|||
LaunchedEffect(appPreferences.debugLogging) {
|
||||
DebugLogTree.INSTANCE.enabled = appPreferences.debugLogging
|
||||
}
|
||||
WholphinTheme(
|
||||
true,
|
||||
appThemeColors = appPreferences.interfacePreferences.appThemeColors,
|
||||
) {
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
shape = RectangleShape,
|
||||
CompositionLocalProvider(LocalImageUrlService provides imageUrlService) {
|
||||
WholphinTheme(
|
||||
true,
|
||||
appThemeColors = appPreferences.interfacePreferences.appThemeColors,
|
||||
) {
|
||||
var isRestoringSession by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
serverRepository.restoreSession(
|
||||
appPreferences.currentServerId?.toUUIDOrNull(),
|
||||
appPreferences.currentUserId?.toUUIDOrNull(),
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception restoring session")
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
shape = RectangleShape,
|
||||
) {
|
||||
var isRestoringSession by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
serverRepository.restoreSession(
|
||||
appPreferences.currentServerId?.toUUIDOrNull(),
|
||||
appPreferences.currentUserId?.toUUIDOrNull(),
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception restoring session")
|
||||
}
|
||||
isRestoringSession = false
|
||||
}
|
||||
isRestoringSession = false
|
||||
}
|
||||
val current by serverRepository.current.observeAsState()
|
||||
val current by serverRepository.current.observeAsState()
|
||||
|
||||
val preferences =
|
||||
UserPreferences(
|
||||
appPreferences,
|
||||
current?.userDto?.configuration ?: DefaultUserConfiguration,
|
||||
)
|
||||
|
||||
if (isRestoringSession) {
|
||||
Box(
|
||||
modifier = Modifier.size(200.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.border,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
val preferences =
|
||||
UserPreferences(
|
||||
appPreferences,
|
||||
current?.userDto?.configuration ?: DefaultUserConfiguration,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
key(current) {
|
||||
val initialDestination =
|
||||
if (current != null) {
|
||||
Destination.Home()
|
||||
} else {
|
||||
Destination.ServerList
|
||||
}
|
||||
val backStack = rememberNavBackStack(initialDestination)
|
||||
navigationManager.backStack = backStack
|
||||
if (UpdateChecker.ACTIVE && appPreferences.autoCheckForUpdates) {
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
updateChecker.maybeShowUpdateToast(appPreferences.updateUrl)
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Failed to check for update")
|
||||
|
||||
if (isRestoringSession) {
|
||||
Box(
|
||||
modifier = Modifier.size(200.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.border,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
key(current) {
|
||||
val initialDestination =
|
||||
if (current != null) {
|
||||
Destination.Home()
|
||||
} else {
|
||||
Destination.ServerList
|
||||
}
|
||||
val backStack = rememberNavBackStack(initialDestination)
|
||||
navigationManager.backStack = backStack
|
||||
if (UpdateChecker.ACTIVE && appPreferences.autoCheckForUpdates) {
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
updateChecker.maybeShowUpdateToast(appPreferences.updateUrl)
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Failed to check for update")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(current, preferences) {
|
||||
withContext(Dispatchers.IO) {
|
||||
deviceProfileService.getOrCreateDeviceProfile(
|
||||
preferences.appPreferences.playbackPreferences,
|
||||
current?.server?.serverVersion,
|
||||
)
|
||||
LaunchedEffect(current, preferences) {
|
||||
withContext(Dispatchers.IO) {
|
||||
deviceProfileService.getOrCreateDeviceProfile(
|
||||
preferences.appPreferences.playbackPreferences,
|
||||
current?.server?.serverVersion,
|
||||
)
|
||||
}
|
||||
}
|
||||
ApplicationContent(
|
||||
user = current?.user,
|
||||
server = current?.server,
|
||||
navigationManager = navigationManager,
|
||||
preferences = preferences,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
ApplicationContent(
|
||||
user = current?.user,
|
||||
server = current?.server,
|
||||
navigationManager = navigationManager,
|
||||
preferences = preferences,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
*
|
||||
* @param T The type of the preference value.
|
||||
*/
|
||||
sealed interface AppPreference<T> {
|
||||
sealed interface AppPreference<Pref, T> {
|
||||
/**
|
||||
* String resource ID for the title of the preference
|
||||
*/
|
||||
|
|
@ -40,12 +40,12 @@ sealed interface AppPreference<T> {
|
|||
* A function that gets the value from the [AppPreferences] object for UI purposes. This means
|
||||
* that it should return the value that is displayed in the UI, which isn't necessarily the raw value
|
||||
*/
|
||||
val getter: (prefs: AppPreferences) -> T
|
||||
val getter: (prefs: Pref) -> T
|
||||
|
||||
/**
|
||||
* A function that sets the value in the [AppPreferences] object from the UI. It should convert the value if needed
|
||||
*/
|
||||
val setter: (prefs: AppPreferences, value: T) -> AppPreferences
|
||||
val setter: (prefs: Pref, value: T) -> Pref
|
||||
|
||||
fun summary(
|
||||
context: Context,
|
||||
|
|
@ -56,7 +56,7 @@ sealed interface AppPreference<T> {
|
|||
|
||||
companion object {
|
||||
val SkipForward =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.skip_forward_preference,
|
||||
defaultValue = 30,
|
||||
min = 10,
|
||||
|
|
@ -85,7 +85,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val SkipBack =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.skip_back_preference,
|
||||
defaultValue = 10,
|
||||
min = 5,
|
||||
|
|
@ -114,7 +114,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
// val GridJumpButtons =
|
||||
// AppSwitchPreference(
|
||||
// AppSwitchPreference<AppPreferences>(
|
||||
// title = R.string.show_grid_jump_buttons,
|
||||
// defaultValue = true,
|
||||
// getter = { it.interfacePreferences.showGridJumpButtons },
|
||||
|
|
@ -126,7 +126,7 @@ sealed interface AppPreference<T> {
|
|||
// )
|
||||
|
||||
// val ShowGridFooter =
|
||||
// AppSwitchPreference(
|
||||
// AppSwitchPreference<AppPreferences>(
|
||||
// title = R.string.grid_position_footer,
|
||||
// defaultValue = true,
|
||||
// getter = { it.interfacePreferences.showPositionFooter },
|
||||
|
|
@ -138,7 +138,7 @@ sealed interface AppPreference<T> {
|
|||
// )
|
||||
|
||||
val ControllerTimeout =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.hide_controller_timeout,
|
||||
defaultValue = 5000,
|
||||
min = 500,
|
||||
|
|
@ -159,7 +159,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val SeekBarSteps =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.seek_bar_steps,
|
||||
defaultValue = 16,
|
||||
min = 4,
|
||||
|
|
@ -173,7 +173,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val HomePageItems =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.max_homepage_items,
|
||||
defaultValue = 25,
|
||||
min = 5,
|
||||
|
|
@ -187,7 +187,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val CombineContinueNext =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.combine_continue_next,
|
||||
defaultValue = false,
|
||||
getter = { it.homePagePreferences.combineContinueNext },
|
||||
|
|
@ -199,7 +199,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val RewatchNextUp =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.rewatch_next_up,
|
||||
defaultValue = false,
|
||||
getter = { it.homePagePreferences.enableRewatchingNextUp },
|
||||
|
|
@ -211,7 +211,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val PlayThemeMusic =
|
||||
AppChoicePreference<ThemeSongVolume>(
|
||||
AppChoicePreference<AppPreferences, ThemeSongVolume>(
|
||||
title = R.string.play_theme_music,
|
||||
defaultValue = ThemeSongVolume.MEDIUM,
|
||||
getter = { it.interfacePreferences.playThemeSongs },
|
||||
|
|
@ -224,7 +224,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val PlaybackDebugInfo =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.playback_debug_info,
|
||||
defaultValue = false,
|
||||
getter = { it.playbackPreferences.showDebugInfo },
|
||||
|
|
@ -236,7 +236,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val AutoPlayNextUp =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.auto_play_next,
|
||||
defaultValue = true,
|
||||
getter = { it.playbackPreferences.autoPlayNext },
|
||||
|
|
@ -248,7 +248,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val SkipBackOnResume =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.skip_back_on_resume_preference,
|
||||
defaultValue = 0,
|
||||
min = 0,
|
||||
|
|
@ -270,7 +270,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val AutoPlayNextDelay =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.auto_play_next_delay,
|
||||
defaultValue = 15,
|
||||
min = 0,
|
||||
|
|
@ -296,7 +296,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val PassOutProtection =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.pass_out_protection,
|
||||
defaultValue = 2,
|
||||
min = 0,
|
||||
|
|
@ -341,7 +341,7 @@ sealed interface AppPreference<T> {
|
|||
*(120..200 step 20).map { it * MEGA_BIT }.toTypedArray(),
|
||||
)
|
||||
val MaxBitrate =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.max_bitrate,
|
||||
defaultValue = bitrateValues.indexOf(DEFAULT_BITRATE).toLong(),
|
||||
min = 0,
|
||||
|
|
@ -370,7 +370,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val Ac3Supported =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.ac3_supported,
|
||||
defaultValue = true,
|
||||
getter = { it.playbackPreferences.overrides.ac3Supported },
|
||||
|
|
@ -381,7 +381,7 @@ sealed interface AppPreference<T> {
|
|||
summaryOff = R.string.disabled,
|
||||
)
|
||||
val DownMixStereo =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.downmix_stereo,
|
||||
defaultValue = false,
|
||||
getter = { it.playbackPreferences.overrides.downmixStereo },
|
||||
|
|
@ -392,7 +392,7 @@ sealed interface AppPreference<T> {
|
|||
summaryOff = R.string.disabled,
|
||||
)
|
||||
val DirectPlayAss =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.direct_play_ass,
|
||||
defaultValue = true,
|
||||
getter = { it.playbackPreferences.overrides.directPlayAss },
|
||||
|
|
@ -403,7 +403,7 @@ sealed interface AppPreference<T> {
|
|||
summaryOff = R.string.disabled,
|
||||
)
|
||||
val DirectPlayPgs =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.direct_play_pgs,
|
||||
defaultValue = true,
|
||||
getter = { it.playbackPreferences.overrides.directPlayPgs },
|
||||
|
|
@ -415,7 +415,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val RememberSelectedTab =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.remember_selected_tab,
|
||||
defaultValue = false,
|
||||
getter = { it.interfacePreferences.rememberSelectedTab },
|
||||
|
|
@ -427,7 +427,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val ThemeColors =
|
||||
AppChoicePreference<AppThemeColors>(
|
||||
AppChoicePreference<AppPreferences, AppThemeColors>(
|
||||
title = R.string.app_theme,
|
||||
defaultValue = AppThemeColors.PURPLE,
|
||||
getter = { it.interfacePreferences.appThemeColors },
|
||||
|
|
@ -440,21 +440,21 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val InstalledVersion =
|
||||
AppClickablePreference(
|
||||
AppClickablePreference<AppPreferences>(
|
||||
title = R.string.installed_version,
|
||||
getter = { },
|
||||
setter = { prefs, _ -> prefs },
|
||||
)
|
||||
|
||||
val Update =
|
||||
AppClickablePreference(
|
||||
AppClickablePreference<AppPreferences>(
|
||||
title = R.string.check_for_updates,
|
||||
getter = { },
|
||||
setter = { prefs, _ -> prefs },
|
||||
)
|
||||
|
||||
val AutoCheckForUpdates =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.auto_check_for_updates,
|
||||
defaultValue = true,
|
||||
getter = { it.autoCheckForUpdates },
|
||||
|
|
@ -466,7 +466,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val UpdateUrl =
|
||||
AppStringPreference(
|
||||
AppStringPreference<AppPreferences>(
|
||||
title = R.string.update_url,
|
||||
defaultValue = "https://api.github.com/repos/damontecres/Wholphin/releases/latest",
|
||||
getter = { it.updateUrl },
|
||||
|
|
@ -477,19 +477,19 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val OssLicenseInfo =
|
||||
AppDestinationPreference(
|
||||
AppDestinationPreference<AppPreferences>(
|
||||
title = R.string.license_info,
|
||||
destination = Destination.License,
|
||||
)
|
||||
|
||||
val AdvancedSettings =
|
||||
AppDestinationPreference(
|
||||
AppDestinationPreference<AppPreferences>(
|
||||
title = R.string.advanced_settings,
|
||||
destination = Destination.Settings(PreferenceScreenOption.ADVANCED),
|
||||
)
|
||||
|
||||
val SkipIntros =
|
||||
AppChoicePreference<SkipSegmentBehavior>(
|
||||
AppChoicePreference<AppPreferences, SkipSegmentBehavior>(
|
||||
title = R.string.skip_intro_behavior,
|
||||
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
||||
getter = { it.playbackPreferences.skipIntros },
|
||||
|
|
@ -502,7 +502,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val SkipOutros =
|
||||
AppChoicePreference<SkipSegmentBehavior>(
|
||||
AppChoicePreference<AppPreferences, SkipSegmentBehavior>(
|
||||
title = R.string.skip_outro_behavior,
|
||||
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
||||
getter = { it.playbackPreferences.skipOutros },
|
||||
|
|
@ -515,7 +515,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val SkipCommercials =
|
||||
AppChoicePreference<SkipSegmentBehavior>(
|
||||
AppChoicePreference<AppPreferences, SkipSegmentBehavior>(
|
||||
title = R.string.skip_commercials_behavior,
|
||||
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
||||
getter = { it.playbackPreferences.skipCommercials },
|
||||
|
|
@ -528,7 +528,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val SkipPreviews =
|
||||
AppChoicePreference<SkipSegmentBehavior>(
|
||||
AppChoicePreference<AppPreferences, SkipSegmentBehavior>(
|
||||
title = R.string.skip_previews_behavior,
|
||||
defaultValue = SkipSegmentBehavior.IGNORE,
|
||||
getter = { it.playbackPreferences.skipPreviews },
|
||||
|
|
@ -541,7 +541,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val SkipRecaps =
|
||||
AppChoicePreference<SkipSegmentBehavior>(
|
||||
AppChoicePreference<AppPreferences, SkipSegmentBehavior>(
|
||||
title = R.string.skip_recap_behavior,
|
||||
defaultValue = SkipSegmentBehavior.IGNORE,
|
||||
getter = { it.playbackPreferences.skipRecaps },
|
||||
|
|
@ -554,7 +554,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val GlobalContentScale =
|
||||
AppChoicePreference<PrefContentScale>(
|
||||
AppChoicePreference<AppPreferences, PrefContentScale>(
|
||||
title = R.string.global_content_scale,
|
||||
defaultValue = PrefContentScale.FIT,
|
||||
getter = { it.playbackPreferences.globalContentScale },
|
||||
|
|
@ -567,7 +567,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val FfmpegPreference =
|
||||
AppChoicePreference<MediaExtensionStatus>(
|
||||
AppChoicePreference<AppPreferences, MediaExtensionStatus>(
|
||||
title = R.string.ffmpeg_extension_pref,
|
||||
defaultValue = MediaExtensionStatus.MES_FALLBACK,
|
||||
getter = { it.playbackPreferences.overrides.mediaExtensionsEnabled },
|
||||
|
|
@ -580,14 +580,14 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val ClearImageCache =
|
||||
AppClickablePreference(
|
||||
AppClickablePreference<AppPreferences>(
|
||||
title = R.string.clear_image_cache,
|
||||
getter = { },
|
||||
setter = { prefs, _ -> prefs },
|
||||
)
|
||||
|
||||
val UserPinnedNavDrawerItems =
|
||||
AppClickablePreference(
|
||||
AppClickablePreference<AppPreferences>(
|
||||
title = R.string.nav_drawer_pins,
|
||||
summary = R.string.nav_drawer_pins_summary,
|
||||
getter = { },
|
||||
|
|
@ -595,7 +595,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val SendCrashReports =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.send_crash_reports,
|
||||
defaultValue = true,
|
||||
getter = {
|
||||
|
|
@ -615,7 +615,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val SendAppLogs =
|
||||
AppClickablePreference(
|
||||
AppClickablePreference<AppPreferences>(
|
||||
title = R.string.send_app_logs,
|
||||
summary = R.string.send_app_logs_summary,
|
||||
getter = { },
|
||||
|
|
@ -623,7 +623,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val NavDrawerSwitchOnFocus =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.nav_drawer_switch_on_focus,
|
||||
defaultValue = true,
|
||||
getter = { it.interfacePreferences.navDrawerSwitchOnFocus },
|
||||
|
|
@ -635,7 +635,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val ShowNextUpTiming =
|
||||
AppChoicePreference<ShowNextUpWhen>(
|
||||
AppChoicePreference<AppPreferences, ShowNextUpWhen>(
|
||||
title = R.string.show_next_up_when,
|
||||
defaultValue = ShowNextUpWhen.END_OF_PLAYBACK,
|
||||
getter = { it.playbackPreferences.showNextUpWhen },
|
||||
|
|
@ -648,7 +648,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val ShowClock =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.show_clock,
|
||||
defaultValue = true,
|
||||
getter = { it.interfacePreferences.showClock },
|
||||
|
|
@ -660,7 +660,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val OneClickPause =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.one_click_pause,
|
||||
defaultValue = false,
|
||||
getter = { it.playbackPreferences.oneClickPause },
|
||||
|
|
@ -672,13 +672,13 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val SubtitleStyle =
|
||||
AppDestinationPreference(
|
||||
AppDestinationPreference<AppPreferences>(
|
||||
title = R.string.subtitle_style,
|
||||
destination = Destination.Settings(PreferenceScreenOption.SUBTITLES),
|
||||
)
|
||||
|
||||
val PlayerBackendPref =
|
||||
AppChoicePreference<PlayerBackend>(
|
||||
AppChoicePreference<AppPreferences, PlayerBackend>(
|
||||
title = R.string.player_backend,
|
||||
defaultValue = PlayerBackend.EXO_PLAYER,
|
||||
getter = { it.playbackPreferences.playerBackend },
|
||||
|
|
@ -691,7 +691,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val MpvHardwareDecoding =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.mpv_hardware_decoding,
|
||||
defaultValue = true,
|
||||
getter = { it.playbackPreferences.mpvOptions.enableHardwareDecoding },
|
||||
|
|
@ -702,7 +702,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val DebugLogging =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.verbose_logging,
|
||||
defaultValue = false,
|
||||
getter = { DebugLogTree.INSTANCE.enabled },
|
||||
|
|
@ -715,7 +715,7 @@ sealed interface AppPreference<T> {
|
|||
)
|
||||
|
||||
val ImageDiskCacheSize =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.image_cache_size,
|
||||
defaultValue = 200,
|
||||
min = 25,
|
||||
|
|
@ -884,16 +884,16 @@ val advancedPreferences =
|
|||
)
|
||||
}
|
||||
|
||||
data class AppSwitchPreference(
|
||||
data class AppSwitchPreference<Pref>(
|
||||
@get:StringRes override val title: Int,
|
||||
override val defaultValue: Boolean,
|
||||
override val getter: (prefs: AppPreferences) -> Boolean,
|
||||
override val setter: (prefs: AppPreferences, value: Boolean) -> AppPreferences,
|
||||
override val getter: (prefs: Pref) -> Boolean,
|
||||
override val setter: (prefs: Pref, value: Boolean) -> Pref,
|
||||
val validator: (value: Boolean) -> PreferenceValidation = { PreferenceValidation.Valid },
|
||||
@param:StringRes val summary: Int? = null,
|
||||
@param:StringRes val summaryOn: Int? = null,
|
||||
@param:StringRes val summaryOff: Int? = null,
|
||||
) : AppPreference<Boolean> {
|
||||
) : AppPreference<Pref, Boolean> {
|
||||
override fun summary(
|
||||
context: Context,
|
||||
value: Boolean?,
|
||||
|
|
@ -905,70 +905,70 @@ data class AppSwitchPreference(
|
|||
}
|
||||
}
|
||||
|
||||
open class AppStringPreference(
|
||||
open class AppStringPreference<Pref>(
|
||||
@param:StringRes override val title: Int,
|
||||
override val defaultValue: String,
|
||||
override val getter: (AppPreferences) -> String,
|
||||
override val setter: (AppPreferences, String) -> AppPreferences,
|
||||
override val getter: (Pref) -> String,
|
||||
override val setter: (Pref, String) -> Pref,
|
||||
@param:StringRes val summary: Int?,
|
||||
) : AppPreference<String> {
|
||||
) : AppPreference<Pref, String> {
|
||||
override fun summary(
|
||||
context: Context,
|
||||
value: String?,
|
||||
): String? = summary?.let { context.getString(it) } ?: value
|
||||
}
|
||||
|
||||
data class AppChoicePreference<T>(
|
||||
data class AppChoicePreference<Pref, T>(
|
||||
@param:StringRes override val title: Int,
|
||||
override val defaultValue: T,
|
||||
@param:ArrayRes val displayValues: Int,
|
||||
val indexToValue: (index: Int) -> T,
|
||||
val valueToIndex: (T) -> Int,
|
||||
override val getter: (prefs: AppPreferences) -> T,
|
||||
override val setter: (prefs: AppPreferences, value: T) -> AppPreferences,
|
||||
override val getter: (prefs: Pref) -> T,
|
||||
override val setter: (prefs: Pref, value: T) -> Pref,
|
||||
@param:StringRes val summary: Int? = null,
|
||||
) : AppPreference<T>
|
||||
) : AppPreference<Pref, T>
|
||||
|
||||
data class AppMultiChoicePreference<T>(
|
||||
data class AppMultiChoicePreference<Pref, T>(
|
||||
@param:StringRes override val title: Int,
|
||||
override val defaultValue: List<T>,
|
||||
val allValues: List<T>,
|
||||
@param:ArrayRes val displayValues: Int,
|
||||
override val getter: (prefs: AppPreferences) -> List<T>,
|
||||
override val setter: (prefs: AppPreferences, value: List<T>) -> AppPreferences,
|
||||
override val getter: (prefs: Pref) -> List<T>,
|
||||
override val setter: (prefs: Pref, value: List<T>) -> Pref,
|
||||
@param:StringRes val summary: Int? = null,
|
||||
val toSharedPrefs: (T) -> String,
|
||||
val fromSharedPrefs: (String) -> T?,
|
||||
) : AppPreference<List<T>>
|
||||
) : AppPreference<Pref, List<T>>
|
||||
|
||||
data class AppClickablePreference(
|
||||
data class AppClickablePreference<Pref>(
|
||||
@param:StringRes override val title: Int,
|
||||
override val defaultValue: Unit = Unit,
|
||||
override val getter: (prefs: AppPreferences) -> Unit = { },
|
||||
override val setter: (prefs: AppPreferences, value: Unit) -> AppPreferences = { prefs, _ -> prefs },
|
||||
override val getter: (prefs: Pref) -> Unit = { },
|
||||
override val setter: (prefs: Pref, value: Unit) -> Pref = { prefs, _ -> prefs },
|
||||
@param:StringRes val summary: Int? = null,
|
||||
) : AppPreference<Unit> {
|
||||
) : AppPreference<Pref, Unit> {
|
||||
override fun summary(
|
||||
context: Context,
|
||||
value: Unit?,
|
||||
): String? = summary?.let { context.getString(it) }
|
||||
}
|
||||
|
||||
data class AppDestinationPreference(
|
||||
data class AppDestinationPreference<Pref>(
|
||||
@param:StringRes override val title: Int,
|
||||
override val defaultValue: Unit = Unit,
|
||||
override val getter: (prefs: AppPreferences) -> Unit = { },
|
||||
override val setter: (prefs: AppPreferences, value: Unit) -> AppPreferences = { prefs, _ -> prefs },
|
||||
override val getter: (prefs: Pref) -> Unit = { },
|
||||
override val setter: (prefs: Pref, value: Unit) -> Pref = { prefs, _ -> prefs },
|
||||
@param:StringRes val summary: Int? = null,
|
||||
val destination: Destination,
|
||||
) : AppPreference<Unit> {
|
||||
) : AppPreference<Pref, Unit> {
|
||||
override fun summary(
|
||||
context: Context,
|
||||
value: Unit?,
|
||||
): String? = summary?.let { context.getString(it) }
|
||||
}
|
||||
|
||||
class AppSliderPreference(
|
||||
class AppSliderPreference<Pref>(
|
||||
@param:StringRes override val title: Int,
|
||||
override val defaultValue: Long,
|
||||
/**
|
||||
|
|
@ -980,11 +980,11 @@ class AppSliderPreference(
|
|||
*/
|
||||
val max: Long = 100,
|
||||
val interval: Int = 1,
|
||||
override val getter: (prefs: AppPreferences) -> Long,
|
||||
override val setter: (prefs: AppPreferences, value: Long) -> AppPreferences,
|
||||
override val getter: (prefs: Pref) -> Long,
|
||||
override val setter: (prefs: Pref, value: Long) -> Pref,
|
||||
@param:StringRes val summary: Int? = null,
|
||||
val summarizer: ((Long?) -> String?)? = null,
|
||||
) : AppPreference<Long> {
|
||||
) : AppPreference<Pref, Long> {
|
||||
override fun summary(
|
||||
context: Context,
|
||||
value: Long?,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageFormat
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides image URLs for items with several convenience methods
|
||||
*
|
||||
* This is available in compose UI code via [com.github.damontecres.wholphin.ui.LocalImageUrlService]
|
||||
*/
|
||||
@Singleton
|
||||
class ImageUrlService
|
||||
@Inject
|
||||
constructor(
|
||||
private val api: ApiClient,
|
||||
) {
|
||||
fun getItemImageUrl(
|
||||
itemId: UUID,
|
||||
itemType: BaseItemKind,
|
||||
seriesId: UUID?,
|
||||
useSeriesForPrimary: Boolean,
|
||||
imageType: ImageType,
|
||||
fillWidth: Int? = null,
|
||||
fillHeight: Int? = null,
|
||||
): String? =
|
||||
when (imageType) {
|
||||
ImageType.BACKDROP,
|
||||
ImageType.LOGO,
|
||||
-> {
|
||||
if (seriesId != null && (itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON)) {
|
||||
getItemImageUrl(
|
||||
itemId = seriesId,
|
||||
imageType = imageType,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
} else {
|
||||
getItemImageUrl(
|
||||
itemId = itemId,
|
||||
imageType = imageType,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ImageType.PRIMARY,
|
||||
ImageType.THUMB,
|
||||
ImageType.BANNER,
|
||||
-> {
|
||||
if (useSeriesForPrimary && seriesId != null &&
|
||||
(itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON)
|
||||
) {
|
||||
getItemImageUrl(
|
||||
itemId = seriesId,
|
||||
imageType = imageType,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
} else {
|
||||
getItemImageUrl(
|
||||
itemId = itemId,
|
||||
imageType = imageType,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else ->
|
||||
getItemImageUrl(
|
||||
itemId = itemId,
|
||||
imageType = imageType,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
}
|
||||
|
||||
fun getItemImageUrl(
|
||||
item: BaseItem?,
|
||||
imageType: ImageType,
|
||||
fillWidth: Int? = null,
|
||||
fillHeight: Int? = null,
|
||||
): String? =
|
||||
if (item != null) {
|
||||
getItemImageUrl(
|
||||
itemId = item.id,
|
||||
itemType = item.type,
|
||||
seriesId = item.data.seriesId,
|
||||
useSeriesForPrimary = item.useSeriesForPrimary,
|
||||
imageType = imageType,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun getItemImageUrl(
|
||||
itemId: UUID,
|
||||
imageType: ImageType,
|
||||
maxWidth: Int? = null,
|
||||
maxHeight: Int? = null,
|
||||
width: Int? = null,
|
||||
height: Int? = null,
|
||||
quality: Int? = QUALITY,
|
||||
fillWidth: Int? = null,
|
||||
fillHeight: Int? = null,
|
||||
tag: String? = null,
|
||||
format: ImageFormat? = null,
|
||||
percentPlayed: Double? = null,
|
||||
unplayedCount: Int? = null,
|
||||
blur: Int? = null,
|
||||
backgroundColor: String? = null,
|
||||
foregroundLayer: String? = null,
|
||||
imageIndex: Int? = null,
|
||||
): String =
|
||||
api.imageApi.getItemImageUrl(
|
||||
itemId = itemId,
|
||||
imageType = imageType,
|
||||
maxWidth = maxWidth,
|
||||
maxHeight = maxHeight,
|
||||
width = width,
|
||||
height = height,
|
||||
quality = quality,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
tag = tag,
|
||||
format = format,
|
||||
percentPlayed = percentPlayed,
|
||||
unplayedCount = unplayedCount,
|
||||
blur = blur,
|
||||
backgroundColor = backgroundColor,
|
||||
foregroundLayer = foregroundLayer,
|
||||
imageIndex = imageIndex,
|
||||
)
|
||||
|
||||
/**
|
||||
* Just a convenient way to get the image URL and remember it
|
||||
*/
|
||||
@Composable
|
||||
fun rememberImageUrl(
|
||||
item: BaseItem?,
|
||||
imageType: ImageType = ImageType.PRIMARY,
|
||||
) = remember(item, imageType) {
|
||||
if (item != null) {
|
||||
getItemImageUrl(item, imageType)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val QUALITY = 96
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ import org.jellyfin.sdk.api.client.ApiClient
|
|||
import org.jellyfin.sdk.api.client.Response
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
|
@ -308,7 +309,7 @@ fun logCoilError(
|
|||
url: String?,
|
||||
errorResult: ErrorResult,
|
||||
) {
|
||||
if (errorResult.throwable is coil3.network.HttpException) {
|
||||
if (errorResult.throwable is coil3.network.HttpException || errorResult.throwable is coil3.request.NullRequestDataException) {
|
||||
Timber.w("Error loading image: %s for %s", errorResult.throwable.localizedMessage, url)
|
||||
} else {
|
||||
Timber.e(errorResult.throwable, "Error loading image: %s", url)
|
||||
|
|
@ -401,3 +402,9 @@ fun Response<BaseItemDtoQueryResult>.toBaseItems(
|
|||
api: ApiClient,
|
||||
useSeriesForPrimary: Boolean,
|
||||
) = this.content.items.map { BaseItem.from(it, api, useSeriesForPrimary) }
|
||||
|
||||
@Composable
|
||||
fun rememberBackDropImage(item: BaseItem) {
|
||||
val imageUrlService = LocalImageUrlService.current
|
||||
return remember(item) { imageUrlService.getItemImageUrl(item, ImageType.BACKDROP) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,23 @@ package com.github.damontecres.wholphin.ui
|
|||
|
||||
import android.content.res.Configuration.UI_MODE_TYPE_TELEVISION
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
|
||||
// This file is for constants used for the UI
|
||||
|
||||
val FontAwesome = FontFamily(Font(resId = R.font.fa_solid_900))
|
||||
|
||||
val LocalImageUrlService =
|
||||
staticCompositionLocalOf<ImageUrlService> { throw IllegalStateException("LocalImageUrlService not set") }
|
||||
|
||||
/**
|
||||
* Colors not associated with the theme
|
||||
*/
|
||||
|
|
@ -81,6 +86,15 @@ object AspectRatios {
|
|||
const val SQUARE = 1f
|
||||
}
|
||||
|
||||
enum class AspectRatio(
|
||||
val ratio: Float,
|
||||
) {
|
||||
TALL(AspectRatios.TALL),
|
||||
WIDE(AspectRatios.WIDE),
|
||||
FOUR_THREE(AspectRatios.FOUR_THREE),
|
||||
SQUARE(AspectRatios.SQUARE),
|
||||
}
|
||||
|
||||
@Preview(
|
||||
device = "spec:parent=tv_1080p",
|
||||
backgroundColor = 0xFF383535,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -33,11 +34,14 @@ import androidx.tv.material3.MaterialTheme
|
|||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
/**
|
||||
* Displays an image as a card. If no image is available, the name will be shown instead
|
||||
|
|
@ -45,7 +49,7 @@ import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
|||
@Composable
|
||||
fun BannerCard(
|
||||
name: String?,
|
||||
imageUrl: String?,
|
||||
item: BaseItem?,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -57,6 +61,29 @@ fun BannerCard(
|
|||
aspectRatio: Float = AspectRatios.WIDE,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
) {
|
||||
val imageUrlService = LocalImageUrlService.current
|
||||
val density = LocalDensity.current
|
||||
val imageUrl =
|
||||
remember(item, cardHeight) {
|
||||
if (item != null) {
|
||||
val fillHeight =
|
||||
if (cardHeight != Dp.Unspecified) {
|
||||
with(density) {
|
||||
cardHeight.roundToPx()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
imageUrlService.getItemImageUrl(
|
||||
item,
|
||||
ImageType.PRIMARY,
|
||||
fillWidth = null,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
var imageError by remember { mutableStateOf(false) }
|
||||
Card(
|
||||
modifier = modifier.size(cardHeight * aspectRatio, cardHeight),
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ fun EpisodeCard(
|
|||
.fillMaxSize(),
|
||||
) {
|
||||
ItemCardImage(
|
||||
imageUrl = item?.imageUrl,
|
||||
item = item,
|
||||
name = item?.name,
|
||||
showOverlay = false,
|
||||
favorite = dto?.userData?.isFavorite ?: false,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.github.damontecres.wholphin.ui.cards
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
|
|
@ -11,6 +12,8 @@ import com.github.damontecres.wholphin.data.ExtrasItem
|
|||
import com.github.damontecres.wholphin.data.pluralRes
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
@Composable
|
||||
fun ExtrasRow(
|
||||
|
|
@ -27,6 +30,17 @@ fun ExtrasRow(
|
|||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
cardContent = { index, item, mod, onClick, onLongClick ->
|
||||
val imageUrlService = LocalImageUrlService.current
|
||||
val imageUrl =
|
||||
remember {
|
||||
val item =
|
||||
when (item) {
|
||||
is ExtrasItem.Group -> item.items.random()
|
||||
is ExtrasItem.Single -> item.item
|
||||
null -> null
|
||||
}
|
||||
imageUrlService.getItemImageUrl(item, ImageType.PRIMARY)
|
||||
}
|
||||
SeasonCard(
|
||||
title =
|
||||
when (item) {
|
||||
|
|
@ -49,7 +63,7 @@ fun ExtrasRow(
|
|||
showImageOverlay = true,
|
||||
imageHeight = Cards.height2x3 * .75f,
|
||||
imageWidth = Dp.Unspecified,
|
||||
imageUrl = item?.imageUrl,
|
||||
imageUrl = imageUrl,
|
||||
isFavorite = false,
|
||||
isPlayed = false,
|
||||
unplayedItemCount = -1,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import androidx.tv.material3.MaterialTheme
|
|||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionImageType
|
||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ fun GridCard(
|
|||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
imageAspectRatio: Float = AspectRatios.TALL,
|
||||
imageContentScale: ContentScale = ContentScale.Fit,
|
||||
imageType: ViewOptionImageType = ViewOptionImageType.PRIMARY,
|
||||
) {
|
||||
val dto = item?.data
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
|
|
@ -78,7 +80,8 @@ fun GridCard(
|
|||
),
|
||||
) {
|
||||
ItemCardImage(
|
||||
imageUrl = item?.imageUrl,
|
||||
item = item,
|
||||
imageType = imageType.imageType,
|
||||
name = item?.name,
|
||||
// showOverlay = !focusedAfterDelay,
|
||||
showOverlay = true,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
package com.github.damontecres.wholphin.ui.cards
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -26,20 +24,79 @@ import androidx.compose.ui.graphics.BlendMode
|
|||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onLayoutRectChanged
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.logCoilError
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
/**
|
||||
* Display an image for an item with optional overlay data
|
||||
*
|
||||
* This will fetch the image using fillWidth/fillHeight based on the layout size
|
||||
*/
|
||||
@Composable
|
||||
fun ItemCardImage(
|
||||
item: BaseItem?,
|
||||
name: String?,
|
||||
showOverlay: Boolean,
|
||||
favorite: Boolean,
|
||||
watched: Boolean,
|
||||
unwatchedCount: Int,
|
||||
watchedPercent: Double?,
|
||||
modifier: Modifier = Modifier,
|
||||
imageType: ImageType = ImageType.PRIMARY,
|
||||
useFallbackText: Boolean = true,
|
||||
contentScale: ContentScale = ContentScale.Fit,
|
||||
) {
|
||||
val imageUrlService = LocalImageUrlService.current
|
||||
var size by remember { mutableStateOf(IntSize.Zero) }
|
||||
val imageUrl =
|
||||
remember(size, item) {
|
||||
if (size != IntSize.Zero && item != null) {
|
||||
imageUrlService.getItemImageUrl(
|
||||
item,
|
||||
imageType,
|
||||
fillWidth = size.width,
|
||||
fillHeight = size.height,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
ItemCardImage(
|
||||
imageUrl = imageUrl,
|
||||
name = name,
|
||||
showOverlay = showOverlay,
|
||||
favorite = favorite,
|
||||
watched = watched,
|
||||
unwatchedCount = unwatchedCount,
|
||||
watchedPercent = watchedPercent,
|
||||
modifier =
|
||||
modifier.onLayoutRectChanged(
|
||||
throttleMillis = 100,
|
||||
debounceMillis = 25,
|
||||
) {
|
||||
size = IntSize(width = it.width, height = it.height)
|
||||
},
|
||||
useFallbackText = useFallbackText,
|
||||
contentScale = contentScale,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemCardImage(
|
||||
|
|
@ -54,8 +111,10 @@ fun ItemCardImage(
|
|||
useFallbackText: Boolean = true,
|
||||
contentScale: ContentScale = ContentScale.Fit,
|
||||
) {
|
||||
var imageError by remember { mutableStateOf(false) }
|
||||
Box(modifier = modifier) {
|
||||
var imageError by remember(imageUrl) { mutableStateOf(false) }
|
||||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
if (!imageError && imageUrl.isNotNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
|
|
@ -72,104 +131,130 @@ fun ItemCardImage(
|
|||
.align(Alignment.TopCenter),
|
||||
)
|
||||
} else {
|
||||
// TODO options for overriding fallback
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.fillMaxSize()
|
||||
.align(Alignment.TopCenter),
|
||||
) {
|
||||
if (useFallbackText && name.isNotNullOrBlank()) {
|
||||
Text(
|
||||
text = name,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 14.sp,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.align(Alignment.Center),
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.video_solid),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
colorFilter =
|
||||
ColorFilter.tint(
|
||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
BlendMode.SrcIn,
|
||||
),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize(.4f)
|
||||
.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
ItemCardImageFallback(
|
||||
name = name,
|
||||
useFallbackText = useFallbackText,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = showOverlay,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (favorite) {
|
||||
Text(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(8.dp),
|
||||
color = colorResource(android.R.color.holo_red_light),
|
||||
text = stringResource(R.string.fa_heart),
|
||||
fontSize = 20.sp,
|
||||
fontFamily = FontAwesome,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(4.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
) {
|
||||
if (watched && (watchedPercent == null || watchedPercent <= 0.0 || watchedPercent >= 100.0)) {
|
||||
WatchedIcon(Modifier.size(24.dp))
|
||||
}
|
||||
if (unwatchedCount > 0) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(
|
||||
AppColors.TransparentBlack50,
|
||||
shape = RoundedCornerShape(25),
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = unwatchedCount.toString(),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
// fontSize = 16.sp,
|
||||
modifier = Modifier.padding(4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watchedPercent?.let { percent ->
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.tertiary,
|
||||
).clip(RectangleShape)
|
||||
.height(Cards.playedPercentHeight)
|
||||
.fillMaxWidth((percent / 100.0).toFloat()),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showOverlay) {
|
||||
ItemCardImageOverlay(
|
||||
favorite = favorite,
|
||||
watched = watched,
|
||||
unwatchedCount = unwatchedCount,
|
||||
watchedPercent = watchedPercent,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.ItemCardImageFallback(
|
||||
name: String?,
|
||||
useFallbackText: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// TODO options for overriding fallback
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.fillMaxSize()
|
||||
.align(Alignment.TopCenter),
|
||||
) {
|
||||
if (useFallbackText && name.isNotNullOrBlank()) {
|
||||
Text(
|
||||
text = name,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 14.sp,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.align(Alignment.Center),
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.video_solid),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
colorFilter =
|
||||
ColorFilter.tint(
|
||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
BlendMode.SrcIn,
|
||||
),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize(.4f)
|
||||
.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemCardImageOverlay(
|
||||
favorite: Boolean,
|
||||
watched: Boolean,
|
||||
unwatchedCount: Int,
|
||||
watchedPercent: Double?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
if (favorite) {
|
||||
Text(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(8.dp),
|
||||
color = colorResource(android.R.color.holo_red_light),
|
||||
text = stringResource(R.string.fa_heart),
|
||||
fontSize = 20.sp,
|
||||
fontFamily = FontAwesome,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(4.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
) {
|
||||
if (watched && (watchedPercent == null || watchedPercent <= 0.0 || watchedPercent >= 100.0)) {
|
||||
WatchedIcon(Modifier.size(24.dp))
|
||||
}
|
||||
if (unwatchedCount > 0) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(
|
||||
AppColors.TransparentBlack50,
|
||||
shape = RoundedCornerShape(25),
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = unwatchedCount.toString(),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
// fontSize = 16.sp,
|
||||
modifier = Modifier.padding(4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watchedPercent?.let { percent ->
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.tertiary,
|
||||
).clip(RectangleShape)
|
||||
.height(Cards.playedPercentHeight)
|
||||
.fillMaxWidth((percent / 100.0).toFloat()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ fun BannerItemRow(
|
|||
cardContent = { index, item, modifier, onClick, onLongClick ->
|
||||
BannerCard(
|
||||
name = title,
|
||||
imageUrl = item?.imageUrl,
|
||||
item = item,
|
||||
aspectRatio =
|
||||
aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat()
|
||||
?: AspectRatios.WIDE,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -27,8 +28,10 @@ import androidx.tv.material3.CardDefaults
|
|||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
/**
|
||||
* A Card for a TV Show Season, but can generically show most items
|
||||
|
|
@ -44,24 +47,57 @@ fun SeasonCard(
|
|||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
showImageOverlay: Boolean = false,
|
||||
aspectRatio: Float = item?.data?.primaryImageAspectRatio?.toFloat() ?: AspectRatios.TALL,
|
||||
) = SeasonCard(
|
||||
title = item?.title,
|
||||
subtitle = item?.subtitle,
|
||||
name = item?.name,
|
||||
imageUrl = item?.imageUrl,
|
||||
isFavorite = item?.data?.userData?.isFavorite ?: false,
|
||||
isPlayed = item?.data?.userData?.played ?: false,
|
||||
unplayedItemCount = item?.data?.userData?.unplayedItemCount ?: 0,
|
||||
playedPercentage = item?.data?.userData?.playedPercentage ?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = modifier,
|
||||
imageHeight = imageHeight,
|
||||
imageWidth = imageWidth,
|
||||
interactionSource = interactionSource,
|
||||
showImageOverlay = showImageOverlay,
|
||||
aspectRatio = aspectRatio,
|
||||
)
|
||||
) {
|
||||
val imageUrlService = LocalImageUrlService.current
|
||||
val density = LocalDensity.current
|
||||
val imageUrl =
|
||||
remember(item, imageHeight, imageWidth) {
|
||||
if (item != null) {
|
||||
val fillHeight =
|
||||
if (imageHeight != Dp.Unspecified) {
|
||||
with(density) {
|
||||
imageHeight.roundToPx()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val fillWidth =
|
||||
if (imageWidth != Dp.Unspecified) {
|
||||
with(density) {
|
||||
imageWidth.roundToPx()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
imageUrlService.getItemImageUrl(
|
||||
item,
|
||||
ImageType.PRIMARY,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
SeasonCard(
|
||||
title = item?.title,
|
||||
subtitle = item?.subtitle,
|
||||
name = item?.name,
|
||||
imageUrl = imageUrl,
|
||||
isFavorite = item?.data?.userData?.isFavorite ?: false,
|
||||
isPlayed = item?.data?.userData?.played ?: false,
|
||||
unplayedItemCount = item?.data?.userData?.unplayedItemCount ?: 0,
|
||||
playedPercentage = item?.data?.userData?.playedPercentage ?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = modifier,
|
||||
imageHeight = imageHeight,
|
||||
imageWidth = imageWidth,
|
||||
interactionSource = interactionSource,
|
||||
showImageOverlay = showImageOverlay,
|
||||
aspectRatio = aspectRatio,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Card for a TV Show Season, but can generically show most items
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
|
|
@ -75,7 +76,11 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
|||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.playback.scale
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
|
|
@ -125,6 +130,7 @@ class CollectionFolderViewModel
|
|||
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
val sortAndDirection = MutableLiveData<SortAndDirection>()
|
||||
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
||||
val viewOptions = MutableLiveData<ViewOptions>()
|
||||
|
||||
private var useSeriesForPrimary: Boolean = true
|
||||
|
||||
|
|
@ -134,6 +140,7 @@ class CollectionFolderViewModel
|
|||
recursive: Boolean,
|
||||
filter: GetItemsFilter,
|
||||
useSeriesForPrimary: Boolean,
|
||||
defaultViewOptions: ViewOptions,
|
||||
): Job =
|
||||
viewModelScope.launch(
|
||||
LoadingExceptionHandler(
|
||||
|
|
@ -151,6 +158,9 @@ class CollectionFolderViewModel
|
|||
serverRepository.currentUser.value?.let { user ->
|
||||
libraryDisplayInfoDao.getItem(user, itemId)
|
||||
}
|
||||
this@CollectionFolderViewModel.viewOptions.setValueOnMain(
|
||||
libraryDisplayInfo?.viewOptions ?: defaultViewOptions,
|
||||
)
|
||||
|
||||
val sortAndDirection =
|
||||
libraryDisplayInfo?.sortAndDirection
|
||||
|
|
@ -167,24 +177,40 @@ class CollectionFolderViewModel
|
|||
loadResults(true, sortAndDirection, recursive, filterToUse, useSeriesForPrimary)
|
||||
}
|
||||
|
||||
fun onFilterChange(
|
||||
newFilter: GetItemsFilter,
|
||||
recursive: Boolean,
|
||||
private fun saveLibraryDisplayInfo(
|
||||
newFilter: GetItemsFilter = this.filter.value!!,
|
||||
newSort: SortAndDirection = this.sortAndDirection.value!!,
|
||||
viewOptions: ViewOptions? = this.viewOptions.value,
|
||||
) {
|
||||
Timber.v("onFilterChange: filter=%s", newFilter)
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val libraryDisplayInfo =
|
||||
LibraryDisplayInfo(
|
||||
userId = user.rowId,
|
||||
itemId = itemId,
|
||||
sort = sortAndDirection.value!!.sort,
|
||||
direction = sortAndDirection.value!!.direction,
|
||||
sort = newSort.sort,
|
||||
direction = newSort.direction,
|
||||
filter = newFilter,
|
||||
viewOptions = viewOptions,
|
||||
)
|
||||
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveViewOptions(viewOptions: ViewOptions) {
|
||||
this.viewOptions.value = viewOptions
|
||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
saveLibraryDisplayInfo(viewOptions = viewOptions)
|
||||
}
|
||||
}
|
||||
|
||||
fun onFilterChange(
|
||||
newFilter: GetItemsFilter,
|
||||
recursive: Boolean,
|
||||
) {
|
||||
Timber.v("onFilterChange: filter=%s", newFilter)
|
||||
saveLibraryDisplayInfo(newFilter, sortAndDirection.value!!)
|
||||
loadResults(false, sortAndDirection.value!!, recursive, newFilter, useSeriesForPrimary)
|
||||
}
|
||||
|
||||
|
|
@ -199,19 +225,7 @@ class CollectionFolderViewModel
|
|||
recursive,
|
||||
filter,
|
||||
)
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val libraryDisplayInfo =
|
||||
LibraryDisplayInfo(
|
||||
userId = user.rowId,
|
||||
itemId = itemId,
|
||||
sort = sortAndDirection.sort,
|
||||
direction = sortAndDirection.direction,
|
||||
filter = filter,
|
||||
)
|
||||
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||
}
|
||||
}
|
||||
saveLibraryDisplayInfo(filter, sortAndDirection)
|
||||
loadResults(true, sortAndDirection, recursive, filter, useSeriesForPrimary)
|
||||
}
|
||||
|
||||
|
|
@ -454,11 +468,11 @@ fun CollectionFolderGrid(
|
|||
onClickItem: (Int, BaseItem) -> Unit,
|
||||
sortOptions: List<ItemSortBy>,
|
||||
playEnabled: Boolean,
|
||||
defaultViewOptions: ViewOptions,
|
||||
modifier: Modifier = Modifier,
|
||||
initialSortAndDirection: SortAndDirection? = null,
|
||||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
params: CollectionFolderGridParameters = CollectionFolderGridParameters(),
|
||||
useSeriesForPrimary: Boolean = true,
|
||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||
) = CollectionFolderGrid(
|
||||
|
|
@ -469,11 +483,11 @@ fun CollectionFolderGrid(
|
|||
onClickItem,
|
||||
sortOptions,
|
||||
playEnabled,
|
||||
modifier,
|
||||
defaultViewOptions = defaultViewOptions,
|
||||
modifier = modifier,
|
||||
initialSortAndDirection = initialSortAndDirection,
|
||||
showTitle = showTitle,
|
||||
positionCallback = positionCallback,
|
||||
params = params,
|
||||
useSeriesForPrimary = useSeriesForPrimary,
|
||||
filterOptions = filterOptions,
|
||||
)
|
||||
|
|
@ -487,13 +501,13 @@ fun CollectionFolderGrid(
|
|||
onClickItem: (Int, BaseItem) -> Unit,
|
||||
sortOptions: List<ItemSortBy>,
|
||||
playEnabled: Boolean,
|
||||
defaultViewOptions: ViewOptions,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: CollectionFolderViewModel = hiltViewModel(),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
initialSortAndDirection: SortAndDirection? = null,
|
||||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
params: CollectionFolderGridParameters = CollectionFolderGridParameters(),
|
||||
useSeriesForPrimary: Boolean = true,
|
||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||
) {
|
||||
|
|
@ -505,6 +519,7 @@ fun CollectionFolderGrid(
|
|||
recursive,
|
||||
initialFilter,
|
||||
useSeriesForPrimary,
|
||||
defaultViewOptions,
|
||||
)
|
||||
}
|
||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState(SortAndDirection.DEFAULT)
|
||||
|
|
@ -513,6 +528,7 @@ fun CollectionFolderGrid(
|
|||
val backgroundLoading by viewModel.backgroundLoading.observeAsState(LoadingState.Loading)
|
||||
val item by viewModel.item.observeAsState()
|
||||
val pager by viewModel.pager.observeAsState()
|
||||
val viewOptions by viewModel.viewOptions.observeAsState(defaultViewOptions)
|
||||
|
||||
var moreDialog by remember { mutableStateOf<Optional<PositionItem>>(Optional.absent()) }
|
||||
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||
|
|
@ -551,7 +567,9 @@ fun CollectionFolderGrid(
|
|||
sortOptions = sortOptions,
|
||||
positionCallback = positionCallback,
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
params = params,
|
||||
viewOptions = viewOptions,
|
||||
defaultViewOptions = defaultViewOptions,
|
||||
onSaveViewOptions = { viewModel.saveViewOptions(it) },
|
||||
playEnabled = playEnabled,
|
||||
onClickPlay = { shuffle ->
|
||||
itemId.toUUIDOrNull()?.let {
|
||||
|
|
@ -656,117 +674,174 @@ fun CollectionFolderGridContent(
|
|||
modifier: Modifier = Modifier,
|
||||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
params: CollectionFolderGridParameters = CollectionFolderGridParameters(),
|
||||
currentFilter: GetItemsFilter = GetItemsFilter(),
|
||||
filterOptions: List<ItemFilterBy<*>> = listOf(),
|
||||
onFilterChange: (GetItemsFilter) -> Unit = {},
|
||||
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
|
||||
defaultViewOptions: ViewOptions,
|
||||
viewOptions: ViewOptions,
|
||||
onSaveViewOptions: (ViewOptions) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val title = item?.name ?: item?.data?.collectionType?.name ?: stringResource(R.string.collection)
|
||||
|
||||
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||
var showViewOptions by rememberSaveable { mutableStateOf(false) }
|
||||
var viewOptions by remember { mutableStateOf(viewOptions) }
|
||||
|
||||
val gridFocusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() }
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
showHeader,
|
||||
enter = slideInVertically() + fadeIn(),
|
||||
exit = slideOutVertically() + fadeOut(),
|
||||
var backdropImageUrl by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var position by rememberInt(0)
|
||||
val focusedItem = pager.getOrNull(position)
|
||||
|
||||
Box(modifier = modifier) {
|
||||
if (viewOptions.showDetails) {
|
||||
DelayedDetailsBackdropImage(
|
||||
item = focusedItem,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
AnimatedVisibility(
|
||||
showHeader,
|
||||
enter = slideInVertically() + fadeIn(),
|
||||
exit = slideOutVertically() + fadeOut(),
|
||||
) {
|
||||
if (showTitle) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
val endPadding =
|
||||
16.dp + if (sortAndDirection.sort == ItemSortBy.SORT_NAME) 24.dp else 0.dp
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp, end = endPadding)
|
||||
.fillMaxWidth(),
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
if (sortOptions.isNotEmpty() || filterOptions.isNotEmpty()) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
if (sortOptions.isNotEmpty()) {
|
||||
SortByButton(
|
||||
sortOptions = sortOptions,
|
||||
current = sortAndDirection,
|
||||
onSortChange = onSortChange,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
if (filterOptions.isNotEmpty()) {
|
||||
FilterByButton(
|
||||
filterOptions = filterOptions,
|
||||
current = currentFilter,
|
||||
onFilterChange = onFilterChange,
|
||||
getPossibleValues = getPossibleFilterValues,
|
||||
if (showTitle) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
val endPadding =
|
||||
16.dp + if (sortAndDirection.sort == ItemSortBy.SORT_NAME) 24.dp else 0.dp
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp, end = endPadding)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
if (sortOptions.isNotEmpty() || filterOptions.isNotEmpty()) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
if (sortOptions.isNotEmpty()) {
|
||||
SortByButton(
|
||||
sortOptions = sortOptions,
|
||||
current = sortAndDirection,
|
||||
onSortChange = onSortChange,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
if (filterOptions.isNotEmpty()) {
|
||||
FilterByButton(
|
||||
filterOptions = filterOptions,
|
||||
current = currentFilter,
|
||||
onFilterChange = onFilterChange,
|
||||
getPossibleValues = getPossibleFilterValues,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
ExpandableFaButton(
|
||||
title = R.string.view_options,
|
||||
iconStringRes = R.string.fa_sliders,
|
||||
onClick = { showViewOptions = true },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (playEnabled) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
ExpandablePlayButton(
|
||||
title = R.string.play,
|
||||
resume = Duration.ZERO,
|
||||
icon = Icons.Default.PlayArrow,
|
||||
onClick = { onClickPlay.invoke(false) },
|
||||
)
|
||||
ExpandableFaButton(
|
||||
title = R.string.shuffle,
|
||||
iconStringRes = R.string.fa_shuffle,
|
||||
onClick = { onClickPlay.invoke(true) },
|
||||
)
|
||||
if (playEnabled) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
ExpandablePlayButton(
|
||||
title = R.string.play,
|
||||
resume = Duration.ZERO,
|
||||
icon = Icons.Default.PlayArrow,
|
||||
onClick = { onClickPlay.invoke(false) },
|
||||
)
|
||||
ExpandableFaButton(
|
||||
title = R.string.shuffle,
|
||||
iconStringRes = R.string.fa_shuffle,
|
||||
onClick = { onClickPlay.invoke(true) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(viewOptions.showDetails) {
|
||||
HomePageHeader(
|
||||
item = focusedItem,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(.6f)
|
||||
// .fillMaxHeight(.25f)
|
||||
.height(140.dp)
|
||||
.padding(16.dp),
|
||||
)
|
||||
}
|
||||
CardGrid(
|
||||
pager = pager,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
letterPosition = letterPosition,
|
||||
gridFocusRequester = gridFocusRequester,
|
||||
showJumpButtons = false, // TODO add preference
|
||||
showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
initialPosition = 0,
|
||||
positionCallback = { columns, newPosition ->
|
||||
showHeader = newPosition < columns
|
||||
position = newPosition
|
||||
positionCallback?.invoke(columns, newPosition)
|
||||
},
|
||||
cardContent = { item, onClick, onLongClick, mod ->
|
||||
GridCard(
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
imageContentScale = viewOptions.contentScale.scale,
|
||||
imageAspectRatio = viewOptions.aspectRatio.ratio,
|
||||
imageType = viewOptions.imageType,
|
||||
modifier = mod,
|
||||
)
|
||||
},
|
||||
columns = viewOptions.columns,
|
||||
spacing = viewOptions.spacing.dp,
|
||||
)
|
||||
AnimatedVisibility(showViewOptions) {
|
||||
ViewOptionsDialog(
|
||||
viewOptions = viewOptions,
|
||||
defaultViewOptions = defaultViewOptions,
|
||||
onDismissRequest = {
|
||||
showViewOptions = false
|
||||
onSaveViewOptions.invoke(viewOptions)
|
||||
},
|
||||
onViewOptionsChange = { viewOptions = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
CardGrid(
|
||||
pager = pager,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
letterPosition = letterPosition,
|
||||
gridFocusRequester = gridFocusRequester,
|
||||
showJumpButtons = false, // TODO add preference
|
||||
showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
initialPosition = 0,
|
||||
positionCallback = { columns, position ->
|
||||
showHeader = position < columns
|
||||
positionCallback?.invoke(columns, position)
|
||||
},
|
||||
cardContent = params.cardContent,
|
||||
columns = params.columns,
|
||||
spacing = params.spacing,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ package com.github.damontecres.wholphin.ui.components
|
|||
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.draw.alpha
|
||||
|
|
@ -10,9 +16,35 @@ import androidx.compose.ui.draw.drawWithContent
|
|||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transitionFactory
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.CrossFadeFactory
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun BoxScope.DetailsBackdropImage(
|
||||
item: BaseItem?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val imageUrlService = LocalImageUrlService.current
|
||||
val backdropImageUrl =
|
||||
remember(item) {
|
||||
if (item != null) {
|
||||
imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
DetailsBackdropImage(backdropImageUrl, modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.DetailsBackdropImage(
|
||||
|
|
@ -50,3 +82,73 @@ fun BoxScope.DetailsBackdropImage(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.DelayedDetailsBackdropImage(
|
||||
item: BaseItem?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val imageUrlService = LocalImageUrlService.current
|
||||
val backdropImageUrl =
|
||||
remember(item) {
|
||||
if (item != null) {
|
||||
imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
DelayedDetailsBackdropImage(backdropImageUrl, modifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a backdrop image, but with a crossfade & delay
|
||||
*
|
||||
* Used for change backdrops when change items frequently
|
||||
*/
|
||||
@Composable
|
||||
fun BoxScope.DelayedDetailsBackdropImage(
|
||||
focusedBackdropImageUrl: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var backdropImageUrl by remember { mutableStateOf<String?>(null) }
|
||||
LaunchedEffect(focusedBackdropImageUrl) {
|
||||
backdropImageUrl = null
|
||||
delay(150)
|
||||
backdropImageUrl = focusedBackdropImageUrl
|
||||
}
|
||||
val gradientColor = MaterialTheme.colorScheme.background
|
||||
AsyncImage(
|
||||
model =
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(backdropImageUrl)
|
||||
.transitionFactory(CrossFadeFactory(250.milliseconds))
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
alignment = Alignment.TopEnd,
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxHeight(.7f)
|
||||
.fillMaxWidth(.7f)
|
||||
.alpha(.75f)
|
||||
.align(Alignment.TopEnd)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(Color.Transparent, gradientColor),
|
||||
startY = size.height * .33f,
|
||||
),
|
||||
)
|
||||
drawRect(
|
||||
Brush.horizontalGradient(
|
||||
colors = listOf(gradientColor, Color.Transparent),
|
||||
startX = 0f,
|
||||
endX = size.width * .5f,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,3 +76,44 @@ fun EpisodeQuickDetails(
|
|||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SeriesQuickDetails(
|
||||
dto: BaseItemDto?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val details =
|
||||
remember(dto) {
|
||||
buildList {
|
||||
if (dto?.productionYear != null) {
|
||||
val date =
|
||||
buildString {
|
||||
append(dto.productionYear.toString())
|
||||
if (dto.status == "Continuing") {
|
||||
append(" - ")
|
||||
append("Present")
|
||||
} else if (dto.status == "Ended") {
|
||||
dto.endDate?.let {
|
||||
append(" - ")
|
||||
append(it.year)
|
||||
}
|
||||
}
|
||||
}
|
||||
add(date)
|
||||
}
|
||||
val duration = dto?.runTimeTicks?.ticks
|
||||
duration
|
||||
?.roundMinutes
|
||||
?.toString()
|
||||
?.let(::add)
|
||||
dto?.officialRating?.let(::add)
|
||||
}
|
||||
}
|
||||
DotSeparatedRow(
|
||||
texts = details,
|
||||
communityRating = dto?.communityRating,
|
||||
criticRating = dto?.criticRating,
|
||||
textStyle = MaterialTheme.typography.titleSmall,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,232 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import android.view.Gravity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.preferences.AppChoicePreference
|
||||
import com.github.damontecres.wholphin.preferences.AppClickablePreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppSliderPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppSwitchPreference
|
||||
import com.github.damontecres.wholphin.preferences.PrefContentScale
|
||||
import com.github.damontecres.wholphin.ui.AspectRatio
|
||||
import com.github.damontecres.wholphin.ui.preferences.ComposablePreference
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
@Composable
|
||||
fun ViewOptionsDialog(
|
||||
viewOptions: ViewOptions,
|
||||
onDismissRequest: () -> Unit,
|
||||
onViewOptionsChange: (ViewOptions) -> Unit,
|
||||
defaultViewOptions: ViewOptions = ViewOptions(),
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
val columnState = rememberLazyListState()
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties =
|
||||
DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
),
|
||||
) {
|
||||
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
|
||||
dialogWindowProvider?.window?.let { window ->
|
||||
window.setGravity(Gravity.END)
|
||||
window.setDimAmount(0f)
|
||||
}
|
||||
LazyColumn(
|
||||
state = columnState,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.width(256.dp)
|
||||
.heightIn(max = 380.dp)
|
||||
.focusRequester(focusRequester)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
),
|
||||
) {
|
||||
stickyHeader {
|
||||
Text(
|
||||
text = stringResource(R.string.view_options),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
items(ViewOptions.OPTIONS) { pref ->
|
||||
pref as AppPreference<ViewOptions, Any>
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val value = pref.getter.invoke(viewOptions)
|
||||
ComposablePreference(
|
||||
preference = pref,
|
||||
value = value,
|
||||
onNavigate = {},
|
||||
onValueChange = { newValue ->
|
||||
onViewOptionsChange.invoke(pref.setter(viewOptions, newValue))
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = Modifier,
|
||||
onClickPreference = { pref ->
|
||||
if (pref == ViewOptions.ViewOptionsReset) {
|
||||
onViewOptionsChange.invoke(defaultViewOptions)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ViewOptions(
|
||||
val columns: Int = 6,
|
||||
val spacing: Int = 16,
|
||||
val contentScale: PrefContentScale = PrefContentScale.FIT,
|
||||
val aspectRatio: AspectRatio = AspectRatio.TALL,
|
||||
val showDetails: Boolean = false,
|
||||
val imageType: ViewOptionImageType = ViewOptionImageType.PRIMARY,
|
||||
) {
|
||||
companion object {
|
||||
val ViewOptionsColumns =
|
||||
AppSliderPreference<ViewOptions>(
|
||||
title = R.string.columns,
|
||||
defaultValue = 6,
|
||||
min = 1,
|
||||
max = 12,
|
||||
interval = 1,
|
||||
getter = { it.columns.toLong() },
|
||||
setter = { prefs, value -> prefs.copy(columns = value.toInt()) },
|
||||
)
|
||||
val ViewOptionsSpacing =
|
||||
AppSliderPreference<ViewOptions>(
|
||||
title = R.string.spacing,
|
||||
defaultValue = 16,
|
||||
min = 0,
|
||||
max = 32,
|
||||
interval = 2,
|
||||
getter = { it.spacing.toLong() },
|
||||
setter = { prefs, value -> prefs.copy(spacing = value.toInt()) },
|
||||
)
|
||||
|
||||
val ViewOptionsContentScale =
|
||||
AppChoicePreference<ViewOptions, PrefContentScale>(
|
||||
title = R.string.global_content_scale,
|
||||
defaultValue = PrefContentScale.FIT,
|
||||
displayValues = R.array.content_scale,
|
||||
getter = { it.contentScale },
|
||||
setter = { viewOptions, value -> viewOptions.copy(contentScale = value) },
|
||||
indexToValue = { PrefContentScale.forNumber(it) },
|
||||
valueToIndex = { it.number },
|
||||
)
|
||||
|
||||
val ViewOptionsAspectRatio =
|
||||
AppChoicePreference<ViewOptions, AspectRatio>(
|
||||
title = R.string.aspect_ratio,
|
||||
defaultValue = AspectRatio.TALL,
|
||||
displayValues = R.array.aspect_ratios,
|
||||
getter = { it.aspectRatio },
|
||||
setter = { viewOptions, value -> viewOptions.copy(aspectRatio = value) },
|
||||
indexToValue = { AspectRatio.entries[it] },
|
||||
valueToIndex = { it.ordinal },
|
||||
)
|
||||
|
||||
val ViewOptionsDetailHeader =
|
||||
AppSwitchPreference<ViewOptions>(
|
||||
title = R.string.show_details,
|
||||
defaultValue = false,
|
||||
getter = { it.showDetails },
|
||||
setter = { vo, value -> vo.copy(showDetails = value) },
|
||||
)
|
||||
|
||||
val ViewOptionsImageType =
|
||||
AppChoicePreference<ViewOptions, ViewOptionImageType>(
|
||||
title = R.string.image_type,
|
||||
defaultValue = ViewOptionImageType.PRIMARY,
|
||||
displayValues = R.array.image_types,
|
||||
getter = { it.imageType },
|
||||
setter = { viewOptions, value ->
|
||||
val aspectRatio =
|
||||
when (value) {
|
||||
ViewOptionImageType.PRIMARY -> AspectRatio.TALL
|
||||
ViewOptionImageType.THUMB -> AspectRatio.WIDE
|
||||
}
|
||||
viewOptions.copy(imageType = value, aspectRatio = aspectRatio)
|
||||
},
|
||||
indexToValue = { ViewOptionImageType.entries[it] },
|
||||
valueToIndex = { it.ordinal },
|
||||
)
|
||||
|
||||
val ViewOptionsReset =
|
||||
AppClickablePreference<ViewOptions>(
|
||||
title = R.string.reset,
|
||||
)
|
||||
|
||||
val OPTIONS =
|
||||
listOf(
|
||||
ViewOptionsImageType,
|
||||
ViewOptionsAspectRatio,
|
||||
ViewOptionsDetailHeader,
|
||||
ViewOptionsColumns,
|
||||
ViewOptionsSpacing,
|
||||
ViewOptionsContentScale,
|
||||
ViewOptionsReset,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val ViewOptionsPoster =
|
||||
ViewOptions(
|
||||
columns = 6,
|
||||
spacing = 16,
|
||||
contentScale = PrefContentScale.FILL,
|
||||
)
|
||||
val ViewOptionsWide =
|
||||
ViewOptions(
|
||||
columns = 4,
|
||||
spacing = 24,
|
||||
contentScale = PrefContentScale.CROP,
|
||||
aspectRatio = AspectRatio.WIDE,
|
||||
)
|
||||
val ViewOptionsSquare =
|
||||
ViewOptions(
|
||||
columns = 6,
|
||||
spacing = 16,
|
||||
contentScale = PrefContentScale.FILL,
|
||||
aspectRatio = AspectRatio.SQUARE,
|
||||
)
|
||||
|
||||
enum class ViewOptionImageType(
|
||||
val imageType: ImageType,
|
||||
) {
|
||||
PRIMARY(ImageType.PRIMARY),
|
||||
THUMB(ImageType.THUMB),
|
||||
// BANNER(ImageType.BANNER),
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
|||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGridParameters
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||
import com.github.damontecres.wholphin.ui.data.BoxSetSortOptions
|
||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||
|
|
@ -48,7 +48,7 @@ fun CollectionFolderBoxSet(
|
|||
positionCallback = { columns, position ->
|
||||
showHeader = position < columns
|
||||
},
|
||||
params = CollectionFolderGridParameters.POSTER,
|
||||
defaultViewOptions = ViewOptionsPoster,
|
||||
playEnabled = playEnabled,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
|||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGridParameters
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsWide
|
||||
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
|
@ -34,12 +35,12 @@ fun CollectionFolderGeneric(
|
|||
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||
) {
|
||||
var showHeader by remember { mutableStateOf(true) }
|
||||
val params =
|
||||
val viewOptions =
|
||||
remember(usePosters) {
|
||||
if (usePosters) {
|
||||
CollectionFolderGridParameters.POSTER
|
||||
ViewOptionsPoster
|
||||
} else {
|
||||
CollectionFolderGridParameters.WIDE
|
||||
ViewOptionsWide
|
||||
}
|
||||
}
|
||||
CollectionFolderGrid(
|
||||
|
|
@ -56,7 +57,7 @@ fun CollectionFolderGeneric(
|
|||
positionCallback = { columns, position ->
|
||||
showHeader = position < columns
|
||||
},
|
||||
params = params,
|
||||
defaultViewOptions = viewOptions,
|
||||
playEnabled = playEnabled,
|
||||
filterOptions = filterOptions,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import com.github.damontecres.wholphin.services.NavigationManager
|
|||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.TabRow
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptions
|
||||
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||
import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule
|
||||
import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid
|
||||
|
|
@ -170,6 +171,7 @@ fun CollectionFolderLiveTv(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
defaultViewOptions = ViewOptions(),
|
||||
)
|
||||
} else {
|
||||
ErrorMessage("Invalid tab index $selectedTabIndex", null)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
|||
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
||||
import com.github.damontecres.wholphin.ui.components.RecommendedMovie
|
||||
import com.github.damontecres.wholphin.ui.components.TabRow
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
||||
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||
import com.github.damontecres.wholphin.ui.logTab
|
||||
|
|
@ -118,6 +119,7 @@ fun CollectionFolderMovie(
|
|||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = MovieSortOptions,
|
||||
defaultViewOptions = ViewOptionsPoster,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
@ -144,6 +146,7 @@ fun CollectionFolderMovie(
|
|||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = VideoSortOptions,
|
||||
defaultViewOptions = ViewOptionsPoster,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
|||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGridParameters
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare
|
||||
import com.github.damontecres.wholphin.ui.data.PlaylistSortOptions
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||
import java.util.UUID
|
||||
|
|
@ -43,7 +43,7 @@ fun CollectionFolderPlaylist(
|
|||
positionCallback = { columns, position ->
|
||||
showHeader = position < columns
|
||||
},
|
||||
params = CollectionFolderGridParameters.SQUARE,
|
||||
defaultViewOptions = ViewOptionsSquare,
|
||||
playEnabled = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGridParameters
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||
import java.util.UUID
|
||||
|
|
@ -41,7 +41,7 @@ fun CollectionFolderRecordings(
|
|||
positionCallback = { columns, position ->
|
||||
showHeader = position < columns
|
||||
},
|
||||
params = CollectionFolderGridParameters.POSTER,
|
||||
defaultViewOptions = ViewOptionsPoster,
|
||||
playEnabled = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
|||
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
||||
import com.github.damontecres.wholphin.ui.components.RecommendedTvShow
|
||||
import com.github.damontecres.wholphin.ui.components.TabRow
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
||||
import com.github.damontecres.wholphin.ui.logTab
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
|
|
@ -118,6 +119,7 @@ fun CollectionFolderTv(
|
|||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = SeriesSortOptions,
|
||||
defaultViewOptions = ViewOptionsPoster,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
|
|||
|
|
@ -29,9 +29,11 @@ import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
|||
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGridParameters
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.TabRow
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsWide
|
||||
import com.github.damontecres.wholphin.ui.data.EpisodeSortOptions
|
||||
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
||||
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
||||
|
|
@ -121,6 +123,7 @@ fun FavoritesPage(
|
|||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = MovieSortOptions,
|
||||
defaultViewOptions = ViewOptionsPoster,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
@ -147,6 +150,7 @@ fun FavoritesPage(
|
|||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = SeriesSortOptions,
|
||||
defaultViewOptions = ViewOptionsPoster,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
@ -173,7 +177,7 @@ fun FavoritesPage(
|
|||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = EpisodeSortOptions,
|
||||
params = CollectionFolderGridParameters.WIDE,
|
||||
defaultViewOptions = ViewOptionsWide,
|
||||
useSeriesForPrimary = false,
|
||||
modifier =
|
||||
Modifier
|
||||
|
|
@ -201,7 +205,7 @@ fun FavoritesPage(
|
|||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = VideoSortOptions,
|
||||
params = CollectionFolderGridParameters.WIDE,
|
||||
defaultViewOptions = ViewOptionsWide,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
@ -228,7 +232,7 @@ fun FavoritesPage(
|
|||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = VideoSortOptions,
|
||||
params = CollectionFolderGridParameters.SQUARE,
|
||||
defaultViewOptions = ViewOptionsSquare,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
@ -260,6 +264,7 @@ fun FavoritesPage(
|
|||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = listOf(),
|
||||
defaultViewOptions = ViewOptionsPoster,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
|
|
@ -69,6 +70,7 @@ import com.github.damontecres.wholphin.util.RowLoadingState
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
|
|
@ -185,11 +187,13 @@ fun PersonPage(
|
|||
person?.let { person ->
|
||||
var showOverviewDialog by remember { mutableStateOf(false) }
|
||||
val name = person.name ?: person.id.toString()
|
||||
val imageUrlService = LocalImageUrlService.current
|
||||
val imageUrl = remember { imageUrlService.getItemImageUrl(itemId = person.id, imageType = ImageType.PRIMARY) }
|
||||
PersonPageContent(
|
||||
preferences = preferences,
|
||||
name = name,
|
||||
overview = person.data.overview,
|
||||
imageUrl = person.imageUrl,
|
||||
imageUrl = imageUrl,
|
||||
birthdate = person.data.premiereDate?.toLocalDate(),
|
||||
deathdate = person.data.endDate?.toLocalDate(),
|
||||
birthPlace = person.data.productionLocations?.firstOrNull(),
|
||||
|
|
|
|||
|
|
@ -32,16 +32,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -53,12 +48,12 @@ import androidx.tv.material3.ListItem
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemCardImage
|
||||
import com.github.damontecres.wholphin.ui.components.DelayedDetailsBackdropImage
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
|
|
@ -69,7 +64,6 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage
|
|||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
|
@ -224,35 +218,7 @@ fun PlaylistDetailsContent(
|
|||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
if (focusedItem?.backdropImageUrl.isNotNullOrBlank()) {
|
||||
val gradientColor = MaterialTheme.colorScheme.background
|
||||
AsyncImage(
|
||||
model = focusedItem.backdropImageUrl,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
alignment = Alignment.TopEnd,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxHeight(.85f)
|
||||
.alpha(.4f)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(Color.Transparent, gradientColor),
|
||||
startY = 500f,
|
||||
),
|
||||
)
|
||||
drawRect(
|
||||
Brush.horizontalGradient(
|
||||
colors = listOf(gradientColor, Color.Transparent),
|
||||
endX = 400f,
|
||||
startX = 100f,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
DelayedDetailsBackdropImage(focusedItem)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier =
|
||||
|
|
@ -438,7 +404,7 @@ fun PlaylistItem(
|
|||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
ItemCardImage(
|
||||
imageUrl = item?.imageUrl,
|
||||
item = item,
|
||||
name = item?.name,
|
||||
showOverlay = true,
|
||||
favorite = item?.data?.userData?.isFavorite ?: false,
|
||||
|
|
|
|||
|
|
@ -271,7 +271,6 @@ fun EpisodeDetailsContent(
|
|||
var position by rememberInt(0)
|
||||
val focusRequesters = remember { List(1) { FocusRequester() } }
|
||||
val dto = ep.data
|
||||
val backdropImageUrl = ep.backdropImageUrl
|
||||
val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
|
||||
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
|
|
@ -279,7 +278,7 @@ fun EpisodeDetailsContent(
|
|||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
DetailsBackdropImage(backdropImageUrl)
|
||||
DetailsBackdropImage(ep)
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
||||
|
|
|
|||
|
|
@ -371,7 +371,6 @@ fun MovieDetailsContent(
|
|||
var position by rememberInt(0)
|
||||
val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
|
||||
val dto = movie.data
|
||||
val backdropImageUrl = movie.backdropImageUrl
|
||||
val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
|
||||
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
|
|
@ -379,7 +378,7 @@ fun MovieDetailsContent(
|
|||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
DetailsBackdropImage(backdropImageUrl)
|
||||
DetailsBackdropImage(movie)
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ fun SeriesDetailsContent(
|
|||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
DetailsBackdropImage(series.backdropImageUrl)
|
||||
DetailsBackdropImage(series)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ fun SeriesOverviewContent(
|
|||
?: episode?.data?.premiereDate?.let(::formatDateTime)
|
||||
BannerCard(
|
||||
name = episode?.name,
|
||||
imageUrl = episode?.imageUrl,
|
||||
item = episode,
|
||||
aspectRatio =
|
||||
episode
|
||||
?.data
|
||||
|
|
|
|||
|
|
@ -28,15 +28,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -45,24 +41,22 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transitionFactory
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.CrossFadeFactory
|
||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||
import com.github.damontecres.wholphin.ui.components.DelayedDetailsBackdropImage
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.EpisodeQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.MovieQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.SeriesQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
|
||||
|
|
@ -79,7 +73,6 @@ import kotlinx.coroutines.delay
|
|||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun HomePage(
|
||||
|
|
@ -242,46 +235,10 @@ fun HomePageContent(
|
|||
LaunchedEffect(position) {
|
||||
listState.animateScrollToItem(position.row)
|
||||
}
|
||||
var backdropImageUrl by remember { mutableStateOf<String?>(null) }
|
||||
LaunchedEffect(focusedItem) {
|
||||
backdropImageUrl = null
|
||||
delay(150)
|
||||
backdropImageUrl = focusedItem?.backdropImageUrl
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
val gradientColor = MaterialTheme.colorScheme.background
|
||||
AsyncImage(
|
||||
model =
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(backdropImageUrl)
|
||||
.transitionFactory(CrossFadeFactory(250.milliseconds))
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
alignment = Alignment.TopEnd,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxHeight(.7f)
|
||||
.fillMaxWidth(.7f)
|
||||
.alpha(.75f)
|
||||
.align(Alignment.TopEnd)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(Color.Transparent, gradientColor),
|
||||
startY = size.height * .33f,
|
||||
),
|
||||
)
|
||||
drawRect(
|
||||
Brush.horizontalGradient(
|
||||
colors = listOf(gradientColor, Color.Transparent),
|
||||
startX = 0f,
|
||||
endX = size.width * .5f,
|
||||
),
|
||||
)
|
||||
},
|
||||
DelayedDetailsBackdropImage(
|
||||
item = focusedItem,
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
|
@ -382,7 +339,7 @@ fun HomePageContent(
|
|||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||
BannerCard(
|
||||
name = item?.data?.seriesName ?: item?.name,
|
||||
imageUrl = item?.imageUrl,
|
||||
item = item,
|
||||
aspectRatio = AspectRatios.TALL,
|
||||
cornerText =
|
||||
item?.data?.indexNumber?.let { "E$it" }
|
||||
|
|
@ -486,10 +443,10 @@ fun HomePageHeader(
|
|||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (isEpisode) {
|
||||
EpisodeQuickDetails(dto, Modifier)
|
||||
} else {
|
||||
MovieQuickDetails(dto, Modifier)
|
||||
when (item.type) {
|
||||
BaseItemKind.EPISODE -> EpisodeQuickDetails(dto, Modifier)
|
||||
BaseItemKind.SERIES -> SeriesQuickDetails(dto, Modifier)
|
||||
else -> MovieQuickDetails(dto, Modifier)
|
||||
}
|
||||
val overviewModifier =
|
||||
Modifier
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ import androidx.tv.material3.DrawerValue
|
|||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.LocalContentColor
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.NavigationDrawer
|
||||
import androidx.tv.material3.ModalNavigationDrawer
|
||||
import androidx.tv.material3.NavigationDrawerItem
|
||||
import androidx.tv.material3.NavigationDrawerItemDefaults
|
||||
import androidx.tv.material3.NavigationDrawerScope
|
||||
|
|
@ -295,7 +295,8 @@ fun NavDrawer(
|
|||
}
|
||||
}
|
||||
|
||||
val drawerWidth by animateDpAsState(if (drawerState.isOpen) 260.dp else 40.dp)
|
||||
val closedDrawerWidth = 40.dp
|
||||
val drawerWidth by animateDpAsState(if (drawerState.isOpen) 260.dp else closedDrawerWidth)
|
||||
val drawerPadding by animateDpAsState(if (drawerState.isOpen) 0.dp else 8.dp)
|
||||
val drawerBackground by animateColorAsState(
|
||||
if (drawerState.isOpen) {
|
||||
|
|
@ -329,7 +330,7 @@ fun NavDrawer(
|
|||
scrollToSelected()
|
||||
}
|
||||
|
||||
NavigationDrawer(
|
||||
ModalNavigationDrawer(
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
|
|
@ -518,7 +519,10 @@ fun NavDrawer(
|
|||
},
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = closedDrawerWidth)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
// Drawer content
|
||||
DestinationContent(
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import com.github.damontecres.wholphin.data.model.Playlist
|
|||
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.TimeFormatter
|
||||
import com.github.damontecres.wholphin.ui.cards.ChapterCard
|
||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||
|
|
@ -62,6 +63,7 @@ import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
|||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||
import java.time.LocalTime
|
||||
|
|
@ -381,14 +383,15 @@ fun PlaybackOverlay(
|
|||
}
|
||||
}
|
||||
}
|
||||
val logoImageUrl = LocalImageUrlService.current.rememberImageUrl(item, ImageType.LOGO)
|
||||
AnimatedVisibility(
|
||||
!showDebugInfo && item?.logoImageUrl.isNotNullOrBlank() && controllerViewState.controlsVisible,
|
||||
!showDebugInfo && logoImageUrl.isNotNullOrBlank() && controllerViewState.controlsVisible,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.TopStart),
|
||||
) {
|
||||
AsyncImage(
|
||||
model = item?.logoImageUrl,
|
||||
model = logoImageUrl,
|
||||
contentDescription = "Logo",
|
||||
modifier =
|
||||
Modifier
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ import com.github.damontecres.wholphin.preferences.PlayerBackend
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.preferences.skipBackOnResume
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
|
|
@ -459,7 +460,7 @@ fun PlaybackPage(
|
|||
it.name,
|
||||
).joinToString(" - "),
|
||||
description = it.data.overview,
|
||||
imageUrl = it.imageUrl,
|
||||
imageUrl = LocalImageUrlService.current.rememberImageUrl(it),
|
||||
aspectRatio =
|
||||
it.data.primaryImageAspectRatio?.toFloat()
|
||||
?: AspectRatios.WIDE,
|
||||
|
|
|
|||
|
|
@ -57,12 +57,13 @@ import java.util.SortedSet
|
|||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun <T> ComposablePreference(
|
||||
preference: AppPreference<T>,
|
||||
preference: AppPreference<*, T>,
|
||||
value: T?,
|
||||
onValueChange: (T) -> Unit,
|
||||
onNavigate: (Destination) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
onClickPreference: (AppClickablePreference<*>) -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -72,13 +73,6 @@ fun <T> ComposablePreference(
|
|||
|
||||
val title = stringResource(preference.title)
|
||||
|
||||
val onClick: () -> Unit = {
|
||||
scope.launch(ExceptionHandler()) {
|
||||
when (preference) {
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
val onLongClick: () -> Unit = {
|
||||
scope.launch(ExceptionHandler()) {
|
||||
when (preference) {
|
||||
|
|
@ -102,7 +96,7 @@ fun <T> ComposablePreference(
|
|||
is AppClickablePreference ->
|
||||
ClickPreference(
|
||||
title = title,
|
||||
onClick = onClick,
|
||||
onClick = { onClickPreference.invoke(preference) },
|
||||
onLongClick = onLongClick,
|
||||
summary = preference.summary(context, value),
|
||||
interactionSource = interactionSource,
|
||||
|
|
@ -196,7 +190,7 @@ fun <T> ComposablePreference(
|
|||
)
|
||||
}
|
||||
|
||||
is AppMultiChoicePreference<*> -> {
|
||||
is AppMultiChoicePreference<*, *> -> {
|
||||
val values = stringArrayResource(preference.displayValues).toSortedSet()
|
||||
val summary =
|
||||
preference.summary?.let { stringResource(it) }
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import kotlinx.serialization.Serializable
|
|||
*/
|
||||
data class PreferenceGroup(
|
||||
@param:StringRes val title: Int,
|
||||
val preferences: List<AppPreference<out Any?>>,
|
||||
val preferences: List<AppPreference<AppPreferences, out Any?>>,
|
||||
val conditionalPreferences: List<ConditionalPreferences> = listOf(),
|
||||
)
|
||||
|
||||
data class ConditionalPreferences(
|
||||
val condition: (AppPreferences) -> Boolean,
|
||||
val preferences: List<AppPreference<out Any?>>,
|
||||
val preferences: List<AppPreference<AppPreferences, out Any?>>,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ fun PreferencesContent(
|
|||
.map { it.preferences }
|
||||
.flatten()
|
||||
groupPreferences.forEachIndexed { prefIndex, pref ->
|
||||
pref as AppPreference<Any>
|
||||
pref as AppPreference<AppPreferences, Any>
|
||||
item {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused = interactionSource.collectIsFocusedAsState().value
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import com.github.damontecres.wholphin.ui.components.SliderBar
|
|||
|
||||
@Composable
|
||||
fun SliderPreference(
|
||||
preference: AppSliderPreference,
|
||||
preference: AppSliderPreference<*>,
|
||||
title: String,
|
||||
summary: String?,
|
||||
value: Long,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.media3.ui.CaptionStyleCompat
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.preferences.AppChoicePreference
|
||||
import com.github.damontecres.wholphin.preferences.AppClickablePreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.AppSliderPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppSwitchPreference
|
||||
import com.github.damontecres.wholphin.preferences.BackgroundStyle
|
||||
|
|
@ -27,7 +28,7 @@ import timber.log.Timber
|
|||
|
||||
object SubtitleSettings {
|
||||
val FontSize =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.font_size,
|
||||
defaultValue = 24,
|
||||
min = 8,
|
||||
|
|
@ -58,7 +59,7 @@ object SubtitleSettings {
|
|||
)
|
||||
|
||||
val FontColor =
|
||||
AppChoicePreference<Color>(
|
||||
AppChoicePreference<AppPreferences, Color>(
|
||||
title = R.string.font_color,
|
||||
defaultValue = Color.White,
|
||||
getter = { Color(it.interfacePreferences.subtitlesPreferences.fontColor) },
|
||||
|
|
@ -74,7 +75,7 @@ object SubtitleSettings {
|
|||
)
|
||||
|
||||
val FontBold =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.bold_font,
|
||||
defaultValue = false,
|
||||
getter = { it.interfacePreferences.subtitlesPreferences.fontBold },
|
||||
|
|
@ -83,7 +84,7 @@ object SubtitleSettings {
|
|||
},
|
||||
)
|
||||
val FontItalic =
|
||||
AppSwitchPreference(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.italic_font,
|
||||
defaultValue = false,
|
||||
getter = { it.interfacePreferences.subtitlesPreferences.fontItalic },
|
||||
|
|
@ -93,7 +94,7 @@ object SubtitleSettings {
|
|||
)
|
||||
|
||||
val FontOpacity =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.font_opacity,
|
||||
defaultValue = 100,
|
||||
min = 10,
|
||||
|
|
@ -110,7 +111,7 @@ object SubtitleSettings {
|
|||
)
|
||||
|
||||
val EdgeStylePref =
|
||||
AppChoicePreference<EdgeStyle>(
|
||||
AppChoicePreference<AppPreferences, EdgeStyle>(
|
||||
title =
|
||||
R.string.edge_style,
|
||||
defaultValue = EdgeStyle.EDGE_SOLID,
|
||||
|
|
@ -124,7 +125,7 @@ object SubtitleSettings {
|
|||
)
|
||||
|
||||
val EdgeColor =
|
||||
AppChoicePreference<Color>(
|
||||
AppChoicePreference<AppPreferences, Color>(
|
||||
title = R.string.edge_color,
|
||||
defaultValue = Color.Black,
|
||||
getter = { Color(it.interfacePreferences.subtitlesPreferences.edgeColor) },
|
||||
|
|
@ -140,7 +141,7 @@ object SubtitleSettings {
|
|||
)
|
||||
|
||||
val BackgroundColor =
|
||||
AppChoicePreference<Color>(
|
||||
AppChoicePreference<AppPreferences, Color>(
|
||||
title = R.string.background_color,
|
||||
defaultValue = Color.Transparent,
|
||||
getter = { Color(it.interfacePreferences.subtitlesPreferences.backgroundColor) },
|
||||
|
|
@ -156,7 +157,7 @@ object SubtitleSettings {
|
|||
)
|
||||
|
||||
val BackgroundOpacity =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.background_opacity,
|
||||
defaultValue = 50,
|
||||
min = 10,
|
||||
|
|
@ -173,7 +174,7 @@ object SubtitleSettings {
|
|||
)
|
||||
|
||||
val BackgroundStylePref =
|
||||
AppChoicePreference<BackgroundStyle>(
|
||||
AppChoicePreference<AppPreferences, BackgroundStyle>(
|
||||
title =
|
||||
R.string.background_style,
|
||||
defaultValue = BackgroundStyle.BG_NONE,
|
||||
|
|
@ -187,7 +188,7 @@ object SubtitleSettings {
|
|||
)
|
||||
|
||||
val Margin =
|
||||
AppSliderPreference(
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.subtitle_margin,
|
||||
defaultValue = 8,
|
||||
min = 0,
|
||||
|
|
@ -204,7 +205,7 @@ object SubtitleSettings {
|
|||
)
|
||||
|
||||
val Reset =
|
||||
AppClickablePreference(
|
||||
AppClickablePreference<AppPreferences>(
|
||||
title = R.string.reset,
|
||||
getter = { },
|
||||
setter = { prefs, _ -> prefs },
|
||||
|
|
|
|||
|
|
@ -82,23 +82,24 @@ private fun ThemeExample(theme: AppThemeColors) {
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
WatchedIcon()
|
||||
BannerCard(
|
||||
name = "Card",
|
||||
imageUrl = null,
|
||||
onClick = { },
|
||||
onLongClick = {},
|
||||
playPercent = .5,
|
||||
cardHeight = 64.dp,
|
||||
)
|
||||
BannerCard(
|
||||
name = "Card",
|
||||
imageUrl = null,
|
||||
onClick = { },
|
||||
onLongClick = {},
|
||||
playPercent = .5,
|
||||
cardHeight = 64.dp,
|
||||
interactionSource = source,
|
||||
)
|
||||
// TODO
|
||||
// BannerCard(
|
||||
// name = "Card",
|
||||
// imageUrl = null,
|
||||
// onClick = { },
|
||||
// onLongClick = {},
|
||||
// playPercent = .5,
|
||||
// cardHeight = 64.dp,
|
||||
// )
|
||||
// BannerCard(
|
||||
// name = "Card",
|
||||
// imageUrl = null,
|
||||
// onClick = { },
|
||||
// onLongClick = {},
|
||||
// playPercent = .5,
|
||||
// cardHeight = 64.dp,
|
||||
// interactionSource = source,
|
||||
// )
|
||||
SeasonCard(
|
||||
title = "Card",
|
||||
subtitle = "2025",
|
||||
|
|
|
|||
|
|
@ -40,4 +40,5 @@
|
|||
<string name="fa_ellipsis" translatable="false"></string>
|
||||
<string name="fa_ellipsis_vertical" translatable="false"></string>
|
||||
<string name="fa_filter" translatable="false"></string>
|
||||
<string name="fa_sliders" translatable="false"></string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -329,6 +329,12 @@
|
|||
<string name="subtitle_margin">Margin</string>
|
||||
<string name="verbose_logging">Verbose logging</string>
|
||||
<string name="image_cache_size">Image disk cache size (MB)</string>
|
||||
<string name="view_options">View options</string>
|
||||
<string name="columns">Columns</string>
|
||||
<string name="spacing">Spacing</string>
|
||||
<string name="aspect_ratio">Aspect Ratio</string>
|
||||
<string name="show_details">Show details</string>
|
||||
<string name="image_type">Image type</string>
|
||||
|
||||
<string-array name="theme_song_volume">
|
||||
<item>Disabled</item>
|
||||
|
|
@ -404,4 +410,17 @@
|
|||
<item>MPV (Experimental)</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="aspect_ratios">
|
||||
<item>Poster (2:3)</item>
|
||||
<item>16:9</item>
|
||||
<item>4:3</item>
|
||||
<item>Square (1:1)</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="image_types">
|
||||
<item>Primary</item>
|
||||
<item>Thumb</item>
|
||||
<!-- <item>Banner</item>-->
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue