diff --git a/android/app/src/main/java/io/narl/protonstream/playback/PlayerScreen.kt b/android/app/src/main/java/io/narl/protonstream/playback/PlayerScreen.kt index 9565f82..80be7df 100644 --- a/android/app/src/main/java/io/narl/protonstream/playback/PlayerScreen.kt +++ b/android/app/src/main/java/io/narl/protonstream/playback/PlayerScreen.kt @@ -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, diff --git a/android/app/src/main/java/io/narl/protonstream/ui/Common.kt b/android/app/src/main/java/io/narl/protonstream/ui/Common.kt index 42e51d5..2dc337b 100644 --- a/android/app/src/main/java/io/narl/protonstream/ui/Common.kt +++ b/android/app/src/main/java/io/narl/protonstream/ui/Common.kt @@ -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" diff --git a/android/app/src/test/java/io/narl/protonstream/ui/EpisodeNameTest.kt b/android/app/src/test/java/io/narl/protonstream/ui/EpisodeNameTest.kt new file mode 100644 index 0000000..175d0cd --- /dev/null +++ b/android/app/src/test/java/io/narl/protonstream/ui/EpisodeNameTest.kt @@ -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()) + } +} diff --git a/ios/ProtonStream/Player/PlayerView.swift b/ios/ProtonStream/Player/PlayerView.swift index 7db0b69..b33353b 100644 --- a/ios/ProtonStream/Player/PlayerView.swift +++ b/ios/ProtonStream/Player/PlayerView.swift @@ -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) diff --git a/ios/ProtonStream/UI/Common.swift b/ios/ProtonStream/UI/Common.swift index 7c54e4c..5e6fbf5 100644 --- a/ios/ProtonStream/UI/Common.swift +++ b/ios/ProtonStream/UI/Common.swift @@ -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 diff --git a/ios/ProtonStreamTests/UiLogicTests.swift b/ios/ProtonStreamTests/UiLogicTests.swift index 9d901b8..25297ea 100644 --- a/ios/ProtonStreamTests/UiLogicTests.swift +++ b/ios/ProtonStreamTests/UiLogicTests.swift @@ -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