mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-23 05:59:51 +00:00
[iOS] Admin Dashboard (#1230)
This commit is contained in:
parent
4cba762226
commit
bc9eacab57
@ -8,24 +8,80 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: see if animation is correct here or should be in caller views
|
||||
|
||||
// TODO: remove and replace with below
|
||||
struct ProgressBar: View {
|
||||
|
||||
@State
|
||||
private var contentSize: CGSize = .zero
|
||||
|
||||
let progress: CGFloat
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule()
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(0.2)
|
||||
|
||||
Capsule()
|
||||
.mask(alignment: .leading) {
|
||||
Rectangle()
|
||||
.scaleEffect(x: progress, anchor: .leading)
|
||||
}
|
||||
}
|
||||
.animation(.linear(duration: 0.1), value: progress)
|
||||
Capsule()
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(0.2)
|
||||
.overlay(alignment: .leading) {
|
||||
Capsule()
|
||||
.mask(alignment: .leading) {
|
||||
Rectangle()
|
||||
}
|
||||
.frame(width: contentSize.width * progress)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
.trackingSize($contentSize)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: fix capsule with low progress
|
||||
|
||||
extension ProgressViewStyle where Self == PlaybackProgressViewStyle {
|
||||
|
||||
static var playback: Self { .init(secondaryProgress: nil) }
|
||||
|
||||
static func playback(secondaryProgress: Double?) -> Self {
|
||||
.init(secondaryProgress: secondaryProgress)
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaybackProgressViewStyle: ProgressViewStyle {
|
||||
|
||||
@State
|
||||
private var contentSize: CGSize = .zero
|
||||
|
||||
let secondaryProgress: Double?
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
Capsule()
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(0.2)
|
||||
.overlay(alignment: .leading) {
|
||||
ZStack(alignment: .leading) {
|
||||
|
||||
if let secondaryProgress {
|
||||
Capsule()
|
||||
.mask(alignment: .leading) {
|
||||
Rectangle()
|
||||
}
|
||||
.frame(width: contentSize.width * clamp(secondaryProgress, min: 0, max: 1))
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
|
||||
Capsule()
|
||||
.mask(alignment: .leading) {
|
||||
Rectangle()
|
||||
}
|
||||
.frame(width: contentSize.width * (configuration.fractionCompleted ?? 0))
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
.trackingSize($contentSize)
|
||||
}
|
||||
}
|
||||
|
||||
// #Preview {
|
||||
// ProgressView(value: 0.3)
|
||||
// .progressViewStyle(.SwiftfinLinear(secondaryProgress: 0.3))
|
||||
// .frame(height: 8)
|
||||
// .padding(.horizontal, 10)
|
||||
// .foregroundStyle(.primary, .secondary, .orange)
|
||||
// }
|
||||
|
@ -13,17 +13,17 @@ import SwiftUI
|
||||
|
||||
struct TextPairView: View {
|
||||
|
||||
let leading: String
|
||||
let trailing: String
|
||||
private let leading: Text
|
||||
private let trailing: Text
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(leading)
|
||||
leading
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(trailing)
|
||||
trailing
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
@ -33,8 +33,22 @@ extension TextPairView {
|
||||
|
||||
init(_ textPair: TextPair) {
|
||||
self.init(
|
||||
leading: textPair.title,
|
||||
trailing: textPair.subtitle
|
||||
leading: Text(textPair.title),
|
||||
trailing: Text(textPair.subtitle)
|
||||
)
|
||||
}
|
||||
|
||||
init(leading: String, trailing: String) {
|
||||
self.init(
|
||||
leading: Text(leading),
|
||||
trailing: Text(trailing)
|
||||
)
|
||||
}
|
||||
|
||||
init(_ title: String, value: @autoclosure () -> Text) {
|
||||
self.init(
|
||||
leading: Text(title),
|
||||
trailing: value()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import JellyfinAPI
|
||||
import PulseUI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
@ -43,12 +44,27 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
@Route(.push)
|
||||
var indicatorSettings = makeIndicatorSettings
|
||||
@Route(.push)
|
||||
var serverDetail = makeServerDetail
|
||||
var serverConnection = makeServerConnection
|
||||
@Route(.push)
|
||||
var videoPlayerSettings = makeVideoPlayerSettings
|
||||
@Route(.push)
|
||||
var customDeviceProfileSettings = makeCustomDeviceProfileSettings
|
||||
|
||||
@Route(.push)
|
||||
var userDashboard = makeUserDashboard
|
||||
@Route(.push)
|
||||
var activeSessions = makeActiveSessions
|
||||
@Route(.push)
|
||||
var activeDeviceDetails = makeActiveDeviceDetails
|
||||
@Route(.modal)
|
||||
var itemOverviewView = makeItemOverviewView
|
||||
@Route(.push)
|
||||
var tasks = makeTasks
|
||||
@Route(.push)
|
||||
var editScheduledTask = makeEditScheduledTask
|
||||
@Route(.push)
|
||||
var serverLogs = makeServerLogs
|
||||
|
||||
@Route(.modal)
|
||||
var editCustomDeviceProfile = makeEditCustomDeviceProfile
|
||||
@Route(.modal)
|
||||
@ -142,10 +158,46 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeServerDetail(server: ServerState) -> some View {
|
||||
func makeServerConnection(server: ServerState) -> some View {
|
||||
EditServerView(server: server)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeUserDashboard() -> some View {
|
||||
UserDashboardView()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeActiveSessions() -> some View {
|
||||
ActiveSessionsView()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeActiveDeviceDetails(box: BindingBox<SessionInfo?>) -> some View {
|
||||
ActiveSessionDetailView(box: box)
|
||||
}
|
||||
|
||||
func makeItemOverviewView(item: BaseItemDto) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
ItemOverviewView(item: item)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeTasks() -> some View {
|
||||
ScheduledTasksView()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeEditScheduledTask(observer: ServerTaskObserver) -> some View {
|
||||
EditScheduledTaskView(observer: observer)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeServerLogs() -> some View {
|
||||
ServerLogsView()
|
||||
}
|
||||
|
||||
func makeItemFilterDrawerSelector(selection: Binding<[ItemFilterType]>) -> some View {
|
||||
OrderedSectionSelectorView(selection: selection, sources: ItemFilterType.allCases)
|
||||
.navigationTitle(L10n.filters)
|
||||
|
@ -22,3 +22,33 @@ extension FormatStyle where Self == HourMinuteFormatStyle {
|
||||
|
||||
static var hourMinute: HourMinuteFormatStyle { HourMinuteFormatStyle() }
|
||||
}
|
||||
|
||||
struct RunTimeFormatStyle: FormatStyle {
|
||||
|
||||
private var negate: Bool = false
|
||||
|
||||
var negated: RunTimeFormatStyle {
|
||||
mutating(\.negate, with: true)
|
||||
}
|
||||
|
||||
func format(_ value: Int) -> String {
|
||||
let hours = value / 3600
|
||||
let minutes = (value % 3600) / 60
|
||||
let seconds = value % 3600 % 60
|
||||
|
||||
let hourText = hours > 0 ? String(hours).appending(":") : ""
|
||||
let minutesText = hours > 0 ? String(minutes).leftPad(maxWidth: 2, with: "0").appending(":") : String(minutes)
|
||||
.appending(":")
|
||||
let secondsText = String(seconds).leftPad(maxWidth: 2, with: "0")
|
||||
|
||||
return hourText
|
||||
.appending(minutesText)
|
||||
.appending(secondsText)
|
||||
.prepending("-", if: negate)
|
||||
}
|
||||
}
|
||||
|
||||
extension FormatStyle where Self == RunTimeFormatStyle {
|
||||
|
||||
static var runtime: RunTimeFormatStyle { RunTimeFormatStyle() }
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ extension BaseItemDto: Poster {
|
||||
|
||||
var systemImage: String {
|
||||
switch type {
|
||||
case .audio, .musicAlbum:
|
||||
"music.note"
|
||||
case .boxSet:
|
||||
"film.stack"
|
||||
case .channel, .tvChannel, .liveTvChannel, .program:
|
||||
@ -93,4 +95,13 @@ extension BaseItemDto: Poster {
|
||||
[imageSource(.backdrop, maxWidth: maxWidth)]
|
||||
}
|
||||
}
|
||||
|
||||
func squareImageSources(maxWidth: CGFloat?) -> [ImageSource] {
|
||||
switch type {
|
||||
case .audio, .musicAlbum:
|
||||
[imageSource(.primary, maxWidth: maxWidth)]
|
||||
default:
|
||||
[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -253,4 +253,16 @@ extension BaseItemDto {
|
||||
|
||||
return L10n.play
|
||||
}
|
||||
|
||||
var parentTitle: String? {
|
||||
switch type {
|
||||
case .audio:
|
||||
album
|
||||
case .episode:
|
||||
seriesName
|
||||
case .program: nil
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import UIKit
|
||||
|
||||
extension JellyfinClient {
|
||||
|
||||
func fullURL<T>(with request: Request<T>) -> URL? {
|
||||
func fullURL<T>(with request: Request<T>, queryAPIKey: Bool = false) -> URL? {
|
||||
|
||||
guard let path = request.url?.path else { return configuration.url }
|
||||
guard let fullPath = fullURL(with: path) else { return nil }
|
||||
@ -21,6 +21,10 @@ extension JellyfinClient {
|
||||
|
||||
components.queryItems = request.query?.map { URLQueryItem(name: $0.0, value: $0.1) } ?? []
|
||||
|
||||
if queryAPIKey, let accessToken {
|
||||
components.queryItems?.append(.init(name: "api_key", value: accessToken))
|
||||
}
|
||||
|
||||
return components.url ?? fullPath
|
||||
}
|
||||
|
||||
|
25
Shared/Extensions/JellyfinAPI/PlayMethod.swift
Normal file
25
Shared/Extensions/JellyfinAPI/PlayMethod.swift
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
extension PlayMethod: Displayable {
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .transcode:
|
||||
return L10n.transcode
|
||||
case .directStream:
|
||||
return L10n.directStream
|
||||
case .directPlay:
|
||||
return L10n.directPlay
|
||||
}
|
||||
}
|
||||
}
|
18
Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift
Normal file
18
Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift
Normal file
@ -0,0 +1,18 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
extension PlayerStateInfo {
|
||||
|
||||
var positionSeconds: Int? {
|
||||
guard let positionTicks else { return nil }
|
||||
return positionTicks / 10_000_000
|
||||
}
|
||||
}
|
26
Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift
Normal file
26
Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift
Normal file
@ -0,0 +1,26 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
extension TaskCompletionStatus: Displayable {
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .completed:
|
||||
return L10n.taskCompleted
|
||||
case .failed:
|
||||
return L10n.taskFailed
|
||||
case .cancelled:
|
||||
return L10n.taskCancelled
|
||||
case .aborted:
|
||||
return L10n.taskAborted
|
||||
}
|
||||
}
|
||||
}
|
104
Shared/Extensions/JellyfinAPI/TranscodeReason.swift
Normal file
104
Shared/Extensions/JellyfinAPI/TranscodeReason.swift
Normal file
@ -0,0 +1,104 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
extension TranscodeReason: Displayable, SystemImageable {
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .containerNotSupported:
|
||||
return L10n.containerNotSupported
|
||||
case .videoCodecNotSupported:
|
||||
return L10n.videoCodecNotSupported
|
||||
case .audioCodecNotSupported:
|
||||
return L10n.audioCodecNotSupported
|
||||
case .subtitleCodecNotSupported:
|
||||
return L10n.subtitleCodecNotSupported
|
||||
case .audioIsExternal:
|
||||
return L10n.audioIsExternal
|
||||
case .secondaryAudioNotSupported:
|
||||
return L10n.secondaryAudioNotSupported
|
||||
case .videoProfileNotSupported:
|
||||
return L10n.videoProfileNotSupported
|
||||
case .videoLevelNotSupported:
|
||||
return L10n.videoLevelNotSupported
|
||||
case .videoResolutionNotSupported:
|
||||
return L10n.videoResolutionNotSupported
|
||||
case .videoBitDepthNotSupported:
|
||||
return L10n.videoBitDepthNotSupported
|
||||
case .videoFramerateNotSupported:
|
||||
return L10n.videoFramerateNotSupported
|
||||
case .refFramesNotSupported:
|
||||
return L10n.refFramesNotSupported
|
||||
case .anamorphicVideoNotSupported:
|
||||
return L10n.anamorphicVideoNotSupported
|
||||
case .interlacedVideoNotSupported:
|
||||
return L10n.interlacedVideoNotSupported
|
||||
case .audioChannelsNotSupported:
|
||||
return L10n.audioChannelsNotSupported
|
||||
case .audioProfileNotSupported:
|
||||
return L10n.audioProfileNotSupported
|
||||
case .audioSampleRateNotSupported:
|
||||
return L10n.audioSampleRateNotSupported
|
||||
case .audioBitDepthNotSupported:
|
||||
return L10n.audioBitDepthNotSupported
|
||||
case .containerBitrateExceedsLimit:
|
||||
return L10n.containerBitrateExceedsLimit
|
||||
case .videoBitrateNotSupported:
|
||||
return L10n.videoBitrateNotSupported
|
||||
case .audioBitrateNotSupported:
|
||||
return L10n.audioBitrateNotSupported
|
||||
case .unknownVideoStreamInfo:
|
||||
return L10n.unknownVideoStreamInfo
|
||||
case .unknownAudioStreamInfo:
|
||||
return L10n.unknownAudioStreamInfo
|
||||
case .directPlayError:
|
||||
return L10n.directPlayError
|
||||
case .videoRangeTypeNotSupported:
|
||||
return L10n.videoRangeTypeNotSupported
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .containerNotSupported,
|
||||
.containerBitrateExceedsLimit:
|
||||
return "shippingbox"
|
||||
case .audioCodecNotSupported,
|
||||
.audioIsExternal,
|
||||
.secondaryAudioNotSupported,
|
||||
.audioChannelsNotSupported,
|
||||
.audioProfileNotSupported,
|
||||
.audioSampleRateNotSupported,
|
||||
.audioBitDepthNotSupported,
|
||||
.audioBitrateNotSupported,
|
||||
.unknownAudioStreamInfo:
|
||||
return "speaker.wave.2"
|
||||
case .videoCodecNotSupported,
|
||||
.videoProfileNotSupported,
|
||||
.videoLevelNotSupported,
|
||||
.videoResolutionNotSupported,
|
||||
.videoBitDepthNotSupported,
|
||||
.videoFramerateNotSupported,
|
||||
.refFramesNotSupported,
|
||||
.anamorphicVideoNotSupported,
|
||||
.interlacedVideoNotSupported,
|
||||
.videoBitrateNotSupported,
|
||||
.unknownVideoStreamInfo,
|
||||
.videoRangeTypeNotSupported:
|
||||
return "photo.tv"
|
||||
case .subtitleCodecNotSupported:
|
||||
return "captions.bubble"
|
||||
default:
|
||||
return "questionmark.app"
|
||||
}
|
||||
}
|
||||
}
|
16
Shared/Extensions/OrderedDictionary.swift
Normal file
16
Shared/Extensions/OrderedDictionary.swift
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import OrderedCollections
|
||||
|
||||
extension OrderedDictionary {
|
||||
|
||||
var isNotEmpty: Bool {
|
||||
!isEmpty
|
||||
}
|
||||
}
|
16
Shared/Extensions/Text.swift
Normal file
16
Shared/Extensions/Text.swift
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension Text {
|
||||
|
||||
init(_ content: some Displayable) {
|
||||
self.init(verbatim: "\(content.displayTitle)")
|
||||
}
|
||||
}
|
@ -108,6 +108,16 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: consolidate handling
|
||||
@ViewBuilder
|
||||
func squarePosterStyle(contentMode: ContentMode = .fill) -> some View {
|
||||
aspectRatio(1.0, contentMode: contentMode)
|
||||
#if !os(tvOS)
|
||||
.posterBorder(ratio: 0.0375, of: \.width)
|
||||
.cornerRadius(ratio: 0.0375, of: \.width)
|
||||
#endif
|
||||
}
|
||||
|
||||
func posterBorder(ratio: CGFloat, of side: KeyPath<CGSize, CGFloat>) -> some View {
|
||||
modifier(OnSizeChangedModifier { size in
|
||||
overlay {
|
||||
|
56
Shared/Objects/CurrentDate.swift
Normal file
56
Shared/Objects/CurrentDate.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
/// A property wrapper that publishes the current
|
||||
/// date at periodic intervals
|
||||
@propertyWrapper
|
||||
struct CurrentDate: DynamicProperty {
|
||||
|
||||
@ObservedObject
|
||||
private var observable: CurrentDataObserver
|
||||
|
||||
var projectedValue: Binding<Date> {
|
||||
$observable.currentDate
|
||||
}
|
||||
|
||||
var wrappedValue: Date {
|
||||
observable.currentDate
|
||||
}
|
||||
|
||||
init(interval: TimeInterval = 1) {
|
||||
self.observable = .init(interval: interval)
|
||||
}
|
||||
|
||||
mutating func update() {
|
||||
_observable.update()
|
||||
}
|
||||
}
|
||||
|
||||
extension CurrentDate {
|
||||
|
||||
class CurrentDataObserver: ObservableObject {
|
||||
|
||||
@Published
|
||||
var currentDate: Date = .now
|
||||
|
||||
private var publisher: AnyCancellable?
|
||||
|
||||
init(interval: TimeInterval) {
|
||||
publisher = Timer.publish(every: 1, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { [weak self] _ in
|
||||
if let self {
|
||||
self.currentDate = .now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -28,6 +28,10 @@ protocol Poster: Displayable, Hashable, Identifiable, SystemImageable {
|
||||
func cinematicImageSources(
|
||||
maxWidth: CGFloat?
|
||||
) -> [ImageSource]
|
||||
|
||||
func squareImageSources(
|
||||
maxWidth: CGFloat?
|
||||
) -> [ImageSource]
|
||||
}
|
||||
|
||||
extension Poster {
|
||||
@ -57,4 +61,10 @@ extension Poster {
|
||||
) -> [ImageSource] {
|
||||
[]
|
||||
}
|
||||
|
||||
func squareImageSources(
|
||||
maxWidth: CGFloat?
|
||||
) -> [ImageSource] {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
// TODO: think about what to do for square (music)
|
||||
enum PosterDisplayType: String, CaseIterable, Displayable, Storable, SystemImageable {
|
||||
|
||||
case landscape
|
||||
|
@ -10,6 +10,7 @@ import Foundation
|
||||
|
||||
// TODO: require remote sign in every time
|
||||
// - actually found to be a bit difficult?
|
||||
// TODO: rename to not confuse with server access/UserDto
|
||||
|
||||
enum UserAccessPolicy: String, CaseIterable, Codable, Displayable {
|
||||
|
||||
|
@ -18,10 +18,14 @@ internal enum L10n {
|
||||
internal static let accentColorDescription = L10n.tr("Localizable", "accentColorDescription", fallback: "Some views may need an app restart to update.")
|
||||
/// Accessibility
|
||||
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
|
||||
/// Active Devices
|
||||
internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices")
|
||||
/// Add Server
|
||||
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
|
||||
/// Add URL
|
||||
internal static let addURL = L10n.tr("Localizable", "addURL", fallback: "Add URL")
|
||||
/// Administration
|
||||
internal static let administration = L10n.tr("Localizable", "administration", fallback: "Administration")
|
||||
/// Advanced
|
||||
internal static let advanced = L10n.tr("Localizable", "advanced", fallback: "Advanced")
|
||||
/// Airs %s
|
||||
@ -34,6 +38,8 @@ internal enum L10n {
|
||||
internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "All Media")
|
||||
/// All Servers
|
||||
internal static let allServers = L10n.tr("Localizable", "allServers", fallback: "All Servers")
|
||||
/// Anamorphic video is not supported
|
||||
internal static let anamorphicVideoNotSupported = L10n.tr("Localizable", "anamorphicVideoNotSupported", fallback: "Anamorphic video is not supported")
|
||||
/// Appearance
|
||||
internal static let appearance = L10n.tr("Localizable", "appearance", fallback: "Appearance")
|
||||
/// App Icon
|
||||
@ -46,8 +52,22 @@ internal enum L10n {
|
||||
internal static let audio = L10n.tr("Localizable", "audio", fallback: "Audio")
|
||||
/// Audio & Captions
|
||||
internal static let audioAndCaptions = L10n.tr("Localizable", "audioAndCaptions", fallback: "Audio & Captions")
|
||||
/// The audio bit depth is not supported
|
||||
internal static let audioBitDepthNotSupported = L10n.tr("Localizable", "audioBitDepthNotSupported", fallback: "The audio bit depth is not supported")
|
||||
/// The audio bitrate is not supported
|
||||
internal static let audioBitrateNotSupported = L10n.tr("Localizable", "audioBitrateNotSupported", fallback: "The audio bitrate is not supported")
|
||||
/// The number of audio channels is not supported
|
||||
internal static let audioChannelsNotSupported = L10n.tr("Localizable", "audioChannelsNotSupported", fallback: "The number of audio channels is not supported")
|
||||
/// The audio codec is not supported
|
||||
internal static let audioCodecNotSupported = L10n.tr("Localizable", "audioCodecNotSupported", fallback: "The audio codec is not supported")
|
||||
/// The audio track is external and requires transcoding
|
||||
internal static let audioIsExternal = L10n.tr("Localizable", "audioIsExternal", fallback: "The audio track is external and requires transcoding")
|
||||
/// Audio Offset
|
||||
internal static let audioOffset = L10n.tr("Localizable", "audioOffset", fallback: "Audio Offset")
|
||||
/// The audio profile is not supported
|
||||
internal static let audioProfileNotSupported = L10n.tr("Localizable", "audioProfileNotSupported", fallback: "The audio profile is not supported")
|
||||
/// The audio sample rate is not supported
|
||||
internal static let audioSampleRateNotSupported = L10n.tr("Localizable", "audioSampleRateNotSupported", fallback: "The audio sample rate is not supported")
|
||||
/// Audio Track
|
||||
internal static let audioTrack = L10n.tr("Localizable", "audioTrack", fallback: "Audio Track")
|
||||
/// Authorize
|
||||
@ -112,12 +132,18 @@ internal enum L10n {
|
||||
internal static let buttons = L10n.tr("Localizable", "buttons", fallback: "Buttons")
|
||||
/// Cancel
|
||||
internal static let cancel = L10n.tr("Localizable", "cancel", fallback: "Cancel")
|
||||
/// Cancelled
|
||||
internal static let canceled = L10n.tr("Localizable", "canceled", fallback: "Cancelled")
|
||||
/// Cancelling...
|
||||
internal static let cancelling = L10n.tr("Localizable", "cancelling", fallback: "Cancelling...")
|
||||
/// Cannot connect to host
|
||||
internal static let cannotConnectToHost = L10n.tr("Localizable", "cannotConnectToHost", fallback: "Cannot connect to host")
|
||||
/// CAST
|
||||
internal static let cast = L10n.tr("Localizable", "cast", fallback: "CAST")
|
||||
/// Cast & Crew
|
||||
internal static let castAndCrew = L10n.tr("Localizable", "castAndCrew", fallback: "Cast & Crew")
|
||||
/// Category
|
||||
internal static let category = L10n.tr("Localizable", "category", fallback: "Category")
|
||||
/// Change Server
|
||||
internal static let changeServer = L10n.tr("Localizable", "changeServer", fallback: "Change Server")
|
||||
/// Channels
|
||||
@ -132,6 +158,8 @@ internal enum L10n {
|
||||
internal static let cinematicBackground = L10n.tr("Localizable", "cinematicBackground", fallback: "Cinematic Background")
|
||||
/// Cinematic Views
|
||||
internal static let cinematicViews = L10n.tr("Localizable", "cinematicViews", fallback: "Cinematic Views")
|
||||
/// Client
|
||||
internal static let client = L10n.tr("Localizable", "client", fallback: "Client")
|
||||
/// Close
|
||||
internal static let close = L10n.tr("Localizable", "close", fallback: "Close")
|
||||
/// Closed Captions
|
||||
@ -152,6 +180,8 @@ internal enum L10n {
|
||||
internal static let compatibility = L10n.tr("Localizable", "compatibility", fallback: "Compatibility")
|
||||
/// Most Compatible
|
||||
internal static let compatible = L10n.tr("Localizable", "compatible", fallback: "Most Compatible")
|
||||
/// Confirm
|
||||
internal static let confirm = L10n.tr("Localizable", "confirm", fallback: "Confirm")
|
||||
/// Confirm Close
|
||||
internal static let confirmClose = L10n.tr("Localizable", "confirmClose", fallback: "Confirm Close")
|
||||
/// Connect
|
||||
@ -166,6 +196,10 @@ internal enum L10n {
|
||||
internal static let connectToJellyfinServerStart = L10n.tr("Localizable", "connectToJellyfinServerStart", fallback: "Connect to a Jellyfin server to get started")
|
||||
/// Connect to Server
|
||||
internal static let connectToServer = L10n.tr("Localizable", "connectToServer", fallback: "Connect to Server")
|
||||
/// The container bitrate exceeds the allowed limit
|
||||
internal static let containerBitrateExceedsLimit = L10n.tr("Localizable", "containerBitrateExceedsLimit", fallback: "The container bitrate exceeds the allowed limit")
|
||||
/// The container format is not supported
|
||||
internal static let containerNotSupported = L10n.tr("Localizable", "containerNotSupported", fallback: "The container format is not supported")
|
||||
/// Containers
|
||||
internal static let containers = L10n.tr("Localizable", "containers", fallback: "Containers")
|
||||
/// Continue
|
||||
@ -190,6 +224,10 @@ internal enum L10n {
|
||||
internal static let customProfile = L10n.tr("Localizable", "customProfile", fallback: "Custom Profile")
|
||||
/// Dark
|
||||
internal static let dark = L10n.tr("Localizable", "dark", fallback: "Dark")
|
||||
/// Dashboard
|
||||
internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard")
|
||||
/// Perform administrative tasks for your Jellyfin server.
|
||||
internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.")
|
||||
/// Default Scheme
|
||||
internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: "Default Scheme")
|
||||
/// Delete
|
||||
@ -198,12 +236,20 @@ internal enum L10n {
|
||||
internal static let deleteServer = L10n.tr("Localizable", "deleteServer", fallback: "Delete Server")
|
||||
/// Delivery
|
||||
internal static let delivery = L10n.tr("Localizable", "delivery", fallback: "Delivery")
|
||||
/// Device
|
||||
internal static let device = L10n.tr("Localizable", "device", fallback: "Device")
|
||||
/// Device Profile
|
||||
internal static let deviceProfile = L10n.tr("Localizable", "deviceProfile", fallback: "Device Profile")
|
||||
/// Direct Play
|
||||
internal static let direct = L10n.tr("Localizable", "direct", fallback: "Direct Play")
|
||||
/// DIRECTOR
|
||||
internal static let director = L10n.tr("Localizable", "director", fallback: "DIRECTOR")
|
||||
/// Direct Play
|
||||
internal static let directPlay = L10n.tr("Localizable", "directPlay", fallback: "Direct Play")
|
||||
/// An error occurred during direct play
|
||||
internal static let directPlayError = L10n.tr("Localizable", "directPlayError", fallback: "An error occurred during direct play")
|
||||
/// Direct Stream
|
||||
internal static let directStream = L10n.tr("Localizable", "directStream", fallback: "Direct Stream")
|
||||
/// Disabled
|
||||
internal static let disabled = L10n.tr("Localizable", "disabled", fallback: "Disabled")
|
||||
/// Discovered Servers
|
||||
@ -214,6 +260,8 @@ internal enum L10n {
|
||||
internal static let displayOrder = L10n.tr("Localizable", "displayOrder", fallback: "Display order")
|
||||
/// Downloads
|
||||
internal static let downloads = L10n.tr("Localizable", "downloads", fallback: "Downloads")
|
||||
/// Edit
|
||||
internal static let edit = L10n.tr("Localizable", "edit", fallback: "Edit")
|
||||
/// Edit Jump Lengths
|
||||
internal static let editJumpLengths = L10n.tr("Localizable", "editJumpLengths", fallback: "Edit Jump Lengths")
|
||||
/// Edit Server
|
||||
@ -248,6 +296,10 @@ internal enum L10n {
|
||||
internal static let filterResults = L10n.tr("Localizable", "filterResults", fallback: "Filter Results")
|
||||
/// Filters
|
||||
internal static let filters = L10n.tr("Localizable", "filters", fallback: "Filters")
|
||||
/// %@fps
|
||||
internal static func fpsWithString(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "fpsWithString", String(describing: p1), fallback: "%@fps")
|
||||
}
|
||||
/// Genres
|
||||
internal static let genres = L10n.tr("Localizable", "genres", fallback: "Genres")
|
||||
/// Gestures
|
||||
@ -264,10 +316,16 @@ internal enum L10n {
|
||||
internal static let indicators = L10n.tr("Localizable", "indicators", fallback: "Indicators")
|
||||
/// Information
|
||||
internal static let information = L10n.tr("Localizable", "information", fallback: "Information")
|
||||
/// Interlaced video is not supported
|
||||
internal static let interlacedVideoNotSupported = L10n.tr("Localizable", "interlacedVideoNotSupported", fallback: "Interlaced video is not supported")
|
||||
/// Inverted Dark
|
||||
internal static let invertedDark = L10n.tr("Localizable", "invertedDark", fallback: "Inverted Dark")
|
||||
/// Inverted Light
|
||||
internal static let invertedLight = L10n.tr("Localizable", "invertedLight", fallback: "Inverted Light")
|
||||
/// %1$@ / %2$@
|
||||
internal static func itemOverItem(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "itemOverItem", String(describing: p1), String(describing: p2), fallback: "%1$@ / %2$@")
|
||||
}
|
||||
/// Items
|
||||
internal static let items = L10n.tr("Localizable", "items", fallback: "Items")
|
||||
/// Jellyfin
|
||||
@ -294,6 +352,14 @@ internal enum L10n {
|
||||
internal static let larger = L10n.tr("Localizable", "larger", fallback: "Larger")
|
||||
/// Largest
|
||||
internal static let largest = L10n.tr("Localizable", "largest", fallback: "Largest")
|
||||
/// Last run
|
||||
internal static let lastRun = L10n.tr("Localizable", "lastRun", fallback: "Last run")
|
||||
/// Last ran %@
|
||||
internal static func lastRunTime(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "lastRunTime", String(describing: p1), fallback: "Last ran %@")
|
||||
}
|
||||
/// Last Seen
|
||||
internal static let lastSeen = L10n.tr("Localizable", "lastSeen", fallback: "Last Seen")
|
||||
/// Latest %@
|
||||
internal static func latestWithString(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "latestWithString", String(describing: p1), fallback: "Latest %@")
|
||||
@ -330,6 +396,8 @@ internal enum L10n {
|
||||
internal static let media = L10n.tr("Localizable", "media", fallback: "Media")
|
||||
/// Menu Buttons
|
||||
internal static let menuButtons = L10n.tr("Localizable", "menuButtons", fallback: "Menu Buttons")
|
||||
/// Method
|
||||
internal static let method = L10n.tr("Localizable", "method", fallback: "Method")
|
||||
/// Missing
|
||||
internal static let missing = L10n.tr("Localizable", "missing", fallback: "Missing")
|
||||
/// Missing Items
|
||||
@ -350,6 +418,8 @@ internal enum L10n {
|
||||
internal static let networking = L10n.tr("Localizable", "networking", fallback: "Networking")
|
||||
/// Network timed out
|
||||
internal static let networkTimedOut = L10n.tr("Localizable", "networkTimedOut", fallback: "Network timed out")
|
||||
/// Never run
|
||||
internal static let neverRun = L10n.tr("Localizable", "neverRun", fallback: "Never run")
|
||||
/// News
|
||||
internal static let news = L10n.tr("Localizable", "news", fallback: "News")
|
||||
/// Next
|
||||
@ -376,6 +446,8 @@ internal enum L10n {
|
||||
internal static let noResults = L10n.tr("Localizable", "noResults", fallback: "No results.")
|
||||
/// Normal
|
||||
internal static let normal = L10n.tr("Localizable", "normal", fallback: "Normal")
|
||||
/// No session
|
||||
internal static let noSession = L10n.tr("Localizable", "noSession", fallback: "No session")
|
||||
/// N/A
|
||||
internal static let notAvailableSlash = L10n.tr("Localizable", "notAvailableSlash", fallback: "N/A")
|
||||
/// Type: %@ not implemented yet :(
|
||||
@ -390,6 +462,8 @@ internal enum L10n {
|
||||
internal static let ok = L10n.tr("Localizable", "ok", fallback: "Ok")
|
||||
/// 1 user
|
||||
internal static let oneUser = L10n.tr("Localizable", "oneUser", fallback: "1 user")
|
||||
/// Online
|
||||
internal static let online = L10n.tr("Localizable", "online", fallback: "Online")
|
||||
/// On Now
|
||||
internal static let onNow = L10n.tr("Localizable", "onNow", fallback: "On Now")
|
||||
/// Operating System
|
||||
@ -496,6 +570,8 @@ internal enum L10n {
|
||||
internal static let recommended = L10n.tr("Localizable", "recommended", fallback: "Recommended")
|
||||
/// Red
|
||||
internal static let red = L10n.tr("Localizable", "red", fallback: "Red")
|
||||
/// The number of reference frames is not supported
|
||||
internal static let refFramesNotSupported = L10n.tr("Localizable", "refFramesNotSupported", fallback: "The number of reference frames is not supported")
|
||||
/// Refresh
|
||||
internal static let refresh = L10n.tr("Localizable", "refresh", fallback: "Refresh")
|
||||
/// Regular
|
||||
@ -526,6 +602,10 @@ internal enum L10n {
|
||||
internal static let resetAppSettings = L10n.tr("Localizable", "resetAppSettings", fallback: "Reset App Settings")
|
||||
/// Reset User Settings
|
||||
internal static let resetUserSettings = L10n.tr("Localizable", "resetUserSettings", fallback: "Reset User Settings")
|
||||
/// Restart Server
|
||||
internal static let restartServer = L10n.tr("Localizable", "restartServer", fallback: "Restart Server")
|
||||
/// Are you sure you want to restart the server?
|
||||
internal static let restartWarning = L10n.tr("Localizable", "restartWarning", fallback: "Are you sure you want to restart the server?")
|
||||
/// Resume
|
||||
internal static let resume = L10n.tr("Localizable", "resume", fallback: "Resume")
|
||||
/// Resume 5 Second Offset
|
||||
@ -542,8 +622,16 @@ internal enum L10n {
|
||||
internal static let retry = L10n.tr("Localizable", "retry", fallback: "Retry")
|
||||
/// Right
|
||||
internal static let `right` = L10n.tr("Localizable", "right", fallback: "Right")
|
||||
/// Run
|
||||
internal static let run = L10n.tr("Localizable", "run", fallback: "Run")
|
||||
/// Running...
|
||||
internal static let running = L10n.tr("Localizable", "running", fallback: "Running...")
|
||||
/// Runtime
|
||||
internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime")
|
||||
/// Scan All Libraries
|
||||
internal static let scanAllLibraries = L10n.tr("Localizable", "scanAllLibraries", fallback: "Scan All Libraries")
|
||||
/// Scheduled Tasks
|
||||
internal static let scheduledTasks = L10n.tr("Localizable", "scheduledTasks", fallback: "Scheduled Tasks")
|
||||
/// Scrub Current Time
|
||||
internal static let scrubCurrentTime = L10n.tr("Localizable", "scrubCurrentTime", fallback: "Scrub Current Time")
|
||||
/// Search
|
||||
@ -560,6 +648,8 @@ internal enum L10n {
|
||||
}
|
||||
/// Seasons
|
||||
internal static let seasons = L10n.tr("Localizable", "seasons", fallback: "Seasons")
|
||||
/// Secondary audio is not supported
|
||||
internal static let secondaryAudioNotSupported = L10n.tr("Localizable", "secondaryAudioNotSupported", fallback: "Secondary audio is not supported")
|
||||
/// See All
|
||||
internal static let seeAll = L10n.tr("Localizable", "seeAll", fallback: "See All")
|
||||
/// Seek Slide Gesture Enabled
|
||||
@ -586,10 +676,14 @@ internal enum L10n {
|
||||
internal static let serverDetails = L10n.tr("Localizable", "serverDetails", fallback: "Server Details")
|
||||
/// Server Information
|
||||
internal static let serverInformation = L10n.tr("Localizable", "serverInformation", fallback: "Server Information")
|
||||
/// Server Logs
|
||||
internal static let serverLogs = L10n.tr("Localizable", "serverLogs", fallback: "Server Logs")
|
||||
/// Servers
|
||||
internal static let servers = L10n.tr("Localizable", "servers", fallback: "Servers")
|
||||
/// Server URL
|
||||
internal static let serverURL = L10n.tr("Localizable", "serverURL", fallback: "Server URL")
|
||||
/// Session
|
||||
internal static let session = L10n.tr("Localizable", "session", fallback: "Session")
|
||||
/// Settings
|
||||
internal static let settings = L10n.tr("Localizable", "settings", fallback: "Settings")
|
||||
/// Show Cast & Crew
|
||||
@ -616,6 +710,10 @@ internal enum L10n {
|
||||
internal static let showUnwatched = L10n.tr("Localizable", "showUnwatched", fallback: "Show Unwatched")
|
||||
/// Show Watched
|
||||
internal static let showWatched = L10n.tr("Localizable", "showWatched", fallback: "Show Watched")
|
||||
/// Shutdown Server
|
||||
internal static let shutdownServer = L10n.tr("Localizable", "shutdownServer", fallback: "Shutdown Server")
|
||||
/// Are you sure you want to shutdown the server?
|
||||
internal static let shutdownWarning = L10n.tr("Localizable", "shutdownWarning", fallback: "Are you sure you want to shutdown the server?")
|
||||
/// Signed in as %@
|
||||
internal static func signedInAsWithString(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "signedInAsWithString", String(describing: p1), fallback: "Signed in as %@")
|
||||
@ -648,12 +746,18 @@ internal enum L10n {
|
||||
internal static let specialFeatures = L10n.tr("Localizable", "specialFeatures", fallback: "Special Features")
|
||||
/// Sports
|
||||
internal static let sports = L10n.tr("Localizable", "sports", fallback: "Sports")
|
||||
/// Stop
|
||||
internal static let stop = L10n.tr("Localizable", "stop", fallback: "Stop")
|
||||
/// Streams
|
||||
internal static let streams = L10n.tr("Localizable", "streams", fallback: "Streams")
|
||||
/// STUDIO
|
||||
internal static let studio = L10n.tr("Localizable", "studio", fallback: "STUDIO")
|
||||
/// Studios
|
||||
internal static let studios = L10n.tr("Localizable", "studios", fallback: "Studios")
|
||||
/// Subtitle
|
||||
internal static let subtitle = L10n.tr("Localizable", "subtitle", fallback: "Subtitle")
|
||||
/// The subtitle codec is not supported
|
||||
internal static let subtitleCodecNotSupported = L10n.tr("Localizable", "subtitleCodecNotSupported", fallback: "The subtitle codec is not supported")
|
||||
/// Subtitle Color
|
||||
internal static let subtitleColor = L10n.tr("Localizable", "subtitleColor", fallback: "Subtitle Color")
|
||||
/// Subtitle Font
|
||||
@ -676,6 +780,20 @@ internal enum L10n {
|
||||
internal static let systemControlGesturesEnabled = L10n.tr("Localizable", "systemControlGesturesEnabled", fallback: "System Control Gestures Enabled")
|
||||
/// Tags
|
||||
internal static let tags = L10n.tr("Localizable", "tags", fallback: "Tags")
|
||||
/// Task
|
||||
internal static let task = L10n.tr("Localizable", "task", fallback: "Task")
|
||||
/// Aborted
|
||||
internal static let taskAborted = L10n.tr("Localizable", "taskAborted", fallback: "Aborted")
|
||||
/// Cancelled
|
||||
internal static let taskCancelled = L10n.tr("Localizable", "taskCancelled", fallback: "Cancelled")
|
||||
/// Completed
|
||||
internal static let taskCompleted = L10n.tr("Localizable", "taskCompleted", fallback: "Completed")
|
||||
/// Failed
|
||||
internal static let taskFailed = L10n.tr("Localizable", "taskFailed", fallback: "Failed")
|
||||
/// Tasks
|
||||
internal static let tasks = L10n.tr("Localizable", "tasks", fallback: "Tasks")
|
||||
/// Tasks are operations that are scheduled to run periodically or can be triggered manually.
|
||||
internal static let tasksDescription = L10n.tr("Localizable", "tasksDescription", fallback: "Tasks are operations that are scheduled to run periodically or can be triggered manually.")
|
||||
/// Test Size
|
||||
internal static let testSize = L10n.tr("Localizable", "testSize", fallback: "Test Size")
|
||||
/// Timestamp
|
||||
@ -686,6 +804,10 @@ internal enum L10n {
|
||||
internal static let tooManyRedirects = L10n.tr("Localizable", "tooManyRedirects", fallback: "Too Many Redirects")
|
||||
/// Trailing Value
|
||||
internal static let trailingValue = L10n.tr("Localizable", "trailingValue", fallback: "Trailing Value")
|
||||
/// Transcode
|
||||
internal static let transcode = L10n.tr("Localizable", "transcode", fallback: "Transcode")
|
||||
/// Transcode Reason(s)
|
||||
internal static let transcodeReasons = L10n.tr("Localizable", "transcodeReasons", fallback: "Transcode Reason(s)")
|
||||
/// Transition
|
||||
internal static let transition = L10n.tr("Localizable", "transition", fallback: "Transition")
|
||||
/// Try again
|
||||
@ -704,8 +826,12 @@ internal enum L10n {
|
||||
internal static let unauthorizedUser = L10n.tr("Localizable", "unauthorizedUser", fallback: "Unauthorized user")
|
||||
/// Unknown
|
||||
internal static let unknown = L10n.tr("Localizable", "unknown", fallback: "Unknown")
|
||||
/// The audio stream information is unknown
|
||||
internal static let unknownAudioStreamInfo = L10n.tr("Localizable", "unknownAudioStreamInfo", fallback: "The audio stream information is unknown")
|
||||
/// Unknown Error
|
||||
internal static let unknownError = L10n.tr("Localizable", "unknownError", fallback: "Unknown Error")
|
||||
/// The video stream information is unknown
|
||||
internal static let unknownVideoStreamInfo = L10n.tr("Localizable", "unknownVideoStreamInfo", fallback: "The video stream information is unknown")
|
||||
/// Unplayed
|
||||
internal static let unplayed = L10n.tr("Localizable", "unplayed", fallback: "Unplayed")
|
||||
/// URL
|
||||
@ -728,10 +854,26 @@ internal enum L10n {
|
||||
internal static let version = L10n.tr("Localizable", "version", fallback: "Version")
|
||||
/// Video
|
||||
internal static let video = L10n.tr("Localizable", "video", fallback: "Video")
|
||||
/// The video bit depth is not supported
|
||||
internal static let videoBitDepthNotSupported = L10n.tr("Localizable", "videoBitDepthNotSupported", fallback: "The video bit depth is not supported")
|
||||
/// The video bitrate is not supported
|
||||
internal static let videoBitrateNotSupported = L10n.tr("Localizable", "videoBitrateNotSupported", fallback: "The video bitrate is not supported")
|
||||
/// The video codec is not supported
|
||||
internal static let videoCodecNotSupported = L10n.tr("Localizable", "videoCodecNotSupported", fallback: "The video codec is not supported")
|
||||
/// The video framerate is not supported
|
||||
internal static let videoFramerateNotSupported = L10n.tr("Localizable", "videoFramerateNotSupported", fallback: "The video framerate is not supported")
|
||||
/// The video level is not supported
|
||||
internal static let videoLevelNotSupported = L10n.tr("Localizable", "videoLevelNotSupported", fallback: "The video level is not supported")
|
||||
/// Video Player
|
||||
internal static let videoPlayer = L10n.tr("Localizable", "videoPlayer", fallback: "Video Player")
|
||||
/// Video Player Type
|
||||
internal static let videoPlayerType = L10n.tr("Localizable", "videoPlayerType", fallback: "Video Player Type")
|
||||
/// The video profile is not supported
|
||||
internal static let videoProfileNotSupported = L10n.tr("Localizable", "videoProfileNotSupported", fallback: "The video profile is not supported")
|
||||
/// The video range type is not supported
|
||||
internal static let videoRangeTypeNotSupported = L10n.tr("Localizable", "videoRangeTypeNotSupported", fallback: "The video range type is not supported")
|
||||
/// The video resolution is not supported
|
||||
internal static let videoResolutionNotSupported = L10n.tr("Localizable", "videoResolutionNotSupported", fallback: "The video resolution is not supported")
|
||||
/// Who's watching?
|
||||
internal static let whosWatching = L10n.tr("Localizable", "WhosWatching", fallback: "Who's watching?")
|
||||
/// WIP
|
||||
|
@ -89,6 +89,14 @@ extension StoredValues.Keys {
|
||||
)
|
||||
}
|
||||
|
||||
static var accessPolicy: Key<UserAccessPolicy> {
|
||||
CurrentUserKey(
|
||||
"currentUserAccessPolicy",
|
||||
domain: "currentUserAccessPolicy",
|
||||
default: .none
|
||||
)
|
||||
}
|
||||
|
||||
static func libraryDisplayType(parentID: String?) -> Key<LibraryDisplayType> {
|
||||
CurrentUserKey(
|
||||
parentID,
|
||||
|
@ -64,6 +64,10 @@ extension UserState {
|
||||
}
|
||||
}
|
||||
|
||||
var isAdministrator: Bool {
|
||||
data.policy?.isAdministrator ?? false
|
||||
}
|
||||
|
||||
var pinHint: String {
|
||||
get {
|
||||
StoredValues[.User.pinHint(id: id)]
|
||||
|
172
Shared/ViewModels/ActiveSessionsViewModel.swift
Normal file
172
Shared/ViewModels/ActiveSessionsViewModel.swift
Normal file
@ -0,0 +1,172 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import OrderedCollections
|
||||
import SwiftUI
|
||||
|
||||
final class ActiveSessionsViewModel: ViewModel, Stateful {
|
||||
|
||||
// MARK: - Action
|
||||
|
||||
enum Action: Equatable {
|
||||
case getSessions
|
||||
case refreshSessions
|
||||
}
|
||||
|
||||
// MARK: - BackgroundState
|
||||
|
||||
enum BackgroundState: Hashable {
|
||||
case gettingSessions
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
enum State: Hashable {
|
||||
case content
|
||||
case error(JellyfinAPIError)
|
||||
case initial
|
||||
}
|
||||
|
||||
@Published
|
||||
final var backgroundStates: OrderedSet<BackgroundState> = []
|
||||
@Published
|
||||
final var sessions: OrderedDictionary<String, BindingBox<SessionInfo?>> = [:]
|
||||
@Published
|
||||
final var state: State = .initial
|
||||
|
||||
private let activeWithinSeconds: Int = 960
|
||||
private var sessionTask: AnyCancellable?
|
||||
|
||||
func respond(to action: Action) -> State {
|
||||
switch action {
|
||||
case .getSessions:
|
||||
sessionTask?.cancel()
|
||||
|
||||
sessionTask = Task { [weak self] in
|
||||
await MainActor.run {
|
||||
let _ = self?.backgroundStates.append(.gettingSessions)
|
||||
}
|
||||
|
||||
do {
|
||||
try await self?.updateSessions()
|
||||
} catch {
|
||||
guard let self else { return }
|
||||
await MainActor.run {
|
||||
self.state = .error(.init(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
let _ = self?.backgroundStates.remove(.gettingSessions)
|
||||
}
|
||||
}
|
||||
.asAnyCancellable()
|
||||
|
||||
return state
|
||||
case .refreshSessions:
|
||||
sessionTask?.cancel()
|
||||
|
||||
sessionTask = Task { [weak self] in
|
||||
await MainActor.run {
|
||||
self?.state = .initial
|
||||
}
|
||||
|
||||
do {
|
||||
try await self?.updateSessions()
|
||||
|
||||
guard let self else { return }
|
||||
|
||||
await MainActor.run {
|
||||
self.state = .content
|
||||
}
|
||||
} catch {
|
||||
guard let self else { return }
|
||||
await MainActor.run {
|
||||
self.state = .error(.init(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
.asAnyCancellable()
|
||||
|
||||
return .initial
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSessions() async throws {
|
||||
var parameters = Paths.GetSessionsParameters()
|
||||
parameters.activeWithinSeconds = activeWithinSeconds
|
||||
|
||||
let request = Paths.getSessions(parameters: parameters)
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
let removedSessionIDs = sessions.keys.filter { !response.value.map(\.id).contains($0) }
|
||||
|
||||
let existingIDs = sessions.keys
|
||||
.filter {
|
||||
response.value.map(\.id).contains($0)
|
||||
}
|
||||
let newSessions = response.value
|
||||
.filter {
|
||||
guard let id = $0.id else { return false }
|
||||
return !sessions.keys.contains(id)
|
||||
}
|
||||
.map { s in
|
||||
BindingBox<SessionInfo?>(
|
||||
source: .init(
|
||||
get: { s },
|
||||
set: { _ in }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
for id in removedSessionIDs {
|
||||
let t = sessions[id]
|
||||
sessions[id] = nil
|
||||
t?.value = nil
|
||||
}
|
||||
|
||||
for id in existingIDs {
|
||||
sessions[id]?.value = response.value.first(where: { $0.id == id })
|
||||
}
|
||||
|
||||
for session in newSessions {
|
||||
guard let id = session.value?.id else { continue }
|
||||
|
||||
sessions[id] = session
|
||||
}
|
||||
|
||||
sessions.sort { x, y in
|
||||
let xs = x.value.value
|
||||
let ys = y.value.value
|
||||
|
||||
let isPlaying0 = xs?.nowPlayingItem != nil
|
||||
let isPlaying1 = ys?.nowPlayingItem != nil
|
||||
|
||||
if isPlaying0 && !isPlaying1 {
|
||||
return true
|
||||
} else if !isPlaying0 && isPlaying1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if xs?.userName != ys?.userName {
|
||||
return (xs?.userName ?? "") < (ys?.userName ?? "")
|
||||
}
|
||||
|
||||
if isPlaying0 && isPlaying1 {
|
||||
return (xs?.nowPlayingItem?.name ?? "") < (ys?.nowPlayingItem?.name ?? "")
|
||||
} else {
|
||||
return (xs?.lastActivityDate ?? Date.now) > (ys?.lastActivityDate ?? Date.now)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
156
Shared/ViewModels/ScheduledTasksViewModel.swift
Normal file
156
Shared/ViewModels/ScheduledTasksViewModel.swift
Normal file
@ -0,0 +1,156 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import OrderedCollections
|
||||
import SwiftUI
|
||||
|
||||
// TODO: do something for errors from restart/shutdown
|
||||
// - toast?
|
||||
|
||||
final class ScheduledTasksViewModel: ViewModel, Stateful {
|
||||
|
||||
// MARK: - Action
|
||||
|
||||
enum Action: Equatable {
|
||||
case restartApplication
|
||||
case shutdownApplication
|
||||
case getTasks
|
||||
case refreshTasks
|
||||
}
|
||||
|
||||
// MARK: - BackgroundState
|
||||
|
||||
enum BackgroundState: Hashable {
|
||||
case gettingTasks
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
enum State: Hashable {
|
||||
case content
|
||||
case error(JellyfinAPIError)
|
||||
case initial
|
||||
}
|
||||
|
||||
@Published
|
||||
final var backgroundStates: OrderedSet<BackgroundState> = []
|
||||
@Published
|
||||
final var state: State = .initial
|
||||
@Published
|
||||
final var tasks: OrderedDictionary<String, [ServerTaskObserver]> = [:]
|
||||
|
||||
private var getTasksCancellable: AnyCancellable?
|
||||
|
||||
func respond(to action: Action) -> State {
|
||||
switch action {
|
||||
case .restartApplication:
|
||||
Task {
|
||||
try await sendRestartRequest()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
return .content
|
||||
case .shutdownApplication:
|
||||
Task {
|
||||
try await sendShutdownRequest()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
return .content
|
||||
case .getTasks:
|
||||
getTasksCancellable?.cancel()
|
||||
|
||||
getTasksCancellable = Task {
|
||||
do {
|
||||
try await getTasks()
|
||||
|
||||
await MainActor.run {
|
||||
self.state = .content
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.state = .error(.init(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
.asAnyCancellable()
|
||||
|
||||
return state
|
||||
case .refreshTasks:
|
||||
tasks.removeAll()
|
||||
getTasksCancellable?.cancel()
|
||||
|
||||
getTasksCancellable = Task {
|
||||
do {
|
||||
await MainActor.run {
|
||||
self.state = .initial
|
||||
}
|
||||
|
||||
try await getTasks()
|
||||
|
||||
await MainActor.run {
|
||||
self.state = .content
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.state = .error(.init(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
.asAnyCancellable()
|
||||
|
||||
return .initial
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Get All Tasks
|
||||
|
||||
// TODO: update tasks like `ActiveSessionsViewModel`
|
||||
private func getTasks() async throws {
|
||||
let request = Paths.getTasks(isHidden: false, isEnabled: true)
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
if tasks.isEmpty {
|
||||
let observers = response.value
|
||||
.sorted(using: \.category)
|
||||
.map { ServerTaskObserver(task: $0) }
|
||||
|
||||
let newTasks = OrderedDictionary(grouping: observers, by: { $0.task.category ?? "" })
|
||||
|
||||
await MainActor.run {
|
||||
self.tasks = newTasks
|
||||
}
|
||||
}
|
||||
|
||||
for runningTask in response.value where runningTask.state == .running {
|
||||
if let observer = tasks.values
|
||||
.flatMap(\.self)
|
||||
.first(where: { $0.task.id == runningTask.id })
|
||||
{
|
||||
await observer.send(.start)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Restart Application
|
||||
|
||||
private func sendRestartRequest() async throws {
|
||||
let request = Paths.restartApplication
|
||||
try await userSession.client.send(request)
|
||||
}
|
||||
|
||||
// MARK: - Shutdown Application
|
||||
|
||||
private func sendShutdownRequest() async throws {
|
||||
let request = Paths.shutdownApplication
|
||||
try await userSession.client.send(request)
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import CoreStore
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
class EditServerViewModel: ViewModel {
|
||||
class ServerConnectionViewModel: ViewModel {
|
||||
|
||||
@Published
|
||||
var server: ServerState
|
64
Shared/ViewModels/ServerLogsViewModel.swift
Normal file
64
Shared/ViewModels/ServerLogsViewModel.swift
Normal file
@ -0,0 +1,64 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import OrderedCollections
|
||||
import SwiftUI
|
||||
|
||||
final class ServerLogsViewModel: ViewModel, Stateful {
|
||||
|
||||
enum Action: Equatable {
|
||||
case getLogs
|
||||
}
|
||||
|
||||
enum State: Hashable {
|
||||
case content
|
||||
case initial
|
||||
case error(JellyfinAPIError)
|
||||
}
|
||||
|
||||
@Published
|
||||
private(set) var logs: OrderedSet<LogFile> = []
|
||||
@Published
|
||||
final var state: State = .initial
|
||||
@Published
|
||||
final var lastAction: Action?
|
||||
|
||||
func respond(to action: Action) -> State {
|
||||
switch action {
|
||||
case .getLogs:
|
||||
cancellables.removeAll()
|
||||
|
||||
Task {
|
||||
do {
|
||||
let newLogs = try await getLogs()
|
||||
|
||||
await MainActor.run {
|
||||
self.logs = newLogs
|
||||
self.state = .content
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.state = .error(.init(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
return .initial
|
||||
}
|
||||
}
|
||||
|
||||
private func getLogs() async throws -> OrderedSet<LogFile> {
|
||||
let request = Paths.getServerLogs
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
return OrderedSet(response.value)
|
||||
}
|
||||
}
|
127
Shared/ViewModels/ServerTaskObserver.swift
Normal file
127
Shared/ViewModels/ServerTaskObserver.swift
Normal file
@ -0,0 +1,127 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
// TODO: refactor with socket implementation
|
||||
// TODO: edit triggers
|
||||
|
||||
final class ServerTaskObserver: ViewModel, Stateful, Identifiable {
|
||||
|
||||
enum Action: Equatable {
|
||||
case start
|
||||
case stop
|
||||
case stopObserving
|
||||
}
|
||||
|
||||
enum State: Hashable {
|
||||
case error(JellyfinAPIError)
|
||||
case initial
|
||||
case running
|
||||
}
|
||||
|
||||
@Published
|
||||
final var state: State = .initial
|
||||
@Published
|
||||
private(set) var task: TaskInfo
|
||||
|
||||
private var progressCancellable: AnyCancellable?
|
||||
private var cancelCancellable: AnyCancellable?
|
||||
|
||||
var id: String? { task.id }
|
||||
|
||||
init(task: TaskInfo) {
|
||||
self.task = task
|
||||
}
|
||||
|
||||
func respond(to action: Action) -> State {
|
||||
switch action {
|
||||
case .start:
|
||||
if case .running = state {
|
||||
return state
|
||||
}
|
||||
|
||||
progressCancellable = Task {
|
||||
do {
|
||||
try await start()
|
||||
|
||||
await MainActor.run {
|
||||
self.state = .initial
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.state = .error(.init(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
.asAnyCancellable()
|
||||
|
||||
return .running
|
||||
case .stop:
|
||||
progressCancellable?.cancel()
|
||||
cancelCancellable?.cancel()
|
||||
|
||||
cancelCancellable = Task {
|
||||
do {
|
||||
try await stop()
|
||||
|
||||
await MainActor.run {
|
||||
self.state = .initial
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.state = .error(.init(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
.asAnyCancellable()
|
||||
|
||||
return .initial
|
||||
case .stopObserving:
|
||||
progressCancellable?.cancel()
|
||||
cancelCancellable?.cancel()
|
||||
|
||||
return .initial
|
||||
}
|
||||
}
|
||||
|
||||
private func start() async throws {
|
||||
guard let id = task.id else { return }
|
||||
|
||||
let request = Paths.startTask(taskID: id)
|
||||
try await userSession.client.send(request)
|
||||
|
||||
try await pollTaskProgress(id: id)
|
||||
}
|
||||
|
||||
private func pollTaskProgress(id: String) async throws {
|
||||
while true {
|
||||
let request = Paths.getTask(taskID: id)
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
await MainActor.run {
|
||||
self.task = response.value
|
||||
}
|
||||
|
||||
guard response.value.state == .running || response.value.state == .cancelling else {
|
||||
break
|
||||
}
|
||||
|
||||
try await Task.sleep(nanoseconds: 2_000_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
private func stop() async throws {
|
||||
guard let id = task.id else { return }
|
||||
|
||||
let request = Paths.stopTask(taskID: id)
|
||||
try await userSession.client.send(request)
|
||||
}
|
||||
}
|
@ -20,10 +20,10 @@ struct EditServerView: View {
|
||||
private var isPresentingConfirmDeletion: Bool = false
|
||||
|
||||
@StateObject
|
||||
private var viewModel: EditServerViewModel
|
||||
private var viewModel: ServerConnectionViewModel
|
||||
|
||||
init(server: ServerState) {
|
||||
self._viewModel = StateObject(wrappedValue: EditServerViewModel(server: server))
|
||||
self._viewModel = StateObject(wrappedValue: ServerConnectionViewModel(server: server))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -9,11 +9,17 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; };
|
||||
091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; };
|
||||
4E0A8FFB2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */; };
|
||||
4E0A8FFC2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */; };
|
||||
4E16FD512C0183DB00110147 /* LetterPickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */; };
|
||||
4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD522C01840C00110147 /* LetterPickerBar.swift */; };
|
||||
4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; };
|
||||
4E16FD582C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; };
|
||||
4E182C9C2C94993200FBEFD5 /* ScheduledTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9B2C94993200FBEFD5 /* ScheduledTasksView.swift */; };
|
||||
4E182C9F2C94A1E000FBEFD5 /* ScheduledTaskButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9E2C94A1E000FBEFD5 /* ScheduledTaskButton.swift */; };
|
||||
4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */; };
|
||||
4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; };
|
||||
4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; };
|
||||
4E2AC4BE2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */; };
|
||||
4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */; };
|
||||
4E2AC4C22C6C491200DD600D /* AudoCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4C12C6C491200DD600D /* AudoCodec.swift */; };
|
||||
@ -29,6 +35,9 @@
|
||||
4E2AC4D62C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */; };
|
||||
4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */; };
|
||||
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; };
|
||||
4E63B9FA2C8A5BEF00C25378 /* UserDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */; };
|
||||
4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; };
|
||||
4E6C27082C8BD0AD00FD2185 /* ActiveSessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */; };
|
||||
4E71D6892C80910900A0174D /* EditCustomDeviceProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E71D6882C80910900A0174D /* EditCustomDeviceProfileView.swift */; };
|
||||
4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; };
|
||||
4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; };
|
||||
@ -41,6 +50,10 @@
|
||||
4E9A24E92C82B79D0023DA83 /* EditCustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */; };
|
||||
4E9A24EB2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */; };
|
||||
4E9A24ED2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift */; };
|
||||
4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; };
|
||||
4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; };
|
||||
4EB1A8CC2C9B1BA200F43898 /* ServerTaskButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* ServerTaskButton.swift */; };
|
||||
4EB1A8CE2C9B2D0800F43898 /* ActiveSessionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */; };
|
||||
4EBE06462C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */; };
|
||||
4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */; };
|
||||
4EBE064D2C7EB6D3004A6C03 /* VideoPlayerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE064C2C7EB6D3004A6C03 /* VideoPlayerType.swift */; };
|
||||
@ -52,6 +65,12 @@
|
||||
4EC1C8532C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8512C7FDFA300E2879E /* PlaybackDeviceProfile.swift */; };
|
||||
4EC1C8692C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8682C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift */; };
|
||||
4EC1C86D2C80903A00E2879E /* CustomProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C86C2C80903A00E2879E /* CustomProfileButton.swift */; };
|
||||
4EC50D612C934B3A00FC3D0E /* ScheduledTasksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC50D602C934B3A00FC3D0E /* ScheduledTasksViewModel.swift */; };
|
||||
4EC50D622C934B3A00FC3D0E /* ScheduledTasksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC50D602C934B3A00FC3D0E /* ScheduledTasksViewModel.swift */; };
|
||||
4EC6C16B2C92999800FC904B /* TranscodeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC6C16A2C92999800FC904B /* TranscodeSection.swift */; };
|
||||
4ECDAA9E2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; };
|
||||
4ECDAA9F2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; };
|
||||
4EE141692C8BABDF0045B661 /* ProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ProgressSection.swift */; };
|
||||
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; };
|
||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; };
|
||||
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; };
|
||||
@ -579,9 +598,9 @@
|
||||
E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E172D3AC2BAC9DF8007B4647 /* SeasonItemViewModel.swift */; };
|
||||
E172D3AE2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E172D3AC2BAC9DF8007B4647 /* SeasonItemViewModel.swift */; };
|
||||
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E172D3B12BACA569007B4647 /* EpisodeContent.swift */; };
|
||||
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; };
|
||||
E173DA5026D048D600CC4EB7 /* EditServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* EditServerView.swift */; };
|
||||
E173DA5226D04AAF00CC4EB7 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* Color.swift */; };
|
||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; };
|
||||
E173DA5426D050F500CC4EB7 /* ServerConnectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerConnectionViewModel.swift */; };
|
||||
E174120F29AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */; };
|
||||
E174121029AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */; };
|
||||
E175AFF3299AC117004DCF52 /* DebugSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */; };
|
||||
@ -707,7 +726,7 @@
|
||||
E193D549271941CC00900D82 /* UserSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D548271941CC00900D82 /* UserSignInView.swift */; };
|
||||
E193D54B271941D300900D82 /* SelectServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54A271941D300900D82 /* SelectServerView.swift */; };
|
||||
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54F2719430400900D82 /* ServerDetailView.swift */; };
|
||||
E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; };
|
||||
E193D5512719432400900D82 /* ServerConnectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerConnectionViewModel.swift */; };
|
||||
E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */; };
|
||||
E19D41A72BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19D41A62BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift */; };
|
||||
E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19D41A62BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift */; };
|
||||
@ -765,6 +784,8 @@
|
||||
E1B490452967E26300D3EDCE /* PersistentLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B490432967E26300D3EDCE /* PersistentLogHandler.swift */; };
|
||||
E1B490472967E2E500D3EDCE /* CoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B490462967E2E500D3EDCE /* CoreStore.swift */; };
|
||||
E1B490482967E2E500D3EDCE /* CoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B490462967E2E500D3EDCE /* CoreStore.swift */; };
|
||||
E1B4E4372CA7795200DC49DE /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B4E4362CA7795200DC49DE /* OrderedDictionary.swift */; };
|
||||
E1B4E4382CA7795200DC49DE /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B4E4362CA7795200DC49DE /* OrderedDictionary.swift */; };
|
||||
E1B5784128F8AFCB00D42911 /* WrappedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B5784028F8AFCB00D42911 /* WrappedView.swift */; };
|
||||
E1B5784228F8AFCB00D42911 /* WrappedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B5784028F8AFCB00D42911 /* WrappedView.swift */; };
|
||||
E1B5861229E32EEF00E45D6E /* Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B5861129E32EEF00E45D6E /* Sequence.swift */; };
|
||||
@ -933,6 +954,15 @@
|
||||
E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EA9F6928F8A79E00BEC442 /* VideoPlayerManager.swift */; };
|
||||
E1EBCB42278BD174009FE6E9 /* TruncatedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */; };
|
||||
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */; };
|
||||
E1ED7FD62CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FD52CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift */; };
|
||||
E1ED7FD82CA8AF7400ACB6E3 /* ServerTaskObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FD72CA8AF7400ACB6E3 /* ServerTaskObserver.swift */; };
|
||||
E1ED7FD92CA8AF7400ACB6E3 /* ServerTaskObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FD72CA8AF7400ACB6E3 /* ServerTaskObserver.swift */; };
|
||||
E1ED7FDB2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */; };
|
||||
E1ED7FDC2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */; };
|
||||
E1ED7FDE2CAA641F00ACB6E3 /* ListTitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FDD2CAA641F00ACB6E3 /* ListTitleSection.swift */; };
|
||||
E1ED7FE02CAA685900ACB6E3 /* ServerLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FDF2CAA685900ACB6E3 /* ServerLogsView.swift */; };
|
||||
E1ED7FE22CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FE12CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift */; };
|
||||
E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED7FE12CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift */; };
|
||||
E1ED91152B95897500802036 /* LatestInLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED91142B95897500802036 /* LatestInLibraryViewModel.swift */; };
|
||||
E1ED91162B95897500802036 /* LatestInLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED91142B95897500802036 /* LatestInLibraryViewModel.swift */; };
|
||||
E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED91172B95993300802036 /* TitledLibraryParent.swift */; };
|
||||
@ -940,6 +970,10 @@
|
||||
E1EF473A289A0F610034046B /* TruncatedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */; };
|
||||
E1EF4C412911B783008CC695 /* StreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EF4C402911B783008CC695 /* StreamType.swift */; };
|
||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
||||
E1F5CF052CB09EA000607465 /* CurrentDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF042CB09EA000607465 /* CurrentDate.swift */; };
|
||||
E1F5CF062CB09EA000607465 /* CurrentDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF042CB09EA000607465 /* CurrentDate.swift */; };
|
||||
E1F5CF082CB0A04500607465 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF072CB0A04500607465 /* Text.swift */; };
|
||||
E1F5CF092CB0A04500607465 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF072CB0A04500607465 /* Text.swift */; };
|
||||
E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA891A289A302300176FEB /* iPadOSCollectionItemView.swift */; };
|
||||
E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */; };
|
||||
E1FAD1C62A0375BA007F5521 /* UDPBroadcast in Frameworks */ = {isa = PBXBuildFile; productRef = E1FAD1C52A0375BA007F5521 /* UDPBroadcast */; };
|
||||
@ -979,10 +1013,14 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = "<group>"; };
|
||||
4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCompletionStatus.swift; sourceTree = "<group>"; };
|
||||
4E16FD502C0183DB00110147 /* LetterPickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerButton.swift; sourceTree = "<group>"; };
|
||||
4E16FD522C01840C00110147 /* LetterPickerBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerBar.swift; sourceTree = "<group>"; };
|
||||
4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerOrientation.swift; sourceTree = "<group>"; };
|
||||
4E182C9B2C94993200FBEFD5 /* ScheduledTasksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduledTasksView.swift; sourceTree = "<group>"; };
|
||||
4E182C9E2C94A1E000FBEFD5 /* ScheduledTaskButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduledTaskButton.swift; sourceTree = "<group>"; };
|
||||
4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeSettingsCoordinator.swift; sourceTree = "<group>"; };
|
||||
4E2182E42CAF67EF0094806B /* PlayMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayMethod.swift; sourceTree = "<group>"; };
|
||||
4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileAction.swift; sourceTree = "<group>"; };
|
||||
4E2AC4C12C6C491200DD600D /* AudoCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudoCodec.swift; sourceTree = "<group>"; };
|
||||
4E2AC4C42C6C492700DD600D /* MediaContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaContainer.swift; sourceTree = "<group>"; };
|
||||
@ -993,6 +1031,9 @@
|
||||
4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackQualitySettingsView.swift; sourceTree = "<group>"; };
|
||||
4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackQualitySettingsView.swift; sourceTree = "<group>"; };
|
||||
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
||||
4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDashboardView.swift; sourceTree = "<group>"; };
|
||||
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = "<group>"; };
|
||||
4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionDetailView.swift; sourceTree = "<group>"; };
|
||||
4E71D6882C80910900A0174D /* EditCustomDeviceProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileView.swift; sourceTree = "<group>"; };
|
||||
4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = "<group>"; };
|
||||
4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = "<group>"; };
|
||||
@ -1001,6 +1042,10 @@
|
||||
4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProfileButton.swift; sourceTree = "<group>"; };
|
||||
4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileCoordinator.swift; sourceTree = "<group>"; };
|
||||
4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileView.swift; sourceTree = "<group>"; };
|
||||
4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = "<group>"; };
|
||||
4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = "<group>"; };
|
||||
4EB1A8CB2C9B1B9700F43898 /* ServerTaskButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskButton.swift; sourceTree = "<group>"; };
|
||||
4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionRow.swift; sourceTree = "<group>"; };
|
||||
4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackCompatibility.swift; sourceTree = "<group>"; };
|
||||
4EBE064C2C7EB6D3004A6C03 /* VideoPlayerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerType.swift; sourceTree = "<group>"; };
|
||||
4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceProfile.swift; sourceTree = "<group>"; };
|
||||
@ -1008,6 +1053,10 @@
|
||||
4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileCoordinator.swift; sourceTree = "<group>"; };
|
||||
4EC1C8682C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileSettingsView.swift; sourceTree = "<group>"; };
|
||||
4EC1C86C2C80903A00E2879E /* CustomProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProfileButton.swift; sourceTree = "<group>"; };
|
||||
4EC50D602C934B3A00FC3D0E /* ScheduledTasksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduledTasksViewModel.swift; sourceTree = "<group>"; };
|
||||
4EC6C16A2C92999800FC904B /* TranscodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeSection.swift; sourceTree = "<group>"; };
|
||||
4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeReason.swift; sourceTree = "<group>"; };
|
||||
4EE141682C8BABDF0045B661 /* ProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressSection.swift; sourceTree = "<group>"; };
|
||||
531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||
531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
|
||||
5321753A2671BCFC005491E6 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
@ -1351,9 +1400,9 @@
|
||||
E1722DB029491C3900CC0239 /* ImageBlurHashes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBlurHashes.swift; sourceTree = "<group>"; };
|
||||
E172D3AC2BAC9DF8007B4647 /* SeasonItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemViewModel.swift; sourceTree = "<group>"; };
|
||||
E172D3B12BACA569007B4647 /* EpisodeContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeContent.swift; sourceTree = "<group>"; };
|
||||
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = "<group>"; };
|
||||
E173DA4F26D048D600CC4EB7 /* EditServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditServerView.swift; sourceTree = "<group>"; };
|
||||
E173DA5126D04AAF00CC4EB7 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
|
||||
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = "<group>"; };
|
||||
E173DA5326D050F500CC4EB7 /* ServerConnectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnectionViewModel.swift; sourceTree = "<group>"; };
|
||||
E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinatable.swift; sourceTree = "<group>"; };
|
||||
E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugSettingsView.swift; sourceTree = "<group>"; };
|
||||
E1763A262BF303C9004DF6AB /* ServerSelectionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionMenu.swift; sourceTree = "<group>"; };
|
||||
@ -1478,6 +1527,7 @@
|
||||
E1B33ED028EB860A0073B0FD /* LargePlaybackButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargePlaybackButtons.swift; sourceTree = "<group>"; };
|
||||
E1B490432967E26300D3EDCE /* PersistentLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentLogHandler.swift; sourceTree = "<group>"; };
|
||||
E1B490462967E2E500D3EDCE /* CoreStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreStore.swift; sourceTree = "<group>"; };
|
||||
E1B4E4362CA7795200DC49DE /* OrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = "<group>"; };
|
||||
E1B5784028F8AFCB00D42911 /* WrappedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedView.swift; sourceTree = "<group>"; };
|
||||
E1B5861129E32EEF00E45D6E /* Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = "<group>"; };
|
||||
E1B90C692BBE68D5007027C8 /* OffsetScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetScrollView.swift; sourceTree = "<group>"; };
|
||||
@ -1605,10 +1655,18 @@
|
||||
E1EA9F6928F8A79E00BEC442 /* VideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerManager.swift; sourceTree = "<group>"; };
|
||||
E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = "<group>"; };
|
||||
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemOverviewView.swift; sourceTree = "<group>"; };
|
||||
E1ED7FD52CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditScheduledTaskView.swift; sourceTree = "<group>"; };
|
||||
E1ED7FD72CA8AF7400ACB6E3 /* ServerTaskObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskObserver.swift; sourceTree = "<group>"; };
|
||||
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStateInfo.swift; sourceTree = "<group>"; };
|
||||
E1ED7FDD2CAA641F00ACB6E3 /* ListTitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTitleSection.swift; sourceTree = "<group>"; };
|
||||
E1ED7FDF2CAA685900ACB6E3 /* ServerLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerLogsView.swift; sourceTree = "<group>"; };
|
||||
E1ED7FE12CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerLogsViewModel.swift; sourceTree = "<group>"; };
|
||||
E1ED91142B95897500802036 /* LatestInLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestInLibraryViewModel.swift; sourceTree = "<group>"; };
|
||||
E1ED91172B95993300802036 /* TitledLibraryParent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitledLibraryParent.swift; sourceTree = "<group>"; };
|
||||
E1EF4C402911B783008CC695 /* StreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamType.swift; sourceTree = "<group>"; };
|
||||
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; };
|
||||
E1F5CF042CB09EA000607465 /* CurrentDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentDate.swift; sourceTree = "<group>"; };
|
||||
E1F5CF072CB0A04500607465 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = "<group>"; };
|
||||
E1FA891A289A302300176FEB /* iPadOSCollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadOSCollectionItemView.swift; sourceTree = "<group>"; };
|
||||
E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadOSCollectionItemContentView.swift; sourceTree = "<group>"; };
|
||||
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
|
||||
@ -1755,6 +1813,24 @@
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E182C9A2C94991800FBEFD5 /* ScheduledTasksView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E182C9D2C94A01600FBEFD5 /* Components */,
|
||||
4E182C9B2C94993200FBEFD5 /* ScheduledTasksView.swift */,
|
||||
);
|
||||
path = ScheduledTasksView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E182C9D2C94A01600FBEFD5 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E182C9E2C94A1E000FBEFD5 /* ScheduledTaskButton.swift */,
|
||||
4EB1A8CB2C9B1B9700F43898 /* ServerTaskButton.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E2AC4C02C6C48EB00DD600D /* MediaComponents */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1775,6 +1851,28 @@
|
||||
path = PlaybackBitrate;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E63B9F52C8A5BEF00C25378 /* UserDashboardView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */,
|
||||
4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */,
|
||||
E1ED7FD52CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift */,
|
||||
4E182C9A2C94991800FBEFD5 /* ScheduledTasksView */,
|
||||
E1ED7FDF2CAA685900ACB6E3 /* ServerLogsView.swift */,
|
||||
4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */,
|
||||
);
|
||||
path = UserDashboardView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */,
|
||||
4EB1A8D32C9B91A200F43898 /* Components */,
|
||||
);
|
||||
path = ActiveSessionDetailView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E9A24E32C82B4700023DA83 /* CustomDeviceProfileSettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1793,6 +1891,33 @@
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */,
|
||||
4EB1A8D02C9B2FB600F43898 /* Components */,
|
||||
);
|
||||
path = ActiveSessionsView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EB1A8D02C9B2FB600F43898 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */,
|
||||
4EE141682C8BABDF0045B661 /* ProgressSection.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EB1A8D32C9B91A200F43898 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EB1404B2C8E45B1008691F3 /* StreamSection.swift */,
|
||||
4EC6C16A2C92999800FC904B /* TranscodeSection.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EC1C86A2C80900B00E2879E /* CustomDeviceProfileSettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1828,6 +1953,7 @@
|
||||
532175392671BCED005491E6 /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */,
|
||||
E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */,
|
||||
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
||||
E17AC96E2954EE4B003D2BC2 /* DownloadListViewModel.swift */,
|
||||
@ -1840,10 +1966,13 @@
|
||||
E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */,
|
||||
6334175C287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift */,
|
||||
E1BCDB4E2BE1F491009F6744 /* ResetUserPasswordViewModel.swift */,
|
||||
4EC50D602C934B3A00FC3D0E /* ScheduledTasksViewModel.swift */,
|
||||
62E632DB267D2E130063E547 /* SearchViewModel.swift */,
|
||||
E13DD3F82717E961009D4DAF /* SelectUserViewModel.swift */,
|
||||
E19D41AD2BF288320082B8B2 /* ServerCheckViewModel.swift */,
|
||||
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */,
|
||||
E173DA5326D050F500CC4EB7 /* ServerConnectionViewModel.swift */,
|
||||
E1ED7FE12CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift */,
|
||||
E1ED7FD72CA8AF7400ACB6E3 /* ServerTaskObserver.swift */,
|
||||
5321753A2671BCFC005491E6 /* SettingsViewModel.swift */,
|
||||
E19D41A62BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift */,
|
||||
E14EA1682BF7330A00DE757A /* UserProfileImageViewModel.swift */,
|
||||
@ -1941,6 +2070,7 @@
|
||||
E129429728F4785200796AC6 /* CaseIterablePicker.swift */,
|
||||
E10231432BCF8A51009D71FC /* ChannelProgram.swift */,
|
||||
E1CB756E2C80E66700217C76 /* CommaStringBuilder.swift */,
|
||||
E1F5CF042CB09EA000607465 /* CurrentDate.swift */,
|
||||
4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */,
|
||||
E17FB55128C119D400311DFE /* Displayable.swift */,
|
||||
E1579EA62B97DC1500A31CA1 /* Eventful.swift */,
|
||||
@ -2234,6 +2364,7 @@
|
||||
4E16FD4E2C0183B500110147 /* LetterPickerBar */,
|
||||
E1A8FDEB2C0574A800D0A51C /* ListRow.swift */,
|
||||
E1AEFA362BE317E200CFAFD8 /* ListRowButton.swift */,
|
||||
E1ED7FDD2CAA641F00ACB6E3 /* ListTitleSection.swift */,
|
||||
E1FE69AF28C2DA4A0021BC93 /* NavigationBarFilterDrawer */,
|
||||
E1DE84132B9531C1008CCE21 /* OrderedSectionSelectorView.swift */,
|
||||
E18E01A5288746AF0022598C /* PillHStack.swift */,
|
||||
@ -2275,11 +2406,13 @@
|
||||
E1AD105226D96D5F003E4A08 /* JellyfinAPI */,
|
||||
E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */,
|
||||
E150C0B82BFD44E900944FFA /* Nuke */,
|
||||
E1B4E4362CA7795200DC49DE /* OrderedDictionary.swift */,
|
||||
E1B490432967E26300D3EDCE /* PersistentLogHandler.swift */,
|
||||
E1B5861129E32EEF00E45D6E /* Sequence.swift */,
|
||||
E145EB442BE0AD4E003BF6F3 /* Set.swift */,
|
||||
621338922660107500A81A2A /* String.swift */,
|
||||
E1DD55362B6EE533007501C0 /* Task.swift */,
|
||||
E1F5CF072CB0A04500607465 /* Text.swift */,
|
||||
E1A2C153279A7D5A005EC829 /* UIApplication.swift */,
|
||||
E1401CB029386C9200E8B599 /* UIColor.swift */,
|
||||
E13DD3C727164B1E009D4DAF /* UIDevice.swift */,
|
||||
@ -2797,6 +2930,7 @@
|
||||
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
||||
E17AC96C2954E9CA003D2BC2 /* DownloadListView.swift */,
|
||||
E13332922953BA9400EE76AB /* DownloadTaskView */,
|
||||
E173DA4F26D048D600CC4EB7 /* EditServerView.swift */,
|
||||
E113133128BDC72000930F75 /* FilterView.swift */,
|
||||
62C83B07288C6A630004ED0C /* FontPickerView.swift */,
|
||||
E168BD07289A4162001A6922 /* HomeView */,
|
||||
@ -2811,7 +2945,6 @@
|
||||
53EE24E5265060780068F029 /* SearchView.swift */,
|
||||
E10B1EAF2BD9769500A92EAF /* SelectUserView */,
|
||||
E19D41AB2BF288110082B8B2 /* ServerCheckView.swift */,
|
||||
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */,
|
||||
E1E5D54A2783E26100692DFE /* SettingsView */,
|
||||
E1171A1A28A2215800FA1AF5 /* UserSignInView */,
|
||||
E193D5452719418B00900D82 /* VideoPlayer */,
|
||||
@ -3351,8 +3484,10 @@
|
||||
E1D37F5B2B9CF02600343D2B /* BaseItemDto */,
|
||||
E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */,
|
||||
E1002B632793CEE700E47059 /* ChapterInfo.swift */,
|
||||
E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */,
|
||||
E1CB758A2C80F9EC00217C76 /* CodecProfile.swift */,
|
||||
4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */,
|
||||
4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */,
|
||||
E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */,
|
||||
E1722DB029491C3900CC0239 /* ImageBlurHashes.swift */,
|
||||
E1D842902933F87500D1041A /* ItemFields.swift */,
|
||||
@ -3363,8 +3498,10 @@
|
||||
E122A9122788EAAD0060FA63 /* MediaStream.swift */,
|
||||
E1AD105E26D9ADDD003E4A08 /* NameGuidPair.swift */,
|
||||
E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */,
|
||||
4E2182E42CAF67EF0094806B /* PlayMethod.swift */,
|
||||
E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */,
|
||||
E1CB757E2C80F28F00217C76 /* SubtitleProfile.swift */,
|
||||
4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */,
|
||||
E1CB757B2C80F00D00217C76 /* TranscodingProfile.swift */,
|
||||
E18CE0B128A229E70092E7F1 /* UserDto.swift */,
|
||||
);
|
||||
@ -3626,12 +3763,13 @@
|
||||
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */,
|
||||
E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */,
|
||||
E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */,
|
||||
E104C86F296E087200C1C3F9 /* IndicatorSettingsView.swift */,
|
||||
E16AF11B292C98A7001422A8 /* GestureSettingsView.swift */,
|
||||
E104C86F296E087200C1C3F9 /* IndicatorSettingsView.swift */,
|
||||
E15756332936851D00976E1F /* NativeVideoPlayerSettingsView.swift */,
|
||||
4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */,
|
||||
E1545BD62BDC559500D9578F /* UserProfileSettingsView */,
|
||||
E1BE1CEB2BDB68BC008176A9 /* SettingsView */,
|
||||
4E63B9F52C8A5BEF00C25378 /* UserDashboardView */,
|
||||
E1545BD62BDC559500D9578F /* UserProfileSettingsView */,
|
||||
E1BDF2E7295148F400CC0294 /* VideoPlayerSettingsView */,
|
||||
);
|
||||
path = SettingsView;
|
||||
@ -4127,6 +4265,7 @@
|
||||
E11C15362BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */,
|
||||
E172D3AE2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */,
|
||||
4EC1C8532C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */,
|
||||
4EC50D622C934B3A00FC3D0E /* ScheduledTasksViewModel.swift in Sources */,
|
||||
E11E374D293E7EC9009EF240 /* ItemFields.swift in Sources */,
|
||||
E1575E6E293E77B5001665B1 /* SpecialFeatureType.swift in Sources */,
|
||||
E12CC1C528D12D9B00678D5D /* SeeAllPosterButton.swift in Sources */,
|
||||
@ -4136,12 +4275,14 @@
|
||||
E1153D9A2BBA3E9800424D36 /* ErrorCard.swift in Sources */,
|
||||
E1ED91162B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
|
||||
E12376B32A33DFAC001F5B44 /* ItemOverviewView.swift in Sources */,
|
||||
E1ED7FDB2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */,
|
||||
E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */,
|
||||
E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */,
|
||||
E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */,
|
||||
E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */,
|
||||
E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */,
|
||||
BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */,
|
||||
4E0A8FFC2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */,
|
||||
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||
C46DD8D32A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift in Sources */,
|
||||
E1549661296CA2EF00C4EF88 /* SwiftfinDefaults.swift in Sources */,
|
||||
@ -4168,6 +4309,7 @@
|
||||
E1575E5F293E77B5001665B1 /* StreamType.swift in Sources */,
|
||||
E1803EA22BFBD6CF0039F90E /* Hashable.swift in Sources */,
|
||||
E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */,
|
||||
E1ED7FD82CA8AF7400ACB6E3 /* ServerTaskObserver.swift in Sources */,
|
||||
E1575E93293E7B1E001665B1 /* Double.swift in Sources */,
|
||||
E1B5784228F8AFCB00D42911 /* WrappedView.swift in Sources */,
|
||||
E11895AA289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */,
|
||||
@ -4191,6 +4333,7 @@
|
||||
E1575E87293E7A00001665B1 /* InvertedDarkAppIcon.swift in Sources */,
|
||||
E10432F72BE4426F006FF9DD /* FormatStyle.swift in Sources */,
|
||||
62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||
4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */,
|
||||
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||
E1549667296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */,
|
||||
E150C0BB2BFD44F500944FFA /* ImagePipeline.swift in Sources */,
|
||||
@ -4206,6 +4349,7 @@
|
||||
E1C9260F2887565C002A7A66 /* AttributeHStack.swift in Sources */,
|
||||
E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */,
|
||||
E1CAF65E2BA345830087D991 /* MediaType.swift in Sources */,
|
||||
E1F5CF062CB09EA000607465 /* CurrentDate.swift in Sources */,
|
||||
E12376B02A33D6AE001F5B44 /* AboutViewCard.swift in Sources */,
|
||||
E12A9EF929499E0100731C3A /* JellyfinClient.swift in Sources */,
|
||||
E148128328C1443D003B8787 /* NameGuidPair.swift in Sources */,
|
||||
@ -4215,6 +4359,7 @@
|
||||
E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */,
|
||||
E1575E5C293E77B5001665B1 /* PlaybackSpeed.swift in Sources */,
|
||||
E1DC9842296DEBD800982F06 /* WatchedIndicator.swift in Sources */,
|
||||
4ECDAA9F2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */,
|
||||
E1575E6C293E77B5001665B1 /* SliderType.swift in Sources */,
|
||||
E1E2F8402B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */,
|
||||
E17DC74B2BE740D900B42379 /* StoredValues+Server.swift in Sources */,
|
||||
@ -4222,6 +4367,7 @@
|
||||
E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */,
|
||||
E13316FF2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */,
|
||||
E148128928C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
|
||||
E1F5CF082CB0A04500607465 /* Text.swift in Sources */,
|
||||
E43918672AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */,
|
||||
E154966F296CA2EF00C4EF88 /* LogManager.swift in Sources */,
|
||||
E1575E75293E77B5001665B1 /* LibraryDisplayType.swift in Sources */,
|
||||
@ -4251,6 +4397,7 @@
|
||||
E14EDEC62B8FB64E000F00A4 /* AnyItemFilter.swift in Sources */,
|
||||
E14EDEC92B8FB65F000F00A4 /* ItemFilterType.swift in Sources */,
|
||||
E1D37F4C2B9CEA5C00343D2B /* ImageSource.swift in Sources */,
|
||||
E1B4E4372CA7795200DC49DE /* OrderedDictionary.swift in Sources */,
|
||||
E1AD104E26D96CE3003E4A08 /* BaseItemDto.swift in Sources */,
|
||||
E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */,
|
||||
62E632DD267D2E130063E547 /* SearchViewModel.swift in Sources */,
|
||||
@ -4261,6 +4408,7 @@
|
||||
E164A7F72BE4816500A54B18 /* SelectUserServerSelection.swift in Sources */,
|
||||
E1575E84293E7A00001665B1 /* PrimaryAppIcon.swift in Sources */,
|
||||
E1153DCD2BBB633B00424D36 /* FastSVGView.swift in Sources */,
|
||||
E1ED7FE22CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */,
|
||||
E1CB75762C80EAFA00217C76 /* ArrayBuilder.swift in Sources */,
|
||||
E102315B2BCF8AF8009D71FC /* ProgramProgressOverlay.swift in Sources */,
|
||||
E1E6C45129B104850064123F /* Button.swift in Sources */,
|
||||
@ -4354,7 +4502,7 @@
|
||||
E1575E65293E77B5001665B1 /* VideoPlayerJumpLength.swift in Sources */,
|
||||
E169C7B8296D2E8200AE25F9 /* SpecialFeaturesHStack.swift in Sources */,
|
||||
E1153D962BBA3E2F00424D36 /* EpisodeHStack.swift in Sources */,
|
||||
E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */,
|
||||
E193D5512719432400900D82 /* ServerConnectionViewModel.swift in Sources */,
|
||||
E1B5861329E32EEF00E45D6E /* Sequence.swift in Sources */,
|
||||
C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */,
|
||||
E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */,
|
||||
@ -4439,6 +4587,7 @@
|
||||
621338932660107500A81A2A /* String.swift in Sources */,
|
||||
E17AC96F2954EE4B003D2BC2 /* DownloadListViewModel.swift in Sources */,
|
||||
BD39577C2C113FAA0078CEF8 /* TimestampSection.swift in Sources */,
|
||||
4EC6C16B2C92999800FC904B /* TranscodeSection.swift in Sources */,
|
||||
62C83B08288C6A630004ED0C /* FontPickerView.swift in Sources */,
|
||||
E122A9132788EAAD0060FA63 /* MediaStream.swift in Sources */,
|
||||
E1E9017F28DAB15F001B1594 /* BarActionButtons.swift in Sources */,
|
||||
@ -4449,12 +4598,14 @@
|
||||
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
||||
62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */,
|
||||
E1F5CF052CB09EA000607465 /* CurrentDate.swift in Sources */,
|
||||
E13316FE2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */,
|
||||
62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */,
|
||||
E1A1528A28FD22F600600579 /* TextPairView.swift in Sources */,
|
||||
E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */,
|
||||
E170D0E2294CC8000017224C /* VideoPlayer+Actions.swift in Sources */,
|
||||
E148128828C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
|
||||
4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */,
|
||||
E1A3E4D12BB7F5BF005C59F8 /* ErrorCard.swift in Sources */,
|
||||
E1AEFA372BE317E200CFAFD8 /* ListRowButton.swift in Sources */,
|
||||
E102314A2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */,
|
||||
@ -4472,6 +4623,7 @@
|
||||
E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */,
|
||||
E154966E296CA2EF00C4EF88 /* LogManager.swift in Sources */,
|
||||
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
|
||||
E1B4E4382CA7795200DC49DE /* OrderedDictionary.swift in Sources */,
|
||||
E18E01E8288747230022598C /* SeriesItemContentView.swift in Sources */,
|
||||
E16AA60828A364A6009A983C /* PosterButton.swift in Sources */,
|
||||
E1E1644128BB301900323B0A /* Array.swift in Sources */,
|
||||
@ -4498,6 +4650,7 @@
|
||||
E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */,
|
||||
E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
||||
E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */,
|
||||
E1ED7FD92CA8AF7400ACB6E3 /* ServerTaskObserver.swift in Sources */,
|
||||
E17AC96A2954D00E003D2BC2 /* URLResponse.swift in Sources */,
|
||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||
E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */,
|
||||
@ -4531,7 +4684,7 @@
|
||||
E17FB55228C119D400311DFE /* Displayable.swift in Sources */,
|
||||
E113132B28BDB4B500930F75 /* NavigationBarDrawerView.swift in Sources */,
|
||||
E164A7F62BE4814700A54B18 /* SelectUserServerSelection.swift in Sources */,
|
||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
||||
E173DA5426D050F500CC4EB7 /* ServerConnectionViewModel.swift in Sources */,
|
||||
E1EA09882BEE9CF3004CDE76 /* UserLocalSecurityView.swift in Sources */,
|
||||
E1559A76294D960C00C1FFBC /* MainOverlay.swift in Sources */,
|
||||
E14EDECC2B8FB709000F00A4 /* ItemYear.swift in Sources */,
|
||||
@ -4567,6 +4720,7 @@
|
||||
E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */,
|
||||
E10B1EB42BD9803100A92EAF /* UserRow.swift in Sources */,
|
||||
E1E6C45029B104840064123F /* Button.swift in Sources */,
|
||||
4ECDAA9E2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */,
|
||||
E1153DCC2BBB633B00424D36 /* FastSVGView.swift in Sources */,
|
||||
E10432F62BE4426F006FF9DD /* FormatStyle.swift in Sources */,
|
||||
E1E5D5492783CDD700692DFE /* VideoPlayerSettingsView.swift in Sources */,
|
||||
@ -4584,10 +4738,13 @@
|
||||
E1ED91152B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
|
||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
||||
E1BAFE102BE921270069C4D7 /* SwiftfinApp+ValueObservation.swift in Sources */,
|
||||
E1ED7FDE2CAA641F00ACB6E3 /* ListTitleSection.swift in Sources */,
|
||||
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||
E10B1E8F2BD7728400A92EAF /* QuickConnectView.swift in Sources */,
|
||||
E1A8FDEC2C0574A800D0A51C /* ListRow.swift in Sources */,
|
||||
4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */,
|
||||
E1DD55372B6EE533007501C0 /* Task.swift in Sources */,
|
||||
E1ED7FE02CAA685900ACB6E3 /* ServerLogsView.swift in Sources */,
|
||||
E1194F4E2BEABA9100888DB6 /* NavigationBarCloseButton.swift in Sources */,
|
||||
E113133428BE988200930F75 /* NavigationBarFilterDrawer.swift in Sources */,
|
||||
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
|
||||
@ -4608,6 +4765,7 @@
|
||||
E18CE0B228A229E70092E7F1 /* UserDto.swift in Sources */,
|
||||
E18E01F0288747230022598C /* AttributeHStack.swift in Sources */,
|
||||
6334175B287DDFB9000603CE /* QuickConnectAuthorizeView.swift in Sources */,
|
||||
4E0A8FFB2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */,
|
||||
E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */,
|
||||
E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
|
||||
E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */,
|
||||
@ -4628,6 +4786,7 @@
|
||||
E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */,
|
||||
E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */,
|
||||
E1937A61288F32DB00CB80AA /* Poster.swift in Sources */,
|
||||
4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */,
|
||||
E145EB482BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift in Sources */,
|
||||
E1CB757C2C80F00D00217C76 /* TranscodingProfile.swift in Sources */,
|
||||
E1CAF65F2BA345830087D991 /* MediaViewModel.swift in Sources */,
|
||||
@ -4652,6 +4811,7 @@
|
||||
E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */,
|
||||
E12CC1AE28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */,
|
||||
E1549666296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */,
|
||||
4EE141692C8BABDF0045B661 /* ProgressSection.swift in Sources */,
|
||||
E1A1528528FD191A00600579 /* TextPair.swift in Sources */,
|
||||
6334175D287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift in Sources */,
|
||||
E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */,
|
||||
@ -4695,6 +4855,7 @@
|
||||
E18E01E2288747230022598C /* EpisodeItemView.swift in Sources */,
|
||||
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||
E14EA1602BF6FF8900DE757A /* UserProfileImagePicker.swift in Sources */,
|
||||
4E182C9C2C94993200FBEFD5 /* ScheduledTasksView.swift in Sources */,
|
||||
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
||||
E1BDF2EF29522A5900CC0294 /* AudioActionButton.swift in Sources */,
|
||||
E174120F29AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */,
|
||||
@ -4735,6 +4896,7 @@
|
||||
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */,
|
||||
E12376AE2A33D680001F5B44 /* AboutView+Card.swift in Sources */,
|
||||
E1A2C154279A7D5A005EC829 /* UIApplication.swift in Sources */,
|
||||
4E6C27082C8BD0AD00FD2185 /* ActiveSessionDetailView.swift in Sources */,
|
||||
E11C15352BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */,
|
||||
E1D8428F2933F2D900D1041A /* MediaSourceInfo.swift in Sources */,
|
||||
E1BDF2EC2952290200CC0294 /* AspectFillActionButton.swift in Sources */,
|
||||
@ -4745,6 +4907,7 @@
|
||||
E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */,
|
||||
E14EA1692BF7330A00DE757A /* UserProfileImageViewModel.swift in Sources */,
|
||||
E18ACA952A15A3E100BB4F35 /* (null) in Sources */,
|
||||
4EB1A8CE2C9B2D0800F43898 /* ActiveSessionRow.swift in Sources */,
|
||||
E1D5C39B28DF993400CDBEFB /* ThumbSlider.swift in Sources */,
|
||||
E1DC983D296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
|
||||
E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */,
|
||||
@ -4756,13 +4919,14 @@
|
||||
4E16FD512C0183DB00110147 /* LetterPickerButton.swift in Sources */,
|
||||
E19D41AE2BF288320082B8B2 /* ServerCheckViewModel.swift in Sources */,
|
||||
E1BDF2F329524C3B00CC0294 /* ChaptersActionButton.swift in Sources */,
|
||||
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */,
|
||||
E173DA5026D048D600CC4EB7 /* EditServerView.swift in Sources */,
|
||||
E1BE1CF02BDB6C97008176A9 /* UserProfileSettingsView.swift in Sources */,
|
||||
E1DC7ACA2C63337C00AEE368 /* iOS15View.swift in Sources */,
|
||||
E1CFE28028FA606800B7D34C /* ChapterTrack.swift in Sources */,
|
||||
E1401CA22938122C00E8B599 /* AppIcons.swift in Sources */,
|
||||
E1BDF2FB2952502300CC0294 /* SubtitleActionButton.swift in Sources */,
|
||||
E17FB55728C1256400311DFE /* CastAndCrewHStack.swift in Sources */,
|
||||
4EC50D612C934B3A00FC3D0E /* ScheduledTasksViewModel.swift in Sources */,
|
||||
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||
E150C0BD2BFD45BD00944FFA /* RedrawOnNotificationView.swift in Sources */,
|
||||
E190704D2C858CEB0004600E /* VideoPlayerType+Shared.swift in Sources */,
|
||||
@ -4771,12 +4935,14 @@
|
||||
E190704F2C8592B40004600E /* PlaybackCompatibility+Video.swift in Sources */,
|
||||
E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */,
|
||||
E102312C2BCF8A08009D71FC /* iOSLiveTVCoordinator.swift in Sources */,
|
||||
E1ED7FDC2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */,
|
||||
E149CCAD2BE6ECC8008B9331 /* Storable.swift in Sources */,
|
||||
E1CB75792C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */,
|
||||
E18ACA8F2A15A2CF00BB4F35 /* (null) in Sources */,
|
||||
E1401CA72938140300E8B599 /* PrimaryAppIcon.swift in Sources */,
|
||||
E1937A3E288F0D3D00CB80AA /* UIScreen.swift in Sources */,
|
||||
E10B1EBE2BD9AD5C00A92EAF /* V1ServerModel.swift in Sources */,
|
||||
4EB1A8CC2C9B1BA200F43898 /* ServerTaskButton.swift in Sources */,
|
||||
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
|
||||
E10B1EB62BD98C6600A92EAF /* AddUserRow.swift in Sources */,
|
||||
E1CB75802C80F28F00217C76 /* SubtitleProfile.swift in Sources */,
|
||||
@ -4793,6 +4959,7 @@
|
||||
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
||||
E1721FAE28FB801C00762992 /* SmallPlaybackButtons.swift in Sources */,
|
||||
E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */,
|
||||
E1ED7FD62CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift in Sources */,
|
||||
E1545BD82BDC55C300D9578F /* ResetUserPasswordView.swift in Sources */,
|
||||
E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */,
|
||||
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */,
|
||||
@ -4803,6 +4970,7 @@
|
||||
C46DD8DD2A8DC3420046A504 /* LiveNativeVideoPlayer.swift in Sources */,
|
||||
E1AD104D26D96CE3003E4A08 /* BaseItemDto.swift in Sources */,
|
||||
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
||||
4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */,
|
||||
535870AD2669D8DD00D05A09 /* ItemFilterCollection.swift in Sources */,
|
||||
E1D27EE72BBC955F00152D16 /* UnmaskSecureField.swift in Sources */,
|
||||
E1CAF65D2BA345830087D991 /* MediaType.swift in Sources */,
|
||||
@ -4810,6 +4978,8 @@
|
||||
E18A8E7D28D606BE00333B9A /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
||||
E18E01F1288747230022598C /* PlayButton.swift in Sources */,
|
||||
E129429028F0BDC300796AC6 /* TimeStampType.swift in Sources */,
|
||||
E1F5CF092CB0A04500607465 /* Text.swift in Sources */,
|
||||
4E182C9F2C94A1E000FBEFD5 /* ScheduledTaskButton.swift in Sources */,
|
||||
E1B490442967E26300D3EDCE /* PersistentLogHandler.swift in Sources */,
|
||||
E1CB756F2C80E66700217C76 /* CommaStringBuilder.swift in Sources */,
|
||||
E19D41AC2BF288110082B8B2 /* ServerCheckView.swift in Sources */,
|
||||
@ -4830,6 +5000,7 @@
|
||||
E1D4BF8A2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */,
|
||||
E1D37F482B9C648E00343D2B /* MaxHeightText.swift in Sources */,
|
||||
BD3957752C112A330078CEF8 /* ButtonSection.swift in Sources */,
|
||||
E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */,
|
||||
E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
||||
E13DD3F92717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
||||
E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */,
|
||||
@ -4853,6 +5024,7 @@
|
||||
E1D842172932AB8F00D1041A /* NativeVideoPlayer.swift in Sources */,
|
||||
E1A3E4C72BB74E50005C59F8 /* EpisodeCard.swift in Sources */,
|
||||
E1153DB42BBA80FB00424D36 /* EmptyCard.swift in Sources */,
|
||||
4E63B9FA2C8A5BEF00C25378 /* UserDashboardView.swift in Sources */,
|
||||
E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */,
|
||||
4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */,
|
||||
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,
|
||||
|
@ -10,6 +10,7 @@ import SwiftUI
|
||||
|
||||
struct ChevronButton: View {
|
||||
|
||||
private let isExternal: Bool
|
||||
private let title: String
|
||||
private let subtitle: String?
|
||||
private var leadingView: () -> any View
|
||||
@ -34,7 +35,7 @@ struct ChevronButton: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
Image(systemName: isExternal ? "arrow.up.forward" : "chevron.right")
|
||||
.font(.body.weight(.regular))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@ -44,8 +45,9 @@ struct ChevronButton: View {
|
||||
|
||||
extension ChevronButton {
|
||||
|
||||
init(_ title: String, subtitle: String? = nil) {
|
||||
init(_ title: String, subtitle: String? = nil, external: Bool = false) {
|
||||
self.init(
|
||||
isExternal: external,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
leadingView: { EmptyView() },
|
||||
|
@ -6,35 +6,80 @@
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct CircularProgressView: View {
|
||||
// SwiftUI gauge style not available on iOS 15
|
||||
|
||||
struct GaugeProgressStyle: ProgressViewStyle {
|
||||
|
||||
@Default(.accentColor)
|
||||
private var accentColor
|
||||
|
||||
@State
|
||||
private var lineWidth: CGFloat = 1
|
||||
private var contentSize: CGSize = .zero
|
||||
|
||||
let progress: Double
|
||||
private var lineWidthRatio: CGFloat
|
||||
private var systemImage: String?
|
||||
|
||||
var body: some View {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
ZStack {
|
||||
|
||||
if let systemImage {
|
||||
Image(systemName: systemImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: contentSize.width / 2.5, maxHeight: contentSize.height / 2.5)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(6)
|
||||
}
|
||||
|
||||
Circle()
|
||||
.stroke(
|
||||
Color.green.opacity(0.5),
|
||||
lineWidth: lineWidth
|
||||
Color.gray.opacity(0.2),
|
||||
lineWidth: contentSize.width / lineWidthRatio
|
||||
)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: progress)
|
||||
.trim(from: 0, to: configuration.fractionCompleted ?? 0)
|
||||
.stroke(
|
||||
Color.green,
|
||||
accentColor,
|
||||
style: StrokeStyle(
|
||||
lineWidth: lineWidth,
|
||||
lineWidth: contentSize.width / lineWidthRatio,
|
||||
lineCap: .round
|
||||
)
|
||||
)
|
||||
.rotationEffect(.degrees(-90))
|
||||
}
|
||||
.onSizeChanged { size in
|
||||
lineWidth = size.width / 3.5
|
||||
}
|
||||
.animation(.linear(duration: 0.1), value: configuration.fractionCompleted)
|
||||
.trackingSize($contentSize)
|
||||
}
|
||||
}
|
||||
|
||||
extension GaugeProgressStyle {
|
||||
|
||||
init() {
|
||||
self.init(
|
||||
lineWidthRatio: 5,
|
||||
systemImage: nil
|
||||
)
|
||||
}
|
||||
|
||||
init(systemImage: String) {
|
||||
self.init(
|
||||
lineWidthRatio: 8,
|
||||
systemImage: systemImage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ProgressViewStyle where Self == GaugeProgressStyle {
|
||||
|
||||
static var gauge: GaugeProgressStyle {
|
||||
GaugeProgressStyle()
|
||||
}
|
||||
|
||||
static func gauge(systemImage: String) -> GaugeProgressStyle {
|
||||
GaugeProgressStyle(systemImage: systemImage)
|
||||
}
|
||||
}
|
||||
|
66
Swiftfin/Components/ListTitleSection.swift
Normal file
66
Swiftfin/Components/ListTitleSection.swift
Normal file
@ -0,0 +1,66 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: image
|
||||
|
||||
struct ListTitleSection: View {
|
||||
|
||||
private let title: String
|
||||
private let description: String?
|
||||
private let onLearnMore: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
VStack(alignment: .center, spacing: 10) {
|
||||
|
||||
Text(title)
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
if let description {
|
||||
Text(description)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
if let onLearnMore {
|
||||
Button("Learn More\u{2026}", action: onLearnMore)
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ListTitleSection {
|
||||
|
||||
init(
|
||||
_ title: String,
|
||||
description: String? = nil
|
||||
) {
|
||||
self.init(
|
||||
title: title,
|
||||
description: description,
|
||||
onLearnMore: nil
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
_ title: String,
|
||||
description: String? = nil,
|
||||
onLearnMore: @escaping () -> Void
|
||||
) {
|
||||
self.init(
|
||||
title: title,
|
||||
description: description,
|
||||
onLearnMore: onLearnMore
|
||||
)
|
||||
}
|
||||
}
|
@ -42,6 +42,8 @@ struct EpisodeSelectorLabelStyle: LabelStyle {
|
||||
|
||||
// MARK: SectionFooterWithImageLabelStyle
|
||||
|
||||
// TODO: rename as not only used in section footers
|
||||
|
||||
extension LabelStyle where Self == SectionFooterWithImageLabelStyle<AnyShapeStyle> {
|
||||
|
||||
static func sectionFooterWithImage<ImageStyle: ShapeStyle>(imageStyle: ImageStyle) -> SectionFooterWithImageLabelStyle<ImageStyle> {
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
|
||||
extension View {
|
||||
|
||||
@ -77,4 +78,22 @@ extension View {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func listRowCornerRadius(_ radius: CGFloat) -> some View {
|
||||
if #unavailable(iOS 16) {
|
||||
introspect(.listCell, on: .iOS(.v15)) { cell in
|
||||
cell.layer.cornerRadius = radius
|
||||
}
|
||||
} else {
|
||||
introspect(
|
||||
.listCell,
|
||||
on: .iOS(.v16),
|
||||
.iOS(.v17),
|
||||
.iOS(.v18)
|
||||
) { cell in
|
||||
cell.layer.cornerRadius = radius
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ struct AboutAppView: View {
|
||||
trailing: "\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))"
|
||||
)
|
||||
|
||||
ChevronButton(L10n.sourceCode)
|
||||
ChevronButton(L10n.sourceCode, external: true)
|
||||
.leadingView {
|
||||
Image(.logoGithub)
|
||||
.resizable()
|
||||
@ -50,7 +50,7 @@ struct AboutAppView: View {
|
||||
UIApplication.shared.open(.swiftfinGithub)
|
||||
}
|
||||
|
||||
ChevronButton(L10n.bugsAndFeatures)
|
||||
ChevronButton(L10n.bugsAndFeatures, external: true)
|
||||
.leadingView {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.resizable()
|
||||
@ -64,7 +64,7 @@ struct AboutAppView: View {
|
||||
UIApplication.shared.open(.swiftfinGithubIssues)
|
||||
}
|
||||
|
||||
ChevronButton(L10n.settings)
|
||||
ChevronButton(L10n.settings, external: true)
|
||||
.leadingView {
|
||||
Image(systemName: "gearshape.fill")
|
||||
.resizable()
|
||||
|
@ -56,9 +56,9 @@ extension DownloadTaskView {
|
||||
.frame(height: 50)
|
||||
case let .downloading(progress):
|
||||
HStack {
|
||||
CircularProgressView(progress: progress)
|
||||
.buttonStyle(.plain)
|
||||
.frame(width: 30, height: 30)
|
||||
// CircularProgressView(progress: progress)
|
||||
// .buttonStyle(.plain)
|
||||
// .frame(width: 30, height: 30)
|
||||
|
||||
Text("\(Int(progress * 100))%")
|
||||
.foregroundColor(.secondary)
|
||||
|
@ -29,10 +29,10 @@ struct EditServerView: View {
|
||||
private var isPresentingConfirmDeletion: Bool = false
|
||||
|
||||
@StateObject
|
||||
private var viewModel: EditServerViewModel
|
||||
private var viewModel: ServerConnectionViewModel
|
||||
|
||||
init(server: ServerState) {
|
||||
self._viewModel = StateObject(wrappedValue: EditServerViewModel(server: server))
|
||||
self._viewModel = StateObject(wrappedValue: ServerConnectionViewModel(server: server))
|
||||
self._currentServerURL = State(initialValue: server.currentURL)
|
||||
}
|
||||
|
@ -30,8 +30,9 @@ struct DownloadTaskButton: View {
|
||||
case .complete:
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
case let .downloading(progress):
|
||||
CircularProgressView(progress: progress)
|
||||
case .downloading:
|
||||
EmptyView()
|
||||
// CircularProgressView(progress: progress)
|
||||
case .error:
|
||||
Image(systemName: "exclamationmark.circle.fill")
|
||||
.foregroundColor(.red)
|
||||
|
@ -14,9 +14,6 @@ extension PagingLibraryView {
|
||||
|
||||
struct LibraryRow: View {
|
||||
|
||||
@State
|
||||
private var contentWidth: CGFloat = 0
|
||||
|
||||
private let item: Element
|
||||
private var action: () -> Void
|
||||
private let posterType: PosterDisplayType
|
||||
|
@ -32,7 +32,7 @@ import SwiftUI
|
||||
|
||||
Note: For `rememberLayout` and `rememberSort`, there are quirks for observing changes while a
|
||||
library is open and the setting has been changed. For simplicity, do not enforce observing
|
||||
changes and doing proper updates since there is complexitry with what "actual" settings
|
||||
changes and doing proper updates since there is complexity with what "actual" settings
|
||||
should be applied.
|
||||
*/
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Factory
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
extension SettingsView {
|
||||
@ -16,12 +17,13 @@ extension SettingsView {
|
||||
@Injected(\.currentUserSession)
|
||||
private var userSession: UserSession!
|
||||
|
||||
let action: () -> Void
|
||||
private let user: UserDto
|
||||
private let action: (() -> Void)?
|
||||
|
||||
@ViewBuilder
|
||||
private var imageView: some View {
|
||||
RedrawOnNotificationView(.didChangeUserProfileImage) {
|
||||
ImageView(userSession.user.profileImageSource(client: userSession.client, maxWidth: 120))
|
||||
ImageView(user.profileImageSource(client: userSession.client, maxWidth: 120))
|
||||
.pipeline(.Swiftfin.branding)
|
||||
.placeholder { _ in
|
||||
SystemImageContentView(systemName: "person.fill", ratio: 0.5)
|
||||
@ -34,6 +36,7 @@ extension SettingsView {
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
guard let action else { return }
|
||||
action()
|
||||
} label: {
|
||||
HStack {
|
||||
@ -47,18 +50,37 @@ extension SettingsView {
|
||||
.clipShape(.circle)
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
Text(userSession.user.username)
|
||||
Text(user.name ?? .emptyDash)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.body.weight(.regular))
|
||||
.foregroundColor(.secondary)
|
||||
if action != nil {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.body.weight(.regular))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.primary, .secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsView.UserProfileRow {
|
||||
|
||||
init(user: UserDto) {
|
||||
self.init(
|
||||
user: user,
|
||||
action: nil
|
||||
)
|
||||
}
|
||||
|
||||
init(user: UserDto, perform action: @escaping () -> Void) {
|
||||
self.init(
|
||||
user: user,
|
||||
action: action
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,8 @@
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Defaults
|
||||
import Factory
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
@ -16,7 +15,6 @@ struct SettingsView: View {
|
||||
|
||||
@Default(.userAccentColor)
|
||||
private var accentColor
|
||||
|
||||
@Default(.userAppearance)
|
||||
private var appearance
|
||||
@Default(.VideoPlayer.videoPlayerType)
|
||||
@ -33,17 +31,23 @@ struct SettingsView: View {
|
||||
|
||||
Section {
|
||||
|
||||
UserProfileRow {
|
||||
UserProfileRow(user: viewModel.userSession.user.data) {
|
||||
router.route(to: \.userProfile, viewModel)
|
||||
}
|
||||
|
||||
// TODO: admin users go to dashboard instead
|
||||
ChevronButton(
|
||||
L10n.server,
|
||||
subtitle: viewModel.userSession.server.name
|
||||
)
|
||||
.onSelect {
|
||||
router.route(to: \.serverDetail, viewModel.userSession.server)
|
||||
router.route(to: \.serverConnection, viewModel.userSession.server)
|
||||
}
|
||||
|
||||
if viewModel.userSession.user.isAdministrator {
|
||||
ChevronButton(L10n.dashboard)
|
||||
.onSelect {
|
||||
router.route(to: \.userDashboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,208 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
|
||||
struct ActiveSessionDetailView: View {
|
||||
|
||||
@CurrentDate
|
||||
private var currentDate: Date
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: SettingsCoordinator.Router
|
||||
|
||||
@ObservedObject
|
||||
var box: BindingBox<SessionInfo?>
|
||||
|
||||
// MARK: Create Idle Content View
|
||||
|
||||
@ViewBuilder
|
||||
private func idleContent(session: SessionInfo) -> some View {
|
||||
List {
|
||||
Section(L10n.user) {
|
||||
if let userID = session.userID {
|
||||
SettingsView.UserProfileRow(
|
||||
user: .init(
|
||||
id: userID,
|
||||
name: session.userName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if let client = session.client {
|
||||
TextPairView(leading: L10n.client, trailing: client)
|
||||
}
|
||||
|
||||
if let device = session.deviceName {
|
||||
TextPairView(leading: L10n.device, trailing: device)
|
||||
}
|
||||
|
||||
if let applicationVersion = session.applicationVersion {
|
||||
TextPairView(leading: L10n.version, trailing: applicationVersion)
|
||||
}
|
||||
|
||||
if let lastActivityDate = session.lastActivityDate {
|
||||
TextPairView(
|
||||
L10n.lastSeen,
|
||||
value: Text(lastActivityDate, format: .relative(presentation: .numeric, unitsStyle: .narrow))
|
||||
)
|
||||
.id(currentDate)
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Create Session Content View
|
||||
|
||||
@ViewBuilder
|
||||
private func sessionContent(
|
||||
session: SessionInfo,
|
||||
nowPlayingItem: BaseItemDto,
|
||||
playState: PlayerStateInfo
|
||||
) -> some View {
|
||||
List {
|
||||
|
||||
nowPlayingSection(item: nowPlayingItem)
|
||||
|
||||
Section(L10n.progress) {
|
||||
ActiveSessionsView.ProgressSection(
|
||||
item: nowPlayingItem,
|
||||
playState: playState,
|
||||
transcodingInfo: session.transcodingInfo
|
||||
)
|
||||
}
|
||||
|
||||
Section(L10n.user) {
|
||||
if let userID = session.userID {
|
||||
SettingsView.UserProfileRow(
|
||||
user: .init(
|
||||
id: userID,
|
||||
name: session.userName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if let client = session.client {
|
||||
TextPairView(leading: L10n.client, trailing: client)
|
||||
}
|
||||
|
||||
if let device = session.deviceName {
|
||||
TextPairView(leading: L10n.device, trailing: device)
|
||||
}
|
||||
|
||||
if let applicationVersion = session.applicationVersion {
|
||||
TextPairView(leading: L10n.version, trailing: applicationVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: allow showing item stream details?
|
||||
// TODO: don't show codec changes on direct play?
|
||||
Section(L10n.streams) {
|
||||
if let playMethod = playState.playMethod {
|
||||
TextPairView(leading: L10n.method, trailing: playMethod.displayTitle)
|
||||
}
|
||||
|
||||
StreamSection(
|
||||
nowPlayingItem: nowPlayingItem,
|
||||
transcodingInfo: session.transcodingInfo
|
||||
)
|
||||
}
|
||||
|
||||
if let transcodeReasons = session.transcodingInfo?.transcodeReasons, transcodeReasons.isNotEmpty {
|
||||
Section(L10n.transcodeReasons) {
|
||||
TranscodeSection(transcodeReasons: transcodeReasons)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Now Playing Section
|
||||
|
||||
@ViewBuilder
|
||||
private func nowPlayingSection(item: BaseItemDto) -> some View {
|
||||
Section {
|
||||
HStack(alignment: .bottom, spacing: 12) {
|
||||
Group {
|
||||
if item.type == .audio {
|
||||
ZStack {
|
||||
Color.clear
|
||||
|
||||
ImageView(item.squareImageSources(maxWidth: 60))
|
||||
.failure {
|
||||
SystemImageContentView(systemName: item.systemImage)
|
||||
}
|
||||
}
|
||||
.squarePosterStyle()
|
||||
} else {
|
||||
ZStack {
|
||||
Color.clear
|
||||
|
||||
ImageView(item.portraitImageSources(maxWidth: 60))
|
||||
.failure {
|
||||
SystemImageContentView(systemName: item.systemImage)
|
||||
}
|
||||
}
|
||||
.posterStyle(.portrait)
|
||||
}
|
||||
}
|
||||
.frame(width: 100)
|
||||
.accessibilityIgnoresInvertColors()
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
if let parent = item.parentTitle {
|
||||
Text(parent)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
Text(item.displayTitle)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(2)
|
||||
|
||||
if let subtitle = item.subtitle {
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowCornerRadius(0)
|
||||
.listRowInsets(.zero)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let session = box.value {
|
||||
if let nowPlayingItem = session.nowPlayingItem, let playState = session.playState {
|
||||
sessionContent(
|
||||
session: session,
|
||||
nowPlayingItem: nowPlayingItem,
|
||||
playState: playState
|
||||
)
|
||||
} else {
|
||||
idleContent(session: session)
|
||||
}
|
||||
} else {
|
||||
Text(L10n.noSession)
|
||||
}
|
||||
}
|
||||
.animation(.linear(duration: 0.2), value: box.value)
|
||||
.navigationTitle(L10n.session)
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
extension ActiveSessionDetailView {
|
||||
|
||||
struct StreamSection: View {
|
||||
|
||||
let nowPlayingItem: BaseItemDto
|
||||
let transcodingInfo: TranscodingInfo?
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
// Create the Audio Codec Flow if the stream uses Audio
|
||||
if let sourceAudioCodec = nowPlayingItem.mediaStreams?.first(where: { $0.type == .audio })?.codec {
|
||||
getMediaComparison(
|
||||
sourceComponent: sourceAudioCodec,
|
||||
destinationComponent: transcodingInfo?.audioCodec ?? sourceAudioCodec
|
||||
)
|
||||
}
|
||||
|
||||
// Create the Video Codec Flow if the stream uses Video
|
||||
if let sourceVideoCodec = nowPlayingItem.mediaStreams?.first(where: { $0.type == .video })?.codec {
|
||||
getMediaComparison(
|
||||
sourceComponent: sourceVideoCodec,
|
||||
destinationComponent: transcodingInfo?.videoCodec ?? sourceVideoCodec
|
||||
)
|
||||
}
|
||||
|
||||
// Create the Container Flow if the stream has a Container
|
||||
if let sourceContainer = nowPlayingItem.container {
|
||||
getMediaComparison(
|
||||
sourceComponent: sourceContainer,
|
||||
destinationComponent: transcodingInfo?.container ?? sourceContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Transcoding Details
|
||||
|
||||
@ViewBuilder
|
||||
private func getMediaComparison(sourceComponent: String, destinationComponent: String) -> some View {
|
||||
HStack {
|
||||
Text(sourceComponent)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
|
||||
Image(systemName: (destinationComponent != sourceComponent) ? "shuffle" : "arrow.right")
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Text(destinationComponent)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
extension ActiveSessionDetailView {
|
||||
|
||||
struct TranscodeSection: View {
|
||||
|
||||
let transcodeReasons: [TranscodeReason]
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
|
||||
let transcodeIcons = Set(transcodeReasons.map(\.systemImage)).sorted()
|
||||
|
||||
HStack {
|
||||
ForEach(transcodeIcons, id: \.self) { icon in
|
||||
Image(systemName: icon)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
ForEach(transcodeReasons, id: \.self) { reason in
|
||||
Text(reason)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import CollectionVGrid
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: filter for streaming/inactive
|
||||
|
||||
struct ActiveSessionsView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: SettingsCoordinator.Router
|
||||
|
||||
@StateObject
|
||||
private var viewModel = ActiveSessionsViewModel()
|
||||
|
||||
private let timer = Timer.publish(every: 5, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
|
||||
// MARK: - Content View
|
||||
|
||||
@ViewBuilder
|
||||
private var contentView: some View {
|
||||
if viewModel.sessions.isEmpty {
|
||||
L10n.noResults.text
|
||||
} else {
|
||||
CollectionVGrid(
|
||||
viewModel.sessions.keys,
|
||||
layout: .columns(1, insets: .zero, itemSpacing: 0, lineSpacing: 0)
|
||||
) { id in
|
||||
ActiveSessionRow(box: viewModel.sessions[id]!) {
|
||||
router.route(
|
||||
to: \.activeDeviceDetails,
|
||||
viewModel.sessions[id]!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func errorView(with error: some Error) -> some View {
|
||||
ErrorView(error: error)
|
||||
.onRetry {
|
||||
viewModel.send(.refreshSessions)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
ZStack {
|
||||
switch viewModel.state {
|
||||
case .content:
|
||||
contentView
|
||||
case let .error(error):
|
||||
errorView(with: error)
|
||||
case .initial:
|
||||
DelayedProgressView()
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.activeDevices)
|
||||
.onFirstAppear {
|
||||
viewModel.send(.refreshSessions)
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
viewModel.send(.getSessions)
|
||||
}
|
||||
.refreshable {
|
||||
viewModel.send(.refreshSessions)
|
||||
}
|
||||
.topBarTrailing {
|
||||
|
||||
if viewModel.backgroundStates.contains(.gettingSessions) {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: inactive session device image
|
||||
|
||||
extension ActiveSessionsView {
|
||||
|
||||
struct ActiveSessionRow: View {
|
||||
|
||||
@CurrentDate
|
||||
private var currentDate: Date
|
||||
|
||||
@ObservedObject
|
||||
private var box: BindingBox<SessionInfo?>
|
||||
|
||||
private let onSelect: () -> Void
|
||||
|
||||
// parent list won't show row if value is nil anyways
|
||||
private var session: SessionInfo {
|
||||
box.value ?? .init()
|
||||
}
|
||||
|
||||
init(box: BindingBox<SessionInfo?>, onSelect action: @escaping () -> Void) {
|
||||
self.box = box
|
||||
self.onSelect = action
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var rowLeading: some View {
|
||||
// TODO: better handling for different poster types
|
||||
Group {
|
||||
if session.nowPlayingItem?.type == .audio {
|
||||
ZStack {
|
||||
Color.clear
|
||||
|
||||
ImageView(session.nowPlayingItem?.squareImageSources(maxWidth: 60) ?? [])
|
||||
.failure {
|
||||
SystemImageContentView(systemName: session.nowPlayingItem?.systemImage)
|
||||
}
|
||||
}
|
||||
.squarePosterStyle()
|
||||
} else {
|
||||
ZStack {
|
||||
Color.clear
|
||||
|
||||
ImageView(session.nowPlayingItem?.portraitImageSources(maxWidth: 60) ?? [])
|
||||
.failure {
|
||||
SystemImageContentView(systemName: session.nowPlayingItem?.systemImage)
|
||||
}
|
||||
}
|
||||
.posterStyle(.portrait)
|
||||
}
|
||||
}
|
||||
.frame(width: 60)
|
||||
.posterShadow()
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func activeSessionDetails(_ nowPlayingItem: BaseItemDto, playState: PlayerStateInfo) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(session.userName ?? L10n.unknown)
|
||||
.font(.headline)
|
||||
|
||||
Text(nowPlayingItem.name ?? L10n.unknown)
|
||||
|
||||
ProgressSection(
|
||||
item: nowPlayingItem,
|
||||
playState: playState,
|
||||
transcodingInfo: session.transcodingInfo
|
||||
)
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var idleSessionDetails: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Text(session.userName ?? L10n.unknown)
|
||||
.font(.headline)
|
||||
|
||||
if let client = session.client {
|
||||
TextPairView(leading: L10n.client, trailing: client)
|
||||
}
|
||||
|
||||
if let device = session.deviceName {
|
||||
TextPairView(leading: L10n.device, trailing: device)
|
||||
}
|
||||
|
||||
if let lastActivityDate = session.lastActivityDate {
|
||||
TextPairView(
|
||||
L10n.lastSeen,
|
||||
value: Text(lastActivityDate, format: .relative(presentation: .numeric, unitsStyle: .narrow))
|
||||
)
|
||||
.id(currentDate)
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ListRow(insets: .init(vertical: 8, horizontal: EdgeInsets.edgePadding)) {
|
||||
rowLeading
|
||||
} content: {
|
||||
if let nowPlayingItem = session.nowPlayingItem, let playState = session.playState {
|
||||
activeSessionDetails(nowPlayingItem, playState: playState)
|
||||
} else {
|
||||
idleSessionDetails
|
||||
}
|
||||
}
|
||||
.onSelect(perform: onSelect)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
extension ActiveSessionsView {
|
||||
|
||||
struct ProgressSection: View {
|
||||
|
||||
@Default(.accentColor)
|
||||
private var accentColor
|
||||
|
||||
let item: BaseItemDto
|
||||
let playState: PlayerStateInfo
|
||||
let transcodingInfo: TranscodingInfo?
|
||||
|
||||
private var playbackPercentage: Double {
|
||||
clamp(Double(playState.positionTicks ?? 0) / Double(item.runTimeTicks ?? 1), min: 0, max: 1)
|
||||
}
|
||||
|
||||
private var transcodingPercentage: Double? {
|
||||
guard let c = transcodingInfo?.completionPercentage else { return nil }
|
||||
return clamp(c / 100.0, min: 0, max: 1)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var playbackInformation: some View {
|
||||
HStack {
|
||||
if playState.isPaused ?? false {
|
||||
Image(systemName: "pause.fill")
|
||||
.transition(.opacity.combined(with: .scale).animation(.bouncy))
|
||||
} else {
|
||||
Image(systemName: "play.fill")
|
||||
.transition(.opacity.combined(with: .scale).animation(.bouncy))
|
||||
}
|
||||
|
||||
if let playMethod = playState.playMethod, playMethod == .transcode {
|
||||
Text(playMethod)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 2) {
|
||||
Text(playState.positionSeconds ?? 0, format: .runtime)
|
||||
|
||||
Text("/")
|
||||
|
||||
Text(item.runTimeSeconds, format: .runtime)
|
||||
}
|
||||
.monospacedDigit()
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ProgressView(value: playbackPercentage)
|
||||
.progressViewStyle(.playback(secondaryProgress: transcodingPercentage))
|
||||
.frame(height: 5)
|
||||
.foregroundStyle(.primary, .secondary, .orange)
|
||||
|
||||
playbackInformation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: last run details
|
||||
// - result, show error if available
|
||||
// TODO: observe running status
|
||||
// - stop
|
||||
// - run
|
||||
// - progress
|
||||
// TODO: triggers
|
||||
|
||||
struct EditScheduledTaskView: View {
|
||||
|
||||
@CurrentDate
|
||||
private var currentDate: Date
|
||||
|
||||
@ObservedObject
|
||||
var observer: ServerTaskObserver
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
|
||||
ListTitleSection(
|
||||
observer.task.name ?? L10n.unknown,
|
||||
description: observer.task.description
|
||||
)
|
||||
|
||||
if let category = observer.task.category {
|
||||
TextPairView(
|
||||
leading: L10n.category,
|
||||
trailing: category
|
||||
)
|
||||
}
|
||||
|
||||
if let lastEndTime = observer.task.lastExecutionResult?.endTimeUtc {
|
||||
TextPairView(
|
||||
L10n.lastRun,
|
||||
value: Text("\(lastEndTime, format: .relative(presentation: .numeric, unitsStyle: .narrow))")
|
||||
)
|
||||
.id(currentDate)
|
||||
.monospacedDigit()
|
||||
|
||||
if let lastStartTime = observer.task.lastExecutionResult?.startTimeUtc {
|
||||
TextPairView(
|
||||
L10n.runtime,
|
||||
value: Text(
|
||||
"\(lastStartTime ..< lastEndTime, format: .components(style: .narrow))"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.task)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove after view done
|
||||
#Preview {
|
||||
NavigationView {
|
||||
EditScheduledTaskView(
|
||||
observer: .init(
|
||||
task: TaskInfo(
|
||||
category: "test",
|
||||
currentProgressPercentage: nil,
|
||||
description: "A test task",
|
||||
id: "123",
|
||||
isHidden: false,
|
||||
key: "123",
|
||||
lastExecutionResult: TaskResult(
|
||||
endTimeUtc: Date(timeIntervalSinceNow: -10),
|
||||
errorMessage: nil,
|
||||
id: nil,
|
||||
key: nil,
|
||||
longErrorMessage: nil,
|
||||
name: nil,
|
||||
startTimeUtc: Date(timeIntervalSinceNow: -30),
|
||||
status: .completed
|
||||
),
|
||||
name: "Test",
|
||||
state: .running,
|
||||
triggers: nil
|
||||
)
|
||||
)
|
||||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
extension ScheduledTasksView {
|
||||
|
||||
struct ScheduledTaskButton: View {
|
||||
|
||||
@CurrentDate
|
||||
private var currentDate: Date
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: SettingsCoordinator.Router
|
||||
|
||||
@ObservedObject
|
||||
var observer: ServerTaskObserver
|
||||
|
||||
@State
|
||||
private var isPresentingConfirmation = false
|
||||
|
||||
// MARK: - Task Details Section
|
||||
|
||||
@ViewBuilder
|
||||
private var taskView: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
|
||||
Text(observer.task.name ?? L10n.unknown)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
taskResultView
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Task Status Section
|
||||
|
||||
@ViewBuilder
|
||||
private var statusView: some View {
|
||||
switch observer.state {
|
||||
case .running:
|
||||
ProgressView(value: (observer.task.currentProgressPercentage ?? 0) / 100)
|
||||
.progressViewStyle(.gauge(systemImage: "stop.fill"))
|
||||
.transition(.opacity.combined(with: .scale).animation(.bouncy))
|
||||
default:
|
||||
Image(systemName: "play.fill")
|
||||
.foregroundStyle(.secondary)
|
||||
.transition(.opacity.combined(with: .scale).animation(.bouncy))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Task Status View
|
||||
|
||||
@ViewBuilder
|
||||
private var taskResultView: some View {
|
||||
if observer.state == .running {
|
||||
Text(L10n.running)
|
||||
} else if observer.task.state == .cancelling {
|
||||
Text(L10n.cancelling)
|
||||
} else {
|
||||
if let taskEndTime = observer.task.lastExecutionResult?.endTimeUtc {
|
||||
Text(L10n.lastRunTime(Date.RelativeFormatStyle(presentation: .numeric, unitsStyle: .narrow).format(taskEndTime)))
|
||||
.id(currentDate)
|
||||
.monospacedDigit()
|
||||
} else {
|
||||
Text(L10n.neverRun)
|
||||
}
|
||||
|
||||
if let status = observer.task.lastExecutionResult?.status, status != .completed {
|
||||
Label(
|
||||
status.displayTitle,
|
||||
systemImage: "exclamationmark.circle.fill"
|
||||
)
|
||||
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||
.foregroundStyle(.orange)
|
||||
.backport
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
Button {
|
||||
isPresentingConfirmation = true
|
||||
} label: {
|
||||
HStack {
|
||||
taskView
|
||||
|
||||
Spacer()
|
||||
|
||||
statusView
|
||||
.frame(width: 25, height: 25)
|
||||
}
|
||||
}
|
||||
.animation(.linear(duration: 0.1), value: observer.state)
|
||||
.foregroundStyle(.primary, .secondary)
|
||||
.confirmationDialog(
|
||||
observer.task.name ?? .emptyDash,
|
||||
isPresented: $isPresentingConfirmation,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Group {
|
||||
if observer.state == .running {
|
||||
Button(L10n.stop) {
|
||||
observer.send(.stop)
|
||||
}
|
||||
} else {
|
||||
Button(L10n.run) {
|
||||
observer.send(.start)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(observer.task.state == .cancelling)
|
||||
|
||||
Button(L10n.edit) {
|
||||
router.route(to: \.editScheduledTask, observer)
|
||||
}
|
||||
} message: {
|
||||
if let description = observer.task.description {
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
extension ScheduledTasksView {
|
||||
|
||||
struct ServerTaskButton: View {
|
||||
|
||||
let title: String
|
||||
let systemImage: String
|
||||
let warningMessage: String
|
||||
let isPresented: Binding<Bool>
|
||||
let action: () -> Void
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
Button(role: .destructive) {
|
||||
isPresented.wrappedValue = true
|
||||
} label: {
|
||||
HStack {
|
||||
Text(title)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: systemImage)
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
title,
|
||||
isPresented: isPresented,
|
||||
titleVisibility: .hidden
|
||||
) {
|
||||
Button(title, role: .destructive, action: action)
|
||||
} message: {
|
||||
Text(warningMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: refactor after socket implementation
|
||||
|
||||
struct ScheduledTasksView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: SettingsCoordinator.Router
|
||||
|
||||
@State
|
||||
private var isPresentingRestartConfirmation = false
|
||||
@State
|
||||
private var isPresentingShutdownConfirmation = false
|
||||
|
||||
@StateObject
|
||||
private var viewModel = ScheduledTasksViewModel()
|
||||
|
||||
private let timer = Timer.publish(every: 5, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
|
||||
// MARK: - Server Function Buttons
|
||||
|
||||
@ViewBuilder
|
||||
private var serverFunctions: some View {
|
||||
ServerTaskButton(
|
||||
title: L10n.restartServer,
|
||||
systemImage: "arrow.clockwise",
|
||||
warningMessage: L10n.restartWarning,
|
||||
isPresented: $isPresentingRestartConfirmation
|
||||
) {
|
||||
viewModel.send(.restartApplication)
|
||||
}
|
||||
|
||||
ServerTaskButton(
|
||||
title: L10n.shutdownServer,
|
||||
systemImage: "power",
|
||||
warningMessage: L10n.shutdownWarning,
|
||||
isPresented: $isPresentingShutdownConfirmation
|
||||
) {
|
||||
viewModel.send(.shutdownApplication)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
@ViewBuilder
|
||||
private var contentView: some View {
|
||||
List {
|
||||
|
||||
ListTitleSection(
|
||||
L10n.tasks,
|
||||
description: L10n.tasksDescription
|
||||
) {
|
||||
UIApplication.shared.open(URL(string: "https://jellyfin.org/docs/general/server/tasks")!)
|
||||
}
|
||||
|
||||
Section(L10n.server) {
|
||||
serverFunctions
|
||||
}
|
||||
|
||||
ForEach(viewModel.tasks.keys, id: \.self) { category in
|
||||
Section(category) {
|
||||
ForEach(viewModel.tasks[category] ?? []) { task in
|
||||
ScheduledTaskButton(observer: task)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func errorView(with error: some Error) -> some View {
|
||||
ErrorView(error: error)
|
||||
.onRetry {
|
||||
viewModel.send(.refreshTasks)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.clear
|
||||
|
||||
switch viewModel.state {
|
||||
case .content:
|
||||
contentView
|
||||
case let .error(error):
|
||||
errorView(with: error)
|
||||
case .initial:
|
||||
DelayedProgressView()
|
||||
}
|
||||
}
|
||||
.animation(.linear(duration: 0.2), value: viewModel.state)
|
||||
.navigationTitle(L10n.tasks)
|
||||
.onFirstAppear {
|
||||
viewModel.send(.refreshTasks)
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
viewModel.send(.getTasks)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: could filter based on known log names from server
|
||||
// - ffmpeg
|
||||
// - record-transcode
|
||||
// TODO: download to device?
|
||||
// TODO: super cool log parser?
|
||||
// - separate package
|
||||
|
||||
struct ServerLogsView: View {
|
||||
|
||||
@StateObject
|
||||
private var viewModel = ServerLogsViewModel()
|
||||
|
||||
@ViewBuilder
|
||||
private var contentView: some View {
|
||||
List {
|
||||
ForEach(viewModel.logs, id: \.self) { log in
|
||||
Button {
|
||||
let request = Paths.getLogFile(name: log.name!)
|
||||
let url = viewModel.userSession.client.fullURL(with: request, queryAPIKey: true)!
|
||||
|
||||
UIApplication.shared.open(url)
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(log.name ?? .emptyDash)
|
||||
|
||||
if let modifiedDate = log.dateModified {
|
||||
Text(modifiedDate, format: .dateTime)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Image(systemName: "arrow.up.forward")
|
||||
.font(.body.weight(.regular))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.primary, .secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func errorView(with error: some Error) -> some View {
|
||||
ErrorView(error: error)
|
||||
.onRetry {
|
||||
viewModel.send(.getLogs)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
switch viewModel.state {
|
||||
case .content:
|
||||
contentView
|
||||
case let .error(error):
|
||||
errorView(with: error)
|
||||
case .initial:
|
||||
DelayedProgressView()
|
||||
}
|
||||
}
|
||||
.animation(.linear(duration: 0.2), value: viewModel.state)
|
||||
.navigationBarTitle(L10n.serverLogs)
|
||||
.onFirstAppear {
|
||||
viewModel.send(.getLogs)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UserDashboardView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: SettingsCoordinator.Router
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
|
||||
ListTitleSection(
|
||||
L10n.dashboard,
|
||||
description: L10n.dashboardDescription
|
||||
)
|
||||
|
||||
ChevronButton(L10n.activeDevices)
|
||||
.onSelect {
|
||||
router.route(to: \.activeSessions)
|
||||
}
|
||||
|
||||
Section(L10n.advanced) {
|
||||
|
||||
ChevronButton(L10n.logs)
|
||||
.onSelect {
|
||||
router.route(to: \.serverLogs)
|
||||
}
|
||||
|
||||
ChevronButton(L10n.tasks)
|
||||
.onSelect {
|
||||
router.route(to: \.tasks)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.dashboard)
|
||||
}
|
||||
}
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user