|
// Variables used by Scriptable. |
|
// These must be at the very top of the file. Do not edit. |
|
// icon-color: blue; icon-glyph: magic; |
|
// Concentric circle V4 |
|
|
|
let today = new Date(); |
|
let dayNumber = Math.ceil((today - new Date(today.getFullYear(),0,1)) / 86400000); |
|
let thisDayDate = today.getDate() |
|
let thisMonth = today.getMonth() |
|
let thisYear = today.getFullYear() |
|
let daysYear = (leapYear(today.getFullYear())) ? 366 : 365; |
|
let daysThisMonth = daysInMonth(thisMonth+1, thisYear) |
|
const dateFormatter = new DateFormatter() |
|
dateFormatter.dateFormat = "MMM" |
|
|
|
const canvSize = 282; |
|
const canvTextSize = 14; |
|
const canvas = new DrawContext(); |
|
canvas.opaque = false |
|
const batteryRemainColor = new Color('A8C686'); //Battery remaining color (green) |
|
const batteryDepletedColor = new Color('E4572E'); //Battery depleted color (red) |
|
const widgetBGColor = new Color('000'); //Widget background color |
|
const circleTextColor = new Color('#fff'); //Widget text color |
|
|
|
const bgCircleColor = new Color('#ccc') // bg circle color, full circle |
|
const monthCircleColor = new Color('669BBC') |
|
const dayCircleColor = new Color('F3A712') |
|
const dayNCircleColor = new Color('bba420') |
|
|
|
const canvWidth = 24; // circle thickness |
|
const canvRadius = 120; // circle radius |
|
|
|
canvas.size = new Size(canvSize, canvSize); |
|
canvas.respectScreenScale = true; |
|
|
|
const batteryLevel = Device.batteryLevel(); |
|
let monthDegree = Math.floor(((thisMonth+1)/12) * 100 * 3.6) |
|
let dayDegree = Math.floor((thisDayDate/daysThisMonth) * 100 * 3.6) |
|
let dayNDegree = Math.floor((dayNumber/daysYear) * 100 * 3.6) |
|
|
|
/* |
|
BEGIN Widget Layout |
|
*/ |
|
|
|
let widget = new ListWidget(); |
|
widget.setPadding(0,5,1,0); |
|
|
|
let batteryDegree = Math.floor(batteryLevel * 100 * 3.6) |
|
makeCircle(0, batteryDepletedColor, batteryRemainColor, batteryDegree, circleTextColor) |
|
|
|
drawMyText( |
|
(Math.floor(batteryLevel * 100)).toString(), |
|
circleTextColor, |
|
258 |
|
) |
|
let monthRadiusOffset = 27 |
|
makeCircle(monthRadiusOffset, bgCircleColor, monthCircleColor, monthDegree, circleTextColor) |
|
|
|
drawMyText( |
|
// dateFormatter.string(today), // Like Jan, Feb, from Scriptable dateFormatter |
|
(thisMonth+1).toString(), |
|
circleTextColor, |
|
232 |
|
) |
|
|
|
let dayRadiusOffset = 54 |
|
makeCircle(dayRadiusOffset, bgCircleColor, dayCircleColor, dayDegree, circleTextColor) |
|
|
|
drawMyText( |
|
thisDayDate.toString(), |
|
circleTextColor, |
|
205 |
|
) |
|
|
|
/* |
|
END Widget Layout |
|
*/ |
|
|
|
function makeCircle (radiusOffset, bgCircleColor, fgCircleColor, degree, txtColor) { |
|
let ctr = new Point(canvSize / 2, canvSize / 2) |
|
// Outer circle, usually full and in the background |
|
CoordOffset = 0 |
|
RadiusOffset = 0 |
|
bgx = ctr.x - (canvRadius - radiusOffset); |
|
bgy = ctr.y - (canvRadius - radiusOffset); |
|
bgd = 2 * (canvRadius - radiusOffset); |
|
bgr = new Rect( |
|
bgx + CoordOffset, |
|
bgy + CoordOffset, |
|
bgd, |
|
bgd |
|
); |
|
|
|
canvas.setStrokeColor(bgCircleColor); |
|
canvas.setLineWidth(canvWidth); |
|
canvas.strokeEllipse(bgr); |
|
|
|
// Inner circle, usually filling and in the foreground |
|
canvas.setFillColor(fgCircleColor); |
|
for (t = 0; t < degree; t++) { |
|
rect_x = ctr.x + (canvRadius - radiusOffset) * sinDeg(t) - canvWidth / 2; |
|
rect_y = ctr.y - (canvRadius - radiusOffset) * cosDeg(t) - canvWidth / 2; |
|
rect_r = new Rect( |
|
rect_x, |
|
rect_y, |
|
canvWidth, |
|
canvWidth |
|
); |
|
canvas.fillEllipse(rect_r); |
|
} |
|
} |
|
|
|
function drawMyText(txt, txtColor, txtOffset) { |
|
const txtRect = new Rect( |
|
canvTextSize / 2 - 10, |
|
txtOffset - (canvTextSize / 2), |
|
canvSize, |
|
canvTextSize |
|
); |
|
canvas.setTextColor(txtColor); |
|
canvas.setFont(Font.boldSystemFont(canvTextSize)); |
|
canvas.setTextAlignedCenter() |
|
canvas.drawTextInRect(txt, txtRect) |
|
} |
|
|
|
function sinDeg(deg) { |
|
return Math.sin((deg * Math.PI) / 180); |
|
} |
|
|
|
function cosDeg(deg) { |
|
return Math.cos((deg * Math.PI) / 180); |
|
} |
|
|
|
function leapYear(year) { |
|
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0); |
|
} |
|
|
|
// Month here is 1-indexed (January is 1, February is 2, etc). This is |
|
// because we're using 0 as the day so that it returns the last day |
|
// of the last month, so you have to add 1 to the month number |
|
// so it returns the correct amount of days |
|
function daysInMonth (month, year) { |
|
return new Date(year, month, 0).getDate(); |
|
} |
|
|
|
const roundedGraph = true |
|
// roundedTemp : true|false > true (Displays the temps rounding the values (29.8 = 30 | 29.3 = 29). |
|
|
|
// Widget Params |
|
// Don't edit this, those are default values for debugging (location for Cupertino). |
|
// You need to give your locations parameters through the widget params, more info below. |
|
const widgetParams = JSON.parse((args.widgetParameter != null) ? args.widgetParameter : '{ "LAT" : "YOUR_LAT" , "LON" : "YOUR_LONG" , "LOC_NAME" : "YOUR_CITY" }') |
|
|
|
// WEATHER API PARAMETERS !important |
|
// API KEY, you need an Open Weather API Key |
|
// You can get one for free at: https://home.openweathermap.org/api_keys (account needed). |
|
const API_KEY = "YOUR_API_KEY" |
|
|
|
// Hardcoded Location, type in your latitude/longitude values and location name |
|
var LAT = widgetParams.LAT // 12.34 |
|
var LON = widgetParams.LON // 12.34 |
|
var LOCATION_NAME = widgetParams.LOC_NAME // "Your place" |
|
|
|
// Set up cache. File located in the Scriptable iCloud folder |
|
let fm = FileManager.iCloud(); |
|
|
|
// cache folder name changed to avoid conflict with the original author's script |
|
let cachePath = fm.joinPath(fm.documentsDirectory(), "weatherCacheCC"); |
|
if(!fm.fileExists(cachePath)){ |
|
fm.createDirectory(cachePath) |
|
} |
|
|
|
let weatherData; |
|
let usingCachedData = false; |
|
let units = 'metric' |
|
let locale = 'en' |
|
|
|
try { |
|
weatherData = await new Request("https://api.openweathermap.org/data/2.5/onecall?lat=" + LAT + "&lon=" + LON + "&exclude=minutely,alerts&units=" + units + "&lang=" + locale + "&appid=" + API_KEY).loadJSON(); |
|
fm.writeString(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON), JSON.stringify(weatherData)); |
|
}catch(e){ |
|
console.log("Offline mode") |
|
try{ |
|
await fm.downloadFileFromiCloud(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON)); |
|
let raw = fm.readString(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON)); |
|
weatherData = JSON.parse(raw); |
|
usingCachedData = true; |
|
}catch(e2){ |
|
console.log("Error: No offline data cached") |
|
} |
|
} |
|
|
|
// 'Night' boolean for line graph and SFSymbols |
|
var night = (today.getHours() > 17 || today.getHours() < 7) |
|
|
|
let temp = weatherData.current.temp |
|
const condition = weatherData.current.weather[0].id |
|
|
|
// SFSymbol function |
|
function symbolForCondition(cond){ |
|
let symbols = { |
|
// Thunderstorm |
|
"2": function(){ |
|
return "cloud.bolt.rain.fill" |
|
}, |
|
// Drizzle |
|
"3": function(){ |
|
return "cloud.drizzle.fill" |
|
}, |
|
// Rain |
|
"5": function(){ |
|
return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill" |
|
}, |
|
// Snow |
|
"6": function(){ |
|
return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow" |
|
}, |
|
// Atmosphere |
|
"7": function(){ |
|
if (cond == 781) { return "tornado" } |
|
if (cond == 701 || cond == 741) { return "cloud.fog.fill" } |
|
return night ? "cloud.fog.fill" : "sun.haze.fill" |
|
}, |
|
// Clear and clouds |
|
"8": function(){ |
|
if (cond == 800) { return night ? "moon.stars.fill" : "sun.max.fill" } |
|
if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" } |
|
return "cloud.fill" |
|
} |
|
} |
|
// Get first condition digit. |
|
let conditionDigit = Math.floor(cond / 100) |
|
// Style and return the symbol. |
|
let sfs = SFSymbol.named(symbols[conditionDigit]()) |
|
sfs.applyFont(Font.systemFont(25)) |
|
return sfs.image |
|
} |
|
|
|
function shouldRound(should, value){ |
|
return ((should) ? Math.round(value) : value) |
|
} |
|
|
|
// additional script required to make transparent background image |
|
const nobg = importModule('no-background.js') |
|
widget.backgroundImage = await nobg.getSlice('small-top-left') |
|
|
|
// Draw weather icon |
|
let weatherIconXOffset = 27 |
|
let weatherIconYOffset = 20 |
|
let weatherIconCoord = new Point(canvSize/3+weatherIconXOffset, canvSize/3+weatherIconYOffset) |
|
canvas.drawImageAtPoint(symbolForCondition(condition), weatherIconCoord) |
|
|
|
// Draw temperature |
|
let temp2 = shouldRound(roundedGraph, temp) |
|
drawMyText(temp2+'°', Color.white(), 165) |
|
|
|
// Presents the Final Image from Canvas |
|
widget.addImage(canvas.getImage()) |
|
Script.setWidget(widget); |
|
widget.presentSmall(); |
|
Script.complete(); |
Hi,
To hide the date info (month and date) you can remove or comment out lines 56 ~ 73 (put /* before 56 and */ after line 73)
Open weather API is safe to apply. It is to prevent people from abusing the company's weather forecast system (they limit you to certain number of API calls per month in a free version, and raise that limit in the paid version)
Hope that helps