|
// 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 {}; |
|
} |