Widget Examples

Actually, it works pretty well if you add a supported (by open weather) locale like French (fr)

We could maybe use Device.language() or .locale() and “validate” from https://openweathermap.org/current#multi with a fallback to en

I know it works, I’ve it in Portuguese but you need to change other values
Now and feels like (if you are a perfectionist :rofl::rofl:),

I have implemented it using const I can share the code if you want

Cheers

The code:

Add:

// Support locales
const locale = “en”
const nowstring = “now”
const feelsstring = "feels like "
// …

Change:

drawTextC((i==0?”now”:hour), (i==0?17: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())

To:

drawTextC((i==0?nowstring:hour), (i==0?17: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())

——

This (i==0?17:18) on the font size is to fix the issue on the Portuguese word (5 chars and 3 chars on the en word)

apple already does that, check out their photos widget, it allows you to select a folder

are you on 14.2b? I don’t see this option in 14.1

Hi, I am extremely new to this and really appreciate your script so much! I added riverwolf’s greeting script to where your code had said to add to it and when I finished running it, only the “invisible widget” showed up but without the good evening text. Any idea on what I can do to make it look like the screenshot you sent? Sorry, I am very new but really I’d really appreciate your help!

It says “Can not find variable “widget” when I attempt to run the script

Today is New Music Friday on Spotify.
And… → There’s a widget for that.

It shuffles through new album releases and opens them in Spotify.

Gist: https://gist.github.com/marco79cgn/509ad40296cc5b205d15283b23ed9d38#gistcomment-3475119

2 Likes

No need to be sorry! I’m not sure what happened there. I just tested this - it’s the complete widget code, you can copy and paste the entire thing into Scriptable. ( @Exhaustion this may help, as the code I posted earlier doesn’t work by itself.)

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

// This widget was created by Max Zeryck @mzeryck

// Widgets are unique based on the name of the script.
const filename = Script.name() + ".jpg"
const files = FileManager.local()
const path = files.joinPath(files.documentsDirectory(), filename)

if (config.runsInWidget) {
  let widget = new ListWidget()
  widget.backgroundImage = files.readImage(path)
  
  // You can your own code here to add additional items to the "invisible" background of the widget.
  // Get the current date
	const date = new Date()

	// Format the greeting (thank you riverwolf)
	let greeting = "Good "
	if (date.getHours() < 6) {
		greeting = greeting + "night."
	} else if (date.getHours() < 12) {
		greeting = greeting + "morning."
	} else if (date.getHours() < 17) {
		greeting = greeting + "afternoon."
	} else if (date.getHours() < 21) {
		greeting = greeting + "evening."
	} else {
		greeting = greeting + "night."
	}

	// Format the date
	let df = new DateFormatter()
	df.dateFormat = "EEEE, MMMM d"

	// Format the widget
	widget.addSpacer(40)

	let greetingText = widget.addText(greeting)
	greetingText.font = Font.boldSystemFont(36)
	greetingText.textColor = Color.white()

	let dateText = widget.addText(df.string(date))
	dateText.font = Font.regularSystemFont(18)
	dateText.textColor = Color.white()

	widget.addSpacer()
  
  Script.setWidget(widget)
  Script.complete()

/*
 * The code below this comment is used to set up the invisible widget.
 * ===================================================================
 */
} else {
  
  // Determine if user has taken the screenshot.
  var message
  message = "Before you start, go to your home screen and enter wiggle mode. Scroll to the empty page on the far right and take a screenshot."
  let exitOptions = ["Continue","Exit to Take Screenshot"]
  let shouldExit = await generateAlert(message,exitOptions)
  if (shouldExit) return
  
  // Get screenshot and determine phone size.
  let img = await Photos.fromLibrary()
  let height = img.size.height
  let phone = phoneSizes()[height]
  if (!phone) {
    message = "It looks like you selected an image that isn't an iPhone screenshot, or your iPhone is not supported. Try again with a different image."
    await generateAlert(message,["OK"])
    return
  }
  
  // Prompt for widget size and position.
  message = "What size of widget are you creating?"
  let sizes = ["Small","Medium","Large"]
  let size = await generateAlert(message,sizes)
  let widgetSize = sizes[size]
  
  message = "What position will it be in?"
  message += (height == 1136 ? " (Note that your device only supports two rows of widgets, so the middle and bottom options are the same.)" : "")
  
  // Determine image crop based on phone size.
  let crop = { w: "", h: "", x: "", y: "" }
  if (widgetSize == "Small") {
    crop.w = phone.small
    crop.h = phone.small
    let positions = ["Top left","Top right","Middle left","Middle right","Bottom left","Bottom right"]
    let position = await generateAlert(message,positions)
    
    // Convert the two words into two keys for the phone size dictionary.
    let keys = positions[position].toLowerCase().split(' ')
    crop.y = phone[keys[0]]
    crop.x = phone[keys[1]]
    
  } else if (widgetSize == "Medium") {
    crop.w = phone.medium
    crop.h = phone.small
    
    // Medium and large widgets have a fixed x-value.
    crop.x = phone.left
    let positions = ["Top","Middle","Bottom"]
    let position = await generateAlert(message,positions)
    let key = positions[position].toLowerCase()
    crop.y = phone[key]
    
  } else if(widgetSize == "Large") {
    crop.w = phone.medium
    crop.h = phone.large
    crop.x = phone.left
    let positions = ["Top","Bottom"]
    let position = await generateAlert(message,positions)
    
    // Large widgets at the bottom have the "middle" y-value.
    crop.y = position ? phone.middle : phone.top
  }
  
  // Crop image and finalize the widget.
  let imgCrop = cropImage(img, new Rect(crop.x,crop.y,crop.w,crop.h))
  
  message = "Your widget background is ready. Would you like to use it in a Scriptable widget or export the image?"
  const exportPhotoOptions = ["Use in Scriptable","Export to Photos"]
  const exportPhoto = await generateAlert(message,exportPhotoOptions)
  
  if (exportPhoto) {
    Photos.save(imgCrop)
  } else {
    files.writeImage(path,imgCrop)
  }
  
  Script.complete()
}

// Generate an alert with the provided array of options.
async function generateAlert(message,options) {
  
  let alert = new Alert()
  alert.message = message
  
  for (const option of options) {
    alert.addAction(option)
  }
  
  let response = await alert.presentAlert()
  return response
}

// Crop an image into the specified rect.
function cropImage(img,rect) {
   
  let draw = new DrawContext()
  draw.size = new Size(rect.width, rect.height)
  
  draw.drawImageAtPoint(img,new Point(-rect.x, -rect.y))  
  return draw.getImage()
}

// Pixel sizes and positions for widgets on all supported phones.
function phoneSizes() {
  let phones = {	
	"2688": {
			"small":  507,
			"medium": 1080,
			"large":  1137,
			"left":  81,
			"right": 654,
			"top":    228,
			"middle": 858,
			"bottom": 1488
	},
	
	"1792": {
			"small":  338,
			"medium": 720,
			"large":  758,
			"left":  54,
			"right": 436,
			"top":    160,
			"middle": 580,
			"bottom": 1000
	},
	
	"2436": {
			"small":  465,
			"medium": 987,
			"large":  1035,
			"left":  69,
			"right": 591,
			"top":    213,
			"middle": 783,
			"bottom": 1353
	},
	
	"2208": {
			"small":  471,
			"medium": 1044,
			"large":  1071,
			"left":  99,
			"right": 672,
			"top":    114,
			"middle": 696,
			"bottom": 1278
	},
	
	"1334": {
			"small":  296,
			"medium": 642,
			"large":  648,
			"left":  54,
			"right": 400,
			"top":    60,
			"middle": 412,
			"bottom": 764
	},
	
	"1136": {
			"small":  282,
			"medium": 584,
			"large":  622,
			"left": 30,
			"right": 332,
			"top":  59,
			"middle": 399,
			"bottom": 399
	}
  }
  return phones
}

Hey mzeryck,

Awesome script. I noticed the dimensions were a bit off on my 11 Pro. This looks good to my eye:

"2436": {
  "small": 465,
  "medium": 987,
  "large": 1035,
  "left": 68, // 69 original
  "right": 591,
  "top": 208, // 213 original
  "middle": 783,
  "bottom": 1353
},

Thank you! And do you have Perspective Zoom on? (where your wallpaper moves slightly as you move your phone) That feature messes with the effect. I ask because those values don’t fit on the iOS point grid.

I do!! That explains why I couldn’t make it work exactly correct.

Would reverting to your numbers and disabling that feature cause it to work correctly?

It appears that fixed it. You’re numbers are right :slight_smile:

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.