commit 3539c9d576418a28c5be0a82d522420f0dab7f0d Author: Aaron Carson Date: Wed Sep 18 14:27:22 2024 +0100 initial commit diff --git a/Random Scriptable API.js b/Random Scriptable API.js new file mode 100644 index 0000000..acbc381 --- /dev/null +++ b/Random Scriptable API.js @@ -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() +} \ No newline at end of file diff --git a/Read MacStories.js b/Read MacStories.js new file mode 100644 index 0000000..254f721 --- /dev/null +++ b/Read MacStories.js @@ -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 = //
+  let html = item[= 2) { + return matches[1] + } else { + return null + } +} + +function decode(str) { + let regex = /&#(\d+);/g + return str.replace(regex, (match, dec) => { + return String.fromCharCode(dec) + }) +} \ No newline at end of file diff --git a/Today's xkcd Comic.js b/Today's xkcd Comic.js new file mode 100644 index 0000000..2f1460e --- /dev/null +++ b/Today's xkcd Comic.js @@ -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() \ No newline at end of file diff --git a/Transparent & Blurred Widgets 1.js b/Transparent & Blurred Widgets 1.js new file mode 100644 index 0000000..432ada2 --- /dev/null +++ b/Transparent & Blurred Widgets 1.js @@ -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 = ` + + + ` + + // 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 +} \ No newline at end of file diff --git a/Transparent & Blurred Widgets.js b/Transparent & Blurred Widgets.js new file mode 100644 index 0000000..e7231df --- /dev/null +++ b/Transparent & Blurred Widgets.js @@ -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 = ` + + + ` + + // 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 +} diff --git a/Untitled Script 1.js b/Untitled Script 1.js new file mode 100644 index 0000000..9c8e091 --- /dev/null +++ b/Untitled Script 1.js @@ -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()); \ No newline at end of file diff --git a/Untitled Script 2.js b/Untitled Script 2.js new file mode 100644 index 0000000..8792671 --- /dev/null +++ b/Untitled Script 2.js @@ -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}`); +}; \ No newline at end of file diff --git a/Untitled Script 3.js b/Untitled Script 3.js new file mode 100644 index 0000000..6a28f50 --- /dev/null +++ b/Untitled Script 3.js @@ -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; diff --git a/Untitled Script 4.js b/Untitled Script 4.js new file mode 100644 index 0000000..d4be0e4 --- /dev/null +++ b/Untitled Script 4.js @@ -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) \ No newline at end of file diff --git a/Untitled Script.js b/Untitled Script.js new file mode 100644 index 0000000..ef978ee --- /dev/null +++ b/Untitled Script.js @@ -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; diff --git a/Welcome to Scriptable.js b/Welcome to Scriptable.js new file mode 100644 index 0000000..aeb89c1 --- /dev/null +++ b/Welcome to Scriptable.js @@ -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 = //
+  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)
+  })
+}
\ No newline at end of file
diff --git a/iTermWidget.js b/iTermWidget.js
new file mode 100644
index 0000000..41ba3aa
--- /dev/null
+++ b/iTermWidget.js
@@ -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 = = 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; +}