223 lines
6.2 KiB
Swift
223 lines
6.2 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 Defaults
|
|
import JellyfinAPI
|
|
import SwiftUI
|
|
|
|
struct ItemImagesView: View {
|
|
|
|
// MARK: - Defaults
|
|
|
|
@Default(.accentColor)
|
|
private var accentColor
|
|
|
|
// MARK: - Observed & Environment Objects
|
|
|
|
@EnvironmentObject
|
|
private var router: ItemImagesCoordinator.Router
|
|
|
|
@StateObject
|
|
var viewModel: ItemImagesViewModel
|
|
|
|
// MARK: - Dialog State
|
|
|
|
@State
|
|
private var selectedImage: ImageInfo?
|
|
@State
|
|
private var selectedType: ImageType?
|
|
@State
|
|
private var isFilePickerPresented = false
|
|
|
|
// MARK: - Error State
|
|
|
|
@State
|
|
private var error: Error?
|
|
|
|
// MARK: - Body
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
switch viewModel.state {
|
|
case .content:
|
|
imageView
|
|
case .initial:
|
|
DelayedProgressView()
|
|
case let .error(error):
|
|
ErrorView(error: error)
|
|
.onRetry {
|
|
viewModel.send(.refresh)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(L10n.images)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.onFirstAppear {
|
|
viewModel.send(.refresh)
|
|
}
|
|
.navigationBarCloseButton {
|
|
router.dismissCoordinator()
|
|
}
|
|
.sheet(item: $selectedImage) {
|
|
selectedImage = nil
|
|
} content: { imageInfo in
|
|
ItemImageDetailsView(
|
|
viewModel: viewModel,
|
|
imageSource: imageInfo.itemImageSource(
|
|
itemID: viewModel.item.id!,
|
|
client: viewModel.userSession.client
|
|
),
|
|
index: imageInfo.imageIndex,
|
|
width: imageInfo.width,
|
|
height: imageInfo.height,
|
|
onClose: {
|
|
selectedImage = nil
|
|
},
|
|
onDelete: {
|
|
viewModel.send(.deleteImage(imageInfo))
|
|
selectedImage = nil
|
|
}
|
|
)
|
|
.environment(\.isEditing, true)
|
|
}
|
|
.fileImporter(
|
|
isPresented: $isFilePickerPresented,
|
|
allowedContentTypes: [.png, .jpeg, .heic],
|
|
allowsMultipleSelection: false
|
|
) {
|
|
switch $0 {
|
|
case let .success(urls):
|
|
if let file = urls.first, let type = selectedType {
|
|
viewModel.send(.uploadFile(file: file, type: type))
|
|
selectedType = nil
|
|
}
|
|
case let .failure(fileError):
|
|
error = fileError
|
|
selectedType = nil
|
|
}
|
|
}
|
|
.onReceive(viewModel.events) { event in
|
|
switch event {
|
|
case .updated: ()
|
|
case let .error(eventError):
|
|
self.error = eventError
|
|
}
|
|
}
|
|
.errorMessage($error)
|
|
}
|
|
|
|
// MARK: - Image View
|
|
|
|
@ViewBuilder
|
|
private var imageView: some View {
|
|
ScrollView {
|
|
ForEach(ImageType.allCases.sorted(using: \.rawValue), id: \.self) { imageType in
|
|
Section {
|
|
imageScrollView(for: imageType)
|
|
|
|
RowDivider()
|
|
.padding(.vertical, 16)
|
|
} header: {
|
|
sectionHeader(for: imageType)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Image Scroll View
|
|
|
|
@ViewBuilder
|
|
private func imageScrollView(for imageType: ImageType) -> some View {
|
|
let images = viewModel.images[imageType] ?? []
|
|
|
|
if images.isNotEmpty {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack {
|
|
ForEach(images, id: \.self) { imageInfo in
|
|
imageButton(imageInfo: imageInfo) {
|
|
selectedImage = imageInfo
|
|
}
|
|
}
|
|
}
|
|
.edgePadding(.horizontal)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Section Header
|
|
|
|
@ViewBuilder
|
|
private func sectionHeader(for imageType: ImageType) -> some View {
|
|
HStack {
|
|
Text(imageType.displayTitle)
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
Menu(L10n.options, systemImage: "plus") {
|
|
Button(L10n.search, systemImage: "magnifyingglass") {
|
|
router.route(
|
|
to: \.addImage,
|
|
imageType
|
|
)
|
|
}
|
|
|
|
Divider()
|
|
|
|
Button(L10n.uploadFile, systemImage: "document.badge.plus") {
|
|
selectedType = imageType
|
|
isFilePickerPresented = true
|
|
}
|
|
|
|
Button(L10n.uploadPhoto, systemImage: "photo.badge.plus") {
|
|
router.route(to: \.photoPicker, imageType)
|
|
}
|
|
}
|
|
.font(.body)
|
|
.labelStyle(.iconOnly)
|
|
.backport
|
|
.fontWeight(.semibold)
|
|
.foregroundStyle(accentColor)
|
|
}
|
|
.edgePadding(.horizontal)
|
|
}
|
|
|
|
// MARK: - Image Button
|
|
|
|
// TODO: instead of using `posterStyle`, should be sized based on
|
|
// the image type and just ignore and poster styling
|
|
@ViewBuilder
|
|
private func imageButton(
|
|
imageInfo: ImageInfo,
|
|
onSelect: @escaping () -> Void
|
|
) -> some View {
|
|
Button(action: onSelect) {
|
|
ZStack {
|
|
Color.secondarySystemFill
|
|
|
|
ImageView(
|
|
imageInfo.itemImageSource(
|
|
itemID: viewModel.item.id!,
|
|
client: viewModel.userSession.client
|
|
)
|
|
)
|
|
.placeholder { _ in
|
|
Image(systemName: "photo")
|
|
}
|
|
.failure {
|
|
Image(systemName: "photo")
|
|
}
|
|
.pipeline(.Swiftfin.other)
|
|
}
|
|
.posterStyle(imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape)
|
|
.frame(maxHeight: 150)
|
|
.posterShadow()
|
|
}
|
|
}
|
|
}
|