jellyflood/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift

183 lines
5.4 KiB
Swift

//
// 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 CollectionVGrid
import Defaults
import Foundation
import JellyfinAPI
import SwiftUI
// TODO: sorting by number/filtering
// - see if can use normal filter view model?
// - how to add custom filters for data context?
// TODO: saving item display type/detailed column count
// - wait until after user refactor
// Note: Repurposes `LibraryDisplayType` to save from creating a new type.
// If there are other places where detailed/compact contextually differ
// from the library types, then create a new type and use it here.
// - list: detailed
// - grid: compact
struct ChannelLibraryView: View {
@EnvironmentObject
private var mainRouter: MainCoordinator.Router
@State
private var channelDisplayType: LibraryDisplayType = .list
@State
private var layout: CollectionVGridLayout
@StateObject
private var viewModel = ChannelLibraryViewModel()
// MARK: init
init() {
if UIDevice.isPhone {
layout = Self.padlayout(channelDisplayType: .list)
} else {
layout = Self.phonelayout(channelDisplayType: .list)
}
}
// MARK: layout
private static func padlayout(
channelDisplayType: LibraryDisplayType
) -> CollectionVGridLayout {
switch channelDisplayType {
case .grid:
.minWidth(150)
case .list:
.minWidth(250)
}
}
private static func phonelayout(
channelDisplayType: LibraryDisplayType
) -> CollectionVGridLayout {
switch channelDisplayType {
case .grid:
.columns(3)
case .list:
.columns(1)
}
}
// MARK: item view
private func compactChannelView(channel: ChannelProgram) -> some View {
CompactChannelView(channel: channel.channel)
.onSelect {
guard let mediaSource = channel.channel.mediaSources?.first else { return }
mainRouter.route(
to: \.liveVideoPlayer,
LiveVideoPlayerManager(item: channel.channel, mediaSource: mediaSource)
)
}
}
private func detailedChannelView(channel: ChannelProgram) -> some View {
DetailedChannelView(channel: channel)
.onSelect {
guard let mediaSource = channel.channel.mediaSources?.first else { return }
mainRouter.route(
to: \.liveVideoPlayer,
LiveVideoPlayerManager(item: channel.channel, mediaSource: mediaSource)
)
}
}
@ViewBuilder
private var contentView: some View {
CollectionVGrid(
uniqueElements: viewModel.elements,
layout: layout
) { channel in
switch channelDisplayType {
case .grid:
compactChannelView(channel: channel)
case .list:
detailedChannelView(channel: channel)
}
}
.onReachedBottomEdge(offset: .offset(300)) {
viewModel.send(.getNextPage)
}
}
private func errorView(with error: some Error) -> some View {
ErrorView(error: error)
.onRetry {
viewModel.send(.refresh)
}
}
var body: some View {
WrappedView {
switch viewModel.state {
case .content:
if viewModel.elements.isEmpty {
L10n.noResults.text
} else {
contentView
}
case let .error(error):
errorView(with: error)
case .initial, .refreshing:
DelayedProgressView()
}
}
.navigationTitle(L10n.channels)
.navigationBarTitleDisplayMode(.inline)
.onChange(of: channelDisplayType) { newValue in
if UIDevice.isPhone {
layout = Self.phonelayout(channelDisplayType: newValue)
} else {
layout = Self.padlayout(channelDisplayType: newValue)
}
}
.onFirstAppear {
if viewModel.state == .initial {
viewModel.send(.refresh)
}
}
.sinceLastDisappear { interval in
// refresh after 3 hours
if interval >= 10800 {
viewModel.send(.refresh)
}
}
.topBarTrailing {
if viewModel.backgroundStates.contains(.gettingNextPage) {
ProgressView()
}
Menu {
// We repurposed `LibraryDisplayType` but want different labels
Picker("Channel Display", selection: $channelDisplayType) {
Label(L10n.compact, systemImage: LibraryDisplayType.grid.systemImage)
.tag(LibraryDisplayType.grid)
Label("Detailed", systemImage: LibraryDisplayType.list.systemImage)
.tag(LibraryDisplayType.list)
}
} label: {
Label(
channelDisplayType.displayTitle,
systemImage: channelDisplayType.systemImage
)
}
}
}
}