Compare commits

...

4 Commits

5 changed files with 111 additions and 86 deletions

View File

@ -110,7 +110,11 @@ struct ContentView: View {
OnboardingView {
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) {
@ -137,7 +141,7 @@ struct ContentView: View {
guard shouldShowKeepAwakePromptForTab() else { return }
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 {

View File

@ -24,30 +24,34 @@ struct AlarmView: View {
ZStack {
AppSurface.primary.ignoresSafeArea()
Group {
if viewModel.alarms.isEmpty {
VStack(spacing: Design.Spacing.large) {
if !isKeepAwakeEnabled {
AlarmLimitationsBanner()
}
EmptyAlarmsView {
showAddAlarm = true
}
.contentShape(Rectangle())
.onTapGesture {
showAddAlarm = true
}
}
.frame(maxWidth: Design.Size.maxContentWidthPortrait)
.frame(maxWidth: .infinity, alignment: .center)
} else {
ScrollView {
VStack(spacing: Design.Spacing.medium) {
GeometryReader { geometry in
let isLandscape = geometry.size.width > geometry.size.height
let maxWidth = isLandscape ? Design.Size.maxContentWidthLandscape : Design.Size.maxContentWidthPortrait
Group {
if viewModel.alarms.isEmpty {
VStack(spacing: Design.Spacing.large) {
if !isKeepAwakeEnabled {
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
AlarmRowView(
alarm: alarm,
@ -58,16 +62,25 @@ struct AlarmView: View {
},
onEdit: {
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)
.padding(.top, Design.Spacing.large)
.listStyle(.plain)
.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")

View File

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

View File

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

View File

@ -24,7 +24,6 @@ struct OnboardingView: View {
@State private var currentPage = 0
@State private var alarmKitPermissionGranted = false
@State private var keepAwakeEnabled = false
@State private var showCelebration = false
private let totalPages = 4
@ -59,11 +58,8 @@ struct OnboardingView: View {
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.bottom, Design.Spacing.xxLarge)
}
// Celebration overlay
if showCelebration {
celebrationOverlay
}
.frame(maxWidth: Design.Size.maxContentWidthPortrait)
.frame(maxWidth: .infinity, alignment: .center)
}
}
@ -119,9 +115,9 @@ struct OnboardingView: View {
Text(text)
.typography(.body)
.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)
}
@ -225,9 +221,9 @@ struct OnboardingView: View {
Text(text)
.typography(.body)
.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)
}
@ -317,6 +313,8 @@ struct OnboardingView: View {
.typography(.callout)
.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)
}
@ -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
private func requestAlarmKitPermission() {
@ -447,13 +421,8 @@ struct OnboardingView: View {
}
private func triggerCelebration() {
withAnimation(.spring(duration: 0.4)) {
showCelebration = true
}
// Dismiss after short celebration
Task {
try? await Task.sleep(for: .milliseconds(1200))
// Snappier transition for a better feel
withAnimation(.spring(duration: 0.45, bounce: 0.2)) {
onComplete()
}
}