mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Fix: Calendar too wide when music is disabled
This commit is contained in:
parent
53a9fdb112
commit
4ec5c84b9c
5 changed files with 499 additions and 18 deletions
|
|
@ -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" */;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: " : {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue