Fix fast user switching

Show watched icon on completed episodes.
Add seek buttons to MPNowPlayingInfoCenter
Fix tvos remote play/pause not showing series name.
This commit is contained in:
Aiden Vigue 2021-07-01 14:34:51 -04:00
parent c0714761b2
commit 42d9b4b8a7
No known key found for this signature in database
GPG Key ID: B9A09843AB079D5B
8 changed files with 77 additions and 34 deletions

View File

@ -47,6 +47,16 @@ struct LandscapeItemElement: View {
ImageView(src: (item.type == "Episode" && !(inSeasonView ?? false) ? item.getSeriesBackdropImage(maxWidth: 445) : item.getBackdropImage(maxWidth: 445)), bh: item.type == "Episode" ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash())
.frame(width: 445, height: 250)
.cornerRadius(10)
.overlay(
ZStack {
if item.userData?.played ?? false {
Image(systemName: "circle.fill")
.foregroundColor(.white)
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(.systemBlue))
}
}.padding(2)
.opacity(1), alignment: .topTrailing).opacity(1)
.overlay(
ZStack(alignment: .leading) {
if focused && item.userData?.playedPercentage != nil {

View File

@ -69,9 +69,10 @@ struct EpisodeItemView: View {
.overlay(RoundedRectangle(cornerRadius: 2)
.stroke(Color.secondary, lineWidth: 1))
}
}.padding(.top, 15)
Spacer()
}.padding(.top, -15)
HStack {
HStack(alignment: .top) {
VStack(alignment: .trailing) {
if(studio != nil) {
Text("STUDIO")
@ -112,13 +113,6 @@ struct EpisodeItemView: View {
Spacer()
}
VStack(alignment: .leading) {
if(!(viewModel.item.taglines ?? []).isEmpty) {
Text(viewModel.item.taglines?.first ?? "")
.font(.body)
.italic()
.fontWeight(.medium)
.foregroundColor(.primary)
}
Text(viewModel.item.overview ?? "")
.font(.body)
.fontWeight(.medium)
@ -174,6 +168,7 @@ struct EpisodeItemView: View {
.frame(height: 360)
}
Spacer()
Spacer()
}.padding(EdgeInsets(top: 90, leading: 90, bottom: 0, trailing: 90))
}.onAppear(perform: onAppear)
}

View File

@ -21,6 +21,19 @@ struct HomeView: View {
ProgressView()
} else {
LazyVStack(alignment: .leading) {
Button {
let nc = NotificationCenter.default
nc.post(name: Notification.Name("didSignOut"), object: nil)
} label: {
HStack {
ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(SessionManager.current.user.user_id!)/Images/Primary?width=500")!)
.frame(width: 50, height: 50)
.cornerRadius(25.0)
Text(SessionManager.current.user.username ?? "")
.font(.headline)
.fontWeight(.semibold)
}
}.padding(.leading, 90)
if !viewModel.resumeItems.isEmpty {
ContinueWatchingView(items: viewModel.resumeItems)
}

View File

@ -82,6 +82,7 @@ struct SeasonItemView: View {
Text(viewModel.isFavorited ? "Unfavorite" : "Favorite")
.font(.caption)
}
VStack {
Button {
viewModel.updateWatchState()

View File

@ -247,32 +247,42 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true
commandCenter.pauseCommand.isEnabled = true
commandCenter.seekForwardCommand.isEnabled = true
commandCenter.seekBackwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.preferredIntervals = [15]
commandCenter.skipForwardCommand.isEnabled = true
commandCenter.skipForwardCommand.preferredIntervals = [30]
commandCenter.changePlaybackPositionCommand.isEnabled = true
commandCenter.enableLanguageOptionCommand.isEnabled = true
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { _ in
self.pause()
self.showingControls = true
self.controlsView.isHidden = false
self.controlsAppearTime = CACurrentMediaTime()
return .success
}
// Add handler for Play command
commandCenter.playCommand.addTarget { _ in
self.play()
self.showingControls = false
self.controlsView.isHidden = true
return .success
}
// Add handler for FF command
commandCenter.seekForwardCommand.addTarget { _ in
commandCenter.skipForwardCommand.addTarget { skipEvent in
self.mediaPlayer.jumpForward(30)
self.sendProgressReport(eventName: "timeupdate")
return .success
}
// Add handler for RW command
commandCenter.seekBackwardCommand.addTarget { _ in
commandCenter.skipBackwardCommand.addTarget { skipEvent in
self.mediaPlayer.jumpBackward(15)
self.sendProgressReport(eventName: "timeupdate")
return .success
@ -314,12 +324,15 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
var nowPlayingInfo = [String: Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video"
if(manifest.type == "Episode") {
nowPlayingInfo[MPMediaItemPropertyArtist] = "\(manifest.seriesName ?? manifest.name ?? "")\(manifest.getEpisodeLocator())"
}
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks
if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) {
if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 500)) {
if let artworkImage = UIImage(data: imageData as Data) {
let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in
return artworkImage

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19141.11" systemVersion="21A5248p" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19141.11" systemVersion="21A5268h" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<entity name="Server" representedClassName="Server" syncable="YES" codeGenerationType="class">
<attribute name="baseURI" attributeType="String" defaultValueString=""/>
<attribute name="name" attributeType="String" defaultValueString=""/>
@ -7,11 +7,12 @@
</entity>
<entity name="SignedInUser" representedClassName="SignedInUser" syncable="YES" codeGenerationType="class">
<attribute name="appletv_id" optional="YES" attributeType="String"/>
<attribute name="device_uuid" attributeType="String" defaultValueString=""/>
<attribute name="user_id" attributeType="String" defaultValueString=""/>
<attribute name="username" attributeType="String" defaultValueString=""/>
</entity>
<elements>
<element name="Server" positionX="-63" positionY="-9" width="128" height="74"/>
<element name="SignedInUser" positionX="-63" positionY="9" width="128" height="74"/>
<element name="SignedInUser" positionX="-63" positionY="9" width="128" height="89"/>
</elements>
</model>

View File

@ -45,11 +45,11 @@ final class SessionManager {
if user != nil {
let authToken = getAuthToken(userID: user.user_id!)
generateAuthHeader(with: authToken)
generateAuthHeader(with: authToken, deviceID: user.device_uuid)
}
}
fileprivate func generateAuthHeader(with authToken: String?) {
fileprivate func generateAuthHeader(with authToken: String?, deviceID devID: String?) {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var deviceName = UIDevice.current.name
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
@ -57,20 +57,28 @@ final class SessionManager {
var header = "MediaBrowser "
#if os(tvOS)
header.append("Client=\"SwiftFin tvOS\", ")
header.append("Client=\"Jellyfin tvOS\", ")
#else
header.append("Client=\"SwiftFin iOS\", ")
#endif
header.append("Device=\"\(deviceName)\", ")
#if os(tvOS)
header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")\", ")
deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")"
#else
header.append("DeviceId=\"iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")\", ")
deviceID = "iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")"
#endif
if(devID == nil) {
#if os(tvOS)
header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ")
deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))"
#else
header.append("DeviceId=\"iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ")
deviceID = "iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))"
#endif
print("generated device id: \(deviceID)")
} else {
print("device id provided: \(devID!)")
header.append("DeviceId=\"\(devID!)\", ")
deviceID = devID!
}
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
if authToken != nil {
@ -108,23 +116,25 @@ final class SessionManager {
func loginWithSavedSession(user: SignedInUser) {
let accessToken = getAuthToken(userID: user.user_id!)
print("logging in with saved session");
self.user = user
generateAuthHeader(with: accessToken)
generateAuthHeader(with: accessToken, deviceID: user.device_uuid)
print(JellyfinAPI.customHeaders)
let nc = NotificationCenter.default
nc.post(name: Notification.Name("didSignIn"), object: nil)
}
func login(username: String, password: String) -> AnyPublisher<SignedInUser, Error> {
generateAuthHeader(with: nil)
generateAuthHeader(with: nil, deviceID: nil)
return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password))
.map { response -> (SignedInUser, String?) in
let user = SignedInUser(context: PersistenceController.shared.container.viewContext)
user.username = response.user?.name
user.user_id = response.user?.id
user.device_uuid = self.deviceID
#if os(tvOS)
// user.appletv_id = tvUserManager.currentUserIdentifier ?? ""
#endif
@ -139,7 +149,7 @@ final class SessionManager {
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
keychain.set(accessToken!, forKey: "AccessToken_\(user.user_id!)")
generateAuthHeader(with: accessToken)
generateAuthHeader(with: accessToken, deviceID: user.device_uuid)
let nc = NotificationCenter.default
nc.post(name: Notification.Name("didSignIn"), object: nil)
@ -151,11 +161,11 @@ final class SessionManager {
func logout() {
let nc = NotificationCenter.default
nc.post(name: Notification.Name("didSignOut"), object: nil)
dump(user)
let keychain = KeychainSwift()
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
keychain.delete("AccessToken_\(user?.user_id ?? "")")
generateAuthHeader(with: nil)
generateAuthHeader(with: nil, deviceID: nil)
let deleteRequest = NSBatchDeleteRequest(objectIDs: [user.objectID])
user = nil

View File

@ -13,7 +13,7 @@ import JellyfinAPI
final class SeasonItemViewModel: DetailItemViewModel {
@Published var episodes = [BaseItemDto]()
override init(item: BaseItemDto) {
super.init(item: item)
self.item = item