Skip to content

Instantly share code, notes, and snippets.

@thisisevanfox
Last active November 14, 2022 08:19
Show Gist options
  • Save thisisevanfox/2eb35791b27d0de1d42ddca8369b85e0 to your computer and use it in GitHub Desktop.
Save thisisevanfox/2eb35791b27d0de1d42ddca8369b85e0 to your computer and use it in GitHub Desktop.
"Date, Calendar, Weather and Shares" Scriptable widget

"Date, Calendar, Weather and Shares" Scriptable widget 📱

Features 💡

Widget supports light- and dark-mode

  • Shows current date
  • Shows weather forecast
  • Shows today's appointments
  • Optional: Shows current status of shares (customizable)
  • Supports light- and dark-mode
  • Supports no-background.js

Widget with no-background.js

New in v1.1.0:

  • Exclude calendars from widget with the new setting property EXCLUDED_CALENDARS. See description in the script for more information and how to use it.

Getting started with the widget 🚀

  1. Download Scriptable from the AppStore. Click here to get to AppStore.
  2. Click the "+"-Icon in the Scriptable-app.
  3. Copy all the text from the DateWeatherCalendarShares.js-file in this Gist.
  4. Step through the user settings in the script.
  5. Add a Scriptable-widget to your homescreen.
    • ATTENTION: If shares are enabled in the script, make sure to add the widget with size "large".
    • Make sure to choose "Run script" for "When Interacting".

Known bugs 🐞

  • Only six shares can be displayed in the widget
  • When for example only 2 shares are configured the spaces in the widget are a bit messed up

References 🏆

