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] HomeView - Error Handling & Refreshing #1382

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9d65eb6
tvOS ErrorViews.
JPKribs Dec 28, 2024
f604a0c
Merge branch 'jellyfin:main' into tvOSHomeView
JPKribs Dec 29, 2024
15bd308
Merge branch 'jellyfin:main' into tvOSHomeView
JPKribs Jan 2, 2025
6a3e59d
Error Views
JPKribs Jan 2, 2025
fe3f97d
Remove all WrappedView?
JPKribs Jan 2, 2025
61b4dfd
Squashed commit of the following:
JPKribs Jan 3, 2025
0de28d5
Replace HomeErrorView with ErrorView
JPKribs Jan 3, 2025
3839226
Squashed commit of the following:
JPKribs Jan 3, 2025
e1ac439
Merge branch 'jellyfin:main' into tvOSHomeView
JPKribs Jan 3, 2025
78a934a
Delete Swiftfin tvOS/Views/HomeView/HomeErrorView.swift
JPKribs Jan 3, 2025
d27e853
Cinematic Views!
JPKribs Jan 3, 2025
a77d0fd
Merge remote-tracking branch 'refs/remotes/origin/tvOSHomeView'
JPKribs Jan 3, 2025
1739c38
Merge branch 'jellyfin:main' into tvOSHomeView
JPKribs Jan 3, 2025
75bf188
Cinematic must exist on top but only one. More visible highlighting o…
JPKribs Jan 4, 2025
1e13fa6
Merge remote-tracking branch 'refs/remotes/origin/tvOSHomeView'
JPKribs Jan 4, 2025
b60b586
Stateful is there for a reason and I should be using it.
JPKribs Jan 4, 2025
26de3be
Discussion might be needed.
JPKribs Jan 9, 2025
82e8731
Merge branch 'main' into tvOSHomeView
JPKribs Jan 9, 2025
31c5cbe
Rename `ResumeView` to `ContinueWatchingView` & add Play/Unplayed to …
JPKribs Jan 9, 2025
611bc50
Cleanup.
JPKribs Jan 10, 2025
9e1de9c
main width fixes. Still need to figure out the HomeViewModel stuff.
JPKribs Jan 10, 2025
536faf6
Skeleton Loading? I think?
JPKribs Jan 10, 2025
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
1 change: 1 addition & 0 deletions Shared/Services/SwiftfinDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ extension Defaults.Keys {
static let itemViewType: Key<ItemViewType> = UserKey("itemViewType", default: .compactLogo)

static let showPosterLabels: Key<Bool> = UserKey("showPosterLabels", default: true)
static let resumePosterType: Key<PosterDisplayType> = UserKey("resumePosterType", default: .landscape)
static let nextUpPosterType: Key<PosterDisplayType> = UserKey("nextUpPosterType", default: .portrait)
static let recentlyAddedPosterType: Key<PosterDisplayType> = UserKey("recentlyAddedPosterType", default: .portrait)
static let latestInLibraryPosterType: Key<PosterDisplayType> = UserKey("latestInLibraryPosterType", default: .portrait)
Expand Down
4 changes: 4 additions & 0 deletions Shared/Strings/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,8 @@ internal enum L10n {
}
/// No title
internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: "No title")
/// This Jellyfin server does not have any libraries that are active in Swiftfin. Swiftfin only supports Shows & Movies at this time.
internal static let noValidLibrariesError = L10n.tr("Localizable", "noValidLibrariesError", fallback: "This Jellyfin server does not have any libraries that are active in Swiftfin. Swiftfin only supports Shows & Movies at this time.")
/// Official Rating
internal static let officialRating = L10n.tr("Localizable", "officialRating", fallback: "Official Rating")
/// Offset
Expand Down Expand Up @@ -972,6 +974,8 @@ internal enum L10n {
internal static let red = L10n.tr("Localizable", "red", fallback: "Red")
/// The number of reference frames is not supported
internal static let refFramesNotSupported = L10n.tr("Localizable", "refFramesNotSupported", fallback: "The number of reference frames is not supported")
/// Refresh
internal static let refresh = L10n.tr("Localizable", "refresh", fallback: "Refresh")
/// Refresh Metadata
internal static let refreshMetadata = L10n.tr("Localizable", "refreshMetadata", fallback: "Refresh Metadata")
/// Regional
Expand Down
35 changes: 30 additions & 5 deletions Shared/ViewModels/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class HomeViewModel: ViewModel, Stateful {
case refresh
}

// MARK: BackgroundState
// MARK: Background State

enum BackgroundState: Hashable {
case refresh
Expand All @@ -39,29 +39,39 @@ final class HomeViewModel: ViewModel, Stateful {
case refreshing
}

// MARK: - Published Variables

@Published
private(set) var libraries: [LatestInLibraryViewModel] = []
@Published
var resumeItems: OrderedSet<BaseItemDto> = []

// MARK: - Stateful Variables

@Published
var backgroundStates: OrderedSet<BackgroundState> = []
@Published
var lastAction: Action? = nil
@Published
var state: State = .initial

private var backgroundRefreshTask: AnyCancellable?
private var refreshTask: AnyCancellable?

// MARK: - Notification Handler

// TODO: replace with views checking what notifications were
// posted since last disappear
@Published
var notificationsReceived: NotificationSet = .init()

private var backgroundRefreshTask: AnyCancellable?
private var refreshTask: AnyCancellable?
// MARK: - Child View Models

var nextUpViewModel: NextUpLibraryViewModel = .init()
var recentlyAddedViewModel: RecentlyAddedLibraryViewModel = .init()

// MARK: - Initializer

override init() {
super.init()

Expand All @@ -78,6 +88,8 @@ final class HomeViewModel: ViewModel, Stateful {
.store(in: &cancellables)
}

// MARK: - Respond to Action

func respond(to action: Action) -> State {
switch action {
case .backgroundRefresh:
Expand Down Expand Up @@ -114,9 +126,11 @@ final class HomeViewModel: ViewModel, Stateful {
.asAnyCancellable()

return state

case let .error(error):
return .error(error)
case let .setIsPlayed(isPlayed, item): ()

case let .setIsPlayed(isPlayed, item):
Task {
try await setIsPlayed(isPlayed, for: item)

Expand All @@ -125,6 +139,7 @@ final class HomeViewModel: ViewModel, Stateful {
.store(in: &cancellables)

return state

case .refresh:
backgroundRefreshTask?.cancel()
refreshTask?.cancel()
Expand Down Expand Up @@ -152,10 +167,12 @@ final class HomeViewModel: ViewModel, Stateful {
}
.asAnyCancellable()

return .refreshing
return state
}
}

// MARK: - Refresh

private func refresh() async throws {

await nextUpViewModel.send(.refresh)
Expand All @@ -174,6 +191,8 @@ final class HomeViewModel: ViewModel, Stateful {
}
}

// MARK: - Get Resume Items

private func getResumeItems() async throws -> [BaseItemDto] {
var parameters = Paths.GetResumeItemsParameters()
parameters.enableUserData = true
Expand All @@ -187,6 +206,8 @@ final class HomeViewModel: ViewModel, Stateful {
return response.value.items ?? []
}

// MARK: - Get Libraries

private func getLibraries() async throws -> [LatestInLibraryViewModel] {

let userViewsPath = Paths.getUserViews(userID: userSession.user.id)
Expand All @@ -200,6 +221,8 @@ final class HomeViewModel: ViewModel, Stateful {
.map { LatestInLibraryViewModel(parent: $0) }
}

// MARK: - Get Excluded Libraries

// TODO: use the more updated server/user data when implemented
private func getExcludedLibraries() async throws -> [String] {
let currentUserPath = Paths.getCurrentUser
Expand All @@ -208,6 +231,8 @@ final class HomeViewModel: ViewModel, Stateful {
return response.value.configuration?.latestItemsExcludes ?? []
}

// MARK: - Toggle Played Status

private func setIsPlayed(_ isPlayed: Bool, for item: BaseItemDto) async throws {
let request: Request<UserItemDataDto>

Expand Down
8 changes: 5 additions & 3 deletions Swiftfin tvOS/Components/CinematicItemSelector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct CinematicItemSelector<Item: Poster>: View {
private var onSelect: (Item) -> Void

let items: [Item]
let type: PosterDisplayType

var body: some View {
ZStack(alignment: .bottomLeading) {
Expand All @@ -46,7 +47,7 @@ struct CinematicItemSelector<Item: Poster>: View {
.transition(.opacity)
}

PosterHStack(type: .landscape, items: items)
PosterHStack(type: type, items: items)
.content(itemContent)
.imageOverlay(itemImageOverlay)
.contextMenu(itemContextMenu)
Expand Down Expand Up @@ -98,15 +99,16 @@ struct CinematicItemSelector<Item: Poster>: View {

extension CinematicItemSelector {

init(items: [Item]) {
init(items: [Item], type: PosterDisplayType) {
self.init(
topContent: { _ in EmptyView() },
itemContent: { _ in EmptyView() },
itemImageOverlay: { _ in EmptyView() },
itemContextMenu: { _ in EmptyView() },
trailingContent: { EmptyView() },
onSelect: { _ in },
items: items
items: items,
type: type
)
}
}
Expand Down
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: 30) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 100))
.foregroundColor(Color.red)

Text(error.localizedDescription)
.frame(maxWidth: 500)
.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)
}
}
2 changes: 1 addition & 1 deletion Swiftfin tvOS/Components/PosterHStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ struct PosterHStack<Element: Poster & Identifiable, Data: Collection>: View wher
.clipsToBounds(false)
.dataPrefix(20)
.insets(horizontal: EdgeInsets.edgePadding, vertical: 20)
.itemSpacing(EdgeInsets.edgePadding - 20)
.itemSpacing(EdgeInsets.edgePadding - (type == .landscape ? 15 : 20))
.scrollBehavior(.continuousLeadingEdge)
}
.focusSection()
Expand Down
56 changes: 56 additions & 0 deletions Swiftfin tvOS/Components/PrimaryButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// 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: - Accent Color

@Default(.accentColor)
private var accentColor

// MARK: - Focus State

@FocusState
private var isFocused

// MARK: - Primary Button Variables

private let title: String
private var onSelect: () -> Void

// MARK: - body

var body: some View {
ZStack {
ListRowButton(title) {
onSelect()
}
.focused($isFocused)
.foregroundStyle(accentColor.overlayColor, isFocused ? Color.jellyfinPurple : Color.gray)
}
.frame(maxWidth: 500)
.frame(height: 75)
}
}

extension PrimaryButton {

init(title: String) {
self.init(
title: title,
onSelect: {}
)
}

func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}
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,7 +49,10 @@ struct ChannelLibraryView: View {
contentView
}
case let .error(error):
Text(error.localizedDescription)
ErrorView(error: error)
.onRetry {
viewModel.send(.refresh)
}
case .initial, .refreshing:
ProgressView()
}
Expand Down

This file was deleted.

Loading