Skip to content
3 changes: 3 additions & 0 deletions boringNotch/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -372,10 +372,13 @@ struct ContentView: View {
switch coordinator.currentView {
case .home:
NotchHomeView(albumArtNamespace: albumArtNamespace)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
case .shelf:
ShelfView()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.transition(
.scale(scale: 0.8, anchor: .top)
.combined(with: .opacity)
Expand Down
191 changes: 156 additions & 35 deletions boringNotch/components/Calendar/BoringCalendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
import Defaults
import SwiftUI

enum CalendarLayoutStyle {
case compact
case monthAndEvents
}

struct Config: Equatable {
// var count: Int = 10 // 3 days past + today + 7 days future
var past: Int = 7
Expand Down Expand Up @@ -233,46 +238,61 @@ struct CalendarView: View {
@EnvironmentObject var vm: BoringViewModel
@ObservedObject private var calendarManager = CalendarManager.shared
@State private var selectedDate = Date()
var layoutStyle: CalendarLayoutStyle = .compact

var body: some View {
VStack(spacing: 0) {
HStack(alignment: .center, spacing: 8) {
VStack(alignment: .leading) {
Text(selectedDate.formatted(.dateTime.month(.abbreviated)))
.font(.title3)
.fontWeight(.semibold)
.foregroundColor(.white)
Text(selectedDate.formatted(.dateTime.year()))
.font(.title3)
.fontWeight(.light)
.foregroundColor(Color(white: 0.65))
}
Group {
switch layoutStyle {
case .compact:
VStack(spacing: 0) {
HStack(alignment: .center, spacing: 8) {
VStack(alignment: .leading) {
Text(selectedDate.formatted(.dateTime.month(.abbreviated)))
.font(.title3)
.fontWeight(.semibold)
.foregroundColor(.white)
Text(selectedDate.formatted(.dateTime.year()))
.font(.title3)
.fontWeight(.light)
.foregroundColor(Color(white: 0.65))
}

ZStack(alignment: .top) {
WheelPicker(selectedDate: $selectedDate, config: Config())
HStack(alignment: .top) {
LinearGradient(
colors: [Color.black, .clear], startPoint: .leading, endPoint: .trailing
)
.frame(width: 20)
Spacer()
LinearGradient(
colors: [.clear, Color.black], startPoint: .leading, endPoint: .trailing
)
.frame(width: 20)
ZStack(alignment: .top) {
WheelPicker(selectedDate: $selectedDate, config: Config())
HStack(alignment: .top) {
LinearGradient(
colors: [Color.black, .clear], startPoint: .leading, endPoint: .trailing
)
.frame(width: 20)
Spacer()
LinearGradient(
colors: [.clear, Color.black], startPoint: .leading, endPoint: .trailing
)
.frame(width: 20)
}
}
}
.fixedSize(horizontal: false, vertical: true)

eventsSection
}
}
.fixedSize(horizontal: false, vertical: true)
case .monthAndEvents:
GeometryReader { geometry in
let monthWidth: CGFloat = 220
let columnSpacing: CGFloat = 12
let eventsWidth = max(0, geometry.size.width - monthWidth - columnSpacing)

let filteredEvents = EventListView.filteredEvents(
events: calendarManager.events
)
if filteredEvents.isEmpty {
EmptyEventsView(selectedDate: selectedDate)
.frame(maxHeight: .infinity, alignment: .center)
} else {
EventListView(events: calendarManager.events)
HStack(alignment: .top, spacing: columnSpacing) {
FullMonthCalendarView(selectedDate: $selectedDate)
.frame(width: monthWidth)

eventsSection
.frame(width: eventsWidth, alignment: .topLeading)
.frame(maxHeight: .infinity, alignment: .topLeading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.onChange(of: selectedDate) {
Expand All @@ -293,6 +313,104 @@ struct CalendarView: View {
}
}
}

@ViewBuilder
private var eventsSection: some View {
if filteredEvents.isEmpty {
EmptyEventsView(selectedDate: selectedDate)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
} else {
EventListView(events: calendarManager.events)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}

private var filteredEvents: [EventModel] {
EventListView.filteredEvents(events: calendarManager.events)
}
}

struct FullMonthCalendarView: View {
@Binding var selectedDate: Date
private let calendar = Calendar.current
private let dayColumns = Array(repeating: GridItem(.flexible(minimum: 22), spacing: 4), count: 7)

var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(selectedDate.formatted(.dateTime.month(.abbreviated).year()))
.font(.headline)
.foregroundColor(.white)

LazyVGrid(columns: dayColumns, spacing: 4) {
ForEach(shortWeekdaySymbols, id: \.self) { symbol in
Text(symbol)
.font(.caption2)
.foregroundColor(Color(white: 0.65))
.frame(maxWidth: .infinity)
}

ForEach(Array(monthGridDates.enumerated()), id: \.offset) { _, value in
if let day = value {
dayButton(day)
} else {
Color.clear
.frame(height: 22)
}
}
}
}
}

private var shortWeekdaySymbols: [String] {
let formatter = DateFormatter()
formatter.locale = .current
let symbols = formatter.veryShortStandaloneWeekdaySymbols ?? formatter.shortStandaloneWeekdaySymbols ?? ["S", "M", "T", "W", "T", "F", "S"]
guard !symbols.isEmpty else { return ["S", "M", "T", "W", "T", "F", "S"] }

let shift = max(0, min(calendar.firstWeekday - 1, symbols.count - 1))
let head = Array(symbols[shift...])
let tail = Array(symbols[..<shift])
return head + tail
}

private var monthGridDates: [Date?] {
guard let monthInterval = calendar.dateInterval(of: .month, for: selectedDate),
let monthRange = calendar.range(of: .day, in: .month, for: selectedDate)
else {
return []
}

let firstDay = monthInterval.start
let weekday = calendar.component(.weekday, from: firstDay)
let firstWeekdayIndex = (weekday - calendar.firstWeekday + 7) % 7

var days: [Date?] = Array(repeating: nil, count: firstWeekdayIndex)
for day in monthRange {
if let date = calendar.date(byAdding: .day, value: day - 1, to: firstDay) {
days.append(date)
}
}

return days
}

private func dayButton(_ day: Date) -> some View {
let isSelected = calendar.isDate(day, inSameDayAs: selectedDate)
let isToday = calendar.isDateInToday(day)

return Button {
selectedDate = day
} label: {
Text("\(calendar.component(.day, from: day))")
.font(.caption)
.fontWeight(.medium)
.foregroundColor(isSelected ? .white : Color(white: isToday ? 0.95 : 0.7))
.frame(maxWidth: .infinity, minHeight: 22)
.background(isSelected ? Color.effectiveAccent : .clear)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
.buttonStyle(PlainButtonStyle())
}
}

struct EmptyEventsView: View {
Expand Down Expand Up @@ -381,14 +499,15 @@ struct EventListView: View {
.scrollIndicators(.never)
.scrollContentBackground(.hidden)
.background(Color.clear)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.onAppear {
scrollToRelevantEvent(proxy: proxy)
}
.onChange(of: filteredEvents) { _, _ in
scrollToRelevantEvent(proxy: proxy)
}
}
Spacer(minLength: 0)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}

private func eventRow(_ event: EventModel) -> some View {
Expand Down Expand Up @@ -443,6 +562,7 @@ struct EventListView: View {
)
}
.padding(.vertical, 4)
.frame(maxWidth: .infinity, alignment: .leading)
)
} else {
return AnyView(
Expand Down Expand Up @@ -487,6 +607,7 @@ struct EventListView: View {
.opacity(
event.eventStatus == .ended && Calendar.current.isDateInToday(event.start)
? 0.6 : 1.0)
.frame(maxWidth: .infinity, alignment: .leading)
)
}
}
Expand Down
82 changes: 64 additions & 18 deletions boringNotch/components/Notch/NotchHomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,8 @@ struct NotchHomeView: View {
@ObservedObject var webcamManager = WebcamManager.shared
@ObservedObject var batteryModel = BatteryStatusViewModel.shared
@ObservedObject var coordinator = BoringViewCoordinator.shared
@ObservedObject var musicManager = MusicManager.shared
@Default(.musicPlayerVisibilityMode) private var musicPlayerVisibilityMode
let albumArtNamespace: Namespace.ID

var body: some View {
Expand All @@ -424,34 +426,78 @@ struct NotchHomeView: View {
}
// simplified: use a straightforward opacity transition
.transition(.opacity)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}

private var shouldShowCamera: Bool {
Defaults[.showMirror] && webcamManager.cameraAvailable && vm.isCameraExpanded
}

private var shouldShowMusicPlayer: Bool {
switch musicPlayerVisibilityMode {
case .always:
return true
case .onlyWhenPlaying:
return musicManager.isPlaying
case .never:
return false
}
}

private var shouldUseMonthAndEventsCalendar: Bool {
!shouldShowMusicPlayer && Defaults[.showCalendar] && !shouldShowCamera
}

private var shouldUseExpandedHomeLayout: Bool {
!shouldShowMusicPlayer
}

private var compactCameraWidth: CGFloat {
max(150, min(220, vm.notchSize.height - 24))
}

private var mainContent: some View {
HStack(alignment: .top, spacing: (shouldShowCamera && Defaults[.showCalendar]) ? 10 : 15) {
MusicPlayerView(albumArtNamespace: albumArtNamespace)

if Defaults[.showCalendar] {
CalendarView()
.frame(width: shouldShowCamera ? 170 : 215)
.onHover { isHovering in
vm.isHoveringCalendar = isHovering
}
.environmentObject(vm)
.transition(.opacity)
}
GeometryReader { geometry in
let spacing: CGFloat = (shouldShowCamera && Defaults[.showCalendar]) ? 10 : 15
let shouldExpandCalendarForCamera = !shouldShowMusicPlayer && Defaults[.showCalendar] && shouldShowCamera
let calendarWidth: CGFloat? = {
guard Defaults[.showCalendar] else { return nil }
if shouldUseMonthAndEventsCalendar {
return geometry.size.width
}
if shouldExpandCalendarForCamera {
return max(220, geometry.size.width - compactCameraWidth - spacing)
}
return (shouldShowMusicPlayer && shouldShowCamera) ? 170 : 215
}()

HStack(alignment: .top, spacing: spacing) {
if shouldShowMusicPlayer {
MusicPlayerView(albumArtNamespace: albumArtNamespace)
}

if shouldShowCamera {
CameraPreviewView(webcamManager: webcamManager)
.scaledToFit()
.opacity(vm.notchState == .closed ? 0 : 1)
.blur(radius: vm.notchState == .closed ? 20 : 0)
.animation(.interactiveSpring(response: 0.32, dampingFraction: 0.76, blendDuration: 0), value: shouldShowCamera)
if Defaults[.showCalendar] {
CalendarView(layoutStyle: shouldUseMonthAndEventsCalendar ? .monthAndEvents : .compact)
.frame(width: calendarWidth, alignment: .leading)
.onHover { isHovering in
vm.isHoveringCalendar = isHovering
}
.environmentObject(vm)
.transition(.opacity)
}

if shouldShowCamera {
CameraPreviewView(webcamManager: webcamManager)
.scaledToFit()
.frame(width: shouldShowMusicPlayer ? nil : compactCameraWidth, alignment: .topLeading)
.opacity(vm.notchState == .closed ? 0 : 1)
.blur(radius: vm.notchState == .closed ? 20 : 0)
.animation(.interactiveSpring(response: 0.32, dampingFraction: 0.76, blendDuration: 0), value: shouldShowCamera)
}
}
.frame(maxWidth: shouldUseExpandedHomeLayout ? .infinity : nil, maxHeight: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.transition(.asymmetric(insertion: .opacity.combined(with: .move(edge: .top)), removal: .opacity))
.blur(radius: vm.notchState == .closed ? 30 : 0)
}
Expand Down
Loading