The initial script was made by Slowlydev and adapted by marco79cgn. The shares part as a standalone widget was initialy made by saiteja09. I just combined these widgets and did a few extensions.

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-gray; icon-glyph: user-circle;
/********************************************************
* script : DateWeatherCalendarShares.js
* version : 1.1.0
* gist-link : http://bit.ly/DateCalendarWeatherSharesWidget
* description: Widget for Scriptable.app, which shows date,
* weather forecast, next calendar events and
* shares
* author : @thisisevanfox
* information: Inital script made by @Slowlydev and adapted
* by @marco79cgn
* Shares part initialy made by @saiteja09
* date : 2020-12-30
*******************************************************/
/********************************************************
******************** USER SETTINGS *********************
************ PLEASE MODIFY BEFORE FIRST RUN ************
*******************************************************/
// Replace PASTE_API_KEY_HERE with your API key from openweathermap.com.
const OPEN_WEATHER_API_KEY = "PASTE_API_KEY_HERE";
// Set appearance of the widget. Default apperance is set to the system color scheme.
// Device.isUsingDarkAppearance() = System color scheme (default)
// true = Widget will be in dark mode.
// false = Widget will be in light mode.
const DARK_MODE = Device.isUsingDarkAppearance();
// Set time mode for weather forecast.
// When set to "true", the widget displays 15, 16, 17, ... (default)
// When set to "false", the widget displays 3PM, 4PM, 5PM, ...
const TIME_MODE_24_HOURS = true;
// Number of hours which are displayed in the weather forecast. Best look with value "3" or "4".
const FORECAST_HOURS = "4";
// Unit of measurement of temperature in the weather forecast.
// "metric" for Celsius and "imperial" for Fahrenheit.
const UNITS = "metric";
// Set true to let the script locate you each time (which takes longer and needs more battery)
// Default: false
const LIVE_WEATHER = false;
// Longitude and Latitude for the place the weather should be shown. Only matters if LIVE_WEATHER is true.
// Visit https://www.latlong.net/ to retrieve the latitude and longitude for your city.
const LATITUDE = "48.864716";
const LONGITUDE = "2.349014";
// Translation for texts which are shown in the widget.
const I18N_ALL_DAY = "ganztägig"; // en: "all day"
const I18N_TIME = "Uhr"; // en: "o'clock"
const I18N_NO_EVENTS = "Heute hast du keine Termine!"; // en: "You don't have any appointments today!"
// URL to calendar app.
// Default: "calshow://" (Apple Calendar App)
// If your favorite calendar app does have a URL scheme feel free to change it.
const CALENDAR_URL = "calshow://";
// Indicator if all day events should be shown or not.
// Default: true
const SHOW_ALLDAY_EVENTS = true;
// Excluded calendars from widget.
// Example:
// User has three calendars on his phone: "Work", "School", "Holidays".
// He only wants to show events from calendar "Work".
// So he has to set this constant like this:
// const EXCLUDED_CALENDARS = ["School", "Holidays"];
// Default: const EXCLUDED_CALENDARS = [];
const EXCLUDED_CALENDARS = [];
// URL to weather app.
// Default: No URL for the Apple Weather App
// If your favorite weather app does have a URL scheme feel free to change it.
const WEATHER_URL = "";
// URL to clock app.
// Default: No URL for the Apple Clock App
// If your favorite clock app does have a URL scheme feel free to change it.
const CLOCK_URL = "";
// Indicator if share section is enabled
// true: use widget size large. (default)
// false: use widget size medium.
const SHARES_ENABLED = true;
// Shares to show in widget.
// Only matters if SHARES_ENABLED is true.
// Default: ["ABEA.DE", "AMZN", "FB2A.DE", "AAPL", "MSF.DE", "BTC-USD"]
// Google, Amazon, Facebook, Apple, Microsoft, Bitcoin-$
// Fetch the symbols for your favorite shares on finance.yahoo.com.
// ONLY SIX (6) SHARES ARE SHOWN REGARDLESS HOW MANY ARE LISTED HERE.
const SHARES = ["ABEA.DE", "AMZN", "FB2A.DE", "AAPL", "MSF.DE", "BTC-USD"];
// Indicates decimal seperator
// Example
// true: 1,47% (default)
// false: 1.47%
const DECIMAL_SEPERATOR_COMMA = true;
// Indicates how the change value of shares is displayed
// Example
// true: -50% (default)
// false: -100
const CHANGE_VALUE_PERCENT = true;
// URL to shares app
// Default: "stocks://" (Apple Stocks App)
// If your favorite stocks app does have a URL scheme feel free to change it.
const SHARES_URL = "stocks://";
// Indicator if no-background.js is installed
// Default: false
// @see: https://github.com/supermamon/scriptable-no-background
const NO_BACKGROUND_INSTALLED = false;
// Indicator if no-background.js should be active
// Only matters if NO_BACKGROUND_INSTALLED is true.
const NO_BACKGROUND_ACTIVE = true;
/********************************************************
********************************************************
*********** DO NOT CHANGE ANYTHING FROM HERE ***********
********************************************************
*******************************************************/
const { transparent } = NO_BACKGROUND_INSTALLED
? importModule("no-background")
: emptyFunction();
const WIDGET_BACKGROUND = DARK_MODE ? new Color("gray") : new Color("#D6D6D6");
const STACK_BACKGROUND = DARK_MODE
? new Color("#1D1D1D")
: new Color("#FFFFFF"); //Smaller Container Background
const STACK_SIZE = new Size(0, 65); //0 means its automatic
let dateAgendaWeatherSharesWidget = await createWidget();
if (config.runsInWidget) {
// The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen.
Script.setWidget(dateAgendaWeatherSharesWidget);
} else {
// The script runs inside the app, so we preview the widget.
if (SHARES_ENABLED) {
dateAgendaWeatherSharesWidget.presentLarge();
} else {
dateAgendaWeatherSharesWidget.presentMedium();
}
}
// Calling Script.complete() signals to Scriptable that the script have finished running.
// This can speed up the execution, in particular when running the script from Shortcuts or using Siri.
Script.complete();
/**
* Creates widget.
*
* @return {ListWidget}
*/
async function createWidget() {
// Initialise widget
const widget = new ListWidget();
if (NO_BACKGROUND_INSTALLED && NO_BACKGROUND_ACTIVE) {
widget.backgroundImage = await transparent(Script.name());
} else {
widget.backgroundColor = WIDGET_BACKGROUND;
}
widget.setPadding(10, 10, 10, 10);
// Initialise first row
let topRow = widget.addStack();
topRow.layoutHorizontally();
// Add date
addDateStack(topRow);
// Add weather
await addWeatherStack(topRow);
// Add calendar
await addCalendarStack(widget);
if (SHARES_ENABLED) {
// Add shares
await addSharesStack(widget);
}
return widget;
}
/**
* Add date stack to row.
*
* @param {WidgetStack} oTopRow
*/
function addDateStack(oTopRow) {
const dDate = new Date();
let dfName = new DateFormatter();
let dfMonth = new DateFormatter();
dfName.dateFormat = "EEEE";
dfMonth.dateFormat = "MMMM";
const sDayName = dfName.string(dDate);
const sDayNumber = dDate.getDate().toString();
const sMonthName = dfMonth.string(dDate);
const oDateStack = oTopRow.addStack();
oDateStack.url = CLOCK_URL;
oDateStack.layoutHorizontally();
oDateStack.centerAlignContent();
oDateStack.setPadding(7, 7, 7, 7);
oDateStack.backgroundColor = STACK_BACKGROUND;
oDateStack.cornerRadius = 12;
oDateStack.size = STACK_SIZE;
oDateStack.addSpacer();
let oDayNumberText = oDateStack.addText(sDayNumber + ".");
oDayNumberText.font = Font.semiboldSystemFont(32);
oDayNumberText.textColor = getColorForCurrentAppearance();
oDateStack.addSpacer(7);
let oDateTextStack = oDateStack.addStack();
oDateTextStack.layoutVertically();
let oMonthNameTxt = oDateTextStack.addText(sMonthName.toUpperCase());
oMonthNameTxt.font = Font.boldSystemFont(10);
oMonthNameTxt.textColor = getColorForCurrentAppearance();
let oDayNameTxt = oDateTextStack.addText(sDayName);
oDayNameTxt.font = Font.boldSystemFont(12);
oDayNameTxt.textColor = DARK_MODE
? new Color("#EA3323")
: new Color("#EA3323");
oDateStack.addSpacer();
oTopRow.addSpacer();
}
/**
* Add weather stack to row.
*
* @param {WidgetStack} oTopRow
*/
async function addWeatherStack(oTopRow) {
const dDateNow = Date.now();
let sLatitude;
let sLongitude;
if (LIVE_WEATHER) {
const oLocation = await Location.current();
sLatitude = oLocation["latitude"];
sLongitude = oLocation["longitude"];
} else {
sLatitude = LATITUDE;
sLongitude = LONGITUDE;
}
const sWeatherURL = `https://api.openweathermap.org/data/2.5/onecall?lat=${sLatitude}&lon=${sLongitude}&exclude=current,minutely,daily,alerts&units=${UNITS}&appid=${OPEN_WEATHER_API_KEY}`;
const oWeatherRequest = new Request(sWeatherURL);
const oWeatherData = await oWeatherRequest.loadJSON();
const aHourlyForecasts = oWeatherData.hourly;
const aNextForecasts = [];
for (const oHourlyForecast of aHourlyForecasts) {
if (aNextForecasts.length == FORECAST_HOURS) {
break;
}
let sTimestamp = removeDigitsFromDate(dDateNow, 3);
if (oHourlyForecast.dt > sTimestamp) {
aNextForecasts.push(oHourlyForecast);
}
}
//Top Row Weather
let oWeatherStack = oTopRow.addStack();
oWeatherStack.layoutHorizontally();
oWeatherStack.centerAlignContent();
oWeatherStack.setPadding(7, 7, 7, 7);
oWeatherStack.backgroundColor = STACK_BACKGROUND;
oWeatherStack.cornerRadius = 12;
oWeatherStack.size = STACK_SIZE;
oWeatherStack.url = WEATHER_URL;
for (const oNextForecast of aNextForecasts) {
const sIconURL = `https://openweathermap.org/img/wn/${oNextForecast.weather[0].icon}@2x.png`;
let oIcon = await loadImage(sIconURL);
oWeatherStack.addSpacer();
//Hour Forecast Stack
let oHourStack = oWeatherStack.addStack();
oHourStack.layoutVertically();
let oHourTxt = oHourStack.addText(formatTimestamp(oNextForecast.dt));
oHourTxt.centerAlignText();
oHourTxt.font = Font.systemFont(10);
oHourTxt.textColor = getColorForCurrentAppearance();
oHourTxt.textOpacity = 1;
let oWeatherIcon = oHourStack.addImage(oIcon);
oWeatherIcon.centerAlignImage();
oWeatherIcon.size = new Size(25, 25);
let oTemperatureText = oHourStack.addText(
" " + Math.round(oNextForecast.temp) + "°"
);
oTemperatureText.centerAlignText();
oTemperatureText.font = Font.systemFont(10);
oTemperatureText.textColor = getColorForCurrentAppearance();
}
oWeatherStack.addSpacer();
}
/**
* Add calendar stack to widget.
*
* @param {ListWidget} widget
*/
async function addCalendarStack(widget) {
const dDateNow = Date.now();
// Add horizontal space before calendar stack
widget.addSpacer();
const aEventsToday = await CalendarEvent.today([]);
let aFutureEvents = [];
for (const oEvent of aEventsToday) {
if (aFutureEvents.length == 2) {
break;
}
if(EXCLUDED_CALENDARS.includes(oEvent.calendar.title)){
continue;
}
if (oEvent.isAllDay && SHOW_ALLDAY_EVENTS) {
aFutureEvents.push(oEvent);
} else if (oEvent.startDate.getTime() >= dDateNow) {
aFutureEvents.push(oEvent);
}
}
let oEventStack = widget.addStack();
oEventStack.layoutHorizontally();
oEventStack.centerAlignContent();
oEventStack.setPadding(7, 7, 7, 7);
oEventStack.backgroundColor = STACK_BACKGROUND;
oEventStack.cornerRadius = 12;
oEventStack.size = STACK_SIZE;
oEventStack.addSpacer(8);
const oFont = Font.lightSystemFont(20);
const oCalendarSymbol = SFSymbol.named("calendar");
oCalendarSymbol.applyFont(oFont);
const oEventIcon = oEventStack.addImage(oCalendarSymbol.image);
oEventIcon.imageSize = new Size(20, 20);
oEventIcon.resizable = false;
oEventIcon.tintColor = getColorForCurrentAppearance();
oEventIcon.centerAlignImage();
oEventStack.addSpacer(14);
oEventStack.url = CALENDAR_URL;
const oEventItemsStack = oEventStack.addStack();
oEventItemsStack.layoutVertically();
let oEventInfoStack;
if (aFutureEvents.length != 0) {
for (let i = 0; i < aFutureEvents.length; i++) {
let oFutureEvent = aFutureEvents[i];
const time =
formatTime(oFutureEvent.startDate) +
" - " +
formatTime(oFutureEvent.endDate);
const oEventColor = new Color("#" + oFutureEvent.calendar.color.hex);
oEventInfoStack = oEventItemsStack.addStack();
oEventInfoStack.layoutVertically();
let oEventTitle = oEventItemsStack.addText(oFutureEvent.title);
oEventTitle.font = Font.semiboldSystemFont(12);
oEventTitle.textColor = oEventColor;
oEventTitle.lineLimit = 1;
let sEventTime;
if (oFutureEvent.isAllDay) {
sEventTime = I18N_ALL_DAY;
} else {
sEventTime = `${time} ${I18N_TIME}`;
}
let oEventTimeText = oEventItemsStack.addText(sEventTime);
oEventTimeText.font = Font.semiboldMonospacedSystemFont(10);
oEventTimeText.textColor = getColorForCurrentAppearance();
oEventTimeText.textOpacity = 1;
if (i == 0) {
oEventItemsStack.addSpacer(3);
}
}
} else {
let oNoEventsText = oEventStack.addText(I18N_NO_EVENTS);
oNoEventsText.font = Font.semiboldMonospacedSystemFont(12);
oNoEventsText.textColor = getColorForCurrentAppearance();
oNoEventsText.textOpacity = 1;
}
oEventStack.addSpacer();
}
/**
* Add shares stack to widget.
*
* @param {ListWidget} oWidget
*/
async function addSharesStack(oWidget) {
// Add horizontal space before shares stack
oWidget.addSpacer();
const aSharesData = await getSharesData();
const oSharesStack = oWidget.addStack();
oSharesStack.layoutVertically();
oSharesStack.centerAlignContent();
oSharesStack.setPadding(7, 7, 7, 7);
oSharesStack.url = SHARES_URL;
oSharesStack.backgroundColor = STACK_BACKGROUND;
oSharesStack.cornerRadius = 12;
oSharesStack.size = new Size(0, 0);
for (let i = 0; i < aSharesData.length; i++) {
let oCurrentShare = aSharesData[i];
let oShareSymbolRow = oSharesStack.addStack();
// Add share Symbol
let oShareSymbol = oShareSymbolRow.addText(oCurrentShare.symbol);
oShareSymbol.textColor = getColorForCurrentAppearance();
oShareSymbol.font = Font.boldMonospacedSystemFont(12);
// Add current Price
oShareSymbolRow.addSpacer();
let oSharePrice = oShareSymbolRow.addText(oCurrentShare.price);
oSharePrice.textColor = getColorForCurrentAppearance();
oSharePrice.font = Font.boldMonospacedSystemFont(12);
// Second Row
oSharesStack.addSpacer(2);
let oShareNameRow = oSharesStack.addStack();
// Add share name
let oShareName = oShareNameRow.addText(oCurrentShare.name);
oShareName.textColor = getColorForCurrentAppearance();
oShareName.textOpacity = 0.7;
oShareName.font = Font.boldMonospacedSystemFont(9);
// Add change value
oShareNameRow.addSpacer();
let oChangeValue = oShareNameRow.addText(
CHANGE_VALUE_PERCENT
? oCurrentShare.changepercent
: oCurrentShare.changevalue
);
if (oCurrentShare.changevalue < "0") {
oChangeValue.textColor = Color.red();
} else if (oCurrentShare.changevalue > "0") {
oChangeValue.textColor = Color.green();
} else {
oChangeValue.textColor = Color.yellow();
}
oChangeValue.font = Font.boldMonospacedSystemFont(9);
// Add ticker icon
oShareNameRow.addSpacer(2);
let oTicker = null;
if (oCurrentShare.changevalue < "0") {
oTicker = oShareNameRow.addImage(SFSymbol.named("chevron.down").image);
oTicker.tintColor = Color.red();
} else if (oCurrentShare.changevalue > "0") {
oTicker = oShareNameRow.addImage(SFSymbol.named("chevron.up").image);
oTicker.tintColor = Color.green();
} else {
oTicker = oShareNameRow.addImage(SFSymbol.named("dot.square.fill").image);
oTicker.tintColor = Color.yellow();
}
oTicker.imageSize = new Size(8, 8);
}
}
/**
* Fetches shares data.
*
* @return {Object[]}
*/
async function getSharesData() {
let aShares = SHARES;
let aSharesData = [];
const loops = aShares.length > 6 ? 6 : aShares.length;
for (i = 0; i < loops; i++) {
let oShareData = await fetchShareData(aShares[i].trim());
let oData = {};
if (oShareData.quoteSummary.error !== null) {
oData.symbol = `ERROR: ${aShares[i]}`;
oData.changepercent = "";
oData.changevalue = "";
oData.price = "";
oData.high = "";
oData.low = "";
oData.prevclose = "";
oData.name = oShareData.quoteSummary.error.description;
} else {
const oPriceData = oShareData.quoteSummary.result[0].price;
oData.symbol = oPriceData.symbol;
oData.changepercent =
formatValue(
(oPriceData.regularMarketChangePercent.raw * 100).toFixed(2)
) + "%";
oData.changevalue = formatValue(
oPriceData.regularMarketChange.raw.toFixed(2)
);
oData.price = formatValue(oPriceData.regularMarketPrice.raw.toFixed(2));
oData.high = formatValue(oPriceData.regularMarketDayHigh.raw.toFixed(2));
oData.low = formatValue(oPriceData.regularMarketDayLow.raw.toFixed(2));
oData.prevclose = formatValue(
oPriceData.regularMarketPreviousClose.raw.toFixed(2)
);
oData.name = oPriceData.shortName;
}
aSharesData.push(oData);
}
return aSharesData;
}
/**
* Formats number value with correct decimal seperator.
*
* @param {String} sValue
* @return {String}
*/
function formatValue(sValue) {
if (DECIMAL_SEPERATOR_COMMA) {
return sValue.replace(/\./g, ",");
} else {
return sValue.replace(/,/g, ".");
}
}
/**
* Fetches share data from fincance.yahoo.com.
*
* @param {String} sShareSymbol
* @return {Object}
*/
async function fetchShareData(sShareSymbol) {
let sUrl = `https://query1.finance.yahoo.com/v10/finance/quoteSummary/${sShareSymbol}?modules=price`;
const oRequest = new Request(sUrl);
return await oRequest.loadJSON();
}
/**
* Returns color object depending if dark mode is active or not.
*
* @return {Object}
*/
function getColorForCurrentAppearance() {
return DARK_MODE ? Color.white() : Color.black();
}
/**
* Removes digits of date and returns timestamp string.
*
* @param {Object} dDate
* @param {Number} iDigitsToRemove
* @return {String}
*/
function removeDigitsFromDate(dDate, iDigitsToRemove) {
return (
(dDate - (dDate % Math.pow(10, iDigitsToRemove))) /
Math.pow(10, iDigitsToRemove)
);
}
/**
* Formats a timestamp to a readable time.
*
* @param {String} sUnixTimestamp
* @return {String}
*/
function formatTimestamp(sUnixTimestamp) {
const dDate = new Date(sUnixTimestamp * 1000);
let oHours = dDate.getHours();
let sTimeString;
if (TIME_MODE_24_HOURS) {
sTimeString = " " + oHours.toString();
} else {
const sAmOrPm = oHours >= 12 ? "PM" : "AM";
oHours = oHours % 12;
oHours = oHours ? oHours : 12;
sTimeString = oHours.toString() + sAmOrPm;
}
return sTimeString;
}
/**
* Formats date to time.
*
* @param {Object} dDate
* @return {String}
*/
function formatTime(dDate) {
const dfDateFormatter = new DateFormatter();
dfDateFormatter.useNoDateStyle();
dfDateFormatter.useShortTimeStyle();
return dfDateFormatter.string(dDate);
}
/**
* Helper function to download an image from a given url
*
* @param {String} sImageUrl
* @return {Object}
*/
async function loadImage(sImageUrl) {
const oRequest = new Request(sImageUrl);
return await oRequest.loadImage();
}
/**
* Placeholder function when no-background.js isn't installed.
*
* @return {Object}
*/
function emptyFunction() {
// Silence
return {};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment