Widget Examples

Great eqsOne and egamez :clap: Thanks a lot to both!

EDIT, Sorry I was too lazy but I’ve done by myself :sweat_smile:

Here’s my version with more data and different fonts size/alignment, 24h etcc., hope it’s okay!

Code here

(PS: I removed my API key, thanks to js_learner that noticed it to me, oh and I’ve deleted the old key, you can’t steal my query to the API anymore :joy: )

Oh, if someone want it, I’m also using a compact version of it on my homescreen, with only the feels like and humidity.

Just change line 165 to

drawText(feelsstring + " " + Math.round(weatherData.current.feels_like) + "° | " + relHumidity + " " + weatherData.current.humidity + "%", footerFontSize, feelsLikeCoords.x, feelsLikeCoords.y, Color.gray())

Edit: That way it saves effort, indeed! Well done.

Don’t worry, don’t worry, doesn’t matter, I’ve already done by myself! SOrry for my previous “laziness” :slight_smile: refresh the page, I’ve update my post with the code and images!

Thanks again btw!

Can’t get over it, to me that new for loop is just code art. Again, thanks for showing how to fix this on a high skill level.

Thank you so much for updatting the script @eqsOne! I slightly modified it but I cant seem to move the date and weather up in the widget. If I add a spacer under the date, then that moves up (which is perfect) but due to how spacers work, the weather goes down and image size shrinks. If I add a spacer under the weather I cant seem to get it to work. Any ideas to shift everything up a bit? (ie so Guelph is right under the mountain line and the date is above the more condensed clouds)

I posted the code below if you need it, it’s a modified version of yours and another posted here.

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-gray; icon-glyph: magic;
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: yellow; icon-glyph: cloud;

// Widget Params
// Don't edit this, those are default values for debugging (location for Cupertino).
// You need to give your locations parameters through the widget params, more info below.
const widgetParams = JSON.parse((args.widgetParameter != null) ? args.widgetParameter : '{ "LAT" : "43.550" , "LON" : "-80.250" , "LOC_NAME" : "Guelph, ON" }')

// WEATHER API PARAMETERS !important
// API KEY, you need an Open Weather API Key
// You can get one for free at: https://home.openweathermap.org/api_keys (account needed).
const API_KEY = "NICETRYhehe"

// Latitude and Longitude of the location where you get the weather of.
// You can get those from the Open Weather website while searching for a city, etc.
// This values are getted from the widget parameters, the widget parameters is a JSON string that looks like this:
// { "LAT" : "<latitude>" , "LON" : "<longitude>" , "LOC_NAME" : "<name to display>" }
// This to allow multiple instances of the widget with different locations, if you will only use one instance (1 widget), you can "hardcode" the values here.
// Note: To debug the widget you need to place the values here, because when playing the script in-app the widget parameters are null (= crash).

// Hardcoded Location, type in your latitude/longitude values and location name
var LAT = widgetParams.LAT // 12.34
var LON = widgetParams.LON // 12.34
var LOCATION_NAME = widgetParams.LOC_NAME // "Your place"

// Looking settings
// This are settings to customize the looking of the widgets, because this was made an iPhone SE (2016) screen, I can't test for bigger screens.
// So feel free to modify this to your taste.

// Support locales
const locale = "en" // "fr" "it" "de" etc. for weather description language
const nowstring = "Now" // Your local term for "now"
const feelsstring = "Feels like" //Your local term for "feels like"

// units : string > Defines the unit used to measure the temps, for temperatures in Fahrenheit use "imperial", "metric" for Celcius and "standard" for Kelvin (Default: "metric").
const units = "metric"
// twelveHours : true|false > Defines if the hours are displayed in a 12h format, use false for 24h format. (Default: true)
const twelveHours = true
// roundedGraph : true|false > true (Use rounded values to draw the graph) | false (Draws the graph using decimal values, this can be used to draw an smoother line).
const roundedGraph = true
// roundedTemp : true|false > true (Displays the temps rounding the values (29.8 = 30 | 29.3 = 29).
const roundedTemp = true
// hoursToShow : number > Number of predicted hours to show, Eg: 3 = a total of 4 hours in the widget (Default: 3 for the small widget and 11 for the medium one).
const hoursToShow = (config.widgetFamily == "small") ? 3 : 11;
// spaceBetweenDays : number > Size of the space between the temps in the graph in pixels. (Default: 60 for the small widget and 44 for the medium one).
const spaceBetweenDays = (config.widgetFamily == "small") ? 60 : 44;

