mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Merge branch 'main' into develop/epg-fix
This commit is contained in:
commit
72b3fac44c
65 changed files with 1370 additions and 501 deletions
|
|
@ -0,0 +1,303 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 6,
|
||||||
|
"identityHash": "24390d64259f19e0e67ecfb827b974fb",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "servers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, 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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, 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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, '24390d64259f19e0e67ecfb827b974fb')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,17 +10,21 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
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.data.model.NavDrawerPinnedItem
|
||||||
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class],
|
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
|
||||||
version = 5,
|
version = 6,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(3, 4),
|
AutoMigration(3, 4),
|
||||||
AutoMigration(4, 5),
|
AutoMigration(4, 5),
|
||||||
|
AutoMigration(5, 6),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
@ -30,6 +34,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun itemPlaybackDao(): ItemPlaybackDao
|
abstract fun itemPlaybackDao(): ItemPlaybackDao
|
||||||
|
|
||||||
abstract fun serverPreferencesDao(): ServerPreferencesDao
|
abstract fun serverPreferencesDao(): ServerPreferencesDao
|
||||||
|
|
||||||
|
abstract fun libraryDisplayInfoDao(): LibraryDisplayInfoDao
|
||||||
}
|
}
|
||||||
|
|
||||||
class Converters {
|
class Converters {
|
||||||
|
|
@ -38,6 +44,18 @@ class Converters {
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun convertToUUID(str: String): UUID = str.toUUID()
|
fun convertToUUID(str: String): UUID = str.toUUID()
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun convertItemSortBy(sort: ItemSortBy): String = sort.serialName
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun convertItemSortBy(sort: String): ItemSortBy? = ItemSortBy.fromNameOrNull(sort)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun convertSortOrder(sort: SortOrder): String = sort.serialName
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun convertSortOrder(sort: String): SortOrder? = SortOrder.fromNameOrNull(sort)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Migrations {
|
object Migrations {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.github.damontecres.wholphin.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
|
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface LibraryDisplayInfoDao {
|
||||||
|
fun getItem(
|
||||||
|
user: JellyfinUser,
|
||||||
|
itemId: UUID,
|
||||||
|
): LibraryDisplayInfo? = getItem(user.rowId, itemId)
|
||||||
|
|
||||||
|
@Query("SELECT * from LibraryDisplayInfo WHERE userId=:userId AND itemId=:itemId")
|
||||||
|
fun getItem(
|
||||||
|
userId: Int,
|
||||||
|
itemId: UUID,
|
||||||
|
): LibraryDisplayInfo?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun saveItem(item: LibraryDisplayInfo): Long
|
||||||
|
|
||||||
|
@Query("SELECT * from LibraryDisplayInfo WHERE userId=:userId")
|
||||||
|
fun getItems(userId: Int): List<LibraryDisplayInfo>
|
||||||
|
}
|
||||||
|
|
@ -84,13 +84,15 @@ class ServerRepository
|
||||||
}
|
}
|
||||||
Timber.v("Changing user to ${user.name} on ${server.url}")
|
Timber.v("Changing user to ${user.name} on ${server.url}")
|
||||||
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
|
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
|
||||||
val userDto =
|
val userDto by apiClient.userApi.getCurrentUser()
|
||||||
apiClient.userApi
|
val updatedServer =
|
||||||
.getCurrentUser()
|
try {
|
||||||
.content
|
val sysInfo by apiClient.systemApi.getPublicSystemInfo()
|
||||||
val sysInfo by apiClient.systemApi.getSystemInfo()
|
server.copy(name = sysInfo.serverName)
|
||||||
|
} catch (ex: Exception) {
|
||||||
val updatedServer = server.copy(name = sysInfo.serverName)
|
Timber.w(ex, "Exception fetching public system info")
|
||||||
|
server
|
||||||
|
}
|
||||||
var updatedUser =
|
var updatedUser =
|
||||||
user.copy(
|
user.copy(
|
||||||
id = userDto.id,
|
id = userDto.id,
|
||||||
|
|
@ -171,8 +173,14 @@ class ServerRepository
|
||||||
}
|
}
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
changeUser(server, user)
|
changeUser(server, user)
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Authentication result's user was null")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Authentication result's serverId not valid: ${authenticationResult.serverId}")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Authentication result's access token was null")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
@file:UseSerializers(UUIDSerializer::class)
|
||||||
|
|
||||||
|
package com.github.damontecres.wholphin.data.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import androidx.room.Index
|
||||||
|
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.UseSerializers
|
||||||
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
|
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = JellyfinUser::class,
|
||||||
|
parentColumns = arrayOf("rowId"),
|
||||||
|
childColumns = arrayOf("userId"),
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
onUpdate = ForeignKey.CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
primaryKeys = ["userId", "itemId"],
|
||||||
|
indices = [Index("userId", "itemId", unique = true)],
|
||||||
|
)
|
||||||
|
@Serializable
|
||||||
|
data class LibraryDisplayInfo(
|
||||||
|
val userId: Int,
|
||||||
|
val itemId: UUID,
|
||||||
|
val sort: ItemSortBy,
|
||||||
|
val direction: SortOrder,
|
||||||
|
) {
|
||||||
|
@Ignore @Transient
|
||||||
|
val sortAndDirection = SortAndDirection(sort, direction)
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import com.github.damontecres.wholphin.BuildConfig
|
import com.github.damontecres.wholphin.BuildConfig
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -47,9 +48,11 @@ annotation class IoCoroutineScope
|
||||||
object AppModule {
|
object AppModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun clientInfo(): ClientInfo =
|
fun clientInfo(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
): ClientInfo =
|
||||||
ClientInfo(
|
ClientInfo(
|
||||||
name = "Wholphin",
|
name = context.getString(R.string.app_name),
|
||||||
version = BuildConfig.VERSION_NAME,
|
version = BuildConfig.VERSION_NAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.room.Room
|
||||||
import com.github.damontecres.wholphin.data.AppDatabase
|
import com.github.damontecres.wholphin.data.AppDatabase
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
||||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||||
|
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
||||||
import com.github.damontecres.wholphin.data.Migrations
|
import com.github.damontecres.wholphin.data.Migrations
|
||||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
|
@ -51,6 +52,10 @@ object DatabaseModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun serverPreferencesDao(db: AppDatabase): ServerPreferencesDao = db.serverPreferencesDao()
|
fun serverPreferencesDao(db: AppDatabase): ServerPreferencesDao = db.serverPreferencesDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun libraryDisplayInfoDao(db: AppDatabase): LibraryDisplayInfoDao = db.libraryDisplayInfoDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun userPreferencesDataStore(
|
fun userPreferencesDataStore(
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,11 @@ sealed interface AppPreference<T> {
|
||||||
},
|
},
|
||||||
summarizer = { value ->
|
summarizer = { value ->
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
"$value seconds"
|
WholphinApplication.instance.resources.getQuantityString(
|
||||||
|
R.plurals.seconds,
|
||||||
|
value.toInt(),
|
||||||
|
value.toInt(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +99,11 @@ sealed interface AppPreference<T> {
|
||||||
},
|
},
|
||||||
summarizer = { value ->
|
summarizer = { value ->
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
"$value seconds"
|
WholphinApplication.instance.resources.getQuantityString(
|
||||||
|
R.plurals.seconds,
|
||||||
|
value.toInt(),
|
||||||
|
value.toInt(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +145,14 @@ sealed interface AppPreference<T> {
|
||||||
setter = { prefs, value ->
|
setter = { prefs, value ->
|
||||||
prefs.updatePlaybackPreferences { controllerTimeoutMs = value }
|
prefs.updatePlaybackPreferences { controllerTimeoutMs = value }
|
||||||
},
|
},
|
||||||
summarizer = { value -> value?.let { "${value / 1000.0} seconds" } },
|
summarizer = { value ->
|
||||||
|
value?.let {
|
||||||
|
WholphinApplication.instance.getString(
|
||||||
|
R.string.decimal_seconds,
|
||||||
|
value / 1000.0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
val SeekBarSteps =
|
val SeekBarSteps =
|
||||||
|
|
@ -243,7 +258,7 @@ sealed interface AppPreference<T> {
|
||||||
},
|
},
|
||||||
summarizer = { value ->
|
summarizer = { value ->
|
||||||
if (value == 0L) {
|
if (value == 0L) {
|
||||||
"Disabled"
|
WholphinApplication.instance.getString(R.string.disabled)
|
||||||
} else {
|
} else {
|
||||||
"${value}s"
|
"${value}s"
|
||||||
}
|
}
|
||||||
|
|
@ -262,10 +277,16 @@ sealed interface AppPreference<T> {
|
||||||
prefs.updatePlaybackPreferences { autoPlayNextDelaySeconds = value }
|
prefs.updatePlaybackPreferences { autoPlayNextDelaySeconds = value }
|
||||||
},
|
},
|
||||||
summarizer = { value ->
|
summarizer = { value ->
|
||||||
if (value == 0L) {
|
if (value == null) {
|
||||||
"Immediate"
|
""
|
||||||
|
} else if (value == 0L) {
|
||||||
|
WholphinApplication.instance.getString(R.string.immediate)
|
||||||
} else {
|
} else {
|
||||||
"$value seconds"
|
WholphinApplication.instance.resources.getQuantityString(
|
||||||
|
R.plurals.seconds,
|
||||||
|
value.toInt(),
|
||||||
|
value.toInt(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -284,10 +305,16 @@ sealed interface AppPreference<T> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
summarizer = { value ->
|
summarizer = { value ->
|
||||||
if (value == 0L) {
|
if (value == null) {
|
||||||
"Disabled"
|
""
|
||||||
|
} else if (value == 0L) {
|
||||||
|
WholphinApplication.instance.getString(R.string.disabled)
|
||||||
} else {
|
} else {
|
||||||
"$value hours"
|
WholphinApplication.instance.resources.getQuantityString(
|
||||||
|
R.plurals.hours,
|
||||||
|
value.toInt(),
|
||||||
|
value.toInt(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -485,7 +512,7 @@ sealed interface AppPreference<T> {
|
||||||
|
|
||||||
val SkipCommercials =
|
val SkipCommercials =
|
||||||
AppChoicePreference<SkipSegmentBehavior>(
|
AppChoicePreference<SkipSegmentBehavior>(
|
||||||
title = R.string.skip_comercials_behavior,
|
title = R.string.skip_commercials_behavior,
|
||||||
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
||||||
getter = { it.playbackPreferences.skipCommercials },
|
getter = { it.playbackPreferences.skipCommercials },
|
||||||
setter = { prefs, value ->
|
setter = { prefs, value ->
|
||||||
|
|
@ -602,6 +629,19 @@ sealed interface AppPreference<T> {
|
||||||
summaryOn = R.string.enabled,
|
summaryOn = R.string.enabled,
|
||||||
summaryOff = R.string.nav_drawer_switch_on_focus_summary_off,
|
summaryOff = R.string.nav_drawer_switch_on_focus_summary_off,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val ShowNextUpTiming =
|
||||||
|
AppChoicePreference<ShowNextUpWhen>(
|
||||||
|
title = R.string.show_next_up_when,
|
||||||
|
defaultValue = ShowNextUpWhen.END_OF_PLAYBACK,
|
||||||
|
getter = { it.playbackPreferences.showNextUpWhen },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updatePlaybackPreferences { showNextUpWhen = value }
|
||||||
|
},
|
||||||
|
displayValues = R.array.show_next_up_when_options,
|
||||||
|
indexToValue = { ShowNextUpWhen.forNumber(it) },
|
||||||
|
valueToIndex = { it.number },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -613,7 +653,6 @@ val basicPreferences =
|
||||||
listOf(
|
listOf(
|
||||||
AppPreference.HomePageItems,
|
AppPreference.HomePageItems,
|
||||||
AppPreference.RewatchNextUp,
|
AppPreference.RewatchNextUp,
|
||||||
AppPreference.CombineContinueNext,
|
|
||||||
AppPreference.PlayThemeMusic,
|
AppPreference.PlayThemeMusic,
|
||||||
AppPreference.RememberSelectedTab,
|
AppPreference.RememberSelectedTab,
|
||||||
AppPreference.ThemeColors,
|
AppPreference.ThemeColors,
|
||||||
|
|
@ -625,11 +664,17 @@ val basicPreferences =
|
||||||
listOf(
|
listOf(
|
||||||
AppPreference.SkipForward,
|
AppPreference.SkipForward,
|
||||||
AppPreference.SkipBack,
|
AppPreference.SkipBack,
|
||||||
AppPreference.ControllerTimeout,
|
AppPreference.SkipBackOnResume,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PreferenceGroup(
|
||||||
|
title = R.string.next_up,
|
||||||
|
preferences =
|
||||||
|
listOf(
|
||||||
|
AppPreference.ShowNextUpTiming,
|
||||||
AppPreference.AutoPlayNextUp,
|
AppPreference.AutoPlayNextUp,
|
||||||
AppPreference.AutoPlayNextDelay,
|
AppPreference.AutoPlayNextDelay,
|
||||||
AppPreference.PassOutProtection,
|
AppPreference.PassOutProtection,
|
||||||
AppPreference.SkipBackOnResume,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PreferenceGroup(
|
PreferenceGroup(
|
||||||
|
|
@ -664,7 +709,10 @@ val advancedPreferences =
|
||||||
title = R.string.ui_interface,
|
title = R.string.ui_interface,
|
||||||
preferences =
|
preferences =
|
||||||
listOf(
|
listOf(
|
||||||
AppPreference.NavDrawerSwitchOnFocus,
|
AppPreference.CombineContinueNext,
|
||||||
|
// Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418
|
||||||
|
// AppPreference.NavDrawerSwitchOnFocus,
|
||||||
|
AppPreference.ControllerTimeout,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PreferenceGroup(
|
PreferenceGroup(
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ class AppPreferencesSerializer
|
||||||
skipRecaps = AppPreference.SkipRecaps.defaultValue
|
skipRecaps = AppPreference.SkipRecaps.defaultValue
|
||||||
passOutProtectionMs =
|
passOutProtectionMs =
|
||||||
AppPreference.PassOutProtection.defaultValue.hours.inWholeMilliseconds
|
AppPreference.PassOutProtection.defaultValue.hours.inWholeMilliseconds
|
||||||
|
showNextUpWhen = AppPreference.ShowNextUpTiming.defaultValue
|
||||||
|
|
||||||
overrides =
|
overrides =
|
||||||
PlaybackOverrides
|
PlaybackOverrides
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.Chapter
|
import com.github.damontecres.wholphin.data.model.Chapter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -28,7 +30,7 @@ fun ChapterRow(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Chapters",
|
text = stringResource(R.string.chapters),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,11 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.Person
|
import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
|
|
||||||
|
|
@ -34,7 +36,7 @@ fun PersonRow(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "People",
|
text = stringResource(R.string.people),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.components
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
|
@ -19,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
@ -26,8 +28,12 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
||||||
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
|
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
|
|
@ -43,6 +49,7 @@ import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -62,6 +69,9 @@ class CollectionFolderViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
|
private val serverRepository: ServerRepository,
|
||||||
|
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
|
||||||
) : ItemViewModel(api) {
|
) : ItemViewModel(api) {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
||||||
|
|
@ -71,23 +81,52 @@ class CollectionFolderViewModel
|
||||||
fun init(
|
fun init(
|
||||||
itemId: UUID?,
|
itemId: UUID?,
|
||||||
potential: BaseItem?,
|
potential: BaseItem?,
|
||||||
sortAndDirection: SortAndDirection,
|
initialSortAndDirection: SortAndDirection?,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
filter: GetItemsFilter,
|
filter: GetItemsFilter,
|
||||||
): Job =
|
): Job =
|
||||||
viewModelScope.launch(
|
viewModelScope.launch(
|
||||||
LoadingExceptionHandler(
|
LoadingExceptionHandler(
|
||||||
loading,
|
loading,
|
||||||
"Error loading collection $itemId",
|
context.getString(R.string.error_loading_collection, itemId),
|
||||||
) + Dispatchers.IO,
|
) + Dispatchers.IO,
|
||||||
) {
|
) {
|
||||||
if (itemId != null) {
|
if (itemId != null) {
|
||||||
fetchItem(itemId, potential)
|
fetchItem(itemId)
|
||||||
|
|
||||||
|
val sortAndDirection =
|
||||||
|
if (initialSortAndDirection == null) {
|
||||||
|
serverRepository.currentUser?.let { user ->
|
||||||
|
libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
|
||||||
|
} ?: SortAndDirection.DEFAULT
|
||||||
|
} else {
|
||||||
|
SortAndDirection.DEFAULT
|
||||||
|
}
|
||||||
|
loadResults(sortAndDirection, recursive, filter)
|
||||||
}
|
}
|
||||||
loadResults(sortAndDirection, recursive, filter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadResults(
|
fun onSortChange(
|
||||||
|
sortAndDirection: SortAndDirection,
|
||||||
|
recursive: Boolean,
|
||||||
|
filter: GetItemsFilter,
|
||||||
|
) {
|
||||||
|
serverRepository.currentUser?.let { user ->
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val libraryDisplayInfo =
|
||||||
|
LibraryDisplayInfo(
|
||||||
|
userId = user.rowId,
|
||||||
|
itemId = itemId,
|
||||||
|
sort = sortAndDirection.sort,
|
||||||
|
direction = sortAndDirection.direction,
|
||||||
|
)
|
||||||
|
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadResults(sortAndDirection, recursive, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadResults(
|
||||||
sortAndDirection: SortAndDirection,
|
sortAndDirection: SortAndDirection,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
filter: GetItemsFilter,
|
filter: GetItemsFilter,
|
||||||
|
|
@ -126,17 +165,25 @@ class CollectionFolderViewModel
|
||||||
recursive = recursive,
|
recursive = recursive,
|
||||||
excludeItemIds = item?.let { listOf(item.id) },
|
excludeItemIds = item?.let { listOf(item.id) },
|
||||||
sortBy =
|
sortBy =
|
||||||
listOf(
|
buildList {
|
||||||
sortAndDirection.sort,
|
add(sortAndDirection.sort)
|
||||||
ItemSortBy.SORT_NAME,
|
if (sortAndDirection.sort != ItemSortBy.SORT_NAME) {
|
||||||
ItemSortBy.PRODUCTION_YEAR,
|
add(ItemSortBy.SORT_NAME)
|
||||||
),
|
}
|
||||||
|
if (item?.data?.collectionType == CollectionType.MOVIES) {
|
||||||
|
add(ItemSortBy.PRODUCTION_YEAR)
|
||||||
|
}
|
||||||
|
},
|
||||||
sortOrder =
|
sortOrder =
|
||||||
listOf(
|
buildList {
|
||||||
sortAndDirection.direction,
|
add(sortAndDirection.direction)
|
||||||
SortOrder.ASCENDING,
|
if (sortAndDirection.sort != ItemSortBy.SORT_NAME) {
|
||||||
SortOrder.ASCENDING,
|
add(SortOrder.ASCENDING)
|
||||||
),
|
}
|
||||||
|
if (item?.data?.collectionType == CollectionType.MOVIES) {
|
||||||
|
add(SortOrder.ASCENDING)
|
||||||
|
}
|
||||||
|
},
|
||||||
fields = SlimItemFields,
|
fields = SlimItemFields,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -158,26 +205,28 @@ class CollectionFolderViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun positionOfLetter(letter: Char): Int? =
|
suspend fun positionOfLetter(letter: Char): Int? =
|
||||||
item.value?.let { item ->
|
withContext(Dispatchers.IO) {
|
||||||
val includeItemTypes =
|
item.value?.let { item ->
|
||||||
when (item.data.collectionType) {
|
val includeItemTypes =
|
||||||
CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE)
|
when (item.data.collectionType) {
|
||||||
CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES)
|
CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE)
|
||||||
CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO)
|
CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES)
|
||||||
|
CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO)
|
||||||
|
|
||||||
else -> listOf()
|
else -> listOf()
|
||||||
}
|
}
|
||||||
val request =
|
val request =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
parentId = item.id,
|
parentId = item.id,
|
||||||
includeItemTypes = includeItemTypes,
|
includeItemTypes = includeItemTypes,
|
||||||
nameLessThan = letter.toString(),
|
nameLessThan = letter.toString(),
|
||||||
limit = 0,
|
limit = 0,
|
||||||
enableTotalRecordCount = true,
|
enableTotalRecordCount = true,
|
||||||
recursive = true,
|
recursive = true,
|
||||||
)
|
)
|
||||||
val result by GetItemsRequestHandler.execute(api, request)
|
val result by GetItemsRequestHandler.execute(api, request)
|
||||||
result.totalRecordCount
|
result.totalRecordCount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,18 +245,14 @@ fun CollectionFolderGrid(
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: CollectionFolderViewModel = hiltViewModel(),
|
viewModel: CollectionFolderViewModel = hiltViewModel(),
|
||||||
initialSortAndDirection: SortAndDirection =
|
initialSortAndDirection: SortAndDirection? = null,
|
||||||
SortAndDirection(
|
|
||||||
ItemSortBy.SORT_NAME,
|
|
||||||
SortOrder.ASCENDING,
|
|
||||||
),
|
|
||||||
showTitle: Boolean = true,
|
showTitle: Boolean = true,
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
OneTimeLaunchedEffect {
|
OneTimeLaunchedEffect {
|
||||||
viewModel.init(itemId, item, initialSortAndDirection, recursive, initialFilter)
|
viewModel.init(itemId, item, initialSortAndDirection, recursive, initialFilter)
|
||||||
}
|
}
|
||||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState(initialSortAndDirection)
|
val sortAndDirection by viewModel.sortAndDirection.observeAsState()
|
||||||
val filter by viewModel.filter.observeAsState(initialFilter)
|
val filter by viewModel.filter.observeAsState(initialFilter)
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
val item by viewModel.item.observeAsState()
|
val item by viewModel.item.observeAsState()
|
||||||
|
|
@ -224,11 +269,11 @@ fun CollectionFolderGrid(
|
||||||
preferences,
|
preferences,
|
||||||
item,
|
item,
|
||||||
pager,
|
pager,
|
||||||
sortAndDirection = sortAndDirection,
|
sortAndDirection = sortAndDirection!!,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onSortChange = {
|
onSortChange = {
|
||||||
viewModel.loadResults(it, recursive, filter)
|
viewModel.onSortChange(it, recursive, filter)
|
||||||
},
|
},
|
||||||
showTitle = showTitle,
|
showTitle = showTitle,
|
||||||
positionCallback = positionCallback,
|
positionCallback = positionCallback,
|
||||||
|
|
@ -252,7 +297,7 @@ fun CollectionFolderGridContent(
|
||||||
showTitle: Boolean = true,
|
showTitle: Boolean = true,
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val title = item?.name ?: item?.data?.collectionType?.name ?: "Collection"
|
val title = item?.name ?: item?.data?.collectionType?.name ?: stringResource(R.string.collection)
|
||||||
val sortOptions =
|
val sortOptions =
|
||||||
when (item?.data?.collectionType) {
|
when (item?.data?.collectionType) {
|
||||||
CollectionType.MOVIES -> MovieSortOptions
|
CollectionType.MOVIES -> MovieSortOptions
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.components
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -462,12 +463,13 @@ fun ConfirmDialogContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun chooseVersionParams(
|
fun chooseVersionParams(
|
||||||
|
context: Context,
|
||||||
sources: List<MediaSourceInfo>,
|
sources: List<MediaSourceInfo>,
|
||||||
onClick: (Int) -> Unit,
|
onClick: (Int) -> Unit,
|
||||||
): DialogParams =
|
): DialogParams =
|
||||||
DialogParams(
|
DialogParams(
|
||||||
fromLongClick = false,
|
fromLongClick = false,
|
||||||
title = "Play Version",
|
title = context.getString(R.string.choose_stream, context.getString(R.string.version)),
|
||||||
items =
|
items =
|
||||||
sources.filter { it.id.isNotNullOrBlank() }.mapIndexed { index, source ->
|
sources.filter { it.id.isNotNullOrBlank() }.mapIndexed { index, source ->
|
||||||
val videoStream =
|
val videoStream =
|
||||||
|
|
@ -485,21 +487,33 @@ fun chooseVersionParams(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
fun resourceFor(type: MediaStreamType): Int =
|
||||||
|
when (type) {
|
||||||
|
MediaStreamType.AUDIO -> R.string.audio
|
||||||
|
MediaStreamType.VIDEO -> R.string.video
|
||||||
|
MediaStreamType.SUBTITLE -> R.string.subtitles
|
||||||
|
MediaStreamType.EMBEDDED_IMAGE -> 0
|
||||||
|
MediaStreamType.DATA -> 0
|
||||||
|
MediaStreamType.LYRIC -> 0
|
||||||
|
}
|
||||||
|
|
||||||
fun chooseStream(
|
fun chooseStream(
|
||||||
|
context: Context,
|
||||||
streams: List<MediaStream>,
|
streams: List<MediaStream>,
|
||||||
type: MediaStreamType,
|
type: MediaStreamType,
|
||||||
onClick: (Int) -> Unit,
|
onClick: (Int) -> Unit,
|
||||||
): DialogParams =
|
): DialogParams =
|
||||||
DialogParams(
|
DialogParams(
|
||||||
fromLongClick = false,
|
fromLongClick = false,
|
||||||
title = "Choose ${type.serialName}", // TODO
|
title = context.getString(R.string.choose_stream, context.getString(resourceFor(type))),
|
||||||
items =
|
items =
|
||||||
buildList {
|
buildList {
|
||||||
if (type == MediaStreamType.SUBTITLE) {
|
if (type == MediaStreamType.SUBTITLE) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(text = "None")
|
Text(text = stringResource(R.string.none))
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -50,7 +52,7 @@ fun ErrorMessage(
|
||||||
modifier = modifier.padding(16.dp),
|
modifier = modifier.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "An error occurred! Press the button to send logs to your server.",
|
text = stringResource(R.string.compose_error_message_title),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
|
|
@ -60,7 +62,7 @@ fun ErrorMessage(
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Send Logs",
|
text = stringResource(R.string.send_app_logs),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
message?.let {
|
message?.let {
|
||||||
|
|
|
||||||
|
|
@ -64,17 +64,18 @@ class GenreViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun positionOfLetter(letter: Char): Int {
|
suspend fun positionOfLetter(letter: Char): Int =
|
||||||
val request =
|
withContext(Dispatchers.IO) {
|
||||||
GetGenresRequest(
|
val request =
|
||||||
parentId = itemId,
|
GetGenresRequest(
|
||||||
nameLessThan = letter.toString(),
|
parentId = itemId,
|
||||||
limit = 0,
|
nameLessThan = letter.toString(),
|
||||||
enableTotalRecordCount = true,
|
limit = 0,
|
||||||
)
|
enableTotalRecordCount = true,
|
||||||
val result by GetGenresRequestHandler.execute(api, request)
|
)
|
||||||
return result.totalRecordCount
|
val result by GetGenresRequestHandler.execute(api, request)
|
||||||
}
|
return@withContext result.totalRecordCount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.ui.Cards
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
|
|
@ -70,7 +72,7 @@ fun LoadingRow(
|
||||||
->
|
->
|
||||||
LoadingRowPlaceholder(
|
LoadingRowPlaceholder(
|
||||||
title = title,
|
title = title,
|
||||||
message = "Loading...",
|
message = stringResource(R.string.loading),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -88,7 +90,7 @@ fun LoadingRow(
|
||||||
} else if (showIfEmpty) {
|
} else if (showIfEmpty) {
|
||||||
LoadingRowPlaceholder(
|
LoadingRowPlaceholder(
|
||||||
title = title,
|
title = title,
|
||||||
message = "No results",
|
message = stringResource(R.string.no_results),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -16,7 +17,6 @@ import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
||||||
import com.github.damontecres.wholphin.ui.main.HomeRow
|
import com.github.damontecres.wholphin.ui.main.HomeRow
|
||||||
import com.github.damontecres.wholphin.ui.main.HomeSection
|
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
|
@ -113,11 +113,11 @@ class RecommendedMovieViewModel
|
||||||
rows.forEach { it.init() }
|
rows.forEach { it.init() }
|
||||||
val homeRows =
|
val homeRows =
|
||||||
listOf(
|
listOf(
|
||||||
HomeRow(HomeSection.RESUME, resumeItems, "Continue Watching"),
|
HomeRow(R.string.continue_watching, resumeItems),
|
||||||
HomeRow(HomeSection.LATEST_MEDIA, recentlyReleasedItems, "Recently Released"),
|
HomeRow(R.string.recently_released, recentlyReleasedItems),
|
||||||
HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"),
|
HomeRow(R.string.recently_added, recentlyAddedItems),
|
||||||
HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"),
|
HomeRow(R.string.suggestions, suggestedItems),
|
||||||
HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"),
|
HomeRow(R.string.top_unwatched, unwatchedTopRatedItems),
|
||||||
).filter { it.items.isNotEmpty() }
|
).filter { it.items.isNotEmpty() }
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@RecommendedMovieViewModel.rows.value = homeRows
|
this@RecommendedMovieViewModel.rows.value = homeRows
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -16,7 +17,6 @@ import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
||||||
import com.github.damontecres.wholphin.ui.main.HomeRow
|
import com.github.damontecres.wholphin.ui.main.HomeRow
|
||||||
import com.github.damontecres.wholphin.ui.main.HomeSection
|
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
|
@ -124,12 +124,12 @@ class RecommendedTvShowViewModel
|
||||||
rows.forEach { it.init() }
|
rows.forEach { it.init() }
|
||||||
val homeRows =
|
val homeRows =
|
||||||
listOf(
|
listOf(
|
||||||
HomeRow(HomeSection.RESUME, resumeItems, "Continue Watching"),
|
HomeRow(R.string.continue_watching, resumeItems),
|
||||||
HomeRow(HomeSection.NEXT_UP, nextUpItems, "Next Up"),
|
HomeRow(R.string.next_up, nextUpItems),
|
||||||
HomeRow(HomeSection.LATEST_MEDIA, recentlyReleasedItems, "Recently Released"),
|
HomeRow(R.string.recently_released, recentlyReleasedItems),
|
||||||
HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"),
|
HomeRow(R.string.recently_added, recentlyAddedItems),
|
||||||
HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"),
|
HomeRow(R.string.suggestions, suggestedItems),
|
||||||
HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"),
|
HomeRow(R.string.top_unwatched, unwatchedTopRatedItems),
|
||||||
).filter { it.items.isNotEmpty() }
|
).filter { it.items.isNotEmpty() }
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@RecommendedTvShowViewModel.rows.value = homeRows
|
this@RecommendedTvShowViewModel.rows.value = homeRows
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.ProvideTextStyle
|
import androidx.tv.material3.ProvideTextStyle
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.components.ScrollableDialog
|
import com.github.damontecres.wholphin.ui.components.ScrollableDialog
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
|
|
@ -74,24 +76,30 @@ fun MediaSourceInfo(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Name: ${source.name}",
|
text = stringResource(R.string.name) + ": ${source.name}",
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "ID: ${source.id}",
|
text = "ID: ${source.id}",
|
||||||
)
|
)
|
||||||
if (showFilePath) {
|
if (showFilePath) {
|
||||||
Text(
|
Text(
|
||||||
text = "Path: ${source.path}",
|
text = stringResource(R.string.path) + ": ${source.path}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
source.size?.let { size ->
|
source.size?.let { size ->
|
||||||
Text(
|
Text(
|
||||||
text = "Size: ${formatBytes(size)}",
|
text = stringResource(R.string.file_size) + ": ${formatBytes(size)}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
source.bitrate?.let { bitrate ->
|
source.bitrate?.let { bitrate ->
|
||||||
Text(
|
Text(
|
||||||
text = "Bitrate: ${formatBytes(bitrate, byteRateSuffixes)}",
|
text =
|
||||||
|
stringResource(R.string.bitrate) + ": ${
|
||||||
|
formatBytes(
|
||||||
|
bitrate,
|
||||||
|
byteRateSuffixes,
|
||||||
|
)
|
||||||
|
}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
source.mediaStreams?.letNotEmpty { streams ->
|
source.mediaStreams?.letNotEmpty { streams ->
|
||||||
|
|
@ -109,7 +117,7 @@ fun MediaSourceInfo(
|
||||||
stream.profile?.let(::add)
|
stream.profile?.let(::add)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "Video: " + data.joinToString(" - "),
|
text = stringResource(R.string.video) + ": " + data.joinToString(" - "),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,10 +129,10 @@ fun MediaSourceInfo(
|
||||||
stream.channelLayout?.let(::add)
|
stream.channelLayout?.let(::add)
|
||||||
stream.bitRate?.let { add(formatBytes(it, byteRateSuffixes)) }
|
stream.bitRate?.let { add(formatBytes(it, byteRateSuffixes)) }
|
||||||
if (stream.audioSpatialFormat != AudioSpatialFormat.NONE) add(stream.audioSpatialFormat.serialName)
|
if (stream.audioSpatialFormat != AudioSpatialFormat.NONE) add(stream.audioSpatialFormat.serialName)
|
||||||
if (stream.isDefault) add("Default")
|
if (stream.isDefault) add(stringResource(R.string.default_track))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "Audio ${index + 1}: " + data.joinToString(" - "),
|
text = stringResource(R.string.audio) + " ${index + 1}: " + data.joinToString(" - "),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,12 +141,14 @@ fun MediaSourceInfo(
|
||||||
buildList {
|
buildList {
|
||||||
stream.language?.let { add(languageName(it)) }
|
stream.language?.let { add(languageName(it)) }
|
||||||
stream.codec?.let(::add)
|
stream.codec?.let(::add)
|
||||||
if (stream.isDefault) add("Default")
|
if (stream.isDefault) add(stringResource(R.string.default_track))
|
||||||
if (stream.isForced) add("Forced")
|
if (stream.isForced) add(stringResource(R.string.forced_track))
|
||||||
if (stream.isExternal) add("External")
|
if (stream.isExternal) add(stringResource(R.string.external_track))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "Subtitle ${index + 1}: " + data.joinToString(" - "),
|
text =
|
||||||
|
stringResource(R.string.subtitle) + " ${index + 1}: " +
|
||||||
|
data.joinToString(" - "),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,14 @@ data class SortAndDirection(
|
||||||
val direction: SortOrder,
|
val direction: SortOrder,
|
||||||
) {
|
) {
|
||||||
fun flip() = copy(direction = direction.flip())
|
fun flip() = copy(direction = direction.flip())
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DEFAULT =
|
||||||
|
SortAndDirection(
|
||||||
|
ItemSortBy.SORT_NAME,
|
||||||
|
SortOrder.ASCENDING,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SortOrder.flip() = if (this == SortOrder.ASCENDING) SortOrder.DESCENDING else SortOrder.ASCENDING
|
fun SortOrder.flip() = if (this == SortOrder.ASCENDING) SortOrder.DESCENDING else SortOrder.ASCENDING
|
||||||
|
|
@ -20,6 +28,9 @@ val MovieSortOptions =
|
||||||
ItemSortBy.PREMIERE_DATE,
|
ItemSortBy.PREMIERE_DATE,
|
||||||
ItemSortBy.DATE_CREATED,
|
ItemSortBy.DATE_CREATED,
|
||||||
ItemSortBy.DATE_PLAYED,
|
ItemSortBy.DATE_PLAYED,
|
||||||
|
ItemSortBy.COMMUNITY_RATING,
|
||||||
|
ItemSortBy.CRITIC_RATING,
|
||||||
|
ItemSortBy.PLAY_COUNT,
|
||||||
ItemSortBy.RANDOM,
|
ItemSortBy.RANDOM,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -30,6 +41,7 @@ val SeriesSortOptions =
|
||||||
ItemSortBy.DATE_CREATED,
|
ItemSortBy.DATE_CREATED,
|
||||||
ItemSortBy.DATE_LAST_CONTENT_ADDED,
|
ItemSortBy.DATE_LAST_CONTENT_ADDED,
|
||||||
ItemSortBy.DATE_PLAYED,
|
ItemSortBy.DATE_PLAYED,
|
||||||
|
ItemSortBy.COMMUNITY_RATING,
|
||||||
ItemSortBy.RANDOM,
|
ItemSortBy.RANDOM,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -50,5 +62,8 @@ fun getStringRes(sort: ItemSortBy): Int =
|
||||||
ItemSortBy.DATE_LAST_CONTENT_ADDED -> R.string.sort_by_date_episode_added
|
ItemSortBy.DATE_LAST_CONTENT_ADDED -> R.string.sort_by_date_episode_added
|
||||||
ItemSortBy.DATE_PLAYED -> R.string.sort_by_date_played
|
ItemSortBy.DATE_PLAYED -> R.string.sort_by_date_played
|
||||||
ItemSortBy.RANDOM -> R.string.sort_by_random
|
ItemSortBy.RANDOM -> R.string.sort_by_random
|
||||||
|
ItemSortBy.COMMUNITY_RATING -> R.string.community_rating
|
||||||
|
ItemSortBy.CRITIC_RATING -> R.string.critic_rating
|
||||||
|
ItemSortBy.PLAY_COUNT -> R.string.play_count
|
||||||
else -> throw IllegalArgumentException("Unsupported sort option: $sort")
|
else -> throw IllegalArgumentException("Unsupported sort option: $sort")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
|
|
@ -59,7 +60,9 @@ import com.github.damontecres.wholphin.ui.playback.isForwardButton
|
||||||
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private const val DEBUG = false
|
private const val DEBUG = false
|
||||||
|
|
@ -296,7 +299,7 @@ fun CardGrid(
|
||||||
// focusedIndex = -1
|
// focusedIndex = -1
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Text(
|
Text(
|
||||||
text = "No results",
|
text = stringResource(R.string.no_results),
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
)
|
)
|
||||||
|
|
@ -324,6 +327,8 @@ fun CardGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val context = LocalContext.current
|
||||||
|
val letters = context.getString(R.string.jump_letters)
|
||||||
// Letters
|
// Letters
|
||||||
val currentLetter =
|
val currentLetter =
|
||||||
remember(focusedIndex) {
|
remember(focusedIndex) {
|
||||||
|
|
@ -342,15 +347,19 @@ fun CardGrid(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?: LETTERS[0]
|
?: letters[0]
|
||||||
}
|
}
|
||||||
if (showLetterButtons && pager.isNotEmpty()) {
|
if (showLetterButtons && pager.isNotEmpty()) {
|
||||||
AlphabetButtons(
|
AlphabetButtons(
|
||||||
|
letters = letters,
|
||||||
currentLetter = currentLetter,
|
currentLetter = currentLetter,
|
||||||
modifier = Modifier.align(Alignment.CenterVertically),
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
letterClicked = { letter ->
|
letterClicked = { letter ->
|
||||||
scope.launch(ExceptionHandler()) {
|
scope.launch(ExceptionHandler()) {
|
||||||
val jumpPosition = letterPosition.invoke(letter)
|
val jumpPosition =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
letterPosition.invoke(letter)
|
||||||
|
}
|
||||||
Timber.d("Alphabet jump to $jumpPosition")
|
Timber.d("Alphabet jump to $jumpPosition")
|
||||||
if (jumpPosition >= 0) {
|
if (jumpPosition >= 0) {
|
||||||
gridState.scrollToItem(jumpPosition)
|
gridState.scrollToItem(jumpPosition)
|
||||||
|
|
@ -399,18 +408,18 @@ fun JumpButton(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val LETTERS = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AlphabetButtons(
|
fun AlphabetButtons(
|
||||||
|
letters: String,
|
||||||
currentLetter: Char,
|
currentLetter: Char,
|
||||||
letterClicked: (Char) -> Unit,
|
letterClicked: (Char) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
val index = LETTERS.indexOf(currentLetter)
|
val index = letters.indexOf(currentLetter)
|
||||||
LaunchedEffect(currentLetter) {
|
LaunchedEffect(currentLetter) {
|
||||||
scope.launch(ExceptionHandler()) {
|
scope.launch(ExceptionHandler()) {
|
||||||
val firstVisibleItemIndex = listState.firstVisibleItemIndex
|
val firstVisibleItemIndex = listState.firstVisibleItemIndex
|
||||||
|
|
@ -423,19 +432,19 @@ fun AlphabetButtons(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val focusRequesters = remember { List(LETTERS.length) { FocusRequester() } }
|
val focusRequesters = remember { List(letters.length) { FocusRequester() } }
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier =
|
modifier =
|
||||||
modifier.focusProperties {
|
modifier.focusProperties {
|
||||||
onEnter = {
|
onEnter = {
|
||||||
focusRequesters[index.coerceIn(0, LETTERS.length - 1)].tryRequestFocus()
|
focusRequesters[index.coerceIn(0, letters.length - 1)].tryRequestFocus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
LETTERS.length,
|
letters.length,
|
||||||
key = { LETTERS[it] },
|
key = { letters[it] },
|
||||||
) { index ->
|
) { index ->
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
|
|
@ -447,17 +456,17 @@ fun AlphabetButtons(
|
||||||
contentPadding = PaddingValues(2.dp),
|
contentPadding = PaddingValues(2.dp),
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = {
|
onClick = {
|
||||||
letterClicked.invoke(LETTERS[index])
|
letterClicked.invoke(letters[index])
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val color =
|
val color =
|
||||||
if (!focused && LETTERS[index] == currentLetter) {
|
if (!focused && letters[index] == currentLetter) {
|
||||||
MaterialTheme.colorScheme.tertiary
|
MaterialTheme.colorScheme.tertiary
|
||||||
} else {
|
} else {
|
||||||
LocalContentColor.current
|
LocalContentColor.current
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = LETTERS[index].toString(),
|
text = letters[index].toString(),
|
||||||
color = color,
|
color = color,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
|
@ -33,6 +34,7 @@ import androidx.tv.material3.TabDefaults
|
||||||
import androidx.tv.material3.TabRow
|
import androidx.tv.material3.TabRow
|
||||||
import androidx.tv.material3.TabRowDefaults
|
import androidx.tv.material3.TabRowDefaults
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
|
|
@ -96,8 +98,8 @@ fun CollectionFolderLiveTv(
|
||||||
|
|
||||||
val tabs =
|
val tabs =
|
||||||
listOf(
|
listOf(
|
||||||
TabId("Guide", UUID.randomUUID()),
|
TabId(stringResource(R.string.tv_guide), UUID.randomUUID()),
|
||||||
TabId("DVR Schedule", UUID.randomUUID()),
|
TabId(stringResource(R.string.tv_dvr_schedule), UUID.randomUUID()),
|
||||||
) + folders
|
) + folders
|
||||||
|
|
||||||
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
@ -29,6 +30,7 @@ import androidx.tv.material3.TabDefaults
|
||||||
import androidx.tv.material3.TabRow
|
import androidx.tv.material3.TabRow
|
||||||
import androidx.tv.material3.TabRowDefaults
|
import androidx.tv.material3.TabRowDefaults
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -52,7 +54,13 @@ fun CollectionFolderMovie(
|
||||||
val rememberedTabIndex =
|
val rememberedTabIndex =
|
||||||
remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) }
|
remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) }
|
||||||
|
|
||||||
val tabs = listOf("Recommended", "Library", "Collections", "Genres")
|
val tabs =
|
||||||
|
listOf(
|
||||||
|
stringResource(R.string.recommended),
|
||||||
|
stringResource(R.string.library),
|
||||||
|
stringResource(R.string.collections),
|
||||||
|
stringResource(R.string.genres),
|
||||||
|
)
|
||||||
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
@ -29,6 +30,7 @@ import androidx.tv.material3.TabDefaults
|
||||||
import androidx.tv.material3.TabRow
|
import androidx.tv.material3.TabRow
|
||||||
import androidx.tv.material3.TabRowDefaults
|
import androidx.tv.material3.TabRowDefaults
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -52,7 +54,12 @@ fun CollectionFolderTv(
|
||||||
val rememberedTabIndex =
|
val rememberedTabIndex =
|
||||||
remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) }
|
remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) }
|
||||||
|
|
||||||
val tabs = listOf("Recommended", "Library", "Genres")
|
val tabs =
|
||||||
|
listOf(
|
||||||
|
stringResource(R.string.recommended),
|
||||||
|
stringResource(R.string.library),
|
||||||
|
stringResource(R.string.genres),
|
||||||
|
)
|
||||||
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail
|
package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.filled.ArrowForward
|
import androidx.compose.material.icons.filled.ArrowForward
|
||||||
|
|
@ -36,6 +37,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
* @param onChooseTracks callback to pick a track for the given type of the item
|
* @param onChooseTracks callback to pick a track for the given type of the item
|
||||||
*/
|
*/
|
||||||
fun buildMoreDialogItems(
|
fun buildMoreDialogItems(
|
||||||
|
context: Context,
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
series: BaseItem?,
|
series: BaseItem?,
|
||||||
sourceId: UUID?,
|
sourceId: UUID?,
|
||||||
|
|
@ -50,7 +52,7 @@ fun buildMoreDialogItems(
|
||||||
buildList {
|
buildList {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Play",
|
context.getString(R.string.play),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -83,7 +85,7 @@ fun buildMoreDialogItems(
|
||||||
series?.let {
|
series?.let {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Go to series",
|
context.getString(R.string.go_to_series),
|
||||||
Icons.AutoMirrored.Filled.ArrowForward,
|
Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
) {
|
) {
|
||||||
navigateTo(
|
navigateTo(
|
||||||
|
|
@ -100,7 +102,10 @@ fun buildMoreDialogItems(
|
||||||
if (sources.size > 1) {
|
if (sources.size > 1) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Choose Version",
|
context.getString(
|
||||||
|
R.string.choose_stream,
|
||||||
|
context.getString(R.string.version),
|
||||||
|
),
|
||||||
R.string.fa_file_video,
|
R.string.fa_file_video,
|
||||||
) {
|
) {
|
||||||
onChooseVersion.invoke()
|
onChooseVersion.invoke()
|
||||||
|
|
@ -116,7 +121,10 @@ fun buildMoreDialogItems(
|
||||||
if (audioCount > 1) {
|
if (audioCount > 1) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Choose audio",
|
context.getString(
|
||||||
|
R.string.choose_stream,
|
||||||
|
context.getString(R.string.audio),
|
||||||
|
),
|
||||||
R.string.fa_volume_low,
|
R.string.fa_volume_low,
|
||||||
) {
|
) {
|
||||||
onChooseTracks.invoke(MediaStreamType.AUDIO)
|
onChooseTracks.invoke(MediaStreamType.AUDIO)
|
||||||
|
|
@ -126,7 +134,10 @@ fun buildMoreDialogItems(
|
||||||
if (subtitleCount > 0) {
|
if (subtitleCount > 0) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Choose subtitles",
|
context.getString(
|
||||||
|
R.string.choose_stream,
|
||||||
|
context.getString(R.string.subtitles),
|
||||||
|
),
|
||||||
R.string.fa_closed_captioning,
|
R.string.fa_closed_captioning,
|
||||||
) {
|
) {
|
||||||
onChooseTracks.invoke(MediaStreamType.SUBTITLE)
|
onChooseTracks.invoke(MediaStreamType.SUBTITLE)
|
||||||
|
|
@ -138,6 +149,7 @@ fun buildMoreDialogItems(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildMoreDialogItemsForHome(
|
fun buildMoreDialogItemsForHome(
|
||||||
|
context: Context,
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
seriesId: UUID?,
|
seriesId: UUID?,
|
||||||
playbackPosition: Duration,
|
playbackPosition: Duration,
|
||||||
|
|
@ -151,7 +163,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
val itemId = item.id
|
val itemId = item.id
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Go To",
|
context.getString(R.string.go_to),
|
||||||
Icons.Default.ArrowForward,
|
Icons.Default.ArrowForward,
|
||||||
) {
|
) {
|
||||||
navigateTo(item.destination())
|
navigateTo(item.destination())
|
||||||
|
|
@ -161,7 +173,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
if (playbackPosition >= 1.seconds) {
|
if (playbackPosition >= 1.seconds) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Resume",
|
context.getString(R.string.resume),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -175,7 +187,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Restart",
|
context.getString(R.string.restart),
|
||||||
Icons.Default.Refresh,
|
Icons.Default.Refresh,
|
||||||
// iconColor = Color.Green.copy(alpha = .8f),
|
// iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -190,7 +202,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
} else {
|
} else {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Play",
|
context.getString(R.string.play),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -224,7 +236,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
seriesId?.let {
|
seriesId?.let {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Go to series",
|
context.getString(R.string.go_to_series),
|
||||||
Icons.AutoMirrored.Filled.ArrowForward,
|
Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
) {
|
) {
|
||||||
navigateTo(
|
navigateTo(
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
@ -29,6 +30,7 @@ import androidx.tv.material3.TabDefaults
|
||||||
import androidx.tv.material3.TabRow
|
import androidx.tv.material3.TabRow
|
||||||
import androidx.tv.material3.TabRowDefaults
|
import androidx.tv.material3.TabRowDefaults
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -55,7 +57,12 @@ fun FavoritesPage(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val tabs = listOf("Movies", "TV Shows", "Episodes")
|
val tabs =
|
||||||
|
listOf(
|
||||||
|
stringResource(R.string.movies),
|
||||||
|
stringResource(R.string.tv_shows),
|
||||||
|
stringResource(R.string.episodes),
|
||||||
|
)
|
||||||
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
|
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
|
||||||
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
|
|
|
||||||
|
|
@ -25,21 +25,11 @@ abstract class ItemViewModel(
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val item = MutableLiveData<BaseItem?>(null)
|
val item = MutableLiveData<BaseItem?>(null)
|
||||||
|
lateinit var itemId: UUID
|
||||||
|
|
||||||
suspend fun fetchItem(
|
suspend fun fetchItem(itemId: UUID): BaseItem =
|
||||||
itemId: UUID,
|
|
||||||
potential: BaseItem?,
|
|
||||||
): BaseItem =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// val fetchedItem =
|
this@ItemViewModel.itemId = itemId
|
||||||
// when {
|
|
||||||
// item.value == null && potential?.id == itemId -> potential
|
|
||||||
// item.value?.id == itemId -> item.value
|
|
||||||
// else -> {
|
|
||||||
// val it = api.userLibraryApi.getItem(itemId).content
|
|
||||||
// BaseItem.from(it, api)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
val it = api.userLibraryApi.getItem(itemId).content
|
val it = api.userLibraryApi.getItem(itemId).content
|
||||||
val fetchedItem = BaseItem.from(it, api)
|
val fetchedItem = BaseItem.from(it, api)
|
||||||
return@withContext fetchedItem.let {
|
return@withContext fetchedItem.let {
|
||||||
|
|
@ -89,10 +79,8 @@ abstract class LoadingItemViewModel(
|
||||||
|
|
||||||
open suspend fun fetchAndSetItem(itemId: UUID): BaseItem =
|
open suspend fun fetchAndSetItem(itemId: UUID): BaseItem =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val fetchedItem = api.userLibraryApi.getItem(itemId).content
|
val item = fetchItem(itemId)
|
||||||
val item = BaseItem.from(fetchedItem, api)
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@LoadingItemViewModel.item.value = item
|
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
}
|
}
|
||||||
return@withContext item
|
return@withContext item
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shadow
|
import androidx.compose.ui.graphics.Shadow
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
@ -33,6 +34,7 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||||
|
|
@ -258,7 +260,7 @@ fun PersonPageContent(
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
LoadingRow(
|
LoadingRow(
|
||||||
title = rowTitle("Movies", movies),
|
title = rowTitle(stringResource(R.string.movies), movies),
|
||||||
state = movies,
|
state = movies,
|
||||||
rowIndex = MOVIE_ROW,
|
rowIndex = MOVIE_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
|
|
@ -271,7 +273,7 @@ fun PersonPageContent(
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
LoadingRow(
|
LoadingRow(
|
||||||
title = rowTitle("Series", series),
|
title = rowTitle(stringResource(R.string.tv_shows), series),
|
||||||
state = series,
|
state = series,
|
||||||
rowIndex = SERIES_ROW,
|
rowIndex = SERIES_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
|
|
@ -284,7 +286,7 @@ fun PersonPageContent(
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
LoadingRow(
|
LoadingRow(
|
||||||
title = rowTitle("Episodes", episodes),
|
title = rowTitle(stringResource(R.string.episodes), episodes),
|
||||||
state = episodes,
|
state = episodes,
|
||||||
rowIndex = EPISODE_ROW,
|
rowIndex = EPISODE_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
|
|
@ -365,9 +367,14 @@ fun PersonHeader(
|
||||||
val age = if (deathdate == null) birthdate.until(LocalDate.now())?.years else null
|
val age = if (deathdate == null) birthdate.until(LocalDate.now())?.years else null
|
||||||
val text =
|
val text =
|
||||||
if (age != null) {
|
if (age != null) {
|
||||||
"Born: ${formatDate(it)} ($age years old)"
|
stringResource(R.string.born) + ": ${formatDate(it)} (${
|
||||||
|
stringResource(
|
||||||
|
R.string.years_old,
|
||||||
|
age,
|
||||||
|
)
|
||||||
|
})"
|
||||||
} else {
|
} else {
|
||||||
"Born: ${formatDate(it)}"
|
stringResource(R.string.born) + ": ${formatDate(it)}"
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
|
|
@ -379,7 +386,7 @@ fun PersonHeader(
|
||||||
}
|
}
|
||||||
birthPlace?.let {
|
birthPlace?.let {
|
||||||
Text(
|
Text(
|
||||||
text = "Birthplace: $it",
|
text = stringResource(R.string.birthplace) + ": $it",
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
@ -390,9 +397,14 @@ fun PersonHeader(
|
||||||
val age = birthdate?.until(it)?.years
|
val age = birthdate?.until(it)?.years
|
||||||
val text =
|
val text =
|
||||||
if (age != null) {
|
if (age != null) {
|
||||||
"Died: ${formatDate(it)} ($age years old)"
|
stringResource(R.string.died) + ": ${formatDate(it)} (${
|
||||||
|
stringResource(
|
||||||
|
R.string.years_old,
|
||||||
|
age,
|
||||||
|
)
|
||||||
|
})"
|
||||||
} else {
|
} else {
|
||||||
"Died: ${formatDate(it)}"
|
stringResource(R.string.died) + ": ${formatDate(it)}"
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
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
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
@ -102,7 +104,7 @@ class PlaylistViewModel
|
||||||
Dispatchers.IO +
|
Dispatchers.IO +
|
||||||
LoadingExceptionHandler(loading, "Failed to fetch playlist $playlistId"),
|
LoadingExceptionHandler(loading, "Failed to fetch playlist $playlistId"),
|
||||||
) {
|
) {
|
||||||
val playlist = fetchItem(playlistId, null)
|
val playlist = fetchItem(playlistId)
|
||||||
val request =
|
val request =
|
||||||
GetPlaylistItemsRequest(
|
GetPlaylistItemsRequest(
|
||||||
playlistId = playlist.id,
|
playlistId = playlist.id,
|
||||||
|
|
@ -123,6 +125,7 @@ fun PlaylistDetails(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: PlaylistViewModel = hiltViewModel(),
|
viewModel: PlaylistViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.init(destination.itemId)
|
viewModel.init(destination.itemId)
|
||||||
}
|
}
|
||||||
|
|
@ -170,13 +173,13 @@ fun PlaylistDetails(
|
||||||
items =
|
items =
|
||||||
listOf(
|
listOf(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Go to",
|
context.getString(R.string.go_to),
|
||||||
Icons.Default.ArrowForward,
|
Icons.Default.ArrowForward,
|
||||||
) {
|
) {
|
||||||
viewModel.navigationManager.navigateTo(item.destination())
|
viewModel.navigationManager.navigateTo(item.destination())
|
||||||
},
|
},
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Play from here",
|
context.getString(R.string.play_from_here),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
) {
|
) {
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigationManager.navigateTo(
|
||||||
|
|
@ -282,7 +285,7 @@ fun PlaylistDetailsContent(
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = playlist.name ?: "Playlist",
|
text = playlist.name ?: stringResource(R.string.playlist),
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.displayMedium,
|
style = MaterialTheme.typography.displayMedium,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
@ -31,6 +32,7 @@ import androidx.tv.material3.ListItemDefaults
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
|
@ -202,7 +204,7 @@ fun DvrScheduleContent(
|
||||||
if (activeRecordings.isEmpty() && scheduledRecordings.isEmpty()) {
|
if (activeRecordings.isEmpty() && scheduledRecordings.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = "No scheduled recordings",
|
text = stringResource(R.string.no_scheduled_recordings),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|
@ -213,7 +215,7 @@ fun DvrScheduleContent(
|
||||||
if (activeRecordings.isNotEmpty()) {
|
if (activeRecordings.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = "Active",
|
text = stringResource(R.string.active_recordings),
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.livetv
|
package com.github.damontecres.wholphin.ui.detail.livetv
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
|
|
@ -17,6 +19,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -46,6 +49,7 @@ const val MAX_HOURS = 48L
|
||||||
class LiveTvViewModel
|
class LiveTvViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
@ -70,7 +74,13 @@ class LiveTvViewModel
|
||||||
if (!firstLoad) {
|
if (!firstLoad) {
|
||||||
loading.value = LoadingState.Loading
|
loading.value = LoadingState.Loading
|
||||||
}
|
}
|
||||||
viewModelScope.launch(Dispatchers.IO + LoadingExceptionHandler(loading, "Could not fetch channels")) {
|
viewModelScope.launch(
|
||||||
|
Dispatchers.IO +
|
||||||
|
LoadingExceptionHandler(
|
||||||
|
loading,
|
||||||
|
"Could not fetch channels",
|
||||||
|
),
|
||||||
|
) {
|
||||||
val channelData by api.liveTvApi.getLiveTvChannels(
|
val channelData by api.liveTvApi.getLiveTvChannels(
|
||||||
GetLiveTvChannelsRequest(
|
GetLiveTvChannelsRequest(
|
||||||
startIndex = 0,
|
startIndex = 0,
|
||||||
|
|
@ -179,7 +189,7 @@ class LiveTvViewModel
|
||||||
startHours = it.toFloat(),
|
startHours = it.toFloat(),
|
||||||
endHours = (it + 1).toFloat(),
|
endHours = (it + 1).toFloat(),
|
||||||
duration = 60.seconds,
|
duration = 60.seconds,
|
||||||
name = "No data",
|
name = context.getString(R.string.no_data),
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
seasonEpisode = null,
|
seasonEpisode = null,
|
||||||
isRecording = false,
|
isRecording = false,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
|
@ -30,6 +31,7 @@ import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
|
@ -165,7 +167,14 @@ fun ProgramDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = if (isSeriesRecording) "Cancel Series Recording" else "Record Series",
|
text =
|
||||||
|
if (isSeriesRecording) {
|
||||||
|
stringResource(
|
||||||
|
R.string.cancel_series_recording,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.record_series)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +202,16 @@ fun ProgramDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = if (isRecording) "Cancel Recording" else "Record Program",
|
text =
|
||||||
|
if (isRecording) {
|
||||||
|
stringResource(
|
||||||
|
R.string.cancel_recording,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(
|
||||||
|
R.string.record_program,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,11 +228,11 @@ fun ProgramDialog(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.PlayArrow,
|
imageVector = Icons.Default.PlayArrow,
|
||||||
contentDescription = "Delete",
|
contentDescription = stringResource(R.string.delete),
|
||||||
tint = Color.Green.copy(alpha = .75f),
|
tint = Color.Green.copy(alpha = .75f),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Watch live",
|
text = stringResource(R.string.watch_live),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
|
@ -109,6 +110,15 @@ fun MovieDetails(
|
||||||
-> LoadingPage()
|
-> LoadingPage()
|
||||||
LoadingState.Success -> {
|
LoadingState.Success -> {
|
||||||
item?.let { movie ->
|
item?.let { movie ->
|
||||||
|
LifecycleStartEffect(destination.itemId) {
|
||||||
|
viewModel.maybePlayThemeSong(
|
||||||
|
destination.itemId,
|
||||||
|
preferences.appPreferences.interfacePreferences.playThemeSongs,
|
||||||
|
)
|
||||||
|
onStopOrDispose {
|
||||||
|
viewModel.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
MovieDetailsContent(
|
MovieDetailsContent(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
movie = movie,
|
movie = movie,
|
||||||
|
|
@ -118,10 +128,10 @@ fun MovieDetails(
|
||||||
trailers = trailers,
|
trailers = trailers,
|
||||||
similar = similar,
|
similar = similar,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
viewModel.navigationManager.navigateTo(it.destination())
|
viewModel.navigateTo(it.destination())
|
||||||
},
|
},
|
||||||
onClickPerson = {
|
onClickPerson = {
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigateTo(
|
||||||
Destination.MediaItem(
|
Destination.MediaItem(
|
||||||
it.id,
|
it.id,
|
||||||
BaseItemKind.PERSON,
|
BaseItemKind.PERSON,
|
||||||
|
|
@ -129,7 +139,7 @@ fun MovieDetails(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
playOnClick = {
|
playOnClick = {
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
movie.id,
|
movie.id,
|
||||||
it.inWholeMilliseconds,
|
it.inWholeMilliseconds,
|
||||||
|
|
@ -140,7 +150,7 @@ fun MovieDetails(
|
||||||
overviewOnClick = {
|
overviewOnClick = {
|
||||||
overviewDialog =
|
overviewDialog =
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = movie.name ?: "Unknown",
|
title = movie.name ?: context.getString(R.string.unknown),
|
||||||
overview = movie.data.overview,
|
overview = movie.data.overview,
|
||||||
files = movie.data.mediaSources.orEmpty(),
|
files = movie.data.mediaSources.orEmpty(),
|
||||||
)
|
)
|
||||||
|
|
@ -152,17 +162,21 @@ fun MovieDetails(
|
||||||
title = movie.name + " (${movie.data.productionYear ?: ""})",
|
title = movie.name + " (${movie.data.productionYear ?: ""})",
|
||||||
items =
|
items =
|
||||||
buildMoreDialogItems(
|
buildMoreDialogItems(
|
||||||
|
context = context,
|
||||||
item = movie,
|
item = movie,
|
||||||
watched = movie.data.userData?.played ?: false,
|
watched = movie.data.userData?.played ?: false,
|
||||||
favorite = movie.data.userData?.isFavorite ?: false,
|
favorite = movie.data.userData?.isFavorite ?: false,
|
||||||
series = null,
|
series = null,
|
||||||
sourceId = chosenStreams?.sourceId,
|
sourceId = chosenStreams?.sourceId,
|
||||||
navigateTo = viewModel.navigationManager::navigateTo,
|
navigateTo = viewModel::navigateTo,
|
||||||
onClickWatch = viewModel::setWatched,
|
onClickWatch = viewModel::setWatched,
|
||||||
onClickFavorite = viewModel::setFavorite,
|
onClickFavorite = viewModel::setFavorite,
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
chooseVersionParams(movie.data.mediaSources!!) { idx ->
|
chooseVersionParams(
|
||||||
|
context,
|
||||||
|
movie.data.mediaSources!!,
|
||||||
|
) { idx ->
|
||||||
val source = movie.data.mediaSources!![idx]
|
val source = movie.data.mediaSources!![idx]
|
||||||
viewModel.savePlayVersion(
|
viewModel.savePlayVersion(
|
||||||
movie,
|
movie,
|
||||||
|
|
@ -178,6 +192,7 @@ fun MovieDetails(
|
||||||
)?.let { source ->
|
)?.let { source ->
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
chooseStream(
|
chooseStream(
|
||||||
|
context = context,
|
||||||
streams = source.mediaStreams.orEmpty(),
|
streams = source.mediaStreams.orEmpty(),
|
||||||
type = type,
|
type = type,
|
||||||
onClick = { trackIndex ->
|
onClick = { trackIndex ->
|
||||||
|
|
@ -203,7 +218,7 @@ fun MovieDetails(
|
||||||
trailerOnClick = { trailer ->
|
trailerOnClick = { trailer ->
|
||||||
when (trailer) {
|
when (trailer) {
|
||||||
is LocalTrailer ->
|
is LocalTrailer ->
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
itemId = trailer.baseItem.id,
|
itemId = trailer.baseItem.id,
|
||||||
item = trailer.baseItem,
|
item = trailer.baseItem,
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ fun MovieDetailsHeader(
|
||||||
?.joinToString(", ") { it.name!! }
|
?.joinToString(", ") { it.name!! }
|
||||||
?.let {
|
?.let {
|
||||||
Text(
|
Text(
|
||||||
text = "Directed by $it",
|
text = stringResource(R.string.directed_by, it),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
@ -183,7 +183,7 @@ fun MovieDetailsHeader(
|
||||||
?.let {
|
?.let {
|
||||||
if (it.isNotNullOrBlank()) {
|
if (it.isNotNullOrBlank()) {
|
||||||
TitleValueText(
|
TitleValueText(
|
||||||
"Subtitles",
|
stringResource(R.string.subtitles),
|
||||||
it,
|
it,
|
||||||
modifier = Modifier.widthIn(max = 200.dp),
|
modifier = Modifier.widthIn(max = 200.dp),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.movie
|
package com.github.damontecres.wholphin.ui.detail.movie
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
|
@ -12,16 +14,20 @@ import com.github.damontecres.wholphin.data.model.LocalTrailer
|
||||||
import com.github.damontecres.wholphin.data.model.Person
|
import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.data.model.RemoteTrailer
|
import com.github.damontecres.wholphin.data.model.RemoteTrailer
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
|
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.detail.LoadingItemViewModel
|
import com.github.damontecres.wholphin.ui.detail.LoadingItemViewModel
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import com.github.damontecres.wholphin.util.ThemeSongPlayer
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -40,21 +46,20 @@ class MovieViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
val navigationManager: NavigationManager,
|
@param:ApplicationContext private val context: Context,
|
||||||
|
private val navigationManager: NavigationManager,
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val itemPlaybackRepository: ItemPlaybackRepository,
|
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||||
|
private val themeSongPlayer: ThemeSongPlayer,
|
||||||
) : LoadingItemViewModel(api) {
|
) : LoadingItemViewModel(api) {
|
||||||
private lateinit var itemId: UUID
|
|
||||||
|
|
||||||
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
||||||
val people = MutableLiveData<List<Person>>(listOf())
|
val people = MutableLiveData<List<Person>>(listOf())
|
||||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||||
val similar = MutableLiveData<List<BaseItem>>()
|
val similar = MutableLiveData<List<BaseItem>>()
|
||||||
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
||||||
|
|
||||||
fun init(itemId: UUID): Job? {
|
fun init(itemId: UUID): Job? =
|
||||||
this.itemId = itemId
|
viewModelScope.launch(
|
||||||
return viewModelScope.launch(
|
|
||||||
Dispatchers.IO +
|
Dispatchers.IO +
|
||||||
LoadingExceptionHandler(
|
LoadingExceptionHandler(
|
||||||
loading,
|
loading,
|
||||||
|
|
@ -78,7 +83,7 @@ class MovieViewModel
|
||||||
// TODO would be nice to clean up the trailer name
|
// TODO would be nice to clean up the trailer name
|
||||||
// ?.replace(item.name ?: "", "")
|
// ?.replace(item.name ?: "", "")
|
||||||
// ?.removePrefix(" - ")
|
// ?.removePrefix(" - ")
|
||||||
?: "Trailer"
|
?: context.getString(R.string.trailer)
|
||||||
RemoteTrailer(name, url)
|
RemoteTrailer(name, url)
|
||||||
}
|
}
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
|
|
@ -120,7 +125,6 @@ class MovieViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun setWatched(played: Boolean) =
|
fun setWatched(played: Boolean) =
|
||||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
|
@ -181,4 +185,25 @@ class MovieViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun maybePlayThemeSong(
|
||||||
|
seriesId: UUID,
|
||||||
|
playThemeSongs: ThemeSongVolume,
|
||||||
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
themeSongPlayer.playThemeFor(seriesId, playThemeSongs)
|
||||||
|
addCloseable {
|
||||||
|
themeSongPlayer.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
themeSongPlayer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateTo(destination: Destination) {
|
||||||
|
release()
|
||||||
|
navigationManager.navigateTo(destination)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ fun FocusedEpisodeFooter(
|
||||||
?.let {
|
?.let {
|
||||||
if (it.isNotNullOrBlank()) {
|
if (it.isNotNullOrBlank()) {
|
||||||
TitleValueText(
|
TitleValueText(
|
||||||
"Subtitles",
|
stringResource(R.string.subtitles),
|
||||||
it,
|
it,
|
||||||
modifier = Modifier.widthIn(max = 160.dp),
|
modifier = Modifier.widthIn(max = 160.dp),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.series
|
package com.github.damontecres.wholphin.ui.detail.series
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -33,6 +34,7 @@ import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -83,15 +85,7 @@ fun SeriesDetails(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SeriesViewModel = hiltViewModel(),
|
viewModel: SeriesViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
LifecycleStartEffect(destination.itemId) {
|
val context = LocalContext.current
|
||||||
viewModel.maybePlayThemeSong(
|
|
||||||
destination.itemId,
|
|
||||||
preferences.appPreferences.interfacePreferences.playThemeSongs,
|
|
||||||
)
|
|
||||||
onStopOrDispose {
|
|
||||||
viewModel.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.init(preferences, destination.itemId, destination.item, null)
|
viewModel.init(preferences, destination.itemId, destination.item, null)
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +107,16 @@ fun SeriesDetails(
|
||||||
-> LoadingPage()
|
-> LoadingPage()
|
||||||
LoadingState.Success -> {
|
LoadingState.Success -> {
|
||||||
item?.let { item ->
|
item?.let { item ->
|
||||||
|
LifecycleStartEffect(destination.itemId) {
|
||||||
|
viewModel.maybePlayThemeSong(
|
||||||
|
destination.itemId,
|
||||||
|
preferences.appPreferences.interfacePreferences.playThemeSongs,
|
||||||
|
)
|
||||||
|
onStopOrDispose {
|
||||||
|
viewModel.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val played = item.data.userData?.played ?: false
|
val played = item.data.userData?.played ?: false
|
||||||
SeriesDetailsContent(
|
SeriesDetailsContent(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
|
|
@ -135,7 +139,8 @@ fun SeriesDetails(
|
||||||
onLongClickItem = { season ->
|
onLongClickItem = { season ->
|
||||||
seasonDialog =
|
seasonDialog =
|
||||||
buildDialogForSeason(
|
buildDialogForSeason(
|
||||||
season,
|
context = context,
|
||||||
|
s = season,
|
||||||
onClickItem = { viewModel.navigateTo(it.destination()) },
|
onClickItem = { viewModel.navigateTo(it.destination()) },
|
||||||
markPlayed = { played ->
|
markPlayed = { played ->
|
||||||
viewModel.setSeasonWatched(season.id, played)
|
viewModel.setSeasonWatched(season.id, played)
|
||||||
|
|
@ -145,7 +150,7 @@ fun SeriesDetails(
|
||||||
overviewOnClick = {
|
overviewOnClick = {
|
||||||
overviewDialog =
|
overviewDialog =
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = item.name ?: "Unknown",
|
title = item.name ?: context.getString(R.string.unknown),
|
||||||
overview = item.data.overview,
|
overview = item.data.overview,
|
||||||
files = listOf(),
|
files = listOf(),
|
||||||
)
|
)
|
||||||
|
|
@ -160,7 +165,8 @@ fun SeriesDetails(
|
||||||
if (showWatchConfirmation) {
|
if (showWatchConfirmation) {
|
||||||
ConfirmDialog(
|
ConfirmDialog(
|
||||||
title = item.name ?: "",
|
title = item.name ?: "",
|
||||||
body = if (played) "Mark entire series as unplayed?" else "Mark entire series as played?",
|
body =
|
||||||
|
stringResource(if (played) R.string.mark_entire_series_as_unplayed else R.string.mark_entire_series_as_played),
|
||||||
onCancel = {
|
onCancel = {
|
||||||
showWatchConfirmation = false
|
showWatchConfirmation = false
|
||||||
},
|
},
|
||||||
|
|
@ -342,7 +348,7 @@ fun SeriesDetailsContent(
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = "Seasons",
|
title = stringResource(R.string.tv_seasons),
|
||||||
items = seasons,
|
items = seasons,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
position = SEASONS_ROW
|
position = SEASONS_ROW
|
||||||
|
|
@ -443,7 +449,7 @@ fun SeriesDetailsHeader(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = series.name ?: "Unknown",
|
text = series.name ?: stringResource(R.string.unknown),
|
||||||
style = MaterialTheme.typography.displaySmall,
|
style = MaterialTheme.typography.displaySmall,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
@ -486,6 +492,7 @@ fun SeriesDetailsHeader(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildDialogForSeason(
|
fun buildDialogForSeason(
|
||||||
|
context: Context,
|
||||||
s: BaseItem,
|
s: BaseItem,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
markPlayed: (Boolean) -> Unit,
|
markPlayed: (Boolean) -> Unit,
|
||||||
|
|
@ -493,26 +500,26 @@ fun buildDialogForSeason(
|
||||||
val items =
|
val items =
|
||||||
buildList {
|
buildList {
|
||||||
add(
|
add(
|
||||||
DialogItem("Go to", Icons.Default.PlayArrow) {
|
DialogItem(context.getString(R.string.go_to), Icons.Default.PlayArrow) {
|
||||||
onClickItem.invoke(s)
|
onClickItem.invoke(s)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (s.data.userData?.played == true) {
|
if (s.data.userData?.played == true) {
|
||||||
add(
|
add(
|
||||||
DialogItem("Mark as unplayed", R.string.fa_eye) {
|
DialogItem(context.getString(R.string.mark_unwatched), R.string.fa_eye) {
|
||||||
markPlayed.invoke(false)
|
markPlayed.invoke(false)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
add(
|
add(
|
||||||
DialogItem("Mark as played", R.string.fa_eye_slash) {
|
DialogItem(context.getString(R.string.mark_watched), R.string.fa_eye_slash) {
|
||||||
markPlayed.invoke(true)
|
markPlayed.invoke(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return DialogParams(
|
return DialogParams(
|
||||||
title = s.name ?: "Season",
|
title = s.name ?: context.getString(R.string.tv_season),
|
||||||
fromLongClick = true,
|
fromLongClick = true,
|
||||||
items = items,
|
items = items,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -72,15 +74,7 @@ fun SeriesOverview(
|
||||||
viewModel: SeriesViewModel = hiltViewModel(),
|
viewModel: SeriesViewModel = hiltViewModel(),
|
||||||
initialSeasonEpisode: SeasonEpisodeIds? = null,
|
initialSeasonEpisode: SeasonEpisodeIds? = null,
|
||||||
) {
|
) {
|
||||||
LifecycleStartEffect(destination.itemId) {
|
val context = LocalContext.current
|
||||||
viewModel.maybePlayThemeSong(
|
|
||||||
destination.itemId,
|
|
||||||
preferences.appPreferences.interfacePreferences.playThemeSongs,
|
|
||||||
)
|
|
||||||
onStopOrDispose {
|
|
||||||
viewModel.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val firstItemFocusRequester = remember { FocusRequester() }
|
val firstItemFocusRequester = remember { FocusRequester() }
|
||||||
val episodeRowFocusRequester = remember { FocusRequester() }
|
val episodeRowFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
|
@ -168,6 +162,15 @@ fun SeriesOverview(
|
||||||
LoadingState.Success -> {
|
LoadingState.Success -> {
|
||||||
series?.let { series ->
|
series?.let { series ->
|
||||||
LaunchedEffect(Unit) { episodeRowFocusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { episodeRowFocusRequester.tryRequestFocus() }
|
||||||
|
LifecycleStartEffect(destination.itemId) {
|
||||||
|
viewModel.maybePlayThemeSong(
|
||||||
|
destination.itemId,
|
||||||
|
preferences.appPreferences.interfacePreferences.playThemeSongs,
|
||||||
|
)
|
||||||
|
onStopOrDispose {
|
||||||
|
viewModel.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun buildMoreForEpisode(
|
fun buildMoreForEpisode(
|
||||||
ep: BaseItem,
|
ep: BaseItem,
|
||||||
|
|
@ -178,7 +181,8 @@ fun SeriesOverview(
|
||||||
title = series.name + " - " + ep.data.seasonEpisode,
|
title = series.name + " - " + ep.data.seasonEpisode,
|
||||||
items =
|
items =
|
||||||
buildMoreDialogItems(
|
buildMoreDialogItems(
|
||||||
ep,
|
context = context,
|
||||||
|
item = ep,
|
||||||
watched = ep.data.userData?.played ?: false,
|
watched = ep.data.userData?.played ?: false,
|
||||||
favorite = ep.data.userData?.isFavorite ?: false,
|
favorite = ep.data.userData?.isFavorite ?: false,
|
||||||
series = series,
|
series = series,
|
||||||
|
|
@ -208,7 +212,10 @@ fun SeriesOverview(
|
||||||
},
|
},
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
chooseVersionParams(ep.data.mediaSources!!) { idx ->
|
chooseVersionParams(
|
||||||
|
context,
|
||||||
|
ep.data.mediaSources!!,
|
||||||
|
) { idx ->
|
||||||
val source = ep.data.mediaSources!![idx]
|
val source = ep.data.mediaSources!![idx]
|
||||||
viewModel.savePlayVersion(
|
viewModel.savePlayVersion(
|
||||||
ep,
|
ep,
|
||||||
|
|
@ -224,6 +231,7 @@ fun SeriesOverview(
|
||||||
)?.let { source ->
|
)?.let { source ->
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
chooseStream(
|
chooseStream(
|
||||||
|
context = context,
|
||||||
streams = source.mediaStreams.orEmpty(),
|
streams = source.mediaStreams.orEmpty(),
|
||||||
type = type,
|
type = type,
|
||||||
onClick = { trackIndex ->
|
onClick = { trackIndex ->
|
||||||
|
|
@ -313,7 +321,7 @@ fun SeriesOverview(
|
||||||
episodeList?.getOrNull(position.episodeRowIndex)?.let {
|
episodeList?.getOrNull(position.episodeRowIndex)?.let {
|
||||||
overviewDialog =
|
overviewDialog =
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = it.name ?: "Unknown",
|
title = it.name ?: context.getString(R.string.unknown),
|
||||||
overview = it.data.overview,
|
overview = it.data.overview,
|
||||||
files = it.data.mediaSources.orEmpty(),
|
files = it.data.mediaSources.orEmpty(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
@ -42,6 +43,7 @@ import androidx.tv.material3.TabRow
|
||||||
import androidx.tv.material3.TabRowDefaults
|
import androidx.tv.material3.TabRowDefaults
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
||||||
|
|
@ -178,7 +180,9 @@ fun SeriesOverviewContent(
|
||||||
.focusRequester(focusRequesters[index]),
|
.focusRequester(focusRequesters[index]),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = season.name ?: "Season ${season.data.indexNumber}",
|
text =
|
||||||
|
season.name
|
||||||
|
?: (stringResource(R.string.tv_season) + " ${season.data.indexNumber}"),
|
||||||
modifier = Modifier.padding(8.dp),
|
modifier = Modifier.padding(8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@ package com.github.damontecres.wholphin.ui.detail.series
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.OptIn
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
|
@ -30,7 +28,6 @@ import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.ThemeSongPlayer
|
import com.github.damontecres.wholphin.util.ThemeSongPlayer
|
||||||
import com.github.damontecres.wholphin.util.profile.Codec
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -41,7 +38,6 @@ import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.universalAudioApi
|
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
|
@ -88,7 +84,7 @@ class SeriesViewModel
|
||||||
"Error loading series $seriesId",
|
"Error loading series $seriesId",
|
||||||
) + Dispatchers.IO,
|
) + Dispatchers.IO,
|
||||||
) {
|
) {
|
||||||
val item = fetchItem(seriesId, potential)
|
val item = fetchItem(seriesId)
|
||||||
val seasons = getSeasons(item)
|
val seasons = getSeasons(item)
|
||||||
|
|
||||||
// If a particular season was requested, fetch those episodes, otherwise get the first season
|
// If a particular season was requested, fetch those episodes, otherwise get the first season
|
||||||
|
|
@ -141,34 +137,14 @@ class SeriesViewModel
|
||||||
/**
|
/**
|
||||||
* If the series has a theme song & app settings allow, play it
|
* If the series has a theme song & app settings allow, play it
|
||||||
*/
|
*/
|
||||||
@OptIn(UnstableApi::class)
|
|
||||||
fun maybePlayThemeSong(
|
fun maybePlayThemeSong(
|
||||||
seriesId: UUID,
|
seriesId: UUID,
|
||||||
playThemeSongs: ThemeSongVolume,
|
playThemeSongs: ThemeSongVolume,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val themeSongs = api.libraryApi.getThemeSongs(seriesId).content
|
themeSongPlayer.playThemeFor(seriesId, playThemeSongs)
|
||||||
themeSongs.items.firstOrNull()?.let { theme ->
|
addCloseable {
|
||||||
theme.mediaSources?.firstOrNull()?.let { source ->
|
themeSongPlayer.stop()
|
||||||
val url =
|
|
||||||
api.universalAudioApi.getUniversalAudioStreamUrl(
|
|
||||||
theme.id,
|
|
||||||
container =
|
|
||||||
listOf(
|
|
||||||
Codec.Audio.OPUS,
|
|
||||||
Codec.Audio.MP3,
|
|
||||||
Codec.Audio.AAC,
|
|
||||||
Codec.Audio.FLAC,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Timber.v("Found theme song for series $seriesId")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
themeSongPlayer.play(playThemeSongs, url)
|
|
||||||
addCloseable {
|
|
||||||
themeSongPlayer.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -287,7 +263,7 @@ class SeriesViewModel
|
||||||
if (listIndex != null) {
|
if (listIndex != null) {
|
||||||
refreshEpisode(itemId, listIndex)
|
refreshEpisode(itemId, listIndex)
|
||||||
} else {
|
} else {
|
||||||
fetchItem(seriesId, null)
|
fetchItem(seriesId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,7 +272,7 @@ class SeriesViewModel
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
setWatched(seasonId, played, null)
|
setWatched(seasonId, played, null)
|
||||||
val series = fetchItem(seriesId, null)
|
val series = fetchItem(seriesId)
|
||||||
val seasons = getSeasons(series)
|
val seasons = getSeasons(series)
|
||||||
this@SeriesViewModel.seasons.setValueOnMain(seasons)
|
this@SeriesViewModel.seasons.setValueOnMain(seasons)
|
||||||
}
|
}
|
||||||
|
|
@ -308,7 +284,7 @@ class SeriesViewModel
|
||||||
} else {
|
} else {
|
||||||
api.playStateApi.markUnplayedItem(seriesId)
|
api.playStateApi.markUnplayedItem(seriesId)
|
||||||
}
|
}
|
||||||
val series = fetchItem(seriesId, null)
|
val series = fetchItem(seriesId)
|
||||||
val seasons = getSeasons(series)
|
val seasons = getSeasons(series)
|
||||||
this@SeriesViewModel.seasons.setValueOnMain(seasons)
|
this@SeriesViewModel.seasons.setValueOnMain(seasons)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.ui.main
|
package com.github.damontecres.wholphin.ui.main
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -71,10 +72,20 @@ import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
data class HomeRow(
|
data class HomeRow(
|
||||||
val section: HomeSection,
|
@param:StringRes val titleRes: Int?,
|
||||||
|
val title: String?,
|
||||||
val items: List<BaseItem?>,
|
val items: List<BaseItem?>,
|
||||||
val title: String? = null,
|
) {
|
||||||
)
|
constructor(
|
||||||
|
@StringRes titleRes: Int,
|
||||||
|
items: List<BaseItem?>,
|
||||||
|
) : this(titleRes, null, items)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
title: String,
|
||||||
|
items: List<BaseItem?>,
|
||||||
|
) : this(null, title, items)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePage(
|
fun HomePage(
|
||||||
|
|
@ -121,6 +132,7 @@ fun HomePage(
|
||||||
onLongClickItem = {
|
onLongClickItem = {
|
||||||
val dialogItems =
|
val dialogItems =
|
||||||
buildMoreDialogItemsForHome(
|
buildMoreDialogItemsForHome(
|
||||||
|
context = context,
|
||||||
item = it,
|
item = it,
|
||||||
seriesId = it.data.seriesId,
|
seriesId = it.data.seriesId,
|
||||||
playbackPosition =
|
playbackPosition =
|
||||||
|
|
@ -242,7 +254,7 @@ fun HomePageContent(
|
||||||
itemsIndexed(homeRows) { rowIndex, row ->
|
itemsIndexed(homeRows) { rowIndex, row ->
|
||||||
if (row.items.isNotEmpty()) {
|
if (row.items.isNotEmpty()) {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = row.title ?: stringResource(row.section.nameRes),
|
title = row.title ?: row.titleRes?.let { stringResource(it) } ?: "",
|
||||||
items = row.items,
|
items = row.items,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
cardOnFocus = { isFocused, index ->
|
cardOnFocus = { isFocused, index ->
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
package com.github.damontecres.wholphin.ui.main
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import com.github.damontecres.wholphin.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All possible homesections, "synced" with jellyfin-web.
|
|
||||||
*
|
|
||||||
* https://github.com/jellyfin/jellyfin-web/blob/master/src/components/homesections/homesections.js
|
|
||||||
*/
|
|
||||||
enum class HomeSection(
|
|
||||||
val key: String,
|
|
||||||
@param:StringRes val nameRes: Int,
|
|
||||||
) {
|
|
||||||
LATEST_MEDIA("latestmedia", R.string.home_section_latest_media),
|
|
||||||
LIBRARY_TILES_SMALL("smalllibrarytiles", R.string.home_section_library),
|
|
||||||
LIBRARY_BUTTONS("librarybuttons", R.string.home_section_library_small),
|
|
||||||
RESUME("resume", R.string.home_section_resume),
|
|
||||||
RESUME_AUDIO("resumeaudio", R.string.home_section_resume_audio),
|
|
||||||
RESUME_BOOK("resumebook", R.string.home_section_resume_book),
|
|
||||||
ACTIVE_RECORDINGS("activerecordings", R.string.home_section_active_recordings),
|
|
||||||
NEXT_UP("nextup", R.string.home_section_next_up),
|
|
||||||
LIVE_TV("livetv", R.string.home_section_livetv),
|
|
||||||
NONE("none", R.string.home_section_none),
|
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromKey(key: String): HomeSection = entries.firstOrNull { it.key == key } ?: NONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package com.github.damontecres.wholphin.ui.main
|
package com.github.damontecres.wholphin.ui.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
|
@ -15,6 +17,7 @@ import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.supportItemKinds
|
import com.github.damontecres.wholphin.util.supportItemKinds
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -40,6 +43,7 @@ import javax.inject.Inject
|
||||||
class HomeViewModel
|
class HomeViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
|
|
@ -86,7 +90,7 @@ class HomeViewModel
|
||||||
if (resume.isNotEmpty()) {
|
if (resume.isNotEmpty()) {
|
||||||
add(
|
add(
|
||||||
HomeRow(
|
HomeRow(
|
||||||
section = HomeSection.RESUME,
|
titleRes = R.string.recently_added,
|
||||||
items = resume,
|
items = resume,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -94,7 +98,7 @@ class HomeViewModel
|
||||||
if (nextUp.isNotEmpty()) {
|
if (nextUp.isNotEmpty()) {
|
||||||
add(
|
add(
|
||||||
HomeRow(
|
HomeRow(
|
||||||
section = HomeSection.NEXT_UP,
|
titleRes = R.string.next_up,
|
||||||
items = nextUp,
|
items = nextUp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -191,10 +195,10 @@ class HomeViewModel
|
||||||
.mapNotNull { view ->
|
.mapNotNull { view ->
|
||||||
val title =
|
val title =
|
||||||
if (view.collectionType == CollectionType.LIVETV) {
|
if (view.collectionType == CollectionType.LIVETV) {
|
||||||
"Recently Recorded"
|
context.getString(R.string.recently_recorded)
|
||||||
} else {
|
} else {
|
||||||
view.name?.let { "Recently Added in $it" }
|
view.name?.let { context.getString(R.string.recently_added_in, it) }
|
||||||
}
|
} ?: context.getString(R.string.recently_added)
|
||||||
val viewId =
|
val viewId =
|
||||||
if (view.collectionType == CollectionType.LIVETV) {
|
if (view.collectionType == CollectionType.LIVETV) {
|
||||||
api.liveTvApi
|
api.liveTvApi
|
||||||
|
|
@ -222,9 +226,8 @@ class HomeViewModel
|
||||||
.content
|
.content
|
||||||
.map { BaseItem.from(it, api, true) }
|
.map { BaseItem.from(it, api, true) }
|
||||||
HomeRow(
|
HomeRow(
|
||||||
section = HomeSection.LATEST_MEDIA,
|
|
||||||
items = latest,
|
|
||||||
title = title,
|
title = title,
|
||||||
|
items = latest,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
|
@ -28,6 +30,7 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.Cards
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
|
|
@ -153,6 +156,7 @@ fun SearchPage(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SearchViewModel = hiltViewModel(),
|
viewModel: SearchViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
val movies by viewModel.movies.observeAsState(SearchResult.NoQuery)
|
val movies by viewModel.movies.observeAsState(SearchResult.NoQuery)
|
||||||
val collections by viewModel.collections.observeAsState(SearchResult.NoQuery)
|
val collections by viewModel.collections.observeAsState(SearchResult.NoQuery)
|
||||||
val series by viewModel.series.observeAsState(SearchResult.NoQuery)
|
val series by viewModel.series.observeAsState(SearchResult.NoQuery)
|
||||||
|
|
@ -198,7 +202,7 @@ fun SearchPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
searchResultRow(
|
searchResultRow(
|
||||||
title = "Movies",
|
title = context.getString(R.string.movies),
|
||||||
result = movies,
|
result = movies,
|
||||||
rowIndex = MOVIE_ROW,
|
rowIndex = MOVIE_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
|
|
@ -208,7 +212,7 @@ fun SearchPage(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
searchResultRow(
|
searchResultRow(
|
||||||
title = "Collections",
|
title = context.getString(R.string.collections),
|
||||||
result = collections,
|
result = collections,
|
||||||
rowIndex = COLLECTION_ROW,
|
rowIndex = COLLECTION_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
|
|
@ -218,7 +222,7 @@ fun SearchPage(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
searchResultRow(
|
searchResultRow(
|
||||||
title = "Series",
|
title = context.getString(R.string.tv_shows),
|
||||||
result = series,
|
result = series,
|
||||||
rowIndex = SERIES_ROW,
|
rowIndex = SERIES_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
|
|
@ -228,7 +232,7 @@ fun SearchPage(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
searchResultRow(
|
searchResultRow(
|
||||||
title = "Episodes",
|
title = context.getString(R.string.episodes),
|
||||||
result = episodes,
|
result = episodes,
|
||||||
rowIndex = EPISODE_ROW,
|
rowIndex = EPISODE_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
|
|
@ -308,7 +312,7 @@ fun LazyListScope.searchResultRow(
|
||||||
SearchResult.Searching ->
|
SearchResult.Searching ->
|
||||||
SearchResultPlaceholder(
|
SearchResultPlaceholder(
|
||||||
title = title,
|
title = title,
|
||||||
message = "Searching...",
|
message = stringResource(R.string.searching),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -316,7 +320,7 @@ fun LazyListScope.searchResultRow(
|
||||||
if (r.items.isEmpty()) {
|
if (r.items.isEmpty()) {
|
||||||
SearchResultPlaceholder(
|
SearchResultPlaceholder(
|
||||||
title = title,
|
title = title,
|
||||||
message = "No results",
|
message = stringResource(R.string.no_results),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,8 @@ fun NavDrawer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (preferences.appPreferences.interfacePreferences.navDrawerSwitchOnFocus) {
|
// Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418
|
||||||
|
if (false && preferences.appPreferences.interfacePreferences.navDrawerSwitchOnFocus) {
|
||||||
LaunchedEffect(derivedFocusedIndex) {
|
LaunchedEffect(derivedFocusedIndex) {
|
||||||
val index = derivedFocusedIndex
|
val index = derivedFocusedIndex
|
||||||
delay(600)
|
delay(600)
|
||||||
|
|
@ -323,7 +324,7 @@ fun NavDrawer(
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
LaunchedEffect(focused) { if (focused) focusedIndex = -2 }
|
LaunchedEffect(focused) { if (focused) focusedIndex = -2 }
|
||||||
IconNavItem(
|
IconNavItem(
|
||||||
text = "Search",
|
text = stringResource(R.string.search),
|
||||||
icon = Icons.Default.Search,
|
icon = Icons.Default.Search,
|
||||||
selected = selectedIndex == -2,
|
selected = selectedIndex == -2,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
@ -344,7 +345,7 @@ fun NavDrawer(
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
LaunchedEffect(focused) { if (focused) focusedIndex = -1 }
|
LaunchedEffect(focused) { if (focused) focusedIndex = -1 }
|
||||||
IconNavItem(
|
IconNavItem(
|
||||||
text = "Home",
|
text = stringResource(R.string.home),
|
||||||
icon = Icons.Default.Home,
|
icon = Icons.Default.Home,
|
||||||
selected = selectedIndex == -1,
|
selected = selectedIndex == -1,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
@ -412,7 +413,7 @@ fun NavDrawer(
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
LaunchedEffect(focused) { if (focused) focusedIndex = Int.MIN_VALUE }
|
LaunchedEffect(focused) { if (focused) focusedIndex = Int.MIN_VALUE }
|
||||||
IconNavItem(
|
IconNavItem(
|
||||||
text = "Settings",
|
text = stringResource(R.string.settings),
|
||||||
icon = Icons.Default.Settings,
|
icon = Icons.Default.Settings,
|
||||||
selected = false,
|
selected = false,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
|
|
@ -35,6 +37,7 @@ import androidx.tv.material3.ListItem
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogItemDivider
|
import com.github.damontecres.wholphin.ui.components.DialogItemDivider
|
||||||
|
|
@ -58,7 +61,7 @@ fun DownloadSubtitlesContent(
|
||||||
SubtitleSearch.Searching -> {
|
SubtitleSearch.Searching -> {
|
||||||
Wrapper {
|
Wrapper {
|
||||||
Text(
|
Text(
|
||||||
text = "Searching...",
|
text = stringResource(R.string.searching),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
@ -68,7 +71,7 @@ fun DownloadSubtitlesContent(
|
||||||
SubtitleSearch.Downloading -> {
|
SubtitleSearch.Downloading -> {
|
||||||
Wrapper {
|
Wrapper {
|
||||||
Text(
|
Text(
|
||||||
text = "Downloading...",
|
text = stringResource(R.string.downloading),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
@ -199,9 +202,15 @@ fun convertRemoteSubtitles(
|
||||||
op.providerName?.let(::add)
|
op.providerName?.let(::add)
|
||||||
op.threeLetterIsoLanguageName?.let(::add)
|
op.threeLetterIsoLanguageName?.let(::add)
|
||||||
if (op.forced == true) {
|
if (op.forced == true) {
|
||||||
add("Forced")
|
add(stringResource(R.string.forced_track))
|
||||||
}
|
}
|
||||||
add("${abbreviateNumber(op.downloadCount ?: 0)} downloads")
|
add(
|
||||||
|
pluralStringResource(
|
||||||
|
R.plurals.downloads,
|
||||||
|
op.downloadCount ?: 0,
|
||||||
|
abbreviateNumber(op.downloadCount ?: 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = strings.joinToString(" - "),
|
text = strings.joinToString(" - "),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package com.github.damontecres.wholphin.ui.playback
|
package com.github.damontecres.wholphin.ui.playback
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.WholphinApplication
|
||||||
|
|
||||||
data class SubtitleStream(
|
data class SubtitleStream(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
val language: String?,
|
val language: String?,
|
||||||
|
|
@ -17,7 +20,8 @@ data class SubtitleStream(
|
||||||
language,
|
language,
|
||||||
title,
|
title,
|
||||||
codec,
|
codec,
|
||||||
).joinToString(" - ").ifBlank { "Unknown" }
|
).joinToString(" - ")
|
||||||
|
.ifBlank { WholphinApplication.instance.getString(R.string.unknown) }
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AudioStream(
|
data class AudioStream(
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -34,6 +35,7 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
|
|
@ -68,7 +70,7 @@ fun NextUpEpisode(
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Up Next...",
|
text = stringResource(R.string.next_up) + "...",
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
@ -66,6 +67,7 @@ import com.github.damontecres.wholphin.ui.seekBack
|
||||||
import com.github.damontecres.wholphin.ui.seekForward
|
import com.github.damontecres.wholphin.ui.seekForward
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.stringRes
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -212,7 +214,7 @@ fun PlaybackControls(
|
||||||
.padding(end = 32.dp),
|
.padding(end = 32.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Skip ${segment.type.serialName}",
|
text = stringResource(R.string.skip) + " " + stringResource(segment.type.stringRes),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,7 +334,7 @@ fun LeftPlaybackButtons(
|
||||||
val options =
|
val options =
|
||||||
buildList {
|
buildList {
|
||||||
addAll(moreButtonOptions.options.keys)
|
addAll(moreButtonOptions.options.keys)
|
||||||
add(if (showDebugInfo) "Hide debug info" else "Show debug info")
|
add(stringResource(if (showDebugInfo) R.string.hide_debug_info else R.string.show_debug_info))
|
||||||
}
|
}
|
||||||
BottomDialog(
|
BottomDialog(
|
||||||
choices = options,
|
choices = options,
|
||||||
|
|
@ -407,7 +409,12 @@ fun RightPlaybackButtons(
|
||||||
val currentChoice =
|
val currentChoice =
|
||||||
subtitleStreams.indexOfFirstOrNull { it.index == subtitleIndex } ?: subtitleStreams.size
|
subtitleStreams.indexOfFirstOrNull { it.index == subtitleIndex } ?: subtitleStreams.size
|
||||||
BottomDialog(
|
BottomDialog(
|
||||||
choices = options + listOf("None", "Search & Download"),
|
choices =
|
||||||
|
options +
|
||||||
|
listOf(
|
||||||
|
stringResource(R.string.none),
|
||||||
|
stringResource(R.string.search_and_download),
|
||||||
|
),
|
||||||
currentChoice = currentChoice,
|
currentChoice = currentChoice,
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
onControllerInteraction.invoke()
|
onControllerInteraction.invoke()
|
||||||
|
|
@ -434,7 +441,12 @@ fun RightPlaybackButtons(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showOptionsDialog) {
|
if (showOptionsDialog) {
|
||||||
val options = listOf("Audio Track", "Playback Speed", "Video Scale")
|
val options =
|
||||||
|
listOf(
|
||||||
|
stringResource(R.string.audio),
|
||||||
|
stringResource(R.string.playback_speed),
|
||||||
|
stringResource(R.string.video_scale),
|
||||||
|
)
|
||||||
BottomDialog(
|
BottomDialog(
|
||||||
choices = options,
|
choices = options,
|
||||||
currentChoice = null,
|
currentChoice = null,
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,13 @@ import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.Chapter
|
import com.github.damontecres.wholphin.data.model.Chapter
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
|
|
@ -199,7 +201,7 @@ fun PlaybackOverlay(
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Chapters",
|
text = stringResource(R.string.chapters),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
)
|
)
|
||||||
LazyRow(
|
LazyRow(
|
||||||
|
|
@ -240,7 +242,7 @@ fun PlaybackOverlay(
|
||||||
}
|
}
|
||||||
if (playlist.hasNext()) {
|
if (playlist.hasNext()) {
|
||||||
Text(
|
Text(
|
||||||
text = "Queue",
|
text = stringResource(R.string.queue),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -282,7 +284,7 @@ fun PlaybackOverlay(
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Queue",
|
text = stringResource(R.string.queue),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
)
|
)
|
||||||
LazyRow(
|
LazyRow(
|
||||||
|
|
@ -489,7 +491,7 @@ fun Controller(
|
||||||
when (nextState) {
|
when (nextState) {
|
||||||
OverlayViewState.CHAPTERS ->
|
OverlayViewState.CHAPTERS ->
|
||||||
Text(
|
Text(
|
||||||
text = "Chapters",
|
text = stringResource(R.string.chapters),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -501,7 +503,7 @@ fun Controller(
|
||||||
|
|
||||||
OverlayViewState.QUEUE ->
|
OverlayViewState.QUEUE ->
|
||||||
Text(
|
Text(
|
||||||
text = "Queue",
|
text = stringResource(R.string.queue),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
|
@ -60,6 +61,7 @@ import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.data.model.Playlist
|
import com.github.damontecres.wholphin.data.model.Playlist
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -69,9 +71,12 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.seasonEpisode
|
import com.github.damontecres.wholphin.util.seasonEpisode
|
||||||
|
import com.github.damontecres.wholphin.util.stringRes
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -162,7 +167,6 @@ fun PlaybackPage(
|
||||||
val scaledModifier =
|
val scaledModifier =
|
||||||
Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
|
Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val controllerFocusRequester = remember { FocusRequester() }
|
|
||||||
val playPauseState = rememberPlayPauseButtonState(player)
|
val playPauseState = rememberPlayPauseButtonState(player)
|
||||||
val seekBarState = rememberSeekBarState(player, scope)
|
val seekBarState = rememberSeekBarState(player, scope)
|
||||||
|
|
||||||
|
|
@ -215,9 +219,9 @@ fun PlaybackPage(
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier
|
||||||
.background(Color.Black),
|
.background(if (nextUp == null) Color.Black else MaterialTheme.colorScheme.background),
|
||||||
) {
|
) {
|
||||||
val playerSize by animateFloatAsState(if (nextUp == null) 1f else .66f)
|
val playerSize by animateFloatAsState(if (nextUp == null) 1f else .6f)
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -410,7 +414,7 @@ fun PlaybackPage(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Skip ${segment.type.serialName}",
|
text = stringResource(R.string.skip) + " " + stringResource(segment.type.stringRes),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -418,7 +422,13 @@ fun PlaybackPage(
|
||||||
|
|
||||||
// Next up episode
|
// Next up episode
|
||||||
BackHandler(nextUp != null) {
|
BackHandler(nextUp != null) {
|
||||||
viewModel.navigationManager.goBack()
|
if (player.isPlaying) {
|
||||||
|
scope.launch(ExceptionHandler()) {
|
||||||
|
viewModel.cancelUpNextEpisode()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.navigationManager.goBack()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
nextUp != null,
|
nextUp != null,
|
||||||
|
|
@ -470,7 +480,7 @@ fun PlaybackPage(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
// .height(128.dp)
|
// .height(128.dp)
|
||||||
.fillMaxHeight(.5f)
|
.fillMaxHeight(1 - playerSize)
|
||||||
.fillMaxWidth(.66f)
|
.fillMaxWidth(.66f)
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.background(
|
.background(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.media3.common.Tracks
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
|
@ -33,6 +34,7 @@ import com.github.damontecres.wholphin.data.model.chooseStream
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
||||||
|
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
|
||||||
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
|
@ -97,12 +99,6 @@ import javax.inject.Inject
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
enum class TranscodeType {
|
|
||||||
DIRECT_PLAY,
|
|
||||||
DIRECT_STREAM,
|
|
||||||
TRANSCODE,
|
|
||||||
}
|
|
||||||
|
|
||||||
data class StreamDecision(
|
data class StreamDecision(
|
||||||
val itemId: UUID,
|
val itemId: UUID,
|
||||||
val type: PlayMethod,
|
val type: PlayMethod,
|
||||||
|
|
@ -159,6 +155,7 @@ class PlaybackViewModel
|
||||||
val trickplay = MutableLiveData<TrickplayInfo?>(null)
|
val trickplay = MutableLiveData<TrickplayInfo?>(null)
|
||||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||||
val currentSegment = EqualityMutableLiveData<MediaSegmentDto?>(null)
|
val currentSegment = EqualityMutableLiveData<MediaSegmentDto?>(null)
|
||||||
|
private val autoSkippedSegments = mutableSetOf<UUID>()
|
||||||
|
|
||||||
private lateinit var preferences: UserPreferences
|
private lateinit var preferences: UserPreferences
|
||||||
private lateinit var deviceProfile: DeviceProfile
|
private lateinit var deviceProfile: DeviceProfile
|
||||||
|
|
@ -244,6 +241,7 @@ class PlaybackViewModel
|
||||||
): Boolean =
|
): Boolean =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Timber.i("Playing ${base.id}")
|
Timber.i("Playing ${base.id}")
|
||||||
|
autoSkippedSegments.clear()
|
||||||
if (base.type !in supportItemKinds) {
|
if (base.type !in supportItemKinds) {
|
||||||
showToast(
|
showToast(
|
||||||
context,
|
context,
|
||||||
|
|
@ -358,11 +356,9 @@ class PlaybackViewModel
|
||||||
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
|
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
|
||||||
subtitleIndex = subtitleIndex ?: TrackIndex.UNSPECIFIED,
|
subtitleIndex = subtitleIndex ?: TrackIndex.UNSPECIFIED,
|
||||||
)
|
)
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
this@PlaybackViewModel.currentItemPlayback.value = itemPlaybackToUse
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
this@PlaybackViewModel.currentItemPlayback.value = itemPlaybackToUse
|
||||||
this@PlaybackViewModel.audioStreams.value = audioStreams
|
this@PlaybackViewModel.audioStreams.value = audioStreams
|
||||||
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
|
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
|
||||||
|
|
||||||
|
|
@ -664,39 +660,53 @@ class PlaybackViewModel
|
||||||
.firstOrNull {
|
.firstOrNull {
|
||||||
it.type != MediaSegmentType.UNKNOWN && currentTicks >= it.startTicks && currentTicks < it.endTicks
|
it.type != MediaSegmentType.UNKNOWN && currentTicks >= it.startTicks && currentTicks < it.endTicks
|
||||||
}
|
}
|
||||||
if (currentSegment != null) {
|
if (currentSegment != null && autoSkippedSegments.add(currentSegment.id)) {
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"Found media segment for %s: %s, %s",
|
"Found media segment for %s: %s, %s",
|
||||||
currentSegment.itemId,
|
currentSegment.itemId,
|
||||||
currentSegment.id,
|
currentSegment.id,
|
||||||
currentSegment.type,
|
currentSegment.type,
|
||||||
)
|
)
|
||||||
val behavior =
|
val playlist = this@PlaybackViewModel.playlist.value
|
||||||
when (currentSegment.type) {
|
|
||||||
MediaSegmentType.COMMERCIAL -> prefs.skipCommercials
|
if (currentSegment.type == MediaSegmentType.OUTRO &&
|
||||||
MediaSegmentType.PREVIEW -> prefs.skipPreviews
|
prefs.showNextUpWhen == ShowNextUpWhen.DURING_CREDITS &&
|
||||||
MediaSegmentType.RECAP -> prefs.skipRecaps
|
playlist != null && playlist.hasNext()
|
||||||
MediaSegmentType.OUTRO -> prefs.skipOutros
|
) {
|
||||||
MediaSegmentType.INTRO -> prefs.skipIntros
|
val nextItem = playlist.peek()
|
||||||
MediaSegmentType.UNKNOWN -> SkipSegmentBehavior.IGNORE
|
Timber.v("Setting next up during outro to ${nextItem?.id}")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
nextUp.value = nextItem
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
} else {
|
||||||
when (behavior) {
|
val behavior =
|
||||||
SkipSegmentBehavior.AUTO_SKIP -> {
|
when (currentSegment.type) {
|
||||||
this@PlaybackViewModel.currentSegment.value = null
|
MediaSegmentType.COMMERCIAL -> prefs.skipCommercials
|
||||||
player.seekTo(currentSegment.endTicks.ticks.inWholeMilliseconds + 1)
|
MediaSegmentType.PREVIEW -> prefs.skipPreviews
|
||||||
|
MediaSegmentType.RECAP -> prefs.skipRecaps
|
||||||
|
MediaSegmentType.OUTRO -> prefs.skipOutros
|
||||||
|
MediaSegmentType.INTRO -> prefs.skipIntros
|
||||||
|
MediaSegmentType.UNKNOWN -> SkipSegmentBehavior.IGNORE
|
||||||
}
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
when (behavior) {
|
||||||
|
SkipSegmentBehavior.AUTO_SKIP -> {
|
||||||
|
this@PlaybackViewModel.currentSegment.value = null
|
||||||
|
player.seekTo(currentSegment.endTicks.ticks.inWholeMilliseconds + 1)
|
||||||
|
}
|
||||||
|
|
||||||
SkipSegmentBehavior.ASK_TO_SKIP -> {
|
SkipSegmentBehavior.ASK_TO_SKIP -> {
|
||||||
this@PlaybackViewModel.currentSegment.value = currentSegment
|
this@PlaybackViewModel.currentSegment.value =
|
||||||
}
|
currentSegment
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
this@PlaybackViewModel.currentSegment.value = null
|
this@PlaybackViewModel.currentSegment.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (currentSegment == null) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@PlaybackViewModel.currentSegment.value = null
|
this@PlaybackViewModel.currentSegment.value = null
|
||||||
}
|
}
|
||||||
|
|
@ -954,7 +964,7 @@ class PlaybackViewModel
|
||||||
if (maxAttempts == 0) {
|
if (maxAttempts == 0) {
|
||||||
showToast(
|
showToast(
|
||||||
context,
|
context,
|
||||||
"Download is taking a long time, you may need to restart playback",
|
context.getString(R.string.subtitle_download_too_long),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Find the new subtitle stream
|
// Find the new subtitle stream
|
||||||
|
|
|
||||||
|
|
@ -97,9 +97,9 @@ fun PreferencesContent(
|
||||||
}
|
}
|
||||||
val screenTitle =
|
val screenTitle =
|
||||||
when (preferenceScreenOption) {
|
when (preferenceScreenOption) {
|
||||||
PreferenceScreenOption.BASIC -> "Preferences"
|
PreferenceScreenOption.BASIC -> R.string.settings
|
||||||
PreferenceScreenOption.ADVANCED -> "Advanced Preferences"
|
PreferenceScreenOption.ADVANCED -> R.string.advanced_settings
|
||||||
PreferenceScreenOption.USER_INTERFACE -> "User Interface Preferences"
|
PreferenceScreenOption.USER_INTERFACE -> R.string.ui_interface
|
||||||
}
|
}
|
||||||
|
|
||||||
var visible by remember { mutableStateOf(false) }
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
|
@ -126,7 +126,7 @@ fun PreferencesContent(
|
||||||
) {
|
) {
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
Text(
|
Text(
|
||||||
text = screenTitle,
|
text = stringResource(screenTitle),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
|
@ -42,6 +43,7 @@ import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
|
@ -221,7 +223,7 @@ fun InstallUpdatePageContent(
|
||||||
).padding(16.dp),
|
).padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Update available",
|
text = stringResource(R.string.update_available),
|
||||||
style = MaterialTheme.typography.displaySmall,
|
style = MaterialTheme.typography.displaySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
@ -234,14 +236,14 @@ fun InstallUpdatePageContent(
|
||||||
onClick = onInstallRelease,
|
onClick = onInstallRelease,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Download & Update",
|
text = stringResource(R.string.download_and_update),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
onClick = onCancel,
|
onClick = onCancel,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Cancel",
|
text = stringResource(R.string.cancel),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
|
|
@ -96,7 +97,7 @@ fun ServerList(
|
||||||
ListItem(
|
ListItem(
|
||||||
enabled = true,
|
enabled = true,
|
||||||
selected = false,
|
selected = false,
|
||||||
headlineContent = { Text(text = "Add Server") },
|
headlineContent = { Text(text = stringResource(R.string.add_server)) },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Add,
|
imageVector = Icons.Default.Add,
|
||||||
|
|
@ -116,11 +117,14 @@ fun ServerList(
|
||||||
title = server.name ?: server.url,
|
title = server.name ?: server.url,
|
||||||
dialogItems =
|
dialogItems =
|
||||||
listOf(
|
listOf(
|
||||||
DialogItem("Switch", R.string.fa_arrow_left_arrow_right) {
|
DialogItem(
|
||||||
|
stringResource(R.string.switch_servers),
|
||||||
|
R.string.fa_arrow_left_arrow_right,
|
||||||
|
) {
|
||||||
onSwitchServer.invoke(server)
|
onSwitchServer.invoke(server)
|
||||||
},
|
},
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Delete",
|
stringResource(R.string.delete),
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
Color.Red.copy(alpha = .8f),
|
Color.Red.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -31,6 +32,7 @@ import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||||
|
|
@ -78,7 +80,7 @@ fun SwitchServerContent(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Select Server",
|
text = stringResource(R.string.select_server),
|
||||||
style = MaterialTheme.typography.displaySmall,
|
style = MaterialTheme.typography.displaySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
@ -120,13 +122,13 @@ fun SwitchServerContent(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Discovered Servers",
|
text = stringResource(R.string.discovered_servers),
|
||||||
style = MaterialTheme.typography.displaySmall,
|
style = MaterialTheme.typography.displaySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
if (discoveredServers.isEmpty()) {
|
if (discoveredServers.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = "Searching...",
|
text = stringResource(R.string.searching),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
@ -184,7 +186,7 @@ fun SwitchServerContent(
|
||||||
.fillMaxWidth(.4f),
|
.fillMaxWidth(.4f),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Enter Server URL",
|
text = stringResource(R.string.enter_server_url),
|
||||||
)
|
)
|
||||||
EditTextBox(
|
EditTextBox(
|
||||||
value = url,
|
value = url,
|
||||||
|
|
@ -227,7 +229,7 @@ fun SwitchServerContent(
|
||||||
if (state == LoadingState.Loading) {
|
if (state == LoadingState.Loading) {
|
||||||
CircularProgress(Modifier.size(32.dp))
|
CircularProgress(Modifier.size(32.dp))
|
||||||
} else {
|
} else {
|
||||||
Text(text = "Submit")
|
Text(text = stringResource(R.string.submit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
|
@ -37,6 +38,7 @@ import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||||
|
|
@ -94,7 +96,7 @@ fun SwitchUserContent(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Select User",
|
text = stringResource(R.string.select_user),
|
||||||
style = MaterialTheme.typography.displaySmall,
|
style = MaterialTheme.typography.displaySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ fun UserList(
|
||||||
ListItem(
|
ListItem(
|
||||||
enabled = true,
|
enabled = true,
|
||||||
selected = false,
|
selected = false,
|
||||||
headlineContent = { Text(text = "Add User") },
|
headlineContent = { Text(text = stringResource(R.string.add_user)) },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Add,
|
imageVector = Icons.Default.Add,
|
||||||
|
|
@ -84,7 +84,7 @@ fun UserList(
|
||||||
ListItem(
|
ListItem(
|
||||||
enabled = true,
|
enabled = true,
|
||||||
selected = false,
|
selected = false,
|
||||||
headlineContent = { Text(text = "Switch servers") },
|
headlineContent = { Text(text = stringResource(R.string.switch_servers)) },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.fa_arrow_left_arrow_right),
|
text = stringResource(R.string.fa_arrow_left_arrow_right),
|
||||||
|
|
@ -102,11 +102,14 @@ fun UserList(
|
||||||
title = user.name ?: user.id.toString(),
|
title = user.name ?: user.id.toString(),
|
||||||
dialogItems =
|
dialogItems =
|
||||||
listOf(
|
listOf(
|
||||||
DialogItem("Switch", R.string.fa_arrow_left_arrow_right) {
|
DialogItem(
|
||||||
|
stringResource(R.string.switch_user),
|
||||||
|
R.string.fa_arrow_left_arrow_right,
|
||||||
|
) {
|
||||||
onSwitchUser.invoke(user)
|
onSwitchUser.invoke(user)
|
||||||
},
|
},
|
||||||
DialogItem(
|
DialogItem(
|
||||||
"Delete",
|
stringResource(R.string.delete),
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
Color.Red.copy(alpha = .8f),
|
Color.Red.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,8 @@
|
||||||
package com.github.damontecres.wholphin.util
|
package com.github.damontecres.wholphin.util
|
||||||
|
|
||||||
import com.github.damontecres.wholphin.ui.main.HomeSection
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
|
|
||||||
val supportedHomeSection =
|
|
||||||
setOf(
|
|
||||||
HomeSection.LATEST_MEDIA,
|
|
||||||
HomeSection.NEXT_UP,
|
|
||||||
HomeSection.RESUME,
|
|
||||||
)
|
|
||||||
|
|
||||||
val supportItemKinds =
|
val supportItemKinds =
|
||||||
setOf(
|
setOf(
|
||||||
BaseItemKind.MOVIE,
|
BaseItemKind.MOVIE,
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class CrashReportSender : ReportSender {
|
||||||
val api =
|
val api =
|
||||||
createJellyfin {
|
createJellyfin {
|
||||||
this.context = context
|
this.context = context
|
||||||
clientInfo = AppModule.clientInfo()
|
clientInfo = AppModule.clientInfo(context)
|
||||||
deviceInfo = AppModule.deviceInfo(context)
|
deviceInfo = AppModule.deviceInfo(context)
|
||||||
apiClientFactory = okHttpFactory
|
apiClientFactory = okHttpFactory
|
||||||
socketConnectionFactory = okHttpFactory
|
socketConnectionFactory = okHttpFactory
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.util
|
package com.github.damontecres.wholphin.util
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
|
|
@ -11,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.chooseStream
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.api.MediaSegmentType
|
||||||
import org.jellyfin.sdk.model.api.MediaStream
|
import org.jellyfin.sdk.model.api.MediaStream
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
@ -164,3 +166,15 @@ fun formatBytes(
|
||||||
}
|
}
|
||||||
return String.format(Locale.getDefault(), "%.2f%s", count, suffixes[unit])
|
return String.format(Locale.getDefault(), "%.2f%s", count, suffixes[unit])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val MediaSegmentType.stringRes: Int
|
||||||
|
get() =
|
||||||
|
when (this) {
|
||||||
|
MediaSegmentType.UNKNOWN -> R.string.unknown
|
||||||
|
MediaSegmentType.COMMERCIAL -> R.string.commercial
|
||||||
|
MediaSegmentType.PREVIEW -> R.string.preview
|
||||||
|
MediaSegmentType.RECAP -> R.string.recap
|
||||||
|
MediaSegmentType.OUTRO -> R.string.outro
|
||||||
|
MediaSegmentType.INTRO -> R.string.intro
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,31 +10,77 @@ import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
import com.github.damontecres.wholphin.hilt.AuthOkHttpClient
|
import com.github.damontecres.wholphin.hilt.AuthOkHttpClient
|
||||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||||
|
import com.github.damontecres.wholphin.util.profile.Codec
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.universalAudioApi
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple service to play theme song music
|
* Simple service to play theme song music
|
||||||
*/
|
*/
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
@Singleton
|
@Singleton
|
||||||
class ThemeSongPlayer
|
class ThemeSongPlayer
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
@param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient,
|
@param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient,
|
||||||
|
private val api: ApiClient,
|
||||||
) {
|
) {
|
||||||
private var player: Player? = null
|
private val player: Player by lazy {
|
||||||
|
ExoPlayer
|
||||||
|
.Builder(context)
|
||||||
|
.setMediaSourceFactory(
|
||||||
|
DefaultMediaSourceFactory(
|
||||||
|
OkHttpDataSource.Factory(authOkHttpClient),
|
||||||
|
),
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
suspend fun playThemeFor(
|
||||||
fun play(
|
itemId: UUID,
|
||||||
volume: ThemeSongVolume,
|
volume: ThemeSongVolume,
|
||||||
|
): Boolean =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (volume == ThemeSongVolume.DISABLED || volume == ThemeSongVolume.UNRECOGNIZED) {
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
val themeSongs by api.libraryApi.getThemeSongs(itemId)
|
||||||
|
return@withContext themeSongs.items.randomOrNull()?.let { theme ->
|
||||||
|
val url =
|
||||||
|
api.universalAudioApi.getUniversalAudioStreamUrl(
|
||||||
|
theme.id,
|
||||||
|
container =
|
||||||
|
listOf(
|
||||||
|
Codec.Audio.OPUS,
|
||||||
|
Codec.Audio.MP3,
|
||||||
|
Codec.Audio.AAC,
|
||||||
|
Codec.Audio.FLAC,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Timber.v("Found theme song for $itemId")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
play(volume, url)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun play(
|
||||||
|
volumeLevel: ThemeSongVolume,
|
||||||
url: String,
|
url: String,
|
||||||
) {
|
) {
|
||||||
stop()
|
stop()
|
||||||
val volumeLevel =
|
val volumeLevel =
|
||||||
when (volume) {
|
when (volumeLevel) {
|
||||||
ThemeSongVolume.UNRECOGNIZED,
|
ThemeSongVolume.UNRECOGNIZED,
|
||||||
ThemeSongVolume.DISABLED,
|
ThemeSongVolume.DISABLED,
|
||||||
-> return
|
-> return
|
||||||
|
|
@ -45,24 +91,16 @@ class ThemeSongPlayer
|
||||||
ThemeSongVolume.HIGH -> .5f
|
ThemeSongVolume.HIGH -> .5f
|
||||||
ThemeSongVolume.HIGHEST -> 75f
|
ThemeSongVolume.HIGHEST -> 75f
|
||||||
}
|
}
|
||||||
val player =
|
player.apply {
|
||||||
ExoPlayer
|
volume = volumeLevel
|
||||||
.Builder(context)
|
setMediaItem(MediaItem.fromUri(url))
|
||||||
.setMediaSourceFactory(
|
prepare()
|
||||||
DefaultMediaSourceFactory(
|
play()
|
||||||
OkHttpDataSource.Factory(authOkHttpClient),
|
}
|
||||||
),
|
|
||||||
).build()
|
|
||||||
.apply {
|
|
||||||
this.volume = volumeLevel
|
|
||||||
playWhenReady = true
|
|
||||||
}
|
|
||||||
player.setMediaItem(MediaItem.fromUri(url))
|
|
||||||
player.prepare()
|
|
||||||
this.player = player
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
player?.release()
|
Timber.v("Stopping theme song")
|
||||||
|
player.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ syntax = "proto3";
|
||||||
option java_package = "com.github.damontecres.wholphin.preferences";
|
option java_package = "com.github.damontecres.wholphin.preferences";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
enum ShowNextUpWhen{
|
||||||
|
END_OF_PLAYBACK = 0;
|
||||||
|
DURING_CREDITS = 1;
|
||||||
|
}
|
||||||
|
|
||||||
enum SkipSegmentBehavior{
|
enum SkipSegmentBehavior{
|
||||||
IGNORE = 0;
|
IGNORE = 0;
|
||||||
AUTO_SKIP = 1;
|
AUTO_SKIP = 1;
|
||||||
|
|
@ -52,6 +57,7 @@ message PlaybackPreferences {
|
||||||
PlaybackOverrides overrides = 15;
|
PlaybackOverrides overrides = 15;
|
||||||
int64 pass_out_protection_ms = 16;
|
int64 pass_out_protection_ms = 16;
|
||||||
PrefContentScale global_content_scale = 17;
|
PrefContentScale global_content_scale = 17;
|
||||||
|
ShowNextUpWhen show_next_up_when = 18;
|
||||||
}
|
}
|
||||||
|
|
||||||
message HomePagePreferences{
|
message HomePagePreferences{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,53 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<string name="pref_key_update_last_check_threshold" translatable="false">preference.update.threshold</string>
|
||||||
|
<string name="pref_key_update_last_check" translatable="false">preference.update.lastTimestamp</string>
|
||||||
|
|
||||||
|
<string name="ac3_supported">Device supports AC3/Dolby Digital</string>
|
||||||
|
<string name="advanced_settings">Advanced Settings</string>
|
||||||
|
<string name="advanced_ui">Advanced UI</string>
|
||||||
|
<string name="app_theme">Application Theme</string>
|
||||||
|
<string name="auto_check_for_updates">Automatically check for updates</string>
|
||||||
|
<string name="auto_play_next_delay">Delay before playing next up</string>
|
||||||
|
<string name="auto_play_next">Auto play next up</string>
|
||||||
|
<string name="check_for_updates">Check for updates</string>
|
||||||
|
<string name="combine_continue_next_summary">Applies to TV Series only</string>
|
||||||
|
<string name="combine_continue_next"><![CDATA[Combine Continue Watching & Next Up]]></string>
|
||||||
|
<string name="direct_play_ass">Direct play ASS subtitles</string>
|
||||||
|
<string name="direct_play_pgs">Direct play PGS subtitles</string>
|
||||||
|
<string name="downmix_stereo">Always downmix to stereo</string>
|
||||||
|
<string name="ffmpeg_extension_pref">Use FFmpeg decoder module</string>
|
||||||
|
<string name="global_content_scale">Default content scale</string>
|
||||||
|
<string name="install_update">Install update</string>
|
||||||
|
<string name="installed_version">Installed version</string>
|
||||||
|
<string name="max_homepage_items">Max items on home page rows</string>
|
||||||
|
<string name="nav_drawer_pins_summary">Choose the default items to show, others will be hidden</string>
|
||||||
|
<string name="nav_drawer_pins">Customize Navigation Drawer Items</string>
|
||||||
|
<string name="nav_drawer_switch_on_focus_summary_off">Click to switch pages</string>
|
||||||
|
<string name="nav_drawer_switch_on_focus">Switch nav drawer pages on focus</string>
|
||||||
|
<string name="pass_out_protection">Passout Protection</string>
|
||||||
|
<string name="play_theme_music">Play theme music</string>
|
||||||
|
<string name="playback_overrides">Playback overrides</string>
|
||||||
|
<string name="remember_selected_tab">Remember selected tabs</string>
|
||||||
|
<string name="rewatch_next_up">Enable rewatching in next up</string>
|
||||||
|
<string name="seek_bar_steps">Seek bar steps</string>
|
||||||
|
<string name="send_app_logs_summary">Useful for debugging</string>
|
||||||
|
<string name="send_app_logs">Send app logs to current server</string>
|
||||||
|
<string name="send_crash_reports_summary">Will try to send to last connected server</string>
|
||||||
|
<string name="send_crash_reports">Send Crash Reports</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
|
<string name="skip_back_on_resume_preference">Skip back when resuming playback</string>
|
||||||
|
<string name="skip_back_preference">Skip back</string>
|
||||||
|
<string name="skip_commercials_behavior">Skip commercials behavior</string>
|
||||||
|
<string name="skip_forward_preference">Skip forward</string>
|
||||||
|
<string name="skip_intro_behavior">Skip intro behavior</string>
|
||||||
|
<string name="skip_outro_behavior">Skip outro behavior</string>
|
||||||
|
<string name="skip_previews_behavior">Skip previews behavior</string>
|
||||||
|
<string name="skip_recap_behavior">Skip recap behavior</string>
|
||||||
|
<string name="update_available">Update available</string>
|
||||||
|
<string name="update_url_summary">URL used to check for app updates</string>
|
||||||
|
<string name="update_url">Update URL</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
<item>Lowest</item>
|
<item>Lowest</item>
|
||||||
|
|
@ -37,4 +85,9 @@
|
||||||
<item>Never use FFmpeg decoders</item>
|
<item>Never use FFmpeg decoders</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="show_next_up_when_options">
|
||||||
|
<item>At the end of playback</item>
|
||||||
|
<item>During end credits/outro</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,104 +1,157 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Wholphin</string>
|
<string name="app_name" translatable="false">Wholphin</string>
|
||||||
|
<string name="app_name_long" translatable="false">Wholphin</string>
|
||||||
|
|
||||||
<string name="home_section_latest_media">Recently added media</string>
|
<string name="about">About</string>
|
||||||
<string name="home_section_library">My media</string>
|
<string name="active_recordings">Active Recordings</string>
|
||||||
<string name="home_section_library_small">My media (small)</string>
|
<string name="add_favorite">Favorite</string>
|
||||||
<string name="home_section_resume">Continue watching</string>
|
<string name="add_server">Add Server</string>
|
||||||
<string name="home_section_resume_audio">Continue listening</string>
|
<string name="add_user">Add User</string>
|
||||||
<string name="home_section_resume_book">Continue reading</string>
|
<string name="audio">Audio</string>
|
||||||
<string name="home_section_active_recordings">Active recordings</string>
|
<string name="birthplace">Birthplace</string>
|
||||||
<string name="home_section_next_up">Next up</string>
|
<string name="bitrate">Bitrate</string>
|
||||||
<string name="home_section_livetv">Live TV</string>
|
<string name="born">Born</string>
|
||||||
<string name="home_section_none">None</string>
|
<string name="cancel_recording">Cancel Recording</string>
|
||||||
<string name="search">Search</string>
|
<string name="cancel_series_recording">Cancel Series Recording</string>
|
||||||
<string name="resume">Resume</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="restart">Restart</string>
|
<string name="chapters">Chapters</string>
|
||||||
<string name="play">Play</string>
|
<string name="choose_stream">Choose %1$s</string>
|
||||||
<string name="more">More</string>
|
<string name="clear_image_cache">Clear image cache</string>
|
||||||
|
<string name="collection">Collection</string>
|
||||||
|
<string name="collections">Collections</string>
|
||||||
|
<string name="commercial">Commercial</string>
|
||||||
|
<string name="community_rating">Community Rating</string>
|
||||||
|
<string name="compose_error_message_title">An error occurred! Press the button to send logs to your server.</string>
|
||||||
|
<string name="confirm">Confirm</string>
|
||||||
|
<string name="continue_watching">Continue watching</string>
|
||||||
|
<string name="critic_rating">Critic Rating</string>
|
||||||
|
<string name="decimal_seconds">%.1f seconds</string>
|
||||||
|
<string name="default_track">Default</string>
|
||||||
|
<string name="delete">Delete</string>
|
||||||
|
<string name="died">Died</string>
|
||||||
|
<string name="directed_by">Directed by %1$s</string>
|
||||||
|
<string name="director">Director</string>
|
||||||
|
<string name="disabled">Disabled</string>
|
||||||
|
<string name="discovered_servers">Discovered Servers</string>
|
||||||
|
<string name="download_and_update"><![CDATA[Download & Update]]></string>
|
||||||
|
<string name="downloading">Downloading…</string>
|
||||||
|
<string name="enabled">Enabled</string>
|
||||||
|
<string name="enter_server_url">Enter Server IP or URL including port</string>
|
||||||
|
<string name="episodes">Episodes</string>
|
||||||
|
<string name="error_loading_collection">Error loading collection %1$s</string>
|
||||||
|
<string name="external_track">External</string>
|
||||||
|
<string name="favorites">Favorites</string>
|
||||||
|
<string name="file_size">Size</string>
|
||||||
|
<string name="forced_track">Forced</string>
|
||||||
|
<string name="genres">Genres</string>
|
||||||
|
<string name="go_to_series">Go to series</string>
|
||||||
|
<string name="go_to">Go To</string>
|
||||||
|
<string name="hide_controller_timeout">Hide playback controls</string>
|
||||||
|
<string name="hide_debug_info">Hide debug info</string>
|
||||||
|
<string name="hide">Hide</string>
|
||||||
|
<string name="home">Home</string>
|
||||||
|
<string name="immediate">Immediate</string>
|
||||||
|
<string name="intro">Intro</string>
|
||||||
|
<string name="jump_letters">#ABCDEFGHIJKLMNOPQRSTUVWXYZ</string>
|
||||||
|
<string name="library">Library</string>
|
||||||
|
<string name="license_info">License information</string>
|
||||||
|
<string name="live_tv">Live TV</string>
|
||||||
|
<string name="loading">Loading…</string>
|
||||||
|
<string name="mark_entire_series_as_played">Mark entire series as played?</string>
|
||||||
|
<string name="mark_entire_series_as_unplayed">Mark entire series as unplayed?</string>
|
||||||
<string name="mark_unwatched">Mark as unwatched</string>
|
<string name="mark_unwatched">Mark as unwatched</string>
|
||||||
<string name="mark_watched">Mark as watched</string>
|
<string name="mark_watched">Mark as watched</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="max_bitrate">Max bitrate</string>
|
||||||
<string name="save">Save</string>
|
<string name="more_like_this">More like this</string>
|
||||||
<string name="app_name_long">Wholphin</string>
|
<string name="more">More</string>
|
||||||
<string name="ui_interface">Interface</string>
|
<string name="movies">Movies</string>
|
||||||
<string name="playback">Playback</string>
|
<string name="name">Name</string>
|
||||||
<string name="about">About</string>
|
<string name="next_up">Next Up</string>
|
||||||
<string name="advanced_settings">Advanced Settings</string>
|
<string name="no_data">No data</string>
|
||||||
<string name="advanced_ui">Advanced UI</string>
|
<string name="no_results">No results</string>
|
||||||
<string name="install_update">Install update</string>
|
<string name="no_scheduled_recordings">No scheduled recordings</string>
|
||||||
<string name="installed_version">Installed version</string>
|
<string name="no_update_available">No update available</string>
|
||||||
<string name="show">Show</string>
|
<string name="none">None</string>
|
||||||
<string name="hide">Hide</string>
|
<string name="outro">Outro</string>
|
||||||
<string name="enabled">Enabled</string>
|
<string name="path">Path</string>
|
||||||
<string name="disabled">Disabled</string>
|
<string name="people">People</string>
|
||||||
<string name="skip_forward_preference">Skip forward</string>
|
<string name="play_count">Play Count</string>
|
||||||
<string name="skip_back_preference">Skip back</string>
|
<string name="play_from_here">Play from here</string>
|
||||||
<string name="hide_controller_timeout">Hide playback controls</string>
|
<string name="play">Play</string>
|
||||||
<string name="seek_bar_steps">Seek bar steps</string>
|
|
||||||
<string name="max_homepage_items">Max items on home page rows</string>
|
|
||||||
<string name="rewatch_next_up">Enable rewatching in next up</string>
|
|
||||||
<string name="studios">Studios</string>
|
|
||||||
<string name="video">Video</string>
|
|
||||||
<string name="audio">Audio</string>
|
|
||||||
<string name="genres">Genres</string>
|
|
||||||
<string name="play_theme_music">Play theme music</string>
|
|
||||||
<string name="license_info">License information</string>
|
|
||||||
<string name="playback_debug_info">Show playback debug info</string>
|
<string name="playback_debug_info">Show playback debug info</string>
|
||||||
<string name="auto_play_next_delay">Delay before playing next up</string>
|
<string name="playback_speed">Playback Speed</string>
|
||||||
<string name="auto_play_next">Auto play next up</string>
|
<string name="playback">Playback</string>
|
||||||
|
<string name="playlist">Playlist</string>
|
||||||
|
<string name="preview">Preview</string>
|
||||||
<string name="sort_by_name">Name</string>
|
<string name="profile_specific_settings">User Profile Settings</string>
|
||||||
<string name="sort_by_random">Random</string>
|
<string name="queue">Queue</string>
|
||||||
<string name="sort_by_date_episode_added">Date Episode Added</string>
|
<string name="recap">Recap</string>
|
||||||
|
<string name="recently_added_in">Recently added in %1$s</string>
|
||||||
|
<string name="recently_added">Recently added</string>
|
||||||
|
<string name="recently_recorded">Recently Recorded</string>
|
||||||
|
<string name="recently_released">Recently Released</string>
|
||||||
|
<string name="recommended">Recommended</string>
|
||||||
|
<string name="record_program">Record Program</string>
|
||||||
|
<string name="record_series">Record Series</string>
|
||||||
|
<string name="remove_favorite">Unfavorite</string>
|
||||||
|
<string name="restart">Restart</string>
|
||||||
|
<string name="resume">Resume</string>
|
||||||
|
<string name="save">Save</string>
|
||||||
|
<string name="search_and_download"><![CDATA[Search & Download]]></string>
|
||||||
|
<string name="search">Search</string>
|
||||||
|
<string name="searching">Searching…</string>
|
||||||
|
<string name="select_server">Select Server</string>
|
||||||
|
<string name="select_user">Select User</string>
|
||||||
|
<string name="show_debug_info">Show debug info</string>
|
||||||
|
<string name="show_next_up_when">Show next up</string>
|
||||||
|
<string name="show">Show</string>
|
||||||
|
<string name="shuffle">Shuffle</string>
|
||||||
|
<string name="skip">Skip</string>
|
||||||
<string name="sort_by_date_added">Date Added</string>
|
<string name="sort_by_date_added">Date Added</string>
|
||||||
|
<string name="sort_by_date_episode_added">Date Episode Added</string>
|
||||||
<string name="sort_by_date_played">Date Played</string>
|
<string name="sort_by_date_played">Date Played</string>
|
||||||
<string name="sort_by_date_released">Date Released</string>
|
<string name="sort_by_date_released">Date Released</string>
|
||||||
<string name="skip_back_on_resume_preference">Skip back when resuming playback</string>
|
<string name="sort_by_name">Name</string>
|
||||||
<string name="pref_key_update_last_check_threshold">preference.update.threshold</string>
|
<string name="sort_by_random">Random</string>
|
||||||
<string name="pref_key_update_last_check">preference.update.lastTimestamp</string>
|
<string name="studios">Studios</string>
|
||||||
<string name="confirm">Confirm</string>
|
<string name="submit">Submit</string>
|
||||||
<string name="remember_selected_tab">Remember selected tabs</string>
|
<string name="subtitle_download_too_long">Download is taking a long time, you may need to restart playback</string>
|
||||||
<string name="director">Director</string>
|
<string name="subtitle">Subtitle</string>
|
||||||
<string name="max_bitrate">Max bitrate</string>
|
<string name="subtitles">Subtitles</string>
|
||||||
<string name="app_theme">Application Theme</string>
|
<string name="suggestions">Suggestions</string>
|
||||||
<string name="skip_recap_behavior">Skip recap behavior</string>
|
<string name="switch_servers">Switch servers</string>
|
||||||
<string name="skip_previews_behavior">Skip previews behavior</string>
|
<string name="switch_user">Switch</string>
|
||||||
<string name="skip_comercials_behavior">Skip commercials behavior</string>
|
<string name="top_unwatched">Top Rated Unwatched</string>
|
||||||
<string name="skip_outro_behavior">Skip outro behavior</string>
|
<string name="trailer">Trailer</string>
|
||||||
<string name="skip_intro_behavior">Skip intro behavior</string>
|
|
||||||
<string name="check_for_updates">Check for updates</string>
|
|
||||||
<string name="auto_check_for_updates">Automatically check for updates</string>
|
|
||||||
<string name="update_url">Update URL</string>
|
|
||||||
<string name="update_url_summary">URL used to check for app updates</string>
|
|
||||||
<string name="updates">Updates</string>
|
|
||||||
<string name="no_update_available">No update available</string>
|
|
||||||
<string name="clear_image_cache">Clear image cache</string>
|
|
||||||
<string name="shuffle">Shuffle</string>
|
|
||||||
<string name="combine_continue_next"><![CDATA[Combine Continue Watching & Next Up]]></string>
|
|
||||||
<string name="ac3_supported">Device supports AC3/Dolby Digital</string>
|
|
||||||
<string name="downmix_stereo">Always downmix to stereo</string>
|
|
||||||
<string name="playback_overrides">Playback overrides</string>
|
|
||||||
<string name="direct_play_ass">Direct play ASS subtitles</string>
|
|
||||||
<string name="direct_play_pgs">Direct play PGS subtitles</string>
|
|
||||||
<string name="trailers">Trailers</string>
|
<string name="trailers">Trailers</string>
|
||||||
<string name="pass_out_protection">Passout Protection</string>
|
<string name="tv_dvr_schedule">DVR Schedule</string>
|
||||||
<string name="more_like_this">More like this</string>
|
<string name="tv_guide">Guide</string>
|
||||||
<string name="remove_favorite">Unfavorite</string>
|
<string name="tv_season">Season</string>
|
||||||
<string name="add_favorite">Favorite</string>
|
<string name="tv_seasons">Seasons</string>
|
||||||
<string name="favorites">Favorites</string>
|
<string name="tv_shows">TV Shows</string>
|
||||||
<string name="nav_drawer_pins">Customize Navigation Drawer Items</string>
|
<string name="ui_interface">Interface</string>
|
||||||
<string name="profile_specific_settings">User Profile Settings</string>
|
<string name="unknown">Unknown</string>
|
||||||
<string name="nav_drawer_pins_summary">Choose the default items to show, others will be hidden</string>
|
<string name="updates">Updates</string>
|
||||||
<string name="combine_continue_next_summary">Applies to TV Series only</string>
|
<string name="version">Version</string>
|
||||||
<string name="send_crash_reports">Send Crash Reports</string>
|
<string name="video_scale">Video Scale</string>
|
||||||
<string name="send_crash_reports_summary">Will try to send to last connected server</string>
|
<string name="video">Video</string>
|
||||||
<string name="send_app_logs">Send app logs to current server</string>
|
<string name="watch_live">Watch live</string>
|
||||||
<string name="send_app_logs_summary">Useful for debugging</string>
|
<string name="years_old">%1$d years old</string>
|
||||||
<string name="global_content_scale">Default content scale</string>
|
|
||||||
<string name="ffmpeg_extension_pref">Use FFmpeg decoder module</string>
|
<plurals name="downloads">
|
||||||
<string name="nav_drawer_switch_on_focus">Switch nav drawer pages on focus</string>
|
<item quantity="zero">%d downloads</item>
|
||||||
<string name="nav_drawer_switch_on_focus_summary_off">Click to switch pages</string>
|
<item quantity="one">%d download</item>
|
||||||
|
<item quantity="other">%d downloads</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="hours">
|
||||||
|
<item quantity="zero">%d hours</item>
|
||||||
|
<item quantity="one">%d hour</item>
|
||||||
|
<item quantity="other">%d hours</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="seconds">
|
||||||
|
<item quantity="zero">%d seconds</item>
|
||||||
|
<item quantity="one">%d second</item>
|
||||||
|
<item quantity="other">%d seconds</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue