RTSDA-iOS/Views/HomeFeedView.swift
RTSDA 00679f927c docs: Update README for v2.0 release and fix git remote URL
- Comprehensive README update documenting v2.0 architectural changes
- Updated git remote to ssh://rockvilleav@git.rockvilletollandsda.church:10443/RTSDA/RTSDA-iOS.git
- Documented unified ChurchService and 60% code reduction
- Added new features: Home Feed, responsive reading, enhanced UI
- Corrected license information (GPL v3 with church content copyright)
- Updated build instructions and technical stack details
2025-08-16 18:41:51 -04:00

451 lines
18 KiB
Swift

import SwiftUI
struct HomeFeedView: View {
@Environment(ChurchDataService.self) private var dataService
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
if horizontalSizeClass == .regular {
// iPad: Enhanced entertainment-focused layout
iPadHomeFeedView()
} else {
// iPhone: Current compact layout
iPhoneHomeFeedView()
}
}
}
struct iPadHomeFeedView: View {
@Environment(ChurchDataService.self) private var dataService
var body: some View {
ScrollView {
LazyVStack(spacing: 32) {
// Hero Section - Latest Featured Sermon
if let latestSermon = dataService.sermons.first {
VStack(alignment: .leading, spacing: 0) {
HStack {
VStack(alignment: .leading, spacing: 8) {
Text("LATEST MESSAGE")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(Color(hex: getBrandColor()))
.textCase(.uppercase)
.tracking(1)
Text("Featured Sermon")
.font(.system(size: 32, weight: .bold))
.foregroundColor(.primary)
}
Spacer()
NavigationLink("Browse All") {
WatchView()
}
.font(.system(size: 16, weight: .medium))
.foregroundColor(Color(hex: getBrandColor()))
}
.padding(.horizontal, 24)
.padding(.top, 16)
Button {
if latestSermon.videoUrl != nil, let url = URL(string: getOptimalStreamingUrl(mediaId: latestSermon.id)) {
SharedVideoManager.shared.playVideo(url: url, title: latestSermon.title, artworkURL: latestSermon.thumbnail)
}
} label: {
HeroSermonCard(sermon: latestSermon)
}
.buttonStyle(PlainButtonStyle())
.padding(.top, 20)
}
}
// Recent Sermons Grid
if dataService.sermons.count > 1 {
VStack(alignment: .leading, spacing: 20) {
HStack {
Text("Recent Sermons")
.font(.system(size: 28, weight: .bold))
Spacer()
NavigationLink("View All") {
WatchView()
}
.font(.system(size: 16, weight: .medium))
.foregroundColor(Color(hex: getBrandColor()))
}
.padding(.horizontal, 24)
// 2x2 grid layout for iPads
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: 2), spacing: 16) {
ForEach(Array(dataService.sermons.dropFirst().prefix(4)), id: \.id) { sermon in
Button {
if sermon.videoUrl != nil, let url = URL(string: getOptimalStreamingUrl(mediaId: sermon.id)) {
SharedVideoManager.shared.playVideo(url: url, title: sermon.title, artworkURL: sermon.thumbnail)
}
} label: {
SermonGridCard(sermon: sermon)
}
.buttonStyle(PlainButtonStyle())
}
}
.padding(.horizontal, 24)
}
}
// Upcoming Events Section
if !dataService.events.isEmpty {
VStack(alignment: .leading, spacing: 20) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("WHAT'S HAPPENING")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(Color(hex: getBrandColor()))
.textCase(.uppercase)
.tracking(1)
Text("Upcoming Events")
.font(.system(size: 28, weight: .bold))
}
Spacer()
NavigationLink("All Events") {
EventsListView()
}
.font(.system(size: 16, weight: .medium))
.foregroundColor(Color(hex: getBrandColor()))
}
.padding(.horizontal, 24)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(dataService.events.prefix(5), id: \.id) { event in
NavigationLink {
EventDetailViewWrapper(event: event)
} label: {
ChurchEventHighlightCard(event: event)
.frame(width: 300)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 24)
}
}
}
// Quick Actions removed - functionality available via main navigation tabs
}
.padding(.vertical, 20)
.padding(.bottom, 100)
}
.navigationTitle("Welcome to RTSDA")
.navigationBarTitleDisplayMode(.large)
.refreshable {
await dataService.loadHomeFeed()
}
}
}
struct iPhoneHomeFeedView: View {
@Environment(ChurchDataService.self) private var dataService
var body: some View {
ScrollView {
LazyVStack(spacing: 20) {
if !dataService.sermons.isEmpty {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Featured Sermons")
.font(.system(size: 24, weight: .bold))
Spacer()
NavigationLink("See All") {
WatchView()
}
.font(.subheadline)
.foregroundColor(.blue)
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(dataService.sermons.prefix(5), id: \.id) { sermon in
SermonCard(sermon: sermon, style: .feed)
.frame(width: 250)
}
}
.padding(.horizontal, 20)
}
}
}
if !dataService.events.isEmpty {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Upcoming Events")
.font(.system(size: 24, weight: .bold))
Spacer()
NavigationLink("See All") {
EventsListView()
}
.font(.subheadline)
.foregroundColor(.blue)
}
ForEach(dataService.events.prefix(3), id: \.id) { event in
FeedItemCard(item: FeedItem(type: .event(event), timestamp: Date()))
}
}
}
}
.padding(.horizontal, 20)
.padding(.bottom, 100)
}
.navigationTitle("Welcome to RTSDA")
.navigationBarTitleDisplayMode(.large)
.refreshable {
await dataService.loadHomeFeed()
}
}
}
// MARK: - iPad-specific Cards
struct HeroSermonCard: View {
let sermon: Sermon
var body: some View {
ZStack {
// Background image with gradient overlay
AsyncImage(url: URL(string: sermon.thumbnail ?? "")) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.offset(y: 20) // Shift DOWN to show heads instead of cutting them off
.scaleEffect(0.9) // Zoom out to ensure heads are visible
} placeholder: {
Rectangle()
.fill(LinearGradient(
colors: [Color(hex: getBrandColor()), Color(hex: getBrandColor()).opacity(0.7)],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
}
.frame(height: 450)
.clipped()
// Gradient overlay
LinearGradient(
colors: [Color.clear, Color.black.opacity(0.8)],
startPoint: .top,
endPoint: .bottom
)
// Content overlay
VStack(alignment: .leading) {
Spacer()
HStack {
VStack(alignment: .leading, spacing: 12) {
// Play button
Image(systemName: "play.circle.fill")
.font(.system(size: 60))
.foregroundColor(.white)
.shadow(color: .black.opacity(0.3), radius: 4)
VStack(alignment: .leading, spacing: 8) {
Text(sermon.title)
.font(.system(size: 28, weight: .bold))
.foregroundColor(.white)
.lineLimit(2)
.shadow(color: .black.opacity(0.5), radius: 2)
HStack(spacing: 16) {
if !sermon.speaker.isEmpty {
HStack(spacing: 4) {
Image(systemName: "person.fill")
.font(.caption)
Text(sermon.speaker)
.font(.subheadline)
.fontWeight(.medium)
}
}
if let duration = sermon.durationFormatted {
HStack(spacing: 4) {
Image(systemName: "clock.fill")
.font(.caption)
Text(duration)
.font(.subheadline)
}
}
Text(sermon.formattedDate)
.font(.subheadline)
}
.foregroundColor(.white.opacity(0.9))
.shadow(color: .black.opacity(0.3), radius: 1)
}
}
Spacer()
}
.padding(32)
}
}
.cornerRadius(16)
.shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
.padding(.horizontal, 24)
}
}
struct SermonGridCard: View {
let sermon: Sermon
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Thumbnail
ZStack {
AsyncImage(url: URL(string: sermon.thumbnail ?? "")) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.offset(y: 10) // Shift DOWN to show heads instead of cutting them off
.scaleEffect(0.95) // Zoom out to ensure heads are visible
} placeholder: {
Rectangle()
.fill(LinearGradient(
colors: [Color(hex: getBrandColor()), Color(hex: getBrandColor()).opacity(0.7)],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
}
.frame(height: 200)
.clipped()
// Play button overlay
Circle()
.fill(.black.opacity(0.6))
.frame(width: 50, height: 50)
.overlay {
Image(systemName: "play.fill")
.font(.title2)
.foregroundColor(.white)
.offset(x: 2) // Visual centering
}
}
.cornerRadius(12)
// Content
VStack(alignment: .leading, spacing: 8) {
Text(sermon.title)
.font(.system(size: 16, weight: .semibold))
.lineLimit(2)
.multilineTextAlignment(.leading)
HStack {
if !sermon.speaker.isEmpty {
Text(sermon.speaker)
.font(.system(size: 14))
.foregroundColor(.secondary)
.lineLimit(1)
}
Spacer()
if let duration = sermon.durationFormatted {
Text(duration)
.font(.system(size: 14))
.foregroundColor(.secondary)
}
}
Text(sermon.formattedDate)
.font(.system(size: 13))
.foregroundColor(.secondary)
}
.padding(.horizontal, 4)
}
.frame(height: 300)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
}
}
struct ChurchEventHighlightCard: View {
let event: ChurchEvent
var body: some View {
HStack(spacing: 16) {
// Date indicator
VStack(spacing: 4) {
Text(event.dayOfMonth)
.font(.system(size: 24, weight: .bold))
.foregroundColor(Color(hex: getBrandColor()))
Text(event.monthAbbreviation)
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.secondary)
.textCase(.uppercase)
}
.frame(width: 50)
.padding(.vertical, 12)
.background(Color(hex: getBrandColor()).opacity(0.1), in: RoundedRectangle(cornerRadius: 12))
VStack(alignment: .leading, spacing: 8) {
Text(event.title)
.font(.system(size: 18, weight: .semibold))
.lineLimit(2)
if !event.description.isEmpty {
Text(event.description.stripHtml())
.font(.system(size: 14))
.foregroundColor(.secondary)
.lineLimit(2)
}
HStack(spacing: 8) {
Image(systemName: "clock.fill")
.font(.caption)
.foregroundColor(Color(hex: getBrandColor()))
Text(event.timeString)
.font(.system(size: 13, weight: .medium))
.foregroundColor(.secondary)
if !event.location.isEmpty {
Image(systemName: "location.fill")
.font(.caption)
.foregroundColor(Color(hex: getBrandColor()))
Text(event.location)
.font(.system(size: 13, weight: .medium))
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
Spacer()
}
.padding(20)
.frame(height: 140)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.05), radius: 6, x: 0, y: 2)
}
}
// HomeQuickActionCard removed - no longer needed
// MARK: - Extensions
// All date/time formatting now handled by Rust church-core crate (RTSDA Architecture Rules compliance)
// ChurchEvent now includes dayOfMonth, monthAbbreviation, and timeString fields directly from Rust
extension String {
func stripHtml() -> String {
return self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil)
}
}