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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
47 changes: 47 additions & 0 deletions Swiftfin tvOS/Components/ErrorView.swift
Original file line number Diff line number Diff line change
@@ -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<ErrorType: Error>: 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)
}
}
29 changes: 27 additions & 2 deletions Swiftfin tvOS/Components/ListRowButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -28,16 +44,23 @@ 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))
}
}
.buttonStyle(.card)
.frame(height: 75)
.frame(maxHeight: 75)
.focused($isFocused)
}

// MARK: - Styles
// MARK: - Primary Style

private var primaryStyle: some ShapeStyle {
if role == .destructive {
Expand All @@ -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))
Expand Down
76 changes: 76 additions & 0 deletions Swiftfin tvOS/Components/PrimaryButton.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
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 {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I brought this over from iOS. It doesn't seem to do anything and it's always disabled. It makes sense to pull this IMO but, if I'm pulling it from this PR I think it makes sense to pull this from iOS as well? Unless we have a usage for this we want to do in which case I think it should be on both platforms and we should TODO this so we know what we want to do later.

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
Loading