mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-27 00:00:37 +00:00
Customizable Device Profiles (#1169)
* Rename ExperimentalSettingsView.swift to PlaybackQualitySettingsView.swift Fix Merge * Rename MaximumBitrateSettingsView.swift to PlaybackQualitySettingsView.swift fix merge * Re-implement on Main. Should now have all the Main changed. Added a new change to use the Device Profile as a Transcoding Profile. * Part 1 -> Making VideoPlayerType into a struct (I Hope) correctly * Part 1.1 -> Making VideoPlayerType into a struct (I Hope) correctly * Remove unneeded Files * Missing file + CustomDeviceProfileSelection -> CustomDeviceProfileAction Rename * Change + to Appending * Attempt to add StorageValues+User. Not sure if this is correct? * Move the Array unwrapping to funcitons. Not required but this should help prevent accidently doing this wrong. Add subtitles back into the custom profiles since that somehow got dropped. Added a PlaybackCompatibility enum. This might need to work for more than just video * Complete rewrite to allow multiple profiles, compatibility mode, and directplay. * Hardward -> Hardware * Update CustomDeviceProfileSettingsView.swift Double Licensing * It was actually really easy to implement iOS... Trash cans still look weird and small. * Swipe to Delete instead of the edit button * wip * wip * Linting * tvOS Implementation * wip * wip * cleanup * Create Package.resolved --------- Co-authored-by: Joseph Kribs <joseph@kribs.net> Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
58dfddeeca
commit
f5bd1b8fcd
45
Shared/Coordinators/CustomDeviceProfileCoordinator.swift
Normal file
45
Shared/Coordinators/CustomDeviceProfileCoordinator.swift
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// 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 Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class CustomDeviceProfileCoordinator: NavigationCoordinatable {
|
||||
|
||||
let stack = NavigationStack(initial: \CustomDeviceProfileCoordinator.start)
|
||||
|
||||
@Root
|
||||
var start = makeStart
|
||||
|
||||
@Route(.push)
|
||||
var customDeviceProfileSettings = makeCustomDeviceProfileSettings
|
||||
@Route(.push)
|
||||
var editCustomDeviceProfile = makeEditCustomDeviceProfile
|
||||
@Route(.push)
|
||||
var createCustomDeviceProfile = makeCreateCustomDeviceProfile
|
||||
|
||||
func makeCustomDeviceProfileSettings() -> NavigationViewCoordinator<PlaybackQualitySettingsCoordinator> {
|
||||
NavigationViewCoordinator(
|
||||
PlaybackQualitySettingsCoordinator()
|
||||
)
|
||||
}
|
||||
|
||||
func makeEditCustomDeviceProfile(profile: Binding<CustomDeviceProfile>)
|
||||
-> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
|
||||
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator(profile: profile))
|
||||
}
|
||||
|
||||
func makeCreateCustomDeviceProfile() -> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
|
||||
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator())
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeStart() -> some View {
|
||||
CustomDeviceProfileSettingsView()
|
||||
}
|
||||
}
|
57
Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift
Normal file
57
Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift
Normal file
@ -0,0 +1,57 @@
|
||||
//
|
||||
// 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 Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class EditCustomDeviceProfileCoordinator: NavigationCoordinatable {
|
||||
|
||||
let stack = NavigationStack(initial: \EditCustomDeviceProfileCoordinator.start)
|
||||
|
||||
@Root
|
||||
var start = makeStart
|
||||
|
||||
// TODO: fix for tvOS
|
||||
|
||||
@Route(.push)
|
||||
var customDeviceAudioEditor = makeCustomDeviceAudioEditor
|
||||
@Route(.push)
|
||||
var customDeviceVideoEditor = makeCustomDeviceVideoEditor
|
||||
@Route(.push)
|
||||
var customDeviceContainerEditor = makeCustomDeviceContainerEditor
|
||||
|
||||
private let profile: Binding<CustomDeviceProfile>?
|
||||
|
||||
init(profile: Binding<CustomDeviceProfile>? = nil) {
|
||||
self.profile = profile
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeCustomDeviceAudioEditor(selection: Binding<[AudioCodec]>) -> some View {
|
||||
OrderedSectionSelectorView(selection: selection, sources: AudioCodec.allCases)
|
||||
.navigationTitle(L10n.audio)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeCustomDeviceVideoEditor(selection: Binding<[VideoCodec]>) -> some View {
|
||||
OrderedSectionSelectorView(selection: selection, sources: VideoCodec.allCases)
|
||||
.navigationTitle(L10n.video)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeCustomDeviceContainerEditor(selection: Binding<[MediaContainer]>) -> some View {
|
||||
OrderedSectionSelectorView(selection: selection, sources: MediaContainer.allCases)
|
||||
.navigationTitle(L10n.containers)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeStart() -> some View {
|
||||
CustomDeviceProfileSettingsView.EditCustomDeviceProfileView(profile: profile)
|
||||
.navigationTitle(L10n.customProfile)
|
||||
}
|
||||
}
|
41
Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift
Normal file
41
Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift
Normal file
@ -0,0 +1,41 @@
|
||||
//
|
||||
// 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 Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class PlaybackQualitySettingsCoordinator: NavigationCoordinatable {
|
||||
|
||||
let stack = NavigationStack(initial: \PlaybackQualitySettingsCoordinator.start)
|
||||
|
||||
@Root
|
||||
var start = makeStart
|
||||
|
||||
@Route(.push)
|
||||
var customDeviceProfileSettings = makeCustomDeviceProfileSettings
|
||||
|
||||
func makeCustomDeviceProfileSettings() -> NavigationViewCoordinator<CustomDeviceProfileCoordinator> {
|
||||
NavigationViewCoordinator(
|
||||
CustomDeviceProfileCoordinator()
|
||||
)
|
||||
}
|
||||
|
||||
func makeEditCustomDeviceProfile(profile: Binding<CustomDeviceProfile>)
|
||||
-> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
|
||||
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator(profile: profile))
|
||||
}
|
||||
|
||||
func makeCreateCustomDeviceProfile() -> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
|
||||
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator())
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeStart() -> some View {
|
||||
PlaybackQualitySettingsView()
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
@Route(.push)
|
||||
var nativePlayerSettings = makeNativePlayerSettings
|
||||
@Route(.push)
|
||||
var maximumBitrateSettings = makeMaximumBitrateSettings
|
||||
var playbackQualitySettings = makePlaybackQualitySettings
|
||||
@Route(.push)
|
||||
var quickConnect = makeQuickConnectAuthorize
|
||||
@Route(.push)
|
||||
@ -46,6 +46,13 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
var serverDetail = makeServerDetail
|
||||
@Route(.push)
|
||||
var videoPlayerSettings = makeVideoPlayerSettings
|
||||
@Route(.push)
|
||||
var customDeviceProfileSettings = makeCustomDeviceProfileSettings
|
||||
|
||||
@Route(.modal)
|
||||
var editCustomDeviceProfile = makeEditCustomDeviceProfile
|
||||
@Route(.modal)
|
||||
var createCustomDeviceProfile = makeCreateCustomDeviceProfile
|
||||
|
||||
#if DEBUG
|
||||
@Route(.push)
|
||||
@ -59,13 +66,15 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
@Route(.modal)
|
||||
var experimentalSettings = makeExperimentalSettings
|
||||
@Route(.modal)
|
||||
var indicatorSettings = makeIndicatorSettings
|
||||
@Route(.modal)
|
||||
var log = makeLog
|
||||
@Route(.modal)
|
||||
var serverDetail = makeServerDetail
|
||||
@Route(.modal)
|
||||
var videoPlayerSettings = makeVideoPlayerSettings
|
||||
@Route(.modal)
|
||||
var maximumBitrateSettings = makeMaximumBitrateSettings
|
||||
var playbackQualitySettings = makePlaybackQualitySettings
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
@ -75,8 +84,22 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeMaximumBitrateSettings() -> some View {
|
||||
MaximumBitrateSettingsView()
|
||||
func makePlaybackQualitySettings() -> some View {
|
||||
PlaybackQualitySettingsView()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeCustomDeviceProfileSettings() -> some View {
|
||||
CustomDeviceProfileSettingsView()
|
||||
}
|
||||
|
||||
func makeEditCustomDeviceProfile(profile: Binding<CustomDeviceProfile>)
|
||||
-> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
|
||||
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator(profile: profile))
|
||||
}
|
||||
|
||||
func makeCreateCustomDeviceProfile() -> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
|
||||
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator())
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@ -123,6 +146,15 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
EditServerView(server: server)
|
||||
}
|
||||
|
||||
func makeItemFilterDrawerSelector(selection: Binding<[ItemFilterType]>) -> some View {
|
||||
OrderedSectionSelectorView(selection: selection, sources: ItemFilterType.allCases)
|
||||
.navigationTitle(L10n.filters)
|
||||
}
|
||||
|
||||
func makeVideoPlayerSettings() -> VideoPlayerSettingsCoordinator {
|
||||
VideoPlayerSettingsCoordinator()
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@ViewBuilder
|
||||
func makeDebugSettings() -> some View {
|
||||
@ -130,20 +162,15 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
}
|
||||
#endif
|
||||
|
||||
func makeItemFilterDrawerSelector(selection: Binding<[ItemFilterType]>) -> some View {
|
||||
OrderedSectionSelectorView(selection: selection, sources: ItemFilterType.allCases)
|
||||
}
|
||||
|
||||
func makeVideoPlayerSettings() -> VideoPlayerSettingsCoordinator {
|
||||
VideoPlayerSettingsCoordinator()
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if os(tvOS)
|
||||
|
||||
func makeCustomizeViewsSettings() -> NavigationViewCoordinator<CustomizeSettingsCoordinator> {
|
||||
NavigationViewCoordinator(CustomizeSettingsCoordinator())
|
||||
func makeCustomizeViewsSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator(
|
||||
BasicNavigationViewCoordinator {
|
||||
CustomizeViewsSettings()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func makeExperimentalSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
@ -154,6 +181,12 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
)
|
||||
}
|
||||
|
||||
func makeIndicatorSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
IndicatorSettingsView()
|
||||
}
|
||||
}
|
||||
|
||||
func makeServerDetail(server: ServerState) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
EditServerView(server: server)
|
||||
@ -161,13 +194,15 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
}
|
||||
|
||||
func makeVideoPlayerSettings() -> NavigationViewCoordinator<VideoPlayerSettingsCoordinator> {
|
||||
NavigationViewCoordinator(VideoPlayerSettingsCoordinator())
|
||||
NavigationViewCoordinator(
|
||||
VideoPlayerSettingsCoordinator()
|
||||
)
|
||||
}
|
||||
|
||||
func makeMaximumBitrateSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
MaximumBitrateSettingsView()
|
||||
}
|
||||
func makePlaybackQualitySettings() -> NavigationViewCoordinator<PlaybackQualitySettingsCoordinator> {
|
||||
NavigationViewCoordinator(
|
||||
PlaybackQualitySettingsCoordinator()
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -50,3 +50,8 @@ extension Array {
|
||||
return removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
// extension Array where Element: RawRepresentable<String> {
|
||||
//
|
||||
// var asCommaString: String {}
|
||||
// }
|
||||
|
@ -13,12 +13,19 @@ import JellyfinAPI
|
||||
import Logging
|
||||
|
||||
extension BaseItemDto {
|
||||
|
||||
func videoPlayerViewModel(with mediaSource: MediaSourceInfo) async throws -> VideoPlayerViewModel {
|
||||
|
||||
let currentVideoPlayerType = Defaults[.VideoPlayer.videoPlayerType]
|
||||
let currentVideoBitrate = Defaults[.VideoPlayer.appMaximumBitrate]
|
||||
let currentVideoBitrate = Defaults[.VideoPlayer.Playback.appMaximumBitrate]
|
||||
let compatibilityMode = Defaults[.VideoPlayer.Playback.compatibilityMode]
|
||||
|
||||
let maxBitrate = try await getMaxBitrate(for: currentVideoBitrate)
|
||||
let profile = DeviceProfile.build(for: currentVideoPlayerType, maxBitrate: maxBitrate)
|
||||
let profile = DeviceProfile.build(
|
||||
for: currentVideoPlayerType,
|
||||
compatibilityMode: compatibilityMode,
|
||||
maxBitrate: maxBitrate
|
||||
)
|
||||
|
||||
let userSession = Container.shared.currentUserSession()!
|
||||
|
||||
@ -46,14 +53,17 @@ extension BaseItemDto {
|
||||
}
|
||||
|
||||
func liveVideoPlayerViewModel(with mediaSource: MediaSourceInfo, logger: Logger) async throws -> VideoPlayerViewModel {
|
||||
|
||||
let currentVideoPlayerType = Defaults[.VideoPlayer.videoPlayerType]
|
||||
let currentVideoBitrate = Defaults[.VideoPlayer.appMaximumBitrate]
|
||||
let currentVideoBitrate = Defaults[.VideoPlayer.Playback.appMaximumBitrate]
|
||||
let compatibilityMode = Defaults[.VideoPlayer.Playback.compatibilityMode]
|
||||
|
||||
let maxBitrate = try await getMaxBitrate(for: currentVideoBitrate)
|
||||
var profile = DeviceProfile.build(for: currentVideoPlayerType, maxBitrate: maxBitrate)
|
||||
if Defaults[.Experimental.liveTVForceDirectPlay] {
|
||||
profile.directPlayProfiles = [DirectPlayProfile(type: .video)]
|
||||
}
|
||||
let profile = DeviceProfile.build(
|
||||
for: currentVideoPlayerType,
|
||||
compatibilityMode: compatibilityMode,
|
||||
maxBitrate: maxBitrate
|
||||
)
|
||||
|
||||
let userSession = Container.shared.currentUserSession()!
|
||||
|
||||
@ -101,7 +111,7 @@ extension BaseItemDto {
|
||||
}
|
||||
|
||||
private func getMaxBitrate(for bitrate: PlaybackBitrate) async throws -> Int {
|
||||
let settingBitrate = Defaults[.VideoPlayer.appMaximumBitrateTest]
|
||||
let settingBitrate = Defaults[.VideoPlayer.Playback.appMaximumBitrateTest]
|
||||
|
||||
guard bitrate != .auto else {
|
||||
return try await testBitrate(with: settingBitrate.rawValue)
|
||||
|
32
Shared/Extensions/JellyfinAPI/CodecProfile.swift
Normal file
32
Shared/Extensions/JellyfinAPI/CodecProfile.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// 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 CodecProfile {
|
||||
|
||||
init(
|
||||
codec: String? = nil,
|
||||
container: String? = nil,
|
||||
type: CodecType? = nil,
|
||||
@ArrayBuilder<ProfileCondition> applyConditions: () -> [ProfileCondition] = { [] },
|
||||
@ArrayBuilder<ProfileCondition> conditions: () -> [ProfileCondition] = { [] }
|
||||
) {
|
||||
let applyConditions = applyConditions()
|
||||
let conditions = conditions()
|
||||
|
||||
self.init(
|
||||
applyConditions: applyConditions.isEmpty ? nil : applyConditions,
|
||||
codec: codec,
|
||||
conditions: conditions.isEmpty ? nil : conditions,
|
||||
container: container,
|
||||
type: type
|
||||
)
|
||||
}
|
||||
}
|
79
Shared/Extensions/JellyfinAPI/DeviceProfile.swift
Normal file
79
Shared/Extensions/JellyfinAPI/DeviceProfile.swift
Normal file
@ -0,0 +1,79 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
extension DeviceProfile {
|
||||
|
||||
static func build(
|
||||
for videoPlayer: VideoPlayerType,
|
||||
compatibilityMode: PlaybackCompatibility,
|
||||
maxBitrate: Int? = nil
|
||||
) -> DeviceProfile {
|
||||
|
||||
var deviceProfile: DeviceProfile = .init()
|
||||
|
||||
// MARK: - Video Player Specific Logic
|
||||
|
||||
deviceProfile.codecProfiles = videoPlayer.codecProfiles
|
||||
deviceProfile.responseProfiles = videoPlayer.responseProfiles
|
||||
deviceProfile.subtitleProfiles = videoPlayer.subtitleProfiles
|
||||
|
||||
// MARK: - DirectPlay & Transcoding Profiles
|
||||
|
||||
switch compatibilityMode {
|
||||
case .auto:
|
||||
deviceProfile.directPlayProfiles = videoPlayer.directPlayProfiles
|
||||
deviceProfile.transcodingProfiles = videoPlayer.transcodingProfiles
|
||||
|
||||
case .mostCompatible:
|
||||
deviceProfile.directPlayProfiles = PlaybackCompatibility.Video.compatibilityDirectPlayProfile
|
||||
deviceProfile.transcodingProfiles = PlaybackCompatibility.Video.compatibilityTranscodingProfile
|
||||
|
||||
case .directPlay:
|
||||
deviceProfile.directPlayProfiles = PlaybackCompatibility.Video.forcedDirectPlayProfile
|
||||
|
||||
case .custom:
|
||||
let customProfileMode = Defaults[.VideoPlayer.Playback.customDeviceProfileAction]
|
||||
let playbackDeviceProfile = StoredValues[.User.customDeviceProfiles]
|
||||
|
||||
if customProfileMode == .add {
|
||||
deviceProfile.directPlayProfiles = videoPlayer.directPlayProfiles
|
||||
deviceProfile.transcodingProfiles = videoPlayer.transcodingProfiles
|
||||
} else {
|
||||
deviceProfile.directPlayProfiles = []
|
||||
|
||||
// Only clear the Transcoding Profiles if one of the CustomProfiles is active as a Transcoding Profile
|
||||
if playbackDeviceProfile.contains(where: { $0.useAsTranscodingProfile == true }) {
|
||||
deviceProfile.transcodingProfiles = []
|
||||
} else {
|
||||
deviceProfile.transcodingProfiles = videoPlayer.transcodingProfiles
|
||||
}
|
||||
}
|
||||
|
||||
for profile in playbackDeviceProfile where profile.type == .video {
|
||||
deviceProfile.directPlayProfiles?.append(profile.directPlayProfile)
|
||||
|
||||
if profile.useAsTranscodingProfile {
|
||||
deviceProfile.transcodingProfiles?.append(profile.transcodingProfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Assign the Bitrate if provided
|
||||
|
||||
if let maxBitrate {
|
||||
deviceProfile.maxStaticBitrate = maxBitrate
|
||||
deviceProfile.maxStreamingBitrate = maxBitrate
|
||||
deviceProfile.musicStreamingTranscodingBitrate = maxBitrate
|
||||
}
|
||||
|
||||
return deviceProfile
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
extension DeviceProfile {
|
||||
|
||||
static func nativeProfile() -> DeviceProfile {
|
||||
|
||||
var profile: DeviceProfile = .init()
|
||||
|
||||
// Build direct play profiles
|
||||
profile.directPlayProfiles = [
|
||||
// Apple limitation: no mp3 in mp4; avi only supports mjpeg with pcm
|
||||
// Right now, mp4 restrictions can't be enforced because mp4, m4v, mov, 3gp,3g2 treated the same
|
||||
DirectPlayProfile(
|
||||
audioCodec: "flac,alac,aac,eac3,ac3,opus",
|
||||
container: "mp4",
|
||||
type: .video,
|
||||
videoCodec: "hevc,h264,mpeg4"
|
||||
),
|
||||
DirectPlayProfile(
|
||||
audioCodec: "alac,aac,ac3",
|
||||
container: "m4v",
|
||||
type: .video,
|
||||
videoCodec: "h264,mpeg4"
|
||||
),
|
||||
DirectPlayProfile(
|
||||
audioCodec: "alac,aac,eac3,ac3,mp3,pcm_s24be,pcm_s24le,pcm_s16be,pcm_s16le",
|
||||
container: "mov",
|
||||
type: .video,
|
||||
videoCodec: "hevc,h264,mpeg4,mjpeg"
|
||||
),
|
||||
DirectPlayProfile(
|
||||
audioCodec: "aac,eac3,ac3,mp3",
|
||||
container: "mpegts",
|
||||
type: .video,
|
||||
videoCodec: "h264"
|
||||
),
|
||||
DirectPlayProfile(
|
||||
audioCodec: "aac,amr_nb",
|
||||
container: "3gp,3g2",
|
||||
type: .video,
|
||||
videoCodec: "h264,mpeg4"
|
||||
),
|
||||
DirectPlayProfile(
|
||||
audioCodec: "pcm_s16le,pcm_mulaw",
|
||||
container: "avi",
|
||||
type: .video,
|
||||
videoCodec: "mjpeg"
|
||||
),
|
||||
]
|
||||
|
||||
// Build transcoding profiles
|
||||
profile.transcodingProfiles = [
|
||||
TranscodingProfile(
|
||||
audioCodec: "flac,alac,aac,eac3,ac3,opus",
|
||||
isBreakOnNonKeyFrames: true,
|
||||
container: "mp4",
|
||||
context: .streaming,
|
||||
maxAudioChannels: "8",
|
||||
minSegments: 2,
|
||||
protocol: "hls",
|
||||
type: .video,
|
||||
videoCodec: "hevc,h264,mpeg4"
|
||||
),
|
||||
]
|
||||
|
||||
// Create subtitle profiles
|
||||
profile.subtitleProfiles = [
|
||||
// FFmpeg can only convert bitmap to bitmap and text to text; burn in bitmap subs
|
||||
SubtitleProfile(format: "pgssub", method: .encode),
|
||||
SubtitleProfile(format: "dvdsub", method: .encode),
|
||||
SubtitleProfile(format: "dvbsub", method: .encode),
|
||||
SubtitleProfile(format: "xsub", method: .encode),
|
||||
// According to Apple HLS authoring specs, WebVTT must be in a text file delivered via HLS
|
||||
SubtitleProfile(format: "vtt", method: .hls), // webvtt
|
||||
// Apple HLS authoring spec has closed captions in video segments and TTML in fmp4
|
||||
SubtitleProfile(format: "ttml", method: .embed),
|
||||
SubtitleProfile(format: "cc_dec", method: .embed),
|
||||
]
|
||||
|
||||
return profile
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
extension DeviceProfile {
|
||||
|
||||
// For now, assume native and VLCKit support same codec conditions
|
||||
static func sharedCodecProfiles() -> [CodecProfile] {
|
||||
|
||||
var codecProfiles: [CodecProfile] = []
|
||||
|
||||
let h264CodecConditions: [ProfileCondition] = [
|
||||
ProfileCondition(
|
||||
condition: .notEquals,
|
||||
isRequired: false,
|
||||
property: .isAnamorphic,
|
||||
value: "true"
|
||||
),
|
||||
ProfileCondition(
|
||||
condition: .equalsAny,
|
||||
isRequired: false,
|
||||
property: .videoProfile,
|
||||
value: "high|main|baseline|constrained baseline"
|
||||
),
|
||||
ProfileCondition(
|
||||
condition: .lessThanEqual,
|
||||
isRequired: false,
|
||||
property: .videoLevel,
|
||||
value: "80"
|
||||
),
|
||||
ProfileCondition(
|
||||
condition: .notEquals,
|
||||
isRequired: false,
|
||||
property: .isInterlaced,
|
||||
value: "true"
|
||||
),
|
||||
]
|
||||
|
||||
codecProfiles.append(CodecProfile(applyConditions: h264CodecConditions, codec: "h264", type: .video))
|
||||
|
||||
let hevcCodecConditions: [ProfileCondition] = [
|
||||
ProfileCondition(
|
||||
condition: .notEquals,
|
||||
isRequired: false,
|
||||
property: .isAnamorphic,
|
||||
value: "true"
|
||||
),
|
||||
ProfileCondition(
|
||||
condition: .equalsAny,
|
||||
isRequired: false,
|
||||
property: .videoProfile,
|
||||
value: "high|main|main 10"
|
||||
),
|
||||
ProfileCondition(
|
||||
condition: .lessThanEqual,
|
||||
isRequired: false,
|
||||
property: .videoLevel,
|
||||
value: "175"
|
||||
),
|
||||
ProfileCondition(
|
||||
condition: .notEquals,
|
||||
isRequired: false,
|
||||
property: .isInterlaced,
|
||||
value: "true"
|
||||
),
|
||||
]
|
||||
|
||||
codecProfiles.append(CodecProfile(applyConditions: hevcCodecConditions, codec: "hevc", type: .video))
|
||||
|
||||
return codecProfiles
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
extension DeviceProfile {
|
||||
|
||||
static func swiftfinProfile() -> DeviceProfile {
|
||||
|
||||
var profile: DeviceProfile = .init()
|
||||
|
||||
// Build direct play profiles
|
||||
profile.directPlayProfiles = [
|
||||
// Just make one profile because if VLCKit can't decode it in a certain container, ffmpeg probably can't decode it for
|
||||
// transcode either
|
||||
DirectPlayProfile(
|
||||
// No need to list containers or videocodecs since if jellyfin server can detect it/ffmpeg can decode it, so can
|
||||
// VLCKit
|
||||
// However, list audiocodecs because ffmpeg can decode TrueHD/mlp but VLCKit cannot
|
||||
audioCodec: "flac,alac,aac,eac3,ac3,dts,opus,vorbis,mp3,mp2,mp1,pcm_s24be,pcm_s24le,pcm_s16be,pcm_s16le,pcm_u8,pcm_alaw,pcm_mulaw,pcm_bluray,pcm_dvd,wavpack,wmav2,wmav1,wmapro,wmalossless,nellymoser,speex,amr_nb,amr_wb",
|
||||
type: .video
|
||||
),
|
||||
]
|
||||
|
||||
// Build transcoding profiles
|
||||
// The only cases where transcoding should occur:
|
||||
// 1) TrueHD/mlp audio
|
||||
// 2) When server forces transcode for bitrate reasons
|
||||
profile.transcodingProfiles = [TranscodingProfile(
|
||||
audioCodec: "flac,alac,aac,eac3,ac3,dts,opus,vorbis,mp3,mp2,mp1",
|
||||
// no PCM,wavpack,wmav2,wmav1,wmapro,wmalossless,nellymoser,speex,amr_nb,amr_wb in mp4
|
||||
isBreakOnNonKeyFrames: true,
|
||||
container: "mp4",
|
||||
context: .streaming,
|
||||
maxAudioChannels: "8",
|
||||
minSegments: 2,
|
||||
protocol: "hls",
|
||||
type: .video,
|
||||
videoCodec: "hevc,h264,av1,vp9,vc1,mpeg4,h263,mpeg2video,mpeg1video,mjpeg" // vp8,msmpeg4v3,msmpeg4v2,msmpeg4v1,theora,ffv1,flv1,wmv3,wmv2,wmv1
|
||||
// not supported in mp4
|
||||
)]
|
||||
|
||||
// Create subtitle profiles
|
||||
profile.subtitleProfiles = [
|
||||
SubtitleProfile(format: "pgssub", method: .embed), // *pgs* normalized to pgssub; includes sup
|
||||
SubtitleProfile(format: "dvdsub", method: .embed),
|
||||
// *dvd* normalized to dvdsub; includes sub/idx I think; microdvd case?
|
||||
SubtitleProfile(format: "subrip", method: .embed), // srt
|
||||
SubtitleProfile(format: "ass", method: .embed),
|
||||
SubtitleProfile(format: "ssa", method: .embed),
|
||||
SubtitleProfile(format: "vtt", method: .embed), // webvtt
|
||||
SubtitleProfile(format: "mov_text", method: .embed), // MPEG-4 Timed Text
|
||||
SubtitleProfile(format: "ttml", method: .embed),
|
||||
SubtitleProfile(format: "text", method: .embed), // txt
|
||||
SubtitleProfile(format: "dvbsub", method: .embed),
|
||||
// dvb_subtitle normalized to dvbsub; burned in during transcode regardless?
|
||||
SubtitleProfile(format: "libzvbi_teletextdec", method: .embed), // dvb_teletext
|
||||
SubtitleProfile(format: "xsub", method: .embed),
|
||||
SubtitleProfile(format: "vplayer", method: .embed),
|
||||
SubtitleProfile(format: "subviewer", method: .embed),
|
||||
SubtitleProfile(format: "subviewer1", method: .embed),
|
||||
SubtitleProfile(format: "sami", method: .embed), // SMI
|
||||
SubtitleProfile(format: "realtext", method: .embed),
|
||||
SubtitleProfile(format: "pjs", method: .embed), // Phoenix Subtitle
|
||||
SubtitleProfile(format: "mpl2", method: .embed),
|
||||
SubtitleProfile(format: "jacosub", method: .embed),
|
||||
SubtitleProfile(format: "cc_dec", method: .embed), // eia_608
|
||||
// Can be passed as external files; ones that jellyfin can encode to must come first
|
||||
SubtitleProfile(format: "subrip", method: .external), // srt
|
||||
SubtitleProfile(format: "ttml", method: .external),
|
||||
SubtitleProfile(format: "vtt", method: .external), // webvtt
|
||||
SubtitleProfile(format: "ass", method: .external),
|
||||
SubtitleProfile(format: "ssa", method: .external),
|
||||
SubtitleProfile(format: "pgssub", method: .external),
|
||||
SubtitleProfile(format: "text", method: .external), // txt
|
||||
SubtitleProfile(format: "dvbsub", method: .external), // dvb_subtitle normalized to dvbsub
|
||||
SubtitleProfile(format: "libzvbi_teletextdec", method: .external), // dvb_teletext
|
||||
SubtitleProfile(format: "dvdsub", method: .external),
|
||||
// *dvd* normalized to dvdsub; includes sub/idx I think; microdvd case?
|
||||
SubtitleProfile(format: "xsub", method: .external),
|
||||
SubtitleProfile(format: "vplayer", method: .external),
|
||||
SubtitleProfile(format: "subviewer", method: .external),
|
||||
SubtitleProfile(format: "subviewer1", method: .external),
|
||||
SubtitleProfile(format: "sami", method: .external), // SMI
|
||||
SubtitleProfile(format: "realtext", method: .external),
|
||||
SubtitleProfile(format: "pjs", method: .external), // Phoenix Subtitle
|
||||
SubtitleProfile(format: "mpl2", method: .external),
|
||||
SubtitleProfile(format: "jacosub", method: .external),
|
||||
]
|
||||
|
||||
return profile
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
extension DeviceProfile {
|
||||
|
||||
static func build(for videoPlayer: VideoPlayerType, maxBitrate: Int? = nil) -> DeviceProfile {
|
||||
|
||||
var deviceProfile: DeviceProfile
|
||||
|
||||
switch videoPlayer {
|
||||
case .native:
|
||||
deviceProfile = nativeProfile()
|
||||
case .swiftfin:
|
||||
deviceProfile = swiftfinProfile()
|
||||
}
|
||||
|
||||
let codecProfiles: [CodecProfile] = sharedCodecProfiles()
|
||||
let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", mimeType: "video/mp4", type: .video)]
|
||||
|
||||
deviceProfile.codecProfiles = codecProfiles
|
||||
deviceProfile.responseProfiles = responseProfiles
|
||||
|
||||
if let maxBitrate {
|
||||
deviceProfile.maxStaticBitrate = maxBitrate
|
||||
deviceProfile.maxStreamingBitrate = maxBitrate
|
||||
deviceProfile.musicStreamingTranscodingBitrate = maxBitrate
|
||||
}
|
||||
|
||||
return deviceProfile
|
||||
}
|
||||
}
|
45
Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift
Normal file
45
Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// 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 DirectPlayProfile {
|
||||
|
||||
init(
|
||||
type: DlnaProfileType,
|
||||
@CommaStringBuilder<AudioCodec> audioCodecs: () -> String = { "" },
|
||||
@CommaStringBuilder<VideoCodec> videoCodecs: () -> String = { "" },
|
||||
@CommaStringBuilder<MediaContainer> containers: () -> String = { "" }
|
||||
) {
|
||||
let audioCodecs = audioCodecs()
|
||||
let videoCodecs = videoCodecs()
|
||||
let containers = containers()
|
||||
|
||||
self.init(
|
||||
audioCodec: audioCodecs.isEmpty ? nil : audioCodecs,
|
||||
container: containers.isEmpty ? nil : containers,
|
||||
type: type,
|
||||
videoCodec: videoCodecs.isEmpty ? nil : videoCodecs
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
type: DlnaProfileType,
|
||||
audioCodecs: [AudioCodec],
|
||||
videoCodecs: [VideoCodec],
|
||||
containers: [MediaContainer]
|
||||
) {
|
||||
self.init(
|
||||
type: type,
|
||||
audioCodecs: { audioCodecs },
|
||||
videoCodecs: { videoCodecs },
|
||||
containers: { containers }
|
||||
)
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ extension MediaSourceInfo {
|
||||
let playbackURL: URL
|
||||
let streamType: StreamType
|
||||
|
||||
if let transcodingURL, !Defaults[.Experimental.forceDirectPlay] {
|
||||
if let transcodingURL {
|
||||
guard let fullTranscodeURL = userSession.client.fullURL(with: transcodingURL)
|
||||
else { throw JellyfinAPIError("Unable to make transcode URL") }
|
||||
playbackURL = fullTranscodeURL
|
||||
@ -61,7 +61,8 @@ extension MediaSourceInfo {
|
||||
subtitleStreams: subtitleStreams,
|
||||
selectedAudioStreamIndex: defaultAudioStreamIndex ?? -1,
|
||||
selectedSubtitleStreamIndex: defaultSubtitleStreamIndex ?? -1,
|
||||
chapters: item.fullChapterInfo,
|
||||
// chapters: item.fullChapterInfo,
|
||||
chapters: [],
|
||||
streamType: streamType
|
||||
)
|
||||
}
|
||||
@ -72,7 +73,7 @@ extension MediaSourceInfo {
|
||||
let playbackURL: URL
|
||||
let streamType: StreamType
|
||||
|
||||
if let transcodingURL, !Defaults[.Experimental.liveTVForceDirectPlay] {
|
||||
if let transcodingURL {
|
||||
guard let fullTranscodeURL = URL(string: transcodingURL, relativeTo: userSession.server.currentURL)
|
||||
else { throw JellyfinAPIError("Unable to construct transcoded url") }
|
||||
playbackURL = fullTranscodeURL
|
||||
|
40
Shared/Extensions/JellyfinAPI/SubtitleProfile.swift
Normal file
40
Shared/Extensions/JellyfinAPI/SubtitleProfile.swift
Normal file
@ -0,0 +1,40 @@
|
||||
//
|
||||
// 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 SubtitleProfile {
|
||||
|
||||
init(
|
||||
didlMode: String? = nil,
|
||||
format: String? = nil,
|
||||
language: String? = nil,
|
||||
method: SubtitleDeliveryMethod? = nil,
|
||||
@ArrayBuilder<SubtitleFormat> containers: () -> String = { "" }
|
||||
) {
|
||||
let containers = containers()
|
||||
|
||||
self.init(
|
||||
container: containers.isEmpty ? nil : containers,
|
||||
didlMode: didlMode,
|
||||
format: format,
|
||||
language: language,
|
||||
method: method
|
||||
)
|
||||
}
|
||||
|
||||
static func build(
|
||||
method: SubtitleDeliveryMethod,
|
||||
@ArrayBuilder<SubtitleFormat> containers: () -> [SubtitleFormat]
|
||||
) -> [SubtitleProfile] {
|
||||
containers().map {
|
||||
SubtitleProfile(container: $0.rawValue, method: method)
|
||||
}
|
||||
}
|
||||
}
|
55
Shared/Extensions/JellyfinAPI/TranscodingProfile.swift
Normal file
55
Shared/Extensions/JellyfinAPI/TranscodingProfile.swift
Normal file
@ -0,0 +1,55 @@
|
||||
//
|
||||
// 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 TranscodingProfile {
|
||||
|
||||
init(
|
||||
isBreakOnNonKeyFrames: Bool? = nil,
|
||||
conditions: [ProfileCondition]? = nil,
|
||||
context: EncodingContext? = nil,
|
||||
isCopyTimestamps: Bool? = nil,
|
||||
enableMpegtsM2TsMode: Bool? = nil,
|
||||
enableSubtitlesInManifest: Bool? = nil,
|
||||
isEstimateContentLength: Bool? = nil,
|
||||
maxAudioChannels: String? = nil,
|
||||
minSegments: Int? = nil,
|
||||
protocol: String? = nil,
|
||||
segmentLength: Int? = nil,
|
||||
transcodeSeekInfo: TranscodeSeekInfo? = nil,
|
||||
type: DlnaProfileType? = nil,
|
||||
@CommaStringBuilder<AudioCodec> audioCodecs: () -> String = { "" },
|
||||
@CommaStringBuilder<VideoCodec> videoCodecs: () -> String = { "" },
|
||||
@CommaStringBuilder<MediaContainer> containers: () -> String = { "" }
|
||||
) {
|
||||
let audioCodecs = audioCodecs()
|
||||
let videoCodecs = videoCodecs()
|
||||
let containers = containers()
|
||||
|
||||
self.init(
|
||||
audioCodec: audioCodecs.isEmpty ? nil : audioCodecs,
|
||||
isBreakOnNonKeyFrames: isBreakOnNonKeyFrames,
|
||||
conditions: conditions,
|
||||
container: containers.isEmpty ? nil : containers,
|
||||
context: context,
|
||||
isCopyTimestamps: isCopyTimestamps,
|
||||
enableMpegtsM2TsMode: enableMpegtsM2TsMode,
|
||||
enableSubtitlesInManifest: enableSubtitlesInManifest,
|
||||
isEstimateContentLength: isEstimateContentLength,
|
||||
maxAudioChannels: maxAudioChannels,
|
||||
minSegments: minSegments,
|
||||
protocol: `protocol`,
|
||||
segmentLength: segmentLength,
|
||||
transcodeSeekInfo: transcodeSeekInfo,
|
||||
type: type,
|
||||
videoCodec: videoCodecs.isEmpty ? nil : videoCodecs
|
||||
)
|
||||
}
|
||||
}
|
41
Shared/Objects/ArrayBuilder.swift
Normal file
41
Shared/Objects/ArrayBuilder.swift
Normal file
@ -0,0 +1,41 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@resultBuilder
|
||||
public enum ArrayBuilder<Component> {
|
||||
|
||||
public static func buildBlock(_ components: [Component]...) -> [Component] {
|
||||
components.flatMap { $0 }
|
||||
}
|
||||
|
||||
public static func buildExpression(_ expression: Component) -> [Component] {
|
||||
[expression]
|
||||
}
|
||||
|
||||
public static func buildOptional(_ component: [Component]?) -> [Component] {
|
||||
component ?? []
|
||||
}
|
||||
|
||||
public static func buildEither(first component: [Component]) -> [Component] {
|
||||
component
|
||||
}
|
||||
|
||||
public static func buildEither(second component: [Component]) -> [Component] {
|
||||
component
|
||||
}
|
||||
|
||||
public static func buildArray(_ components: [[Component]]) -> [Component] {
|
||||
components.flatMap { $0 }
|
||||
}
|
||||
|
||||
public static func buildExpression(_ expression: [Component]) -> [Component] {
|
||||
expression
|
||||
}
|
||||
}
|
27
Shared/Objects/CommaStringBuilder.swift
Normal file
27
Shared/Objects/CommaStringBuilder.swift
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Result builder that build a comma-separated string from its components
|
||||
@resultBuilder
|
||||
struct CommaStringBuilder<Component> where Component: RawRepresentable<String> {
|
||||
|
||||
static func buildBlock(_ components: String...) -> String {
|
||||
components.joined(separator: ",")
|
||||
}
|
||||
|
||||
static func buildExpression(_ expression: Component) -> String {
|
||||
expression.rawValue
|
||||
}
|
||||
|
||||
static func buildExpression(_ expression: [Component]) -> String {
|
||||
expression.map(\.rawValue)
|
||||
.joined(separator: ",")
|
||||
}
|
||||
}
|
@ -9,17 +9,17 @@
|
||||
import Defaults
|
||||
import Foundation
|
||||
|
||||
enum VideoPlayerType: String, CaseIterable, Defaults.Serializable, Displayable {
|
||||
enum CustomDeviceProfileAction: String, CaseIterable, Displayable, Storable {
|
||||
|
||||
case native
|
||||
case swiftfin
|
||||
case add
|
||||
case replace
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .native:
|
||||
return "Native"
|
||||
case .swiftfin:
|
||||
return "Swiftfin"
|
||||
case .add:
|
||||
return "Add"
|
||||
case .replace:
|
||||
return "Replace"
|
||||
}
|
||||
}
|
||||
}
|
114
Shared/Objects/MediaComponents/AudoCodec.swift
Normal file
114
Shared/Objects/MediaComponents/AudoCodec.swift
Normal file
@ -0,0 +1,114 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum AudioCodec: String, CaseIterable, Codable, Displayable, Defaults.Serializable {
|
||||
|
||||
case aac
|
||||
case ac3
|
||||
case amr_nb
|
||||
case amr_wb
|
||||
case dts
|
||||
case dts_hd
|
||||
case eac3
|
||||
case flac
|
||||
case alac
|
||||
case mlp
|
||||
case mp1
|
||||
case mp2
|
||||
case mp3
|
||||
case nellymoser
|
||||
case opus
|
||||
case pcm_alaw
|
||||
case pcm_bluray
|
||||
case pcm_dvd
|
||||
case pcm_mulaw
|
||||
case pcm_s16be
|
||||
case pcm_s16le
|
||||
case pcm_s24be
|
||||
case pcm_s24le
|
||||
case pcm_u8
|
||||
case speex
|
||||
case truehd
|
||||
case vorbis
|
||||
case wavpack
|
||||
case wmalossless
|
||||
case wmapro
|
||||
case wmav1
|
||||
case wmav2
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .aac:
|
||||
return "AAC"
|
||||
case .ac3:
|
||||
return "AC-3"
|
||||
case .amr_nb:
|
||||
return "AMR-NB"
|
||||
case .amr_wb:
|
||||
return "AMR-WB"
|
||||
case .dts:
|
||||
return "DTS"
|
||||
case .dts_hd:
|
||||
return "DTS-HD"
|
||||
case .eac3:
|
||||
return "E-AC-3"
|
||||
case .flac:
|
||||
return "FLAC"
|
||||
case .alac:
|
||||
return "ALAC"
|
||||
case .mlp:
|
||||
return "MLP"
|
||||
case .mp1:
|
||||
return "MP1"
|
||||
case .mp2:
|
||||
return "MP2"
|
||||
case .mp3:
|
||||
return "MP3"
|
||||
case .nellymoser:
|
||||
return "Nellymoser"
|
||||
case .opus:
|
||||
return "Opus"
|
||||
case .pcm_alaw:
|
||||
return "PCM ALAW"
|
||||
case .pcm_bluray:
|
||||
return "PCM Bluray"
|
||||
case .pcm_dvd:
|
||||
return "PCM DVD"
|
||||
case .pcm_mulaw:
|
||||
return "PCM MULAW"
|
||||
case .pcm_s16be:
|
||||
return "PCM S16BE"
|
||||
case .pcm_s16le:
|
||||
return "PCM S16LE"
|
||||
case .pcm_s24be:
|
||||
return "PCM S24BE"
|
||||
case .pcm_s24le:
|
||||
return "PCM S24LE"
|
||||
case .pcm_u8:
|
||||
return "PCM U8"
|
||||
case .speex:
|
||||
return "Speex"
|
||||
case .truehd:
|
||||
return "TrueHD"
|
||||
case .vorbis:
|
||||
return "Vorbis"
|
||||
case .wavpack:
|
||||
return "WavPack"
|
||||
case .wmalossless:
|
||||
return "WMA Lossless"
|
||||
case .wmapro:
|
||||
return "WMA Pro"
|
||||
case .wmav1:
|
||||
return "WMA V1"
|
||||
case .wmav2:
|
||||
return "WMA V2"
|
||||
}
|
||||
}
|
||||
}
|
51
Shared/Objects/MediaComponents/MediaContainer.swift
Normal file
51
Shared/Objects/MediaComponents/MediaContainer.swift
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum MediaContainer: String, CaseIterable, Codable, Displayable, Defaults.Serializable {
|
||||
|
||||
case avi
|
||||
case flv
|
||||
case m4v
|
||||
case mkv
|
||||
case mov
|
||||
case mp4
|
||||
case mpegts
|
||||
case ts
|
||||
case threeG2 = "3g2"
|
||||
case threeGP = "3gp"
|
||||
case webm
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .avi:
|
||||
return "AVI"
|
||||
case .flv:
|
||||
return "FLV"
|
||||
case .m4v:
|
||||
return "M4V"
|
||||
case .mkv:
|
||||
return "MKV"
|
||||
case .mov:
|
||||
return "MOV"
|
||||
case .mp4:
|
||||
return "MP4"
|
||||
case .mpegts:
|
||||
return "MPEG-TS"
|
||||
case .ts:
|
||||
return "TS"
|
||||
case .threeG2:
|
||||
return "3G2"
|
||||
case .threeGP:
|
||||
return "3GP"
|
||||
case .webm:
|
||||
return "WEBM"
|
||||
}
|
||||
}
|
||||
}
|
82
Shared/Objects/MediaComponents/SubtitleFormat.swift
Normal file
82
Shared/Objects/MediaComponents/SubtitleFormat.swift
Normal file
@ -0,0 +1,82 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum SubtitleFormat: String, CaseIterable, Codable, Displayable, Defaults.Serializable {
|
||||
|
||||
case ass
|
||||
case cc_dec
|
||||
case dvdsub
|
||||
case dvbsub
|
||||
case jacosub
|
||||
case libzvbi_teletextdec
|
||||
case mov_text
|
||||
case mpl2
|
||||
case pjs
|
||||
case pgssub
|
||||
case realtext
|
||||
case sami
|
||||
case ssa
|
||||
case subrip
|
||||
case subviewer
|
||||
case subviewer1
|
||||
case text
|
||||
case ttml
|
||||
case vplayer
|
||||
case vtt
|
||||
case xsub
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .ass:
|
||||
return "ASS"
|
||||
case .cc_dec:
|
||||
return "EIA-608"
|
||||
case .dvdsub:
|
||||
return "DVD Subtitle"
|
||||
case .dvbsub:
|
||||
return "DVB Subtitle"
|
||||
case .jacosub:
|
||||
return "Jacosub"
|
||||
case .libzvbi_teletextdec:
|
||||
return "DVB Teletext"
|
||||
case .mov_text:
|
||||
return "MPEG-4 Timed Text"
|
||||
case .mpl2:
|
||||
return "MPL2"
|
||||
case .pjs:
|
||||
return "Phoenix Subtitle"
|
||||
case .pgssub:
|
||||
return "PGS Subtitle"
|
||||
case .realtext:
|
||||
return "RealText"
|
||||
case .sami:
|
||||
return "SMI"
|
||||
case .ssa:
|
||||
return "SSA"
|
||||
case .subrip:
|
||||
return "SRT"
|
||||
case .subviewer:
|
||||
return "SubViewer"
|
||||
case .subviewer1:
|
||||
return "SubViewer1"
|
||||
case .text:
|
||||
return "TXT"
|
||||
case .ttml:
|
||||
return "TTML"
|
||||
case .vplayer:
|
||||
return "VPlayer"
|
||||
case .vtt:
|
||||
return "WebVTT"
|
||||
case .xsub:
|
||||
return "XSUB"
|
||||
}
|
||||
}
|
||||
}
|
90
Shared/Objects/MediaComponents/VideoCodec.swift
Normal file
90
Shared/Objects/MediaComponents/VideoCodec.swift
Normal file
@ -0,0 +1,90 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum VideoCodec: String, CaseIterable, Codable, Displayable, Defaults.Serializable {
|
||||
|
||||
case av1
|
||||
case dv
|
||||
case dirac
|
||||
case ffv1
|
||||
case flv1
|
||||
case h261
|
||||
case h263
|
||||
case h264
|
||||
case hevc
|
||||
case mjpeg
|
||||
case mpeg1video
|
||||
case mpeg2video
|
||||
case mpeg4
|
||||
case msmpeg4v1
|
||||
case msmpeg4v2
|
||||
case msmpeg4v3
|
||||
case prores
|
||||
case theora
|
||||
case vc1
|
||||
case vp8
|
||||
case vp9
|
||||
case wmv1
|
||||
case wmv2
|
||||
case wmv3
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .av1:
|
||||
return "AV1"
|
||||
case .dv:
|
||||
return "DV"
|
||||
case .dirac:
|
||||
return "Dirac"
|
||||
case .ffv1:
|
||||
return "FFV1"
|
||||
case .flv1:
|
||||
return "FLV1"
|
||||
case .h261:
|
||||
return "H.261"
|
||||
case .h263:
|
||||
return "H.263"
|
||||
case .h264:
|
||||
return "H.264"
|
||||
case .hevc:
|
||||
return "HEVC"
|
||||
case .mjpeg:
|
||||
return "MJPEG"
|
||||
case .mpeg1video:
|
||||
return "MPEG-1 Video"
|
||||
case .mpeg2video:
|
||||
return "MPEG-2 Video"
|
||||
case .mpeg4:
|
||||
return "MPEG-4"
|
||||
case .msmpeg4v1:
|
||||
return "MS MPEG-4 v1"
|
||||
case .msmpeg4v2:
|
||||
return "MS MPEG-4 v2"
|
||||
case .msmpeg4v3:
|
||||
return "MS MPEG-4 v3"
|
||||
case .prores:
|
||||
return "ProRes"
|
||||
case .theora:
|
||||
return "Theora"
|
||||
case .vc1:
|
||||
return "VC-1"
|
||||
case .vp8:
|
||||
return "VP8"
|
||||
case .vp9:
|
||||
return "VP9"
|
||||
case .wmv1:
|
||||
return "WMV1"
|
||||
case .wmv2:
|
||||
return "WMV2"
|
||||
case .wmv3:
|
||||
return "WMV3"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
//
|
||||
// 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 PlaybackCompatibility {
|
||||
|
||||
enum Video {
|
||||
|
||||
// MARK: - Compatibility Profiles
|
||||
|
||||
@ArrayBuilder<DirectPlayProfile>
|
||||
static var compatibilityDirectPlayProfile: [DirectPlayProfile] {
|
||||
DirectPlayProfile(type: .video) {
|
||||
AudioCodec.aac
|
||||
} videoCodecs: {
|
||||
VideoCodec.h264
|
||||
} containers: {
|
||||
MediaContainer.mp4
|
||||
}
|
||||
}
|
||||
|
||||
@ArrayBuilder<TranscodingProfile>
|
||||
static var compatibilityTranscodingProfile: [TranscodingProfile] {
|
||||
TranscodingProfile(
|
||||
isBreakOnNonKeyFrames: true,
|
||||
context: .streaming,
|
||||
maxAudioChannels: "8",
|
||||
minSegments: 2,
|
||||
protocol: StreamType.hls.rawValue,
|
||||
type: .video
|
||||
) {
|
||||
AudioCodec.aac
|
||||
} videoCodecs: {
|
||||
VideoCodec.h264
|
||||
} containers: {
|
||||
MediaContainer.mp4
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Direct Profile
|
||||
|
||||
@ArrayBuilder<DirectPlayProfile>
|
||||
static var forcedDirectPlayProfile: [DirectPlayProfile] {
|
||||
DirectPlayProfile(type: .video)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum PlaybackCompatibility: String, CaseIterable, Defaults.Serializable, Displayable {
|
||||
|
||||
case auto
|
||||
case mostCompatible
|
||||
case directPlay
|
||||
case custom
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .auto:
|
||||
return L10n.auto
|
||||
case .mostCompatible:
|
||||
return L10n.compatible
|
||||
case .directPlay:
|
||||
return L10n.direct
|
||||
case .custom:
|
||||
return L10n.custom
|
||||
}
|
||||
}
|
||||
}
|
73
Shared/Objects/PlaybackDeviceProfile.swift
Normal file
73
Shared/Objects/PlaybackDeviceProfile.swift
Normal file
@ -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 Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
struct CustomDeviceProfile: Hashable, Storable {
|
||||
|
||||
let type: DlnaProfileType
|
||||
var useAsTranscodingProfile: Bool
|
||||
|
||||
var audio: [AudioCodec]
|
||||
var video: [VideoCodec]
|
||||
var container: [MediaContainer]
|
||||
|
||||
init(
|
||||
type: DlnaProfileType,
|
||||
useAsTranscodingProfile: Bool = false,
|
||||
audio: [AudioCodec] = [],
|
||||
video: [VideoCodec] = [],
|
||||
container: [MediaContainer] = []
|
||||
) {
|
||||
self.type = type
|
||||
self.useAsTranscodingProfile = useAsTranscodingProfile
|
||||
self.audio = audio
|
||||
self.video = video
|
||||
self.container = container
|
||||
}
|
||||
|
||||
var directPlayProfile: DirectPlayProfile {
|
||||
switch type {
|
||||
case .video:
|
||||
return DirectPlayProfile(
|
||||
type: type,
|
||||
audioCodecs: audio,
|
||||
videoCodecs: video,
|
||||
containers: container
|
||||
)
|
||||
default:
|
||||
assertionFailure("Only Video is currently supported.")
|
||||
return DirectPlayProfile()
|
||||
}
|
||||
}
|
||||
|
||||
var transcodingProfile: TranscodingProfile {
|
||||
switch type {
|
||||
case .video:
|
||||
return TranscodingProfile(
|
||||
isBreakOnNonKeyFrames: true,
|
||||
context: .streaming,
|
||||
maxAudioChannels: "8",
|
||||
minSegments: 2,
|
||||
protocol: StreamType.hls.rawValue,
|
||||
type: .video
|
||||
) {
|
||||
audio
|
||||
} videoCodecs: {
|
||||
video
|
||||
} containers: {
|
||||
container
|
||||
}
|
||||
default:
|
||||
assertionFailure("Only Video is currently supported.")
|
||||
return TranscodingProfile(audioCodec: nil)
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum StreamType: Displayable {
|
||||
enum StreamType: String, Displayable {
|
||||
|
||||
case direct
|
||||
case transcode
|
||||
|
143
Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift
Normal file
143
Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift
Normal file
@ -0,0 +1,143 @@
|
||||
//
|
||||
// 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 VideoPlayerType {
|
||||
|
||||
// MARK: direct play
|
||||
|
||||
@ArrayBuilder<DirectPlayProfile>
|
||||
static var _nativeDirectPlayProfiles: [DirectPlayProfile] {
|
||||
DirectPlayProfile(type: .video) {
|
||||
AudioCodec.aac
|
||||
AudioCodec.ac3
|
||||
AudioCodec.alac
|
||||
AudioCodec.eac3
|
||||
AudioCodec.flac
|
||||
AudioCodec.opus
|
||||
} videoCodecs: {
|
||||
VideoCodec.h261
|
||||
VideoCodec.hevc
|
||||
VideoCodec.mpeg4
|
||||
} containers: {
|
||||
MediaContainer.mp4
|
||||
}
|
||||
|
||||
DirectPlayProfile(type: .video) {
|
||||
AudioCodec.aac
|
||||
AudioCodec.ac3
|
||||
AudioCodec.alac
|
||||
} videoCodecs: {
|
||||
VideoCodec.h264
|
||||
VideoCodec.mpeg4
|
||||
} containers: {
|
||||
MediaContainer.m4v
|
||||
}
|
||||
|
||||
DirectPlayProfile(type: .video) {
|
||||
AudioCodec.aac
|
||||
AudioCodec.ac3
|
||||
AudioCodec.alac
|
||||
AudioCodec.eac3
|
||||
AudioCodec.mp3
|
||||
AudioCodec.pcm_s16be
|
||||
AudioCodec.pcm_s16le
|
||||
AudioCodec.pcm_s24be
|
||||
AudioCodec.pcm_s24le
|
||||
} videoCodecs: {
|
||||
VideoCodec.h264
|
||||
VideoCodec.hevc
|
||||
VideoCodec.mjpeg
|
||||
VideoCodec.mpeg4
|
||||
} containers: {
|
||||
MediaContainer.mov
|
||||
}
|
||||
|
||||
DirectPlayProfile(type: .video) {
|
||||
AudioCodec.aac
|
||||
AudioCodec.ac3
|
||||
AudioCodec.eac3
|
||||
AudioCodec.mp3
|
||||
} videoCodecs: {
|
||||
VideoCodec.h264
|
||||
} containers: {
|
||||
MediaContainer.mpegts
|
||||
}
|
||||
|
||||
DirectPlayProfile(type: .video) {
|
||||
AudioCodec.aac
|
||||
AudioCodec.amr_nb
|
||||
} videoCodecs: {
|
||||
VideoCodec.h264
|
||||
VideoCodec.mpeg4
|
||||
} containers: {
|
||||
MediaContainer.threeG2
|
||||
MediaContainer.threeGP
|
||||
}
|
||||
|
||||
DirectPlayProfile(type: .video) {
|
||||
AudioCodec.pcm_mulaw
|
||||
AudioCodec.pcm_s16le
|
||||
} videoCodecs: {
|
||||
VideoCodec.mjpeg
|
||||
} containers: {
|
||||
MediaContainer.avi
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: transcoding
|
||||
|
||||
@ArrayBuilder<TranscodingProfile>
|
||||
static var _nativeTranscodingProfiles: [TranscodingProfile] {
|
||||
TranscodingProfile(
|
||||
isBreakOnNonKeyFrames: true,
|
||||
context: .streaming,
|
||||
enableSubtitlesInManifest: true,
|
||||
maxAudioChannels: "8",
|
||||
minSegments: 2,
|
||||
protocol: "hls",
|
||||
type: .video
|
||||
) {
|
||||
AudioCodec.aac
|
||||
AudioCodec.ac3
|
||||
AudioCodec.alac
|
||||
AudioCodec.eac3
|
||||
AudioCodec.flac
|
||||
AudioCodec.opus
|
||||
} videoCodecs: {
|
||||
VideoCodec.h264
|
||||
VideoCodec.hevc
|
||||
VideoCodec.mpeg4
|
||||
} containers: {
|
||||
MediaContainer.mp4
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: subtitle
|
||||
|
||||
@ArrayBuilder<SubtitleProfile>
|
||||
static var _nativeSubtitleProfiles: [SubtitleProfile] {
|
||||
SubtitleProfile.build(method: .embed) {
|
||||
SubtitleFormat.cc_dec
|
||||
SubtitleFormat.ttml
|
||||
}
|
||||
|
||||
SubtitleProfile.build(method: .encode) {
|
||||
SubtitleFormat.dvbsub
|
||||
SubtitleFormat.dvdsub
|
||||
SubtitleFormat.pgssub
|
||||
SubtitleFormat.xsub
|
||||
}
|
||||
|
||||
SubtitleProfile.build(method: .hls) {
|
||||
SubtitleFormat.vtt
|
||||
}
|
||||
}
|
||||
}
|
97
Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift
Normal file
97
Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift
Normal file
@ -0,0 +1,97 @@
|
||||
//
|
||||
// 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 VideoPlayerType {
|
||||
|
||||
// MARK: codec profiles
|
||||
|
||||
@ArrayBuilder<CodecProfile>
|
||||
var codecProfiles: [CodecProfile] {
|
||||
CodecProfile(
|
||||
codec: VideoCodec.h264.rawValue,
|
||||
type: .video,
|
||||
applyConditions: {
|
||||
ProfileCondition(
|
||||
condition: .notEquals,
|
||||
isRequired: false,
|
||||
property: .isAnamorphic,
|
||||
value: "true"
|
||||
)
|
||||
|
||||
ProfileCondition(
|
||||
condition: .equalsAny,
|
||||
isRequired: false,
|
||||
property: .videoProfile,
|
||||
value: "high|main|baseline|constrained baseline"
|
||||
)
|
||||
|
||||
ProfileCondition(
|
||||
condition: .lessThanEqual,
|
||||
isRequired: false,
|
||||
property: .videoLevel,
|
||||
value: "80"
|
||||
)
|
||||
|
||||
ProfileCondition(
|
||||
condition: .notEquals,
|
||||
isRequired: false,
|
||||
property: .isInterlaced,
|
||||
value: "true"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
CodecProfile(
|
||||
codec: VideoCodec.hevc.rawValue,
|
||||
type: .video,
|
||||
applyConditions: {
|
||||
ProfileCondition(
|
||||
condition: .notEquals,
|
||||
isRequired: false,
|
||||
property: .isAnamorphic,
|
||||
value: "true"
|
||||
)
|
||||
|
||||
ProfileCondition(
|
||||
condition: .equalsAny,
|
||||
isRequired: false,
|
||||
property: .videoProfile,
|
||||
value: "high|main|main 10"
|
||||
)
|
||||
|
||||
ProfileCondition(
|
||||
condition: .lessThanEqual,
|
||||
isRequired: false,
|
||||
property: .videoLevel,
|
||||
value: "175"
|
||||
)
|
||||
|
||||
ProfileCondition(
|
||||
condition: .notEquals,
|
||||
isRequired: false,
|
||||
property: .isInterlaced,
|
||||
value: "true"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - response profiles
|
||||
|
||||
@ArrayBuilder<ResponseProfile>
|
||||
var responseProfiles: [ResponseProfile] {
|
||||
ResponseProfile(
|
||||
container: MediaContainer.m4v.rawValue,
|
||||
mimeType: "video/mp4",
|
||||
type: .video
|
||||
)
|
||||
}
|
||||
}
|
140
Shared/Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift
Normal file
140
Shared/Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift
Normal file
@ -0,0 +1,140 @@
|
||||
//
|
||||
// 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 VideoPlayerType {
|
||||
|
||||
// MARK: direct play
|
||||
|
||||
@ArrayBuilder<DirectPlayProfile>
|
||||
static var _swiftfinDirectPlayProfiles: [DirectPlayProfile] {
|
||||
DirectPlayProfile(type: .video) {
|
||||
AudioCodec.aac
|
||||
AudioCodec.ac3
|
||||
AudioCodec.alac
|
||||
AudioCodec.amr_nb
|
||||
AudioCodec.amr_wb
|
||||
AudioCodec.dts
|
||||
AudioCodec.eac3
|
||||
AudioCodec.flac
|
||||
AudioCodec.mp1
|
||||
AudioCodec.mp2
|
||||
AudioCodec.mp3
|
||||
AudioCodec.nellymoser
|
||||
AudioCodec.opus
|
||||
AudioCodec.pcm_alaw
|
||||
AudioCodec.pcm_bluray
|
||||
AudioCodec.pcm_dvd
|
||||
AudioCodec.pcm_mulaw
|
||||
AudioCodec.pcm_s16be
|
||||
AudioCodec.pcm_s16le
|
||||
AudioCodec.pcm_s24be
|
||||
AudioCodec.pcm_s24le
|
||||
AudioCodec.pcm_u8
|
||||
AudioCodec.speex
|
||||
AudioCodec.vorbis
|
||||
AudioCodec.wavpack
|
||||
AudioCodec.wmalossless
|
||||
AudioCodec.wmapro
|
||||
AudioCodec.wmav1
|
||||
AudioCodec.wmav2
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: transcoding
|
||||
|
||||
@ArrayBuilder<TranscodingProfile>
|
||||
static var _swiftfinTranscodingProfiles: [TranscodingProfile] {
|
||||
TranscodingProfile(
|
||||
isBreakOnNonKeyFrames: true,
|
||||
context: .streaming,
|
||||
maxAudioChannels: "8",
|
||||
minSegments: 2,
|
||||
protocol: StreamType.hls.rawValue,
|
||||
type: .video
|
||||
) {
|
||||
AudioCodec.aac
|
||||
AudioCodec.ac3
|
||||
AudioCodec.alac
|
||||
AudioCodec.dts
|
||||
AudioCodec.eac3
|
||||
AudioCodec.flac
|
||||
AudioCodec.mp1
|
||||
AudioCodec.mp2
|
||||
AudioCodec.mp3
|
||||
AudioCodec.opus
|
||||
AudioCodec.vorbis
|
||||
} videoCodecs: {
|
||||
VideoCodec.av1
|
||||
VideoCodec.h263
|
||||
VideoCodec.h264
|
||||
VideoCodec.hevc
|
||||
VideoCodec.mjpeg
|
||||
VideoCodec.mpeg1video
|
||||
VideoCodec.mpeg2video
|
||||
VideoCodec.mpeg4
|
||||
VideoCodec.vc1
|
||||
VideoCodec.vp9
|
||||
} containers: {
|
||||
MediaContainer.mp4
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: subtitle
|
||||
|
||||
@ArrayBuilder<SubtitleProfile>
|
||||
static var _swiftfinSubtitleProfiles: [SubtitleProfile] {
|
||||
SubtitleProfile.build(method: .embed) {
|
||||
SubtitleFormat.ass
|
||||
SubtitleFormat.cc_dec
|
||||
SubtitleFormat.dvbsub
|
||||
SubtitleFormat.dvdsub
|
||||
SubtitleFormat.jacosub
|
||||
SubtitleFormat.libzvbi_teletextdec
|
||||
SubtitleFormat.mov_text
|
||||
SubtitleFormat.mpl2
|
||||
SubtitleFormat.pgssub
|
||||
SubtitleFormat.pjs
|
||||
SubtitleFormat.realtext
|
||||
SubtitleFormat.sami
|
||||
SubtitleFormat.ssa
|
||||
SubtitleFormat.subrip
|
||||
SubtitleFormat.subviewer
|
||||
SubtitleFormat.subviewer1
|
||||
SubtitleFormat.text
|
||||
SubtitleFormat.ttml
|
||||
SubtitleFormat.vplayer
|
||||
SubtitleFormat.vtt
|
||||
SubtitleFormat.xsub
|
||||
}
|
||||
|
||||
SubtitleProfile.build(method: .external) {
|
||||
SubtitleFormat.ass
|
||||
SubtitleFormat.dvbsub
|
||||
SubtitleFormat.dvdsub
|
||||
SubtitleFormat.jacosub
|
||||
SubtitleFormat.libzvbi_teletextdec
|
||||
SubtitleFormat.mpl2
|
||||
SubtitleFormat.pgssub
|
||||
SubtitleFormat.pjs
|
||||
SubtitleFormat.realtext
|
||||
SubtitleFormat.sami
|
||||
SubtitleFormat.ssa
|
||||
SubtitleFormat.subrip
|
||||
SubtitleFormat.subviewer
|
||||
SubtitleFormat.subviewer1
|
||||
SubtitleFormat.text
|
||||
SubtitleFormat.ttml
|
||||
SubtitleFormat.vplayer
|
||||
SubtitleFormat.vtt
|
||||
SubtitleFormat.xsub
|
||||
}
|
||||
}
|
||||
}
|
54
Shared/Objects/VideoPlayerType/VideoPlayerType.swift
Normal file
54
Shared/Objects/VideoPlayerType/VideoPlayerType.swift
Normal file
@ -0,0 +1,54 @@
|
||||
//
|
||||
// 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 Foundation
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
enum VideoPlayerType: String, CaseIterable, Defaults.Serializable, Displayable {
|
||||
|
||||
case native
|
||||
case swiftfin
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .native:
|
||||
"Native"
|
||||
case .swiftfin:
|
||||
"Swiftfin"
|
||||
}
|
||||
}
|
||||
|
||||
var directPlayProfiles: [DirectPlayProfile] {
|
||||
switch self {
|
||||
case .native:
|
||||
Self._nativeDirectPlayProfiles
|
||||
case .swiftfin:
|
||||
Self._swiftfinDirectPlayProfiles
|
||||
}
|
||||
}
|
||||
|
||||
var transcodingProfiles: [TranscodingProfile] {
|
||||
switch self {
|
||||
case .native:
|
||||
Self._nativeTranscodingProfiles
|
||||
case .swiftfin:
|
||||
Self._swiftfinTranscodingProfiles
|
||||
}
|
||||
}
|
||||
|
||||
var subtitleProfiles: [SubtitleProfile] {
|
||||
switch self {
|
||||
case .native:
|
||||
Self._nativeSubtitleProfiles
|
||||
case .swiftfin:
|
||||
Self._swiftfinSubtitleProfiles
|
||||
}
|
||||
}
|
||||
}
|
@ -218,6 +218,13 @@ extension Defaults.Keys {
|
||||
static let timestampType: Key<TimestampType> = UserKey("timestampType", default: .split)
|
||||
}
|
||||
|
||||
enum Playback {
|
||||
static let appMaximumBitrate: Key<PlaybackBitrate> = UserKey("appMaximumBitrate", default: .auto)
|
||||
static let appMaximumBitrateTest: Key<PlaybackBitrateTestSize> = UserKey("appMaximumBitrateTest", default: .regular)
|
||||
static let compatibilityMode: Key<PlaybackCompatibility> = UserKey("compatibilityMode", default: .auto)
|
||||
static let customDeviceProfileAction: Key<CustomDeviceProfileAction> = UserKey("customDeviceProfileAction", default: .add)
|
||||
}
|
||||
|
||||
enum Subtitle {
|
||||
|
||||
static let subtitleColor: Key<Color> = UserKey("subtitleColor", default: .white)
|
||||
@ -235,8 +242,6 @@ extension Defaults.Keys {
|
||||
enum Experimental {
|
||||
|
||||
static let downloads: Key<Bool> = UserKey("experimentalDownloads", default: false)
|
||||
static let forceDirectPlay: Key<Bool> = UserKey("forceDirectPlay", default: false)
|
||||
static let liveTVForceDirectPlay: Key<Bool> = UserKey("liveTVForceDirectPlay", default: false)
|
||||
}
|
||||
|
||||
// tvos specific
|
||||
|
@ -52,14 +52,22 @@ internal enum L10n {
|
||||
internal static let audioTrack = L10n.tr("Localizable", "audioTrack", fallback: "Audio Track")
|
||||
/// Authorize
|
||||
internal static let authorize = L10n.tr("Localizable", "authorize", fallback: "Authorize")
|
||||
/// Auto
|
||||
internal static let auto = L10n.tr("Localizable", "auto", fallback: "Auto")
|
||||
/// Auto Play
|
||||
internal static let autoPlay = L10n.tr("Localizable", "autoPlay", fallback: "Auto Play")
|
||||
/// Back
|
||||
internal static let back = L10n.tr("Localizable", "back", fallback: "Back")
|
||||
/// Bar Buttons
|
||||
internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons")
|
||||
/// Behavior
|
||||
internal static let behavior = L10n.tr("Localizable", "behavior", fallback: "Behavior")
|
||||
/// Auto
|
||||
internal static let bitrateAuto = L10n.tr("Localizable", "bitrateAuto", fallback: "Auto")
|
||||
/// Default Bitrate
|
||||
internal static let bitrateDefault = L10n.tr("Localizable", "bitrateDefault", fallback: "Default Bitrate")
|
||||
/// Limits the internet bandwidth used during video playback
|
||||
internal static let bitrateDefaultDescription = L10n.tr("Localizable", "bitrateDefaultDescription", fallback: "Limits the internet bandwidth used during video playback")
|
||||
/// 480p - 1.5 Mbps
|
||||
internal static let bitrateKbps1500 = L10n.tr("Localizable", "bitrateKbps1500", fallback: "480p - 1.5 Mbps")
|
||||
/// 360p - 420 Kbps
|
||||
@ -90,8 +98,12 @@ internal enum L10n {
|
||||
internal static let bitrateMbps8 = L10n.tr("Localizable", "bitrateMbps8", fallback: "720p - 8 Mbps")
|
||||
/// 4K - 80 Mbps
|
||||
internal static let bitrateMbps80 = L10n.tr("Localizable", "bitrateMbps80", fallback: "4K - 80 Mbps")
|
||||
/// Larger tests result in a more accurate bitrate but may delay playback
|
||||
internal static let bitrateTestDescription = L10n.tr("Localizable", "bitrateTestDescription", fallback: "Larger tests result in a more accurate bitrate but may delay playback")
|
||||
/// Bitrate Test
|
||||
internal static let bitrateTest = L10n.tr("Localizable", "bitrateTest", fallback: "Bitrate Test")
|
||||
/// Determines the length of the 'Auto' bitrate test used to find the available internet bandwidth
|
||||
internal static let bitrateTestDescription = L10n.tr("Localizable", "bitrateTestDescription", fallback: "Determines the length of the 'Auto' bitrate test used to find the available internet bandwidth")
|
||||
/// Longer tests are more accurate but may result in a delayed playback
|
||||
internal static let bitrateTestDisclaimer = L10n.tr("Localizable", "bitrateTestDisclaimer", fallback: "Longer tests are more accurate but may result in a delayed playback")
|
||||
/// Blue
|
||||
internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue")
|
||||
/// Bugs and Features
|
||||
@ -136,6 +148,10 @@ internal enum L10n {
|
||||
internal static let compactLogo = L10n.tr("Localizable", "compactLogo", fallback: "Compact Logo")
|
||||
/// Compact Poster
|
||||
internal static let compactPoster = L10n.tr("Localizable", "compactPoster", fallback: "Compact Poster")
|
||||
/// Compatibility
|
||||
internal static let compatibility = L10n.tr("Localizable", "compatibility", fallback: "Compatibility")
|
||||
/// Most Compatible
|
||||
internal static let compatible = L10n.tr("Localizable", "compatible", fallback: "Most Compatible")
|
||||
/// Confirm Close
|
||||
internal static let confirmClose = L10n.tr("Localizable", "confirmClose", fallback: "Confirm Close")
|
||||
/// Connect
|
||||
@ -160,8 +176,18 @@ internal enum L10n {
|
||||
internal static let current = L10n.tr("Localizable", "current", fallback: "Current")
|
||||
/// Current Position
|
||||
internal static let currentPosition = L10n.tr("Localizable", "currentPosition", fallback: "Current Position")
|
||||
/// Custom
|
||||
internal static let custom = L10n.tr("Localizable", "custom", fallback: "Custom")
|
||||
/// The custom device profiles will be added to the default Swiftfin device profiles
|
||||
internal static let customDeviceProfileAdd = L10n.tr("Localizable", "customDeviceProfileAdd", fallback: "The custom device profiles will be added to the default Swiftfin device profiles")
|
||||
/// Dictates back to the Jellyfin Server what this device hardware is capable of playing
|
||||
internal static let customDeviceProfileDescription = L10n.tr("Localizable", "customDeviceProfileDescription", fallback: "Dictates back to the Jellyfin Server what this device hardware is capable of playing")
|
||||
/// The custom device profiles will replace the default Swiftfin device profiles
|
||||
internal static let customDeviceProfileReplace = L10n.tr("Localizable", "customDeviceProfileReplace", fallback: "The custom device profiles will replace the default Swiftfin device profiles")
|
||||
/// Customize
|
||||
internal static let customize = L10n.tr("Localizable", "customize", fallback: "Customize")
|
||||
/// Custom Profile
|
||||
internal static let customProfile = L10n.tr("Localizable", "customProfile", fallback: "Custom Profile")
|
||||
/// Dark
|
||||
internal static let dark = L10n.tr("Localizable", "dark", fallback: "Dark")
|
||||
/// Default Scheme
|
||||
@ -172,6 +198,10 @@ 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 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")
|
||||
/// Disabled
|
||||
@ -294,6 +324,8 @@ internal enum L10n {
|
||||
internal static let logs = L10n.tr("Localizable", "logs", fallback: "Logs")
|
||||
/// Maximum Bitrate
|
||||
internal static let maximumBitrate = L10n.tr("Localizable", "maximumBitrate", fallback: "Maximum Bitrate")
|
||||
/// This setting may result in media failing to start playback
|
||||
internal static let mayResultInPlaybackFailure = L10n.tr("Localizable", "mayResultInPlaybackFailure", fallback: "This setting may result in media failing to start playback")
|
||||
/// Media
|
||||
internal static let media = L10n.tr("Localizable", "media", fallback: "Media")
|
||||
/// Menu Buttons
|
||||
@ -426,6 +458,8 @@ internal enum L10n {
|
||||
internal static let previousItem = L10n.tr("Localizable", "previousItem", fallback: "Previous Item")
|
||||
/// Primary
|
||||
internal static let primary = L10n.tr("Localizable", "primary", fallback: "Primary")
|
||||
/// Profiles
|
||||
internal static let profiles = L10n.tr("Localizable", "profiles", fallback: "Profiles")
|
||||
/// Programs
|
||||
internal static let programs = L10n.tr("Localizable", "programs", fallback: "Programs")
|
||||
/// Progress
|
||||
@ -676,6 +710,8 @@ internal enum L10n {
|
||||
internal static let unplayed = L10n.tr("Localizable", "unplayed", fallback: "Unplayed")
|
||||
/// URL
|
||||
internal static let url = L10n.tr("Localizable", "url", fallback: "URL")
|
||||
/// Use as Transcoding Profile
|
||||
internal static let useAsTranscodingProfile = L10n.tr("Localizable", "useAsTranscodingProfile", fallback: "Use as Transcoding Profile")
|
||||
/// Use Primary Image
|
||||
internal static let usePrimaryImage = L10n.tr("Localizable", "usePrimaryImage", fallback: "Use Primary Image")
|
||||
/// Uses the primary image and hides the logo.
|
||||
|
@ -132,5 +132,13 @@ extension StoredValues.Keys {
|
||||
default: ""
|
||||
)
|
||||
}
|
||||
|
||||
static var customDeviceProfiles: Key<[CustomDeviceProfile]> {
|
||||
CurrentUserKey(
|
||||
"customDeviceProfiles",
|
||||
domain: "customDeviceProfiles",
|
||||
default: []
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
199
Swiftfin tvOS/Components/OrderedSectionSelectorView.swift
Normal file
199
Swiftfin tvOS/Components/OrderedSectionSelectorView.swift
Normal file
@ -0,0 +1,199 @@
|
||||
//
|
||||
// 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 Factory
|
||||
import SwiftUI
|
||||
|
||||
struct OrderedSectionSelectorView<Element: Displayable & Hashable>: View {
|
||||
|
||||
@Environment(\.editMode)
|
||||
private var editMode
|
||||
|
||||
@State
|
||||
private var focusedElement: Element?
|
||||
|
||||
@StateObject
|
||||
private var selection: BindingBox<[Element]>
|
||||
|
||||
private var disabledSelection: [Element] {
|
||||
sources.filter { !selection.value.contains($0) }
|
||||
}
|
||||
|
||||
private var label: (Element) -> any View
|
||||
private let sources: [Element]
|
||||
private var systemImage: String
|
||||
|
||||
private func move(from source: IndexSet, to destination: Int) {
|
||||
selection.value.move(fromOffsets: source, toOffset: destination)
|
||||
editMode?.wrappedValue = .inactive
|
||||
}
|
||||
|
||||
private func select(element: Element) {
|
||||
if selection.value.contains(element) {
|
||||
selection.value.removeAll(where: { $0 == element })
|
||||
} else {
|
||||
selection.value.append(element)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
SplitFormWindowView()
|
||||
.descriptionView {
|
||||
Image(systemName: systemImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400)
|
||||
}
|
||||
.contentView {
|
||||
List {
|
||||
EnabledSection(
|
||||
elements: $selection.value,
|
||||
label: label,
|
||||
isEditing: editMode?.wrappedValue.isEditing ?? false,
|
||||
select: select,
|
||||
move: move,
|
||||
header: {
|
||||
Group {
|
||||
HStack {
|
||||
Text(L10n.enabled)
|
||||
Spacer()
|
||||
if editMode?.wrappedValue.isEditing ?? false {
|
||||
Button("Done") {
|
||||
withAnimation {
|
||||
editMode?.wrappedValue = .inactive
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button("Edit") {
|
||||
withAnimation {
|
||||
editMode?.wrappedValue = .active
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
DisabledSection(
|
||||
elements: disabledSelection,
|
||||
label: label,
|
||||
isEditing: editMode?.wrappedValue.isEditing ?? false,
|
||||
select: select
|
||||
)
|
||||
}
|
||||
.environment(\.editMode, editMode)
|
||||
}
|
||||
.withDescriptionTopPadding()
|
||||
.animation(.linear(duration: 0.2), value: selection.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct EnabledSection<Element: Displayable & Hashable>: View {
|
||||
|
||||
@Binding
|
||||
var elements: [Element]
|
||||
|
||||
let label: (Element) -> any View
|
||||
let isEditing: Bool
|
||||
let select: (Element) -> Void
|
||||
let move: (IndexSet, Int) -> Void
|
||||
let header: () -> any View
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
if elements.isEmpty {
|
||||
Text(L10n.none)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
ForEach(elements, id: \.self) { element in
|
||||
Button {
|
||||
if !isEditing {
|
||||
select(element)
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
label(element)
|
||||
.eraseToAnyView()
|
||||
|
||||
Spacer()
|
||||
|
||||
if !isEditing {
|
||||
Image(systemName: "minus.circle.fill")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
.onMove(perform: move)
|
||||
} header: {
|
||||
header()
|
||||
.eraseToAnyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DisabledSection<Element: Displayable & Hashable>: View {
|
||||
|
||||
let elements: [Element]
|
||||
let label: (Element) -> any View
|
||||
let isEditing: Bool
|
||||
let select: (Element) -> Void
|
||||
|
||||
var body: some View {
|
||||
Section(L10n.disabled) {
|
||||
if elements.isEmpty {
|
||||
Text(L10n.none)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
ForEach(elements, id: \.self) { element in
|
||||
Button {
|
||||
if !isEditing {
|
||||
select(element)
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
label(element)
|
||||
.eraseToAnyView()
|
||||
|
||||
Spacer()
|
||||
|
||||
if !isEditing {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OrderedSectionSelectorView {
|
||||
|
||||
init(selection: Binding<[Element]>, sources: [Element]) {
|
||||
self._selection = StateObject(wrappedValue: BindingBox(source: selection))
|
||||
self.sources = sources
|
||||
self.label = { Text($0.displayTitle).foregroundColor(.primary).eraseToAnyView() }
|
||||
self.systemImage = "filemenu.and.selection"
|
||||
}
|
||||
|
||||
func label(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self {
|
||||
copy(modifying: \.label, with: content)
|
||||
}
|
||||
|
||||
func systemImage(_ systemName: String) -> Self {
|
||||
copy(modifying: \.systemImage, with: systemName)
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
extension CustomDeviceProfileSettingsView {
|
||||
struct CustomProfileButton: View {
|
||||
let profile: CustomDeviceProfile
|
||||
var onSelect: () -> Void
|
||||
|
||||
@ViewBuilder
|
||||
private func profileDetailsView(title: String, detail: String) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text(detail)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: onSelect) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
profileDetailsView(
|
||||
title: L10n.audio,
|
||||
detail: profile.audio.map(\.displayTitle).joined(separator: ", ")
|
||||
)
|
||||
|
||||
profileDetailsView(
|
||||
title: L10n.video,
|
||||
detail: profile.video.map(\.displayTitle).joined(separator: ", ")
|
||||
)
|
||||
|
||||
profileDetailsView(
|
||||
title: L10n.containers,
|
||||
detail: profile.container.map(\.displayTitle).joined(separator: ", ")
|
||||
)
|
||||
|
||||
profileDetailsView(
|
||||
title: L10n.useAsTranscodingProfile,
|
||||
detail: profile.useAsTranscodingProfile ? "Yes" : "No"
|
||||
)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.body.weight(.regular))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
//
|
||||
// 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 CustomDeviceProfileSettingsView {
|
||||
|
||||
struct EditCustomDeviceProfileView: View {
|
||||
|
||||
@StoredValue(.User.customDeviceProfiles)
|
||||
private var customDeviceProfiles
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: EditCustomDeviceProfileCoordinator.Router
|
||||
|
||||
@State
|
||||
private var isPresentingNotSaved = false
|
||||
|
||||
@StateObject
|
||||
private var profile: BindingBox<CustomDeviceProfile>
|
||||
|
||||
private let createProfile: Bool
|
||||
|
||||
private var isValid: Bool {
|
||||
profile.value.audio.isNotEmpty &&
|
||||
profile.value.video.isNotEmpty &&
|
||||
profile.value.container.isNotEmpty
|
||||
}
|
||||
|
||||
init(profile: Binding<CustomDeviceProfile>?) {
|
||||
createProfile = profile == nil
|
||||
|
||||
if let profile {
|
||||
self._profile = StateObject(wrappedValue: BindingBox(source: profile))
|
||||
} else {
|
||||
let empty = Binding<CustomDeviceProfile>(
|
||||
get: { .init(type: .video) },
|
||||
set: { _ in }
|
||||
)
|
||||
|
||||
self._profile = StateObject(
|
||||
wrappedValue: BindingBox(source: empty)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func codecSection(
|
||||
title: String,
|
||||
content: String,
|
||||
onSelect: @escaping () -> Void
|
||||
) -> some View {
|
||||
Button(action: onSelect) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
if content.isEmpty {
|
||||
Label(L10n.none, systemImage: "exclamationmark.circle.fill")
|
||||
} else {
|
||||
Text(content)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.body.weight(.regular))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SplitFormWindowView()
|
||||
.descriptionView {
|
||||
Image(systemName: "doc")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400)
|
||||
}
|
||||
.contentView {
|
||||
Section {
|
||||
Toggle(L10n.useAsTranscodingProfile, isOn: $profile.value.useAsTranscodingProfile)
|
||||
.padding(.vertical)
|
||||
} header: {
|
||||
HStack {
|
||||
Text(L10n.customProfile)
|
||||
Spacer()
|
||||
Button("Save") {
|
||||
if createProfile {
|
||||
customDeviceProfiles.append(profile.value)
|
||||
}
|
||||
router.dismissCoordinator()
|
||||
}
|
||||
.disabled(!isValid)
|
||||
}
|
||||
}
|
||||
|
||||
codecSection(
|
||||
title: L10n.audio,
|
||||
content: profile.value.audio.map(\.displayTitle).joined(separator: ", ")
|
||||
) {
|
||||
router.route(to: \.customDeviceAudioEditor, $profile.value.audio)
|
||||
}
|
||||
.padding(.vertical)
|
||||
|
||||
codecSection(
|
||||
title: L10n.video,
|
||||
content: profile.value.video.map(\.displayTitle).joined(separator: ", ")
|
||||
) {
|
||||
router.route(to: \.customDeviceVideoEditor, $profile.value.video)
|
||||
}
|
||||
.padding(.vertical)
|
||||
|
||||
codecSection(
|
||||
title: L10n.containers,
|
||||
content: profile.value.container.map(\.displayTitle).joined(separator: ", ")
|
||||
) {
|
||||
router.route(to: \.customDeviceContainerEditor, $profile.value.container)
|
||||
}
|
||||
.padding(.vertical)
|
||||
|
||||
if !isValid {
|
||||
Label("Current profile values may cause playback issues", systemImage: "exclamationmark.circle.fill")
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.customProfile)
|
||||
.alert("Profile not saved", isPresented: $isPresentingNotSaved) {
|
||||
Button("Close", role: .destructive) {
|
||||
router.dismissCoordinator()
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled(true)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
//
|
||||
// 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 Factory
|
||||
import SwiftUI
|
||||
|
||||
struct CustomDeviceProfileSettingsView: View {
|
||||
|
||||
@Default(.VideoPlayer.Playback.customDeviceProfileAction)
|
||||
private var customDeviceProfileAction
|
||||
|
||||
@StoredValue(.User.customDeviceProfiles)
|
||||
private var customProfiles: [CustomDeviceProfile]
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: CustomDeviceProfileCoordinator.Router
|
||||
|
||||
private var isValid: Bool {
|
||||
customDeviceProfileAction == .add || customProfiles.isNotEmpty
|
||||
}
|
||||
|
||||
private func removeProfile(at offsets: IndexSet) {
|
||||
customProfiles.remove(atOffsets: offsets)
|
||||
}
|
||||
|
||||
private func deleteProfile(_ profile: CustomDeviceProfile) {
|
||||
if let index = customProfiles.firstIndex(of: profile) {
|
||||
customProfiles.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SplitFormWindowView()
|
||||
.descriptionView {
|
||||
Image(systemName: "doc.on.doc")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400)
|
||||
}
|
||||
.contentView {
|
||||
Section {
|
||||
InlineEnumToggle(
|
||||
title: L10n.behavior,
|
||||
selection: $customDeviceProfileAction
|
||||
)
|
||||
} header: {
|
||||
L10n.behavior.text
|
||||
} footer: {
|
||||
VStack(spacing: 8) {
|
||||
switch customDeviceProfileAction {
|
||||
case .add:
|
||||
L10n.customDeviceProfileAdd.text
|
||||
case .replace:
|
||||
L10n.customDeviceProfileReplace.text
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
Label("No profiles defined. Playback issues may occur.", systemImage: "exclamationmark.circle.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
if customProfiles.isEmpty {
|
||||
Button("Add profile") {
|
||||
router.route(to: \.createCustomDeviceProfile)
|
||||
}
|
||||
}
|
||||
|
||||
List {
|
||||
ForEach($customProfiles, id: \.self) { $profile in
|
||||
CustomProfileButton(profile: profile) {
|
||||
router.route(to: \.editCustomDeviceProfile, $profile)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
deleteProfile(profile)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete(perform: removeProfile)
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text(L10n.profiles)
|
||||
Spacer()
|
||||
if customProfiles.isNotEmpty {
|
||||
Button("Add") {
|
||||
router.route(to: \.createCustomDeviceProfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.profiles)
|
||||
}
|
||||
}
|
@ -9,12 +9,10 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct ExperimentalSettingsView: View {
|
||||
// Note: Used for experimental settings that may be removed or implemented
|
||||
// officially. Keep for future settings.
|
||||
|
||||
@Default(.Experimental.forceDirectPlay)
|
||||
private var forceDirectPlay
|
||||
@Default(.Experimental.liveTVForceDirectPlay)
|
||||
private var liveTVForceDirectPlay
|
||||
struct ExperimentalSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
SplitFormWindowView()
|
||||
@ -24,18 +22,7 @@ struct ExperimentalSettingsView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400)
|
||||
}
|
||||
.contentView {
|
||||
|
||||
Section("Video Player") {
|
||||
|
||||
Toggle("Force Direct Play", isOn: $forceDirectPlay)
|
||||
}
|
||||
|
||||
Section("Live TV") {
|
||||
|
||||
Toggle("Live TV Force Direct Play", isOn: $liveTVForceDirectPlay)
|
||||
}
|
||||
}
|
||||
.contentView {}
|
||||
.navigationTitle(L10n.experimental)
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct MaximumBitrateSettingsView: View {
|
||||
@Default(.VideoPlayer.appMaximumBitrate)
|
||||
private var appMaximumBitrate
|
||||
@Default(.VideoPlayer.appMaximumBitrateTest)
|
||||
private var appMaximumBitrateTest
|
||||
|
||||
var body: some View {
|
||||
SplitFormWindowView()
|
||||
.descriptionView {
|
||||
Image(systemName: "network")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400)
|
||||
}
|
||||
.contentView {
|
||||
|
||||
Section {
|
||||
|
||||
InlineEnumToggle(title: L10n.maximumBitrate, selection: $appMaximumBitrate)
|
||||
|
||||
if appMaximumBitrate == PlaybackBitrate.auto {
|
||||
InlineEnumToggle(title: L10n.testSize, selection: $appMaximumBitrateTest)
|
||||
}
|
||||
} header: {
|
||||
L10n.playbackQuality.text
|
||||
} footer: {
|
||||
if appMaximumBitrate == PlaybackBitrate.auto {
|
||||
L10n.bitrateTestDescription.text
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.maximumBitrate)
|
||||
}
|
||||
}
|
@ -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 Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct PlaybackQualitySettingsView: View {
|
||||
@Default(.VideoPlayer.Playback.appMaximumBitrate)
|
||||
private var appMaximumBitrate
|
||||
@Default(.VideoPlayer.Playback.appMaximumBitrateTest)
|
||||
private var appMaximumBitrateTest
|
||||
@Default(.VideoPlayer.Playback.compatibilityMode)
|
||||
private var compatibilityMode
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: PlaybackQualitySettingsCoordinator.Router
|
||||
|
||||
var body: some View {
|
||||
SplitFormWindowView()
|
||||
.descriptionView {
|
||||
Image(systemName: "play.rectangle.on.rectangle")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400)
|
||||
}
|
||||
.contentView {
|
||||
Section {
|
||||
InlineEnumToggle(
|
||||
title: L10n.maximumBitrate,
|
||||
selection: $appMaximumBitrate
|
||||
)
|
||||
} header: {
|
||||
L10n.bitrateDefault.text
|
||||
} footer: {
|
||||
VStack(alignment: .leading) {
|
||||
L10n.bitrateDefaultDescription.text
|
||||
}
|
||||
}
|
||||
.animation(.none, value: appMaximumBitrate)
|
||||
|
||||
if appMaximumBitrate == .auto {
|
||||
Section {
|
||||
InlineEnumToggle(
|
||||
title: L10n.testSize,
|
||||
selection: $appMaximumBitrateTest
|
||||
)
|
||||
} header: {
|
||||
L10n.bitrateTest.text
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
L10n.bitrateTestDescription.text
|
||||
L10n.bitrateTestDisclaimer.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
InlineEnumToggle(
|
||||
title: L10n.compatibility,
|
||||
selection: $compatibilityMode
|
||||
)
|
||||
.animation(.none, value: compatibilityMode)
|
||||
|
||||
if compatibilityMode == .custom {
|
||||
ChevronButton(L10n.profiles)
|
||||
.onSelect {
|
||||
router.route(to: \.customDeviceProfileSettings)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
L10n.deviceProfile.text
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.playbackQuality)
|
||||
}
|
||||
}
|
@ -74,9 +74,9 @@ struct SettingsView: View {
|
||||
router.route(to: \.videoPlayerSettings)
|
||||
}
|
||||
|
||||
ChevronButton(L10n.maximumBitrate)
|
||||
ChevronButton(L10n.playbackQuality)
|
||||
.onSelect {
|
||||
router.route(to: \.maximumBitrateSettings)
|
||||
router.route(to: \.playbackQualitySettings)
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,11 +86,11 @@ struct SettingsView: View {
|
||||
.onSelect {
|
||||
router.route(to: \.customizeViewsSettings)
|
||||
}
|
||||
|
||||
ChevronButton(L10n.experimental)
|
||||
.onSelect {
|
||||
router.route(to: \.experimentalSettings)
|
||||
}
|
||||
//
|
||||
// ChevronButton(L10n.experimental)
|
||||
// .onSelect {
|
||||
// router.route(to: \.experimentalSettings)
|
||||
// }
|
||||
}
|
||||
|
||||
Section {
|
||||
|
@ -14,15 +14,44 @@
|
||||
4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; };
|
||||
4E16FD582C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; };
|
||||
4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.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 */; };
|
||||
4E2AC4C32C6C491200DD600D /* AudoCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4C12C6C491200DD600D /* AudoCodec.swift */; };
|
||||
4E2AC4C52C6C492700DD600D /* MediaContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4C42C6C492700DD600D /* MediaContainer.swift */; };
|
||||
4E2AC4C62C6C492700DD600D /* MediaContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4C42C6C492700DD600D /* MediaContainer.swift */; };
|
||||
4E2AC4C82C6C493C00DD600D /* SubtitleFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4C72C6C493C00DD600D /* SubtitleFormat.swift */; };
|
||||
4E2AC4C92C6C493C00DD600D /* SubtitleFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4C72C6C493C00DD600D /* SubtitleFormat.swift */; };
|
||||
4E2AC4CB2C6C494E00DD600D /* VideoCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4CA2C6C494E00DD600D /* VideoCodec.swift */; };
|
||||
4E2AC4CC2C6C494E00DD600D /* VideoCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4CA2C6C494E00DD600D /* VideoCodec.swift */; };
|
||||
4E2AC4CF2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4CD2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift */; };
|
||||
4E2AC4D42C6C4C1200DD600D /* OrderedSectionSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
4E73E2AE2C420207002D2A78 /* MaximumBitrateSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2AD2C420207002D2A78 /* MaximumBitrateSettingsView.swift */; };
|
||||
4E73E2B02C4211CA002D2A78 /* MaximumBitrateSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2AF2C4211CA002D2A78 /* MaximumBitrateSettingsView.swift */; };
|
||||
4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; };
|
||||
4E762AAF2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; };
|
||||
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; };
|
||||
4E8B34EB2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; };
|
||||
4E9A24E62C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24E52C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift */; };
|
||||
4E9A24E82C82B6190023DA83 /* CustomProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
4EBE064E2C7EB6D3004A6C03 /* VideoPlayerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE064C2C7EB6D3004A6C03 /* VideoPlayerType.swift */; };
|
||||
4EBE064F2C7ECE8D004A6C03 /* InlineEnumToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */; };
|
||||
4EBE06532C7ED0E1004A6C03 /* DeviceProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */; };
|
||||
4EBE06542C7ED0E1004A6C03 /* DeviceProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */; };
|
||||
4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8512C7FDFA300E2879E /* PlaybackDeviceProfile.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@ -352,6 +381,7 @@
|
||||
E13332942953BAA100EE76AB /* DownloadTaskContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13332932953BAA100EE76AB /* DownloadTaskContentView.swift */; };
|
||||
E1356E0329A730B200382563 /* SeparatorHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1356E0129A7309D00382563 /* SeparatorHStack.swift */; };
|
||||
E1356E0429A731EB00382563 /* SeparatorHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1356E0129A7309D00382563 /* SeparatorHStack.swift */; };
|
||||
E1366A222C826DA700A36DED /* EditCustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */; };
|
||||
E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1388A40293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift */; };
|
||||
E1388A43293F0AAD009721B1 /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1388A41293F0AAD009721B1 /* PreferenceUIHostingController.swift */; };
|
||||
E1388A46293F0ABA009721B1 /* SwizzleSwift in Frameworks */ = {isa = PBXBuildFile; productRef = E1388A45293F0ABA009721B1 /* SwizzleSwift */; };
|
||||
@ -458,7 +488,6 @@
|
||||
E157563029355B7900976E1F /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E157562F29355B7900976E1F /* UpdateView.swift */; };
|
||||
E15756322935642A00976E1F /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15756312935642A00976E1F /* Double.swift */; };
|
||||
E15756342936851D00976E1F /* NativeVideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15756332936851D00976E1F /* NativeVideoPlayerSettingsView.swift */; };
|
||||
E15756362936856700976E1F /* VideoPlayerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15756352936856700976E1F /* VideoPlayerType.swift */; };
|
||||
E1575E3C293C6B15001665B1 /* Files in Frameworks */ = {isa = PBXBuildFile; productRef = E1575E3B293C6B15001665B1 /* Files */; };
|
||||
E1575E56293E7650001665B1 /* VLCUI in Frameworks */ = {isa = PBXBuildFile; productRef = E1575E55293E7650001665B1 /* VLCUI */; };
|
||||
E1575E58293E7685001665B1 /* Files in Frameworks */ = {isa = PBXBuildFile; productRef = E1575E57293E7685001665B1 /* Files */; };
|
||||
@ -481,7 +510,6 @@
|
||||
E1575E72293E77B5001665B1 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D8429229340B8300D1041A /* Utilities.swift */; };
|
||||
E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */; };
|
||||
E1575E75293E77B5001665B1 /* LibraryDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */; };
|
||||
E1575E76293E77B5001665B1 /* VideoPlayerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15756352936856700976E1F /* VideoPlayerType.swift */; };
|
||||
E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C8CE7B28FF015000DF5D7B /* TrailingTimestampType.swift */; };
|
||||
E1575E7A293E77B5001665B1 /* TimeStampType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E129428F28F0BDC300796AC6 /* TimeStampType.swift */; };
|
||||
E1575E7C293E77B5001665B1 /* TimerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E306CC28EF6E8000537998 /* TimerProxy.swift */; };
|
||||
@ -652,6 +680,11 @@
|
||||
E18E021E2887492B0022598C /* RowDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01FF288749200022598C /* RowDivider.swift */; };
|
||||
E18E021F2887492B0022598C /* SystemImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */; };
|
||||
E18E02232887492B0022598C /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; };
|
||||
E19070492C84F2BB0004600E /* ButtonStyle-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19070482C84F2BB0004600E /* ButtonStyle-iOS.swift */; };
|
||||
E190704C2C858CEB0004600E /* VideoPlayerType+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = E190704B2C858CEB0004600E /* VideoPlayerType+Shared.swift */; };
|
||||
E190704D2C858CEB0004600E /* VideoPlayerType+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = E190704B2C858CEB0004600E /* VideoPlayerType+Shared.swift */; };
|
||||
E190704F2C8592B40004600E /* PlaybackCompatibility+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = E190704E2C8592B40004600E /* PlaybackCompatibility+Video.swift */; };
|
||||
E19070502C8592B40004600E /* PlaybackCompatibility+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = E190704E2C8592B40004600E /* PlaybackCompatibility+Video.swift */; };
|
||||
E1921B7428E61914003A5238 /* SpecialFeatureHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1921B7328E61914003A5238 /* SpecialFeatureHStack.swift */; };
|
||||
E1921B7628E63306003A5238 /* GestureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1921B7528E63306003A5238 /* GestureView.swift */; };
|
||||
E192608328D2D0DB002314B4 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = E192608228D2D0DB002314B4 /* Factory */; };
|
||||
@ -786,6 +819,22 @@
|
||||
E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CAF65B2BA345830087D991 /* MediaViewModel.swift */; };
|
||||
E1CAF6622BA363840087D991 /* UIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CAF6612BA363840087D991 /* UIHostingController.swift */; };
|
||||
E1CAF6632BA363840087D991 /* UIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CAF6612BA363840087D991 /* UIHostingController.swift */; };
|
||||
E1CB756F2C80E66700217C76 /* CommaStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB756E2C80E66700217C76 /* CommaStringBuilder.swift */; };
|
||||
E1CB75702C80E66700217C76 /* CommaStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB756E2C80E66700217C76 /* CommaStringBuilder.swift */; };
|
||||
E1CB75722C80E71800217C76 /* DirectPlayProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */; };
|
||||
E1CB75732C80E71800217C76 /* DirectPlayProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */; };
|
||||
E1CB75752C80EAFA00217C76 /* ArrayBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB75742C80EAFA00217C76 /* ArrayBuilder.swift */; };
|
||||
E1CB75762C80EAFA00217C76 /* ArrayBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB75742C80EAFA00217C76 /* ArrayBuilder.swift */; };
|
||||
E1CB75782C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB75772C80ECF100217C76 /* VideoPlayerType+Native.swift */; };
|
||||
E1CB75792C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB75772C80ECF100217C76 /* VideoPlayerType+Native.swift */; };
|
||||
E1CB757C2C80F00D00217C76 /* TranscodingProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB757B2C80F00D00217C76 /* TranscodingProfile.swift */; };
|
||||
E1CB757D2C80F00D00217C76 /* TranscodingProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB757B2C80F00D00217C76 /* TranscodingProfile.swift */; };
|
||||
E1CB757F2C80F28F00217C76 /* SubtitleProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB757E2C80F28F00217C76 /* SubtitleProfile.swift */; };
|
||||
E1CB75802C80F28F00217C76 /* SubtitleProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB757E2C80F28F00217C76 /* SubtitleProfile.swift */; };
|
||||
E1CB75822C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB75812C80F66900217C76 /* VideoPlayerType+Swiftfin.swift */; };
|
||||
E1CB75832C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB75812C80F66900217C76 /* VideoPlayerType+Swiftfin.swift */; };
|
||||
E1CB758B2C80F9EC00217C76 /* CodecProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB758A2C80F9EC00217C76 /* CodecProfile.swift */; };
|
||||
E1CB758C2C80F9EC00217C76 /* CodecProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CB758A2C80F9EC00217C76 /* CodecProfile.swift */; };
|
||||
E1CCF12E28ABF989006CAC9E /* PosterDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF12D28ABF989006CAC9E /* PosterDisplayType.swift */; };
|
||||
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */; };
|
||||
E1CD13EF28EF364100CB46CA /* DetectOrientationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */; };
|
||||
@ -798,14 +847,6 @@
|
||||
E1D37F492B9C648E00343D2B /* MaxHeightText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F472B9C648E00343D2B /* MaxHeightText.swift */; };
|
||||
E1D37F4B2B9CEA5C00343D2B /* ImageSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F4A2B9CEA5C00343D2B /* ImageSource.swift */; };
|
||||
E1D37F4C2B9CEA5C00343D2B /* ImageSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F4A2B9CEA5C00343D2B /* ImageSource.swift */; };
|
||||
E1D37F4E2B9CEDC400343D2B /* DeviceProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F4D2B9CEDC400343D2B /* DeviceProfile.swift */; };
|
||||
E1D37F4F2B9CEDC400343D2B /* DeviceProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F4D2B9CEDC400343D2B /* DeviceProfile.swift */; };
|
||||
E1D37F522B9CEF1E00343D2B /* DeviceProfile+SharedCodecProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F512B9CEF1E00343D2B /* DeviceProfile+SharedCodecProfiles.swift */; };
|
||||
E1D37F532B9CEF1E00343D2B /* DeviceProfile+SharedCodecProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F512B9CEF1E00343D2B /* DeviceProfile+SharedCodecProfiles.swift */; };
|
||||
E1D37F552B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F542B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift */; };
|
||||
E1D37F562B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F542B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift */; };
|
||||
E1D37F582B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F572B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift */; };
|
||||
E1D37F592B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F572B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift */; };
|
||||
E1D4BF7C2719D05000A11E64 /* AppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7B2719D05000A11E64 /* AppSettingsView.swift */; };
|
||||
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; };
|
||||
E1D4BF8A2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF892719D3D000A11E64 /* AppSettingsCoordinator.swift */; };
|
||||
@ -942,12 +983,31 @@
|
||||
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>"; };
|
||||
4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeSettingsCoordinator.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>"; };
|
||||
4E2AC4C72C6C493C00DD600D /* SubtitleFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleFormat.swift; sourceTree = "<group>"; };
|
||||
4E2AC4CA2C6C494E00DD600D /* VideoCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCodec.swift; sourceTree = "<group>"; };
|
||||
4E2AC4CD2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackQualitySettingsCoordinator.swift; sourceTree = "<group>"; };
|
||||
4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedSectionSelectorView.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
4E73E2AD2C420207002D2A78 /* MaximumBitrateSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaximumBitrateSettingsView.swift; sourceTree = "<group>"; };
|
||||
4E73E2AF2C4211CA002D2A78 /* MaximumBitrateSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaximumBitrateSettingsView.swift; sourceTree = "<group>"; };
|
||||
4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = "<group>"; };
|
||||
4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = "<group>"; };
|
||||
4E9A24E52C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileSettingsView.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
4EC1C8512C7FDFA300E2879E /* PlaybackDeviceProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackDeviceProfile.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1263,7 +1323,6 @@
|
||||
E157562F29355B7900976E1F /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
||||
E15756312935642A00976E1F /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
||||
E15756332936851D00976E1F /* NativeVideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeVideoPlayerSettingsView.swift; sourceTree = "<group>"; };
|
||||
E15756352936856700976E1F /* VideoPlayerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerType.swift; sourceTree = "<group>"; };
|
||||
E1575EA5293E7D40001665B1 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
|
||||
E1579EA62B97DC1500A31CA1 /* Eventful.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Eventful.swift; sourceTree = "<group>"; };
|
||||
E1581E26291EF59800D6C640 /* SplitContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitContentView.swift; sourceTree = "<group>"; };
|
||||
@ -1368,6 +1427,9 @@
|
||||
E18E01FF288749200022598C /* RowDivider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowDivider.swift; sourceTree = "<group>"; };
|
||||
E18E0202288749200022598C /* AttributeStyleModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeStyleModifier.swift; sourceTree = "<group>"; };
|
||||
E18E0203288749200022598C /* BlurView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurView.swift; sourceTree = "<group>"; };
|
||||
E19070482C84F2BB0004600E /* ButtonStyle-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ButtonStyle-iOS.swift"; sourceTree = "<group>"; };
|
||||
E190704B2C858CEB0004600E /* VideoPlayerType+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VideoPlayerType+Shared.swift"; sourceTree = "<group>"; };
|
||||
E190704E2C8592B40004600E /* PlaybackCompatibility+Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaybackCompatibility+Video.swift"; sourceTree = "<group>"; };
|
||||
E1921B7328E61914003A5238 /* SpecialFeatureHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialFeatureHStack.swift; sourceTree = "<group>"; };
|
||||
E1921B7528E63306003A5238 /* GestureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureView.swift; sourceTree = "<group>"; };
|
||||
E1937A3A288E54AD00CB80AA /* BaseItemDto+Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Images.swift"; sourceTree = "<group>"; };
|
||||
@ -1458,6 +1520,14 @@
|
||||
E1CAF65A2BA345830087D991 /* MediaType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaType.swift; sourceTree = "<group>"; };
|
||||
E1CAF65B2BA345830087D991 /* MediaViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaViewModel.swift; sourceTree = "<group>"; };
|
||||
E1CAF6612BA363840087D991 /* UIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIHostingController.swift; sourceTree = "<group>"; };
|
||||
E1CB756E2C80E66700217C76 /* CommaStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommaStringBuilder.swift; sourceTree = "<group>"; };
|
||||
E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectPlayProfile.swift; sourceTree = "<group>"; };
|
||||
E1CB75742C80EAFA00217C76 /* ArrayBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayBuilder.swift; sourceTree = "<group>"; };
|
||||
E1CB75772C80ECF100217C76 /* VideoPlayerType+Native.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VideoPlayerType+Native.swift"; sourceTree = "<group>"; };
|
||||
E1CB757B2C80F00D00217C76 /* TranscodingProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodingProfile.swift; sourceTree = "<group>"; };
|
||||
E1CB757E2C80F28F00217C76 /* SubtitleProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleProfile.swift; sourceTree = "<group>"; };
|
||||
E1CB75812C80F66900217C76 /* VideoPlayerType+Swiftfin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VideoPlayerType+Swiftfin.swift"; sourceTree = "<group>"; };
|
||||
E1CB758A2C80F9EC00217C76 /* CodecProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodecProfile.swift; sourceTree = "<group>"; };
|
||||
E1CCF12D28ABF989006CAC9E /* PosterDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterDisplayType.swift; sourceTree = "<group>"; };
|
||||
E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterHStack.swift; sourceTree = "<group>"; };
|
||||
E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectOrientationModifier.swift; sourceTree = "<group>"; };
|
||||
@ -1468,10 +1538,6 @@
|
||||
E1D3044328D1991900587289 /* LibraryViewTypeToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewTypeToggle.swift; sourceTree = "<group>"; };
|
||||
E1D37F472B9C648E00343D2B /* MaxHeightText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxHeightText.swift; sourceTree = "<group>"; };
|
||||
E1D37F4A2B9CEA5C00343D2B /* ImageSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSource.swift; sourceTree = "<group>"; };
|
||||
E1D37F4D2B9CEDC400343D2B /* DeviceProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfile.swift; sourceTree = "<group>"; };
|
||||
E1D37F512B9CEF1E00343D2B /* DeviceProfile+SharedCodecProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceProfile+SharedCodecProfiles.swift"; sourceTree = "<group>"; };
|
||||
E1D37F542B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceProfile+NativeProfile.swift"; sourceTree = "<group>"; };
|
||||
E1D37F572B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceProfile+SwiftfinProfile.swift"; sourceTree = "<group>"; };
|
||||
E1D4BF7B2719D05000A11E64 /* AppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsView.swift; sourceTree = "<group>"; };
|
||||
E1D4BF802719D22800A11E64 /* AppAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAppearance.swift; sourceTree = "<group>"; };
|
||||
E1D4BF892719D3D000A11E64 /* AppSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsCoordinator.swift; sourceTree = "<group>"; };
|
||||
@ -1689,6 +1755,17 @@
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E2AC4C02C6C48EB00DD600D /* MediaComponents */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E2AC4C12C6C491200DD600D /* AudoCodec.swift */,
|
||||
4E2AC4C42C6C492700DD600D /* MediaContainer.swift */,
|
||||
4E2AC4C72C6C493C00DD600D /* SubtitleFormat.swift */,
|
||||
4E2AC4CA2C6C494E00DD600D /* VideoCodec.swift */,
|
||||
);
|
||||
path = MediaComponents;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1698,6 +1775,42 @@
|
||||
path = PlaybackBitrate;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E9A24E32C82B4700023DA83 /* CustomDeviceProfileSettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E9A24E42C82B5440023DA83 /* Components */,
|
||||
4E9A24E52C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift */,
|
||||
);
|
||||
path = CustomDeviceProfileSettingsView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E9A24E42C82B5440023DA83 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */,
|
||||
4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EC1C86A2C80900B00E2879E /* CustomDeviceProfileSettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EC1C86B2C80902200E2879E /* Components */,
|
||||
4EC1C8682C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift */,
|
||||
);
|
||||
path = CustomDeviceProfileSettingsView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EC1C86B2C80902200E2879E /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EC1C86C2C80903A00E2879E /* CustomProfileButton.swift */,
|
||||
4E71D6882C80910900A0174D /* EditCustomDeviceProfileView.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5310694F2684E7EE00CFFDBA /* VideoPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1823,9 +1936,12 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1D4BF802719D22800A11E64 /* AppAppearance.swift */,
|
||||
E1CB75742C80EAFA00217C76 /* ArrayBuilder.swift */,
|
||||
E11562942C818CB2001D5DE4 /* BindingBox.swift */,
|
||||
E129429728F4785200796AC6 /* CaseIterablePicker.swift */,
|
||||
E10231432BCF8A51009D71FC /* ChannelProgram.swift */,
|
||||
E1CB756E2C80E66700217C76 /* CommaStringBuilder.swift */,
|
||||
4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */,
|
||||
E17FB55128C119D400311DFE /* Displayable.swift */,
|
||||
E1579EA62B97DC1500A31CA1 /* Eventful.swift */,
|
||||
E1092F4B29106F9F00163F57 /* GestureAction.swift */,
|
||||
@ -1834,9 +1950,12 @@
|
||||
E1C925F328875037002A7A66 /* ItemViewType.swift */,
|
||||
E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */,
|
||||
E1DE2B4E2B983F3200F6715F /* LibraryParent */,
|
||||
4E2AC4C02C6C48EB00DD600D /* MediaComponents */,
|
||||
E1AA331E2782639D00F6439C /* OverlayType.swift */,
|
||||
E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */,
|
||||
4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */,
|
||||
E190704A2C858B7B0004600E /* PlaybackCompatibility */,
|
||||
4EC1C8512C7FDFA300E2879E /* PlaybackDeviceProfile.swift */,
|
||||
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
|
||||
E1937A60288F32DB00CB80AA /* Poster.swift */,
|
||||
E1CCF12D28ABF989006CAC9E /* PosterDisplayType.swift */,
|
||||
@ -1845,7 +1964,6 @@
|
||||
E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */,
|
||||
E164A7F52BE4814700A54B18 /* SelectUserServerSelection.swift */,
|
||||
E129429228F2845000796AC6 /* SliderType.swift */,
|
||||
E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */,
|
||||
E11042742B8013DF00821020 /* Stateful.swift */,
|
||||
E149CCAC2BE6ECC8008B9331 /* Storable.swift */,
|
||||
E1EF4C402911B783008CC695 /* StreamType.swift */,
|
||||
@ -1860,7 +1978,7 @@
|
||||
E1D8429229340B8300D1041A /* Utilities.swift */,
|
||||
E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */,
|
||||
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */,
|
||||
E15756352936856700976E1F /* VideoPlayerType.swift */,
|
||||
E1CB757A2C80EF9D00217C76 /* VideoPlayerType */,
|
||||
);
|
||||
path = Objects;
|
||||
sourceTree = "<group>";
|
||||
@ -1878,6 +1996,7 @@
|
||||
E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */,
|
||||
E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */,
|
||||
E10E842B29A589860064EA49 /* NonePosterButton.swift */,
|
||||
4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */,
|
||||
E111D8F928D0400900400001 /* PagingLibraryView.swift */,
|
||||
E1C92617288756BD002A7A66 /* PosterButton.swift */,
|
||||
E1C92619288756BD002A7A66 /* PosterHStack.swift */,
|
||||
@ -2191,8 +2310,10 @@
|
||||
E1D4BF892719D3D000A11E64 /* AppSettingsCoordinator.swift */,
|
||||
E154967D296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift */,
|
||||
4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */,
|
||||
4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */,
|
||||
E17AC9702954F636003D2BC2 /* DownloadListCoordinator.swift */,
|
||||
E13332902953B91000EE76AB /* DownloadTaskCoordinator.swift */,
|
||||
4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */,
|
||||
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */,
|
||||
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */,
|
||||
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
|
||||
@ -2202,6 +2323,7 @@
|
||||
E193D5412719404B00900D82 /* MainCoordinator */,
|
||||
62C29EA726D103D500C1D2E7 /* MediaCoordinator.swift */,
|
||||
E170D106294D23BA0017224C /* MediaSourceInfoCoordinator.swift */,
|
||||
4E2AC4CD2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift */,
|
||||
E1A1528F28FD23D600600579 /* PlaybackSettingsCoordinator.swift */,
|
||||
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
|
||||
E13DD4012717EE79009D4DAF /* SelectUserCoordinator.swift */,
|
||||
@ -2564,6 +2686,7 @@
|
||||
E11CEB85289984F5003E74C7 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E19070482C84F2BB0004600E /* ButtonStyle-iOS.swift */,
|
||||
E1A3E4CC2BB7D8C8005C59F8 /* Label-iOS.swift */,
|
||||
E11CEB8828998522003E74C7 /* View */,
|
||||
);
|
||||
@ -3153,6 +3276,15 @@
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E190704A2C858B7B0004600E /* PlaybackCompatibility */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */,
|
||||
E190704E2C8592B40004600E /* PlaybackCompatibility+Video.swift */,
|
||||
);
|
||||
path = PlaybackCompatibility;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E193D5412719404B00900D82 /* MainCoordinator */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -3219,7 +3351,9 @@
|
||||
E1D37F5B2B9CF02600343D2B /* BaseItemDto */,
|
||||
E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */,
|
||||
E1002B632793CEE700E47059 /* ChapterInfo.swift */,
|
||||
E1D37F502B9CEF1300343D2B /* DeviceProfile */,
|
||||
E1CB758A2C80F9EC00217C76 /* CodecProfile.swift */,
|
||||
4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */,
|
||||
E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */,
|
||||
E1722DB029491C3900CC0239 /* ImageBlurHashes.swift */,
|
||||
E1D842902933F87500D1041A /* ItemFields.swift */,
|
||||
E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */,
|
||||
@ -3229,6 +3363,9 @@
|
||||
E122A9122788EAAD0060FA63 /* MediaStream.swift */,
|
||||
E1AD105E26D9ADDD003E4A08 /* NameGuidPair.swift */,
|
||||
E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */,
|
||||
E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */,
|
||||
E1CB757E2C80F28F00217C76 /* SubtitleProfile.swift */,
|
||||
E1CB757B2C80F00D00217C76 /* TranscodingProfile.swift */,
|
||||
E18CE0B128A229E70092E7F1 /* UserDto.swift */,
|
||||
);
|
||||
path = JellyfinAPI;
|
||||
@ -3373,15 +3510,15 @@
|
||||
path = MediaViewModel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1D37F502B9CEF1300343D2B /* DeviceProfile */ = {
|
||||
E1CB757A2C80EF9D00217C76 /* VideoPlayerType */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1D37F4D2B9CEDC400343D2B /* DeviceProfile.swift */,
|
||||
E1D37F542B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift */,
|
||||
E1D37F512B9CEF1E00343D2B /* DeviceProfile+SharedCodecProfiles.swift */,
|
||||
E1D37F572B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift */,
|
||||
4EBE064C2C7EB6D3004A6C03 /* VideoPlayerType.swift */,
|
||||
E1CB75772C80ECF100217C76 /* VideoPlayerType+Native.swift */,
|
||||
E190704B2C858CEB0004600E /* VideoPlayerType+Shared.swift */,
|
||||
E1CB75812C80F66900217C76 /* VideoPlayerType+Swiftfin.swift */,
|
||||
);
|
||||
path = DeviceProfile;
|
||||
path = VideoPlayerType;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */ = {
|
||||
@ -3485,13 +3622,14 @@
|
||||
E1E5D54A2783E26100692DFE /* SettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EC1C86A2C80900B00E2879E /* CustomDeviceProfileSettingsView */,
|
||||
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */,
|
||||
E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */,
|
||||
E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */,
|
||||
E104C86F296E087200C1C3F9 /* IndicatorSettingsView.swift */,
|
||||
E16AF11B292C98A7001422A8 /* GestureSettingsView.swift */,
|
||||
4E73E2AD2C420207002D2A78 /* MaximumBitrateSettingsView.swift */,
|
||||
E15756332936851D00976E1F /* NativeVideoPlayerSettingsView.swift */,
|
||||
4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */,
|
||||
E1545BD62BDC559500D9578F /* UserProfileSettingsView */,
|
||||
E1BE1CEB2BDB68BC008176A9 /* SettingsView */,
|
||||
E1BDF2E7295148F400CC0294 /* VideoPlayerSettingsView */,
|
||||
@ -3502,10 +3640,11 @@
|
||||
E1E5D54D2783E66600692DFE /* SettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E9A24E32C82B4700023DA83 /* CustomDeviceProfileSettingsView */,
|
||||
E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */,
|
||||
E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */,
|
||||
E104C872296E0D0A00C1C3F9 /* IndicatorSettingsView.swift */,
|
||||
4E73E2AF2C4211CA002D2A78 /* MaximumBitrateSettingsView.swift */,
|
||||
4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */,
|
||||
5398514426B64DA100101B49 /* SettingsView.swift */,
|
||||
E1549679296CB4B000C4EF88 /* VideoPlayerSettingsView.swift */,
|
||||
);
|
||||
@ -3918,8 +4057,9 @@
|
||||
E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */,
|
||||
E18E021E2887492B0022598C /* RowDivider.swift in Sources */,
|
||||
E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
|
||||
4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */,
|
||||
4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */,
|
||||
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
|
||||
E1D37F592B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift in Sources */,
|
||||
53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */,
|
||||
E1575E99293E7B1E001665B1 /* UIColor.swift in Sources */,
|
||||
E1575E92293E7B1E001665B1 /* CGSize.swift in Sources */,
|
||||
@ -3931,6 +4071,7 @@
|
||||
E10B1ED12BD9AFF200A92EAF /* V2UserModel.swift in Sources */,
|
||||
E18A8E7E28D606BE00333B9A /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
||||
E1E6C43B29AECBD30064123F /* BottomBarView.swift in Sources */,
|
||||
E190704C2C858CEB0004600E /* VideoPlayerType+Shared.swift in Sources */,
|
||||
E152107D2947ACA000375CC2 /* InvertedLightAppIcon.swift in Sources */,
|
||||
E1549663296CA2EF00C4EF88 /* UserSession.swift in Sources */,
|
||||
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */,
|
||||
@ -3941,12 +4082,12 @@
|
||||
E187A60529AD2E25008387E6 /* StepperView.swift in Sources */,
|
||||
E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */,
|
||||
E1D4BF8B2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */,
|
||||
4E73E2B02C4211CA002D2A78 /* MaximumBitrateSettingsView.swift in Sources */,
|
||||
E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */,
|
||||
E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */,
|
||||
E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
||||
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */,
|
||||
E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */,
|
||||
E1CB757F2C80F28F00217C76 /* SubtitleProfile.swift in Sources */,
|
||||
E1E0BEB829EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */,
|
||||
E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */,
|
||||
E1C926162887565C002A7A66 /* SeriesItemView.swift in Sources */,
|
||||
@ -3967,11 +4108,13 @@
|
||||
E1549665296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */,
|
||||
E1E9EFEB28C7EA2C00CC1F8B /* UserDto.swift in Sources */,
|
||||
E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */,
|
||||
4E2AC4C32C6C491200DD600D /* AudoCodec.swift in Sources */,
|
||||
E1575EA6293E7D40001665B1 /* VideoPlayer.swift in Sources */,
|
||||
E185920628CDAA6400326F80 /* CastAndCrewHStack.swift in Sources */,
|
||||
E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */,
|
||||
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
|
||||
E122A9142788EAAD0060FA63 /* MediaStream.swift in Sources */,
|
||||
4E2AC4D62C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift in Sources */,
|
||||
E102314E2BCF8A7E009D71FC /* AlternateLayoutView.swift in Sources */,
|
||||
E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */,
|
||||
E1575E85293E7A00001665B1 /* DarkAppIcon.swift in Sources */,
|
||||
@ -3983,11 +4126,13 @@
|
||||
E1575E95293E7B1E001665B1 /* Font.swift in Sources */,
|
||||
E11C15362BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */,
|
||||
E172D3AE2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */,
|
||||
4EC1C8532C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */,
|
||||
E11E374D293E7EC9009EF240 /* ItemFields.swift in Sources */,
|
||||
E1575E6E293E77B5001665B1 /* SpecialFeatureType.swift in Sources */,
|
||||
E12CC1C528D12D9B00678D5D /* SeeAllPosterButton.swift in Sources */,
|
||||
E18A8E8128D6083700333B9A /* MediaSourceInfo+ItemVideoPlayerViewModel.swift in Sources */,
|
||||
E1002B652793CEE800E47059 /* ChapterInfo.swift in Sources */,
|
||||
4E9A24E62C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift in Sources */,
|
||||
E1153D9A2BBA3E9800424D36 /* ErrorCard.swift in Sources */,
|
||||
E1ED91162B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
|
||||
E12376B32A33DFAC001F5B44 /* ItemOverviewView.swift in Sources */,
|
||||
@ -4003,25 +4148,29 @@
|
||||
E158C8D12A31947500C527C5 /* MediaSourceInfoView.swift in Sources */,
|
||||
E11BDF782B8513B40045C54A /* ItemGenre.swift in Sources */,
|
||||
E14EA16A2BF7333B00DE757A /* UserProfileImageViewModel.swift in Sources */,
|
||||
4EBE06542C7ED0E1004A6C03 /* DeviceProfile.swift in Sources */,
|
||||
E1575E98293E7B1E001665B1 /* UIApplication.swift in Sources */,
|
||||
4E2AC4C92C6C493C00DD600D /* SubtitleFormat.swift in Sources */,
|
||||
E12376B12A33DB33001F5B44 /* MediaSourceInfoCoordinator.swift in Sources */,
|
||||
4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */,
|
||||
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */,
|
||||
E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
||||
E1763A272BF303C9004DF6AB /* ServerSelectionMenu.swift in Sources */,
|
||||
4E9A24E92C82B79D0023DA83 /* EditCustomDeviceProfileCoordinator.swift in Sources */,
|
||||
E1C9261C288756BD002A7A66 /* PosterHStack.swift in Sources */,
|
||||
E1722DB229491C3900CC0239 /* ImageBlurHashes.swift in Sources */,
|
||||
E12E30F329638B140022FAC9 /* ChevronButton.swift in Sources */,
|
||||
4E9A24ED2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift in Sources */,
|
||||
E12CC1BC28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */,
|
||||
E1575E9E293E7B1E001665B1 /* Equatable.swift in Sources */,
|
||||
E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */,
|
||||
E1CB75782C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */,
|
||||
E1575E5F293E77B5001665B1 /* StreamType.swift in Sources */,
|
||||
E1803EA22BFBD6CF0039F90E /* Hashable.swift in Sources */,
|
||||
E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */,
|
||||
E1575E93293E7B1E001665B1 /* Double.swift in Sources */,
|
||||
E1B5784228F8AFCB00D42911 /* WrappedView.swift in Sources */,
|
||||
E11895AA289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */,
|
||||
E1575E76293E77B5001665B1 /* VideoPlayerType.swift in Sources */,
|
||||
E10B1EBF2BD9AD5C00A92EAF /* V1ServerModel.swift in Sources */,
|
||||
E17AC96B2954D00E003D2BC2 /* URLResponse.swift in Sources */,
|
||||
E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */,
|
||||
@ -4052,6 +4201,7 @@
|
||||
E1C926122887565C002A7A66 /* SeriesItemContentView.swift in Sources */,
|
||||
E193D53727193F8700900D82 /* MediaCoordinator.swift in Sources */,
|
||||
E12CC1CB28D1333400678D5D /* CinematicResumeItemView.swift in Sources */,
|
||||
4E2AC4D42C6C4C1200DD600D /* OrderedSectionSelectorView.swift in Sources */,
|
||||
E1575E9C293E7B1E001665B1 /* Collection.swift in Sources */,
|
||||
E1C9260F2887565C002A7A66 /* AttributeHStack.swift in Sources */,
|
||||
E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */,
|
||||
@ -4060,6 +4210,7 @@
|
||||
E12A9EF929499E0100731C3A /* JellyfinClient.swift in Sources */,
|
||||
E148128328C1443D003B8787 /* NameGuidPair.swift in Sources */,
|
||||
E1579EA82B97DC1500A31CA1 /* Eventful.swift in Sources */,
|
||||
4E9A24EB2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift in Sources */,
|
||||
E185920828CDAAA200326F80 /* SimilarItemsHStack.swift in Sources */,
|
||||
E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */,
|
||||
E1575E5C293E77B5001665B1 /* PlaybackSpeed.swift in Sources */,
|
||||
@ -4073,7 +4224,6 @@
|
||||
E148128928C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
|
||||
E43918672AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */,
|
||||
E154966F296CA2EF00C4EF88 /* LogManager.swift in Sources */,
|
||||
E1D37F562B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift in Sources */,
|
||||
E1575E75293E77B5001665B1 /* LibraryDisplayType.swift in Sources */,
|
||||
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */,
|
||||
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
|
||||
@ -4092,6 +4242,7 @@
|
||||
E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */,
|
||||
E18A17F0298C68B700C22F62 /* Overlay.swift in Sources */,
|
||||
E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */,
|
||||
4E2AC4CF2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift in Sources */,
|
||||
E1D37F492B9C648E00343D2B /* MaxHeightText.swift in Sources */,
|
||||
E146A9DC2BE6E9BF0034DA1E /* StoredValues+User.swift in Sources */,
|
||||
E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */,
|
||||
@ -4110,6 +4261,7 @@
|
||||
E164A7F72BE4816500A54B18 /* SelectUserServerSelection.swift in Sources */,
|
||||
E1575E84293E7A00001665B1 /* PrimaryAppIcon.swift in Sources */,
|
||||
E1153DCD2BBB633B00424D36 /* FastSVGView.swift in Sources */,
|
||||
E1CB75762C80EAFA00217C76 /* ArrayBuilder.swift in Sources */,
|
||||
E102315B2BCF8AF8009D71FC /* ProgramProgressOverlay.swift in Sources */,
|
||||
E1E6C45129B104850064123F /* Button.swift in Sources */,
|
||||
E19D41B52BF2C0130082B8B2 /* V2AnyData.swift in Sources */,
|
||||
@ -4132,6 +4284,7 @@
|
||||
E193D5432719407E00900D82 /* tvOSMainCoordinator.swift in Sources */,
|
||||
E1DABAFA2A270E62008AC34A /* OverviewCard.swift in Sources */,
|
||||
E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */,
|
||||
4E2AC4CC2C6C494E00DD600D /* VideoCodec.swift in Sources */,
|
||||
E129428628F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */,
|
||||
53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */,
|
||||
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||
@ -4148,6 +4301,7 @@
|
||||
E193D549271941CC00900D82 /* UserSignInView.swift in Sources */,
|
||||
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
|
||||
E148128628C15475003B8787 /* SortOrder+ItemSortOrder.swift in Sources */,
|
||||
E1CB75722C80E71800217C76 /* DirectPlayProfile.swift in Sources */,
|
||||
E1E1E24E28DF8A2E000DF5FD /* PreferenceKeys.swift in Sources */,
|
||||
E1575E9B293E7B1E001665B1 /* EnvironmentValue+Keys.swift in Sources */,
|
||||
E133328929538D8D00EE76AB /* Files.swift in Sources */,
|
||||
@ -4161,6 +4315,7 @@
|
||||
E1575E5D293E77B5001665B1 /* ItemViewType.swift in Sources */,
|
||||
E12CC1AF28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */,
|
||||
E1575E7A293E77B5001665B1 /* TimeStampType.swift in Sources */,
|
||||
E1CB758B2C80F9EC00217C76 /* CodecProfile.swift in Sources */,
|
||||
E1763A252BF2F77B004DF6AB /* ScrollIfLargerThanContainerModifier.swift in Sources */,
|
||||
E11E374E293E7F08009EF240 /* MediaSourceInfo.swift in Sources */,
|
||||
E1E1643A28BAC2EF00323B0A /* SearchView.swift in Sources */,
|
||||
@ -4180,7 +4335,6 @@
|
||||
E1AEFA382BE36C4900CFAFD8 /* SwiftinStore+UserState.swift in Sources */,
|
||||
E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
||||
E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */,
|
||||
E1D37F532B9CEF1E00343D2B /* DeviceProfile+SharedCodecProfiles.swift in Sources */,
|
||||
E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */,
|
||||
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
||||
E102312F2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift in Sources */,
|
||||
@ -4196,6 +4350,7 @@
|
||||
DFB7C3E02C7AA43A00CE7CDC /* UserSignInState.swift in Sources */,
|
||||
E1575E67293E77B5001665B1 /* OverlayType.swift in Sources */,
|
||||
E1E9EFEA28C6B96500CC1F8B /* ServerButton.swift in Sources */,
|
||||
4E9A24E82C82B6190023DA83 /* CustomProfileButton.swift in Sources */,
|
||||
E1575E65293E77B5001665B1 /* VideoPlayerJumpLength.swift in Sources */,
|
||||
E169C7B8296D2E8200AE25F9 /* SpecialFeaturesHStack.swift in Sources */,
|
||||
E1153D962BBA3E2F00424D36 /* EpisodeHStack.swift in Sources */,
|
||||
@ -4205,11 +4360,14 @@
|
||||
E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */,
|
||||
E113133B28BEB71D00930F75 /* FilterViewModel.swift in Sources */,
|
||||
4E16FD582C01A32700110147 /* LetterPickerOrientation.swift in Sources */,
|
||||
E19070502C8592B40004600E /* PlaybackCompatibility+Video.swift in Sources */,
|
||||
E1575E70293E77B5001665B1 /* TextPair.swift in Sources */,
|
||||
4E2AC4C62C6C492700DD600D /* MediaContainer.swift in Sources */,
|
||||
E18E021C2887492B0022598C /* BlurView.swift in Sources */,
|
||||
E187F7682B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */,
|
||||
E1E6C44729AECD5D0064123F /* PlayPreviousItemActionButton.swift in Sources */,
|
||||
E1E6C44E29AEE9DC0064123F /* SmallMenuOverlay.swift in Sources */,
|
||||
E1CB75832C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */,
|
||||
E10B1ECB2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */,
|
||||
E154966B296CA2EF00C4EF88 /* DownloadManager.swift in Sources */,
|
||||
535870632669D21600D05A09 /* SwiftfinApp.swift in Sources */,
|
||||
@ -4220,7 +4378,7 @@
|
||||
E1575E9A293E7B1E001665B1 /* Array.swift in Sources */,
|
||||
E1575E8D293E7B1E001665B1 /* URLComponents.swift in Sources */,
|
||||
E187A60329AB28F0008387E6 /* RotateContentView.swift in Sources */,
|
||||
E1D37F4F2B9CEDC400343D2B /* DeviceProfile.swift in Sources */,
|
||||
4EBE064E2C7EB6D3004A6C03 /* VideoPlayerType.swift in Sources */,
|
||||
E1575E94293E7B1E001665B1 /* VerticalAlignment.swift in Sources */,
|
||||
E1575EA3293E7B1E001665B1 /* UIDevice.swift in Sources */,
|
||||
E193D547271941C500900D82 /* SelectUserView.swift in Sources */,
|
||||
@ -4233,6 +4391,7 @@
|
||||
E18E021D2887492B0022598C /* AttributeStyleModifier.swift in Sources */,
|
||||
E1CAF6632BA363840087D991 /* UIHostingController.swift in Sources */,
|
||||
E10231492BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */,
|
||||
E1CB75702C80E66700217C76 /* CommaStringBuilder.swift in Sources */,
|
||||
5364F456266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */,
|
||||
E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */,
|
||||
E1575E7C293E77B5001665B1 /* TimerProxy.swift in Sources */,
|
||||
@ -4249,6 +4408,7 @@
|
||||
E1B90C8A2BC475E7007027C8 /* ScalingButtonStyle.swift in Sources */,
|
||||
E1DABAFE2A27B982008AC34A /* RatingsCard.swift in Sources */,
|
||||
E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */,
|
||||
E1CB757D2C80F00D00217C76 /* TranscodingProfile.swift in Sources */,
|
||||
E104C873296E0D0A00C1C3F9 /* IndicatorSettingsView.swift in Sources */,
|
||||
E18ACA8D2A14773500BB4F35 /* (null) in Sources */,
|
||||
E10B1E8E2BD7708900A92EAF /* QuickConnectView.swift in Sources */,
|
||||
@ -4270,6 +4430,7 @@
|
||||
E17AC96D2954E9CA003D2BC2 /* DownloadListView.swift in Sources */,
|
||||
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */,
|
||||
E1A1528828FD229500600579 /* ChevronButton.swift in Sources */,
|
||||
E1CB75732C80E71800217C76 /* DirectPlayProfile.swift in Sources */,
|
||||
E1B490472967E2E500D3EDCE /* CoreStore.swift in Sources */,
|
||||
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */,
|
||||
E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */,
|
||||
@ -4322,10 +4483,12 @@
|
||||
E1E2F8422B757E0900B75998 /* OnFirstAppearModifier.swift in Sources */,
|
||||
E1763A742BF3FA4C004DF6AB /* AppLoadingView.swift in Sources */,
|
||||
E17AC9712954F636003D2BC2 /* DownloadListCoordinator.swift in Sources */,
|
||||
4EBE06532C7ED0E1004A6C03 /* DeviceProfile.swift in Sources */,
|
||||
E10EAA4F277BBCC4000269ED /* CGSize.swift in Sources */,
|
||||
E150C0BA2BFD44F500944FFA /* ImagePipeline.swift in Sources */,
|
||||
E18E01EB288747230022598C /* MovieItemContentView.swift in Sources */,
|
||||
E17FB55B28C1266400311DFE /* GenresHStack.swift in Sources */,
|
||||
4E2AC4BE2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */,
|
||||
E18E01FA288747580022598C /* AboutAppView.swift in Sources */,
|
||||
E170D103294CE8BF0017224C /* LoadingView.swift in Sources */,
|
||||
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||
@ -4355,6 +4518,7 @@
|
||||
E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */,
|
||||
E1B5861229E32EEF00E45D6E /* Sequence.swift in Sources */,
|
||||
E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
||||
4E2AC4C82C6C493C00DD600D /* SubtitleFormat.swift in Sources */,
|
||||
E19D41B02BF2B7540082B8B2 /* URLSessionConfiguration.swift in Sources */,
|
||||
E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */,
|
||||
4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */,
|
||||
@ -4376,6 +4540,7 @@
|
||||
E104C870296E087200C1C3F9 /* IndicatorSettingsView.swift in Sources */,
|
||||
E12A9EF829499E0100731C3A /* JellyfinClient.swift in Sources */,
|
||||
E1722DB129491C3900CC0239 /* ImageBlurHashes.swift in Sources */,
|
||||
4EBE064F2C7ECE8D004A6C03 /* InlineEnumToggle.swift in Sources */,
|
||||
E14EDEC82B8FB65F000F00A4 /* ItemFilterType.swift in Sources */,
|
||||
E1EBCB42278BD174009FE6E9 /* TruncatedText.swift in Sources */,
|
||||
62133890265F83A900A81A2A /* MediaView.swift in Sources */,
|
||||
@ -4384,13 +4549,17 @@
|
||||
E129429B28F4A5E300796AC6 /* PlaybackSettingsView.swift in Sources */,
|
||||
E1E9017B28DAAE4D001B1594 /* RoundedCorner.swift in Sources */,
|
||||
E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */,
|
||||
4E2AC4C52C6C492700DD600D /* MediaContainer.swift in Sources */,
|
||||
4E2AC4CB2C6C494E00DD600D /* VideoCodec.swift in Sources */,
|
||||
E1EA09692BED78BB004CDE76 /* UserAccessPolicy.swift in Sources */,
|
||||
E18E0204288749200022598C /* RowDivider.swift in Sources */,
|
||||
E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */,
|
||||
E1CB75752C80EAFA00217C76 /* ArrayBuilder.swift in Sources */,
|
||||
E1047E2327E5880000CB0D4A /* SystemImageContentView.swift in Sources */,
|
||||
E1C8CE5B28FE512400DF5D7B /* CGPoint.swift in Sources */,
|
||||
E18ACA922A15A32F00BB4F35 /* (null) in Sources */,
|
||||
E1A3E4C92BB74EA3005C59F8 /* LoadingCard.swift in Sources */,
|
||||
4E71D6892C80910900A0174D /* EditCustomDeviceProfileView.swift in Sources */,
|
||||
E1E1E24D28DF8A2E000DF5FD /* PreferenceKeys.swift in Sources */,
|
||||
E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */,
|
||||
E15756322935642A00976E1F /* Double.swift in Sources */,
|
||||
@ -4422,7 +4591,6 @@
|
||||
E1194F4E2BEABA9100888DB6 /* NavigationBarCloseButton.swift in Sources */,
|
||||
E113133428BE988200930F75 /* NavigationBarFilterDrawer.swift in Sources */,
|
||||
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
|
||||
E1D37F552B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift in Sources */,
|
||||
E129428528F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */,
|
||||
E11E0E8C2BF7E76F007676DD /* DataCache.swift in Sources */,
|
||||
E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */,
|
||||
@ -4448,6 +4616,7 @@
|
||||
E1401CB129386C9200E8B599 /* UIColor.swift in Sources */,
|
||||
E1E2F8452B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */,
|
||||
E18E01AB288746AF0022598C /* PillHStack.swift in Sources */,
|
||||
E19070492C84F2BB0004600E /* ButtonStyle-iOS.swift in Sources */,
|
||||
E1401CAB2938140A00E8B599 /* LightAppIcon.swift in Sources */,
|
||||
E19D41A72BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift in Sources */,
|
||||
E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */,
|
||||
@ -4460,6 +4629,7 @@
|
||||
E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */,
|
||||
E1937A61288F32DB00CB80AA /* Poster.swift in Sources */,
|
||||
E145EB482BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift in Sources */,
|
||||
E1CB757C2C80F00D00217C76 /* TranscodingProfile.swift in Sources */,
|
||||
E1CAF65F2BA345830087D991 /* MediaViewModel.swift in Sources */,
|
||||
E1EA9F6A28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */,
|
||||
E133328D2953AE4B00EE76AB /* CircularProgressView.swift in Sources */,
|
||||
@ -4473,6 +4643,7 @@
|
||||
E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */,
|
||||
E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */,
|
||||
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
|
||||
E1CB75822C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */,
|
||||
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
|
||||
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */,
|
||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||
@ -4491,6 +4662,7 @@
|
||||
E1401CA5293813F400E8B599 /* InvertedDarkAppIcon.swift in Sources */,
|
||||
E1C8CE7C28FF015000DF5D7B /* TrailingTimestampType.swift in Sources */,
|
||||
C46DD8E22A8DC7FB0046A504 /* LiveMainOverlay.swift in Sources */,
|
||||
4EC1C86D2C80903A00E2879E /* CustomProfileButton.swift in Sources */,
|
||||
E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */,
|
||||
E13332912953B91000EE76AB /* DownloadTaskCoordinator.swift in Sources */,
|
||||
E43918662AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */,
|
||||
@ -4512,7 +4684,6 @@
|
||||
E1DE84142B9531C1008CCE21 /* OrderedSectionSelectorView.swift in Sources */,
|
||||
E13DD3FC2717EAE8009D4DAF /* SelectUserView.swift in Sources */,
|
||||
E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */,
|
||||
E1D37F4E2B9CEDC400343D2B /* DeviceProfile.swift in Sources */,
|
||||
E1EF4C412911B783008CC695 /* StreamType.swift in Sources */,
|
||||
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
|
||||
E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */,
|
||||
@ -4532,10 +4703,13 @@
|
||||
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
||||
E10B1ECA2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */,
|
||||
E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */,
|
||||
4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */,
|
||||
E18E01E3288747230022598C /* CompactPortraitScrollView.swift in Sources */,
|
||||
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||
E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */,
|
||||
4EBE064D2C7EB6D3004A6C03 /* VideoPlayerType.swift in Sources */,
|
||||
E1366A222C826DA700A36DED /* EditCustomDeviceProfileCoordinator.swift in Sources */,
|
||||
E1CCF12E28ABF989006CAC9E /* PosterDisplayType.swift in Sources */,
|
||||
E10B1EC12BD9AD6100A92EAF /* V1UserModel.swift in Sources */,
|
||||
E1E7506A2A33E9B400B2C1EE /* RatingsCard.swift in Sources */,
|
||||
@ -4552,7 +4726,6 @@
|
||||
62E632E0267D30CA0063E547 /* ItemLibraryViewModel.swift in Sources */,
|
||||
E1B33EB028EA890D0073B0FD /* Equatable.swift in Sources */,
|
||||
E1549662296CA2EF00C4EF88 /* UserSession.swift in Sources */,
|
||||
E15756362936856700976E1F /* VideoPlayerType.swift in Sources */,
|
||||
E1DA654C28E69B0500592A73 /* SpecialFeatureType.swift in Sources */,
|
||||
E11CEB8B28998552003E74C7 /* View-iOS.swift in Sources */,
|
||||
E10B1ECD2BD9AFD800A92EAF /* SwiftfinStore+V2.swift in Sources */,
|
||||
@ -4572,11 +4745,11 @@
|
||||
E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */,
|
||||
E14EA1692BF7330A00DE757A /* UserProfileImageViewModel.swift in Sources */,
|
||||
E18ACA952A15A3E100BB4F35 /* (null) in Sources */,
|
||||
4E73E2AE2C420207002D2A78 /* MaximumBitrateSettingsView.swift in Sources */,
|
||||
E1D5C39B28DF993400CDBEFB /* ThumbSlider.swift in Sources */,
|
||||
E1DC983D296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
|
||||
E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */,
|
||||
E1C925F72887504B002A7A66 /* PanDirectionGestureRecognizer.swift in Sources */,
|
||||
E1CB758C2C80F9EC00217C76 /* CodecProfile.swift in Sources */,
|
||||
E18E01E9288747230022598C /* SeriesItemView.swift in Sources */,
|
||||
E15756342936851D00976E1F /* NativeVideoPlayerSettingsView.swift in Sources */,
|
||||
E1D4BF7C2719D05000A11E64 /* AppSettingsView.swift in Sources */,
|
||||
@ -4592,19 +4765,22 @@
|
||||
E17FB55728C1256400311DFE /* CastAndCrewHStack.swift in Sources */,
|
||||
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||
E150C0BD2BFD45BD00944FFA /* RedrawOnNotificationView.swift in Sources */,
|
||||
E190704D2C858CEB0004600E /* VideoPlayerType+Shared.swift in Sources */,
|
||||
E113133828BEADBA00930F75 /* LibraryParent.swift in Sources */,
|
||||
E17DC74D2BE7601E00B42379 /* SettingsBarButton.swift in Sources */,
|
||||
E190704F2C8592B40004600E /* PlaybackCompatibility+Video.swift in Sources */,
|
||||
E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */,
|
||||
E102312C2BCF8A08009D71FC /* iOSLiveTVCoordinator.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 */,
|
||||
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
|
||||
E10B1EB62BD98C6600A92EAF /* AddUserRow.swift in Sources */,
|
||||
E1CB75802C80F28F00217C76 /* SubtitleProfile.swift in Sources */,
|
||||
E1DD20412BE1EB8C00C0DE51 /* AddUserButton.swift in Sources */,
|
||||
E1D37F582B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift in Sources */,
|
||||
E145EB422BE0A6EE003BF6F3 /* ServerSelectionMenu.swift in Sources */,
|
||||
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */,
|
||||
E14EA15E2BF6F72900DE757A /* PhotoPicker.swift in Sources */,
|
||||
@ -4620,8 +4796,10 @@
|
||||
E1545BD82BDC55C300D9578F /* ResetUserPasswordView.swift in Sources */,
|
||||
E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */,
|
||||
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */,
|
||||
4E2AC4C22C6C491200DD600D /* AudoCodec.swift in Sources */,
|
||||
E10B1EC72BD9AF6100A92EAF /* V2ServerModel.swift in Sources */,
|
||||
E13DD3C827164B1E009D4DAF /* UIDevice.swift in Sources */,
|
||||
4EBE06462C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */,
|
||||
C46DD8DD2A8DC3420046A504 /* LiveNativeVideoPlayer.swift in Sources */,
|
||||
E1AD104D26D96CE3003E4A08 /* BaseItemDto.swift in Sources */,
|
||||
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
||||
@ -4633,11 +4811,13 @@
|
||||
E18E01F1288747230022598C /* PlayButton.swift in Sources */,
|
||||
E129429028F0BDC300796AC6 /* TimeStampType.swift in Sources */,
|
||||
E1B490442967E26300D3EDCE /* PersistentLogHandler.swift in Sources */,
|
||||
E1CB756F2C80E66700217C76 /* CommaStringBuilder.swift in Sources */,
|
||||
E19D41AC2BF288110082B8B2 /* ServerCheckView.swift in Sources */,
|
||||
E1D5C39928DF914700CDBEFB /* CapsuleSlider.swift in Sources */,
|
||||
62E1DCC3273CE19800C9AE76 /* URL.swift in Sources */,
|
||||
E11BDF7A2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */,
|
||||
E170D0E4294CC8AB0017224C /* VideoPlayer+KeyCommands.swift in Sources */,
|
||||
4EC1C8692C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift in Sources */,
|
||||
E18E01E6288747230022598C /* CollectionItemView.swift in Sources */,
|
||||
E15D4F0A2B1BD88900442DB8 /* Edge.swift in Sources */,
|
||||
6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */,
|
||||
@ -4676,6 +4856,7 @@
|
||||
E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */,
|
||||
4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */,
|
||||
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,
|
||||
4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */,
|
||||
DFB7C3DF2C7AA43A00CE7CDC /* UserSignInState.swift in Sources */,
|
||||
E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */,
|
||||
4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */,
|
||||
@ -4686,7 +4867,6 @@
|
||||
E145EB4D2BE1688E003BF6F3 /* SwiftinStore+UserState.swift in Sources */,
|
||||
53EE24E6265060780068F029 /* SearchView.swift in Sources */,
|
||||
E164A8152BE58C2F00A54B18 /* V2AnyData.swift in Sources */,
|
||||
E1D37F522B9CEF1E00343D2B /* DeviceProfile+SharedCodecProfiles.swift in Sources */,
|
||||
E1DC9841296DEBD800982F06 /* WatchedIndicator.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "54fc43873cff9b3db2ad273a82066d201e4ea59316a81526b530004e4d98b974",
|
||||
"originHash" : "323b2ad9aaa9c000faf264d68272f0e9fab1349d9f910a0b95ee6aea10460f31",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "blurhashkit",
|
||||
|
@ -28,6 +28,9 @@ struct OrderedSectionSelectorView<Element: Displayable & Hashable>: View {
|
||||
}
|
||||
|
||||
private func select(element: Element) {
|
||||
|
||||
UIDevice.impact(.light)
|
||||
|
||||
if selection.value.contains(element) {
|
||||
selection.value.removeAll(where: { $0 == element })
|
||||
} else {
|
||||
|
53
Swiftfin/Extensions/ButtonStyle-iOS.swift
Normal file
53
Swiftfin/Extensions/ButtonStyle-iOS.swift
Normal file
@ -0,0 +1,53 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
extension ButtonStyle where Self == ToolbarPillButtonStyle {
|
||||
|
||||
static var toolbarPill: ToolbarPillButtonStyle {
|
||||
ToolbarPillButtonStyle()
|
||||
}
|
||||
}
|
||||
|
||||
struct ToolbarPillButtonStyle: ButtonStyle {
|
||||
|
||||
@Default(.accentColor)
|
||||
private var accentColor
|
||||
|
||||
@Environment(\.isEnabled)
|
||||
private var isEnabled
|
||||
|
||||
private var foregroundStyle: some ShapeStyle {
|
||||
if isEnabled {
|
||||
accentColor.overlayColor
|
||||
} else {
|
||||
Color.secondary.overlayColor
|
||||
}
|
||||
}
|
||||
|
||||
private var background: some ShapeStyle {
|
||||
if isEnabled {
|
||||
accentColor
|
||||
} else {
|
||||
Color.secondary
|
||||
}
|
||||
}
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.foregroundStyle(foregroundStyle)
|
||||
.font(.headline)
|
||||
.padding(.vertical, 5)
|
||||
.padding(.horizontal, 10)
|
||||
.background(background)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.opacity(isEnabled && !configuration.isPressed ? 1 : 0.5)
|
||||
}
|
||||
}
|
@ -8,6 +8,10 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: see if could be moved to `Shared`
|
||||
|
||||
// MARK: EpisodeSelectorLabelStyle
|
||||
|
||||
extension LabelStyle where Self == EpisodeSelectorLabelStyle {
|
||||
|
||||
static var episodeSelector: EpisodeSelectorLabelStyle {
|
||||
@ -35,3 +39,28 @@ struct EpisodeSelectorLabelStyle: LabelStyle {
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SectionFooterWithImageLabelStyle
|
||||
|
||||
extension LabelStyle where Self == SectionFooterWithImageLabelStyle<AnyShapeStyle> {
|
||||
|
||||
static func sectionFooterWithImage<ImageStyle: ShapeStyle>(imageStyle: ImageStyle) -> SectionFooterWithImageLabelStyle<ImageStyle> {
|
||||
SectionFooterWithImageLabelStyle(imageStyle: imageStyle)
|
||||
}
|
||||
}
|
||||
|
||||
struct SectionFooterWithImageLabelStyle<ImageStyle: ShapeStyle>: LabelStyle {
|
||||
|
||||
let imageStyle: ImageStyle
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack {
|
||||
configuration.icon
|
||||
.foregroundStyle(imageStyle)
|
||||
.backport
|
||||
.fontWeight(.bold)
|
||||
|
||||
configuration.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension CustomDeviceProfileSettingsView {
|
||||
|
||||
struct CustomProfileButton: View {
|
||||
|
||||
let profile: CustomDeviceProfile
|
||||
let onSelect: () -> Void
|
||||
|
||||
@ViewBuilder
|
||||
private func profileDetailsView(title: String, detail: String) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text(detail)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: onSelect) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
profileDetailsView(
|
||||
title: L10n.audio,
|
||||
detail: profile.audio.map(\.displayTitle).joined(separator: ", ")
|
||||
)
|
||||
|
||||
profileDetailsView(
|
||||
title: L10n.video,
|
||||
detail: profile.video.map(\.displayTitle).joined(separator: ", ")
|
||||
)
|
||||
|
||||
profileDetailsView(
|
||||
title: L10n.containers,
|
||||
detail: profile.container.map(\.displayTitle).joined(separator: ", ")
|
||||
)
|
||||
|
||||
profileDetailsView(
|
||||
title: L10n.useAsTranscodingProfile,
|
||||
detail: profile.useAsTranscodingProfile ? "Yes" : "No"
|
||||
)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.body.weight(.regular))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
extension CustomDeviceProfileSettingsView {
|
||||
|
||||
struct EditCustomDeviceProfileView: View {
|
||||
|
||||
@Default(.accentColor)
|
||||
private var accentColor
|
||||
|
||||
@StoredValue(.User.customDeviceProfiles)
|
||||
private var customDeviceProfiles
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: EditCustomDeviceProfileCoordinator.Router
|
||||
|
||||
@State
|
||||
private var isPresentingNotSaved = false
|
||||
@State
|
||||
private var profile: CustomDeviceProfile
|
||||
|
||||
private let createProfile: Bool
|
||||
private let source: Binding<CustomDeviceProfile>?
|
||||
|
||||
private var isValid: Bool {
|
||||
profile.audio.isNotEmpty &&
|
||||
profile.video.isNotEmpty &&
|
||||
profile.container.isNotEmpty
|
||||
}
|
||||
|
||||
init(profile: Binding<CustomDeviceProfile>?) {
|
||||
|
||||
createProfile = profile == nil
|
||||
|
||||
if let profile {
|
||||
self._profile = State(initialValue: profile.wrappedValue)
|
||||
self.source = profile
|
||||
} else {
|
||||
self._profile = State(initialValue: .init(type: .video))
|
||||
self.source = nil
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func codecSection(
|
||||
title: String,
|
||||
content: String,
|
||||
onSelect: @escaping () -> Void
|
||||
) -> some View {
|
||||
Button(action: onSelect) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
if content.isEmpty {
|
||||
Label(L10n.none, systemImage: "exclamationmark.circle.fill")
|
||||
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Text(content)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.body.weight(.regular))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Toggle(L10n.useAsTranscodingProfile, isOn: $profile.useAsTranscodingProfile)
|
||||
|
||||
Section {
|
||||
codecSection(
|
||||
title: L10n.audio,
|
||||
content: profile.audio.map(\.displayTitle).joined(separator: ", ")
|
||||
) {
|
||||
router.route(to: \.customDeviceAudioEditor, $profile.audio)
|
||||
}
|
||||
|
||||
codecSection(
|
||||
title: L10n.video,
|
||||
content: profile.video.map(\.displayTitle).joined(separator: ", ")
|
||||
) {
|
||||
router.route(to: \.customDeviceVideoEditor, $profile.video)
|
||||
}
|
||||
|
||||
codecSection(
|
||||
title: L10n.containers,
|
||||
content: profile.container.map(\.displayTitle).joined(separator: ", ")
|
||||
) {
|
||||
router.route(to: \.customDeviceContainerEditor, $profile.container)
|
||||
}
|
||||
} footer: {
|
||||
if !isValid {
|
||||
Label("Missing codec values", systemImage: "exclamationmark.circle.fill")
|
||||
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||
}
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled(true)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden()
|
||||
.navigationBarCloseButton {
|
||||
isPresentingNotSaved = true
|
||||
}
|
||||
.navigationTitle(L10n.customProfile)
|
||||
.topBarTrailing {
|
||||
Button("Save") {
|
||||
if createProfile {
|
||||
customDeviceProfiles.append(profile)
|
||||
} else {
|
||||
source?.wrappedValue = profile
|
||||
}
|
||||
|
||||
UIDevice.impact(.light)
|
||||
router.dismissCoordinator()
|
||||
}
|
||||
.buttonStyle(.toolbarPill)
|
||||
.disabled(!isValid)
|
||||
}
|
||||
.alert("Profile not saved", isPresented: $isPresentingNotSaved) {
|
||||
Button("Close", role: .destructive) {
|
||||
router.dismissCoordinator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
//
|
||||
// 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 Factory
|
||||
import SwiftUI
|
||||
|
||||
struct CustomDeviceProfileSettingsView: View {
|
||||
|
||||
@Default(.VideoPlayer.Playback.customDeviceProfileAction)
|
||||
private var customDeviceProfileAction
|
||||
|
||||
@StoredValue(.User.customDeviceProfiles)
|
||||
private var customProfiles: [CustomDeviceProfile]
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: SettingsCoordinator.Router
|
||||
|
||||
private var isValid: Bool {
|
||||
customDeviceProfileAction == .add ||
|
||||
customProfiles.isNotEmpty
|
||||
}
|
||||
|
||||
private func removeProfile(at offsets: IndexSet) {
|
||||
customProfiles.remove(atOffsets: offsets)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
CaseIterablePicker(
|
||||
L10n.behavior,
|
||||
selection: $customDeviceProfileAction
|
||||
)
|
||||
} footer: {
|
||||
VStack(spacing: 8) {
|
||||
switch customDeviceProfileAction {
|
||||
case .add:
|
||||
L10n.customDeviceProfileAdd.text
|
||||
case .replace:
|
||||
L10n.customDeviceProfileReplace.text
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
Label("No profiles defined. Playback issues may occur.", systemImage: "exclamationmark.circle.fill")
|
||||
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(L10n.profiles) {
|
||||
|
||||
if customProfiles.isEmpty {
|
||||
Button("Add profile") {
|
||||
router.route(to: \.createCustomDeviceProfile)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach($customProfiles, id: \.self) { $profile in
|
||||
CustomProfileButton(profile: profile) {
|
||||
router.route(to: \.editCustomDeviceProfile, $profile)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: removeProfile)
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.profiles)
|
||||
.topBarTrailing {
|
||||
if customProfiles.isNotEmpty {
|
||||
Button("Add") {
|
||||
UIDevice.impact(.light)
|
||||
router.route(to: \.createCustomDeviceProfile)
|
||||
}
|
||||
.buttonStyle(.toolbarPill)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -9,31 +9,13 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
// Note: Used for experimental settings that may be removed or implemented
|
||||
// officially. Keep for future settings.
|
||||
|
||||
struct ExperimentalSettingsView: View {
|
||||
|
||||
@Default(.Experimental.forceDirectPlay)
|
||||
private var forceDirectPlay
|
||||
@Default(.Experimental.liveTVForceDirectPlay)
|
||||
private var liveTVForceDirectPlay
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
|
||||
Toggle("Force Direct Play", isOn: $forceDirectPlay)
|
||||
|
||||
} header: {
|
||||
Text("Video Player")
|
||||
}
|
||||
|
||||
Section {
|
||||
|
||||
Toggle("Live TV Force Direct Play", isOn: $liveTVForceDirectPlay)
|
||||
|
||||
} header: {
|
||||
Text("Live TV")
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.experimental)
|
||||
Form {}
|
||||
.navigationTitle(L10n.experimental)
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct MaximumBitrateSettingsView: View {
|
||||
|
||||
@Default(.VideoPlayer.appMaximumBitrate)
|
||||
private var appMaximumBitrate
|
||||
@Default(.VideoPlayer.appMaximumBitrateTest)
|
||||
private var appMaximumBitrateTest
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
CaseIterablePicker(
|
||||
L10n.maximumBitrate,
|
||||
selection: $appMaximumBitrate
|
||||
)
|
||||
|
||||
if appMaximumBitrate == PlaybackBitrate.auto {
|
||||
CaseIterablePicker(
|
||||
L10n.testSize,
|
||||
selection: $appMaximumBitrateTest
|
||||
)
|
||||
}
|
||||
} footer: {
|
||||
if appMaximumBitrate == PlaybackBitrate.auto {
|
||||
Text(L10n.bitrateTestDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.maximumBitrate)
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct PlaybackQualitySettingsView: View {
|
||||
|
||||
@Default(.VideoPlayer.Playback.appMaximumBitrate)
|
||||
private var appMaximumBitrate
|
||||
@Default(.VideoPlayer.Playback.appMaximumBitrateTest)
|
||||
private var appMaximumBitrateTest
|
||||
@Default(.VideoPlayer.Playback.compatibilityMode)
|
||||
private var compatibilityMode
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: SettingsCoordinator.Router
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
CaseIterablePicker(
|
||||
L10n.maximumBitrate,
|
||||
selection: $appMaximumBitrate
|
||||
)
|
||||
} header: {
|
||||
L10n.bitrateDefault.text
|
||||
} footer: {
|
||||
L10n.bitrateDefaultDescription.text
|
||||
}
|
||||
.animation(.none, value: appMaximumBitrate)
|
||||
|
||||
if appMaximumBitrate == .auto {
|
||||
Section {
|
||||
CaseIterablePicker(
|
||||
L10n.testSize,
|
||||
selection: $appMaximumBitrateTest
|
||||
)
|
||||
} header: {
|
||||
L10n.bitrateTest.text
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
L10n.bitrateTestDescription.text
|
||||
L10n.bitrateTestDisclaimer.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Have a small description and a "Learn More..."
|
||||
// button that will open a page for longer descriptions
|
||||
// of each option. See: iOS Settings/Accessibility/VoiceOver
|
||||
// for reference
|
||||
|
||||
Section {
|
||||
CaseIterablePicker(
|
||||
L10n.compatibility,
|
||||
selection: $compatibilityMode
|
||||
)
|
||||
.animation(.none, value: compatibilityMode)
|
||||
|
||||
if compatibilityMode == .custom {
|
||||
ChevronButton(L10n.profiles)
|
||||
.onSelect {
|
||||
router.route(to: \.customDeviceProfileSettings)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
L10n.deviceProfile.text
|
||||
}
|
||||
}
|
||||
.animation(.linear, value: appMaximumBitrate)
|
||||
.animation(.linear, value: compatibilityMode)
|
||||
.navigationTitle(L10n.playbackQuality)
|
||||
}
|
||||
}
|
@ -72,9 +72,9 @@ struct SettingsView: View {
|
||||
router.route(to: \.videoPlayerSettings)
|
||||
}
|
||||
|
||||
ChevronButton(L10n.maximumBitrate)
|
||||
ChevronButton(L10n.playbackQuality)
|
||||
.onSelect {
|
||||
router.route(to: \.maximumBitrateSettings)
|
||||
router.route(to: \.playbackQualitySettings)
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,10 +86,13 @@ struct SettingsView: View {
|
||||
router.route(to: \.customizeViewsSettings)
|
||||
}
|
||||
|
||||
ChevronButton(L10n.experimental)
|
||||
.onSelect {
|
||||
router.route(to: \.experimentalSettings)
|
||||
}
|
||||
// Note: uncomment if there are current
|
||||
// experimental settings
|
||||
|
||||
// ChevronButton(L10n.experimental)
|
||||
// .onSelect {
|
||||
// router.route(to: \.experimentalSettings)
|
||||
// }
|
||||
}
|
||||
|
||||
Section {
|
||||
|
@ -74,12 +74,8 @@ struct ResetUserPasswordView: View {
|
||||
Text("Confirm New Password")
|
||||
} footer: {
|
||||
if newPassword != confirmNewPassword {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.circle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
|
||||
Text("New passwords do not match")
|
||||
}
|
||||
Label("New passwords to not match", systemImage: "exclamationmark.circle.fill")
|
||||
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,23 +208,11 @@ struct UserSignInView: View {
|
||||
} footer: {
|
||||
switch accessPolicy {
|
||||
case .requireDeviceAuthentication:
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.circle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
.backport
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text("This user will require device authentication.")
|
||||
}
|
||||
Label("This user will require device authentication.", systemImage: "exclamationmark.circle.fill")
|
||||
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||
case .requirePin:
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.circle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
.backport
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text("This user will require a pin.")
|
||||
}
|
||||
Label("This user will require a pin.", systemImage: "exclamationmark.circle.fill")
|
||||
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user