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:
parent
de349e213e
commit
fdd1cdc15b
|
@ -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.
|
|
@ -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.
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -14,6 +14,10 @@ struct SplitFormWindowView: View {
|
||||||
private var descriptionView: () -> any View
|
private var descriptionView: () -> any View
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Color(red: 0.15, green: 0.05, blue: 0.1)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
descriptionView()
|
descriptionView()
|
||||||
|
@ -29,6 +33,7 @@ struct SplitFormWindowView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension SplitFormWindowView {
|
extension SplitFormWindowView {
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue