diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj index bf91817..f099972 100644 --- a/DynamicIsland.xcodeproj/project.pbxproj +++ b/DynamicIsland.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 7E8840DB2E9A347800C5CB8C /* Orange Cat Peeping.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E8840DA2E9A347800C5CB8C /* Orange Cat Peeping.json */; }; 7E9911B02EFD7D1B002EDFD5 /* Reindeer.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E9911AF2EFD7D1B002EDFD5 /* Reindeer.json */; }; 7E9918722EFDAE5B002EDFD5 /* MediaRemoteAdapter.framework in Resources */ = {isa = PBXBuildFile; fileRef = F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */; }; + 7ECFE0C62F4A2323000DD9BD /* MijickCalendarView in Frameworks */ = {isa = PBXBuildFile; productRef = 7ECFE0C52F4A2323000DD9BD /* MijickCalendarView */; }; 7EE1D73D2E6B1E0B00997219 /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */; }; 7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73E2E6B1E1200997219 /* IOKit.framework */; }; 7EF1D4B92FFFCE3C008D2C32 /* ExtensionEventBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF1D4B82FFFCE3C008D2C32 /* ExtensionEventBridge.swift */; }; @@ -71,6 +72,7 @@ 7E6118172E24071E0050006C /* Collections in Frameworks */, 7E6118162E24071E0050006C /* KeyboardShortcuts in Frameworks */, 7E6118152E24071E0050006C /* Sparkle in Frameworks */, + 7ECFE0C62F4A2323000DD9BD /* MijickCalendarView in Frameworks */, 7E288D122F1659EF0020C117 /* AtollExtensionKit in Frameworks */, 7E6118142E24071E0050006C /* LaunchAtLogin in Frameworks */, ); @@ -171,6 +173,7 @@ 146BC4362D366E9B0047C66A /* MacroVisionKit */, 7E61181E2E24071E0050006C /* SkyLightWindow */, 7E288D112F1659EF0020C117 /* AtollExtensionKit */, + 7ECFE0C52F4A2323000DD9BD /* MijickCalendarView */, ); productName = DynamicIsland; productReference = F54406E72CD4987A00C7AFD9 /* Atoll.app */; @@ -216,6 +219,7 @@ 7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */, 7E09D5962EB7B497005AF41D /* XCRemoteSwiftPackageReference "open-meteo" */, 7E288D102F1659EF0020C117 /* XCRemoteSwiftPackageReference "AtollExtensionKit" */, + 7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */, ); preferredProjectObjectVersion = 77; productRefGroup = F54406E82CD4987A00C7AFD9 /* Products */; @@ -589,6 +593,14 @@ kind = branch; }; }; + 7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Mijick/CalendarView"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.1; + }; + }; 9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/EmergeTools/Pow"; @@ -665,6 +677,11 @@ package = 7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */; productName = SkyLightWindow; }; + 7ECFE0C52F4A2323000DD9BD /* MijickCalendarView */ = { + isa = XCSwiftPackageProductDependency; + package = 7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */; + productName = MijickCalendarView; + }; 9A987A0F2C73CA8D005CA465 /* Collections */ = { isa = XCSwiftPackageProductDependency; package = F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */; diff --git a/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 37ec1ab..7f72980 100644 --- a/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "6fbfa8cda0c383746edd66361426fdc2bd266fc082867294374a1818f02798f3", + "originHash" : "802e15fea0dccd2d325dc082d8d36162007adcd2fa301070eb26448d0a2cf6c5", "pins" : [ { "identity" : "atollextensionkit", @@ -10,6 +10,15 @@ "revision" : "ebe35277572a349fad26abeb6fc552cb5251d584" } }, + { + "identity" : "calendarview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Mijick/CalendarView", + "state" : { + "revision" : "168cf14a50d33684caa2a411bbd0b3f988e9dff6", + "version" : "1.1.1" + } + }, { "identity" : "defaults", "kind" : "remoteSourceControl", diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 1792bb1..9d3475b 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -1592,7 +1592,7 @@ } }, "±10 Seconds" : { - "comment" : "Name of the music skip behavior that skips the current track within ±10 seconds.", + "comment" : "Displayed as the name of the music skip behavior option.", "isCommentAutoGenerated" : true }, "0" : { @@ -1912,7 +1912,7 @@ } }, "Accent" : { - "comment" : "Name of the tint mode for the caps lock indicator that uses the accent color.", + "comment" : "Display name for the \"Accent\" caps lock indicator tint mode.", "isCommentAutoGenerated" : true }, "Accent color" : { @@ -3854,7 +3854,7 @@ } }, "Apple Music" : { - "comment" : "Names for the different media player types available in the app.", + "comment" : "Localized name for \"Now Playing\" media controller type.", "isCommentAutoGenerated" : true }, "Applies the notch-style parallax effect to the lock screen media widget album art." : { @@ -4269,7 +4269,7 @@ } }, "Bar" : { - "comment" : "Localized name for \"Bar\" timer progress style.", + "comment" : "Localized name for the \"Bar\" timer progress style.", "isCommentAutoGenerated" : true }, "Battery" : { @@ -8512,7 +8512,7 @@ } }, "Digital" : { - "comment" : "Name for the \"Digital\" style of reminder presentation.", + "comment" : "The display name for the \"Digital\" reminder presentation style.", "isCommentAutoGenerated" : true }, "Dimensions" : { @@ -11886,7 +11886,7 @@ } }, "Green" : { - "comment" : "Display name for the \"Green\" caps lock indicator tint mode.", + "comment" : "Name of the \"Green\" Caps Lock Indicator Tint Mode.", "isCommentAutoGenerated" : true }, "Height:" : { @@ -18135,7 +18135,7 @@ } }, "Panel" : { - "comment" : "Display name for the \"Panel\" clipboard display mode.", + "comment" : "Name for the \"Panel\" clipboard display mode.", "isCommentAutoGenerated" : true }, "Panel mode shows clipboard in a floating window near the notch." : { @@ -21615,7 +21615,7 @@ } }, "Shows screen assistant as a dropdown attached to the AI button" : { - "comment" : "Description of the \"Popover\" screen assistant display mode.", + "comment" : "Description of the \"Popover\" display mode for the screen assistant.", "isCommentAutoGenerated" : true }, "Shows screen assistant in a floating panel near the notch" : { @@ -21685,7 +21685,7 @@ } }, "Skip forward or backward by ten seconds" : { - "comment" : "Description of the \"Skip forward or backward by ten seconds\" music skip behavior option.", + "comment" : "Description of a music skip behavior where the user can skip forward or backward by ten seconds.", "isCommentAutoGenerated" : true }, "Slider color" : { @@ -22256,7 +22256,7 @@ } }, "Spotify" : { - "comment" : "Name of a media controller type for Spotify.", + "comment" : "Add more media player types here.", "isCommentAutoGenerated" : true }, "Square" : { @@ -22369,7 +22369,7 @@ }, "Standard previous/next track controls" : { - "comment" : "Description of a music skip behavior option that uses standard previous/next track controls.", + "comment" : "Description of a music skip behavior option that skips to the previous or next track.", "isCommentAutoGenerated" : true }, "Start" : { @@ -23721,7 +23721,7 @@ "shouldTranslate" : false }, "Track Skip" : { - "comment" : "Display name for the \"Track Skip\" music skip behavior option.", + "comment" : "Display name for the \"Track Skip\" music skip behavior.", "isCommentAutoGenerated" : true }, "Transform" : { @@ -25635,7 +25635,7 @@ "isCommentAutoGenerated" : true }, "Youtube Music" : { - "comment" : "Name for the \"Youtube Music\" media controller type in settings.", + "comment" : "Name for the media controller type that links to YouTube Music.", "isCommentAutoGenerated" : true }, "YouTube Music requires this third-party app to be installed: " : { diff --git a/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift b/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift index 36fe0ff..7211240 100644 --- a/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift +++ b/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift @@ -251,6 +251,290 @@ struct CalendarView: View { } } +struct StandaloneCalendarView: View { + @EnvironmentObject var vm: DynamicIslandViewModel + @ObservedObject private var calendarManager = CalendarManager.shared + @State private var selectedDate = Date() + @State private var displayedMonth = Date() + @State private var datePickerScrollTarget: Date? + @Default(.hideAllDayEvents) private var hideAllDayEvents + @Default(.hideCompletedReminders) private var hideCompletedReminders + + private let calendar = Calendar.current + private let weekdaySymbols = Calendar.current.shortWeekdaySymbols + + private var monthTitle: String { + displayedMonth.formatted(.dateTime.month(.wide)) + } + + private var yearTitle: String { + displayedMonth.formatted(.dateTime.year()) + } + + private var monthDays: [Date] { + guard let monthInterval = calendar.dateInterval(of: .month, for: displayedMonth), + let firstWeekInterval = calendar.dateInterval(of: .weekOfMonth, for: monthInterval.start), + let lastDay = calendar.date(byAdding: .day, value: -1, to: monthInterval.end), + let lastWeekInterval = calendar.dateInterval(of: .weekOfMonth, for: lastDay) + else { return [] } + + var days: [Date] = [] + var current = firstWeekInterval.start + while current < lastWeekInterval.end { + days.append(current) + guard let next = calendar.date(byAdding: .day, value: 1, to: current) else { break } + current = next + } + return days + } + + private var filteredEvents: [EventModel] { + EventListView.filteredEvents( + events: calendarManager.events, + hideCompletedReminders: hideCompletedReminders, + hideAllDayEvents: hideAllDayEvents + ) + } + + private var resolvedNotchHeight: CGFloat { + let height = vm.notchSize.height + return height > 0 ? height : openNotchSize.height + } + + private var headerHeight: CGFloat { + max(24, vm.effectiveClosedNotchHeight) + } + + private var maxTabContentHeight: CGFloat { + let available = resolvedNotchHeight - headerHeight - 36 + return max(130, available) + } + + var body: some View { + GeometryReader { geometry in + let paneSpacing: CGFloat = 12 + let paneWidth = max((geometry.size.width - paneSpacing) / 2, 0) + let paneHeight = max(0, geometry.size.height) + + HStack(alignment: .top, spacing: paneSpacing) { + leftPickerPane + .frame(width: paneWidth, alignment: .topLeading) + .frame(height: paneHeight, alignment: .topLeading) + .layoutPriority(1) + + rightEventsPane + .frame(width: paneWidth, alignment: .topLeading) + .frame(height: paneHeight, alignment: .topLeading) + .layoutPriority(1) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .clipped() + } + .frame(height: maxTabContentHeight) + .clipped() + .onAppear { + selectedDate = Date.now + displayedMonth = selectedDate.startOfMonth + requestDatePickerCenterOnCurrentDate() + Task { + await calendarManager.updateCurrentDate(selectedDate) + } + } + .onChange(of: selectedDate) { _, newDate in + withAnimation(.smooth(duration: 0.22)) { + displayedMonth = newDate.startOfMonth + } + Task { + await calendarManager.updateCurrentDate(newDate) + } + } + .onChange(of: vm.notchState) { _, newState in + guard newState == .open else { return } + selectedDate = Date.now + displayedMonth = selectedDate.startOfMonth + requestDatePickerCenterOnCurrentDate() + Task { + await calendarManager.updateCurrentDate(selectedDate) + } + } + } + + private var leftPickerPane: some View { + GeometryReader { geometry in + let pickerViewportHeight = max(96, geometry.size.height - 56) + + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .center) { + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(monthTitle) + .font(.title3) + .fontWeight(.semibold) + .foregroundStyle(.white) + Text(yearTitle) + .font(.subheadline) + .fontWeight(.medium) + .foregroundStyle(Color(white: 0.65)) + } + Spacer() + HStack(spacing: 6) { + Button(action: showPreviousMonth) { + Image(systemName: "chevron.left") + .font(.system(size: 11, weight: .bold)) + .frame(width: 24, height: 24) + } + .buttonStyle(.plain) + + Button(action: showNextMonth) { + Image(systemName: "chevron.right") + .font(.system(size: 11, weight: .bold)) + .frame(width: 24, height: 24) + } + .buttonStyle(.plain) + } + .foregroundStyle(.white) + } + + ScrollViewReader { proxy in + VStack(spacing: 6) { + LazyVGrid(columns: Array(repeating: GridItem(.flexible(minimum: 14), spacing: 6), count: 7), spacing: 6) { + ForEach(weekdaySymbols, id: \.self) { symbol in + Text(symbol.prefix(1)) + .font(.caption2) + .fontWeight(.semibold) + .foregroundStyle(Color(white: 0.55)) + .frame(maxWidth: .infinity) + } + } + + ZStack { + ScrollView(.vertical, showsIndicators: false) { + LazyVGrid(columns: Array(repeating: GridItem(.flexible(minimum: 14), spacing: 6), count: 7), spacing: 6) { + ForEach(monthDays, id: \.self) { day in + dayCell(for: day) + .id(calendar.startOfDay(for: day)) + } + } + .padding(.bottom, 2) + } + .onChange(of: datePickerScrollTarget) { _, target in + guard let target else { return } + centerDatePicker(on: target, proxy: proxy) + } + + LinearGradient(colors: [Color.black.opacity(0.65), .clear], startPoint: .top, endPoint: .bottom) + .frame(height: 16) + .allowsHitTesting(false) + .frame(maxHeight: .infinity, alignment: .top) + + LinearGradient(colors: [.clear, Color.black.opacity(0.65)], startPoint: .top, endPoint: .bottom) + .frame(height: 16) + .allowsHitTesting(false) + .frame(maxHeight: .infinity, alignment: .bottom) + } + .frame(height: max(0, pickerViewportHeight - 22)) + .clipped() + } + .frame(height: pickerViewportHeight) + } + .frame(height: pickerViewportHeight) + .clipped() + } + .frame(maxHeight: .infinity, alignment: .top) + } + .padding(.horizontal, 6) + .padding(.top, 4) + .clipped() + } + + private var rightEventsPane: some View { + Group { + if filteredEvents.isEmpty { + EmptyEventsView(selectedDate: selectedDate) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + StandaloneEventCardList( + events: filteredEvents, + showFullEventTitles: Defaults[.showFullEventTitles], + onToggleReminder: { reminderID, completed in + Task { + await calendarManager.setReminderCompleted(reminderID: reminderID, completed: completed) + } + } + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + } + + private func dayCell(for day: Date) -> some View { + let isCurrentMonth = calendar.isDate(day, equalTo: displayedMonth, toGranularity: .month) + let isSelected = calendar.isDate(day, inSameDayAs: selectedDate) + let isToday = calendar.isDateInToday(day) + + return Button { + withAnimation(.smooth(duration: 0.18)) { + selectedDate = day + } + } label: { + ZStack { + if isSelected { + Circle() + .fill(Color.effectiveAccent) + .frame(width: 28, height: 28) + } + + Text(day.formatted(.dateTime.day())) + .font(.system(size: 12, weight: isSelected ? .semibold : .medium)) + .foregroundStyle(dayTextColor(isCurrentMonth: isCurrentMonth, isSelected: isSelected, isToday: isToday)) + } + .frame(maxWidth: .infinity, minHeight: 30) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + private func dayTextColor(isCurrentMonth: Bool, isSelected: Bool, isToday: Bool) -> Color { + if isSelected { return .white } + if !isCurrentMonth { return Color(white: 0.35) } + if isToday { return Color.effectiveAccent } + return .white + } + + private func showPreviousMonth() { + guard let newMonth = calendar.date(byAdding: .month, value: -1, to: displayedMonth) else { return } + withAnimation(.smooth(duration: 0.22)) { + displayedMonth = newMonth.startOfMonth + selectedDate = newMonth.startOfMonth + } + } + + private func showNextMonth() { + guard let newMonth = calendar.date(byAdding: .month, value: 1, to: displayedMonth) else { return } + withAnimation(.smooth(duration: 0.22)) { + displayedMonth = newMonth.startOfMonth + selectedDate = newMonth.startOfMonth + } + } + + private func requestDatePickerCenterOnCurrentDate() { + datePickerScrollTarget = calendar.startOfDay(for: selectedDate) + } + + private func centerDatePicker(on target: Date, proxy: ScrollViewProxy) { + let normalizedTarget = calendar.startOfDay(for: target) + DispatchQueue.main.async { + withAnimation(.smooth(duration: 0.24)) { + proxy.scrollTo(normalizedTarget, anchor: .center) + } + if datePickerScrollTarget == normalizedTarget { + datePickerScrollTarget = nil + } + } + } +} + struct EmptyEventsView: View { let selectedDate: Date @@ -269,6 +553,170 @@ struct EmptyEventsView: View { } } +private extension Date { + var startOfMonth: Date { + Calendar.current.date(from: Calendar.current.dateComponents([.year, .month], from: self)) ?? self + } +} + +private struct StandaloneEventCardList: View { + @Environment(\.openURL) private var openURL + let events: [EventModel] + let showFullEventTitles: Bool + let onToggleReminder: (String, Bool) -> Void + + var body: some View { + ZStack { + ScrollView { + LazyVStack(spacing: 8) { + ForEach(events) { event in + eventCard(event) + } + } + .padding(.vertical, 2) + } + .clipped() + + LinearGradient(colors: [Color.black.opacity(0.65), .clear], startPoint: .top, endPoint: .bottom) + .frame(height: 16) + .allowsHitTesting(false) + .frame(maxHeight: .infinity, alignment: .top) + + LinearGradient(colors: [.clear, Color.black.opacity(0.65)], startPoint: .top, endPoint: .bottom) + .frame(height: 16) + .allowsHitTesting(false) + .frame(maxHeight: .infinity, alignment: .bottom) + } + .clipped() + } + + @ViewBuilder + private func eventCard(_ event: EventModel) -> some View { + if event.type.isReminder { + Button { + if let url = event.calendarAppURL() { + openURL(url) + } + } label: { + reminderCard(event) + } + .buttonStyle(.plain) + .padding(.horizontal, 2) + } else { + calendarEventCard(event) + .contentShape(Rectangle()) + .onTapGesture { + if let url = event.calendarAppURL() { + openURL(url) + } + } + .padding(.horizontal, 2) + } + } + + private func reminderCard(_ event: EventModel) -> some View { + let isCompleted: Bool + if case .reminder(let completed) = event.type { + isCompleted = completed + } else { + isCompleted = false + } + + return HStack(spacing: 10) { + ReminderToggle( + isOn: Binding( + get: { isCompleted }, + set: { newValue in onToggleReminder(event.id, newValue) } + ), + color: Color(event.calendar.color) + ) + + VStack(alignment: .leading, spacing: 4) { + Text(event.title) + .font(.callout) + .fontWeight(.medium) + .foregroundColor(.white) + .lineLimit(showFullEventTitles ? nil : 2) + + if event.isAllDay { + Text("All-day") + .font(.caption) + .foregroundColor(Color(white: 0.65)) + } + } + + Spacer(minLength: 8) + + if !event.isAllDay { + Text(event.start, style: .time) + .font(.caption) + .foregroundColor(Color(white: 0.75)) + } + } + .padding(.horizontal, 11) + .padding(.vertical, 8) + .frame(maxWidth: .infinity, minHeight: 50, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color.white.opacity(0.05)) + ) + .opacity(isCompleted ? 0.55 : 1) + } + + private func calendarEventCard(_ event: EventModel) -> some View { + HStack(alignment: .top, spacing: 10) { + RoundedRectangle(cornerRadius: 2) + .fill(Color(event.calendar.color)) + .frame(width: 4, height: 36) + + VStack(alignment: .leading, spacing: 4) { + Text(event.title) + .font(.callout) + .fontWeight(.semibold) + .foregroundColor(.white) + .lineLimit(showFullEventTitles ? nil : 2) + + if let location = event.location, !location.isEmpty { + Text(location) + .font(.caption) + .foregroundColor(Color(white: 0.65)) + .lineLimit(1) + } + + if let conferenceURL = event.conferenceURL { + ConferenceJoinButton(url: conferenceURL, event: event) + } + } + + Spacer(minLength: 8) + + VStack(alignment: .trailing, spacing: 2) { + if event.isAllDay { + Text("All-day") + .font(.caption) + .fontWeight(.medium) + .foregroundColor(.white) + } else { + Text(event.start, style: .time) + .font(.caption) + .foregroundColor(.white) + Text(event.end, style: .time) + .font(.caption2) + .foregroundColor(Color(white: 0.65)) + } + } + } + .padding(.horizontal, 11) + .padding(.vertical, 8) + .frame(maxWidth: .infinity, minHeight: 50, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color.white.opacity(0.05)) + ) + .opacity(event.eventStatus == .ended && Calendar.current.isDateInToday(event.start) ? 0.6 : 1) + } +} + struct EventListView: View { @Environment(\.openURL) private var openURL @ObservedObject private var calendarManager = CalendarManager.shared diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index 7e68b84..1b3be04 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -552,11 +552,18 @@ struct NotchHomeView: View { } if Defaults[.showCalendar] { - CalendarView() - .onHover { isHovering in - vm.isHoveringCalendar = isHovering + Group { + if showStandardMediaControls { + CalendarView() + } else { + StandaloneCalendarView() } - .environmentObject(vm) + } + .frame(maxWidth: .infinity, alignment: .leading) + .onHover { isHovering in + vm.isHoveringCalendar = isHovering + } + .environmentObject(vm) } if Defaults[.showMirror],