From d078d71393a2db256873db116c49bb1315828b7d Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sun, 28 Aug 2022 22:06:56 -0600 Subject: [PATCH] Transition Media View (#541) --- .../Coordinators/LibraryListCoordinator.swift | 46 --------- .../iOSMainTabCoordinator.swift | 18 ++-- .../tvOSMainTabCoordinator.swift | 16 ++-- Shared/Coordinators/MediaCoordinator.swift | 48 ++++++++++ Shared/Extensions/ArrayExtensions.swift | 27 ++++++ Shared/Objects/LibraryItem.swift | 32 +++++++ Shared/Objects/Typings.swift | 1 + Shared/ViewModels/LibraryListViewModel.swift | 47 ---------- Shared/ViewModels/MediaViewModel.swift | 66 +++++++++++++ Shared/ViewModels/SearchViewModel.swift | 1 - Swiftfin tvOS/Components/PosterButton.swift | 22 +++-- Swiftfin tvOS/Views/LibraryListView.swift | 66 ------------- Swiftfin tvOS/Views/MediaView.swift | 63 +++++++++++++ Swiftfin.xcodeproj/project.pbxproj | 52 +++++++---- Swiftfin/Components/PosterButton.swift | 20 ++-- Swiftfin/Views/LibraryListView.swift | 93 ------------------- Swiftfin/Views/MediaView.swift | 79 ++++++++++++++++ 17 files changed, 388 insertions(+), 309 deletions(-) delete mode 100644 Shared/Coordinators/LibraryListCoordinator.swift create mode 100644 Shared/Coordinators/MediaCoordinator.swift create mode 100644 Shared/Extensions/ArrayExtensions.swift create mode 100644 Shared/Objects/LibraryItem.swift delete mode 100644 Shared/ViewModels/LibraryListViewModel.swift create mode 100644 Shared/ViewModels/MediaViewModel.swift delete mode 100644 Swiftfin tvOS/Views/LibraryListView.swift create mode 100644 Swiftfin tvOS/Views/MediaView.swift delete mode 100644 Swiftfin/Views/LibraryListView.swift create mode 100644 Swiftfin/Views/MediaView.swift diff --git a/Shared/Coordinators/LibraryListCoordinator.swift b/Shared/Coordinators/LibraryListCoordinator.swift deleted file mode 100644 index dceec45f..00000000 --- a/Shared/Coordinators/LibraryListCoordinator.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// 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) 2022 Jellyfin & Jellyfin Contributors -// - -import Foundation -import Stinsen -import SwiftUI - -final class LibraryListCoordinator: NavigationCoordinatable { - - let stack = NavigationStack(initial: \LibraryListCoordinator.start) - - @Root - var start = makeStart - @Route(.push) - var library = makeLibrary - #if os(iOS) - @Route(.push) - var liveTV = makeLiveTV - #endif - - let viewModel: LibraryListViewModel - - init(viewModel: LibraryListViewModel) { - self.viewModel = viewModel - } - - func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { - LibraryCoordinator(viewModel: params.viewModel, title: params.title) - } - - #if os(iOS) - func makeLiveTV() -> LiveTVCoordinator { - LiveTVCoordinator() - } - #endif - - @ViewBuilder - func makeStart() -> some View { - LibraryListView(viewModel: self.viewModel) - } -} diff --git a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift index d6d8eb88..4f6a89a3 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift @@ -14,15 +14,15 @@ final class MainTabCoordinator: TabCoordinatable { var child = TabChild(startingItems: [ \MainTabCoordinator.home, \MainTabCoordinator.search, - \MainTabCoordinator.allMedia, + \MainTabCoordinator.media, ]) @Route(tabItem: makeHomeTab, onTapped: onHomeTapped) var home = makeHome @Route(tabItem: makeSearchTab, onTapped: onSearchTapped) var search = makeSearch - @Route(tabItem: makeAllMediaTab, onTapped: onMediaTapped) - var allMedia = makeAllMedia + @Route(tabItem: makeMediaTab, onTapped: onMediaTapped) + var media = makeMedia func makeHome() -> NavigationViewCoordinator { NavigationViewCoordinator(HomeCoordinator()) @@ -56,20 +56,20 @@ final class MainTabCoordinator: TabCoordinatable { L10n.search.text } - func makeAllMedia() -> NavigationViewCoordinator { - NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel())) + func makeMedia() -> NavigationViewCoordinator { + NavigationViewCoordinator(MediaCoordinator()) } - func onMediaTapped(isRepeat: Bool, coordinator: NavigationViewCoordinator) { + func onMediaTapped(isRepeat: Bool, coordinator: NavigationViewCoordinator) { if isRepeat { coordinator.child.popToRoot() } } @ViewBuilder - func makeAllMediaTab(isActive: Bool) -> some View { - Image(systemName: "folder") - L10n.allMedia.text + func makeMediaTab(isActive: Bool) -> some View { + Image(systemName: "rectangle.stack.fill") + L10n.media.text } @ViewBuilder diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift index 47f879c4..f7b7d460 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift @@ -16,7 +16,7 @@ final class MainTabCoordinator: TabCoordinatable { \MainTabCoordinator.tv, \MainTabCoordinator.movies, \MainTabCoordinator.search, - \MainTabCoordinator.other, + \MainTabCoordinator.media, \MainTabCoordinator.settings, ]) @@ -28,8 +28,8 @@ final class MainTabCoordinator: TabCoordinatable { var movies = makeMovies @Route(tabItem: makeSearchTab) var search = makeSearch - @Route(tabItem: makeOtherTab) - var other = makeOther + @Route(tabItem: makeMediaTab) + var media = makeMedia @Route(tabItem: makeSettingsTab) var settings = makeSettings @@ -81,15 +81,15 @@ final class MainTabCoordinator: TabCoordinatable { } } - func makeOther() -> NavigationViewCoordinator { - NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel())) + func makeMedia() -> NavigationViewCoordinator { + NavigationViewCoordinator(MediaCoordinator()) } @ViewBuilder - func makeOtherTab(isActive: Bool) -> some View { + func makeMediaTab(isActive: Bool) -> some View { HStack { - Image(systemName: "folder") - L10n.other.text + Image(systemName: "rectangle.stack") + L10n.media.text } } diff --git a/Shared/Coordinators/MediaCoordinator.swift b/Shared/Coordinators/MediaCoordinator.swift new file mode 100644 index 00000000..17ff3c53 --- /dev/null +++ b/Shared/Coordinators/MediaCoordinator.swift @@ -0,0 +1,48 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import Stinsen +import SwiftUI + +final class MediaCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \MediaCoordinator.start) + + @Root + var start = makeStart + #if os(tvOS) + @Route(.modal) + var library = makeLibrary + #else + @Route(.push) + var library = makeLibrary + @Route(.push) + var liveTV = makeLiveTV + #endif + + #if os(tvOS) + func makeLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator { + NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title)) + } + + #else + func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { + LibraryCoordinator(viewModel: params.viewModel, title: params.title) + } + + func makeLiveTV() -> LiveTVCoordinator { + LiveTVCoordinator() + } + #endif + + @ViewBuilder + func makeStart() -> some View { + MediaView(viewModel: .init()) + } +} diff --git a/Shared/Extensions/ArrayExtensions.swift b/Shared/Extensions/ArrayExtensions.swift new file mode 100644 index 00000000..f3f95b62 --- /dev/null +++ b/Shared/Extensions/ArrayExtensions.swift @@ -0,0 +1,27 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +extension Array { + func appending(_ element: Element) -> [Element] { + self + [element] + } + + func appending(_ element: Element, if condition: Bool) -> [Element] { + if condition { + return self + [element] + } else { + return self + } + } + + func appending(_ contents: [Element]) -> [Element] { + self + contents + } +} diff --git a/Shared/Objects/LibraryItem.swift b/Shared/Objects/LibraryItem.swift new file mode 100644 index 00000000..c2d2dfc6 --- /dev/null +++ b/Shared/Objects/LibraryItem.swift @@ -0,0 +1,32 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct LibraryItem: Equatable, Poster { + + var library: BaseItemDto + var viewModel: MediaViewModel + var title: String = "" + var subtitle: String? + var showTitle: Bool = false + + func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource { + .init() + } + + func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] { + viewModel.libraryImages[library.id ?? ""] ?? [] + } + + static func == (lhs: LibraryItem, rhs: LibraryItem) -> Bool { + lhs.library == rhs.library && + lhs.viewModel.libraryImages[lhs.library.id ?? ""] == rhs.viewModel.libraryImages[rhs.library.id ?? ""] + } +} diff --git a/Shared/Objects/Typings.swift b/Shared/Objects/Typings.swift index aa93fd17..878bd8dd 100644 --- a/Shared/Objects/Typings.swift +++ b/Shared/Objects/Typings.swift @@ -18,6 +18,7 @@ struct LibraryFilters: Codable, Hashable { var sortBy: [SortBy] = [.name] static let `default` = LibraryFilters() + static let favorites: LibraryFilters = .init(filters: [.isFavorite], sortOrder: [.ascending], sortBy: [.name]) } public enum SortBy: String, Codable, CaseIterable { diff --git a/Shared/ViewModels/LibraryListViewModel.swift b/Shared/ViewModels/LibraryListViewModel.swift deleted file mode 100644 index 71df2a9a..00000000 --- a/Shared/ViewModels/LibraryListViewModel.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// 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) 2022 Jellyfin & Jellyfin Contributors -// - -import Defaults -import Foundation -import JellyfinAPI - -final class LibraryListViewModel: ViewModel { - - @Published - var libraries: [BaseItemDto] = [] - - var filteredLibraries: [BaseItemDto] { - var supportedLibraries = ["movies", "tvshows", "unknown"] - - if Defaults[.Experimental.liveTVAlphaEnabled] { - supportedLibraries.append("livetv") - } - - return libraries.filter { supportedLibraries.contains($0.collectionType ?? "unknown") } - } - - // temp - let withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: []) - - override init() { - super.init() - - requestLibraries() - } - - func requestLibraries() { - UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) - .trackActivity(loading) - .sink(receiveCompletion: { completion in - self.handleAPIRequestError(completion: completion) - }, receiveValue: { response in - self.libraries = response.items ?? [] - }) - .store(in: &cancellables) - } -} diff --git a/Shared/ViewModels/MediaViewModel.swift b/Shared/ViewModels/MediaViewModel.swift new file mode 100644 index 00000000..e2ae588d --- /dev/null +++ b/Shared/ViewModels/MediaViewModel.swift @@ -0,0 +1,66 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Foundation +import JellyfinAPI + +final class MediaViewModel: ViewModel { + + @Published + var libraries: [BaseItemDto] = [] + @Published + var libraryImages: [String: [ImageSource]] = [:] + + private var supportedLibraries: [String] { + ["movies", "tvshows", "unknown"] + .appending("livetv", if: Defaults[.Experimental.liveTVAlphaEnabled]) + } + + override init() { + super.init() + + requestLibraries() + getRandomItemImageSource(with: [.isFavorite], id: nil, key: "favorites") + } + + func requestLibraries() { + UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) + .trackActivity(loading) + .sink(receiveCompletion: { completion in + self.handleAPIRequestError(completion: completion) + }, receiveValue: { response in + guard let items = response.items else { return } + self.libraries = items.filter { self.supportedLibraries.contains($0.collectionType ?? "unknown") } + self.libraries.forEach { + self.getRandomItemImageSource(with: nil, id: $0.id, key: $0.id ?? "") + } + }) + .store(in: &cancellables) + } + + private func getRandomItemImageSource(with filters: [ItemFilter]?, id: String?, key: String) { + ItemsAPI.getItemsByUserId( + userId: SessionManager.main.currentLogin.user.id, + limit: 3, + recursive: true, + parentId: id, + includeItemTypes: [.movie, .series], + filters: filters, + sortBy: ["Random"] + ) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + guard let items = response.items else { return } + let imageSources = items.map { $0.imageSource(.backdrop, maxWidth: 500) } + self?.libraryImages[key] = imageSources + }) + .store(in: &cancellables) + } +} diff --git a/Shared/ViewModels/SearchViewModel.swift b/Shared/ViewModels/SearchViewModel.swift index 2dff86ef..2a72177e 100644 --- a/Shared/ViewModels/SearchViewModel.swift +++ b/Shared/ViewModels/SearchViewModel.swift @@ -53,7 +53,6 @@ final class SearchViewModel: ViewModel { private func cancelPreviousSearch() { searchCancellables.forEach { $0.cancel() } - print(searchCancellables.count) } func search(with query: String) { diff --git a/Swiftfin tvOS/Components/PosterButton.swift b/Swiftfin tvOS/Components/PosterButton.swift index b5664111..577d52eb 100644 --- a/Swiftfin tvOS/Components/PosterButton.swift +++ b/Swiftfin tvOS/Components/PosterButton.swift @@ -61,12 +61,18 @@ struct PosterButton