Skip to content

Instantly share code, notes, and snippets.

@malesfth
Last active December 13, 2023 01:56
Show Gist options
  • Save malesfth/d5fb2eb36aab4e726727c14ba32f9f8b to your computer and use it in GitHub Desktop.
Save malesfth/d5fb2eb36aab4e726727c14ba32f9f8b to your computer and use it in GitHub Desktop.
iOS Scriptable Widget for Pi-hole
// Parameters:
// {"url":"https://pihole","token": "x"}
// Optional key in parameters: "theme": system|light|dark
let piholeURL = "" // set the URL here for debug
let apiToken = "" // set the api token here for debug
let wTheme = "" // set the theme for debug
if (config.runsInWidget) {
const widgetParams = (args.widgetParameter != null ? JSON.parse(args.widgetParameter) : null)
if (widgetParams==null) {
throw new Error("Please long press the widget and add the parameters.")
} else if (!widgetParams.hasOwnProperty("url") || !widgetParams.hasOwnProperty("token")) {
throw new Error("Wrong parameters.")
}
piholeURL = widgetParams.url
apiToken = widgetParams.token
if (widgetParams.hasOwnProperty("theme")) {
wTheme = widgetParams.theme
}
}
let wBackground = new LinearGradient()
let wColor = new Color("#ffffff")
setTheme()
let piholeStats = await getStats()
let wSize = config.widgetFamily || "large" //set size of widget for debug
let widget = await createWidget() || null
if (!config.runsInWidget) {
if (wSize=="large") { await widget.presentLarge() }
else if (wSize=="medium") { await widget.presentMedium() }
else { await widget.presentSmall() }
}
Script.setWidget(widget)
Script.complete()
async function createWidget() {
let w = new ListWidget()
w.backgroundGradient = wBackground
w.addSpacer()
w.setPadding(5, 15, 0, (wSize=="small" ? 0 : 10))
let state = (piholeStats!=null ? (piholeStats.status=="enabled" ? true : false) : null)
let icn = null
if (state==true) {
icn = SFSymbol.named((state ? "checkmark.shield.fill" : "xmark.shield.fill"))
} else {
icn = SFSymbol.named("xmark.circle.fill")
state = false
}
let topStack = w.addStack()
topStack.layoutHorizontally()
topStack.setPadding(5, 0, 0, 10)
let content = topStack.addImage(icn.image)
content.tintColor = (state ? Color.green() : Color.red())
content.imageSize = new Size(16,16)
topStack.addSpacer(5)
content = topStack.addText("Pi-hole")
content.font = Font.blackSystemFont(16)
content.textColor = wColor
if (state==true && wSize != "small") {
topStack.addSpacer()
topStack.addText(" ") // same line with distance to title
addUpdateItem(topStack, (piholeStats.core_current==piholeStats.core_latest ? true : false), "Pi" + ((wSize=="small") ? "" : "-hole"))
topStack.addText(" ")
addUpdateItem(topStack, (piholeStats.web_current==piholeStats.web_latest ? true : false), "WebUI")
topStack.addText(" ")
addUpdateItem(topStack, (piholeStats.FTL_current==piholeStats.FTL_latest ? true : false), "FTL")
}
w.addSpacer(8)
if (piholeStats==null) {
content = w.addText("No connection")
content.font = Font.thinSystemFont(14)
content.textColor = wColor
w.addSpacer()
return w
}
w.url = piholeURL + "/admin/"
addItem(w, "Total Queries", piholeStats.dns_queries_all_types)
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
layoutStack.centerAlignContent()
addItem(w, "Queries Blocked", piholeStats.ads_blocked_today)
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
layoutStack.centerAlignContent()
addItem(w, "Percent Blocked", Number.parseFloat(piholeStats.ads_percentage_today).toFixed(1).replace(".", ",") + "%")
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
layoutStack.centerAlignContent()
addItem(w, "Domains on Blocklist", piholeStats.domains_being_blocked)
if (wSize=="large") {
addItem(w, "Unique Domains", piholeStats.unique_domains)
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
addItem(w, "Cached Queries", piholeStats.queries_cached)
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
addItem(w, "Queries Forwarded", piholeStats.queries_forwarded)
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
addItem(w, "Clients Seen / Unique", piholeStats.clients_ever_seen + " / " + piholeStats.unique_clients)
}
w.addSpacer()
return w
}
function addItem(w, strHeadline, strData) {
let fontSizeHeadline = 12
let fontSizeString = 9
switch (wSize) {
case "large":
fontSizeHeadline = 18
fontSizeString = 14
break;
case "medium":
fontSizeHeadline = 14
fontSizeString = 11
break;
}
let layoutStack = w.addStack()
layoutStack.setPadding(3, 0, 0, 10)
layoutStack.centerAlignContent()
content = layoutStack.addText(strHeadline)
content.font = Font.mediumSystemFont(fontSizeHeadline)
content.textColor = wColor
layoutStack.addSpacer()
content = layoutStack.addText(strData.toString())
content.font = Font.mediumSystemFont(fontSizeString)
content.textColor = wColor
}
function addUpdateItem(stack, status, text) {
let icn = SFSymbol.named((status ? "checkmark.circle.fill" : "exclamationmark.triangle.fill"))
let content = stack.addImage(icn.image)
content.tintColor = (status ? Color.green() : Color.red())
content.imageSize = new Size(14,14)
content = stack.addText(((wSize!="small") ? " " : "" ) + text)
content.font = Font.semiboldMonospacedSystemFont(12)
content.textColor = wColor
}
function setTheme() {
if (wTheme=="system") {
if (Device.isUsingDarkAppearance()) {
wTheme = "dark"
} else {
wTheme = "light"
}
}
wBackground.locations = [0, 1]
if (wTheme=="dark") {
wBackground.colors = [
new Color("#384d54"),
new Color("#384d54")
]
wColor = new Color("#ffffff")
} else {
wBackground.colors = [
new Color("#ffffffe6"), //ffffffe6
new Color("#ffffffe6")
]
wColor = new Color("#000000")
}
}
async function getStats() {
try {
let req = new Request(piholeURL + "/admin/api.php?versions&summary&auth="+apiToken)
let json = await req.loadJSON()
if (json.length === 0) { return null }
return json
} catch(err) {
console.error(err)
return null
}
}
@marked80
Copy link

