Merge branch 'main' into subtitle-sizes

# Conflicts:
#	Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift
This commit is contained in:
koen 2022-01-09 13:23:22 +01:00
commit 213259fde5
18 changed files with 349 additions and 105 deletions

View File

@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img alt="Swiftfin" height="125" src="https://github.com/jellyfin/SwiftFin/raw/main/JellyfinPlayer/Assets.xcassets/AppIcon.appiconset/152.png"> <img alt="Swiftfin" height="125" src="https://github.com/jellyfin/Swiftfin/blob/main/Swiftfin/Assets.xcassets/AppIcon.appiconset/152.png">
<h2 align="center">Swiftfin</h2> <h2 align="center">Swiftfin</h2>
<a href="https://translate.jellyfin.org/engage/swiftfin/"> <a href="https://translate.jellyfin.org/engage/swiftfin/">
<img src="https://translate.jellyfin.org/widgets/swiftfin/-/svg-badge.svg"/> <img src="https://translate.jellyfin.org/widgets/swiftfin/-/svg-badge.svg"/>
@ -32,17 +32,19 @@ Check out our [Weblate instance](https://translate.jellyfin.org/projects/swiftfi
## ⚙️ Development ## ⚙️ Development
Xcode 13.0 with command line tools. Thank you for your interest in Swiftfin, please check out the [Contribution Guidelines](https://github.com/jellyfin/Swiftfin/contributing.md) to get started.
### Build Process -----
```bash ### Intended Behaviors Due to Technical Limitations
# install Cocoapods (if not installed)
$ sudo gem install cocoapods
# install dependencies The following behaviors are intended due to technical limitations:
$ pod install
# open workspace and build it - Pausing playback when app is backgrounded
$ open Swiftfin.xcworkspace - Due to VLCKit pausing video output at the same moment
```
- Audio delay after un-pausing
- Due to VLCKit, may be fixed in VLCKit v4
- No aspect fill
- VLCKit doesn't have the ability to aspect fill the view that the video output occupies

View File

@ -0,0 +1,22 @@
//
/*
* 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
import JellyfinAPI
extension MediaStream {
func externalURL(base: String) -> URL? {
guard let deliveryURL = deliveryUrl else { return nil }
var baseComponents = URLComponents(string: base)
baseComponents?.path += deliveryURL
return baseComponents?.url
}
}

View File

@ -67,6 +67,8 @@ final class VideoPlayerViewModel: ViewModel {
} }
@Published var autoplayEnabled: Bool { @Published var autoplayEnabled: Bool {
willSet { willSet {
previousItemVideoPlayerViewModel?.autoplayEnabled = newValue
nextItemVideoPlayerViewModel?.autoplayEnabled = newValue
Defaults[.autoplayEnabled] = newValue Defaults[.autoplayEnabled] = newValue
} }
} }
@ -115,6 +117,16 @@ final class VideoPlayerViewModel: ViewModel {
return Int64(currentSeconds) * 10_000_000 return Int64(currentSeconds) * 10_000_000
} }
// MARK: Helpers
var currentAudioStream: MediaStream? {
return audioStreams.first(where: { $0.index == selectedAudioStreamIndex })
}
var currentSubtitleStream: MediaStream? {
return subtitleStreams.first(where: { $0.index == selectedSubtitleStreamIndex })
}
// Necessary PassthroughSubject to capture manual scrubbing from sliders // Necessary PassthroughSubject to capture manual scrubbing from sliders
let sliderScrubbingSubject = PassthroughSubject<VideoPlayerViewModel, Never>() let sliderScrubbingSubject = PassthroughSubject<VideoPlayerViewModel, Never>()

View File

@ -7,6 +7,7 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors * Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/ */
import CachedAsyncImage
import SwiftUI import SwiftUI
struct ImageView: View { struct ImageView: View {
@ -31,7 +32,7 @@ struct ImageView: View {
private var failureImage: some View { private var failureImage: some View {
ZStack { ZStack {
Rectangle() Rectangle()
.foregroundColor(Color.systemFill) .foregroundColor(Color(UIColor.darkGray))
Text(failureInitials) Text(failureInitials)
.font(.largeTitle) .font(.largeTitle)
@ -40,21 +41,36 @@ struct ImageView: View {
} }
var body: some View { var body: some View {
AsyncImage(url: source) { phase in CachedAsyncImage(url: source, urlCache: .imageCache, transaction: Transaction(animation: .easeInOut)) { phase in
if let image = phase.image { switch phase {
case .success(let image):
image image
.resizable() .resizable()
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
} else if phase.error != nil { case .failure(_):
failureImage failureImage
} else { default:
// TODO: remove once placeholder hash image fixed // TODO: remove once placeholder hash image fixed
#if os(tvOS)
ZStack { ZStack {
Color.gray.ignoresSafeArea() Color.black.ignoresSafeArea()
ProgressView() ProgressView()
} }
#else
ZStack {
Color.gray.ignoresSafeArea()
ProgressView()
}
#endif
} }
} }
} }
} }
extension URLCache {
static let imageCache = URLCache(memoryCapacity: 512*1000*1000, diskCapacity: 10*1000*1000*1000)
}

View File

@ -84,7 +84,7 @@ struct EpisodesRowView: View {
HStack(alignment: .top) { HStack(alignment: .top) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
ImageView(src: episode.getBackdropImage(maxWidth: 445), ImageView(src: episode.getBackdropImage(maxWidth: 500),
bh: episode.getBackdropImageBlurHash()) bh: episode.getBackdropImageBlurHash())
.mask(Rectangle().frame(width: 500, height: 280)) .mask(Rectangle().frame(width: 500, height: 280))
.frame(width: 500, height: 280) .frame(width: 500, height: 280)

View File

@ -22,8 +22,13 @@ struct ContinueWatchingCard: View {
} label: { } label: {
ZStack(alignment: .bottom) { ZStack(alignment: .bottom) {
ImageView(src: item.getBackdropImage(maxWidth: 500)) if item.itemType == .episode {
.frame(width: 500, height: 281.25) ImageView(src: item.getSeriesBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
} else {
ImageView(src: item.getBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
}
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text(item.getItemProgressString() ?? "") Text(item.getItemProgressString() ?? "")
@ -57,6 +62,7 @@ struct ContinueWatchingCard: View {
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.primary) .foregroundColor(.primary)
.lineLimit(1) .lineLimit(1)
.frame(width: 500, alignment: .leading)
if item.itemType == .episode { if item.itemType == .episode {
Text(item.getEpisodeLocator() ?? "") Text(item.getEpisodeLocator() ?? "")
@ -64,8 +70,11 @@ struct ContinueWatchingCard: View {
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
} else {
Text("")
} }
} }
} }
.padding(.vertical)
} }
} }

View File

@ -25,7 +25,7 @@ struct ContinueWatchingView: View {
.padding(.leading, 50) .padding(.leading, 50)
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { LazyHStack(alignment: .top) {
ForEach(items, id: \.self) { item in ForEach(items, id: \.self) { item in
ContinueWatchingCard(item: item) ContinueWatchingCard(item: item)
} }

View File

@ -31,6 +31,7 @@ struct CinematicEpisodeItemView: View {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920),
bh: viewModel.item.getBackdropImageBlurHash()) bh: viewModel.item.getBackdropImageBlurHash())
.frame(height: UIScreen.main.bounds.height - 10)
.ignoresSafeArea() .ignoresSafeArea()
ScrollView { ScrollView {

View File

@ -16,10 +16,60 @@ struct LatestMediaView: View {
@Default(.showPosterLabels) var showPosterLabels @Default(.showPosterLabels) var showPosterLabels
var body: some View { var body: some View {
PortraitItemsRowView(rowTitle: L10n.latestWithString(viewModel.library.name ?? ""), VStack(alignment: .leading) {
items: viewModel.items,
showItemTitles: showPosterLabels) { item in L10n.latestWithString(viewModel.library.name ?? "").text
homeRouter.route(to: \.modalItem, item) .font(.title3)
.padding(.horizontal, 50)
ScrollView(.horizontal) {
HStack(alignment: .top) {
ForEach(viewModel.items, id: \.self) { item in
VStack(spacing: 15) {
Button {
homeRouter.route(to: \.modalItem, item)
} label: {
ImageView(src: item.portraitHeaderViewURL(maxWidth: 257))
.frame(width: 257, height: 380)
}
.frame(height: 380)
.buttonStyle(PlainButtonStyle())
if showPosterLabels {
Text(item.title)
.lineLimit(2)
.frame(width: 257)
}
}
}
Button {
homeRouter.route(to: \.library, (viewModel: .init(parentID: viewModel.library.id!,
filters: LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])),
title: viewModel.library.name ?? ""))
} label: {
ZStack {
Color(UIColor.darkGray)
.opacity(0.5)
VStack(spacing: 20) {
Image(systemName: "chevron.right")
.font(.title)
L10n.seeAll.text
.font(.title3)
}
}
}
.frame(width: 257, height: 380)
.buttonStyle(PlainButtonStyle())
}
.padding(.horizontal, 50)
.padding(.vertical)
}
.edgesIgnoringSafeArea(.horizontal)
} }
.focusSection()
} }
} }

View File

@ -20,8 +20,13 @@ struct NextUpCard: View {
Button { Button {
homeRouter.route(to: \.modalItem, item) homeRouter.route(to: \.modalItem, item)
} label: { } label: {
ImageView(src: item.getBackdropImage(maxWidth: 500)) if item.itemType == .episode {
.frame(width: 500, height: 281.25) ImageView(src: item.getSeriesBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
} else {
ImageView(src: item.getBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
}
} }
.buttonStyle(CardButtonStyle()) .buttonStyle(CardButtonStyle())
.padding(.top) .padding(.top)

View File

@ -24,7 +24,7 @@ class VLCPlayerViewController: UIViewController {
// MARK: variables // MARK: variables
private var viewModel: VideoPlayerViewModel private var viewModel: VideoPlayerViewModel
private var vlcMediaPlayer = VLCMediaPlayer() private var vlcMediaPlayer: VLCMediaPlayer
private var lastPlayerTicks: Int64 = 0 private var lastPlayerTicks: Int64 = 0
private var lastProgressReportTicks: Int64 = 0 private var lastProgressReportTicks: Int64 = 0
private var viewModelListeners = Set<AnyCancellable>() private var viewModelListeners = Set<AnyCancellable>()
@ -59,6 +59,7 @@ class VLCPlayerViewController: UIViewController {
init(viewModel: VideoPlayerViewModel) { init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
self.vlcMediaPlayer = VLCMediaPlayer()
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
@ -118,14 +119,6 @@ class VLCPlayerViewController: UIViewController {
view.backgroundColor = .black view.backgroundColor = .black
// Outside of 'setupMediaPlayer' such that they
// aren't unnecessarily set more than once
vlcMediaPlayer.delegate = self
vlcMediaPlayer.drawable = videoContentView
// TODO: custom font sizes
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16)
setupMediaPlayer(newViewModel: viewModel) setupMediaPlayer(newViewModel: viewModel)
setupPanGestureRecognizer() setupPanGestureRecognizer()
@ -211,20 +204,20 @@ class VLCPlayerViewController: UIViewController {
hideConfirmCloseOverlay() hideConfirmCloseOverlay()
if Defaults[.downActionShowsMenu] { if Defaults[.downActionShowsMenu] {
if !displayingContentOverlay { if !displayingContentOverlay && !displayingOverlay {
didSelectMenu() didSelectMenu()
} }
} }
case .leftArrow: case .leftArrow:
hideConfirmCloseOverlay() hideConfirmCloseOverlay()
if !displayingContentOverlay { if !displayingContentOverlay && !displayingOverlay {
didSelectBackward() didSelectBackward()
} }
case .rightArrow: case .rightArrow:
hideConfirmCloseOverlay() hideConfirmCloseOverlay()
if !displayingContentOverlay { if !displayingContentOverlay && !displayingOverlay {
didSelectForward() didSelectForward()
} }
case .pageUp: () case .pageUp: ()
@ -246,9 +239,6 @@ class VLCPlayerViewController: UIViewController {
hideOverlay() hideOverlay()
} else if displayingContentOverlay { } else if displayingContentOverlay {
hideOverlayContent() hideOverlayContent()
showOverlay()
restartOverlayDismissTimer()
} else if viewModel.confirmClose && !displayingConfirmClose { } else if viewModel.confirmClose && !displayingConfirmClose {
showConfirmCloseOverlay() showConfirmCloseOverlay()
@ -387,6 +377,28 @@ extension VLCPlayerViewController {
/// Use case for this is setting new media within the same VLCPlayerViewController /// Use case for this is setting new media within the same VLCPlayerViewController
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) { func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
// remove old player
if vlcMediaPlayer.media != nil {
viewModelListeners.forEach({ $0.cancel() })
vlcMediaPlayer.stop()
viewModel.sendStopReport()
viewModel.playerOverlayDelegate = nil
}
vlcMediaPlayer = VLCMediaPlayer()
// setup with new player and view model
vlcMediaPlayer = VLCMediaPlayer()
vlcMediaPlayer.delegate = self
vlcMediaPlayer.drawable = videoContentView
// TODO: Custom subtitle sizes
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16)
stopOverlayDismissTimer() stopOverlayDismissTimer()
// Stop current media if there is one // Stop current media if there is one
@ -436,6 +448,13 @@ extension VLCPlayerViewController {
func startPlayback() { func startPlayback() {
vlcMediaPlayer.play() vlcMediaPlayer.play()
// Setup external subtitles
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) {
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
}
}
setMediaPlayerTimeAtCurrentSlider() setMediaPlayerTimeAtCurrentSlider()
viewModel.sendPlayReport() viewModel.sendPlayReport()
@ -672,7 +691,8 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
} }
// If needing to fix subtitle streams during playback // If needing to fix subtitle streams during playback
if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled { if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex &&
viewModel.subtitlesEnabled {
didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex) didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex)
} }

View File

@ -47,7 +47,7 @@ struct tvOSVLCOverlay: View {
if let subtitle = viewModel.subtitle { if let subtitle = viewModel.subtitle {
Text(subtitle) Text(subtitle)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.lightGray) .foregroundColor(.white)
} }
Text(viewModel.title) Text(viewModel.title)

View File

@ -527,12 +527,14 @@ public final class TvOSSlider: UIControl {
@objc @objc
private func leftTapWasTriggered() { private func leftTapWasTriggered() {
setValue(value-stepValue, animated: true) // setValue(value-stepValue, animated: true)
viewModel.playerOverlayDelegate?.didSelectBackward()
} }
@objc @objc
private func rightTapWasTriggered() { private func rightTapWasTriggered() {
setValue(value+stepValue, animated: true) // setValue(value+stepValue, animated: true)
viewModel.playerOverlayDelegate?.didSelectForward()
} }
public override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) { public override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {

View File

@ -274,10 +274,9 @@
E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; }; E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; };
E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; }; E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; };
E12186DE2718F1C50010884C /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E12186DD2718F1C50010884C /* Defaults */; }; E12186DE2718F1C50010884C /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E12186DD2718F1C50010884C /* Defaults */; };
E1218C9A271A26BA00EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C99271A26BA00EA0737 /* Nuke */; };
E1218C9C271A26C400EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9B271A26C400EA0737 /* Nuke */; };
E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9D271A2CD600EA0737 /* CombineExt */; }; E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9D271A2CD600EA0737 /* CombineExt */; };
E1218CA0271A2CF200EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9F271A2CF200EA0737 /* Nuke */; }; 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 */; }; E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */; };
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
@ -376,6 +375,8 @@
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */; }; E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */; };
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; };
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */; }; E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */; };
E1AE8E7C2789135A00FBDDAA /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1AE8E7B2789135A00FBDDAA /* Nuke */; };
E1AE8E7E2789136D00FBDDAA /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1AE8E7D2789136D00FBDDAA /* Nuke */; };
E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */; }; E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */; };
E1B59FD92786AE4600A5287E /* NextUpCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD82786AE4600A5287E /* NextUpCard.swift */; }; E1B59FD92786AE4600A5287E /* NextUpCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD82786AE4600A5287E /* NextUpCard.swift */; };
E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE7271A23780015B715 /* CombineExt */; }; E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE7271A23780015B715 /* CombineExt */; };
@ -407,9 +408,12 @@
E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; }; E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; };
E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */; }; E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */; };
E1D7E5A827892566009D0EF7 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1D7E5A727892566009D0EF7 /* Nuke */; };
E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; }; E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; };
E1E00A36278628A40022235B /* 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 */; }; 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 */; }; E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; };
E1E5D5372783A52C00692DFE /* CinematicEpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5362783A52C00692DFE /* CinematicEpisodeItemView.swift */; }; E1E5D5372783A52C00692DFE /* CinematicEpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5362783A52C00692DFE /* CinematicEpisodeItemView.swift */; };
E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */; }; E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */; };
@ -651,6 +655,7 @@
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; }; E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; }; E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = "<group>"; }; 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>"; }; E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; };
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.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>"; }; E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
@ -754,13 +759,14 @@
files = ( files = (
53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */, 53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */,
E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */, E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */,
E1218CA0271A2CF200EA0737 /* Nuke in Frameworks */,
6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */, 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */,
535870912669D7A800D05A09 /* Introspect in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */,
536D3D84267BEA550004248C /* ParallaxView in Frameworks */, 536D3D84267BEA550004248C /* ParallaxView in Frameworks */,
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */, 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */, E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */,
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */, E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
E1E0F4DA278911A30084F701 /* CachedAsyncImage in Frameworks */,
E1AE8E7E2789136D00FBDDAA /* Nuke in Frameworks */,
E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */, E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */,
E12186DE2718F1C50010884C /* Defaults in Frameworks */, E12186DE2718F1C50010884C /* Defaults in Frameworks */,
53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */, 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */,
@ -777,10 +783,11 @@
E10EAA4D277BB716000269ED /* Sliders in Frameworks */, E10EAA4D277BB716000269ED /* Sliders in Frameworks */,
62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */, 62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */,
E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */, E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */,
E1218C9A271A26BA00EA0737 /* Nuke in Frameworks */,
E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */, E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */,
53352571265EA0A0006CCA86 /* Introspect in Frameworks */, 53352571265EA0A0006CCA86 /* Introspect in Frameworks */,
E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */, E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */,
E1E0F4D8278911680084F701 /* CachedAsyncImage in Frameworks */,
E1AE8E7C2789135A00FBDDAA /* Nuke in Frameworks */,
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */, 625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */,
E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */, E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */,
E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */, E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */,
@ -792,13 +799,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E1D7E5A827892566009D0EF7 /* Nuke in Frameworks */,
628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */, 628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */,
531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */, 531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */,
E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */, E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */,
53649AB5269D423A00A2D8B7 /* Puppy in Frameworks */, 53649AB5269D423A00A2D8B7 /* Puppy in Frameworks */,
536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */, 536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */,
E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */, E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */,
E1218C9C271A26C400EA0737 /* Nuke in Frameworks */,
E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */, E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */,
3B8BA25B211CA261017ABA16 /* Pods_Swiftfin_Widget.framework in Frameworks */, 3B8BA25B211CA261017ABA16 /* Pods_Swiftfin_Widget.framework in Frameworks */,
); );
@ -1518,10 +1525,11 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */, E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */,
E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */,
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */, E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */,
E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */,
5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */, 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */,
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */,
E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */,
E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */, E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */,
); );
path = JellyfinAPIExtensions; path = JellyfinAPIExtensions;
@ -1651,9 +1659,10 @@
E13DD3CC27164CA7009D4DAF /* CoreStore */, E13DD3CC27164CA7009D4DAF /* CoreStore */,
E12186DD2718F1C50010884C /* Defaults */, E12186DD2718F1C50010884C /* Defaults */,
E1218C9D271A2CD600EA0737 /* CombineExt */, E1218C9D271A2CD600EA0737 /* CombineExt */,
E1218C9F271A2CF200EA0737 /* Nuke */,
E1A9999A271A343C008E78C0 /* SwiftUICollection */, E1A9999A271A343C008E78C0 /* SwiftUICollection */,
E178857C278037FD0094FBCF /* JellyfinAPI */, E178857C278037FD0094FBCF /* JellyfinAPI */,
E1E0F4D9278911A30084F701 /* CachedAsyncImage */,
E1AE8E7D2789136D00FBDDAA /* Nuke */,
); );
productName = "JellyfinPlayer tvOS"; productName = "JellyfinPlayer tvOS";
productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */; productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */;
@ -1689,10 +1698,11 @@
E13DD3D227168E65009D4DAF /* Defaults */, E13DD3D227168E65009D4DAF /* Defaults */,
E1B6DCE7271A23780015B715 /* CombineExt */, E1B6DCE7271A23780015B715 /* CombineExt */,
E1B6DCE9271A23880015B715 /* SwiftyJSON */, E1B6DCE9271A23880015B715 /* SwiftyJSON */,
E1218C99271A26BA00EA0737 /* Nuke */,
E1A99998271A3429008E78C0 /* SwiftUICollection */, E1A99998271A3429008E78C0 /* SwiftUICollection */,
E10EAA44277BB646000269ED /* JellyfinAPI */, E10EAA44277BB646000269ED /* JellyfinAPI */,
E10EAA4C277BB716000269ED /* Sliders */, E10EAA4C277BB716000269ED /* Sliders */,
E1E0F4D7278911680084F701 /* CachedAsyncImage */,
E1AE8E7B2789135A00FBDDAA /* Nuke */,
); );
productName = JellyfinPlayer; productName = JellyfinPlayer;
productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */; productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */;
@ -1717,8 +1727,8 @@
53649AB4269D423A00A2D8B7 /* Puppy */, 53649AB4269D423A00A2D8B7 /* Puppy */,
E13DD3CE27164E1F009D4DAF /* CoreStore */, E13DD3CE27164E1F009D4DAF /* CoreStore */,
E13DD3DC27175CE3009D4DAF /* Defaults */, E13DD3DC27175CE3009D4DAF /* Defaults */,
E1218C9B271A26C400EA0737 /* Nuke */,
E10EAA46277BB670000269ED /* JellyfinAPI */, E10EAA46277BB670000269ED /* JellyfinAPI */,
E1D7E5A727892566009D0EF7 /* Nuke */,
); );
productName = WidgetExtensionExtension; productName = WidgetExtensionExtension;
productReference = 628B95202670CABD0091AF3B /* Swiftfin Widget.appex */; productReference = 628B95202670CABD0091AF3B /* Swiftfin Widget.appex */;
@ -1781,10 +1791,11 @@
E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */, E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */,
E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */, E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */,
E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */,
C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */, C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */,
E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */, E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */,
E1E0F4D6278911680084F701 /* XCRemoteSwiftPackageReference "SwiftUI-CachedAsyncImage" */,
E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */,
); );
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -2103,6 +2114,7 @@
E1C812CC277AE40A00918266 /* VideoPlayerView.swift in Sources */, E1C812CC277AE40A00918266 /* VideoPlayerView.swift in Sources */,
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */,
E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, E178859E2780F53B0094FBCF /* SliderView.swift in Sources */,
536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */,
E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */,
@ -2244,6 +2256,7 @@
E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */, E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */,
621338932660107500A81A2A /* StringExtensions.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */,
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */, E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */,
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
@ -2976,14 +2989,6 @@
kind = branch; kind = branch;
}; };
}; };
E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kean/Nuke";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 9.0.0;
};
};
E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */ = { E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CombineCommunity/CombineExt"; repositoryURL = "https://github.com/CombineCommunity/CombineExt";
@ -3008,6 +3013,14 @@
minimumVersion = 6.0.0; minimumVersion = 6.0.0;
}; };
}; };
E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kean/Nuke";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 9.6.0;
};
};
E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON";
@ -3016,6 +3029,14 @@
minimumVersion = 5.0.0; 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 */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@ -3094,26 +3115,11 @@
package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */;
productName = Defaults; productName = Defaults;
}; };
E1218C99271A26BA00EA0737 /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E1218C9B271A26C400EA0737 /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E1218C9D271A2CD600EA0737 /* CombineExt */ = { E1218C9D271A2CD600EA0737 /* CombineExt */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */; package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */;
productName = CombineExt; productName = CombineExt;
}; };
E1218C9F271A2CF200EA0737 /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E13DD3C52716499E009D4DAF /* CoreStore */ = { E13DD3C52716499E009D4DAF /* CoreStore */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */;
@ -3154,6 +3160,16 @@
package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */; package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
productName = SwiftUICollection; productName = SwiftUICollection;
}; };
E1AE8E7B2789135A00FBDDAA /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E1AE8E7D2789136D00FBDDAA /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E1B6DCE7271A23780015B715 /* CombineExt */ = { E1B6DCE7271A23780015B715 /* CombineExt */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */; package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */;
@ -3164,6 +3180,21 @@
package = E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; package = E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */;
productName = SwiftyJSON; productName = SwiftyJSON;
}; };
E1D7E5A727892566009D0EF7 /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
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 */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 5377CBE9263B596A003A4E83 /* Project object */; rootObject = 5377CBE9263B596A003A4E83 /* Project object */;

View File

@ -100,6 +100,15 @@
"version": "1.4.2" "version": "1.4.2"
} }
}, },
{
"package": "CachedAsyncImage",
"repositoryURL": "https://github.com/lorenzofiamingo/SwiftUI-CachedAsyncImage",
"state": {
"branch": "main",
"revision": "eb489a699be1f6e6c1a19fecdd6bfdc556474fd6",
"version": null
}
},
{ {
"package": "Introspect", "package": "Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect", "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect",

View File

@ -64,19 +64,20 @@ struct SettingsView: View {
} }
} }
Section(header: Text("Networking")) { // TODO: Implement these for playback
Picker("Default local quality", selection: $inNetworkStreamBitrate) { // Section(header: Text("Networking")) {
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in // Picker("Default local quality", selection: $inNetworkStreamBitrate) {
Text(bitrate.name).tag(bitrate.value) // ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
} // Text(bitrate.name).tag(bitrate.value)
} // }
// }
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { //
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in // Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
Text(bitrate.name).tag(bitrate.value) // ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
} // Text(bitrate.name).tag(bitrate.value)
} // }
} // }
// }
Section(header: Text("Video Player")) { Section(header: Text("Video Player")) {
Picker("Jump Forward Length", selection: $jumpForwardLength) { Picker("Jump Forward Length", selection: $jumpForwardLength) {

View File

@ -24,7 +24,7 @@ class VLCPlayerViewController: UIViewController {
// MARK: variables // MARK: variables
private var viewModel: VideoPlayerViewModel private var viewModel: VideoPlayerViewModel
private var vlcMediaPlayer = VLCMediaPlayer() private var vlcMediaPlayer: VLCMediaPlayer
private var lastPlayerTicks: Int64 = 0 private var lastPlayerTicks: Int64 = 0
private var lastProgressReportTicks: Int64 = 0 private var lastProgressReportTicks: Int64 = 0
private var viewModelListeners = Set<AnyCancellable>() private var viewModelListeners = Set<AnyCancellable>()
@ -49,6 +49,7 @@ class VLCPlayerViewController: UIViewController {
init(viewModel: VideoPlayerViewModel) { init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
self.vlcMediaPlayer = VLCMediaPlayer()
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
@ -97,13 +98,6 @@ class VLCPlayerViewController: UIViewController {
view.backgroundColor = .black view.backgroundColor = .black
// These are kept outside of 'setupMediaPlayer' such that
// they aren't unnecessarily set more than once
vlcMediaPlayer.delegate = self
vlcMediaPlayer.drawable = videoContentView
vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize])
setupMediaPlayer(newViewModel: viewModel) setupMediaPlayer(newViewModel: viewModel)
refreshJumpBackwardOverlayView(with: viewModel.jumpBackwardLength) refreshJumpBackwardOverlayView(with: viewModel.jumpBackwardLength)
@ -286,9 +280,8 @@ extension VLCPlayerViewController {
/// Use case for this is setting new media within the same VLCPlayerViewController /// Use case for this is setting new media within the same VLCPlayerViewController
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) { func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
stopOverlayDismissTimer() // remove old player
// Stop current media if there is one
if vlcMediaPlayer.media != nil { if vlcMediaPlayer.media != nil {
viewModelListeners.forEach({ $0.cancel() }) viewModelListeners.forEach({ $0.cancel() })
@ -297,6 +290,19 @@ extension VLCPlayerViewController {
viewModel.playerOverlayDelegate = nil viewModel.playerOverlayDelegate = nil
} }
vlcMediaPlayer = VLCMediaPlayer()
// setup with new player and view model
vlcMediaPlayer = VLCMediaPlayer()
vlcMediaPlayer.delegate = self
vlcMediaPlayer.drawable = videoContentView
vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize])
stopOverlayDismissTimer()
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
@ -333,6 +339,13 @@ extension VLCPlayerViewController {
func startPlayback() { func startPlayback() {
vlcMediaPlayer.play() vlcMediaPlayer.play()
// Setup external subtitles
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) {
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
}
}
setMediaPlayerTimeAtCurrentSlider() setMediaPlayerTimeAtCurrentSlider()
viewModel.sendPlayReport() viewModel.sendPlayReport()
@ -525,7 +538,8 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
} }
// If needing to fix subtitle streams during playback // If needing to fix subtitle streams during playback
if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled { if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex &&
viewModel.subtitlesEnabled {
didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex) didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex)
} }

50
contributing.md Normal file
View File

@ -0,0 +1,50 @@
# Contributing to Swiftfin
> Thank you for your interest in contributing to the Jellyfin (Swiftfin) project! This page and its children describe the ways you can contribute, as well as some of our policies. This should help guide you through your first Issue or PR.
> Even if you can't contribute code, you can still help Jellyfin (Swiftfin)! The two main things you can help with are testing and creating issues. Contributing to code, ..., and other non-code components are all outlined in the sections below.
## Setup
Fork the Swiftfin repo and install the necessary CocoaPods with Xcode 13:
```bash
# install Cocoapods (if not installed)
$ sudo gem install cocoapods
# install dependencies
$ pod install
# open workspace and build
$ open Swiftfin.xcworkspace
```
## Git Flow
Pull Requests must be created from branch that is _**not**_ your fork's `main` branch. This is to prevent many potential problems that come from unnecessary or irrelevant commits and rebasing your fork.
If your Pull Request relates to an Issue, link the issue or mention it in the Issue itself.
Pull Requests must pass the automated `iOS` and `tvOS` builds in order to be merged and cannot have your developer account attached.
Swiftfin follows the same Pull Request Guidelines as outlined in the [official Jellyfin contribution guidelines](https://jellyfin.org/docs/general/contributing/development.html#pull-request-guidelines).
## Architecture
Swiftfin is developed using SwiftUI with some UIKit components where deemed necessary, as SwiftUI is still in relatively early development. Swiftfin consists of both the iOS and tvOS Jellyfin clients with a shared general underlying structure where each client has their own respective views. Because of this architecture, keep in mind while developing you may have to work for both clients.
Playback is done using [VLCKit](https://code.videolan.org/videolan/VLCKit) for its great codec support.
While there are no design guidelines for UI/UX features, Swiftfin has the goal to use native SwiftUI components with specific theming to Jellyfin. If your feature creates new UI/UX components, you are welcome to introduce a general design that may receive feedback during the PR process or may be re-designed later on. Some UI/UX features are intended to be user customizable but not every item should be to keep to some idea of Swiftfin's own design. Taking inspiration, but not always copying, from other applications is encouraged.
## New Features
If you would like to develop a new feature, create an issue with a description of the feature such that a discussion can be made for its possibility, whether it belongs in Swiftfin, and finally its general implementation. Leave a comment when you start work on an approved feature such that duplicate work among developers doesn't conflict.
## Other Code Work
Other code work like bug fixes, issues with `Developer` tags, and localization efforts are welcome to be picked up anytime. Just leave a comment when you start work on a bug fix or `Developer` issue.
If you notice undesirable behavior or would like to make a UI/UX tweak, create an issue or ask in the iOS Matrix/Discord channel and a discussion will be made.
If you have a question about any existing implementations, ask the iOS Matrix/Discord channel for developer insights.