Skip to content

Commit

Permalink
Merge remote-tracking branch 'refs/remotes/origin/addToPlaylists'
Browse files Browse the repository at this point in the history
  • Loading branch information
JPKribs committed Mar 3, 2025
2 parents d1cf7d5 + 41f7be0 commit d3fc1e4
Show file tree
Hide file tree
Showing 16 changed files with 198 additions and 69 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 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
Expand Up @@ -29,17 +29,6 @@ extension EpisodeItemView {
ItemView.CastAndCrewHStack(people: castAndCrew)
}

if let seriesItem = viewModel.seriesItem {
PosterHStack(
title: L10n.series,
type: .portrait,
items: [seriesItem]
)
.onSelect { item in
router.route(to: \.item, item)
}
}

ItemView.AboutView(viewModel: viewModel)
}
.background {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ extension SelectUserView {
),
pipeline: .Swiftfin.local
)
.aspectRatio(1, contentMode: .fill)
.clipShape(.circle)
.overlay {
if isEditing {
ZStack(alignment: .bottom) {
Expand Down
12 changes: 12 additions & 0 deletions Swiftfin.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
4E556AB02D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; };
4E556AB12D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; };
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; };
4E5EE5512D67CE9500982290 /* ImageCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5EE5502D67CE9000982290 /* ImageCard.swift */; };
4E5EE5532D67CFAB00982290 /* ImageCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5EE5522D67CFAB00982290 /* ImageCard.swift */; };
4E63B9FA2C8A5BEF00C25378 /* AdminDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */; };
4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; };
4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E656C2F2D0798A900F993F3 /* ParentalRating.swift */; };
Expand Down Expand Up @@ -232,6 +234,7 @@
4ECF5D8B2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */; };
4ED25CA12D07E3590010333C /* EditAccessScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ED25CA02D07E3520010333C /* EditAccessScheduleView.swift */; };
4ED25CA42D07E4990010333C /* EditAccessScheduleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ED25CA22D07E4990010333C /* EditAccessScheduleRow.swift */; };
4EDDB49C2D596E1200DA16E8 /* VersionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDDB49B2D596E0700DA16E8 /* VersionMenu.swift */; };
4EE07CBB2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; };
4EE07CBC2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; };
4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */; };
Expand Down Expand Up @@ -1350,6 +1353,8 @@
4E5508722D13AFE3002A5345 /* UserProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImage.swift; sourceTree = "<group>"; };
4E556AAF2D036F5E00733377 /* UserPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissions.swift; sourceTree = "<group>"; };
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
4E5EE5502D67CE9000982290 /* ImageCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCard.swift; sourceTree = "<group>"; };
4E5EE5522D67CFAB00982290 /* ImageCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCard.swift; sourceTree = "<group>"; };
4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = "<group>"; };
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = "<group>"; };
4E656C2F2D0798A900F993F3 /* ParentalRating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentalRating.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1453,6 +1458,7 @@
4ED25CA02D07E3520010333C /* EditAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessScheduleView.swift; sourceTree = "<group>"; };
4ED25CA22D07E4990010333C /* EditAccessScheduleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessScheduleRow.swift; sourceTree = "<group>"; };
4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = "<group>"; };
4EDDB49B2D596E0700DA16E8 /* VersionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionMenu.swift; sourceTree = "<group>"; };
4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.swift; sourceTree = "<group>"; };
4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyItemView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2510,6 +2516,7 @@
E1C926032887565C002A7A66 /* ActionButtonHStack.swift */,
4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */,
4E97D1842D064B43004B89AD /* RefreshMetadataButton.swift */,
4EDDB49B2D596E0700DA16E8 /* VersionMenu.swift */,
);
path = ActionButtons;
sourceTree = "<group>";
Expand Down Expand Up @@ -4808,6 +4815,7 @@
isa = PBXGroup;
children = (
E12376AD2A33D680001F5B44 /* AboutView+Card.swift */,
4E5EE5502D67CE9000982290 /* ImageCard.swift */,
E1E750662A33E9B400B2C1EE /* MediaSourcesCard.swift */,
E1E750652A33E9B400B2C1EE /* OverviewCard.swift */,
E1E750672A33E9B400B2C1EE /* RatingsCard.swift */,
Expand Down Expand Up @@ -5279,6 +5287,7 @@
isa = PBXGroup;
children = (
E12376AF2A33D6AE001F5B44 /* AboutViewCard.swift */,
4E5EE5522D67CFAB00982290 /* ImageCard.swift */,
E1DABAFB2A270EE7008AC34A /* MediaSourcesCard.swift */,
E1DABAF92A270E62008AC34A /* OverviewCard.swift */,
E1DABAFD2A27B982008AC34A /* RatingsCard.swift */,
Expand Down Expand Up @@ -5912,6 +5921,7 @@
E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */,
4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */,
4E5EE5532D67CFAB00982290 /* ImageCard.swift in Sources */,
4E661A2B2CEFE6F400025C99 /* Video3DFormat.swift in Sources */,
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */,
Expand Down Expand Up @@ -6013,6 +6023,7 @@
E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */,
E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */,
E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */,
4EDDB49C2D596E1200DA16E8 /* VersionMenu.swift in Sources */,
E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */,
BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */,
4E0A8FFC2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */,
Expand Down Expand Up @@ -6934,6 +6945,7 @@
4E35CE662CBED8B600DBD886 /* ServerTicks.swift in Sources */,
E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */,
4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */,
4E5EE5512D67CE9500982290 /* ImageCard.swift in Sources */,
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,
4E1A39342D56C84200BAC1C7 /* ItemViewAttributes.swift in Sources */,
4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */,
Expand Down
19 changes: 2 additions & 17 deletions Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,6 @@ extension ItemView {
return CGSize(width: width, height: height)
}

@ViewBuilder
private var imageView: some View {
ZStack {
Color.clear

ImageView(
viewModel.item.type == .episode ? viewModel.item.seriesImageSource(.primary, maxWidth: 300) : viewModel
.item.imageSource(.primary, maxWidth: 300)
)
.accessibilityIgnoresInvertColors()
}
.posterStyle(.portrait)
.posterShadow()
.frame(width: UIDevice.isPad ? padImageWidth : phoneImageWidth)
}

var body: some View {
VStack(alignment: .leading) {
L10n.about.text
Expand All @@ -136,7 +120,8 @@ extension ItemView {
) { item in
switch item {
case .image:
imageView
ImageCard(viewModel: viewModel)
.frame(width: UIDevice.isPad ? padImageWidth : phoneImageWidth)
case .overview:
OverviewCard(item: viewModel.item)
.frame(width: cardSize.width, height: cardSize.height)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// 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)
}

// 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
}
}
}
}
Loading

0 comments on commit d3fc1e4

Please sign in to comment.