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 */; };
|
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 */; };
|
7E9911B02EFD7D1B002EDFD5 /* Reindeer.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E9911AF2EFD7D1B002EDFD5 /* Reindeer.json */; };
|
||||||
7E9918722EFDAE5B002EDFD5 /* MediaRemoteAdapter.framework in Resources */ = {isa = PBXBuildFile; fileRef = F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */; };
|
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 */; };
|
7EE1D73D2E6B1E0B00997219 /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */; };
|
||||||
7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73E2E6B1E1200997219 /* IOKit.framework */; };
|
7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73E2E6B1E1200997219 /* IOKit.framework */; };
|
||||||
7EF1D4B92FFFCE3C008D2C32 /* ExtensionEventBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF1D4B82FFFCE3C008D2C32 /* ExtensionEventBridge.swift */; };
|
7EF1D4B92FFFCE3C008D2C32 /* ExtensionEventBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF1D4B82FFFCE3C008D2C32 /* ExtensionEventBridge.swift */; };
|
||||||
|
|
@ -71,6 +72,7 @@
|
||||||
7E6118172E24071E0050006C /* Collections in Frameworks */,
|
7E6118172E24071E0050006C /* Collections in Frameworks */,
|
||||||
7E6118162E24071E0050006C /* KeyboardShortcuts in Frameworks */,
|
7E6118162E24071E0050006C /* KeyboardShortcuts in Frameworks */,
|
||||||
7E6118152E24071E0050006C /* Sparkle in Frameworks */,
|
7E6118152E24071E0050006C /* Sparkle in Frameworks */,
|
||||||
|
7ECFE0C62F4A2323000DD9BD /* MijickCalendarView in Frameworks */,
|
||||||
7E288D122F1659EF0020C117 /* AtollExtensionKit in Frameworks */,
|
7E288D122F1659EF0020C117 /* AtollExtensionKit in Frameworks */,
|
||||||
7E6118142E24071E0050006C /* LaunchAtLogin in Frameworks */,
|
7E6118142E24071E0050006C /* LaunchAtLogin in Frameworks */,
|
||||||
);
|
);
|
||||||
|
|
@ -171,6 +173,7 @@
|
||||||
146BC4362D366E9B0047C66A /* MacroVisionKit */,
|
146BC4362D366E9B0047C66A /* MacroVisionKit */,
|
||||||
7E61181E2E24071E0050006C /* SkyLightWindow */,
|
7E61181E2E24071E0050006C /* SkyLightWindow */,
|
||||||
7E288D112F1659EF0020C117 /* AtollExtensionKit */,
|
7E288D112F1659EF0020C117 /* AtollExtensionKit */,
|
||||||
|
7ECFE0C52F4A2323000DD9BD /* MijickCalendarView */,
|
||||||
);
|
);
|
||||||
productName = DynamicIsland;
|
productName = DynamicIsland;
|
||||||
productReference = F54406E72CD4987A00C7AFD9 /* Atoll.app */;
|
productReference = F54406E72CD4987A00C7AFD9 /* Atoll.app */;
|
||||||
|
|
@ -216,6 +219,7 @@
|
||||||
7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */,
|
7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */,
|
||||||
7E09D5962EB7B497005AF41D /* XCRemoteSwiftPackageReference "open-meteo" */,
|
7E09D5962EB7B497005AF41D /* XCRemoteSwiftPackageReference "open-meteo" */,
|
||||||
7E288D102F1659EF0020C117 /* XCRemoteSwiftPackageReference "AtollExtensionKit" */,
|
7E288D102F1659EF0020C117 /* XCRemoteSwiftPackageReference "AtollExtensionKit" */,
|
||||||
|
7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = F54406E82CD4987A00C7AFD9 /* Products */;
|
productRefGroup = F54406E82CD4987A00C7AFD9 /* Products */;
|
||||||
|
|
@ -589,6 +593,14 @@
|
||||||
kind = branch;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/Mijick/CalendarView";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.1.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = {
|
9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/EmergeTools/Pow";
|
repositoryURL = "https://github.com/EmergeTools/Pow";
|
||||||
|
|
@ -665,6 +677,11 @@
|
||||||
package = 7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */;
|
package = 7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */;
|
||||||
productName = SkyLightWindow;
|
productName = SkyLightWindow;
|
||||||
};
|
};
|
||||||
|
7ECFE0C52F4A2323000DD9BD /* MijickCalendarView */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 7ECFE0C42F4A2323000DD9BD /* XCRemoteSwiftPackageReference "CalendarView" */;
|
||||||
|
productName = MijickCalendarView;
|
||||||
|
};
|
||||||
9A987A0F2C73CA8D005CA465 /* Collections */ = {
|
9A987A0F2C73CA8D005CA465 /* Collections */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
package = F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"originHash" : "6fbfa8cda0c383746edd66361426fdc2bd266fc082867294374a1818f02798f3",
|
"originHash" : "802e15fea0dccd2d325dc082d8d36162007adcd2fa301070eb26448d0a2cf6c5",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "atollextensionkit",
|
"identity" : "atollextensionkit",
|
||||||
|
|
@ -10,6 +10,15 @@
|
||||||
"revision" : "ebe35277572a349fad26abeb6fc552cb5251d584"
|
"revision" : "ebe35277572a349fad26abeb6fc552cb5251d584"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "calendarview",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/Mijick/CalendarView",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "168cf14a50d33684caa2a411bbd0b3f988e9dff6",
|
||||||
|
"version" : "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "defaults",
|
"identity" : "defaults",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
|
||||||
|
|
@ -1592,7 +1592,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"±10 Seconds" : {
|
"±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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"0" : {
|
"0" : {
|
||||||
|
|
@ -1912,7 +1912,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Accent" : {
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Accent color" : {
|
"Accent color" : {
|
||||||
|
|
@ -3854,7 +3854,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Apple Music" : {
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Applies the notch-style parallax effect to the lock screen media widget album art." : {
|
"Applies the notch-style parallax effect to the lock screen media widget album art." : {
|
||||||
|
|
@ -4269,7 +4269,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Bar" : {
|
"Bar" : {
|
||||||
"comment" : "Localized name for \"Bar\" timer progress style.",
|
"comment" : "Localized name for the \"Bar\" timer progress style.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Battery" : {
|
"Battery" : {
|
||||||
|
|
@ -8512,7 +8512,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Digital" : {
|
"Digital" : {
|
||||||
"comment" : "Name for the \"Digital\" style of reminder presentation.",
|
"comment" : "The display name for the \"Digital\" reminder presentation style.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Dimensions" : {
|
"Dimensions" : {
|
||||||
|
|
@ -11886,7 +11886,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Green" : {
|
"Green" : {
|
||||||
"comment" : "Display name for the \"Green\" caps lock indicator tint mode.",
|
"comment" : "Name of the \"Green\" Caps Lock Indicator Tint Mode.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Height:" : {
|
"Height:" : {
|
||||||
|
|
@ -18135,7 +18135,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Panel" : {
|
"Panel" : {
|
||||||
"comment" : "Display name for the \"Panel\" clipboard display mode.",
|
"comment" : "Name for the \"Panel\" clipboard display mode.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Panel mode shows clipboard in a floating window near the notch." : {
|
"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" : {
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Shows screen assistant in a floating panel near the notch" : {
|
"Shows screen assistant in a floating panel near the notch" : {
|
||||||
|
|
@ -21685,7 +21685,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Skip forward or backward by ten seconds" : {
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Slider color" : {
|
"Slider color" : {
|
||||||
|
|
@ -22256,7 +22256,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Spotify" : {
|
"Spotify" : {
|
||||||
"comment" : "Name of a media controller type for Spotify.",
|
"comment" : "Add more media player types here.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Square" : {
|
"Square" : {
|
||||||
|
|
@ -22369,7 +22369,7 @@
|
||||||
|
|
||||||
},
|
},
|
||||||
"Standard previous/next track controls" : {
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Start" : {
|
"Start" : {
|
||||||
|
|
@ -23721,7 +23721,7 @@
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
"Track Skip" : {
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Transform" : {
|
"Transform" : {
|
||||||
|
|
@ -25635,7 +25635,7 @@
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Youtube Music" : {
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"YouTube Music requires this third-party app to be installed: " : {
|
"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 {
|
struct EmptyEventsView: View {
|
||||||
let selectedDate: Date
|
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 {
|
struct EventListView: View {
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
@ObservedObject private var calendarManager = CalendarManager.shared
|
@ObservedObject private var calendarManager = CalendarManager.shared
|
||||||
|
|
|
||||||
|
|
@ -552,7 +552,14 @@ struct NotchHomeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if Defaults[.showCalendar] {
|
if Defaults[.showCalendar] {
|
||||||
|
Group {
|
||||||
|
if showStandardMediaControls {
|
||||||
CalendarView()
|
CalendarView()
|
||||||
|
} else {
|
||||||
|
StandaloneCalendarView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.onHover { isHovering in
|
.onHover { isHovering in
|
||||||
vm.isHoveringCalendar = isHovering
|
vm.isHoveringCalendar = isHovering
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue