import SwiftUI import FlixyDesign import MediaKit /// Die Detailseite von etwas, das sich direkt abspielen lässt — Film oder /// einzelne Folge. struct PlayableDetailView: View { let item: MediaItem @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.versions, 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.cast.isEmpty { CastRow(cast: item.cast) } if let media = currentMedia, let part = media.files.first { TechnicalDetails( media: media, part: part, isExpanded: $showsTechnicalDetails ) .padding(.horizontal, FlixySpacing.screenMargin) } } .padding(.bottom, FlixySpacing.section) } .scrollIndicators(.hidden) } private var currentMedia: MediaVersion? { guard item.versions.indices.contains(selectedVersion) else { return item.versions.first } return item.versions[selectedVersion] } private var header: some View { HeroBanner( title: item.rowTitle, metadata: metadataLine, summary: nil, artworkURL: session.backend?.artworkURL( path: item.art ?? item.seriesArt ?? item.thumb, width: 900, height: 1200 ), logoURL: session.backend?.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?.files.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.seriesTitle { 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: [MediaVersion] @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: [String] var body: some View { ScrollView(.horizontal) { HStack(spacing: FlixySpacing.snug) { ForEach(tags, id: \.self) { tag in Text(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 cast: [MediaPerson] @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(cast.prefix(20)) { role in VStack(spacing: FlixySpacing.snug) { FlixyArtwork( url: session.backend?.artworkURL(path: role.thumb, width: 160, height: 160), aspectRatio: 1, fallbackSymbol: "person" ) .frame(width: 72, height: 72) .clipShape(.circle) Text(role.name) .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) } } }