Compare commits

..

4 Commits

5 changed files with 111 additions and 86 deletions

View File

@ -110,7 +110,11 @@ struct ContentView: View {
OnboardingView { OnboardingView {
onboardingState.completeWelcome() onboardingState.completeWelcome()
} }
.transition(.opacity) .transition(.asymmetric(
insertion: .opacity,
removal: .opacity.combined(with: .move(edge: .bottom)).combined(with: .scale(scale: 0.9))
))
.zIndex(1) // Ensure it stays on top during transition
} }
} }
.sheet(isPresented: $keepAwakePromptState.isPresented) { .sheet(isPresented: $keepAwakePromptState.isPresented) {
@ -137,7 +141,7 @@ struct ContentView: View {
guard shouldShowKeepAwakePromptForTab() else { return } guard shouldShowKeepAwakePromptForTab() else { return }
keepAwakePromptState.showIfNeeded(isKeepAwakeEnabled: clockViewModel.style.keepAwake) keepAwakePromptState.showIfNeeded(isKeepAwakeEnabled: clockViewModel.style.keepAwake)
} }
.animation(.easeInOut(duration: 0.3), value: onboardingState.hasCompletedWelcome) .animation(.spring(duration: 0.45, bounce: 0.2), value: onboardingState.hasCompletedWelcome)
} }
private func shouldShowKeepAwakePromptForTab() -> Bool { private func shouldShowKeepAwakePromptForTab() -> Bool {

View File

@ -24,30 +24,34 @@ struct AlarmView: View {
ZStack { ZStack {
AppSurface.primary.ignoresSafeArea() AppSurface.primary.ignoresSafeArea()
Group { GeometryReader { geometry in
if viewModel.alarms.isEmpty { let isLandscape = geometry.size.width > geometry.size.height
VStack(spacing: Design.Spacing.large) { let maxWidth = isLandscape ? Design.Size.maxContentWidthLandscape : Design.Size.maxContentWidthPortrait
if !isKeepAwakeEnabled {
AlarmLimitationsBanner()
}
EmptyAlarmsView { Group {
showAddAlarm = true if viewModel.alarms.isEmpty {
} VStack(spacing: Design.Spacing.large) {
.contentShape(Rectangle())
.onTapGesture {
showAddAlarm = true
}
}
.frame(maxWidth: Design.Size.maxContentWidthPortrait)
.frame(maxWidth: .infinity, alignment: .center)
} else {
ScrollView {
VStack(spacing: Design.Spacing.medium) {
if !isKeepAwakeEnabled { if !isKeepAwakeEnabled {
AlarmLimitationsBanner() AlarmLimitationsBanner()
} }
EmptyAlarmsView {
showAddAlarm = true
}
.contentShape(Rectangle())
.onTapGesture {
showAddAlarm = true
}
}
} else {
List {
if !isKeepAwakeEnabled {
AlarmLimitationsBanner()
.listRowInsets(EdgeInsets(top: Design.Spacing.large, leading: Design.Spacing.large, bottom: Design.Spacing.small, trailing: Design.Spacing.large))
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
ForEach(viewModel.alarms) { alarm in ForEach(viewModel.alarms) { alarm in
AlarmRowView( AlarmRowView(
alarm: alarm, alarm: alarm,
@ -58,16 +62,25 @@ struct AlarmView: View {
}, },
onEdit: { onEdit: {
selectedAlarmForEdit = alarm selectedAlarmForEdit = alarm
},
onDelete: {
Task {
await viewModel.deleteAlarm(id: alarm.id)
}
} }
) )
.listRowInsets(EdgeInsets(top: Design.Spacing.small, leading: Design.Spacing.large, bottom: Design.Spacing.small, trailing: Design.Spacing.large))
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
} }
} }
.padding(.horizontal, Design.Spacing.large) .listStyle(.plain)
.padding(.top, Design.Spacing.large) .scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea())
} }
.frame(maxWidth: Design.Size.maxContentWidthPortrait)
.frame(maxWidth: .infinity, alignment: .center)
} }
.frame(maxWidth: maxWidth)
.frame(maxWidth: .infinity, alignment: .center)
} }
} }
.navigationTitle(isPad ? "" : "Alarms") .navigationTitle(isPad ? "" : "Alarms")

View File

@ -16,6 +16,7 @@ struct AlarmRowView: View {
let alarm: Alarm let alarm: Alarm
let onToggle: () -> Void let onToggle: () -> Void
let onEdit: () -> Void let onEdit: () -> Void
let onDelete: () -> Void
@AppStorage(ClockStyle.appStorageKey) private var clockStyleData: Data = Data() @AppStorage(ClockStyle.appStorageKey) private var clockStyleData: Data = Data()
// MARK: - Body // MARK: - Body
@ -65,6 +66,13 @@ struct AlarmRowView: View {
onEdit() onEdit()
} }
} }
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
onDelete()
} label: {
Label("Delete", systemImage: "trash")
}
}
} }
private var isKeepAwakeEnabled: Bool { private var isKeepAwakeEnabled: Bool {
@ -82,7 +90,8 @@ struct AlarmRowView: View {
AlarmRowView( AlarmRowView(
alarm: Alarm(time: Date()), alarm: Alarm(time: Date()),
onToggle: {}, onToggle: {},
onEdit: {} onEdit: {},
onDelete: {}
) )
} }
} }

View File

@ -37,25 +37,50 @@ struct NoiseView: View {
let isLandscape = geometry.size.width > geometry.size.height let isLandscape = geometry.size.width > geometry.size.height
let maxWidth = isLandscape ? Design.Size.maxContentWidthLandscape : Design.Size.maxContentWidthPortrait let maxWidth = isLandscape ? Design.Size.maxContentWidthLandscape : Design.Size.maxContentWidthPortrait
Group { VStack(spacing: 0) {
if isLandscape { // Custom Search Bar - Constrained to maxWidth
// Landscape layout: Player on left, sounds on right HStack {
landscapeLayout HStack(spacing: Design.Spacing.small) {
} else { Image(systemName: "magnifyingglass")
// Portrait layout: Stacked vertically .foregroundColor(AppTextColors.secondary)
portraitLayout
TextField("Search sounds", text: $searchText)
.textFieldStyle(.plain)
.foregroundColor(AppTextColors.primary)
if !searchText.isEmpty {
Button(action: { searchText = "" }) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(AppTextColors.tertiary)
}
}
}
.padding(Design.Spacing.small)
.background(AppSurface.overlay)
.cornerRadius(Design.CornerRadius.medium)
} }
.padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.medium)
.padding(.bottom, Design.Spacing.small)
.frame(maxWidth: maxWidth)
Group {
if isLandscape {
// Landscape layout: Player on left, sounds on right
landscapeLayout
} else {
// Portrait layout: Stacked vertically
portraitLayout
}
}
.frame(maxWidth: maxWidth)
} }
.frame(maxWidth: maxWidth)
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
} }
} }
.navigationTitle("Noise")
.navigationBarTitleDisplayMode(.inline)
.animation(.easeInOut(duration: 0.3), value: selectedSound) .animation(.easeInOut(duration: 0.3), value: selectedSound)
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .automatic),
prompt: "Search sounds"
)
} }
// MARK: - Computed Properties // MARK: - Computed Properties
@ -82,6 +107,7 @@ struct NoiseView: View {
if selectedSound != nil { if selectedSound != nil {
soundControlView soundControlView
.centered() .centered()
.padding(.bottom, Design.Spacing.medium)
} else { } else {
// Placeholder when no sound selected - Enhanced for CRO // Placeholder when no sound selected - Enhanced for CRO
VStack(spacing: Design.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
@ -108,27 +134,31 @@ struct NoiseView: View {
} }
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.xLarge) .padding(.vertical, Design.Spacing.large)
.background(AppSurface.overlay, in: RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge)) .background(AppSurface.overlay, in: RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge))
.overlay( .overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge) RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge)
.stroke(AppBorder.subtle, lineWidth: Design.LineWidth.thin) .stroke(AppBorder.subtle, lineWidth: Design.LineWidth.thin)
) )
.padding(.bottom, Design.Spacing.medium)
} }
} }
.contentPadding(horizontal: Design.Spacing.large) .padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.large) .padding(.top, Design.Spacing.small)
.background(AppSurface.primary) .background(AppSurface.primary)
// Scrollable sound selection // Scrollable sound selection
ScrollView { List {
SoundCategoryView( SoundCategoryView(
sounds: viewModel.availableSounds, sounds: viewModel.availableSounds,
selectedSound: $selectedSound, selectedSound: $selectedSound,
searchText: $searchText searchText: $searchText
) )
.contentPadding(horizontal: Design.Spacing.large, vertical: Design.Spacing.large) .listRowInsets(EdgeInsets(top: 0, leading: Design.Spacing.large, bottom: Design.Spacing.large, trailing: Design.Spacing.large))
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
} }
.listStyle(.plain)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
} }
} }

View File

@ -24,7 +24,6 @@ struct OnboardingView: View {
@State private var currentPage = 0 @State private var currentPage = 0
@State private var alarmKitPermissionGranted = false @State private var alarmKitPermissionGranted = false
@State private var keepAwakeEnabled = false @State private var keepAwakeEnabled = false
@State private var showCelebration = false
private let totalPages = 4 private let totalPages = 4
@ -59,11 +58,8 @@ struct OnboardingView: View {
.padding(.horizontal, Design.Spacing.xLarge) .padding(.horizontal, Design.Spacing.xLarge)
.padding(.bottom, Design.Spacing.xxLarge) .padding(.bottom, Design.Spacing.xxLarge)
} }
.frame(maxWidth: Design.Size.maxContentWidthPortrait)
// Celebration overlay .frame(maxWidth: .infinity, alignment: .center)
if showCelebration {
celebrationOverlay
}
} }
} }
@ -119,9 +115,9 @@ struct OnboardingView: View {
Text(text) Text(text)
.typography(.body) .typography(.body)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
Spacer()
} }
.frame(maxWidth: 320, alignment: .leading) // Constrain width and align content to leading
.frame(maxWidth: .infinity, alignment: .center) // Center the constrained box in the parent
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)
} }
@ -225,9 +221,9 @@ struct OnboardingView: View {
Text(text) Text(text)
.typography(.body) .typography(.body)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
Spacer()
} }
.frame(maxWidth: 320, alignment: .leading) // Constrain width and align content to leading
.frame(maxWidth: .infinity, alignment: .center) // Center the constrained box in the parent
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)
} }
@ -317,6 +313,8 @@ struct OnboardingView: View {
.typography(.callout) .typography(.callout)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
} }
.frame(maxWidth: 320, alignment: .leading) // Constrain width and align content to leading
.frame(maxWidth: .infinity, alignment: .center) // Center the constrained box in the parent
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)
} }
@ -375,30 +373,6 @@ struct OnboardingView: View {
} }
} }
// MARK: - Celebration
private var celebrationOverlay: some View {
ZStack {
Color.black.opacity(0.3)
.ignoresSafeArea()
VStack(spacing: Design.Spacing.large) {
Image(systemName: "party.popper.fill")
.font(.system(size: 60))
.foregroundStyle(AppAccent.primary)
Text("Let's go!")
.typography(.heroBold)
.foregroundStyle(AppTextColors.primary)
}
.padding(Design.Spacing.xxxLarge)
.background(AppSurface.overlay)
.cornerRadius(Design.CornerRadius.xxLarge)
.shadow(radius: 20)
}
.transition(.opacity.combined(with: .scale))
}
// MARK: - Actions // MARK: - Actions
private func requestAlarmKitPermission() { private func requestAlarmKitPermission() {
@ -447,13 +421,8 @@ struct OnboardingView: View {
} }
private func triggerCelebration() { private func triggerCelebration() {
withAnimation(.spring(duration: 0.4)) { // Snappier transition for a better feel
showCelebration = true withAnimation(.spring(duration: 0.45, bounce: 0.2)) {
}
// Dismiss after short celebration
Task {
try? await Task.sleep(for: .milliseconds(1200))
onComplete() onComplete()
} }
} }