From d276bd7449f080c8bc377f95caf7985b984d8457 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 11 Nov 2024 20:24:32 -0700 Subject: [PATCH] [tvOS] ItemView Button Cleanup (#1296) --- .../Components/ActionButtonHStack.swift | 61 ------------------- .../ActionButtons/ActionButton.swift | 53 ++++++++++++++++ .../ActionButtons/ActionButtonHStack.swift | 57 +++++++++++++++++ .../Components/ActionButtons/ActionMenu.swift | 50 +++++++++++++++ .../Components/EpisodeCard.swift | 2 +- .../Components/EpisodeContent.swift | 7 +-- .../Components/EpisodeHStack.swift | 1 + Swiftfin.xcodeproj/project.pbxproj | 18 +++++- 8 files changed, 182 insertions(+), 67 deletions(-) delete mode 100644 Swiftfin tvOS/Views/ItemView/Components/ActionButtonHStack.swift create mode 100644 Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift create mode 100644 Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift create mode 100644 Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtonHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtonHStack.swift deleted file mode 100644 index 8886d7af..00000000 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtonHStack.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// 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) 2024 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -extension ItemView { - - struct ActionButtonHStack: View { - - @ObservedObject - var viewModel: ItemViewModel - - var body: some View { - HStack { - Button { - viewModel.send(.toggleIsPlayed) - } label: { - Group { - if viewModel.item.userData?.isPlayed ?? false { - Image(systemName: "checkmark.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle( - .primary, - Color.jellyfinPurple - ) - } else { - Image(systemName: "checkmark.circle") - } - } - .font(.title3) - .frame(height: 100) - .frame(maxWidth: .infinity) - } - .buttonStyle(.plain) - - Button { - viewModel.send(.toggleIsFavorite) - } label: { - Group { - if viewModel.item.userData?.isFavorite ?? false { - Image(systemName: "heart.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .pink) - } else { - Image(systemName: "heart.circle") - } - } - .font(.title3) - .frame(height: 100) - .frame(maxWidth: .infinity) - } - .buttonStyle(.plain) - } - } - } -} diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift new file mode 100644 index 00000000..c6f6c425 --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift @@ -0,0 +1,53 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +extension ItemView { + + struct ActionButton: View { + + @Environment(\.isSelected) + private var isSelected + @FocusState + private var isFocused: Bool + + let title: String + let icon: String + let selectedIcon: String + let onSelect: () -> Void + + // MARK: - Body + + var body: some View { + Button(action: onSelect) { + ZStack { + if isSelected { + Rectangle() + .fill( + isFocused ? AnyShapeStyle(HierarchicalShapeStyle.primary) : + AnyShapeStyle(HierarchicalShapeStyle.primary.opacity(0.5)) + ) + } else { + Rectangle() + .fill(isFocused ? Color.white : Color.white.opacity(0.5)) + } + + Label(title, systemImage: isSelected ? selectedIcon : icon) + .font(.title3) + .fontWeight(.semibold) + .foregroundStyle(.black) + .labelStyle(.iconOnly) + } + } + .focused($isFocused) + .buttonStyle(.card) + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift new file mode 100644 index 00000000..2e6377fa --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift @@ -0,0 +1,57 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct ActionButtonHStack: View { + + @ObservedObject + var viewModel: ItemViewModel + + // TODO: Shrink to minWWith 100 (button) / 50 (menu) and 16 spacing to get 4 buttons inline + var body: some View { + HStack(alignment: .center, spacing: 24) { + + // MARK: - Toggle Played + + ActionButton( + title: L10n.played, + icon: "checkmark.circle", + selectedIcon: "checkmark.circle.fill" + ) { + viewModel.send(.toggleIsPlayed) + } + .foregroundStyle(.purple) + .environment(\.isSelected, viewModel.item.userData?.isPlayed ?? false) + .frame(minWidth: 140, maxWidth: .infinity) + + // MARK: - Toggle Favorite + + ActionButton( + title: L10n.favorited, + icon: "heart.circle", + selectedIcon: "heart.circle.fill" + ) { + viewModel.send(.toggleIsFavorite) + } + .foregroundStyle(.pink) + .environment(\.isSelected, viewModel.item.userData?.isFavorite ?? false) + .frame(minWidth: 140, maxWidth: .infinity) + + // MARK: - Additional Menu Options + + // TODO: Enable if there are more items needed + /* ActionMenu {} + .frame(width: 70)*/ + } + .frame(height: 100) + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift new file mode 100644 index 00000000..8fc090df --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift @@ -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) 2024 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +extension ItemView { + + struct ActionMenu: View { + + @FocusState + private var isFocused: Bool + + @ViewBuilder + let menuItems: Content + + // MARK: - Body + + var body: some View { + Menu { + menuItems + } label: { + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(isFocused ? Color.white : Color.white.opacity(0.5)) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.clear, lineWidth: 2) + ) + + Label(L10n.menuButtons, systemImage: "ellipsis") + .font(.title3) + .fontWeight(.semibold) + .foregroundStyle(.black) + .labelStyle(.iconOnly) + .rotationEffect(.degrees(90)) + } + } + .focused($isFocused) + .scaleEffect(isFocused ? 1.20 : 1.0) + .animation(.easeInOut(duration: 0.15), value: isFocused) + .menuStyle(.borderlessButton) + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift index 1a8a4fc1..80c08508 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift @@ -37,7 +37,7 @@ extension SeriesEpisodeSelector { Image(systemName: "play.fill") .resizable() .frame(width: 50, height: 50) - .foregroundColor(.white) + .foregroundStyle(.secondary) } } } diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift index 986bac74..ec4ee0d7 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift @@ -52,20 +52,19 @@ extension SeriesEpisodeSelector { Button { onSelect() } label: { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 8) { subHeaderView headerView contentView - // Removing the alignment below makes the text center - .frame(maxWidth: .infinity, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .leading) L10n.seeMore.text .font(.caption.weight(.light)) .foregroundStyle(accentColor) } - .padding(5) + .padding() } .buttonStyle(.card) } diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift index e228d506..36937358 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift @@ -41,6 +41,7 @@ extension SeriesEpisodeSelector { ) { episode in SeriesEpisodeSelector.EpisodeCard(episode: episode) .focused($focusedEpisodeID, equals: episode.id) + .padding(.horizontal, 4) } .scrollBehavior(.continuousLeadingEdge) .insets(horizontal: EdgeInsets.edgePadding) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 61ab1aeb..1b06d6c6 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 4E35CE6D2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; }; 4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; }; + 4E5334A22CD1A28700D59FA8 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */; }; 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; 4E63B9FA2C8A5BEF00C25378 /* AdminDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */; }; 4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; }; @@ -121,6 +122,7 @@ 4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; }; 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; }; 4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; }; + 4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; }; @@ -1099,6 +1101,7 @@ 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeek.swift; sourceTree = ""; }; 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskState.swift; sourceTree = ""; }; 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysViewModel.swift; sourceTree = ""; }; + 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; }; 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = ""; }; 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = ""; }; @@ -1152,6 +1155,7 @@ 4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = ""; }; 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = ""; }; 4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; + 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMenu.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; @@ -2018,6 +2022,16 @@ path = PlaybackBitrate; sourceTree = ""; }; + 4E5334A02CD1A27C00D59FA8 /* ActionButtons */ = { + isa = PBXGroup; + children = ( + 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */, + E1C926032887565C002A7A66 /* ActionButtonHStack.swift */, + 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */, + ); + path = ActionButtons; + sourceTree = ""; + }; 4E63B9F52C8A5BEF00C25378 /* AdminDashboardView */ = { isa = PBXGroup; children = ( @@ -3967,7 +3981,7 @@ isa = PBXGroup; children = ( E1A16CA2288A7D0000EA4679 /* AboutView */, - E1C926032887565C002A7A66 /* ActionButtonHStack.swift */, + 4E5334A02CD1A27C00D59FA8 /* ActionButtons */, E1C926012887565C002A7A66 /* AttributeHStack.swift */, E185920528CDAA6400326F80 /* CastAndCrewHStack.swift */, E1153D982BBA3E6100424D36 /* EpisodeSelector */, @@ -4572,6 +4586,7 @@ E152107D2947ACA000375CC2 /* InvertedLightAppIcon.swift in Sources */, E1549663296CA2EF00C4EF88 /* UserSession.swift in Sources */, 4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */, + 4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, E145EB232BDCCA43003BF6F3 /* BulletedList.swift in Sources */, E104DC972B9E7E29008F506D /* AssertionFailureView.swift in Sources */, @@ -4893,6 +4908,7 @@ E1575E9A293E7B1E001665B1 /* Array.swift in Sources */, E1575E8D293E7B1E001665B1 /* URLComponents.swift in Sources */, E187A60329AB28F0008387E6 /* RotateContentView.swift in Sources */, + 4E5334A22CD1A28700D59FA8 /* ActionButton.swift in Sources */, 4EBE064E2C7EB6D3004A6C03 /* VideoPlayerType.swift in Sources */, E1575E94293E7B1E001665B1 /* VerticalAlignment.swift in Sources */, E1575EA3293E7B1E001665B1 /* UIDevice.swift in Sources */,