Skip to content

Commit

Permalink
Merge branch 'main' into tvOSLetterPicker
Browse files Browse the repository at this point in the history
  • Loading branch information
JPKribs authored Mar 4, 2025
2 parents 94684eb + 718ea0f commit 361b601
Show file tree
Hide file tree
Showing 23 changed files with 376 additions and 103 deletions.
6 changes: 6 additions & 0 deletions Shared/ViewModels/ItemViewModel/ItemViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class ItemViewModel: ViewModel, Stateful {
case replace(BaseItemDto)
case toggleIsFavorite
case toggleIsPlayed
case selectMediaSource(MediaSourceInfo)
}

// MARK: BackgroundState
Expand Down Expand Up @@ -272,6 +273,11 @@ class ItemViewModel: ViewModel, Stateful {
}
.asAnyCancellable()

return state
case let .selectMediaSource(newSource):

selectedMediaSource = newSource

return state
}
}
Expand Down
1 change: 1 addition & 0 deletions Swiftfin tvOS/Components/PosterButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ struct PosterButton<Item: Poster>: View {
)
}
}
.accessibilityIgnoresInvertColors()

imageOverlay()
.eraseToAnyView()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,7 @@ extension ItemView {

ScrollView(.horizontal) {
HStack(alignment: .top, spacing: 30) {
PosterButton(item: viewModel.item, type: .portrait)
.content {
EmptyView()
}
.imageOverlay {
EmptyView()
}
.frame(height: 405)
ImageCard(viewModel: viewModel)

OverviewCard(item: viewModel.item)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//

import Defaults
import JellyfinAPI
import SwiftUI

extension ItemView.AboutView {

struct ImageCard: View {

// MARK: - Environment & Observed Objects

@EnvironmentObject
private var router: ItemCoordinator.Router

@ObservedObject
var viewModel: ItemViewModel

// MARK: - Body

var body: some View {
PosterButton(item: viewModel.item, type: .portrait)
.content { EmptyView() }
.imageOverlay { EmptyView() }
.onSelect(onSelect)
.frame(height: 405)
}

// MARK: - On Select

// Switch case to allow other funcitonality if we need to expand this beyond episode > series
private func onSelect() {
switch viewModel.item.type {
case .episode:
if let episodeViewModel = viewModel as? EpisodeItemViewModel,
let seriesItem = episodeViewModel.seriesItem
{
router.route(to: \.item, seriesItem)
}
default:
break
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ extension ItemView {
.labelStyle(.iconOnly)
}
}
.padding(0)
.focused($isFocused)
.buttonStyle(.card)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ extension ItemView {
var viewModel: ItemViewModel

@StateObject
var deleteViewModel: DeleteItemViewModel
private var deleteViewModel: DeleteItemViewModel

// MARK: - Defaults

Expand Down Expand Up @@ -73,7 +73,6 @@ extension ItemView {

// MARK: - Body

/// Shrink to minWidth 100 (button) / 50 (menu) and 16 spacing to get 3 buttons + menu
var body: some View {
HStack(alignment: .center, spacing: 24) {

Expand All @@ -88,7 +87,7 @@ extension ItemView {
}
.foregroundStyle(.purple)
.environment(\.isSelected, viewModel.item.userData?.isPlayed ?? false)
.frame(minWidth: 140, maxWidth: .infinity)
.frame(minWidth: 80, maxWidth: .infinity)

// MARK: - Toggle Favorite

Expand All @@ -101,7 +100,14 @@ extension ItemView {
}
.foregroundStyle(.pink)
.environment(\.isSelected, viewModel.item.userData?.isFavorite ?? false)
.frame(minWidth: 140, maxWidth: .infinity)
.frame(minWidth: 80, maxWidth: .infinity)

// MARK: - Select Merged Version

if let mediaSources = viewModel.playButtonItem?.mediaSources, mediaSources.count > 1 {
VersionMenu(viewModel: viewModel, mediaSources: mediaSources)
.frame(minWidth: 80, maxWidth: .infinity)
}

// MARK: - Additional Menu Options

Expand All @@ -118,7 +124,7 @@ extension ItemView {
}
}
}
.frame(width: 70)
.frame(minWidth: 30, maxWidth: 50)
}
}
.frame(height: 100)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//

import JellyfinAPI
import SwiftUI

extension ItemView {

struct VersionMenu: View {

// MARK: - Focus State

@FocusState
private var isFocused: Bool

@ObservedObject
var viewModel: ItemViewModel

let mediaSources: [MediaSourceInfo]

// MARK: - Body

var body: some View {
Menu {
ForEach(mediaSources, id: \.hashValue) { mediaSource in
Button {
viewModel.send(.selectMediaSource(mediaSource))
} label: {
if let selectedMediaSource = viewModel.selectedMediaSource, selectedMediaSource == mediaSource {
Label(selectedMediaSource.displayTitle, systemImage: "checkmark")
} else {
Text(mediaSource.displayTitle)
}
}
}
} label: {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(isFocused ? Color.white : Color.white.opacity(0.5))

Label(L10n.version, systemImage: "list.dash")
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(.black)
.labelStyle(.iconOnly)
}
}
.focused($isFocused)
.scaleEffect(isFocused ? 1.20 : 1.0)
.animation(.easeInOut(duration: 0.15), value: isFocused)
.menuStyle(.borderlessButton)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//

import SwiftUI

extension SeriesEpisodeSelector {

struct EmptyCard: View {

private var onSelect: () -> Void

init() {
self.onSelect = {}
}

func onSelect(perform action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}

var body: some View {
VStack(alignment: .leading) {
Button {
onSelect()
} label: {
Color.secondarySystemFill
.opacity(0.75)
.posterStyle(.landscape)
.overlay {
Image(systemName: "questionmark")
.font(.system(size: 40))
}
}
.buttonStyle(.card)
.posterShadow()

SeriesEpisodeSelector.EpisodeContent(
subHeader: .emptyDash,
header: L10n.noResults,
content: L10n.noEpisodesAvailable
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//

import Defaults
import Factory
import JellyfinAPI
import SwiftUI

extension SeriesEpisodeSelector {

struct EpisodeCard: View {

@EnvironmentObject
private var router: ItemCoordinator.Router

Expand All @@ -22,15 +22,24 @@ extension SeriesEpisodeSelector {
private var isFocused: Bool

@ViewBuilder
private var imageOverlay: some View {
private var overlayView: some View {
ZStack {
if episode.userData?.isPlayed ?? false {
WatchedIndicator(size: 45)
} else if (episode.userData?.playbackPositionTicks ?? 0) > 0 {
if let progressLabel = episode.progressLabel {
LandscapePosterProgressBar(
title: episode.progressLabel ?? L10n.continue,
title: progressLabel,
progress: (episode.userData?.playedPercentage ?? 0) / 100
)
} else if episode.userData?.isPlayed ?? false {
ZStack(alignment: .bottomTrailing) {
Color.clear

Image(systemName: "checkmark.circle.fill")
.resizable()
.frame(width: 30, height: 30, alignment: .bottomTrailing)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .black)
.padding()
}
}

if isFocused {
Expand Down Expand Up @@ -64,7 +73,7 @@ extension SeriesEpisodeSelector {
SystemImageContentView(systemName: episode.systemImage)
}

imageOverlay
overlayView
}
.posterStyle(.landscape)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import JellyfinAPI
import SwiftUI

extension SeriesEpisodeSelector {

struct EpisodeContent: View {

@Default(.accentColor)
private var accentColor

Expand All @@ -26,6 +28,7 @@ extension SeriesEpisodeSelector {
Text(subHeader)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(1)
}

@ViewBuilder
Expand All @@ -46,6 +49,7 @@ extension SeriesEpisodeSelector {
.multilineTextAlignment(.leading)
.backport
.lineLimit(3, reservesSpace: true)
.font(.caption.weight(.light))
}

var body: some View {
Expand Down
Loading

0 comments on commit 361b601

Please sign in to comment.