import SwiftUI import FlixyDesign import PlexKit /// Die Detailseite von etwas, das sich direkt abspielen lässt — Film oder /// einzelne Folge. struct PlayableDetailView: View { let item: PlexItem @Environment(FlixySession.self) private var session @Environment(PlaybackCoordinator.self) private var playback @Environment(\.flixyAccent) private var accent @State private var selectedVersion = 0 @State private var showsTechnicalDetails = false var body: some View { ScrollView { VStack(alignment: .leading, spacing: FlixySpacing.section) { header ParentLinks(item: item) if item.hasMultipleVersions { VersionPicker(media: item.media, selection: $selectedVersion) .padding(.horizontal, FlixySpacing.screenMargin) } if let summary = item.summary, !summary.isEmpty { Text(summary) .font(.flixyBody) .foregroundStyle(Color.flixyTextSecondary) .padding(.horizontal, FlixySpacing.screenMargin) } if !item.genres.isEmpty { TagStrip(tags: item.genres) } if !item.roles.isEmpty { CastRow(roles: item.roles) } if let media = currentMedia, let part = media.parts.first { TechnicalDetails( media: media, part: part, isExpanded: $showsTechnicalDetails ) .padding(.horizontal, FlixySpacing.screenMargin) } } .padding(.bottom, FlixySpacing.section) } .scrollIndicators(.hidden) } private var currentMedia: PlexMedia? { guard item.media.indices.contains(selectedVersion) else { return item.media.first } return item.media[selectedVersion] } private var header: some View { HeroBanner( title: item.rowTitle, metadata: metadataLine, summary: nil, artworkURL: session.server?.artworkURL( path: item.art ?? item.grandparentArt ?? item.thumb, width: 900, height: 1200 ), logoURL: session.server?.rawImageURL(path: item.clearLogoPath) ) { Button { playback.play(item, mediaIndex: selectedVersion) } label: { Label( item.watchProgress == nil ? "hero.play" : "hero.resume", systemImage: "play.fill" ) .font(.flixyCardTitle) .padding(.horizontal, FlixySpacing.loose) .padding(.vertical, FlixySpacing.snug) } .buttonStyle(.borderedProminent) .tint(accent) .disabled(currentMedia?.parts.first == nil) if let progress = item.watchProgress, progress > 0 { Button { playback.play(item, mediaIndex: selectedVersion, resuming: false) } label: { Label("detail.playFromStart", systemImage: "gobackward") .labelStyle(.iconOnly) .font(.flixyCardTitle) .padding(FlixySpacing.snug) } .buttonStyle(.bordered) .tint(Color.flixyTextSecondary) } DownloadButton(item: item, mediaIndex: selectedVersion) } } private var metadataLine: String? { var pieces: [String] = [] if item.kind == .episode { // Der Folgentitel steht jetzt groß darüber; ohne die Serie hier // wüsste man auf dieser Seite nicht, wozu die Folge gehört. if let series = item.grandparentTitle { pieces.append(series) } if let season = item.parentIndex, let episode = item.index { pieces.append(String(localized: "episode.short \(season) \(episode)")) } } if let year = item.year { pieces.append(String(year)) } if let duration = item.duration { pieces.append(Duration.milliseconds(duration).runtimeLabel) } if let rating = item.contentRating { pieces.append(rating) } if let resolution = currentMedia?.videoResolution { pieces.append(resolution.uppercased()) } return pieces.isEmpty ? nil : pieces.joined(separator: " · ") } } /// Auswahl zwischen mehreren Fassungen desselben Titels. struct VersionPicker: View { let media: [PlexMedia] @Binding var selection: Int var body: some View { VStack(alignment: .leading, spacing: FlixySpacing.snug) { Text("detail.versions") .font(.flixyRowHeader) .foregroundStyle(Color.flixyTextPrimary) Picker("detail.versions", selection: $selection) { ForEach(Array(media.enumerated()), id: \.offset) { index, version in Text(version.qualityLabel).tag(index) } } .pickerStyle(.segmented) .labelsHidden() } } } /// Genres als Streifen. struct TagStrip: View { let tags: [PlexTag] var body: some View { ScrollView(.horizontal) { HStack(spacing: FlixySpacing.snug) { ForEach(tags) { tag in Text(tag.tag) .font(.flixyCaption) .foregroundStyle(Color.flixyTextSecondary) .padding(.horizontal, FlixySpacing.regular) .padding(.vertical, FlixySpacing.tight) .background(Color.flixyElevated, in: .capsule) } } .padding(.horizontal, FlixySpacing.screenMargin) } .scrollIndicators(.hidden) } } /// Die Besetzung mit Porträts. struct CastRow: View { let roles: [PlexTag] @Environment(FlixySession.self) private var session var body: some View { VStack(alignment: .leading, spacing: FlixySpacing.regular) { Text("detail.cast") .font(.flixyRowHeader) .foregroundStyle(Color.flixyTextPrimary) .padding(.horizontal, FlixySpacing.screenMargin) ScrollView(.horizontal) { HStack(alignment: .top, spacing: FlixySpacing.regular) { ForEach(roles.prefix(20)) { role in VStack(spacing: FlixySpacing.snug) { FlixyArtwork( url: session.server?.artworkURL(path: role.thumb, width: 160, height: 160), aspectRatio: 1, fallbackSymbol: "person" ) .frame(width: 72, height: 72) .clipShape(.circle) Text(role.tag) .font(.flixyCaption) .foregroundStyle(Color.flixyTextPrimary) .lineLimit(2) .multilineTextAlignment(.center) if let character = role.role { Text(character) .font(.flixyCaption) .foregroundStyle(Color.flixyTextMuted) .lineLimit(1) } } .frame(width: 90) .accessibilityElement(children: .combine) } } .padding(.horizontal, FlixySpacing.screenMargin) } .scrollIndicators(.hidden) } } }