diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index a62ae0991..ada0c58f6 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -27,6 +27,7 @@ class ItemViewModel: ViewModel, Stateful { case replace(BaseItemDto) case toggleIsFavorite case toggleIsPlayed + case selectMediaSource(MediaSourceInfo) } // MARK: BackgroundState @@ -272,6 +273,11 @@ class ItemViewModel: ViewModel, Stateful { } .asAnyCancellable() + return state + case let .selectMediaSource(newSource): + + selectedMediaSource = newSource + return state } } diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift index 0660a6f34..b03acaecd 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift @@ -56,6 +56,7 @@ extension ItemView { .labelStyle(.iconOnly) } } + .padding(0) .focused($isFocused) .buttonStyle(.card) } diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift index 26a35e408..89e0ee539 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift @@ -21,7 +21,7 @@ extension ItemView { var viewModel: ItemViewModel @StateObject - var deleteViewModel: DeleteItemViewModel + private var deleteViewModel: DeleteItemViewModel // MARK: - Defaults @@ -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) { @@ -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 @@ -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 @@ -118,7 +124,7 @@ extension ItemView { } } } - .frame(width: 70) + .frame(minWidth: 30, maxWidth: 50) } } .frame(height: 100) diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/VersionMenu.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/VersionMenu.swift new file mode 100644 index 000000000..2ca3bd580 --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/VersionMenu.swift @@ -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) + } + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 722c42089..bd0043b37 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -225,6 +225,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 */; }; @@ -1441,6 +1442,7 @@ 4ED25CA02D07E3520010333C /* EditAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessScheduleView.swift; sourceTree = ""; }; 4ED25CA22D07E4990010333C /* EditAccessScheduleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessScheduleRow.swift; sourceTree = ""; }; 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = ""; }; + 4EDDB49B2D596E0700DA16E8 /* VersionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionMenu.swift; sourceTree = ""; }; 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.swift; sourceTree = ""; }; 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyItemView.swift; sourceTree = ""; }; @@ -2490,6 +2492,7 @@ E1C926032887565C002A7A66 /* ActionButtonHStack.swift */, 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */, 4E97D1842D064B43004B89AD /* RefreshMetadataButton.swift */, + 4EDDB49B2D596E0700DA16E8 /* VersionMenu.swift */, ); path = ActionButtons; sourceTree = ""; @@ -5971,6 +5974,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 */, diff --git a/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift b/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift index f08905431..51e30cc01 100644 --- a/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift +++ b/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift @@ -80,7 +80,7 @@ extension ItemView { Menu { ForEach(mediaSources, id: \.hashValue) { mediaSource in Button { -// viewModel.selectedMediaSource = mediaSource + viewModel.send(.selectMediaSource(mediaSource)) } label: { if let selectedMediaSource = viewModel.selectedMediaSource, selectedMediaSource == mediaSource { Label(selectedMediaSource.displayTitle, systemImage: "checkmark")