Skip to content

Commit

Permalink
[iOS] Media Item Menu - Edit Item Images (#1345)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
JPKribs and LePips authored Jan 20, 2025
1 parent 1530668 commit 553441d
Show file tree
Hide file tree
Showing 51 changed files with 2,923 additions and 365 deletions.
11 changes: 11 additions & 0 deletions Shared/Coordinators/ItemEditorCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable {
@Route(.modal)
var editMetadata = makeEditMetadata

// MARK: - Route to Images

@Route(.modal)
var editImages = makeEditImages

// MARK: - Route to Genres

@Route(.push)
Expand Down Expand Up @@ -73,6 +78,12 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable {
}
}

// MARK: - Item Images

func makeEditImages(viewModel: ItemImagesViewModel) -> NavigationViewCoordinator<ItemImagesCoordinator> {
NavigationViewCoordinator(ItemImagesCoordinator(viewModel: viewModel))
}

// MARK: - Item Genres

@ViewBuilder
Expand Down
52 changes: 52 additions & 0 deletions Shared/Coordinators/ItemImagePickerCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// 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 JellyfinAPI
import Stinsen
import SwiftUI

final class ItemImagePickerCoordinator: NavigationCoordinatable {

// MARK: - Navigation Stack

let stack = Stinsen.NavigationStack(initial: \ItemImagePickerCoordinator.start)

@Root
var start = makeStart

// MARK: - Routes

@Route(.push)
var cropImage = makeCropImage

// MARK: - Observed Object

private let viewModel: ItemImagesViewModel

// MARK: - Image Variable

let type: ImageType

// MARK: - Initializer

init(viewModel: ItemImagesViewModel, type: ImageType) {
self.viewModel = viewModel
self.type = type
}

// MARK: - Crop Image View

func makeCropImage(image: UIImage) -> some View {
ItemPhotoCropView(viewModel: viewModel, image: image, type: type)
}

@ViewBuilder
func makeStart() -> some View {
ItemImagePicker()
}
}
60 changes: 60 additions & 0 deletions Shared/Coordinators/ItemImagesCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// 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 Factory
import JellyfinAPI
import Stinsen
import SwiftUI

final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable {

// MARK: - Navigation Stack

let stack = NavigationStack(initial: \ItemImagesCoordinator.start)

@Root
var start = makeStart

private let viewModel: ItemImagesViewModel

// MARK: - Route to Add Remote Image

@Route(.push)
var addImage = makeAddImage

// MARK: - Route to Photo Picker

@Route(.modal)
var photoPicker = makePhotoPicker

// MARK: - Initializer

init(viewModel: ItemImagesViewModel) {
self.viewModel = viewModel
}

// MARK: - Add Remote Images View

@ViewBuilder
func makeAddImage(imageType: ImageType) -> some View {
AddItemImageView(viewModel: viewModel, imageType: imageType)
}

// MARK: - Photo Picker View

func makePhotoPicker(type: ImageType) -> NavigationViewCoordinator<ItemImagePickerCoordinator> {
NavigationViewCoordinator(ItemImagePickerCoordinator(viewModel: self.viewModel, type: type))
}

// MARK: - Start

@ViewBuilder
func makeStart() -> some View {
ItemImagesView(viewModel: self.viewModel)
}
}
14 changes: 3 additions & 11 deletions Shared/Coordinators/UserProfileImageCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import SwiftUI

final class UserProfileImageCoordinator: NavigationCoordinatable {

// MARK: - Navigation Components
// MARK: - Navigation Stack

let stack = Stinsen.NavigationStack(initial: \UserProfileImageCoordinator.start)

Expand All @@ -37,19 +37,11 @@ final class UserProfileImageCoordinator: NavigationCoordinatable {
// MARK: - Views

func makeCropImage(image: UIImage) -> some View {
#if os(iOS)
UserProfileImagePicker.SquareImageCropView(viewModel: viewModel, image: image)
#else
AssertionFailureView("not implemented")
#endif
UserProfileImageCropView(viewModel: viewModel, image: image)
}

@ViewBuilder
func makeStart() -> some View {
#if os(iOS)
UserProfileImagePicker(viewModel: viewModel)
#else
AssertionFailureView("not implemented")
#endif
UserProfileImagePickerView()
}
}
36 changes: 36 additions & 0 deletions Shared/Extensions/JellyfinAPI/ImageInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// 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 Foundation
import JellyfinAPI

extension ImageInfo: @retroactive Identifiable {

public var id: Int {
hashValue
}
}

extension ImageInfo {

func itemImageSource(itemID: String, client: JellyfinClient) -> ImageSource {
let parameters = Paths.GetItemImageParameters(
tag: imageTag,
imageIndex: imageIndex
)
let request = Paths.getItemImage(
itemID: itemID,
imageType: imageType?.rawValue ?? "",
parameters: parameters
)

let itemImageURL = client.fullURL(with: request)

return ImageSource(url: itemImageURL)
}
}
44 changes: 44 additions & 0 deletions Shared/Extensions/JellyfinAPI/ImageType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// 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 Foundation
import JellyfinAPI

extension ImageType: Displayable {

var displayTitle: String {
switch self {
case .primary:
return L10n.primary
case .art:
return L10n.art
case .backdrop:
return L10n.backdrop
case .banner:
return L10n.banner
case .logo:
return L10n.logo
case .thumb:
return L10n.thumb
case .disc:
return L10n.disc
case .box:
return L10n.box
case .screenshot:
return L10n.screenshot
case .menu:
return L10n.menu
case .chapter:
return L10n.chapter
case .boxRear:
return L10n.boxRear
case .profile:
return L10n.profile
}
}
}
34 changes: 34 additions & 0 deletions Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// 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 Foundation
import JellyfinAPI
import SwiftUI

extension RemoteImageInfo: @retroactive Identifiable, Poster {

var displayTitle: String {
providerName ?? L10n.unknown
}

var unwrappedIDHashOrZero: Int {
id
}

var subtitle: String? {
language
}

var systemImage: String {
"photo"
}

public var id: Int {
hashValue
}
}
3 changes: 3 additions & 0 deletions Shared/Extensions/Nuke/ImagePipeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ extension ImagePipeline.Swiftfin {
static let local: ImagePipeline = ImagePipeline(delegate: SwiftfinImagePipelineDelegate()) {
$0.dataCache = DataCache.Swiftfin.local
}

/// An `ImagePipeline` for images to prevent more important images from losing their cache.
static let other: ImagePipeline = ImagePipeline(configuration: .withURLCache)
}

final class SwiftfinImagePipelineDelegate: ImagePipelineDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//

import Foundation
import JellyfinAPI

extension Hashable {
extension RatingType: Displayable {

var hashString: String {
"\(hashValue)"
var displayTitle: String {
switch self {
case .score:
return L10n.score
case .likes:
return L10n.likes
}
}
}
Loading

0 comments on commit 553441d

Please sign in to comment.