Fix `MediaView` Items (#1023)
This commit is contained in:
parent
4ac0547be8
commit
913dda5fea
|
@ -25,19 +25,20 @@ private let imagePipeline = {
|
|||
// - instead of removing first source on failure, just safe index into sources
|
||||
// TODO: currently SVGs are only supported for logos, which are only used in a few places.
|
||||
// make it so when displaying an SVG there is a unified `image` caller modifier
|
||||
// TODO: probably don't need both `placeholder` modifiers
|
||||
struct ImageView: View {
|
||||
|
||||
@State
|
||||
private var sources: [ImageSource]
|
||||
|
||||
private var image: (Image) -> any View
|
||||
private var placeholder: (() -> any View)?
|
||||
private var placeholder: ((ImageSource) -> any View)?
|
||||
private var failure: () -> any View
|
||||
|
||||
@ViewBuilder
|
||||
private func _placeholder(_ currentSource: ImageSource) -> some View {
|
||||
if let placeholder = placeholder {
|
||||
placeholder()
|
||||
placeholder(currentSource)
|
||||
.eraseToAnyView()
|
||||
} else {
|
||||
DefaultPlaceholderView(blurHash: currentSource.blurHash)
|
||||
|
@ -124,6 +125,10 @@ extension ImageView {
|
|||
}
|
||||
|
||||
func placeholder(@ViewBuilder _ content: @escaping () -> any View) -> Self {
|
||||
copy(modifying: \.placeholder, with: { _ in content() })
|
||||
}
|
||||
|
||||
func placeholder(@ViewBuilder _ content: @escaping (ImageSource) -> any View) -> Self {
|
||||
copy(modifying: \.placeholder, with: content)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,71 +6,9 @@
|
|||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import CollectionVGrid
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct MediaView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var mainRouter: MainCoordinator.Router
|
||||
@EnvironmentObject
|
||||
private var router: MediaCoordinator.Router
|
||||
|
||||
@StateObject
|
||||
private var viewModel = MediaViewModel()
|
||||
|
||||
private var contentView: some View {
|
||||
CollectionVGrid(
|
||||
$viewModel.mediaItems,
|
||||
layout: .columns(4, insets: .init(50), itemSpacing: 50, lineSpacing: 50)
|
||||
) { mediaType in
|
||||
MediaItem(viewModel: viewModel, type: mediaType)
|
||||
.onSelect {
|
||||
switch mediaType {
|
||||
case let .collectionFolder(item):
|
||||
let viewModel = ItemLibraryViewModel(
|
||||
parent: item,
|
||||
filters: .default
|
||||
)
|
||||
router.route(to: \.library, viewModel)
|
||||
case .downloads: ()
|
||||
case .favorites:
|
||||
let viewModel = ItemLibraryViewModel(
|
||||
title: L10n.favorites,
|
||||
filters: .favorites
|
||||
)
|
||||
router.route(to: \.library, viewModel)
|
||||
case .liveTV:
|
||||
mainRouter.root(\.liveTV)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
WrappedView {
|
||||
Group {
|
||||
switch viewModel.state {
|
||||
case .content:
|
||||
contentView
|
||||
case let .error(error):
|
||||
Text(error.localizedDescription)
|
||||
case .initial, .refreshing:
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.transition(.opacity.animation(.linear(duration: 0.2)))
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onFirstAppear {
|
||||
viewModel.send(.refresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MediaView {
|
||||
|
||||
// TODO: custom view for folders and tv (allow customization?)
|
||||
|
@ -94,6 +32,12 @@ extension MediaView {
|
|||
self.mediaType = type
|
||||
}
|
||||
|
||||
private var useTitleLabel: Bool {
|
||||
useRandomImage ||
|
||||
mediaType == .downloads ||
|
||||
mediaType == .favorites
|
||||
}
|
||||
|
||||
private func setImageSources() {
|
||||
Task { @MainActor in
|
||||
if useRandomImage {
|
||||
|
@ -118,6 +62,18 @@ extension MediaView {
|
|||
.frame(alignment: .center)
|
||||
}
|
||||
|
||||
private func titleLabelOverlay<Content: View>(with content: Content) -> some View {
|
||||
ZStack {
|
||||
content
|
||||
|
||||
Color.black
|
||||
.opacity(0.5)
|
||||
|
||||
titleLabel
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
onSelect()
|
||||
|
@ -127,23 +83,15 @@ extension MediaView {
|
|||
|
||||
ImageView(imageSources)
|
||||
.image { image in
|
||||
if useRandomImage ||
|
||||
mediaType == .downloads ||
|
||||
mediaType == .favorites
|
||||
{
|
||||
ZStack {
|
||||
image
|
||||
|
||||
Color.black
|
||||
.opacity(0.5)
|
||||
|
||||
titleLabel
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
if useTitleLabel {
|
||||
titleLabelOverlay(with: image)
|
||||
} else {
|
||||
image
|
||||
}
|
||||
}
|
||||
.placeholder { imageSource in
|
||||
titleLabelOverlay(with: ImageView.DefaultPlaceholderView(blurHash: imageSource.blurHash))
|
||||
}
|
||||
.failure {
|
||||
ImageView.DefaultFailureView()
|
||||
.overlay {
|
|
@ -0,0 +1,72 @@
|
|||
//
|
||||
// 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 JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct MediaView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var mainRouter: MainCoordinator.Router
|
||||
@EnvironmentObject
|
||||
private var router: MediaCoordinator.Router
|
||||
|
||||
@StateObject
|
||||
private var viewModel = MediaViewModel()
|
||||
|
||||
private var contentView: some View {
|
||||
CollectionVGrid(
|
||||
$viewModel.mediaItems,
|
||||
layout: .columns(4, insets: .init(50), itemSpacing: 50, lineSpacing: 50)
|
||||
) { mediaType in
|
||||
MediaItem(viewModel: viewModel, type: mediaType)
|
||||
.onSelect {
|
||||
switch mediaType {
|
||||
case let .collectionFolder(item):
|
||||
let viewModel = ItemLibraryViewModel(
|
||||
parent: item,
|
||||
filters: .default
|
||||
)
|
||||
router.route(to: \.library, viewModel)
|
||||
case .downloads: ()
|
||||
case .favorites:
|
||||
let viewModel = ItemLibraryViewModel(
|
||||
title: L10n.favorites,
|
||||
filters: .favorites
|
||||
)
|
||||
router.route(to: \.library, viewModel)
|
||||
case .liveTV:
|
||||
mainRouter.root(\.liveTV)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
WrappedView {
|
||||
Group {
|
||||
switch viewModel.state {
|
||||
case .content:
|
||||
contentView
|
||||
case let .error(error):
|
||||
Text(error.localizedDescription)
|
||||
case .initial, .refreshing:
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.transition(.opacity.animation(.linear(duration: 0.2)))
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onFirstAppear {
|
||||
viewModel.send(.refresh)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -195,6 +195,8 @@
|
|||
E1002B652793CEE800E47059 /* ChapterInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfo.swift */; };
|
||||
E1002B682793CFBA00E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B672793CFBA00E47059 /* Algorithms */; };
|
||||
E1002B6B2793E36600E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B6A2793E36600E47059 /* Algorithms */; };
|
||||
E103DF902BCF2F1C000229B2 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103DF8F2BCF2F1C000229B2 /* MediaItem.swift */; };
|
||||
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103DF942BCF31CD000229B2 /* MediaItem.swift */; };
|
||||
E1047E2327E5880000CB0D4A /* SystemImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */; };
|
||||
E104C870296E087200C1C3F9 /* IndicatorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104C86F296E087200C1C3F9 /* IndicatorSettingsView.swift */; };
|
||||
E104C873296E0D0A00C1C3F9 /* IndicatorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104C872296E0D0A00C1C3F9 /* IndicatorSettingsView.swift */; };
|
||||
|
@ -986,6 +988,8 @@
|
|||
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
|
||||
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
|
||||
E1002B632793CEE700E47059 /* ChapterInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfo.swift; sourceTree = "<group>"; };
|
||||
E103DF8F2BCF2F1C000229B2 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = "<group>"; };
|
||||
E103DF942BCF31CD000229B2 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = "<group>"; };
|
||||
E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemImageContentView.swift; sourceTree = "<group>"; };
|
||||
E104C86F296E087200C1C3F9 /* IndicatorSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndicatorSettingsView.swift; sourceTree = "<group>"; };
|
||||
E104C872296E0D0A00C1C3F9 /* IndicatorSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndicatorSettingsView.swift; sourceTree = "<group>"; };
|
||||
|
@ -2052,6 +2056,32 @@
|
|||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E103DF912BCF2F1F000229B2 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E103DF8F2BCF2F1C000229B2 /* MediaItem.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E103DF922BCF2F23000229B2 /* MediaView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E103DF912BCF2F1F000229B2 /* Components */,
|
||||
6213388F265F83A900A81A2A /* MediaView.swift */,
|
||||
);
|
||||
path = MediaView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E103DF932BCF31C5000229B2 /* MediaView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4E508172703E8190045C9AB /* MediaView.swift */,
|
||||
E103DF942BCF31CD000229B2 /* MediaItem.swift */,
|
||||
);
|
||||
path = MediaView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E107BB9127880A4000354E07 /* ItemViewModel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2207,7 +2237,7 @@
|
|||
C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */,
|
||||
C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */,
|
||||
E158C8D02A31947500C527C5 /* MediaSourceInfoView.swift */,
|
||||
C4E508172703E8190045C9AB /* MediaView.swift */,
|
||||
E103DF932BCF31C5000229B2 /* MediaView */,
|
||||
E1E1643928BAC2EF00323B0A /* SearchView.swift */,
|
||||
E193D54F2719430400900D82 /* ServerDetailView.swift */,
|
||||
E193D54A271941D300900D82 /* ServerListView.swift */,
|
||||
|
@ -2279,7 +2309,7 @@
|
|||
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
|
||||
E170D104294D21FA0017224C /* MediaSourceInfoView.swift */,
|
||||
E19F6C5C28F5189300C5197E /* MediaStreamInfoView.swift */,
|
||||
6213388F265F83A900A81A2A /* MediaView.swift */,
|
||||
E103DF922BCF2F23000229B2 /* MediaView */,
|
||||
E1EDA8D62B92C9D700F9A57E /* PagingLibraryView */,
|
||||
E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */,
|
||||
53EE24E5265060780068F029 /* SearchView.swift */,
|
||||
|
@ -3388,7 +3418,6 @@
|
|||
E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */,
|
||||
C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */,
|
||||
C46DD8EA2A8FB45C0046A504 /* LiveOverlay.swift in Sources */,
|
||||
E1575E96293E7B1E001665B1 /* UIScrollView.swift in Sources */,
|
||||
E11E376D293E9CC1009EF240 /* VideoPlayerCoordinator.swift in Sources */,
|
||||
E1575E6F293E77B5001665B1 /* GestureAction.swift in Sources */,
|
||||
E18A8E7E28D606BE00333B9A /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
||||
|
@ -3475,6 +3504,7 @@
|
|||
E1EF473A289A0F610034046B /* TruncatedText.swift in Sources */,
|
||||
E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */,
|
||||
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */,
|
||||
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */,
|
||||
E18E02252887492B0022598C /* PlainNavigationLinkButton.swift in Sources */,
|
||||
E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
||||
62E632E1267D30CA0063E547 /* ItemLibraryViewModel.swift in Sources */,
|
||||
|
@ -3750,7 +3780,6 @@
|
|||
E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */,
|
||||
C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */,
|
||||
E133328829538D8D00EE76AB /* Files.swift in Sources */,
|
||||
E1D3043228D175CE00587289 /* StaticLibraryViewModel.swift in Sources */,
|
||||
C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */,
|
||||
E1401CA02937DFF500E8B599 /* AppIconSelectorView.swift in Sources */,
|
||||
E1092F4C29106F9F00163F57 /* GestureAction.swift in Sources */,
|
||||
|
@ -3849,7 +3878,6 @@
|
|||
E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */,
|
||||
E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */,
|
||||
C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */,
|
||||
E1D3043A28D189C500587289 /* CastAndCrewLibraryView.swift in Sources */,
|
||||
E18E01AD288746AF0022598C /* DotHStack.swift in Sources */,
|
||||
E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */,
|
||||
E1937A61288F32DB00CB80AA /* Poster.swift in Sources */,
|
||||
|
@ -3989,7 +4017,6 @@
|
|||
E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */,
|
||||
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */,
|
||||
E13DD3C827164B1E009D4DAF /* UIDevice.swift in Sources */,
|
||||
E1D3044228D1976600587289 /* CastAndCrewItemRow.swift in Sources */,
|
||||
C46DD8DD2A8DC3420046A504 /* LiveNativeVideoPlayer.swift in Sources */,
|
||||
E1AD104D26D96CE3003E4A08 /* BaseItemDto.swift in Sources */,
|
||||
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
||||
|
@ -4021,6 +4048,7 @@
|
|||
E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.swift in Sources */,
|
||||
E133328F2953B71000EE76AB /* DownloadTaskView.swift in Sources */,
|
||||
E1E6C44029AECC6D0064123F /* ActionButtons.swift in Sources */,
|
||||
E103DF902BCF2F1C000229B2 /* MediaItem.swift in Sources */,
|
||||
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
|
||||
E1E2F83F2B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */,
|
||||
E15D4F072B1B12C300442DB8 /* Backport.swift in Sources */,
|
||||
|
|
|
@ -6,96 +6,11 @@
|
|||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import CollectionVGrid
|
||||
import Defaults
|
||||
import Factory
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
// TODO: seems to redraw view when popped to sometimes?
|
||||
// - similar to HomeView TODO bug?
|
||||
// TODO: list view
|
||||
// TODO: `afterLastDisappear` with `backgroundRefresh`
|
||||
struct MediaView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: MediaCoordinator.Router
|
||||
|
||||
@StateObject
|
||||
private var viewModel = MediaViewModel()
|
||||
|
||||
private var padLayout: CollectionVGridLayout {
|
||||
.minWidth(200)
|
||||
}
|
||||
|
||||
private var phoneLayout: CollectionVGridLayout {
|
||||
.columns(2)
|
||||
}
|
||||
|
||||
private var contentView: some View {
|
||||
CollectionVGrid(
|
||||
$viewModel.mediaItems,
|
||||
layout: UIDevice.isPhone ? phoneLayout : padLayout
|
||||
) { mediaType in
|
||||
MediaItem(viewModel: viewModel, type: mediaType)
|
||||
.onSelect {
|
||||
switch mediaType {
|
||||
case let .collectionFolder(item):
|
||||
let viewModel = ItemLibraryViewModel(
|
||||
parent: item,
|
||||
filters: .default
|
||||
)
|
||||
router.route(to: \.library, viewModel)
|
||||
case .downloads:
|
||||
router.route(to: \.downloads)
|
||||
case .favorites:
|
||||
let viewModel = ItemLibraryViewModel(
|
||||
title: L10n.favorites,
|
||||
filters: .favorites
|
||||
)
|
||||
router.route(to: \.library, viewModel)
|
||||
case .liveTV:
|
||||
router.route(to: \.liveTV)
|
||||
}
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
viewModel.send(.refresh)
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
contentView
|
||||
case let .error(error):
|
||||
errorView(with: error)
|
||||
case .initial, .refreshing:
|
||||
DelayedProgressView()
|
||||
}
|
||||
}
|
||||
.transition(.opacity.animation(.linear(duration: 0.2)))
|
||||
.ignoresSafeArea()
|
||||
.navigationTitle(L10n.allMedia)
|
||||
.topBarTrailing {
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.onFirstAppear {
|
||||
viewModel.send(.refresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: the design reason to not have a local label always on top
|
||||
// is to have the same failure/empty color for all views
|
||||
|
||||
extension MediaView {
|
||||
|
||||
|
@ -124,6 +39,12 @@ extension MediaView {
|
|||
self.mediaType = type
|
||||
}
|
||||
|
||||
private var useTitleLabel: Bool {
|
||||
useRandomImage ||
|
||||
mediaType == .downloads ||
|
||||
mediaType == .favorites
|
||||
}
|
||||
|
||||
private func setImageSources() {
|
||||
Task { @MainActor in
|
||||
if useRandomImage {
|
||||
|
@ -148,6 +69,19 @@ extension MediaView {
|
|||
.frame(alignment: .center)
|
||||
}
|
||||
|
||||
// TODO: find a different way to do this local-label-wackiness if possible
|
||||
private func titleLabelOverlay<Content: View>(with content: Content) -> some View {
|
||||
ZStack {
|
||||
content
|
||||
|
||||
Color.black
|
||||
.opacity(0.5)
|
||||
|
||||
titleLabel
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
onSelect()
|
||||
|
@ -157,23 +91,15 @@ extension MediaView {
|
|||
|
||||
ImageView(imageSources)
|
||||
.image { image in
|
||||
if useRandomImage ||
|
||||
mediaType == .downloads ||
|
||||
mediaType == .favorites
|
||||
{
|
||||
ZStack {
|
||||
image
|
||||
|
||||
Color.black
|
||||
.opacity(0.5)
|
||||
|
||||
titleLabel
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
if useTitleLabel {
|
||||
titleLabelOverlay(with: image)
|
||||
} else {
|
||||
image
|
||||
}
|
||||
}
|
||||
.placeholder { imageSource in
|
||||
titleLabelOverlay(with: ImageView.DefaultPlaceholderView(blurHash: imageSource.blurHash))
|
||||
}
|
||||
.failure {
|
||||
ImageView.DefaultFailureView()
|
||||
.overlay {
|
|
@ -0,0 +1,98 @@
|
|||
//
|
||||
// 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 Factory
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
// TODO: seems to redraw view when popped to sometimes?
|
||||
// - similar to HomeView TODO bug?
|
||||
// TODO: list view
|
||||
// TODO: `afterLastDisappear` with `backgroundRefresh`
|
||||
struct MediaView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: MediaCoordinator.Router
|
||||
|
||||
@StateObject
|
||||
private var viewModel = MediaViewModel()
|
||||
|
||||
private var padLayout: CollectionVGridLayout {
|
||||
.minWidth(200)
|
||||
}
|
||||
|
||||
private var phoneLayout: CollectionVGridLayout {
|
||||
.columns(2)
|
||||
}
|
||||
|
||||
private var contentView: some View {
|
||||
CollectionVGrid(
|
||||
$viewModel.mediaItems,
|
||||
layout: UIDevice.isPhone ? phoneLayout : padLayout
|
||||
) { mediaType in
|
||||
MediaItem(viewModel: viewModel, type: mediaType)
|
||||
.onSelect {
|
||||
switch mediaType {
|
||||
case let .collectionFolder(item):
|
||||
let viewModel = ItemLibraryViewModel(
|
||||
parent: item,
|
||||
filters: .default
|
||||
)
|
||||
router.route(to: \.library, viewModel)
|
||||
case .downloads:
|
||||
router.route(to: \.downloads)
|
||||
case .favorites:
|
||||
let viewModel = ItemLibraryViewModel(
|
||||
title: L10n.favorites,
|
||||
filters: .favorites
|
||||
)
|
||||
router.route(to: \.library, viewModel)
|
||||
case .liveTV:
|
||||
router.route(to: \.liveTV)
|
||||
}
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
viewModel.send(.refresh)
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
contentView
|
||||
case let .error(error):
|
||||
errorView(with: error)
|
||||
case .initial, .refreshing:
|
||||
DelayedProgressView()
|
||||
}
|
||||
}
|
||||
.transition(.opacity.animation(.linear(duration: 0.2)))
|
||||
.ignoresSafeArea()
|
||||
.navigationTitle(L10n.allMedia)
|
||||
.topBarTrailing {
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.onFirstAppear {
|
||||
viewModel.send(.refresh)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue