From 897d158707948ff94c6b73202a5cf23ee066e1c8 Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Mon, 28 Jun 2021 16:43:13 -0400 Subject: [PATCH] basic movie item view --- .../Components/PortraitItemElement.swift | 32 ++++ .../ContinueWatchingView.swift | 2 +- JellyfinPlayer tvOS/HomeView.swift | 5 +- JellyfinPlayer tvOS/ItemView.swift | 47 +++++ .../JellyfinPlayer_tvOSApp.swift | 1 - JellyfinPlayer tvOS/LatestMediaView.swift | 2 +- JellyfinPlayer tvOS/LibraryView.swift | 84 ++++++++ JellyfinPlayer tvOS/MovieItemView.swift | 180 ++++++++++++++++++ JellyfinPlayer tvOS/NextUpView.swift | 2 +- ...oard.storyboard => VideoPlayer.storyboard} | 0 .../VideoPlayer/VideoPlayer.swift | 2 +- .../VideoPlayerViewController.swift | 33 ++-- JellyfinPlayer.xcodeproj/project.pbxproj | 24 ++- .../xcshareddata/swiftpm/Package.resolved | 4 +- JellyfinPlayer/ConnectToServerView.swift | 2 +- Shared/Extensions/APIExtensions.swift | 8 +- Shared/Extensions/ImageView.swift | 18 +- .../UDPBroadCastConnection.swift | 8 - Shared/ViewModels/LibraryViewModel.swift | 34 ++++ 19 files changed, 431 insertions(+), 57 deletions(-) create mode 100644 JellyfinPlayer tvOS/ItemView.swift create mode 100644 JellyfinPlayer tvOS/LibraryView.swift create mode 100644 JellyfinPlayer tvOS/MovieItemView.swift rename JellyfinPlayer tvOS/VideoPlayer/{VideoPlayerStoryboard.storyboard => VideoPlayer.storyboard} (100%) diff --git a/JellyfinPlayer tvOS/Components/PortraitItemElement.swift b/JellyfinPlayer tvOS/Components/PortraitItemElement.swift index c2adad4f..c8e12201 100644 --- a/JellyfinPlayer tvOS/Components/PortraitItemElement.swift +++ b/JellyfinPlayer tvOS/Components/PortraitItemElement.swift @@ -24,6 +24,38 @@ struct PortraitItemElement: View { .cornerRadius(10) .shadow(radius: focused ? 10.0 : 0) .shadow(radius: focused ? 10.0 : 0) + .overlay( + ZStack { + if item.userData?.isFavorite ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + .opacity(0.6) + Image(systemName: "heart.fill") + .foregroundColor(Color(.systemRed)) + .font(.system(size: 10)) + } + } + .padding(2) + .opacity(1) + , alignment: .bottomLeading) + .overlay( + ZStack { + if item.userData?.played ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + Image(systemName: "checkmark.circle.fill") + .foregroundColor(Color(.systemBlue)) + } else { + if(item.userData?.unplayedItemCount != nil) { + Image(systemName: "circle.fill") + .foregroundColor(Color(.systemBlue)) + Text(String(item.userData!.unplayedItemCount ?? 0)) + .foregroundColor(.white) + .font(.caption2) + } + } + }.padding(2) + .opacity(1), alignment: .topTrailing).opacity(1) } .onChange(of: envFocused) { envFocus in withAnimation(.linear(duration: 0.15)) { diff --git a/JellyfinPlayer tvOS/ContinueWatchingView.swift b/JellyfinPlayer tvOS/ContinueWatchingView.swift index f73167de..ad793923 100644 --- a/JellyfinPlayer tvOS/ContinueWatchingView.swift +++ b/JellyfinPlayer tvOS/ContinueWatchingView.swift @@ -25,7 +25,7 @@ struct ContinueWatchingView: View { LazyHStack { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in - NavigationLink(destination: VideoPlayerView(item: item)) { + NavigationLink(destination: LazyView { ItemView(item: item) }) { LandscapeItemElement(item: item) } .buttonStyle(PlainNavigationLinkButtonStyle()) diff --git a/JellyfinPlayer tvOS/HomeView.swift b/JellyfinPlayer tvOS/HomeView.swift index ab9262ed..8642a1a6 100644 --- a/JellyfinPlayer tvOS/HomeView.swift +++ b/JellyfinPlayer tvOS/HomeView.swift @@ -33,7 +33,9 @@ struct HomeView: View { VStack(alignment: .leading) { let library = viewModel.libraries.first(where: { $0.id == libraryID }) - NavigationLink(destination: Text("library_latest")) { + NavigationLink(destination: LazyView { + LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "") + }) { HStack { Text("Latest \(library?.name ?? "")") .font(.headline) @@ -45,6 +47,7 @@ struct HomeView: View { } } } + Spacer().frame(height: 30) } } } diff --git a/JellyfinPlayer tvOS/ItemView.swift b/JellyfinPlayer tvOS/ItemView.swift new file mode 100644 index 00000000..36ed9877 --- /dev/null +++ b/JellyfinPlayer tvOS/ItemView.swift @@ -0,0 +1,47 @@ +/* JellyfinPlayer/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 SwiftUI +import Introspect +import JellyfinAPI + +class VideoPlayerItem: ObservableObject { + @Published var shouldShowPlayer: Bool = false + @Published var itemToPlay: BaseItemDto = BaseItemDto() +} + +struct ItemView: View { + private var item: BaseItemDto + + @StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem() + @State private var videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view. + @State private var isLoading: Bool = false + @State private var viewDidLoad: Bool = false + + init(item: BaseItemDto) { + self.item = item + } + + var body: some View { + ZStack { + NavigationLink(destination: VideoPlayerView(item: videoPlayerItem.itemToPlay), isActive: $videoPlayerItem.shouldShowPlayer) { + EmptyView() + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + .focusable(false) + + Group { + if item.type == "Movie" { + MovieItemView(item: item) + } else { + Text("Type: \(item.type ?? "") not implemented yet :(") + } + } + .environmentObject(videoPlayerItem) + } + } +} diff --git a/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift b/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift index 86dcfd74..9ab606cb 100644 --- a/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift +++ b/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift @@ -7,7 +7,6 @@ import SwiftUI import UIKit - @main struct JellyfinPlayer_tvOSApp: App { let persistenceController = PersistenceController.shared diff --git a/JellyfinPlayer tvOS/LatestMediaView.swift b/JellyfinPlayer tvOS/LatestMediaView.swift index ad58190c..247586a5 100644 --- a/JellyfinPlayer tvOS/LatestMediaView.swift +++ b/JellyfinPlayer tvOS/LatestMediaView.swift @@ -42,7 +42,7 @@ struct LatestMediaView: View { LazyHStack { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in - NavigationLink(destination: Text("itemv")) { + NavigationLink(destination: LazyView { ItemView(item: item) }) { PortraitItemElement(item: item) }.buttonStyle(PlainNavigationLinkButtonStyle()) } diff --git a/JellyfinPlayer tvOS/LibraryView.swift b/JellyfinPlayer tvOS/LibraryView.swift new file mode 100644 index 00000000..cf6a0f0a --- /dev/null +++ b/JellyfinPlayer tvOS/LibraryView.swift @@ -0,0 +1,84 @@ +/* + * JellyfinPlayer/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 SwiftUI + +struct LibraryView: View { + @StateObject var viewModel: LibraryViewModel + var title: String + + // MARK: tracks for grid + var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) + + @State var isShowingSearchView = false + @State var isShowingFilterView = false + + @State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 250) + + var body: some View { + Group { + if viewModel.isLoading == true { + ProgressView() + } else if !viewModel.items.isEmpty { + ScrollView(.vertical) { + Spacer().frame(height: 16) + LazyVGrid(columns: tracks) { + ForEach(viewModel.items, id: \.id) { item in + if(item.type != "Folder") { + NavigationLink(destination: LazyView { ItemView(item: item) }) { + PortraitItemElement(item: item) + }.buttonStyle(PlainNavigationLinkButtonStyle()) + .onAppear() { + if item == viewModel.items.last && viewModel.hasNextPage { + print("Last item visible, load more items.") + viewModel.requestNextPageAsync() + } + } + } + } + } + Spacer().frame(height: 16) + } + } else { + Text("No results.") + } + } + .navigationBarTitle(title) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + if viewModel.hasPreviousPage { + Button { + viewModel.requestPreviousPage() + } label: { + Image(systemName: "chevron.left") + }.disabled(viewModel.isLoading) + } + if viewModel.hasNextPage { + Button { + viewModel.requestNextPage() + } label: { + Image(systemName: "chevron.right") + }.disabled(viewModel.isLoading) + } + } + }/* + .sheet(isPresented: $isShowingFilterView) { + LibraryFilterView(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, parentId: viewModel.parentID ?? "") + } + .background( + NavigationLink(destination: LibrarySearchView(viewModel: .init(parentID: viewModel.parentID)), + isActive: $isShowingSearchView) { + EmptyView() + } + ) + */ + } +} + +// stream BM^S by nicki! +// diff --git a/JellyfinPlayer tvOS/MovieItemView.swift b/JellyfinPlayer tvOS/MovieItemView.swift new file mode 100644 index 00000000..deb0de44 --- /dev/null +++ b/JellyfinPlayer tvOS/MovieItemView.swift @@ -0,0 +1,180 @@ +// + /* + * 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 SwiftUI +import JellyfinAPI + +struct MovieItemView: View { + let item: BaseItemDto + @EnvironmentObject private var playbackInfo: VideoPlayerItem + + @State var actors: [BaseItemPerson] = []; + @State var studio: String? = nil; + @State var director: String? = nil; + + @Namespace private var namespace + + func onAppear() { + actors = [] + director = nil + studio = nil + var actor_index = 0; + item.people?.forEach { person in + if(person.type == "Actor") { + if(actor_index < 8) { + actors.append(person) + } + actor_index = actor_index + 1; + } + if(person.type == "Director") { + director = person.name ?? "" + } + } + } + + var body: some View { + ZStack { + ImageView(src: item.getBackdropImage(maxWidth: 1920), bh: item.getBackdropImageBlurHash()) + .opacity(0.4) + ScrollView { + LazyVStack { + HStack { + VStack(alignment: .leading) { + Text(item.name ?? "") + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + HStack { + if item.productionYear != nil { + Text(String(item.productionYear!)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + } + Text(item.getItemRuntime()).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if item.officialRating != nil { + Text(item.officialRating!).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + } + + HStack { + VStack(alignment: .trailing) { + if(studio != nil) { + Text("STUDIO") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + Text(studio!) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .padding(.bottom, 40) + } + + if(director != nil) { + Text("DIRECTOR") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + Text(director!) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .padding(.bottom, 40) + } + + if(!actors.isEmpty) { + Text("CAST") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + ForEach(actors, id: \.id) { person in + Text(person.name!) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.secondary) + } + } + Spacer() + } + VStack(alignment: .leading) { + Text(item.taglines?.first ?? "") + .font(.body) + .italic() + .fontWeight(.medium) + .foregroundColor(.primary) + + Text(item.overview ?? "") + .font(.body) + .fontWeight(.medium) + .foregroundColor(.primary) + + HStack { + VStack { + Button { + playbackInfo.shouldShowPlayer = true + } label: { + Image(systemName: "heart.fill") + .font(.system(size: 40)) + .padding(.vertical, 12).padding(.horizontal, 20) + } + Text("Favorite") + .font(.caption) + } + VStack { + Button { + playbackInfo.itemToPlay = item + playbackInfo.shouldShowPlayer = true + } label: { + Image(systemName: "play.fill") + .font(.system(size: 40)) + .padding(.vertical, 12).padding(.horizontal, 20) + }.prefersDefaultFocus(in: namespace) + Text("Play") + .font(.caption) + } + VStack { + Button { + playbackInfo.shouldShowPlayer = true + } label: { + Image(systemName: "eye.fill") + .font(.system(size: 40)) + .padding(.vertical, 12).padding(.horizontal, 20) + } + Text("Mark Watched") + .font(.caption) + } + }.padding(.top, 15) + Spacer() + } + }.padding(.top, 50) + } + + VStack { + ImageView(src: item.getPrimaryImage(maxWidth: 450), bh: item.getPrimaryImageBlurHash()) + .frame(width: 450, height: 675) + .cornerRadius(10) + Spacer() + } + } + }.padding(EdgeInsets(top: 90, leading: 90, bottom: 0, trailing: 90)) + } + }.onAppear(perform: onAppear) + .focusScope(namespace) + } +} diff --git a/JellyfinPlayer tvOS/NextUpView.swift b/JellyfinPlayer tvOS/NextUpView.swift index 8b12f5cc..1db5d360 100644 --- a/JellyfinPlayer tvOS/NextUpView.swift +++ b/JellyfinPlayer tvOS/NextUpView.swift @@ -24,7 +24,7 @@ struct NextUpView: View { LazyHStack { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in - NavigationLink(destination: VideoPlayerView(item: item)) { + NavigationLink(destination: LazyView { ItemView(item: item) }) { LandscapeItemElement(item: item) }.buttonStyle(PlainNavigationLinkButtonStyle()) } diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.storyboard similarity index 100% rename from JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard rename to JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.storyboard diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift index d2017909..809ad75a 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift @@ -15,7 +15,7 @@ struct VideoPlayerView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> some UIViewController { - let storyboard = UIStoryboard(name: "VideoPlayerStoryboard", bundle: nil) + let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil) let viewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! VideoPlayerViewController viewController.manifest = item diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 1e2ff466..581442ba 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -244,17 +244,12 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, // Pause and load captions into memory. mediaPlayer.pause() - var shouldHaveSubtitleTracks = 0 subtitleTrackArray.forEach { sub in - if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" { - shouldHaveSubtitleTracks = shouldHaveSubtitleTracks + 1 + if sub.id != -1 && sub.delivery == .external { mediaPlayer.addPlaybackSlave(sub.url!, type: .subtitle, enforce: false) } } - // Wait for captions to load - while mediaPlayer.numberOfSubtitlesTracks != shouldHaveSubtitleTracks {} - // Select default track & resume playback mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack mediaPlayer.pause() @@ -714,18 +709,8 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, // Move time along transport bar func mediaPlayerTimeChanged(_ aNotification: Notification!) { - - if loading { - loading = false - DispatchQueue.main.async { [self] in - activityIndicator.isHidden = true - activityIndicator.stopAnimating() - } - updateNowPlayingCenter(time: nil, playing: true) - } - let time = mediaPlayer.position - if time != lastTime { + if abs(time-lastTime) > 0.00005 { self.currentTimeLabel.text = formatSecondsToHMS(Double(mediaPlayer.time.intValue/1000)) self.remainingTimeLabel.text = "-" + formatSecondsToHMS(Double(abs(mediaPlayer.remainingTime.intValue/1000))) @@ -749,15 +734,23 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, controlsAppearTime = 999_999_999_999_999 } } - + lastTime = time } - lastTime = time - if CACurrentMediaTime() - lastProgressReportTime > 5 { + mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack sendProgressReport(eventName: "timeupdate") lastProgressReportTime = CACurrentMediaTime() } + + if loading { + loading = false + DispatchQueue.main.async { [self] in + activityIndicator.isHidden = true + activityIndicator.stopAnimating() + } + updateNowPlayingCenter(time: nil, playing: true) + } } // MARK: Settings Delegate diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 3864743e..042fb487 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -21,7 +21,7 @@ 5310695A2684E7EE00CFFDBA /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069532684E7EE00CFFDBA /* VideoPlayer.swift */; }; 5310695B2684E7EE00CFFDBA /* AudioView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069542684E7EE00CFFDBA /* AudioView.swift */; }; 5310695C2684E7EE00CFFDBA /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069552684E7EE00CFFDBA /* VideoPlayerViewController.swift */; }; - 5310695D2684E7EE00CFFDBA /* VideoPlayerStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 531069562684E7EE00CFFDBA /* VideoPlayerStoryboard.storyboard */; }; + 5310695D2684E7EE00CFFDBA /* VideoPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */; }; 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E4267ABD5C005D8AB9 /* MainTabView.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; }; @@ -80,6 +80,7 @@ 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A089CF264DA9DA00D57806 /* MovieItemView.swift */; }; 53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BC266B0FF20016769F /* JellyfinAPI */; }; 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BE266B0FFE0016769F /* JellyfinAPI */; }; + 53A83C33268A309300DF3D92 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A83C32268A309300DF3D92 /* LibraryView.swift */; }; 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53ABFDDB267972BF00886593 /* TVServices.framework */; }; 53ABFDDE267974E300886593 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ABFDDD267974E300886593 /* SplashView.swift */; }; 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; }; @@ -93,6 +94,8 @@ 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA526572F0700E7EA70 /* SeriesItemView.swift */; }; 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; }; + 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CD2A3F268A49C2002ABD4E /* ItemView.swift */; }; + 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */; }; 53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */; }; 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; }; 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; }; @@ -214,7 +217,7 @@ 531069532684E7EE00CFFDBA /* VideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 531069542684E7EE00CFFDBA /* AudioView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioView.swift; sourceTree = ""; }; 531069552684E7EE00CFFDBA /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = ""; }; - 531069562684E7EE00CFFDBA /* VideoPlayerStoryboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = VideoPlayerStoryboard.storyboard; sourceTree = ""; }; + 531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = VideoPlayer.storyboard; sourceTree = ""; }; 531690E4267ABD5C005D8AB9 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; @@ -276,11 +279,14 @@ 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = ""; }; 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 53A089CF264DA9DA00D57806 /* MovieItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = ""; }; + 53A83C32268A309300DF3D92 /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; 53ABFDDA267972BF00886593 /* JellyfinPlayer tvOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "JellyfinPlayer tvOS.entitlements"; sourceTree = ""; }; 53ABFDDB267972BF00886593 /* TVServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TVServices.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.0.sdk/System/Library/Frameworks/TVServices.framework; sourceTree = DEVELOPER_DIR; }; 53ABFDDD267974E300886593 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; 53ABFDEA2679753200886593 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = JellyfinPlayer.entitlements; sourceTree = ""; }; + 53CD2A3F268A49C2002ABD4E /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; + 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = ""; }; 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = ""; }; 53DE4BD1267098F300739748 /* SearchBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarView.swift; sourceTree = ""; }; 53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; @@ -400,7 +406,7 @@ 531069502684E7EE00CFFDBA /* InfoTabBarViewController.swift */, 531069532684E7EE00CFFDBA /* VideoPlayer.swift */, 531069552684E7EE00CFFDBA /* VideoPlayerViewController.swift */, - 531069562684E7EE00CFFDBA /* VideoPlayerStoryboard.storyboard */, + 531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */, ); path = VideoPlayer; sourceTree = ""; @@ -449,6 +455,9 @@ 531690E6267ABD79005D8AB9 /* HomeView.swift */, 531690F8267AD135005D8AB9 /* README.md */, 536D3D7E267BDF100004248C /* LatestMediaView.swift */, + 53A83C32268A309300DF3D92 /* LibraryView.swift */, + 53CD2A3F268A49C2002ABD4E /* ItemView.swift */, + 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */, ); path = "JellyfinPlayer tvOS"; sourceTree = ""; @@ -806,7 +815,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5310695D2684E7EE00CFFDBA /* VideoPlayerStoryboard.storyboard in Resources */, + 5310695D2684E7EE00CFFDBA /* VideoPlayer.storyboard in Resources */, 5358706A2669D21700D05A09 /* Preview Assets.xcassets in Resources */, 535870672669D21700D05A09 /* Assets.xcassets in Resources */, 5358707E2669D64F00D05A09 /* bitrates.json in Resources */, @@ -948,6 +957,8 @@ 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, + 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, + 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, @@ -955,6 +966,7 @@ 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */, 62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */, 535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */, + 53A83C33268A309300DF3D92 /* LibraryView.swift in Sources */, 62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, 5310695A2684E7EE00CFFDBA /* VideoPlayer.swift in Sources */, @@ -1471,8 +1483,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kean/NukeUI"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.3.0; + kind = exactVersion; + version = 0.3.0; }; }; 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */ = { diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved index f98581b0..02abe99e 100644 --- a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,8 +69,8 @@ "repositoryURL": "https://github.com/kean/NukeUI", "state": { "branch": null, - "revision": "4516371912149ac024dec361827931b46a69c217", - "version": "0.6.2" + "revision": "d2580b8d22b29c6244418d8e4b568f3162191460", + "version": "0.3.0" } }, { diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 5cf7d969..836beeeb 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -67,7 +67,7 @@ struct ConnectToServerView: View { Text(publicUser.name ?? "").font(.subheadline).fontWeight(.semibold) Spacer() if publicUser.primaryImageTag != nil { - ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=120&quality=80&tag=\(publicUser.primaryImageTag!)")!) + ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=60&quality=80&tag=\(publicUser.primaryImageTag!)")!) .frame(width: 60, height: 60) .cornerRadius(30.0) } else { diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift index a45b0880..55a4f296 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/APIExtensions.swift @@ -70,7 +70,7 @@ extension BaseItemDto { } let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -79,7 +79,7 @@ extension BaseItemDto { let imageTag = (self.parentBackdropImageTags ?? [""])[0] let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -87,7 +87,7 @@ extension BaseItemDto { let imageType = "Primary" let imageTag = self.seriesPrimaryImageTag ?? "" let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -103,7 +103,7 @@ extension BaseItemDto { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } diff --git a/Shared/Extensions/ImageView.swift b/Shared/Extensions/ImageView.swift index 0ce4d739..ac64f37a 100644 --- a/Shared/Extensions/ImageView.swift +++ b/Shared/Extensions/ImageView.swift @@ -24,16 +24,14 @@ struct ImageView: View { } var body: some View { - LazyImage(source: source) { state in - if let image = state.image { - image - } else if state.error != nil { - Rectangle() - .fill(Color.gray) - } else { - Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 16, height: 16))!) - .resizable() - } + LazyImage(source: source) + .placeholder { + Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 8, height: 8))!) + .resizable() + } + .failure { + Rectangle() + .background(Color.gray) } } } diff --git a/Shared/ServerLocator/UDPBroadCastConnection.swift b/Shared/ServerLocator/UDPBroadCastConnection.swift index de0cd770..c00d0de6 100644 --- a/Shared/ServerLocator/UDPBroadCastConnection.swift +++ b/Shared/ServerLocator/UDPBroadCastConnection.swift @@ -210,17 +210,9 @@ open class UDPBroadcastConnection { } guard sent > 0 else { - if let errorString = String(validatingUTF8: strerror(errno)) { - // debugPrint("UDP connection failed to send data: \(errorString)") - } closeConnection() throw ConnectionError.sendingMessageFailed(code: errno) } - - if sent == broadcastMessageLength { - // Success - // debugPrint("UDP connection sent \(broadcastMessageLength) bytes") - } } } diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index e5921998..daabc25b 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -87,11 +87,45 @@ final class LibraryViewModel: ViewModel { }) .store(in: &cancellables) } + + func requestItemsAsync(with filters: LibraryFilters) { + let personIDs: [String] = [person].compactMap(\.?.id) + let studioIDs: [String] = [studio].compactMap(\.?.id) + let genreIDs: [String] + if filters.withGenres.isEmpty { + genreIDs = [genre].compactMap(\.?.id) + } else { + genreIDs = filters.withGenres.compactMap(\.id) + } + let sortBy = filters.sortBy.map(\.rawValue) + + ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: filters.filters.contains(.isFavorite) ? true : false, + searchTerm: nil, sortOrder: filters.sortOrder, parentId: parentID, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], + filters: filters.filters, sortBy: sortBy, tags: filters.tags, + enableUserData: true, personIds: personIDs, studioIds: studioIDs, genreIds: genreIDs, enableImages: true) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestCompletion(completion: completion) + }, receiveValue: { [weak self] response in + guard let self = self else { return } + let totalPages = ceil(Double(response.totalRecordCount ?? 0) / 100.0) + self.totalPages = Int(totalPages) + self.hasPreviousPage = self.currentPage > 0 + self.hasNextPage = self.currentPage < self.totalPages - 1 + self.items.append(contentsOf: response.items ?? []) + }) + .store(in: &cancellables) + } func requestNextPage() { currentPage += 1 requestItems(with: filters) } + + func requestNextPageAsync() { + currentPage += 1 + requestItemsAsync(with: filters) + } func requestPreviousPage() { currentPage -= 1