mirror of
https://github.com/Toxblh/MTMR.git
synced 2026-01-10 00:58:37 +00:00
Up next calendar widget (#348)
* WIP Implementation of up next widget * Seperated button view and event source logic * Adjusted default parameters * Added the ability to view multiple events * Added ability to click touchbar item and go to calendar * renamed nthEvent to maxToShow and changed default * Updated CFBundleVersion * Renamed UpNext class and fix ups * Added "autoResize" property (same functionality as dock) * Added EKEventStore listener to reduce perfomance impact * Log cleanup * Made button blue for current/past events * Added handling of unauthorised access to calendar
This commit is contained in:
parent
14282b86a9
commit
87141e381b
@ -71,6 +71,7 @@
|
||||
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */; };
|
||||
BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5624317B4300B04904 /* BasicView.swift */; };
|
||||
BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5824317CAF00B04904 /* SwipeItem.swift */; };
|
||||
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@ -163,6 +164,7 @@
|
||||
B0F8771B207AC92700D6E430 /* TouchBarSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchBarSupport.h; sourceTree = "<group>"; };
|
||||
BAF5AB5624317B4300B04904 /* BasicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicView.swift; sourceTree = "<group>"; };
|
||||
BAF5AB5824317CAF00B04904 /* SwipeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeItem.swift; sourceTree = "<group>"; };
|
||||
F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpNextScrubberTouchBarItem.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -322,6 +324,7 @@
|
||||
4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */,
|
||||
B08126F0217BE19000A98970 /* WidgetProtocol.swift */,
|
||||
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */,
|
||||
F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */,
|
||||
);
|
||||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
@ -488,6 +491,7 @@
|
||||
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */,
|
||||
4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */,
|
||||
607EEA4B2087835F009DA5F0 /* WeatherBarItem.swift in Sources */,
|
||||
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */,
|
||||
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */,
|
||||
4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */,
|
||||
6042B6AA2083E27000C525C8 /* DeprecatedCarbonAPI.c in Sources */,
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.26.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>405</string>
|
||||
<string>425</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@ -227,6 +227,7 @@ enum ItemType: Decodable {
|
||||
case network(flip: Bool)
|
||||
case darkMode
|
||||
case swipe(direction: String, fingers: Int, minOffset: Float, sourceApple: SourceProtocol?, sourceBash: SourceProtocol?)
|
||||
case upnext(from: Double, to: Double, maxToShow: Int, autoResize: Bool)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
@ -258,6 +259,7 @@ enum ItemType: Decodable {
|
||||
case direction
|
||||
case fingers
|
||||
case minOffset
|
||||
case maxToShow
|
||||
}
|
||||
|
||||
enum ItemTypeRaw: String, Decodable {
|
||||
@ -281,6 +283,7 @@ enum ItemType: Decodable {
|
||||
case network
|
||||
case darkMode
|
||||
case swipe
|
||||
case upnext
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
@ -378,6 +381,14 @@ enum ItemType: Decodable {
|
||||
let fingers = try container.decode(Int.self, forKey: .fingers)
|
||||
let minOffset = try container.decodeIfPresent(Float.self, forKey: .minOffset) ?? 0.0
|
||||
self = .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
|
||||
|
||||
case .upnext:
|
||||
let from = try container.decodeIfPresent(Double.self, forKey: .from) ?? 0 // Lower bounds of period of time in hours to search for events
|
||||
let to = try container.decodeIfPresent(Double.self, forKey: .to) ?? 12 // Upper bounds of period of time in hours to search for events
|
||||
let maxToShow = try container.decodeIfPresent(Int.self, forKey: .maxToShow) ?? 3 // 1 indexed array. Get the 1st, 2nd, 3rd event to display multiple notifications
|
||||
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
|
||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 60.0
|
||||
self = .upnext(from: from, to: to, maxToShow: maxToShow, autoResize: autoResize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +59,8 @@ extension ItemType {
|
||||
return DarkModeBarItem.identifier
|
||||
case .swipe(direction: _, fingers: _, minOffset: _, sourceApple: _, sourceBash: _):
|
||||
return "com.toxblh.mtmr.swipe."
|
||||
case .upnext(from: _, to: _, maxToShow: _, autoResize: _):
|
||||
return "com.connorgmeehan.mtmrup.next."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -301,6 +303,8 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
barItem = DarkModeBarItem(identifier: identifier)
|
||||
case let .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash):
|
||||
barItem = SwipeItem(identifier: identifier, direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
|
||||
case let .upnext(from: from, to: to, maxToShow: maxToShow, autoResize: autoResize):
|
||||
barItem = UpNextScrubberTouchBarItem(identifier: identifier, interval: 60, from: from, to: to, maxToShow: maxToShow, autoResize: autoResize)
|
||||
}
|
||||
|
||||
if let action = self.action(forItem: item), let item = barItem as? CustomButtonTouchBarItem {
|
||||
|
||||
256
MTMR/Widgets/UpNextScrubberTouchBarItem.swift
Normal file
256
MTMR/Widgets/UpNextScrubberTouchBarItem.swift
Normal file
@ -0,0 +1,256 @@
|
||||
//
|
||||
// UpNextScrubberTouchBarItems.swift
|
||||
// MTMR
|
||||
//
|
||||
// Created by Connor Meehan on 13/7/20.
|
||||
// Copyright © 2020 Anton Palgunov. All rights reserved.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import EventKit
|
||||
|
||||
class UpNextScrubberTouchBarItem: NSCustomTouchBarItem {
|
||||
// Dependencies
|
||||
private let scrollView = NSScrollView()
|
||||
private let activity: NSBackgroundActivityScheduler // Update scheduler
|
||||
private var eventSources : [IUpNextSource] = []
|
||||
private var items: [UpNextItem] = []
|
||||
|
||||
// Settings
|
||||
private var futureSearchCutoff: Double
|
||||
private var pastSearchCutoff: Double
|
||||
private var maxToShow: Int
|
||||
private var widthConstraint: NSLayoutConstraint?
|
||||
private var autoResize: Bool = false
|
||||
|
||||
/// <#Description#>
|
||||
/// - Parameters:
|
||||
/// - identifier: Unique identifier of widget
|
||||
/// - interval: Update view interval in seconds
|
||||
/// - from: Relative to current time, how far back we search for events in hours
|
||||
/// - to: Relative to current time, how far forward we search for events in hours
|
||||
/// - maxToShow: Which event to show (1 is first, 2 is second, and so on)
|
||||
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, from: Double, to: Double, maxToShow: Int, autoResize: Bool) {
|
||||
// Initialise member properties
|
||||
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updateCheck")
|
||||
pastSearchCutoff = from * 3600
|
||||
futureSearchCutoff = to * 3600
|
||||
self.maxToShow = maxToShow
|
||||
self.autoResize = autoResize
|
||||
UpNextItem.df.dateFormat = "HH:mm"
|
||||
// Error handling
|
||||
if (maxToShow <= 0) {
|
||||
fatalError("Error on UpNext bar item. maxToShow property must be greater than 0.")
|
||||
}
|
||||
// Init super
|
||||
super.init(identifier: identifier)
|
||||
view = scrollView
|
||||
// Add event sources
|
||||
// Can optionally pass an update view callback to an event source to redraw element
|
||||
self.eventSources.append(UpNextCalenderSource(updateCallback: self.updateView))
|
||||
// Fallback interactivity via interval
|
||||
activity.interval = interval
|
||||
activity.repeats = true
|
||||
activity.qualityOfService = .utility
|
||||
activity.schedule { (completion: NSBackgroundActivityScheduler.CompletionHandler) in
|
||||
self.updateView()
|
||||
completion(NSBackgroundActivityScheduler.Result.finished)
|
||||
}
|
||||
updateView()
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateView() -> Void {
|
||||
items = []
|
||||
var upcomingEvents = self.getUpcomingEvents()
|
||||
upcomingEvents.sort(by: {$0.startDate.compare($1.startDate) == .orderedAscending})
|
||||
var index = 1
|
||||
DispatchQueue.main.async {
|
||||
for event in upcomingEvents {
|
||||
// Create UpNextItem
|
||||
let item = UpNextItem(event: event)
|
||||
item.backgroundColor = self.getBackgroundColor(startDate: event.startDate)
|
||||
// Bind tap event
|
||||
item.tapClosure = { [weak self] in
|
||||
self?.switchToApp(event: event)
|
||||
}
|
||||
// Add to view
|
||||
self.items.append(item)
|
||||
// Check if should display any more
|
||||
if (index == self.maxToShow) {
|
||||
break;
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
self.reloadData()
|
||||
self.updateSize()
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadData() {
|
||||
let stackView = NSStackView(views: items.compactMap { $0.view })
|
||||
stackView.spacing = 5
|
||||
stackView.orientation = .horizontal
|
||||
let visibleRect = self.scrollView.documentVisibleRect
|
||||
self.scrollView.documentView = stackView
|
||||
stackView.scroll(visibleRect.origin)
|
||||
}
|
||||
|
||||
func updateSize() {
|
||||
if self.autoResize {
|
||||
self.widthConstraint?.isActive = false
|
||||
|
||||
let width = self.scrollView.documentView?.fittingSize.width ?? 0
|
||||
self.widthConstraint = self.scrollView.widthAnchor.constraint(equalToConstant: width)
|
||||
self.widthConstraint!.isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func getUpcomingEvents() -> [UpNextEventModel] {
|
||||
var upcomingEvents: [UpNextEventModel] = []
|
||||
|
||||
// Calculate the range we're going to search for events in
|
||||
let dateLowerBounds = Date(timeIntervalSinceNow: self.pastSearchCutoff)
|
||||
let dateUpperBounds = Date(timeIntervalSinceNow: self.futureSearchCutoff)
|
||||
|
||||
// Get all events from all sources
|
||||
for eventSource in self.eventSources {
|
||||
if (eventSource.hasPermission) {
|
||||
let events = eventSource.getUpcomingEvents(dateLowerBounds: dateLowerBounds, dateUpperBounds: dateUpperBounds)
|
||||
upcomingEvents.append(contentsOf: events)
|
||||
}
|
||||
}
|
||||
|
||||
return upcomingEvents
|
||||
}
|
||||
|
||||
public func switchToApp(event: UpNextEventModel) {
|
||||
var bundleIdentifier: String
|
||||
switch(event.sourceType) {
|
||||
case .iCalendar:
|
||||
bundleIdentifier = UpNextCalenderSource.bundleIdentifier
|
||||
}
|
||||
|
||||
NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleIdentifier, options: [.default], additionalEventParamDescriptor: nil, launchIdentifier: nil)
|
||||
|
||||
// NB: if you can't open app which on another space, try to check mark
|
||||
// "When switching to an application, switch to a Space with open windows for the application"
|
||||
// in Mission control settings
|
||||
}
|
||||
|
||||
|
||||
func getBackgroundColor(startDate: Date) -> NSColor {
|
||||
let distance = startDate.timeIntervalSinceReferenceDate/60 - Date().timeIntervalSinceReferenceDate/60 // Get time difference in minutes
|
||||
if (distance < 0 as TimeInterval) { // If it's in the past, draw as blue
|
||||
return NSColor.systemBlue
|
||||
} else if (distance < 30 as TimeInterval) { // Less than 30 minutes, backround is red
|
||||
return NSColor.systemRed
|
||||
}
|
||||
return NSColor.clear
|
||||
}
|
||||
}
|
||||
|
||||
private class UpNextItem : CustomButtonTouchBarItem {
|
||||
static public let df = DateFormatter()
|
||||
|
||||
init(event: UpNextEventModel) {
|
||||
let identifier = UpNextItem.getIdentifier(event: event)
|
||||
let title = UpNextItem.getTitle(event: event)
|
||||
super.init(identifier: NSTouchBarItem.Identifier(rawValue: identifier), title: title)
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private static func getTitle(event: UpNextEventModel) -> String {
|
||||
var title = ""
|
||||
let startDateString = UpNextItem.df.string(for: event.startDate)
|
||||
switch event.sourceType {
|
||||
case .iCalendar:
|
||||
title = String.init(format: "🗓 %@ - %@ ", event.title, startDateString!)
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
private static func getIdentifier(event: UpNextEventModel) -> String {
|
||||
var identifier : String
|
||||
switch event.sourceType {
|
||||
case .iCalendar:
|
||||
identifier = "com.mtmr.iCalendarEvent"
|
||||
}
|
||||
return identifier + "." + event.title
|
||||
}
|
||||
}
|
||||
|
||||
enum UpNextSourceType {
|
||||
case iCalendar
|
||||
}
|
||||
|
||||
// Model for events to be displayed in dock
|
||||
struct UpNextEventModel {
|
||||
let title: String
|
||||
let startDate: Date
|
||||
let sourceType: UpNextSourceType
|
||||
}
|
||||
|
||||
|
||||
// Interface for any event source
|
||||
protocol IUpNextSource {
|
||||
static var bundleIdentifier: String { get }
|
||||
var hasPermission : Bool { get }
|
||||
var updateCallback : () -> Void { get set }
|
||||
|
||||
init(updateCallback: @escaping () -> Void)
|
||||
func getUpcomingEvents(dateLowerBounds: Date, dateUpperBounds: Date) -> [UpNextEventModel]
|
||||
}
|
||||
|
||||
class UpNextCalenderSource : IUpNextSource {
|
||||
static public let bundleIdentifier: String = "com.apple.iCal"
|
||||
|
||||
public var hasPermission: Bool = false
|
||||
private var eventStore : EKEventStore
|
||||
internal var updateCallback: () -> Void
|
||||
|
||||
required init(updateCallback: @escaping () -> Void = {}) {
|
||||
self.updateCallback = updateCallback
|
||||
eventStore = EKEventStore()
|
||||
NotificationCenter.default.addObserver(forName: .EKEventStoreChanged, object: eventStore, queue: nil, using: handleUpdate)
|
||||
let authStatus = EKEventStore.authorizationStatus(for: .event)
|
||||
if (authStatus != .authorized) {
|
||||
eventStore.requestAccess(to: .event){ granted, error in
|
||||
self.hasPermission = granted;
|
||||
self.handleUpdate()
|
||||
if(!granted) {
|
||||
NSLog("Error: MTMR UpNextBarWidget not given calendar access.")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.handleUpdate()
|
||||
}
|
||||
|
||||
}
|
||||
public func handleUpdate() {
|
||||
self.handleUpdate(note: Notification(name: Notification.Name("refresh view")))
|
||||
}
|
||||
public func handleUpdate(note: Notification) {
|
||||
self.updateCallback()
|
||||
}
|
||||
|
||||
public func getUpcomingEvents(dateLowerBounds: Date, dateUpperBounds: Date) -> [UpNextEventModel] {
|
||||
var upcomingEvents: [UpNextEventModel] = []
|
||||
let calendars = self.eventStore.calendars(for: .event)
|
||||
let predicate = self.eventStore.predicateForEvents(withStart: dateLowerBounds, end: dateUpperBounds, calendars: calendars)
|
||||
let events = self.eventStore.events(matching: predicate)
|
||||
for event in events {
|
||||
upcomingEvents.append(UpNextEventModel(title: event.title, startDate: event.startDate, sourceType: UpNextSourceType.iCalendar))
|
||||
}
|
||||
return upcomingEvents
|
||||
}
|
||||
}
|
||||
18
README.md
18
README.md
@ -84,6 +84,7 @@ The pre-installed configuration contains less or more than you'll probably want,
|
||||
- darkMode
|
||||
- pomodoro
|
||||
- network
|
||||
- upnext (Calendar events)
|
||||
|
||||
> Media Keys
|
||||
|
||||
@ -343,6 +344,23 @@ To close a group, use the button:
|
||||
},
|
||||
```
|
||||
|
||||
#### `upnext`
|
||||
|
||||
> Calender next event plugin
|
||||
Displays upcoming events from MacOS Calendar. Does not display current event.
|
||||
|
||||
```js
|
||||
{
|
||||
"type": "upnext",
|
||||
"from": 0, // Lower bound of search range for next event in hours. Default 0 (current time)(can be negative to view events in the past)
|
||||
"to": 12, // Upper bounds of search range for next event in hours. Default 12 (12 hours in the future)
|
||||
"maxToShow": 3 // Limits the maximum number of events displayed. Default 3 (the first 3 upcoming events)
|
||||
"autoResize": false // If true, widget will expand to display all events. Default false (scrollable view within "width")
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Actions:
|
||||
|
||||
- `hidKey`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user