-
-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor event options + add context menu actions
- Loading branch information
1 parent
8fa9d00
commit b369575
Showing
20 changed files
with
643 additions
and
387 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// | ||
// ContextMenu.swift | ||
// Calendr | ||
// | ||
// Created by Paker on 18/02/23. | ||
// | ||
|
||
import AppKit | ||
import RxSwift | ||
|
||
protocol ContextMenuAction { | ||
var icon: NSImage? { get } | ||
var title: String { get } | ||
} | ||
|
||
extension ContextMenuAction { | ||
var icon: NSImage? { nil } | ||
} | ||
|
||
enum ContextMenuViewModelItem<Action: ContextMenuAction> { | ||
case separator | ||
case action(Action) | ||
} | ||
|
||
extension ContextMenuViewModelItem: Equatable where Action: Equatable { } | ||
|
||
protocol ContextMenuViewModel { | ||
associatedtype Action: ContextMenuAction | ||
typealias ActionItem = ContextMenuViewModelItem<Action> | ||
|
||
var items: [ActionItem] { get } | ||
var actionCallback: Observable<Void> { get } | ||
|
||
func triggerAction(_ action: Action) | ||
} | ||
|
||
private class ContextMenuItem<Action: ContextMenuAction>: NSMenuItem { | ||
|
||
private let value: Action | ||
private let trigger: (Action) -> Void | ||
|
||
init(action: Action, trigger: @escaping (Action) -> Void) { | ||
self.value = action | ||
self.trigger = trigger | ||
super.init(title: action.title, action: #selector(selected), keyEquivalent: "") | ||
self.target = self | ||
self.image = action.icon | ||
} | ||
|
||
@objc private func selected() { | ||
trigger(value) | ||
} | ||
|
||
required init(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
} | ||
|
||
class ContextMenu<ViewModel: ContextMenuViewModel>: NSMenu { | ||
|
||
init(viewModel: ViewModel) { | ||
|
||
super.init(title: "") | ||
|
||
for item in viewModel.items { | ||
switch item { | ||
case .separator: | ||
addItem(.separator()) | ||
case .action(let action): | ||
addItem(ContextMenuItem(action: action, trigger: viewModel.triggerAction)) | ||
} | ||
} | ||
} | ||
|
||
required init(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// | ||
// ContextMenuFactory.swift | ||
// Calendr | ||
// | ||
// Created by Paker on 18/02/23. | ||
// | ||
|
||
import Foundation | ||
|
||
enum ContextMenuFactory { | ||
|
||
static func makeViewModel( | ||
event: EventModel, | ||
dateProvider: DateProviding, | ||
calendarService: CalendarServiceProviding | ||
) -> (any ContextMenuViewModel)? { | ||
|
||
switch event.type { | ||
case .event(let status) where status != .unknown: | ||
return EventOptionsViewModel(event: event, calendarService: calendarService) | ||
|
||
case .reminder: | ||
return ReminderOptionsViewModel(event: event, dateProvider: dateProvider, calendarService: calendarService) | ||
|
||
default: | ||
return nil | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// | ||
// EventOptionsViewModel.swift | ||
// Calendr | ||
// | ||
// Created by Paker on 18/02/23. | ||
// | ||
|
||
import AppKit | ||
import RxSwift | ||
|
||
enum EventAction { | ||
case accept | ||
case maybe | ||
case decline | ||
} | ||
|
||
class EventOptionsViewModel: ContextMenuViewModel { | ||
typealias Action = EventAction | ||
|
||
private let actionCallbackObserver: AnyObserver<Void> | ||
let actionCallback: Observable<Void> | ||
|
||
private(set) var items: [ActionItem] = [] | ||
|
||
private let event: EventModel | ||
private let calendarService: CalendarServiceProviding | ||
|
||
private let disposeBag = DisposeBag() | ||
|
||
init( | ||
event: EventModel, | ||
calendarService: CalendarServiceProviding | ||
) { | ||
self.event = event | ||
self.calendarService = calendarService | ||
|
||
(actionCallback, actionCallbackObserver) = PublishSubject.pipe() | ||
|
||
if event.status != .accepted { | ||
items.append(.action(.accept)) | ||
} | ||
if event.status != .maybe { | ||
items.append(.action(.maybe)) | ||
} | ||
if event.status != .declined { | ||
items.append(.action(.decline)) | ||
} | ||
} | ||
|
||
func triggerAction(_ action: Action) { | ||
|
||
calendarService.changeEventStatus(id: event.id, date: event.start, to: action.status) | ||
.subscribe( | ||
onNext: actionCallbackObserver.onNext, | ||
onError: actionCallbackObserver.onError | ||
) | ||
.disposed(by: disposeBag) | ||
} | ||
} | ||
|
||
private extension EventAction { | ||
|
||
var status: EventStatus { | ||
switch self { | ||
case .accept: | ||
return .accepted | ||
case .maybe: | ||
return .maybe | ||
case .decline: | ||
return .declined | ||
} | ||
} | ||
} | ||
|
||
extension EventAction: ContextMenuAction { | ||
|
||
var icon: NSImage? { | ||
switch self { | ||
case .accept: | ||
return Icons.EventStatus.accepted.with(color: .systemGreen) | ||
case .maybe: | ||
return Icons.EventStatus.maybe.with(color: .systemOrange) | ||
case .decline: | ||
return Icons.EventStatus.declined.with(color: .systemRed) | ||
} | ||
} | ||
|
||
var title: String { | ||
switch self { | ||
case .accept: | ||
return Strings.EventStatus.Action.accept | ||
case .maybe: | ||
return Strings.EventStatus.Action.maybe | ||
case .decline: | ||
return Strings.EventStatus.Action.decline | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// | ||
// ReminderOptionsViewModel.swift | ||
// Calendr | ||
// | ||
// Created by Paker on 18/02/23. | ||
// | ||
|
||
import Foundation | ||
import RxSwift | ||
|
||
enum ReminderAction: Equatable { | ||
case complete | ||
case remind(DateComponents) | ||
} | ||
|
||
class ReminderOptionsViewModel: ContextMenuViewModel { | ||
typealias Action = ReminderAction | ||
|
||
private let actionCallbackObserver: AnyObserver<Void> | ||
let actionCallback: Observable<Void> | ||
|
||
let items: [ActionItem] | ||
|
||
private let event: EventModel | ||
private let dateProvider: DateProviding | ||
private let calendarService: CalendarServiceProviding | ||
|
||
private let disposeBag = DisposeBag() | ||
|
||
init( | ||
event: EventModel, | ||
dateProvider: DateProviding, | ||
calendarService: CalendarServiceProviding | ||
) { | ||
self.event = event | ||
self.dateProvider = dateProvider | ||
self.calendarService = calendarService | ||
|
||
(actionCallback, actionCallbackObserver) = PublishSubject.pipe() | ||
|
||
items = [ | ||
.action(.complete), | ||
.separator, | ||
.action(.remind(.init(minute: 5))), | ||
.action(.remind(.init(minute: 15))), | ||
.action(.remind(.init(minute: 30))), | ||
.action(.remind(.init(hour: 1))), | ||
.action(.remind(.init(day: 1))) | ||
] | ||
} | ||
|
||
private func triggerAction( _ action: Action) -> Observable<Void> { | ||
|
||
switch action { | ||
case .complete: | ||
return calendarService.completeReminder(id: event.id) | ||
|
||
case .remind(let dateComponents): | ||
let date = dateProvider.calendar.date(byAdding: dateComponents, to: dateProvider.now)! | ||
return calendarService.rescheduleReminder(id: event.id, to: date) | ||
} | ||
} | ||
|
||
func triggerAction(_ action: Action) { | ||
|
||
triggerAction(action) | ||
.subscribe( | ||
onNext: actionCallbackObserver.onNext, | ||
onError: actionCallbackObserver.onError | ||
) | ||
.disposed(by: disposeBag) | ||
} | ||
} | ||
|
||
private enum Constants { | ||
|
||
static let formatter = { | ||
let formatter = RelativeDateTimeFormatter() | ||
formatter.dateTimeStyle = .named | ||
return formatter | ||
}() | ||
} | ||
|
||
extension ReminderAction: ContextMenuAction { | ||
|
||
var title: String { | ||
switch self { | ||
case .complete: | ||
return Strings.Reminder.Options.complete | ||
case .remind(let dateComponents): | ||
return Strings.Reminder.Options.remind(Constants.formatter.localizedString(from: dateComponents)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.