Last active
October 27, 2020 15:02
-
-
Save ParoTheParrot/39e0a6b00f0b2a93d57f452222d77320 to your computer and use it in GitHub Desktop.
Calendar and reminder for scriptable #scriptable
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
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: red; icon-glyph: calendar-alt; | |
// NOTE: this currently only works in beta because it takes advantage of the widgetStack and URL scheme for on-tap. | |
// TEST MODE: run the first time with it true to get access to your calendars; you can also run the sample Scriptable overdue Reminders script to get access to your reminders | |
const TEST_MODE = false | |
// CALENDAR/REMINDERS SETUP: calendar and reminder names should match what's shown in the Calendar and Reminder apps | |
const YOUR_NAME = "Tiberiu" | |
const VISIBLE_CALENDARS = ["Personal", "edu", "com"] | |
const VISIBLE_REMINDERS = ["Reminders", "School"] | |
const CALENDAR_URL = "calendars://open" // For Calendars 5 | |
const REMINDERS_URL = "calendars://open" // For Calendars 5 | |
const NUM_ITEMS_TO_SHOW = 4 // 4 is the max without it being cramped | |
const NO_ITEMS_MESSAGE = "Enjoy it." // what's displayed when you have no items for the day | |
// COLOR SETUP: you can choose the background image and color, and all text colors | |
// NOTE: nothing changes with light/dark mode, everything is static | |
const USE_BACKGROUND_IMAGE = true | |
const IMAGE_SOURCE = "Unsplash" // options are Bing and Unsplash | |
const IMAGE_SEARCH_TERMS = "nature,water" | |
const FORCE_IMAGE_UPDATE = false // whether to update the image on every refresh | |
const BACKGROUND_COLOR = new Color("#C3C4C8") | |
const GREETING_COLOR = new Color("#eeeeee") | |
const DATE_COLOR = Color.red() | |
const ITEM_NAME_COLOR = Color.white() | |
const ITEM_TIME_COLOR = new Color("#eeeeee") | |
// NOTE: All calendars must have a color mapping, or else they'll show up white | |
const CALENDAR_COLORS = { | |
"Personal": Color.blue(), | |
"edu": new Color("#3f51b5"), // blueberry | |
"com": new Color("#e67c73") // flamingo | |
} | |
const REMINDER_COLORS = { | |
"Reminders": Color.yellow(), | |
"School": new Color("#3f51b5") // blueberry | |
} | |
// FONT SETUP | |
const GREETING_SIZE = 16 | |
const ITEM_NAME_SIZE = 14 | |
const ITEM_TIME_SIZE = 12 | |
const ITEM_TIME_FONT = "Menlo-Regular"; // Monospace font so the names are aligned | |
// INTERNAL CONSTS | |
const DATE_FORMATTER = new DateFormatter() | |
const NOW = new Date() | |
// If we're running the script normally, go to the set calendar app | |
if (!config.runsInWidget && !TEST_MODE) { | |
const appleDate = new Date('2001/01/01') | |
const timestamp = (NOW.getTime() - appleDate.getTime()) / 1000 | |
const callback = new CallbackURL(CALENDAR_URL + timestamp) | |
callback.open() | |
Script.complete() | |
} else { // Otherwise, work on the widget | |
// Collect events and reminders to show | |
// Store custom objects here with the fields: id, name, startDate, endDate, dateIncludesTime, isReminder, calendarTitle | |
let itemsToShow = [] | |
// Find future events that aren't all day, aren't canceled, and are part of the calendar list | |
const events = await CalendarEvent.today([]) | |
for (const event of events) { | |
if (event.endDate.getTime() > NOW.getTime() | |
&& VISIBLE_CALENDARS.includes(event.calendar.title) | |
&& !event.isAllDay && !event.title.startsWith("Canceled:")) { | |
itemsToShow.push({ | |
id: event.identifier, | |
name: event.title, | |
startDate: event.startDate, | |
endDate: event.endDate, | |
dateIncludesTime: true, | |
isReminder: false, | |
calendarTitle: event.calendar.title | |
}) | |
} | |
} | |
// Find today's reminders that are part of the reminder list | |
// NOTE: all-day reminders have their time set to 00:00 of the same day, but aren't returned with incompleteDueToday... | |
let queryStartTime = new Date(NOW) | |
queryStartTime.setDate(queryStartTime.getDate() - 1) | |
queryStartTime.setHours(23, 59, 59, 0) | |
let queryEndTime = new Date(NOW) | |
queryEndTime.setHours(23, 59, 59, 0) | |
const reminders = await Reminder.incompleteDueBetween(queryStartTime, queryEndTime) | |
for (const reminder of reminders) { | |
if (VISIBLE_REMINDERS.includes(reminder.calendar.title)) { | |
itemsToShow.push({ | |
id: reminder.identifier, | |
name: reminder.title, | |
startDate: reminder.dueDate, | |
endDate: null, | |
dateIncludesTime: reminder.dueDateIncludesTime, | |
isReminder: true, | |
calendarTitle: reminder.calendar.title | |
}) | |
} | |
} | |
// Sort and truncate them: events / timed reminders, in order, then all-day reminders | |
itemsToShow = itemsToShow.sort(sortItems).slice(0, NUM_ITEMS_TO_SHOW) | |
// Lay out the widget! | |
let widget = new ListWidget() | |
widget.backgroundColor = BACKGROUND_COLOR | |
// Add the top date and greeting | |
let topStack = widget.addStack() | |
topStack.layoutHorizontally() | |
topStack.topAlignContent() | |
// Greeting is left aligned, date is right aligned | |
let greetingStack = topStack.addStack() | |
let greeting = greetingStack.addText(getGreeting()) | |
greeting.textColor = GREETING_COLOR | |
greeting.font = Font.lightSystemFont(GREETING_SIZE) | |
topStack.addSpacer() | |
let dateStack = topStack.addStack() | |
DATE_FORMATTER.dateFormat = "EEEE d" | |
let topDate = dateStack.addText(DATE_FORMATTER.string(NOW).toUpperCase()) | |
topDate.textColor = DATE_COLOR | |
topDate.font = Font.semiboldSystemFont(GREETING_SIZE) | |
if (USE_BACKGROUND_IMAGE === true) { | |
// Look for the image file | |
let files = FileManager.local() | |
const path = files.documentsDirectory() + "/up_next_medium.jpg" | |
const modificationDate = files.modificationDate(path) | |
// Download image if it doesn't exist, wasn't created this hour, or update is forced | |
if (!modificationDate || !sameHour(modificationDate, NOW) || FORCE_IMAGE_UPDATE) { | |
try { | |
let img = await provideImage(IMAGE_SOURCE, IMAGE_SEARCH_TERMS) | |
files.writeImage(path, img) | |
widget.backgroundImage = img | |
} catch { | |
widget.backgroundImage = files.readImage(path) | |
} | |
} else { | |
widget.backgroundImage = files.readImage(path) | |
} | |
} | |
// Put all of the event items on the bottom | |
widget.addSpacer() | |
// If there is at least one item today | |
if (itemsToShow.length > 0) { | |
if (USE_BACKGROUND_IMAGE === true) { | |
// Add a darker overlay | |
let gradient = new LinearGradient() | |
gradient.colors = [new Color("#000000", 0.75), new Color("#000000", 0.15)] | |
gradient.locations = [0, 1] | |
widget.backgroundGradient = gradient | |
} | |
for (i = 0; i < itemsToShow.length; i++) { | |
// Add space between events | |
if (i != 0) { | |
widget.addSpacer(12) | |
} | |
// Add nested stacks so everything aligns nicely... | |
let itemStack = widget.addStack() | |
itemStack.layoutHorizontally() | |
itemStack.centerAlignContent() | |
itemStack.url = getItemUrl(itemsToShow[i]) | |
let itemDate = itemStack.addText(formatItemDate(itemsToShow[i])) | |
itemDate.font = new Font(ITEM_TIME_FONT, ITEM_TIME_SIZE) | |
itemDate.textColor = ITEM_TIME_COLOR | |
itemStack.addSpacer(12) | |
let itemPrefix = itemStack.addText(formatItemPrefix(itemsToShow[i])) | |
itemPrefix.font = Font.semiboldSystemFont(ITEM_NAME_SIZE) | |
itemPrefix.textColor = getItemColor(itemsToShow[i]) | |
itemStack.addSpacer(4) | |
let itemName = itemStack.addText(formatItemName(itemsToShow[i])) | |
itemName.lineLimit = 1 | |
itemName.font = Font.semiboldSystemFont(ITEM_NAME_SIZE) | |
itemName.textColor = ITEM_NAME_COLOR | |
} | |
} else { // If there are no more items today | |
if (USE_BACKGROUND_IMAGE === true) { | |
// Add a more minimal overlay | |
let gradient = new LinearGradient() | |
gradient.colors = [new Color("#000000", 0.5), new Color("#000000", 0)] | |
gradient.locations = [0, 0.5] | |
widget.backgroundGradient = gradient | |
} | |
// Simple message to show you're done | |
let message = widget.addText(NO_ITEMS_MESSAGE) | |
message.textColor = ITEM_NAME_COLOR | |
message.font = Font.lightSystemFont(ITEM_NAME_SIZE) | |
} | |
// Finalize widget settings | |
widget.setPadding(16, 16, 16, 16) | |
widget.spacing = -3 | |
Script.setWidget(widget) | |
widget.presentSmall() | |
Script.complete() | |
} | |
// WIDGET TEXT HELPERS | |
function getGreeting() { | |
let greeting = "Good " | |
if (NOW.getHours() < 6) { | |
greeting = greeting + "night, " | |
} else if (NOW.getHours() < 12) { | |
greeting = greeting + "morning, " | |
} else if (NOW.getHours() < 17) { | |
greeting = greeting + "afternoon, " | |
} else if (NOW.getHours() < 21) { | |
greeting = greeting + "evening, " | |
} else { | |
greeting = greeting + "night, " | |
} | |
return greeting + YOUR_NAME + "." | |
} | |
function sortItems(first, second) { | |
if (first.dateIncludesTime === false && second.dateIncludesTime === false) { | |
return 0 | |
} else if (first.dateIncludesTime === false) { | |
return 1 | |
} else if (second.dateIncludesTime === false) { | |
return -1 | |
} else { | |
return first.startDate - second.startDate | |
} | |
} | |
function formatItemDate(item) { | |
DATE_FORMATTER.dateFormat = "hh:mma" | |
if (item.dateIncludesTime === true) { | |
return DATE_FORMATTER.string(item.startDate) // always 7 chars | |
} else { | |
return "TO-DO " // Not a TODO in the code, literally return that | |
} | |
} | |
function formatItemName(item) { | |
return item.name | |
} | |
function formatItemPrefix(item) { | |
if (item.isReminder === false) { | |
return "▐ " | |
} else { | |
return "□" | |
} | |
} | |
function getItemUrl(item) { | |
if (item.isReminder === false) { | |
return CALENDAR_URL + item.id | |
} else { | |
return REMINDERS_URL + item.id | |
} | |
} | |
function getItemColor(item) { | |
if (item.isReminder === true) { | |
return REMINDER_COLORS[item.calendarTitle] | |
} else { | |
return CALENDAR_COLORS[item.calendarTitle] | |
} | |
} | |
// BACKGROUND IMAGE HELPERS | |
// Helper function to interpret sources and terms | |
async function provideImage(source, terms) { | |
if (source === "Bing") { | |
const url = "http://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=en-US" | |
const req = new Request(url) | |
const json = await req.loadJSON() | |
const imgURL = "http://bing.com" + json.images[0].url | |
const img = await downloadImage(imgURL) | |
const rect = new Rect(-78, 0, 356, 200) | |
return cropImage(img, rect) | |
} else if (source === "Unsplash") { | |
const img = await downloadImage("https://source.unsplash.com/featured/?" + terms) | |
return img | |
} | |
} | |
// Helper function to download images | |
async function downloadImage(url) { | |
const req = new Request(url) | |
return await req.loadImage() | |
} | |
// Crop an image into a rect | |
function cropImage(img, rect) { | |
let draw = new DrawContext() | |
draw.respectScreenScale = true | |
draw.drawImageInRect(img, rect) | |
return draw.getImage() | |
} | |
// Determines if two dates occur on the same hour | |
function sameHour(d1, d2) { | |
return d1.getFullYear() === d2.getFullYear() && | |
d1.getMonth() === d2.getMonth() && | |
d1.getDate() === d2.getDate() && | |
d1.getHours() === d2.getHours() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment