Widget Markup Library

Hi all, I spent a few hours hacking away a simple widget markup library for Scriptable . Basically its a different way of making widgets. Its like html tags for widgets. I found that when I build a widget this way in scriptable I can visualize the structure of the widget better. Is anyone else interested in building widgets this way? Or does something similar already exists for Scriptable?

Currently the library supports the most basic functionality in structuring/styling widgets. The markup language currently just supports the following tags:

  • <widget />
  • <stack />
  • <spacer />
  • <text />
  • <image />
  • <date />

Here is a sample code using the library I built:

const { widgetMarkup, concatMarkup } = importModule('WidgetMarkup');

let startColor =  Color.orange(),
    endColor = new Color('#880ED4');
const gradient = new LinearGradient();
gradient.colors = [startColor, endColor];
gradient.locations = [0.0, 1];

const questionListStyles = {
    '*rightAlignText': null,
    font: Font.lightSystemFont(15),
    textColor: new Color("#ffffff")
};
const leftStackStyles = {
    '*layoutVertically': null,
    size: new Size(183, 20)
};
const rightStackStyles = {
    '*layoutVertically': null,
    size: new Size(120, 20),
    '*setPadding': [5, 9, 5, 5],
    cornerRadius: 10,
    backgroundColor: Color.yellow()
};
const rightStackTextStyles = {
    '*centerAlignText': null,
    font: Font.lightSystemFont(10),
    textColor: new Color('#A020F0')  
};
const questions = [
    'Is this in my control?',
    'Is this essential?',
    'Do I really need to say it?',
    'Does this make me a better person?'
];

const widget = await widgetMarkup`
  <widget styles="${{backgroundGradient: gradient}}">
    <spacer value="10" />
    ${questions.map((q) => concatMarkup`<text styles="${questionListStyles}">${q}</text>`)}
    <spacer value="20" />
    <stack styles="${{ '*layoutHorizontally': null }}">
        <stack styles="${leftStackStyles}">
            <text styles="${{ font: Font.semiboldSystemFont(18) }}">
                ${(new Date()).toString().replace(/\s\d\d?:.+/ig, " 🚀")}
            </text>
        </stack>
        <stack styles="${rightStackStyles}">
            <text styles="${rightStackTextStyles}">www.rafaelgandi.com</text>
        </stack>
    </stack>
    <spacer value="15" />
  </widget>
`;

if (config.runsInWidget) {
    Script.setWidget(widget);
}
else {
    widget.presentMedium();
}
Script.complete();

And the above code produces this widget:

Not the most useful or good looking widget in the world :sweat_smile:

Any feedback is greatly appreciated.

5 Likes

Interesting! I’d love to see how you implemented this.

Here is a gist of my complete implementation of the library. Have not gotten around to making a public repo for it as I am still wondering if the community would be interested in something like it.

1 Like

This is fantastic. I like making widgets, but I find the process for placing and styling elements to be tedious. This strikes me as much more intuitive, at least for this someone who has been writing html a lot longer than javascript.

2 Likes

Agreed. I also find writing html/markup much more intuitive when structuring layout and styles.

Here’s a re-implementation of the Random Scriptable API widget using the markup library.

// This script shows a random Scriptable API in a widget. The script is meant to be used with a widget configured on the Home Screen.
// You can run the script in the app to preview the widget or you can go to the Home Screen, add a new Scriptable widget and configure the widget to run this script.
// You can also try creating a shortcut that runs this script. Running the shortcut will show widget.

const { widgetMarkup, concatMarkup } = importModule('WidgetMarkup');

async function randomAPI() {
    let docs = await loadDocs()
    let apiNames = Object.keys(docs)
    let num = Math.round(Math.random() * apiNames.length)
    let apiName = apiNames[num]
    let api = docs[apiName]
    return {
        name: apiName,
        description: api["!doc"],
        url: api["!url"]
    }
}

async function loadDocs() {
    let url = "https://docs.scriptable.app/scriptable.json"
    let req = new Request(url)
    return await req.loadJSON()
}

async function loadAppIcon() {
    let url = "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/21/1e/13/211e13de-2e74-4221-f7db-d6d2c53b4323/AppIcon-1x_U007emarketing-0-7-0-85-220.png/540x540sr.jpg"
    let req = new Request(url)
    return req.loadImage()
}

async function createWidget(api) {
    let appIcon = await loadAppIcon()
    let title = "Random Scriptable API"
    // Add background gradient
    let gradient = new LinearGradient()
    gradient.locations = [0, 1]
    gradient.colors = [
        new Color("141414"),
        new Color("13233F")
    ]
    const styles = {
        widget: {
            backgroundGradient: gradient
        },
        appIconImage: {
            imageSize: new Size(15, 15),
            cornerRadius: 4
        },
        titleText: {
            textColor: Color.white(),
            textOpacity: 0.7,
            font: Font.mediumSystemFont(13)
        },
        nameText: {
            textColor: Color.white(),
            font: Font.boldSystemFont(18)
        },
        descriptionText: {
            minimumScaleFactor: 0.5,
            textColor: Color.white(),
            font: Font.systemFont(18)
        },
        linkStack: {
            '*centerAlignContent': null,
            url: api.url
        },
        linkText: {
            font: Font.mediumSystemFont(13),
            textColor: Color.blue()
        },
        linkImage: {
            imageSize: new Size(11, 11),
            tintColor: Color.blue()
        },
        docsImage: {
            imageSize: new Size(20, 20),
            tintColor: Color.white(),
            imageOpacity: 0.5,
            url: "https://docs.scriptable.app"
        }
    };

    const widget = await widgetMarkup`
    <widget styles="${styles.widget}">
        <stack id="titleStack">
            <image src="${appIcon}" styles="${styles.appIconImage}" />
            <spacer value="4" />
            <text styles="${styles.titleText}">${title}</text>
        </stack>
        <spacer value="12" />
        <text styles="${styles.nameText}">${api.name}</text>
        <spacer value="2" />
        <text styles="${styles.descriptionText}">${api.description}</text>
        ${(() => {
            if (!config.runsWithSiri) {
                let linkSymbol = SFSymbol.named("arrow.up.forward");
                let docsSymbol = SFSymbol.named("book");
                return concatMarkup`
                    <spacer value="8" />
                    <stack id="footerStack">
                        <stack styles="${styles.linkStack}">
                            <text styles="${styles.linkText}">Read more</text>
                            <spacer value="3" />
                            <image src="${linkSymbol.image}" styles="${styles.linkImage}" />
                        </stack>
                        <spacer value="0" />
                        <image src="${docsSymbol.image}" styles="${styles.docsImage}" />
                    </stack>
                `;
            }
            else { return ''; }
        })()}
    </widget>
    `;
    return widget
}

let api = await randomAPI()
let widget = await createWidget(api)
if (config.runsInWidget) {
    // The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen.
    Script.setWidget(widget)
} else {
    // The script runs inside the app, so we preview the widget.
    widget.presentMedium()
}
// Calling Script.complete() signals to Scriptable that the script have finished running.
// This can speed up the execution, in particular when running the script from Shortcuts or using Siri.
Script.complete()

Also made a few bug fixes and updated the library’s gist. :blush:

1 Like

This looks great - could you provide more comments in the sample files and/or more detailed docs on how to get started?

Hello… Yep I will be working on the documentation for the library this weekend.

Hi all, for anyone who is interested I have made a public repo for this library and added a few documentation about it here: GitHub - rafaelgandi/WidgetMarkup-Scriptable: Simple implementation of markup for Scriptable widgets.

Cheers :blush:

5 Likes