[tvOS] Episode Selector - State & Focus Handling (#1435)
* Catch empty episodes * Linting. * Mirror iOS more. Remove unused imports. Turn non-used cards into buttons to allow focus. * Allow focusing on Empty / Error cards. * Make ErrorCard Selectable * cleanup * Focusable Loading Card. * Fall back to empty season. * Last of the MacOS Catalyst stuff * Force Unwrap. * Don't force unwrap. * Remove unneeded `focusedSection` from `EpisodeSelector`. --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
d2c5ac9985
commit
718ea0f187
|
@ -0,0 +1,49 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
extension SeriesEpisodeSelector {
|
||||||
|
|
||||||
|
struct EmptyCard: View {
|
||||||
|
|
||||||
|
private var onSelect: () -> Void
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.onSelect = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onSelect(perform action: @escaping () -> Void) -> Self {
|
||||||
|
copy(modifying: \.onSelect, with: action)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Button {
|
||||||
|
onSelect()
|
||||||
|
} label: {
|
||||||
|
Color.secondarySystemFill
|
||||||
|
.opacity(0.75)
|
||||||
|
.posterStyle(.landscape)
|
||||||
|
.overlay {
|
||||||
|
Image(systemName: "questionmark")
|
||||||
|
.font(.system(size: 40))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.card)
|
||||||
|
.posterShadow()
|
||||||
|
|
||||||
|
SeriesEpisodeSelector.EpisodeContent(
|
||||||
|
subHeader: .emptyDash,
|
||||||
|
header: L10n.noResults,
|
||||||
|
content: L10n.noEpisodesAvailable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,13 +6,13 @@
|
||||||
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
import Defaults
|
|
||||||
import Factory
|
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension SeriesEpisodeSelector {
|
extension SeriesEpisodeSelector {
|
||||||
|
|
||||||
struct EpisodeCard: View {
|
struct EpisodeCard: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: ItemCoordinator.Router
|
private var router: ItemCoordinator.Router
|
||||||
|
|
||||||
|
@ -22,15 +22,24 @@ extension SeriesEpisodeSelector {
|
||||||
private var isFocused: Bool
|
private var isFocused: Bool
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var imageOverlay: some View {
|
private var overlayView: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if episode.userData?.isPlayed ?? false {
|
if let progressLabel = episode.progressLabel {
|
||||||
WatchedIndicator(size: 45)
|
|
||||||
} else if (episode.userData?.playbackPositionTicks ?? 0) > 0 {
|
|
||||||
LandscapePosterProgressBar(
|
LandscapePosterProgressBar(
|
||||||
title: episode.progressLabel ?? L10n.continue,
|
title: progressLabel,
|
||||||
progress: (episode.userData?.playedPercentage ?? 0) / 100
|
progress: (episode.userData?.playedPercentage ?? 0) / 100
|
||||||
)
|
)
|
||||||
|
} else if episode.userData?.isPlayed ?? false {
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
Color.clear
|
||||||
|
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 30, height: 30, alignment: .bottomTrailing)
|
||||||
|
.symbolRenderingMode(.palette)
|
||||||
|
.foregroundStyle(.white, .black)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isFocused {
|
if isFocused {
|
||||||
|
@ -64,7 +73,7 @@ extension SeriesEpisodeSelector {
|
||||||
SystemImageContentView(systemName: episode.systemImage)
|
SystemImageContentView(systemName: episode.systemImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
imageOverlay
|
overlayView
|
||||||
}
|
}
|
||||||
.posterStyle(.landscape)
|
.posterStyle(.landscape)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,9 @@ import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension SeriesEpisodeSelector {
|
extension SeriesEpisodeSelector {
|
||||||
|
|
||||||
struct EpisodeContent: View {
|
struct EpisodeContent: View {
|
||||||
|
|
||||||
@Default(.accentColor)
|
@Default(.accentColor)
|
||||||
private var accentColor
|
private var accentColor
|
||||||
|
|
||||||
|
@ -26,6 +28,7 @@ extension SeriesEpisodeSelector {
|
||||||
Text(subHeader)
|
Text(subHeader)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
@ -46,6 +49,7 @@ extension SeriesEpisodeSelector {
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.backport
|
.backport
|
||||||
.lineLimit(3, reservesSpace: true)
|
.lineLimit(3, reservesSpace: true)
|
||||||
|
.font(.caption.weight(.light))
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
|
@ -34,6 +34,8 @@ extension SeriesEpisodeSelector {
|
||||||
|
|
||||||
let playButtonItem: BaseItemDto?
|
let playButtonItem: BaseItemDto?
|
||||||
|
|
||||||
|
// MARK: - Content View
|
||||||
|
|
||||||
private func contentView(viewModel: SeasonItemViewModel) -> some View {
|
private func contentView(viewModel: SeasonItemViewModel) -> some View {
|
||||||
CollectionHStack(
|
CollectionHStack(
|
||||||
uniqueElements: viewModel.elements,
|
uniqueElements: viewModel.elements,
|
||||||
|
@ -53,7 +55,6 @@ extension SeriesEpisodeSelector {
|
||||||
|
|
||||||
lastFocusedEpisodeID = playButtonItem?.id
|
lastFocusedEpisodeID = playButtonItem?.id
|
||||||
|
|
||||||
// good enough?
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
guard let playButtonItem else { return }
|
guard let playButtonItem else { return }
|
||||||
proxy.scrollTo(element: playButtonItem, animated: false)
|
proxy.scrollTo(element: playButtonItem, animated: false)
|
||||||
|
@ -61,22 +62,42 @@ extension SeriesEpisodeSelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WrappedView {
|
ZStack {
|
||||||
switch viewModel.state {
|
switch viewModel.state {
|
||||||
case .content:
|
case .content:
|
||||||
contentView(viewModel: viewModel)
|
if viewModel.elements.isEmpty {
|
||||||
|
EmptyHStack(focusedEpisodeID: $focusedEpisodeID)
|
||||||
|
} else {
|
||||||
|
contentView(viewModel: viewModel)
|
||||||
|
}
|
||||||
case let .error(error):
|
case let .error(error):
|
||||||
ErrorHStack(viewModel: viewModel, error: error)
|
ErrorHStack(viewModel: viewModel, error: error, focusedEpisodeID: $focusedEpisodeID)
|
||||||
case .initial, .refreshing:
|
case .initial, .refreshing:
|
||||||
LoadingHStack()
|
LoadingHStack(focusedEpisodeID: $focusedEpisodeID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.bottom, 45)
|
||||||
.focusSection()
|
.focusSection()
|
||||||
.focusGuide(
|
.focusGuide(
|
||||||
focusGuide,
|
focusGuide,
|
||||||
tag: "episodes",
|
tag: "episodes",
|
||||||
onContentFocus: { focusedEpisodeID = lastFocusedEpisodeID },
|
onContentFocus: {
|
||||||
|
switch viewModel.state {
|
||||||
|
case .content:
|
||||||
|
if viewModel.elements.isEmpty {
|
||||||
|
focusedEpisodeID = "EmptyCard"
|
||||||
|
} else {
|
||||||
|
focusedEpisodeID = lastFocusedEpisodeID
|
||||||
|
}
|
||||||
|
case .error:
|
||||||
|
focusedEpisodeID = "ErrorCard"
|
||||||
|
case .initial, .refreshing:
|
||||||
|
focusedEpisodeID = "LoadingCard"
|
||||||
|
}
|
||||||
|
},
|
||||||
top: "seasons"
|
top: "seasons"
|
||||||
)
|
)
|
||||||
.onChange(of: viewModel.id) {
|
.onChange(of: viewModel.id) {
|
||||||
|
@ -94,12 +115,36 @@ extension SeriesEpisodeSelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Empty HStack
|
||||||
|
|
||||||
|
struct EmptyHStack: View {
|
||||||
|
|
||||||
|
let focusedEpisodeID: FocusState<String?>.Binding
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
CollectionHStack(
|
||||||
|
count: 1,
|
||||||
|
columns: 3.5
|
||||||
|
) { _ in
|
||||||
|
SeriesEpisodeSelector.EmptyCard()
|
||||||
|
.focused(focusedEpisodeID, equals: "EmptyCard")
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
}
|
||||||
|
.allowScrolling(false)
|
||||||
|
.insets(horizontal: EdgeInsets.edgePadding)
|
||||||
|
.itemSpacing(EdgeInsets.edgePadding / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Error HStack
|
||||||
|
|
||||||
struct ErrorHStack: View {
|
struct ErrorHStack: View {
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: SeasonItemViewModel
|
var viewModel: SeasonItemViewModel
|
||||||
|
|
||||||
let error: JellyfinAPIError
|
let error: JellyfinAPIError
|
||||||
|
let focusedEpisodeID: FocusState<String?>.Binding
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CollectionHStack(
|
CollectionHStack(
|
||||||
|
@ -110,6 +155,8 @@ extension SeriesEpisodeSelector {
|
||||||
.onSelect {
|
.onSelect {
|
||||||
viewModel.send(.refresh)
|
viewModel.send(.refresh)
|
||||||
}
|
}
|
||||||
|
.focused(focusedEpisodeID, equals: "ErrorCard")
|
||||||
|
.padding(.horizontal, 4)
|
||||||
}
|
}
|
||||||
.allowScrolling(false)
|
.allowScrolling(false)
|
||||||
.insets(horizontal: EdgeInsets.edgePadding)
|
.insets(horizontal: EdgeInsets.edgePadding)
|
||||||
|
@ -117,14 +164,20 @@ extension SeriesEpisodeSelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Loading HStack
|
||||||
|
|
||||||
struct LoadingHStack: View {
|
struct LoadingHStack: View {
|
||||||
|
|
||||||
|
let focusedEpisodeID: FocusState<String?>.Binding
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CollectionHStack(
|
CollectionHStack(
|
||||||
count: Int.random(in: 2 ..< 5),
|
count: 1,
|
||||||
columns: 3.5
|
columns: 3.5
|
||||||
) { _ in
|
) { _ in
|
||||||
SeriesEpisodeSelector.LoadingCard()
|
SeriesEpisodeSelector.LoadingCard()
|
||||||
|
.focused(focusedEpisodeID, equals: "LoadingCard")
|
||||||
|
.padding(.horizontal, 4)
|
||||||
}
|
}
|
||||||
.allowScrolling(false)
|
.allowScrolling(false)
|
||||||
.insets(horizontal: EdgeInsets.edgePadding)
|
.insets(horizontal: EdgeInsets.edgePadding)
|
||||||
|
|
|
@ -25,24 +25,26 @@ extension SeriesEpisodeSelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
VStack(alignment: .leading) {
|
||||||
onSelect()
|
Button {
|
||||||
} label: {
|
onSelect()
|
||||||
VStack(alignment: .leading) {
|
} label: {
|
||||||
Color.secondarySystemFill
|
Color.secondarySystemFill
|
||||||
.opacity(0.75)
|
.opacity(0.75)
|
||||||
.posterStyle(.landscape)
|
.posterStyle(.landscape)
|
||||||
.overlay {
|
.overlay {
|
||||||
Image(systemName: "arrow.clockwise.circle.fill")
|
Image(systemName: "arrow.clockwise")
|
||||||
.font(.system(size: 40))
|
.font(.system(size: 40))
|
||||||
}
|
}
|
||||||
|
|
||||||
SeriesEpisodeSelector.EpisodeContent(
|
|
||||||
subHeader: .emptyDash,
|
|
||||||
header: L10n.error,
|
|
||||||
content: error.localizedDescription
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.card)
|
||||||
|
.posterShadow()
|
||||||
|
|
||||||
|
SeriesEpisodeSelector.EpisodeContent(
|
||||||
|
subHeader: .emptyDash,
|
||||||
|
header: L10n.error,
|
||||||
|
content: error.localizedDescription
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,19 +6,36 @@
|
||||||
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import JellyfinAPI
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension SeriesEpisodeSelector {
|
extension SeriesEpisodeSelector {
|
||||||
|
|
||||||
struct LoadingCard: View {
|
struct LoadingCard: View {
|
||||||
|
|
||||||
|
private var onSelect: () -> Void
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.onSelect = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onSelect(perform action: @escaping () -> Void) -> Self {
|
||||||
|
copy(modifying: \.onSelect, with: action)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Color.secondarySystemFill
|
Button {
|
||||||
.opacity(0.75)
|
onSelect()
|
||||||
.posterStyle(.landscape)
|
} label: {
|
||||||
|
Color.secondarySystemFill
|
||||||
|
.opacity(0.75)
|
||||||
|
.posterStyle(.landscape)
|
||||||
|
.overlay {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.card)
|
||||||
|
.posterShadow()
|
||||||
|
|
||||||
SeriesEpisodeSelector.EpisodeContent(
|
SeriesEpisodeSelector.EpisodeContent(
|
||||||
subHeader: String.random(count: 7 ..< 12),
|
subHeader: String.random(count: 7 ..< 12),
|
||||||
|
|
|
@ -12,21 +12,29 @@ import SwiftUI
|
||||||
|
|
||||||
struct SeriesEpisodeSelector: View {
|
struct SeriesEpisodeSelector: View {
|
||||||
|
|
||||||
|
// MARK: - Observed & Environment Objects
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: SeriesItemViewModel
|
var viewModel: SeriesItemViewModel
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var parentFocusGuide: FocusGuide
|
private var parentFocusGuide: FocusGuide
|
||||||
|
|
||||||
|
// MARK: - State Variables
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var didSelectPlayButtonSeason = false
|
private var didSelectPlayButtonSeason = false
|
||||||
@State
|
@State
|
||||||
private var selection: SeasonItemViewModel.ID?
|
private var selection: SeasonItemViewModel.ID?
|
||||||
|
|
||||||
|
// MARK: - Calculated Variables
|
||||||
|
|
||||||
private var selectionViewModel: SeasonItemViewModel? {
|
private var selectionViewModel: SeasonItemViewModel? {
|
||||||
viewModel.seasons.first(where: { $0.id == selection })
|
viewModel.seasons.first(where: { $0.id == selection })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SeasonsHStack(viewModel: viewModel, selection: $selection)
|
SeasonsHStack(viewModel: viewModel, selection: $selection)
|
||||||
|
@ -35,8 +43,6 @@ struct SeriesEpisodeSelector: View {
|
||||||
if let selectionViewModel {
|
if let selectionViewModel {
|
||||||
EpisodeHStack(viewModel: selectionViewModel, playButtonItem: viewModel.playButtonItem)
|
EpisodeHStack(viewModel: selectionViewModel, playButtonItem: viewModel.playButtonItem)
|
||||||
.environmentObject(parentFocusGuide)
|
.environmentObject(parentFocusGuide)
|
||||||
} else {
|
|
||||||
LoadingHStack()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(viewModel.playButtonItem.publisher) { newValue in
|
.onReceive(viewModel.playButtonItem.publisher) { newValue in
|
||||||
|
|
|
@ -151,6 +151,7 @@
|
||||||
4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; };
|
4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; };
|
||||||
4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; };
|
4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; };
|
||||||
4E762AAF2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; };
|
4E762AAF2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; };
|
||||||
|
4E79F27C2D6BAAC500FE1A52 /* EmptyCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E79F27B2D6BAAC200FE1A52 /* EmptyCard.swift */; };
|
||||||
4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */; };
|
4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */; };
|
||||||
4E884C652CEBB301004CF6AD /* LearnMoreModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */; };
|
4E884C652CEBB301004CF6AD /* LearnMoreModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */; };
|
||||||
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; };
|
4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; };
|
||||||
|
@ -1380,6 +1381,7 @@
|
||||||
4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = "<group>"; };
|
4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = "<group>"; };
|
||||||
4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeUnusedStrings.swift; sourceTree = "<group>"; };
|
4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeUnusedStrings.swift; sourceTree = "<group>"; };
|
||||||
4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = "<group>"; };
|
4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = "<group>"; };
|
||||||
|
4E79F27B2D6BAAC200FE1A52 /* EmptyCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyCard.swift; sourceTree = "<group>"; };
|
||||||
4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsCoordinator.swift; sourceTree = "<group>"; };
|
4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsCoordinator.swift; sourceTree = "<group>"; };
|
||||||
4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreModal.swift; sourceTree = "<group>"; };
|
4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreModal.swift; sourceTree = "<group>"; };
|
||||||
4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = "<group>"; };
|
4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = "<group>"; };
|
||||||
|
@ -4289,6 +4291,7 @@
|
||||||
E1153D972BBA3E5300424D36 /* Components */ = {
|
E1153D972BBA3E5300424D36 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4E79F27B2D6BAAC200FE1A52 /* EmptyCard.swift */,
|
||||||
E1C926092887565C002A7A66 /* EpisodeCard.swift */,
|
E1C926092887565C002A7A66 /* EpisodeCard.swift */,
|
||||||
E1153D932BBA3D3000424D36 /* EpisodeContent.swift */,
|
E1153D932BBA3D3000424D36 /* EpisodeContent.swift */,
|
||||||
E1153D952BBA3E2F00424D36 /* EpisodeHStack.swift */,
|
E1153D952BBA3E2F00424D36 /* EpisodeHStack.swift */,
|
||||||
|
@ -5907,6 +5910,7 @@
|
||||||
4E4DAC372D11EE5E00E13FF9 /* SplitLoginWindowView.swift in Sources */,
|
4E4DAC372D11EE5E00E13FF9 /* SplitLoginWindowView.swift in Sources */,
|
||||||
4E97D1832D064748004B89AD /* ItemSection.swift in Sources */,
|
4E97D1832D064748004B89AD /* ItemSection.swift in Sources */,
|
||||||
E145EB232BDCCA43003BF6F3 /* BulletedList.swift in Sources */,
|
E145EB232BDCCA43003BF6F3 /* BulletedList.swift in Sources */,
|
||||||
|
4E79F27C2D6BAAC500FE1A52 /* EmptyCard.swift in Sources */,
|
||||||
E104DC972B9E7E29008F506D /* AssertionFailureView.swift in Sources */,
|
E104DC972B9E7E29008F506D /* AssertionFailureView.swift in Sources */,
|
||||||
E1E1643E28BB074000323B0A /* SelectorView.swift in Sources */,
|
E1E1643E28BB074000323B0A /* SelectorView.swift in Sources */,
|
||||||
E1A1529128FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */,
|
E1A1529128FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */,
|
||||||
|
|
Loading…
Reference in New Issue