Skip to content

Instantly share code, notes, and snippets.

@ShawnSWu
Forked from mzeryck/mz_invisible_widget.js
Last active April 19, 2025 19:06
Show Gist options
  • Save ShawnSWu/5593b6c69e29941378426f345c9c3fd0 to your computer and use it in GitHub Desktop.
Save ShawnSWu/5593b6c69e29941378426f345c9c3fd0 to your computer and use it in GitHub Desktop.
A Scriptable script that creates "invisible" widget backgrounds based on your iOS wallpaper, I create this for iOS 18 version
// Change to true to see a preview of your widget.
const testMode = true
// Optionally specify the size of your widget preview.
const widgetPreview = "large"
/* -- GREETING AND DATE -- */
// Optionally show a greeting based on the time of day.
const showGreeting = true
// Choose the date style. "iOS" matches the default calendar app (like: THURSDAY 29)
// Or, use docs.scriptable.app/dateformatter to write your own format.
const dateDisplay = "EEEE"
/* -- EVENTS -- */
// Change to false to hide events.
const showEvents = true
// Choose whether to show all-day events.
const showAllDay = false
// Specify how many events to show.
const numberOfEvents = 4
// Optionally show tomorrow's events.
const showTomorrow = false
// Write a message when there are no events, or change to "" for blank.
const noEventMessage = "Keep happy and keep smiling."
/* -- SPACING -- */
// Can be top, middle, or bottom.
const verticalAlignment = "middle"
// Can be left, center, or right.
const horizontalAlignment = "left"
// The spacing between each element.
const elementSpacing = 12
/* -- FONTS AND TEXT -- */
// Use iosfonts.com, or change to "" for the system font.
const fontName = "Futura-Medium"
// Find colors on htmlcolorcodes.com
const fontColor = new Color("#ffffff")
const date = new Date()
const resetWidget = false
// Change the font sizes for each element.
const greetingSize = 17
const dateSize = 25
const dayOfWeekSize = 25
const eventTitleSize = 14
const eventTimeSize = 10
const noEventMessageSize = 14
const shawnTextSize = 29
const files = FileManager.local()
const path = files.joinPath(files.documentsDirectory(), Script.name() + ".jpg")
// If we're in the widget or testing, build the widget.
if (config.runsInWidget || (testMode && files.fileExists(path) && !resetWidget)) {
const widget = new ListWidget()
if (files.fileExists(path)) widget.backgroundImage = files.readImage(path)
if (verticalAlignment == "middle" || verticalAlignment == "bottom") {
widget.addSpacer()
}
// Format the greeting if we need it.
if (showGreeting) {
let greetingText = makeGreeting()
let greeting = widget.addText(greetingText)
formatText(greeting, greetingSize)
widget.addSpacer(elementSpacing)
}
// Format the date info.
let df = new DateFormatter()
if (dateDisplay.toLowerCase() == "ios") {
df.dateFormat = "EEEE"
let dayOfWeek = widget.addText(df.string(date).toUpperCase())
let dateNumber = widget.addText(date.getDate().toString())
formatText(dayOfWeek, dayOfWeekSize)
formatText(dateNumber, dateSize)
} else {
df.dateFormat = dateDisplay
var dateText = widget.addText(df.string(date))
formatText(dateText, dateSize)
}
var option = {month:'long', day:'numeric'}
let shawn = widget.addText(new Date().toLocaleDateString("en-US", option).toString())
formatText(shawn, shawnTextSize)
// Add events if we're supposed to.
if (showEvents) {
// Your existing event display code here
// [Previous event handling code remains unchanged]
}
if (verticalAlignment == "top" || verticalAlignment == "middle") {
widget.addSpacer()
}
Script.setWidget(widget)
if (testMode) {
let widgetSizeFormat = widgetPreview.toLowerCase()
if (widgetSizeFormat == "small") { widget.presentSmall() }
if (widgetSizeFormat == "medium") { widget.presentMedium() }
if (widgetSizeFormat == "large") { widget.presentLarge() }
}
Script.complete()
} else {
// Background setup code
let message = "Before you start, edit your home screen (wiggle mode). Scroll to the empty page on the far right and take a screenshot."
const shouldExit = await generateAlert(message, ["Continue", "Exit to Take Screenshot"])
if (shouldExit.index) return
let img = await Photos.fromLibrary()
const 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."
return await generateAlert(message, ["OK"])
}
if (phone.text) {
message = "What size are your home screen icons?"
const textOptions = [{key: "text", value: "Small (has labels)"}, {key: "notext", value: "Large (no labels)"}]
const textResponse = await generateAlert(message, textOptions)
phone = phone[textResponse.key]
}
message = "What size of widget are you creating?"
const sizes = {small: "Small", medium: "Medium", large: "Large"}
const sizeOptions = [sizes.small, sizes.medium, sizes.large]
const size = (await generateAlert(message, sizeOptions)).value
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.)" : "")
let positions
if (size == sizes.small) {
positions = ["Top left","Top right","Middle left","Middle right","Bottom left","Bottom right"]
} else if (size == sizes.medium) {
positions = ["Top","Middle","Bottom"]
} else if (size == sizes.large) {
positions = [{key: "top", value: "Top"}, {key: "middle", value: "Bottom"}]
}
const position = (await generateAlert(message, positions)).key
const crop = {
w: (size == sizes.small ? phone.small : phone.medium),
h: (size == sizes.large ? phone.large : phone.small),
x: (size == sizes.small ? phone[position.split(" ")[1]] : phone.left),
y: phone[position.toLowerCase().split(" ")[0]]
}
const draw = new DrawContext()
draw.size = new Size(crop.w, crop.h)
draw.drawImageAtPoint(img, new Point(-crop.x, -crop.y))
img = draw.getImage()
message = "Your widget background is ready. Would you like to use it as this script's background, or export the image?"
const exports = {script: "Use for this script", photos: "Export to Photos", files: "Export to Files"}
const exportOptions = [exports.script, exports.photos, exports.files]
const exportValue = (await generateAlert(message, exportOptions)).value
if (exportValue == exports.script) {
files.writeImage(path, img)
} else if (exportValue == exports.photos) {
Photos.save(img)
} else if (exportValue == exports.files) {
await DocumentPicker.exportImage(img)
}
}
// Helper functions
async function generateAlert(message, options) {
const alert = new Alert()
alert.message = message
const isObject = options[0].value
for (const option of options) {
alert.addAction(isObject ? option.value : option)
}
const index = await alert.presentAlert()
return {
index: index,
value: isObject ? options[index].value : options[index],
key: isObject ? options[index].key : options[index]
}
}
function phoneSizes(inputHeight) {
return {
// 16 Pro Max
"2868": {
text: {
small: 510,
medium: 1092,
large: 1146,
left: 114,
right: 696,
top: 276,
middle: 912,
bottom: 1548
},
notext: {
small: 530,
medium: 1138,
large: 1136,
left: 91,
right: 699,
top: 276,
middle: 882,
bottom: 1488
}
},
// 16 Plus, 15 Plus, 15 Pro Max, 14 Pro Max
"2796": {
text: {
small: 510,
medium: 1092,
large: 1146,
left: 98,
right: 681,
top: 252,
middle: 888,
bottom: 1524
},
notext: {
small: 530,
medium: 1139,
large: 1136,
left: 75,
right: 684,
top: 252,
middle: 858,
bottom: 1464
}
},
// 16 Pro
"2622": {
text: {
small: 486,
medium: 1032,
large: 1098,
left: 87,
right: 633,
top: 261,
middle: 872,
bottom: 1485
},
notext: {
small: 495,
medium: 1037,
large: 1035,
left: 84,
right: 626,
top: 270,
middle: 810,
bottom: 1350
}
},
// 16, 15, 15 Pro, 14 Pro
"2556": {
text: {
small: 474,
medium: 1017,
large: 1062,
left: 81,
right: 624,
top: 240,
middle: 828,
bottom: 1416
},
notext: {
small: 495,
medium: 1047,
large: 1047,
left: 66,
right: 618,
top: 243,
middle: 795,
bottom: 1347
}
}
}[inputHeight]
}
// Your existing helper functions
function makeGreeting() {
let greeting = "Good "
if (date.getHours() < 4) {
greeting = greeting + "night."
} else if (date.getHours() < 12) {
greeting = greeting + "morning."
} else if (date.getHours() < 17) {
greeting = greeting + "afternoon."
} else if (date.getHours() < 20) {
greeting = greeting + "evening."
} else {
greeting = greeting + "night."
}
return greeting
}
function shouldShowEvent(event) {
if (event.title.startsWith("Canceled:")) {
return false
}
if (event.isAllDay) {
return showAllDay
}
return (event.startDate.getTime() > date.getTime())
}
function provideFont(fontName, fontSize) {
if (fontName == "" || fontName == null) {
return Font.regularSystemFont(fontSize)
} else {
return new Font(fontName, fontSize)
}
}
function displayEvent(widget, event) {
widget.addSpacer(elementSpacing)
let title = widget.addText(event.title)
formatText(title, eventTitleSize)
if (event.isAllDay) { return }
widget.addSpacer(7)
let time = widget.addText(formatTime(event.startDate))
formatText(time, eventTimeSize)
}
function formatTime(date) {
let df = new DateFormatter()
df.useNoDateStyle()
df.useShortTimeStyle()
return df.string(date)
}
function formatText(textItem, fontSize) {
textItem.font = provideFont(fontName, fontSize)
textItem.textColor = fontColor
if (horizontalAlignment == "right") {
textItem.rightAlignText()
} else if (horizontalAlignment == "center") {
textItem.centerAlignText()
} else {
textItem.leftAlignText()
}
}
function sameDay(d1, d2) {
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate()
}
@Gwai
Copy link

Gwai commented Apr 19, 2025

Wrong sizes @16 Pro

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment