initial commit

This commit is contained in:
Aaron Carson 2024-09-18 14:27:22 +01:00
commit 3539c9d576
12 changed files with 2666 additions and 0 deletions

103
Random Scriptable API.js Normal file
View file

@ -0,0 +1,103 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-blue; icon-glyph: book;
// 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.
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()
async function createWidget(api) {
let appIcon = await loadAppIcon()
let title = "Random Scriptable API"
let widget = new ListWidget()
// Add background gradient
let gradient = new LinearGradient()
gradient.locations = [0, 1]
gradient.colors = [
new Color("141414"),
new Color("13233F")
]
widget.backgroundGradient = gradient
// Show app icon and title
let titleStack = widget.addStack()
let appIconElement = titleStack.addImage(appIcon)
appIconElement.imageSize = new Size(15, 15)
appIconElement.cornerRadius = 4
titleStack.addSpacer(4)
let titleElement = titleStack.addText(title)
titleElement.textColor = Color.white()
titleElement.textOpacity = 0.7
titleElement.font = Font.mediumSystemFont(13)
widget.addSpacer(12)
// Show API
let nameElement = widget.addText(api.name)
nameElement.textColor = Color.white()
nameElement.font = Font.boldSystemFont(18)
widget.addSpacer(2)
let descriptionElement = widget.addText(api.description)
descriptionElement.minimumScaleFactor = 0.5
descriptionElement.textColor = Color.white()
descriptionElement.font = Font.systemFont(18)
// UI presented in Siri ans Shortcuta is non-interactive, so we only show the footer when not running the script from Siri.
if (!config.runsWithSiri) {
widget.addSpacer(8)
// Add button to open documentation
let linkSymbol = SFSymbol.named("arrow.up.forward")
let footerStack = widget.addStack()
let linkStack = footerStack.addStack()
linkStack.centerAlignContent()
linkStack.url = api.url
let linkElement = linkStack.addText("Read more")
linkElement.font = Font.mediumSystemFont(13)
linkElement.textColor = Color.blue()
linkStack.addSpacer(3)
let linkSymbolElement = linkStack.addImage(linkSymbol.image)
linkSymbolElement.imageSize = new Size(11, 11)
linkSymbolElement.tintColor = Color.blue()
footerStack.addSpacer()
// Add link to documentation
let docsSymbol = SFSymbol.named("book")
let docsElement = footerStack.addImage(docsSymbol.image)
docsElement.imageSize = new Size(20, 20)
docsElement.tintColor = Color.white()
docsElement.imageOpacity = 0.5
docsElement.url = "https://docs.scriptable.app"
}
return widget
}
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()
}

123
Read MacStories.js Normal file
View file

@ -0,0 +1,123 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: red; icon-glyph: newspaper;
// This script shows articles from MacStories. The script can be used either in the app, as a widget on your Home Screen or through Shortcuts. The behaviour of the script will vary slightly depending on where it's used.
let items = await loadItems()
if (config.runsInWidget) {
// Tell the widget on the Home Screen to show our ListWidget instance.
let widget = await createWidget(items)
Script.setWidget(widget)
} else if (config.runsWithSiri) {
// Present a table with a subset of the news.
let firstItems = items.slice(0, 5)
let table = createTable(firstItems)
await QuickLook.present(table)
} else {
// Present the full list of news.
let table = createTable(items)
await QuickLook.present(table)
}
// 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()
async function createWidget(items) {
let item = items[0]
let authors = item.authors.map(a => {
return a.name
}).join(", ")
let imgURL = extractImageURL(item)
let rawDate = item["date_published"]
let date = new Date(Date.parse(rawDate))
let dateFormatter = new DateFormatter()
dateFormatter.useMediumDateStyle()
dateFormatter.useShortTimeStyle()
let strDate = dateFormatter.string(date)
let gradient = new LinearGradient()
gradient.locations = [0, 1]
gradient.colors = [
new Color("#b00a0fe6"),
new Color("#b00a0fb3")
]
let widget = new ListWidget()
if (imgURL != null) {
let imgReq = new Request(imgURL)
let img = await imgReq.loadImage()
widget.backgroundImage = img
}
widget.backgroundColor = new Color("#b00a0f")
widget.backgroundGradient = gradient
// Add spacer above content to center it vertically.
widget.addSpacer()
// Show article headline.
let title = decode(item.title)
let titleElement = widget.addText(title)
titleElement.font = Font.boldSystemFont(16)
titleElement.textColor = Color.white()
titleElement.minimumScaleFactor = 0.75
// Add spacing below headline.
widget.addSpacer(8)
// Add footer woth authors and date.
let footerStack = widget.addStack()
let authorsElement = footerStack.addText(authors)
authorsElement.font = Font.mediumSystemFont(12)
authorsElement.textColor = Color.white()
authorsElement.textOpacity = 0.9
footerStack.addSpacer()
let dateElement = footerStack.addText(strDate)
dateElement.font = Font.mediumSystemFont(12)
dateElement.textColor = Color.white()
dateElement.textOpacity = 0.9
// Add spacing below content to center it vertically.
widget.addSpacer()
// Set URL to open when tapping widget.
widget.url = item.url
return widget
}
function createTable(items) {
let table = new UITable()
for (item of items) {
let row = new UITableRow()
let imageURL = extractImageURL(item)
let title = decode(item.title)
let imageCell = row.addImageAtURL(imageURL)
let titleCell = row.addText(title)
imageCell.widthWeight = 20
titleCell.widthWeight = 80
row.height = 60
row.cellSpacing = 10
row.onSelect = (idx) => {
let item = items[idx]
Safari.open(item.url)
}
row.dismissOnSelect = false
table.addRow(row)
}
return table
}
async function loadItems() {
let url = "https://macstories.net/feed/json"
let req = new Request(url)
let json = await req.loadJSON()
return json.items
}
function extractImageURL(item) {
let regex = /<img src="(.*)" alt="/
let html = item["content_html"]
let matches = html.match(regex)
if (matches && matches.length >= 2) {
return matches[1]
} else {
return null
}
}
function decode(str) {
let regex = /&#(\d+);/g
return str.replace(regex, (match, dec) => {
return String.fromCharCode(dec)
})
}

26
Today's xkcd Comic.js Normal file
View file

@ -0,0 +1,26 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: gray; icon-glyph: image;
// Loads the latest XKCD comic from
// xkcd.com and shows it. Works well
// when run from a Siri Shortcut.
// The comic will be shown inline and
// Siri will read out the text that
// supplements the comic.
let url = "https://xkcd.com/info.0.json"
let req = new Request(url)
let json = await req.loadJSON()
let imgURL = json["img"]
alt = json["alt"]
req = new Request(imgURL)
let img = await req.loadImage()
// QuickLook is a powerful API that
// finds the best way to present a
// value. It works both in the app
// and with Siri.
QuickLook.present(img)
if (config.runsWithSiri) {
Speech.speak(alt)
}
// It is good practice to call Script.complete() at the end of a script, especially when the script is used with Siri or in the Shortcuts app. This lets Scriptable report the results faster. Please see the documentation for details.
Script.complete()

View file

@ -0,0 +1,807 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: cyan; icon-glyph: layer-group;
// This script was created by Max Zeryck.
// The amount of blurring. Default is 150.
let blur = 150
// Determine if user has taken the screenshot.
var message
message = "Before you start, go to your home screen and enter wiggle mode. Scroll to the empty page on the far right and take a screenshot."
let options = ["Continue to select image","Exit to take screenshot","Update code"]
let response = await generateAlert(message,options)
// Return if we need to exit.
if (response == 1) return
// Update the code.
if (response == 2) {
// Determine if the user is using iCloud.
let files = FileManager.local()
const iCloudInUse = files.isFileStoredIniCloud(module.filename)
// If so, use an iCloud file manager.
files = iCloudInUse ? FileManager.iCloud() : files
// Try to download the file.
try {
const req = new Request("https://raw.githubusercontent.com/mzeryck/Widget-Blur/main/widget-blur.js")
const codeString = await req.loadString()
files.writeString(module.filename, codeString)
message = "The code has been updated. If the script is open, close it for the change to take effect."
} catch {
message = "The update failed. Please try again later."
}
options = ["OK"]
await generateAlert(message,options)
return
}
// Get screenshot and determine phone size.
let img = await Photos.fromLibrary()
let height = img.size.height
let phone = phoneSizes()[height]
if (!phone) {
message = "It looks like you selected an image that isn't an iPhone screenshot, or your iPhone is not supported. Try again with a different image."
await generateAlert(message,["OK"])
return
}
// Extra setup needed for 2436-sized phones.
if (height == 2436) {
let files = FileManager.local()
let cacheName = "mz-phone-type"
let cachePath = files.joinPath(files.libraryDirectory(), cacheName)
// If we already cached the phone size, load it.
if (files.fileExists(cachePath)) {
let typeString = files.readString(cachePath)
phone = phone[typeString]
// Otherwise, prompt the user.
} else {
message = "What type of iPhone do you have?"
let types = ["iPhone 12 mini", "iPhone 11 Pro, XS, or X"]
let typeIndex = await generateAlert(message, types)
let type = (typeIndex == 0) ? "mini" : "x"
phone = phone[type]
files.writeString(cachePath, type)
}
}
// Prompt for widget size and position.
message = "What size of widget are you creating?"
let sizes = ["Small","Medium","Large"]
let size = await generateAlert(message,sizes)
let widgetSize = sizes[size]
message = "What position will it be in?"
message += (height == 1136 ? " (Note that your device only supports two rows of widgets, so the middle and bottom options are the same.)" : "")
// Determine image crop based on phone size.
let crop = { w: "", h: "", x: "", y: "" }
if (widgetSize == "Small") {
crop.w = phone.small
crop.h = phone.small
let positions = ["Top left","Top right","Middle left","Middle right","Bottom left","Bottom right"]
let position = await generateAlert(message,positions)
// Convert the two words into two keys for the phone size dictionary.
let keys = positions[position].toLowerCase().split(' ')
crop.y = phone[keys[0]]
crop.x = phone[keys[1]]
} else if (widgetSize == "Medium") {
crop.w = phone.medium
crop.h = phone.small
// Medium and large widgets have a fixed x-value.
crop.x = phone.left
let positions = ["Top","Middle","Bottom"]
let position = await generateAlert(message,positions)
let key = positions[position].toLowerCase()
crop.y = phone[key]
} else if(widgetSize == "Large") {
crop.w = phone.medium
crop.h = phone.large
crop.x = phone.left
let positions = ["Top","Bottom"]
let position = await generateAlert(message,positions)
// Large widgets at the bottom have the "middle" y-value.
crop.y = position ? phone.middle : phone.top
}
// Prompt for blur style.
message = "Do you want a fully transparent widget, or a translucent blur effect?"
let blurOptions = ["Transparent","Light blur","Dark blur","Just blur"]
let blurred = await generateAlert(message,blurOptions)
// We always need the cropped image.
let imgCrop = cropImage(img)
// If it's blurred, set the blur style.
if (blurred) {
const styles = ["", "light", "dark", "none"]
const style = styles[blurred]
imgCrop = await blurImage(img,imgCrop,style)
}
message = "Your widget background is ready. Choose where to save the image:"
const exportPhotoOptions = ["Export to the Photos app","Export to the Files app"]
const exportToFiles = await generateAlert(message,exportPhotoOptions)
if (exportToFiles) {
await DocumentPicker.exportImage(imgCrop)
} else {
Photos.save(imgCrop)
}
Script.complete()
// Generate an alert with the provided array of options.
async function generateAlert(message,options) {
let alert = new Alert()
alert.message = message
for (const option of options) {
alert.addAction(option)
}
let response = await alert.presentAlert()
return response
}
// Crop an image into the specified rect.
function cropImage(image) {
let draw = new DrawContext()
let rect = new Rect(crop.x,crop.y,crop.w,crop.h)
draw.size = new Size(rect.width, rect.height)
draw.drawImageAtPoint(image,new Point(-rect.x, -rect.y))
return draw.getImage()
}
async function blurImage(img,imgCrop,style) {
const js = `
/*
StackBlur - a fast almost Gaussian Blur For Canvas
Version: 0.5
Author: Mario Klingemann
Contact: mario@quasimondo.com
Website: http://quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
Twitter: @quasimondo
In case you find this class useful - especially in commercial projects -
I am not totally unhappy for a small donation to my PayPal account
mario@quasimondo.de
Or support me on flattr:
https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript
Copyright (c) 2010 Mario Klingemann
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
var mul_table = [
512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512,
454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512,
482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456,
437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512,
497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328,
320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456,
446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335,
329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512,
505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405,
399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328,
324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271,
268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456,
451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388,
385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335,
332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292,
289,287,285,282,280,278,275,273,271,269,267,265,263,261,259];
var shg_table = [
9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 ];
function stackBlurCanvasRGB( id, top_x, top_y, width, height, radius )
{
if ( isNaN(radius) || radius < 1 ) return;
radius |= 0;
var canvas = document.getElementById( id );
var context = canvas.getContext("2d");
var imageData;
try {
try {
imageData = context.getImageData( top_x, top_y, width, height );
} catch(e) {
// NOTE: this part is supposedly only needed if you want to work with local files
// so it might be okay to remove the whole try/catch block and just use
// imageData = context.getImageData( top_x, top_y, width, height );
try {
netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
imageData = context.getImageData( top_x, top_y, width, height );
} catch(e) {
alert("Cannot access local image");
throw new Error("unable to access local image data: " + e);
return;
}
}
} catch(e) {
alert("Cannot access image");
throw new Error("unable to access image data: " + e);
}
var pixels = imageData.data;
var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum,
r_out_sum, g_out_sum, b_out_sum,
r_in_sum, g_in_sum, b_in_sum,
pr, pg, pb, rbs;
var div = radius + radius + 1;
var w4 = width << 2;
var widthMinus1 = width - 1;
var heightMinus1 = height - 1;
var radiusPlus1 = radius + 1;
var sumFactor = radiusPlus1 * ( radiusPlus1 + 1 ) / 2;
var stackStart = new BlurStack();
var stack = stackStart;
for ( i = 1; i < div; i++ )
{
stack = stack.next = new BlurStack();
if ( i == radiusPlus1 ) var stackEnd = stack;
}
stack.next = stackStart;
var stackIn = null;
var stackOut = null;
yw = yi = 0;
var mul_sum = mul_table[radius];
var shg_sum = shg_table[radius];
for ( y = 0; y < height; y++ )
{
r_in_sum = g_in_sum = b_in_sum = r_sum = g_sum = b_sum = 0;
r_out_sum = radiusPlus1 * ( pr = pixels[yi] );
g_out_sum = radiusPlus1 * ( pg = pixels[yi+1] );
b_out_sum = radiusPlus1 * ( pb = pixels[yi+2] );
r_sum += sumFactor * pr;
g_sum += sumFactor * pg;
b_sum += sumFactor * pb;
stack = stackStart;
for( i = 0; i < radiusPlus1; i++ )
{
stack.r = pr;
stack.g = pg;
stack.b = pb;
stack = stack.next;
}
for( i = 1; i < radiusPlus1; i++ )
{
p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 );
r_sum += ( stack.r = ( pr = pixels[p])) * ( rbs = radiusPlus1 - i );
g_sum += ( stack.g = ( pg = pixels[p+1])) * rbs;
b_sum += ( stack.b = ( pb = pixels[p+2])) * rbs;
r_in_sum += pr;
g_in_sum += pg;
b_in_sum += pb;
stack = stack.next;
}
stackIn = stackStart;
stackOut = stackEnd;
for ( x = 0; x < width; x++ )
{
pixels[yi] = (r_sum * mul_sum) >> shg_sum;
pixels[yi+1] = (g_sum * mul_sum) >> shg_sum;
pixels[yi+2] = (b_sum * mul_sum) >> shg_sum;
r_sum -= r_out_sum;
g_sum -= g_out_sum;
b_sum -= b_out_sum;
r_out_sum -= stackIn.r;
g_out_sum -= stackIn.g;
b_out_sum -= stackIn.b;
p = ( yw + ( ( p = x + radius + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2;
r_in_sum += ( stackIn.r = pixels[p]);
g_in_sum += ( stackIn.g = pixels[p+1]);
b_in_sum += ( stackIn.b = pixels[p+2]);
r_sum += r_in_sum;
g_sum += g_in_sum;
b_sum += b_in_sum;
stackIn = stackIn.next;
r_out_sum += ( pr = stackOut.r );
g_out_sum += ( pg = stackOut.g );
b_out_sum += ( pb = stackOut.b );
r_in_sum -= pr;
g_in_sum -= pg;
b_in_sum -= pb;
stackOut = stackOut.next;
yi += 4;
}
yw += width;
}
for ( x = 0; x < width; x++ )
{
g_in_sum = b_in_sum = r_in_sum = g_sum = b_sum = r_sum = 0;
yi = x << 2;
r_out_sum = radiusPlus1 * ( pr = pixels[yi]);
g_out_sum = radiusPlus1 * ( pg = pixels[yi+1]);
b_out_sum = radiusPlus1 * ( pb = pixels[yi+2]);
r_sum += sumFactor * pr;
g_sum += sumFactor * pg;
b_sum += sumFactor * pb;
stack = stackStart;
for( i = 0; i < radiusPlus1; i++ )
{
stack.r = pr;
stack.g = pg;
stack.b = pb;
stack = stack.next;
}
yp = width;
for( i = 1; i <= radius; i++ )
{
yi = ( yp + x ) << 2;
r_sum += ( stack.r = ( pr = pixels[yi])) * ( rbs = radiusPlus1 - i );
g_sum += ( stack.g = ( pg = pixels[yi+1])) * rbs;
b_sum += ( stack.b = ( pb = pixels[yi+2])) * rbs;
r_in_sum += pr;
g_in_sum += pg;
b_in_sum += pb;
stack = stack.next;
if( i < heightMinus1 )
{
yp += width;
}
}
yi = x;
stackIn = stackStart;
stackOut = stackEnd;
for ( y = 0; y < height; y++ )
{
p = yi << 2;
pixels[p] = (r_sum * mul_sum) >> shg_sum;
pixels[p+1] = (g_sum * mul_sum) >> shg_sum;
pixels[p+2] = (b_sum * mul_sum) >> shg_sum;
r_sum -= r_out_sum;
g_sum -= g_out_sum;
b_sum -= b_out_sum;
r_out_sum -= stackIn.r;
g_out_sum -= stackIn.g;
b_out_sum -= stackIn.b;
p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2;
r_sum += ( r_in_sum += ( stackIn.r = pixels[p]));
g_sum += ( g_in_sum += ( stackIn.g = pixels[p+1]));
b_sum += ( b_in_sum += ( stackIn.b = pixels[p+2]));
stackIn = stackIn.next;
r_out_sum += ( pr = stackOut.r );
g_out_sum += ( pg = stackOut.g );
b_out_sum += ( pb = stackOut.b );
r_in_sum -= pr;
g_in_sum -= pg;
b_in_sum -= pb;
stackOut = stackOut.next;
yi += width;
}
}
context.putImageData( imageData, top_x, top_y );
}
function BlurStack()
{
this.r = 0;
this.g = 0;
this.b = 0;
this.a = 0;
this.next = null;
}
// https://gist.github.com/mjackson/5311256
function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function lightBlur(hsl) {
// Adjust the luminance.
let lumCalc = 0.35 + (0.3 / hsl[2]);
if (lumCalc < 1) { lumCalc = 1; }
else if (lumCalc > 3.3) { lumCalc = 3.3; }
const l = hsl[2] * lumCalc;
// Adjust the saturation.
const colorful = 2 * hsl[1] * l;
const s = hsl[1] * colorful * 1.5;
return [hsl[0],s,l];
}
function darkBlur(hsl) {
// Adjust the saturation.
const colorful = 2 * hsl[1] * hsl[2];
const s = hsl[1] * (1 - hsl[2]) * 3;
return [hsl[0],s,hsl[2]];
}
// Set up the canvas.
const img = document.getElementById("blurImg");
const canvas = document.getElementById("mainCanvas");
const w = img.naturalWidth;
const h = img.naturalHeight;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
canvas.width = w;
canvas.height = h;
const context = canvas.getContext("2d");
context.clearRect( 0, 0, w, h );
context.drawImage( img, 0, 0 );
// Get the image data from the context.
var imageData = context.getImageData(0,0,w,h);
var pix = imageData.data;
// Set the image function, if any.
var imageFunc;
var style = "${style}";
if (style == "dark") { imageFunc = darkBlur; }
else if (style == "light") { imageFunc = lightBlur; }
for (let i=0; i < pix.length; i+=4) {
// Convert to HSL.
let hsl = rgbToHsl(pix[i],pix[i+1],pix[i+2]);
// Apply the image function if it exists.
if (imageFunc) { hsl = imageFunc(hsl); }
// Convert back to RGB.
const rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
// Put the values back into the data.
pix[i] = rgb[0];
pix[i+1] = rgb[1];
pix[i+2] = rgb[2];
}
// Draw over the old image.
context.putImageData(imageData,0,0);
// Blur the image.
stackBlurCanvasRGB("mainCanvas", 0, 0, w, h, ${blur});
// Perform the additional processing for dark images.
if (style == "dark") {
// Draw the hard light box over it.
context.globalCompositeOperation = "hard-light";
context.fillStyle = "rgba(55,55,55,0.2)";
context.fillRect(0, 0, w, h);
// Draw the soft light box over it.
context.globalCompositeOperation = "soft-light";
context.fillStyle = "rgba(55,55,55,1)";
context.fillRect(0, 0, w, h);
// Draw the regular box over it.
context.globalCompositeOperation = "source-over";
context.fillStyle = "rgba(55,55,55,0.4)";
context.fillRect(0, 0, w, h);
// Otherwise process light images.
} else if (style == "light") {
context.fillStyle = "rgba(255,255,255,0.4)";
context.fillRect(0, 0, w, h);
}
// Return a base64 representation.
canvas.toDataURL();
`
// Convert the images and create the HTML.
let blurImgData = Data.fromPNG(img).toBase64String()
let html = `
<img id="blurImg" src="data:image/png;base64,${blurImgData}" />
<canvas id="mainCanvas" />
`
// Make the web view and get its return value.
let view = new WebView()
await view.loadHTML(html)
let returnValue = await view.evaluateJavaScript(js)
// Remove the data type from the string and convert to data.
let imageDataString = returnValue.slice(22)
let imageData = Data.fromBase64String(imageDataString)
// Convert to image and crop before returning.
let imageFromData = Image.fromData(imageData)
return cropImage(imageFromData)
}
// Pixel sizes and positions for widgets on all supported phones.
function phoneSizes() {
let phones = {
// 12 Pro Max
"2778": {
small: 510,
medium: 1092,
large: 1146,
left: 96,
right: 678,
top: 246,
middle: 882,
bottom: 1518
},
// 12 and 12 Pro
"2532": {
small: 474,
medium: 1014,
large: 1062,
left: 78,
right: 618,
top: 231,
middle: 819,
bottom: 1407
},
// 11 Pro Max, XS Max
"2688": {
small: 507,
medium: 1080,
large: 1137,
left: 81,
right: 654,
top: 228,
middle: 858,
bottom: 1488
},
// 11, XR
"1792": {
small: 338,
medium: 720,
large: 758,
left: 54,
right: 436,
top: 160,
middle: 580,
bottom: 1000
},
// 11 Pro, XS, X, 12 mini
"2436": {
x: {
small: 465,
medium: 987,
large: 1035,
left: 69,
right: 591,
top: 213,
middle: 783,
bottom: 1353,
},
mini: {
small: 465,
medium: 987,
large: 1035,
left: 69,
right: 591,
top: 231,
middle: 801,
bottom: 1371,
}
},
// Plus phones
"2208": {
small: 471,
medium: 1044,
large: 1071,
left: 99,
right: 672,
top: 114,
middle: 696,
bottom: 1278
},
// SE2 and 6/6S/7/8
"1334": {
small: 296,
medium: 642,
large: 648,
left: 54,
right: 400,
top: 60,
middle: 412,
bottom: 764
},
// SE1
"1136": {
small: 282,
medium: 584,
large: 622,
left: 30,
right: 332,
top: 59,
middle: 399,
bottom: 399
},
// 11 and XR in Display Zoom mode
"1624": {
small: 310,
medium: 658,
large: 690,
left: 46,
right: 394,
top: 142,
middle: 522,
bottom: 902
},
// Plus in Display Zoom mode
"2001" : {
small: 444,
medium: 963,
large: 972,
left: 81,
right: 600,
top: 90,
middle: 618,
bottom: 1146
},
}
return phones
}

View file

@ -0,0 +1,807 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: yellow; icon-glyph: magic;
// This script was created by Max Zeryck.
// The amount of blurring. Default is 150.
let blur = 150
// Determine if user has taken the screenshot.
var message
message = "Before you start, go to your home screen and enter wiggle mode. Scroll to the empty page on the far right and take a screenshot."
let options = ["Continue to select image","Exit to take screenshot","Update code"]
let response = await generateAlert(message,options)
// Return if we need to exit.
if (response == 1) return
// Update the code.
if (response == 2) {
// Determine if the user is using iCloud.
let files = FileManager.local()
const iCloudInUse = files.isFileStoredIniCloud(module.filename)
// If so, use an iCloud file manager.
files = iCloudInUse ? FileManager.iCloud() : files
// Try to download the file.
try {
const req = new Request("https://raw.githubusercontent.com/mzeryck/Widget-Blur/main/widget-blur.js")
const codeString = await req.loadString()
files.writeString(module.filename, codeString)
message = "The code has been updated. If the script is open, close it for the change to take effect."
} catch {
message = "The update failed. Please try again later."
}
options = ["OK"]
await generateAlert(message,options)
return
}
// Get screenshot and determine phone size.
let img = await Photos.fromLibrary()
let height = img.size.height
let phone = phoneSizes()[height]
if (!phone) {
message = "It looks like you selected an image that isn't an iPhone screenshot, or your iPhone is not supported. Try again with a different image."
await generateAlert(message,["OK"])
return
}
// Extra setup needed for 2436-sized phones.
if (height == 2436) {
let files = FileManager.local()
let cacheName = "mz-phone-type"
let cachePath = files.joinPath(files.libraryDirectory(), cacheName)
// If we already cached the phone size, load it.
if (files.fileExists(cachePath)) {
let typeString = files.readString(cachePath)
phone = phone[typeString]
// Otherwise, prompt the user.
} else {
message = "What type of iPhone do you have?"
let types = ["iPhone 12 mini", "iPhone 11 Pro, XS, or X"]
let typeIndex = await generateAlert(message, types)
let type = (typeIndex == 0) ? "mini" : "x"
phone = phone[type]
files.writeString(cachePath, type)
}
}
// Prompt for widget size and position.
message = "What size of widget are you creating?"
let sizes = ["Small","Medium","Large"]
let size = await generateAlert(message,sizes)
let widgetSize = sizes[size]
message = "What position will it be in?"
message += (height == 1136 ? " (Note that your device only supports two rows of widgets, so the middle and bottom options are the same.)" : "")
// Determine image crop based on phone size.
let crop = { w: "", h: "", x: "", y: "" }
if (widgetSize == "Small") {
crop.w = phone.small
crop.h = phone.small
let positions = ["Top left","Top right","Middle left","Middle right","Bottom left","Bottom right"]
let position = await generateAlert(message,positions)
// Convert the two words into two keys for the phone size dictionary.
let keys = positions[position].toLowerCase().split(' ')
crop.y = phone[keys[0]]
crop.x = phone[keys[1]]
} else if (widgetSize == "Medium") {
crop.w = phone.medium
crop.h = phone.small
// Medium and large widgets have a fixed x-value.
crop.x = phone.left
let positions = ["Top","Middle","Bottom"]
let position = await generateAlert(message,positions)
let key = positions[position].toLowerCase()
crop.y = phone[key]
} else if(widgetSize == "Large") {
crop.w = phone.medium
crop.h = phone.large
crop.x = phone.left
let positions = ["Top","Bottom"]
let position = await generateAlert(message,positions)
// Large widgets at the bottom have the "middle" y-value.
crop.y = position ? phone.middle : phone.top
}
// Prompt for blur style.
message = "Do you want a fully transparent widget, or a translucent blur effect?"
let blurOptions = ["Transparent","Light blur","Dark blur","Just blur"]
let blurred = await generateAlert(message,blurOptions)
// We always need the cropped image.
let imgCrop = cropImage(img)
// If it's blurred, set the blur style.
if (blurred) {
const styles = ["", "light", "dark", "none"]
const style = styles[blurred]
imgCrop = await blurImage(img,imgCrop,style)
}
message = "Your widget background is ready. Choose where to save the image:"
const exportPhotoOptions = ["Export to the Photos app","Export to the Files app"]
const exportToFiles = await generateAlert(message,exportPhotoOptions)
if (exportToFiles) {
await DocumentPicker.exportImage(imgCrop)
} else {
Photos.save(imgCrop)
}
Script.complete()
// Generate an alert with the provided array of options.
async function generateAlert(message,options) {
let alert = new Alert()
alert.message = message
for (const option of options) {
alert.addAction(option)
}
let response = await alert.presentAlert()
return response
}
// Crop an image into the specified rect.
function cropImage(image) {
let draw = new DrawContext()
let rect = new Rect(crop.x,crop.y,crop.w,crop.h)
draw.size = new Size(rect.width, rect.height)
draw.drawImageAtPoint(image,new Point(-rect.x, -rect.y))
return draw.getImage()
}
async function blurImage(img,imgCrop,style) {
const js = `
/*
StackBlur - a fast almost Gaussian Blur For Canvas
Version: 0.5
Author: Mario Klingemann
Contact: mario@quasimondo.com
Website: http://quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
Twitter: @quasimondo
In case you find this class useful - especially in commercial projects -
I am not totally unhappy for a small donation to my PayPal account
mario@quasimondo.de
Or support me on flattr:
https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript
Copyright (c) 2010 Mario Klingemann
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
var mul_table = [
512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512,
454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512,
482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456,
437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512,
497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328,
320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456,
446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335,
329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512,
505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405,
399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328,
324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271,
268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456,
451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388,
385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335,
332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292,
289,287,285,282,280,278,275,273,271,269,267,265,263,261,259];
var shg_table = [
9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 ];
function stackBlurCanvasRGB( id, top_x, top_y, width, height, radius )
{
if ( isNaN(radius) || radius < 1 ) return;
radius |= 0;
var canvas = document.getElementById( id );
var context = canvas.getContext("2d");
var imageData;
try {
try {
imageData = context.getImageData( top_x, top_y, width, height );
} catch(e) {
// NOTE: this part is supposedly only needed if you want to work with local files
// so it might be okay to remove the whole try/catch block and just use
// imageData = context.getImageData( top_x, top_y, width, height );
try {
netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
imageData = context.getImageData( top_x, top_y, width, height );
} catch(e) {
alert("Cannot access local image");
throw new Error("unable to access local image data: " + e);
return;
}
}
} catch(e) {
alert("Cannot access image");
throw new Error("unable to access image data: " + e);
}
var pixels = imageData.data;
var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum,
r_out_sum, g_out_sum, b_out_sum,
r_in_sum, g_in_sum, b_in_sum,
pr, pg, pb, rbs;
var div = radius + radius + 1;
var w4 = width << 2;
var widthMinus1 = width - 1;
var heightMinus1 = height - 1;
var radiusPlus1 = radius + 1;
var sumFactor = radiusPlus1 * ( radiusPlus1 + 1 ) / 2;
var stackStart = new BlurStack();
var stack = stackStart;
for ( i = 1; i < div; i++ )
{
stack = stack.next = new BlurStack();
if ( i == radiusPlus1 ) var stackEnd = stack;
}
stack.next = stackStart;
var stackIn = null;
var stackOut = null;
yw = yi = 0;
var mul_sum = mul_table[radius];
var shg_sum = shg_table[radius];
for ( y = 0; y < height; y++ )
{
r_in_sum = g_in_sum = b_in_sum = r_sum = g_sum = b_sum = 0;
r_out_sum = radiusPlus1 * ( pr = pixels[yi] );
g_out_sum = radiusPlus1 * ( pg = pixels[yi+1] );
b_out_sum = radiusPlus1 * ( pb = pixels[yi+2] );
r_sum += sumFactor * pr;
g_sum += sumFactor * pg;
b_sum += sumFactor * pb;
stack = stackStart;
for( i = 0; i < radiusPlus1; i++ )
{
stack.r = pr;
stack.g = pg;
stack.b = pb;
stack = stack.next;
}
for( i = 1; i < radiusPlus1; i++ )
{
p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 );
r_sum += ( stack.r = ( pr = pixels[p])) * ( rbs = radiusPlus1 - i );
g_sum += ( stack.g = ( pg = pixels[p+1])) * rbs;
b_sum += ( stack.b = ( pb = pixels[p+2])) * rbs;
r_in_sum += pr;
g_in_sum += pg;
b_in_sum += pb;
stack = stack.next;
}
stackIn = stackStart;
stackOut = stackEnd;
for ( x = 0; x < width; x++ )
{
pixels[yi] = (r_sum * mul_sum) >> shg_sum;
pixels[yi+1] = (g_sum * mul_sum) >> shg_sum;
pixels[yi+2] = (b_sum * mul_sum) >> shg_sum;
r_sum -= r_out_sum;
g_sum -= g_out_sum;
b_sum -= b_out_sum;
r_out_sum -= stackIn.r;
g_out_sum -= stackIn.g;
b_out_sum -= stackIn.b;
p = ( yw + ( ( p = x + radius + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2;
r_in_sum += ( stackIn.r = pixels[p]);
g_in_sum += ( stackIn.g = pixels[p+1]);
b_in_sum += ( stackIn.b = pixels[p+2]);
r_sum += r_in_sum;
g_sum += g_in_sum;
b_sum += b_in_sum;
stackIn = stackIn.next;
r_out_sum += ( pr = stackOut.r );
g_out_sum += ( pg = stackOut.g );
b_out_sum += ( pb = stackOut.b );
r_in_sum -= pr;
g_in_sum -= pg;
b_in_sum -= pb;
stackOut = stackOut.next;
yi += 4;
}
yw += width;
}
for ( x = 0; x < width; x++ )
{
g_in_sum = b_in_sum = r_in_sum = g_sum = b_sum = r_sum = 0;
yi = x << 2;
r_out_sum = radiusPlus1 * ( pr = pixels[yi]);
g_out_sum = radiusPlus1 * ( pg = pixels[yi+1]);
b_out_sum = radiusPlus1 * ( pb = pixels[yi+2]);
r_sum += sumFactor * pr;
g_sum += sumFactor * pg;
b_sum += sumFactor * pb;
stack = stackStart;
for( i = 0; i < radiusPlus1; i++ )
{
stack.r = pr;
stack.g = pg;
stack.b = pb;
stack = stack.next;
}
yp = width;
for( i = 1; i <= radius; i++ )
{
yi = ( yp + x ) << 2;
r_sum += ( stack.r = ( pr = pixels[yi])) * ( rbs = radiusPlus1 - i );
g_sum += ( stack.g = ( pg = pixels[yi+1])) * rbs;
b_sum += ( stack.b = ( pb = pixels[yi+2])) * rbs;
r_in_sum += pr;
g_in_sum += pg;
b_in_sum += pb;
stack = stack.next;
if( i < heightMinus1 )
{
yp += width;
}
}
yi = x;
stackIn = stackStart;
stackOut = stackEnd;
for ( y = 0; y < height; y++ )
{
p = yi << 2;
pixels[p] = (r_sum * mul_sum) >> shg_sum;
pixels[p+1] = (g_sum * mul_sum) >> shg_sum;
pixels[p+2] = (b_sum * mul_sum) >> shg_sum;
r_sum -= r_out_sum;
g_sum -= g_out_sum;
b_sum -= b_out_sum;
r_out_sum -= stackIn.r;
g_out_sum -= stackIn.g;
b_out_sum -= stackIn.b;
p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2;
r_sum += ( r_in_sum += ( stackIn.r = pixels[p]));
g_sum += ( g_in_sum += ( stackIn.g = pixels[p+1]));
b_sum += ( b_in_sum += ( stackIn.b = pixels[p+2]));
stackIn = stackIn.next;
r_out_sum += ( pr = stackOut.r );
g_out_sum += ( pg = stackOut.g );
b_out_sum += ( pb = stackOut.b );
r_in_sum -= pr;
g_in_sum -= pg;
b_in_sum -= pb;
stackOut = stackOut.next;
yi += width;
}
}
context.putImageData( imageData, top_x, top_y );
}
function BlurStack()
{
this.r = 0;
this.g = 0;
this.b = 0;
this.a = 0;
this.next = null;
}
// https://gist.github.com/mjackson/5311256
function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function lightBlur(hsl) {
// Adjust the luminance.
let lumCalc = 0.35 + (0.3 / hsl[2]);
if (lumCalc < 1) { lumCalc = 1; }
else if (lumCalc > 3.3) { lumCalc = 3.3; }
const l = hsl[2] * lumCalc;
// Adjust the saturation.
const colorful = 2 * hsl[1] * l;
const s = hsl[1] * colorful * 1.5;
return [hsl[0],s,l];
}
function darkBlur(hsl) {
// Adjust the saturation.
const colorful = 2 * hsl[1] * hsl[2];
const s = hsl[1] * (1 - hsl[2]) * 3;
return [hsl[0],s,hsl[2]];
}
// Set up the canvas.
const img = document.getElementById("blurImg");
const canvas = document.getElementById("mainCanvas");
const w = img.naturalWidth;
const h = img.naturalHeight;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
canvas.width = w;
canvas.height = h;
const context = canvas.getContext("2d");
context.clearRect( 0, 0, w, h );
context.drawImage( img, 0, 0 );
// Get the image data from the context.
var imageData = context.getImageData(0,0,w,h);
var pix = imageData.data;
// Set the image function, if any.
var imageFunc;
var style = "${style}";
if (style == "dark") { imageFunc = darkBlur; }
else if (style == "light") { imageFunc = lightBlur; }
for (let i=0; i < pix.length; i+=4) {
// Convert to HSL.
let hsl = rgbToHsl(pix[i],pix[i+1],pix[i+2]);
// Apply the image function if it exists.
if (imageFunc) { hsl = imageFunc(hsl); }
// Convert back to RGB.
const rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
// Put the values back into the data.
pix[i] = rgb[0];
pix[i+1] = rgb[1];
pix[i+2] = rgb[2];
}
// Draw over the old image.
context.putImageData(imageData,0,0);
// Blur the image.
stackBlurCanvasRGB("mainCanvas", 0, 0, w, h, ${blur});
// Perform the additional processing for dark images.
if (style == "dark") {
// Draw the hard light box over it.
context.globalCompositeOperation = "hard-light";
context.fillStyle = "rgba(55,55,55,0.2)";
context.fillRect(0, 0, w, h);
// Draw the soft light box over it.
context.globalCompositeOperation = "soft-light";
context.fillStyle = "rgba(55,55,55,1)";
context.fillRect(0, 0, w, h);
// Draw the regular box over it.
context.globalCompositeOperation = "source-over";
context.fillStyle = "rgba(55,55,55,0.4)";
context.fillRect(0, 0, w, h);
// Otherwise process light images.
} else if (style == "light") {
context.fillStyle = "rgba(255,255,255,0.4)";
context.fillRect(0, 0, w, h);
}
// Return a base64 representation.
canvas.toDataURL();
`
// Convert the images and create the HTML.
let blurImgData = Data.fromPNG(img).toBase64String()
let html = `
<img id="blurImg" src="data:image/png;base64,${blurImgData}" />
<canvas id="mainCanvas" />
`
// Make the web view and get its return value.
let view = new WebView()
await view.loadHTML(html)
let returnValue = await view.evaluateJavaScript(js)
// Remove the data type from the string and convert to data.
let imageDataString = returnValue.slice(22)
let imageData = Data.fromBase64String(imageDataString)
// Convert to image and crop before returning.
let imageFromData = Image.fromData(imageData)
return cropImage(imageFromData)
}
// Pixel sizes and positions for widgets on all supported phones.
function phoneSizes() {
let phones = {
// 12 Pro Max
"2778": {
small: 510,
medium: 1092,
large: 1146,
left: 96,
right: 678,
top: 246,
middle: 882,
bottom: 1518
},
// 12 and 12 Pro
"2532": {
small: 474,
medium: 1014,
large: 1062,
left: 78,
right: 618,
top: 231,
middle: 819,
bottom: 1407
},
// 11 Pro Max, XS Max
"2688": {
small: 507,
medium: 1080,
large: 1137,
left: 81,
right: 654,
top: 228,
middle: 858,
bottom: 1488
},
// 11, XR
"1792": {
small: 338,
medium: 720,
large: 758,
left: 55,
right: 437,
top: 159,
middle: 579,
bottom: 999
},
// 11 Pro, XS, X, 12 mini
"2436": {
x: {
small: 465,
medium: 987,
large: 1035,
left: 69,
right: 591,
top: 213,
middle: 783,
bottom: 1353,
},
mini: {
small: 465,
medium: 987,
large: 1035,
left: 69,
right: 591,
top: 231,
middle: 801,
bottom: 1371,
}
},
// Plus phones
"2208": {
small: 471,
medium: 1044,
large: 1071,
left: 99,
right: 672,
top: 114,
middle: 696,
bottom: 1278
},
// SE2 and 6/6S/7/8
"1334": {
small: 296,
medium: 642,
large: 648,
left: 54,
right: 400,
top: 60,
middle: 412,
bottom: 764
},
// SE1
"1136": {
small: 282,
medium: 584,
large: 622,
left: 30,
right: 332,
top: 59,
middle: 399,
bottom: 399
},
// 11 and XR in Display Zoom mode
"1624": {
small: 310,
medium: 658,
large: 690,
left: 46,
right: 394,
top: 142,
middle: 522,
bottom: 902
},
// Plus in Display Zoom mode
"2001" : {
small: 444,
medium: 963,
large: 972,
left: 81,
right: 600,
top: 90,
middle: 618,
bottom: 1146
},
}
return phones
}

166
Untitled Script 1.js Normal file
View file

@ -0,0 +1,166 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: red; icon-glyph: magic;
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: gray; icon-glyph: magic;
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: gray; icon-glyph: magic;
const fm = FileManager.iCloud();
const dir = fm.documentsDirectory();
const downloadIfNeeded = async (pkg, isAutoUpdateOn) => {
let name = getPackageName(pkg);
let filePath = fm.joinPath(dir, name + '.js');
let isInstalled = await isFound(filePath);
// If the package exists and autoupdate is off, stop checking further
if (isInstalled && !isAutoUpdateOn) {
console.log(`'${name}' is already installed, and autoupdate is disabled! Proceeding to import from disk...`);
return;
}
// Get the package information which satisfies the given semver range
let versionInfo = await getStatus(pkg);
let versions = versionInfo.satisfied;
let version = versionInfo.highest;
// Download the newer version if necessary
if (isInstalled && isAutoUpdateOn) {
let installedVersion = await getInstalledVersion(name);
// Check if the installed version satisfies the semver range
if (versions.includes(installedVersion)) {
console.log(`'${name}@${installedVersion}' satisfies the requested version. Good to go!`);
return;
} else {
console.log(`'${name}@${installedVersion}' doesn't match the version requested. Reinstalling '${version}' now...`);
}
} else {
console.log(`'${name}' was never installed previously. Downloading now...`);
}
// Download the package source and save to disk
let source = await getPackageSource(pkg);
savePackageToDisk(name, version, source);
};
const getInstalledVersion = async name => {
// Read the version from {package}.ver
let filePath = fm.joinPath(dir, name + '.ver');
let version;
if (isFound(filePath)) {
let content = fm.readString(filePath);
if (/^\d+\.\d+\.\d+$/g.test(content)) {
version = content;
}
}
console.log(`The installed version of '${name}' is ${version}.`);
return version;
};
const getPackageSource = async pkg => {
// Get the standalone package source from wzrd.in
let request = new Request(`https://wzrd.in/standalone/${encodeURIComponent(pkg)}`);
let response = await request.loadString();
return response;
};
const getPackageName = pkg => {
return pkg.split('@')[0];
};
const getStatus = async pkg => {
// Retrieve the information about the package
let request = new Request(`https://wzrd.in/status/${encodeURIComponent(pkg)}`);
let response = await request.loadJSON();
// Fail if the response is not good
if (response.statusCode >= 400 || response.ok === false) {
throw response.message;
}
// Fail if the semver did not satisfy any versions available on npm
// Otherwise, sort the versions in descending order
let versions = response.builds && Object.keys(response.builds);
if (versions.length < 1) {
throw `'${pkg}' did not satisfy any versions available on npm!`;
} else {
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
}
// Get all the satisfied versions and the highest version
let result = {
highest: versions[0],
satisfied: versions,
};
return result;
};
const isFound = async filePath => {
// Check if the package is already downloaded
if (fm.fileExists(filePath)) {
return true;
}
// Sync with iCloud and check again
await syncFileWithiCloud(filePath);
if (fm.fileExists(filePath)) {
return true;
}
return false;
};
const savePackageToDisk = (name, version, source) => {
// Write the package source and version info to disk
let filename = fm.joinPath(dir, name);
let jsFilePath = filename + '.js';
let versionFilePath = filename + '.ver';
let pkg = `${name}@${version}`;
tryWriteFile(jsFilePath, source, pkg);
tryWriteFile(versionFilePath, version, pkg);
console.log(`Successfully installed ${name}@${version}!`);
};
const syncFileWithiCloud = async filePath => {
// Try to sync with iCloud in case the package exists only on iCloud
try {
console.log(`Attempting to sync with iCloud just in case...`);
await fm.downloadFileFromiCloud(filePath);
console.log(`Finished syncing ${filePath}`);
} catch (err) {
console.log(`${filePath} does not exist on iCloud.`);
}
};
const tryWriteFile = (path, content, pkg) => {
// Sometimes wzrd.in is acting up and the file content is undefined.
// So, here is a little trick to let you know what's going on.
try {
console.log(`Saving ${pkg} to disk at ${path}...`);
fm.writeString(path, content);
} catch (err) {
throw `The package source from 'https://wzrd.in/standalone/${pkg}' is probably corrupted! Try with the different patch version.`;
}
};
module.exports = async (pkg, isAutoUpdateOn = false) => {
let name = getPackageName(pkg);
await downloadIfNeeded(pkg, isAutoUpdateOn);
return importModule(`${name}`);
};
// Defaults to the latest version and no automatic update; if the file exists, just use it
const moment = await require('moment');
// Use any SemVer options to specify a version you want
// Refer to the calculator here: https://semver.npmjs.com/
const lodash = await require('lodash@^3.9.1');
// Pass the second parameter to auto-update or force-download to satisfy the version specified
const d3 = await require('d3@>5.3.0', true);
console.log(moment());

