Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tvOS] ErrorViews - Creation #1414

Merged
merged 8 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Swiftfin tvOS/Components/ErrorView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// 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

// TODO: should use environment refresh instead?
struct ErrorView<ErrorType: Error>: View {

@Default(.accentColor)
private var accentColor

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 {
ListRowButton(L10n.retry, action: onRetry)
.foregroundStyle(accentColor.overlayColor, accentColor)
.frame(maxWidth: 750)
}
}
}
}

extension ErrorView {

init(error: ErrorType) {
self.init(
error: error,
onRetry: nil
)
}

func onRetry(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onRetry, with: action)
}
}
38 changes: 29 additions & 9 deletions Swiftfin tvOS/Components/ListRowButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,70 @@

import SwiftUI

// TODO: on focus, make the cancel and destructive style
// match style like in an `alert`
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()
} label: {
Button(action: action) {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(secondaryStyle)
.brightness(isFocused ? 0.25 : 0)

Text(title)
.foregroundStyle(primaryStyle)
.font(.body.weight(.bold))
}
}
.buttonStyle(.card)
.frame(height: 75)
.frame(maxHeight: 75)
.focused($isFocused)
}

// MARK: - Styles
// 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)
}
}
}
12 changes: 11 additions & 1 deletion Swiftfin tvOS/Views/AppLoadingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ struct ChannelLibraryView: View {
}

var body: some View {
WrappedView {
ZStack {
switch viewModel.state {
case .content:
if viewModel.elements.isEmpty {
Expand All @@ -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 {
Expand Down
70 changes: 0 additions & 70 deletions Swiftfin tvOS/Views/HomeView/HomeErrorView.swift

This file was deleted.

26 changes: 15 additions & 11 deletions Swiftfin tvOS/Views/HomeView/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
9 changes: 6 additions & 3 deletions Swiftfin tvOS/Views/ItemView/ItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
26 changes: 15 additions & 11 deletions Swiftfin tvOS/Views/MediaView/MediaView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 4 additions & 5 deletions Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,10 @@ struct PagingLibraryView<Element: Poster & Identifiable>: 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
Expand Down
8 changes: 6 additions & 2 deletions Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ struct ProgramsView: View {
}

var body: some View {
WrappedView {
ZStack {
switch programsViewModel.state {
case .content:
if programsViewModel.hasNoResults {
Expand All @@ -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 {
Expand Down
Loading