/* -- PREVIEW YOUR WIDGET -- */

// Change to true to see a preview of your widget.
const testMode = true

// Optionally specify the size of your widget preview.
const widgetPreview = "large"

/* -- FONTS AND TEXT -- */

// Use iosfonts.com, or change to "" for the system font.
const fontName = "Futura-Medium"

// Find colors on htmlcolorcodes.com
const fontColor = new Color("#ffffff")

// Change the font sizes for each element.
const dayOfWeekSize = 60
const todaySize = 18
const activeTimerHeaderSize = 14
const activeTimerSize = 36
const untilSize = 14

// Widget Size !important.
// Since the widget works "making" an image and displaying it as the widget background, you need to specify the exact size of the widget to
// get an 1:1 display ratio, if you specify an smaller size than the widget itself it will be displayed blurry.
// You can get the size simply taking an screenshot of your widgets on the home screen and measuring them in an image-proccessing software.
// contextSize : number > Height of the widget in screen pixels, this depends on you screen size (for an 4 inch display the small widget is 282 * 282 pixels on the home screen)
const contextSize = 282
// mediumWidgetWidth : number > Width of the medium widget in pixels, this depends on you screen size (for an 4 inch display the medium widget is 584 pixels long on the home screen)
const mediumWidgetWidth = 584

// accentColor : Color > Accent color of some elements (Graph lines and the location label).
const accentColor = new Color("#EB6E4E", 1)
// backgroundColor : Color > Background color of the widgets.
const backgroundColor = new Color("#1C1C1E", 1)

// Position and size of the elements on the widget.
// All coordinates make reference to the top-left of the element.
// locationNameCoords : Point > Define the position in pixels of the location label.
const locationNameCoords = new Point(30, 20)
// locationNameFontSize : number > Size in pixels of the font of the location label.
const locationNameFontSize = 36
// locationNameCoords : Point > Define the position in pixels of the location label.
const tempCoords = new Rect((config.widgetFamily == "small") ? 150 : 450, 20, 100, 36)
// locationNameFontSize : number > Size in pixels of the font of the location label.
const tempFontSize = 36
// weatherDescriptionCoords : Point > Position of the weather description label in pixels.
const weatherDescriptionCoords = new Rect((config.widgetFamily == "small") ? 150 : 450, 62, 100, 36)
// weatherDescriptionFontSize : number > Font size of the weather description label.
const weatherDescriptionFontSize = 18
//footerFontSize : number > Font size of the footer labels (feels like... and last update time).
const footerFontSize = 20
//feelsLikeCoords : Point > Coordinates of the "feels like" label.
const feelsLikeCoords = new Point(30, 60)
//lastUpdateTimePosAndSize : Rect > Defines the coordinates and size of the last updated time label.
const lastUpdateTimePosAndSize = new Rect((config.widgetFamily == "small") ? 150 : 450, 230, 100, footerFontSize+1)

/** From here proceed with caution. **/



// Set up cache. File located in the Scriptable iCloud folder
let fm = FileManager.iCloud();
let cachePath = fm.joinPath(fm.documentsDirectory(), "weatherCache"); // <- file name
if(!fm.fileExists(cachePath)){
  fm.createDirectory(cachePath)
}

let weatherData;
let usingCachedData = false;
let drawContext = new DrawContext();

drawContext.size = new Size((config.widgetFamily == "small") ? contextSize : mediumWidgetWidth, contextSize)
drawContext.opaque = false


