Calendar & Reminders

Huge shoutout to @mzeryck for getting me inspired with Scriptable.
One thing that I’ve been searching for in widgets is seeing BOTH my calendar events and timed reminders in one. Even Calendars 5, which is an old paid app I have, doesn’t show both in smaller widgets.

So I went ahead and modified the original script I found for my own liking! It’s totally customizable too: background images, colors, and font sizes. Works for both calendar and reminder accounts.

Note that the medium widget only works with the new Testflight beta. The small widget does also work in the medium size – it’s just not “optimized” for space and doesn’t have the cute greeting.

Here’s the code.
Medium:

// 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()
}

Small:

// 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 widget does not need beta, and does not actually yet support opening the proper calendar URL on-tap. (widgetStack required to use that)


// 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 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 = 3 // 3 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("#111111")
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 DATE_SIZE = 16
const ITEM_NAME_SIZE = 14
const ITEM_TIME_SIZE = 12

// 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
    DATE_FORMATTER.dateFormat = "EEEE d"
    let topDate = widget.addText(DATE_FORMATTER.string(NOW))
    topDate.textColor = DATE_COLOR
    topDate.font = Font.semiboldSystemFont(DATE_SIZE)

    if (USE_BACKGROUND_IMAGE === true) {
        // Look for the image file
        let files = FileManager.local()
        const path = files.documentsDirectory() + "/up_next_widget_small.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(10)
            }
            
            // TODO: create a widgetStack and add the URL
            
            let itemName = widget.addText(formatItemName(itemsToShow[i]))
            itemName.lineLimit = 1
            itemName.font = Font.semiboldSystemFont(ITEM_NAME_SIZE)
            itemName.textColor = ITEM_NAME_COLOR
            widget.addSpacer(5)
            
            let itemDate = widget.addText(formatItemDate(itemsToShow[i]))
            itemDate.font = Font.mediumSystemFont(ITEM_TIME_SIZE)
            itemDate.textColor = getItemColor(itemsToShow[i])
        }
    } 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(12, 12, 12, 0)
    widget.spacing = -3

    Script.setWidget(widget)
    widget.presentSmall()
    Script.complete()
}

// WIDGET TEXT HELPERS

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) {
    if (item.dateIncludesTime === true) {
        if (item.isReminder === false) {
            DATE_FORMATTER.dateFormat = "hh:mm"
            let startDate = DATE_FORMATTER.string(item.startDate)
            DATE_FORMATTER.dateFormat = "hh:mma"
            let endDate = DATE_FORMATTER.string(item.endDate)
            return "▐  " + startDate + "—" + endDate
        } else {
            DATE_FORMATTER.dateFormat = "hh:mma"
            let startDate = DATE_FORMATTER.string(item.startDate)
            return "□ " + startDate
        }
    } else {
        return "□ TO-DO" // Not a TODO in the code, literally return that
    }
}

function getItemColor(item) {
    if (item.isReminder === true) {
        return REMINDER_COLORS[item.calendarTitle]
    } else {
        return CALENDAR_COLORS[item.calendarTitle]
    }
}

function formatItemName(item) {
    return item.name
}

function getItemUrl(item) {
    if (item.isReminder === false) {
        return CALENDAR_URL + item.id
    } else {
        return REMINDERS_URL + item.id
    }
}

// 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()
}

Hope this helps people!

4 Likes

This is great, I need to add reminders to mine!

Super Tool. Danke.
Ist es möglich es in das
Weather-Cal widget von mzeryck zu integrieren
Ich nutze es als großes Widget, aber.meine Termine stehen nur in calendars 5
Besten Dank im Voraus

Super Tool. Danke.
Ist es möglich es in das
Weather-Cal widget von mzeryck zu integrieren
Ich nutze es als großes Widget, aber.meine Termine stehen nur in calendars 5
Besten Dank im Voraus

1 Like

Ich glaube du meinst @mzeryck.

(I believe you mean @mzeryck.)

Ja. Ich konnte nur den Link nicht einfügen. Es kam immer nicht zulässig

OK, first this script is awesome and I am 99% of the way to having it setup like I want. However I am struggling with all day events. How do I get them to show up? I use all days for items like Birthdays and i want them to display as a calendar event as well.

In the lines here:

for (const event of events) {
        if (event.endDate.getTime() > NOW.getTime()
            && VISIBLE_CALENDARS.includes(event.calendar.title)
            && !event.isAllDay && !event.title.startsWith("Canceled:"))

Delete && !event.isAllDay. So the resulting code block would be

for (const event of events) {
        if (event.endDate.getTime() > NOW.getTime()
            && VISIBLE_CALENDARS.includes(event.calendar.title)
            && !event.title.startsWith("Canceled:"))

First, I appreciate the help, thank you! One last question for you or anyone else, when I take out the All Day code it displays as 12:00 AM. Anyway to get it to say All Day vs 12:00 AM?

No problem. Try using the updated code blocks below. I haven’t tested it but I think it will work.

The change in this block is adding the isAllDay: event.isAllDay parameter:

const events = await CalendarEvent.today([]);
  for (const event of events) {
    if (
      event.endDate.getTime() > NOW.getTime() &&
      VISIBLE_CALENDARS.includes(event.calendar.title) &&
      !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,
        isAllDay: event.isAllDay,
      });
    }
  }

The change in this block is adding another if clause at the beginning that formats the date if it’s all day (just change that string to whatever you want to show up):

function formatItemDate(item) {
  DATE_FORMATTER.dateFormat = 'hh:mma';
  if (item.isAllDay) {
    return 'All day';
  } else 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
  }
}

This seems to be working perfectly. Thank you so much for the help!

1 Like

Glad to hear it ! :slight_smile: