Merge branch 'main' into subtitle-sizes
This commit is contained in:
commit
63e02f46bf
|
@ -19,6 +19,7 @@ final class ItemCoordinator: NavigationCoordinatable {
|
|||
@Root var start = makeStart
|
||||
@Route(.push) var item = makeItem
|
||||
@Route(.push) var library = makeLibrary
|
||||
@Route(.modal) var itemOverview = makeItemOverview
|
||||
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
|
||||
|
||||
let itemDto: BaseItemDto
|
||||
|
@ -34,6 +35,10 @@ final class ItemCoordinator: NavigationCoordinatable {
|
|||
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||
ItemCoordinator(item: item)
|
||||
}
|
||||
|
||||
func makeItemOverview(item: BaseItemDto) -> NavigationViewCoordinator<ItemOverviewCoordinator> {
|
||||
NavigationViewCoordinator(ItemOverviewCoordinator(item: itemDto))
|
||||
}
|
||||
|
||||
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
|
||||
NavigationViewCoordinator(VideoPlayerCoordinator(viewModel: viewModel))
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
/*
|
||||
* 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 Stinsen
|
||||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
|
||||
final class ItemOverviewCoordinator: NavigationCoordinatable {
|
||||
|
||||
let stack = NavigationStack(initial: \ItemOverviewCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
||||
let item: BaseItemDto
|
||||
|
||||
init(item: BaseItemDto) {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
#if os(tvOS)
|
||||
EmptyView()
|
||||
#else
|
||||
ItemOverviewView(item: item)
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ extension Color {
|
|||
public static let lightGray = Color(UIColor.lightGray)
|
||||
#else
|
||||
public static let systemFill = Color(UIColor.systemFill)
|
||||
public static let systemBackground = Color(UIColor.systemBackground)
|
||||
public static let secondarySystemFill = Color(UIColor.secondarySystemBackground)
|
||||
public static let tertiarySystemFill = Color(UIColor.tertiarySystemBackground)
|
||||
#endif
|
||||
|
|
|
@ -41,45 +41,24 @@ extension BaseItemDto {
|
|||
|
||||
let defaultSubtitleStream = subtitleStreams.first(where: { $0.index! == mediaSource.defaultSubtitleStreamIndex ?? -1 })
|
||||
|
||||
let videoStream = mediaSource.mediaStreams!.first(where: { $0.type! == MediaStreamType.video })
|
||||
|
||||
let audioCodecs = mediaSource.mediaStreams!.filter({ $0.type! == MediaStreamType.audio }).map({ $0.codec! })
|
||||
|
||||
// MARK: basic stream
|
||||
// MARK: Stream
|
||||
var streamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI)!
|
||||
streamURL.path = "/Videos/\(self.id!)/stream"
|
||||
|
||||
let streamType: ServerStreamType
|
||||
|
||||
if let transcodeURL = mediaSource.transcodingUrl {
|
||||
streamType = .transcode
|
||||
streamURL.path = transcodeURL
|
||||
} else {
|
||||
streamType = .direct
|
||||
streamURL.path = "/Videos/\(self.id!)/stream"
|
||||
}
|
||||
|
||||
streamURL.addQueryItem(name: "Static", value: "true")
|
||||
streamURL.addQueryItem(name: "MediaSourceId", value: self.id!)
|
||||
streamURL.addQueryItem(name: "Tag", value: self.etag)
|
||||
streamURL.addQueryItem(name: "MinSegments", value: "6")
|
||||
|
||||
// MARK: hls stream
|
||||
var hlsURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI)!
|
||||
hlsURL.path = "/videos/\(self.id!)/master.m3u8"
|
||||
|
||||
hlsURL.addQueryItem(name: "DeviceId", value: UIDevice.vendorUUIDString)
|
||||
hlsURL.addQueryItem(name: "MediaSourceId", value: self.id!)
|
||||
hlsURL.addQueryItem(name: "VideoCodec", value: videoStream?.codec!)
|
||||
hlsURL.addQueryItem(name: "AudioCodec", value: audioCodecs.joined(separator: ","))
|
||||
hlsURL.addQueryItem(name: "AudioStreamIndex", value: "\(defaultAudioStream!.index!)")
|
||||
hlsURL.addQueryItem(name: "VideoBitrate", value: "\(videoStream!.bitRate!)")
|
||||
hlsURL.addQueryItem(name: "AudioBitrate", value: "\(defaultAudioStream!.bitRate!)")
|
||||
hlsURL.addQueryItem(name: "PlaySessionId", value: response.playSessionId!)
|
||||
hlsURL.addQueryItem(name: "TranscodingMaxAudioChannels", value: "6")
|
||||
hlsURL.addQueryItem(name: "RequireAvc", value: "false")
|
||||
hlsURL.addQueryItem(name: "Tag", value: mediaSource.eTag!)
|
||||
hlsURL.addQueryItem(name: "SegmentContainer", value: "ts")
|
||||
hlsURL.addQueryItem(name: "MinSegments", value: "2")
|
||||
hlsURL.addQueryItem(name: "BreakOnNonKeyFrames", value: "true")
|
||||
hlsURL.addQueryItem(name: "TranscodeReasons", value: "VideoCodecNotSupported,AudioCodecNotSupported")
|
||||
hlsURL.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
||||
|
||||
if defaultSubtitleStream?.index != nil {
|
||||
hlsURL.addQueryItem(name: "SubtitleMethod", value: "Encode")
|
||||
hlsURL.addQueryItem(name: "SubtitleStreamIndex", value: "\(defaultSubtitleStream!.index!)")
|
||||
}
|
||||
|
||||
// MARK: VidoPlayerViewModel Creation
|
||||
|
||||
var subtitle: String? = nil
|
||||
|
@ -110,7 +89,7 @@ extension BaseItemDto {
|
|||
title: modifiedSelfItem.name ?? "",
|
||||
subtitle: subtitle,
|
||||
streamURL: streamURL.url!,
|
||||
hlsURL: hlsURL.url!,
|
||||
streamType: streamType,
|
||||
response: response,
|
||||
audioStreams: audioStreams,
|
||||
subtitleStreams: subtitleStreams,
|
||||
|
|
|
@ -15,13 +15,13 @@ class LogManager {
|
|||
let log = Puppy()
|
||||
|
||||
init() {
|
||||
let console = ConsoleLogger("me.vigue.jellyfin.ConsoleLogger")
|
||||
let console = ConsoleLogger("com.swiftfin.ConsoleLogger")
|
||||
let fileURL = self.getDocumentsDirectory().appendingPathComponent("logs.txt")
|
||||
let FM = FileManager()
|
||||
_ = try? FM.removeItem(at: fileURL)
|
||||
|
||||
do {
|
||||
let file = try FileLogger("me.vigue.jellyfin", fileURL: fileURL)
|
||||
let file = try FileLogger("com.swiftfin", fileURL: fileURL)
|
||||
file.format = LogFormatter()
|
||||
log.add(file, withLevel: .debug)
|
||||
} catch let err {
|
||||
|
|
|
@ -14,10 +14,11 @@ import JellyfinAPI
|
|||
|
||||
final class HomeViewModel: ViewModel {
|
||||
|
||||
@Published var librariesShowRecentlyAddedIDs: [String] = []
|
||||
@Published var libraries: [BaseItemDto] = []
|
||||
@Published var latestAddedItems: [BaseItemDto] = []
|
||||
@Published var resumeItems: [BaseItemDto] = []
|
||||
@Published var nextUpItems: [BaseItemDto] = []
|
||||
@Published var librariesShowRecentlyAddedIDs: [String] = []
|
||||
@Published var libraries: [BaseItemDto] = []
|
||||
|
||||
// temp
|
||||
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])
|
||||
|
@ -59,6 +60,7 @@ final class HomeViewModel: ViewModel {
|
|||
LogManager.shared.log.debug("Refresh called.")
|
||||
|
||||
refreshLibrariesLatest()
|
||||
refreshLatestAddedItems()
|
||||
refreshResumeItems()
|
||||
refreshNextUpItems()
|
||||
}
|
||||
|
@ -111,13 +113,34 @@ final class HomeViewModel: ViewModel {
|
|||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: Latest Added Items
|
||||
private func refreshLatestAddedItems() {
|
||||
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
|
||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
|
||||
enableImageTypes: [.primary, .backdrop, .thumb],
|
||||
enableUserData: true,
|
||||
limit: 8)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .finished: ()
|
||||
case .failure:
|
||||
self.nextUpItems = []
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
}
|
||||
} receiveValue: { items in
|
||||
LogManager.shared.log.debug("Retrieved \(String(items.count)) resume items")
|
||||
|
||||
self.latestAddedItems = items
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: Resume Items
|
||||
private func refreshResumeItems() {
|
||||
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12,
|
||||
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id,
|
||||
limit: 6,
|
||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
|
||||
mediaTypes: ["Video"],
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: [.primary, .backdrop, .thumb])
|
||||
enableUserData: true)
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
|
@ -136,8 +159,10 @@ final class HomeViewModel: ViewModel {
|
|||
|
||||
// MARK: Next Up Items
|
||||
private func refreshNextUpItems() {
|
||||
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, limit: 12,
|
||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters])
|
||||
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
|
||||
limit: 6,
|
||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
|
||||
enableUserData: true)
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
|
|
|
@ -17,14 +17,9 @@ class ItemViewModel: ViewModel {
|
|||
@Published var item: BaseItemDto
|
||||
@Published var playButtonItem: BaseItemDto? {
|
||||
didSet {
|
||||
playButtonItem?.createVideoPlayerViewModel()
|
||||
.sink { completion in
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { videoPlayerViewModel in
|
||||
self.itemVideoPlayerViewModel = videoPlayerViewModel
|
||||
self.mediaItems = videoPlayerViewModel.item.createMediaItems()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
if let playButtonItem = playButtonItem {
|
||||
refreshItemVideoPlayerViewModel(for: playButtonItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var similarItems: [BaseItemDto] = []
|
||||
|
@ -52,6 +47,10 @@ class ItemViewModel: ViewModel {
|
|||
|
||||
getSimilarItems()
|
||||
|
||||
refreshItemVideoPlayerViewModel(for: item)
|
||||
}
|
||||
|
||||
func refreshItemVideoPlayerViewModel(for item: BaseItemDto) {
|
||||
item.createVideoPlayerViewModel()
|
||||
.sink { completion in
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
/*
|
||||
* 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 Foundation
|
||||
|
||||
enum ServerStreamType {
|
||||
case direct
|
||||
case transcode
|
||||
}
|
|
@ -85,12 +85,12 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
let title: String
|
||||
let subtitle: String?
|
||||
let streamURL: URL
|
||||
let hlsURL: URL
|
||||
let audioStreams: [MediaStream]
|
||||
let subtitleStreams: [MediaStream]
|
||||
let overlayType: OverlayType
|
||||
let jumpGesturesEnabled: Bool
|
||||
let resumeOffset: Bool
|
||||
let streamType: ServerStreamType
|
||||
|
||||
// MARK: Experimental
|
||||
let syncSubtitleStateWithAdjacent: Bool
|
||||
|
@ -141,7 +141,7 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
title: String,
|
||||
subtitle: String?,
|
||||
streamURL: URL,
|
||||
hlsURL: URL,
|
||||
streamType: ServerStreamType,
|
||||
response: PlaybackInfoResponse,
|
||||
audioStreams: [MediaStream],
|
||||
subtitleStreams: [MediaStream],
|
||||
|
@ -157,7 +157,7 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.streamURL = streamURL
|
||||
self.hlsURL = hlsURL
|
||||
self.streamType = streamType
|
||||
self.response = response
|
||||
self.audioStreams = audioStreams
|
||||
self.subtitleStreams = subtitleStreams
|
|
@ -7,7 +7,6 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import CachedAsyncImage
|
||||
import SwiftUI
|
||||
|
||||
struct ImageView: View {
|
||||
|
@ -41,7 +40,7 @@ struct ImageView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
CachedAsyncImage(url: source, urlCache: .imageCache, transaction: Transaction(animation: .easeInOut)) { phase in
|
||||
AsyncImage(url: source, transaction: Transaction(animation: .easeInOut)) { phase in
|
||||
switch phase {
|
||||
case .success(let image):
|
||||
image
|
||||
|
@ -69,8 +68,3 @@ struct ImageView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URLCache {
|
||||
|
||||
static let imageCache = URLCache(memoryCapacity: 512*1000*1000, diskCapacity: 10*1000*1000*1000)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ struct ParallaxHeaderScrollView<Header: View, StaticOverlayView: View, Content:
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
ScrollView(showsIndicators: false) {
|
||||
GeometryReader { proxy in
|
||||
let yOffset = proxy.frame(in: .global).minY > 0 ? -proxy.frame(in: .global).minY : 0
|
||||
header
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
/*
|
||||
* 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
|
||||
|
||||
extension View {
|
||||
|
||||
/// Applies Portrait Poster frame with proper corner radius ratio against the width
|
||||
func portraitPoster(width: CGFloat) -> some View {
|
||||
self.frame(width: width, height: width * 1.5)
|
||||
.cornerRadius((width * 1.5) / 40)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
/*
|
||||
* 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 JellyfinAPI
|
||||
import Nuke
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
class DynamicCinematicBackgroundViewModel: ObservableObject {
|
||||
|
||||
@Published var currentItem: BaseItemDto?
|
||||
@Published var currentImageView: UIImageView?
|
||||
|
||||
func select(item: BaseItemDto) {
|
||||
|
||||
guard item.id != currentItem?.id else { return }
|
||||
|
||||
currentItem = item
|
||||
|
||||
let itemImageView = UIImageView()
|
||||
|
||||
let backdropImage: URL
|
||||
|
||||
if item.itemType == .episode {
|
||||
backdropImage = item.getSeriesBackdropImage(maxWidth: 1920)
|
||||
} else {
|
||||
backdropImage = item.getBackdropImage(maxWidth: 1920)
|
||||
}
|
||||
|
||||
let options = ImageLoadingOptions(transition: .fadeIn(duration: 0.2))
|
||||
|
||||
Nuke.loadImage(with: backdropImage, options: options, into: itemImageView, completion: { _ in })
|
||||
|
||||
currentImageView = itemImageView
|
||||
}
|
||||
}
|
||||
|
||||
struct CinematicBackgroundView: UIViewRepresentable {
|
||||
|
||||
@ObservedObject var viewModel: DynamicCinematicBackgroundViewModel
|
||||
|
||||
func updateUIView(_ uiView: UICinematicBackgroundView, context: Context) {
|
||||
uiView.update(imageView: viewModel.currentImageView ?? UIImageView())
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> UICinematicBackgroundView {
|
||||
return UICinematicBackgroundView(initialImageView: viewModel.currentImageView ?? UIImageView())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
/*
|
||||
* 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 JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct CinematicNextUpCardView: View {
|
||||
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
let item: BaseItemDto
|
||||
let showOverlay: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Button {
|
||||
homeRouter.route(to: \.modalItem, item)
|
||||
} label: {
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
|
||||
if item.itemType == .episode {
|
||||
ImageView(src: item.getSeriesBackdropImage(maxWidth: 350))
|
||||
.frame(width: 350, height: 210)
|
||||
} else {
|
||||
ImageView(src: item.getBackdropImage(maxWidth: 350))
|
||||
.frame(width: 350, height: 210)
|
||||
}
|
||||
|
||||
LinearGradient(colors: [.clear, .black],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
.frame(height: 105)
|
||||
.ignoresSafeArea()
|
||||
|
||||
if showOverlay {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text("Next")
|
||||
.font(.subheadline)
|
||||
.padding(.vertical, 5)
|
||||
.padding(.leading, 10)
|
||||
.foregroundColor(.white)
|
||||
|
||||
HStack {
|
||||
Color.clear
|
||||
.frame(width: 1, height: 7)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 350, height: 210)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.padding(.top)
|
||||
}
|
||||
.padding(.vertical)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
/*
|
||||
* 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 JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct CinematicResumeCardView: View {
|
||||
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
let item: BaseItemDto
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Button {
|
||||
homeRouter.route(to: \.modalItem, item)
|
||||
} label: {
|
||||
ZStack(alignment: .bottom) {
|
||||
|
||||
if item.itemType == .episode {
|
||||
ImageView(src: item.getSeriesBackdropImage(maxWidth: 350))
|
||||
.frame(width: 350, height: 210)
|
||||
} else {
|
||||
ImageView(src: item.getBackdropImage(maxWidth: 350))
|
||||
.frame(width: 350, height: 210)
|
||||
}
|
||||
|
||||
LinearGradient(colors: [.clear, .black],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
.frame(height: 105)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(item.getItemProgressString() ?? "")
|
||||
.font(.subheadline)
|
||||
.padding(.vertical, 5)
|
||||
.padding(.leading, 10)
|
||||
.foregroundColor(.white)
|
||||
|
||||
HStack {
|
||||
Color(UIColor.systemPurple)
|
||||
.frame(width: 350 * (item.userData?.playedPercentage ?? 0) / 100, height: 7)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 350, height: 210)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.padding(.top)
|
||||
}
|
||||
.padding(.vertical)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
/*
|
||||
* 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 UIKit
|
||||
import JellyfinAPI
|
||||
|
||||
// TODO: Generalize this view such that it can be used in other contexts like for a library
|
||||
|
||||
struct HomeCinematicViewItem: Hashable {
|
||||
|
||||
enum TopRowType {
|
||||
case resume
|
||||
case nextUp
|
||||
case plain
|
||||
}
|
||||
|
||||
let item: BaseItemDto
|
||||
let type: TopRowType
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(item)
|
||||
hasher.combine(type)
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeCinematicView: View {
|
||||
|
||||
@FocusState var selectedItem: BaseItemDto?
|
||||
@State private var updatedSelectedItem: BaseItemDto?
|
||||
@State private var initiallyAppeared = false
|
||||
private let forcedItemSubtitle: String?
|
||||
private let items: [HomeCinematicViewItem]
|
||||
private let backgroundViewModel = DynamicCinematicBackgroundViewModel()
|
||||
|
||||
init(items: [HomeCinematicViewItem], forcedItemSubtitle: String? = nil) {
|
||||
self.items = items
|
||||
self.forcedItemSubtitle = forcedItemSubtitle
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
ZStack(alignment: .bottom) {
|
||||
|
||||
CinematicBackgroundView(viewModel: backgroundViewModel)
|
||||
.frame(height: UIScreen.main.bounds.height - 10)
|
||||
|
||||
LinearGradient(stops: [.init(color: .clear, location: 0.5),
|
||||
.init(color: .black.opacity(0.6), location: 0.7),
|
||||
.init(color: .black, location: 1)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
||||
if let forcedItemSubtitle = forcedItemSubtitle {
|
||||
Text(forcedItemSubtitle)
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(Color.secondary)
|
||||
} else {
|
||||
if updatedSelectedItem?.itemType == .episode {
|
||||
Text(updatedSelectedItem?.getEpisodeLocator() ?? "")
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(Color.secondary)
|
||||
} else {
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
|
||||
Text("\(updatedSelectedItem?.seriesName ?? updatedSelectedItem?.name ?? "")")
|
||||
.font(.title)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.horizontal, 50)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(items, id: \.self) { item in
|
||||
switch item.type {
|
||||
case .nextUp:
|
||||
CinematicNextUpCardView(item: item.item, showOverlay: true)
|
||||
.focused($selectedItem, equals: item.item)
|
||||
case .resume:
|
||||
CinematicResumeCardView(item: item.item)
|
||||
.focused($selectedItem, equals: item.item)
|
||||
case .plain:
|
||||
CinematicNextUpCardView(item: item.item, showOverlay: false)
|
||||
.focused($selectedItem, equals: item.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 50)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.focusSection()
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedItem) { newValue in
|
||||
if let newItem = newValue {
|
||||
backgroundViewModel.select(item: newItem)
|
||||
updatedSelectedItem = newItem
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
guard !initiallyAppeared else { return }
|
||||
selectedItem = items.first?.item
|
||||
updatedSelectedItem = items.first?.item
|
||||
initiallyAppeared = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
/*
|
||||
* 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 UIKit
|
||||
|
||||
class UICinematicBackgroundView: UIView {
|
||||
|
||||
private var currentImageView: UIView?
|
||||
|
||||
private var selectDelayTimer: Timer?
|
||||
|
||||
init(initialImageView: UIImageView) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
initialImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
initialImageView.alpha = 0
|
||||
|
||||
addSubview(initialImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
initialImageView.topAnchor.constraint(equalTo: topAnchor),
|
||||
initialImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
initialImageView.leftAnchor.constraint(equalTo: leftAnchor),
|
||||
initialImageView.rightAnchor.constraint(equalTo: rightAnchor)
|
||||
])
|
||||
|
||||
self.currentImageView = initialImageView
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(imageView: UIImageView) {
|
||||
|
||||
selectDelayTimer?.invalidate()
|
||||
|
||||
selectDelayTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(delayTimerTimed), userInfo: imageView, repeats: false)
|
||||
|
||||
}
|
||||
|
||||
@objc private func delayTimerTimed(timer: Timer) {
|
||||
let newImageView = timer.userInfo as! UIImageView
|
||||
|
||||
newImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
newImageView.alpha = 0
|
||||
|
||||
addSubview(newImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
newImageView.topAnchor.constraint(equalTo: topAnchor),
|
||||
newImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
newImageView.leftAnchor.constraint(equalTo: leftAnchor),
|
||||
newImageView.rightAnchor.constraint(equalTo: rightAnchor)
|
||||
])
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
newImageView.alpha = 1
|
||||
self.currentImageView?.alpha = 0
|
||||
} completion: { _ in
|
||||
self.currentImageView?.removeFromSuperview()
|
||||
self.currentImageView = newImageView
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -6,13 +6,16 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
import Stinsen
|
||||
|
||||
struct ConnectToServerView: View {
|
||||
|
||||
@StateObject var viewModel = ConnectToServerViewModel()
|
||||
@StateObject var viewModel: ConnectToServerViewModel
|
||||
@State var uri = ""
|
||||
|
||||
@Default(.defaultHTTPScheme) var defaultHTTPScheme
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
|
@ -21,6 +24,12 @@ struct ConnectToServerView: View {
|
|||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
.onAppear {
|
||||
if uri == "" {
|
||||
uri = "\(defaultHTTPScheme.rawValue)://"
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
viewModel.connectToServer(uri: uri)
|
||||
} label: {
|
||||
|
|
|
@ -7,13 +7,16 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
|
||||
struct HomeView: View {
|
||||
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
@ObservedObject var viewModel = HomeViewModel()
|
||||
@Default(.showPosterLabels) var showPosterLabels
|
||||
|
||||
@State var showingSettings = false
|
||||
|
||||
|
@ -24,16 +27,33 @@ struct HomeView: View {
|
|||
} else {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
if !viewModel.resumeItems.isEmpty {
|
||||
ContinueWatchingView(items: viewModel.resumeItems)
|
||||
}
|
||||
|
||||
if !viewModel.nextUpItems.isEmpty {
|
||||
NextUpView(items: viewModel.nextUpItems)
|
||||
if viewModel.resumeItems.isEmpty {
|
||||
HomeCinematicView(items: viewModel.latestAddedItems.map({ .init(item: $0, type: .plain) }),
|
||||
forcedItemSubtitle: "Recently Added")
|
||||
|
||||
if !viewModel.nextUpItems.isEmpty {
|
||||
NextUpView(items: viewModel.nextUpItems)
|
||||
.focusSection()
|
||||
}
|
||||
} else {
|
||||
HomeCinematicView(items: viewModel.resumeItems.map({ .init(item: $0, type: .resume) }))
|
||||
|
||||
if !viewModel.nextUpItems.isEmpty {
|
||||
NextUpView(items: viewModel.nextUpItems)
|
||||
.focusSection()
|
||||
}
|
||||
|
||||
PortraitItemsRowView(rowTitle: "Recently Added",
|
||||
items: viewModel.latestAddedItems,
|
||||
showItemTitles: showPosterLabels) { item in
|
||||
homeRouter.route(to: \.modalItem, item)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(viewModel.libraries, id: \.self) { library in
|
||||
LatestMediaView(viewModel: LatestMediaViewModel(library: library))
|
||||
.focusSection()
|
||||
}
|
||||
|
||||
Spacer(minLength: 100)
|
||||
|
@ -52,6 +72,7 @@ struct HomeView: View {
|
|||
.focusSection()
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,12 @@ struct CinematicItemAboutView: View {
|
|||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 257))
|
||||
.frame(width: 257, height: 380)
|
||||
.cornerRadius(10)
|
||||
.portraitPoster(width: 257)
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
Color(UIColor.darkGray).opacity(focused ? 0.2 : 0)
|
||||
.cornerRadius(30)
|
||||
.frame(height: 380)
|
||||
.cornerRadius(9.5)
|
||||
.frame(height: 385.5)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("About")
|
||||
|
|
|
@ -1,164 +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 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import AVKit
|
||||
import Combine
|
||||
import JellyfinAPI
|
||||
import UIKit
|
||||
|
||||
class NativePlayerViewController: AVPlayerViewController {
|
||||
|
||||
let viewModel: VideoPlayerViewModel
|
||||
|
||||
var timeObserverToken: Any?
|
||||
|
||||
var lastProgressTicks: Int64 = 0
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(viewModel: VideoPlayerViewModel) {
|
||||
|
||||
self.viewModel = viewModel
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
let player = AVPlayer(url: viewModel.hlsURL)
|
||||
|
||||
player.appliesMediaSelectionCriteriaAutomatically = false
|
||||
player.currentItem?.externalMetadata = createMetadata()
|
||||
player.currentItem?.navigationMarkerGroups = createNavigationMarkerGroups()
|
||||
|
||||
// let chevron = UIImage(systemName: "chevron.right.circle.fill")!
|
||||
// let testAction = UIAction(title: "Next", image: chevron) { action in
|
||||
// SessionAPI.sendSystemCommand(sessionId: viewModel.response.playSessionId!, command: .setSubtitleStreamIndex)
|
||||
// .sink { completion in
|
||||
// print(completion)
|
||||
// } receiveValue: { _ in
|
||||
// print("idk but we're here")
|
||||
// }
|
||||
// .store(in: &self.cancellables)
|
||||
// }
|
||||
|
||||
// self.transportBarCustomMenuItems = [testAction]
|
||||
|
||||
// self.infoViewActions.append(testAction)
|
||||
|
||||
let timeScale = CMTimeScale(NSEC_PER_SEC)
|
||||
let time = CMTime(seconds: 5, preferredTimescale: timeScale)
|
||||
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in
|
||||
// print("Timer timed: \(time)")
|
||||
|
||||
if time.seconds != 0 {
|
||||
self?.sendProgressReport(seconds: time.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
self.player = player
|
||||
|
||||
self.allowsPictureInPicturePlayback = true
|
||||
self.player?.allowsExternalPlayback = true
|
||||
}
|
||||
|
||||
private func createMetadata() -> [AVMetadataItem] {
|
||||
let allMetadata: [AVMetadataIdentifier: Any] = [
|
||||
.commonIdentifierTitle: viewModel.title,
|
||||
.iTunesMetadataTrackSubTitle: viewModel.subtitle ?? "",
|
||||
.commonIdentifierArtwork: UIImage(data: try! Data(contentsOf: viewModel.item.getBackdropImage(maxWidth: 200)))?.pngData() as Any,
|
||||
.commonIdentifierDescription: viewModel.item.overview ?? "",
|
||||
.iTunesMetadataContentRating: viewModel.item.officialRating ?? "",
|
||||
.quickTimeMetadataGenre: viewModel.item.genres?.first ?? ""
|
||||
]
|
||||
|
||||
return allMetadata.compactMap { createMetadataItem(for:$0, value:$1) }
|
||||
}
|
||||
|
||||
private func createMetadataItem(for identifier: AVMetadataIdentifier,
|
||||
value: Any) -> AVMetadataItem {
|
||||
let item = AVMutableMetadataItem()
|
||||
item.identifier = identifier
|
||||
item.value = value as? NSCopying & NSObjectProtocol
|
||||
// Specify "und" to indicate an undefined language.
|
||||
item.extendedLanguageTag = "und"
|
||||
return item.copy() as! AVMetadataItem
|
||||
}
|
||||
|
||||
private func createNavigationMarkerGroups() -> [AVNavigationMarkersGroup] {
|
||||
guard let chapters = viewModel.item.chapters else { return [] }
|
||||
|
||||
var metadataGroups: [AVTimedMetadataGroup] = []
|
||||
|
||||
// TODO: Determine range between chapters
|
||||
chapters.forEach { chapterInfo in
|
||||
var chapterMetadata: [AVMetadataItem] = []
|
||||
|
||||
let titleItem = createMetadataItem(for: .commonIdentifierTitle, value: chapterInfo.name ?? "No Name")
|
||||
chapterMetadata.append(titleItem)
|
||||
|
||||
let imageItem = createMetadataItem(for: .commonIdentifierArtwork, value: UIImage(data: try! Data(contentsOf: viewModel.item.getBackdropImage(maxWidth: 200)))?.pngData() as Any)
|
||||
chapterMetadata.append(imageItem)
|
||||
|
||||
let startTime = CMTimeMake(value: chapterInfo.startPositionTicks ?? 0, timescale: 10_000_000)
|
||||
let endTime = CMTimeMake(value: (chapterInfo.startPositionTicks ?? 0) + 50_000_000, timescale: 10_000_000)
|
||||
let timeRange = CMTimeRangeFromTimeToTime(start: startTime, end: endTime)
|
||||
|
||||
metadataGroups.append(AVTimedMetadataGroup(items: chapterMetadata, timeRange: timeRange))
|
||||
}
|
||||
|
||||
return [AVNavigationMarkersGroup(title: nil, timedNavigationMarkers: metadataGroups)]
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
stop()
|
||||
removePeriodicTimeObserver()
|
||||
}
|
||||
|
||||
func removePeriodicTimeObserver() {
|
||||
if let timeObserverToken = timeObserverToken {
|
||||
player?.removeTimeObserver(timeObserverToken)
|
||||
self.timeObserverToken = nil
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
player?.seek(to: CMTimeMake(value: viewModel.item.userData?.playbackPositionTicks ?? 0, timescale: 10_000_000), toleranceBefore: CMTimeMake(value: 5, timescale: 1), toleranceAfter: CMTimeMake(value: 5, timescale: 1), completionHandler: { _ in
|
||||
self.play()
|
||||
})
|
||||
}
|
||||
|
||||
private func play() {
|
||||
player?.play()
|
||||
|
||||
// viewModel.sendPlayReport(startTimeTicks: viewModel.item.userData?.playbackPositionTicks ?? 0)
|
||||
viewModel.sendPlayReport()
|
||||
}
|
||||
|
||||
private func sendProgressReport(seconds: Double) {
|
||||
// viewModel.sendProgressReport(ticks: Int64(seconds) * 10_000_000)
|
||||
viewModel.sendProgressReport()
|
||||
}
|
||||
|
||||
private func stop() {
|
||||
self.player?.pause()
|
||||
viewModel.sendStopReport()
|
||||
// viewModel.sendStopReport(ticks: 10_000_000)
|
||||
}
|
||||
}
|
|
@ -10,22 +10,6 @@
|
|||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct NativePlayerView: UIViewControllerRepresentable {
|
||||
|
||||
let viewModel: VideoPlayerViewModel
|
||||
|
||||
typealias UIViewControllerType = NativePlayerViewController
|
||||
|
||||
func makeUIViewController(context: Context) -> NativePlayerViewController {
|
||||
|
||||
return NativePlayerViewController(viewModel: viewModel)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct VLCPlayerView: UIViewControllerRepresentable {
|
||||
|
||||
let viewModel: VideoPlayerViewModel
|
||||
|
|
|
@ -140,7 +140,7 @@ struct tvOSVLCOverlay_Previews: PreviewProvider {
|
|||
title: "Glorious Purpose",
|
||||
subtitle: "Loki - S1E1",
|
||||
streamURL: URL(string: "www.apple.com")!,
|
||||
hlsURL: URL(string: "www.apple.com")!,
|
||||
streamType: .direct,
|
||||
response: PlaybackInfoResponse(),
|
||||
audioStreams: [MediaStream(displayTitle: "English", index: -1)],
|
||||
subtitleStreams: [MediaStream(displayTitle: "None", index: -1)],
|
||||
|
|
|
@ -252,9 +252,16 @@
|
|||
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
|
||||
C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; };
|
||||
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; };
|
||||
E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */; };
|
||||
E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */; };
|
||||
E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */; };
|
||||
E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */; };
|
||||
E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */; };
|
||||
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; };
|
||||
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; };
|
||||
E107BB972788104100354E07 /* CinematicCollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB952788104100354E07 /* CinematicCollectionItemView.swift */; };
|
||||
E10C0941278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */; };
|
||||
E10C0942278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */; };
|
||||
E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */; };
|
||||
E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */; };
|
||||
E10D87DE278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
|
||||
|
@ -280,6 +287,8 @@
|
|||
E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */; };
|
||||
E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */; };
|
||||
E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */; };
|
||||
E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; };
|
||||
E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; };
|
||||
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
|
||||
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
|
||||
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
|
||||
|
@ -390,7 +399,6 @@
|
|||
E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */; };
|
||||
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */; };
|
||||
E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */; };
|
||||
E1C812CB277AE40900918266 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C7277AE40900918266 /* NativePlayerViewController.swift */; };
|
||||
E1C812CC277AE40A00918266 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C8277AE40900918266 /* VideoPlayerView.swift */; };
|
||||
E1C812CD277AE40A00918266 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C9277AE40900918266 /* VideoPlayerViewModel.swift */; };
|
||||
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C9277AE40900918266 /* VideoPlayerViewModel.swift */; };
|
||||
|
@ -414,8 +422,6 @@
|
|||
E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; };
|
||||
E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; };
|
||||
E1E00A37278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; };
|
||||
E1E0F4D8278911680084F701 /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = E1E0F4D7278911680084F701 /* CachedAsyncImage */; };
|
||||
E1E0F4DA278911A30084F701 /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = E1E0F4D9278911A30084F701 /* CachedAsyncImage */; };
|
||||
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; };
|
||||
E1E5D5372783A52C00692DFE /* CinematicEpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5362783A52C00692DFE /* CinematicEpisodeItemView.swift */; };
|
||||
E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */; };
|
||||
|
@ -430,6 +436,10 @@
|
|||
E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */; };
|
||||
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */; };
|
||||
E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D552278419D900692DFE /* ConfirmCloseOverlay.swift */; };
|
||||
E1EBCB42278BD174009FE6E9 /* TruncatedTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */; };
|
||||
E1EBCB44278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */; };
|
||||
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */; };
|
||||
E1EBCB4A278BE443009FE6E9 /* ItemOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */; };
|
||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
||||
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
||||
E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */; };
|
||||
|
@ -647,8 +657,14 @@
|
|||
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
|
||||
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
||||
E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICinematicBackgroundView.swift; sourceTree = "<group>"; };
|
||||
E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicBackgroundView.swift; sourceTree = "<group>"; };
|
||||
E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCinematicView.swift; sourceTree = "<group>"; };
|
||||
E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicResumeCardView.swift; sourceTree = "<group>"; };
|
||||
E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicNextUpCardView.swift; sourceTree = "<group>"; };
|
||||
E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = "<group>"; };
|
||||
E107BB952788104100354E07 /* CinematicCollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicCollectionItemView.swift; sourceTree = "<group>"; };
|
||||
E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemSize.swift; sourceTree = "<group>"; };
|
||||
E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewDetailsView.swift; sourceTree = "<group>"; };
|
||||
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowView.swift; sourceTree = "<group>"; };
|
||||
E10D87DD278510E300BD264C /* PosterSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterSize.swift; sourceTree = "<group>"; };
|
||||
|
@ -659,6 +675,7 @@
|
|||
E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = "<group>"; };
|
||||
E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaStreamExtension.swift; sourceTree = "<group>"; };
|
||||
E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; };
|
||||
E126F740278A656C00A522BF /* ServerStreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerStreamType.swift; sourceTree = "<group>"; };
|
||||
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = "<group>"; };
|
||||
E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
E13D02842788B634000FCB04 /* Swiftfin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swiftfin.entitlements; sourceTree = "<group>"; };
|
||||
|
@ -720,7 +737,6 @@
|
|||
E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerOverlayView.swift; sourceTree = "<group>"; };
|
||||
E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsExtensions.swift; sourceTree = "<group>"; };
|
||||
E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = "<group>"; };
|
||||
E1C812C7277AE40900918266 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; };
|
||||
E1C812C8277AE40900918266 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
|
||||
E1C812C9277AE40900918266 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
||||
E1C812D0277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSVideoPlayerCoordinator.swift; sourceTree = "<group>"; };
|
||||
|
@ -746,6 +762,9 @@
|
|||
E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = "<group>"; };
|
||||
E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsView.swift; sourceTree = "<group>"; };
|
||||
E1E5D552278419D900692DFE /* ConfirmCloseOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmCloseOverlay.swift; sourceTree = "<group>"; };
|
||||
E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedTextView.swift; sourceTree = "<group>"; };
|
||||
E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemOverviewCoordinator.swift; sourceTree = "<group>"; };
|
||||
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemOverviewView.swift; sourceTree = "<group>"; };
|
||||
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; };
|
||||
E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallMenuOverlay.swift; sourceTree = "<group>"; };
|
||||
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
|
||||
|
@ -767,7 +786,6 @@
|
|||
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
|
||||
E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */,
|
||||
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
|
||||
E1E0F4DA278911A30084F701 /* CachedAsyncImage in Frameworks */,
|
||||
E1AE8E7E2789136D00FBDDAA /* Nuke in Frameworks */,
|
||||
E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */,
|
||||
E12186DE2718F1C50010884C /* Defaults in Frameworks */,
|
||||
|
@ -788,7 +806,6 @@
|
|||
E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */,
|
||||
53352571265EA0A0006CCA86 /* Introspect in Frameworks */,
|
||||
E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */,
|
||||
E1E0F4D8278911680084F701 /* CachedAsyncImage in Frameworks */,
|
||||
E1AE8E7C2789135A00FBDDAA /* Nuke in Frameworks */,
|
||||
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */,
|
||||
E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */,
|
||||
|
@ -828,7 +845,6 @@
|
|||
5310694F2684E7EE00CFFDBA /* VideoPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1C812C7277AE40900918266 /* NativePlayerViewController.swift */,
|
||||
E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */,
|
||||
E178859C2780F5300094FBCF /* tvOSSLider */,
|
||||
E17885A7278130690094FBCF /* tvOSOverlay */,
|
||||
|
@ -862,7 +878,7 @@
|
|||
E13DD3F82717E961009D4DAF /* UserListViewModel.swift */,
|
||||
E13DD3EB27178A54009D4DAF /* UserSignInViewModel.swift */,
|
||||
09389CC626819B4500AE350E /* VideoPlayerModel.swift */,
|
||||
E1C812C9277AE40900918266 /* VideoPlayerViewModel.swift */,
|
||||
E126F73F278A655300A522BF /* VideoPlayerViewModel */,
|
||||
625CB57B2678CE1000530A6E /* ViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
|
@ -980,7 +996,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */,
|
||||
E13F26B22787597300DF4761 /* SingleSeasonEpisodesRowView.swift */,
|
||||
E103A6A1278A7EB500820EC7 /* HomeCinematicView */,
|
||||
E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */,
|
||||
531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */,
|
||||
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */,
|
||||
|
@ -990,6 +1006,7 @@
|
|||
E1E5D5412783B33900692DFE /* PortraitItemsRowView.swift */,
|
||||
536D3D87267C17350004248C /* PublicUserButton.swift */,
|
||||
E17885A3278105170094FBCF /* SFSymbolButton.swift */,
|
||||
E13F26B22787597300DF4761 /* SingleSeasonEpisodesRowView.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1196,11 +1213,13 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */,
|
||||
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */,
|
||||
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
|
||||
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
|
||||
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */,
|
||||
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */,
|
||||
E1AA331C2782541500F6439C /* PrimaryButtonView.swift */,
|
||||
E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1268,11 +1287,12 @@
|
|||
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */,
|
||||
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */,
|
||||
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
|
||||
E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */,
|
||||
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */,
|
||||
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */,
|
||||
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */,
|
||||
C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */,
|
||||
C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */,
|
||||
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */,
|
||||
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */,
|
||||
E193D5412719404B00900D82 /* MainCoordinator */,
|
||||
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */,
|
||||
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
|
||||
|
@ -1334,6 +1354,18 @@
|
|||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E103A6A1278A7EB500820EC7 /* HomeCinematicView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */,
|
||||
E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */,
|
||||
E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */,
|
||||
E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */,
|
||||
E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */,
|
||||
);
|
||||
path = HomeCinematicView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E107BB9127880A4000354E07 /* ItemViewModel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1384,6 +1416,15 @@
|
|||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E126F73F278A655300A522BF /* VideoPlayerViewModel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E126F740278A656C00A522BF /* ServerStreamType.swift */,
|
||||
E1C812C9277AE40900918266 /* VideoPlayerViewModel.swift */,
|
||||
);
|
||||
path = VideoPlayerViewModel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E13DD3BB27163C3E009D4DAF /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1410,18 +1451,19 @@
|
|||
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
||||
5389276D263C25100035E14B /* ContinueWatchingView.swift */,
|
||||
625CB56E2678C23300530A6E /* HomeView.swift */,
|
||||
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */,
|
||||
E14F7D0A26DB3714007C3AE6 /* ItemView */,
|
||||
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */,
|
||||
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
|
||||
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
|
||||
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */,
|
||||
6213388F265F83A900A81A2A /* LibraryListView.swift */,
|
||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
|
||||
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
|
||||
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
|
||||
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
|
||||
5389276F263C25230035E14B /* NextUpView.swift */,
|
||||
E1E5D54A2783E26100692DFE /* SettingsView */,
|
||||
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */,
|
||||
E13DD3E427177D15009D4DAF /* ServerListView.swift */,
|
||||
E1E5D54A2783E26100692DFE /* SettingsView */,
|
||||
E13DD3FB2717EAE8009D4DAF /* UserListView.swift */,
|
||||
E13DD3F4271793BB009D4DAF /* UserSignInView.swift */,
|
||||
E193D5452719418B00900D82 /* VideoPlayer */,
|
||||
|
@ -1443,7 +1485,6 @@
|
|||
E14F7D0A26DB3714007C3AE6 /* ItemView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */,
|
||||
535BAE9E2649E569005FA86D /* ItemView.swift */,
|
||||
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */,
|
||||
E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */,
|
||||
|
@ -1540,12 +1581,13 @@
|
|||
E1AD105326D96F5A003E4A08 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */,
|
||||
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */,
|
||||
531AC8BE26750DE20091C7EB /* ImageView.swift */,
|
||||
621338B22660A07800A81A2A /* LazyView.swift */,
|
||||
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */,
|
||||
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */,
|
||||
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */,
|
||||
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */,
|
||||
E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */,
|
||||
624C21742685CF60007F1390 /* SearchablePickerView.swift */,
|
||||
53DE4BD1267098F300739748 /* SearchBarView.swift */,
|
||||
);
|
||||
|
@ -1663,7 +1705,6 @@
|
|||
E1218C9D271A2CD600EA0737 /* CombineExt */,
|
||||
E1A9999A271A343C008E78C0 /* SwiftUICollection */,
|
||||
E178857C278037FD0094FBCF /* JellyfinAPI */,
|
||||
E1E0F4D9278911A30084F701 /* CachedAsyncImage */,
|
||||
E1AE8E7D2789136D00FBDDAA /* Nuke */,
|
||||
);
|
||||
productName = "JellyfinPlayer tvOS";
|
||||
|
@ -1703,7 +1744,6 @@
|
|||
E1A99998271A3429008E78C0 /* SwiftUICollection */,
|
||||
E10EAA44277BB646000269ED /* JellyfinAPI */,
|
||||
E10EAA4C277BB716000269ED /* Sliders */,
|
||||
E1E0F4D7278911680084F701 /* CachedAsyncImage */,
|
||||
E1AE8E7B2789135A00FBDDAA /* Nuke */,
|
||||
);
|
||||
productName = JellyfinPlayer;
|
||||
|
@ -1796,7 +1836,6 @@
|
|||
C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */,
|
||||
E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
|
||||
E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */,
|
||||
E1E0F4D6278911680084F701 /* XCRemoteSwiftPackageReference "SwiftUI-CachedAsyncImage" */,
|
||||
E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||
);
|
||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||
|
@ -2091,6 +2130,7 @@
|
|||
E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */,
|
||||
E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */,
|
||||
6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */,
|
||||
E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */,
|
||||
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
|
||||
C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */,
|
||||
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */,
|
||||
|
@ -2103,13 +2143,13 @@
|
|||
5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */,
|
||||
E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */,
|
||||
E1E5D53B2783A80900692DFE /* CinematicItemViewTopRow.swift in Sources */,
|
||||
E1C812CB277AE40900918266 /* NativePlayerViewController.swift in Sources */,
|
||||
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */,
|
||||
C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
||||
62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||
E1E5D5462783C28100692DFE /* CinematicItemAboutView.swift in Sources */,
|
||||
E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
||||
E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */,
|
||||
E1EBCB4A278BE443009FE6E9 /* ItemOverviewCoordinator.swift in Sources */,
|
||||
53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */,
|
||||
C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
|
||||
536D3D88267C17350004248C /* PublicUserButton.swift in Sources */,
|
||||
|
@ -2118,11 +2158,14 @@
|
|||
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
|
||||
53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */,
|
||||
E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
|
||||
E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */,
|
||||
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */,
|
||||
536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */,
|
||||
E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */,
|
||||
091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
||||
E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */,
|
||||
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
||||
E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */,
|
||||
E1E5D5422783B33900692DFE /* PortraitItemsRowView.swift in Sources */,
|
||||
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */,
|
||||
|
@ -2151,6 +2194,7 @@
|
|||
C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
|
||||
E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */,
|
||||
E10D87DF278510E400BD264C /* PosterSize.swift in Sources */,
|
||||
E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */,
|
||||
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */,
|
||||
E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */,
|
||||
E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */,
|
||||
|
@ -2176,6 +2220,7 @@
|
|||
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
|
||||
5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */,
|
||||
E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */,
|
||||
E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */,
|
||||
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
||||
5398514726B64E4100101B49 /* SearchBarView.swift in Sources */,
|
||||
E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
||||
|
@ -2221,6 +2266,7 @@
|
|||
6264E88D273850380081A12A /* Strings.swift in Sources */,
|
||||
536D3D76267BA9BB0004248C /* MainTabViewModel.swift in Sources */,
|
||||
E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */,
|
||||
E10C0942278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */,
|
||||
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */,
|
||||
E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */,
|
||||
E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */,
|
||||
|
@ -2261,6 +2307,7 @@
|
|||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
||||
E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
|
||||
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */,
|
||||
E1EBCB44278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift in Sources */,
|
||||
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
||||
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */,
|
||||
|
@ -2292,6 +2339,7 @@
|
|||
E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
|
||||
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
||||
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */,
|
||||
E1EBCB42278BD174009FE6E9 /* TruncatedTextView.swift in Sources */,
|
||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
|
||||
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */,
|
||||
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||
|
@ -2347,6 +2395,7 @@
|
|||
E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */,
|
||||
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */,
|
||||
E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */,
|
||||
E10C0941278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */,
|
||||
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
|
||||
|
@ -2364,6 +2413,7 @@
|
|||
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
|
||||
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */,
|
||||
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
|
||||
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
||||
62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
|
||||
5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */,
|
||||
|
@ -2387,6 +2437,7 @@
|
|||
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
|
||||
E1D4BF8A2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */,
|
||||
E13DD3F92717E961009D4DAF /* UserListViewModel.swift in Sources */,
|
||||
E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */,
|
||||
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
|
||||
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
|
||||
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
|
||||
|
@ -3033,14 +3084,6 @@
|
|||
minimumVersion = 5.0.0;
|
||||
};
|
||||
};
|
||||
E1E0F4D6278911680084F701 /* XCRemoteSwiftPackageReference "SwiftUI-CachedAsyncImage" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/lorenzofiamingo/SwiftUI-CachedAsyncImage";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
@ -3189,16 +3232,6 @@
|
|||
package = E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||
productName = Nuke;
|
||||
};
|
||||
E1E0F4D7278911680084F701 /* CachedAsyncImage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E1E0F4D6278911680084F701 /* XCRemoteSwiftPackageReference "SwiftUI-CachedAsyncImage" */;
|
||||
productName = CachedAsyncImage;
|
||||
};
|
||||
E1E0F4D9278911A30084F701 /* CachedAsyncImage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E1E0F4D6278911680084F701 /* XCRemoteSwiftPackageReference "SwiftUI-CachedAsyncImage" */;
|
||||
productName = CachedAsyncImage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 5377CBE9263B596A003A4E83 /* Project object */;
|
||||
|
|
|
@ -100,15 +100,6 @@
|
|||
"version": "1.4.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CachedAsyncImage",
|
||||
"repositoryURL": "https://github.com/lorenzofiamingo/SwiftUI-CachedAsyncImage",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "eb489a699be1f6e6c1a19fecdd6bfdc556474fd6",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Introspect",
|
||||
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect",
|
||||
|
|
|
@ -106,6 +106,13 @@ struct EpisodesRowView: View {
|
|||
bh: episode.getBackdropImageBlurHash())
|
||||
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
||||
.frame(width: 200, height: 112)
|
||||
.overlay {
|
||||
if episode.id == viewModel.episodeItemViewModel.item.id {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(Color.jellyfinPurple, lineWidth: 4)
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(episode.getEpisodeLocator() ?? "")
|
|
@ -46,8 +46,7 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
|
|||
ImageView(src: item.imageURLContsructor(maxWidth: Int(maxWidth)),
|
||||
bh: item.blurHash,
|
||||
failureInitials: item.failureInitials)
|
||||
.frame(width: maxWidth, height: maxWidth * 1.5)
|
||||
.cornerRadius(10)
|
||||
.portraitPoster(width: maxWidth)
|
||||
.shadow(radius: 4, y: 2)
|
||||
|
||||
if item.showTitle {
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
//
|
||||
/*
|
||||
* 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 TruncatedTextView: View {
|
||||
|
||||
@State private var truncated: Bool = false
|
||||
@State private var shrinkText: String
|
||||
private var text: String
|
||||
let font: UIFont
|
||||
let lineLimit: Int
|
||||
let seeMoreAction: () -> Void
|
||||
|
||||
private var moreLessText: String {
|
||||
if !truncated {
|
||||
return ""
|
||||
} else {
|
||||
return "See More"
|
||||
}
|
||||
}
|
||||
|
||||
init(_ text: String,
|
||||
lineLimit: Int,
|
||||
font: UIFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body),
|
||||
seeMoreAction: @escaping () -> Void) {
|
||||
self.text = text
|
||||
self.lineLimit = lineLimit
|
||||
_shrinkText = State(wrappedValue: text)
|
||||
self.font = font
|
||||
self.seeMoreAction = seeMoreAction
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
Group {
|
||||
Text(shrinkText)
|
||||
.overlay {
|
||||
if truncated {
|
||||
LinearGradient(stops: [.init(color: .systemBackground.opacity(0), location: 0.5),
|
||||
.init(color: .systemBackground.opacity(0.8), location: 0.7),
|
||||
.init(color: .systemBackground, location: 1)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.lineLimit(lineLimit)
|
||||
.background {
|
||||
// Render the limited text and measure its size
|
||||
Text(text)
|
||||
.lineLimit(lineLimit + 2)
|
||||
.background {
|
||||
GeometryReader { visibleTextGeometry in
|
||||
Color.clear
|
||||
.onAppear {
|
||||
let size = CGSize(width: visibleTextGeometry.size.width, height: .greatestFiniteMagnitude)
|
||||
let attributes:[NSAttributedString.Key:Any] = [NSAttributedString.Key.font: font]
|
||||
var low = 0
|
||||
var heigh = shrinkText.count
|
||||
var mid = heigh
|
||||
while ((heigh - low) > 1) {
|
||||
let attributedText = NSAttributedString(string: shrinkText, attributes: attributes)
|
||||
let boundingRect = attributedText.boundingRect(with: size, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
|
||||
if boundingRect.size.height > visibleTextGeometry.size.height {
|
||||
truncated = true
|
||||
heigh = mid
|
||||
mid = (heigh + low)/2
|
||||
|
||||
} else {
|
||||
if mid == text.count {
|
||||
break
|
||||
} else {
|
||||
low = mid
|
||||
mid = (low + heigh)/2
|
||||
}
|
||||
}
|
||||
shrinkText = String(text.prefix(mid))
|
||||
}
|
||||
|
||||
if truncated {
|
||||
shrinkText = String(shrinkText.prefix(shrinkText.count - 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.hidden()
|
||||
}
|
||||
.font(Font(font))
|
||||
|
||||
if truncated {
|
||||
Button {
|
||||
seeMoreAction()
|
||||
} label: {
|
||||
Text(moreLessText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,9 +11,10 @@ import UIKit
|
|||
|
||||
// A more general derivative of
|
||||
// https://stackoverflow.com/questions/65812080/introspect-library-uirefreshcontrol-with-swiftui-not-working
|
||||
class RefreshHelper {
|
||||
final class RefreshHelper {
|
||||
var refreshControl: UIRefreshControl?
|
||||
var refreshAction: (() -> Void)?
|
||||
private var lastAutomaticRefresh = Date()
|
||||
|
||||
@objc func didRefresh() {
|
||||
guard let refreshControl = refreshControl else { return }
|
||||
|
@ -21,3 +22,19 @@ class RefreshHelper {
|
|||
refreshControl.endRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - automatic refreshing
|
||||
|
||||
extension RefreshHelper {
|
||||
private static let timeUntilStale = TimeInterval(60)
|
||||
|
||||
func refreshStaleData() {
|
||||
guard isStale else { return }
|
||||
lastAutomaticRefresh = .now
|
||||
refreshAction?()
|
||||
}
|
||||
|
||||
private var isStale: Bool {
|
||||
lastAutomaticRefresh.addingTimeInterval(Self.timeUntilStale) < .now
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ struct HomeView: View {
|
|||
if !viewModel.resumeItems.isEmpty {
|
||||
ContinueWatchingView(viewModel: viewModel)
|
||||
}
|
||||
|
||||
if !viewModel.nextUpItems.isEmpty {
|
||||
PortraitImageHStackView(items: viewModel.nextUpItems,
|
||||
horizontalAlignment: .leading) {
|
||||
|
@ -63,7 +64,17 @@ struct HomeView: View {
|
|||
} selectedAction: { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !viewModel.latestAddedItems.isEmpty {
|
||||
PortraitImageHStackView(items: viewModel.latestAddedItems) {
|
||||
Text("Recently Added")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.padding()
|
||||
} selectedAction: { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(viewModel.libraries, id: \.self) { library in
|
||||
|
@ -119,5 +130,8 @@ struct HomeView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
refreshHelper.refreshStaleData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
/*
|
||||
* 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 JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct ItemOverviewView: View {
|
||||
|
||||
@EnvironmentObject var itemOverviewRouter: ItemOverviewCoordinator.Router
|
||||
let item: BaseItemDto
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
Text(item.overview ?? "")
|
||||
.font(.footnote)
|
||||
.padding()
|
||||
}
|
||||
.navigationBarTitle("Overview", displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
itemOverviewRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,6 +13,8 @@ import SwiftUI
|
|||
|
||||
struct ItemViewBody: View {
|
||||
|
||||
@Environment(\.horizontalSizeClass) private var hSizeClass
|
||||
@Environment(\.verticalSizeClass) private var vSizeClass
|
||||
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||
@EnvironmentObject private var viewModel: ItemViewModel
|
||||
@Default(.showCastAndCrew) var showCastAndCrew
|
||||
|
@ -20,11 +22,26 @@ struct ItemViewBody: View {
|
|||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
// MARK: Overview
|
||||
|
||||
Text(viewModel.item.overview ?? "")
|
||||
.font(.footnote)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 3)
|
||||
|
||||
if let itemOverview = viewModel.item.overview {
|
||||
if hSizeClass == .compact && vSizeClass == .regular {
|
||||
TruncatedTextView(itemOverview,
|
||||
lineLimit: 5,
|
||||
font: UIFont.preferredFont(forTextStyle: .footnote)) {
|
||||
itemRouter.route(to: \.itemOverview, viewModel.item)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
} else {
|
||||
Text(itemOverview)
|
||||
.font(.footnote)
|
||||
.padding()
|
||||
}
|
||||
} else {
|
||||
Text("No overview available")
|
||||
.font(.footnote)
|
||||
.padding()
|
||||
}
|
||||
|
||||
// MARK: Seasons
|
||||
|
||||
|
|
|
@ -21,8 +21,7 @@ struct PortraitHeaderOverlayView: View {
|
|||
|
||||
// MARK: Portrait Image
|
||||
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 130))
|
||||
.frame(width: 130, height: 195)
|
||||
.cornerRadius(10)
|
||||
.portraitPoster(width: 130)
|
||||
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Spacer()
|
||||
|
|
|
@ -382,21 +382,21 @@ struct VLCPlayerOverlayView: View {
|
|||
struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
|
||||
|
||||
static let videoPlayerViewModel = VideoPlayerViewModel(item: BaseItemDto(),
|
||||
title: "Glorious Purpose",
|
||||
subtitle: "Loki - S1E1",
|
||||
streamURL: URL(string: "www.apple.com")!,
|
||||
hlsURL: URL(string: "www.apple.com")!,
|
||||
response: PlaybackInfoResponse(),
|
||||
audioStreams: [MediaStream(displayTitle: "English", index: -1)],
|
||||
subtitleStreams: [MediaStream(displayTitle: "None", index: -1)],
|
||||
selectedAudioStreamIndex: -1,
|
||||
selectedSubtitleStreamIndex: -1,
|
||||
subtitlesEnabled: true,
|
||||
autoplayEnabled: false,
|
||||
overlayType: .compact,
|
||||
shouldShowPlayPreviousItem: true,
|
||||
shouldShowPlayNextItem: true,
|
||||
shouldShowAutoPlay: true)
|
||||
title: "Glorious Purpose",
|
||||
subtitle: "Loki - S1E1",
|
||||
streamURL: URL(string: "www.apple.com")!,
|
||||
streamType: .direct,
|
||||
response: PlaybackInfoResponse(),
|
||||
audioStreams: [MediaStream(displayTitle: "English", index: -1)],
|
||||
subtitleStreams: [MediaStream(displayTitle: "None", index: -1)],
|
||||
selectedAudioStreamIndex: -1,
|
||||
selectedSubtitleStreamIndex: -1,
|
||||
subtitlesEnabled: true,
|
||||
autoplayEnabled: false,
|
||||
overlayType: .compact,
|
||||
shouldShowPlayPreviousItem: true,
|
||||
shouldShowPlayNextItem: true,
|
||||
shouldShowAutoPlay: true)
|
||||
|
||||
static var previews: some View {
|
||||
ZStack {
|
||||
|
|
Loading…
Reference in New Issue