Merge pull request #193 from LePips/update-home-view-items

iOS - Home Screen Pull To Refresh
This commit is contained in:
aiden 3 2021-10-27 20:30:57 -04:00 committed by GitHub
commit 63676a81bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 111 additions and 36 deletions

View File

@ -345,6 +345,7 @@
E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; }; E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; };
E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */; }; E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */; };
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; };
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
@ -601,6 +602,7 @@
E1D4BF862719D27100A11E64 /* Bitrates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bitrates.swift; sourceTree = "<group>"; }; E1D4BF862719D27100A11E64 /* Bitrates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bitrates.swift; sourceTree = "<group>"; };
E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsCoordinator.swift; sourceTree = "<group>"; }; E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsCoordinator.swift; sourceTree = "<group>"; };
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = "<group>"; }; E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = "<group>"; };
E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = "<group>"; };
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; }; E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; };
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; }; E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; }; E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
@ -861,6 +863,7 @@
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = { 5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E1DD1127271E7D15005BE12F /* Objects */,
E13DD3BB27163C3E009D4DAF /* App */, E13DD3BB27163C3E009D4DAF /* App */,
62ECA01926FA6D6900E8EBB7 /* AppURLHandler */, 62ECA01926FA6D6900E8EBB7 /* AppURLHandler */,
5377CBF8263B596B003A4E83 /* Assets.xcassets */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */,
@ -1298,6 +1301,14 @@
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E1DD1127271E7D15005BE12F /* Objects */ = {
isa = PBXGroup;
children = (
E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */,
);
path = Objects;
sourceTree = "<group>";
};
E1FCD08E26C466F3007C8DCF /* Errors */ = { E1FCD08E26C466F3007C8DCF /* Errors */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1848,6 +1859,7 @@
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */,
E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */,
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */, E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,

View File

@ -0,0 +1,23 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
import UIKit
// A more general derivative of
// https://stackoverflow.com/questions/65812080/introspect-library-uirefreshcontrol-with-swiftui-not-working
class RefreshHelper {
var refreshControl: UIRefreshControl?
var refreshAction: (() -> Void)?
@objc func didRefresh() {
guard let refreshControl = refreshControl else { return }
refreshAction?()
refreshControl.endRefreshing()
}
}

View File

@ -8,6 +8,7 @@
*/ */
import Foundation import Foundation
import Introspect
import SwiftUI import SwiftUI
struct HomeView: View { struct HomeView: View {
@ -15,6 +16,8 @@ struct HomeView: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router @EnvironmentObject var homeRouter: HomeCoordinator.Router
@StateObject var viewModel = HomeViewModel() @StateObject var viewModel = HomeViewModel()
private let refreshHelper = RefreshHelper()
@ViewBuilder @ViewBuilder
var innerBody: some View { var innerBody: some View {
if viewModel.isLoading { if viewModel.isLoading {
@ -28,33 +31,40 @@ struct HomeView: View {
if !viewModel.nextUpItems.isEmpty { if !viewModel.nextUpItems.isEmpty {
NextUpView(items: viewModel.nextUpItems) NextUpView(items: viewModel.nextUpItems)
} }
if !viewModel.librariesShowRecentlyAddedIDs.isEmpty {
ForEach(viewModel.librariesShowRecentlyAddedIDs, id: \.self) { libraryID in ForEach(viewModel.libraries, id: \.self) { library in
let library = viewModel.libraries.first(where: { $0.id == libraryID }) HStack {
HStack { Text("Latest \(library.name ?? "")")
Text("Latest \(library?.name ?? "")") .font(.title2)
.font(.title2) .fontWeight(.bold)
.fontWeight(.bold) Spacer()
Spacer() Button {
Button { homeRouter
homeRouter .route(to: \.library, (viewModel: .init(parentID: library.id!,
.route(to: \.library, (viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet),
filters: viewModel.recentFilterSet), title: library.name ?? ""))
title: library?.name ?? "")) } label: {
} label: { HStack {
HStack { Text("See All").font(.subheadline).fontWeight(.bold)
Text("See All").font(.subheadline).fontWeight(.bold) Image(systemName: "chevron.right").font(Font.subheadline.bold())
Image(systemName: "chevron.right").font(Font.subheadline.bold())
}
} }
}.padding(.leading, 16) }
.padding(.trailing, 16) }.padding(.leading, 16)
LatestMediaView(viewModel: .init(libraryID: libraryID)) .padding(.trailing, 16)
} LatestMediaView(viewModel: .init(libraryID: library.id!))
} }
} }
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) .padding(.bottom, UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
} }
.introspectScrollView { scrollView in
let control = UIRefreshControl()
refreshHelper.refreshControl = control
refreshHelper.refreshAction = viewModel.refresh
control.addTarget(refreshHelper, action: #selector(RefreshHelper.didRefresh), for: .valueChanged)
scrollView.refreshControl = control
}
} }
} }

View File

@ -14,10 +14,10 @@ import JellyfinAPI
final class HomeViewModel: ViewModel { final class HomeViewModel: ViewModel {
@Published var librariesShowRecentlyAddedIDs = [String]() @Published var librariesShowRecentlyAddedIDs: [String] = []
@Published var libraries = [BaseItemDto]() @Published var libraries: [BaseItemDto] = []
@Published var resumeItems = [BaseItemDto]() @Published var resumeItems: [BaseItemDto] = []
@Published var nextUpItems = [BaseItemDto]() @Published var nextUpItems: [BaseItemDto] = []
// temp // temp
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded]) var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])
@ -32,26 +32,42 @@ final class HomeViewModel: ViewModel {
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion) switch completion {
case .finished: ()
case .failure(_):
self.libraries = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in }, receiveValue: { response in
var newLibraries: [BaseItemDto] = []
response.items!.forEach { item in response.items!.forEach { item in
LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")") LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")")
if item.collectionType == "movies" || item.collectionType == "tvshows" { if item.collectionType == "movies" || item.collectionType == "tvshows" {
self.libraries.append(item) newLibraries.append(item)
} }
} }
UserAPI.getCurrentUser() UserAPI.getCurrentUser()
.trackActivity(self.loading) .trackActivity(self.loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion) switch completion {
case .finished: ()
case .failure(_):
self.libraries = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in }, receiveValue: { response in
self.libraries.forEach { library in let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!.latestItemsExcludes! : []
if !(response.configuration?.latestItemsExcludes?.contains(library.id!))! {
LogManager.shared.log.debug("Adding library \(library.id!) (\(library.name ?? "nil")) to recently added list") for excludeID in excludeIDs {
self.librariesShowRecentlyAddedIDs.append(library.id!) newLibraries.removeAll { library in
return library.id == excludeID
} }
} }
self.libraries = newLibraries
}) })
.store(in: &self.cancellables) .store(in: &self.cancellables)
}) })
@ -59,12 +75,20 @@ final class HomeViewModel: ViewModel {
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12, ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) mediaTypes: ["Video"],
imageTypeLimit: 1,
enableImageTypes: [.primary, .backdrop, .thumb])
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion) switch completion {
case .finished: ()
case .failure(_):
self.resumeItems = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in }, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items") LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items")
self.resumeItems = response.items ?? [] self.resumeItems = response.items ?? []
}) })
.store(in: &cancellables) .store(in: &cancellables)
@ -73,9 +97,15 @@ final class HomeViewModel: ViewModel {
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion) switch completion {
case .finished: ()
case .failure(_):
self.nextUpItems = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in }, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items") LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items")
self.nextUpItems = response.items ?? [] self.nextUpItems = response.items ?? []
}) })
.store(in: &cancellables) .store(in: &cancellables)