diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 8382552e..92612ce6 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -478,7 +478,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 26; + CURRENT_PROJECT_VERSION = 29; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_BITCODE = NO; @@ -504,7 +504,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 26; + CURRENT_PROJECT_VERSION = 29; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 9R8RREG67J; diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 692bb5be..62a804a7 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -32,6 +32,7 @@ struct ConnectToServerView: View { @State private var isSignInErrored = false; @State private var isConnected = false; @State private var serverName = ""; + @State private var usernameDisabled: Bool = false; @State private var publicUsers: [publicUser] = []; @State private var lastPublicUsers: [publicUser] = []; @Binding var rootIsActive : Bool @@ -244,6 +245,7 @@ struct ConnectToServerView: View { TextField("Username", text: $username) .disableAutocorrection(true) .autocapitalization(.none) + .disabled(usernameDisabled) SecureField("Password", text: $password) .disableAutocorrection(true) .autocapitalization(.none) @@ -285,6 +287,7 @@ struct ConnectToServerView: View { Section() { Button { _publicUsers.wrappedValue = _lastPublicUsers.wrappedValue + _usernameDisabled.wrappedValue = false; } label: { HStack() { HStack() { @@ -304,6 +307,7 @@ struct ConnectToServerView: View { if(pubuser.hasPassword) { _lastPublicUsers.wrappedValue = _publicUsers.wrappedValue _username.wrappedValue = pubuser.username + _usernameDisabled.wrappedValue = true; _publicUsers.wrappedValue = [] } else { _publicUsers.wrappedValue = [] @@ -339,7 +343,9 @@ struct ConnectToServerView: View { Section() { Button() { + _lastPublicUsers.wrappedValue = _publicUsers.wrappedValue; _publicUsers.wrappedValue = [] + _username.wrappedValue = "" } label: { HStack() { Text("Other User").font(.subheadline).fontWeight(.semibold) diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift index 17ac0507..c36783f0 100644 --- a/JellyfinPlayer/ContentView.swift +++ b/JellyfinPlayer/ContentView.swift @@ -204,7 +204,7 @@ struct ContentView: View { LatestMediaView(library: library_id) }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) } - Spacer().frame(height: 20) + Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) } .navigationTitle("Home") .toolbar { diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index 95f695bc..858be88a 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -172,6 +172,7 @@ struct ContinueWatchingView: View { }.padding(.trailing, 5) } } + Spacer().frame(width: 2) }.frame(height: 215) } else { EmptyView() diff --git a/JellyfinPlayer/Info.plist b/JellyfinPlayer/Info.plist index 161fabef..0f27d5d9 100644 --- a/JellyfinPlayer/Info.plist +++ b/JellyfinPlayer/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 26 + 29 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index b94254be..b4a8e26a 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -24,7 +24,7 @@ struct ItemView: View { var body: some View { if(playback.shouldPlay) { - LoadingView(isShowing: $shouldShowLoadingView) { + LoadingViewNoBlur(isShowing: $shouldShowLoadingView) { VLCPlayerWithControls(item: playback.itemToPlay, loadBinding: $shouldShowLoadingView, pBinding: _playback.projectedValue.shouldPlay) .navigationBarHidden(true) .navigationBarBackButtonHidden(true) diff --git a/JellyfinPlayer/LoadingView.swift b/JellyfinPlayer/LoadingView.swift index 738ea724..820eef35 100644 --- a/JellyfinPlayer/LoadingView.swift +++ b/JellyfinPlayer/LoadingView.swift @@ -2,10 +2,10 @@ import SwiftUI struct LoadingView: View where Content: View { @Environment(\.colorScheme) var colorScheme - @Binding var isShowing: Bool // should the modal be visible? + @Binding var isShowing: Bool // should the modal be visible? var content: () -> Content var text: String? // the text to display under the ProgressView - defaults to "Loading..." - + var body: some View { GeometryReader { geometry in ZStack(alignment: .center) { @@ -38,3 +38,42 @@ struct LoadingView: View where Content: View { } } } + +struct LoadingViewNoBlur: View where Content: View { + @Environment(\.colorScheme) var colorScheme + @Binding var isShowing: Bool // should the modal be visible? + var content: () -> Content + var text: String? // the text to display under the ProgressView - defaults to "Loading..." + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .center) { + // the content to display - if the modal is showing, we'll blur it + content() + .disabled(isShowing) + + // all contents inside here will only be shown when isShowing is true + if isShowing { + // this Rectangle is a semi-transparent black overlay + Rectangle() + .fill(Color.black).opacity(isShowing ? 0.6 : 0) + .edgesIgnoringSafeArea(.all) + + // the magic bit - our ProgressView just displays an activity + // indicator, with some text underneath showing what we are doing + HStack() { + ProgressView() + Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60) + Spacer() + } + .padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 10)) + .frame(width: 250) + .background(colorScheme == .dark ? Color(UIColor.systemGray6) : Color.white) + .foregroundColor(Color.primary) + .cornerRadius(16) + } + } + } + } +} + diff --git a/JellyfinPlayer/Views/VideoPlayer.storyboard b/JellyfinPlayer/Views/VideoPlayer.storyboard index d4b80988..fd4b4ff9 100644 --- a/JellyfinPlayer/Views/VideoPlayer.storyboard +++ b/JellyfinPlayer/Views/VideoPlayer.storyboard @@ -44,7 +44,7 @@ - + @@ -125,7 +125,7 @@ - + diff --git a/JellyfinPlayer/Views/VideoPlayer.swift b/JellyfinPlayer/Views/VideoPlayer.swift index fb498b4a..26248a40 100644 --- a/JellyfinPlayer/Views/VideoPlayer.swift +++ b/JellyfinPlayer/Views/VideoPlayer.swift @@ -39,7 +39,7 @@ protocol PlayerViewControllerDelegate: AnyObject { func exitPlayer(_ viewController: PlayerViewController) } -class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate, VideoPlayerSettingsDelegate { +class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate { weak var delegate: PlayerViewControllerDelegate? @@ -58,14 +58,23 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe var shouldShowLoadingScreen: Bool = false; var ssTargetValueOffset: Int = 0; var ssStartValue: Int = 0; + var optionsVC: VideoPlayerSettingsView?; var paused: Bool = true; var lastTime: Float = 0.0; var startTime: Int = 0; var controlsAppearTime: Double = 0; - var selectedAudioTrack: Int32 = -1; - var selectedCaptionTrack: Int32 = -1; + var selectedAudioTrack: Int32 = -1 { + didSet { + print(selectedAudioTrack) + } + }; + var selectedCaptionTrack: Int32 = -1 { + didSet { + print(selectedCaptionTrack) + } + } var playSessionId: String = ""; var lastProgressReportTime: Double = 0; @@ -155,23 +164,15 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } @IBAction func settingsButtonTapped(_ sender: UIButton) { - let optionsVC = VideoPlayerSettingsView() - print(self.selectedAudioTrack) - print(self.selectedCaptionTrack) - optionsVC.currentSubtitleTrack = self.selectedCaptionTrack - optionsVC.currentAudioTrack = self.selectedAudioTrack - optionsVC.delegate = self; - optionsVC.subtitles = subtitleTrackArray - optionsVC.audioTracks = audioTrackArray - // Use the popover presentation style for your view controller. - let navVC = UINavigationController(rootViewController: optionsVC) - navVC.modalPresentationStyle = .popover - navVC.popoverPresentationController?.sourceView = playerSettingsButton - + optionsVC = VideoPlayerSettingsView() + optionsVC?.delegate = self + + optionsVC?.modalPresentationStyle = .popover + optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton // Present the view controller (in a popover). - self.present(navVC, animated: true) { + self.present(optionsVC!, animated: true) { print("popover visible, pause playback") self.mediaPlayer.pause() self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) @@ -179,6 +180,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } func settingsPopoverDismissed() { + optionsVC?.dismiss(animated: true, completion: nil) self.mediaPlayer.play() self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } @@ -188,8 +190,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe //View has loaded. //Show loading screen - delegate?.showLoadingView(self) - + mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14) //mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate") @@ -197,11 +198,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe mediaPlayer.delegate = self mediaPlayer.drawable = videoContentView - if(manifest.Type == "Episode") { - titleLabel.text = "\(manifest.Name) - S\(String(manifest.ParentIndexNumber ?? 0)):E\(String(manifest.IndexNumber ?? 0)) - \(manifest.SeriesName ?? "")" - } else { - titleLabel.text = manifest.Name - } + titleLabel.text = manifest.Name //Fetch max bitrate from UserDefaults depending on current connection mode let defaults = UserDefaults.standard @@ -333,10 +330,12 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe //MARK: VideoPlayerSettings Delegate func subtitleTrackChanged(newTrackID: Int32) { + selectedCaptionTrack = newTrackID mediaPlayer.currentVideoSubTitleIndex = newTrackID } func audioTrackChanged(newTrackID: Int32) { + selectedAudioTrack = newTrackID mediaPlayer.currentAudioTrackIndex = newTrackID } @@ -400,7 +399,12 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe timeText.text = timeTextStr if(CACurrentMediaTime() - controlsAppearTime > 5) { - videoControlsView.isHidden = true; + UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: { + self.videoControlsView.alpha = 0.0 + }, completion: { (finished: Bool) in + self.videoControlsView.isHidden = true; + self.videoControlsView.alpha = 1 + }) controlsAppearTime = 10000000000000000000000; } } else { @@ -419,10 +423,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe var progressBody: String = ""; progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":\(mediaPlayer.state == .paused ? "true" : "false"),\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(mediaPlayer.position * Float(manifest.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":569735888.888889}],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"EventName\":\"\(eventName)\"}"; - print(""); - print("Sending progress report") - print(progressBody) - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Progress") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" @@ -445,10 +445,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":true,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(mediaPlayer.position * Float(manifest.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":100000}],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(manifest.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; - print(""); - print("Sending stop report") - print(progressBody) - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Stopped") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" @@ -472,10 +468,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":false,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(manifest.Progress)),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(manifest.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; - print(""); - print("Sending play report") - print(progressBody) - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" diff --git a/JellyfinPlayer/Views/VideoPlayerSettingsView.swift b/JellyfinPlayer/Views/VideoPlayerSettingsView.swift index ff40adbd..7b744ef0 100644 --- a/JellyfinPlayer/Views/VideoPlayerSettingsView.swift +++ b/JellyfinPlayer/Views/VideoPlayerSettingsView.swift @@ -6,139 +6,76 @@ // import Foundation -import UIKit import SwiftUI -import Combine - -enum SettingsChangedEventTypes { - case subTrackChanged - case audioTrackChanged -} - -struct settingsChangedEvent { - let eventType: SettingsChangedEventTypes - let payload: AnyObject -} - -protocol VideoPlayerSettingsDelegate: AnyObject { - func subtitleTrackChanged(newTrackID: Int32) - func audioTrackChanged(newTrackID: Int32) - func settingsPopoverDismissed() -} - -class SettingsViewDelegate: ObservableObject { - - var subtitlesDidChange = PassthroughSubject() - - var subtitleTrackID: Int32 = 0 { - didSet { - self.subtitlesDidChange.send(self) - } - } - - var audioTrackDidChange = PassthroughSubject() - - var audioTrackID: Int32 = 0 { - didSet { - self.audioTrackDidChange.send(self) - } - } - - var shouldClose = PassthroughSubject() - - var close: Bool = false { - didSet { - self.shouldClose.send(self) - } - } -} class VideoPlayerSettingsView: UIViewController { - private var ctntView: VideoPlayerSettings? - private var contentViewDelegate: SettingsViewDelegate = SettingsViewDelegate() - weak var delegate: VideoPlayerSettingsDelegate? - private var subChangePublisher: AnyCancellable? - private var audioChangePublisher: AnyCancellable? - private var shouldClosePublisher: AnyCancellable? - var subtitles: [Subtitle] = [] - var audioTracks: [AudioTrack] = [] - var currentSubtitleTrack: Int32? - var currentAudioTrack: Int32? + private var contentView: UIHostingController! + weak var delegate: PlayerViewController? override func viewDidLoad() { super.viewDidLoad() - ctntView = VideoPlayerSettings(delegate: self.contentViewDelegate, subtitles: self.subtitles, audioTracks: self.audioTracks, initSub: currentSubtitleTrack ?? -1, initAudio: currentAudioTrack ?? 1) - let contentView = UIHostingController(rootView: ctntView) + contentView = UIHostingController(rootView: VideoPlayerSettings(delegate: self.delegate ?? PlayerViewController())) self.view.addSubview(contentView.view) contentView.view.translatesAutoresizingMaskIntoConstraints = false contentView.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - - self.subChangePublisher = self.contentViewDelegate.subtitlesDidChange.sink { suiDelegate in - self.delegate?.subtitleTrackChanged(newTrackID: suiDelegate.subtitleTrackID) - } - - self.audioChangePublisher = self.contentViewDelegate.audioTrackDidChange.sink { suiDelegate in - self.delegate?.audioTrackChanged(newTrackID: suiDelegate.audioTrackID) - } - - self.shouldClosePublisher = self.contentViewDelegate.shouldClose.sink { suiDelegate in - if(suiDelegate.close == true) { - self.delegate?.settingsPopoverDismissed() - self.dismiss(animated: true, completion: nil) - } - } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + self.delegate?.settingsPopoverDismissed() } } struct VideoPlayerSettings: View { - @ObservedObject var delegate: SettingsViewDelegate - @State private var subtitles: [Subtitle] - @State private var audioTracks: [AudioTrack] - @State private var subtitleSelection: Int32 - @State private var audioTrackSelection: Int32 + @State var delegate: PlayerViewController + @State var captionTrack: Int32 = -99; + @State var audioTrack: Int32 = -99; - init(delegate: SettingsViewDelegate, subtitles: [Subtitle], audioTracks: [AudioTrack], initSub: Int32, initAudio: Int32) { + init(delegate: PlayerViewController) { self.delegate = delegate - self.subtitles = subtitles - self.audioTracks = audioTracks - - subtitleSelection = initSub - audioTrackSelection = initAudio } var body: some View { - Form() { - if(UIDevice.current.userInterfaceIdiom == .phone) { - Button { - delegate.close = true - } label: { - HStack() { - Image(systemName: "chevron.left") - Text("Back").font(.callout) + NavigationView() { + Form() { + Picker("Closed Captions", selection: $captionTrack) { + ForEach(delegate.subtitleTrackArray, id: \.id) { caption in + Text(caption.name).tag(caption.id) + } + } + .onChange(of: captionTrack) { track in + self.delegate.subtitleTrackChanged(newTrackID: track) + } + Picker("Audio Track", selection: $audioTrack) { + ForEach(delegate.audioTrackArray, id: \.id) { caption in + Text(caption.name).tag(caption.id).lineLimit(1) + } + }.onChange(of: audioTrack) { track in + self.delegate.audioTrackChanged(newTrackID: track) + } + }.navigationBarTitleDisplayMode(.inline) + .navigationTitle("Audio & Captions") + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + if(UIDevice.current.userInterfaceIdiom == .phone) { + Button { + self.delegate.settingsPopoverDismissed() + } label: { + HStack() { + Image(systemName: "chevron.left") + Text("Back").font(.callout) + } + } } } } - Picker("Closed Captions", selection: $subtitleSelection) { - ForEach(subtitles, id: \.id) { caption in - Text(caption.name).tag(caption.id) - } - }.onChange(of: subtitleSelection) { id in - delegate.subtitleTrackID = id - } - Picker("Audio Track", selection: $audioTrackSelection) { - ForEach(audioTracks, id: \.id) { caption in - Text(caption.name).tag(caption.id).lineLimit(1) - } - }.onChange(of: audioTrackSelection) { id in - delegate.audioTrackID = id - } - } + }.offset(y: 14) + .onAppear(perform: { + _captionTrack.wrappedValue = self.delegate.selectedCaptionTrack + _audioTrack.wrappedValue = self.delegate.selectedAudioTrack + }) } } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ac4ac7ca..9dcf555b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -31,14 +31,6 @@ platform :ios do project_slug: 'jellyfin-swift-ios', dsym_path: "JellyfinPlayer.app.dSYM.zip" ) - set_github_release( - repository_name: "jellyfin/JellyfinPlayer", - name: "Release #{identifier_v}@#{identifier_s}", - tag_name: "v#{identifier_s}", - description: (File.read("Release Notes.rtf") rescue "No changelog provided"), - commitish: "main", - upload_assets: ["JellyfinPlayer.ipa"] - ) upload_to_testflight dynatrace_process_symbols( appId: "8c1f6941-ec78-480c-b589-b41aca29a52e",