// // MainDashboardView.swift // HaruDanim // // The main dashboard: shows currently running time sessions and a grid of // Actions the user can tap to start/stop tracking or add a count. // import SwiftUI import SwiftData struct MainDashboardView: View { @Environment(\.modelContext) private var context @Query(sort: \Action.createdAt, order: .forward) private var actions: [Action] @State private var viewModel = TrackingViewModel() private let columns = [ GridItem(.flexible(), spacing: 12), GridItem(.flexible(), spacing: 12) ] var body: some View { NavigationStack { ScrollView { VStack(alignment: .leading, spacing: 24) { activeSection actionsSection } .padding() } .navigationTitle("메인") .onAppear { viewModel.refreshActiveSessions(context: context) } } } // MARK: - Currently tracking @ViewBuilder private var activeSection: some View { if !viewModel.activeSessions.isEmpty { VStack(alignment: .leading, spacing: 12) { Text("현재 진행 중") .font(.headline) ForEach(viewModel.activeSessions) { session in ActiveSessionRow(session: session) { viewModel.stopSession(session, context: context) } } } } } // MARK: - Actions grid @ViewBuilder private var actionsSection: some View { if actions.isEmpty { Text("등록된 행동이 없습니다.") .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .center) .padding(.top, 40) } else { LazyVGrid(columns: columns, spacing: 12) { ForEach(actions) { action in ActionTile( action: action, isTracking: viewModel.isTracking(action) ) { tapped(action) } } } } } private func tapped(_ action: Action) { switch action.type { case .time: viewModel.toggleTimeTracking(for: action, context: context) case .count: viewModel.incrementCount(for: action, context: context) } } } // MARK: - Active session row private struct ActiveSessionRow: View { let session: TimeSession let onStop: () -> Void var body: some View { HStack(spacing: 12) { Image(systemName: session.action?.iconName ?? "circle") .font(.title3) .foregroundStyle(Color.brandPrimary) VStack(alignment: .leading, spacing: 2) { Text(session.action?.name ?? "행동") .font(.subheadline.weight(.semibold)) TimelineView(.periodic(from: session.startTime, by: 1)) { _ in Text(elapsedString) .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } } Spacer() Button(action: onStop) { Image(systemName: "stop.fill") .font(.title3) .foregroundStyle(.white) .frame(width: 40, height: 40) .background(Color.brandPrimary, in: Circle()) } .buttonStyle(.plain) } .padding(12) .background(Color.brandPrimary.opacity(0.12), in: RoundedRectangle(cornerRadius: 14)) } private var elapsedString: String { let seconds = Int(Date.now.timeIntervalSince(session.startTime)) let h = seconds / 3600 let m = (seconds % 3600) / 60 let s = seconds % 60 return String(format: "%02d:%02d:%02d", h, m, s) } } // MARK: - Action tile private struct ActionTile: View { let action: Action let isTracking: Bool let onTap: () -> Void private var tileColor: Color { Color(hex: action.tags.first?.colorHex ?? "#2F6B4F") } var body: some View { Button(action: onTap) { VStack(spacing: 10) { Image(systemName: action.iconName) .font(.system(size: 30)) Text(action.name) .font(.subheadline.weight(.semibold)) .lineLimit(1) if action.type == .time && isTracking { Text("진행 중") .font(.caption2.weight(.bold)) } } .foregroundStyle(.white) .frame(maxWidth: .infinity) .frame(height: 120) .background(tileColor, in: RoundedRectangle(cornerRadius: 18)) .overlay { if isTracking { RoundedRectangle(cornerRadius: 18) .strokeBorder(.white, lineWidth: 3) } } } .buttonStyle(.plain) } } // MARK: - Color helpers extension Color { /// App primary green (adapts light/dark via asset-free literal fallback). static let brandPrimary = Color(hex: "#2F6B4F") /// Builds a `Color` from a `#RRGGBB` (or `RRGGBB`) hex string. init(hex: String) { let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var value: UInt64 = 0 Scanner(string: cleaned).scanHexInt64(&value) let r, g, b, a: Double switch cleaned.count { case 6: r = Double((value & 0xFF0000) >> 16) / 255 g = Double((value & 0x00FF00) >> 8) / 255 b = Double(value & 0x0000FF) / 255 a = 1 case 8: r = Double((value & 0xFF000000) >> 24) / 255 g = Double((value & 0x00FF0000) >> 16) / 255 b = Double((value & 0x0000FF00) >> 8) / 255 a = Double(value & 0x000000FF) / 255 default: r = 0.18; g = 0.42; b = 0.31; a = 1 // fallback to primary green } self.init(.sRGB, red: r, green: g, blue: b, opacity: a) } } #Preview { MainDashboardView() .modelContainer(for: [Action.self, Tag.self, TimeSession.self, CountEntry.self], inMemory: true) }