├── Doc_Img ├── IMG_Widget.PNG └── IMG_Widget_20201122.PNG ├── InvisibleWidgetPicture.js ├── WidgetHelloV2.js ├── WigetBlur.js └── readme.md /Doc_Img/IMG_Widget.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizhd/IOS_Scriptable/da85b60c82c604066ea26d21680b83f701cdeb54/Doc_Img/IMG_Widget.PNG -------------------------------------------------------------------------------- /Doc_Img/IMG_Widget_20201122.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizhd/IOS_Scriptable/da85b60c82c604066ea26d21680b83f701cdeb54/Doc_Img/IMG_Widget_20201122.PNG -------------------------------------------------------------------------------- /InvisibleWidgetPicture.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: red; icon-glyph: magic; 4 | // This widget was created by Max Zeryck @mzeryck 5 | 6 | if (config.runsInWidget) { 7 | let widget = new ListWidget() 8 | widget.backgroundImage = files.readImage(path) 9 | 10 | // You can your own code here to add additional items to the "invisible" background of the widget. 11 | 12 | Script.setWidget(widget) 13 | Script.complete() 14 | 15 | /* 16 | * The code below this comment is used to set up the invisible widget. 17 | * =================================================================== 18 | */ 19 | } else { 20 | 21 | // Determine if user has taken the screenshot. 22 | var message 23 | 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." 24 | let exitOptions = ["Continue", "Exit to Take Screenshot"] 25 | let shouldExit = await generateAlert(message, exitOptions) 26 | if (shouldExit) return 27 | 28 | // Get screenshot and determine phone size. 29 | let img = await Photos.fromLibrary() 30 | let height = img.size.height 31 | let phone = phoneSizes()[height] 32 | if (!phone) { 33 | 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." 34 | await generateAlert(message, ["OK"]) 35 | return 36 | } 37 | 38 | // Prompt for widget size and position. 39 | message = "What size of widget are you creating?" 40 | let sizes = ["Small", "Medium", "Large"] 41 | let size = await generateAlert(message, sizes) 42 | let widgetSize = sizes[size] 43 | 44 | message = "What position will it be in?" 45 | message += (height == 1136 ? " (Note that your device only supports two rows of widgets, so the middle and bottom options are the same.)" : "") 46 | 47 | // Determine image crop based on phone size. 48 | let crop = { 49 | w: "", 50 | h: "", 51 | x: "", 52 | y: "" 53 | } 54 | if (widgetSize == "Small") { 55 | crop.w = phone.small 56 | crop.h = phone.small 57 | let positions = ["Top left", "Top right", "Middle left", "Middle right", "Bottom left", "Bottom right"] 58 | let position = await generateAlert(message, positions) 59 | 60 | // Convert the two words into two keys for the phone size dictionary. 61 | let keys = positions[position].toLowerCase().split(' ') 62 | crop.y = phone[keys[0]] 63 | crop.x = phone[keys[1]] 64 | 65 | } else if (widgetSize == "Medium") { 66 | crop.w = phone.medium 67 | crop.h = phone.small 68 | 69 | // Medium and large widgets have a fixed x-value. 70 | crop.x = phone.left 71 | let positions = ["Top", "Middle", "Bottom"] 72 | let position = await generateAlert(message, positions) 73 | let key = positions[position].toLowerCase() 74 | crop.y = phone[key] 75 | 76 | } else if (widgetSize == "Large") { 77 | crop.w = phone.medium 78 | crop.h = phone.large 79 | crop.x = phone.left 80 | let positions = ["Top", "Bottom"] 81 | let position = await generateAlert(message, positions) 82 | 83 | // Large widgets at the bottom have the "middle" y-value. 84 | crop.y = position ? phone.middle : phone.top 85 | } 86 | 87 | // Crop image and finalize the widget. 88 | let imgCrop = cropImage(img, new Rect(crop.x, crop.y, crop.w, crop.h)) 89 | 90 | message = "Your widget background is ready. Would you like to use it in a Scriptable widget or export the image?" 91 | const exportPhotoOptions = ["Export to Files", "Export to Photos"] 92 | const exportPhoto = await generateAlert(message, exportPhotoOptions) 93 | 94 | if (exportPhoto) { 95 | Photos.save(imgCrop) 96 | } else { 97 | await DocumentPicker.exportImage(imgCrop) 98 | } 99 | 100 | Script.complete() 101 | } 102 | 103 | // Generate an alert with the provided array of options. 104 | async function generateAlert(message, options) { 105 | 106 | let alert = new Alert() 107 | alert.message = message 108 | 109 | for (const option of options) { 110 | alert.addAction(option) 111 | } 112 | 113 | let response = await alert.presentAlert() 114 | return response 115 | } 116 | 117 | // Crop an image into the specified rect. 118 | function cropImage(img, rect) { 119 | 120 | let draw = new DrawContext() 121 | draw.size = new Size(rect.width, rect.height) 122 | 123 | draw.drawImageAtPoint(img, new Point(-rect.x, -rect.y)) 124 | return draw.getImage() 125 | } 126 | 127 | // Pixel sizes and positions for widgets on all supported phones. 128 | function phoneSizes() { 129 | let phones = { 130 | "2688": { 131 | "small": 507, 132 | "medium": 1080, 133 | "large": 1137, 134 | "left": 81, 135 | "right": 654, 136 | "top": 228, 137 | "middle": 858, 138 | "bottom": 1488 139 | }, 140 | 141 | "1792": { 142 | "small": 338, 143 | "medium": 720, 144 | "large": 758, 145 | "left": 54, 146 | "right": 436, 147 | "top": 160, 148 | "middle": 580, 149 | "bottom": 1000 150 | }, 151 | 152 | "2436": { 153 | "small": 465, 154 | "medium": 987, 155 | "large": 1035, 156 | "left": 69, 157 | "right": 591, 158 | "top": 213, 159 | "middle": 783, 160 | "bottom": 1353 161 | }, 162 | 163 | "2208": { 164 | "small": 471, 165 | "medium": 1044, 166 | "large": 1071, 167 | "left": 99, 168 | "right": 672, 169 | "top": 114, 170 | "middle": 696, 171 | "bottom": 1278 172 | }, 173 | 174 | "1334": { 175 | "small": 296, 176 | "medium": 642, 177 | "large": 648, 178 | "left": 54, 179 | "right": 400, 180 | "top": 60, 181 | "middle": 412, 182 | "bottom": 764 183 | }, 184 | 185 | "1136": { 186 | "small": 282, 187 | "medium": 584, 188 | "large": 622, 189 | "left": 30, 190 | "right": 332, 191 | "top": 59, 192 | "middle": 399, 193 | "bottom": 399 194 | } 195 | } 196 | return phones 197 | } -------------------------------------------------------------------------------- /WidgetHelloV2.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: cyan; icon-glyph: child; 4 | // To use, add a parameter to the widget with a format of: image.png|padding-top|text-color 5 | // The image should be placed in the iCloud Scriptable folder (case-sensitive). 6 | // The padding-top spacing parameter moves the text down by a set amount. 7 | // The text color parameter should be a hex value. 8 | 9 | // For example, to use the image bkg_fall.PNG with a padding of 40 and a text color of red, 10 | // the parameter should be typed as: bkg_fall.png|40|#ff0000 11 | 12 | // All parameters are required and separated with "|" 13 | 14 | // Parameters allow different settings for multiple widget instances. 15 | 16 | let widgetHello = new ListWidget(); 17 | var today = new Date(); 18 | 19 | var widgetInputRAW = args.widgetParameter; 20 | 21 | try { 22 | widgetInputRAW.toString(); 23 | } catch (e) { 24 | throw new Error("Please long press the widget and add a parameter."); 25 | } 26 | 27 | var widgetInput = widgetInputRAW.toString(); 28 | 29 | var inputArr = widgetInput.split("|"); 30 | 31 | // iCloud file path 32 | var scriptableFilePath = "/var/mobile/Library/Mobile Documents/iCloud~dk~simonbs~Scriptable/Documents/BKG_IMG/"; 33 | var removeSpaces1 = inputArr[0].split(" "); // Remove spaces from file name 34 | var removeSpaces2 = removeSpaces1.join(''); 35 | var tempPath = removeSpaces2.split("."); 36 | var backgroundImageURLRAW = scriptableFilePath + tempPath[0]; 37 | 38 | var fm = FileManager.iCloud(); 39 | var backgroundImageURL = scriptableFilePath + tempPath[0] + "."; 40 | var backgroundImageURLInput = scriptableFilePath + removeSpaces2; 41 | 42 | // For users having trouble with extensions 43 | // Uses user-input file path is the file is found 44 | // Checks for common file format extensions if the file is not found 45 | if (fm.fileExists(backgroundImageURLInput) == false) { 46 | var fileTypes = ['png', 'jpg', 'jpeg', 'tiff', 'webp', 'gif']; 47 | 48 | fileTypes.forEach(function (item) { 49 | if (fm.fileExists((backgroundImageURL + item.toLowerCase())) == true) { 50 | backgroundImageURL = backgroundImageURLRAW + "." + item.toLowerCase(); 51 | } else if (fm.fileExists((backgroundImageURL + item.toUpperCase())) == true) { 52 | backgroundImageURL = backgroundImageURLRAW + "." + item.toUpperCase(); 53 | } 54 | }); 55 | } else { 56 | backgroundImageURL = scriptableFilePath + removeSpaces2; 57 | } 58 | 59 | var spacing = parseInt(inputArr[1]); 60 | 61 | // Long-form days and months 62 | var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 63 | var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 64 | 65 | // Greetings arrays per time period. 66 | var greetingsMorning = [ 67 | 'Good morning.' 68 | ]; 69 | var greetingsAfternoon = [ 70 | 'Good afternoon.' 71 | ]; 72 | var greetingsEvening = [ 73 | 'Good evening.' 74 | ]; 75 | var greetingsNight = [ 76 | 'Bedtime.' 77 | ]; 78 | var greetingsLateNight = [ 79 | 'Go to sleep!' 80 | ]; 81 | 82 | // Holiday customization 83 | var holidaysByKey = { 84 | // month,week,day: datetext 85 | "11,4,4": "Happy Thanksgiving!" 86 | } 87 | 88 | var holidaysByDate = { 89 | // month,date: greeting 90 | "1,1": "Happy " + (today.getFullYear()).toString() + "!", 91 | "10,31": "Happy Halloween!", 92 | "12,25": "Merry Christmas!" 93 | } 94 | 95 | var holidayKey = (today.getMonth() + 1).toString() + "," + (Math.ceil(today.getDate() / 7)).toString() + "," + (today.getDay()).toString(); 96 | 97 | var holidayKeyDate = (today.getMonth() + 1).toString() + "," + (today.getDate()).toString(); 98 | 99 | // Date Calculations 100 | var weekday = days[today.getDay()]; 101 | var month = months[today.getMonth()]; 102 | var date = today.getDate(); 103 | var hour = today.getHours(); 104 | 105 | // Append ordinal suffix to date 106 | function ordinalSuffix(input) { 107 | if (input % 10 == 1 && date != 11) { 108 | return input.toString() + "st"; 109 | } else if (input % 10 == 2 && date != 12) { 110 | return input.toString() + "nd"; 111 | } else if (input % 10 == 3 && date != 13) { 112 | return input.toString() + "rd"; 113 | } else { 114 | return input.toString() + "th"; 115 | } 116 | } 117 | 118 | // Generate date string 119 | var datefull = weekday + ", " + month + " " + ordinalSuffix(date); 120 | 121 | // Support for multiple greetings per time period 122 | function randomGreeting(greetingArray) { 123 | return Math.floor(Math.random() * greetingArray.length); 124 | } 125 | 126 | var greeting = new String("Howdy.") 127 | if (hour < 5 && hour >= 1) { // 1am - 5am 128 | greeting = greetingsLateNight[randomGreeting(greetingsLateNight)]; 129 | } else if (hour >= 23 || hour < 1) { // 11pm - 1am 130 | greeting = greetingsNight[randomGreeting(greetingsNight)]; 131 | } else if (hour < 12) { // Before noon (5am - 12pm) 132 | greeting = greetingsMorning[randomGreeting(greetingsMorning)]; 133 | } else if (hour >= 12 && hour <= 17) { // 12pm - 5pm 134 | greeting = greetingsAfternoon[randomGreeting(greetingsAfternoon)]; 135 | } else if (hour > 17 && hour < 23) { // 5pm - 11pm 136 | greeting = greetingsEvening[randomGreeting(greetingsEvening)]; 137 | } 138 | 139 | // Overwrite greeting if calculated holiday 140 | if (holidaysByKey[holidayKey]) { 141 | greeting = holidaysByKey[holidayKey]; 142 | } 143 | 144 | // Overwrite all greetings if specific holiday 145 | if (holidaysByDate[holidayKeyDate]) { 146 | greeting = holidaysByDate[holidayKeyDate]; 147 | } 148 | 149 | // Try/catch for color input parameter 150 | try { 151 | inputArr[2].toString(); 152 | } catch (e) { 153 | throw new Error("Please long press the widget and add a parameter."); 154 | } 155 | 156 | let themeColor = new Color(inputArr[2].toString()); 157 | 158 | 159 | // Widget Cache Path Settings 160 | const wBasePath = "/var/mobile/Library/Mobile Documents/iCloud~dk~simonbs~Scriptable/Documents/WidgetCache/"; 161 | 162 | // Open weather API_KEY 163 | const API_WEATHER = "1412c61a30b2f29c47bce78c3523722d"; //Load api here 164 | // Suzhou: 101190401 Nanjing: 101190101 165 | const CITY_NAME = "Suzhou"; // add city ID 166 | 167 | // Get Location 168 | 169 | // Location.setAccuracyToBest(); 170 | // let curLocation = await Location.current(); 171 | 172 | let weatherUrl = "https://api.openweathermap.org/data/2.5/weather?q=" + 173 | CITY_NAME + "&appid=" + API_WEATHER + "&units=metric"; 174 | // let weatherUrl = "http://api.openweathermap.org/data/2.5/weather?lat=" +curLocation.latitude + "&lon=" + curLocation.longitude +"&appid=" + API_WEATHER + "&units=metric"; 175 | // let weatherUrl = "http://api.openweathermap.org/data/2.5/weather?id=" + CITY_WEATHER + "&APPID=" + API_WEATHER + "&units=metric"; 176 | 177 | 178 | const dataJSON = await fetchWeatherData(weatherUrl); 179 | const cityName = dataJSON.name; 180 | const weatherArray = dataJSON.weather; 181 | const iconData = weatherArray[0].icon; 182 | const weatherName = weatherArray[0].description; 183 | const curTempObj = dataJSON.main; 184 | const curTemp = curTempObj.temp; 185 | const highTemp = curTempObj.temp_max; 186 | const lowTemp = curTempObj.temp_min; 187 | const feelsLike = curTempObj.feels_like; 188 | 189 | //Completed loading weather data 190 | 191 | 192 | /* ------------------------------------------------------------------------------ */ 193 | /* Assemble Widget */ 194 | /* ------------------------------------------------------------------------------ */ 195 | if (config.runsInWidget) { 196 | // Top spacing 197 | widgetHello.addSpacer(parseInt(spacing)); 198 | // Background image 199 | widgetHello.backgroundImage = Image.fromFile(backgroundImageURL); 200 | 201 | /* ------------------------------------------------------ */ 202 | /* Greeting label */ 203 | let hello = widgetHello.addText(greeting); 204 | hello.font = Font.boldSystemFont(40); 205 | hello.textColor = themeColor; 206 | 207 | /* Date label */ 208 | let datetext = widgetHello.addText(datefull); 209 | datetext.font = Font.boldSystemFont(25); 210 | datetext.textColor = themeColor; 211 | widgetHello.addSpacer(10); 212 | 213 | /* ------------------------------------------------------ */ 214 | /* Add weather */ 215 | let weatherStack = widgetHello.addStack(); 216 | weatherStack.layoutVertically(); 217 | weatherStack.setPadding(10, 20, 0, 0); 218 | weatherStack.centerAlignContent(); 219 | 220 | // Weather Image 221 | var img = Image.fromFile(await fetchImageLocal(iconData + "_ico")); 222 | 223 | let wSubStack = weatherStack.addStack(); 224 | wSubStack.layoutHorizontally(); 225 | wSubStack.setPadding(5, 0, 10, 0); 226 | 227 | // Weather image in stack 228 | let wUpperStack = wSubStack.addStack(); 229 | wUpperStack.layoutHorizontally(); 230 | 231 | let widgetImg = wUpperStack.addImage(img); 232 | widgetImg.imageSize = new Size(40, 40); 233 | widgetImg.leftAlignImage(); 234 | 235 | 236 | // City and temp in stack 237 | let cityTempStack = wUpperStack.addStack(); 238 | cityTempStack.layoutVertically(); 239 | cityTempStack.setPadding(0, 20, 0, 0); 240 | 241 | let cityText = cityTempStack.addText(cityName); 242 | cityText.font = Font.boldSystemFont(20); 243 | cityText.textColor = themeColor; 244 | cityText.textOpacity = 1; 245 | cityText.leftAlignText(); 246 | 247 | let tempText = cityTempStack.addText(Math.round(curTemp).toString() + "\u2103"); 248 | tempText.font = Font.boldSystemFont(20); 249 | tempText.textColor = new Color('#0278AE'); 250 | tempText.textOpacity = 1; 251 | tempText.leftAlignText(); 252 | 253 | // Weather details in stack 254 | let wDetailStack = weatherStack.addStack(); 255 | wDetailStack.layoutVertically(); 256 | wDetailStack.setPadding(0, 5, 0, 0); 257 | 258 | let details1 = weatherName.replace(weatherName[0], weatherName[0].toUpperCase()) + 259 | " today" + "."; 260 | 261 | let wDetailText1 = wDetailStack.addText(details1); 262 | wDetailText1.font = Font.regularSystemFont(15); 263 | wDetailText1.textColor = themeColor; 264 | wDetailText1.textOpacity = 1; 265 | wDetailText1.leftAlignText(); 266 | 267 | let details2 = "It feels like " + Math.round(feelsLike) + "\u2103," + 268 | " the high will be " + Math.round(highTemp) + "\u2103" + "."; 269 | 270 | let wDetailText2 = wDetailStack.addText(details2); 271 | wDetailText2.font = Font.regularSystemFont(15); 272 | wDetailText2.textColor = themeColor; 273 | wDetailText2.textOpacity = 1; 274 | wDetailText2.leftAlignText(); 275 | 276 | weatherStack.addSpacer(20); 277 | 278 | /*-----------------------------------------------------------*/ 279 | /* Year Progress */ 280 | 281 | let yearStack = widgetHello.addStack(); 282 | yearStack.layoutVertically(); 283 | 284 | const yearFont = new Font('Menlo', 16); 285 | const yearColor = new Color('#80DEEA'); 286 | 287 | // Year icon in stack 288 | const yearProgressIcon = yearStack.addText("◕ " + new Date().getFullYear()); 289 | yearProgressIcon.font = yearFont; 290 | yearProgressIcon.textColor = yearColor; 291 | yearProgressIcon.textOpacity = 1; 292 | yearProgressIcon.leftAlignText(); 293 | 294 | // Year label in stack 295 | let yProgressStack = yearStack.addStack(); 296 | yProgressStack.layoutHorizontally(); 297 | yProgressStack.setPadding(0, 25, 0, 0); 298 | 299 | const yearProgress = yProgressStack.addText(renderYearProgress()); 300 | yearProgress.font = yearFont; 301 | yearProgress.textColor = yearColor; 302 | yearProgress.textOpacity = 1; 303 | yearProgress.leftAlignText(); 304 | widgetHello.addSpacer(20); 305 | 306 | /* ------------------------------------------------------ */ 307 | /* Battery labels group */ 308 | let batteryStack = widgetHello.addStack(); 309 | batteryStack.layoutVertically(); 310 | 311 | let batteryTitleStack = batteryStack.addStack(); 312 | batteryStack.layoutVertically(); 313 | const batteryIcon = batteryTitleStack.addImage(provideBatteryIcon()) 314 | batteryIcon.imageSize = new Size(16, 16); 315 | 316 | const batteryFont = new Font("Menlo", 16); 317 | let batteryLevel = Device.batteryLevel(); 318 | 319 | 320 | var batteryColor; 321 | if (batteryLevel >= 0.6) 322 | batteryColor = new Color("#A8DF65"); 323 | else if (batteryLevel >= 0.2) 324 | batteryColor = new Color("#FFCC00"); 325 | else 326 | batteryColor = new Color("#FFD571"); 327 | 328 | 329 | batteryTitleText = batteryTitleStack.addText(" Battery\t"); 330 | batteryTitleText.font = batteryFont; 331 | batteryTitleText.textColor = batteryColor; 332 | batteryIcon.tintColor = batteryColor; 333 | batteryTitleText.textOpacity = 1; 334 | batteryTitleText.leftAlignText(); 335 | 336 | // Battery Progress in stack 337 | let bProgressStack = batteryStack.addStack(); 338 | bProgressStack.layoutHorizontally(); 339 | bProgressStack.setPadding(0, 25, 0, 0); 340 | 341 | 342 | const batteryLine = bProgressStack.addText(renderBattery(batteryLevel)); 343 | batteryLine.font = batteryFont; 344 | batteryLine.textColor = batteryColor; 345 | batteryLine.textOpacity = 1; 346 | batteryLine.leftAlignText(); 347 | 348 | 349 | let batterytext = bProgressStack.addText( 350 | "[" + Math.round(batteryLevel * 100) + "%] "); 351 | batterytext.font = batteryFont; 352 | batterytext.textColor = batteryColor; 353 | batterytext.textOpacity = (1); 354 | batterytext.leftAlignText(); 355 | 356 | /* ------------------------------------------------------ */ 357 | // Bottom Spacer 358 | widgetHello.addSpacer(); 359 | widgetHello.setPadding(0, 20, 10, 0); 360 | 361 | // Set widget 362 | Script.setWidget(widgetHello); 363 | Script.complete(); 364 | 365 | 366 | /* ------------------------ */ 367 | /* Assemble Widget Finished */ 368 | /* ------------------------ */ 369 | } 370 | 371 | 372 | /* ---------------------------------------------------------------------- */ 373 | /* Functions */ 374 | /* ---------------------------------------------------------------------- */ 375 | 376 | function provideBatteryIcon() { 377 | 378 | // If we're charging, show the charging icon. 379 | if (Device.isCharging()) { 380 | return SFSymbol.named("battery.100.bolt").image 381 | } 382 | 383 | // Set the size of the battery icon. 384 | const batteryWidth = 87 385 | const batteryHeight = 41 386 | 387 | // Start our draw context. 388 | let draw = new DrawContext() 389 | draw.opaque = false 390 | draw.respectScreenScale = true 391 | draw.size = new Size(batteryWidth, batteryHeight) 392 | 393 | // Draw the battery. 394 | draw.drawImageInRect(SFSymbol.named("battery.0").image, new Rect(0, 0, batteryWidth, batteryHeight)) 395 | 396 | // Match the battery level values to the SFSymbol. 397 | const x = batteryWidth * 0.1525 398 | const y = batteryHeight * 0.247 399 | const width = batteryWidth * 0.602 400 | const height = batteryHeight * 0.505 401 | 402 | // Prevent unreadable icons. 403 | let level = Device.batteryLevel() 404 | if (level < 0.05) { 405 | level = 0.05 406 | } 407 | 408 | // Determine the width and radius of the battery level. 409 | const current = width * level 410 | let radius = height / 6.5 411 | 412 | // When it gets low, adjust the radius to match. 413 | if (current < (radius * 2)) { 414 | radius = current / 2 415 | } 416 | 417 | // Make the path for the battery level. 418 | let barPath = new Path() 419 | barPath.addRoundedRect(new Rect(x, y, current, height), radius, radius) 420 | draw.addPath(barPath) 421 | draw.setFillColor(Color.black()) 422 | draw.fillPath() 423 | return draw.getImage() 424 | } 425 | 426 | // Render battery with data 427 | function renderBattery(batteryLevel) { 428 | const left = "▓".repeat(Math.floor(batteryLevel * 25)); 429 | const used = "░".repeat(25 - left.length) 430 | const batteryAscii = left + used + " "; 431 | return batteryAscii; 432 | } 433 | 434 | // Render year progress 435 | function renderYearProgress() { 436 | const now = new Date() 437 | const start = new Date(now.getFullYear(), 0, 1) // Start of this year 438 | const end = new Date(now.getFullYear() + 1, 0, 1) // End of this year 439 | const progress = (now - start) / (end - start); 440 | return renderProgress(progress) 441 | } 442 | 443 | function renderProgress(progress) { 444 | const used = '▓'.repeat(Math.floor(progress * 25)) 445 | const left = '░'.repeat(25 - used.length) 446 | return `${used}${left} [${Math.floor(progress * 100)}%]`; 447 | } 448 | 449 | 450 | // Download weather img 451 | async function fetchImgUrl(url) { 452 | const request = new Request(url) 453 | var res = await request.loadImage(); 454 | return res; 455 | } 456 | 457 | async function downloadWeatherImg(path) { 458 | const url = "http://a.animedlweb.ga/weather/weathers25_2.json"; 459 | const data = await fetchWeatherPicData(url); 460 | var dataimg = null; 461 | var name = null; 462 | if (path.includes("bg")) { 463 | dataimg = data.background; 464 | name = path.replace("_bg", ""); 465 | } else { 466 | dataimg = data.icon; 467 | name = path.replace("_ico", ""); 468 | } 469 | var imgurl = null; 470 | switch (name) { 471 | case "01d": 472 | imgurl = dataimg._01d; 473 | break; 474 | case "01n": 475 | imgurl = dataimg._01n; 476 | break; 477 | case "02d": 478 | imgurl = dataimg._02d; 479 | break; 480 | case "02n": 481 | imgurl = dataimg._02n; 482 | break; 483 | case "03d": 484 | imgurl = dataimg._03d; 485 | break; 486 | case "03n": 487 | imgurl = dataimg._03n; 488 | break; 489 | case "04d": 490 | imgurl = dataimg._04d; 491 | break; 492 | case "04n": 493 | imgurl = dataimg._04n; 494 | break; 495 | case "09d": 496 | imgurl = dataimg._09d; 497 | break; 498 | case "09n": 499 | imgurl = dataimg._09n; 500 | break; 501 | case "10d": 502 | imgurl = dataimg._10d; 503 | break; 504 | case "10n": 505 | imgurl = dataimg._10n; 506 | break; 507 | case "11d": 508 | imgurl = dataimg._11d; 509 | break; 510 | case "11n": 511 | imgurl = dataimg._11n; 512 | break; 513 | case "13d": 514 | imgurl = dataimg._13d; 515 | break; 516 | case "13n": 517 | imgurl = dataimg._13n; 518 | break; 519 | case "50d": 520 | imgurl = dataimg._50d; 521 | break; 522 | case "50n": 523 | imgurl = dataimg._50n; 524 | break; 525 | } 526 | const image = await fetchImgUrl(imgurl); 527 | fm.writeImage(wBasePath + path + ".png", image); 528 | } 529 | 530 | // Get Weather Json 531 | async function fetchWeatherPicData(url) { 532 | 533 | const cachePath = wBasePath + "weather-pic-cache"; 534 | const cacheExists = fm.fileExists(cachePath); 535 | const cacheDate = cacheExists ? fm.modificationDate(cachePath) : 0; 536 | let weatherDataRaw; 537 | // If cache exists and it's been less than 60 seconds since last request, use cached data. 538 | if (cacheExists && (new Date().getTime() - cacheDate.getTime()) < 60000) { 539 | const cache = fm.readString(cachePath); 540 | weatherDataRaw = JSON.parse(cache); 541 | } else { 542 | // Otherwise, use the API to get new weather data. 543 | const request = new Request(url); 544 | weatherDataRaw = await request.loadJSON(); 545 | fm.writeString(cachePath, JSON.stringify(weatherDataRaw)); 546 | 547 | } 548 | 549 | return weatherDataRaw; 550 | } 551 | 552 | // Get Weather Json 553 | async function fetchWeatherData(url) { 554 | 555 | const cachePath = wBasePath + "weather-cache"; 556 | const cacheExists = fm.fileExists(cachePath); 557 | const cacheDate = cacheExists ? fm.modificationDate(cachePath) : 0; 558 | let weatherDataRaw; 559 | // If cache exists and it's been less than 60 seconds since last request, use cached data. 560 | if (cacheExists && (new Date().getTime() - cacheDate.getTime()) < 60000) { 561 | const cache = fm.readString(cachePath); 562 | weatherDataRaw = JSON.parse(cache); 563 | } else { 564 | // Otherwise, use the API to get new weather data. 565 | const request = new Request(url); 566 | weatherDataRaw = await request.loadJSON(); 567 | fm.writeString(cachePath, JSON.stringify(weatherDataRaw)); 568 | 569 | } 570 | 571 | return weatherDataRaw; 572 | } 573 | 574 | 575 | // Load image from local drive 576 | async function fetchImageLocal(path) { 577 | var finalPath = wBasePath + path + ".png"; 578 | if (fm.fileExists(finalPath) == true) { 579 | console.log("file exists: " + finalPath); 580 | return finalPath; 581 | } else { 582 | //throw new Error("Error file not found: " + path); 583 | if (fm.fileExists(wBasePath) == false) { 584 | console.log("Directry not exist creating one."); 585 | fm.createDirectory(wBasePath); 586 | } 587 | console.log("Downloading file: " + finalPath); 588 | await downloadWeatherImg(path); 589 | if (fm.fileExists(finalPath) == true) { 590 | console.log("file exists after download: " + finalPath); 591 | return finalPath; 592 | } else { 593 | throw new Error("Error file not found: " + path); 594 | } 595 | } 596 | } -------------------------------------------------------------------------------- /WigetBlur.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: yellow; icon-glyph: magic; 4 | 5 | // This script was created by Max Zeryck. 6 | 7 | // The amount of blurring. Default is 150. 8 | let blur = 150 9 | 10 | // Determine if user has taken the screenshot. 11 | var message 12 | 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." 13 | let options = ["Continue to select image","Exit to take screenshot","Update code"] 14 | let response = await generateAlert(message,options) 15 | 16 | // Return if we need to exit. 17 | if (response == 1) return 18 | 19 | // Update the code. 20 | if (response == 2) { 21 | 22 | // Determine if the user is using iCloud. 23 | let files = FileManager.local() 24 | const iCloudInUse = files.isFileStoredIniCloud(module.filename) 25 | 26 | // If so, use an iCloud file manager. 27 | files = iCloudInUse ? FileManager.iCloud() : files 28 | 29 | // Try to download the file. 30 | try { 31 | const req = new Request("https://raw.githubusercontent.com/mzeryck/Widget-Blur/main/widget-blur.js") 32 | const codeString = await req.loadString() 33 | files.writeString(module.filename, codeString) 34 | message = "The code has been updated. If the script is open, close it for the change to take effect." 35 | } catch { 36 | message = "The update failed. Please try again later." 37 | } 38 | options = ["OK"] 39 | await generateAlert(message,options) 40 | return 41 | } 42 | 43 | // Get screenshot and determine phone size. 44 | let img = await Photos.fromLibrary() 45 | let height = img.size.height 46 | let phone = phoneSizes()[height] 47 | if (!phone) { 48 | 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." 49 | await generateAlert(message,["OK"]) 50 | return 51 | } 52 | 53 | // Extra setup needed for 2436-sized phones. 54 | if (height == 2436) { 55 | 56 | let files = FileManager.local() 57 | let cacheName = "mz-phone-type" 58 | let cachePath = files.joinPath(files.libraryDirectory(), cacheName) 59 | 60 | // If we already cached the phone size, load it. 61 | if (files.fileExists(cachePath)) { 62 | let typeString = files.readString(cachePath) 63 | phone = phone[typeString] 64 | 65 | // Otherwise, prompt the user. 66 | } else { 67 | message = "What type of iPhone do you have?" 68 | let types = ["iPhone 12 mini", "iPhone 11 Pro, XS, or X"] 69 | let typeIndex = await generateAlert(message, types) 70 | let type = (typeIndex == 0) ? "mini" : "x" 71 | phone = phone[type] 72 | files.writeString(cachePath, type) 73 | } 74 | } 75 | 76 | // Prompt for widget size and position. 77 | message = "What size of widget are you creating?" 78 | let sizes = ["Small","Medium","Large"] 79 | let size = await generateAlert(message,sizes) 80 | let widgetSize = sizes[size] 81 | 82 | message = "What position will it be in?" 83 | message += (height == 1136 ? " (Note that your device only supports two rows of widgets, so the middle and bottom options are the same.)" : "") 84 | 85 | // Determine image crop based on phone size. 86 | let crop = { w: "", h: "", x: "", y: "" } 87 | if (widgetSize == "Small") { 88 | crop.w = phone.small 89 | crop.h = phone.small 90 | let positions = ["Top left","Top right","Middle left","Middle right","Bottom left","Bottom right"] 91 | let position = await generateAlert(message,positions) 92 | 93 | // Convert the two words into two keys for the phone size dictionary. 94 | let keys = positions[position].toLowerCase().split(' ') 95 | crop.y = phone[keys[0]] 96 | crop.x = phone[keys[1]] 97 | 98 | } else if (widgetSize == "Medium") { 99 | crop.w = phone.medium 100 | crop.h = phone.small 101 | 102 | // Medium and large widgets have a fixed x-value. 103 | crop.x = phone.left 104 | let positions = ["Top","Middle","Bottom"] 105 | let position = await generateAlert(message,positions) 106 | let key = positions[position].toLowerCase() 107 | crop.y = phone[key] 108 | 109 | } else if(widgetSize == "Large") { 110 | crop.w = phone.medium 111 | crop.h = phone.large 112 | crop.x = phone.left 113 | let positions = ["Top","Bottom"] 114 | let position = await generateAlert(message,positions) 115 | 116 | // Large widgets at the bottom have the "middle" y-value. 117 | crop.y = position ? phone.middle : phone.top 118 | } 119 | 120 | // Prompt for blur style. 121 | message = "Do you want a fully transparent widget, or a translucent blur effect?" 122 | let blurOptions = ["Transparent","Light blur","Dark blur","Just blur"] 123 | let blurred = await generateAlert(message,blurOptions) 124 | 125 | // We always need the cropped image. 126 | let imgCrop = cropImage(img) 127 | 128 | // If it's blurred, set the blur style. 129 | if (blurred) { 130 | const styles = ["", "light", "dark", "none"] 131 | const style = styles[blurred] 132 | imgCrop = await blurImage(img,imgCrop,style) 133 | } 134 | 135 | message = "Your widget background is ready. Choose where to save the image:" 136 | const exportPhotoOptions = ["Export to the Photos app","Export to the Files app"] 137 | const exportToFiles = await generateAlert(message,exportPhotoOptions) 138 | 139 | if (exportToFiles) { 140 | await DocumentPicker.exportImage(imgCrop) 141 | } else { 142 | Photos.save(imgCrop) 143 | } 144 | 145 | Script.complete() 146 | 147 | // Generate an alert with the provided array of options. 148 | async function generateAlert(message,options) { 149 | 150 | let alert = new Alert() 151 | alert.message = message 152 | 153 | for (const option of options) { 154 | alert.addAction(option) 155 | } 156 | 157 | let response = await alert.presentAlert() 158 | return response 159 | } 160 | 161 | // Crop an image into the specified rect. 162 | function cropImage(image) { 163 | 164 | let draw = new DrawContext() 165 | let rect = new Rect(crop.x,crop.y,crop.w,crop.h) 166 | draw.size = new Size(rect.width, rect.height) 167 | 168 | draw.drawImageAtPoint(image,new Point(-rect.x, -rect.y)) 169 | return draw.getImage() 170 | } 171 | 172 | async function blurImage(img,imgCrop,style) { 173 | const js = ` 174 | /* 175 | StackBlur - a fast almost Gaussian Blur For Canvas 176 | Version: 0.5 177 | Author: Mario Klingemann 178 | Contact: mario@quasimondo.com 179 | Website: http://quasimondo.com/StackBlurForCanvas/StackBlurDemo.html 180 | Twitter: @quasimondo 181 | In case you find this class useful - especially in commercial projects - 182 | I am not totally unhappy for a small donation to my PayPal account 183 | mario@quasimondo.de 184 | Or support me on flattr: 185 | https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript 186 | Copyright (c) 2010 Mario Klingemann 187 | Permission is hereby granted, free of charge, to any person 188 | obtaining a copy of this software and associated documentation 189 | files (the "Software"), to deal in the Software without 190 | restriction, including without limitation the rights to use, 191 | copy, modify, merge, publish, distribute, sublicense, and/or sell 192 | copies of the Software, and to permit persons to whom the 193 | Software is furnished to do so, subject to the following 194 | conditions: 195 | The above copyright notice and this permission notice shall be 196 | included in all copies or substantial portions of the Software. 197 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 198 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 199 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 200 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 201 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 202 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 203 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 204 | OTHER DEALINGS IN THE SOFTWARE. 205 | */ 206 | var mul_table = [ 207 | 512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512, 208 | 454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512, 209 | 482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456, 210 | 437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512, 211 | 497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328, 212 | 320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456, 213 | 446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335, 214 | 329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512, 215 | 505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405, 216 | 399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328, 217 | 324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271, 218 | 268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456, 219 | 451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388, 220 | 385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335, 221 | 332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292, 222 | 289,287,285,282,280,278,275,273,271,269,267,265,263,261,259]; 223 | 224 | 225 | var shg_table = [ 226 | 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 227 | 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 228 | 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 229 | 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 230 | 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 231 | 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 232 | 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 233 | 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 234 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 235 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 236 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 237 | 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 238 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 239 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 240 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 241 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 ]; 242 | function stackBlurCanvasRGB( id, top_x, top_y, width, height, radius ) 243 | { 244 | if ( isNaN(radius) || radius < 1 ) return; 245 | radius |= 0; 246 | 247 | var canvas = document.getElementById( id ); 248 | var context = canvas.getContext("2d"); 249 | var imageData; 250 | 251 | try { 252 | try { 253 | imageData = context.getImageData( top_x, top_y, width, height ); 254 | } catch(e) { 255 | 256 | // NOTE: this part is supposedly only needed if you want to work with local files 257 | // so it might be okay to remove the whole try/catch block and just use 258 | // imageData = context.getImageData( top_x, top_y, width, height ); 259 | try { 260 | netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"); 261 | imageData = context.getImageData( top_x, top_y, width, height ); 262 | } catch(e) { 263 | alert("Cannot access local image"); 264 | throw new Error("unable to access local image data: " + e); 265 | return; 266 | } 267 | } 268 | } catch(e) { 269 | alert("Cannot access image"); 270 | throw new Error("unable to access image data: " + e); 271 | } 272 | 273 | var pixels = imageData.data; 274 | 275 | var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, 276 | r_out_sum, g_out_sum, b_out_sum, 277 | r_in_sum, g_in_sum, b_in_sum, 278 | pr, pg, pb, rbs; 279 | 280 | var div = radius + radius + 1; 281 | var w4 = width << 2; 282 | var widthMinus1 = width - 1; 283 | var heightMinus1 = height - 1; 284 | var radiusPlus1 = radius + 1; 285 | var sumFactor = radiusPlus1 * ( radiusPlus1 + 1 ) / 2; 286 | 287 | var stackStart = new BlurStack(); 288 | var stack = stackStart; 289 | for ( i = 1; i < div; i++ ) 290 | { 291 | stack = stack.next = new BlurStack(); 292 | if ( i == radiusPlus1 ) var stackEnd = stack; 293 | } 294 | stack.next = stackStart; 295 | var stackIn = null; 296 | var stackOut = null; 297 | 298 | yw = yi = 0; 299 | 300 | var mul_sum = mul_table[radius]; 301 | var shg_sum = shg_table[radius]; 302 | 303 | for ( y = 0; y < height; y++ ) 304 | { 305 | r_in_sum = g_in_sum = b_in_sum = r_sum = g_sum = b_sum = 0; 306 | 307 | r_out_sum = radiusPlus1 * ( pr = pixels[yi] ); 308 | g_out_sum = radiusPlus1 * ( pg = pixels[yi+1] ); 309 | b_out_sum = radiusPlus1 * ( pb = pixels[yi+2] ); 310 | 311 | r_sum += sumFactor * pr; 312 | g_sum += sumFactor * pg; 313 | b_sum += sumFactor * pb; 314 | 315 | stack = stackStart; 316 | 317 | for( i = 0; i < radiusPlus1; i++ ) 318 | { 319 | stack.r = pr; 320 | stack.g = pg; 321 | stack.b = pb; 322 | stack = stack.next; 323 | } 324 | 325 | for( i = 1; i < radiusPlus1; i++ ) 326 | { 327 | p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 ); 328 | r_sum += ( stack.r = ( pr = pixels[p])) * ( rbs = radiusPlus1 - i ); 329 | g_sum += ( stack.g = ( pg = pixels[p+1])) * rbs; 330 | b_sum += ( stack.b = ( pb = pixels[p+2])) * rbs; 331 | 332 | r_in_sum += pr; 333 | g_in_sum += pg; 334 | b_in_sum += pb; 335 | 336 | stack = stack.next; 337 | } 338 | 339 | 340 | stackIn = stackStart; 341 | stackOut = stackEnd; 342 | for ( x = 0; x < width; x++ ) 343 | { 344 | pixels[yi] = (r_sum * mul_sum) >> shg_sum; 345 | pixels[yi+1] = (g_sum * mul_sum) >> shg_sum; 346 | pixels[yi+2] = (b_sum * mul_sum) >> shg_sum; 347 | 348 | r_sum -= r_out_sum; 349 | g_sum -= g_out_sum; 350 | b_sum -= b_out_sum; 351 | 352 | r_out_sum -= stackIn.r; 353 | g_out_sum -= stackIn.g; 354 | b_out_sum -= stackIn.b; 355 | 356 | p = ( yw + ( ( p = x + radius + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2; 357 | 358 | r_in_sum += ( stackIn.r = pixels[p]); 359 | g_in_sum += ( stackIn.g = pixels[p+1]); 360 | b_in_sum += ( stackIn.b = pixels[p+2]); 361 | 362 | r_sum += r_in_sum; 363 | g_sum += g_in_sum; 364 | b_sum += b_in_sum; 365 | 366 | stackIn = stackIn.next; 367 | 368 | r_out_sum += ( pr = stackOut.r ); 369 | g_out_sum += ( pg = stackOut.g ); 370 | b_out_sum += ( pb = stackOut.b ); 371 | 372 | r_in_sum -= pr; 373 | g_in_sum -= pg; 374 | b_in_sum -= pb; 375 | 376 | stackOut = stackOut.next; 377 | yi += 4; 378 | } 379 | yw += width; 380 | } 381 | 382 | for ( x = 0; x < width; x++ ) 383 | { 384 | g_in_sum = b_in_sum = r_in_sum = g_sum = b_sum = r_sum = 0; 385 | 386 | yi = x << 2; 387 | r_out_sum = radiusPlus1 * ( pr = pixels[yi]); 388 | g_out_sum = radiusPlus1 * ( pg = pixels[yi+1]); 389 | b_out_sum = radiusPlus1 * ( pb = pixels[yi+2]); 390 | 391 | r_sum += sumFactor * pr; 392 | g_sum += sumFactor * pg; 393 | b_sum += sumFactor * pb; 394 | 395 | stack = stackStart; 396 | 397 | for( i = 0; i < radiusPlus1; i++ ) 398 | { 399 | stack.r = pr; 400 | stack.g = pg; 401 | stack.b = pb; 402 | stack = stack.next; 403 | } 404 | 405 | yp = width; 406 | 407 | for( i = 1; i <= radius; i++ ) 408 | { 409 | yi = ( yp + x ) << 2; 410 | 411 | r_sum += ( stack.r = ( pr = pixels[yi])) * ( rbs = radiusPlus1 - i ); 412 | g_sum += ( stack.g = ( pg = pixels[yi+1])) * rbs; 413 | b_sum += ( stack.b = ( pb = pixels[yi+2])) * rbs; 414 | 415 | r_in_sum += pr; 416 | g_in_sum += pg; 417 | b_in_sum += pb; 418 | 419 | stack = stack.next; 420 | 421 | if( i < heightMinus1 ) 422 | { 423 | yp += width; 424 | } 425 | } 426 | 427 | yi = x; 428 | stackIn = stackStart; 429 | stackOut = stackEnd; 430 | for ( y = 0; y < height; y++ ) 431 | { 432 | p = yi << 2; 433 | pixels[p] = (r_sum * mul_sum) >> shg_sum; 434 | pixels[p+1] = (g_sum * mul_sum) >> shg_sum; 435 | pixels[p+2] = (b_sum * mul_sum) >> shg_sum; 436 | 437 | r_sum -= r_out_sum; 438 | g_sum -= g_out_sum; 439 | b_sum -= b_out_sum; 440 | 441 | r_out_sum -= stackIn.r; 442 | g_out_sum -= stackIn.g; 443 | b_out_sum -= stackIn.b; 444 | 445 | p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2; 446 | 447 | r_sum += ( r_in_sum += ( stackIn.r = pixels[p])); 448 | g_sum += ( g_in_sum += ( stackIn.g = pixels[p+1])); 449 | b_sum += ( b_in_sum += ( stackIn.b = pixels[p+2])); 450 | 451 | stackIn = stackIn.next; 452 | 453 | r_out_sum += ( pr = stackOut.r ); 454 | g_out_sum += ( pg = stackOut.g ); 455 | b_out_sum += ( pb = stackOut.b ); 456 | 457 | r_in_sum -= pr; 458 | g_in_sum -= pg; 459 | b_in_sum -= pb; 460 | 461 | stackOut = stackOut.next; 462 | 463 | yi += width; 464 | } 465 | } 466 | 467 | context.putImageData( imageData, top_x, top_y ); 468 | 469 | } 470 | function BlurStack() 471 | { 472 | this.r = 0; 473 | this.g = 0; 474 | this.b = 0; 475 | this.a = 0; 476 | this.next = null; 477 | } 478 | 479 | // https://gist.github.com/mjackson/5311256 480 | function rgbToHsl(r, g, b){ 481 | r /= 255, g /= 255, b /= 255; 482 | var max = Math.max(r, g, b), min = Math.min(r, g, b); 483 | var h, s, l = (max + min) / 2; 484 | if(max == min){ 485 | h = s = 0; // achromatic 486 | }else{ 487 | var d = max - min; 488 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 489 | switch(max){ 490 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 491 | case g: h = (b - r) / d + 2; break; 492 | case b: h = (r - g) / d + 4; break; 493 | } 494 | h /= 6; 495 | } 496 | return [h, s, l]; 497 | } 498 | function hslToRgb(h, s, l){ 499 | var r, g, b; 500 | if(s == 0){ 501 | r = g = b = l; // achromatic 502 | }else{ 503 | var hue2rgb = function hue2rgb(p, q, t){ 504 | if(t < 0) t += 1; 505 | if(t > 1) t -= 1; 506 | if(t < 1/6) return p + (q - p) * 6 * t; 507 | if(t < 1/2) return q; 508 | if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; 509 | return p; 510 | } 511 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s; 512 | var p = 2 * l - q; 513 | r = hue2rgb(p, q, h + 1/3); 514 | g = hue2rgb(p, q, h); 515 | b = hue2rgb(p, q, h - 1/3); 516 | } 517 | return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; 518 | } 519 | 520 | function lightBlur(hsl) { 521 | 522 | // Adjust the luminance. 523 | let lumCalc = 0.35 + (0.3 / hsl[2]); 524 | if (lumCalc < 1) { lumCalc = 1; } 525 | else if (lumCalc > 3.3) { lumCalc = 3.3; } 526 | const l = hsl[2] * lumCalc; 527 | 528 | // Adjust the saturation. 529 | const colorful = 2 * hsl[1] * l; 530 | const s = hsl[1] * colorful * 1.5; 531 | 532 | return [hsl[0],s,l]; 533 | 534 | } 535 | 536 | function darkBlur(hsl) { 537 | // Adjust the saturation. 538 | const colorful = 2 * hsl[1] * hsl[2]; 539 | const s = hsl[1] * (1 - hsl[2]) * 3; 540 | 541 | return [hsl[0],s,hsl[2]]; 542 | 543 | } 544 | // Set up the canvas. 545 | const img = document.getElementById("blurImg"); 546 | const canvas = document.getElementById("mainCanvas"); 547 | const w = img.naturalWidth; 548 | const h = img.naturalHeight; 549 | canvas.style.width = w + "px"; 550 | canvas.style.height = h + "px"; 551 | canvas.width = w; 552 | canvas.height = h; 553 | const context = canvas.getContext("2d"); 554 | context.clearRect( 0, 0, w, h ); 555 | context.drawImage( img, 0, 0 ); 556 | 557 | // Get the image data from the context. 558 | var imageData = context.getImageData(0,0,w,h); 559 | var pix = imageData.data; 560 | 561 | // Set the image function, if any. 562 | var imageFunc; 563 | var style = "${style}"; 564 | if (style == "dark") { imageFunc = darkBlur; } 565 | else if (style == "light") { imageFunc = lightBlur; } 566 | for (let i=0; i < pix.length; i+=4) { 567 | // Convert to HSL. 568 | let hsl = rgbToHsl(pix[i],pix[i+1],pix[i+2]); 569 | 570 | // Apply the image function if it exists. 571 | if (imageFunc) { hsl = imageFunc(hsl); } 572 | 573 | // Convert back to RGB. 574 | const rgb = hslToRgb(hsl[0], hsl[1], hsl[2]); 575 | 576 | // Put the values back into the data. 577 | pix[i] = rgb[0]; 578 | pix[i+1] = rgb[1]; 579 | pix[i+2] = rgb[2]; 580 | } 581 | // Draw over the old image. 582 | context.putImageData(imageData,0,0); 583 | // Blur the image. 584 | stackBlurCanvasRGB("mainCanvas", 0, 0, w, h, ${blur}); 585 | 586 | // Perform the additional processing for dark images. 587 | if (style == "dark") { 588 | 589 | // Draw the hard light box over it. 590 | context.globalCompositeOperation = "hard-light"; 591 | context.fillStyle = "rgba(55,55,55,0.2)"; 592 | context.fillRect(0, 0, w, h); 593 | // Draw the soft light box over it. 594 | context.globalCompositeOperation = "soft-light"; 595 | context.fillStyle = "rgba(55,55,55,1)"; 596 | context.fillRect(0, 0, w, h); 597 | // Draw the regular box over it. 598 | context.globalCompositeOperation = "source-over"; 599 | context.fillStyle = "rgba(55,55,55,0.4)"; 600 | context.fillRect(0, 0, w, h); 601 | 602 | // Otherwise process light images. 603 | } else if (style == "light") { 604 | context.fillStyle = "rgba(255,255,255,0.4)"; 605 | context.fillRect(0, 0, w, h); 606 | } 607 | // Return a base64 representation. 608 | canvas.toDataURL(); 609 | ` 610 | 611 | // Convert the images and create the HTML. 612 | let blurImgData = Data.fromPNG(img).toBase64String() 613 | let html = ` 614 | 615 | 616 | ` 617 | 618 | // Make the web view and get its return value. 619 | let view = new WebView() 620 | await view.loadHTML(html) 621 | let returnValue = await view.evaluateJavaScript(js) 622 | 623 | // Remove the data type from the string and convert to data. 624 | let imageDataString = returnValue.slice(22) 625 | let imageData = Data.fromBase64String(imageDataString) 626 | 627 | // Convert to image and crop before returning. 628 | let imageFromData = Image.fromData(imageData) 629 | return cropImage(imageFromData) 630 | } 631 | 632 | // Pixel sizes and positions for widgets on all supported phones. 633 | function phoneSizes() { 634 | let phones = { 635 | 636 | // 12 Pro Max 637 | "2778": { 638 | small: 510, 639 | medium: 1092, 640 | large: 1146, 641 | left: 96, 642 | right: 678, 643 | top: 246, 644 | middle: 882, 645 | bottom: 1518 646 | }, 647 | 648 | // 12 and 12 Pro 649 | "2532": { 650 | small: 474, 651 | medium: 1014, 652 | large: 1062, 653 | left: 78, 654 | right: 618, 655 | top: 231, 656 | middle: 819, 657 | bottom: 1407 658 | }, 659 | 660 | // 11 Pro Max, XS Max 661 | "2688": { 662 | small: 507, 663 | medium: 1080, 664 | large: 1137, 665 | left: 81, 666 | right: 654, 667 | top: 228, 668 | middle: 858, 669 | bottom: 1488 670 | }, 671 | 672 | // 11, XR 673 | "1792": { 674 | small: 338, 675 | medium: 720, 676 | large: 758, 677 | left: 54, 678 | right: 436, 679 | top: 160, 680 | middle: 580, 681 | bottom: 1000 682 | }, 683 | 684 | 685 | // 11 Pro, XS, X, 12 mini 686 | "2436": { 687 | 688 | x: { 689 | small: 465, 690 | medium: 987, 691 | large: 1035, 692 | left: 69, 693 | right: 591, 694 | top: 213, 695 | middle: 783, 696 | bottom: 1353, 697 | }, 698 | 699 | mini: { 700 | small: 465, 701 | medium: 987, 702 | large: 1035, 703 | left: 69, 704 | right: 591, 705 | top: 231, 706 | middle: 801, 707 | bottom: 1371, 708 | } 709 | 710 | }, 711 | 712 | // Plus phones 713 | "2208": { 714 | small: 471, 715 | medium: 1044, 716 | large: 1071, 717 | left: 99, 718 | right: 672, 719 | top: 114, 720 | middle: 696, 721 | bottom: 1278 722 | }, 723 | 724 | // SE2 and 6/6S/7/8 725 | "1334": { 726 | small: 296, 727 | medium: 642, 728 | large: 648, 729 | left: 54, 730 | right: 400, 731 | top: 60, 732 | middle: 412, 733 | bottom: 764 734 | }, 735 | 736 | 737 | // SE1 738 | "1136": { 739 | small: 282, 740 | medium: 584, 741 | large: 622, 742 | left: 30, 743 | right: 332, 744 | top: 59, 745 | middle: 399, 746 | bottom: 399 747 | }, 748 | 749 | // 11 and XR in Display Zoom mode 750 | "1624": { 751 | small: 310, 752 | medium: 658, 753 | large: 690, 754 | left: 46, 755 | right: 394, 756 | top: 142, 757 | middle: 522, 758 | bottom: 902 759 | }, 760 | 761 | // Plus in Display Zoom mode 762 | "2001" : { 763 | small: 444, 764 | medium: 963, 765 | large: 972, 766 | left: 81, 767 | right: 600, 768 | top: 90, 769 | middle: 618, 770 | bottom: 1146 771 | }, 772 | } 773 | return phones 774 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Widget Hello 2 | 3 | 4 | ## Screenshot 5 | ### 2020.11.22 Change Battery Icon 6 | ![Widget Hello Screenshot](https://github.com/elizhd/IOS_Scriptable/blob/master/Doc_Img/IMG_Widget_20201122.PNG) 7 | 8 | --- 9 | 10 | ### 2020.10.9 11 | ![Widget Hello Screenshot](https://github.com/elizhd/IOS_Scriptable/blob/master/Doc_Img/IMG_Widget.PNG) 12 | 13 | 14 | 15 | ## Reference 16 | 1. [Github - mzeryck/mz_invisible_widget.js](https://gist.github.com/mzeryck/3a97ccd1e059b3afa3c6666d27a496c9) 17 | 2. [Github - xkerwin/Scriptbale](https://github.com/xkerwin/Scriptbale) 18 | 3. [Github - mzeryck/mz_invisible_widget.js](https://gist.github.com/mzeryck/3a97ccd1e059b3afa3c6666d27a496c9) --------------------------------------------------------------------------------