150
Untitled Script 2.js Normal file
View file

@ -0,0 +1,150 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-blue; icon-glyph: magic;
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: gray; icon-glyph: magic;
const fm = FileManager.iCloud();
const dir = fm.documentsDirectory();
const downloadIfNeeded = async (pkg, isAutoUpdateOn) => {
let name = getPackageName(pkg);
let filePath = fm.joinPath(dir, name + '.js');
let isInstalled = await isFound(filePath);
// If the package exists and autoupdate is off, stop checking further
if (isInstalled && !isAutoUpdateOn) {
console.log(`'${name}' is already installed, and autoupdate is disabled! Proceeding to import from disk...`);
return;
}
// Get the package information which satisfies the given semver range
let versionInfo = await getStatus(pkg);
let versions = versionInfo.satisfied;
let version = versionInfo.highest;
// Download the newer version if necessary
if (isInstalled && isAutoUpdateOn) {
let installedVersion = await getInstalledVersion(name);
// Check if the installed version satisfies the semver range
if (versions.includes(installedVersion)) {
console.log(`'${name}@${installedVersion}' satisfies the requested version. Good to go!`);
return;
} else {
console.log(`'${name}@${installedVersion}' doesn't match the version requested. Reinstalling '${version}' now...`);
}
} else {
console.log(`'${name}' was never installed previously. Downloading now...`);
}
// Download the package source and save to disk
let source = await getPackageSource(pkg);
savePackageToDisk(name, version, source);
};
const getInstalledVersion = async name => {
// Read the version from {package}.ver
let filePath = fm.joinPath(dir, name + '.ver');
let version;
if (isFound(filePath)) {
let content = fm.readString(filePath);
if (/^\d+\.\d+\.\d+$/g.test(content)) {
version = content;
}
}
console.log(`The installed version of '${name}' is ${version}.`);
return version;
};
const getPackageSource = async pkg => {
// Get the standalone package source from wzrd.in
let request = new Request(`https://wzrd.in/standalone/${encodeURIComponent(pkg)}`);
let response = await request.loadString();
return response;
};
const getPackageName = pkg => {
return pkg.split('@')[0];
};
const getStatus = async pkg => {
// Retrieve the information about the package
let request = new Request(`https://wzrd.in/status/${encodeURIComponent(pkg)}`);
let response = await request.loadJSON();
// Fail if the response is not good
if (response.statusCode >= 400 || response.ok === false) {
throw response.message;
}
// Fail if the semver did not satisfy any versions available on npm
// Otherwise, sort the versions in descending order
let versions = response.builds && Object.keys(response.builds);
if (versions.length < 1) {
throw `'${pkg}' did not satisfy any versions available on npm!`;
} else {
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
}
// Get all the satisfied versions and the highest version
let result = {
highest: versions[0],
satisfied: versions,
};
return result;
};
const isFound = async filePath => {
// Check if the package is already downloaded
if (fm.fileExists(filePath)) {
return true;
}
// Sync with iCloud and check again
await syncFileWithiCloud(filePath);
if (fm.fileExists(filePath)) {
return true;
}
return false;
};
const savePackageToDisk = (name, version, source) => {
// Write the package source and version info to disk
let filename = fm.joinPath(dir, name);
let jsFilePath = filename + '.js';
let versionFilePath = filename + '.ver';
let pkg = `${name}@${version}`;
tryWriteFile(jsFilePath, source, pkg);
tryWriteFile(versionFilePath, version, pkg);
console.log(`Successfully installed ${name}@${version}!`);
};
const syncFileWithiCloud = async filePath => {
// Try to sync with iCloud in case the package exists only on iCloud
try {
console.log(`Attempting to sync with iCloud just in case...`);
await fm.downloadFileFromiCloud(filePath);
console.log(`Finished syncing ${filePath}`);
} catch (err) {
console.log(`${filePath} does not exist on iCloud.`);
}
};
const tryWriteFile = (path, content, pkg) => {
// Sometimes wzrd.in is acting up and the file content is undefined.
// So, here is a little trick to let you know what's going on.
try {
console.log(`Saving ${pkg} to disk at ${path}...`);
fm.writeString(path, content);
} catch (err) {
throw `The package source from 'https://wzrd.in/standalone/${pkg}' is probably corrupted! Try with the different patch version.`;
}
};
module.exports = async (pkg, isAutoUpdateOn = false) => {
let name = getPackageName(pkg);
await downloadIfNeeded(pkg, isAutoUpdateOn);
return importModule(`${name}`);
};

3
Untitled Script 3.js Normal file
View file

@ -0,0 +1,3 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: light-gray; icon-glyph: magic;

18
Untitled Script 4.js Normal file
View file

@ -0,0 +1,18 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: purple; icon-glyph: magic;
const HASS_LLAT = Keychain.get("hass_llat");
const HASS_API_URL = "https://hass-new.adhd.energy/api";
async function fetchStates() {
var wordle_req = new Request(HASS_API_URL + "/states/sensor.aaron_wordle")
wordle_req.headers = {
"Authorization": "Bearer " + HASS_LLAT,
"Content-Type": "application/json"
}
console.log(wordle_req)
return await wordle_req.loadJSON();
}
console.log(fetchStates().response)

3
Untitled Script.js Normal file
View file

@ -0,0 +1,3 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: magic;

95
Welcome to Scriptable.js Normal file
View file

@ -0,0 +1,95 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: magic-wand;
/************
* Thank you for downloading Scriptable.
* This script highlights some of the
* features in the app. It fetches the
* latest news from MacStories and
* presents the headlines along with an
* image in a table. You can even run the
* script from a Siri Shortcut.
*
* Press the play button to run
* the script.
*
* Congratulations! You've just run your
* first script in Scriptable.
*
* Now let's create a Siri Shortcut.
* Press the "toggles" to open the script
* settings. Rhen press "Add to Siri".
* Follow the instructions on the screen.
*
* If you've created a Siri Shortcut,
* trigger the shortcut with Siri.
* This presents the latest news inside
* of Siri without even opening the app.
*
* Alright. It's time to become familiar
* with some of the APIs that Scriptable
* provides. Remember that you can always
* find the documentation for these APIs
* by pressing the paper button.
*
* You will find comments explaining what
* is going on in each of the steps in the
* script below.
*/
// First we need to fetch the news. We create a Request object which can make HTTP requests. MacStories provide their news in a JSON feed. The Request object can automatically parse JSON by calling the "loadJSON()" function. Note that "await" keyword here. "loadJSON()" returns a native JavaScript promise. This is an object which will provide a value sometime in the future. We use "await" to wait for this value and halt execution of the script in the mean time.
let url = "https://macstories.net/feed/json"
let req = new Request(url)
let json = await req.loadJSON()
// We want to present the articles in a table, so we create a new UITable. A table contains rows which are displayed vertically. A row in turn contains cells which are displayed horizontally.
let table = new UITable()
for (item of json.items) {
// For each item, i.e. each story, we create a row in the table.
let row = new UITableRow()
// Call our extractImageURL function to extract an image URL from the HTML body of the story.
let body = item["content_html"]
let imageURL = extractImageURL(body)
// Call our decode() function to decode HTML entities from the title.
let title = decode(item.title)
// Add an image cell to the row. Cells are displayed in the order they are added, from left to right.
let imageCell = row.addImageAtURL(imageURL)
// Add the title cell to the row.
let titleCell = row.addText(title)
// Set the width weights of our cells. Cell widths are relative. In this case we have two cells, imageCell with a widthWeight of 20 and titleCell with a widthWeight of 80. This gives us a total widthWeight of 20 + 80 = 100. So the imageCell will fill 20/100 (20%) of the available screen space and the titleCell will fill 80/100 (80%) of the available screen space.
imageCell.widthWeight = 20
titleCell.widthWeight = 80
// Set height of the row and spacing between cells, in pixels.
row.height = 60
row.cellSpacing = 10
// Add the row to the table. Rows are displayed in the order they are added.
table.addRow(row)
}
// Presents the table using the QuickLook bridge. "Bridges" is the concept that allows JavaScript to use native iOS APIs. For example, presenting the table with QuickLook will present a native view containing the table. The same API also works in Siri.
QuickLook.present(table)
// We want Siri to say a kind message whenthe script is run using a Siri Shortcut.
// We use the global variable "config" to determine how the script is being run.
// The Speech API will speak a text using Siri. While this also works when the script is run with the app, it's much more enjoyable when the script is run from a Siri Shortcut.
if (config.runsWithSiri) {
Speech.speak("Here's the latest news.")
}
// It is good practice to call Script.complete() at the end of a script, especially when the script is used with Siri or in the Shortcuts app. This lets Scriptable report the results faster. Please see the documentation for details.
Script.complete()
// Finds the first image in the HTML and returns its URL. Returns null if no image is found.
function extractImageURL(html) {
let regex = /<img src="(.*)" alt="/
let matches = html.match(regex)
if (matches && matches.length >= 2) {
return matches[1]
} else {
return null
}
}
// Decodes HTML entities in the input string. Returns the result.
function decode(str) {
let regex = /&#(\d+);/g
return str.replace(regex, (match, dec) => {
return String.fromCharCode(dec)
})
}

365
iTermWidget.js Normal file
View file

@ -0,0 +1,365 @@
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-blue; icon-glyph: terminal;
/******************************************************************************
* Constants and Configurations
*****************************************************************************/
// Cache keys and default location
const CACHE_KEY_LAST_UPDATED = 'last_updated';
const CACHE_KEY_LOCATION = 'location';
const DEFAULT_LOCATION = { latitude: 0, longitude: 0 };
// Font name and size
const FONT_NAME = 'Menlo';
const FONT_SIZE = 10;
// Colors
const COLORS = {
bg0: '#29323c',
bg1: '#1c1c1c',
personalCalendar: '#5BD2F0',
workCalendar: '#9D90FF',
weather: '#FDFD97',
location: '#FEB144',
period: '#FF6663',
deviceStats: '#7AE7B9',
};
// TODO: PLEASE SET THESE VALUES
const NAME = 'TODO';
const WEATHER_API_KEY = 'TODO';
const WORK_CALENDAR_NAME = 'TODO';
const PERSONAL_CALENDAR_NAME = 'TODO';
const PERIOD_CALENDAR_NAME = 'TODO';
const PERIOD_EVENT_NAME = 'TODO';
/******************************************************************************
* Initial Setups
*****************************************************************************/
/**
* Convenience function to add days to a Date.
*
* @param {*} days The number of days to add
*/
Date.prototype.addDays = function(days) {
var date = new Date(this.valueOf());
date.setDate(date.getDate() + days);
return date;
};
// Import and setup Cache
const Cache = importModule('Cache');
const cache = new Cache('terminalWidget');
// Fetch data and create widget
const data = await fetchData();
const widget = createWidget(data);
Script.setWidget(widget);
Script.complete();
/******************************************************************************
* Main Functions (Widget and Data-Fetching)
*****************************************************************************/
/**
* Main widget function.
*
* @param {} data The data for the widget to display
*/
function createWidget(data) {
console.log(`Creating widget with data: ${JSON.stringify(data)}`);
const widget = new ListWidget();
const bgColor = new LinearGradient();
bgColor.colors = [new Color(COLORS.bg0), new Color(COLORS.bg1)];
bgColor.locations = [0.0, 1.0];
widget.backgroundGradient = bgColor;
widget.setPadding(10, 15, 15, 10);
const stack = widget.addStack();
stack.layoutVertically();
stack.spacing = 4;
stack.size = new Size(320, 0);
// Line 0 - Last Login
const timeFormatter = new DateFormatter();
timeFormatter.locale = "en";
timeFormatter.useNoDateStyle();
timeFormatter.useShortTimeStyle();
const lastLoginLine = stack.addText(`Last login: ${timeFormatter.string(new Date())} on ttys001`);
lastLoginLine.textColor = Color.white();
lastLoginLine.textOpacity = 0.7;
lastLoginLine.font = new Font(FONT_NAME, FONT_SIZE);
// Line 1 - Input
const inputLine = stack.addText(`iPhone:~ ${NAME}$ info`);
inputLine.textColor = Color.white();
inputLine.font = new Font(FONT_NAME, FONT_SIZE);
// Line 2 - Next Personal Calendar Event
const nextPersonalCalendarEventLine = stack.addText(`🗓 | ${getCalendarEventTitle(data.nextPersonalEvent, false)}`);
nextPersonalCalendarEventLine.textColor = new Color(COLORS.personalCalendar);
nextPersonalCalendarEventLine.font = new Font(FONT_NAME, FONT_SIZE);
// Line 3 - Next Work Calendar Event
const nextWorkCalendarEventLine = stack.addText(`🗓 | ${getCalendarEventTitle(data.nextWorkEvent, true)}`);
nextWorkCalendarEventLine.textColor = new Color(COLORS.workCalendar);
nextWorkCalendarEventLine.font = new Font(FONT_NAME, FONT_SIZE);
// Line 4 - Weather
const weatherLine = stack.addText(`${data.weather.icon} | ${data.weather.temperature}° (${data.weather.high}°-${data.weather.low}°), ${data.weather.description}, feels like ${data.weather.feelsLike}°`);
weatherLine.textColor = new Color(COLORS.weather);
weatherLine.font = new Font(FONT_NAME, FONT_SIZE);
// Line 5 - Location
const locationLine = stack.addText(`📍 | ${data.weather.location}`);
locationLine.textColor = new Color(COLORS.location);
locationLine.font = new Font(FONT_NAME, FONT_SIZE);
// Line 6 - Period
const periodLine = stack.addText(`🩸 | ${data.period}`);
periodLine.textColor = new Color(COLORS.period);
periodLine.font = new Font(FONT_NAME, FONT_SIZE);
// Line 7 - Various Device Stats
const deviceStatsLine = stack.addText(`📊 | ⚡︎ ${data.device.battery}%, ☀ ${data.device.brightness}%`);
deviceStatsLine.textColor = new Color(COLORS.deviceStats);
deviceStatsLine.font = new Font(FONT_NAME, FONT_SIZE);
return widget;
}
/**
* Fetch pieces of data for the widget.
*/
async function fetchData() {
// Get the weather data
const weather = await fetchWeather();
// Get next work/personal calendar events
const nextWorkEvent = await fetchNextCalendarEvent(WORK_CALENDAR_NAME);
const nextPersonalEvent = await fetchNextCalendarEvent(PERSONAL_CALENDAR_NAME);
// Get period data
const period = await fetchPeriodData();
// Get last data update time (and set)
const lastUpdated = await getLastUpdated();
cache.write(CACHE_KEY_LAST_UPDATED, new Date().getTime());
return {
weather,
nextWorkEvent,
nextPersonalEvent,
period,
device: {
battery: Math.round(Device.batteryLevel() * 100),
brightness: Math.round(Device.screenBrightness() * 100),
},
lastUpdated,
};
}
/******************************************************************************
* Helper Functions
*****************************************************************************/
//-------------------------------------
// Weather Helper Functions
//-------------------------------------
/**
* Fetch the weather data from Open Weather Map
*/
async function fetchWeather() {
let location = await cache.read(CACHE_KEY_LOCATION);
if (!location) {
try {
Location.setAccuracyToThreeKilometers();
location = await Location.current();
} catch(error) {
location = await cache.read(CACHE_KEY_LOCATION);
}
}
if (!location) {
location = DEFAULT_LOCATION;
}
const url = "https://api.openweathermap.org/data/2.5/onecall?lat=" + location.latitude + "&lon=" + location.longitude + "&exclude=minutely,hourly,alerts&units=imperial&lang=en&appid=" + WEATHER_API_KEY;
const address = await Location.reverseGeocode(location.latitude, location.longitude);
const data = await fetchJson(`weather_${address[0].locality}`, url);
const currentTime = new Date().getTime() / 1000;
const isNight = currentTime >= data.current.sunset || currentTime <= data.current.sunrise
return {
location: `${address[0].postalAddress.city}, ${address[0].postalAddress.state}`,
icon: getWeatherEmoji(data.current.weather[0].id, isNight),
description: data.current.weather[0].main,
temperature: Math.round(data.current.temp),
wind: Math.round(data.current.wind_speed),
high: Math.round(data.daily[0].temp.max),
low: Math.round(data.daily[0].temp.min),
feelsLike: Math.round(data.current.feels_like),
}
}
/**
* Given a weather code from Open Weather Map, determine the best emoji to show.
*
* @param {*} code Weather code from Open Weather Map
* @param {*} isNight Is `true` if it is after sunset and before sunrise
*/
function getWeatherEmoji(code, isNight) {
if (code >= 200 && code < 300 || code == 960 || code == 961) {
return "⛈"
} else if ((code >= 300 && code < 600) || code == 701) {
return "🌧"
} else if (code >= 600 && code < 700) {
return "❄️"
} else if (code == 711) {
return "🔥"
} else if (code == 800) {
return isNight ? "🌕" : "☀️"
} else if (code == 801) {
return isNight ? "☁️" : "🌤"
} else if (code == 802) {
return isNight ? "☁️" : "⛅️"
} else if (code == 803) {
return isNight ? "☁️" : "🌥"
} else if (code == 804) {
return "☁️"
} else if (code == 900 || code == 962 || code == 781) {
return "🌪"
} else if (code >= 700 && code < 800) {
return "🌫"
} else if (code == 903) {
return "🥶"
} else if (code == 904) {
return "🥵"
} else if (code == 905 || code == 957) {
return "💨"
} else if (code == 906 || code == 958 || code == 959) {
return "🧊"
} else {
return "❓"
}
}
//-------------------------------------
// Calendar Helper Functions
//-------------------------------------
/**
* Fetch the next calendar event from the given calendar
*
* @param {*} calendarName The calendar to get events from
*/
async function fetchNextCalendarEvent(calendarName) {
const calendar = await Calendar.forEventsByTitle(calendarName);
const events = await CalendarEvent.today([calendar]);
const tomorrow = await CalendarEvent.tomorrow([calendar]);
console.log(`Got ${events.length} events for ${calendarName}`);
console.log(`Got ${tomorrow.length} events for ${calendarName} tomorrow`);
const upcomingEvents = events.concat(tomorrow).filter(e => (new Date(e.endDate)).getTime() >= (new Date()).getTime());
return upcomingEvents ? upcomingEvents[0] : null;
}
/**
* Given a calendar event, return the display text with title and time.
*
* @param {*} calendarEvent The calendar event
* @param {*} isWorkEvent Is this a work event?
*/
function getCalendarEventTitle(calendarEvent, isWorkEvent) {
if (!calendarEvent) {
return `No upcoming ${isWorkEvent ? 'work ' : ''}events`;
}
const timeFormatter = new DateFormatter();
timeFormatter.locale = 'en';
timeFormatter.useNoDateStyle();
timeFormatter.useShortTimeStyle();
const eventTime = new Date(calendarEvent.startDate);
return `[${timeFormatter.string(eventTime)}] ${calendarEvent.title}`;
}
/**
* Fetch data from the Period calendar and determine number of days until period start/end.
*/
async function fetchPeriodData() {
const periodCalendar = await Calendar.forEventsByTitle(PERIOD_CALENDAR_NAME);
const events = await CalendarEvent.between(new Date(), new Date().addDays(30), [periodCalendar]);
console.log(`Got ${events.length} period events`);
const periodEvent = events.filter(e => e.title === PERIOD_EVENT_NAME)[0];
if (periodEvent) {
const current = new Date().getTime();
if (new Date(periodEvent.startDate).getTime() <= current && new Date(periodEvent.endDate).getTime() >= current) {
const timeUntilPeriodEndMs = new Date(periodEvent.endDate).getTime() - current;
return `${Math.round(timeUntilPeriodEndMs / 86400000)} days until period ends`; ;
} else {
const timeUntilPeriodStartMs = new Date(periodEvent.startDate).getTime() - current;
return `${Math.round(timeUntilPeriodStartMs / 86400000)} days until period starts`;
}
} else {
return 'Unknown period data';
}
}
//-------------------------------------
// Misc. Helper Functions
//-------------------------------------
/**
* Make a REST request and return the response
*
* @param {*} key Cache key
* @param {*} url URL to make the request to
* @param {*} headers Headers for the request
*/
async function fetchJson(key, url, headers) {
const cached = await cache.read(key, 5);
if (cached) {
return cached;
}
try {
console.log(`Fetching url: ${url}`);
const req = new Request(url);
req.headers = headers;
const resp = await req.loadJSON();
cache.write(key, resp);
return resp;
} catch (error) {
try {
return cache.read(key, 5);
} catch (error) {
console.log(`Couldn't fetch ${url}`);
}
}
}
/**
* Get the last updated timestamp from the Cache.
*/
async function getLastUpdated() {
let cachedLastUpdated = await cache.read(CACHE_KEY_LAST_UPDATED);
if (!cachedLastUpdated) {
cachedLastUpdated = new Date().getTime();
cache.write(CACHE_KEY_LAST_UPDATED, cachedLastUpdated);
}
return cachedLastUpdated;
}