Widget Examples

Hmm, personally I don’t care too much about displaying wind values but I think I’d prefer using an SFSymbol as well. Of cause there’s more than one way, by now I couldn’t find how to set up image roation within Scriptable, maybe someone else could help with that.

Here’s a small topic-related example to play with. It calls windpeed and degrees, displays the SFSymbol ‘location.north’ and adds it to a stack, rotate function still missing:

// Wind Arrow - R&D

const widget = new ListWidget()
const row = widget.addStack()
const API_KEY = "add_your_key_here"
var LAT = 52.45 // change to your latitude
var LON = 13.29 // change to your longitude
const locale = "en"
const units = "metric"
try {
  weatherData = await new Request("https://api.openweathermap.org/data/2.5/onecall?lat=" + LAT + "&lon=" + LON + "&exclude=daily,minutely,alerts&units=" + units + "&lang=" + locale + "&appid=" + API_KEY).loadJSON();
}catch{}

var windSpeed = weatherData.current.wind_speed
var windDeg = weatherData.current.wind_deg

log(windSpeed)
log(windDeg)

row.addText(windSpeed + " ")
windSpeed.textColor = Color.darkGray()
windSpeed.font = Font.systemFont(20)

var stack = row.addStack()
stack.setPadding(4, 4, 0, 0)
// stack.layoutHorizontally()
// stack.centerAlignContent()

// setup SFSymbol, pattern: (SFSymbol name, stack name, color, size, wind_deg number)
addSFS('location.north', stack, Color.darkGray(), 14, windDeg)

widget.presentSmall()

// SFSymbol function
function addSFS(sfs, stk, col, sze, deg) {
  const s = SFSymbol.named(sfs)
  s.applyFont(Font.systemFont(64))
//   s.transform = "rotate(45deg)" // doesn't do
//   deg still to add somewhere
  const a = stk.addImage(s.image)
//   a.transform = "rotate(45deg)" // doesn't do
  a.tintColor = col
  a.imageSize = new Size(sze, sze)
}

Works fine thanks! But without the rotation is not much useful :sweat_smile: unfortunately there is no location.south/west/etc… SFSymbol. With other directions symbols it would be much easy.

Anyway I changed some fonts and alignment because with all of these functions, the footer font was to big with 20pt, I set 18pt, bigger and upper city name also:

// 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, 26)
// locationNameFontSize : number > Size in pixels of the font of the location label.
const locationNameFontSize = 26
// 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 = 18
//feelsLikeCoords : Point > Coordinates of the "feels like" label.
const feelsLikeCoords = new Point(28, 230)

I thinks now the big (4x2) widget looks a bit better, with the small one (2x2) I’m only using the feels like + relative humidity.

Yep, I am aware of this bug (the line color), and already fixed it but I haven’t uploaded the fix, I will do it in a moment.

Wow, I’m somewhat curious to see how you attempted that. This almost made my head blast.

I have some kind-of-fix with altered sunrise/sunset values to use after sunset. But I think this will fail in summer, when the next sunrise date appears on the line graph before the current day’s sunset passed. Tricky topic, so easy to overlook logical details…

@egamez Well wow. That is some elegant code.

Edit: &exclude=daily :see_no_evil:

Updated the weather widget on the Gist.

Added:

  • Shows an :warning: symbol when there is not internet connection when trying to update.
  • Displays the last updated time label in yellow (to match the symbol color) when unable to update (offline) the weather to draw focus on it, indicating that there was a problem fetching the latest weather.
    caution
    Both the time color and the symbol are customizable in the code.

Fixed:

  • The weather graph after midnight, shows all hours as daytime color (orange), and before midnight it doesn’t show the next day daytime color.

    I have been testing it for several days and it worked flawlessly :ok_hand:.

I wanted to draw the offline symbol with an SFSymbol, but when drawing the symbol image into a DrawContext, it shows black, and can’t be tinted, only when added directly to the ListWidget you can change the color (@simonbs :pray:), and I was waiting for a way to do it, to release this fix update.
I didn’t saw anybody noticing the line color bug tho, until now @eqsOne, so here is it :blush:.

6 Likes

@egamez Awesome code. Thanks for sharing!

1 Like

To those who prefer the SFSymbol version, here’s the beauty with an adapted version of @mzeryck’s tweak applied.

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

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

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

/** 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
drawContext.setTextAlignedCenter()

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();
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();
  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))
  }
  
  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(), 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)

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

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()
4 Likes


I don’t know what’s happening

Really good job. Thanks for sharing!

1 Like

Missing API key in thge beginning of the script :smiley:

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