[tvOS] "Native" Styled Menu Button (#1451)

* WIP

* Other Menu inits and AppSettingsView

* Linting & a touch of spacing.

* cleanup

* Init from CaseIterable

* User a Picker instead of just a ForEach

* Remove InlineEnumToggle.

* cleanup

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2025-03-17 10:22:35 -06:00 committed by GitHub
parent 36be3cc43f
commit a0a20caf1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 185 additions and 135 deletions

View File

@ -1,48 +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) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct InlineEnumToggle<ItemType: CaseIterable & Displayable & Hashable>: View {
@Binding
private var selection: ItemType
private let title: String
var body: some View {
Button {
guard let currentSelectionIndex = ItemType.allCases.firstIndex(of: selection) else { return }
if ItemType.allCases.index(currentSelectionIndex, offsetBy: 1) == ItemType.allCases.endIndex {
selection = ItemType.allCases[ItemType.allCases.startIndex]
} else {
selection = ItemType.allCases[ItemType.allCases.index(currentSelectionIndex, offsetBy: 1)]
}
} label: {
HStack {
Text(title)
Spacer()
Text(selection.displayTitle)
.foregroundColor(.secondary)
}
}
}
}
extension InlineEnumToggle {
init(title: String, selection: Binding<ItemType>) {
self.init(
selection: selection,
title: title
)
}
}

View File

@ -0,0 +1,139 @@
//
// 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) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct ListRowMenu<Content: View, Subtitle: View>: View {
// MARK: - Focus State
@FocusState
private var isFocused: Bool
// MARK: - Properties
private let title: Text
private let subtitle: Subtitle?
private let content: () -> Content
// MARK: - Body
var body: some View {
Menu(content: content) {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(isFocused ? Color.white : Color.clear)
HStack {
title
.foregroundStyle(isFocused ? Color.black : Color.white)
.padding(.leading, 4)
Spacer()
if let subtitle {
subtitle
.foregroundStyle(isFocused ? Color.black : Color.secondary)
.brightness(isFocused ? 0.4 : 0)
}
Image(systemName: "chevron.up.chevron.down")
.font(.body.weight(.regular))
.foregroundStyle(isFocused ? Color.black : Color.secondary)
.brightness(isFocused ? 0.4 : 0)
}
.padding(.horizontal)
}
.scaleEffect(isFocused ? 1.05 : 1.0)
.animation(.spring(response: 0.15, dampingFraction: 0.75), value: isFocused)
}
.menuStyle(.borderlessButton)
.listRowInsets(.zero)
.focused($isFocused)
}
}
// MARK: - Initializers
// Base initializer
extension ListRowMenu where Subtitle == Text? {
init(_ title: Text, @ViewBuilder content: @escaping () -> Content) {
self.title = title
self.subtitle = nil
self.content = content
}
init(_ title: Text, subtitle: Text?, @ViewBuilder content: @escaping () -> Content) {
self.title = title
self.subtitle = subtitle
self.content = content
}
init(_ title: Text, subtitle: String?, @ViewBuilder content: @escaping () -> Content) {
self.title = title
self.subtitle = subtitle.map { Text($0) }
self.content = content
}
init(_ title: String, @ViewBuilder content: @escaping () -> Content) {
self.title = Text(title)
self.subtitle = nil
self.content = content
}
init(_ title: String, subtitle: String?, @ViewBuilder content: @escaping () -> Content) {
self.title = Text(title)
self.subtitle = subtitle.map { Text($0) }
self.content = content
}
init(_ title: String, subtitle: Text?, @ViewBuilder content: @escaping () -> Content) {
self.title = Text(title)
self.subtitle = subtitle
self.content = content
}
}
// Custom view subtitles
extension ListRowMenu {
init(_ title: String, @ViewBuilder subtitle: @escaping () -> Subtitle, @ViewBuilder content: @escaping () -> Content) {
self.title = Text(title)
self.subtitle = subtitle()
self.content = content
}
init(_ title: Text, @ViewBuilder subtitle: @escaping () -> Subtitle, @ViewBuilder content: @escaping () -> Content) {
self.title = title
self.subtitle = subtitle()
self.content = content
}
}
// Initialize from a CaseIterable Enum
extension ListRowMenu where Subtitle == Text, Content == AnyView {
init<ItemType>(
_ title: String,
selection: Binding<ItemType>
) where ItemType: CaseIterable & Displayable & Hashable,
ItemType.AllCases: RandomAccessCollection
{
self.title = Text(title)
self.subtitle = Text(selection.wrappedValue.displayTitle)
self.content = {
Picker(title, selection: selection) {
ForEach(Array(ItemType.allCases), id: \.self) { option in
Text(option.displayTitle).tag(option)
}
}
.eraseToAnyView()
}
}
}

View File

@ -31,6 +31,12 @@ struct AppSettingsView: View {
@State @State
private var removeAllServersSelected: Bool = false private var removeAllServersSelected: Bool = false
private var selectedServer: ServerState? {
viewModel.servers.first { server in
selectUserAllServersSplashscreen == .server(id: server.id)
}
}
var body: some View { var body: some View {
SplitFormWindowView() SplitFormWindowView()
.descriptionView { .descriptionView {
@ -51,9 +57,16 @@ struct AppSettingsView: View {
Toggle(L10n.useSplashscreen, isOn: $selectUserUseSplashscreen) Toggle(L10n.useSplashscreen, isOn: $selectUserUseSplashscreen)
if selectUserUseSplashscreen { if selectUserUseSplashscreen {
Menu { ListRowMenu(L10n.servers) {
if selectUserAllServersSplashscreen == .all {
Label(L10n.random, systemImage: "dice.fill")
} else if let selectedServer {
Text(selectedServer.name)
} else {
Text(L10n.none)
}
} content: {
Picker(L10n.servers, selection: $selectUserAllServersSplashscreen) { Picker(L10n.servers, selection: $selectUserAllServersSplashscreen) {
Label(L10n.random, systemImage: "dice.fill") Label(L10n.random, systemImage: "dice.fill")
.tag(SelectUserServerSelection.all) .tag(SelectUserServerSelection.all)
@ -62,24 +75,7 @@ struct AppSettingsView: View {
.tag(SelectUserServerSelection.server(id: server.id)) .tag(SelectUserServerSelection.server(id: server.id))
} }
} }
} label: {
HStack {
Text(L10n.servers)
.frame(maxWidth: .infinity, alignment: .leading)
if selectUserAllServersSplashscreen == .all {
Label(L10n.random, systemImage: "dice.fill")
} else if let server = viewModel.servers.first(
where: { server in
selectUserAllServersSplashscreen == .server(id: server.id)
}
) {
Text(server.name)
}
}
} }
.listRowBackground(Color.clear)
.listRowInsets(.zero)
} }
} header: { } header: {
Text(L10n.splashscreen) Text(L10n.splashscreen)
@ -96,5 +92,6 @@ struct AppSettingsView: View {
router.route(to: \.log) router.route(to: \.log)
} }
} }
.navigationTitle(L10n.advanced)
} }
} }

View File

