Add Quick Connect sign in to tvOS (v2) (#487)
This commit is contained in:
parent
30cb980ed3
commit
ed519744f4
|
@ -89,7 +89,7 @@ final class MainCoordinator: NavigationCoordinatable {
|
||||||
else { fatalError("Need to have new current login state server") }
|
else { fatalError("Need to have new current login state server") }
|
||||||
guard SessionManager.main.currentLogin != nil else { return }
|
guard SessionManager.main.currentLogin != nil else { return }
|
||||||
if newCurrentServerState.id == SessionManager.main.currentLogin.server.id {
|
if newCurrentServerState.id == SessionManager.main.currentLogin.server.id {
|
||||||
SessionManager.main.loginUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user)
|
SessionManager.main.signInUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,137 +44,137 @@ extension BaseItemDto {
|
||||||
var viewModels: [VideoPlayerViewModel] = []
|
var viewModels: [VideoPlayerViewModel] = []
|
||||||
|
|
||||||
for currentMediaSource in mediaSources {
|
for currentMediaSource in mediaSources {
|
||||||
let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first
|
let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first
|
||||||
let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
|
let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
|
||||||
let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
|
let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
|
||||||
|
|
||||||
let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! })
|
let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! })
|
||||||
|
|
||||||
let defaultSubtitleStream = subtitleStreams
|
let defaultSubtitleStream = subtitleStreams
|
||||||
.first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 })
|
.first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 })
|
||||||
|
|
||||||
// MARK: Build Streams
|
// MARK: Build Streams
|
||||||
|
|
||||||
let directStreamURL: URL
|
let directStreamURL: URL
|
||||||
let transcodedStreamURL: URLComponents?
|
let transcodedStreamURL: URLComponents?
|
||||||
var hlsStreamURL: URL
|
var hlsStreamURL: URL
|
||||||
let mediaSourceID: String
|
let mediaSourceID: String
|
||||||
let streamType: ServerStreamType
|
let streamType: ServerStreamType
|
||||||
|
|
||||||
if mediaSources.count > 1 {
|
if mediaSources.count > 1 {
|
||||||
mediaSourceID = currentMediaSource.id!
|
mediaSourceID = currentMediaSource.id!
|
||||||
} else {
|
} else {
|
||||||
mediaSourceID = self.id!
|
mediaSourceID = self.id!
|
||||||
}
|
|
||||||
|
|
||||||
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(
|
|
||||||
itemId: self.id!,
|
|
||||||
_static: true,
|
|
||||||
tag: self.etag,
|
|
||||||
playSessionId: response.playSessionId,
|
|
||||||
minSegments: 6,
|
|
||||||
mediaSourceId: mediaSourceID
|
|
||||||
)
|
|
||||||
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
|
||||||
|
|
||||||
if let transcodeURL = currentMediaSource.transcodingUrl {
|
|
||||||
streamType = .transcode
|
|
||||||
transcodedStreamURL = URLComponents(
|
|
||||||
string: SessionManager.main.currentLogin.server.currentURI
|
|
||||||
.appending(transcodeURL)
|
|
||||||
)!
|
|
||||||
} else {
|
|
||||||
streamType = .direct
|
|
||||||
transcodedStreamURL = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(
|
|
||||||
itemId: id ?? "",
|
|
||||||
mediaSourceId: id ?? "",
|
|
||||||
_static: true,
|
|
||||||
tag: currentMediaSource.eTag,
|
|
||||||
deviceProfileId: nil,
|
|
||||||
playSessionId: response.playSessionId,
|
|
||||||
segmentContainer: "ts",
|
|
||||||
segmentLength: nil,
|
|
||||||
minSegments: 2,
|
|
||||||
deviceId: UIDevice.vendorUUIDString,
|
|
||||||
audioCodec: audioStreams
|
|
||||||
.compactMap(\.codec)
|
|
||||||
.joined(separator: ","),
|
|
||||||
breakOnNonKeyFrames: true,
|
|
||||||
requireAvc: true,
|
|
||||||
transcodingMaxAudioChannels: 6,
|
|
||||||
videoCodec: videoStream?.codec,
|
|
||||||
videoStreamIndex: videoStream?.index,
|
|
||||||
enableAdaptiveBitrateStreaming: true
|
|
||||||
)
|
|
||||||
|
|
||||||
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
|
||||||
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
|
||||||
|
|
||||||
hlsStreamURL = hlsStreamComponents.url!
|
|
||||||
|
|
||||||
// MARK: VidoPlayerViewModel Creation
|
|
||||||
|
|
||||||
var subtitle: String?
|
|
||||||
|
|
||||||
// MARK: Attach media content to self
|
|
||||||
|
|
||||||
var modifiedSelfItem = self
|
|
||||||
modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams
|
|
||||||
|
|
||||||
// TODO: other forms of media subtitle
|
|
||||||
if self.itemType == .episode {
|
|
||||||
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
|
||||||
subtitle = "\(seriesName) - \(episodeLocator)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(
|
||||||
|
itemId: self.id!,
|
||||||
|
_static: true,
|
||||||
|
tag: self.etag,
|
||||||
|
playSessionId: response.playSessionId,
|
||||||
|
minSegments: 6,
|
||||||
|
mediaSourceId: mediaSourceID
|
||||||
|
)
|
||||||
|
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
||||||
|
|
||||||
|
if let transcodeURL = currentMediaSource.transcodingUrl {
|
||||||
|
streamType = .transcode
|
||||||
|
transcodedStreamURL = URLComponents(
|
||||||
|
string: SessionManager.main.currentLogin.server.currentURI
|
||||||
|
.appending(transcodeURL)
|
||||||
|
)!
|
||||||
|
} else {
|
||||||
|
streamType = .direct
|
||||||
|
transcodedStreamURL = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(
|
||||||
|
itemId: id ?? "",
|
||||||
|
mediaSourceId: id ?? "",
|
||||||
|
_static: true,
|
||||||
|
tag: currentMediaSource.eTag,
|
||||||
|
deviceProfileId: nil,
|
||||||
|
playSessionId: response.playSessionId,
|
||||||
|
segmentContainer: "ts",
|
||||||
|
segmentLength: nil,
|
||||||
|
minSegments: 2,
|
||||||
|
deviceId: UIDevice.vendorUUIDString,
|
||||||
|
audioCodec: audioStreams
|
||||||
|
.compactMap(\.codec)
|
||||||
|
.joined(separator: ","),
|
||||||
|
breakOnNonKeyFrames: true,
|
||||||
|
requireAvc: true,
|
||||||
|
transcodingMaxAudioChannels: 6,
|
||||||
|
videoCodec: videoStream?.codec,
|
||||||
|
videoStreamIndex: videoStream?.index,
|
||||||
|
enableAdaptiveBitrateStreaming: true
|
||||||
|
)
|
||||||
|
|
||||||
|
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
||||||
|
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
||||||
|
|
||||||
|
hlsStreamURL = hlsStreamComponents.url!
|
||||||
|
|
||||||
|
// MARK: VidoPlayerViewModel Creation
|
||||||
|
|
||||||
|
var subtitle: String?
|
||||||
|
|
||||||
|
// MARK: Attach media content to self
|
||||||
|
|
||||||
|
var modifiedSelfItem = self
|
||||||
|
modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams
|
||||||
|
|
||||||
|
// TODO: other forms of media subtitle
|
||||||
|
if self.itemType == .episode {
|
||||||
|
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
||||||
|
subtitle = "\(seriesName) - \(episodeLocator)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subtitlesEnabled = defaultSubtitleStream != nil
|
||||||
|
|
||||||
|
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
||||||
|
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
||||||
|
|
||||||
|
let overlayType = Defaults[.overlayType]
|
||||||
|
|
||||||
|
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
||||||
|
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
||||||
|
|
||||||
|
var fileName: String?
|
||||||
|
if let lastInPath = currentMediaSource.path?.split(separator: "/").last {
|
||||||
|
fileName = String(lastInPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoPlayerViewModel = VideoPlayerViewModel(
|
||||||
|
item: modifiedSelfItem,
|
||||||
|
title: modifiedSelfItem.name ?? "",
|
||||||
|
subtitle: subtitle,
|
||||||
|
directStreamURL: directStreamURL,
|
||||||
|
transcodedStreamURL: transcodedStreamURL?.url,
|
||||||
|
hlsStreamURL: hlsStreamURL,
|
||||||
|
streamType: streamType,
|
||||||
|
response: response,
|
||||||
|
audioStreams: audioStreams,
|
||||||
|
subtitleStreams: subtitleStreams,
|
||||||
|
chapters: modifiedSelfItem.chapters ?? [],
|
||||||
|
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
||||||
|
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
||||||
|
subtitlesEnabled: subtitlesEnabled,
|
||||||
|
autoplayEnabled: autoplayEnabled,
|
||||||
|
overlayType: overlayType,
|
||||||
|
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
||||||
|
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
||||||
|
shouldShowAutoPlay: shouldShowAutoPlay,
|
||||||
|
container: currentMediaSource.container ?? "",
|
||||||
|
filename: fileName,
|
||||||
|
versionName: currentMediaSource.name
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModels.append(videoPlayerViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtitlesEnabled = defaultSubtitleStream != nil
|
return viewModels
|
||||||
|
|
||||||
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
|
||||||
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
|
||||||
|
|
||||||
let overlayType = Defaults[.overlayType]
|
|
||||||
|
|
||||||
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
|
||||||
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
|
||||||
|
|
||||||
var fileName: String?
|
|
||||||
if let lastInPath = currentMediaSource.path?.split(separator: "/").last {
|
|
||||||
fileName = String(lastInPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
let videoPlayerViewModel = VideoPlayerViewModel(
|
|
||||||
item: modifiedSelfItem,
|
|
||||||
title: modifiedSelfItem.name ?? "",
|
|
||||||
subtitle: subtitle,
|
|
||||||
directStreamURL: directStreamURL,
|
|
||||||
transcodedStreamURL: transcodedStreamURL?.url,
|
|
||||||
hlsStreamURL: hlsStreamURL,
|
|
||||||
streamType: streamType,
|
|
||||||
response: response,
|
|
||||||
audioStreams: audioStreams,
|
|
||||||
subtitleStreams: subtitleStreams,
|
|
||||||
chapters: modifiedSelfItem.chapters ?? [],
|
|
||||||
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
|
||||||
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
|
||||||
subtitlesEnabled: subtitlesEnabled,
|
|
||||||
autoplayEnabled: autoplayEnabled,
|
|
||||||
overlayType: overlayType,
|
|
||||||
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
|
||||||
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
|
||||||
shouldShowAutoPlay: shouldShowAutoPlay,
|
|
||||||
container: currentMediaSource.container ?? "",
|
|
||||||
filename: fileName,
|
|
||||||
versionName: currentMediaSource.name
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModels.append(videoPlayerViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
return viewModels
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -211,137 +211,137 @@ extension BaseItemDto {
|
||||||
var viewModels: [VideoPlayerViewModel] = []
|
var viewModels: [VideoPlayerViewModel] = []
|
||||||
|
|
||||||
for currentMediaSource in mediaSources {
|
for currentMediaSource in mediaSources {
|
||||||
let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first
|
let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first
|
||||||
let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
|
let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
|
||||||
let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
|
let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
|
||||||
|
|
||||||
let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! })
|
let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! })
|
||||||
|
|
||||||
let defaultSubtitleStream = subtitleStreams
|
let defaultSubtitleStream = subtitleStreams
|
||||||
.first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 })
|
.first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 })
|
||||||
|
|
||||||
// MARK: Build Streams
|
// MARK: Build Streams
|
||||||
|
|
||||||
let directStreamURL: URL
|
let directStreamURL: URL
|
||||||
let transcodedStreamURL: URLComponents?
|
let transcodedStreamURL: URLComponents?
|
||||||
var hlsStreamURL: URL
|
var hlsStreamURL: URL
|
||||||
let mediaSourceID: String
|
let mediaSourceID: String
|
||||||
let streamType: ServerStreamType
|
let streamType: ServerStreamType
|
||||||
|
|
||||||
if mediaSources.count > 1 {
|
if mediaSources.count > 1 {
|
||||||
mediaSourceID = currentMediaSource.id!
|
mediaSourceID = currentMediaSource.id!
|
||||||
} else {
|
} else {
|
||||||
mediaSourceID = self.id!
|
mediaSourceID = self.id!
|
||||||
}
|
|
||||||
|
|
||||||
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(
|
|
||||||
itemId: self.id!,
|
|
||||||
_static: true,
|
|
||||||
tag: self.etag,
|
|
||||||
playSessionId: response.playSessionId,
|
|
||||||
minSegments: 6,
|
|
||||||
mediaSourceId: mediaSourceID
|
|
||||||
)
|
|
||||||
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
|
||||||
|
|
||||||
if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.liveTVForceDirectPlay] {
|
|
||||||
streamType = .transcode
|
|
||||||
transcodedStreamURL = URLComponents(
|
|
||||||
string: SessionManager.main.currentLogin.server.currentURI
|
|
||||||
.appending(transcodeURL)
|
|
||||||
)!
|
|
||||||
} else {
|
|
||||||
streamType = .direct
|
|
||||||
transcodedStreamURL = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(
|
|
||||||
itemId: id ?? "",
|
|
||||||
mediaSourceId: id ?? "",
|
|
||||||
_static: true,
|
|
||||||
tag: currentMediaSource.eTag,
|
|
||||||
deviceProfileId: nil,
|
|
||||||
playSessionId: response.playSessionId,
|
|
||||||
segmentContainer: "ts",
|
|
||||||
segmentLength: nil,
|
|
||||||
minSegments: 2,
|
|
||||||
deviceId: UIDevice.vendorUUIDString,
|
|
||||||
audioCodec: audioStreams
|
|
||||||
.compactMap(\.codec)
|
|
||||||
.joined(separator: ","),
|
|
||||||
breakOnNonKeyFrames: true,
|
|
||||||
requireAvc: true,
|
|
||||||
transcodingMaxAudioChannels: 6,
|
|
||||||
videoCodec: videoStream?.codec,
|
|
||||||
videoStreamIndex: videoStream?.index,
|
|
||||||
enableAdaptiveBitrateStreaming: true
|
|
||||||
)
|
|
||||||
|
|
||||||
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
|
||||||
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
|
||||||
|
|
||||||
hlsStreamURL = hlsStreamComponents.url!
|
|
||||||
|
|
||||||
// MARK: VidoPlayerViewModel Creation
|
|
||||||
|
|
||||||
var subtitle: String?
|
|
||||||
|
|
||||||
// MARK: Attach media content to self
|
|
||||||
|
|
||||||
var modifiedSelfItem = self
|
|
||||||
modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams
|
|
||||||
|
|
||||||
// TODO: other forms of media subtitle
|
|
||||||
if self.itemType == .episode {
|
|
||||||
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
|
||||||
subtitle = "\(seriesName) - \(episodeLocator)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(
|
||||||
|
itemId: self.id!,
|
||||||
|
_static: true,
|
||||||
|
tag: self.etag,
|
||||||
|
playSessionId: response.playSessionId,
|
||||||
|
minSegments: 6,
|
||||||
|
mediaSourceId: mediaSourceID
|
||||||
|
)
|
||||||
|
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
||||||
|
|
||||||
|
if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.liveTVForceDirectPlay] {
|
||||||
|
streamType = .transcode
|
||||||
|
transcodedStreamURL = URLComponents(
|
||||||
|
string: SessionManager.main.currentLogin.server.currentURI
|
||||||
|
.appending(transcodeURL)
|
||||||
|
)!
|
||||||
|
} else {
|
||||||
|
streamType = .direct
|
||||||
|
transcodedStreamURL = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(
|
||||||
|
itemId: id ?? "",
|
||||||
|
mediaSourceId: id ?? "",
|
||||||
|
_static: true,
|
||||||
|
tag: currentMediaSource.eTag,
|
||||||
|
deviceProfileId: nil,
|
||||||
|
playSessionId: response.playSessionId,
|
||||||
|
segmentContainer: "ts",
|
||||||
|
segmentLength: nil,
|
||||||
|
minSegments: 2,
|
||||||
|
deviceId: UIDevice.vendorUUIDString,
|
||||||
|
audioCodec: audioStreams
|
||||||
|
.compactMap(\.codec)
|
||||||
|
.joined(separator: ","),
|
||||||
|
breakOnNonKeyFrames: true,
|
||||||
|
requireAvc: true,
|
||||||
|
transcodingMaxAudioChannels: 6,
|
||||||
|
videoCodec: videoStream?.codec,
|
||||||
|
videoStreamIndex: videoStream?.index,
|
||||||
|
enableAdaptiveBitrateStreaming: true
|
||||||
|
)
|
||||||
|
|
||||||
|
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
||||||
|
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
||||||
|
|
||||||
|
hlsStreamURL = hlsStreamComponents.url!
|
||||||
|
|
||||||
|
// MARK: VidoPlayerViewModel Creation
|
||||||
|
|
||||||
|
var subtitle: String?
|
||||||
|
|
||||||
|
// MARK: Attach media content to self
|
||||||
|
|
||||||
|
var modifiedSelfItem = self
|
||||||
|
modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams
|
||||||
|
|
||||||
|
// TODO: other forms of media subtitle
|
||||||
|
if self.itemType == .episode {
|
||||||
|
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
||||||
|
subtitle = "\(seriesName) - \(episodeLocator)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subtitlesEnabled = defaultSubtitleStream != nil
|
||||||
|
|
||||||
|
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
||||||
|
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
||||||
|
|
||||||
|
let overlayType = Defaults[.overlayType]
|
||||||
|
|
||||||
|
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
||||||
|
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
||||||
|
|
||||||
|
var fileName: String?
|
||||||
|
if let lastInPath = currentMediaSource.path?.split(separator: "/").last {
|
||||||
|
fileName = String(lastInPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoPlayerViewModel = VideoPlayerViewModel(
|
||||||
|
item: modifiedSelfItem,
|
||||||
|
title: modifiedSelfItem.name ?? "",
|
||||||
|
subtitle: subtitle,
|
||||||
|
directStreamURL: directStreamURL,
|
||||||
|
transcodedStreamURL: transcodedStreamURL?.url,
|
||||||
|
hlsStreamURL: hlsStreamURL,
|
||||||
|
streamType: streamType,
|
||||||
|
response: response,
|
||||||
|
audioStreams: audioStreams,
|
||||||
|
subtitleStreams: subtitleStreams,
|
||||||
|
chapters: modifiedSelfItem.chapters ?? [],
|
||||||
|
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
||||||
|
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
||||||
|
subtitlesEnabled: subtitlesEnabled,
|
||||||
|
autoplayEnabled: autoplayEnabled,
|
||||||
|
overlayType: overlayType,
|
||||||
|
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
||||||
|
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
||||||
|
shouldShowAutoPlay: shouldShowAutoPlay,
|
||||||
|
container: currentMediaSource.container ?? "",
|
||||||
|
filename: fileName,
|
||||||
|
versionName: currentMediaSource.name
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModels.append(videoPlayerViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtitlesEnabled = defaultSubtitleStream != nil
|
return viewModels
|
||||||
|
|
||||||
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
|
||||||
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
|
||||||
|
|
||||||
let overlayType = Defaults[.overlayType]
|
|
||||||
|
|
||||||
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
|
||||||
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
|
||||||
|
|
||||||
var fileName: String?
|
|
||||||
if let lastInPath = currentMediaSource.path?.split(separator: "/").last {
|
|
||||||
fileName = String(lastInPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
let videoPlayerViewModel = VideoPlayerViewModel(
|
|
||||||
item: modifiedSelfItem,
|
|
||||||
title: modifiedSelfItem.name ?? "",
|
|
||||||
subtitle: subtitle,
|
|
||||||
directStreamURL: directStreamURL,
|
|
||||||
transcodedStreamURL: transcodedStreamURL?.url,
|
|
||||||
hlsStreamURL: hlsStreamURL,
|
|
||||||
streamType: streamType,
|
|
||||||
response: response,
|
|
||||||
audioStreams: audioStreams,
|
|
||||||
subtitleStreams: subtitleStreams,
|
|
||||||
chapters: modifiedSelfItem.chapters ?? [],
|
|
||||||
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
|
||||||
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
|
||||||
subtitlesEnabled: subtitlesEnabled,
|
|
||||||
autoplayEnabled: autoplayEnabled,
|
|
||||||
overlayType: overlayType,
|
|
||||||
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
|
||||||
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
|
||||||
shouldShowAutoPlay: shouldShowAutoPlay,
|
|
||||||
container: currentMediaSource.container ?? "",
|
|
||||||
filename: fileName,
|
|
||||||
versionName: currentMediaSource.name
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModels.append(videoPlayerViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
return viewModels
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -270,6 +270,14 @@ internal enum L10n {
|
||||||
internal static var quickConnectCode: String { return L10n.tr("Localizable", "quickConnectCode") }
|
internal static var quickConnectCode: String { return L10n.tr("Localizable", "quickConnectCode") }
|
||||||
/// Invalid Quick Connect code
|
/// Invalid Quick Connect code
|
||||||
internal static var quickConnectInvalidError: String { return L10n.tr("Localizable", "quickConnectInvalidError") }
|
internal static var quickConnectInvalidError: String { return L10n.tr("Localizable", "quickConnectInvalidError") }
|
||||||
|
/// Note: Quick Connect not enabled
|
||||||
|
internal static var quickConnectNotEnabled: String { return L10n.tr("Localizable", "quickConnectNotEnabled") }
|
||||||
|
/// 1. Open the Jellyfin app on your phone or web browser and sign in with your account
|
||||||
|
internal static var quickConnectStep1: String { return L10n.tr("Localizable", "quickConnectStep1") }
|
||||||
|
/// 2. Open the user menu and go to the Quick Connect page
|
||||||
|
internal static var quickConnectStep2: String { return L10n.tr("Localizable", "quickConnectStep2") }
|
||||||
|
/// 3. Enter the following code:
|
||||||
|
internal static var quickConnectStep3: String { return L10n.tr("Localizable", "quickConnectStep3") }
|
||||||
/// Authorizing Quick Connect successful. Please continue on your other device.
|
/// Authorizing Quick Connect successful. Please continue on your other device.
|
||||||
internal static var quickConnectSuccessMessage: String { return L10n.tr("Localizable", "quickConnectSuccessMessage") }
|
internal static var quickConnectSuccessMessage: String { return L10n.tr("Localizable", "quickConnectSuccessMessage") }
|
||||||
/// Rated
|
/// Rated
|
||||||
|
|
|
@ -22,7 +22,7 @@ final class SessionManager {
|
||||||
|
|
||||||
// MARK: currentLogin
|
// MARK: currentLogin
|
||||||
|
|
||||||
private(set) var currentLogin: CurrentLogin!
|
fileprivate(set) var currentLogin: CurrentLogin!
|
||||||
|
|
||||||
// MARK: main
|
// MARK: main
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@ final class SessionManager {
|
||||||
// MARK: init
|
// MARK: init
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
|
setAuthHeader(with: "")
|
||||||
|
|
||||||
if let lastUserID = Defaults[.lastServerUserID],
|
if let lastUserID = Defaults[.lastServerUserID],
|
||||||
let user = try? SwiftfinStore.dataStack.fetchOne(
|
let user = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
From<SwiftfinStore.Models.StoredUser>(),
|
From<SwiftfinStore.Models.StoredUser>(),
|
||||||
|
@ -195,86 +197,31 @@ final class SessionManager {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: loginUser publisher
|
// MARK: signInUser publisher
|
||||||
|
|
||||||
// Logs in a user with an associated server, storing if successful
|
// Logs in a user with an associated server, storing if successful
|
||||||
func loginUser(
|
func signInUser(
|
||||||
server: SwiftfinStore.State.Server,
|
server: SwiftfinStore.State.Server,
|
||||||
username: String,
|
username: String,
|
||||||
password: String
|
password: String
|
||||||
) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
||||||
setAuthHeader(with: "")
|
|
||||||
|
|
||||||
JellyfinAPIAPI.basePath = server.currentURI
|
JellyfinAPIAPI.basePath = server.currentURI
|
||||||
|
|
||||||
return UserAPI.authenticateUserByName(authenticateUserByNameRequest: .init(username: username, pw: password))
|
return UserAPI.authenticateUserByName(authenticateUserByNameRequest: .init(username: username, pw: password))
|
||||||
.tryMap { response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in
|
.processAuthenticationRequest(with: self, server: server)
|
||||||
|
|
||||||
guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") }
|
|
||||||
|
|
||||||
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
|
||||||
let newUser = transaction.create(Into<SwiftfinStore.Models.StoredUser>())
|
|
||||||
|
|
||||||
guard let username = response.user?.name,
|
|
||||||
let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") }
|
|
||||||
|
|
||||||
newUser.username = username
|
|
||||||
newUser.id = id
|
|
||||||
newUser.appleTVID = ""
|
|
||||||
|
|
||||||
// Check for existing user on device
|
|
||||||
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(
|
|
||||||
From<SwiftfinStore.Models.StoredUser>(),
|
|
||||||
[Where<SwiftfinStore.Models.StoredUser>(
|
|
||||||
"id == %@",
|
|
||||||
newUser.id
|
|
||||||
)]
|
|
||||||
) {
|
|
||||||
throw SwiftfinStore.Error.existingUser(existingUser.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>())
|
|
||||||
newAccessToken.value = accessToken
|
|
||||||
newUser.accessToken = newAccessToken
|
|
||||||
|
|
||||||
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(
|
|
||||||
From<SwiftfinStore.Models.StoredServer>(),
|
|
||||||
[
|
|
||||||
Where<SwiftfinStore.Models.StoredServer>(
|
|
||||||
"id == %@",
|
|
||||||
server.id
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else { fatalError("No stored server associated with given state server?") }
|
|
||||||
|
|
||||||
guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") }
|
|
||||||
editUserServer.users.insert(newUser)
|
|
||||||
|
|
||||||
return (editUserServer, newUser, transaction)
|
|
||||||
}
|
|
||||||
.handleEvents(receiveOutput: { [unowned self] server, user, transaction in
|
|
||||||
setAuthHeader(with: user.accessToken?.value ?? "")
|
|
||||||
try? transaction.commitAndWait()
|
|
||||||
|
|
||||||
// Fetch for the right queue
|
|
||||||
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
|
|
||||||
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
|
|
||||||
|
|
||||||
Defaults[.lastServerUserID] = user.id
|
|
||||||
|
|
||||||
currentLogin = (server: currentServer.state, user: currentUser.state)
|
|
||||||
Notifications[.didSignIn].post()
|
|
||||||
})
|
|
||||||
.map { _, user, _ in
|
|
||||||
user.state
|
|
||||||
}
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: loginUser
|
// Logs in a user with an associated server, storing if successful
|
||||||
|
func signInUser(server: SwiftfinStore.State.Server, quickConnectSecret: String) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
||||||
|
JellyfinAPIAPI.basePath = server.currentURI
|
||||||
|
|
||||||
func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
|
return UserAPI.authenticateWithQuickConnect(authenticateWithQuickConnectRequest: .init(secret: quickConnectSecret))
|
||||||
|
.processAuthenticationRequest(with: self, server: server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: signInUser
|
||||||
|
|
||||||
|
func signInUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
|
||||||
JellyfinAPIAPI.basePath = server.currentURI
|
JellyfinAPIAPI.basePath = server.currentURI
|
||||||
Defaults[.lastServerUserID] = user.id
|
Defaults[.lastServerUserID] = user.id
|
||||||
setAuthHeader(with: user.accessToken)
|
setAuthHeader(with: user.accessToken)
|
||||||
|
@ -347,7 +294,7 @@ final class SessionManager {
|
||||||
try? transaction.commitAndWait()
|
try? transaction.commitAndWait()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setAuthHeader(with accessToken: String) {
|
fileprivate func setAuthHeader(with accessToken: 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)
|
||||||
|
@ -370,3 +317,74 @@ final class SessionManager {
|
||||||
JellyfinAPIAPI.customHeaders["X-Emby-Authorization"] = header
|
JellyfinAPIAPI.customHeaders["X-Emby-Authorization"] = header
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AnyPublisher where Output == AuthenticationResult {
|
||||||
|
func processAuthenticationRequest(
|
||||||
|
with sessionManager: SessionManager,
|
||||||
|
server: SwiftfinStore.State.Server
|
||||||
|
) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
||||||
|
self
|
||||||
|
.tryMap { response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in
|
||||||
|
|
||||||
|
guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") }
|
||||||
|
|
||||||
|
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
||||||
|
let newUser = transaction.create(Into<SwiftfinStore.Models.StoredUser>())
|
||||||
|
|
||||||
|
guard let username = response.user?.name,
|
||||||
|
let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") }
|
||||||
|
|
||||||
|
newUser.username = username
|
||||||
|
newUser.id = id
|
||||||
|
newUser.appleTVID = ""
|
||||||
|
|
||||||
|
// Check for existing user on device
|
||||||
|
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
|
From<SwiftfinStore.Models.StoredUser>(),
|
||||||
|
[Where<SwiftfinStore.Models.StoredUser>(
|
||||||
|
"id == %@",
|
||||||
|
newUser.id
|
||||||
|
)]
|
||||||
|
) {
|
||||||
|
throw SwiftfinStore.Error.existingUser(existingUser.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>())
|
||||||
|
newAccessToken.value = accessToken
|
||||||
|
newUser.accessToken = newAccessToken
|
||||||
|
|
||||||
|
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
|
From<SwiftfinStore.Models.StoredServer>(),
|
||||||
|
[
|
||||||
|
Where<SwiftfinStore.Models.StoredServer>(
|
||||||
|
"id == %@",
|
||||||
|
server.id
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else { fatalError("No stored server associated with given state server?") }
|
||||||
|
|
||||||
|
guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") }
|
||||||
|
editUserServer.users.insert(newUser)
|
||||||
|
|
||||||
|
return (editUserServer, newUser, transaction)
|
||||||
|
}
|
||||||
|
.handleEvents(receiveOutput: { server, user, transaction in
|
||||||
|
sessionManager.setAuthHeader(with: user.accessToken?.value ?? "")
|
||||||
|
try? transaction.commitAndWait()
|
||||||
|
|
||||||
|
// Fetch for the right queue
|
||||||
|
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
|
||||||
|
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
|
||||||
|
|
||||||
|
Defaults[.lastServerUserID] = user.id
|
||||||
|
|
||||||
|
sessionManager.currentLogin = (server: currentServer.state, user: currentUser.state)
|
||||||
|
Notifications[.didSignIn].post()
|
||||||
|
})
|
||||||
|
.map { _, user, _ in
|
||||||
|
user.state
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,9 +34,9 @@ class UserListViewModel: ViewModel {
|
||||||
self.users = SessionManager.main.fetchUsers(for: server)
|
self.users = SessionManager.main.fetchUsers(for: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
func login(user: SwiftfinStore.State.User) {
|
func signIn(user: SwiftfinStore.State.User) {
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
SessionManager.main.loginUser(server: server, user: user)
|
SessionManager.main.signInUser(server: server, user: user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(user: SwiftfinStore.State.User) {
|
func remove(user: SwiftfinStore.State.User) {
|
||||||
|
|
|
@ -15,14 +15,24 @@ final class UserSignInViewModel: ViewModel {
|
||||||
|
|
||||||
@RouterObject
|
@RouterObject
|
||||||
var router: UserSignInCoordinator.Router?
|
var router: UserSignInCoordinator.Router?
|
||||||
let server: SwiftfinStore.State.Server
|
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var publicUsers: [UserDto] = []
|
var publicUsers: [UserDto] = []
|
||||||
|
@Published
|
||||||
|
var quickConnectCode: String?
|
||||||
|
@Published
|
||||||
|
var quickConnectEnabled = false
|
||||||
|
|
||||||
|
let server: SwiftfinStore.State.Server
|
||||||
|
private var quickConnectTimer: Timer?
|
||||||
|
private var quickConnectSecret: String?
|
||||||
|
|
||||||
init(server: SwiftfinStore.State.Server) {
|
init(server: SwiftfinStore.State.Server) {
|
||||||
self.server = server
|
self.server = server
|
||||||
|
super.init()
|
||||||
|
|
||||||
JellyfinAPIAPI.basePath = server.currentURI
|
JellyfinAPIAPI.basePath = server.currentURI
|
||||||
|
checkQuickConnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
var alertTitle: String {
|
var alertTitle: String {
|
||||||
|
@ -34,10 +44,10 @@ final class UserSignInViewModel: ViewModel {
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
func login(username: String, password: String) {
|
func signIn(username: String, password: String) {
|
||||||
LogManager.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login")
|
LogManager.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login")
|
||||||
|
|
||||||
SessionManager.main.loginUser(server: server, username: username, password: password)
|
SessionManager.main.signInUser(server: server, username: username, password: password)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
|
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
|
||||||
|
@ -79,4 +89,65 @@ final class UserSignInViewModel: ViewModel {
|
||||||
let urlString = ImageAPI.getSplashscreenWithRequestBuilder().URLString
|
let urlString = ImageAPI.getSplashscreenWithRequestBuilder().URLString
|
||||||
return URL(string: urlString)
|
return URL(string: urlString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkQuickConnect() {
|
||||||
|
QuickConnectAPI.getEnabled()
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { enabled in
|
||||||
|
self.quickConnectEnabled = enabled
|
||||||
|
if enabled {
|
||||||
|
self.startQuickConnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startQuickConnect() {
|
||||||
|
QuickConnectAPI.initiate()
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { response in
|
||||||
|
|
||||||
|
self.quickConnectSecret = response.secret
|
||||||
|
self.quickConnectCode = response.code
|
||||||
|
LogManager.log.debug("QuickConnect code: \(response.code ?? "--")")
|
||||||
|
|
||||||
|
self.quickConnectTimer = Timer.scheduledTimer(
|
||||||
|
timeInterval: 5,
|
||||||
|
target: self,
|
||||||
|
selector: #selector(self.checkAuthStatus),
|
||||||
|
userInfo: nil,
|
||||||
|
repeats: true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func checkAuthStatus() {
|
||||||
|
guard let quickConnectSecret = quickConnectSecret else { return }
|
||||||
|
|
||||||
|
QuickConnectAPI.connect(secret: quickConnectSecret)
|
||||||
|
.sink(receiveCompletion: { _ in
|
||||||
|
// Prefer not to handle error handling like normal as
|
||||||
|
// this is a repeated call
|
||||||
|
}, receiveValue: { value in
|
||||||
|
guard let authenticated = value.authenticated, authenticated else {
|
||||||
|
LogManager.log.debug("QuickConnect not authenticated yet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.quickConnectTimer?.invalidate()
|
||||||
|
|
||||||
|
SessionManager.main.signInUser(server: self.server, quickConnectSecret: quickConnectSecret)
|
||||||
|
.trackActivity(self.loading)
|
||||||
|
.sink { completion in
|
||||||
|
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
|
||||||
|
} receiveValue: { _ in
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ struct UserListView: View {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(viewModel.users, id: \.id) { user in
|
ForEach(viewModel.users, id: \.id) { user in
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(user: user)
|
viewModel.signIn(user: user)
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text(user.username)
|
Text(user.username)
|
||||||
|
|
|
@ -27,41 +27,83 @@ struct UserSignInView: View {
|
||||||
.opacity(0.9)
|
.opacity(0.9)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
Form {
|
HStack(alignment: .top) {
|
||||||
Section {
|
VStack(alignment: .leading) {
|
||||||
TextField(L10n.username, text: $username)
|
Section {
|
||||||
.disableAutocorrection(true)
|
TextField(L10n.username, text: $username)
|
||||||
.autocapitalization(.none)
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
|
||||||
SecureField(L10n.password, text: $password)
|
SecureField(L10n.password, text: $password)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(username: username, password: password)
|
viewModel.signIn(username: username, password: password)
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
L10n.connect.text
|
L10n.connect.text
|
||||||
Spacer()
|
|
||||||
if viewModel.isLoading {
|
Spacer()
|
||||||
ProgressView()
|
|
||||||
|
if viewModel.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.disabled(viewModel.isLoading || username.isEmpty)
|
||||||
.disabled(viewModel.isLoading || username.isEmpty)
|
|
||||||
|
|
||||||
} header: {
|
} header: {
|
||||||
L10n.signInToServer(viewModel.server.name).text
|
L10n.signInToServer(viewModel.server.name).text
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if !viewModel.quickConnectEnabled {
|
||||||
|
L10n.quickConnectNotEnabled.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.alert(item: $viewModel.errorMessage) { _ in
|
||||||
|
Alert(
|
||||||
|
title: Text(viewModel.alertTitle),
|
||||||
|
message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
|
||||||
|
dismissButton: .cancel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.navigationTitle(L10n.signIn)
|
||||||
|
|
||||||
|
if viewModel.quickConnectEnabled {
|
||||||
|
VStack(alignment: .center) {
|
||||||
|
L10n.quickConnect.text
|
||||||
|
.font(.title3)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
L10n.quickConnectStep1.text
|
||||||
|
|
||||||
|
L10n.quickConnectStep2.text
|
||||||
|
|
||||||
|
L10n.quickConnectStep3.text
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
|
Text(viewModel.quickConnectCode ?? "------")
|
||||||
|
.tracking(10)
|
||||||
|
.font(.title)
|
||||||
|
.monospacedDigit()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(item: $viewModel.errorMessage) { _ in
|
|
||||||
Alert(
|
|
||||||
title: Text(viewModel.alertTitle),
|
|
||||||
message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
|
|
||||||
dismissButton: .cancel()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.navigationTitle(L10n.signIn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct UserSignInView_Preivews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
UserSignInView(viewModel: .init(server: .sample))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ struct UserLoginCellView: View {
|
||||||
DisclosureGroup {
|
DisclosureGroup {
|
||||||
SecureField(L10n.password, text: $enteredPassword)
|
SecureField(L10n.password, text: $enteredPassword)
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(username: user.name ?? "--", password: enteredPassword)
|
viewModel.signIn(username: user.name ?? "--", password: enteredPassword)
|
||||||
} label: {
|
} label: {
|
||||||
L10n.signIn.text
|
L10n.signIn.text
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ struct UserListView: View {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(viewModel.users, id: \.id) { user in
|
ForEach(viewModel.users, id: \.id) { user in
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(user: user)
|
viewModel.signIn(user: user)
|
||||||
} label: {
|
} label: {
|
||||||
ZStack(alignment: Alignment.leading) {
|
ZStack(alignment: Alignment.leading) {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
|
|
|
@ -37,7 +37,7 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(username: username, password: password)
|
viewModel.signIn(username: username, password: password)
|
||||||
} label: {
|
} label: {
|
||||||
L10n.signIn.text
|
L10n.signIn.text
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue