mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
MPV: Adjust subtitle delay/offset (#470)
**This is for MPV playback backend only!** Since this only works in MPV for now, the option is not shown if using ExoPlayer. Adds a setting (the gear icon during playback) to adjust the subtitle delay. A negative delay shows subtitles sooner. A positive delay shows them later. For example, if a subtitle is supposed to show between 1000ms-2000ms, a negative 250ms delay will show it at 750ms-1750ms instead. The delay is persisted between plays of the same media & subtitle track. Ref: #12 
This commit is contained in:
parent
3f5ce703d2
commit
ab8bbf2bd7
12 changed files with 751 additions and 9 deletions
|
|
@ -0,0 +1,434 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 12,
|
||||||
|
"identityHash": "e9bcae45448f995d1f4ebf291e45c510",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "servers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "users",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` 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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pin",
|
||||||
|
"columnName": "pin",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_users_id_serverId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_users_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_users_serverId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "servers",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ItemPlayback",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "rowId",
|
||||||
|
"columnName": "rowId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "userId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sourceId",
|
||||||
|
"columnName": "sourceId",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "audioIndex",
|
||||||
|
"columnName": "audioIndex",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subtitleIndex",
|
||||||
|
"columnName": "subtitleIndex",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_ItemPlayback_userId_itemId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "NavDrawerPinnedItem",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "userId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "LibraryDisplayInfo",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "userId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sort",
|
||||||
|
"columnName": "sort",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "direction",
|
||||||
|
"columnName": "direction",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "filter",
|
||||||
|
"columnName": "filter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "'{}'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewOptions",
|
||||||
|
"columnName": "viewOptions",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_LibraryDisplayInfo_userId_itemId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "PlaybackLanguageChoice",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "userId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesId",
|
||||||
|
"columnName": "seriesId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "audioLanguage",
|
||||||
|
"columnName": "audioLanguage",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subtitleLanguage",
|
||||||
|
"columnName": "subtitleLanguage",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subtitlesDisabled",
|
||||||
|
"columnName": "subtitlesDisabled",
|
||||||
|
"affinity": "INTEGER"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"seriesId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ItemTrackModification",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), 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": "trackIndex",
|
||||||
|
"columnName": "trackIndex",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "delayMs",
|
||||||
|
"columnName": "delayMs",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId",
|
||||||
|
"trackIndex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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, 'e9bcae45448f995d1f4ebf291e45c510')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
|
import com.github.damontecres.wholphin.data.model.ItemTrackModification
|
||||||
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.LibraryDisplayInfo
|
||||||
|
|
@ -30,8 +31,9 @@ import java.util.UUID
|
||||||
NavDrawerPinnedItem::class,
|
NavDrawerPinnedItem::class,
|
||||||
LibraryDisplayInfo::class,
|
LibraryDisplayInfo::class,
|
||||||
PlaybackLanguageChoice::class,
|
PlaybackLanguageChoice::class,
|
||||||
|
ItemTrackModification::class,
|
||||||
],
|
],
|
||||||
version = 11,
|
version = 12,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(3, 4),
|
AutoMigration(3, 4),
|
||||||
|
|
@ -42,6 +44,7 @@ import java.util.UUID
|
||||||
AutoMigration(8, 9),
|
AutoMigration(8, 9),
|
||||||
AutoMigration(9, 10),
|
AutoMigration(9, 10),
|
||||||
AutoMigration(10, 11),
|
AutoMigration(10, 11),
|
||||||
|
AutoMigration(11, 12),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
|
import com.github.damontecres.wholphin.data.model.ItemTrackModification
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
@ -26,4 +27,20 @@ interface ItemPlaybackDao {
|
||||||
|
|
||||||
@Query("SELECT * from ItemPlayback WHERE userId=:userId")
|
@Query("SELECT * from ItemPlayback WHERE userId=:userId")
|
||||||
fun getItems(userId: Int): List<ItemPlayback>
|
fun getItems(userId: Int): List<ItemPlayback>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM ItemTrackModification WHERE userId=:userId AND itemId=:itemId")
|
||||||
|
suspend fun getTrackModifications(
|
||||||
|
userId: Int,
|
||||||
|
itemId: UUID,
|
||||||
|
): List<ItemTrackModification>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM ItemTrackModification WHERE userId=:userId AND itemId=:itemId AND trackIndex=:trackIndex")
|
||||||
|
suspend fun getTrackModifications(
|
||||||
|
userId: Int,
|
||||||
|
itemId: UUID,
|
||||||
|
trackIndex: Int,
|
||||||
|
): ItemTrackModification
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun saveItem(item: ItemTrackModification): Long
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.data
|
||||||
|
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
|
import com.github.damontecres.wholphin.data.model.ItemTrackModification
|
||||||
import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice
|
import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice
|
||||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -16,6 +17,7 @@ import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ItemPlaybackRepository
|
class ItemPlaybackRepository
|
||||||
|
|
@ -179,6 +181,32 @@ class ItemPlaybackRepository
|
||||||
val id = itemPlaybackDao.saveItem(toSave)
|
val id = itemPlaybackDao.saveItem(toSave)
|
||||||
return toSave.copy(rowId = id)
|
return toSave.copy(rowId = id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getTrackModifications(
|
||||||
|
itemId: UUID,
|
||||||
|
trackIndex: Int,
|
||||||
|
): ItemTrackModification? =
|
||||||
|
serverRepository.currentUser.value?.rowId?.let { userId ->
|
||||||
|
itemPlaybackDao.getTrackModifications(userId, itemId, trackIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveTrackModifications(
|
||||||
|
itemId: UUID,
|
||||||
|
trackIndex: Int,
|
||||||
|
delay: Duration,
|
||||||
|
) {
|
||||||
|
serverRepository.currentUser.value?.rowId?.let { userId ->
|
||||||
|
Timber.v("Saving track mod item=%s, track=%s, delay=%s", itemId, trackIndex, delay)
|
||||||
|
itemPlaybackDao.saveItem(
|
||||||
|
ItemTrackModification(
|
||||||
|
userId,
|
||||||
|
itemId,
|
||||||
|
trackIndex,
|
||||||
|
delay.inWholeMilliseconds,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ChosenStreams(
|
data class ChosenStreams(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
@file:UseSerializers(UUIDSerializer::class)
|
||||||
|
|
||||||
|
package com.github.damontecres.wholphin.data.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.UseSerializers
|
||||||
|
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", "trackIndex"],
|
||||||
|
)
|
||||||
|
@Serializable
|
||||||
|
data class ItemTrackModification(
|
||||||
|
val userId: Int,
|
||||||
|
val itemId: UUID,
|
||||||
|
val trackIndex: Int,
|
||||||
|
val delayMs: Long,
|
||||||
|
)
|
||||||
|
|
@ -6,6 +6,7 @@ import com.github.damontecres.wholphin.util.TrackSupport
|
||||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
import org.jellyfin.sdk.model.api.PlayMethod
|
import org.jellyfin.sdk.model.api.PlayMethod
|
||||||
import org.jellyfin.sdk.model.api.TranscodingInfo
|
import org.jellyfin.sdk.model.api.TranscodingInfo
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
data class CurrentPlayback(
|
data class CurrentPlayback(
|
||||||
val item: BaseItem,
|
val item: BaseItem,
|
||||||
|
|
@ -18,4 +19,5 @@ data class CurrentPlayback(
|
||||||
val videoDecoder: String? = null,
|
val videoDecoder: String? = null,
|
||||||
val audioDecoder: String? = null,
|
val audioDecoder: String? = null,
|
||||||
val transcodeInfo: TranscodingInfo? = null,
|
val transcodeInfo: TranscodingInfo? = null,
|
||||||
|
val subtitleDelay: Duration = Duration.ZERO,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,26 @@
|
||||||
package com.github.damontecres.wholphin.ui.playback
|
package com.github.damontecres.wholphin.ui.playback
|
||||||
|
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||||
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
enum class PlaybackDialogType {
|
enum class PlaybackDialogType {
|
||||||
MORE,
|
MORE,
|
||||||
|
|
@ -16,6 +29,7 @@ enum class PlaybackDialogType {
|
||||||
AUDIO,
|
AUDIO,
|
||||||
PLAYBACK_SPEED,
|
PLAYBACK_SPEED,
|
||||||
VIDEO_SCALE,
|
VIDEO_SCALE,
|
||||||
|
SUBTITLE_DELAY,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PlaybackSettings(
|
data class PlaybackSettings(
|
||||||
|
|
@ -26,16 +40,19 @@ data class PlaybackSettings(
|
||||||
val subtitleStreams: List<SubtitleStream>,
|
val subtitleStreams: List<SubtitleStream>,
|
||||||
val playbackSpeed: Float,
|
val playbackSpeed: Float,
|
||||||
val contentScale: ContentScale,
|
val contentScale: ContentScale,
|
||||||
|
val subtitleDelay: Duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaybackDialog(
|
fun PlaybackDialog(
|
||||||
|
enableSubtitleDelay: Boolean,
|
||||||
type: PlaybackDialogType,
|
type: PlaybackDialogType,
|
||||||
settings: PlaybackSettings,
|
settings: PlaybackSettings,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onControllerInteraction: () -> Unit,
|
onControllerInteraction: () -> Unit,
|
||||||
onClickPlaybackDialogType: (PlaybackDialogType) -> Unit,
|
onClickPlaybackDialogType: (PlaybackDialogType) -> Unit,
|
||||||
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
||||||
|
onChangeSubtitleDelay: (Duration) -> Unit,
|
||||||
) {
|
) {
|
||||||
when (type) {
|
when (type) {
|
||||||
PlaybackDialogType.MORE -> {
|
PlaybackDialogType.MORE -> {
|
||||||
|
|
@ -97,11 +114,14 @@ fun PlaybackDialog(
|
||||||
|
|
||||||
PlaybackDialogType.SETTINGS -> {
|
PlaybackDialogType.SETTINGS -> {
|
||||||
val options =
|
val options =
|
||||||
listOf(
|
buildList {
|
||||||
stringResource(R.string.audio),
|
add(stringResource(R.string.audio))
|
||||||
stringResource(R.string.playback_speed),
|
add(stringResource(R.string.playback_speed))
|
||||||
stringResource(R.string.video_scale),
|
add(stringResource(R.string.video_scale))
|
||||||
)
|
if (enableSubtitleDelay) {
|
||||||
|
add(stringResource(R.string.subtitle_delay))
|
||||||
|
}
|
||||||
|
}
|
||||||
BottomDialog(
|
BottomDialog(
|
||||||
choices = options,
|
choices = options,
|
||||||
currentChoice = null,
|
currentChoice = null,
|
||||||
|
|
@ -111,6 +131,7 @@ fun PlaybackDialog(
|
||||||
0 -> onClickPlaybackDialogType(PlaybackDialogType.AUDIO)
|
0 -> onClickPlaybackDialogType(PlaybackDialogType.AUDIO)
|
||||||
1 -> onClickPlaybackDialogType(PlaybackDialogType.PLAYBACK_SPEED)
|
1 -> onClickPlaybackDialogType(PlaybackDialogType.PLAYBACK_SPEED)
|
||||||
2 -> onClickPlaybackDialogType(PlaybackDialogType.VIDEO_SCALE)
|
2 -> onClickPlaybackDialogType(PlaybackDialogType.VIDEO_SCALE)
|
||||||
|
3 -> onClickPlaybackDialogType(PlaybackDialogType.SUBTITLE_DELAY)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
gravity = Gravity.END,
|
gravity = Gravity.END,
|
||||||
|
|
@ -173,5 +194,31 @@ fun PlaybackDialog(
|
||||||
gravity = Gravity.END,
|
gravity = Gravity.END,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlaybackDialogType.SUBTITLE_DELAY -> {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
) {
|
||||||
|
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
|
||||||
|
dialogWindowProvider?.window?.setDimAmount(0f)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.background(
|
||||||
|
AppColors.TransparentBlack50,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
SubtitleDelay(
|
||||||
|
delay = settings.subtitleDelay,
|
||||||
|
onChangeDelay = onChangeSubtitleDelay,
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,13 @@ 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.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.Media3SubtitleOverride
|
import com.github.damontecres.wholphin.util.Media3SubtitleOverride
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
@ -163,6 +165,11 @@ fun PlaybackPage(
|
||||||
var playbackSpeed by remember { mutableFloatStateOf(1.0f) }
|
var playbackSpeed by remember { mutableFloatStateOf(1.0f) }
|
||||||
LaunchedEffect(playbackSpeed) { player.setPlaybackSpeed(playbackSpeed) }
|
LaunchedEffect(playbackSpeed) { player.setPlaybackSpeed(playbackSpeed) }
|
||||||
|
|
||||||
|
val subtitleDelay = currentPlayback?.subtitleDelay ?: Duration.ZERO
|
||||||
|
LaunchedEffect(subtitleDelay) {
|
||||||
|
(player as? MpvPlayer)?.subtitleDelay = subtitleDelay
|
||||||
|
}
|
||||||
|
|
||||||
val presentationState = rememberPresentationState(player, false)
|
val presentationState = rememberPresentationState(player, false)
|
||||||
val scaledModifier =
|
val scaledModifier =
|
||||||
Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
|
Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
|
||||||
|
|
@ -543,6 +550,7 @@ fun PlaybackPage(
|
||||||
subtitleStreams = mediaInfo?.subtitleStreams.orEmpty(),
|
subtitleStreams = mediaInfo?.subtitleStreams.orEmpty(),
|
||||||
playbackSpeed = playbackSpeed,
|
playbackSpeed = playbackSpeed,
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
|
subtitleDelay = subtitleDelay,
|
||||||
),
|
),
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
playbackDialog = null
|
playbackDialog = null
|
||||||
|
|
@ -553,8 +561,16 @@ fun PlaybackPage(
|
||||||
onControllerInteraction = {
|
onControllerInteraction = {
|
||||||
controllerViewState.pulseControls(Long.MAX_VALUE)
|
controllerViewState.pulseControls(Long.MAX_VALUE)
|
||||||
},
|
},
|
||||||
onClickPlaybackDialogType = { playbackDialog = it },
|
onClickPlaybackDialogType = {
|
||||||
|
if (it == PlaybackDialogType.SUBTITLE_DELAY) {
|
||||||
|
// Hide controls so subtitles are fully visible
|
||||||
|
controllerViewState.hideControls()
|
||||||
|
}
|
||||||
|
playbackDialog = it
|
||||||
|
},
|
||||||
onPlaybackActionClick = onPlaybackActionClick,
|
onPlaybackActionClick = onPlaybackActionClick,
|
||||||
|
onChangeSubtitleDelay = { viewModel.updateSubtitleDelay(it) },
|
||||||
|
enableSubtitleDelay = player is MpvPlayer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -515,7 +515,7 @@ class PlaybackViewModel
|
||||||
|
|
||||||
this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
|
this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
|
||||||
}
|
}
|
||||||
|
loadSubtitleDelay()
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -686,6 +686,7 @@ class PlaybackViewModel
|
||||||
if (result.bothSelected) {
|
if (result.bothSelected) {
|
||||||
player.removeListener(this)
|
player.removeListener(this)
|
||||||
}
|
}
|
||||||
|
viewModelScope.launchIO { loadSubtitleDelay() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1150,4 +1151,47 @@ class PlaybackViewModel
|
||||||
Timber.d("decoder: onAudioDisabled")
|
Timber.d("decoder: onAudioDisabled")
|
||||||
currentPlayback.update { it?.copy(audioDecoder = null) }
|
currentPlayback.update { it?.copy(audioDecoder = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var subtitleDelaySaveJob: Job? = null
|
||||||
|
|
||||||
|
fun updateSubtitleDelay(delta: Duration) {
|
||||||
|
subtitleDelaySaveJob?.cancel()
|
||||||
|
currentPlayback.update {
|
||||||
|
it?.let {
|
||||||
|
val newDelay = it.subtitleDelay + delta
|
||||||
|
val result = it.copy(subtitleDelay = it.subtitleDelay + delta)
|
||||||
|
subtitleDelaySaveJob =
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
// Debounce & save
|
||||||
|
currentItemPlayback.value?.let { item ->
|
||||||
|
delay(1500)
|
||||||
|
itemPlaybackRepository.saveTrackModifications(
|
||||||
|
item.itemId,
|
||||||
|
item.subtitleIndex,
|
||||||
|
newDelay,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadSubtitleDelay() {
|
||||||
|
currentItemPlayback.value?.let {
|
||||||
|
if (it.subtitleIndexEnabled) {
|
||||||
|
val result =
|
||||||
|
itemPlaybackRepository.getTrackModifications(it.itemId, it.subtitleIndex)
|
||||||
|
if (result != null) {
|
||||||
|
Timber.v(
|
||||||
|
"Loading subtitle delay %s for track=%s, itemId=%s",
|
||||||
|
result.delayMs,
|
||||||
|
it.subtitleIndex,
|
||||||
|
it.itemId,
|
||||||
|
)
|
||||||
|
currentPlayback.update { it?.copy(subtitleDelay = result.delayMs.milliseconds) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.playback
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
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.unit.dp
|
||||||
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||||
|
import com.github.damontecres.wholphin.ui.components.Button
|
||||||
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
private val delayIncrements = listOf(50.milliseconds, 250.milliseconds, 1.seconds)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SubtitleDelay(
|
||||||
|
delay: Duration,
|
||||||
|
onChangeDelay: (Duration) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.subtitle_delay) + ": " + delay.toString(),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
delayIncrements.reversed().forEach {
|
||||||
|
SubtitleDelayButton(
|
||||||
|
text = "-$it",
|
||||||
|
onClick = { onChangeDelay.invoke(-it) },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SubtitleDelayButton(
|
||||||
|
text = stringResource(R.string.reset),
|
||||||
|
onClick = { onChangeDelay.invoke(-delay) },
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
delayIncrements.forEach {
|
||||||
|
SubtitleDelayButton(
|
||||||
|
text = "+$it",
|
||||||
|
onClick = { onChangeDelay.invoke(it) },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SubtitleDelayButton(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier.width(64.dp),
|
||||||
|
shape =
|
||||||
|
ClickableSurfaceDefaults.shape(
|
||||||
|
shape = RoundedCornerShape(33),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewTvSpec
|
||||||
|
@Composable
|
||||||
|
private fun SubtitleDelayPreview() {
|
||||||
|
WholphinTheme {
|
||||||
|
SubtitleDelay(
|
||||||
|
delay = 1.5.seconds,
|
||||||
|
onChangeDelay = {},
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,7 @@ import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_PLAYBA
|
||||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_VIDEO_RECONFIG
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_VIDEO_RECONFIG
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -799,7 +800,7 @@ class MpvPlayer(
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
var subtitleDelay: Double
|
var subtitleDelaySeconds: Double
|
||||||
get() {
|
get() {
|
||||||
if (isReleased) return 0.0
|
if (isReleased) return 0.0
|
||||||
return MPVLib.getPropertyDouble("sub-delay") ?: 0.0
|
return MPVLib.getPropertyDouble("sub-delay") ?: 0.0
|
||||||
|
|
@ -808,6 +809,16 @@ class MpvPlayer(
|
||||||
if (isReleased) return
|
if (isReleased) return
|
||||||
MPVLib.setPropertyDouble("sub-delay", value)
|
MPVLib.setPropertyDouble("sub-delay", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var subtitleDelay: Duration
|
||||||
|
get() {
|
||||||
|
if (isReleased) return Duration.ZERO
|
||||||
|
return subtitleDelaySeconds.seconds
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (isReleased) return
|
||||||
|
subtitleDelaySeconds = value.inWholeMilliseconds / 1000.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MPVLib.setPropertyColor(
|
fun MPVLib.setPropertyColor(
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,7 @@
|
||||||
<string name="favorite_channels_at_beginning">Show favorite channels first</string>
|
<string name="favorite_channels_at_beginning">Show favorite channels first</string>
|
||||||
<string name="sort_channels_recently_watched">Sort channels by recently watched</string>
|
<string name="sort_channels_recently_watched">Sort channels by recently watched</string>
|
||||||
<string name="color_code_programs">Color-code programs</string>
|
<string name="color_code_programs">Color-code programs</string>
|
||||||
|
<string name="subtitle_delay">Subtitle delay</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue