Implement Electronic Program Guide (EPG) for Live TV

Features:
- Full EPG grid with channels and time slots
- 12-hour program window with auto-refresh every 5 minutes
- Duration-based cell widths (3px/min)
- Live program highlighting with progress bars
- Current time indicator (red line)
- Direct channel playback from guide
- Auto-scroll to currently airing programs

Changes:
- Add EPGViewModel for data fetching and state management
- Add EPGProgramCell, EPGChannelRow, EPGTimelineHeader, EPGCurrentTimeIndicator components
- Update ProgramGuideView with complete EPG implementation
- Make Channels default Live TV tab (was Program Guide)
- Fix channel images in EPG to match Channels view display
- Fix Live TV playback crash (audioStreams array bounds check)
- Apply dark pink background throughout app

Slash Commands:
- Add /init-dev - Initialize dev session with project context
- Add /sim - Build and launch in Apple TV simulator

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ashik K 2025-10-17 19:23:53 +02:00
parent de349e213e
commit fdd1cdc15b
18 changed files with 764 additions and 21 deletions

View File

@ -0,0 +1,16 @@
---
description: Initialize development session by reading project context and displaying available commands
---
Read the chats-summary.txt file from the parent directory (/Users/ashikkizhakkepallathu/Documents/claude/jellypig/chats-summary.txt) to understand the project context, then display a quick summary of what you can help with.
Steps:
1. Read /Users/ashikkizhakkepallathu/Documents/claude/jellypig/chats-summary.txt
2. Display a concise summary including:
- Project name and description
- Available custom slash commands (/sim, etc.)
- Recent features implemented
- Key configuration details
- Common tasks you can help with
Make the output brief and actionable - focus on what's immediately useful for the developer.

14
.claude/commands/sim.md Normal file
View File

@ -0,0 +1,14 @@
---
description: Build jellypig tvOS and launch in Apple TV simulator
---
Build the latest version of jellypig tvOS in Debug configuration, install it on the Apple TV simulator, and launch it.
Steps:
1. Boot the Apple TV simulator (16A71179-729D-4F1B-8698-8371F137025B)
2. Open Simulator.app
3. Build the project for tvOS Simulator
4. Install the built app on the simulator
5. Launch the app with bundle identifier org.ashik.jellypig
Use xcodebuild to build, xcrun simctl to manage the simulator, and report success when the app is running.

View File

