<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Shaun Hevey | Blog</title><description>Shaun Hevey&apos;s personal website and blog.</description><link>https://shaunhevey.com/</link><language>en-au</language><item><title>Fixing .NET SDK error when using GitHub actions</title><link>https://shaunhevey.com/posts/fixing-net-sdk-error-when-using-github-actions/</link><guid isPermaLink="true">https://shaunhevey.com/posts/fixing-net-sdk-error-when-using-github-actions/</guid><pubDate>Sun, 12 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have just started exploring GitHub actions, and in this post, I will talk about an error I came across trying to build a .NET application and how I fixed it.&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;I am not going to detail what GitHub actions are, but at a high level, they are a feature of GitHub that allows you to run other applications or scripts over a GitHub repo. These actions can be triggered manually by a user, or they can set up to start when a specific event occurs, an example being when you commit code and push it up to GitHub on a specific branch of your repo.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you would like to deep dive into GitHub Actions, you can read all about them over at the &lt;a href=&quot;https://docs.github.com/en/actions&quot;&gt;GitHub docs website&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The error&lt;/h2&gt;
&lt;p&gt;The error that I was getting when running any &lt;code&gt;dotnet&lt;/code&gt; command was the following.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1&amp;gt;/usr/share/dotnet/sdk/5.0.403/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(141,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 6.0.  Either target .NET 5.0 or lower, or use a version of the .NET SDK that supports .NET 6.0.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, the error is that the runner that my action was being executed on did not contain the correct version of .NET. The application I was trying to build was targeting .NET 6, and this is not currently installed by default on the hosted runners provided out of the box by GitHub.&lt;/p&gt;
&lt;p&gt;It turns out that GitHub has documented what is installed on the hosted runners that they provide, and you can find them &lt;a href=&quot;https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners&quot;&gt;here&lt;/a&gt; and as an example, the list of .NET SDKs that come preinstalled on the Ubuntu 20.04 runner &lt;a href=&quot;https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md#net-core-sdk&quot;&gt;here&lt;/a&gt; and at the time of writing it only goes up to .NET 5.0.403.&lt;/p&gt;
&lt;h2&gt;How to fix this error?&lt;/h2&gt;
&lt;p&gt;Reading through the documentation, GitHub actually &lt;a href=&quot;https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#using-preinstalled-software&quot;&gt;recommend&lt;/a&gt; the use of actions to interact with preinstalled software so that you can make sure the version you are expecting to be on the runner is available for you to use and that it won&apos;t change when GitHub update the runner images.&lt;/p&gt;
&lt;p&gt;With that in mind, there is an action that you can add to your action YAML definition that will install and select the version of .NET that will be used with the &lt;code&gt;dotnet&lt;/code&gt; command. You can add it to your own GitHub actions by using the following snippet. This snippet will need to be added to your action definition before using the &lt;code&gt;dotnet&lt;/code&gt; command to set up and select the correct version.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Setup dotnet
    uses: actions/setup-dotnet@v1
    with:
        dotnet-version: &apos;6.0&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see from the snippet above that it will make sure the .NET 6 SDK is available on the runner. This action can do a lot more than install a version of .NET, so be sure to check out its &lt;a href=&quot;https://github.com/actions/setup-dotnet&quot;&gt;GitHub repo&lt;/a&gt; for more information.&lt;/p&gt;
</content:encoded></item><item><title>First look at ASP.NET Core minimal API</title><link>https://shaunhevey.com/posts/first-look-at-aspnet-core-minimal-api/</link><guid isPermaLink="true">https://shaunhevey.com/posts/first-look-at-aspnet-core-minimal-api/</guid><pubDate>Sun, 28 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this post, I wanted to take a quick look at how you can get started with the newly released with .NET 6, which is ASP.NET Core minimal API. What is a minimal API, you might be wondering? Well, it is a new way to write your ASP.NET API endpoints with very little ceremony. &amp;lt;!--more--&amp;gt;Let&apos;s take a look at what I mean.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet(&quot;/&quot;, () =&amp;gt; &quot;Hello World!&quot;);
app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a fully complete Program.cs in .NET 6 using C# 10 and ASP.NET Minimal APIs. This was generated from the new template. If you want to follow along at home, you can run the command &lt;code&gt;dotnet new web -o MinimalAPI&lt;/code&gt; replace &lt;code&gt;MinimalAPI&lt;/code&gt; with the name of your application.&lt;/p&gt;
&lt;p&gt;This post isn&apos;t about that other feature that allows the code to be only four lines. As you can see, Microsoft has made many improvements across the board to remove some of the ceremony that is required to get started building applications with C# and .NET.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: if you want to put the using statements or the namespace back in, you can include them, and the compiler will still be happy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The out of the box template seen above creates an API that contains one GET endpoint that returns &quot;Hello World!&quot;. Let&apos;s take a quick look at this example.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.MapGet(&quot;/&quot;, () =&amp;gt; &quot;Hello World!&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As this example calls the &lt;code&gt;MapGet&lt;/code&gt; method, this will create a GET endpoint on your API. This method takes two parameters; the first is the route to add to your API (in this case, &apos;/&apos; is the base address), and the second is the code to invoke when a request is made to this API endpoint. In my mind, this is a pretty dull API so let&apos;s go about adding some of our endpoints and dive a little deeper into how minimal APIs work.&lt;/p&gt;
&lt;p&gt;I am going to demonstrate this by creating some developer tool-focused endpoints. I will add a short explanation and comments as required, but all of these examples will show you the code needed to add each of these types of endpoints. If you want to add each of these manually, replace the Program.cs from the template above with the following code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpsRedirection();

//Add endpoint code here

app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GET request&lt;/h2&gt;
&lt;p&gt;This simple endpoint returns a new guid as a string.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.MapGet(&quot;/guid&quot;, () =&amp;gt; Guid.NewGuid().ToString());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GET request with a parameter&lt;/h2&gt;
&lt;p&gt;For completeness, you are getting two endpoints this time. These endpoints are will either encode a string to base64 or decode the provided base64 string. These demonstrate the ability for you to add a query parameter to your GET requests.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//The {data} format in the string gets mapped to the data parameter
app.MapGet(&quot;/encode-base64/{data}&quot;, (string data) =&amp;gt;
{
    var bytesArray = System.Text.Encoding.UTF8.GetBytes(data);
    return Convert.ToBase64String(bytesArray);
});
//The {data} format in the string gets mapped to the data parameter

app.MapGet(&quot;/decode-base64/{data}&quot;, (string data) =&amp;gt;
{
    var base64EncodedBytes = Convert.FromBase64String(data);
    return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;POST request with extras&lt;/h2&gt;
&lt;p&gt;This endpoint shows you how to add another HTTP VERB, in this case, POST. It is also showing you how to inject built-in objects, as well as how to read the body of the request directly. This post endpoint will take any valid JSON and return it nicely formatted (I have excluded checking if the JSON is valid to keep the example small).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Other HTTP VERBs are available; check out the official docs linked at the end of the post to find out how to use them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;//The built-in HttpRequest object that ASP.NET creates when a request comes in can be given to your endpoint by just adding it as a parameter. From that request object, you can read the body. I have included the full namespaces on these objects, so the code isn&apos;t complicated with adding using, but you could easily add the correct Using statements and make this code easier to read.
app.MapPost(&quot;/format-json&quot;, async (HttpRequest request) =&amp;gt;
{
    using var reader = new StreamReader(request.Body, System.Text.Encoding.UTF8, true, 1024, true);
    var jDoc = System.Text.Json.JsonDocument.Parse(await reader.ReadToEndAsync());
    return System.Text.Json.JsonSerializer.Serialize(jDoc, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Injecting DI data into your endpoint&lt;/h2&gt;
&lt;p&gt;This example demonstrates how you can use the built-in DI container to inject objects into code at runtime. The endpoint should return the IP address that the request came from.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var builder = WebApplication.CreateBuilder(args);

//this line adds the required object with the correct lifetime scope to the DI container.
builder.Services.AddSingleton&amp;lt;IHttpContextAccessor, HttpContextAccessor&amp;gt;();

...

//To have ASP.NET inject the required object from the DI container, just add it as a parameter (in this example, it is IHttpContextAccessor).
app.MapGet(&quot;/current-ip&quot;, (IHttpContextAccessor request) =&amp;gt; request.HttpContext?.Connection?.RemoteIpAddress?.ToString());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;These are just some things you can achieve with minimal APIs if you want to dive deeper into what else can be achieved, including authentication and authorizing endpoints, returning different status codes, etc. Check out the &lt;a href=&quot;https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-6.0&quot;&gt;offical docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can find the complete code from the examples above right below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton&amp;lt;IHttpContextAccessor, HttpContextAccessor&amp;gt;();
var app = builder.Build();

app.UseHttpsRedirection();

app.MapGet(&quot;/guid&quot;, () =&amp;gt; Guid.NewGuid().ToString());
//The {data} format in the string gets mapped to the data parameter
app.MapGet(&quot;/encode-base64/{data}&quot;, (string data) =&amp;gt;
{
    var bytesArray = System.Text.Encoding.UTF8.GetBytes(data);
    return Convert.ToBase64String(bytesArray);
});
//The {data} format in the string gets mapped to the data parameter
app.MapGet(&quot;/decode-base64/{data}&quot;, (string data) =&amp;gt;
{
    var base64EncodedBytes = Convert.FromBase64String(data);
    return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
});
//The built-in HttpRequest object that ASP.NET creates when a request comes in can be given to your endpoint by just adding it as a parameter. From that request object, you can read the body. I have included the full namespaces on these objects, so the code isn&apos;t complicated with adding using, but you could easily add the correct Using statements and make this code easier to read.
app.MapPost(&quot;/format-json&quot;, async (HttpRequest request) =&amp;gt;
{
    using var reader = new StreamReader(request.Body, System.Text.Encoding.UTF8, true, 1024, true);
    var jDoc = System.Text.Json.JsonDocument.Parse(await reader.ReadToEndAsync());
    return System.Text.Json.JsonSerializer.Serialize(jDoc, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
});
//To have ASP.NET inject the required object from the DI container, just add it as a parameter (in this example, it is IHttpContextAccessor).
app.MapGet(&quot;/current-ip&quot;, (IHttpContextAccessor request) =&amp;gt; request.HttpContext?.Connection?.RemoteIpAddress?.ToString());

app.Run();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>How to use FFmpeg on iOS</title><link>https://shaunhevey.com/posts/how-to-use-ffmpeg-on-ios/</link><guid isPermaLink="true">https://shaunhevey.com/posts/how-to-use-ffmpeg-on-ios/</guid><pubDate>Sun, 07 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every once in a while, I am amazed by some fantastic developer that changes my mind about what I can achieve on my iPhone or iPad. This is usually because they have made an application that I would have thought would never make it through Apple and their reviewers. One of these developers is Nicolas Holzschuch, and the application they have created called &lt;a href=&quot;https://apps.apple.com/au/app/a-shell/id1473805438&quot;&gt;a-Shell&lt;/a&gt; &amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;What is a-shell and FFmpeg&lt;/h2&gt;
&lt;p&gt;a-Shell is a terminal-based application for iOS and iPadOS that allows you to run many applications in a terminal environment like Python, JavaScript, and several terminal commands that you typically expect in a terminal like ping, md5 and ssh. That isn&apos;t all it does but this post isn&apos;t about a-Shell, just one of the applications it comes with, FFmpeg. If you have not come across FFmpeg before, here is a little description from the &lt;a href=&quot;https://www.ffmpeg.org/about.html&quot;&gt;ffmpeg website&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In short, it allows you to convert between various audio and video formats, extract audio from video or even combine audio and video files together.&lt;/p&gt;
&lt;h2&gt;How to use FFmpeg in a-Shell&lt;/h2&gt;
&lt;p&gt;Ok, so how do you go about using FFmpeg in a-Shell? Well, it is as simple as opening a-Shell and typing the command &lt;code&gt;ffmpeg&lt;/code&gt;. You can see the output of the command in the image below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./how-to-use-ffmpeg-on-ios/ffmpeg-in-a-shell.png&quot; alt=&quot;FFmpeg in a-Shell&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That isn&apos;t very interesting and doesn&apos;t show you how to use FFmpeg with your files.&lt;/p&gt;
&lt;h3&gt;FFmpeg in a-Shell in-app example&lt;/h3&gt;
&lt;p&gt;Now you might be aware that each application on iOS is running in its own sandbox, this means they can&apos;t just go and get files from other applications, but Apple has created ways for applications to break out of their sandboxes in controlled ways with interaction from the user and a-Shell has these functions built-in, so let&apos;s take a look.&lt;/p&gt;
&lt;p&gt;As you can see in the image below, I have a folder that contains a &quot;gif&quot; file called &lt;code&gt;Test.gif&lt;/code&gt;, and I want to use FFmpeg to convert it to an mp4 file. As this folder is outside of a-Shell&apos;s sandbox, we need to use the command &lt;code&gt;pickFolder&lt;/code&gt; to choose the folder that contains this file to process it with FFmpeg. When you run the command &lt;code&gt;pickFolder&lt;/code&gt;, a-Shell will present the iOS folder picker, so select the folder you want to use and then you will be returned to a-Shell with the terminal context now pointing to that folder you selected. You can see that I can see &lt;code&gt;Test.gif&lt;/code&gt; when I run the &lt;code&gt;ls&lt;/code&gt; command now.&lt;/p&gt;
&lt;p&gt;Now to convert the gif to an mp4, you can run the following FFmpeg command. &lt;code&gt;ffmpeg -i Test.gif -pix_fmt yuv420p output.mp4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Let&apos;s take a look at the command and break it down into its parts. &lt;code&gt;ffmpeg&lt;/code&gt; tells a-Shell to run this application &lt;code&gt;-i Test.gif&lt;/code&gt; is telling ffmpeg that we want it to use the Test.gif file in this folder as the file to work on. &lt;code&gt;-pix_fmt yuv420p&lt;/code&gt; tells ffmpeg to convert the input file the yuv420p format. The last part is &lt;code&gt;output.mp4&lt;/code&gt;; this tells FFmpeg to save the converted video as output.mp4 in the same folder.&lt;/p&gt;
&lt;p&gt;As you can see in the last part of the image, the folder contains a file named &lt;code&gt;output&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./how-to-use-ffmpeg-on-ios/a-shell-ffmpeg-in-app-example.png&quot; alt=&quot;a-Shell ffmpeg in-app example&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;FFmpeg in a-Shell shortcuts example&lt;/h3&gt;
&lt;p&gt;Here is a bonus if you have gotten this far into the post. a-Shell also has excellent iOS Shortcuts support, which means we can do a similar conversion from within the Shortcuts application. Let&apos;s look at an example shortcut, first and then I will share how you can also achieve this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./how-to-use-ffmpeg-on-ios/gif-to-video-conversion-with-ffmpeg-in-shortcuts.gif&quot; alt=&quot;Gif to video conversion with ffmpeg in shortcuts&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The steps in this shortcut that I will share at the bottom of this post are;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Search Giphy for a gif to process.&lt;/li&gt;
&lt;li&gt;Set the name of the chosen gif to input.gif to be processed by the command in a-Shell.&lt;/li&gt;
&lt;li&gt;Sends the file to a-Shell so that it can be processed within the application.&lt;/li&gt;
&lt;li&gt;It then executes a list of commands in a-Shell
&lt;ul&gt;
&lt;li&gt;rm output.mp4 (this is cleanup any previous runs)&lt;/li&gt;
&lt;li&gt;ffmpeg -i input.gif -pix_fmt yuv420p output.mp4 (this is the same ffmpeg conversion command as above)&lt;/li&gt;
&lt;li&gt;open shortcuts:// (return to the Shortcuts application)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Wait to return (this tells Shortcuts to wait until the application is opened again)&lt;/li&gt;
&lt;li&gt;Get file (this command is configured to get the file converted output.mp4 file from a-Shell and return it to be used in the Shortcuts application)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can get a copy of this Shortcut from &lt;a href=&quot;https://www.icloud.com/shortcuts/d8505c4410ac467fb0b7c3630a3abb2d&quot;&gt;here&lt;/a&gt;
As you can see, you are now able to use the power of FFmpeg on your iOS or iPadOS device with a little bit of help from a-Shell.&lt;/p&gt;
</content:encoded></item><item><title>Create Video from HTML Canvas</title><link>https://shaunhevey.com/posts/create-video-from-html-canvas/</link><guid isPermaLink="true">https://shaunhevey.com/posts/create-video-from-html-canvas/</guid><pubDate>Sun, 31 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this post, I will discuss another little feature that you might be unaware of available in most major browsers called &lt;code&gt;MediaRecorder&lt;/code&gt;.&amp;lt;!--more--&amp;gt;  If the name didn&apos;t give away what it allows you to record media. If you want to dive into what &lt;code&gt;MediaRecorder&lt;/code&gt; has to offer, you can read all about it &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder&quot;&gt;here&lt;/a&gt;. In this post, I will show you how you can use &lt;code&gt;MediaRecorder&lt;/code&gt; to record a canvas. Let&apos;s dive in.&lt;/p&gt;
&lt;h2&gt;Canvas to MP4&lt;/h2&gt;
&lt;p&gt;If you want to skip the explanation and see a complete working example (tested in chrome 95), just head to the bottom of the page, otherwise read on.&lt;/p&gt;
&lt;p&gt;The first thing you need to do is set you some variables, check out the comments for some context.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let frames = []; //This is used to store the captured frames.
let mediaRecorder; // This is the variable that stores the mediaRecorder object.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we need to get a reference to the canvas that you want to create the video from.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let canvas = document.getElementById(&apos;canvas&apos;);   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ok, the last thing that needs to happen is to create a couple of functions to start and end the recording and just a utility function. Again check out the comments for a little bit of context.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function startRecording() {
    //Let&apos;s create the MediaRecorder object. This needs a &quot;stream&quot; that it will use to create the frames for the video in our context.
    //It just so happens that the canvas object has the perfect function to allow it to be used with MediaRecorder, which is the captureStream function.

    mediaRecorder = new MediaRecorder(canvas.captureStream());

    //The next thing you need to do is set a function so that when some event is fired (like a frame update) 
    //then you can capture the data from that event to later be turned into a video.
    mediaRecorder.ondataavailable = (event) =&amp;gt; {
        if (event.data)
            frames.push(event.data);
    }

    //I am using the &quot;onstop&quot; event to call the method to populate the video player with the data that has been captured.	
    mediaRecorder.onstop = showVideo;

    //This method start the MediaRecorder recording.
    mediaRecorder.start();
}

//This is just a utility function to stop the MediaRecorder recording.
function endRecording() {
    mediaRecorder.stop();
}

//This is the function that takes the frames that are captured above and turns them into a blob object, which is passed to the video player that has been created previously.
function showVideo() {
    video.src = URL.createObjectURL(new Blob(frames, { type: mediaRecorder.mimeType }));
    video.setAttribute(&quot;controls&quot;,&quot;controls&quot;)   
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There you have a quick and easy way to record the output of a canvas object. In this example, I only showed how you could populate a video player, but you could easily take the blob object and use it with an &quot;a&quot; tag so that the user could download it to be used outside of the browser.&lt;/p&gt;
&lt;p&gt;The complete example below also contains some code to animate a box moving across the canvas. This was only used to demonstrate an animation on the canvas. I found this webkit &lt;a href=&quot;https://webkit.org/blog/11353/mediarecorder-api/&quot;&gt;blog post&lt;/a&gt; very helpful with creating this example.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;canvas id=&quot;canvas&quot; height=&quot;300&quot; width=&quot;600&quot;&amp;gt;&amp;lt;/canvas&amp;gt;
        
    &amp;lt;button onclick=&quot;startRecording()&quot;&amp;gt;start&amp;lt;/button&amp;gt;
    &amp;lt;button onclick=&quot;endRecording()&quot;&amp;gt;end&amp;lt;/button&amp;gt;
    &amp;lt;video id=&quot;video&quot;&amp;gt;&amp;lt;/video&amp;gt;
    &amp;lt;script&amp;gt;
        let frames = [];
        let mediaRecorder;

        let canvas = document.getElementById(&apos;canvas&apos;);
        let ctx = canvas.getContext(&apos;2d&apos;);        
			let video = document.getElementById(&apos;video&apos;);

        let x = 0;

        function startRecording() {
            mediaRecorder = new MediaRecorder(canvas.captureStream());
            mediaRecorder.ondataavailable = (event) =&amp;gt; {
            if (event.data)
                frames.push(event.data);
            }
            mediaRecorder.onstop = showVideo;
            mediaRecorder.start();
        }
        function endRecording() {
            mediaRecorder.stop();
        }
        function showVideo() {
            video.src = URL.createObjectURL(new Blob(frames, { type: mediaRecorder.mimeType }));
            video.setAttribute(&quot;controls&quot;,&quot;controls&quot;)   
        }

        function draw() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.fillRect(x++, 50, 50,50);
            requestAnimationFrame(draw);
        }

        draw()
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Convert SVG to PNG with HTML Canvas and JavaScript</title><link>https://shaunhevey.com/posts/convert-svg-to-png-with-html-canvas-and-javascript/</link><guid isPermaLink="true">https://shaunhevey.com/posts/convert-svg-to-png-with-html-canvas-and-javascript/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this post, I will show you how to take an SVG file and turn it into a PNG. Before diving into how you convert and SVG, let&apos;s look at SVG and PNG file formats. &amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;The battle of the file formats&lt;/h2&gt;
&lt;p&gt;Let&apos;s start with what both these file formats are and what they have in common. Essentially both are types of image formats, though they are very different. SVG is a vector-based image format, and PNG is a raster-based image format.&lt;/p&gt;
&lt;p&gt;Now, if you are not familiar with what these are, here is a very quick definition. Vector files store image data using shapes such as lines, circles and paths. In comparison, raster files store image data as a grid of pixels (tiny squares) and each of these pixels stores which colour should be displayed by the program rendering the image.&lt;/p&gt;
&lt;p&gt;This means raster images are fixed in size, and you can&apos;t upscale them without losing image quality. On the other hand, though vector images can be scaled up and down and they will retain the image quality as the shapes internally to the image will just be scaled.&lt;/p&gt;
&lt;h2&gt;Converting with Canvas and Javascript&lt;/h2&gt;
&lt;p&gt;Now that little lesson about what SVG and PNG file formats are, let&apos;s move on to using canvas and javascript. Here is the complete code, and then I will break it down and explain each step.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;canvas id=&quot;myCanvas&quot; style=&quot;display:none;&quot;&amp;gt;&amp;lt;/canvas&amp;gt;
&amp;lt;p id=&quot;imageOutput&quot;&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;script&amp;gt;
let svg_data = `&amp;lt;!-- INSERT SVG DATA HERE --&amp;gt;`;

var canvas = document.getElementById(&quot;myCanvas&quot;);
            
canvas.width = 1170;
canvas.height = 2532;
function drawImage(imageObj) {
    var context = canvas.getContext(&apos;2d&apos;);
    var x = 0;
    var y = 0;
    context.drawImage(imageObj, 0,0);
    var imageData = context.getImageData(0, 0, imageObj.width, imageObj.height);
    var data = imageData.data;
    context.putImageData(imageData, 0, 0);
}
                
window.onload = function() {
    var imageObj = new Image();
    imageObj.onload = function() {
        drawImage(this);
        document.getElementById(&quot;imageOutput&quot;).appendChild(convertCanvasToImage(canvas));
    };
    imageObj.src = &quot;data:image/svg+xml,&quot; + encodeURIComponent(svg_data);
}
        
function convertCanvasToImage(canvas) {
    var image = new Image();
    image.src = canvas.toDataURL(&quot;image/png&quot;);
    return image;

}
&amp;lt;/script&amp;gt;
 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s start with the first section with all the required html tags. The first tag is the canvas tag that will do the work, and in my example I am hiding it as it is not required to be shown. This requires an id attribute so that it can be referenced in the JavaScript code.&lt;/p&gt;
&lt;p&gt;The second tag is the placeholder section where the converted imaged will be stored. In my example, this is the paragraph tag; the only thing to make sure you include is the id attribute again to be referenced in the JavaScript code later.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;canvas id=&quot;myCanvas&quot; style=&quot;display:none;&quot;&amp;gt;&amp;lt;/canvas&amp;gt;
&amp;lt;p id=&quot;imageOutput&quot;&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ok, the rest of the code does the work of converting the SVG image with JavaScript. I have commented this code section as required.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
//1. this is the variable where the contents of the SVG file needs to be stored.
let svg_data = `&amp;lt;!-- INSERT SVG DATA HERE --&amp;gt;`;

//2. This gets a reference to the canvas mentioned above.
var canvas = document.getElementById(&quot;myCanvas&quot;);

//3. The canvas needs to be set to the width and height of the image.
canvas.width = 1170;
canvas.height = 2532;

//4. This function does the hard work of drawing the SVG image to the canvas.
function drawImage(imageObj) {
    var context = canvas.getContext(&apos;2d&apos;);
    var x = 0;
    var y = 0;
    context.drawImage(imageObj, 0,0);
    var imageData = context.getImageData(0, 0, imageObj.width, imageObj.height);
    var data = imageData.data;
    context.putImageData(imageData, 0, 0);
}

//5. This function is set to be called with the window is loaded.
// It sets up a new Image and sets a onload to that image to call the drawImage function explained above and then calls the function to convert to canvas to image and stores it in placeholder tag. 
// It also sets the source of the newly created image to be the SVG data stored in the svg_data variable.
window.onload = function() {
    var imageObj = new Image();
    imageObj.onload = function() {
        drawImage(this);
        document.getElementById(&quot;imageOutput&quot;).appendChild(convertCanvasToImage(canvas));
    };
    imageObj.src = &quot;data:image/svg+xml,&quot; + encodeURIComponent(svg_data);
}

//6. This function takes the canvas object as returns a new png image using the toDataURL method.
function convertCanvasToImage(canvas) {
    var image = new Image();
    image.src = canvas.toDataURL(&quot;image/png&quot;);
    return image;

}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, in less than 30 lines of code, you can convert an SVG file into a PNG so that it can be used where SVG files are not supported.&lt;/p&gt;
</content:encoded></item><item><title>JetBrains Rider tips and tricks</title><link>https://shaunhevey.com/posts/jetbrains-rider-tips-and-tricks/</link><guid isPermaLink="true">https://shaunhevey.com/posts/jetbrains-rider-tips-and-tricks/</guid><pubDate>Sun, 17 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For those that use JetBrains Rider to develop your .NET applications, in this post, I have three tips and tricks that you might not have known about to help improve your productivity.&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;NuGet Tools&lt;/h2&gt;
&lt;p&gt;The first tip I have is around NuGet tooling in Rider. You might have already known that Rider has a NuGet client built into it to allow you to install and manage your NuGet packages in your solution. The tooling though isn&apos;t just limited to managing your NuGet packages. Rider has a menu with several NuGet commands like &lt;code&gt;NuGet Restore&lt;/code&gt; that forces NuGet to download the required packages for your solution to compile. As well as &lt;code&gt;Upgrade all packages in Solution&lt;/code&gt; which will tell Rider to upgrade all the packages in your solution to the latest published versions. You can find all of these commands under the following menu &lt;code&gt;Tools &amp;gt; NuGet&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./jetbrains-rider-tips-and-tricks/rider-nuget-menu.png&quot; alt=&quot;Menu showing location of NuGet tools&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It also contains a quick menu to allow you to access the same commands with keyboard shortcuts. You can see in the previous image that it is called &lt;code&gt;NuGet Quick List&lt;/code&gt;, and the keyboard shortcut is currently set on my system. If you select it, you get the following popover to access the same NuGet commands, and you can use the numbers to run the command in that popover. An example is if I type 1 on my keyboard, then it will run the &lt;code&gt;NuGet restore&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./jetbrains-rider-tips-and-tricks/rider-nuget-quick-list.png&quot; alt=&quot;JetBrains Rider NuGet Quick list menu&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Productivity Guide&lt;/h2&gt;
&lt;p&gt;The next feature I want to explore is an analytics feature that is built into Rider. As you are using features within Rider, it keeps track of what feature you are using and when you last used a feature.&lt;/p&gt;
&lt;p&gt;With this data, you can get Rider to show you the information to analyse what features you are using and ones you might not know about. This feature is called the Productivity guide and is available under the following menu &lt;code&gt;Help &amp;gt; My Productivity&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When you select this command, a new popover will appear and show you a list of the features, how many times you have used them and the last time you used them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./jetbrains-rider-tips-and-tricks/rider-productivity-guide.png&quot; alt=&quot;JetBrains Rider productivity window&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you select one of the features in the list, it will give you a description of the feature and how you invoke it.&lt;/p&gt;
&lt;h2&gt;Keyboard Shortcuts PDF&lt;/h2&gt;
&lt;p&gt;The last one I want to discuss is not a feature built into Rider but a series of PDFs that JetBrains have created to help you learn the keyboard shortcuts you have configured in Rider to make you the most productive developer you can be.&lt;/p&gt;
&lt;p&gt;These don&apos;t live within Rider, but there is a menu option, &lt;code&gt;Help &amp;gt; Keyboard Shortcuts PDF&lt;/code&gt;, that will take you to the JetBrains website where the PDFs are hosted.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./jetbrains-rider-tips-and-tricks/rider-keyboard-shortcut-menu.png&quot; alt=&quot;Menu showing location of Keyboard Shortcuts PDF&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Below is a screenshot of the Website where the links are located.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./jetbrains-rider-tips-and-tricks/rider-website-keyboard-links.png&quot; alt=&quot;List of links on webstie&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you need to know which keyboard scheme you have configured, you can find it in the Preferences menu &lt;code&gt;JetBrains &amp;gt; Preferences &amp;gt; Keymap&lt;/code&gt;
Below is a screenshot showing the location of the keymap setting.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./jetbrains-rider-tips-and-tricks/rider-preferences-menu.png&quot; alt=&quot;Rider preferences menu where Keymap setting is located&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is just a quick look at a few features of JetBrains Rider that will hopefully make you more productive while you are developing in JetBrains Rider.&lt;/p&gt;
</content:encoded></item><item><title>Backup, Restore and Sharing Shortcuts in iOS 15</title><link>https://shaunhevey.com/posts/backup-restore-and-sharing-shortcuts-in-ios-15/</link><guid isPermaLink="true">https://shaunhevey.com/posts/backup-restore-and-sharing-shortcuts-in-ios-15/</guid><pubDate>Mon, 04 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In a &lt;a href=&quot;/posts/backup-and-restore-shortcuts/&quot;&gt;previous post&lt;/a&gt;, I wrote about how you could backup and restore Shortcuts. In that post, I had to use several applications to achieve this as Apple didn&apos;t allow you to backup and restore without importing from an iCloud link. &amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;In this post, I will share with you how to backup and restore your shortcuts. I will also show that you can share shortcuts without going through iCloud links anymore, as all of these have now improved with iOS 15.&lt;/p&gt;
&lt;h2&gt;Backup&lt;/h2&gt;
&lt;p&gt;In iOS 15, Apple doesn&apos;t require you to import Shortcuts anymore just through an iCloud link but more on that later in the post. Backing up in a way that allows you to restore them is as simple as saving the Shortcut file to any folder on your device. To demonstrate this, I have created a simple Shortcut that allows you to backup all the available Shortcuts currently in your library. You can select a single Shortcuts folder, and all the Shortcuts in that folder will be backed up.&lt;/p&gt;
&lt;p&gt;Let&apos;s take a quick look at this in action.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./backup-restore-and-sharing-shortcuts-in-ios-15/backup-shortcuts-example.gif&quot; alt=&quot;Running shortcuts to show backing up&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, the Shortcut allows you to choose any folder available in the files app. This means you are not just limited to your device or even to just iCloud. You can save them to any app that supports folder sharing via the files app. A great example of this is &lt;a href=&quot;https://workingcopyapp.com&quot;&gt;Working Copy&lt;/a&gt;; with Working Copy installed, you could back up all your Shortcuts to a Git repository if you wanted to.&lt;/p&gt;
&lt;p&gt;If you would like to use this Shortcut, you can find it &lt;a href=&quot;https://www.icloud.com/shortcuts/dc9f514ed4c04c158b9c16806ec02793&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Restore&lt;/h2&gt;
&lt;p&gt;In iOS 15, Apple has allowed signed Shortcuts to be imported from a file. You can achieve this by tapping on a Shortcut file. Let&apos;s take a look at how you go about restoring a Shortcut backed up from the previous step.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./backup-restore-and-sharing-shortcuts-in-ios-15/restore-shortcuts-example.gif&quot; alt=&quot;Restoring Shortcut from file&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now that you have seen how to backup and restore your Shortcuts, how about sharing your Shortcuts.&lt;/p&gt;
&lt;h2&gt;Sharing&lt;/h2&gt;
&lt;p&gt;In iOS 15, Apple now allows you multiple ways to share your Shortcuts. Previously you could only share Shortcuts via an iCloud link. This option is still available in iOS 15, but you can also share your Shortcut as a file.&lt;/p&gt;
&lt;p&gt;Here are the steps to allow you to share your Shortcut as a file.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open the Shortcuts application.&lt;/li&gt;
&lt;li&gt;Open the Shortcut that you want to share in the editor.&lt;/li&gt;
&lt;li&gt;Click the share button.&lt;/li&gt;
&lt;li&gt;When the share sheet has been presented, click the Options &amp;gt; link at the top under the Shortcut title.&lt;/li&gt;
&lt;li&gt;Choose &quot;file&quot; from the menu.&lt;/li&gt;
&lt;li&gt;It will then present you with two options
&lt;ul&gt;
&lt;li&gt;Anyone (this one will be importable by anyone on iOS 15 or later but is signed by Apple)&lt;/li&gt;
&lt;li&gt;People Who Know Me (this one will include your contact information and will only be importable by people that have you in their contacts and is not signed by Apple)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click done and choose the application that you want to use to share your Shortcut.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Check out the image below to see these steps in action.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./backup-restore-and-sharing-shortcuts-in-ios-15/sharing-shortcuts-as-a-file.gif&quot; alt=&quot;sharing shortcuts as a file&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, Apple has made improvements with iOS 15 to make it easier for you to backup, restore and share your Shortcuts.&lt;/p&gt;
</content:encoded></item><item><title>Creating Gradients in Shortcuts</title><link>https://shaunhevey.com/posts/creating-gradients-in-shortcuts/</link><guid isPermaLink="true">https://shaunhevey.com/posts/creating-gradients-in-shortcuts/</guid><pubDate>Mon, 25 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have been looking for a while for a way to create images that contained a gradient from within Shortcuts. The reason is to be able to use them with other actions to achieve the some examples below. The first is to create gradient icons like these with the help of Toolbox Pro. &amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./creating-gradients-in-shortcuts/ArgonIcons.png&quot; alt=&quot;Gradient Icons&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It would also allow you to create the following gradient screenshots.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./creating-gradients-in-shortcuts/GradientScreenshots.png&quot; alt=&quot;Gradient Screenshots&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The possibilities are endless, now it has been possible through the use of html and canvas element and then using a series of encoding the html as a base64 string and getting a web view and getting images from it, etc. This is not the ideal work flow though.&lt;/p&gt;
&lt;p&gt;If you do want to see how I did this with something similar make sure you check out this post &lt;a href=&quot;https://shaunhevey.com/2020/02/Extending-iOS-Shortcuts-with-JavaScript/&quot;&gt;Extending iOS Shortcuts with Javascript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That has worked great but the setup of this has meant I have not really wanted to share a lot of these add ons I have been creating. So for a while I have been looking for a way I can improve this experience and I think I have come up with a better way to achieve this with the help of Scriptable.&lt;/p&gt;
&lt;h2&gt;Scriptable to the rescue&lt;/h2&gt;
&lt;p&gt;I want to start by saying this method is still using html and the canvas element to actually create the gradient but I a different method to expose this in Shortcuts. This is where Scriptable comes in very handy.&lt;/p&gt;
&lt;p&gt;If you haven&apos;t used Scriptable before it is a fantastic free (with a tip jar) application for iOS that allows you to write JavaScript that can interact with some internals of iOS itself. It is created by &lt;a href=&quot;https://twitter.com/simonbs&quot;&gt;Simon Støvring&lt;/a&gt; and you can read all about it &lt;a href=&quot;https://scriptable.app&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To actually talk about how I am creating the gradient images I am using the Run Inline Script action that Scriptable exposes in Shortcuts.&lt;/p&gt;
&lt;p&gt;One of the benefits of the Run Inline Script action is the code required to perform the action can be shared with your shortcut and you don&apos;t have to ask the user to create a module with in Scriptable to expose this functionality in Shortcuts.&lt;/p&gt;
&lt;p&gt;One thing to note you have to set the action to &quot;Run in app&quot; as it was not able to generate the gradient image if it ran just shortcuts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var wv = new WebView()
wv.loadHTML(`&amp;lt;html&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;canvas id=&quot;myCanvas&quot;&amp;gt;&amp;lt;/canvas&amp;gt;
        &amp;lt;script&amp;gt;
            var width = 1920;
            var height = 1080;
			var colours = [&quot;orange&quot;, &quot;red&quot;, &quot;purple&quot;];

            var points = 1 / (colours.length - 1)
            var c = document.getElementById(&quot;myCanvas&quot;);
            c.width = width;
            c.height = height;
            var ctx = c.getContext(&quot;2d&quot;);

            var grd = ctx.createLinearGradient(0, 0, width, 0);                    

            for(var i = 0; i &amp;lt; colours.length; i++) {
                var offset = 0;
                if(i == colours.length -1) {
                    offset = 1;
                }
                else {
                    offset = points * i;
                }
                grd.addColorStop(offset, colours[i]);
            }

            ctx.fillStyle = grd;
            ctx.fillRect(0, 0, width, height);
        &amp;lt;/script&amp;gt;
    &amp;lt;/body&amp;gt; 
&amp;lt;/html&amp;gt;`)


var js = `var gradCanvas = document.getElementById(&quot;myCanvas&quot;);
completion(gradCanvas.toDataURL())`;
await wv.waitForLoad()
var data = await wv.evaluateJavaScript(js, true);
var b64Data = data.replace(&apos;data:image/png;base64,&apos;,&apos;&apos;);
var img = Image.fromData(Data.fromBase64String(b64Data));
Pasteboard.copyImage(img)
Safari.open(&quot;shortcuts://&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The high level steps of the following code are&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a Webview&lt;/li&gt;
&lt;li&gt;Load the HTML that will create the gradient (out of scope of this post)&lt;/li&gt;
&lt;li&gt;Inject some JavaScript in to the HTML to pull the image data out with the evaluateJavaScript method&lt;/li&gt;
&lt;li&gt;Turn the image data that was extracted from the HTML into an Image object and the copy that Image to the clipboard.&lt;/li&gt;
&lt;li&gt;Open the Shortcuts app&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The reason that second last step is important is because you can&apos;t return an image object from the Scriptable Shortcuts action so putting the image in to the clipboard means it will be available to your shortcut after it is generated.&lt;/p&gt;
&lt;p&gt;If you do want to use the image that has been generated by this action you will need to pair this action with a Wait to return Shortcut action so Shortcuts won&apos;t continue to run while Scriptable is creating the image. Then once you return you can just get the image with the Get from clipboard action.&lt;/p&gt;
&lt;p&gt;If you want to see this in action I have created a shortcut that has a list of gradients that have been submitted to &lt;a href=&quot;https://uiGradients.com&quot;&gt;UIGradients.com&lt;/a&gt; It then presents a menu for you to select one and creates an image of it. You can find that shortcut from the link below&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.icloud.com/shortcuts/b362faf86039424e914c93024c55c0c6&quot;&gt;Create UIGradient Image&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There are many improvements that could be made with the above example like passing parameters in to the script instead of changing the text like it is to pick the colours, but I decided to leave that for another post 😀&lt;/p&gt;
</content:encoded></item><item><title>Scriptable and Shortcuts</title><link>https://shaunhevey.com/posts/scriptable-and-shortcuts/</link><guid isPermaLink="true">https://shaunhevey.com/posts/scriptable-and-shortcuts/</guid><pubDate>Mon, 10 Feb 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Lately I have been playing around with &lt;a href=&quot;https://scriptable.app&quot;&gt;Scriptable&lt;/a&gt; and running these scripts from within the &lt;a href=&quot;https://support.apple.com/en-au/HT208309&quot;&gt;Shortcuts&lt;/a&gt; application. &amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;If you haven&apos;t played around with running your Scriptable scripts in Shortcuts, Scriptable allows you to run scripts in Shortcuts without leaving the Shortcuts application. You can read more about this feature in the &lt;a href=&quot;https://docs.scriptable.app&quot;&gt;docs&lt;/a&gt; checkout the &lt;a href=&quot;https://docs.scriptable.app/args/&quot;&gt;args&lt;/a&gt; section to find out how you can pass information to your script from shortcuts and the &lt;a href=&quot;https://docs.scriptable.app/script/&quot;&gt;Script&lt;/a&gt; section to find out how to pass information from your script to Shortcuts.&lt;/p&gt;
&lt;h2&gt;Let&apos;s create a basic script that can transfer data to and from Shortcuts&lt;/h2&gt;
&lt;p&gt;For this tutorial I want to create a script that takes some text and formats the text and passes it back to the Shortcut action. Let&apos;s start with creating the script and then how you set up a Shortcut to use this script.&lt;/p&gt;
&lt;h3&gt;Creating the script&lt;/h3&gt;
&lt;h4&gt;Let&apos;s start by Creating a new script and calling it &lt;code&gt;ShortcutsTextFormatter&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./scriptable-and-shortcuts/8c2bda5a-0250-ae20-96c9-47248294c3a7.gif&quot; alt=&quot;Setting script name&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Now add the following functions these are the functions that will actually format the text&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const UpperCase = (text) =&amp;gt; {
	return text.toUpperCase();
};

const LowerCase = (text) =&amp;gt; {
	return text.toLowerCase();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These two functions are pretty simple the both take a simple parameter and the return the parameter with upper cased or lower cased.&lt;/p&gt;
&lt;h4&gt;Allow Shortcuts to interact with the script&lt;/h4&gt;
&lt;p&gt;By default any script in Scriptable can be run in the Shortcuts app but if you want to be able to provide data to the script or return data from the script to Shortcuts you will need to add a few more lines of code.&lt;/p&gt;
&lt;p&gt;Let&apos;s add the following lines to the script.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var actionParam = args.shortcutParameter;

var text = args.plainTexts[0];
						
if (actionParam == &quot;uppercase&quot;) {
	Script.setShortcutOutput(text.toUpperCase());
}
else if (actionParam == &quot;lowercase&quot;) {
	Script.setShortcutOutput(text.toLowerCase());
}

Script.complete();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s look at this code block by block.&lt;/p&gt;
&lt;p&gt;The following line of code gets the parameter that was passed from the Shortcuts action.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var actionParam = args.shortcutParameter;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is mapped to this parameter in the Shortcuts action.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./scriptable-and-shortcuts/d47ff444-0ef9-1679-963b-9cd4b5d34829.png&quot; alt=&quot;Scriptable Shortcut parameter&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Scriptable has a number of options for passing data in from Shortcuts to your script the following line is passing text data in. This code will only work on the first item that is passed in.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var text = args.plainTexts[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is mapped to this parameter in the Shortcuts action.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./scriptable-and-shortcuts/d3a1bfdb-d5ea-261b-f799-01e8279dc8da.png&quot; alt=&quot;Scriptable Shortcut text input&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The next block of code is actually doing the work on the data that is passed in. This code is only handling the two cases of &apos;uppercase&apos; and &apos;lowercase&apos; but it isn&apos;t handling if any other parameter is passed in it wont be handled.&lt;/p&gt;
&lt;p&gt;This block is also setting the output of the script for Shortcuts with the `Script.setShortcutOutput&apos; method.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (actionParam == &quot;uppercase&quot;) {
	Script.setShortcutOutput(text.toUpperCase());
}
else if (actionParam == &quot;lowercase&quot;) {
	Script.setShortcutOutput(text.toLowerCase());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This last block of code tells the Shortcuts action that the script has been completed, if you don&apos;t add this line of code then Shortcuts has to use heuristics to work out when it has been completed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Script.complete();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./scriptable-and-shortcuts/2fde5bbb-6d5b-ffce-0721-815daab56550.gif&quot; alt=&quot;Scriptable Shortcut in action&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A few notes about this code it is doing the minimum to actually be able to pass data in and out to Shortcuts but there is a whole lot more you should do. That is where this next module I have created comes in.&lt;/p&gt;
&lt;h2&gt;ShortcutActions module&lt;/h2&gt;
&lt;p&gt;This module has been created to make it easy to expose actions from your script to Shortcuts.&lt;/p&gt;
&lt;p&gt;To use this module you have to import the module and then you use the &lt;code&gt;AddAction&lt;/code&gt; method to expose an action to Shortcuts but more on that later in this post. When you have defined all your actions, you need to call the Run method.&lt;/p&gt;
&lt;p&gt;Here is the preceding code example now using this module to expose the same functionality. As you can see it is slightly different to previous example as it will handle multiple parameters passed in.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let sa = importModule(&apos;ShortcutActions&apos;);

sa.AddAction(&quot;Uppercase&quot;, &quot;text&quot;, function(text) {
	var results = [];
	text.forEach(function(item, index) {
	var data = item.toUpperCase();
	results.push(data);
	});

	return results;
});
sa.AddAction(&quot;Lowercase&quot;, &quot;text&quot;, function(text) {
	var results = [];
	text.forEach(function(item, index) {
	var data = item.toLowerCase();
	results.push(data);
	});

	return results;
});

sa.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s take a look at the AddAction method in depth. This method takes three parameters the first is the name of the action that you want to expose. This is the text that needs to be passed in to the script in this parameter.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./scriptable-and-shortcuts/d47ff444-0ef9-1679-963b-9cd4b5d34829.png&quot; alt=&quot;Scriptable Shortcut parameter&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The second parameter is to define the parameters that will be passed in to the method for the action. This can either be an array of identifiers or a single identifier. The following identifiers are available &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt; if no parameters are to be passed in. Your function that you have defined for the action must have the same number of parameters as identifiers defined. &lt;em&gt;Note:&lt;/em&gt; The order of the identifiers defined will be the order the arguments will be passed in the action function.&lt;/p&gt;
&lt;p&gt;Each of the identifier map to the four optional parameters from the Shortcut action see the image below for the mapping.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./scriptable-and-shortcuts/d3a1bfdb-d5ea-261b-f799-01e8279dc8da.png&quot; alt=&quot;Scriptable Shortcut text input&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The last parameter is the function that will be called by the module if it is called by the Shortcuts action. As defined above the number and order of the parameters must match the identifiers passed in for the second parameter. This method must return the data that you want to be passed back to Shortcuts.&lt;/p&gt;
&lt;p&gt;This module also has the ability to expose the list of actions that have been defined to Shortcuts. This will happen if you pass no parameter to the script (this can be turned off by the following line. &lt;code&gt;sa.exposeHelpAsDefault = false&lt;/code&gt;) or by passing the following text in as the parameter &lt;code&gt;Script:GetActions&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let&apos;s take a look at what this looks like in Shortcuts.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./scriptable-and-shortcuts/86a3b093-b777-3e4b-3c45-e15087d9ff9e.gif&quot; alt=&quot;Scriptable Shortcuts module in action&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you would like to grab a copy of this module then you can find it &lt;a href=&quot;https://gist.github.com/shaun-h/64fbec06833171a6fb3f04193f3bb9db&quot;&gt;here&lt;/a&gt; or from the script below.&lt;/p&gt;
&lt;p&gt;Once you have saved it in to your Scriptable library then you can import it in to any script to start exposing actions easily.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var actionList = [];
	
const AddAction = (name, inputType, callback) =&amp;gt; {
	if(actionList == undefined) {
		actionList = [];
	}
	
	var inputTypesArr = [];
	
	if(Array.isArray(inputType)) {
		inputTypesArr = inputType;
	}
	else if(inputType != undefined &amp;amp;&amp;amp; inputType != null) {
		inputTypesArr.push(inputType);
	}
	
	
	inputTypesArr.forEach(function (item, index) {
		if ([&quot;text&quot;,&quot;url&quot;,&quot;image&quot;,&quot;file&quot;,&quot;name&quot;].indexOf(item) == -1) {
			throw new Error(&quot;Incorrect input type provided: &quot; + item);
		}
	});
	
	actionList.push({&quot;name&quot;: name, &quot;inputType&quot;: inputTypesArr, &quot;callback&quot;:callback});
}

const GetActions = () =&amp;gt; {
	var actions = [];
	if(actionList != undefined) {
		for(var i = 0; i &amp;lt; actionList.length; i++) {
			var action = actionList[i];
			actions.push(action.name);
		}
	}
	return {&quot;Actions&quot;:actions};	

}

const RunAction = async (name) =&amp;gt; {
	if(actionList == undefined) {
		throw new Error(&quot;No actions have been defined&quot;);
	}
	
	for(var i = 0; i &amp;lt; actionList.length; i++) {
		var action = actionList[i];
		if(action.name == name) {
			var params = [];
			action.inputType.forEach(function (item, index) {
				switch (item) {
					case &quot;text&quot;:
						params.push(args.plainTexts);
						break;
					case &quot;url&quot;:
						params.push(args.urls);
						break;
					case &quot;image&quot;:
						params.push(args.images);
						break;
					case &quot;file&quot;:
						params.push(args.fileURLs);
						break;
					case &quot;name&quot;:
						params.push(name);
						break;
				};
			});
			
			switch(params.length) {
				case 0:
					return await action.callback();
					break;
				case 1:
					return await action.callback(params[0]);
					break;
				case 2:
					return await action.callback(params[0], params[1]);
					break;
				case 3:
					 return await action.callback(params[0], params[1], params[2]);
					break;
				case 4:
					return await action.callback(params[0], params[1], params[2], params[3]);
					break;
				case 5:
					return await action.callback(params[0], params[1], params[2], params[3], params[4]);
					break;
			}
		}
	}
}

const Run = async () =&amp;gt; {
	var actionParam = args.shortcutParameter;
	if (actionParam == &quot;Script:GetActions&quot; || (module.exports.exposeHelpAsDefault &amp;amp;&amp;amp; actionParam == undefined)) {
		Script.setShortcutOutput(JSON.stringify(GetActions()));
	}
	else {
		var result = await RunAction(actionParam);
		if(result != undefined) {
			var output = result;
			if(Array.isArray(result)) {
				if(result.length &amp;gt; 1) {
					output = {&quot;Values&quot;:result};	
				}
				else {
					output = result[0];
				}
			}
			Script.setShortcutOutput(JSON.stringify(output));
		}
		else {
			Script.setShortcutOutput(&quot;No output&quot;);
		}
	}

	Script.complete();
}

module.exports.exposeHelpAsDefault = true;
module.exports.AddAction = AddAction;
module.exports.Run = Run;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>