@ -12,6 +12,7 @@ import SwiftUI
extension AppSettingsView { extension AppSettingsView {
struct SignOutIntervalSection: View { struct SignOutIntervalSection: View {
@EnvironmentObject @EnvironmentObject
private var router: AppSettingsCoordinator.Router private var router: AppSettingsCoordinator.Router
@ -36,24 +37,12 @@ extension AppSettingsView {
Toggle(L10n.signoutBackground, isOn: $signOutOnBackground) Toggle(L10n.signoutBackground, isOn: $signOutOnBackground)
if signOutOnBackground { if signOutOnBackground {
HStack { ChevronButton(
Text(L10n.duration) L10n.duration,
subtitle: Text(backgroundSignOutInterval, format: .hourMinute)
Spacer() )
.onSelect {
Button { router.route(to: \.hourPicker)
router.route(to: \.hourPicker)
} label: {
HStack {
Text(backgroundSignOutInterval, format: .hourMinute)
.foregroundStyle(.secondary)
Image(systemName: "chevron.right")
.font(.body.weight(.semibold))
.foregroundStyle(.secondary)
}
}
.foregroundStyle(.primary, .secondary)
} }
} }
} footer: { } footer: {

View File

@ -45,12 +45,12 @@ struct EditServerView: View {
} }
Section(L10n.url) { Section(L10n.url) {
Menu { ListRowMenu(L10n.serverURL, subtitle: viewModel.server.currentURL.absoluteString) {
ForEach(viewModel.server.urls.sorted(using: \.absoluteString), id: \.self) { url in ForEach(viewModel.server.urls.sorted(using: \.absoluteString), id: \.self) { url in
Button(action: { Button {
guard viewModel.server.currentURL != url else { return } guard viewModel.server.currentURL != url else { return }
viewModel.setCurrentURL(to: url) viewModel.setCurrentURL(to: url)
}) { } label: {
HStack { HStack {
Text(url.absoluteString) Text(url.absoluteString)
.foregroundColor(.primary) .foregroundColor(.primary)
@ -65,18 +65,7 @@ struct EditServerView: View {
} }
} }
} }
} label: {
HStack {
Text(viewModel.server.currentURL.absoluteString)
.foregroundColor(.primary)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.secondary)
}
} }
.listRowBackground(Color.clear)
.listRowInsets(.zero)
} }
if isEditing { if isEditing {

View File

@ -45,10 +45,7 @@ struct CustomDeviceProfileSettingsView: View {
} }
.contentView { .contentView {
Section { Section {
InlineEnumToggle( ListRowMenu(L10n.behavior, selection: $customDeviceProfileAction)
title: L10n.behavior,
selection: $customDeviceProfileAction
)
} header: { } header: {
L10n.behavior.text L10n.behavior.text
} footer: { } footer: {

View File

@ -49,9 +49,9 @@ extension CustomizeViewsSettings {
Section(L10n.library) { Section(L10n.library) {
Toggle(L10n.cinematicBackground, isOn: $cinematicBackground) Toggle(L10n.cinematicBackground, isOn: $cinematicBackground)
InlineEnumToggle(title: L10n.posters, selection: $libraryPosterType) ListRowMenu(L10n.posters, selection: $libraryPosterType)
InlineEnumToggle(title: L10n.library, selection: $libraryDisplayType) ListRowMenu(L10n.library, selection: $libraryDisplayType)
if libraryDisplayType == .list { if libraryDisplayType == .list {
ChevronButton( ChevronButton(

View File

@ -60,15 +60,15 @@ struct CustomizeViewsSettings: View {
Toggle(L10n.showPosterLabels, isOn: $showPosterLabels) Toggle(L10n.showPosterLabels, isOn: $showPosterLabels)
InlineEnumToggle(title: L10n.next, selection: $nextUpPosterType) ListRowMenu(L10n.next, selection: $nextUpPosterType)
InlineEnumToggle(title: L10n.recentlyAdded, selection: $recentlyAddedPosterType) ListRowMenu(L10n.recentlyAdded, selection: $recentlyAddedPosterType)
InlineEnumToggle(title: L10n.latestWithString(L10n.library), selection: $latestInLibraryPosterType) ListRowMenu(L10n.latestWithString(L10n.library), selection: $latestInLibraryPosterType)
InlineEnumToggle(title: L10n.recommended, selection: $similarPosterType) ListRowMenu(L10n.recommended, selection: $similarPosterType)
InlineEnumToggle(title: L10n.search, selection: $searchPosterType) ListRowMenu(L10n.search, selection: $searchPosterType)
} }
LibrarySection() LibrarySection()

View File

@ -39,11 +39,8 @@ struct PlaybackQualitySettingsView: View {
} }
.contentView { .contentView {
Section { Section {
InlineEnumToggle( ListRowMenu(L10n.maximumBitrate, selection: $appMaximumBitrate)
title: L10n.maximumBitrate, .focused($focusedItem, equals: .maximumBitrate)
selection: $appMaximumBitrate
)
.focused($focusedItem, equals: .maximumBitrate)
} header: { } header: {
L10n.bitrateDefault.text L10n.bitrateDefault.text
} footer: { } footer: {
@ -53,21 +50,15 @@ struct PlaybackQualitySettingsView: View {
if appMaximumBitrate == .auto { if appMaximumBitrate == .auto {
Section { Section {
InlineEnumToggle( ListRowMenu(L10n.testSize, selection: $appMaximumBitrateTest)
title: L10n.testSize,
selection: $appMaximumBitrateTest
)
} footer: { } footer: {
L10n.bitrateTestDisclaimer.text L10n.bitrateTestDisclaimer.text
} }
} }
Section { Section {
InlineEnumToggle( ListRowMenu(L10n.compatibility, selection: $compatibilityMode)
title: L10n.compatibility, .focused($focusedItem, equals: .compatibility)
selection: $compatibilityMode
)
.focused($focusedItem, equals: .compatibility)
if compatibilityMode == .custom { if compatibilityMode == .custom {
ChevronButton(L10n.profiles) ChevronButton(L10n.profiles)

View File

@ -56,7 +56,7 @@ struct SettingsView: View {
Section(L10n.videoPlayer) { Section(L10n.videoPlayer) {
InlineEnumToggle(title: L10n.videoPlayerType, selection: $videoPlayerType) ListRowMenu(L10n.videoPlayerType, selection: $videoPlayerType)
ChevronButton(L10n.videoPlayer) ChevronButton(L10n.videoPlayer)
.onSelect { .onSelect {

View File

@ -207,7 +207,6 @@
4EBE06472C7E9509004A6C03 /* 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 */; }; 4EBE064D2C7EB6D3004A6C03 /* VideoPlayerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE064C2C7EB6D3004A6C03 /* VideoPlayerType.swift */; };
4EBE064E2C7EB6D3004A6C03 /* 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 */; }; 4EBE06532C7ED0E1004A6C03 /* DeviceProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */; };
4EBE06542C7ED0E1004A6C03 /* 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 */; }; 4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8512C7FDFA300E2879E /* PlaybackDeviceProfile.swift */; };
@ -221,6 +220,8 @@
4EC2B1A92CC97C0700D866BE /* ServerUserDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC2B1A82CC97C0400D866BE /* ServerUserDetailsView.swift */; }; 4EC2B1A92CC97C0700D866BE /* ServerUserDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC2B1A82CC97C0400D866BE /* ServerUserDetailsView.swift */; };
4EC50D612C934B3A00FC3D0E /* ServerTasksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */; }; 4EC50D612C934B3A00FC3D0E /* ServerTasksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */; };
4EC6C16B2C92999800FC904B /* TranscodeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC6C16A2C92999800FC904B /* TranscodeSection.swift */; }; 4EC6C16B2C92999800FC904B /* TranscodeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC6C16A2C92999800FC904B /* TranscodeSection.swift */; };
4ECCF9F92D8504420048B331 /* WrappingHStack in Frameworks */ = {isa = PBXBuildFile; productRef = 321BE8311E445482ED5C95C3 /* WrappingHStack */; };
4ECCF9FB2D8505890048B331 /* ListRowMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECCF9FA2D8505860048B331 /* ListRowMenu.swift */; };
4ECDAA9E2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; }; 4ECDAA9E2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; };
4ECDAA9F2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; }; 4ECDAA9F2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; };
4ECF5D882D0A3D0200F066B1 /* AddAccessScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */; }; 4ECF5D882D0A3D0200F066B1 /* AddAccessScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */; };
@ -761,7 +762,6 @@
E154966B296CA2EF00C4EF88 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965B296CA2EF00C4EF88 /* DownloadManager.swift */; }; E154966B296CA2EF00C4EF88 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965B296CA2EF00C4EF88 /* DownloadManager.swift */; };
E154966E296CA2EF00C4EF88 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965D296CA2EF00C4EF88 /* LogManager.swift */; }; E154966E296CA2EF00C4EF88 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965D296CA2EF00C4EF88 /* LogManager.swift */; };
E154966F296CA2EF00C4EF88 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965D296CA2EF00C4EF88 /* LogManager.swift */; }; E154966F296CA2EF00C4EF88 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965D296CA2EF00C4EF88 /* LogManager.swift */; };
E1549678296CB22B00C4EF88 /* InlineEnumToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */; };
E154967A296CB4B000C4EF88 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549679296CB4B000C4EF88 /* VideoPlayerSettingsView.swift */; }; E154967A296CB4B000C4EF88 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549679296CB4B000C4EF88 /* VideoPlayerSettingsView.swift */; };
E154967C296CBB1A00C4EF88 /* FontPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154967B296CBB1A00C4EF88 /* FontPickerView.swift */; }; E154967C296CBB1A00C4EF88 /* FontPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154967B296CBB1A00C4EF88 /* FontPickerView.swift */; };
E154967E296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154967D296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift */; }; E154967E296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154967D296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift */; };
@ -922,10 +922,6 @@
E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18A8E8228D60BC400333B9A /* VideoPlayer.swift */; }; E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18A8E8228D60BC400333B9A /* VideoPlayer.swift */; };
E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18A8E8428D60D0000333B9A /* VideoPlayerCoordinator.swift */; }; E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18A8E8428D60D0000333B9A /* VideoPlayerCoordinator.swift */; };
E18ACA8B2A14301800BB4F35 /* ScalingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */; }; E18ACA8B2A14301800BB4F35 /* ScalingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */; };
E18ACA8D2A14773500BB4F35 /* BuildFile in Sources */ = {isa = PBXBuildFile; };
E18ACA8F2A15A2CF00BB4F35 /* BuildFile in Sources */ = {isa = PBXBuildFile; };
E18ACA922A15A32F00BB4F35 /* BuildFile in Sources */ = {isa = PBXBuildFile; };
E18ACA952A15A3E100BB4F35 /* BuildFile in Sources */ = {isa = PBXBuildFile; };
E18CE0AF28A222240092E7F1 /* PublicUserRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0AE28A222240092E7F1 /* PublicUserRow.swift */; }; E18CE0AF28A222240092E7F1 /* PublicUserRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0AE28A222240092E7F1 /* PublicUserRow.swift */; };
E18CE0B228A229E70092E7F1 /* UserDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B128A229E70092E7F1 /* UserDto.swift */; }; E18CE0B228A229E70092E7F1 /* UserDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B128A229E70092E7F1 /* UserDto.swift */; };
E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */; }; E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */; };
@ -1448,6 +1444,7 @@
4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksViewModel.swift; sourceTree = "<group>"; }; 4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksViewModel.swift; sourceTree = "<group>"; };
4EC6C16A2C92999800FC904B /* TranscodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeSection.swift; sourceTree = "<group>"; }; 4EC6C16A2C92999800FC904B /* TranscodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeSection.swift; sourceTree = "<group>"; };
4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlphabetizeStrings.swift; sourceTree = "<group>"; }; 4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlphabetizeStrings.swift; sourceTree = "<group>"; };
4ECCF9FA2D8505860048B331 /* ListRowMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowMenu.swift; sourceTree = "<group>"; };
4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeReason.swift; sourceTree = "<group>"; }; 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeReason.swift; sourceTree = "<group>"; };
4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessScheduleView.swift; sourceTree = "<group>"; }; 4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessScheduleView.swift; sourceTree = "<group>"; };
4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDayOfWeek.swift; sourceTree = "<group>"; }; 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDayOfWeek.swift; sourceTree = "<group>"; };
@ -1829,7 +1826,6 @@
E1549659296CA2EF00C4EF88 /* Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; }; E1549659296CA2EF00C4EF88 /* Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
E154965B296CA2EF00C4EF88 /* DownloadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; }; E154965B296CA2EF00C4EF88 /* DownloadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; };
E154965D296CA2EF00C4EF88 /* LogManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = "<group>"; }; E154965D296CA2EF00C4EF88 /* LogManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = "<group>"; };
E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineEnumToggle.swift; sourceTree = "<group>"; };
E1549679296CB4B000C4EF88 /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = "<group>"; }; E1549679296CB4B000C4EF88 /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = "<group>"; };
E154967B296CBB1A00C4EF88 /* FontPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontPickerView.swift; sourceTree = "<group>"; }; E154967B296CBB1A00C4EF88 /* FontPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontPickerView.swift; sourceTree = "<group>"; };
E154967D296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicNavigationCoordinator.swift; sourceTree = "<group>"; }; E154967D296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicNavigationCoordinator.swift; sourceTree = "<group>"; };
@ -2191,6 +2187,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4ECCF9F92D8504420048B331 /* WrappingHStack in Frameworks */,
E12B93072947CD0F00CE0BD9 /* Pulse in Frameworks */, E12B93072947CD0F00CE0BD9 /* Pulse in Frameworks */,
62666E3E27E503FA00EC0ECD /* MediaAccessibility.framework in Frameworks */, 62666E3E27E503FA00EC0ECD /* MediaAccessibility.framework in Frameworks */,
62666DFF27E5016400EC0ECD /* CFNetwork.framework in Frameworks */, 62666DFF27E5016400EC0ECD /* CFNetwork.framework in Frameworks */,
@ -3384,9 +3381,9 @@
E1C92618288756BD002A7A66 /* DotHStack.swift */, E1C92618288756BD002A7A66 /* DotHStack.swift */,
E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */, E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */,
4EF0DCA82D49751B005A5194 /* ErrorView.swift */, 4EF0DCA82D49751B005A5194 /* ErrorView.swift */,
E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */,
E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */, E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */,
E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */, E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */,
4ECCF9FA2D8505860048B331 /* ListRowMenu.swift */,
E10E842B29A589860064EA49 /* NonePosterButton.swift */, E10E842B29A589860064EA49 /* NonePosterButton.swift */,
4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */, 4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */,
E1C92617288756BD002A7A66 /* PosterButton.swift */, E1C92617288756BD002A7A66 /* PosterButton.swift */,
@ -5915,6 +5912,7 @@
E1575E92293E7B1E001665B1 /* CGSize.swift in Sources */, E1575E92293E7B1E001665B1 /* CGSize.swift in Sources */,
E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */, E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */,
4E98F7D22D123AD4001E7518 /* NavigationBarMenuButton.swift in Sources */, 4E98F7D22D123AD4001E7518 /* NavigationBarMenuButton.swift in Sources */,
4ECCF9FB2D8505890048B331 /* ListRowMenu.swift in Sources */,
4E98F7D32D123AD4001E7518 /* View-tvOS.swift in Sources */, 4E98F7D32D123AD4001E7518 /* View-tvOS.swift in Sources */,
C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */, C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */,
4EE766FB2D132954009658F0 /* RemoteSearchResult.swift in Sources */, 4EE766FB2D132954009658F0 /* RemoteSearchResult.swift in Sources */,
@ -6173,7 +6171,6 @@
E1BCDB502BE1F491009F6744 /* ResetUserPasswordViewModel.swift in Sources */, E1BCDB502BE1F491009F6744 /* ResetUserPasswordViewModel.swift in Sources */,
C46DD8D72A8DC2990046A504 /* LiveVideoPlayer.swift in Sources */, C46DD8D72A8DC2990046A504 /* LiveVideoPlayer.swift in Sources */,
E1575E88293E7A00001665B1 /* LightAppIcon.swift in Sources */, E1575E88293E7A00001665B1 /* LightAppIcon.swift in Sources */,
E1549678296CB22B00C4EF88 /* InlineEnumToggle.swift in Sources */,
E193D5432719407E00900D82 /* tvOSMainCoordinator.swift in Sources */, E193D5432719407E00900D82 /* tvOSMainCoordinator.swift in Sources */,
E1DABAFA2A270E62008AC34A /* OverviewCard.swift in Sources */, E1DABAFA2A270E62008AC34A /* OverviewCard.swift in Sources */,
E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */, E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */,
@ -6503,7 +6500,6 @@
E104C870296E087200C1C3F9 /* IndicatorSettingsView.swift in Sources */, E104C870296E087200C1C3F9 /* IndicatorSettingsView.swift in Sources */,
E12A9EF829499E0100731C3A /* JellyfinClient.swift in Sources */, E12A9EF829499E0100731C3A /* JellyfinClient.swift in Sources */,
E1722DB129491C3900CC0239 /* ImageBlurHashes.swift in Sources */, E1722DB129491C3900CC0239 /* ImageBlurHashes.swift in Sources */,
4EBE064F2C7ECE8D004A6C03 /* InlineEnumToggle.swift in Sources */,
E14EDEC82B8FB65F000F00A4 /* ItemFilterType.swift in Sources */, E14EDEC82B8FB65F000F00A4 /* ItemFilterType.swift in Sources */,
E1EBCB42278BD174009FE6E9 /* TruncatedText.swift in Sources */, E1EBCB42278BD174009FE6E9 /* TruncatedText.swift in Sources */,
4ED25CA12D07E3590010333C /* EditAccessScheduleView.swift in Sources */, 4ED25CA12D07E3590010333C /* EditAccessScheduleView.swift in Sources */,