Skip to content

Commit

Permalink
Show next pending events (#345)
Browse files Browse the repository at this point in the history
Closes #343
Fixes #344
  • Loading branch information
pakerwreah authored Jan 6, 2025
1 parent 1200aaa commit 7f79d6c
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 81 deletions.
20 changes: 16 additions & 4 deletions Calendr.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
34B5A09325B0CE6F00F7F7ED /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B5A09225B0CE6F00F7F7ED /* SettingsViewController.swift */; };
34B5A09725B0F8A500F7F7ED /* NSButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B5A09625B0F8A500F7F7ED /* NSButton.swift */; };
34B5A09D25B118EC00F7F7ED /* CalendarPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B5A09C25B118EC00F7F7ED /* CalendarPickerViewModel.swift */; };
34B99FBB2D2C8605003D4EA4 /* EventBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B99FBA2D2C8605003D4EA4 /* EventBackground.swift */; };
34C1D41E2C6E567100295E5E /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C1D41D2C6E567100295E5E /* ProcessInfo.swift */; };
34C1D4212C6E632F00295E5E /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C1D4202C6E632F00295E5E /* Sentry.swift */; };
34C2CF9125C2070400FC2CFF /* EventViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C2CF9025C2070400FC2CFF /* EventViewModelTests.swift */; };
Expand Down Expand Up @@ -363,6 +364,7 @@
34B5A09225B0CE6F00F7F7ED /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
34B5A09625B0F8A500F7F7ED /* NSButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSButton.swift; sourceTree = "<group>"; };
34B5A09C25B118EC00F7F7ED /* CalendarPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarPickerViewModel.swift; sourceTree = "<group>"; };
34B99FBA2D2C8605003D4EA4 /* EventBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBackground.swift; sourceTree = "<group>"; };
34C1D41D2C6E567100295E5E /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = "<group>"; };
34C1D4202C6E632F00295E5E /* Sentry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentry.swift; sourceTree = "<group>"; };
34C2CF9025C2070400FC2CFF /* EventViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewModelTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -800,6 +802,7 @@
34ABB9E429A12A720021F3CF /* Events */ = {
isa = PBXGroup;
children = (
34B99FBC2D2C861E003D4EA4 /* Background */,
34ABB9E129A129F00021F3CF /* ContextMenu */,
34ABB9DE29A129D10021F3CF /* EventDetails */,
34ABB9E329A129FE0021F3CF /* EventList */,
Expand All @@ -808,6 +811,14 @@
path = Events;
sourceTree = "<group>";
};
34B99FBC2D2C861E003D4EA4 /* Background */ = {
isa = PBXGroup;
children = (
34B99FBA2D2C8605003D4EA4 /* EventBackground.swift */,
);
path = Background;
sourceTree = "<group>";
};
34C1D41F2C6E631700295E5E /* Utils */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1150,6 +1161,7 @@
34ABB9EE29A163890021F3CF /* ContextMenuFactory.swift in Sources */,
347C04A32D136BF800F67E7C /* SearchSuggestionView.swift in Sources */,
34299F732C724FA000A0269B /* MockNetworkServiceProvider.swift in Sources */,
34B99FBB2D2C8605003D4EA4 /* EventBackground.swift in Sources */,
3468E653284BD44600B21EC8 /* EventBarStyle.swift in Sources */,
34F128E12597B9C9007DF31C /* MainViewController.swift in Sources */,
34934CD828E71B0F009635D4 /* NSObject+Rx.swift in Sources */,
Expand Down Expand Up @@ -1498,7 +1510,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 32973VC289;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -1507,7 +1519,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.15.2;
MARKETING_VERSION = 1.15.3;
PRODUCT_BUNDLE_IDENTIFIER = br.paker.Calendr;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Calendr/Config/Calendr-Bridging-Header.h";
Expand All @@ -1526,7 +1538,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 32973VC289;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -1535,7 +1547,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.15.2;
MARKETING_VERSION = 1.15.3;
PRODUCT_BUNDLE_IDENTIFIER = br.paker.Calendr;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Calendr/Config/Calendr-Bridging-Header.h";
Expand Down
45 changes: 45 additions & 0 deletions Calendr/Events/Background/EventBackground.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// EventBackground.swift
// Calendr
//
// Created by Paker on 06/01/2025.
//

import Cocoa

enum EventBackground: Equatable {
case clear
case pending
case color(NSColor)
}

extension EventBackground {

var cgColor: CGColor {
switch self {
case .clear: .clear
case .pending: pendingBackground
case .color(let color): color.cgColor
}
}
}

private let pendingBackground: CGColor = {

let stripes = CIFilter.stripesGenerator()
stripes.color0 = CIColor(color: NSColor.gray.withAlphaComponent(0.25))!
stripes.color1 = .clear
stripes.width = 2.5
stripes.sharpness = 0

let rotated = CIFilter.affineClamp()
rotated.inputImage = stripes.outputImage!
rotated.transform = CGAffineTransform(rotationAngle: -.pi / 4)

let ciImage = rotated.outputImage!.cropped(to: CGRect(x: 0, y: 0, width: 300, height: 300))
let rep = NSCIImageRep(ciImage: ciImage)
let nsImage = NSImage(size: rep.size)
nsImage.addRepresentation(rep)

return NSColor(patternImage: nsImage).cgColor
}()
55 changes: 17 additions & 38 deletions Calendr/Events/EventList/EventView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,30 +67,29 @@ class EventView: NSView {

switch viewModel.type {

case .birthday:
birthdayIcon.isHidden = false
case .birthday:
birthdayIcon.isHidden = false

case .reminder:
priority.textColor = viewModel.color
priority.stringValue = viewModel.priority ?? ""
priority.isHidden = viewModel.priority == nil
case .reminder:
priority.textColor = viewModel.color
priority.stringValue = viewModel.priority ?? ""
priority.isHidden = viewModel.priority == nil

completeBtn.contentTintColor = viewModel.color
completeBtn.isHidden = false
completeBtn.contentTintColor = viewModel.color
completeBtn.isHidden = false

case .event(let status):
if status ~= .pending {
layer?.backgroundColor = Self.pendingBackground
}
case .event:
break
}

switch viewModel.barStyle {
case .filled:
colorBar.layer?.backgroundColor = viewModel.color.cgColor

case .bordered:
colorBar.layer?.borderWidth = 1
colorBar.layer?.borderColor = viewModel.color.cgColor
case .filled:
colorBar.layer?.backgroundColor = viewModel.color.cgColor

case .bordered:
colorBar.layer?.borderWidth = 1
colorBar.layer?.borderColor = viewModel.color.cgColor
}

title.attributedStringValue = .init(
Expand Down Expand Up @@ -280,7 +279,7 @@ class EventView: NSView {
Observable
.combineLatest(viewModel.isCompleted, Scaling.observable)
.map { completed, scaling in
let icon = completed ? Icons.Reminder.complete : Icons.Reminder.incomplete
let icon = completed ? Icons.Reminder.complete : Icons.Reminder.incomplete
return icon.with(pointSize: 12 * scaling)
}
.bind(to: completeBtn.rx.image)
Expand Down Expand Up @@ -351,26 +350,6 @@ class EventView: NSView {
}
}

private static let pendingBackground: CGColor = {

let stripes = CIFilter.stripesGenerator()
stripes.color0 = CIColor(color: NSColor.gray.withAlphaComponent(0.25))!
stripes.color1 = .clear
stripes.width = 2.5
stripes.sharpness = 0

let rotated = CIFilter.affineClamp()
rotated.inputImage = stripes.outputImage!
rotated.transform = CGAffineTransform(rotationAngle: -.pi / 4)

let ciImage = rotated.outputImage!.cropped(to: CGRect(x: 0, y: 0, width: 300, height: 300))
let rep = NSCIImageRep(ciImage: ciImage)
let nsImage = NSImage(size: rep.size)
nsImage.addRepresentation(rep)

return NSColor(patternImage: nsImage).cgColor
}()

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Expand Down
9 changes: 5 additions & 4 deletions Calendr/Events/EventList/EventViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class EventViewModel {

let duration: Observable<String>
let isInProgress: Observable<Bool>
let backgroundColor: Observable<NSColor>
let backgroundColor: Observable<EventBackground>
let isFaded: Observable<Bool>
let progress: Observable<CGFloat?>
let isCompleted: Observable<Bool>
Expand Down Expand Up @@ -277,9 +277,10 @@ class EventViewModel {

isInProgress = progress.map(\.isNotNil).distinctUntilChanged()

let progressBackgroundColor = color.withAlphaComponent(0.15)

backgroundColor = isInProgress.map { $0 ? progressBackgroundColor : .clear }
backgroundColor = isInProgress.map { isInProgress in
guard event.status != .pending else { return .pending }
return isInProgress ? .color(event.calendar.color.withAlphaComponent(0.15)) : .clear
}

showRecurrenceIndicator = settings.showRecurrenceIndicator.map { $0 && event.hasRecurrenceRules }
}
Expand Down
69 changes: 47 additions & 22 deletions Calendr/MenuBar/NextEventViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class NextEventViewModel {
let time: Observable<String>
let barStyle: Observable<EventBarStyle>
let barColor: Observable<NSColor>
let backgroundColor: Observable<NSColor>
let backgroundColor: Observable<EventBackground>
let hasEvent: Observable<Bool>
let isInProgress: Observable<Bool>
let textScaling: Observable<Double>
Expand Down Expand Up @@ -97,43 +97,66 @@ class NextEventViewModel {
self.isShowingDetails = isShowingDetails
self.textScaling = settings.eventStatusItemTextScaling

let nextEvents = settings.showEventStatusItem
.flatMapLatest { isEnabled -> Observable<[EventModel]> in
let nextEvents = Observable
.combineLatest(
settings.showEventStatusItem,
nextEventCalendars
)
.repeat(when: calendarService.changeObservable)
.repeat(when: dateProvider.calendarUpdated.void())
.flatMapLatest { isEnabled, calendars -> Single<[EventModel]> in

!isEnabled ? .just([]) : nextEventCalendars
.repeat(when: calendarService.changeObservable)
.flatMapLatest { calendars -> Single<[EventModel]> in
let start = dateProvider.calendar.startOfDay(for: dateProvider.now)
let end = dateProvider.calendar.date(byAdding: .hour, value: 48, to: start)!
return calendarService.events(from: start, to: end, calendars: calendars)
}
.map {
$0.filter { $0.type != .reminder(completed: true) }
}
guard isEnabled else { return .just([]) }

let start = dateProvider.calendar.startOfDay(for: dateProvider.now)
let end = dateProvider.calendar.date(byAdding: .hour, value: 48, to: start)!
let events = calendarService.events(from: start, to: end, calendars: calendars)

return events
}

let eventsObservable = Observable.combineLatest(nextEvents, skippedEvents)
let filteredEvents = Observable
.combineLatest(
nextEvents, skippedEvents
)
.map { events, skipped in
events.filter { event in
type.matches(event.type) &&
event.type != .reminder(completed: true) &&
!event.isAllDay &&
![.pending, .declined].contains(event.status) &&
event.status != .declined &&
!skipped.contains(Skipped(event))
}
}

let statusOrder: [EventStatus] = [.accepted, .maybe, .pending, .unknown]

// This is a low effort strategy to untie events starting at the same time
// where one has a chosen status with higher priority than the other
let sortedEvents = filteredEvents.map { events in
events.sorted {
guard
$0.start == $1.start,
$0.status != $1.status,
let firstStatusIndex = statusOrder.firstIndex(of: $0.status),
let secondStatusIndex = statusOrder.firstIndex(of: $1.status)
else {
return $0.start < $1.start
}
return firstStatusIndex < secondStatusIndex
}
}

let nextEventObservable = Observable
.combineLatest(eventsObservable, settings.eventStatusItemCheckRange)
.combineLatest(sortedEvents, settings.eventStatusItemCheckRange)
.flatMapLatest { [dateProvider] events, hoursToCheck -> Observable<NextEvent?> in

Observable<Int>.interval(.seconds(1), scheduler: scheduler)
.void()
.startWith(())
.map {
events
.sorted(by: \.start)
.first(where: { event in
type.matches(event.type)
&&
dateProvider.calendar.isDate(
dateProvider.now, lessThan: event.end, granularity: .second
)
Expand Down Expand Up @@ -174,8 +197,10 @@ class NextEventViewModel {
) { ($0, $1.0, $1.1) }
.map { [dateProvider] nextEvent, flashing, sound in

guard nextEvent.event.status != .pending else { return .pending }

guard !nextEvent.isInProgress else {
return nextEvent.event.calendar.color.withAlphaComponent(0.2)
return .color(nextEvent.event.calendar.color.withAlphaComponent(0.2))
}

let diff = dateProvider.calendar.dateComponents([.minute, .second], from: dateProvider.now, to: nextEvent.event.start)
Expand All @@ -197,12 +222,12 @@ class NextEventViewModel {
if flashing {
// flash continuously under 30 seconds to start
if minutes == 0 && seconds <= 30 {
return seconds % 2 == 0 ? .systemRed : .clear
return seconds % 2 == 0 ? .color(.systemRed) : .clear
}

// flash 5x every minute
if minutes <= 5 {
return seconds > 50 && seconds % 2 == 1 ? .systemRed : .clear
return seconds > 50 && seconds % 2 == 1 ? .color(.systemRed) : .clear
}
}

Expand Down
4 changes: 2 additions & 2 deletions Calendr/Previews/EventViewPreview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct EventViewPreview: PreviewProvider {
title: "Test Event",
location: "Brasil",
notes: "Join at http://meet.google.com",
type: .event(.accepted),
type: .event(.pending),
calendar: .make(color: .systemYellow)
),
dateProvider: dateProvider,
Expand All @@ -45,7 +45,7 @@ struct EventViewPreview: PreviewProvider {
)
.preview()
.frame(width: 180, height: 50)
.padding(5)
.padding(20)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Calendr/Previews/NextEventPreview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ struct NextEventPreview: PreviewProvider {

static let events: [EventModel] = [
.make(
start: dateProvider.now + 70,
start: dateProvider.now + 5,
end: dateProvider.now + 999,
title: "Test with a very long event name and some more extra text",
type: .event(.accepted),
type: .event(.pending),
calendar: .make(color: .systemYellow)
)
]
Expand Down
Loading

0 comments on commit 7f79d6c

Please sign in to comment.