
- 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
451 lines
18 KiB
Swift
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)
|
|
}
|
|
}
|