jellyflood/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift
Joe Kribs 553441d83e
[iOS] Media Item Menu - Edit Item Images (#1345)
* Good start but some missing items:

- Upload image isn't working
- Only a single image is shown per section. Need to make this the HCollection of all images for the group

* Upload still failing but now update and set are 2 different processes because I think that's better. Spacing on the add screen is still all wrong but we're getting closer

* ~70% Complete

TODO:

- Spacing for remote portrait images is wrong & cramped
- Upload image from file browser never works & produces 400 error
- Show all images for an item.imageType opposed to just the first
- Setting image works but produces a 400 error
- Error alert looks bad

* Merge with Main

* URL Changes

* Updating logic and confirmation screen

* Lots of changes:

Selecting a Remote image is now working without error and works consistently!

Upload a local file is still broken

Item types with multiple images is working as intended now!

Overriding an image on index doesn't seem to work but it doesn't work for Web either so........

UI is way more jank but the hard parts are getting solved!

* Breaking this even more with the hopes of a better tomorrow.

* Getting better?

* Refreshing is working but I might need to make this work mroe effiently...

* 90% There!

* Ability to cancel the update

* Still no luck uploading images?

* Stop reordering on deletion/addition

* 2025 disclaimers

* Uploading finally works!

* Functional but messy.

TODO:
- Figure out better resizing if too big?
- Upload from Photos
- Move upload logic to imageViewModel and make RemtoeImageViewModel PagingLibraryViewModel conformant
- Create a ImageInfoView for Selection & Deletion.

* Now conforms to PagingLIbraryViewModel but everything else is a mess

* Close!

* First no all appears

* Fix double pop/routerdismiss

* Uploading from Photos is (Finally) Ready!

* wip

* Reuse PhotoPicker and Crop code.

* 4/6 of the codefactor changes

* Pass around the URL NOT the UIImage

* Clean up ItemImageDetails types.

* Make sure the ImageView mirrors the real shape of the image. Posters should be uniform but this is the selection for the image so the dimensions are important to demonstrate.

* Rating Type label.

* Delete confirmation dialog.

* Remove double sizing. Remove Unused ViewModel. Change PhotoPicker to a checkmark instead a 1. Since there is only ever one picture selected, no need to count the images.

* Get the image URL as needed. No more Truples.  Localize ImageTypes.

* Remove attempt at ImageInfo Poster Comformance.

* Even more cleanup

* Delete vs Save flip

* Hide delete button

* Even more cleanup

* Fix tvOS build issues.

* Reduce delay & remove unused comment. Should finally be ready again.

* wip

* Update ItemImagesView.swift

* Event Only on upload failures.

* Remove unnecessary ViewModel's from tvOS.

* Add dismiss action to RemoteSearchResultView. While I am doing this here, fix it there.

* Move From Coordinator -> .Sheet. This fixes the popping issue / delay requirement!

* wip

* wip

* wip

* wip

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
2025-01-20 16:17:35 -05:00

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)
}
}