try {
  weatherData = await new Request("https://api.openweathermap.org/data/2.5/onecall?lat=" + LAT + "&lon=" + LON + "&exclude=minutely,alerts&units=" + units + "&lang" + locale + "&appid=" + API_KEY).loadJSON();
  fm.writeString(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON), JSON.stringify(weatherData));
}catch(e){
  console.log("Offline mode")
  try{
    await fm.downloadFileFromiCloud(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON));
    let raw = fm.readString(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON));
    weatherData = JSON.parse(raw);
    usingCachedData = true;
  }catch(e2){
    console.log("Error: No offline data cached")
  }
}



let widget = new ListWidget();


let req = new Request ("https://i.imgur.com/eqr7FyN.jpg")

let image = await req.loadImage()

widget.backgroundImage = image

//date up Top


let formatter = new DateFormatter()

formatter.dateFormat = "EEEE"
let dayOfWeekString = formatter.string(new Date()).toUpperCase()
let dayOfWeekText = widget.addText(dayOfWeekString)
dayOfWeekText.textColor = fontColor
dayOfWeekText.font = new Font(fontName, dayOfWeekSize)
dayOfWeekText.lineLimit = 1
dayOfWeekText.minimumScaleFactor = 0.2
dayOfWeekText.centerAlignText()


formatter.dateFormat = "MMMM d, yyyy"
let dateString = formatter.string(new Date())
let dateText = widget.addText(dateString)
dateText.textColor = fontColor
dateText.font = new Font(fontName, todaySize)
dateText.lineLimit = 1
dateText.minimumScaleFactor = 0.2
dateText.centerAlignText()




// weather info

drawText(LOCATION_NAME, locationNameFontSize, locationNameCoords.x, locationNameCoords.y, Color.white());

drawContext.setTextAlignedRight();
drawTextC(Math.round(weatherData.current.temp) + "°", tempFontSize, tempCoords.x, tempCoords.y, tempCoords.width, tempCoords.height, Color.white());

drawTextC(weatherData.current.weather[0].description, weatherDescriptionFontSize, weatherDescriptionCoords.x, weatherDescriptionCoords.y, weatherDescriptionCoords.width, weatherDescriptionCoords.height, Color.white())

drawContext.setTextAlignedRight();
let min, max, diff;
for(let i = 0; i<=hoursToShow ;i++){
  let temp = shouldRound(roundedGraph, weatherData.hourly[i].temp);
  min = (temp < min || min == undefined ? temp : min)
  max = (temp > max || max == undefined ? temp : max)
}
diff = max -min;

for(let i = 0; i<=hoursToShow ;i++){
  let hourData = weatherData.hourly[i];
  let nextHourTemp = shouldRound(roundedGraph, weatherData.hourly[i+1].temp);
  let hour = epochToDate(hourData.dt).getHours();
  if(twelveHours)
    hour = (hour > 12 ? hour - 12 : (hour == 0 ? "12a" : ((hour == 12) ? "12p" : hour)))
  let temp = i==0?weatherData.current.temp : hourData.temp
  let delta = (diff>0)?(shouldRound(roundedGraph, temp) - min) / diff:0.5;
  let nextDelta = (diff>0)?(nextHourTemp - min) / diff:0.5

  if(i < hoursToShow){
    let hourDay = epochToDate(hourData.dt);
    for(let i2 = 0 ; i2 < weatherData.daily.length ; i2++){
      let day = weatherData.daily[i2];
      if(isSameDay(epochToDate(day.dt), epochToDate(hourData.dt))){
        hourDay = day;
        break;
      }
    }
		// 'Night' boolean for line graph and SFSymbols
		var night = (hourData.dt > hourDay.sunset || hourData.dt < hourDay.sunrise)
    drawLine(spaceBetweenDays * (i) + 50, 175 - (50 * delta),spaceBetweenDays * (i+1) + 50 , 175 - (50 * nextDelta), 4, (night ? Color.gray() : accentColor))
  }

  drawContext.setTextAlignedCenter();
  drawTextC(shouldRound(roundedTemp, temp)+"°", 20, spaceBetweenDays*i+30, 135 - (50*delta), 50, 21, Color.white())

  // Next 2 lines SFSymbols tweak
  const condition = i==0?weatherData.current.weather[0].id:hourData.weather[0].id
  drawImage(symbolForCondition(condition), spaceBetweenDays * i + 34, 161 - (50*delta)); //40, 165

  drawTextC((i==0?nowstring:hour), 18, spaceBetweenDays*i+25, 200,50, 21, Color.gray())

  previousDelta = delta;

}

drawText(feelsstring + " " + Math.round(weatherData.current.feels_like) + "°", footerFontSize, feelsLikeCoords.x, feelsLikeCoords.y, Color.gray())

drawContext.setTextAlignedRight();
drawTextC(epochToDate(weatherData.current.dt).toLocaleTimeString().slice(0,-6), footerFontSize, lastUpdateTimePosAndSize.x, lastUpdateTimePosAndSize.y, lastUpdateTimePosAndSize.width, lastUpdateTimePosAndSize.height, (usingCachedData) ? Color.yellow() : Color.gray())

if(usingCachedData)
  drawText("⚠️", 32, ((config.widgetFamily == "small") ? contextSize : mediumWidgetWidth)-72,30)

let weatherUI = widget.addImage(drawContext.getImage())
widget.presentLarge()


function epochToDate(epoch){
  return new Date(epoch * 1000)


}


function drawText(text, fontSize, x, y, color = Color.black()){
  drawContext.setFont(Font.boldSystemFont(fontSize))
  drawContext.setTextColor(color)
  drawContext.drawText(new String(text).toString(), new Point(x, y))
}

function drawImage(image, x, y){
  drawContext.drawImageAtPoint(image, new Point(x, y))
}

function drawTextC(text, fontSize, x, y, w, h, color = Color.black()){
  drawContext.setFont(Font.boldSystemFont(fontSize))
  drawContext.setTextColor(color)
  drawContext.drawTextInRect(new String(text).toString(), new Rect(x, y, w, h))
}

function drawLine(x1, y1, x2, y2, width, color){
  const path = new Path()
  path.move(new Point(x1, y1))
  path.addLine(new Point(x2, y2))
  drawContext.addPath(path)
  drawContext.setStrokeColor(color)
  drawContext.setLineWidth(width)
  drawContext.strokePath()
}

function shouldRound(should, value){
  return ((should) ? Math.round(value) : value)
}

function isSameDay(date1, date2){
  return (date1.getYear() == date2.getYear() && date1.getMonth() == date2.getMonth() &&  date1.getDate() == date2.getDate())
}

// SFSymbol function
function symbolForCondition(cond){
  let symbols = {
  // Thunderstorm
    "2": function(){
      return "cloud.bolt.rain.fill"
    },
  // Drizzle
    "3": function(){
      return "cloud.drizzle.fill"
    },
  // Rain
    "5": function(){
      return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill"
    },
  // Snow
    "6": function(){
      return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow"
    },
  // Atmosphere
    "7": function(){
      if (cond == 781) { return "tornado" }
      if (cond == 701 || cond == 741) { return "cloud.fog.fill" }
      return night ? "cloud.fog.fill" : "sun.haze.fill"
    },
  // Clear and clouds
    "8": function(){
      if (cond == 800) { return night ? "moon.stars.fill" : "sun.max.fill" }
      if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" }
      return "cloud.fill"
    }
  }
  // Get first condition digit.
  let conditionDigit = Math.floor(cond / 100)
  // Style and return the symbol.
  let sfs = SFSymbol.named(symbols[conditionDigit]())
  sfs.applyFont(Font.systemFont(25))
  return sfs.image
}


Script.complete()

1 Like

Thanks, but I haven’t done nothing at all, this wouldn’t be without @egamez and @mzeryck.

Take a look at these lines for the styling and play with the digits in new Point(x, x)

// Position and size of the elements on the widget.
// All coordinates make reference to the top-left of the element.
// locationNameCoords : Point > Define the position in pixels of the location label.
const locationNameCoords = new Point(30, 20)
// locationNameFontSize : number > Size in pixels of the font of the location label.
const locationNameFontSize = 36
// locationNameCoords : Point > Define the position in pixels of the location label.
const tempCoords = new Rect((config.widgetFamily == "small") ? 150 : 450, 20, 100, 36)
// locationNameFontSize : number > Size in pixels of the font of the location label.
const tempFontSize = 36
// weatherDescriptionCoords : Point > Position of the weather description label in pixels.
const weatherDescriptionCoords = new Rect((config.widgetFamily == "small") ? 150 : 450, 62, 100, 36)
// weatherDescriptionFontSize : number > Font size of the weather description label.
const weatherDescriptionFontSize = 18
//footerFontSize : number > Font size of the footer labels (feels like... and last update time).
const footerFontSize = 20
//feelsLikeCoords : Point > Coordinates of the "feels like" label.
const feelsLikeCoords = new Point(30, 60)
//lastUpdateTimePosAndSize : Rect > Defines the coordinates and size of the last updated time label.
const lastUpdateTimePosAndSize = new Rect((config.widgetFamily == "small") ? 150 : 450, 230, 100, footerFontSize+1)
1 Like

Err, sorry. You use an enhanced version, please wait a minute…

1 Like

Thanks for looking into this for me. Yeah there’s some random stuff I added in that I don’t think has any function, hopefully, I didn’t make it too messy! I’m not too experienced with this stuff.

All I tried to do was edit the updated one you posted by adding the small section to add the date up top, make it a large widget, change the background, and reformat how the info is shown in terms of the weather (move feels like up, current temp + desc on the right side, and remove seconds from time updated).

Hmm not really clean but you might add a

widget.setPadding(0, 16, 30, 16) after let widget = ...

and then a widget.addSpacer(30) after your date text block to see if it fits your needs.

Edit: The setPadding pattern is (top, left, bottom, right) in pixels.

1 Like

Aha, that did it! Once I did that and changed new Point(x , x) and it’s perfect! Thank you, I spent way too long trying to figure this out myself I had to make a post for some assistance. I appreciate it :slight_smile:

Thank you @egamez and @mzeryck for this wonderful widget that I use daily now :blush:

Learning another program language is so neat!

added wind speed and cardinal arrows.
Wind gusts if there are any.
True or false to show HI and Low for the day instead of updated time.
Also Alerts image if there is an weather alert out. (I added this and saw you added same icon for cached weather. :joy: )
You can click alert icon and will take you to whatever webpage you put in to see the alerts.

Here’s a pic of the alerts before I edited more and switched over to SFSymbols.

P.S. Still learning forums and GitHub so I will let y’all know when I get it uploaded.

Thanks again for the wonderful weather widget :blush:

Here is the code. Hopefully this works.

3 Likes

may i have change the background image choice mode? because i want let the desk look colorful,but i don’t know how to do it…

First off, thanks for @mzeryck for sharing the “invisible” widget code to emulate transparent backgrounds.
Inspired by that code, I created a module to make it easier to pull the necessary wallpaper “slices”. The module handles both light and dark mode. As long as you make slices for both modes, your widgets will follow every time you switch modes.

How to Use

Setup

Download both scripts. Then run the No Background Config script. This should guide you on how to create the slices that can be used for your widgets.

Your Widget

There are different ways to get the wallpaper slice that you need to set for your widget background.

  1. By getting the actual image for a specific position.
const nobg = importModule('no-background.js')
const widget = new ListWidget();
widget.backgroundImage = await nobg.getSlice('small-top-left')

Valid slice names are:

  • small-top-left / small-top-right
  • small-middle-left / small-middle-right
  • small-bottom-left / small-bottom-right
  • medium-top / medium-middle / medium-bottom,
  • large-top / large-bottom.
  1. Getting the path of the image for a specific position.
const nobg = importModule('no-background.js')
const widget = new ListWidget();

const bgpath = nobg.getPathForSlice('small-top-left')
widget.backgroundImage = Image.fromFile(bgpath)
  1. By storing the position for a widget and pulling that using a widgetID as a key. The key can be the name of the script or maybe pass the parameter when adding the widget on your home screen.
const widgetID = "mywidget"
const nobg = importModule('no-background.js')

// store the widget position by calling this statement
// await nobg.chooseBackgroundSlice(widgetID)

const widget = new ListWidget();
widget.backgroundImage = await nobg.getSliceForWidget(widgetID)

You can find examples on the Github Repo.

Hope you find this useful.

12 Likes

Nice addition but have a look at the siplified SFSymol tweak again. Now we can use @egamez new for loop to get the ‘night’ bool and simplify all other SFSymbol related parts:

This is deprecated, we don’t need to call extra sunrise/sunset data:

// Prepare for the SFSymbol request by getting sunset/sunrise times.
const date = new Date()
const sunData = await new Request("https://api.sunrise-sunset.org/json?lat=" + LAT + "&lng=" + LON + "&formatted=0&date=" + date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate()).loadJSON();

We can grab the ‘night’ boolean straight from @egamez’s fix:

// 'Night' boolean for line graph and SFSymbols
var night = (hourData.dt > hourDay.sunset || hourData.dt < hourDay.sunrise)

Then this:

// The next three lines were modified for SFSymbol support.
const condition = i==0?weatherData.current.weather[0].id:hourData.weather[0].id
const condDate = i==0?weatherData.current.dt:hourData.dt
drawImage(symbolForCondition(condition,condDate), spaceBetweenDays * i + 34, 161 - (50*delta));

simplifies to this:

// Next 2 lines SFSymbols tweak 
const condition = i==0?weatherData.current.weather[0].id:hourData.weather[0].id
drawImage(symbolForCondition(condition), spaceBetweenDays * i + 34, 161 - (50*delta)); //40, 165

And this

// This function returns an SFSymbol image for a weather condition.
function symbolForCondition(cond,condDate) {
  // get sunrise and sunset
  const sunrise = new Date(sunData.results.sunrise).getTime()
  const sunset = new Date(sunData.results.sunset).getTime()
  const timeValue = condDate * 1000
  // Is it night at the provided date?
  const night = (timeValue < sunrise) || (timeValue > sunset)
  // Define our symbol equivalencies.
  let symbols = {
    // Thunderstorm
    "2": function() {
      return "cloud.bolt.rain.fill"
    },
(...)

…turns into this and will always match the new fixed line-graph values:

// SFSymbol function
function symbolForCondition(cond){
  // Define our symbol equivalencies.
  let symbols = {
  // Thunderstorm
    "2": function(){
      return "cloud.bolt.rain.fill"
    },
(...)
2 Likes

Thank you very much,may i have the small-bottom-right widgets complete code,(also to thanks for @eqsOne for sharing the widget code )i like it very much,image ,i want make it’s background change

do you need it for the weather widget? if so do this:

  widget.backgroundColor = backgroundColor;  // <-- look for this line

  // add these lines
  var bgImage = await nobg.getSlice('small-bottom-right')
  if (bgImage) {
    var rect = new Rect(0,0,(config.widgetFamily == "small") ? contextSize : mediumWidgetWidth, contextSize)
    drawContext.drawImageInRect(bgImage,rect)
  }

but if you need it for another widget, use this code

const nobg = importModule('no-background.js')
const widget = new ListWidget();
widget.backgroundImage = await nobg.getSlice('small-bottom-right')

// widget content code goes here
widget.addSpacer()
let text = widget.addText('Hello Scriptable')
text.textColor = Color.white()
text.centerAlignText()
widget.addSpacer()
// end of widget content code

if (config.runsInWidget) {
  Script.setWidget(widget)
} else {
  await widget.presentSmall()
}
Script.complete()
1 Like

Thank you for your help😊
I will go back and edit it later on in evening.
Have a great day :grin:

Hey eqsOne, thank you for sharing your Code! I wanted to adopt this to our City and found the following page:

Here you can add the ID (for Example Mainz ist 163) of your City an get the numbers you need. But I don’t know how to get the Data from the tr / td: Bildschirmfoto 2020-10-21 um 18.03.49|308x62
Can you help me? So we can use the Script for every City / State.

Thank you :slight_smile:

yep,thank you very much` i just add the code for the weather widegt,but it can’t work,

,if you have all compelete code ,may i have one all?

there are so many versions of the weather script, I’m not sure which one you use.
to fix your error, add this line on the top of the script

const nobg = importModule('no-background.js')