No Tab Characters and Before First for Argument and Parameter Wrapping (#482)
This commit is contained in:
parent
88f350b71e
commit
cfb3aa1faa
|
@ -7,7 +7,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: "Lint 🧹"
|
name: "Lint 🧹"
|
||||||
runs-on: macos-latest
|
runs-on: macos-12
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
# version: 0.47.5
|
# version: 0.49.11
|
||||||
|
|
||||||
--swiftversion 5.5
|
--swiftversion 5.5
|
||||||
|
|
||||||
--indent tab
|
|
||||||
--tabwidth 4
|
--tabwidth 4
|
||||||
--xcodeindentation enabled
|
--xcodeindentation enabled
|
||||||
--semicolons never
|
--semicolons never
|
||||||
--stripunusedargs closure-only
|
--stripunusedargs closure-only
|
||||||
--maxwidth 140
|
--maxwidth 140
|
||||||
--assetliterals visual-width
|
--assetliterals visual-width
|
||||||
--wraparguments after-first
|
--wraparguments before-first
|
||||||
--wrapparameters after-first
|
--wrapparameters before-first
|
||||||
--wrapcollections before-first
|
--wrapcollections before-first
|
||||||
--wrapconditions after-first
|
--wrapconditions after-first
|
||||||
--funcattributes prev-line
|
--funcattributes prev-line
|
||||||
|
@ -44,7 +43,6 @@
|
||||||
redundantClosure, \
|
redundantClosure, \
|
||||||
redundantType
|
redundantType
|
||||||
|
|
||||||
--exclude Pods
|
|
||||||
--exclude Shared/Generated/Strings.swift
|
--exclude Shared/Generated/Strings.swift
|
||||||
|
|
||||||
--header "\nSwiftfin is subject to the terms of the Mozilla Public\nLicense, v2.0. If a copy of the MPL was not distributed with this\nfile, you can obtain one at https://mozilla.org/MPL/2.0/.\n\nCopyright (c) {year} Jellyfin & Jellyfin Contributors\n"
|
--header "\nSwiftfin is subject to the terms of the Mozilla Public\nLicense, v2.0. If a copy of the MPL was not distributed with this\nfile, you can obtain one at https://mozilla.org/MPL/2.0/.\n\nCopyright (c) {year} Jellyfin & Jellyfin Contributors\n"
|
||||||
|
|
|
@ -46,9 +46,11 @@ final class LibraryCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> {
|
func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> {
|
||||||
NavigationViewCoordinator(FilterCoordinator(filters: params.filters,
|
NavigationViewCoordinator(FilterCoordinator(
|
||||||
|
filters: params.filters,
|
||||||
enabledFilterType: params.enabledFilterType,
|
enabledFilterType: params.enabledFilterType,
|
||||||
parentId: params.parentId))
|
parentId: params.parentId
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||||
|
|
|
@ -41,21 +41,29 @@ enum NetworkError: Error {
|
||||||
// https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes
|
// https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes
|
||||||
switch err._code {
|
switch err._code {
|
||||||
case -1001:
|
case -1001:
|
||||||
errorMessage = ErrorMessage(code: err._code,
|
errorMessage = ErrorMessage(
|
||||||
|
code: err._code,
|
||||||
title: L10n.error,
|
title: L10n.error,
|
||||||
message: L10n.networkTimedOut)
|
message: L10n.networkTimedOut
|
||||||
|
)
|
||||||
case -1003:
|
case -1003:
|
||||||
errorMessage = ErrorMessage(code: err._code,
|
errorMessage = ErrorMessage(
|
||||||
|
code: err._code,
|
||||||
title: L10n.error,
|
title: L10n.error,
|
||||||
message: L10n.unableToFindHost)
|
message: L10n.unableToFindHost
|
||||||
|
)
|
||||||
case -1004:
|
case -1004:
|
||||||
errorMessage = ErrorMessage(code: err._code,
|
errorMessage = ErrorMessage(
|
||||||
|
code: err._code,
|
||||||
title: L10n.error,
|
title: L10n.error,
|
||||||
message: L10n.cannotConnectToHost)
|
message: L10n.cannotConnectToHost
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
errorMessage = ErrorMessage(code: err._code,
|
errorMessage = ErrorMessage(
|
||||||
|
code: err._code,
|
||||||
title: L10n.error,
|
title: L10n.error,
|
||||||
message: L10n.unknownError)
|
message: L10n.unknownError
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,9 +76,11 @@ enum NetworkError: Error {
|
||||||
// Not implemented as has not run into one of these errors as time of writing
|
// Not implemented as has not run into one of these errors as time of writing
|
||||||
switch response {
|
switch response {
|
||||||
case .error:
|
case .error:
|
||||||
errorMessage = ErrorMessage(code: 0,
|
errorMessage = ErrorMessage(
|
||||||
|
code: 0,
|
||||||
title: L10n.error,
|
title: L10n.error,
|
||||||
message: "An HTTP URL error has occurred")
|
message: "An HTTP URL error has occurred"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorMessage
|
return errorMessage
|
||||||
|
@ -85,13 +95,17 @@ enum NetworkError: Error {
|
||||||
// Generic HTTP status codes
|
// Generic HTTP status codes
|
||||||
switch code {
|
switch code {
|
||||||
case 401:
|
case 401:
|
||||||
errorMessage = ErrorMessage(code: code,
|
errorMessage = ErrorMessage(
|
||||||
|
code: code,
|
||||||
title: L10n.unauthorized,
|
title: L10n.unauthorized,
|
||||||
message: L10n.unauthorizedUser)
|
message: L10n.unauthorizedUser
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
errorMessage = ErrorMessage(code: code,
|
errorMessage = ErrorMessage(
|
||||||
|
code: code,
|
||||||
title: L10n.error,
|
title: L10n.error,
|
||||||
message: displayMessage ?? L10n.unknownError)
|
message: displayMessage ?? L10n.unknownError
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,9 +69,19 @@ public extension UIImage {
|
||||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
|
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
|
||||||
|
|
||||||
guard let provider = CGDataProvider(data: data) else { return nil }
|
guard let provider = CGDataProvider(data: data) else { return nil }
|
||||||
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
|
guard let cgImage = CGImage(
|
||||||
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil,
|
width: width,
|
||||||
shouldInterpolate: true, intent: .defaultIntent) else { return nil }
|
height: height,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bitsPerPixel: 24,
|
||||||
|
bytesPerRow: bytesPerRow,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
bitmapInfo: bitmapInfo,
|
||||||
|
provider: provider,
|
||||||
|
decode: nil,
|
||||||
|
shouldInterpolate: true,
|
||||||
|
intent: .defaultIntent
|
||||||
|
) else { return nil }
|
||||||
|
|
||||||
self.init(cgImage: cgImage)
|
self.init(cgImage: cgImage)
|
||||||
}
|
}
|
||||||
|
@ -89,9 +99,11 @@ private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float
|
||||||
let quantG = (value / 19) % 19
|
let quantG = (value / 19) % 19
|
||||||
let quantB = value % 19
|
let quantB = value % 19
|
||||||
|
|
||||||
let rgb = (signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
|
let rgb = (
|
||||||
|
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
|
||||||
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
|
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
|
||||||
signPow((Float(quantB) - 9) / 9, 2) * maximumValue)
|
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
|
||||||
|
)
|
||||||
|
|
||||||
return rgb
|
return rgb
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,18 +22,22 @@ extension BaseItemDto {
|
||||||
builder.setMaxBitrate(bitrate: tempOverkillBitrate)
|
builder.setMaxBitrate(bitrate: tempOverkillBitrate)
|
||||||
let profile = builder.buildProfile()
|
let profile = builder.buildProfile()
|
||||||
|
|
||||||
let getPostedPlaybackInfoRequest = GetPostedPlaybackInfoRequest(userId: SessionManager.main.currentLogin.user.id,
|
let getPostedPlaybackInfoRequest = GetPostedPlaybackInfoRequest(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
maxStreamingBitrate: tempOverkillBitrate,
|
maxStreamingBitrate: tempOverkillBitrate,
|
||||||
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
|
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
|
||||||
deviceProfile: profile,
|
deviceProfile: profile,
|
||||||
autoOpenLiveStream: true)
|
autoOpenLiveStream: true
|
||||||
|
)
|
||||||
|
|
||||||
return MediaInfoAPI.getPostedPlaybackInfo(itemId: self.id!,
|
return MediaInfoAPI.getPostedPlaybackInfo(
|
||||||
|
itemId: self.id!,
|
||||||
userId: SessionManager.main.currentLogin.user.id,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
maxStreamingBitrate: tempOverkillBitrate,
|
maxStreamingBitrate: tempOverkillBitrate,
|
||||||
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
|
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
|
||||||
autoOpenLiveStream: true,
|
autoOpenLiveStream: true,
|
||||||
getPostedPlaybackInfoRequest: getPostedPlaybackInfoRequest)
|
getPostedPlaybackInfoRequest: getPostedPlaybackInfoRequest
|
||||||
|
)
|
||||||
.map { response -> [VideoPlayerViewModel] in
|
.map { response -> [VideoPlayerViewModel] in
|
||||||
let mediaSources = response.mediaSources!
|
let mediaSources = response.mediaSources!
|
||||||
|
|
||||||
|
@ -63,24 +67,29 @@ extension BaseItemDto {
|
||||||
mediaSourceID = self.id!
|
mediaSourceID = self.id!
|
||||||
}
|
}
|
||||||
|
|
||||||
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(itemId: self.id!,
|
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(
|
||||||
|
itemId: self.id!,
|
||||||
_static: true,
|
_static: true,
|
||||||
tag: self.etag,
|
tag: self.etag,
|
||||||
playSessionId: response.playSessionId,
|
playSessionId: response.playSessionId,
|
||||||
minSegments: 6,
|
minSegments: 6,
|
||||||
mediaSourceId: mediaSourceID)
|
mediaSourceId: mediaSourceID
|
||||||
|
)
|
||||||
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
||||||
|
|
||||||
if let transcodeURL = currentMediaSource.transcodingUrl {
|
if let transcodeURL = currentMediaSource.transcodingUrl {
|
||||||
streamType = .transcode
|
streamType = .transcode
|
||||||
transcodedStreamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI
|
transcodedStreamURL = URLComponents(
|
||||||
.appending(transcodeURL))!
|
string: SessionManager.main.currentLogin.server.currentURI
|
||||||
|
.appending(transcodeURL)
|
||||||
|
)!
|
||||||
} else {
|
} else {
|
||||||
streamType = .direct
|
streamType = .direct
|
||||||
transcodedStreamURL = nil
|
transcodedStreamURL = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(itemId: id ?? "",
|
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(
|
||||||
|
itemId: id ?? "",
|
||||||
mediaSourceId: id ?? "",
|
mediaSourceId: id ?? "",
|
||||||
_static: true,
|
_static: true,
|
||||||
tag: currentMediaSource.eTag,
|
tag: currentMediaSource.eTag,
|
||||||
|
@ -98,7 +107,8 @@ extension BaseItemDto {
|
||||||
transcodingMaxAudioChannels: 6,
|
transcodingMaxAudioChannels: 6,
|
||||||
videoCodec: videoStream?.codec,
|
videoCodec: videoStream?.codec,
|
||||||
videoStreamIndex: videoStream?.index,
|
videoStreamIndex: videoStream?.index,
|
||||||
enableAdaptiveBitrateStreaming: true)
|
enableAdaptiveBitrateStreaming: true
|
||||||
|
)
|
||||||
|
|
||||||
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
||||||
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
||||||
|
@ -136,7 +146,8 @@ extension BaseItemDto {
|
||||||
fileName = String(lastInPath)
|
fileName = String(lastInPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
let videoPlayerViewModel = VideoPlayerViewModel(item: modifiedSelfItem,
|
let videoPlayerViewModel = VideoPlayerViewModel(
|
||||||
|
item: modifiedSelfItem,
|
||||||
title: modifiedSelfItem.name ?? "",
|
title: modifiedSelfItem.name ?? "",
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
directStreamURL: directStreamURL,
|
directStreamURL: directStreamURL,
|
||||||
|
@ -157,7 +168,8 @@ extension BaseItemDto {
|
||||||
shouldShowAutoPlay: shouldShowAutoPlay,
|
shouldShowAutoPlay: shouldShowAutoPlay,
|
||||||
container: currentMediaSource.container ?? "",
|
container: currentMediaSource.container ?? "",
|
||||||
filename: fileName,
|
filename: fileName,
|
||||||
versionName: currentMediaSource.name)
|
versionName: currentMediaSource.name
|
||||||
|
)
|
||||||
|
|
||||||
viewModels.append(videoPlayerViewModel)
|
viewModels.append(videoPlayerViewModel)
|
||||||
}
|
}
|
||||||
|
@ -177,18 +189,22 @@ extension BaseItemDto {
|
||||||
builder.setMaxBitrate(bitrate: tempOverkillBitrate)
|
builder.setMaxBitrate(bitrate: tempOverkillBitrate)
|
||||||
let profile = builder.buildProfile()
|
let profile = builder.buildProfile()
|
||||||
|
|
||||||
let getPostedPlaybackInfoRequest = GetPostedPlaybackInfoRequest(userId: SessionManager.main.currentLogin.user.id,
|
let getPostedPlaybackInfoRequest = GetPostedPlaybackInfoRequest(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
maxStreamingBitrate: tempOverkillBitrate,
|
maxStreamingBitrate: tempOverkillBitrate,
|
||||||
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
|
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
|
||||||
deviceProfile: profile,
|
deviceProfile: profile,
|
||||||
autoOpenLiveStream: true)
|
autoOpenLiveStream: true
|
||||||
|
)
|
||||||
|
|
||||||
return MediaInfoAPI.getPostedPlaybackInfo(itemId: self.id!,
|
return MediaInfoAPI.getPostedPlaybackInfo(
|
||||||
|
itemId: self.id!,
|
||||||
userId: SessionManager.main.currentLogin.user.id,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
maxStreamingBitrate: tempOverkillBitrate,
|
maxStreamingBitrate: tempOverkillBitrate,
|
||||||
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
|
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
|
||||||
autoOpenLiveStream: true,
|
autoOpenLiveStream: true,
|
||||||
getPostedPlaybackInfoRequest: getPostedPlaybackInfoRequest)
|
getPostedPlaybackInfoRequest: getPostedPlaybackInfoRequest
|
||||||
|
)
|
||||||
.map { response -> [VideoPlayerViewModel] in
|
.map { response -> [VideoPlayerViewModel] in
|
||||||
let mediaSources = response.mediaSources!
|
let mediaSources = response.mediaSources!
|
||||||
|
|
||||||
|
@ -218,24 +234,29 @@ extension BaseItemDto {
|
||||||
mediaSourceID = self.id!
|
mediaSourceID = self.id!
|
||||||
}
|
}
|
||||||
|
|
||||||
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(itemId: self.id!,
|
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(
|
||||||
|
itemId: self.id!,
|
||||||
_static: true,
|
_static: true,
|
||||||
tag: self.etag,
|
tag: self.etag,
|
||||||
playSessionId: response.playSessionId,
|
playSessionId: response.playSessionId,
|
||||||
minSegments: 6,
|
minSegments: 6,
|
||||||
mediaSourceId: mediaSourceID)
|
mediaSourceId: mediaSourceID
|
||||||
|
)
|
||||||
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
||||||
|
|
||||||
if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.liveTVForceDirectPlay] {
|
if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.liveTVForceDirectPlay] {
|
||||||
streamType = .transcode
|
streamType = .transcode
|
||||||
transcodedStreamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI
|
transcodedStreamURL = URLComponents(
|
||||||
.appending(transcodeURL))!
|
string: SessionManager.main.currentLogin.server.currentURI
|
||||||
|
.appending(transcodeURL)
|
||||||
|
)!
|
||||||
} else {
|
} else {
|
||||||
streamType = .direct
|
streamType = .direct
|
||||||
transcodedStreamURL = nil
|
transcodedStreamURL = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(itemId: id ?? "",
|
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(
|
||||||
|
itemId: id ?? "",
|
||||||
mediaSourceId: id ?? "",
|
mediaSourceId: id ?? "",
|
||||||
_static: true,
|
_static: true,
|
||||||
tag: currentMediaSource.eTag,
|
tag: currentMediaSource.eTag,
|
||||||
|
@ -253,7 +274,8 @@ extension BaseItemDto {
|
||||||
transcodingMaxAudioChannels: 6,
|
transcodingMaxAudioChannels: 6,
|
||||||
videoCodec: videoStream?.codec,
|
videoCodec: videoStream?.codec,
|
||||||
videoStreamIndex: videoStream?.index,
|
videoStreamIndex: videoStream?.index,
|
||||||
enableAdaptiveBitrateStreaming: true)
|
enableAdaptiveBitrateStreaming: true
|
||||||
|
)
|
||||||
|
|
||||||
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
||||||
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
||||||
|
@ -291,7 +313,8 @@ extension BaseItemDto {
|
||||||
fileName = String(lastInPath)
|
fileName = String(lastInPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
let videoPlayerViewModel = VideoPlayerViewModel(item: modifiedSelfItem,
|
let videoPlayerViewModel = VideoPlayerViewModel(
|
||||||
|
item: modifiedSelfItem,
|
||||||
title: modifiedSelfItem.name ?? "",
|
title: modifiedSelfItem.name ?? "",
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
directStreamURL: directStreamURL,
|
directStreamURL: directStreamURL,
|
||||||
|
@ -312,7 +335,8 @@ extension BaseItemDto {
|
||||||
shouldShowAutoPlay: shouldShowAutoPlay,
|
shouldShowAutoPlay: shouldShowAutoPlay,
|
||||||
container: currentMediaSource.container ?? "",
|
container: currentMediaSource.container ?? "",
|
||||||
filename: fileName,
|
filename: fileName,
|
||||||
versionName: currentMediaSource.name)
|
versionName: currentMediaSource.name
|
||||||
|
)
|
||||||
|
|
||||||
viewModels.append(videoPlayerViewModel)
|
viewModels.append(videoPlayerViewModel)
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,21 +88,25 @@ public extension BaseItemDto {
|
||||||
|
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
|
|
||||||
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId,
|
let urlString = ImageAPI.getItemImageWithRequestBuilder(
|
||||||
|
itemId: imageItemId,
|
||||||
imageType: imageType,
|
imageType: imageType,
|
||||||
maxWidth: Int(x),
|
maxWidth: Int(x),
|
||||||
quality: 96,
|
quality: 96,
|
||||||
tag: imageTag).URLString
|
tag: imageTag
|
||||||
|
).URLString
|
||||||
return URL(string: urlString)!
|
return URL(string: urlString)!
|
||||||
}
|
}
|
||||||
|
|
||||||
func getThumbImage(maxWidth: Int) -> URL {
|
func getThumbImage(maxWidth: Int) -> URL {
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
|
|
||||||
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: id ?? "",
|
let urlString = ImageAPI.getItemImageWithRequestBuilder(
|
||||||
|
itemId: id ?? "",
|
||||||
imageType: .thumb,
|
imageType: .thumb,
|
||||||
maxWidth: Int(x),
|
maxWidth: Int(x),
|
||||||
quality: 96).URLString
|
quality: 96
|
||||||
|
).URLString
|
||||||
return URL(string: urlString)!
|
return URL(string: urlString)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,11 +119,13 @@ public extension BaseItemDto {
|
||||||
|
|
||||||
func getSeriesBackdropImage(maxWidth: Int) -> URL {
|
func getSeriesBackdropImage(maxWidth: Int) -> URL {
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: parentBackdropItemId ?? "",
|
let urlString = ImageAPI.getItemImageWithRequestBuilder(
|
||||||
|
itemId: parentBackdropItemId ?? "",
|
||||||
imageType: .backdrop,
|
imageType: .backdrop,
|
||||||
maxWidth: Int(x),
|
maxWidth: Int(x),
|
||||||
quality: 96,
|
quality: 96,
|
||||||
tag: parentBackdropImageTags?.first).URLString
|
tag: parentBackdropImageTags?.first
|
||||||
|
).URLString
|
||||||
return URL(string: urlString)!
|
return URL(string: urlString)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,21 +135,25 @@ public extension BaseItemDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: seriesId,
|
let urlString = ImageAPI.getItemImageWithRequestBuilder(
|
||||||
|
itemId: seriesId,
|
||||||
imageType: .primary,
|
imageType: .primary,
|
||||||
maxWidth: Int(x),
|
maxWidth: Int(x),
|
||||||
quality: 96,
|
quality: 96,
|
||||||
tag: seriesPrimaryImageTag).URLString
|
tag: seriesPrimaryImageTag
|
||||||
|
).URLString
|
||||||
return URL(string: urlString)!
|
return URL(string: urlString)!
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSeriesThumbImage(maxWidth: Int) -> URL {
|
func getSeriesThumbImage(maxWidth: Int) -> URL {
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: seriesId ?? "",
|
let urlString = ImageAPI.getItemImageWithRequestBuilder(
|
||||||
|
itemId: seriesId ?? "",
|
||||||
imageType: .thumb,
|
imageType: .thumb,
|
||||||
maxWidth: Int(x),
|
maxWidth: Int(x),
|
||||||
quality: 96,
|
quality: 96,
|
||||||
tag: seriesPrimaryImageTag).URLString
|
tag: seriesPrimaryImageTag
|
||||||
|
).URLString
|
||||||
return URL(string: urlString)!
|
return URL(string: urlString)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,11 +169,13 @@ public extension BaseItemDto {
|
||||||
|
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
|
|
||||||
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId,
|
let urlString = ImageAPI.getItemImageWithRequestBuilder(
|
||||||
|
itemId: imageItemId,
|
||||||
imageType: imageType,
|
imageType: imageType,
|
||||||
maxWidth: Int(x),
|
maxWidth: Int(x),
|
||||||
quality: 96,
|
quality: 96,
|
||||||
tag: imageTag).URLString
|
tag: imageTag
|
||||||
|
).URLString
|
||||||
return URL(string: urlString)!
|
return URL(string: urlString)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,10 +378,12 @@ public extension BaseItemDto {
|
||||||
var chapterImageURLs: [URL] = []
|
var chapterImageURLs: [URL] = []
|
||||||
|
|
||||||
for chapterIndex in 0 ..< chapters.count {
|
for chapterIndex in 0 ..< chapters.count {
|
||||||
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: id ?? "",
|
let urlString = ImageAPI.getItemImageWithRequestBuilder(
|
||||||
|
itemId: id ?? "",
|
||||||
imageType: .chapter,
|
imageType: .chapter,
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
imageIndex: chapterIndex).URLString
|
imageIndex: chapterIndex
|
||||||
|
).URLString
|
||||||
chapterImageURLs.append(URL(string: urlString)!)
|
chapterImageURLs.append(URL(string: urlString)!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,13 @@ extension BaseItemPerson {
|
||||||
func getImage(baseURL: String, maxWidth: Int) -> URL {
|
func getImage(baseURL: String, maxWidth: Int) -> URL {
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
|
|
||||||
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: id ?? "",
|
let urlString = ImageAPI.getItemImageWithRequestBuilder(
|
||||||
|
itemId: id ?? "",
|
||||||
imageType: .primary,
|
imageType: .primary,
|
||||||
maxWidth: Int(x),
|
maxWidth: Int(x),
|
||||||
quality: 96,
|
quality: 96,
|
||||||
tag: primaryImageTag).URLString
|
tag: primaryImageTag
|
||||||
|
).URLString
|
||||||
return URL(string: urlString)!
|
return URL(string: urlString)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,9 @@ extension VLCMediaPlayer {
|
||||||
///
|
///
|
||||||
/// This is pretty hacky until VLCKit 4 has a public API to support this
|
/// This is pretty hacky until VLCKit 4 has a public API to support this
|
||||||
func setSubtitleSize(_ size: SubtitleSize) {
|
func setSubtitleSize(_ size: SubtitleSize) {
|
||||||
perform(Selector(("setTextRendererFontSize:")),
|
perform(
|
||||||
with: size.textRendererFontSize)
|
Selector(("setTextRendererFontSize:")),
|
||||||
|
with: size.textRendererFontSize
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,28 +55,40 @@ class DeviceProfileBuilder {
|
||||||
// Device supports Dolby Digital (AC3, EAC3)
|
// Device supports Dolby Digital (AC3, EAC3)
|
||||||
if supportsFeature(minimumSupported: .A8X) {
|
if supportsFeature(minimumSupported: .A8X) {
|
||||||
if supportsFeature(minimumSupported: .A9) {
|
if supportsFeature(minimumSupported: .A9) {
|
||||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus",
|
directPlayProfiles = [DirectPlayProfile(
|
||||||
|
container: "mov,mp4,mkv,webm",
|
||||||
|
audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus",
|
||||||
videoCodec: "hevc,h264,hev1,mpeg4,vp9",
|
videoCodec: "hevc,h264,hev1,mpeg4,vp9",
|
||||||
type: .video)] // HEVC/H.264 with Dolby Digital
|
type: .video
|
||||||
|
)] // HEVC/H.264 with Dolby Digital
|
||||||
} else {
|
} else {
|
||||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "ac3,eac3,aac,mp3,wav,opus",
|
directPlayProfiles = [DirectPlayProfile(
|
||||||
videoCodec: "h264,mpeg4,vp9", type: .video)] // H.264 with Dolby Digital
|
container: "mov,mp4,mkv,webm",
|
||||||
|
audioCodec: "ac3,eac3,aac,mp3,wav,opus",
|
||||||
|
videoCodec: "h264,mpeg4,vp9",
|
||||||
|
type: .video
|
||||||
|
)] // H.264 with Dolby Digital
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device supports Dolby Vision?
|
// Device supports Dolby Vision?
|
||||||
if supportsFeature(minimumSupported: .A10X) {
|
if supportsFeature(minimumSupported: .A10X) {
|
||||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus",
|
directPlayProfiles = [DirectPlayProfile(
|
||||||
|
container: "mov,mp4,mkv,webm",
|
||||||
|
audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus",
|
||||||
videoCodec: "dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9",
|
videoCodec: "dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9",
|
||||||
type: .video)] // H.264/HEVC with Dolby Digital - No Atmos - Vision
|
type: .video
|
||||||
|
)] // H.264/HEVC with Dolby Digital - No Atmos - Vision
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device supports Dolby Atmos?
|
// Device supports Dolby Atmos?
|
||||||
if supportsFeature(minimumSupported: .A12) {
|
if supportsFeature(minimumSupported: .A12) {
|
||||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm",
|
directPlayProfiles = [DirectPlayProfile(
|
||||||
|
container: "mov,mp4,mkv,webm",
|
||||||
audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus",
|
audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus",
|
||||||
videoCodec: "h264,hevc,dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9",
|
videoCodec: "h264,hevc,dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9",
|
||||||
type: .video)] // H.264/HEVC with Dolby Digital & Atmos - Vision
|
type: .video
|
||||||
|
)] // H.264/HEVC with Dolby Digital & Atmos - Vision
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build transcoding profiles
|
// Build transcoding profiles
|
||||||
|
@ -86,32 +98,57 @@ class DeviceProfileBuilder {
|
||||||
// Device supports Dolby Digital (AC3, EAC3)
|
// Device supports Dolby Digital (AC3, EAC3)
|
||||||
if supportsFeature(minimumSupported: .A8X) {
|
if supportsFeature(minimumSupported: .A8X) {
|
||||||
if supportsFeature(minimumSupported: .A9) {
|
if supportsFeature(minimumSupported: .A9) {
|
||||||
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,hevc,mpeg4",
|
transcodingProfiles = [TranscodingProfile(
|
||||||
audioCodec: "aac,mp3,wav,eac3,ac3,flac,opus", _protocol: "hls",
|
container: "ts",
|
||||||
context: .streaming, maxAudioChannels: "6", minSegments: 2,
|
type: .video,
|
||||||
breakOnNonKeyFrames: true)]
|
videoCodec: "h264,hevc,mpeg4",
|
||||||
|
audioCodec: "aac,mp3,wav,eac3,ac3,flac,opus",
|
||||||
|
_protocol: "hls",
|
||||||
|
context: .streaming,
|
||||||
|
maxAudioChannels: "6",
|
||||||
|
minSegments: 2,
|
||||||
|
breakOnNonKeyFrames: true
|
||||||
|
)]
|
||||||
} else {
|
} else {
|
||||||
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,mpeg4",
|
transcodingProfiles = [TranscodingProfile(
|
||||||
audioCodec: "aac,mp3,wav,eac3,ac3,opus", _protocol: "hls",
|
container: "ts",
|
||||||
context: .streaming, maxAudioChannels: "6", minSegments: 2,
|
type: .video,
|
||||||
breakOnNonKeyFrames: true)]
|
videoCodec: "h264,mpeg4",
|
||||||
|
audioCodec: "aac,mp3,wav,eac3,ac3,opus",
|
||||||
|
_protocol: "hls",
|
||||||
|
context: .streaming,
|
||||||
|
maxAudioChannels: "6",
|
||||||
|
minSegments: 2,
|
||||||
|
breakOnNonKeyFrames: true
|
||||||
|
)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device supports FLAC?
|
// Device supports FLAC?
|
||||||
if supportsFeature(minimumSupported: .A10X) {
|
if supportsFeature(minimumSupported: .A10X) {
|
||||||
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "hevc,h264,mpeg4",
|
transcodingProfiles = [TranscodingProfile(
|
||||||
audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", _protocol: "hls",
|
container: "ts",
|
||||||
context: .streaming, maxAudioChannels: "6", minSegments: 2,
|
type: .video,
|
||||||
breakOnNonKeyFrames: true)]
|
videoCodec: "hevc,h264,mpeg4",
|
||||||
|
audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus",
|
||||||
|
_protocol: "hls",
|
||||||
|
context: .streaming,
|
||||||
|
maxAudioChannels: "6",
|
||||||
|
minSegments: 2,
|
||||||
|
breakOnNonKeyFrames: true
|
||||||
|
)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var codecProfiles: [CodecProfile] = []
|
var codecProfiles: [CodecProfile] = []
|
||||||
|
|
||||||
let h264CodecConditions: [ProfileCondition] = [
|
let h264CodecConditions: [ProfileCondition] = [
|
||||||
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
|
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
|
||||||
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|baseline|constrained baseline",
|
ProfileCondition(
|
||||||
isRequired: false),
|
condition: .equalsAny,
|
||||||
|
property: .videoProfile,
|
||||||
|
value: "high|main|baseline|constrained baseline",
|
||||||
|
isRequired: false
|
||||||
|
),
|
||||||
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "80", isRequired: false),
|
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "80", isRequired: false),
|
||||||
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false),
|
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false),
|
||||||
]
|
]
|
||||||
|
@ -147,12 +184,17 @@ class DeviceProfileBuilder {
|
||||||
|
|
||||||
let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", type: .video, mimeType: "video/mp4")]
|
let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", type: .video, mimeType: "video/mp4")]
|
||||||
|
|
||||||
let profile = ClientCapabilitiesDeviceProfile(maxStreamingBitrate: maxStreamingBitrate, maxStaticBitrate: maxStaticBitrate,
|
let profile = ClientCapabilitiesDeviceProfile(
|
||||||
|
maxStreamingBitrate: maxStreamingBitrate,
|
||||||
|
maxStaticBitrate: maxStaticBitrate,
|
||||||
musicStreamingTranscodingBitrate: musicStreamingTranscodingBitrate,
|
musicStreamingTranscodingBitrate: musicStreamingTranscodingBitrate,
|
||||||
directPlayProfiles: directPlayProfiles, transcodingProfiles: transcodingProfiles,
|
directPlayProfiles: directPlayProfiles,
|
||||||
|
transcodingProfiles: transcodingProfiles,
|
||||||
containerProfiles: [],
|
containerProfiles: [],
|
||||||
codecProfiles: codecProfiles, responseProfiles: responseProfiles,
|
codecProfiles: codecProfiles,
|
||||||
subtitleProfiles: subtitleProfiles)
|
responseProfiles: responseProfiles,
|
||||||
|
subtitleProfiles: subtitleProfiles
|
||||||
|
)
|
||||||
|
|
||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,17 +18,21 @@ class LogManager {
|
||||||
let logsDirectory = getDocumentsDirectory().appendingPathComponent("logs", isDirectory: true)
|
let logsDirectory = getDocumentsDirectory().appendingPathComponent("logs", isDirectory: true)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try FileManager.default.createDirectory(atPath: logsDirectory.path,
|
try FileManager.default.createDirectory(
|
||||||
|
atPath: logsDirectory.path,
|
||||||
withIntermediateDirectories: true,
|
withIntermediateDirectories: true,
|
||||||
attributes: nil)
|
attributes: nil
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
// logs directory already created
|
// logs directory already created
|
||||||
}
|
}
|
||||||
|
|
||||||
let logFileURL = logsDirectory.appendingPathComponent("swiftfin_log.log")
|
let logFileURL = logsDirectory.appendingPathComponent("swiftfin_log.log")
|
||||||
|
|
||||||
let fileRotationLogger = try! FileRotationLogger("org.jellyfin.swiftfin.logger.file-rotation",
|
let fileRotationLogger = try! FileRotationLogger(
|
||||||
fileURL: logFileURL)
|
"org.jellyfin.swiftfin.logger.file-rotation",
|
||||||
|
fileURL: logFileURL
|
||||||
|
)
|
||||||
fileRotationLogger.format = LogFormatter()
|
fileRotationLogger.format = LogFormatter()
|
||||||
|
|
||||||
let consoleLogger = ConsoleLogger("org.jellyfin.swiftfin.logger.console")
|
let consoleLogger = ConsoleLogger("org.jellyfin.swiftfin.logger.console")
|
||||||
|
@ -48,10 +52,18 @@ class LogManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogFormatter: LogFormattable {
|
class LogFormatter: LogFormattable {
|
||||||
func formatMessage(_ level: LogLevel, message: String, tag: String, function: String,
|
func formatMessage(
|
||||||
file: String, line: UInt, swiftLogInfo: [String: String],
|
_ level: LogLevel,
|
||||||
label: String, date: Date, threadID: UInt64) -> String
|
message: String,
|
||||||
{
|
tag: String,
|
||||||
|
function: String,
|
||||||
|
file: String,
|
||||||
|
line: UInt,
|
||||||
|
swiftLogInfo: [String: String],
|
||||||
|
label: String,
|
||||||
|
date: Date,
|
||||||
|
threadID: UInt64
|
||||||
|
) -> String {
|
||||||
let file = shortFileName(file).replacingOccurrences(of: ".swift", with: "")
|
let file = shortFileName(file).replacingOccurrences(of: ".swift", with: "")
|
||||||
return " [\(level.emoji) \(level)] \(file)#\(line):\(function) \(message)"
|
return " [\(level.emoji) \(level)] \(file)#\(line):\(function) \(message)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,10 @@ final class SessionManager {
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
if let lastUserID = Defaults[.lastServerUserID],
|
if let lastUserID = Defaults[.lastServerUserID],
|
||||||
let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
let user = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
[Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)])
|
From<SwiftfinStore.Models.StoredUser>(),
|
||||||
|
[Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)]
|
||||||
|
)
|
||||||
{
|
{
|
||||||
|
|
||||||
guard let server = user.server,
|
guard let server = user.server,
|
||||||
|
@ -56,8 +58,10 @@ final class SessionManager {
|
||||||
// MARK: fetchUsers
|
// MARK: fetchUsers
|
||||||
|
|
||||||
func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] {
|
func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] {
|
||||||
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id))
|
From<SwiftfinStore.Models.StoredServer>(),
|
||||||
|
Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)
|
||||||
|
)
|
||||||
else { fatalError("No stored server associated with given state server?") }
|
else { fatalError("No stored server associated with given state server?") }
|
||||||
return storedServer.users.map(\.state).sorted(by: { $0.username < $1.username })
|
return storedServer.users.map(\.state).sorted(by: { $0.username < $1.username })
|
||||||
}
|
}
|
||||||
|
@ -100,10 +104,13 @@ final class SessionManager {
|
||||||
newServer.users = []
|
newServer.users = []
|
||||||
|
|
||||||
// Check for existing server on device
|
// Check for existing server on device
|
||||||
if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
if let existingServer = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
[Where<SwiftfinStore.Models.StoredServer>("id == %@",
|
From<SwiftfinStore.Models.StoredServer>(),
|
||||||
newServer.id)])
|
[Where<SwiftfinStore.Models.StoredServer>(
|
||||||
{
|
"id == %@",
|
||||||
|
newServer.id
|
||||||
|
)]
|
||||||
|
) {
|
||||||
throw SwiftfinStore.Error.existingServer(existingServer.state)
|
throw SwiftfinStore.Error.existingServer(existingServer.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,9 +133,13 @@ final class SessionManager {
|
||||||
|
|
||||||
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
||||||
|
|
||||||
guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
[Where<SwiftfinStore.Models.StoredServer>("id == %@",
|
From<SwiftfinStore.Models.StoredServer>(),
|
||||||
server.id)])
|
[Where<SwiftfinStore.Models.StoredServer>(
|
||||||
|
"id == %@",
|
||||||
|
server.id
|
||||||
|
)]
|
||||||
|
)
|
||||||
else {
|
else {
|
||||||
fatalError("No stored server associated with given state server?")
|
fatalError("No stored server associated with given state server?")
|
||||||
}
|
}
|
||||||
|
@ -155,9 +166,13 @@ final class SessionManager {
|
||||||
|
|
||||||
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
||||||
|
|
||||||
guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
[Where<SwiftfinStore.Models.StoredServer>("id == %@",
|
From<SwiftfinStore.Models.StoredServer>(),
|
||||||
server.id)])
|
[Where<SwiftfinStore.Models.StoredServer>(
|
||||||
|
"id == %@",
|
||||||
|
server.id
|
||||||
|
)]
|
||||||
|
)
|
||||||
else {
|
else {
|
||||||
fatalError("No stored server associated with given state server?")
|
fatalError("No stored server associated with given state server?")
|
||||||
}
|
}
|
||||||
|
@ -183,9 +198,11 @@ final class SessionManager {
|
||||||
// MARK: loginUser publisher
|
// MARK: loginUser 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(server: SwiftfinStore.State.Server, username: String,
|
func loginUser(
|
||||||
password: String) -> AnyPublisher<SwiftfinStore.State.User, Error>
|
server: SwiftfinStore.State.Server,
|
||||||
{
|
username: String,
|
||||||
|
password: String
|
||||||
|
) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
||||||
setAuthHeader(with: "")
|
setAuthHeader(with: "")
|
||||||
|
|
||||||
JellyfinAPIAPI.basePath = server.currentURI
|
JellyfinAPIAPI.basePath = server.currentURI
|
||||||
|
@ -206,10 +223,13 @@ final class SessionManager {
|
||||||
newUser.appleTVID = ""
|
newUser.appleTVID = ""
|
||||||
|
|
||||||
// Check for existing user on device
|
// Check for existing user on device
|
||||||
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
[Where<SwiftfinStore.Models.StoredUser>("id == %@",
|
From<SwiftfinStore.Models.StoredUser>(),
|
||||||
newUser.id)])
|
[Where<SwiftfinStore.Models.StoredUser>(
|
||||||
{
|
"id == %@",
|
||||||
|
newUser.id
|
||||||
|
)]
|
||||||
|
) {
|
||||||
throw SwiftfinStore.Error.existingUser(existingUser.state)
|
throw SwiftfinStore.Error.existingUser(existingUser.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,11 +237,15 @@ final class SessionManager {
|
||||||
newAccessToken.value = accessToken
|
newAccessToken.value = accessToken
|
||||||
newUser.accessToken = newAccessToken
|
newUser.accessToken = newAccessToken
|
||||||
|
|
||||||
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
|
From<SwiftfinStore.Models.StoredServer>(),
|
||||||
[
|
[
|
||||||
Where<SwiftfinStore.Models.StoredServer>("id == %@",
|
Where<SwiftfinStore.Models.StoredServer>(
|
||||||
server.id),
|
"id == %@",
|
||||||
])
|
server.id
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
else { fatalError("No stored server associated with given state server?") }
|
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?") }
|
guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") }
|
||||||
|
@ -284,8 +308,10 @@ final class SessionManager {
|
||||||
// MARK: delete user
|
// MARK: delete user
|
||||||
|
|
||||||
func delete(user: SwiftfinStore.State.User) {
|
func delete(user: SwiftfinStore.State.User) {
|
||||||
guard let storedUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
guard let storedUser = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
[Where<SwiftfinStore.Models.StoredUser>("id == %@", user.id)])
|
From<SwiftfinStore.Models.StoredUser>(),
|
||||||
|
[Where<SwiftfinStore.Models.StoredUser>("id == %@", user.id)]
|
||||||
|
)
|
||||||
else { fatalError("No stored user for state user?") }
|
else { fatalError("No stored user for state user?") }
|
||||||
_delete(user: storedUser, transaction: nil)
|
_delete(user: storedUser, transaction: nil)
|
||||||
}
|
}
|
||||||
|
@ -293,8 +319,10 @@ final class SessionManager {
|
||||||
// MARK: delete server
|
// MARK: delete server
|
||||||
|
|
||||||
func delete(server: SwiftfinStore.State.Server) {
|
func delete(server: SwiftfinStore.State.Server) {
|
||||||
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)])
|
From<SwiftfinStore.Models.StoredServer>(),
|
||||||
|
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]
|
||||||
|
)
|
||||||
else { fatalError("No stored server for state server?") }
|
else { fatalError("No stored server for state server?") }
|
||||||
_delete(server: storedServer, transaction: nil)
|
_delete(server: storedServer, transaction: nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,15 @@ enum SwiftfinStore {
|
||||||
let version: String
|
let version: String
|
||||||
let userIDs: [String]
|
let userIDs: [String]
|
||||||
|
|
||||||
fileprivate init(uris: Set<String>, currentURI: String, name: String, id: String, os: String, version: String,
|
fileprivate init(
|
||||||
usersIDs: [String])
|
uris: Set<String>,
|
||||||
{
|
currentURI: String,
|
||||||
|
name: String,
|
||||||
|
id: String,
|
||||||
|
os: String,
|
||||||
|
version: String,
|
||||||
|
usersIDs: [String]
|
||||||
|
) {
|
||||||
self.uris = uris
|
self.uris = uris
|
||||||
self.currentURI = currentURI
|
self.currentURI = currentURI
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -40,13 +46,15 @@ enum SwiftfinStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
static var sample: Server {
|
static var sample: Server {
|
||||||
Server(uris: ["https://www.notaurl.com", "http://www.maybeaurl.org"],
|
Server(
|
||||||
|
uris: ["https://www.notaurl.com", "http://www.maybeaurl.org"],
|
||||||
currentURI: "https://www.notaurl.com",
|
currentURI: "https://www.notaurl.com",
|
||||||
name: "Johnny's Tree",
|
name: "Johnny's Tree",
|
||||||
id: "123abc",
|
id: "123abc",
|
||||||
os: "macOS",
|
os: "macOS",
|
||||||
version: "1.1.1",
|
version: "1.1.1",
|
||||||
usersIDs: ["1", "2"])
|
usersIDs: ["1", "2"]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,10 +72,12 @@ enum SwiftfinStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
static var sample: User {
|
static var sample: User {
|
||||||
User(username: "JohnnyAppleseed",
|
User(
|
||||||
|
username: "JohnnyAppleseed",
|
||||||
id: "123abc",
|
id: "123abc",
|
||||||
serverID: "123abc",
|
serverID: "123abc",
|
||||||
accessToken: "open-sesame")
|
accessToken: "open-sesame"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,13 +110,15 @@ enum SwiftfinStore {
|
||||||
var users: Set<StoredUser>
|
var users: Set<StoredUser>
|
||||||
|
|
||||||
var state: State.Server {
|
var state: State.Server {
|
||||||
State.Server(uris: uris,
|
State.Server(
|
||||||
|
uris: uris,
|
||||||
currentURI: currentURI,
|
currentURI: currentURI,
|
||||||
name: name,
|
name: name,
|
||||||
id: id,
|
id: id,
|
||||||
os: os,
|
os: os,
|
||||||
version: version,
|
version: version,
|
||||||
usersIDs: users.map(\.id))
|
usersIDs: users.map(\.id)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,10 +142,12 @@ enum SwiftfinStore {
|
||||||
var state: State.User {
|
var state: State.User {
|
||||||
guard let server = server else { fatalError("No server associated with user") }
|
guard let server = server else { fatalError("No server associated with user") }
|
||||||
guard let accessToken = accessToken else { fatalError("No access token associated with user") }
|
guard let accessToken = accessToken else { fatalError("No access token associated with user") }
|
||||||
return State.User(username: username,
|
return State.User(
|
||||||
|
username: username,
|
||||||
id: id,
|
id: id,
|
||||||
serverID: server.id,
|
serverID: server.id,
|
||||||
accessToken: accessToken.value)
|
accessToken: accessToken.value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +171,8 @@ enum SwiftfinStore {
|
||||||
// MARK: dataStack
|
// MARK: dataStack
|
||||||
|
|
||||||
static let dataStack: DataStack = {
|
static let dataStack: DataStack = {
|
||||||
let schema = CoreStoreSchema(modelVersion: "V1",
|
let schema = CoreStoreSchema(
|
||||||
|
modelVersion: "V1",
|
||||||
entities: [
|
entities: [
|
||||||
Entity<SwiftfinStore.Models.StoredServer>("Server"),
|
Entity<SwiftfinStore.Models.StoredServer>("Server"),
|
||||||
Entity<SwiftfinStore.Models.StoredUser>("User"),
|
Entity<SwiftfinStore.Models.StoredUser>("User"),
|
||||||
|
@ -182,11 +197,14 @@ enum SwiftfinStore {
|
||||||
0x9EDA_7328_21A1_5EA9,
|
0x9EDA_7328_21A1_5EA9,
|
||||||
0xB5A_FA53_1E41_CE8A,
|
0xB5A_FA53_1E41_CE8A,
|
||||||
],
|
],
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
let _dataStack = DataStack(schema)
|
let _dataStack = DataStack(schema)
|
||||||
try! _dataStack.addStorageAndWait(SQLiteStore(fileName: "Swiftfin.sqlite",
|
try! _dataStack.addStorageAndWait(SQLiteStore(
|
||||||
localStorageOptions: .recreateStoreOnModelMismatch))
|
fileName: "Swiftfin.sqlite",
|
||||||
|
localStorageOptions: .recreateStoreOnModelMismatch
|
||||||
|
))
|
||||||
return _dataStack
|
return _dataStack
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,11 @@ extension Defaults.Keys {
|
||||||
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode",
|
static let autoSelectSubtitlesLangCode = Key<String>(
|
||||||
|
"AutoSelectSubtitlesLangCode",
|
||||||
default: "Auto",
|
default: "Auto",
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite
|
||||||
|
)
|
||||||
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
|
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
|
||||||
// Customize settings
|
// Customize settings
|
||||||
|
@ -40,21 +42,31 @@ extension Defaults.Keys {
|
||||||
// Video player / overlay settings
|
// Video player / overlay settings
|
||||||
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
|
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let systemControlGesturesEnabled = Key<Bool>("systemControlGesturesEnabled",
|
static let systemControlGesturesEnabled = Key<Bool>(
|
||||||
|
"systemControlGesturesEnabled",
|
||||||
default: true,
|
default: true,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite
|
||||||
static let playerGesturesLockGestureEnabled = Key<Bool>("playerGesturesLockGestureEnabled",
|
)
|
||||||
|
static let playerGesturesLockGestureEnabled = Key<Bool>(
|
||||||
|
"playerGesturesLockGestureEnabled",
|
||||||
default: true,
|
default: true,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite
|
||||||
static let seekSlideGestureEnabled = Key<Bool>("seekSlideGestureEnabled",
|
)
|
||||||
|
static let seekSlideGestureEnabled = Key<Bool>(
|
||||||
|
"seekSlideGestureEnabled",
|
||||||
default: true,
|
default: true,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite
|
||||||
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward",
|
)
|
||||||
|
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>(
|
||||||
|
"videoPlayerJumpForward",
|
||||||
default: .fifteen,
|
default: .fifteen,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite
|
||||||
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward",
|
)
|
||||||
|
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>(
|
||||||
|
"videoPlayerJumpBackward",
|
||||||
default: .fifteen,
|
default: .fifteen,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite
|
||||||
|
)
|
||||||
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let subtitleSize = Key<SubtitleSize>("subtitleSize", default: .regular, suite: SwiftfinStore.Defaults.generalSuite)
|
static let subtitleSize = Key<SubtitleSize>("subtitleSize", default: .regular, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
@ -69,19 +81,25 @@ extension Defaults.Keys {
|
||||||
static let shouldShowMissingEpisodes = Key<Bool>("shouldShowMissingEpisodes", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
static let shouldShowMissingEpisodes = Key<Bool>("shouldShowMissingEpisodes", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
|
||||||
// Should show video player items in overlay menu
|
// Should show video player items in overlay menu
|
||||||
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu",
|
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>(
|
||||||
|
"shouldShowJumpButtonsInMenu",
|
||||||
default: true,
|
default: true,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite
|
||||||
|
)
|
||||||
|
|
||||||
static let shouldShowChaptersInfoInBottomOverlay = Key<Bool>("shouldShowChaptersInfoInBottomOverlay",
|
static let shouldShowChaptersInfoInBottomOverlay = Key<Bool>(
|
||||||
|
"shouldShowChaptersInfoInBottomOverlay",
|
||||||
default: true,
|
default: true,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite
|
||||||
|
)
|
||||||
|
|
||||||
// Experimental settings
|
// Experimental settings
|
||||||
enum Experimental {
|
enum Experimental {
|
||||||
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState",
|
static let syncSubtitleStateWithAdjacent = Key<Bool>(
|
||||||
|
"experimental.syncSubtitleState",
|
||||||
default: false,
|
default: false,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite
|
||||||
|
)
|
||||||
static let forceDirectPlay = Key<Bool>("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
static let forceDirectPlay = Key<Bool>("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let nativePlayer = Key<Bool>("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
static let nativePlayer = Key<Bool>("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
|
|
@ -74,9 +74,11 @@ final class ConnectToServerViewModel: ViewModel {
|
||||||
self.handleAPIRequestError(displayMessage: L10n.tooManyRedirects, completion: completion)
|
self.handleAPIRequestError(displayMessage: L10n.tooManyRedirects, completion: completion)
|
||||||
} else {
|
} else {
|
||||||
self
|
self
|
||||||
.connectToServer(uri: newURL.absoluteString
|
.connectToServer(
|
||||||
|
uri: newURL.absoluteString
|
||||||
.removeRegexMatches(pattern: "/web/index.html"),
|
.removeRegexMatches(pattern: "/web/index.html"),
|
||||||
redirectCount: redirectCount + 1)
|
redirectCount: redirectCount + 1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
|
|
@ -27,9 +27,11 @@ extension EpisodesRowManager {
|
||||||
|
|
||||||
// Also retrieves the current season episodes if available
|
// Also retrieves the current season episodes if available
|
||||||
func retrieveSeasons() {
|
func retrieveSeasons() {
|
||||||
TvShowsAPI.getSeasons(seriesId: item.seriesId ?? "",
|
TvShowsAPI.getSeasons(
|
||||||
|
seriesId: item.seriesId ?? "",
|
||||||
userId: SessionManager.main.currentLogin.user.id,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
isMissing: Defaults[.shouldShowMissingSeasons] ? nil : false)
|
isMissing: Defaults[.shouldShowMissingSeasons] ? nil : false
|
||||||
|
)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
|
@ -52,11 +54,13 @@ extension EpisodesRowManager {
|
||||||
func retrieveEpisodesForSeason(_ season: BaseItemDto) {
|
func retrieveEpisodesForSeason(_ season: BaseItemDto) {
|
||||||
guard let seasonID = season.id else { return }
|
guard let seasonID = season.id else { return }
|
||||||
|
|
||||||
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "",
|
TvShowsAPI.getEpisodes(
|
||||||
|
seriesId: item.seriesId ?? "",
|
||||||
userId: SessionManager.main.currentLogin.user.id,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
seasonId: seasonID,
|
seasonId: seasonID,
|
||||||
isMissing: Defaults[.shouldShowMissingEpisodes] ? nil : false)
|
isMissing: Defaults[.shouldShowMissingEpisodes] ? nil : false
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
|
|
@ -125,7 +125,8 @@ final class HomeViewModel: ViewModel {
|
||||||
// MARK: Latest Added Items
|
// MARK: Latest Added Items
|
||||||
|
|
||||||
private func refreshLatestAddedItems() {
|
private func refreshLatestAddedItems() {
|
||||||
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
|
UserLibraryAPI.getLatestMedia(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [
|
fields: [
|
||||||
.primaryImageAspectRatio,
|
.primaryImageAspectRatio,
|
||||||
.seriesPrimaryImage,
|
.seriesPrimaryImage,
|
||||||
|
@ -138,7 +139,8 @@ final class HomeViewModel: ViewModel {
|
||||||
includeItemTypes: [.movie, .series],
|
includeItemTypes: [.movie, .series],
|
||||||
enableImageTypes: [.primary, .backdrop, .thumb],
|
enableImageTypes: [.primary, .backdrop, .thumb],
|
||||||
enableUserData: true,
|
enableUserData: true,
|
||||||
limit: 8)
|
limit: 8
|
||||||
|
)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished: ()
|
case .finished: ()
|
||||||
|
@ -157,7 +159,8 @@ final class HomeViewModel: ViewModel {
|
||||||
// MARK: Resume Items
|
// MARK: Resume Items
|
||||||
|
|
||||||
private func refreshResumeItems() {
|
private func refreshResumeItems() {
|
||||||
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id,
|
ItemsAPI.getResumeItems(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
limit: 6,
|
limit: 6,
|
||||||
fields: [
|
fields: [
|
||||||
.primaryImageAspectRatio,
|
.primaryImageAspectRatio,
|
||||||
|
@ -168,7 +171,8 @@ final class HomeViewModel: ViewModel {
|
||||||
.people,
|
.people,
|
||||||
.chapters,
|
.chapters,
|
||||||
],
|
],
|
||||||
enableUserData: true)
|
enableUserData: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
|
@ -188,8 +192,10 @@ final class HomeViewModel: ViewModel {
|
||||||
func removeItemFromResume(_ item: BaseItemDto) {
|
func removeItemFromResume(_ item: BaseItemDto) {
|
||||||
guard let itemID = item.id, resumeItems.contains(where: { $0.id == itemID }) else { return }
|
guard let itemID = item.id, resumeItems.contains(where: { $0.id == itemID }) else { return }
|
||||||
|
|
||||||
PlaystateAPI.markUnplayedItem(userId: SessionManager.main.currentLogin.user.id,
|
PlaystateAPI.markUnplayedItem(
|
||||||
itemId: item.id!)
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
itemId: item.id!
|
||||||
|
)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
}, receiveValue: { _ in
|
}, receiveValue: { _ in
|
||||||
|
@ -202,7 +208,8 @@ final class HomeViewModel: ViewModel {
|
||||||
// MARK: Next Up Items
|
// MARK: Next Up Items
|
||||||
|
|
||||||
private func refreshNextUpItems() {
|
private func refreshNextUpItems() {
|
||||||
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
|
TvShowsAPI.getNextUp(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
limit: 6,
|
limit: 6,
|
||||||
fields: [
|
fields: [
|
||||||
.primaryImageAspectRatio,
|
.primaryImageAspectRatio,
|
||||||
|
@ -213,7 +220,8 @@ final class HomeViewModel: ViewModel {
|
||||||
.people,
|
.people,
|
||||||
.chapters,
|
.chapters,
|
||||||
],
|
],
|
||||||
enableUserData: true)
|
enableUserData: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
|
|
|
@ -22,9 +22,11 @@ final class CollectionItemViewModel: ItemViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getCollectionItems() {
|
private func getCollectionItems() {
|
||||||
ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id,
|
ItemsAPI.getItems(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
parentId: item.id,
|
parentId: item.id,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink { [weak self] completion in
|
.sink { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
|
|
@ -47,7 +47,8 @@ final class EpisodeItemViewModel: ItemViewModel, EpisodesRowManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateItem() {
|
override func updateItem() {
|
||||||
ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id,
|
ItemsAPI.getItems(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
fields: [
|
fields: [
|
||||||
.primaryImageAspectRatio,
|
.primaryImageAspectRatio,
|
||||||
|
@ -59,7 +60,8 @@ final class EpisodeItemViewModel: ItemViewModel, EpisodesRowManager {
|
||||||
.chapters,
|
.chapters,
|
||||||
],
|
],
|
||||||
enableUserData: true,
|
enableUserData: true,
|
||||||
ids: [item.id ?? ""])
|
ids: [item.id ?? ""]
|
||||||
|
)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
|
|
|
@ -113,10 +113,12 @@ class ItemViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSimilarItems() {
|
func getSimilarItems() {
|
||||||
LibraryAPI.getSimilarItems(itemId: item.id!,
|
LibraryAPI.getSimilarItems(
|
||||||
|
itemId: item.id!,
|
||||||
userId: SessionManager.main.currentLogin.user.id,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
@ -128,8 +130,10 @@ class ItemViewModel: ViewModel {
|
||||||
|
|
||||||
func updateWatchState() {
|
func updateWatchState() {
|
||||||
if isWatched {
|
if isWatched {
|
||||||
PlaystateAPI.markUnplayedItem(userId: SessionManager.main.currentLogin.user.id,
|
PlaystateAPI.markUnplayedItem(
|
||||||
itemId: item.id!)
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
itemId: item.id!
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
@ -138,8 +142,10 @@ class ItemViewModel: ViewModel {
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
} else {
|
} else {
|
||||||
PlaystateAPI.markPlayedItem(userId: SessionManager.main.currentLogin.user.id,
|
PlaystateAPI.markPlayedItem(
|
||||||
itemId: item.id!)
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
itemId: item.id!
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
|
|
@ -13,7 +13,8 @@ import JellyfinAPI
|
||||||
final class MovieItemViewModel: ItemViewModel {
|
final class MovieItemViewModel: ItemViewModel {
|
||||||
|
|
||||||
override func updateItem() {
|
override func updateItem() {
|
||||||
ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id,
|
ItemsAPI.getItems(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
fields: [
|
fields: [
|
||||||
.primaryImageAspectRatio,
|
.primaryImageAspectRatio,
|
||||||
|
@ -25,7 +26,8 @@ final class MovieItemViewModel: ItemViewModel {
|
||||||
.chapters,
|
.chapters,
|
||||||
],
|
],
|
||||||
enableUserData: true,
|
enableUserData: true,
|
||||||
ids: [item.id ?? ""])
|
ids: [item.id ?? ""]
|
||||||
|
)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
|
|
|
@ -46,9 +46,12 @@ final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager {
|
||||||
private func requestEpisodes() {
|
private func requestEpisodes() {
|
||||||
LogManager.log
|
LogManager.log
|
||||||
.debug("Getting episodes in season \(item.id!) (\(item.name!)) of show \(item.seriesId!) (\(item.seriesName!))")
|
.debug("Getting episodes in season \(item.id!) (\(item.name!)) of show \(item.seriesId!) (\(item.seriesName!))")
|
||||||
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.main.currentLogin.user.id,
|
TvShowsAPI.getEpisodes(
|
||||||
|
seriesId: item.seriesId ?? "",
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
seasonId: item.id ?? "")
|
seasonId: item.id ?? ""
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
@ -64,9 +67,12 @@ final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager {
|
||||||
|
|
||||||
private func setNextUpInSeason() {
|
private func setNextUpInSeason() {
|
||||||
|
|
||||||
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
|
TvShowsAPI.getNextUp(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
seriesId: item.seriesId ?? "", enableUserData: true)
|
seriesId: item.seriesId ?? "",
|
||||||
|
enableUserData: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
@ -109,8 +115,10 @@ final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager {
|
||||||
|
|
||||||
private func getSeriesItem() {
|
private func getSeriesItem() {
|
||||||
guard let seriesID = item.seriesId else { return }
|
guard let seriesID = item.seriesId else { return }
|
||||||
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id,
|
UserLibraryAPI.getItem(
|
||||||
itemId: seriesID)
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
itemId: seriesID
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink { [weak self] completion in
|
.sink { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
|
|
@ -44,10 +44,12 @@ final class SeriesItemViewModel: ItemViewModel {
|
||||||
private func getNextUp() {
|
private func getNextUp() {
|
||||||
|
|
||||||
LogManager.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))")
|
LogManager.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))")
|
||||||
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
|
TvShowsAPI.getNextUp(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
seriesId: self.item.id!,
|
seriesId: self.item.id!,
|
||||||
enableUserData: true)
|
enableUserData: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
@ -79,10 +81,13 @@ final class SeriesItemViewModel: ItemViewModel {
|
||||||
|
|
||||||
private func requestSeasons() {
|
private func requestSeasons() {
|
||||||
LogManager.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))")
|
LogManager.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))")
|
||||||
TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.main.currentLogin.user.id,
|
TvShowsAPI.getSeasons(
|
||||||
|
seriesId: item.id ?? "",
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
isMissing: Defaults[.shouldShowMissingSeasons] ? nil : false,
|
isMissing: Defaults[.shouldShowMissingSeasons] ? nil : false,
|
||||||
enableUserData: true)
|
enableUserData: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
|
|
@ -26,7 +26,8 @@ final class LatestMediaViewModel: ViewModel {
|
||||||
|
|
||||||
func requestLatestMedia() {
|
func requestLatestMedia() {
|
||||||
LogManager.log.debug("Requesting latest media for user id \(SessionManager.main.currentLogin.user.id)")
|
LogManager.log.debug("Requesting latest media for user id \(SessionManager.main.currentLogin.user.id)")
|
||||||
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
|
UserLibraryAPI.getLatestMedia(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
parentId: library.id ?? "",
|
parentId: library.id ?? "",
|
||||||
fields: [
|
fields: [
|
||||||
.primaryImageAspectRatio,
|
.primaryImageAspectRatio,
|
||||||
|
@ -37,7 +38,9 @@ final class LatestMediaViewModel: ViewModel {
|
||||||
.people,
|
.people,
|
||||||
],
|
],
|
||||||
includeItemTypes: [.series, .movie],
|
includeItemTypes: [.series, .movie],
|
||||||
enableUserData: true, limit: 12)
|
enableUserData: true,
|
||||||
|
limit: 12
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
|
|
@ -51,9 +51,11 @@ final class LibraryFilterViewModel: ViewModel {
|
||||||
modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
||||||
}
|
}
|
||||||
|
|
||||||
init(filters: LibraryFilters? = nil,
|
init(
|
||||||
enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter], parentId: String)
|
filters: LibraryFilters? = nil,
|
||||||
{
|
enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter],
|
||||||
|
parentId: String
|
||||||
|
) {
|
||||||
self.enabledFilterType = enabledFilterType
|
self.enabledFilterType = enabledFilterType
|
||||||
self.selectedSortBy = filters?.sortBy.first ?? .name
|
self.selectedSortBy = filters?.sortBy.first ?? .name
|
||||||
self.selectedSortOrder = filters?.sortOrder.first ?? .descending
|
self.selectedSortOrder = filters?.sortOrder.first ?? .descending
|
||||||
|
@ -67,8 +69,10 @@ final class LibraryFilterViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestQueryFilters() {
|
func requestQueryFilters() {
|
||||||
FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id,
|
FilterAPI.getQueryFilters(
|
||||||
parentId: self.parentId)
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
parentId: self.parentId
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
|
|
@ -90,7 +90,8 @@ final class LibrarySearchViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestSuggestions() {
|
func requestSuggestions() {
|
||||||
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id,
|
ItemsAPI.getItemsByUserId(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
parentId: parentID,
|
parentId: parentID,
|
||||||
|
@ -98,7 +99,8 @@ final class LibrarySearchViewModel: ViewModel {
|
||||||
sortBy: ["IsFavoriteOrLiked", "Random"],
|
sortBy: ["IsFavoriteOrLiked", "Random"],
|
||||||
imageTypeLimit: 0,
|
imageTypeLimit: 0,
|
||||||
enableTotalRecordCount: false,
|
enableTotalRecordCount: false,
|
||||||
enableImages: false)
|
enableImages: false
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
@ -110,11 +112,19 @@ final class LibrarySearchViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func search(with query: String) {
|
func search(with query: String) {
|
||||||
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
|
ItemsAPI.getItemsByUserId(
|
||||||
sortOrder: [.ascending], parentId: parentID,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
limit: 50,
|
||||||
|
recursive: true,
|
||||||
|
searchTerm: query,
|
||||||
|
sortOrder: [.ascending],
|
||||||
|
parentId: parentID,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
includeItemTypes: [.movie], sortBy: ["SortName"], enableUserData: true,
|
includeItemTypes: [.movie],
|
||||||
enableImages: true)
|
sortBy: ["SortName"],
|
||||||
|
enableUserData: true,
|
||||||
|
enableImages: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
@ -123,11 +133,19 @@ final class LibrarySearchViewModel: ViewModel {
|
||||||
self?.movieItems = response.items ?? []
|
self?.movieItems = response.items ?? []
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
|
ItemsAPI.getItemsByUserId(
|
||||||
sortOrder: [.ascending], parentId: parentID,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
limit: 50,
|
||||||
|
recursive: true,
|
||||||
|
searchTerm: query,
|
||||||
|
sortOrder: [.ascending],
|
||||||
|
parentId: parentID,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
includeItemTypes: [.series], sortBy: ["SortName"], enableUserData: true,
|
includeItemTypes: [.series],
|
||||||
enableImages: true)
|
sortBy: ["SortName"],
|
||||||
|
enableUserData: true,
|
||||||
|
enableImages: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
@ -136,11 +154,19 @@ final class LibrarySearchViewModel: ViewModel {
|
||||||
self?.showItems = response.items ?? []
|
self?.showItems = response.items ?? []
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
|
ItemsAPI.getItemsByUserId(
|
||||||
sortOrder: [.ascending], parentId: parentID,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
limit: 50,
|
||||||
|
recursive: true,
|
||||||
|
searchTerm: query,
|
||||||
|
sortOrder: [.ascending],
|
||||||
|
parentId: parentID,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
includeItemTypes: [.episode], sortBy: ["SortName"], enableUserData: true,
|
includeItemTypes: [.episode],
|
||||||
enableImages: true)
|
sortBy: ["SortName"],
|
||||||
|
enableUserData: true,
|
||||||
|
enableImages: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
|
|
@ -54,13 +54,14 @@ final class LibraryViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(parentID: String? = nil,
|
init(
|
||||||
|
parentID: String? = nil,
|
||||||
person: BaseItemPerson? = nil,
|
person: BaseItemPerson? = nil,
|
||||||
genre: NameGuidPair? = nil,
|
genre: NameGuidPair? = nil,
|
||||||
studio: NameGuidPair? = nil,
|
studio: NameGuidPair? = nil,
|
||||||
filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]),
|
filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]),
|
||||||
columns: Int = 7)
|
columns: Int = 7
|
||||||
{
|
) {
|
||||||
self.parentID = parentID
|
self.parentID = parentID
|
||||||
self.person = person
|
self.person = person
|
||||||
self.genre = genre
|
self.genre = genre
|
||||||
|
@ -106,7 +107,9 @@ final class LibraryViewModel: ViewModel {
|
||||||
includeItemTypes = [.movie, .series, .boxSet] + (Defaults[.showFlattenView] ? [] : [.folder])
|
includeItemTypes = [.movie, .series, .boxSet] + (Defaults[.showFlattenView] ? [] : [.folder])
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * pageItemSize,
|
ItemsAPI.getItemsByUserId(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
startIndex: currentPage * pageItemSize,
|
||||||
limit: pageItemSize,
|
limit: pageItemSize,
|
||||||
recursive: queryRecursive,
|
recursive: queryRecursive,
|
||||||
searchTerm: nil,
|
searchTerm: nil,
|
||||||
|
@ -129,7 +132,8 @@ final class LibraryViewModel: ViewModel {
|
||||||
personIds: personIDs,
|
personIds: personIDs,
|
||||||
studioIds: studioIDs,
|
studioIds: studioIDs,
|
||||||
genreIds: genreIDs,
|
genreIds: genreIDs,
|
||||||
enableImages: true)
|
enableImages: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
@ -174,8 +178,10 @@ final class LibraryViewModel: ViewModel {
|
||||||
rowCells.append(loadingCell)
|
rowCells.append(loadingCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatedRows.append(LibraryRow(section: i,
|
calculatedRows.append(LibraryRow(
|
||||||
items: rowCells))
|
section: i,
|
||||||
|
items: rowCells
|
||||||
|
))
|
||||||
}
|
}
|
||||||
return calculatedRows
|
return calculatedRows
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,12 +77,14 @@ final class LiveTVChannelsViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getChannels() {
|
func getChannels() {
|
||||||
LiveTvAPI.getLiveTvChannels(userId: SessionManager.main.currentLogin.user.id,
|
LiveTvAPI.getLiveTvChannels(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
enableImageTypes: [.primary],
|
enableImageTypes: [.primary],
|
||||||
enableUserData: false,
|
enableUserData: false,
|
||||||
enableFavoriteSorting: true)
|
enableFavoriteSorting: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
@ -106,7 +108,8 @@ final class LiveTVChannelsViewModel: ViewModel {
|
||||||
let minEndDate = Date.now.addComponentsToDate(hours: -1)
|
let minEndDate = Date.now.addComponentsToDate(hours: -1)
|
||||||
let maxStartDate = minEndDate.addComponentsToDate(hours: 6)
|
let maxStartDate = minEndDate.addComponentsToDate(hours: 6)
|
||||||
|
|
||||||
let getProgramsRequest = GetProgramsRequest(channelIds: channelIds,
|
let getProgramsRequest = GetProgramsRequest(
|
||||||
|
channelIds: channelIds,
|
||||||
userId: SessionManager.main.currentLogin.user.id,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
maxStartDate: maxStartDate,
|
maxStartDate: maxStartDate,
|
||||||
minEndDate: minEndDate,
|
minEndDate: minEndDate,
|
||||||
|
@ -115,7 +118,8 @@ final class LiveTVChannelsViewModel: ViewModel {
|
||||||
enableTotalRecordCount: false,
|
enableTotalRecordCount: false,
|
||||||
imageTypeLimit: 1,
|
imageTypeLimit: 1,
|
||||||
enableImageTypes: [.primary],
|
enableImageTypes: [.primary],
|
||||||
enableUserData: false)
|
enableUserData: false
|
||||||
|
)
|
||||||
|
|
||||||
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
|
|
@ -37,12 +37,14 @@ final class LiveTVProgramsViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getChannels() {
|
private func getChannels() {
|
||||||
LiveTvAPI.getLiveTvChannels(userId: SessionManager.main.currentLogin.user.id,
|
LiveTvAPI.getLiveTvChannels(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
enableImageTypes: [.primary],
|
enableImageTypes: [.primary],
|
||||||
enableUserData: false,
|
enableUserData: false,
|
||||||
enableFavoriteSorting: true)
|
enableFavoriteSorting: true
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
@ -67,13 +69,15 @@ final class LiveTVProgramsViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getRecommendedPrograms() {
|
private func getRecommendedPrograms() {
|
||||||
LiveTvAPI.getRecommendedPrograms(userId: SessionManager.main.currentLogin.user.id,
|
LiveTvAPI.getRecommendedPrograms(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
limit: 9,
|
limit: 9,
|
||||||
isAiring: true,
|
isAiring: true,
|
||||||
imageTypeLimit: 1,
|
imageTypeLimit: 1,
|
||||||
enableImageTypes: [.primary, .thumb],
|
enableImageTypes: [.primary, .thumb],
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio],
|
fields: [.channelInfo, .primaryImageAspectRatio],
|
||||||
enableTotalRecordCount: false)
|
enableTotalRecordCount: false
|
||||||
|
)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
@ -86,7 +90,8 @@ final class LiveTVProgramsViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getSeries() {
|
private func getSeries() {
|
||||||
let getProgramsRequest = GetProgramsRequest(userId: SessionManager.main.currentLogin.user.id,
|
let getProgramsRequest = GetProgramsRequest(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
hasAired: false,
|
hasAired: false,
|
||||||
isMovie: false,
|
isMovie: false,
|
||||||
isSeries: true,
|
isSeries: true,
|
||||||
|
@ -96,7 +101,8 @@ final class LiveTVProgramsViewModel: ViewModel {
|
||||||
limit: 9,
|
limit: 9,
|
||||||
enableTotalRecordCount: false,
|
enableTotalRecordCount: false,
|
||||||
enableImageTypes: [.primary, .thumb],
|
enableImageTypes: [.primary, .thumb],
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio])
|
fields: [.channelInfo, .primaryImageAspectRatio]
|
||||||
|
)
|
||||||
|
|
||||||
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
@ -111,7 +117,8 @@ final class LiveTVProgramsViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getMovies() {
|
private func getMovies() {
|
||||||
let getProgramsRequest = GetProgramsRequest(userId: SessionManager.main.currentLogin.user.id,
|
let getProgramsRequest = GetProgramsRequest(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
hasAired: false,
|
hasAired: false,
|
||||||
isMovie: true,
|
isMovie: true,
|
||||||
isSeries: false,
|
isSeries: false,
|
||||||
|
@ -121,7 +128,8 @@ final class LiveTVProgramsViewModel: ViewModel {
|
||||||
limit: 9,
|
limit: 9,
|
||||||
enableTotalRecordCount: false,
|
enableTotalRecordCount: false,
|
||||||
enableImageTypes: [.primary, .thumb],
|
enableImageTypes: [.primary, .thumb],
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio])
|
fields: [.channelInfo, .primaryImageAspectRatio]
|
||||||
|
)
|
||||||
|
|
||||||
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
@ -136,13 +144,15 @@ final class LiveTVProgramsViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getSports() {
|
private func getSports() {
|
||||||
let getProgramsRequest = GetProgramsRequest(userId: SessionManager.main.currentLogin.user.id,
|
let getProgramsRequest = GetProgramsRequest(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
hasAired: false,
|
hasAired: false,
|
||||||
isSports: true,
|
isSports: true,
|
||||||
limit: 9,
|
limit: 9,
|
||||||
enableTotalRecordCount: false,
|
enableTotalRecordCount: false,
|
||||||
enableImageTypes: [.primary, .thumb],
|
enableImageTypes: [.primary, .thumb],
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio])
|
fields: [.channelInfo, .primaryImageAspectRatio]
|
||||||
|
)
|
||||||
|
|
||||||
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
@ -157,13 +167,15 @@ final class LiveTVProgramsViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getKids() {
|
private func getKids() {
|
||||||
let getProgramsRequest = GetProgramsRequest(userId: SessionManager.main.currentLogin.user.id,
|
let getProgramsRequest = GetProgramsRequest(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
hasAired: false,
|
hasAired: false,
|
||||||
isKids: true,
|
isKids: true,
|
||||||
limit: 9,
|
limit: 9,
|
||||||
enableTotalRecordCount: false,
|
enableTotalRecordCount: false,
|
||||||
enableImageTypes: [.primary, .thumb],
|
enableImageTypes: [.primary, .thumb],
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio])
|
fields: [.channelInfo, .primaryImageAspectRatio]
|
||||||
|
)
|
||||||
|
|
||||||
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
@ -178,13 +190,15 @@ final class LiveTVProgramsViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getNews() {
|
private func getNews() {
|
||||||
let getProgramsRequest = GetProgramsRequest(userId: SessionManager.main.currentLogin.user.id,
|
let getProgramsRequest = GetProgramsRequest(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
hasAired: false,
|
hasAired: false,
|
||||||
isNews: true,
|
isNews: true,
|
||||||
limit: 9,
|
limit: 9,
|
||||||
enableTotalRecordCount: false,
|
enableTotalRecordCount: false,
|
||||||
enableImageTypes: [.primary, .thumb],
|
enableImageTypes: [.primary, .thumb],
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio])
|
fields: [.channelInfo, .primaryImageAspectRatio]
|
||||||
|
)
|
||||||
|
|
||||||
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
LiveTvAPI.getPrograms(getProgramsRequest: getProgramsRequest)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
|
|
@ -84,8 +84,10 @@ final class MovieLibrariesViewModel: ViewModel {
|
||||||
rowCells.append(loadingCell)
|
rowCells.append(loadingCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatedRows.append(LibraryRow(section: i,
|
calculatedRows.append(LibraryRow(
|
||||||
items: rowCells))
|
section: i,
|
||||||
|
items: rowCells
|
||||||
|
))
|
||||||
}
|
}
|
||||||
return calculatedRows
|
return calculatedRows
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,8 +84,10 @@ final class TVLibrariesViewModel: ViewModel {
|
||||||
rowCells.append(loadingCell)
|
rowCells.append(loadingCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatedRows.append(LibraryRow(section: i,
|
calculatedRows.append(LibraryRow(
|
||||||
items: rowCells))
|
section: i,
|
||||||
|
items: rowCells
|
||||||
|
))
|
||||||
}
|
}
|
||||||
return calculatedRows
|
return calculatedRows
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,10 +66,12 @@ final class UserSignInViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProfileImageUrl(user: UserDto) -> URL? {
|
func getProfileImageUrl(user: UserDto) -> URL? {
|
||||||
let urlString = ImageAPI.getUserImageWithRequestBuilder(userId: user.id ?? "--",
|
let urlString = ImageAPI.getUserImageWithRequestBuilder(
|
||||||
|
userId: user.id ?? "--",
|
||||||
imageType: .primary,
|
imageType: .primary,
|
||||||
width: 200,
|
width: 200,
|
||||||
quality: 90).URLString
|
quality: 90
|
||||||
|
).URLString
|
||||||
return URL(string: urlString)
|
return URL(string: urlString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -211,7 +211,8 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
|
|
||||||
// MARK: init
|
// MARK: init
|
||||||
|
|
||||||
init(item: BaseItemDto,
|
init(
|
||||||
|
item: BaseItemDto,
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String?,
|
subtitle: String?,
|
||||||
directStreamURL: URL,
|
directStreamURL: URL,
|
||||||
|
@ -232,8 +233,8 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
shouldShowAutoPlay: Bool,
|
shouldShowAutoPlay: Bool,
|
||||||
container: String,
|
container: String,
|
||||||
filename: String?,
|
filename: String?,
|
||||||
versionName: String?)
|
versionName: String?
|
||||||
{
|
) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.title = title
|
self.title = title
|
||||||
self.subtitle = subtitle
|
self.subtitle = subtitle
|
||||||
|
@ -334,11 +335,13 @@ extension VideoPlayerViewModel {
|
||||||
func getAdjacentEpisodes() {
|
func getAdjacentEpisodes() {
|
||||||
guard let seriesID = item.seriesId, item.itemType == .episode else { return }
|
guard let seriesID = item.seriesId, item.itemType == .episode else { return }
|
||||||
|
|
||||||
TvShowsAPI.getEpisodes(seriesId: seriesID,
|
TvShowsAPI.getEpisodes(
|
||||||
|
seriesId: seriesID,
|
||||||
userId: SessionManager.main.currentLogin.user.id,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [.chapters],
|
fields: [.chapters],
|
||||||
adjacentTo: item.id,
|
adjacentTo: item.id,
|
||||||
limit: 3)
|
limit: 3
|
||||||
|
)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
|
@ -459,11 +462,13 @@ extension VideoPlayerViewModel {
|
||||||
extension VideoPlayerViewModel {
|
extension VideoPlayerViewModel {
|
||||||
private func sendNewProgressReportWithTimer() {
|
private func sendNewProgressReportWithTimer() {
|
||||||
progressReportTimer?.invalidate()
|
progressReportTimer?.invalidate()
|
||||||
progressReportTimer = Timer.scheduledTimer(timeInterval: 0.7,
|
progressReportTimer = Timer.scheduledTimer(
|
||||||
|
timeInterval: 0.7,
|
||||||
target: self,
|
target: self,
|
||||||
selector: #selector(_sendProgressReport),
|
selector: #selector(_sendProgressReport),
|
||||||
userInfo: nil,
|
userInfo: nil,
|
||||||
repeats: false)
|
repeats: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,7 +482,8 @@ extension VideoPlayerViewModel {
|
||||||
|
|
||||||
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
|
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
|
||||||
|
|
||||||
let reportPlaybackStartRequest = ReportPlaybackStartRequest(canSeek: true,
|
let reportPlaybackStartRequest = ReportPlaybackStartRequest(
|
||||||
|
canSeek: true,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
sessionId: response.playSessionId,
|
sessionId: response.playSessionId,
|
||||||
mediaSourceId: item.id,
|
mediaSourceId: item.id,
|
||||||
|
@ -495,7 +501,8 @@ extension VideoPlayerViewModel {
|
||||||
playSessionId: response.playSessionId,
|
playSessionId: response.playSessionId,
|
||||||
repeatMode: .repeatNone,
|
repeatMode: .repeatNone,
|
||||||
nowPlayingQueue: nil,
|
nowPlayingQueue: nil,
|
||||||
playlistItemId: "playlistItem0")
|
playlistItemId: "playlistItem0"
|
||||||
|
)
|
||||||
|
|
||||||
PlaystateAPI.reportPlaybackStart(reportPlaybackStartRequest: reportPlaybackStartRequest)
|
PlaystateAPI.reportPlaybackStart(reportPlaybackStartRequest: reportPlaybackStartRequest)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
|
@ -511,7 +518,8 @@ extension VideoPlayerViewModel {
|
||||||
func sendPauseReport(paused: Bool) {
|
func sendPauseReport(paused: Bool) {
|
||||||
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
|
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
|
||||||
|
|
||||||
let reportPlaybackStartRequest = ReportPlaybackStartRequest(canSeek: true,
|
let reportPlaybackStartRequest = ReportPlaybackStartRequest(
|
||||||
|
canSeek: true,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
sessionId: response.playSessionId,
|
sessionId: response.playSessionId,
|
||||||
mediaSourceId: item.id,
|
mediaSourceId: item.id,
|
||||||
|
@ -529,7 +537,8 @@ extension VideoPlayerViewModel {
|
||||||
playSessionId: response.playSessionId,
|
playSessionId: response.playSessionId,
|
||||||
repeatMode: .repeatNone,
|
repeatMode: .repeatNone,
|
||||||
nowPlayingQueue: nil,
|
nowPlayingQueue: nil,
|
||||||
playlistItemId: "playlistItem0")
|
playlistItemId: "playlistItem0"
|
||||||
|
)
|
||||||
|
|
||||||
PlaystateAPI.reportPlaybackStart(reportPlaybackStartRequest: reportPlaybackStartRequest)
|
PlaystateAPI.reportPlaybackStart(reportPlaybackStartRequest: reportPlaybackStartRequest)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
|
@ -545,7 +554,8 @@ extension VideoPlayerViewModel {
|
||||||
func sendProgressReport() {
|
func sendProgressReport() {
|
||||||
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
|
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
|
||||||
|
|
||||||
let progressInfo = ReportPlaybackProgressRequest(canSeek: true,
|
let progressInfo = ReportPlaybackProgressRequest(
|
||||||
|
canSeek: true,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
sessionId: response.playSessionId,
|
sessionId: response.playSessionId,
|
||||||
mediaSourceId: item.id,
|
mediaSourceId: item.id,
|
||||||
|
@ -563,7 +573,8 @@ extension VideoPlayerViewModel {
|
||||||
playSessionId: response.playSessionId,
|
playSessionId: response.playSessionId,
|
||||||
repeatMode: .repeatNone,
|
repeatMode: .repeatNone,
|
||||||
nowPlayingQueue: nil,
|
nowPlayingQueue: nil,
|
||||||
playlistItemId: "playlistItem0")
|
playlistItemId: "playlistItem0"
|
||||||
|
)
|
||||||
|
|
||||||
lastProgressReport = progressInfo
|
lastProgressReport = progressInfo
|
||||||
|
|
||||||
|
@ -588,7 +599,8 @@ extension VideoPlayerViewModel {
|
||||||
// MARK: sendStopReport
|
// MARK: sendStopReport
|
||||||
|
|
||||||
func sendStopReport() {
|
func sendStopReport() {
|
||||||
let reportPlaybackStoppedRequest = ReportPlaybackStoppedRequest(itemId: item.id,
|
let reportPlaybackStoppedRequest = ReportPlaybackStoppedRequest(
|
||||||
|
itemId: item.id,
|
||||||
sessionId: response.playSessionId,
|
sessionId: response.playSessionId,
|
||||||
mediaSourceId: item.id,
|
mediaSourceId: item.id,
|
||||||
positionTicks: currentSecondTicks,
|
positionTicks: currentSecondTicks,
|
||||||
|
@ -597,7 +609,8 @@ extension VideoPlayerViewModel {
|
||||||
failed: nil,
|
failed: nil,
|
||||||
nextMediaType: nil,
|
nextMediaType: nil,
|
||||||
playlistItemId: "playlistItem0",
|
playlistItemId: "playlistItem0",
|
||||||
nowPlayingQueue: nil)
|
nowPlayingQueue: nil
|
||||||
|
)
|
||||||
|
|
||||||
PlaystateAPI.reportPlaybackStopped(reportPlaybackStoppedRequest: reportPlaybackStoppedRequest)
|
PlaystateAPI.reportPlaybackStopped(reportPlaybackStoppedRequest: reportPlaybackStoppedRequest)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
|
|
|
@ -40,7 +40,9 @@ class ViewModel: ObservableObject {
|
||||||
networkError = .URLError(response: errorResponse, displayMessage: displayMessage)
|
networkError = .URLError(response: errorResponse, displayMessage: displayMessage)
|
||||||
// Use the errorResponse description for debugging, rather than the user-facing friendly description which may not be implemented
|
// Use the errorResponse description for debugging, rather than the user-facing friendly description which may not be implemented
|
||||||
LogManager.log
|
LogManager.log
|
||||||
.error("Request failed: URL request failed with error \(networkError.errorMessage.code): \(errorResponse.localizedDescription)")
|
.error(
|
||||||
|
"Request failed: URL request failed with error \(networkError.errorMessage.code): \(errorResponse.localizedDescription)"
|
||||||
|
)
|
||||||
case .error(-2, _, _, _):
|
case .error(-2, _, _, _):
|
||||||
networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage)
|
networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage)
|
||||||
LogManager.log
|
LogManager.log
|
||||||
|
@ -49,23 +51,29 @@ class ViewModel: ObservableObject {
|
||||||
networkError = .JellyfinError(response: errorResponse, displayMessage: displayMessage)
|
networkError = .JellyfinError(response: errorResponse, displayMessage: displayMessage)
|
||||||
// Able to use user-facing friendly description here since just HTTP status codes
|
// Able to use user-facing friendly description here since just HTTP status codes
|
||||||
LogManager.log
|
LogManager.log
|
||||||
.error("Request failed: \(networkError.errorMessage.code) - \(networkError.errorMessage.title): \(networkError.errorMessage.message)\n\(error.localizedDescription)")
|
.error(
|
||||||
|
"Request failed: \(networkError.errorMessage.code) - \(networkError.errorMessage.title): \(networkError.errorMessage.message)\n\(error.localizedDescription)"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.errorMessage = networkError.errorMessage
|
self.errorMessage = networkError.errorMessage
|
||||||
|
|
||||||
case is SwiftfinStore.Error:
|
case is SwiftfinStore.Error:
|
||||||
let swiftfinError = error as! SwiftfinStore.Error
|
let swiftfinError = error as! SwiftfinStore.Error
|
||||||
let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
|
let errorMessage = ErrorMessage(
|
||||||
|
code: ErrorMessage.noShowErrorCode,
|
||||||
title: swiftfinError.title,
|
title: swiftfinError.title,
|
||||||
message: swiftfinError.errorDescription ?? "")
|
message: swiftfinError.errorDescription ?? ""
|
||||||
|
)
|
||||||
self.errorMessage = errorMessage
|
self.errorMessage = errorMessage
|
||||||
LogManager.log.error("Request failed: \(swiftfinError.errorDescription ?? "")")
|
LogManager.log.error("Request failed: \(swiftfinError.errorDescription ?? "")")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
|
let genericErrorMessage = ErrorMessage(
|
||||||
|
code: ErrorMessage.noShowErrorCode,
|
||||||
title: "Generic Error",
|
title: "Generic Error",
|
||||||
message: error.localizedDescription)
|
message: error.localizedDescription
|
||||||
|
)
|
||||||
self.errorMessage = genericErrorMessage
|
self.errorMessage = genericErrorMessage
|
||||||
LogManager.log.error("Request failed: Generic error - \(error.localizedDescription)")
|
LogManager.log.error("Request failed: Generic error - \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,9 +65,11 @@ struct MultiSelector<Selectable: Hashable>: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func multiSelectionView() -> some View {
|
private func multiSelectionView() -> some View {
|
||||||
MultiSelectionView(options: options,
|
MultiSelectionView(
|
||||||
|
options: options,
|
||||||
optionToString: optionToString,
|
optionToString: optionToString,
|
||||||
label: self.label,
|
label: self.label,
|
||||||
selected: selected)
|
selected: selected
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,13 @@ struct ParallaxHeaderScrollView<Header: View, StaticOverlayView: View, Content:
|
||||||
var headerHeight: CGFloat
|
var headerHeight: CGFloat
|
||||||
var content: () -> Content
|
var content: () -> Content
|
||||||
|
|
||||||
init(header: Header,
|
init(
|
||||||
|
header: Header,
|
||||||
staticOverlayView: StaticOverlayView,
|
staticOverlayView: StaticOverlayView,
|
||||||
overlayAlignment: Alignment = .center,
|
overlayAlignment: Alignment = .center,
|
||||||
headerHeight: CGFloat,
|
headerHeight: CGFloat,
|
||||||
content: @escaping () -> Content)
|
content: @escaping () -> Content
|
||||||
{
|
) {
|
||||||
self.header = header
|
self.header = header
|
||||||
self.staticOverlayView = staticOverlayView
|
self.staticOverlayView = staticOverlayView
|
||||||
self.overlayAlignment = overlayAlignment
|
self.overlayAlignment = overlayAlignment
|
||||||
|
|
|
@ -67,9 +67,11 @@ struct SearchablePicker<Selectable: Hashable>: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func searchablePickerView() -> some View {
|
private func searchablePickerView() -> some View {
|
||||||
SearchablePickerView(options: options,
|
SearchablePickerView(
|
||||||
|
options: options,
|
||||||
optionToString: optionToString,
|
optionToString: optionToString,
|
||||||
label: label,
|
label: label,
|
||||||
selected: $selected)
|
selected: $selected
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,10 @@ struct EpisodeRowCard: View {
|
||||||
Button {
|
Button {
|
||||||
itemRouter.route(to: \.item, episode)
|
itemRouter.route(to: \.item, episode)
|
||||||
} label: {
|
} label: {
|
||||||
ImageView(episode.getBackdropImage(maxWidth: 550),
|
ImageView(
|
||||||
blurHash: episode.getBackdropImageBlurHash())
|
episode.getBackdropImage(maxWidth: 550),
|
||||||
|
blurHash: episode.getBackdropImageBlurHash()
|
||||||
|
)
|
||||||
.mask(Rectangle().frame(width: 550, height: 308))
|
.mask(Rectangle().frame(width: 550, height: 308))
|
||||||
.frame(width: 550, height: 308)
|
.frame(width: 550, height: 308)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,9 +37,11 @@ struct CinematicNextUpCardView: View {
|
||||||
.frame(width: 350, height: 210)
|
.frame(width: 350, height: 210)
|
||||||
}
|
}
|
||||||
|
|
||||||
LinearGradient(colors: [.clear, .black],
|
LinearGradient(
|
||||||
|
colors: [.clear, .black],
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom
|
||||||
|
)
|
||||||
.frame(height: 105)
|
.frame(height: 105)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
|
|
@ -38,9 +38,11 @@ struct CinematicResumeCardView: View {
|
||||||
.frame(width: 350, height: 210)
|
.frame(width: 350, height: 210)
|
||||||
}
|
}
|
||||||
|
|
||||||
LinearGradient(colors: [.clear, .black],
|
LinearGradient(
|
||||||
|
colors: [.clear, .black],
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom
|
||||||
|
)
|
||||||
.frame(height: 105)
|
.frame(height: 105)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
|
|
@ -56,13 +56,15 @@ struct HomeCinematicView: View {
|
||||||
CinematicBackgroundView(viewModel: backgroundViewModel)
|
CinematicBackgroundView(viewModel: backgroundViewModel)
|
||||||
.frame(height: UIScreen.main.bounds.height - 10)
|
.frame(height: UIScreen.main.bounds.height - 10)
|
||||||
|
|
||||||
LinearGradient(stops: [
|
LinearGradient(
|
||||||
|
stops: [
|
||||||
.init(color: .clear, location: 0.5),
|
.init(color: .clear, location: 0.5),
|
||||||
.init(color: .black.opacity(0.6), location: 0.7),
|
.init(color: .black.opacity(0.6), location: 0.7),
|
||||||
.init(color: .black, location: 1),
|
.init(color: .black, location: 1),
|
||||||
],
|
],
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom
|
||||||
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
|
|
@ -41,8 +41,13 @@ class UICinematicBackgroundView: UIView {
|
||||||
|
|
||||||
selectDelayTimer?.invalidate()
|
selectDelayTimer?.invalidate()
|
||||||
|
|
||||||
selectDelayTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(delayTimerTimed), userInfo: imageView,
|
selectDelayTimer = Timer.scheduledTimer(
|
||||||
repeats: false)
|
timeInterval: 0.5,
|
||||||
|
target: self,
|
||||||
|
selector: #selector(delayTimerTimed),
|
||||||
|
userInfo: imageView,
|
||||||
|
repeats: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
|
|
@ -23,11 +23,19 @@ struct CutOffShadow: Shape {
|
||||||
path.move(to: tl)
|
path.move(to: tl)
|
||||||
path.addLine(to: tr)
|
path.addLine(to: tr)
|
||||||
path.addLine(to: brs)
|
path.addLine(to: brs)
|
||||||
path.addRelativeArc(center: brc, radius: 6,
|
path.addRelativeArc(
|
||||||
startAngle: Angle.degrees(0), delta: Angle.degrees(90))
|
center: brc,
|
||||||
|
radius: 6,
|
||||||
|
startAngle: Angle.degrees(0),
|
||||||
|
delta: Angle.degrees(90)
|
||||||
|
)
|
||||||
path.addLine(to: bls)
|
path.addLine(to: bls)
|
||||||
path.addRelativeArc(center: blc, radius: 6,
|
path.addRelativeArc(
|
||||||
startAngle: Angle.degrees(90), delta: Angle.degrees(90))
|
center: blc,
|
||||||
|
radius: 6,
|
||||||
|
startAngle: Angle.degrees(90),
|
||||||
|
delta: Angle.degrees(90)
|
||||||
|
)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
@ -46,13 +54,16 @@ struct LandscapeItemElement: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ImageView(item.type == .episode && !(inSeasonView ?? false) ? item.getSeriesBackdropImage(maxWidth: 445) : item
|
ImageView(
|
||||||
|
item.type == .episode && !(inSeasonView ?? false) ? item.getSeriesBackdropImage(maxWidth: 445) : item
|
||||||
.getBackdropImage(maxWidth: 445),
|
.getBackdropImage(maxWidth: 445),
|
||||||
blurHash: item.type == .episode ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash())
|
blurHash: item.type == .episode ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash()
|
||||||
|
)
|
||||||
.frame(width: 445, height: 250)
|
.frame(width: 445, height: 250)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.overlay(ZStack {
|
.overlay(
|
||||||
|
ZStack {
|
||||||
if item.userData?.played ?? false {
|
if item.userData?.played ?? false {
|
||||||
Image(systemName: "circle.fill")
|
Image(systemName: "circle.fill")
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
@ -60,13 +71,17 @@ struct LandscapeItemElement: View {
|
||||||
.foregroundColor(Color(.systemBlue))
|
.foregroundColor(Color(.systemBlue))
|
||||||
}
|
}
|
||||||
}.padding(2)
|
}.padding(2)
|
||||||
.opacity(1), alignment: .topTrailing).opacity(1)
|
.opacity(1),
|
||||||
|
alignment: .topTrailing
|
||||||
|
).opacity(1)
|
||||||
.overlay(ZStack(alignment: .leading) {
|
.overlay(ZStack(alignment: .leading) {
|
||||||
if focused && item.userData?.playedPercentage != nil {
|
if focused && item.userData?.playedPercentage != nil {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(LinearGradient(gradient: Gradient(colors: [.black, .clear]),
|
.fill(LinearGradient(
|
||||||
|
gradient: Gradient(colors: [.black, .clear]),
|
||||||
startPoint: .bottom,
|
startPoint: .bottom,
|
||||||
endPoint: .top))
|
endPoint: .top
|
||||||
|
))
|
||||||
.frame(width: 445, height: 90)
|
.frame(width: 445, height: 90)
|
||||||
.mask(CutOffShadow())
|
.mask(CutOffShadow())
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
|
@ -41,8 +41,11 @@ struct MediaPlayButtonRowView: View {
|
||||||
Button {
|
Button {
|
||||||
viewModel.updateFavoriteState()
|
viewModel.updateFavoriteState()
|
||||||
} label: {
|
} label: {
|
||||||
MediaViewActionButton(icon: "heart.fill", scrollView: $wrappedScrollView,
|
MediaViewActionButton(
|
||||||
iconColor: viewModel.isFavorited ? .red : .white)
|
icon: "heart.fill",
|
||||||
|
scrollView: $wrappedScrollView,
|
||||||
|
iconColor: viewModel.isFavorited ? .red : .white
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Text(viewModel.isFavorited ? "Unfavorite" : "Favorite")
|
Text(viewModel.isFavorited ? "Unfavorite" : "Favorite")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|
|
@ -21,13 +21,16 @@ struct PortraitItemElement: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ImageView(item.type == .episode ? item.getSeriesPrimaryImage(maxWidth: 200) : item.getPrimaryImage(maxWidth: 200),
|
ImageView(
|
||||||
blurHash: item.type == .episode ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash())
|
item.type == .episode ? item.getSeriesPrimaryImage(maxWidth: 200) : item.getPrimaryImage(maxWidth: 200),
|
||||||
|
blurHash: item.type == .episode ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash()
|
||||||
|
)
|
||||||
.frame(width: 200, height: 300)
|
.frame(width: 200, height: 300)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: focused ? 10.0 : 0)
|
.shadow(radius: focused ? 10.0 : 0)
|
||||||
.shadow(radius: focused ? 10.0 : 0)
|
.shadow(radius: focused ? 10.0 : 0)
|
||||||
.overlay(ZStack {
|
.overlay(
|
||||||
|
ZStack {
|
||||||
if item.userData?.isFavorite ?? false {
|
if item.userData?.isFavorite ?? false {
|
||||||
Image(systemName: "circle.fill")
|
Image(systemName: "circle.fill")
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
@ -38,8 +41,11 @@ struct PortraitItemElement: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(2)
|
.padding(2)
|
||||||
.opacity(1), alignment: .bottomLeading)
|
.opacity(1),
|
||||||
.overlay(ZStack {
|
alignment: .bottomLeading
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
ZStack {
|
||||||
if item.userData?.played ?? false {
|
if item.userData?.played ?? false {
|
||||||
Image(systemName: "circle.fill")
|
Image(systemName: "circle.fill")
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
@ -55,7 +61,9 @@ struct PortraitItemElement: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.padding(2)
|
}.padding(2)
|
||||||
.opacity(1), alignment: .topTrailing).opacity(1)
|
.opacity(1),
|
||||||
|
alignment: .topTrailing
|
||||||
|
).opacity(1)
|
||||||
Text(item.title)
|
Text(item.title)
|
||||||
.frame(width: 200, height: 30, alignment: .center)
|
.frame(width: 200, height: 30, alignment: .center)
|
||||||
if item.type == .movie || item.type == .series {
|
if item.type == .movie || item.type == .series {
|
||||||
|
|
|
@ -19,11 +19,12 @@ struct PortraitItemsRowView: View {
|
||||||
let showItemTitles: Bool
|
let showItemTitles: Bool
|
||||||
let selectedAction: (BaseItemDto) -> Void
|
let selectedAction: (BaseItemDto) -> Void
|
||||||
|
|
||||||
init(rowTitle: String,
|
init(
|
||||||
|
rowTitle: String,
|
||||||
items: [BaseItemDto],
|
items: [BaseItemDto],
|
||||||
showItemTitles: Bool = true,
|
showItemTitles: Bool = true,
|
||||||
selectedAction: @escaping (BaseItemDto) -> Void)
|
selectedAction: @escaping (BaseItemDto) -> Void
|
||||||
{
|
) {
|
||||||
self.rowTitle = rowTitle
|
self.rowTitle = rowTitle
|
||||||
self.items = items
|
self.items = items
|
||||||
self.showItemTitles = showItemTitles
|
self.showItemTitles = showItemTitles
|
||||||
|
|
|
@ -20,7 +20,11 @@ struct PublicUserButton: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if publicUser.primaryImageTag != nil {
|
if publicUser.primaryImageTag != nil {
|
||||||
ImageView(URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!)
|
ImageView(
|
||||||
|
URL(
|
||||||
|
string: "\(SessionManager.main.currentLogin.server.currentURI)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)"
|
||||||
|
)!
|
||||||
|
)
|
||||||
.frame(width: 250, height: 250)
|
.frame(width: 250, height: 250)
|
||||||
.cornerRadius(125.0)
|
.cornerRadius(125.0)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -76,9 +76,11 @@ struct ConnectToServerView: View {
|
||||||
.headerProminence(.increased)
|
.headerProminence(.increased)
|
||||||
}
|
}
|
||||||
.alert(item: $viewModel.errorMessage) { _ in
|
.alert(item: $viewModel.errorMessage) { _ in
|
||||||
Alert(title: Text(viewModel.alertTitle),
|
Alert(
|
||||||
|
title: Text(viewModel.alertTitle),
|
||||||
message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
|
message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
|
||||||
dismissButton: .cancel())
|
dismissButton: .cancel()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.navigationTitle(L10n.connect)
|
.navigationTitle(L10n.connect)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,9 +45,11 @@ struct ContinueWatchingCard: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background {
|
.background {
|
||||||
LinearGradient(colors: [.clear, .black.opacity(0.5), .black.opacity(0.7)],
|
LinearGradient(
|
||||||
|
colors: [.clear, .black.opacity(0.5), .black.opacity(0.7)],
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom
|
||||||
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,26 +32,32 @@ struct HomeView: View {
|
||||||
LazyVStack(alignment: .leading) {
|
LazyVStack(alignment: .leading) {
|
||||||
|
|
||||||
if viewModel.resumeItems.isEmpty {
|
if viewModel.resumeItems.isEmpty {
|
||||||
HomeCinematicView(viewModel: viewModel,
|
HomeCinematicView(
|
||||||
|
viewModel: viewModel,
|
||||||
items: viewModel.latestAddedItems.map { .init(item: $0, type: .plain) },
|
items: viewModel.latestAddedItems.map { .init(item: $0, type: .plain) },
|
||||||
forcedItemSubtitle: L10n.recentlyAdded)
|
forcedItemSubtitle: L10n.recentlyAdded
|
||||||
|
)
|
||||||
|
|
||||||
if !viewModel.nextUpItems.isEmpty {
|
if !viewModel.nextUpItems.isEmpty {
|
||||||
NextUpView(items: viewModel.nextUpItems)
|
NextUpView(items: viewModel.nextUpItems)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HomeCinematicView(viewModel: viewModel,
|
HomeCinematicView(
|
||||||
items: viewModel.resumeItems.map { .init(item: $0, type: .resume) })
|
viewModel: viewModel,
|
||||||
|
items: viewModel.resumeItems.map { .init(item: $0, type: .resume) }
|
||||||
|
)
|
||||||
|
|
||||||
if !viewModel.nextUpItems.isEmpty {
|
if !viewModel.nextUpItems.isEmpty {
|
||||||
NextUpView(items: viewModel.nextUpItems)
|
NextUpView(items: viewModel.nextUpItems)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
}
|
}
|
||||||
|
|
||||||
PortraitItemsRowView(rowTitle: L10n.recentlyAdded,
|
PortraitItemsRowView(
|
||||||
|
rowTitle: L10n.recentlyAdded,
|
||||||
items: viewModel.latestAddedItems,
|
items: viewModel.latestAddedItems,
|
||||||
showItemTitles: showPosterLabels) { item in
|
showItemTitles: showPosterLabels
|
||||||
|
) { item in
|
||||||
homeRouter.route(to: \.modalItem, item)
|
homeRouter.route(to: \.modalItem, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,17 +24,21 @@ struct CinematicCollectionItemView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
||||||
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920),
|
ImageView(
|
||||||
blurHash: viewModel.item.getBackdropImageBlurHash())
|
viewModel.item.getBackdropImage(maxWidth: 1920),
|
||||||
|
blurHash: viewModel.item.getBackdropImageBlurHash()
|
||||||
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|
||||||
CinematicItemViewTopRow(viewModel: viewModel,
|
CinematicItemViewTopRow(
|
||||||
|
viewModel: viewModel,
|
||||||
wrappedScrollView: wrappedScrollView,
|
wrappedScrollView: wrappedScrollView,
|
||||||
title: viewModel.item.name ?? "",
|
title: viewModel.item.name ?? "",
|
||||||
showDetails: false)
|
showDetails: false
|
||||||
|
)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
.frame(height: UIScreen.main.bounds.height - 10)
|
.frame(height: UIScreen.main.bounds.height - 10)
|
||||||
|
|
||||||
|
@ -46,15 +50,19 @@ struct CinematicCollectionItemView: View {
|
||||||
|
|
||||||
CinematicItemAboutView(viewModel: viewModel)
|
CinematicItemAboutView(viewModel: viewModel)
|
||||||
|
|
||||||
PortraitItemsRowView(rowTitle: L10n.items,
|
PortraitItemsRowView(
|
||||||
items: viewModel.collectionItems) { item in
|
rowTitle: L10n.items,
|
||||||
|
items: viewModel.collectionItems
|
||||||
|
) { item in
|
||||||
itemRouter.route(to: \.item, item)
|
itemRouter.route(to: \.item, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !viewModel.similarItems.isEmpty {
|
if !viewModel.similarItems.isEmpty {
|
||||||
PortraitItemsRowView(rowTitle: L10n.recommended,
|
PortraitItemsRowView(
|
||||||
|
rowTitle: L10n.recommended,
|
||||||
items: viewModel.similarItems,
|
items: viewModel.similarItems,
|
||||||
showItemTitles: showPosterLabels) { item in
|
showItemTitles: showPosterLabels
|
||||||
|
) { item in
|
||||||
itemRouter.route(to: \.item, item)
|
itemRouter.route(to: \.item, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,18 +32,22 @@ struct CinematicEpisodeItemView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
||||||
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920),
|
ImageView(
|
||||||
blurHash: viewModel.item.getBackdropImageBlurHash())
|
viewModel.item.getBackdropImage(maxWidth: 1920),
|
||||||
|
blurHash: viewModel.item.getBackdropImageBlurHash()
|
||||||
|
)
|
||||||
.frame(height: UIScreen.main.bounds.height - 10)
|
.frame(height: UIScreen.main.bounds.height - 10)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|
||||||
CinematicItemViewTopRow(viewModel: viewModel,
|
CinematicItemViewTopRow(
|
||||||
|
viewModel: viewModel,
|
||||||
wrappedScrollView: wrappedScrollView,
|
wrappedScrollView: wrappedScrollView,
|
||||||
title: viewModel.item.name ?? "",
|
title: viewModel.item.name ?? "",
|
||||||
subtitle: generateSubtitle())
|
subtitle: generateSubtitle()
|
||||||
|
)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
.frame(height: UIScreen.main.bounds.height - 10)
|
.frame(height: UIScreen.main.bounds.height - 10)
|
||||||
|
|
||||||
|
@ -60,16 +64,20 @@ struct CinematicEpisodeItemView: View {
|
||||||
.focusSection()
|
.focusSection()
|
||||||
|
|
||||||
if let seriesItem = viewModel.series {
|
if let seriesItem = viewModel.series {
|
||||||
PortraitItemsRowView(rowTitle: L10n.series,
|
PortraitItemsRowView(
|
||||||
items: [seriesItem]) { seriesItem in
|
rowTitle: L10n.series,
|
||||||
|
items: [seriesItem]
|
||||||
|
) { seriesItem in
|
||||||
itemRouter.route(to: \.item, seriesItem)
|
itemRouter.route(to: \.item, seriesItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !viewModel.similarItems.isEmpty {
|
if !viewModel.similarItems.isEmpty {
|
||||||
PortraitItemsRowView(rowTitle: L10n.recommended,
|
PortraitItemsRowView(
|
||||||
|
rowTitle: L10n.recommended,
|
||||||
items: viewModel.similarItems,
|
items: viewModel.similarItems,
|
||||||
showItemTitles: showPosterLabels) { item in
|
showItemTitles: showPosterLabels
|
||||||
|
) { item in
|
||||||
itemRouter.route(to: \.item, item)
|
itemRouter.route(to: \.item, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,13 @@ struct CinematicItemViewTopRow: View {
|
||||||
private var playButtonText: String = ""
|
private var playButtonText: String = ""
|
||||||
let showDetails: Bool
|
let showDetails: Bool
|
||||||
|
|
||||||
init(viewModel: ItemViewModel,
|
init(
|
||||||
|
viewModel: ItemViewModel,
|
||||||
wrappedScrollView: UIScrollView? = nil,
|
wrappedScrollView: UIScrollView? = nil,
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String? = nil,
|
subtitle: String? = nil,
|
||||||
showDetails: Bool = true)
|
showDetails: Bool = true
|
||||||
{
|
) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.wrappedScrollView = wrappedScrollView
|
self.wrappedScrollView = wrappedScrollView
|
||||||
self.title = title
|
self.title = title
|
||||||
|
@ -43,9 +44,11 @@ struct CinematicItemViewTopRow: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom
|
||||||
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.frame(height: 210)
|
.frame(height: 210)
|
||||||
|
|
||||||
|
@ -138,8 +141,10 @@ struct CinematicItemViewTopRow: View {
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
.overlay(
|
||||||
.stroke(Color.secondary, lineWidth: 1))
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.stroke(Color.secondary, lineWidth: 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.item.unaired {
|
if viewModel.item.unaired {
|
||||||
|
|
|
@ -24,17 +24,21 @@ struct CinematicMovieItemView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
||||||
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920),
|
ImageView(
|
||||||
blurHash: viewModel.item.getBackdropImageBlurHash())
|
viewModel.item.getBackdropImage(maxWidth: 1920),
|
||||||
|
blurHash: viewModel.item.getBackdropImageBlurHash()
|
||||||
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|
||||||
CinematicItemViewTopRow(viewModel: viewModel,
|
CinematicItemViewTopRow(
|
||||||
|
viewModel: viewModel,
|
||||||
wrappedScrollView: wrappedScrollView,
|
wrappedScrollView: wrappedScrollView,
|
||||||
title: viewModel.item.name ?? "",
|
title: viewModel.item.name ?? "",
|
||||||
subtitle: nil)
|
subtitle: nil
|
||||||
|
)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
.frame(height: UIScreen.main.bounds.height - 10)
|
.frame(height: UIScreen.main.bounds.height - 10)
|
||||||
|
|
||||||
|
@ -47,9 +51,11 @@ struct CinematicMovieItemView: View {
|
||||||
CinematicItemAboutView(viewModel: viewModel)
|
CinematicItemAboutView(viewModel: viewModel)
|
||||||
|
|
||||||
if !viewModel.similarItems.isEmpty {
|
if !viewModel.similarItems.isEmpty {
|
||||||
PortraitItemsRowView(rowTitle: L10n.recommended,
|
PortraitItemsRowView(
|
||||||
|
rowTitle: L10n.recommended,
|
||||||
items: viewModel.similarItems,
|
items: viewModel.similarItems,
|
||||||
showItemTitles: showPosterLabels) { item in
|
showItemTitles: showPosterLabels
|
||||||
|
) { item in
|
||||||
itemRouter.route(to: \.item, item)
|
itemRouter.route(to: \.item, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,16 +30,20 @@ struct CinematicSeasonItemView: View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|
||||||
if let seriesItem = viewModel.seriesItem {
|
if let seriesItem = viewModel.seriesItem {
|
||||||
CinematicItemViewTopRow(viewModel: viewModel,
|
CinematicItemViewTopRow(
|
||||||
|
viewModel: viewModel,
|
||||||
wrappedScrollView: wrappedScrollView,
|
wrappedScrollView: wrappedScrollView,
|
||||||
title: viewModel.item.name ?? "",
|
title: viewModel.item.name ?? "",
|
||||||
subtitle: seriesItem.name)
|
subtitle: seriesItem.name
|
||||||
|
)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
.frame(height: UIScreen.main.bounds.height - 10)
|
.frame(height: UIScreen.main.bounds.height - 10)
|
||||||
} else {
|
} else {
|
||||||
CinematicItemViewTopRow(viewModel: viewModel,
|
CinematicItemViewTopRow(
|
||||||
|
viewModel: viewModel,
|
||||||
wrappedScrollView: wrappedScrollView,
|
wrappedScrollView: wrappedScrollView,
|
||||||
title: viewModel.item.name ?? "")
|
title: viewModel.item.name ?? ""
|
||||||
|
)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
.frame(height: UIScreen.main.bounds.height - 10)
|
.frame(height: UIScreen.main.bounds.height - 10)
|
||||||
}
|
}
|
||||||
|
@ -57,16 +61,20 @@ struct CinematicSeasonItemView: View {
|
||||||
.focusSection()
|
.focusSection()
|
||||||
|
|
||||||
if let seriesItem = viewModel.seriesItem {
|
if let seriesItem = viewModel.seriesItem {
|
||||||
PortraitItemsRowView(rowTitle: L10n.series,
|
PortraitItemsRowView(
|
||||||
items: [seriesItem]) { seriesItem in
|
rowTitle: L10n.series,
|
||||||
|
items: [seriesItem]
|
||||||
|
) { seriesItem in
|
||||||
itemRouter.route(to: \.item, seriesItem)
|
itemRouter.route(to: \.item, seriesItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !viewModel.similarItems.isEmpty {
|
if !viewModel.similarItems.isEmpty {
|
||||||
PortraitItemsRowView(rowTitle: L10n.recommended,
|
PortraitItemsRowView(
|
||||||
|
rowTitle: L10n.recommended,
|
||||||
items: viewModel.similarItems,
|
items: viewModel.similarItems,
|
||||||
showItemTitles: showPosterLabels) { item in
|
showItemTitles: showPosterLabels
|
||||||
|
) { item in
|
||||||
itemRouter.route(to: \.item, item)
|
itemRouter.route(to: \.item, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,10 +29,12 @@ struct CinematicSeriesItemView: View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|
||||||
CinematicItemViewTopRow(viewModel: viewModel,
|
CinematicItemViewTopRow(
|
||||||
|
viewModel: viewModel,
|
||||||
wrappedScrollView: wrappedScrollView,
|
wrappedScrollView: wrappedScrollView,
|
||||||
title: viewModel.item.name ?? "",
|
title: viewModel.item.name ?? "",
|
||||||
subtitle: nil)
|
subtitle: nil
|
||||||
|
)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
.frame(height: UIScreen.main.bounds.height - 10)
|
.frame(height: UIScreen.main.bounds.height - 10)
|
||||||
|
|
||||||
|
@ -45,16 +47,20 @@ struct CinematicSeriesItemView: View {
|
||||||
|
|
||||||
CinematicItemAboutView(viewModel: viewModel)
|
CinematicItemAboutView(viewModel: viewModel)
|
||||||
|
|
||||||
PortraitItemsRowView(rowTitle: L10n.seasons,
|
PortraitItemsRowView(
|
||||||
|
rowTitle: L10n.seasons,
|
||||||
items: viewModel.seasons,
|
items: viewModel.seasons,
|
||||||
showItemTitles: showPosterLabels) { season in
|
showItemTitles: showPosterLabels
|
||||||
|
) { season in
|
||||||
itemRouter.route(to: \.item, season)
|
itemRouter.route(to: \.item, season)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !viewModel.similarItems.isEmpty {
|
if !viewModel.similarItems.isEmpty {
|
||||||
PortraitItemsRowView(rowTitle: L10n.recommended,
|
PortraitItemsRowView(
|
||||||
|
rowTitle: L10n.recommended,
|
||||||
items: viewModel.similarItems,
|
items: viewModel.similarItems,
|
||||||
showItemTitles: showPosterLabels) { item in
|
showItemTitles: showPosterLabels
|
||||||
|
) { item in
|
||||||
itemRouter.route(to: \.item, item)
|
itemRouter.route(to: \.item, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,8 +76,10 @@ struct EpisodeItemView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
.overlay(
|
||||||
.stroke(Color.secondary, lineWidth: 1))
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.stroke(Color.secondary, lineWidth: 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}.padding(.top, -15)
|
}.padding(.top, -15)
|
||||||
|
|
|
@ -78,8 +78,10 @@ struct MovieItemView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
.overlay(
|
||||||
.stroke(Color.secondary, lineWidth: 1))
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.stroke(Color.secondary, lineWidth: 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,10 @@ struct SeasonItemView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
.overlay(
|
||||||
.stroke(Color.secondary, lineWidth: 1))
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.stroke(Color.secondary, lineWidth: 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if viewModel.item.communityRating != nil {
|
if viewModel.item.communityRating != nil {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -81,8 +83,11 @@ struct SeasonItemView: View {
|
||||||
Button {
|
Button {
|
||||||
viewModel.updateFavoriteState()
|
viewModel.updateFavoriteState()
|
||||||
} label: {
|
} label: {
|
||||||
MediaViewActionButton(icon: "heart.fill", scrollView: $wrappedScrollView,
|
MediaViewActionButton(
|
||||||
iconColor: viewModel.isFavorited ? .red : .white)
|
icon: "heart.fill",
|
||||||
|
scrollView: $wrappedScrollView,
|
||||||
|
iconColor: viewModel.isFavorited ? .red : .white
|
||||||
|
)
|
||||||
}.prefersDefaultFocus(in: namespace)
|
}.prefersDefaultFocus(in: namespace)
|
||||||
Text(viewModel.isFavorited ? "Unfavorite" : "Favorite")
|
Text(viewModel.isFavorited ? "Unfavorite" : "Favorite")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
@ -92,8 +97,11 @@ struct SeasonItemView: View {
|
||||||
Button {
|
Button {
|
||||||
viewModel.updateWatchState()
|
viewModel.updateWatchState()
|
||||||
} label: {
|
} label: {
|
||||||
MediaViewActionButton(icon: "eye.fill", scrollView: $wrappedScrollView,
|
MediaViewActionButton(
|
||||||
iconColor: viewModel.isWatched ? .red : .white)
|
icon: "eye.fill",
|
||||||
|
scrollView: $wrappedScrollView,
|
||||||
|
iconColor: viewModel.isWatched ? .red : .white
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Text(viewModel.isWatched ? "Unwatch" : "Mark Watched")
|
Text(viewModel.isWatched ? "Unwatch" : "Mark Watched")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|
|
@ -69,8 +69,10 @@ struct SeriesItemView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
.overlay(
|
||||||
.stroke(Color.secondary, lineWidth: 1))
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.stroke(Color.secondary, lineWidth: 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if viewModel.item.communityRating != nil {
|
if viewModel.item.communityRating != nil {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
|
@ -49,10 +49,17 @@ struct LatestMediaView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
homeRouter.route(to: \.library, (viewModel: .init(parentID: viewModel.library.id!,
|
homeRouter.route(to: \.library, (
|
||||||
filters: LibraryFilters(filters: [], sortOrder: [.descending],
|
viewModel: .init(
|
||||||
sortBy: [.dateAdded])),
|
parentID: viewModel.library.id!,
|
||||||
title: viewModel.library.name ?? ""))
|
filters: LibraryFilters(
|
||||||
|
filters: [],
|
||||||
|
sortOrder: [.descending],
|
||||||
|
sortBy: [.dateAdded]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
title: viewModel.library.name ?? ""
|
||||||
|
))
|
||||||
} label: {
|
} label: {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color(UIColor.darkGray)
|
Color(UIColor.darkGray)
|
||||||
|
|
|
@ -35,22 +35,28 @@ struct LibraryFilterView: View {
|
||||||
} else {
|
} else {
|
||||||
Form {
|
Form {
|
||||||
if viewModel.enabledFilterType.contains(.genre) {
|
if viewModel.enabledFilterType.contains(.genre) {
|
||||||
MultiSelector(label: L10n.genres,
|
MultiSelector(
|
||||||
|
label: L10n.genres,
|
||||||
options: viewModel.possibleGenres,
|
options: viewModel.possibleGenres,
|
||||||
optionToString: { $0.name ?? "" },
|
optionToString: { $0.name ?? "" },
|
||||||
selected: $viewModel.modifiedFilters.withGenres)
|
selected: $viewModel.modifiedFilters.withGenres
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if viewModel.enabledFilterType.contains(.filter) {
|
if viewModel.enabledFilterType.contains(.filter) {
|
||||||
MultiSelector(label: L10n.filters,
|
MultiSelector(
|
||||||
|
label: L10n.filters,
|
||||||
options: viewModel.possibleItemFilters,
|
options: viewModel.possibleItemFilters,
|
||||||
optionToString: { $0.localized },
|
optionToString: { $0.localized },
|
||||||
selected: $viewModel.modifiedFilters.filters)
|
selected: $viewModel.modifiedFilters.filters
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if viewModel.enabledFilterType.contains(.tag) {
|
if viewModel.enabledFilterType.contains(.tag) {
|
||||||
MultiSelector(label: L10n.tags,
|
MultiSelector(
|
||||||
|
label: L10n.tags,
|
||||||
options: viewModel.possibleTags,
|
options: viewModel.possibleTags,
|
||||||
optionToString: { $0 },
|
optionToString: { $0 },
|
||||||
selected: $viewModel.modifiedFilters.tags)
|
selected: $viewModel.modifiedFilters.tags
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if viewModel.enabledFilterType.contains(.sortBy) {
|
if viewModel.enabledFilterType.contains(.sortBy) {
|
||||||
Picker(selection: $viewModel.selectedSortBy, label: L10n.sortBy.text) {
|
Picker(selection: $viewModel.selectedSortBy, label: L10n.sortBy.text) {
|
||||||
|
|
|
@ -46,8 +46,10 @@ struct LibraryListView: View {
|
||||||
if itemType == .liveTV {
|
if itemType == .liveTV {
|
||||||
self.mainCoordinator.root(\.liveTV)
|
self.mainCoordinator.root(\.liveTV)
|
||||||
} else {
|
} else {
|
||||||
self.libraryListRouter.route(to: \.library,
|
self.libraryListRouter.route(
|
||||||
(viewModel: LibraryViewModel(parentID: library.id), title: library.name ?? ""))
|
to: \.library,
|
||||||
|
(viewModel: LibraryViewModel(parentID: library.id), title: library.name ?? "")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
label: {
|
label: {
|
||||||
|
|
|
@ -31,20 +31,30 @@ struct LibraryView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else if !viewModel.rows.isEmpty {
|
} else if !viewModel.rows.isEmpty {
|
||||||
CollectionView(rows: viewModel.rows) { _, _ in
|
CollectionView(rows: viewModel.rows) { _, _ in
|
||||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
|
let itemSize = NSCollectionLayoutSize(
|
||||||
heightDimension: .fractionalHeight(1))
|
widthDimension: .fractionalWidth(1),
|
||||||
|
heightDimension: .fractionalHeight(1)
|
||||||
|
)
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
|
||||||
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(200),
|
let groupSize = NSCollectionLayoutSize(
|
||||||
heightDimension: .absolute(300))
|
widthDimension: .absolute(200),
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
|
heightDimension: .absolute(300)
|
||||||
subitems: [item])
|
)
|
||||||
|
let group = NSCollectionLayoutGroup.horizontal(
|
||||||
|
layoutSize: groupSize,
|
||||||
|
subitems: [item]
|
||||||
|
)
|
||||||
|
|
||||||
let header =
|
let header =
|
||||||
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
|
NSCollectionLayoutBoundarySupplementaryItem(
|
||||||
heightDimension: .absolute(44)),
|
layoutSize: NSCollectionLayoutSize(
|
||||||
|
widthDimension: .fractionalWidth(1),
|
||||||
|
heightDimension: .absolute(44)
|
||||||
|
),
|
||||||
elementKind: UICollectionView.elementKindSectionHeader,
|
elementKind: UICollectionView.elementKindSectionHeader,
|
||||||
alignment: .topLeading)
|
alignment: .topLeading
|
||||||
|
)
|
||||||
|
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
|
||||||
|
|
|
@ -88,19 +88,31 @@ struct LiveTVChannelItemElement: View {
|
||||||
.foregroundColor(Color.jellyfinPurple)
|
.foregroundColor(Color.jellyfinPurple)
|
||||||
.padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0))
|
.padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0))
|
||||||
|
|
||||||
programLabel(timeText: currentProgramText.timeDisplay, titleText: currentProgramText.title,
|
programLabel(
|
||||||
color: Color("TextHighlightColor"), font: Font.system(size: 20, weight: .bold, design: .default))
|
timeText: currentProgramText.timeDisplay,
|
||||||
|
titleText: currentProgramText.title,
|
||||||
|
color: Color("TextHighlightColor"),
|
||||||
|
font: Font.system(size: 20, weight: .bold, design: .default)
|
||||||
|
)
|
||||||
if !nextProgramsText.isEmpty,
|
if !nextProgramsText.isEmpty,
|
||||||
let nextItem = nextProgramsText[0]
|
let nextItem = nextProgramsText[0]
|
||||||
{
|
{
|
||||||
programLabel(timeText: nextItem.timeDisplay, titleText: nextItem.title, color: Color.gray,
|
programLabel(
|
||||||
font: Font.system(size: 20, design: .default))
|
timeText: nextItem.timeDisplay,
|
||||||
|
titleText: nextItem.title,
|
||||||
|
color: Color.gray,
|
||||||
|
font: Font.system(size: 20, design: .default)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if nextProgramsText.count > 1,
|
if nextProgramsText.count > 1,
|
||||||
let nextItem2 = nextProgramsText[1]
|
let nextItem2 = nextProgramsText[1]
|
||||||
{
|
{
|
||||||
programLabel(timeText: nextItem2.timeDisplay, titleText: nextItem2.title, color: Color.gray,
|
programLabel(
|
||||||
font: Font.system(size: 20, design: .default))
|
timeText: nextItem2.timeDisplay,
|
||||||
|
titleText: nextItem2.title,
|
||||||
|
color: Color.gray,
|
||||||
|
font: Font.system(size: 20, design: .default)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
|
@ -128,8 +140,10 @@ struct LiveTVChannelItemElement: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay(RoundedRectangle(cornerRadius: 20)
|
.overlay(
|
||||||
.stroke(isFocused ? Color.blue : Color.clear, lineWidth: 4))
|
RoundedRectangle(cornerRadius: 20)
|
||||||
|
.stroke(isFocused ? Color.blue : Color.clear, lineWidth: 4)
|
||||||
|
)
|
||||||
.cornerRadius(20)
|
.cornerRadius(20)
|
||||||
.scaleEffect(isFocused ? 1.1 : 1)
|
.scaleEffect(isFocused ? 1.1 : 1)
|
||||||
.focusable(true)
|
.focusable(true)
|
||||||
|
|
|
@ -66,7 +66,8 @@ struct LiveTVChannelsView: View {
|
||||||
}
|
}
|
||||||
return start > currentStart
|
return start > currentStart
|
||||||
}
|
}
|
||||||
LiveTVChannelItemElement(channel: channel,
|
LiveTVChannelItemElement(
|
||||||
|
channel: channel,
|
||||||
currentProgram: item.currentProgram,
|
currentProgram: item.currentProgram,
|
||||||
currentProgramText: currentProgramDisplayText,
|
currentProgramText: currentProgramDisplayText,
|
||||||
nextProgramsText: nextProgramsDisplayText(nextItems: nextItems, timeFormatter: viewModel.timeFormatter),
|
nextProgramsText: nextProgramsDisplayText(nextItems: nextItems, timeFormatter: viewModel.timeFormatter),
|
||||||
|
@ -78,7 +79,8 @@ struct LiveTVChannelsView: View {
|
||||||
loadingAction(false)
|
loadingAction(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createGridLayout() -> NSCollectionLayoutSection {
|
private func createGridLayout() -> NSCollectionLayoutSection {
|
||||||
|
@ -86,22 +88,32 @@ struct LiveTVChannelsView: View {
|
||||||
// But it does, even with contentInset = .zero and ignoreSafeArea.
|
// But it does, even with contentInset = .zero and ignoreSafeArea.
|
||||||
let sideMargin = CGFloat(30)
|
let sideMargin = CGFloat(30)
|
||||||
let itemWidth = (UIScreen.main.bounds.width / 4) - (sideMargin * 2)
|
let itemWidth = (UIScreen.main.bounds.width / 4) - (sideMargin * 2)
|
||||||
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(itemWidth),
|
let itemSize = NSCollectionLayoutSize(
|
||||||
heightDimension: .absolute(itemWidth))
|
widthDimension: .absolute(itemWidth),
|
||||||
|
heightDimension: .absolute(itemWidth)
|
||||||
|
)
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
item.edgeSpacing = .init(leading: .fixed(8),
|
item.edgeSpacing = .init(
|
||||||
|
leading: .fixed(8),
|
||||||
top: .fixed(8),
|
top: .fixed(8),
|
||||||
trailing: .fixed(8),
|
trailing: .fixed(8),
|
||||||
bottom: .fixed(8))
|
bottom: .fixed(8)
|
||||||
|
)
|
||||||
|
|
||||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
|
let groupSize = NSCollectionLayoutSize(
|
||||||
heightDimension: .absolute(itemWidth))
|
widthDimension: .fractionalWidth(1.0),
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
|
heightDimension: .absolute(itemWidth)
|
||||||
subitems: [item])
|
)
|
||||||
group.edgeSpacing = .init(leading: .fixed(0),
|
let group = NSCollectionLayoutGroup.horizontal(
|
||||||
|
layoutSize: groupSize,
|
||||||
|
subitems: [item]
|
||||||
|
)
|
||||||
|
group.edgeSpacing = .init(
|
||||||
|
leading: .fixed(0),
|
||||||
top: .fixed(16),
|
top: .fixed(16),
|
||||||
trailing: .fixed(0),
|
trailing: .fixed(0),
|
||||||
bottom: .fixed(16))
|
bottom: .fixed(16)
|
||||||
|
)
|
||||||
group.contentInsets = .zero
|
group.contentInsets = .zero
|
||||||
|
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
|
|
@ -22,20 +22,30 @@ struct MovieLibrariesView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else if !viewModel.rows.isEmpty {
|
} else if !viewModel.rows.isEmpty {
|
||||||
CollectionView(rows: viewModel.rows) { _, _ in
|
CollectionView(rows: viewModel.rows) { _, _ in
|
||||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
|
let itemSize = NSCollectionLayoutSize(
|
||||||
heightDimension: .fractionalHeight(1))
|
widthDimension: .fractionalWidth(1),
|
||||||
|
heightDimension: .fractionalHeight(1)
|
||||||
|
)
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
|
||||||
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(200),
|
let groupSize = NSCollectionLayoutSize(
|
||||||
heightDimension: .absolute(300))
|
widthDimension: .absolute(200),
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
|
heightDimension: .absolute(300)
|
||||||
subitems: [item])
|
)
|
||||||
|
let group = NSCollectionLayoutGroup.horizontal(
|
||||||
|
layoutSize: groupSize,
|
||||||
|
subitems: [item]
|
||||||
|
)
|
||||||
|
|
||||||
let header =
|
let header =
|
||||||
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
|
NSCollectionLayoutBoundarySupplementaryItem(
|
||||||
heightDimension: .absolute(44)),
|
layoutSize: NSCollectionLayoutSize(
|
||||||
|
widthDimension: .fractionalWidth(1),
|
||||||
|
heightDimension: .absolute(44)
|
||||||
|
),
|
||||||
elementKind: UICollectionView.elementKindSectionHeader,
|
elementKind: UICollectionView.elementKindSectionHeader,
|
||||||
alignment: .topLeading)
|
alignment: .topLeading
|
||||||
|
)
|
||||||
|
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
|
||||||
|
|
|
@ -22,20 +22,30 @@ struct TVLibrariesView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else if !viewModel.rows.isEmpty {
|
} else if !viewModel.rows.isEmpty {
|
||||||
CollectionView(rows: viewModel.rows) { _, _ in
|
CollectionView(rows: viewModel.rows) { _, _ in
|
||||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
|
let itemSize = NSCollectionLayoutSize(
|
||||||
heightDimension: .fractionalHeight(1))
|
widthDimension: .fractionalWidth(1),
|
||||||
|
heightDimension: .fractionalHeight(1)
|
||||||
|
)
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
|
||||||
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(200),
|
let groupSize = NSCollectionLayoutSize(
|
||||||
heightDimension: .absolute(300))
|
widthDimension: .absolute(200),
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
|
heightDimension: .absolute(300)
|
||||||
subitems: [item])
|
)
|
||||||
|
let group = NSCollectionLayoutGroup.horizontal(
|
||||||
|
layoutSize: groupSize,
|
||||||
|
subitems: [item]
|
||||||
|
)
|
||||||
|
|
||||||
let header =
|
let header =
|
||||||
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
|
NSCollectionLayoutBoundarySupplementaryItem(
|
||||||
heightDimension: .absolute(44)),
|
layoutSize: NSCollectionLayoutSize(
|
||||||
|
widthDimension: .fractionalWidth(1),
|
||||||
|
heightDimension: .absolute(44)
|
||||||
|
),
|
||||||
elementKind: UICollectionView.elementKindSectionHeader,
|
elementKind: UICollectionView.elementKindSectionHeader,
|
||||||
alignment: .topLeading)
|
alignment: .topLeading
|
||||||
|
)
|
||||||
|
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
|
||||||
|
|
|
@ -55,9 +55,11 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(item: $viewModel.errorMessage) { _ in
|
.alert(item: $viewModel.errorMessage) { _ in
|
||||||
Alert(title: Text(viewModel.alertTitle),
|
Alert(
|
||||||
|
title: Text(viewModel.alertTitle),
|
||||||
message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
|
message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
|
||||||
dismissButton: .cancel())
|
dismissButton: .cancel()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.navigationTitle(L10n.signIn)
|
.navigationTitle(L10n.signIn)
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,12 +126,24 @@ class LiveTVPlayerViewController: UIViewController {
|
||||||
addButtonPressRecognizer(pressType: .menu, action: #selector(didPressMenu))
|
addButtonPressRecognizer(pressType: .menu, action: #selector(didPressMenu))
|
||||||
|
|
||||||
let defaultNotificationCenter = NotificationCenter.default
|
let defaultNotificationCenter = NotificationCenter.default
|
||||||
defaultNotificationCenter.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification,
|
defaultNotificationCenter.addObserver(
|
||||||
object: nil)
|
self,
|
||||||
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive),
|
selector: #selector(appWillTerminate),
|
||||||
name: UIApplication.willResignActiveNotification, object: nil)
|
name: UIApplication.willTerminateNotification,
|
||||||
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive),
|
object: nil
|
||||||
name: UIApplication.didEnterBackgroundNotification, object: nil)
|
)
|
||||||
|
defaultNotificationCenter.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appWillResignActive),
|
||||||
|
name: UIApplication.willResignActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
defaultNotificationCenter.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appWillResignActive),
|
||||||
|
name: UIApplication.didEnterBackgroundNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
@ -659,8 +671,13 @@ extension LiveTVPlayerViewController {
|
||||||
|
|
||||||
private func restartOverlayDismissTimer(interval: Double = 5) {
|
private func restartOverlayDismissTimer(interval: Double = 5) {
|
||||||
self.overlayDismissTimer?.invalidate()
|
self.overlayDismissTimer?.invalidate()
|
||||||
self.overlayDismissTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(dismissTimerFired),
|
self.overlayDismissTimer = Timer.scheduledTimer(
|
||||||
userInfo: nil, repeats: false)
|
timeInterval: interval,
|
||||||
|
target: self,
|
||||||
|
selector: #selector(dismissTimerFired),
|
||||||
|
userInfo: nil,
|
||||||
|
repeats: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
@ -679,9 +696,13 @@ extension LiveTVPlayerViewController {
|
||||||
|
|
||||||
private func restartConfirmCloseDismissTimer() {
|
private func restartConfirmCloseDismissTimer() {
|
||||||
self.confirmCloseOverlayDismissTimer?.invalidate()
|
self.confirmCloseOverlayDismissTimer?.invalidate()
|
||||||
self.confirmCloseOverlayDismissTimer = Timer.scheduledTimer(timeInterval: 5, target: self,
|
self.confirmCloseOverlayDismissTimer = Timer.scheduledTimer(
|
||||||
selector: #selector(confirmCloseTimerFired), userInfo: nil,
|
timeInterval: 5,
|
||||||
repeats: false)
|
target: self,
|
||||||
|
selector: #selector(confirmCloseTimerFired),
|
||||||
|
userInfo: nil,
|
||||||
|
repeats: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
|
|
@ -68,9 +68,10 @@ class NativePlayerViewController: AVPlayerViewController {
|
||||||
return allMetadata.compactMap { createMetadataItem(for: $0, value: $1) }
|
return allMetadata.compactMap { createMetadataItem(for: $0, value: $1) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createMetadataItem(for identifier: AVMetadataIdentifier,
|
private func createMetadataItem(
|
||||||
value: Any) -> AVMetadataItem
|
for identifier: AVMetadataIdentifier,
|
||||||
{
|
value: Any
|
||||||
|
) -> AVMetadataItem {
|
||||||
let item = AVMutableMetadataItem()
|
let item = AVMutableMetadataItem()
|
||||||
item.identifier = identifier
|
item.identifier = identifier
|
||||||
item.value = value as? NSCopying & NSObjectProtocol
|
item.value = value as? NSCopying & NSObjectProtocol
|
||||||
|
@ -105,11 +106,14 @@ class NativePlayerViewController: AVPlayerViewController {
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
player?.seek(to: CMTimeMake(value: viewModel.currentSecondTicks, timescale: 10_000_000),
|
player?.seek(
|
||||||
toleranceBefore: CMTimeMake(value: 1, timescale: 1), toleranceAfter: CMTimeMake(value: 1, timescale: 1),
|
to: CMTimeMake(value: viewModel.currentSecondTicks, timescale: 10_000_000),
|
||||||
|
toleranceBefore: CMTimeMake(value: 1, timescale: 1),
|
||||||
|
toleranceAfter: CMTimeMake(value: 1, timescale: 1),
|
||||||
completionHandler: { _ in
|
completionHandler: { _ in
|
||||||
self.play()
|
self.play()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func play() {
|
private func play() {
|
||||||
|
|
|
@ -61,9 +61,11 @@ struct SmallMediaStreamSelectionView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom
|
||||||
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.frame(height: 300)
|
.frame(height: 300)
|
||||||
|
|
||||||
|
|
|
@ -32,9 +32,11 @@ struct tvOSLiveTVOverlay: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
|
|
||||||
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom
|
||||||
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.frame(height: viewModel.subtitle == nil ? 180 : 210)
|
.frame(height: viewModel.subtitle == nil ? 180 : 210)
|
||||||
|
|
||||||
|
@ -138,7 +140,8 @@ struct tvOSLiveTVOverlay: View {
|
||||||
|
|
||||||
struct tvOSLiveTVOverlay_Previews: PreviewProvider {
|
struct tvOSLiveTVOverlay_Previews: PreviewProvider {
|
||||||
|
|
||||||
static let videoPlayerViewModel = VideoPlayerViewModel(item: BaseItemDto(),
|
static let videoPlayerViewModel = VideoPlayerViewModel(
|
||||||
|
item: BaseItemDto(),
|
||||||
title: "Glorious Purpose",
|
title: "Glorious Purpose",
|
||||||
subtitle: "Loki - S1E1",
|
subtitle: "Loki - S1E1",
|
||||||
directStreamURL: URL(string: "www.apple.com")!,
|
directStreamURL: URL(string: "www.apple.com")!,
|
||||||
|
@ -159,7 +162,8 @@ struct tvOSLiveTVOverlay_Previews: PreviewProvider {
|
||||||
shouldShowAutoPlay: true,
|
shouldShowAutoPlay: true,
|
||||||
container: "",
|
container: "",
|
||||||
filename: nil,
|
filename: nil,
|
||||||
versionName: nil)
|
versionName: nil
|
||||||
|
)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
|
@ -32,9 +32,11 @@ struct tvOSVLCOverlay: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
|
|
||||||
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]),
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom
|
||||||
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.frame(height: viewModel.subtitle == nil ? 180 : 210)
|
.frame(height: viewModel.subtitle == nil ? 180 : 210)
|
||||||
|
|
||||||
|
@ -138,7 +140,8 @@ struct tvOSVLCOverlay: View {
|
||||||
|
|
||||||
struct tvOSVLCOverlay_Previews: PreviewProvider {
|
struct tvOSVLCOverlay_Previews: PreviewProvider {
|
||||||
|
|
||||||
static let videoPlayerViewModel = VideoPlayerViewModel(item: BaseItemDto(),
|
static let videoPlayerViewModel = VideoPlayerViewModel(
|
||||||
|
item: BaseItemDto(),
|
||||||
title: "Glorious Purpose",
|
title: "Glorious Purpose",
|
||||||
subtitle: "Loki - S1E1",
|
subtitle: "Loki - S1E1",
|
||||||
directStreamURL: URL(string: "www.apple.com")!,
|
directStreamURL: URL(string: "www.apple.com")!,
|
||||||
|
@ -159,7 +162,8 @@ struct tvOSVLCOverlay_Previews: PreviewProvider {
|
||||||
shouldShowAutoPlay: true,
|
shouldShowAutoPlay: true,
|
||||||
container: "",
|
container: "",
|
||||||
filename: nil,
|
filename: nil,
|
||||||
versionName: nil)
|
versionName: nil
|
||||||
|
)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
|
@ -126,12 +126,24 @@ class VLCPlayerViewController: UIViewController {
|
||||||
addButtonPressRecognizer(pressType: .menu, action: #selector(didPressMenu))
|
addButtonPressRecognizer(pressType: .menu, action: #selector(didPressMenu))
|
||||||
|
|
||||||
let defaultNotificationCenter = NotificationCenter.default
|
let defaultNotificationCenter = NotificationCenter.default
|
||||||
defaultNotificationCenter.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification,
|
defaultNotificationCenter.addObserver(
|
||||||
object: nil)
|
self,
|
||||||
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive),
|
selector: #selector(appWillTerminate),
|
||||||
name: UIApplication.willResignActiveNotification, object: nil)
|
name: UIApplication.willTerminateNotification,
|
||||||
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive),
|
object: nil
|
||||||
name: UIApplication.didEnterBackgroundNotification, object: nil)
|
)
|
||||||
|
defaultNotificationCenter.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appWillResignActive),
|
||||||
|
name: UIApplication.willResignActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
defaultNotificationCenter.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appWillResignActive),
|
||||||
|
name: UIApplication.didEnterBackgroundNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
@ -659,8 +671,13 @@ extension VLCPlayerViewController {
|
||||||
|
|
||||||
private func restartOverlayDismissTimer(interval: Double = 5) {
|
private func restartOverlayDismissTimer(interval: Double = 5) {
|
||||||
self.overlayDismissTimer?.invalidate()
|
self.overlayDismissTimer?.invalidate()
|
||||||
self.overlayDismissTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(dismissTimerFired),
|
self.overlayDismissTimer = Timer.scheduledTimer(
|
||||||
userInfo: nil, repeats: false)
|
timeInterval: interval,
|
||||||
|
target: self,
|
||||||
|
selector: #selector(dismissTimerFired),
|
||||||
|
userInfo: nil,
|
||||||
|
repeats: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
@ -679,9 +696,13 @@ extension VLCPlayerViewController {
|
||||||
|
|
||||||
private func restartConfirmCloseDismissTimer() {
|
private func restartConfirmCloseDismissTimer() {
|
||||||
self.confirmCloseOverlayDismissTimer?.invalidate()
|
self.confirmCloseOverlayDismissTimer?.invalidate()
|
||||||
self.confirmCloseOverlayDismissTimer = Timer.scheduledTimer(timeInterval: 5, target: self,
|
self.confirmCloseOverlayDismissTimer = Timer.scheduledTimer(
|
||||||
selector: #selector(confirmCloseTimerFired), userInfo: nil,
|
timeInterval: 5,
|
||||||
repeats: false)
|
target: self,
|
||||||
|
selector: #selector(confirmCloseTimerFired),
|
||||||
|
userInfo: nil,
|
||||||
|
repeats: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
|
|
@ -331,8 +331,12 @@ public final class TvOSSlider: UIControl {
|
||||||
|
|
||||||
setUpGestures()
|
setUpGestures()
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(controllerConnected(note:)), name: .GCControllerDidConnect,
|
NotificationCenter.default.addObserver(
|
||||||
object: nil)
|
self,
|
||||||
|
selector: #selector(controllerConnected(note:)),
|
||||||
|
name: .GCControllerDidConnect,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
updateStateDependantViews()
|
updateStateDependantViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,9 +433,7 @@ public final class TvOSSlider: UIControl {
|
||||||
|
|
||||||
let threshold: Float = 0.7
|
let threshold: Float = 0.7
|
||||||
micro.reportsAbsoluteDpadValues = true
|
micro.reportsAbsoluteDpadValues = true
|
||||||
micro.dpad.valueChangedHandler = {
|
micro.dpad.valueChangedHandler = { [weak self] _, x, _ in
|
||||||
[weak self] _, x, _ in
|
|
||||||
|
|
||||||
if x < -threshold {
|
if x < -threshold {
|
||||||
self?.dPadState = .left
|
self?.dPadState = .left
|
||||||
} else if x > threshold {
|
} else if x > threshold {
|
||||||
|
@ -514,8 +516,13 @@ public final class TvOSSlider: UIControl {
|
||||||
if abs(velocity) > fineTunningVelocityThreshold {
|
if abs(velocity) > fineTunningVelocityThreshold {
|
||||||
let direction: Float = velocity > 0 ? 1 : -1
|
let direction: Float = velocity > 0 ? 1 : -1
|
||||||
deceleratingVelocity = abs(velocity) > decelerationMaxVelocity ? decelerationMaxVelocity * direction : velocity
|
deceleratingVelocity = abs(velocity) > decelerationMaxVelocity ? decelerationMaxVelocity * direction : velocity
|
||||||
deceleratingTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self,
|
deceleratingTimer = Timer.scheduledTimer(
|
||||||
selector: #selector(handleDeceleratingTimer(timer:)), userInfo: nil, repeats: true)
|
timeInterval: 0.01,
|
||||||
|
target: self,
|
||||||
|
selector: #selector(handleDeceleratingTimer(timer:)),
|
||||||
|
userInfo: nil,
|
||||||
|
repeats: true
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
viewModel.sliderIsScrubbing = false
|
viewModel.sliderIsScrubbing = false
|
||||||
stopDeceleratingTimer()
|
stopDeceleratingTimer()
|
||||||
|
|
|
@ -13,9 +13,10 @@ import UIKit
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
static var orientationLock = UIInterfaceOrientationMask.all
|
static var orientationLock = UIInterfaceOrientationMask.all
|
||||||
|
|
||||||
func application(_ application: UIApplication,
|
func application(
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
|
_ application: UIApplication,
|
||||||
{
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||||
|
) -> Bool {
|
||||||
|
|
||||||
// Lazily initialize datastack
|
// Lazily initialize datastack
|
||||||
_ = SwiftfinStore.dataStack
|
_ = SwiftfinStore.dataStack
|
||||||
|
|
|
@ -14,14 +14,16 @@ import UIKit
|
||||||
class PreferenceUIHostingController: UIHostingController<AnyView> {
|
class PreferenceUIHostingController: UIHostingController<AnyView> {
|
||||||
init<V: View>(wrappedView: V) {
|
init<V: View>(wrappedView: V) {
|
||||||
let box = Box()
|
let box = Box()
|
||||||
super.init(rootView: AnyView(wrappedView
|
super.init(rootView: AnyView(
|
||||||
|
wrappedView
|
||||||
.onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) {
|
.onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) {
|
||||||
box.value?._prefersHomeIndicatorAutoHidden = $0
|
box.value?._prefersHomeIndicatorAutoHidden = $0
|
||||||
}.onPreferenceChange(SupportedOrientationsPreferenceKey.self) {
|
}.onPreferenceChange(SupportedOrientationsPreferenceKey.self) {
|
||||||
box.value?._orientations = $0
|
box.value?._orientations = $0
|
||||||
}.onPreferenceChange(ViewPreferenceKey.self) {
|
}.onPreferenceChange(ViewPreferenceKey.self) {
|
||||||
box.value?._viewPreference = $0
|
box.value?._viewPreference = $0
|
||||||
}))
|
}
|
||||||
|
))
|
||||||
box.value = self
|
box.value = self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,9 +57,10 @@ struct DetectBottomScrollView<Content: View>: View {
|
||||||
let content: () -> Content
|
let content: () -> Content
|
||||||
let didReachBottom: (Bool) -> Void
|
let didReachBottom: (Bool) -> Void
|
||||||
|
|
||||||
init(content: @escaping () -> Content,
|
init(
|
||||||
didReachBottom: @escaping (Bool) -> Void)
|
content: @escaping () -> Content,
|
||||||
{
|
didReachBottom: @escaping (Bool) -> Void
|
||||||
|
) {
|
||||||
self.content = content
|
self.content = content
|
||||||
self.didReachBottom = didReachBottom
|
self.didReachBottom = didReachBottom
|
||||||
}
|
}
|
||||||
|
@ -70,10 +71,13 @@ struct DetectBottomScrollView<Content: View>: View {
|
||||||
ChildSizeReader(size: $scrollViewSize) {
|
ChildSizeReader(size: $scrollViewSize) {
|
||||||
content()
|
content()
|
||||||
.background(GeometryReader { proxy in
|
.background(GeometryReader { proxy in
|
||||||
Color.clear.preference(key: ViewOffsetKey.self,
|
Color.clear.preference(
|
||||||
value: -1 * proxy.frame(in: .named(spaceName)).origin.y)
|
key: ViewOffsetKey.self,
|
||||||
|
value: -1 * proxy.frame(in: .named(spaceName)).origin.y
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.onPreferenceChange(ViewOffsetKey.self,
|
.onPreferenceChange(
|
||||||
|
ViewOffsetKey.self,
|
||||||
perform: { value in
|
perform: { value in
|
||||||
|
|
||||||
if value >= scrollViewSize.height - wholeSize.height {
|
if value >= scrollViewSize.height - wholeSize.height {
|
||||||
|
@ -87,7 +91,8 @@ struct DetectBottomScrollView<Content: View>: View {
|
||||||
didReachBottom(false)
|
didReachBottom(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.coordinateSpace(name: spaceName)
|
.coordinateSpace(name: spaceName)
|
||||||
|
|
|
@ -23,8 +23,10 @@ struct EpisodeRowCard: View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
ImageView(episode.getBackdropImage(maxWidth: 200),
|
ImageView(
|
||||||
blurHash: episode.getBackdropImageBlurHash())
|
episode.getBackdropImage(maxWidth: 200),
|
||||||
|
blurHash: episode.getBackdropImageBlurHash()
|
||||||
|
)
|
||||||
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
||||||
.frame(width: 200, height: 112)
|
.frame(width: 200, height: 112)
|
||||||
.overlay {
|
.overlay {
|
||||||
|
|
|
@ -29,8 +29,10 @@ struct EpisodesRowView<RowManager>: View where RowManager: EpisodesRowManager {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(viewModel.sortedSeasons,
|
ForEach(
|
||||||
id: \.self) { season in
|
viewModel.sortedSeasons,
|
||||||
|
id: \.self
|
||||||
|
) { season in
|
||||||
Button {
|
Button {
|
||||||
viewModel.select(season: season)
|
viewModel.select(season: season)
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -17,13 +17,14 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
|
||||||
let topBarView: () -> TopBarView
|
let topBarView: () -> TopBarView
|
||||||
let selectedAction: (ItemType) -> Void
|
let selectedAction: (ItemType) -> Void
|
||||||
|
|
||||||
init(items: [ItemType],
|
init(
|
||||||
|
items: [ItemType],
|
||||||
maxWidth: CGFloat = 110,
|
maxWidth: CGFloat = 110,
|
||||||
horizontalAlignment: HorizontalAlignment = .leading,
|
horizontalAlignment: HorizontalAlignment = .leading,
|
||||||
textAlignment: TextAlignment = .leading,
|
textAlignment: TextAlignment = .leading,
|
||||||
topBarView: @escaping () -> TopBarView,
|
topBarView: @escaping () -> TopBarView,
|
||||||
selectedAction: @escaping (ItemType) -> Void)
|
selectedAction: @escaping (ItemType) -> Void
|
||||||
{
|
) {
|
||||||
self.items = items
|
self.items = items
|
||||||
self.maxWidth = maxWidth
|
self.maxWidth = maxWidth
|
||||||
self.horizontalAlignment = horizontalAlignment
|
self.horizontalAlignment = horizontalAlignment
|
||||||
|
@ -43,11 +44,13 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
|
||||||
selectedAction(item)
|
selectedAction(item)
|
||||||
} label: {
|
} label: {
|
||||||
VStack(alignment: horizontalAlignment) {
|
VStack(alignment: horizontalAlignment) {
|
||||||
ImageView(item.imageURLConstructor(maxWidth: Int(maxWidth)),
|
ImageView(
|
||||||
|
item.imageURLConstructor(maxWidth: Int(maxWidth)),
|
||||||
blurHash: item.blurHash,
|
blurHash: item.blurHash,
|
||||||
failureView: {
|
failureView: {
|
||||||
InitialFailureView(item.failureInitials)
|
InitialFailureView(item.failureInitials)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.portraitPoster(width: maxWidth)
|
.portraitPoster(width: maxWidth)
|
||||||
.shadow(radius: 4, y: 2)
|
.shadow(radius: 4, y: 2)
|
||||||
.accessibilityIgnoresInvertColors()
|
.accessibilityIgnoresInvertColors()
|
||||||
|
|
|
@ -17,12 +17,13 @@ struct PortraitItemButton<ItemType: PortraitImageStackable>: View {
|
||||||
let textAlignment: TextAlignment
|
let textAlignment: TextAlignment
|
||||||
let selectedAction: (ItemType) -> Void
|
let selectedAction: (ItemType) -> Void
|
||||||
|
|
||||||
init(item: ItemType,
|
init(
|
||||||
|
item: ItemType,
|
||||||
maxWidth: CGFloat = 110,
|
maxWidth: CGFloat = 110,
|
||||||
horizontalAlignment: HorizontalAlignment = .leading,
|
horizontalAlignment: HorizontalAlignment = .leading,
|
||||||
textAlignment: TextAlignment = .leading,
|
textAlignment: TextAlignment = .leading,
|
||||||
selectedAction: @escaping (ItemType) -> Void)
|
selectedAction: @escaping (ItemType) -> Void
|
||||||
{
|
) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.maxWidth = maxWidth
|
self.maxWidth = maxWidth
|
||||||
self.horizontalAlignment = horizontalAlignment
|
self.horizontalAlignment = horizontalAlignment
|
||||||
|
@ -35,11 +36,13 @@ struct PortraitItemButton<ItemType: PortraitImageStackable>: View {
|
||||||
selectedAction(item)
|
selectedAction(item)
|
||||||
} label: {
|
} label: {
|
||||||
VStack(alignment: horizontalAlignment) {
|
VStack(alignment: horizontalAlignment) {
|
||||||
ImageView(item.imageURLConstructor(maxWidth: Int(maxWidth)),
|
ImageView(
|
||||||
|
item.imageURLConstructor(maxWidth: Int(maxWidth)),
|
||||||
blurHash: item.blurHash,
|
blurHash: item.blurHash,
|
||||||
failureView: {
|
failureView: {
|
||||||
InitialFailureView(item.failureInitials)
|
InitialFailureView(item.failureInitials)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.portraitPoster(width: maxWidth)
|
.portraitPoster(width: maxWidth)
|
||||||
.shadow(radius: 4, y: 2)
|
.shadow(radius: 4, y: 2)
|
||||||
.accessibilityIgnoresInvertColors()
|
.accessibilityIgnoresInvertColors()
|
||||||
|
|
|
@ -27,11 +27,12 @@ struct TruncatedTextView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ text: String,
|
init(
|
||||||
|
_ text: String,
|
||||||
lineLimit: Int,
|
lineLimit: Int,
|
||||||
font: UIFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body),
|
font: UIFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body),
|
||||||
seeMoreAction: @escaping () -> Void)
|
seeMoreAction: @escaping () -> Void
|
||||||
{
|
) {
|
||||||
self.text = text
|
self.text = text
|
||||||
self.lineLimit = lineLimit
|
self.lineLimit = lineLimit
|
||||||
_shrinkText = State(wrappedValue: text)
|
_shrinkText = State(wrappedValue: text)
|
||||||
|
@ -45,13 +46,15 @@ struct TruncatedTextView: View {
|
||||||
Text(shrinkText)
|
Text(shrinkText)
|
||||||
.overlay {
|
.overlay {
|
||||||
if truncated {
|
if truncated {
|
||||||
LinearGradient(stops: [
|
LinearGradient(
|
||||||
|
stops: [
|
||||||
.init(color: .systemBackground.opacity(0), location: 0.5),
|
.init(color: .systemBackground.opacity(0), location: 0.5),
|
||||||
.init(color: .systemBackground.opacity(0.8), location: 0.7),
|
.init(color: .systemBackground.opacity(0.8), location: 0.7),
|
||||||
.init(color: .systemBackground, location: 1),
|
.init(color: .systemBackground, location: 1),
|
||||||
],
|
],
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,10 +74,12 @@ struct TruncatedTextView: View {
|
||||||
var mid = heigh
|
var mid = heigh
|
||||||
while (heigh - low) > 1 {
|
while (heigh - low) > 1 {
|
||||||
let attributedText = NSAttributedString(string: shrinkText, attributes: attributes)
|
let attributedText = NSAttributedString(string: shrinkText, attributes: attributes)
|
||||||
let boundingRect = attributedText.boundingRect(with: size,
|
let boundingRect = attributedText.boundingRect(
|
||||||
|
with: size,
|
||||||
options: NSStringDrawingOptions
|
options: NSStringDrawingOptions
|
||||||
.usesLineFragmentOrigin,
|
.usesLineFragmentOrigin,
|
||||||
context: nil)
|
context: nil
|
||||||
|
)
|
||||||
if boundingRect.size.height > visibleTextGeometry.size.height {
|
if boundingRect.size.height > visibleTextGeometry.size.height {
|
||||||
truncated = true
|
truncated = true
|
||||||
heigh = mid
|
heigh = mid
|
||||||
|
|
|
@ -47,8 +47,10 @@ struct AboutView: View {
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
Link(L10n.sourceCode,
|
Link(
|
||||||
destination: URL(string: "https://github.com/jellyfin/Swiftfin")!)
|
L10n.sourceCode,
|
||||||
|
destination: URL(string: "https://github.com/jellyfin/Swiftfin")!
|
||||||
|
)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -59,8 +61,10 @@ struct AboutView: View {
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "plus.circle.fill")
|
Image(systemName: "plus.circle.fill")
|
||||||
Link(L10n.requestFeature,
|
Link(
|
||||||
destination: URL(string: "https://github.com/jellyfin/Swiftfin/issues")!)
|
L10n.requestFeature,
|
||||||
|
destination: URL(string: "https://github.com/jellyfin/Swiftfin/issues")!
|
||||||
|
)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -71,8 +75,10 @@ struct AboutView: View {
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "xmark.circle.fill")
|
Image(systemName: "xmark.circle.fill")
|
||||||
Link(L10n.reportIssue,
|
Link(
|
||||||
destination: URL(string: "https://github.com/jellyfin/Swiftfin/issues")!)
|
L10n.reportIssue,
|
||||||
|
destination: URL(string: "https://github.com/jellyfin/Swiftfin/issues")!
|
||||||
|
)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
|
@ -102,17 +102,21 @@ struct ConnectToServerView: View {
|
||||||
.headerProminence(.increased)
|
.headerProminence(.increased)
|
||||||
}
|
}
|
||||||
.alert(item: $viewModel.errorMessage) { _ in
|
.alert(item: $viewModel.errorMessage) { _ in
|
||||||
Alert(title: Text(viewModel.alertTitle),
|
Alert(
|
||||||
|
title: Text(viewModel.alertTitle),
|
||||||
message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
|
message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
|
||||||
dismissButton: .cancel())
|
dismissButton: .cancel()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.alert(item: $viewModel.addServerURIPayload) { _ in
|
.alert(item: $viewModel.addServerURIPayload) { _ in
|
||||||
Alert(title: L10n.existingServer.text,
|
Alert(
|
||||||
|
title: L10n.existingServer.text,
|
||||||
message: L10n.serverAlreadyExistsPrompt(viewModel.addServerURIPayload?.server.name ?? "").text,
|
message: L10n.serverAlreadyExistsPrompt(viewModel.addServerURIPayload?.server.name ?? "").text,
|
||||||
primaryButton: .default(L10n.addURL.text, action: {
|
primaryButton: .default(L10n.addURL.text, action: {
|
||||||
viewModel.addURIToServer(addServerURIPayload: viewModel.backAddServerURIPayload!)
|
viewModel.addURIToServer(addServerURIPayload: viewModel.backAddServerURIPayload!)
|
||||||
}),
|
}),
|
||||||
secondaryButton: .cancel())
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.navigationTitle(L10n.connect)
|
.navigationTitle(L10n.connect)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|
|
@ -51,9 +51,11 @@ struct ContinueWatchingView: View {
|
||||||
|
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
|
|
||||||
LinearGradient(colors: [.clear, .black.opacity(0.5), .black.opacity(0.7)],
|
LinearGradient(
|
||||||
|
colors: [.clear, .black.opacity(0.5), .black.opacity(0.7)],
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom)
|
endPoint: .bottom
|
||||||
|
)
|
||||||
.frame(height: 35)
|
.frame(height: 35)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
|
|
@ -56,8 +56,10 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !viewModel.nextUpItems.isEmpty {
|
if !viewModel.nextUpItems.isEmpty {
|
||||||
PortraitImageHStackView(items: viewModel.nextUpItems,
|
PortraitImageHStackView(
|
||||||
horizontalAlignment: .leading) {
|
items: viewModel.nextUpItems,
|
||||||
|
horizontalAlignment: .leading
|
||||||
|
) {
|
||||||
L10n.nextUp.text
|
L10n.nextUp.text
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
@ -93,9 +95,13 @@ struct HomeView: View {
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
homeRouter
|
homeRouter
|
||||||
.route(to: \.library, (viewModel: .init(parentID: library.id!,
|
.route(to: \.library, (
|
||||||
filters: viewModel.recentFilterSet),
|
viewModel: .init(
|
||||||
title: library.name ?? ""))
|
parentID: library.id!,
|
||||||
|
filters: viewModel.recentFilterSet
|
||||||
|
),
|
||||||
|
title: library.name ?? ""
|
||||||
|
))
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
L10n.seeAll.text.font(.subheadline).fontWeight(.bold)
|
L10n.seeAll.text.font(.subheadline).fontWeight(.bold)
|
||||||
|
|
|
@ -29,9 +29,11 @@ struct ItemViewBody: View {
|
||||||
|
|
||||||
if let itemOverview = viewModel.item.overview {
|
if let itemOverview = viewModel.item.overview {
|
||||||
if hSizeClass == .compact && vSizeClass == .regular {
|
if hSizeClass == .compact && vSizeClass == .regular {
|
||||||
TruncatedTextView(itemOverview,
|
TruncatedTextView(
|
||||||
|
itemOverview,
|
||||||
lineLimit: 5,
|
lineLimit: 5,
|
||||||
font: UIFont.preferredFont(forTextStyle: .footnote)) {
|
font: UIFont.preferredFont(forTextStyle: .footnote)
|
||||||
|
) {
|
||||||
itemRouter.route(to: \.itemOverview, viewModel.item)
|
itemRouter.route(to: \.itemOverview, viewModel.item)
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
@ -50,33 +52,40 @@ struct ItemViewBody: View {
|
||||||
// MARK: Seasons
|
// MARK: Seasons
|
||||||
|
|
||||||
if let seriesViewModel = viewModel as? SeriesItemViewModel {
|
if let seriesViewModel = viewModel as? SeriesItemViewModel {
|
||||||
PortraitImageHStackView(items: seriesViewModel.seasons,
|
PortraitImageHStackView(
|
||||||
|
items: seriesViewModel.seasons,
|
||||||
topBarView: {
|
topBarView: {
|
||||||
L10n.seasons.text
|
L10n.seasons.text
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.padding()
|
.padding()
|
||||||
.accessibility(addTraits: [.isHeader])
|
.accessibility(addTraits: [.isHeader])
|
||||||
}, selectedAction: { season in
|
},
|
||||||
|
selectedAction: { season in
|
||||||
itemRouter.route(to: \.item, season)
|
itemRouter.route(to: \.item, season)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Genres
|
// MARK: Genres
|
||||||
|
|
||||||
if let genres = viewModel.item.genreItems, !genres.isEmpty {
|
if let genres = viewModel.item.genreItems, !genres.isEmpty {
|
||||||
PillHStackView(title: L10n.genres,
|
PillHStackView(
|
||||||
|
title: L10n.genres,
|
||||||
items: genres,
|
items: genres,
|
||||||
selectedAction: { genre in
|
selectedAction: { genre in
|
||||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Studios
|
// MARK: Studios
|
||||||
|
|
||||||
if let studios = viewModel.item.studios {
|
if let studios = viewModel.item.studios {
|
||||||
PillHStackView(title: L10n.studios,
|
PillHStackView(
|
||||||
items: studios) { studio in
|
title: L10n.studios,
|
||||||
|
items: studios
|
||||||
|
) { studio in
|
||||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||||
}
|
}
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
|
@ -124,7 +133,8 @@ struct ItemViewBody: View {
|
||||||
|
|
||||||
if showCastAndCrew {
|
if showCastAndCrew {
|
||||||
if let castAndCrew = viewModel.item.people, !castAndCrew.isEmpty {
|
if let castAndCrew = viewModel.item.people, !castAndCrew.isEmpty {
|
||||||
PortraitImageHStackView(items: castAndCrew.filter { BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") },
|
PortraitImageHStackView(
|
||||||
|
items: castAndCrew.filter { BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") },
|
||||||
topBarView: {
|
topBarView: {
|
||||||
L10n.castAndCrew.text
|
L10n.castAndCrew.text
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
@ -134,14 +144,16 @@ struct ItemViewBody: View {
|
||||||
},
|
},
|
||||||
selectedAction: { person in
|
selectedAction: { person in
|
||||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Recommended
|
// MARK: Recommended
|
||||||
|
|
||||||
if !viewModel.similarItems.isEmpty {
|
if !viewModel.similarItems.isEmpty {
|
||||||
PortraitImageHStackView(items: viewModel.similarItems,
|
PortraitImageHStackView(
|
||||||
|
items: viewModel.similarItems,
|
||||||
topBarView: {
|
topBarView: {
|
||||||
L10n.recommended.text
|
L10n.recommended.text
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
@ -151,7 +163,8 @@ struct ItemViewBody: View {
|
||||||
},
|
},
|
||||||
selectedAction: { item in
|
selectedAction: { item in
|
||||||
itemRouter.route(to: \.item, item)
|
itemRouter.route(to: \.item, item)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Details
|
// MARK: Details
|
||||||
|
|
|
@ -24,8 +24,10 @@ struct ItemLandscapeMainView: View {
|
||||||
// MARK: Sidebar Image
|
// MARK: Sidebar Image
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
ImageView(viewModel.item.portraitHeaderViewURL(maxWidth: 130),
|
ImageView(
|
||||||
blurHash: viewModel.item.getPrimaryImageBlurHash())
|
viewModel.item.portraitHeaderViewURL(maxWidth: 130),
|
||||||
|
blurHash: viewModel.item.getPrimaryImageBlurHash()
|
||||||
|
)
|
||||||
.frame(width: 130, height: 195)
|
.frame(width: 130, height: 195)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.accessibilityIgnoresInvertColors()
|
.accessibilityIgnoresInvertColors()
|
||||||
|
@ -95,8 +97,10 @@ struct ItemLandscapeMainView: View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// MARK: Backdrop
|
// MARK: Backdrop
|
||||||
|
|
||||||
ImageView(viewModel.item.getBackdropImage(maxWidth: 200),
|
ImageView(
|
||||||
blurHash: viewModel.item.getBackdropImageBlurHash())
|
viewModel.item.getBackdropImage(maxWidth: 200),
|
||||||
|
blurHash: viewModel.item.getBackdropImageBlurHash()
|
||||||
|
)
|
||||||
.opacity(0.3)
|
.opacity(0.3)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
.blur(radius: 8)
|
.blur(radius: 8)
|
||||||
|
|
|
@ -62,8 +62,10 @@ struct ItemLandscapeTopBarView: View {
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
.overlay(
|
||||||
.stroke(Color.secondary, lineWidth: 1))
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.stroke(Color.secondary, lineWidth: 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
|
@ -24,8 +24,10 @@ struct PortraitHeaderOverlayView: View {
|
||||||
|
|
||||||
// MARK: Portrait Image
|
// MARK: Portrait Image
|
||||||
|
|
||||||
ImageView(viewModel.item.portraitHeaderViewURL(maxWidth: 130),
|
ImageView(
|
||||||
blurHash: viewModel.item.getPrimaryImageBlurHash())
|
viewModel.item.portraitHeaderViewURL(maxWidth: 130),
|
||||||
|
blurHash: viewModel.item.getPrimaryImageBlurHash()
|
||||||
|
)
|
||||||
.portraitPoster(width: 130)
|
.portraitPoster(width: 130)
|
||||||
.accessibilityIgnoresInvertColors()
|
.accessibilityIgnoresInvertColors()
|
||||||
|
|
||||||
|
@ -79,8 +81,10 @@ struct PortraitHeaderOverlayView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
.overlay(
|
||||||
.stroke(Color.secondary, lineWidth: 1))
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.stroke(Color.secondary, lineWidth: 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,10 @@ struct ItemPortraitMainView: View {
|
||||||
// MARK: portraitHeaderView
|
// MARK: portraitHeaderView
|
||||||
|
|
||||||
var portraitHeaderView: some View {
|
var portraitHeaderView: some View {
|
||||||
ImageView(viewModel.item.getBackdropImage(maxWidth: Int(UIScreen.main.bounds.width)),
|
ImageView(
|
||||||
blurHash: viewModel.item.getBackdropImageBlurHash())
|
viewModel.item.getBackdropImage(maxWidth: Int(UIScreen.main.bounds.width)),
|
||||||
|
blurHash: viewModel.item.getBackdropImageBlurHash()
|
||||||
|
)
|
||||||
.opacity(0.4)
|
.opacity(0.4)
|
||||||
.blur(radius: 2.0)
|
.blur(radius: 2.0)
|
||||||
.accessibilityIgnoresInvertColors()
|
.accessibilityIgnoresInvertColors()
|
||||||
|
@ -39,10 +41,12 @@ struct ItemPortraitMainView: View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
// MARK: ParallaxScrollView
|
// MARK: ParallaxScrollView
|
||||||
|
|
||||||
ParallaxHeaderScrollView(header: portraitHeaderView,
|
ParallaxHeaderScrollView(
|
||||||
|
header: portraitHeaderView,
|
||||||
staticOverlayView: portraitStaticOverlayView,
|
staticOverlayView: portraitStaticOverlayView,
|
||||||
overlayAlignment: .bottomLeading,
|
overlayAlignment: .bottomLeading,
|
||||||
headerHeight: UIScreen.main.bounds.width * 0.5625) {
|
headerHeight: UIScreen.main.bounds.width * 0.5625
|
||||||
|
) {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 70)
|
.frame(height: 70)
|
||||||
|
|
|
@ -18,8 +18,10 @@ struct LatestMediaView<TopBarView: View>: View {
|
||||||
var topBarView: () -> TopBarView
|
var topBarView: () -> TopBarView
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
PortraitImageHStackView(items: viewModel.items,
|
PortraitImageHStackView(
|
||||||
horizontalAlignment: .leading) {
|
items: viewModel.items,
|
||||||
|
horizontalAlignment: .leading
|
||||||
|
) {
|
||||||
topBarView()
|
topBarView()
|
||||||
} selectedAction: { item in
|
} selectedAction: { item in
|
||||||
homeRouter.route(to: \.item, item)
|
homeRouter.route(to: \.item, item)
|
||||||
|
|
|
@ -35,22 +35,28 @@ struct LibraryFilterView: View {
|
||||||
} else {
|
} else {
|
||||||
Form {
|
Form {
|
||||||
if viewModel.enabledFilterType.contains(.genre) {
|
if viewModel.enabledFilterType.contains(.genre) {
|
||||||
MultiSelector(label: L10n.genres,
|
MultiSelector(
|
||||||
|
label: L10n.genres,
|
||||||
options: viewModel.possibleGenres,
|
options: viewModel.possibleGenres,
|
||||||
optionToString: { $0.name ?? "" },
|
optionToString: { $0.name ?? "" },
|
||||||
selected: $viewModel.modifiedFilters.withGenres)
|
selected: $viewModel.modifiedFilters.withGenres
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if viewModel.enabledFilterType.contains(.filter) {
|
if viewModel.enabledFilterType.contains(.filter) {
|
||||||
MultiSelector(label: L10n.filters,
|
MultiSelector(
|
||||||
|
label: L10n.filters,
|
||||||
options: viewModel.possibleItemFilters,
|
options: viewModel.possibleItemFilters,
|
||||||
optionToString: { $0.localized },
|
optionToString: { $0.localized },
|
||||||
selected: $viewModel.modifiedFilters.filters)
|
selected: $viewModel.modifiedFilters.filters
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if viewModel.enabledFilterType.contains(.tag) {
|
if viewModel.enabledFilterType.contains(.tag) {
|
||||||
MultiSelector(label: L10n.tags,
|
MultiSelector(
|
||||||
|
label: L10n.tags,
|
||||||
options: viewModel.possibleTags,
|
options: viewModel.possibleTags,
|
||||||
optionToString: { $0 },
|
optionToString: { $0 },
|
||||||
selected: $viewModel.modifiedFilters.tags)
|
selected: $viewModel.modifiedFilters.tags
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if viewModel.enabledFilterType.contains(.sortBy) {
|
if viewModel.enabledFilterType.contains(.sortBy) {
|
||||||
Picker(selection: $viewModel.selectedSortBy, label: L10n.sortBy.text) {
|
Picker(selection: $viewModel.selectedSortBy, label: L10n.sortBy.text) {
|
||||||
|
|
|
@ -33,8 +33,10 @@ struct LibraryListView: View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
Button {
|
Button {
|
||||||
libraryListRouter.route(to: \.library,
|
libraryListRouter.route(
|
||||||
(viewModel: LibraryViewModel(filters: viewModel.withFavorites), title: L10n.favorites))
|
to: \.library,
|
||||||
|
(viewModel: LibraryViewModel(filters: viewModel.withFavorites), title: L10n.favorites)
|
||||||
|
)
|
||||||
} label: {
|
} label: {
|
||||||
ZStack {
|
ZStack {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -65,9 +67,13 @@ struct LibraryListView: View {
|
||||||
if itemType == .liveTV {
|
if itemType == .liveTV {
|
||||||
libraryListRouter.route(to: \.liveTV)
|
libraryListRouter.route(to: \.liveTV)
|
||||||
} else {
|
} else {
|
||||||
libraryListRouter.route(to: \.library,
|
libraryListRouter.route(
|
||||||
(viewModel: LibraryViewModel(parentID: library.id),
|
to: \.library,
|
||||||
title: library.name ?? ""))
|
(
|
||||||
|
viewModel: LibraryViewModel(parentID: library.id),
|
||||||
|
title: library.name ?? ""
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
|
@ -22,8 +22,10 @@ struct LibraryView: View {
|
||||||
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var tracks: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top),
|
private var tracks: [GridItem] = Array(
|
||||||
count: Int(UIScreen.main.bounds.size.width) / 125)
|
repeating: .init(.flexible(), alignment: .top),
|
||||||
|
count: Int(UIScreen.main.bounds.size.width) / 125
|
||||||
|
)
|
||||||
|
|
||||||
func recalcTracks() {
|
func recalcTracks() {
|
||||||
tracks = Array(repeating: .init(.flexible(), alignment: .top), count: Int(UIScreen.main.bounds.size.width) / 125)
|
tracks = Array(repeating: .init(.flexible(), alignment: .top), count: Int(UIScreen.main.bounds.size.width) / 125)
|
||||||
|
@ -82,8 +84,11 @@ struct LibraryView: View {
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
libraryRouter
|
libraryRouter
|
||||||
.route(to: \.filter, (filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType,
|
.route(to: \.filter, (
|
||||||
parentId: viewModel.parentID ?? ""))
|
filters: $viewModel.filters,
|
||||||
|
enabledFilterType: viewModel.enabledFilterType,
|
||||||
|
parentId: viewModel.parentID ?? ""
|
||||||
|
))
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "line.horizontal.3.decrease.circle")
|
Image(systemName: "line.horizontal.3.decrease.circle")
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,9 @@ struct LiveTVChannelItemElement: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay(RoundedRectangle(cornerRadius: 0)
|
.overlay(
|
||||||
.stroke(Color.blue, lineWidth: 0))
|
RoundedRectangle(cornerRadius: 0)
|
||||||
|
.stroke(Color.blue, lineWidth: 0)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,8 +99,11 @@ struct LiveTVChannelItemWideElement: View {
|
||||||
.foregroundColor(Color.jellyfinPurple)
|
.foregroundColor(Color.jellyfinPurple)
|
||||||
.frame(alignment: .leading)
|
.frame(alignment: .leading)
|
||||||
.padding(.init(top: 0, leading: 0, bottom: 4, trailing: 0))
|
.padding(.init(top: 0, leading: 0, bottom: 4, trailing: 0))
|
||||||
programLabel(timeText: currentProgramText.timeDisplay, titleText: currentProgramText.title,
|
programLabel(
|
||||||
color: Color("TextHighlightColor"))
|
timeText: currentProgramText.timeDisplay,
|
||||||
|
titleText: currentProgramText.title,
|
||||||
|
color: Color("TextHighlightColor")
|
||||||
|
)
|
||||||
if !nextProgramsText.isEmpty,
|
if !nextProgramsText.isEmpty,
|
||||||
let nextItem = nextProgramsText[0]
|
let nextItem = nextProgramsText[0]
|
||||||
{
|
{
|
||||||
|
|
|
@ -41,10 +41,12 @@ struct LiveTVChannelsView: View {
|
||||||
makeCellView(channelProgram)
|
makeCellView(channelProgram)
|
||||||
}
|
}
|
||||||
.layout {
|
.layout {
|
||||||
.grid(layoutMode: .fixedNumberOfColumns(columns),
|
.grid(
|
||||||
|
layoutMode: .fixedNumberOfColumns(columns),
|
||||||
itemSpacing: 16,
|
itemSpacing: 16,
|
||||||
lineSpacing: 4,
|
lineSpacing: 4,
|
||||||
itemSize: .absolute(144))
|
itemSize: .absolute(144)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
@ -84,11 +86,14 @@ struct LiveTVChannelsView: View {
|
||||||
}
|
}
|
||||||
return start > currentStart
|
return start > currentStart
|
||||||
}
|
}
|
||||||
LiveTVChannelItemWideElement(channel: channel,
|
LiveTVChannelItemWideElement(
|
||||||
|
channel: channel,
|
||||||
currentProgram: channelProgram.currentProgram,
|
currentProgram: channelProgram.currentProgram,
|
||||||
currentProgramText: currentProgramDisplayText,
|
currentProgramText: currentProgramDisplayText,
|
||||||
nextProgramsText: nextProgramsDisplayText(nextItems: nextItems,
|
nextProgramsText: nextProgramsDisplayText(
|
||||||
timeFormatter: viewModel.timeFormatter),
|
nextItems: nextItems,
|
||||||
|
timeFormatter: viewModel.timeFormatter
|
||||||
|
),
|
||||||
onSelect: { loadingAction in
|
onSelect: { loadingAction in
|
||||||
loadingAction(true)
|
loadingAction(true)
|
||||||
self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in
|
self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in
|
||||||
|
@ -97,7 +102,8 @@ struct LiveTVChannelsView: View {
|
||||||
loadingAction(false)
|
loadingAction(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkOrientation() {
|
private func checkOrientation() {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue