Multiple widget parameters

This is more of a feature request, but I would love to be able to set multiple separate parameters on a widget. Would that be possible?

I know I could ask users to comma separate their parameters but I think that would be problematic. Has anyone else solved this in a good way?

An example for using this could be a Youtube channel statistics widget (something that I’m working on at the moment) where you can pass in the channel ID and some other options to show/hide information on the widget but be able to change those options each time the widget is used, for example when you have more than one Youtube channel that you want stats for.

The alternative is to duplicate the widget script and change some variables within each script…

/cc @simonbs

As a workaround, could your script recognize each parameter by its structure (RegEx)? Then you could have a single script, and users could create multiple widgets all attached to the same script, but get different results by passing different parameters.

If the widget contains the necessary logic to handle any combination of settings, why not use an object to hold those settings and then have the widget reference that. You could also make your own object that has a widget creation method and self references its own properties.

1 Like

And if the goal is to make a script others can use, a setup script or shortcut could prompt them for that data, then save it as a JSON object in a file somewhere (along with a flag telling the script not to ask the setup questions again).

1 Like

That sounds interesting, I would never have thought of that. Are there any existing scripts that do something similar?

Sorry, I haven’t done something like this on javascript yet. I have an idea for one I might try that would take one or more calendar names as parameters; if I do that I’ll try to remember to post it here.

No worries, I think a simple version would be for users to create a config-n.js file for each instance of the widget that they want and then add its name in the widget parameter input.

Hey, first post here so let me know if there’s a better way to share scripts, but I figured since I was working on something like this I could share what I’ve done so far. Definitely still WIP but it’s good enough to be usable so maybe some ideas will be useful.

First, the widget. It accepts text that it simply displays, but it also optionally allows for the addition of json at the beginning to change settings. So you could have the parameter be

Display this text

or it could be

{“fontSize”:8}Display lots of text

or more complicated

{“fontSize”:11,“fontColor”:“#c2c2c2”,“bgColor”:“#004d65”,“bgGradient”:“37”}Multiple\nLine\nText

let text = args.widgetParameter

let widgetConfig = {
    fontSize:12,
    fontColor:"#ffffff",
    bgColor:"#111111",
    bgGradient:"88"
}

if (text == null) {
  text = "Change the widget parameter to change this text. Use the TextWidgetSetup script to change additional settings"
}

let paramConfig = text.match("^\\{.*\\}")
console.log(paramConfig)
if (paramConfig != null) {
  paramConfig = paramConfig[0]
  let paramConfigJ = JSON.parse(paramConfig)
  copyIfNotNull(widgetConfig, paramConfigJ, "fontSize")
  copyIfNotNull(widgetConfig, paramConfigJ, "fontColor")
  copyIfNotNull(widgetConfig, paramConfigJ, "align")
  copyIfNotNull(widgetConfig, paramConfigJ, "bgColor")
  copyIfNotNull(widgetConfig, paramConfigJ, "bgGradient")
  text = text.substring(paramConfig.length)
}

text = text.replaceAll("\\n", "\n")

let widget = await createWidget()
// Check if the script is running in
// a widget. If not, show a preview of
// the widget to easier debug it.
if (!config.runsInWidget) {
  await widget.presentSmall()
}
// Tell the system to show the widget.
Script.setWidget(widget)
Script.complete()

async function createWidget() {
  let gradient = new LinearGradient()
  gradient.locations = [0, 1]
  let cTop = widgetConfig.bgColor
  let cBottom = cTop+widgetConfig.bgGradient
  gradient.colors = [
    new Color(cTop),
    new Color(cBottom)
  ]
  
  let w = new ListWidget()
  w.backgroundGradient = gradient
  w.addSpacer()
  let contents= w.addText(text)
  contents.font = Font.lightRoundedSystemFont(widgetConfig.fontSize)
  contents.textColor = new Color(widgetConfig.fontColor)
  
  w.addSpacer()
  //top, leading, bottom, trailing
  w.setPadding(0, 8, 0, 8)
  return w
}

function copyIfNotNull(to,from,prop) {
  let fromProp = from[prop]
  if (fromProp != null) {
    to[prop] = fromProp
  }
}

Then I made this (still very rough) UI script that allows generating the whole parameter with all configurable values


let webView = new WebView()
webView.loadHTML(getHtml())
webView.present(true)

function getHtml() {
  return `
<!DOCTYPE html>
<html>
<head>
  <title>Color Picker</title>
<style>
body {
  background-color: #000000;
  font-family: HelveticaNeue-Bold;
  color: white;
}

label {
	font-size: 40pt;
}
input {
  font-size: 40pt;
}

#backgroundgradient {
  background-color: blue;
}

.textinput {
  font-size: 40pt;
  background-color: #555555;
  color: #ffffff;
}

.colorinput {
	height: 40pt;
	width: 40pt;
}

.rangewrapper {
  background-color: #555555;
  display: inline-block;
}

.rangeinput {
  height: 40pt;
}

.colorwrapper {
  background-color: #555555;
  display: inline-block;
}
</style>
</head>
<body>
<div>
  <label for="contentsinput">Contents:</label><br>
  <textarea id="contentsinput" class="textinput" oninput="update()" onpropertychange="update()"></textarea>
</div>

<div>
  <label for="fontsize">Font size:</label>
  <input type="number" class="textinput" min="1" max="1000" id="fontsize" oninput="updateFontSize()" onpropertychange="updateFontSize()">
</div>

<div>
  <label for="fontcolor">Font color: </label>
  <span class=colorwrapper>
    <input type="color" class="colorinput" id="fontcolor" onchange="updateFontColor()" value="#ffffff">
  </span>
</div>

<div>
  <label for="backgroundcolor">Background color: </label>
  <span class=colorwrapper>
    <input type="color" class="colorinput" id="backgroundcolor" onchange="updateBackgroundColor()" value="#000000">
  </span>
</div>

<div>
  <label for="backgroundgradient">Background gradient: </label>
  <span class=rangewrapper>
    <input type="range" class="rangeinput" id="backgroundgradientrange" min="0" max="255" oninput="updateBackgroundGradientRange()" value="0">
  </span>
  <input type="number" class="textinput" min="0" max="255" id="backgroundgradientdisplay" oninput="updateBackgroundGradientDisplay()" onpropertychange="updateBackgroundGradientDisplay()">
</div>

<div>copy the entire text below and paste into the widget parameter for the TextWidget:</div>
<div class="result" id="result"></div>

</body>
<script>
  function updateFontSize() {
    document.getElementById('fontsize').classList.add('edited')
    update()
  }
  function updateFontColor() {
    document.getElementById('fontcolor').classList.add('edited')
    update()
  }
  function updateBackgroundColor() {
    document.getElementById('backgroundcolor').classList.add('edited')
    update()
  }
  function updateBackgroundGradientRange() {
    document.getElementById('backgroundgradientdisplay').classList.add('edited')
    document.getElementById('backgroundgradientdisplay').value = document.getElementById('backgroundgradientrange').value
    update()
  }
  function updateBackgroundGradientDisplay() {
    let display = document.getElementById('backgroundgradientdisplay')
    let range = document.getElementById('backgroundgradientrange')
    display.classList.add('edited')
    if (display.value == ""){
      range.value = 0
    } else {
      range.value = display.value
    }
    update()
  }
  function update() {
    let valToCopy = "";
    valToCopy = addPropertyToJson(valToCopy, 'fontsize', 'fontSize')
    valToCopy = addPropertyToJson(valToCopy, 'fontcolor', 'fontColor', addQuotes)
    valToCopy = addPropertyToJson(valToCopy, 'backgroundcolor', 'bgColor', addQuotes)
    valToCopy = addPropertyToJson(valToCopy, 'backgroundgradientdisplay', 'bgGradient', convertGradient)
    if (valToCopy != "") {
      valToCopy +='}'
    }
    valToCopy += document.getElementById('contentsinput').value.replaceAll("\\n", "\\\\n")
    document.getElementById('result').innerHTML = valToCopy
  }
  function convertGradient(gradient) {
    gradient = parseInt(gradient)
    gradient = 255 - gradient
    return addQuotes(gradient.toString(16))
  }
  function addQuotes(value) {
    return '"' + value + '"'
  }
  function addPropertyToJson(currentJson, tagId, propertyName, modification) {
    let htmlTag = document.getElementById(tagId)
    if (!htmlTag.classList.contains('edited')) {
      return currentJson
    }
    let value = htmlTag.value
    if (value == "") {
      return currentJson
    }

    if (modification != null) {
      value = modification(value)
    }
    let toAdd = '"'+ propertyName + '":' + value

    if (currentJson == "") {
      toAdd = "{" + toAdd
    } else {
      toAdd = "," + toAdd
    }
    return currentJson + toAdd
  }
</script>
</html>
  `
}

It’s similar to the previous suggestion, using one script to create a configure some settings and another script to display the widget, but this uses copy/paste instead of saving/retrieving a file. But I think the principle is similar and adaptable and hopefully this gives some practical ideas for getting through the main hurdles of setting things up to parse json from the widget parameter, have default values, etc. But yeah a built-in way to specify that a widget accepts multiple parameters for the user to input would be nice for sure.

As far as I can tell Apple doesn’t allow developers to support a variadic number of parameters, e.g. I can’t add an “Add parameter” button that would add a new parameter below the existing one. Therefore I would need to choose the maximum number of parameters to show and always have those text fields shown in the widget configurator.

I chose to limit it to one parameter for now and let it be up to the user to define a format where multiple parameters can be encoded into a single parameter.

2 Likes