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:
parent
c0714761b2
commit
42d9b4b8a7
|
@ -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())
|
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)
|
.frame(width: 445, height: 250)
|
||||||
.cornerRadius(10)
|
.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(
|
.overlay(
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
if focused && item.userData?.playedPercentage != nil {
|
if focused && item.userData?.playedPercentage != nil {
|
||||||
|
|
|
@ -69,9 +69,10 @@ struct EpisodeItemView: View {
|
||||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
.overlay(RoundedRectangle(cornerRadius: 2)
|
||||||
.stroke(Color.secondary, lineWidth: 1))
|
.stroke(Color.secondary, lineWidth: 1))
|
||||||
}
|
}
|
||||||
}.padding(.top, 15)
|
Spacer()
|
||||||
|
}.padding(.top, -15)
|
||||||
|
|
||||||
HStack {
|
HStack(alignment: .top) {
|
||||||
VStack(alignment: .trailing) {
|
VStack(alignment: .trailing) {
|
||||||
if(studio != nil) {
|
if(studio != nil) {
|
||||||
Text("STUDIO")
|
Text("STUDIO")
|
||||||
|
@ -112,13 +113,6 @@ struct EpisodeItemView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if(!(viewModel.item.taglines ?? []).isEmpty) {
|
|
||||||
Text(viewModel.item.taglines?.first ?? "")
|
|
||||||
.font(.body)
|
|
||||||
.italic()
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
Text(viewModel.item.overview ?? "")
|
Text(viewModel.item.overview ?? "")
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
|
@ -174,6 +168,7 @@ struct EpisodeItemView: View {
|
||||||
.frame(height: 360)
|
.frame(height: 360)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
Spacer()
|
||||||
}.padding(EdgeInsets(top: 90, leading: 90, bottom: 0, trailing: 90))
|
}.padding(EdgeInsets(top: 90, leading: 90, bottom: 0, trailing: 90))
|
||||||
}.onAppear(perform: onAppear)
|
}.onAppear(perform: onAppear)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,19 @@ struct HomeView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else {
|
} else {
|
||||||
LazyVStack(alignment: .leading) {
|
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 {
|
if !viewModel.resumeItems.isEmpty {
|
||||||
ContinueWatchingView(items: viewModel.resumeItems)
|
ContinueWatchingView(items: viewModel.resumeItems)
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ struct SeasonItemView: View {
|
||||||
Text(viewModel.isFavorited ? "Unfavorite" : "Favorite")
|
Text(viewModel.isFavorited ? "Unfavorite" : "Favorite")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
Button {
|
Button {
|
||||||
viewModel.updateWatchState()
|
viewModel.updateWatchState()
|
||||||
|
|
|
@ -247,32 +247,42 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
let commandCenter = MPRemoteCommandCenter.shared()
|
let commandCenter = MPRemoteCommandCenter.shared()
|
||||||
commandCenter.playCommand.isEnabled = true
|
commandCenter.playCommand.isEnabled = true
|
||||||
commandCenter.pauseCommand.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.changePlaybackPositionCommand.isEnabled = true
|
||||||
commandCenter.enableLanguageOptionCommand.isEnabled = true
|
commandCenter.enableLanguageOptionCommand.isEnabled = true
|
||||||
|
|
||||||
// Add handler for Pause Command
|
// Add handler for Pause Command
|
||||||
commandCenter.pauseCommand.addTarget { _ in
|
commandCenter.pauseCommand.addTarget { _ in
|
||||||
self.pause()
|
self.pause()
|
||||||
|
self.showingControls = true
|
||||||
|
self.controlsView.isHidden = false
|
||||||
|
self.controlsAppearTime = CACurrentMediaTime()
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add handler for Play command
|
// Add handler for Play command
|
||||||
commandCenter.playCommand.addTarget { _ in
|
commandCenter.playCommand.addTarget { _ in
|
||||||
self.play()
|
self.play()
|
||||||
|
self.showingControls = false
|
||||||
|
self.controlsView.isHidden = true
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add handler for FF command
|
// Add handler for FF command
|
||||||
commandCenter.seekForwardCommand.addTarget { _ in
|
commandCenter.skipForwardCommand.addTarget { skipEvent in
|
||||||
self.mediaPlayer.jumpForward(30)
|
self.mediaPlayer.jumpForward(30)
|
||||||
self.sendProgressReport(eventName: "timeupdate")
|
self.sendProgressReport(eventName: "timeupdate")
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add handler for RW command
|
// Add handler for RW command
|
||||||
commandCenter.seekBackwardCommand.addTarget { _ in
|
commandCenter.skipBackwardCommand.addTarget { skipEvent in
|
||||||
self.mediaPlayer.jumpBackward(15)
|
self.mediaPlayer.jumpBackward(15)
|
||||||
self.sendProgressReport(eventName: "timeupdate")
|
self.sendProgressReport(eventName: "timeupdate")
|
||||||
return .success
|
return .success
|
||||||
|
@ -314,12 +324,15 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
var nowPlayingInfo = [String: Any]()
|
var nowPlayingInfo = [String: Any]()
|
||||||
|
|
||||||
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video"
|
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video"
|
||||||
|
if(manifest.type == "Episode") {
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyArtist] = "\(manifest.seriesName ?? manifest.name ?? "") • \(manifest.getEpisodeLocator())"
|
||||||
|
}
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video
|
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video
|
||||||
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks
|
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) {
|
if let artworkImage = UIImage(data: imageData as Data) {
|
||||||
let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in
|
let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in
|
||||||
return artworkImage
|
return artworkImage
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?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">
|
<entity name="Server" representedClassName="Server" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="baseURI" attributeType="String" defaultValueString=""/>
|
<attribute name="baseURI" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||||
|
@ -7,11 +7,12 @@
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SignedInUser" representedClassName="SignedInUser" syncable="YES" codeGenerationType="class">
|
<entity name="SignedInUser" representedClassName="SignedInUser" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="appletv_id" optional="YES" attributeType="String"/>
|
<attribute name="appletv_id" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="device_uuid" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="user_id" attributeType="String" defaultValueString=""/>
|
<attribute name="user_id" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="username" attributeType="String" defaultValueString=""/>
|
<attribute name="username" attributeType="String" defaultValueString=""/>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Server" positionX="-63" positionY="-9" width="128" height="74"/>
|
<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>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -45,11 +45,11 @@ final class SessionManager {
|
||||||
|
|
||||||
if user != nil {
|
if user != nil {
|
||||||
let authToken = getAuthToken(userID: user.user_id!)
|
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
|
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||||
var deviceName = UIDevice.current.name
|
var deviceName = UIDevice.current.name
|
||||||
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
||||||
|
@ -57,20 +57,28 @@ final class SessionManager {
|
||||||
|
|
||||||
var header = "MediaBrowser "
|
var header = "MediaBrowser "
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
header.append("Client=\"SwiftFin tvOS\", ")
|
header.append("Client=\"Jellyfin tvOS\", ")
|
||||||
#else
|
#else
|
||||||
header.append("Client=\"SwiftFin iOS\", ")
|
header.append("Client=\"SwiftFin iOS\", ")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
header.append("Device=\"\(deviceName)\", ")
|
header.append("Device=\"\(deviceName)\", ")
|
||||||
|
|
||||||
#if os(tvOS)
|
if(devID == nil) {
|
||||||
header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")\", ")
|
#if os(tvOS)
|
||||||
deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")"
|
header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ")
|
||||||
#else
|
deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))"
|
||||||
header.append("DeviceId=\"iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")\", ")
|
#else
|
||||||
deviceID = "iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")"
|
header.append("DeviceId=\"iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ")
|
||||||
#endif
|
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")\", ")
|
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
|
||||||
|
|
||||||
if authToken != nil {
|
if authToken != nil {
|
||||||
|
@ -108,23 +116,25 @@ final class SessionManager {
|
||||||
|
|
||||||
func loginWithSavedSession(user: SignedInUser) {
|
func loginWithSavedSession(user: SignedInUser) {
|
||||||
let accessToken = getAuthToken(userID: user.user_id!)
|
let accessToken = getAuthToken(userID: user.user_id!)
|
||||||
|
print("logging in with saved session");
|
||||||
|
|
||||||
self.user = user
|
self.user = user
|
||||||
generateAuthHeader(with: accessToken)
|
generateAuthHeader(with: accessToken, deviceID: user.device_uuid)
|
||||||
print(JellyfinAPI.customHeaders)
|
print(JellyfinAPI.customHeaders)
|
||||||
let nc = NotificationCenter.default
|
let nc = NotificationCenter.default
|
||||||
nc.post(name: Notification.Name("didSignIn"), object: nil)
|
nc.post(name: Notification.Name("didSignIn"), object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func login(username: String, password: String) -> AnyPublisher<SignedInUser, Error> {
|
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))
|
return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password))
|
||||||
.map { response -> (SignedInUser, String?) in
|
.map { response -> (SignedInUser, String?) in
|
||||||
let user = SignedInUser(context: PersistenceController.shared.container.viewContext)
|
let user = SignedInUser(context: PersistenceController.shared.container.viewContext)
|
||||||
user.username = response.user?.name
|
user.username = response.user?.name
|
||||||
user.user_id = response.user?.id
|
user.user_id = response.user?.id
|
||||||
|
user.device_uuid = self.deviceID
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
// user.appletv_id = tvUserManager.currentUserIdentifier ?? ""
|
// user.appletv_id = tvUserManager.currentUserIdentifier ?? ""
|
||||||
#endif
|
#endif
|
||||||
|
@ -139,7 +149,7 @@ final class SessionManager {
|
||||||
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
|
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
|
||||||
keychain.set(accessToken!, forKey: "AccessToken_\(user.user_id!)")
|
keychain.set(accessToken!, forKey: "AccessToken_\(user.user_id!)")
|
||||||
|
|
||||||
generateAuthHeader(with: accessToken)
|
generateAuthHeader(with: accessToken, deviceID: user.device_uuid)
|
||||||
|
|
||||||
let nc = NotificationCenter.default
|
let nc = NotificationCenter.default
|
||||||
nc.post(name: Notification.Name("didSignIn"), object: nil)
|
nc.post(name: Notification.Name("didSignIn"), object: nil)
|
||||||
|
@ -151,11 +161,11 @@ final class SessionManager {
|
||||||
func logout() {
|
func logout() {
|
||||||
let nc = NotificationCenter.default
|
let nc = NotificationCenter.default
|
||||||
nc.post(name: Notification.Name("didSignOut"), object: nil)
|
nc.post(name: Notification.Name("didSignOut"), object: nil)
|
||||||
|
dump(user)
|
||||||
let keychain = KeychainSwift()
|
let keychain = KeychainSwift()
|
||||||
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
|
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
|
||||||
keychain.delete("AccessToken_\(user?.user_id ?? "")")
|
keychain.delete("AccessToken_\(user?.user_id ?? "")")
|
||||||
generateAuthHeader(with: nil)
|
generateAuthHeader(with: nil, deviceID: nil)
|
||||||
|
|
||||||
let deleteRequest = NSBatchDeleteRequest(objectIDs: [user.objectID])
|
let deleteRequest = NSBatchDeleteRequest(objectIDs: [user.objectID])
|
||||||
user = nil
|
user = nil
|
||||||
|
|
|
@ -13,7 +13,7 @@ import JellyfinAPI
|
||||||
|
|
||||||
final class SeasonItemViewModel: DetailItemViewModel {
|
final class SeasonItemViewModel: DetailItemViewModel {
|
||||||
@Published var episodes = [BaseItemDto]()
|
@Published var episodes = [BaseItemDto]()
|
||||||
|
|
||||||
override init(item: BaseItemDto) {
|
override init(item: BaseItemDto) {
|
||||||
super.init(item: item)
|
super.init(item: item)
|
||||||
self.item = item
|
self.item = item
|
||||||
|
|
Loading…
Reference in New Issue