@ -13,24 +13,25 @@ import SwiftUI
final class LiveTVCoordinator: TabCoordinatable { final class LiveTVCoordinator: TabCoordinatable {
var child = TabChild(startingItems: [ var child = TabChild(startingItems: [
\LiveTVCoordinator.programs,
\LiveTVCoordinator.channels, \LiveTVCoordinator.channels,
\LiveTVCoordinator.programGuide,
]) ])
@Route(tabItem: makeProgramsTab) @Route(tabItem: makeProgramGuideTab)
var programs = makePrograms var programGuide = makeProgramGuide
@Route(tabItem: makeChannelsTab) @Route(tabItem: makeChannelsTab)
var channels = makeChannels var channels = makeChannels
func makePrograms() -> VideoPlayerWrapperCoordinator { func makeProgramGuide() -> VideoPlayerWrapperCoordinator {
VideoPlayerWrapperCoordinator { VideoPlayerWrapperCoordinator {
ProgramsView() ProgramGuideView()
} }
} }
@ViewBuilder @ViewBuilder
func makeProgramsTab(isActive: Bool) -> some View { func makeProgramGuideTab(isActive: Bool) -> some View {
Label(L10n.programs, systemImage: "tv") Label("Guide", systemImage: "list.bullet.rectangle")
} }
func makeChannels() -> VideoPlayerWrapperCoordinator { func makeChannels() -> VideoPlayerWrapperCoordinator {

View File

@ -0,0 +1,138 @@
//
// 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 Combine
import Foundation
import JellyfinAPI
import SwiftUI
final class EPGViewModel: ViewModel, Stateful {
enum Action: Equatable {
case refresh
}
enum BackgroundState: Hashable {
case refresh
}
enum State: Hashable {
case initial
case refreshing
case content
case error(JellyfinAPIError)
}
@Published
var channelPrograms: [ChannelProgram] = []
@Published
var state: State = .initial
@Published
var backgroundStates: Set<BackgroundState> = []
// Time window configuration
var timeWindowStart: Date {
Calendar.current.date(byAdding: .hour, value: -1, to: .now) ?? .now
}
var timeWindowEnd: Date {
Calendar.current.date(byAdding: .hour, value: 12, to: .now) ?? .now
}
// Auto-refresh timer
private var refreshTimer: Timer?
private let refreshInterval: TimeInterval = 300 // 5 minutes
override init() {
super.init()
setupRefreshTimer()
}
deinit {
refreshTimer?.invalidate()
}
func respond(to action: Action) -> State {
switch action {
case .refresh:
Task {
await fetchChannelsAndPrograms()
}
return .refreshing
}
}
func fetchChannelsAndPrograms() async {
do {
state = .refreshing
// Fetch all channels
var channelParameters = Paths.GetLiveTvChannelsParameters()
channelParameters.fields = .MinimumFields
channelParameters.userID = userSession.user.id
channelParameters.sortBy = [ItemSortBy.name]
// No limit - fetch all channels for EPG
let channelRequest = Paths.getLiveTvChannels(parameters: channelParameters)
let channelResponse = try await userSession.client.send(channelRequest)
guard let channels = channelResponse.value.items, !channels.isEmpty else {
state = .content
return
}
// Fetch programs for all channels in time window
var programParameters = Paths.GetLiveTvProgramsParameters()
programParameters.channelIDs = channels.compactMap(\.id)
programParameters.userID = userSession.user.id
programParameters.minEndDate = timeWindowStart
programParameters.maxStartDate = timeWindowEnd
programParameters.sortBy = [ItemSortBy.startDate]
programParameters.fields = .MinimumFields
let programRequest = Paths.getLiveTvPrograms(parameters: programParameters)
let programResponse = try await userSession.client.send(programRequest)
// Group programs by channel
let groupedPrograms = (programResponse.value.items ?? [])
.grouped { program in
channels.first(where: { $0.id == program.channelID })
}
// Create ChannelProgram objects
let programs: [ChannelProgram] = channels
.reduce(into: [:]) { partialResult, channel in
partialResult[channel] = (groupedPrograms[channel] ?? [])
.sorted(using: \.startDate)
}
.map(ChannelProgram.init)
.sorted(using: \.channel.name)
await MainActor.run {
self.channelPrograms = programs
self.state = .content
}
} catch {
await MainActor.run {
self.state = .error(JellyfinAPIError(error.localizedDescription))
}
}
}
private func setupRefreshTimer() {
refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { [weak self] _ in
guard let self = self else { return }
Task {
await self.fetchChannelsAndPrograms()
}
}
}
}

View File

@ -66,7 +66,7 @@ final class VideoPlayerViewModel: ViewModel {
let configuration = VLCVideoPlayer.Configuration(url: playbackURL) let configuration = VLCVideoPlayer.Configuration(url: playbackURL)
configuration.autoPlay = true configuration.autoPlay = true
configuration.startTime = .seconds(max(0, item.startTimeSeconds - Defaults[.VideoPlayer.resumeOffset])) configuration.startTime = .seconds(max(0, item.startTimeSeconds - Defaults[.VideoPlayer.resumeOffset]))
if self.audioStreams[0].path != nil { if !self.audioStreams.isEmpty, self.audioStreams[0].path != nil {
configuration.audioIndex = .absolute(selectedAudioStreamIndex) configuration.audioIndex = .absolute(selectedAudioStreamIndex)
} }
configuration.subtitleIndex = .absolute(selectedSubtitleStreamIndex) configuration.subtitleIndex = .absolute(selectedSubtitleStreamIndex)

View File

@ -14,18 +14,23 @@ struct SplitFormWindowView: View {
private var descriptionView: () -> any View private var descriptionView: () -> any View
var body: some View { var body: some View {
HStack { ZStack {
Color(red: 0.15, green: 0.05, blue: 0.1)
.ignoresSafeArea()
descriptionView() HStack {
.eraseToAnyView()
.frame(maxWidth: .infinity)
Form { descriptionView()
contentView()
.eraseToAnyView() .eraseToAnyView()
.frame(maxWidth: .infinity)
Form {
contentView()
.eraseToAnyView()
}
.padding(.top)
.scrollClipDisabled()
} }
.padding(.top)
.scrollClipDisabled()
} }
} }
} }

View File

@ -41,6 +41,9 @@ struct ChannelLibraryView: View {
var body: some View { var body: some View {
ZStack { ZStack {
Color(red: 0.15, green: 0.05, blue: 0.1)
.ignoresSafeArea()
switch viewModel.state { switch viewModel.state {
case .content: case .content:
if viewModel.elements.isEmpty { if viewModel.elements.isEmpty {

View File

@ -53,8 +53,8 @@ struct HomeView: View {
var body: some View { var body: some View {
ZStack { ZStack {
// This keeps the ErrorView vertically aligned with the PagingLibraryView Color(red: 0.15, green: 0.05, blue: 0.1)
Color.clear .ignoresSafeArea()
switch viewModel.state { switch viewModel.state {
case .content: case .content:

View File

@ -53,8 +53,8 @@ struct MediaView: View {
var body: some View { var body: some View {
ZStack { ZStack {
// This keeps the ErrorView vertically aligned with the PagingLibraryView Color(red: 0.15, green: 0.05, blue: 0.1)
Color.clear .ignoresSafeArea()
switch viewModel.state { switch viewModel.state {
case .content: case .content:

View File

@ -349,7 +349,8 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
var body: some View { var body: some View {
ZStack { ZStack {
Color.clear Color(red: 0.15, green: 0.05, blue: 0.1)
.ignoresSafeArea()
if cinematicBackground { if cinematicBackground {
CinematicBackgroundView(viewModel: cinematicBackgroundViewModel) CinematicBackgroundView(viewModel: cinematicBackgroundViewModel)

View File

@ -0,0 +1,142 @@
//
// 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 ProgramGuideView: View {
@StateObject
private var viewModel = EPGViewModel()
// Configuration
private let pixelsPerMinute: CGFloat = 3.0
@State
private var currentTime = Date.now
private let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Color(red: 0.15, green: 0.05, blue: 0.1)
.ignoresSafeArea()
content
}
.onAppear {
viewModel.send(.refresh)
}
.onReceive(timer) { _ in
currentTime = Date.now
}
}
@ViewBuilder
private var content: some View {
switch viewModel.state {
case .initial:
ProgressView()
.scaleEffect(1.5)
case .refreshing:
VStack(spacing: 20) {
ProgressView()
.scaleEffect(1.5)
Text("Loading Program Guide...")
.font(.headline)
.foregroundColor(.secondary)
}
case .content:
if viewModel.channelPrograms.isEmpty {
emptyView
} else {
epgGridView
}
case let .error(error):
VStack(spacing: 20) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 60))
.foregroundColor(.red)
Text("Error Loading Guide")
.font(.title2)
.fontWeight(.semibold)
Text(error.localizedDescription)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 100)
Button("Retry") {
viewModel.send(.refresh)
}
.buttonStyle(.card)
}
}
}
private var epgGridView: some View {
ZStack {
VStack(spacing: 0) {
// Timeline header
EPGTimelineHeader(
timeWindowStart: viewModel.timeWindowStart,
timeWindowEnd: viewModel.timeWindowEnd,
pixelsPerMinute: pixelsPerMinute
)
Divider()
// Channel rows
ScrollView(.vertical, showsIndicators: true) {
VStack(spacing: 0) {
ForEach(viewModel.channelPrograms, id: \.id) { channelProgram in
EPGChannelRow(
channelProgram: channelProgram,
timeWindowStart: viewModel.timeWindowStart,
timeWindowEnd: viewModel.timeWindowEnd,
pixelsPerMinute: pixelsPerMinute
)
Divider()
}
}
}
}
// Current time indicator overlay
EPGCurrentTimeIndicator(
timeWindowStart: viewModel.timeWindowStart,
pixelsPerMinute: pixelsPerMinute
)
}
.navigationTitle("Program Guide")
}
private var emptyView: some View {
VStack(spacing: 20) {
Image(systemName: "tv")
.font(.system(size: 80))
.foregroundColor(.secondary)
Text("No Channels Available")
.font(.title)
.fontWeight(.semibold)
Text("Check your Live TV setup in Jellyfin")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 100)
}
}
}

View File

@ -0,0 +1,95 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
import SwiftUI
struct EPGChannelRow: View {
let channelProgram: ChannelProgram
let timeWindowStart: Date
let timeWindowEnd: Date
let pixelsPerMinute: CGFloat
private let channelColumnWidth: CGFloat = 200
var body: some View {
HStack(spacing: 0) {
// Channel info column (fixed width on left)
channelInfoView
.frame(width: channelColumnWidth)
// Programs timeline
programsTimeline
}
.frame(height: 120)
}
private var channelInfoView: some View {
VStack(spacing: 8) {
ZStack {
Color.clear
ImageView(channelProgram.portraitImageSources(maxWidth: 80))
.image {
$0.aspectRatio(contentMode: .fit)
}
.failure {
SystemImageContentView(systemName: channelProgram.systemImage, ratio: 0.66)
.background(color: .clear)
}
.placeholder { _ in
EmptyView()
}
}
.frame(width: 80, height: 80)
.aspectRatio(1.0, contentMode: .fit)
Text(channelProgram.displayTitle)
.font(.caption)
.lineLimit(2)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 8)
}
private var programsTimeline: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(channelProgram.programs, id: \.id) { program in
if let startDate = program.startDate,
let endDate = program.endDate
{
let duration = endDate.timeIntervalSince(startDate) / 60 // minutes
let cellWidth = CGFloat(duration) * pixelsPerMinute
let isCurrentlyAiring = (startDate ... endDate).contains(Date.now)
EPGProgramCell(
program: program,
channel: channelProgram.channel,
cellWidth: max(cellWidth, 150), // Minimum width for readability
isCurrentlyAiring: isCurrentlyAiring
)
.id(program.id)
}
}
}
.padding(.horizontal, 8)
}
.onAppear {
// Scroll to currently airing program
if let currentProgram = channelProgram.currentProgram {
proxy.scrollTo(currentProgram.id, anchor: .leading)
}
}
}
}
}

View File

@ -0,0 +1,64 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct EPGCurrentTimeIndicator: View {
let timeWindowStart: Date
let pixelsPerMinute: CGFloat
let channelColumnWidth: CGFloat = 200
@State
private var currentTime = Date.now
private let timer = Timer.publish(every: 30, on: .main, in: .common).autoconnect()
var body: some View {
GeometryReader { geometry in
if currentTime >= timeWindowStart {
let offsetMinutes = currentTime.timeIntervalSince(timeWindowStart) / 60
let xPosition = channelColumnWidth + (CGFloat(offsetMinutes) * pixelsPerMinute)
if xPosition >= channelColumnWidth && xPosition <= geometry.size.width {
VStack(spacing: 0) {
// Time marker at top
ZStack {
Circle()
.fill(Color.red)
.frame(width: 20, height: 20)
Text(currentTime, style: .time)
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
Capsule()
.fill(Color.red)
)
.offset(y: -25)
}
.frame(height: 50)
// Vertical line
Rectangle()
.fill(Color.red)
.frame(width: 2)
}
.offset(x: xPosition)
}
}
}
.allowsHitTesting(false) // Allow interactions to pass through
.onReceive(timer) { _ in
currentTime = Date.now
}
}
}

View File

@ -0,0 +1,120 @@
//
// 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 JellyfinAPI
import SwiftUI
struct EPGProgramCell: View {
@EnvironmentObject
private var router: VideoPlayerWrapperCoordinator.Router
@Environment(\.isFocused)
private var isFocused
let program: BaseItemDto
let channel: BaseItemDto
let cellWidth: CGFloat
let isCurrentlyAiring: Bool
@State
private var currentTime = Date.now
private let timer = Timer.publish(every: 30, on: .main, in: .common).autoconnect()
var body: some View {
Button {
handleSelection()
} label: {
ZStack(alignment: .leading) {
// Background
RoundedRectangle(cornerRadius: 8)
.fill(backgroundColor)
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(borderColor, lineWidth: isFocused ? 4 : 2)
)
// Progress indicator for currently airing programs
if isCurrentlyAiring, let progress = program.programProgress(relativeTo: currentTime) {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 8)
.fill(Color.accentColor.opacity(0.3))
.frame(width: geometry.size.width * progress)
}
}
// Content
VStack(alignment: .leading, spacing: 4) {
Text(program.displayTitle)
.font(.callout)
.fontWeight(isFocused ? .semibold : .regular)
.lineLimit(2)
.foregroundColor(isFocused ? .black : .white)
if let startDate = program.startDate,
let endDate = program.endDate
{
HStack(spacing: 4) {
Text(startDate, style: .time)
Text("-")
Text(endDate, style: .time)
}
.font(.caption2)
.foregroundColor(isFocused ? .black.opacity(0.7) : .secondary)
}
if isCurrentlyAiring {
Text("Live")
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(isFocused ? .black : .red)
}
}
.padding(8)
}
}
.frame(width: cellWidth, height: 100)
.buttonStyle(.card)
.onReceive(timer) { _ in
currentTime = Date.now
}
}
private var backgroundColor: Color {
if isFocused {
return .white
} else if isCurrentlyAiring {
return Color.accentColor.opacity(0.2)
} else {
return Color(white: 0.2)
}
}
private var borderColor: Color {
if isFocused {
return .white
} else if isCurrentlyAiring {
return .accentColor
} else {
return Color(white: 0.3)
}
}
private func handleSelection() {
// For Live TV from EPG, we play the channel
// If program is currently airing, playback will start from current position
// If program is in the future, channel will start playing whatever is currently on
guard let mediaSource = channel.mediaSources?.first else { return }
router.route(
to: \.liveVideoPlayer,
LiveVideoPlayerManager(item: channel, mediaSource: mediaSource)
)
}
}

View File

@ -0,0 +1,66 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct EPGTimelineHeader: View {
let timeWindowStart: Date
let timeWindowEnd: Date
let pixelsPerMinute: CGFloat
private let channelColumnWidth: CGFloat = 200
private let timeSlotInterval: TimeInterval = 1800 // 30 minutes
var body: some View {
HStack(spacing: 0) {
// Empty space for channel column
Color.clear
.frame(width: channelColumnWidth)
// Time markers
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(timeSlots, id: \.self) { time in
timeMarker(for: time)
}
}
.padding(.horizontal, 8)
}
}
.frame(height: 50)
.background(Color(white: 0.15))
}
private var timeSlots: [Date] {
var slots: [Date] = []
var currentTime = timeWindowStart
while currentTime <= timeWindowEnd {
slots.append(currentTime)
currentTime = currentTime.addingTimeInterval(timeSlotInterval)
}
return slots
}
private func timeMarker(for date: Date) -> some View {
let slotWidth = CGFloat(timeSlotInterval / 60) * pixelsPerMinute
return VStack(spacing: 4) {
Text(date, style: .time)
.font(.headline)
.fontWeight(.semibold)
Rectangle()
.fill(Color.secondary)
.frame(width: 2, height: 15)
}
.frame(width: slotWidth)
}
}

View File

@ -79,6 +79,9 @@ struct ProgramsView: View {
var body: some View { var body: some View {
ZStack { ZStack {
Color(red: 0.15, green: 0.05, blue: 0.1)
.ignoresSafeArea()
switch programsViewModel.state { switch programsViewModel.state {
case .content: case .content:
if programsViewModel.hasNoResults { if programsViewModel.hasNoResults {

View File

@ -111,6 +111,9 @@ struct SearchView: View {
var body: some View { var body: some View {
ZStack { ZStack {
Color(red: 0.15, green: 0.05, blue: 0.1)
.ignoresSafeArea()
switch viewModel.state { switch viewModel.state {
case .initial: case .initial:
suggestionsView suggestionsView

View File

@ -10,6 +10,8 @@
091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; };
21951AC22D9D2010002E03E0 /* AddUserBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21951AC12D9D2010002E03E0 /* AddUserBottomButton.swift */; }; 21951AC22D9D2010002E03E0 /* AddUserBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21951AC12D9D2010002E03E0 /* AddUserBottomButton.swift */; };
21BCDEF72D9C822000E1D180 /* AddUserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21BCDEF62D9C822000E1D180 /* AddUserGridButton.swift */; }; 21BCDEF72D9C822000E1D180 /* AddUserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21BCDEF62D9C822000E1D180 /* AddUserGridButton.swift */; };
43D8DAACB1A6D59470D31082 /* EPGViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67ED7169D761EBCCC34650B1 /* EPGViewModel.swift */; };
4C0A02DD28ED5E02DDE52088 /* EPGProgramCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C527D44A53C7443E836A3DAD /* EPGProgramCell.swift */; };
4E01446C2D0292E200193038 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E01446B2D0292E000193038 /* Trie.swift */; }; 4E01446C2D0292E200193038 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E01446B2D0292E000193038 /* Trie.swift */; };
4E0253BD2CBF0C06007EB9CD /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E12F9152CBE9615006C217E /* DeviceType.swift */; }; 4E0253BD2CBF0C06007EB9CD /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E12F9152CBE9615006C217E /* DeviceType.swift */; };
4E0A8FFC2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */; }; 4E0A8FFC2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */; };
@ -94,6 +96,7 @@
4EF36F652D962A430065BB79 /* ItemSortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF36F632D962A430065BB79 /* ItemSortBy.swift */; }; 4EF36F652D962A430065BB79 /* ItemSortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF36F632D962A430065BB79 /* ItemSortBy.swift */; };
4EF36F672D9649050065BB79 /* SessionInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDBDCD02CBDD6510033D347 /* SessionInfoDto.swift */; }; 4EF36F672D9649050065BB79 /* SessionInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDBDCD02CBDD6510033D347 /* SessionInfoDto.swift */; };
4EFE0C7D2D0156A900D4834D /* PersonKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */; }; 4EFE0C7D2D0156A900D4834D /* PersonKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */; };
527E650F94265266C416CFD8 /* ProgramGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C1FE779A04986789C1D943 /* ProgramGuideView.swift */; };
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; };
534D4FF126A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEE26A7D7CC000A7A48 /* Localizable.strings */; }; 534D4FF126A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEE26A7D7CC000A7A48 /* Localizable.strings */; };
534D4FF426A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FE726A7D7CC000A7A48 /* Localizable.strings */; }; 534D4FF426A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FE726A7D7CC000A7A48 /* Localizable.strings */; };
@ -101,6 +104,7 @@
535870632669D21600D05A09 /* jellypigapp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870622669D21600D05A09 /* jellypigapp.swift */; }; 535870632669D21600D05A09 /* jellypigapp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870622669D21600D05A09 /* jellypigapp.swift */; };
535870672669D21700D05A09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 535870662669D21700D05A09 /* Assets.xcassets */; }; 535870672669D21700D05A09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 535870662669D21700D05A09 /* Assets.xcassets */; };
5364F456266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPerson.swift */; }; 5364F456266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPerson.swift */; };
536803180875374165091699 /* EPGTimelineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48E6BA368CDEEDAD2A877B96 /* EPGTimelineHeader.swift */; };
53913BF026D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; }; 53913BF026D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; };
53913BF326D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BCC26D323FE00EB3286 /* Localizable.strings */; }; 53913BF326D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BCC26D323FE00EB3286 /* Localizable.strings */; };
53913BF626D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BCF26D323FE00EB3286 /* Localizable.strings */; }; 53913BF626D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BCF26D323FE00EB3286 /* Localizable.strings */; };
@ -178,6 +182,7 @@
62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; }; 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; };
9F6FDB6C675373491EB57B41 /* SeasonHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2919FFF7C404A6AD31658B2 /* SeasonHStack.swift */; }; 9F6FDB6C675373491EB57B41 /* SeasonHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2919FFF7C404A6AD31658B2 /* SeasonHStack.swift */; };
B553DE52A96FE4289D1E6996 /* AttributeBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65CB977628965AA9099742F /* AttributeBadge.swift */; }; B553DE52A96FE4289D1E6996 /* AttributeBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65CB977628965AA9099742F /* AttributeBadge.swift */; };
B56E653D9B9A40ED072ED318 /* EPGCurrentTimeIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89069FCB78E1759D27632E1 /* EPGCurrentTimeIndicator.swift */; };
BD0BA22C2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */; }; BD0BA22C2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */; };
BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */; }; BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */; };
BD88CB422D77E6A0006BB5E3 /* TVOSPicker in Frameworks */ = {isa = PBXBuildFile; productRef = BD88CB412D77E6A0006BB5E3 /* TVOSPicker */; }; BD88CB422D77E6A0006BB5E3 /* TVOSPicker in Frameworks */ = {isa = PBXBuildFile; productRef = BD88CB412D77E6A0006BB5E3 /* TVOSPicker */; };
@ -196,6 +201,7 @@
C46DD8EC2A8FB49A0046A504 /* LiveMainOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46DD8EB2A8FB49A0046A504 /* LiveMainOverlay.swift */; }; C46DD8EC2A8FB49A0046A504 /* LiveMainOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46DD8EB2A8FB49A0046A504 /* LiveMainOverlay.swift */; };
C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46DD8EE2A8FB56E0046A504 /* LiveBottomBarView.swift */; }; C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46DD8EE2A8FB56E0046A504 /* LiveBottomBarView.swift */; };
C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* MediaView.swift */; }; C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* MediaView.swift */; };
C606AA8295DF9645C57237D6 /* EPGChannelRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BFAA71C5194626E96E5F838 /* EPGChannelRow.swift */; };
CC787DD1C212FF9BB2542D28 /* VideoRangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12C80CEDC871A21D98141BBE /* VideoRangeType.swift */; }; CC787DD1C212FF9BB2542D28 /* VideoRangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12C80CEDC871A21D98141BBE /* VideoRangeType.swift */; };
DFB7C3E02C7AA43A00CE7CDC /* UserSignInState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB7C3DE2C7AA42700CE7CDC /* UserSignInState.swift */; }; DFB7C3E02C7AA43A00CE7CDC /* UserSignInState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFB7C3DE2C7AA42700CE7CDC /* UserSignInState.swift */; };
E1002B652793CEE800E47059 /* ChapterInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfo.swift */; }; E1002B652793CEE800E47059 /* ChapterInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfo.swift */; };
@ -551,9 +557,11 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = "<group>"; }; 091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = "<group>"; };
0BFAA71C5194626E96E5F838 /* EPGChannelRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EPGChannelRow.swift; path = "../jellypig tvOS/Views/ProgramGuideView/Components/EPGChannelRow.swift"; sourceTree = "<group>"; };
12C80CEDC871A21D98141BBE /* VideoRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRangeType.swift; sourceTree = "<group>"; }; 12C80CEDC871A21D98141BBE /* VideoRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRangeType.swift; sourceTree = "<group>"; };
21951AC12D9D2010002E03E0 /* AddUserBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserBottomButton.swift; sourceTree = "<group>"; }; 21951AC12D9D2010002E03E0 /* AddUserBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserBottomButton.swift; sourceTree = "<group>"; };
21BCDEF62D9C822000E1D180 /* AddUserGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserGridButton.swift; sourceTree = "<group>"; }; 21BCDEF62D9C822000E1D180 /* AddUserGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserGridButton.swift; sourceTree = "<group>"; };
48E6BA368CDEEDAD2A877B96 /* EPGTimelineHeader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EPGTimelineHeader.swift; path = "../jellypig tvOS/Views/ProgramGuideView/Components/EPGTimelineHeader.swift"; sourceTree = "<group>"; };
4E01446B2D0292E000193038 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; }; 4E01446B2D0292E000193038 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
4E0195E32CE04678007844F4 /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = "<group>"; }; 4E0195E32CE04678007844F4 /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = "<group>"; };
4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetUserPasswordView.swift; sourceTree = "<group>"; }; 4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetUserPasswordView.swift; sourceTree = "<group>"; };
@ -917,6 +925,8 @@
6334175A287DDFB9000603CE /* QuickConnectAuthorizeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectAuthorizeView.swift; sourceTree = "<group>"; }; 6334175A287DDFB9000603CE /* QuickConnectAuthorizeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectAuthorizeView.swift; sourceTree = "<group>"; };
6334175C287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectAuthorizeViewModel.swift; sourceTree = "<group>"; }; 6334175C287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectAuthorizeViewModel.swift; sourceTree = "<group>"; };
637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = UDPBroadcast.xcframework; path = Carthage/Build/UDPBroadcast.xcframework; sourceTree = "<group>"; }; 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = UDPBroadcast.xcframework; path = Carthage/Build/UDPBroadcast.xcframework; sourceTree = "<group>"; };
67ED7169D761EBCCC34650B1 /* EPGViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EPGViewModel.swift; path = ../Shared/ViewModels/EPGViewModel.swift; sourceTree = "<group>"; };
B2C1FE779A04986789C1D943 /* ProgramGuideView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProgramGuideView.swift; path = "../jellypig tvOS/Views/ProgramGuideView.swift"; sourceTree = "<group>"; };
B65CB977628965AA9099742F /* AttributeBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributeBadge.swift; sourceTree = "<group>"; }; B65CB977628965AA9099742F /* AttributeBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributeBadge.swift; sourceTree = "<group>"; };
BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineVideoPlayerManager.swift; sourceTree = "<group>"; }; BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineVideoPlayerManager.swift; sourceTree = "<group>"; };
BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadVideoPlayerManager.swift; sourceTree = "<group>"; }; BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadVideoPlayerManager.swift; sourceTree = "<group>"; };
@ -948,6 +958,7 @@
C46DD8EB2A8FB49A0046A504 /* LiveMainOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveMainOverlay.swift; sourceTree = "<group>"; }; C46DD8EB2A8FB49A0046A504 /* LiveMainOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveMainOverlay.swift; sourceTree = "<group>"; };
C46DD8EE2A8FB56E0046A504 /* LiveBottomBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveBottomBarView.swift; sourceTree = "<group>"; }; C46DD8EE2A8FB56E0046A504 /* LiveBottomBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveBottomBarView.swift; sourceTree = "<group>"; };
C4E508172703E8190045C9AB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; }; C4E508172703E8190045C9AB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
C527D44A53C7443E836A3DAD /* EPGProgramCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EPGProgramCell.swift; path = "../jellypig tvOS/Views/ProgramGuideView/Components/EPGProgramCell.swift"; sourceTree = "<group>"; };
DFB7C3DE2C7AA42700CE7CDC /* UserSignInState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInState.swift; sourceTree = "<group>"; }; DFB7C3DE2C7AA42700CE7CDC /* UserSignInState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInState.swift; sourceTree = "<group>"; };
E1002B632793CEE700E47059 /* ChapterInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfo.swift; sourceTree = "<group>"; }; E1002B632793CEE700E47059 /* ChapterInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfo.swift; sourceTree = "<group>"; };
E101ECD42CD40489001EA89E /* DeviceDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDetailViewModel.swift; sourceTree = "<group>"; }; E101ECD42CD40489001EA89E /* DeviceDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDetailViewModel.swift; sourceTree = "<group>"; };
@ -1408,6 +1419,7 @@
E1FE69A628C29B720021BC93 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = "<group>"; }; E1FE69A628C29B720021BC93 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = "<group>"; };
E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; }; E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; };
E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnScenePhaseChangedModifier.swift; sourceTree = "<group>"; }; E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnScenePhaseChangedModifier.swift; sourceTree = "<group>"; };
E89069FCB78E1759D27632E1 /* EPGCurrentTimeIndicator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EPGCurrentTimeIndicator.swift; path = "../jellypig tvOS/Views/ProgramGuideView/Components/EPGCurrentTimeIndicator.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -2725,6 +2737,8 @@
E1DD1127271E7D15005BE12F /* Objects */, E1DD1127271E7D15005BE12F /* Objects */,
E1DCDE3B2A2D134000FA9C91 /* Resources */, E1DCDE3B2A2D134000FA9C91 /* Resources */,
E13DD3D027165886009D4DAF /* Views */, E13DD3D027165886009D4DAF /* Views */,
A9B355418BEC3896FA6490EA /* Shared */,
7921755ED42A950421AEEF8E /* jellypig tvOS */,
); );
path = jellypig; path = jellypig;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3172,6 +3186,17 @@
path = "zh-Hant.lproj"; path = "zh-Hant.lproj";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6211D44F6675191660F709BC /* Components */ = {
isa = PBXGroup;
children = (
C527D44A53C7443E836A3DAD /* EPGProgramCell.swift */,
0BFAA71C5194626E96E5F838 /* EPGChannelRow.swift */,
48E6BA368CDEEDAD2A877B96 /* EPGTimelineHeader.swift */,
E89069FCB78E1759D27632E1 /* EPGCurrentTimeIndicator.swift */,
);
name = Components;
sourceTree = "<group>";
};
621338912660106C00A81A2A /* Extensions */ = { 621338912660106C00A81A2A /* Extensions */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -3278,6 +3303,39 @@
path = HStacks; path = HStacks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6C70C653BDDF18CB02C02D82 /* Views */ = {
isa = PBXGroup;
children = (
7D7484DE244C0DAFD185432A /* ProgramGuideView */,
);
name = Views;
sourceTree = "<group>";
};
7921755ED42A950421AEEF8E /* jellypig tvOS */ = {
isa = PBXGroup;
children = (
6C70C653BDDF18CB02C02D82 /* Views */,
);
name = "jellypig tvOS";
sourceTree = "<group>";
};
7D7484DE244C0DAFD185432A /* ProgramGuideView */ = {
isa = PBXGroup;
children = (
6211D44F6675191660F709BC /* Components */,
B2C1FE779A04986789C1D943 /* ProgramGuideView.swift */,
);
name = ProgramGuideView;
sourceTree = "<group>";
};
A9B355418BEC3896FA6490EA /* Shared */ = {
isa = PBXGroup;
children = (
F6DB96C6C0E59551858F8D9F /* ViewModels */,
);
name = Shared;
sourceTree = "<group>";
};
BD0BA2292AD6501300306A8D /* VideoPlayerManager */ = { BD0BA2292AD6501300306A8D /* VideoPlayerManager */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -4628,6 +4686,14 @@
path = NavigationBarFilterDrawer; path = NavigationBarFilterDrawer;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
F6DB96C6C0E59551858F8D9F /* ViewModels */ = {
isa = PBXGroup;
children = (
67ED7169D761EBCCC34650B1 /* EPGViewModel.swift */,
);
name = ViewModels;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -5346,6 +5412,12 @@
B553DE52A96FE4289D1E6996 /* AttributeBadge.swift in Sources */, B553DE52A96FE4289D1E6996 /* AttributeBadge.swift in Sources */,
CC787DD1C212FF9BB2542D28 /* VideoRangeType.swift in Sources */, CC787DD1C212FF9BB2542D28 /* VideoRangeType.swift in Sources */,
9F6FDB6C675373491EB57B41 /* SeasonHStack.swift in Sources */, 9F6FDB6C675373491EB57B41 /* SeasonHStack.swift in Sources */,
43D8DAACB1A6D59470D31082 /* EPGViewModel.swift in Sources */,
4C0A02DD28ED5E02DDE52088 /* EPGProgramCell.swift in Sources */,
C606AA8295DF9645C57237D6 /* EPGChannelRow.swift in Sources */,
536803180875374165091699 /* EPGTimelineHeader.swift in Sources */,
B56E653D9B9A40ED072ED318 /* EPGCurrentTimeIndicator.swift in Sources */,
527E650F94265266C416CFD8 /* ProgramGuideView.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };