Compare commits
2 commits
d89296ea3a
...
3f84638c7c
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f84638c7c | |||
| 3539c9d576 |
12 changed files with 2666 additions and 0 deletions
103
Random Scriptable API.js
Normal file
103
Random Scriptable API.js
Normal 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
123
Read MacStories.js
Normal 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
26
Today's xkcd Comic.js
Normal 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()
|
||||
807
Transparent & Blurred Widgets 1.js
Normal file
807
Transparent & Blurred Widgets 1.js
Normal 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
|
||||
}
|
||||
807
Transparent & Blurred Widgets.js
Normal file
807
Transparent & Blurred Widgets.js
Normal 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
166
Untitled Script 1.js
Normal 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
150
Untitled Script 2.js
Normal 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
3
Untitled Script 3.js
Normal 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
18
Untitled Script 4.js
Normal 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
3
Untitled Script.js
Normal 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
95
Welcome to Scriptable.js
Normal 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
365
iTermWidget.js
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue