Widget Examples

I’ve gotten that feedback a couple times - I should’ve mentioned that originally! I edited my post to make it more clear. (That feature has always made me a bit sick to my stomach… so I always turn it off and forget about it!)

1 Like

I copied and pasted this in and it worked perfectly! Thank you so so much!! :slight_smile:

1 Like

@egamez I love your widget, but the colour orange doesn’t go good on my home screen so well, so I decided to change to hex colour values. But I can’t find the place to change the colour values of the sun. I would like to make it yellow instead of orange. I also need a little help getting that text under the location to be a bit below. Sorry, I’m just not good at coding, so I just try finding words that make sense and changing them, lol.

the weather icons are actually images that come “ready-made” from open weather map, so there’s no simple way to change how they look unfortunately.

1 Like

You can tweak the positioning slightly to add some space by editing the following line. The second number is the vertical orientation (original code used 52). The following works well for my devices…


const weatherDescriptionCoords = new Point(30, 60)

1 Like

I am currently working on a weather widget, and I’ve used the new SFSymbol support in the current Scriptable beta (version 1.5.1) to write a function that correlates weather conditions to SFSymbols. I added my function to @egamez’s code and made a couple slight modifications. It worked pretty well for me!


Here’s the full code:

// 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" : "37.32" , "LON" : "-122.03" , "LOC_NAME" : "Cupertino, US" }')

// 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 = ""

// 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).
const LAT = widgetParams.LAT
const LON = widgetParams.LON
const LOCATION_NAME = widgetParams.LOC_NAME

// 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.

// 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"
// 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;

// 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, 30)
// locationNameFontSize : number > Size in pixels of the font of the location label.
const locationNameFontSize = 24
// weatherDescriptionCoords : Point > Position of the weather description label in pixels.
const weatherDescriptionCoords = new Point(30, 52)
// 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, 230)
//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)

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

//From here proceed with caution.
let fm = FileManager.iCloud();
let cachePath = fm.joinPath(fm.documentsDirectory(), "weatherCache");
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
drawContext.setTextAlignedCenter()

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

let widget = new ListWidget();
widget.setPadding(0, 0, 0, 0);
widget.backgroundColor = backgroundColor;

drawText(LOCATION_NAME, locationNameFontSize, locationNameCoords.x, locationNameCoords.y, accentColor);
drawText(weatherData.current.weather[0].description, weatherDescriptionFontSize, weatherDescriptionCoords.x, weatherDescriptionCoords.y, Color.white())

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();
  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)
  drawLine(spaceBetweenDays * (i) + 50, 175 - (50 * delta),spaceBetweenDays * (i+1) + 50 , 175 - (50 * nextDelta), 4, (hourData.dt > weatherData.current.sunset? Color.gray():accentColor))
  
  drawTextC(shouldRound(roundedTemp, temp)+"°", 20, spaceBetweenDays*i+30, 135 - (50*delta), 50, 21, Color.white())
  
  // 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 + 40, 165 - (50*delta));
  
  drawTextC((i==0?"Now":hour), 18, spaceBetweenDays*i+25, 200,50, 21, Color.gray())
  
  previousDelta = delta;
}

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

drawContext.setTextAlignedRight();
drawTextC(epochToDate(weatherData.current.dt).toLocaleTimeString(), footerFontSize, lastUpdateTimePosAndSize.x, lastUpdateTimePosAndSize.y, lastUpdateTimePosAndSize.width, lastUpdateTimePosAndSize.height, Color.gray())

widget.backgroundImage = (drawContext.getImage())
widget.presentMedium()

async function loadImage(imgName){
  if(fm.fileExists(fm.joinPath(cachePath, imgName))){
    return Image.fromData(Data.fromFile(fm.joinPath(cachePath, imgName)))
  }else{
    let imgdata = await new Request("https://openweathermap.org/img/wn/"+imgName+".png").load();
    let img = Image.fromData(imgdata);
    fm.write(fm.joinPath(cachePath, imgName), imgdata);
	return img;
  }
}

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

// This function returns an SFSymbol image for a weather condition.
function symbolForCondition(cond,condDate) {

  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"
    },
    
    // 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"
    }
  }
  
  // Find out the first digit.
  let conditionDigit = Math.floor(cond / 100)
  
  // Get the symbol.
  return SFSymbol.named(symbols[conditionDigit]()).image
  
}

Script.complete()
5 Likes

I pasted the code but it’s throwing an error on line 248:18 Reference Error Can’t find variable SFSymbol. Am I doing something wrong?

Fixed it. Sorry for the earlier question.

The two different weather widgets are showing different data’s each. The original one says it’ll be cloudy with a little sunshine, and the other says it’ll rain. Lol. But thanks for the help!

Is it possible to make a widget which opens an app through its URL scheme, covered by an clickable button image? If I were to use shortcuts, covering it with an image wouldn’t be possible… I REALLY wish I knew how to code at this point.

That is possible in version 1.5.1, which is currently in beta through TestFlight but should be out within a week or so.

I have posted an example of a launcher widget on the link below. It requires version 1.5.1.

Is there any way to have the SF Symbols bigger? They look so small that it is difficult to see if there is rain or just clouds

Yes. Just set the size on the WidgetImage.

let sym = SFSymbol.named("tortoise")
sym.applyFont(Font.systemFont(64))
let w = new ListWidget()
let wimg = w.addImage(sym.image)
wimg.imageSize = new Size(100, 100)
w.presentSmall()

For the weather widget, I’m getting this error after pasting and running the script. Any help to fix this would be appreciated. Thanks.

Error on line 105:29: TypeError: undefined is not an object (evaluating ‘weatherData.current.weather’)
[/quote]

1 Like

Could you please provide the parameters for it?

All I really want to do is make a widget for AdGuard. I’m surprised how hard it is.

what kind of widget ?

Wow :exploding_head:, clever use of SFSymbols!, I was aware that custom icons are possible, but editing the cache files, making “icon packs”, but this is better I think, maybe I will merge this if you don’t mind.
I was waiting for the SFSymbols update to include some offline mode indicator icon, but never though of this use.
Anwering @TECKBAT, the colors of the icons are setted by OpenWeather, that’s why I choose orange as the default accent color :sweat_smile:, If you want to change the icons, just edit the files in the cache folder, add “.png” to set the extension and you could edit them as normal png images, you can see the list of the icons here: https://openweathermap.org/weather-conditions.
To move the weather description label edit weatherDescriptionCoords (line 61), where the second parameter of Point is the Y coordinate (52 by default), just increase it to lower the text.

Sorry for being absent, I had a busy week, I’m aware of some bugs tho that I need to fix first.
I never thought I would receive so many contributions, having in consideration all of them!
Thanks!

3 Likes

If I use the SFSymbol as a background image for a widget it looks black, is there a way to specify the color? I tried to specify the font color and the tint color.

Is it possible to change the display of a widget based on if dark mode is enabled?

I’ve tested with this:

Copy to clipboard

  if (Device.isUsingDarkAppearance()) {
    w.backgroundColor = Color.black()
  } else {
    w.backgroundColor = Color.white()
  }

It displays correctly when testing with .presentSmall() (or medium or large) but does not change on the homescreen widget. It will show just the dark condition even when creating a new widget when dark mode is disabled.

1 Like