From 3f203200cc11c03bfe7128b45b15b42bebfa8e57 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 29 Jan 2025 10:29:19 -0700 Subject: [PATCH 1/4] Button cleanup & errorViews --- Swiftfin tvOS/Components/ErrorView.swift | 47 ++++++++++++ Swiftfin tvOS/Components/ListRowButton.swift | 27 ++++++- Swiftfin tvOS/Components/PrimaryButton.swift | 76 +++++++++++++++++++ Swiftfin tvOS/Views/AppLoadingView.swift | 12 ++- .../ChannelLibraryView.swift | 8 +- .../Views/HomeView/HomeErrorView.swift | 70 ----------------- Swiftfin tvOS/Views/HomeView/HomeView.swift | 26 ++++--- Swiftfin tvOS/Views/ItemView/ItemView.swift | 9 ++- Swiftfin tvOS/Views/MediaView/MediaView.swift | 26 ++++--- .../PagingLibraryView/PagingLibraryView.swift | 9 +-- .../Views/ProgramsView/ProgramsView.swift | 8 +- Swiftfin tvOS/Views/QuickConnectView.swift | 9 ++- Swiftfin tvOS/Views/SearchView.swift | 33 ++++---- Swiftfin.xcodeproj/project.pbxproj | 12 ++- 14 files changed, 243 insertions(+), 129 deletions(-) create mode 100644 Swiftfin tvOS/Components/ErrorView.swift create mode 100644 Swiftfin tvOS/Components/PrimaryButton.swift delete mode 100644 Swiftfin tvOS/Views/HomeView/HomeErrorView.swift diff --git a/Swiftfin tvOS/Components/ErrorView.swift b/Swiftfin tvOS/Components/ErrorView.swift new file mode 100644 index 000000000..0a9c33c85 --- /dev/null +++ b/Swiftfin tvOS/Components/ErrorView.swift @@ -0,0 +1,47 @@ +// +// 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 + +// TODO: should use environment refresh instead? +struct ErrorView: View { + + private let error: ErrorType + private var onRetry: (() -> Void)? + + var body: some View { + VStack(spacing: 20) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 150)) + .foregroundColor(Color.red) + + Text(error.localizedDescription) + .frame(minWidth: 250, maxWidth: 750) + .multilineTextAlignment(.center) + + if let onRetry { + PrimaryButton(title: L10n.retry) + .onSelect(onRetry) + } + } + } +} + +extension ErrorView { + + init(error: ErrorType) { + self.init( + error: error, + onRetry: nil + ) + } + + func onRetry(_ action: @escaping () -> Void) -> Self { + copy(modifying: \.onRetry, with: action) + } +} diff --git a/Swiftfin tvOS/Components/ListRowButton.swift b/Swiftfin tvOS/Components/ListRowButton.swift index 7b964e3d8..3e72396c1 100644 --- a/Swiftfin tvOS/Components/ListRowButton.swift +++ b/Swiftfin tvOS/Components/ListRowButton.swift @@ -10,16 +10,32 @@ import SwiftUI struct ListRowButton: View { + // MARK: - Environment + + @Environment(\.isEnabled) + private var isEnabled + + // MARK: - Focus State + + @FocusState + private var isFocused: Bool + + // MARK: - Button Variables + let title: String let role: ButtonRole? let action: () -> Void + // MARK: - Initializer + init(_ title: String, role: ButtonRole? = nil, action: @escaping () -> Void) { self.title = title self.role = role self.action = action } + // MARK: - Body + var body: some View { Button { action() @@ -28,6 +44,12 @@ struct ListRowButton: View { RoundedRectangle(cornerRadius: 10) .fill(secondaryStyle) + if !isEnabled { + Color.black.opacity(0.5) + } else if isFocused { + Color.white.opacity(0.25) + } + Text(title) .foregroundStyle(primaryStyle) .font(.body.weight(.bold)) @@ -35,9 +57,10 @@ struct ListRowButton: View { } .buttonStyle(.card) .frame(height: 75) + .focused($isFocused) } - // MARK: - Styles + // MARK: - Primary Style private var primaryStyle: some ShapeStyle { if role == .destructive { @@ -47,6 +70,8 @@ struct ListRowButton: View { } } + // MARK: - Secondary Style + private var secondaryStyle: some ShapeStyle { if role == .destructive { return AnyShapeStyle(Color.red.opacity(0.2)) diff --git a/Swiftfin tvOS/Components/PrimaryButton.swift b/Swiftfin tvOS/Components/PrimaryButton.swift new file mode 100644 index 000000000..c7118bd80 --- /dev/null +++ b/Swiftfin tvOS/Components/PrimaryButton.swift @@ -0,0 +1,76 @@ +// +// 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 SwiftUI + +struct PrimaryButton: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - Environment + + @Environment(\.isEnabled) + private var isEnabled + + // MARK: - Focus State + + @FocusState + private var isFocused: Bool + + // MARK: - Button Variables + + private let title: String + private var onSelect: () -> Void + + // MARK: - Body + + var body: some View { + Button { + onSelect() + } label: { + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(accentColor) + + if !isEnabled { + Color.black.opacity(0.5) + } else if isFocused { + Color.white.opacity(0.25) + } + + Text(title) + .fontWeight(.bold) + .foregroundStyle(isFocused ? Color.black : accentColor.overlayColor) + } + } + .buttonStyle(.card) + .frame(height: 75) + .frame(maxWidth: 750) + .focused($isFocused) + } +} + +extension PrimaryButton { + + // MARK: - Initializer + + init(title: String) { + self.init( + title: title, + onSelect: {} + ) + } + + func onSelect(_ action: @escaping () -> Void) -> Self { + copy(modifying: \.onSelect, with: action) + } +} diff --git a/Swiftfin tvOS/Views/AppLoadingView.swift b/Swiftfin tvOS/Views/AppLoadingView.swift index 1fcdc22ce..21ccb3fcf 100644 --- a/Swiftfin tvOS/Views/AppLoadingView.swift +++ b/Swiftfin tvOS/Views/AppLoadingView.swift @@ -18,10 +18,20 @@ struct AppLoadingView: View { ZStack { Color.clear + if !didFailMigration { + ProgressView() + } + if didFailMigration { - Text("An internal error occurred.") + ErrorView(error: JellyfinAPIError("An internal error occurred.")) } } + .topBarTrailing { + Button(L10n.advanced, systemImage: "gearshape.fill") {} + .foregroundStyle(.secondary) + .disabled(true) + .opacity(didFailMigration ? 0 : 1) + } .onNotification(.didFailMigration) { _ in didFailMigration = true } diff --git a/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift b/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift index 8a2ad4c48..3fe78240c 100644 --- a/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift +++ b/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift @@ -40,7 +40,7 @@ struct ChannelLibraryView: View { } var body: some View { - WrappedView { + ZStack { switch viewModel.state { case .content: if viewModel.elements.isEmpty { @@ -49,11 +49,15 @@ struct ChannelLibraryView: View { contentView } case let .error(error): - Text(error.localizedDescription) + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } case .initial, .refreshing: ProgressView() } } + .animation(.linear(duration: 0.1), value: viewModel.state) .ignoresSafeArea() .onFirstAppear { if viewModel.state == .initial { diff --git a/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift b/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift deleted file mode 100644 index b7d62961a..000000000 --- a/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift +++ /dev/null @@ -1,70 +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) 2025 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -// TODO: make general `ErrorView` like iOS - -#warning("TODO: implement") - -extension HomeView { - - struct ErrorView: View { - - @ObservedObject - var viewModel: HomeViewModel - - var body: some View { - Text("TODO") - } - } -} - -// extension HomeView { -// -// struct ErrorView: View { -// -// @ObservedObject -// var viewModel: HomeViewModel -// -// let errorMessage: ErrorMessage -// -// var body: some View { -// VStack { -// if viewModel.isLoading { -// ProgressView() -// .frame(width: 100, height: 100) -// .scaleEffect(2) -// } else { -// Image(systemName: "xmark.circle.fill") -// .font(.system(size: 72)) -// .foregroundColor(Color.red) -// .frame(width: 100, height: 100) -// } -// -//// Text("\(errorMessage.code)") -// -// Text(errorMessage.message) -// .frame(minWidth: 50, maxWidth: 240) -// .multilineTextAlignment(.center) -// -// Button { -//// viewModel.refresh() -// } label: { -// L10n.retry.text -// .bold() -// .font(.callout) -// .frame(width: 400, height: 75) -// .background(Color.jellyfinPurple) -// } -// .buttonStyle(.card) -// } -// .offset(y: -50) -// } -// } -// } diff --git a/Swiftfin tvOS/Views/HomeView/HomeView.swift b/Swiftfin tvOS/Views/HomeView/HomeView.swift index 5cfb0b65b..73abe5cb9 100644 --- a/Swiftfin tvOS/Views/HomeView/HomeView.swift +++ b/Swiftfin tvOS/Views/HomeView/HomeView.swift @@ -50,19 +50,23 @@ struct HomeView: View { } var body: some View { - WrappedView { - Group { - switch viewModel.state { - case .content: - contentView - case let .error(error): - Text(error.localizedDescription) - case .initial, .refreshing: - ProgressView() - } + ZStack { + // This keeps the ErrorView vertically aligned with the PagingLibraryView + Color.clear + + switch viewModel.state { + case .content: + contentView + case let .error(error): + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } + case .initial, .refreshing: + ProgressView() } - .transition(.opacity.animation(.linear(duration: 0.2))) } + .animation(.linear(duration: 0.1), value: viewModel.state) .onFirstAppear { viewModel.send(.refresh) } diff --git a/Swiftfin tvOS/Views/ItemView/ItemView.swift b/Swiftfin tvOS/Views/ItemView/ItemView.swift index cce801081..ae32609ab 100644 --- a/Swiftfin tvOS/Views/ItemView/ItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/ItemView.swift @@ -52,17 +52,20 @@ struct ItemView: View { } var body: some View { - WrappedView { + ZStack { switch viewModel.state { case .content: contentView case let .error(error): - Text(error.localizedDescription) + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } case .initial, .refreshing: ProgressView() } } - .transition(.opacity.animation(.linear(duration: 0.2))) + .animation(.linear(duration: 0.1), value: viewModel.state) .onFirstAppear { viewModel.send(.refresh) } diff --git a/Swiftfin tvOS/Views/MediaView/MediaView.swift b/Swiftfin tvOS/Views/MediaView/MediaView.swift index 961f95b3a..2d3cbc2a6 100644 --- a/Swiftfin tvOS/Views/MediaView/MediaView.swift +++ b/Swiftfin tvOS/Views/MediaView/MediaView.swift @@ -52,19 +52,23 @@ struct MediaView: View { } var body: some View { - WrappedView { - Group { - switch viewModel.state { - case .content: - contentView - case let .error(error): - Text(error.localizedDescription) - case .initial, .refreshing: - ProgressView() - } + ZStack { + // This keeps the ErrorView vertically aligned with the PagingLibraryView + Color.clear + + switch viewModel.state { + case .content: + contentView + case let .error(error): + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } + case .initial, .refreshing: + ProgressView() } - .transition(.opacity.animation(.linear(duration: 0.2))) } + .animation(.linear(duration: 0.1), value: viewModel.state) .ignoresSafeArea() .onFirstAppear { viewModel.send(.refresh) diff --git a/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift index b319408a1..d9fe716b3 100644 --- a/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift +++ b/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift @@ -247,11 +247,10 @@ struct PagingLibraryView: View { @ViewBuilder private func errorView(with error: some Error) -> some View { - Text(error.localizedDescription) - /* ErrorView(error: error) - .onRetry { - viewModel.send(.refresh) - } */ + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } } // MARK: Grid View diff --git a/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift b/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift index 686854cf0..7eabf0b66 100644 --- a/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift +++ b/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift @@ -78,7 +78,7 @@ struct ProgramsView: View { } var body: some View { - WrappedView { + ZStack { switch programsViewModel.state { case .content: if programsViewModel.hasNoResults { @@ -87,11 +87,15 @@ struct ProgramsView: View { contentView } case let .error(error): - Text(error.localizedDescription) + ErrorView(error: error) + .onRetry { + programsViewModel.send(.refresh) + } case .initial, .refreshing: ProgressView() } } + .animation(.linear(duration: 0.1), value: programsViewModel.state) .ignoresSafeArea(edges: [.bottom, .horizontal]) .onFirstAppear { if programsViewModel.state == .initial { diff --git a/Swiftfin tvOS/Views/QuickConnectView.swift b/Swiftfin tvOS/Views/QuickConnectView.swift index ad5e68939..28848e2f0 100644 --- a/Swiftfin tvOS/Views/QuickConnectView.swift +++ b/Swiftfin tvOS/Views/QuickConnectView.swift @@ -47,7 +47,7 @@ struct QuickConnectView: View { } var body: some View { - WrappedView { + ZStack { switch viewModel.state { case .idle, .authenticated: Color.clear @@ -56,10 +56,13 @@ struct QuickConnectView: View { case let .polling(code): pollingView(code: code) case let .error(error): - Text(error.localizedDescription) -// ErrorView(error: error) + ErrorView(error: error) + .onRetry { + viewModel.start() + } } } + .animation(.linear(duration: 0.1), value: viewModel.state) .edgePadding() .navigationTitle(L10n.quickConnect) .onFirstAppear { diff --git a/Swiftfin tvOS/Views/SearchView.swift b/Swiftfin tvOS/Views/SearchView.swift index ce6b6ec97..2ba04fa94 100644 --- a/Swiftfin tvOS/Views/SearchView.swift +++ b/Swiftfin tvOS/Views/SearchView.swift @@ -110,25 +110,26 @@ struct SearchView: View { } var body: some View { - WrappedView { - Group { - switch viewModel.state { - case let .error(error): - Text(error.localizedDescription) - case .initial: - suggestionsView - case .content: - if viewModel.hasNoResults { - L10n.noResults.text - } else { - resultsView - } - case .searching: - ProgressView() + ZStack { + switch viewModel.state { + case .initial: + suggestionsView + case .content: + if viewModel.hasNoResults { + L10n.noResults.text + } else { + resultsView } + case let .error(error): + ErrorView(error: error) + .onRetry { + viewModel.send(.search(query: searchQuery)) + } + case .searching: + ProgressView() } - .transition(.opacity.animation(.linear(duration: 0.2))) } + .animation(.linear(duration: 0.1), value: viewModel.state) .ignoresSafeArea(edges: [.bottom, .horizontal]) .onFirstAppear { viewModel.send(.getSuggestions) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 1353a9ba4..fd08385e2 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -244,6 +244,8 @@ 4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87482CBF824B002354D2 /* DevicesView.swift */; }; 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */; }; 4EEEEA242CFA8E1500527D79 /* NavigationBarMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEEEA232CFA8E1500527D79 /* NavigationBarMenuButton.swift */; }; + 4EF0DCA72D4965D9005A5194 /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF0DCA62D4965D9005A5194 /* PrimaryButton.swift */; }; + 4EF0DCA92D49751B005A5194 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF0DCA82D49751B005A5194 /* ErrorView.swift */; }; 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 */; }; @@ -955,7 +957,6 @@ E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A3E4CE2BB7E02B005C59F8 /* DelayedProgressView.swift */; }; E1A3E4D12BB7F5BF005C59F8 /* ErrorCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A3E4D02BB7F5BF005C59F8 /* ErrorCard.swift */; }; E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */; }; - E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */; }; E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */; }; E1A5056A2D0B733F007EE305 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A505692D0B733F007EE305 /* Optional.swift */; }; E1A5056B2D0B733F007EE305 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A505692D0B733F007EE305 /* Optional.swift */; }; @@ -1397,6 +1398,8 @@ 4EED87482CBF824B002354D2 /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = ""; }; 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = ""; }; 4EEEEA232CFA8E1500527D79 /* NavigationBarMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarMenuButton.swift; sourceTree = ""; }; + 4EF0DCA62D4965D9005A5194 /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = ""; }; + 4EF0DCA82D49751B005A5194 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 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 = ""; }; @@ -1864,7 +1867,6 @@ E1A3E4CE2BB7E02B005C59F8 /* DelayedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayedProgressView.swift; sourceTree = ""; }; E1A3E4D02BB7F5BF005C59F8 /* ErrorCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorCard.swift; sourceTree = ""; }; E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemSelector.swift; sourceTree = ""; }; - E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = ""; }; E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = ""; }; E1A505692D0B733F007EE305 /* Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Optional.swift; sourceTree = ""; }; E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; @@ -3243,6 +3245,7 @@ E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */, E1C92618288756BD002A7A66 /* DotHStack.swift */, E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */, + 4EF0DCA82D49751B005A5194 /* ErrorView.swift */, E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */, E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */, E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */, @@ -3250,6 +3253,7 @@ 4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */, E1C92617288756BD002A7A66 /* PosterButton.swift */, E1C92619288756BD002A7A66 /* PosterHStack.swift */, + 4EF0DCA62D4965D9005A5194 /* PrimaryButton.swift */, E12CC1C428D12D9B00678D5D /* SeeAllPosterButton.swift */, E1E9EFE928C6B96400CC1F8B /* ServerButton.swift */, E17885A3278105170094FBCF /* SFSymbolButton.swift */, @@ -4601,7 +4605,6 @@ isa = PBXGroup; children = ( E12CC1C328D12D6300678D5D /* Components */, - E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */, 531690E6267ABD79005D8AB9 /* HomeView.swift */, ); path = HomeView; @@ -5478,7 +5481,6 @@ 4E2AC4C32C6C491200DD600D /* AudoCodec.swift in Sources */, E1575EA6293E7D40001665B1 /* VideoPlayer.swift in Sources */, E185920628CDAA6400326F80 /* CastAndCrewHStack.swift in Sources */, - E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */, 4E8F74AB2CE03DD300CC8969 /* DeleteItemViewModel.swift in Sources */, 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, E122A9142788EAAD0060FA63 /* MediaStream.swift in Sources */, @@ -5653,6 +5655,7 @@ E1575E84293E7A00001665B1 /* PrimaryAppIcon.swift in Sources */, E1153DCD2BBB633B00424D36 /* FastSVGView.swift in Sources */, E1ED7FE22CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */, + 4EF0DCA92D49751B005A5194 /* ErrorView.swift in Sources */, E1CB75762C80EAFA00217C76 /* ArrayBuilder.swift in Sources */, E102315B2BCF8AF8009D71FC /* ProgramProgressOverlay.swift in Sources */, E1E6C45129B104850064123F /* Button.swift in Sources */, @@ -5662,6 +5665,7 @@ E1A1528B28FD22F600600579 /* TextPairView.swift in Sources */, E11042762B8013DF00821020 /* Stateful.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, + 4EF0DCA72D4965D9005A5194 /* PrimaryButton.swift in Sources */, E1575E66293E77B5001665B1 /* Poster.swift in Sources */, E18E021F2887492B0022598C /* SystemImageContentView.swift in Sources */, E19D41B42BF2C0020082B8B2 /* StoredValues+Temp.swift in Sources */, From 0a40e30c2097422fb830dba0dc4153edc516f305 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 29 Jan 2025 11:37:25 -0700 Subject: [PATCH 2/4] Change the Sign Out button to be `ListRowButton`. Sets a better height value using `maxHeight` to ensure that it doesn't exceed the `ListRow` sizing. --- Swiftfin tvOS/Components/ListRowButton.swift | 2 +- .../Views/SettingsView/SettingsView.swift | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/Swiftfin tvOS/Components/ListRowButton.swift b/Swiftfin tvOS/Components/ListRowButton.swift index 3e72396c1..f5818cbf9 100644 --- a/Swiftfin tvOS/Components/ListRowButton.swift +++ b/Swiftfin tvOS/Components/ListRowButton.swift @@ -56,7 +56,7 @@ struct ListRowButton: View { } } .buttonStyle(.card) - .frame(height: 75) + .frame(maxHeight: 75) .focused($isFocused) } diff --git a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift index 69be51e15..89e553c35 100644 --- a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift @@ -44,22 +44,14 @@ struct SettingsView: View { .onSelect { router.route(to: \.serverDetail, viewModel.userSession.server) } + } - Button { + Section { + ListRowButton(L10n.switchUser) { viewModel.signOut() - } label: { - HStack { - - Text(L10n.switchUser) - .foregroundColor(.jellyfinPurple) - - Spacer() - - Image(systemName: "chevron.right") - .font(.body.weight(.regular)) - .foregroundColor(.secondary) - } } + .foregroundStyle(Color.jellyfinPurple.overlayColor, Color.jellyfinPurple) + .listRowInsets(.zero) } Section(L10n.videoPlayer) { From 4d943629655e914fdf63130ee58fd3979d5d4b99 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 29 Jan 2025 11:39:24 -0700 Subject: [PATCH 3/4] deleteUsersButton needs to be manually set back to 75 --- .../Views/SelectUserView/Components/SelectUserBottomBar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift b/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift index ece7b9b16..d6d6a0b3f 100644 --- a/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift +++ b/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift @@ -72,7 +72,7 @@ extension SelectUserView { ListRowButton(L10n.delete, role: .destructive) { onDelete() } - .frame(width: 400, height: 50) + .frame(width: 400, height: 75) .disabled(!areUsersSelected) } From 9ed1b0b3d55816398d2528500ecb1dd172a086e5 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sat, 15 Feb 2025 15:01:50 -0700 Subject: [PATCH 4/4] wip --- Swiftfin tvOS/Components/ErrorView.swift | 9 ++- Swiftfin tvOS/Components/ListRowButton.swift | 21 ++--- Swiftfin tvOS/Components/PrimaryButton.swift | 76 ------------------- .../Views/UserSignInView/UserSignInView.swift | 3 +- Swiftfin.xcodeproj/project.pbxproj | 4 - 5 files changed, 16 insertions(+), 97 deletions(-) delete mode 100644 Swiftfin tvOS/Components/PrimaryButton.swift diff --git a/Swiftfin tvOS/Components/ErrorView.swift b/Swiftfin tvOS/Components/ErrorView.swift index 0a9c33c85..d3bceda83 100644 --- a/Swiftfin tvOS/Components/ErrorView.swift +++ b/Swiftfin tvOS/Components/ErrorView.swift @@ -6,11 +6,15 @@ // Copyright (c) 2025 Jellyfin & Jellyfin Contributors // +import Defaults import SwiftUI // TODO: should use environment refresh instead? struct ErrorView: View { + @Default(.accentColor) + private var accentColor + private let error: ErrorType private var onRetry: (() -> Void)? @@ -25,8 +29,9 @@ struct ErrorView: View { .multilineTextAlignment(.center) if let onRetry { - PrimaryButton(title: L10n.retry) - .onSelect(onRetry) + ListRowButton(L10n.retry, action: onRetry) + .foregroundStyle(accentColor.overlayColor, accentColor) + .frame(maxWidth: 750) } } } diff --git a/Swiftfin tvOS/Components/ListRowButton.swift b/Swiftfin tvOS/Components/ListRowButton.swift index f5818cbf9..03aebb926 100644 --- a/Swiftfin tvOS/Components/ListRowButton.swift +++ b/Swiftfin tvOS/Components/ListRowButton.swift @@ -8,6 +8,8 @@ import SwiftUI +// TODO: on focus, make the cancel and destructive style +// match style like in an `alert` struct ListRowButton: View { // MARK: - Environment @@ -37,18 +39,11 @@ struct ListRowButton: View { // MARK: - Body var body: some View { - Button { - action() - } label: { + Button(action: action) { ZStack { RoundedRectangle(cornerRadius: 10) .fill(secondaryStyle) - - if !isEnabled { - Color.black.opacity(0.5) - } else if isFocused { - Color.white.opacity(0.25) - } + .brightness(isFocused ? 0.25 : 0) Text(title) .foregroundStyle(primaryStyle) @@ -63,20 +58,20 @@ struct ListRowButton: View { // MARK: - Primary Style private var primaryStyle: some ShapeStyle { - if role == .destructive { + if role == .destructive || role == .cancel { return AnyShapeStyle(Color.red) } else { - return AnyShapeStyle(.primary) + return AnyShapeStyle(HierarchicalShapeStyle.primary) } } // MARK: - Secondary Style private var secondaryStyle: some ShapeStyle { - if role == .destructive { + if role == .destructive || role == .cancel { return AnyShapeStyle(Color.red.opacity(0.2)) } else { - return AnyShapeStyle(.secondary) + return AnyShapeStyle(HierarchicalShapeStyle.secondary) } } } diff --git a/Swiftfin tvOS/Components/PrimaryButton.swift b/Swiftfin tvOS/Components/PrimaryButton.swift deleted file mode 100644 index c7118bd80..000000000 --- a/Swiftfin tvOS/Components/PrimaryButton.swift +++ /dev/null @@ -1,76 +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) 2025 Jellyfin & Jellyfin Contributors -// - -import Defaults -import SwiftUI - -struct PrimaryButton: View { - - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor - - // MARK: - Environment - - @Environment(\.isEnabled) - private var isEnabled - - // MARK: - Focus State - - @FocusState - private var isFocused: Bool - - // MARK: - Button Variables - - private let title: String - private var onSelect: () -> Void - - // MARK: - Body - - var body: some View { - Button { - onSelect() - } label: { - ZStack { - RoundedRectangle(cornerRadius: 10) - .fill(accentColor) - - if !isEnabled { - Color.black.opacity(0.5) - } else if isFocused { - Color.white.opacity(0.25) - } - - Text(title) - .fontWeight(.bold) - .foregroundStyle(isFocused ? Color.black : accentColor.overlayColor) - } - } - .buttonStyle(.card) - .frame(height: 75) - .frame(maxWidth: 750) - .focused($isFocused) - } -} - -extension PrimaryButton { - - // MARK: - Initializer - - init(title: String) { - self.init( - title: title, - onSelect: {} - ) - } - - func onSelect(_ action: @escaping () -> Void) -> Self { - copy(modifying: \.onSelect, with: action) - } -} diff --git a/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift b/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift index 45ffd2725..8fe10156e 100644 --- a/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift +++ b/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift @@ -85,10 +85,9 @@ struct UserSignInView: View { } if case .signingIn = viewModel.state { - ListRowButton(L10n.cancel) { + ListRowButton(L10n.cancel, role: .cancel) { viewModel.send(.cancel) } - .foregroundStyle(.red, accentColor) .padding(.vertical) } else { ListRowButton(L10n.signIn) { diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 3d06f939e..64260098f 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -244,7 +244,6 @@ 4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87482CBF824B002354D2 /* DevicesView.swift */; }; 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */; }; 4EEEEA242CFA8E1500527D79 /* NavigationBarMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEEEA232CFA8E1500527D79 /* NavigationBarMenuButton.swift */; }; - 4EF0DCA72D4965D9005A5194 /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF0DCA62D4965D9005A5194 /* PrimaryButton.swift */; }; 4EF0DCA92D49751B005A5194 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF0DCA82D49751B005A5194 /* ErrorView.swift */; }; 4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; }; 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; }; @@ -1457,7 +1456,6 @@ 4EED87482CBF824B002354D2 /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = ""; }; 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = ""; }; 4EEEEA232CFA8E1500527D79 /* NavigationBarMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarMenuButton.swift; sourceTree = ""; }; - 4EF0DCA62D4965D9005A5194 /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = ""; }; 4EF0DCA82D49751B005A5194 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = ""; }; 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = ""; }; @@ -3371,7 +3369,6 @@ 4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */, E1C92617288756BD002A7A66 /* PosterButton.swift */, E1C92619288756BD002A7A66 /* PosterHStack.swift */, - 4EF0DCA62D4965D9005A5194 /* PrimaryButton.swift */, E12CC1C428D12D9B00678D5D /* SeeAllPosterButton.swift */, E1E9EFE928C6B96400CC1F8B /* ServerButton.swift */, E17885A3278105170094FBCF /* SFSymbolButton.swift */, @@ -6119,7 +6116,6 @@ E1A1528B28FD22F600600579 /* TextPairView.swift in Sources */, E11042762B8013DF00821020 /* Stateful.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, - 4EF0DCA72D4965D9005A5194 /* PrimaryButton.swift in Sources */, E1575E66293E77B5001665B1 /* Poster.swift in Sources */, E18E021F2887492B0022598C /* SystemImageContentView.swift in Sources */, E19D41B42BF2C0020082B8B2 /* StoredValues+Temp.swift in Sources */,