fix(android,ios): the player names the episode, not its file

The header under the episode's numbering showed `Episode::detail`, which
falls back to the file name — `[Judas] Chainsaw Man - S01E01.mkv` on a
phone, where the desktop shows `Dog & Chainsaw`. It now shows the
provider's name for the episode, else the one the file name gave it, and
nothing rather than the file name: the release group's tag is no caption,
and it is what a screenshot of the player would carry away.
This commit is contained in:
nvrl committed 2026-10-29 15:44:55 +01:00
1 parent 79f0d74e1a
commit b5ba59b41d
6 files changed
+60 -4

No files matched your search

@@ -97,6 +97,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import io.narl.protonstream.settings.SettingsStore
import io.narl.protonstream.native.NativeRuntime
import io.narl.protonstream.ui.shownName
import io.narl.protonstream.ui.theme.AccentButton
import io.narl.protonstream.ui.theme.AccentTrack
import io.narl.protonstream.ui.theme.EdgedButton
@@ -567,8 +568,11 @@ fun PlayerScreen(
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleMedium,
)
// The episode's name, never its file name: a release
// group's tag is no caption, and it is what a screenshot
// of the player would carry away.
Text(
listOfNotNull(title.displayName, episode.detail.takeIf(String::isNotBlank))
listOfNotNull(title.displayName, episode.shownName())
.joinToString(" • "),
color = Color.White.copy(alpha = 0.7f),
maxLines = 1,
@@ -221,6 +221,13 @@ internal fun rememberQueueReason(key: Any?): String? {
}
}
/**
* The episode's own name: the provider's, else the one the filename gave it.
* `null` rather than the filename, which is a release group's tag and the
* episode's numbering again.
*/
internal fun EpisodeRecord.shownName(): String? = providerName ?: detail.takeIf { it != name && it.isNotBlank() }
/**
* What an episode row is headed with: "1. Mother and Children".
*
@@ -229,7 +236,7 @@ internal fun rememberQueueReason(key: Any?): String? {
* of `[Group] Show - S01E01.mkv` is forty rows that differ in two characters.
*/
internal fun EpisodeRecord.heading(position: Int): String {
val named = providerName ?: detail.takeIf { it != name }
val named = shownName()
val number = number?.toInt()
return when {
named != null && number != null -> "$number. $named"
@@ -0,0 +1,24 @@
package io.narl.protonstream.ui
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import uniffi.pstr_android.EpisodeRecord
class EpisodeNameTest {
private fun episode(name: String, detail: String, providerName: String? = null) = EpisodeRecord(
shareId = "s", volumeId = "v", linkId = "l", name = name, label = "S01E01", detail = detail,
season = 1u, number = 1u, size = null, progress = null, resumeAt = null,
watched = false, offline = false, providerName = providerName, providerOverview = null, stillUrl = null,
airDate = null, lastPlayed = 0,
)
@Test
fun `the player captions an episode with its name and never its filename`() {
val file = "[Group] Show - S01E01.mkv"
assertEquals("Dog & Chainsaw", episode(file, file, providerName = "Dog & Chainsaw").shownName())
assertEquals("Winter Is Coming", episode(file, "Winter Is Coming").shownName())
assertNull(episode(file, file).shownName())
assertNull(episode(file, "").shownName())
}
}
+3 -1
View File
@@ -210,7 +210,9 @@ struct PlayerView: View {
// thing a viewer who has just woken the controls is checking.
VStack(alignment: .leading, spacing: 0) {
Text(host.episode?.label ?? "").textStyle(.titleMedium).foregroundStyle(.white).lineLimit(1)
Text([host.title?.displayName, host.episode?.detail].compactMap { $0 }.filter { !$0.isEmpty }.joined(separator: " • "))
// The episode's name, never its file name: a release group's
// tag is no caption, and it is what a screenshot would carry away.
Text([host.title?.displayName, host.episode?.shownName].compactMap { $0 }.filter { !$0.isEmpty }.joined(separator: " • "))
.textStyle(.bodySmall).foregroundStyle(.white.opacity(0.7)).lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
+8 -1
View File
@@ -342,12 +342,19 @@ func downloadStatusLine(_ download: RetainedDownload, waitingFor: String? = nil)
}
extension EpisodeRecord {
/// The episode's own name: the provider's, else the one the filename gave
/// it. `nil` rather than the filename, which is a release group's tag and
/// the episode's numbering again.
var shownName: String? {
providerName ?? (detail != name && !detail.isEmpty ? detail : nil)
}
/// What an episode row is headed with: "1. Mother and Children".
///
/// The provider's name for the episode, else the name the filename gave it,
/// else "Episode 1". The bare filename only when none of those exist.
func heading(_ position: Int) -> String {
let named = providerName ?? (detail != name ? detail : nil)
let named = shownName
switch (named, number) {
case let (named?, number?): return "\(number). \(named)"
case let (named?, nil): return named
+12
View File
@@ -171,6 +171,18 @@ final class CommonTests: XCTestCase {
XCTAssertEqual(bare.heading(0), "Episode 4")
}
func testThePlayerCaptionsAnEpisodeWithItsNameAndNeverItsFilename() {
var named = episode("e")
named.providerName = "Dog & Chainsaw"
XCTAssertEqual(named.shownName, "Dog & Chainsaw")
var bare = episode("e")
bare.name = "[Group] Show - S01E01.mkv"
bare.detail = bare.name
XCTAssertNil(bare.shownName)
bare.detail = ""
XCTAssertNil(bare.shownName)
}
func testEpisodesAreNumberedBySeasonThenByPlaceInTheList() {
var numbered = episode("e", number: 38)
numbered.season = 3