jellyflood/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift

215 lines
6.6 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) 2025 Jellyfin & Jellyfin Contributors
//
import BlurHashKit
import CollectionVGrid
import JellyfinAPI
import SwiftUI
// TODO: different layouts per image type
// - also based on iOS vs iPadOS
struct AddItemImageView: View {
// MARK: - Observed, & Environment Objects
@EnvironmentObject
private var router: ItemImagesCoordinator.Router
@ObservedObject
private var viewModel: ItemImagesViewModel
@StateObject
private var remoteImageInfoViewModel: RemoteImageInfoViewModel
// MARK: - Dialog State
@State
private var selectedImage: RemoteImageInfo?
@State
private var error: Error?
// MARK: - Collection Layout
@State
private var layout: CollectionVGridLayout = .minWidth(150)
// MARK: - Initializer
init(viewModel: ItemImagesViewModel, imageType: ImageType) {
self.viewModel = viewModel
self._remoteImageInfoViewModel = StateObject(
wrappedValue: RemoteImageInfoViewModel(
imageType: imageType,
parent: viewModel.item
)
)
}
// MARK: - Body
var body: some View {
ZStack {
switch remoteImageInfoViewModel.state {
case .initial, .refreshing:
DelayedProgressView()
case .content:
gridView
case let .error(error):
ErrorView(error: error)
.onRetry {
viewModel.send(.refresh)
}
}
}
.animation(.linear(duration: 0.1), value: remoteImageInfoViewModel.state)
.navigationTitle(remoteImageInfoViewModel.imageType.displayTitle)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(viewModel.backgroundStates.contains(.updating))
.navigationBarMenuButton(isLoading: viewModel.backgroundStates.contains(.updating)) {
Button {
remoteImageInfoViewModel.includeAllLanguages.toggle()
} label: {
if remoteImageInfoViewModel.includeAllLanguages {
Label(L10n.allLanguages, systemImage: "checkmark")
} else {
Text(L10n.allLanguages)
}
}
if remoteImageInfoViewModel.providers.isNotEmpty {
Menu {
Button {
remoteImageInfoViewModel.provider = nil
} label: {
if remoteImageInfoViewModel.provider == nil {
Label(L10n.all, systemImage: "checkmark")
} else {
Text(L10n.all)
}
}
ForEach(remoteImageInfoViewModel.providers, id: \.self) { provider in
Button {
remoteImageInfoViewModel.provider = provider
} label: {
if remoteImageInfoViewModel.provider == provider {
Label(provider, systemImage: "checkmark")
} else {
Text(provider)
}
}
}
} label: {
Text(L10n.provider)
Text(remoteImageInfoViewModel.provider ?? L10n.all)
}
}
}
.sheet(item: $selectedImage) {
selectedImage = nil
} content: { remoteImageInfo in
ItemImageDetailsView(
viewModel: viewModel,
imageSource: ImageSource(url: remoteImageInfo.url?.url),
width: remoteImageInfo.width,
height: remoteImageInfo.height,
language: remoteImageInfo.language,
provider: remoteImageInfo.providerName,
rating: remoteImageInfo.communityRating,
ratingVotes: remoteImageInfo.voteCount,
onClose: {
selectedImage = nil
},
onSave: {
viewModel.send(.setImage(remoteImageInfo))
selectedImage = nil
}
)
}
.onFirstAppear {
remoteImageInfoViewModel.send(.refresh)
}
.onReceive(viewModel.events) { event in
switch event {
case .updated:
UIDevice.feedback(.success)
router.pop()
case let .error(eventError):
UIDevice.feedback(.error)
error = eventError
}
}
.errorMessage($error)
}
// MARK: - Content Grid View
@ViewBuilder
private var gridView: some View {
if remoteImageInfoViewModel.elements.isEmpty {
Text(L10n.none)
} else {
CollectionVGrid(
uniqueElements: remoteImageInfoViewModel.elements,
layout: layout
) { image in
imageButton(image)
}
.onReachedBottomEdge(offset: .offset(300)) {
remoteImageInfoViewModel.send(.getNextPage)
}
}
}
// MARK: - Poster Image Button
@ViewBuilder
private func imageButton(_ image: RemoteImageInfo) -> some View {
Button {
selectedImage = image
} label: {
posterImage(
image,
posterStyle: (image.height ?? 0) > (image.width ?? 0) ? .portrait : .landscape
)
}
}
// MARK: - Poster Image
@ViewBuilder
private func posterImage(
_ posterImageInfo: RemoteImageInfo?,
posterStyle: PosterDisplayType
) -> some View {
ZStack {
Color.secondarySystemFill
.frame(maxWidth: .infinity, maxHeight: .infinity)
ImageView(posterImageInfo?.url?.url)
.placeholder { source in
if let blurHash = source.blurHash {
BlurHashView(blurHash: blurHash)
.scaledToFit()
} else {
Image(systemName: "photo")
}
}
.failure {
Image(systemName: "photo")
}
.pipeline(.Swiftfin.other)
.foregroundStyle(.secondary)
.font(.headline)
}
.posterStyle(posterStyle)
}
}