Files
denshooter 5bc81d5b3b Initial commit: nightly iOS app + Supabase backend
iOS SwiftUI app with Supabase auth/realtime, Node.js backend,
Docker/Supabase self-hosted infrastructure, and APNs scheduler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 23:31:38 +02:00

200 lines
5.8 KiB
Swift

import SwiftUI
struct MainTabView: View {
@EnvironmentObject var appState: AppState
@StateObject private var realtime = RealtimeService()
@State private var selectedTab = 0
@State private var showCompose = false
@State private var showSettings = false
var body: some View {
ZStack(alignment: .bottom) {
Color.nightBase.ignoresSafeArea()
// Content
TabContent(
selectedTab: selectedTab,
realtime: realtime
)
.environmentObject(appState)
// Floating Tab Bar
FloatingTabBar(
selectedTab: $selectedTab,
windowState: appState.windowState,
onCompose: { showCompose = true },
onSettings: { showSettings = true }
)
}
.ignoresSafeArea(edges: .bottom)
.sheet(isPresented: $showCompose) {
ComposeView().environmentObject(appState)
}
.sheet(isPresented: $showSettings) {
SettingsView().environmentObject(appState)
}
.onDisappear {
Task { await realtime.stopListening() }
}
}
}
// MARK: - Tab Content
private struct TabContent: View {
let selectedTab: Int
@ObservedObject var realtime: RealtimeService
@EnvironmentObject var appState: AppState
var body: some View {
ZStack {
FeedView(realtime: realtime)
.environmentObject(appState)
.opacity(selectedTab == 0 ? 1 : 0)
.allowsHitTesting(selectedTab == 0)
DiaryView()
.environmentObject(appState)
.opacity(selectedTab == 1 ? 1 : 0)
.allowsHitTesting(selectedTab == 1)
ProfileView(
user: appState.currentUser ?? .preview,
isCurrentUser: true
)
.environmentObject(appState)
.opacity(selectedTab == 2 ? 1 : 0)
.allowsHitTesting(selectedTab == 2)
}
}
}
// MARK: - Floating Tab Bar
struct FloatingTabBar: View {
@Binding var selectedTab: Int
let windowState: AppState.WindowState
let onCompose: () -> Void
let onSettings: () -> Void
var body: some View {
HStack(spacing: 0) {
// Feed
TabIcon(icon: "moon.stars", activeIcon: "moon.stars.fill", isSelected: selectedTab == 0) {
selectedTab = 0
}
Spacer()
// Diary
TabIcon(icon: "book.closed", activeIcon: "book.closed.fill", isSelected: selectedTab == 1) {
selectedTab = 1
}
Spacer()
// Center: Compose
ComposeTabButton(windowState: windowState, onTap: onCompose)
Spacer()
// Profile
TabIcon(icon: "person", activeIcon: "person.fill", isSelected: selectedTab == 2) {
selectedTab = 2
}
Spacer()
// Settings
TabIcon(icon: "gearshape", activeIcon: "gearshape.fill", isSelected: false) {
onSettings()
}
}
.padding(.horizontal, 20)
.padding(.vertical, 10)
.padding(.bottom, 18)
.background(
Rectangle()
.fill(.ultraThinMaterial.opacity(0.8))
.background(Color.nightBase.opacity(0.85))
.ignoresSafeArea()
)
.overlay(
Rectangle().fill(Color.nightBorder).frame(height: 1),
alignment: .top
)
}
}
struct TabIcon: View {
let icon: String
let activeIcon: String
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: isSelected ? activeIcon : icon)
.font(.system(size: 21))
.foregroundColor(isSelected ? .nightPrimary : .nightSecondary)
.frame(width: 44, height: 44)
}
}
}
struct ComposeTabButton: View {
let windowState: AppState.WindowState
let onTap: () -> Void
@State private var glow = false
var body: some View {
Button {
guard windowState == .open else { return }
onTap()
} label: {
ZStack {
if windowState == .open {
Circle()
.fill(Color.nightPurple.opacity(0.18))
.frame(width: 62, height: 62)
.scaleEffect(glow ? 1.15 : 1.0)
.animation(.easeInOut(duration: 2.2).repeatForever(autoreverses: true), value: glow)
}
Circle()
.fill(buttonFill)
.frame(width: 50, height: 50)
Image(systemName: buttonIcon)
.font(.system(size: 19, weight: .semibold))
.foregroundColor(.white)
}
}
.onAppear { glow = true }
.animation(.easeInOut(duration: 0.4), value: windowState)
}
var buttonFill: AnyShapeStyle {
switch windowState {
case .open:
return AnyShapeStyle(LinearGradient(
colors: [Color(hex: "8B5CF6"), Color(hex: "6D28D9")],
startPoint: .topLeading, endPoint: .bottomTrailing
))
case .posted:
return AnyShapeStyle(LinearGradient(
colors: [Color(hex: "059669"), Color(hex: "047857")],
startPoint: .top, endPoint: .bottom
))
default:
return AnyShapeStyle(Color.nightRaised)
}
}
var buttonIcon: String {
switch windowState {
case .open: return "plus"
case .posted: return "checkmark"
default: return "moon.zzz"
}
}
}