marked80 commented Feb 3, 2023

Just replaced the comma’s and dots again (in Europe that is the used decimal setting)
@marked80
How did you replaced them?

With a .replace function.
Below is the modified script:

// Parameters:
// {"url":"https://pihole","token": "x"}
// Optional key in parameters: "theme": system|light|dark

let piholeURL = "http://192.xxx.x.x" // set the URL here for debug
let apiToken = "xxxxxxx" // set the api token here for debug
let wTheme = "" // set the theme for debug

if (config.runsInWidget) {
const widgetParams = (args.widgetParameter != null ? JSON.parse(args.widgetParameter) : null)
if (widgetParams==null) {
throw new Error("Please long press the widget and add the parameters.")
} else if (!widgetParams.hasOwnProperty("url")) {
throw new Error("Wrong parameters.")
}

piholeURL = widgetParams.url
apiToken = widgetParams.token
if (widgetParams.hasOwnProperty("theme")) {
	wTheme = widgetParams.theme
}

}

let wBackground = new LinearGradient()
let wColor = new Color("#ffffff")
setTheme()

let piholeStats = await getStats()
let adminPage = await getAdminPage()
let isLatestVersion = await getUpdateStats(adminPage)

let wSize = config.widgetFamily || "large" //set size of widget for debug
let widget = await createWidget() || null

if (!config.runsInWidget) {
if (wSize=="large") { await widget.presentLarge() }
else if (wSize=="medium") { await widget.presentMedium() }
else { await widget.presentSmall() }
}
Script.setWidget(widget)
Script.complete()

async function createWidget() {
let w = new ListWidget()
w.backgroundGradient = wBackground
w.addSpacer()
w.setPadding(5, 15, 0, (wSize=="small" ? 0 : 10))

let state = (piholeStats!=null ? (piholeStats.status=="enabled" ? true : false) : null)
let icn = null

if (state==true) {
	icn = SFSymbol.named((state ? "checkmark.shield.fill" : "xmark.shield.fill"))
} else {
	icn = SFSymbol.named("xmark.circle.fill")
	state = false
}

let topStack = w.addStack()
topStack.layoutHorizontally()
topStack.setPadding(5, 0, 0, 10)

let content = topStack.addImage(icn.image)
content.tintColor = (state ? Color.green() : Color.red())
content.imageSize = new Size(16,16)
topStack.addSpacer(5)

content = topStack.addText("Pi-hole")
content.font = Font.blackSystemFont(16)
content.textColor = wColor

if (state==true && wSize != "small") {
	topStack.addSpacer()
  	topStack.addText("    ") // same line with distance to title

addUpdateItem(topStack, isLatestVersion[0], "Pi" + ((wSize=="small") ? "" : "-hole"))
topStack.addText("  ")
addUpdateItem(topStack, isLatestVersion[2], "WebUI")
topStack.addText("  ")
addUpdateItem(topStack, isLatestVersion[1], "FTL")
	
}

w.addSpacer(8)

if (piholeStats==null) {
	content = w.addText("No connection")
    content.font = Font.thinSystemFont(14)
    content.textColor = wColor
	w.addSpacer()
	return w
}

w.url = piholeURL + "/admin/"

addItem(w, "Total Queries", piholeStats.dns_queries_all_types.replace(",", "."))
	
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
layoutStack.centerAlignContent()
	
addItem(w, "Queries Blocked", piholeStats.ads_blocked_today.replace(",", "."))
	
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
layoutStack.centerAlignContent()
	
addItem(w, "Percent Blocked", Number.parseFloat(piholeStats.ads_percentage_today).toFixed(1).replace(".", ",") + "%")
	
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
layoutStack.centerAlignContent()
	
addItem(w, "Domains on Blocklist", piholeStats.domains_being_blocked.replace(",", "."))

if (wSize=="large") {
	addItem(w, "Unique Domains", piholeStats.unique_domains)
	
	layoutStack = w.addStack()
	layoutStack.setPadding(5, 0, 0, 10)
	
	addItem(w, "Cached Queries", piholeStats.queries_cached.replace(",", "."))
	
	layoutStack = w.addStack()
	layoutStack.setPadding(5, 0, 0, 10)
	
	addItem(w, "Queries Forwarded", piholeStats.queries_forwarded.replace(",", "."))
	
	layoutStack = w.addStack()
	layoutStack.setPadding(5, 0, 0, 10)
	
	addItem(w, "Clients Seen / Unique", piholeStats.clients_ever_seen + " / " + piholeStats.unique_clients)
}

w.addSpacer()
return w

}

function addItem(w, strHeadline, strData) {
let fontSizeHeadline = 12
let fontSizeString = 9
switch (wSize) {
case "large":
fontSizeHeadline = 18
fontSizeString = 14
break;
case "medium":
fontSizeHeadline = 14
fontSizeString = 11
break;
}

let layoutStack = w.addStack()
layoutStack.setPadding(3, 0, 0, 10)
layoutStack.centerAlignContent()
	
content = layoutStack.addText(strHeadline)
content.font = Font.mediumSystemFont(fontSizeHeadline)
content.textColor = wColor
layoutStack.addSpacer()

content = layoutStack.addText(strData.toString())
content.font = Font.mediumSystemFont(fontSizeString)
content.textColor = wColor

}

function addUpdateItem(stack, status, text) {
let icn = SFSymbol.named((status ? "checkmark.circle.fill" : "exclamationmark.triangle.fill"))
let content = stack.addImage(icn.image)
content.tintColor = (status ? Color.green() : Color.red())
content.imageSize = new Size(14,14)
content = stack.addText(((wSize!="small") ? " " : "" ) + text)
content.font = Font.semiboldMonospacedSystemFont(12)
content.textColor = wColor
}

function setTheme() {
if (wTheme=="system") {
if (Device.isUsingDarkAppearance()) {
wTheme = "dark"
} else {
wTheme = "light"
}
}
wBackground.locations = [0, 1]
if (wTheme=="dark") {
wBackground.colors = [
new Color("#384d54"),
new Color("#384d54")
]
wColor = new Color("#ffffff")
} else {
wBackground.colors = [
new Color("#ffffffe6"), //ffffffe6
new Color("#ffffffe6")
]
wColor = new Color("#000000")
}
}

async function getUpdateStats (webPage) {
let latestVersions = [true, true, true]
try {
let view = new WebView()
await view.loadHTML(webPage);
let data = await view.evaluateJavaScript(`
let retoure = [true, true, true]
let a = Array.from(document.querySelectorAll('div.version-info div ul li'))

		for (let i=0; i<a.length; i++) {
			let el = a[i]

			if (el.getElementsByTagName('a').length == 2) {
				retoure[i] = false	
			}	
		}	
		completion(retoure)`, true)
	latestVersions = data
} catch(e) {
	console.log("Cannot get update stats: " + e)
}
return latestVersions

}

async function getStats() {
try {
let req = new Request(piholeURL + "/admin/api.php?summary&auth="+apiToken)
let json = await req.loadJSON()
if (json.length === 0) { json == null }
return json
} catch(err) {
console.error(err)
return null
}
}

async function getAdminPage() {
try {
let req = new Request(piholeURL + "/admin/")
let adminHTMLPage = await req.loadString()
return adminHTMLPage
} catch(err) {
console.log(err)
return null
}
}

@palasinio
Copy link

THX

@Token2K
Copy link

Token2K commented Feb 21, 2023

Thx @malesfth ... for this Update!
Thx @marked80 ... a very old "Bug/Error"! (for European's) ;-)

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