Skip to content

Instantly share code, notes, and snippets.

@ParoTheParrot
Last active October 27, 2020 15:02
Show Gist options
  • Save ParoTheParrot/39e0a6b00f0b2a93d57f452222d77320 to your computer and use it in GitHub Desktop.
Save ParoTheParrot/39e0a6b00f0b2a93d57f452222d77320 to your computer and use it in GitHub Desktop.
Calendar and reminder for scriptable #scriptable
// 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