-
-
Save Saudumm/374497679e126cc454af896234f3f5a2 to your computer and use it in GitHub Desktop.
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: blue; icon-glyph: file-alt; | |
/******************************************** | |
* * | |
* NEWS WIDGET (WORDPRESS AND RSS) * | |
* * | |
* v1.1.3 - made by @saudumm * | |
* https://twitter.com/saudumm * | |
* * | |
******************************************** | |
Feel free to contact me on Twitter or | |
GitHub, if you have any questions or issues. | |
GitHub Repo: | |
https://github.com/Saudumm/scriptable-News-Widget | |
******************************************** | |
* * | |
* INSTRUCTIONS / MANUAL * | |
* For instructions on how to set up the * | |
* widget please check the GitHub Repo * | |
* * | |
* DOWNLOAD UPDATES * | |
* To update News Widget or download * | |
* the latest version of the script * | |
* check out "News Widget Update.js" * | |
* in my GitHub Repo * | |
* Just add "News Widget Update.js" to * | |
* Scriptable and run it to download the * | |
* latest version of News Widget! * | |
* * | |
******************************************** | |
WIDGET PARAMETERS: you can long press on the | |
widget on your homescreen and edit | |
parameters | |
- Example: | |
small|https://www.stadt-bremerhaven.de|Caschys Blog|true|background.jpg|false|true|Avenir-Heavy | |
(displays a small layout widget for | |
Caschys Blog with post images, a custom | |
background image, no blur, a gradient over | |
the image and with the font Avenir-Heavy) | |
- Parameter order has to be: widget size, | |
site url, site name, show post images, | |
background image, blur background image, | |
background image gradient, font name | |
- Parameters have to be separated by | | |
- You can omit all subsequent parameters, | |
for example background image: | |
small|https://www.stadt-bremerhaven.de|Caschys Blog | |
- You can just set "small", "medium" or | |
"large" as a parameter | |
- Parameters that are not set will be set | |
by the standard widget config | |
*/ | |
// Check for script updates and get notified | |
// as soon as a new version is released | |
// true = check for script updates | |
// false = don't check for updates | |
const CHECK_FOR_SCRIPT_UPDATE = true | |
/* ============ CONFIG START ============ */ | |
/*******************************************/ | |
/* */ | |
/* STANDARD WIDGET CONFIG */ | |
/* */ | |
/* Everything in this section can be */ | |
/* overwritten with Widget Parameters */ | |
/* */ | |
/*******************************************/ | |
// Add Addresses (URLs/Links) of the | |
// website(s) and/or the RSS Feed(s) you want | |
// to fetch posts from. | |
// Format of a new line has to be: | |
// | |
// ["Link to site/feed", "Name of site"], | |
// | |
// Please note, the more sites you add, the | |
// longer the widgets needs to load all data. | |
// It's possible that the widget on your | |
// homescreen won't load anything or takes a | |
// very long time if you add too many links. | |
var PARAM_LINKS = | |
[ | |
["https://venturebeat.com", "VentureBeat"], | |
["http://rss.cnn.com/rss/edition_world.rss", "CNN"], | |
] | |
// Name of the website/feed to display in the | |
// widget (at the top). | |
// If only one site is configured (in the | |
// code or parameters), the name of the site | |
// is used. | |
var PARAM_WIDGET_TITLE = "NEWS WIDGET"; | |
// Note: custom background image files have | |
// to be in the Scriptable (iCloud) Files | |
// folder (same as the script .js file). | |
// Change to the filename of a custom | |
// background image (CASE SENSITIVE!) or set | |
// to "none" if you don't want a custom image | |
var PARAM_BG_IMAGE_NAME = "none"; | |
// Blur the background image (custom or the | |
// news image in small widgets). | |
// "true" = blur the background image | |
// "false" = no blur | |
var PARAM_BG_IMAGE_BLUR = "true"; | |
// "true" = gradient over the bg image | |
// "false" = no gradient | |
var PARAM_BG_IMAGE_GRADIENT = "true"; | |
// Note: combining | |
// PARAM_SHOW_POST_IMAGES = true + small | |
// widget will ignore CONF_BG_GRADIENT_COLOR | |
// values in small config widgets. | |
// "true" = display images next to headlines | |
// "false" = no images next to posts | |
var PARAM_SHOW_POST_IMAGES = "true"; | |
/*******************************************/ | |
/* */ | |
/* CONFIGURE LOOK AND FEEL */ | |
/* */ | |
/* NOTE ON DYNAMIC COLORS: */ | |
/* the first value is used in iOS light */ | |
/* mode, second value will be used in */ | |
/* dark mode */ | |
/* */ | |
/* Values are hexadecimal color values */ | |
/* Visit sites like */ | |
/* https://htmlcolorcodes.com */ | |
/* to find hex values for colors */ | |
/* */ | |
/*******************************************/ | |
// Configure which time format to use | |
// true = 12h time format | |
// false = 24h time format | |
const CONF_12_HOUR = false | |
// Set the background color of your widget | |
var CONF_BG_COLOR = | |
Color.dynamic( | |
new Color("#fefefe"), | |
new Color("#1c1c1e") | |
); | |
// Configure to use a color gradient instead | |
// of the single background color (above) | |
// true = use a color gradient | |
// - (colors configured below) | |
// false = use a single color | |
// - (color configured above) | |
var CONF_BG_GRADIENT = false; | |
// gradient color from the top of the widget | |
var CONF_BG_GRADIENT_COLOR_TOP = | |
Color.dynamic( | |
new Color("#dddddd"), | |
new Color("#222222") | |
); | |
// gradient color to the bottom of the widget | |
var CONF_BG_GRADIENT_COLOR_BTM = | |
Color.dynamic( | |
new Color("#bbbbbb"), | |
new Color("#444444") | |
); | |
// gradient color image overlay from the top | |
// of the widget | |
// used if a background image is displayed | |
// and PARAM_BG_IMAGE_GRADIENT = "true" | |
const CONF_BG_GRADIENT_OVERLAY_TOP = | |
Color.dynamic( | |
new Color("#fefefe", 0.3), | |
new Color("#1c1c1e", 0.3) | |
); | |
// gradient color image overlay to the bottom | |
// of the widget | |
// used if a background image is displayed | |
// and PARAM_BG_IMAGE_GRADIENT = "true" | |
const CONF_BG_GRADIENT_OVERLAY_BTM = | |
Color.dynamic( | |
new Color("#fefefe", 1.0), | |
new Color("#1c1c1e", 1.0) | |
); | |
/*******************************************/ | |
/* */ | |
/* TEXT FONTS AND SIZES */ | |
/* */ | |
/* NOTE ON FONTS: */ | |
/* Use System if you want to use the iOS */ | |
/* system font (SF Pro) and choose your */ | |
/* font weight */ | |
/* */ | |
/* Font weight options are: */ | |
/* ultralight, thin, light, regular, */ | |
/* medium, semibold, bold, heavy, black */ | |
/* */ | |
/* Refer to http://iosfonts.com if you */ | |
/* want to use other fonts and replace */ | |
/* System withyour chosen font name */ | |
/* (e.g. Copperplate or Copperplate-Bold) */ | |
/* */ | |
/*******************************************/ | |
// Set the font, size and text color of the | |
// name at the top of the widget | |
var CONF_FONT_SITENAME = "System" | |
const CONF_FONT_WEIGHT_SITENAME = "bold"; | |
const CONF_FONT_SIZE_SITENAME = 16; | |
const CONF_FONT_COLOR_SITENAME = | |
Color.dynamic( | |
new Color("#1c1c1e"), | |
new Color("#fefefe") | |
); | |
// Set the font, size and text color of the | |
// date and time line(s) in the widget | |
var CONF_FONT_DATE = "System" | |
const CONF_FONT_WEIGHT_DATE = "bold"; | |
const CONF_FONT_SIZE_DATE = 12; | |
const CONF_FONT_COLOR_DATE = | |
Color.dynamic( | |
Color.darkGray(), | |
Color.gray() | |
); | |
// Set the font, size and text color of the | |
// news titles in the widget | |
var CONF_FONT_TITLE = "System" | |
const CONF_FONT_WEIGHT_TITLE = "bold"; | |
const CONF_FONT_SIZE_TITLE = 12; | |
const CONF_FONT_COLOR_TITLE = | |
Color.dynamic( | |
new Color("#1c1c1e"), | |
new Color("#fefefe") | |
); | |
/* ============= CONFIG END ============= */ | |
/*******************************************/ | |
/* */ | |
/* DO NOT CHANGE ANYTHING BELOW! */ | |
/* */ | |
/*******************************************/ | |
var SINGLE_SITE_MODE = false | |
var WIDGET_SIZE = (config.runsInWidget ? config.widgetFamily : "small"); | |
// process widget parameters | |
if (args.widgetParameter) { | |
let param = args.widgetParameter.split("|"); | |
if (param.length == 1) { | |
SINGLE_SITE_MODE = (PARAM_LINKS.length == 1 ? true : false); | |
if (SINGLE_SITE_MODE) {PARAM_WIDGET_TITLE = PARAM_LINKS[0][1];} | |
} | |
if (param.length >= 1) {WIDGET_SIZE = param[0];} | |
if (param.length >= 2) { | |
if (param[1].substring(0, 4) == "http") { | |
PARAM_LINKS = [[param[1], ""]]; | |
SINGLE_SITE_MODE = true; | |
} else { | |
PARAM_LINKS = await loadTextFileToArray(param[1]) | |
if (PARAM_LINKS) { | |
SINGLE_SITE_MODE = (PARAM_LINKS.length == 1 ? true : false); | |
if (SINGLE_SITE_MODE) {PARAM_WIDGET_TITLE = PARAM_LINKS[0][1];} | |
} | |
} | |
} | |
if (param.length >= 3) { | |
PARAM_WIDGET_TITLE = param[2]; | |
} | |
if (param.length >= 4) {PARAM_SHOW_POST_IMAGES = param[3];} | |
if (param.length >= 5) {PARAM_BG_IMAGE_NAME = param[4];} | |
if (param.length >= 6) {PARAM_BG_IMAGE_BLUR = param[5];} | |
if (param.length >= 7) {PARAM_BG_IMAGE_GRADIENT = param[6];} | |
if (param.length >= 8) { | |
CONF_FONT_SITENAME = param[7]; | |
CONF_FONT_DATE = param[7]; | |
CONF_FONT_TITLE = param[7]; | |
} | |
} else { | |
SINGLE_SITE_MODE = (PARAM_LINKS.length == 1 ? true : false); | |
if (SINGLE_SITE_MODE) {PARAM_WIDGET_TITLE = PARAM_LINKS[0][1];} | |
} | |
// set the number of posts depending on WIDGET_SIZE | |
var POST_COUNT = (WIDGET_SIZE == "small") ? 1 : (WIDGET_SIZE == "medium") ? 2 : 5; | |
// check for updates | |
var UPDATE_AVAILABLE = false; | |
if (CHECK_FOR_SCRIPT_UPDATE) { | |
const CURRENT_VERSION = "v1.1.3" | |
const LATEST_VERSION = await loadGitHubVersion(); | |
if (CURRENT_VERSION.replace(/[^1-9]+/g, "") < LATEST_VERSION.replace(/[^1-9]+/g, "")) { | |
UPDATE_AVAILABLE = true; | |
} | |
} | |
// check directories | |
checkFileDirs() | |
// create widget | |
const widget = await createWidget(); | |
// show widget if run in app | |
if (!config.runsInWidget) { | |
switch (WIDGET_SIZE) { | |
case "small": | |
await widget.presentSmall(); | |
break; | |
case "medium": | |
await widget.presentMedium(); | |
break; | |
case "large": | |
await widget.presentLarge(); | |
break; | |
} | |
} | |
// set widget and end script | |
Script.setWidget(widget); | |
Script.complete(); | |
/* ============ FUNCTIONS ============ */ | |
// create the widget | |
async function createWidget() { | |
const postData = await getData(); | |
const list = new ListWidget(); | |
const fontSiteName = await loadFont(CONF_FONT_SITENAME, CONF_FONT_WEIGHT_SITENAME, CONF_FONT_SIZE_SITENAME); | |
const fontDate = await loadFont(CONF_FONT_DATE, CONF_FONT_WEIGHT_DATE, CONF_FONT_SIZE_DATE); | |
const fontTitle = await loadFont(CONF_FONT_TITLE, CONF_FONT_WEIGHT_TITLE, CONF_FONT_SIZE_TITLE); | |
// display name of the website | |
const siteName = list.addText(PARAM_WIDGET_TITLE); | |
siteName.font = fontSiteName | |
siteName.textColor = CONF_FONT_COLOR_SITENAME; | |
siteName.lineLimit = 1; | |
siteName.minimumScaleFactor = 0.5; | |
list.addSpacer(); | |
if (postData) { | |
if (POST_COUNT == 1 || postData.length == 1) { | |
// load widget background image (if PARAM_SHOW_POST_IMAGES = true or PARAM_BG_IMAGE_NAME is set) | |
if (PARAM_SHOW_POST_IMAGES == "true" && PARAM_BG_IMAGE_NAME == "none") { | |
if (postData.aPostIMGPaths[0] != "none") { | |
list.backgroundImage = await loadLocalImage(postData.aPostIMGPaths[0]+(PARAM_BG_IMAGE_BLUR == "true" ? "-bg-blur" : "-bg")); | |
} | |
// draw gradient over background image for better readability | |
CONF_BG_GRADIENT = true; | |
CONF_BG_GRADIENT_COLOR_TOP = CONF_BG_GRADIENT_OVERLAY_TOP; | |
CONF_BG_GRADIENT_COLOR_BTM = CONF_BG_GRADIENT_OVERLAY_BTM; | |
// small shadow outline on PARAM_WIDGET_TITLE for better readability | |
//siteName.shadowRadius = 1; | |
//siteName.shadowColor = Color.dynamic(new Color("#ffffff", 0), new Color("#000000", 0)); | |
} | |
const postStack = list.addStack(); | |
postStack.layoutVertically(); | |
if (!SINGLE_SITE_MODE) { | |
const labelSiteName = postStack.addText(postData.aPostSiteNames[0]); | |
labelSiteName.font = fontTitle; | |
labelSiteName.textColor = CONF_FONT_COLOR_DATE;; | |
labelSiteName.lineLimit = 1; | |
labelSiteName.minimumScaleFactor = 0.5; | |
} | |
const labelDateTime = postStack.addText(await new Date(postData.aPostDates[0]).toLocaleString([], {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: (CONF_12_HOUR ? true : false)})); | |
labelDateTime.font = fontDate; | |
labelDateTime.textColor = CONF_FONT_COLOR_DATE; | |
labelDateTime.lineLimit = 1; | |
labelDateTime.minimumScaleFactor = 0.5; | |
const labelHeadline = postStack.addText(postData.aPostTitles[0]); | |
labelHeadline.font = fontTitle; | |
labelHeadline.textColor = CONF_FONT_COLOR_TITLE; | |
labelHeadline.lineLimit = 3; | |
list.url = postData.aPostURLs[0]; | |
} else { | |
if (POST_COUNT < postData.length) {POST_COUNT = postData.length;} | |
const aStackRow = await new Array(POST_COUNT); | |
const aStackCol = await new Array(POST_COUNT); | |
const aLblSiteName = await new Array(POST_COUNT); | |
const aLblPostDate = await new Array(POST_COUNT); | |
const aLblPostTitle = await new Array(POST_COUNT); | |
const aLblPostIMG = await new Array(POST_COUNT); | |
let i; | |
for (i = 0; i < POST_COUNT; i++) { | |
aStackRow[i] = list.addStack(); | |
aStackRow[i].layoutHorizontally(); | |
aStackRow[i].url = postData.aPostURLs[i]; | |
aStackCol[i] = aStackRow[i].addStack(); | |
aStackCol[i].layoutVertically(); | |
aLblPostDate[i] = aStackCol[i].addText((SINGLE_SITE_MODE ? "" : postData.aPostSiteNames[i]+" - ")+await new Date(postData.aPostDates[i]).toLocaleString([], {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: (CONF_12_HOUR ? true : false)})); | |
aLblPostDate[i].font = fontDate; | |
aLblPostDate[i].textColor = CONF_FONT_COLOR_DATE; | |
aLblPostDate[i].lineLimit = 1; | |
aLblPostDate[i].minimumScaleFactor = 0.5; | |
aLblPostTitle[i] = aStackCol[i].addText(postData.aPostTitles[i]); | |
aLblPostTitle[i].font = fontTitle; | |
aLblPostTitle[i].textColor = CONF_FONT_COLOR_TITLE; | |
aLblPostTitle[i].lineLimit = 2; | |
if (PARAM_SHOW_POST_IMAGES == "true" && postData.aPostIMGPaths[i] != "none") { | |
aStackRow[i].addSpacer(); | |
aLblPostIMG[i] = aStackRow[i].addImage(await loadLocalImage(postData.aPostIMGPaths[i])); | |
aLblPostIMG[i].imageSize = new Size(45,45); | |
aLblPostIMG[i].cornerRadius = 8; | |
aLblPostIMG[i].rightAlignImage(); | |
} | |
if (i < POST_COUNT-1) {list.addSpacer();} | |
} | |
} | |
} else { | |
siteName.textColor = Color.white(); | |
const sad_face = list.addText(":(") | |
sad_face.font = Font.regularSystemFont(config.widgetFamily === "large" ? 190 : 60); | |
sad_face.textColor = Color.white(); | |
sad_face.lineLimit = 1; | |
sad_face.minimumScaleFactor = 0.1; | |
list.addSpacer(); | |
const err_msg = list.addText("Couldn't load data"); | |
err_msg.font = Font.regularSystemFont(12); | |
err_msg.textColor = Color.white(); | |
CONF_BG_COLOR = new Color("#1f67b1"); | |
CONF_BG_GRADIENT = false; | |
PARAM_BG_IMAGE_NAME = "none"; | |
} | |
if (UPDATE_AVAILABLE) { | |
const updateMsg = list.addText("Script Update available on GitHub"); | |
updateMsg.font = Font.lightSystemFont(9); | |
updateMsg.textColor = Color.white(); | |
updateMsg.lineLimit = 1; | |
updateMsg.minimumScaleFactor = 0.1; | |
updateMsg.url = "https://github.com/Saudumm/scriptable-News-Widget" | |
} | |
// widget background (image, single color or gradient) | |
if (PARAM_BG_IMAGE_NAME != "none") { | |
const customBGImage = await loadBGImage(PARAM_BG_IMAGE_NAME, PARAM_BG_IMAGE_BLUR); | |
if (customBGImage != "not found") { | |
list.backgroundImage = customBGImage; | |
if (PARAM_BG_IMAGE_GRADIENT == "true") { | |
// draw gradient over background image for better readability | |
const gradient = new LinearGradient(); | |
gradient.locations = [0, 1]; | |
gradient.colors = [CONF_BG_GRADIENT_OVERLAY_TOP, CONF_BG_GRADIENT_OVERLAY_BTM]; | |
list.backgroundGradient = gradient; | |
} | |
// small shadow outline on PARAM_WIDGET_TITLE for better readability | |
//siteName.shadowRadius = 1; | |
//siteName.shadowColor = Color.dynamic(new Color("#ffffff", 0), new Color("#000000", 0)); | |
} else { | |
list.backgroundColor = CONF_BG_COLOR; | |
} | |
} else if (CONF_BG_GRADIENT == true) { | |
const gradient = new LinearGradient(); | |
gradient.locations = [0, 1]; | |
gradient.colors = [CONF_BG_GRADIENT_COLOR_TOP, CONF_BG_GRADIENT_COLOR_BTM]; | |
list.backgroundGradient = gradient; | |
} else { | |
list.backgroundColor = CONF_BG_COLOR; | |
} | |
return list; | |
} | |
// get data from all websites and extract necessary data | |
async function getData() { | |
try { | |
const aData = await new Array(); | |
for (iLink = 0; iLink < PARAM_LINKS.length; iLink++) { | |
if (PARAM_LINKS[iLink][0].substring(0, 7) != "http://" && PARAM_LINKS[iLink][0].substring(0, 8) != "https://") { | |
PARAM_LINKS[iLink][0] = "https://"+PARAM_LINKS[iLink][0] | |
} | |
if (PARAM_LINKS[iLink][0].slice(-1) == "/") {PARAM_LINKS[iLink][0] = PARAM_LINKS[iLink][0].slice(0, -1);} | |
if (await isJSON(PARAM_LINKS[iLink][0]+"/wp-json/wp/v2/posts?per_page=1")) { | |
// WordPress JSON | |
try { | |
const loadedJSON = await new Request(PARAM_LINKS[iLink][0]+"/wp-json/wp/v2/posts?per_page=5").loadJSON(); | |
let loadPosts = (loadedJSON.length >= 5) ? 5 : loadedJSON.length | |
let iPost; | |
for (iPost = 0; iPost < loadPosts; iPost++) { | |
let postDate = loadedJSON[iPost].date; | |
let postDateSort = await new Date(postDate).toLocaleString(["fr-CA"], {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit"}); | |
let postTitle = loadedJSON[iPost].title.rendered; | |
postTitle = formatPostTitle(postTitle); | |
let postURL = loadedJSON[iPost].guid.rendered; | |
let postIMGURL = await getMediaURL(PARAM_LINKS[iLink][0], loadedJSON[iPost].featured_media, loadedJSON[iPost].id); | |
aData.push([postDateSort, postDate+"|||"+postTitle+"|||"+postURL+"|||"+postIMGURL+"|||"+PARAM_LINKS[iLink][1]]); | |
} | |
} catch(err) { | |
log(err); | |
} | |
} else { | |
// RSS Feeds | |
try { | |
const loadRSSFeed = await new Request(PARAM_LINKS[iLink][0]).loadString(); | |
const aRSSItems = await new Array(); | |
let itemValue = null; | |
let currentItem = null; | |
let searchForImage = true; | |
let xmlParser = new XMLParser(loadRSSFeed); | |
xmlParser.didStartElement = (name, elementContent) => { | |
itemValue = ""; | |
if (name == "entry" || name == "item") { | |
currentItem = {}; | |
searchForImage = true; | |
} | |
// possibel image values in element name | |
if ((name.substring(0, 6) == "media:" || name == "enclosure") && searchForImage) { | |
if (elementContent.url != null && currentItem != null) { | |
let imgLink = elementContent.url.match(/"?(http(?!.*http)s?\:\/\/.*?\.)(jpe?g|png|bmp)"?/i); | |
if (imgLink && imgLink.length == 3) { | |
imgLink = imgLink[1]+imgLink[2]; | |
currentItem["image"] = imgLink; | |
searchForImage = false; | |
} | |
} | |
} | |
} | |
xmlParser.didEndElement = name => { | |
const hasItem = currentItem != null; | |
// possible url location | |
if (hasItem && name == "id") { | |
currentItem["id"] = itemValue; | |
} | |
// possible url location | |
if (hasItem && name == "link") { | |
currentItem["link"] = itemValue; | |
} | |
// title value | |
if (hasItem && name == "title") { | |
currentItem["title"] = itemValue; | |
} | |
// published date | |
if (hasItem && (name == "published" || name == "pubDate")) { | |
currentItem["published"] = itemValue; | |
} | |
// possible image link location | |
if (hasItem && name == "image" && searchForImage) { | |
let imgLink = itemValue.match(/"?(http(?!.*http)s?\:\/\/.*?\.)(jpe?g|png|bmp)"?/i); | |
if (imgLink && imgLink.length == 3) { | |
imgLink = imgLink[1]+imgLink[2]; | |
currentItem["image"] = imgLink; | |
searchForImage = false; | |
} | |
} | |
// possible image link location | |
if (hasItem && name.includes("content")) { | |
let imgLink = itemValue.match(/src="(https?\:\/\/.*?\.)(jpe?g|png|bmp).*?"/i); | |
if (imgLink && imgLink.length == 3) { | |
imgLink = imgLink[1]+imgLink[2]; | |
currentItem["image"] = imgLink; | |
searchForImage = false; | |
} | |
} | |
// possible image link location | |
if (hasItem && name == "description") { | |
let imgLink = itemValue.match(/src="(https?\:\/\/.*?\.)(jpe?g|png|bmp).*?"/i); | |
if (imgLink && imgLink.length == 3) { | |
imgLink = imgLink[1]+imgLink[2]; | |
currentItem["image"] = imgLink; | |
searchForImage = false; | |
} | |
} | |
// end of item/entry block | |
if (name == "entry" || name == "item") { | |
aRSSItems.push(currentItem); | |
currentItem = null; | |
} | |
} | |
xmlParser.foundCharacters = str => { | |
itemValue += str; | |
} | |
xmlParser.didEndDocument = () => {} | |
await xmlParser.parse(); | |
let loadPosts = (aRSSItems.length >= 5) ? 5 : aRSSItems.length | |
for (iRSS = 0; iRSS < loadPosts; iRSS++) { | |
let rssDate = "none"; | |
let rssDateSort = "none"; | |
let rssTitle = "none"; | |
let rssURL = "none"; | |
let rssIMGURL = "none"; | |
if (aRSSItems[iRSS].published != null) { | |
rssDate = aRSSItems[iRSS].published; | |
rssDateSort = await new Date(rssDate).toLocaleString(["fr-CA"], {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit"}); | |
} | |
if (aRSSItems[iRSS].title) {rssTitle = aRSSItems[iRSS].title;} | |
if (aRSSItems[iRSS].id) { | |
rssURL = aRSSItems[iRSS].id; | |
} else if (aRSSItems[iRSS].link) { | |
rssURL = aRSSItems[iRSS].link; | |
} | |
if (aRSSItems[iRSS].image) { | |
// double check if link is image link | |
let rssIMGRegEx = await aRSSItems[iRSS].image.match(/"?(http(?!.*http)s?\:\/\/.*?\.)(jpe?g|png|bmp)"?/i); | |
if (rssIMGRegEx && rssIMGRegEx.length == 3) { | |
rssIMGURL = rssIMGRegEx[1]+rssIMGRegEx[2]; | |
} | |
} | |
aData.push([rssDateSort, rssDate+"|||"+rssTitle+"|||"+rssURL+"|||"+rssIMGURL+"|||"+PARAM_LINKS[iLink][1]]); | |
} | |
} catch(err) { | |
log(err); | |
} | |
} | |
} | |
if (aData.length >= 1) { | |
// sort all post according to date | |
aData.sort(function sortFunction(a, b) { | |
if (a[0] === b[0]) { | |
return 0; | |
} else { | |
return (a[0] < b[0]) ? -1 : 1; | |
} | |
}) | |
// reverse sorting - new to old date | |
aData.reverse() | |
const POSTS_TO_LOAD = (aData.length >= 5) ? 5 : aData.length; | |
const aDates = await new Array(POSTS_TO_LOAD); | |
const aTitles = await new Array(POSTS_TO_LOAD); | |
const aURLs = await new Array(POSTS_TO_LOAD); | |
const aIMGURLs = await new Array(POSTS_TO_LOAD); | |
const aIMGPaths = await new Array(POSTS_TO_LOAD); | |
const aSiteNames = await new Array(POSTS_TO_LOAD); | |
const aFileNames = await new Array(POSTS_TO_LOAD); | |
for (iNewPost = 0; iNewPost < POSTS_TO_LOAD; iNewPost++) { | |
const aStrSplit = aData[iNewPost][1].split("|||") | |
if (aStrSplit[0] != "none") { | |
aDates[iNewPost] = await new Date(aStrSplit[0]); | |
} else { | |
aDates[iNewPost] = await Date.now(); | |
} | |
aTitles[iNewPost] = aStrSplit[1]; | |
aTitles[iNewPost] = formatPostTitle(aTitles[iNewPost]); | |
aURLs[iNewPost] = aStrSplit[2]; | |
aSiteNames[iNewPost] = aStrSplit[4]; | |
if (PARAM_SHOW_POST_IMAGES == "true") { | |
aIMGURLs[iNewPost] = aStrSplit[3]; | |
if (aIMGURLs[iNewPost] != "none") { | |
let fileID = await hashCode(aIMGURLs[iNewPost]) | |
fileID = Math.abs(fileID) | |
aFileNames[iNewPost] = await getFileName(aSiteNames[iNewPost], fileID); | |
const addBGImage = (iNewPost == 0 ? true : false); | |
aIMGURLs[iNewPost] = await encodeURI(aIMGURLs[iNewPost]); | |
aIMGURLs[iNewPost] = await aIMGURLs[iNewPost].replaceAll("%25", "%"); // hack for some image URLs with % | |
aIMGPaths[iNewPost] = await downloadPostImage(aFileNames[iNewPost], aIMGURLs[iNewPost], addBGImage); | |
} else { | |
aIMGPaths[iNewPost] = "none"; | |
} | |
} | |
} | |
if (PARAM_SHOW_POST_IMAGES == "true") { | |
aFileNames.push(aFileNames[0]+"-bg"); | |
aFileNames.push(aFileNames[0]+"-bg-blur"); | |
await cleanUpImages(aFileNames); | |
} | |
return { | |
aPostDates: aDates, | |
aPostTitles: aTitles, | |
aPostURLs: aURLs, | |
aPostIMGPaths: aIMGPaths, | |
aPostSiteNames: aSiteNames | |
}; | |
} else { | |
return null; | |
} | |
} catch(err) { | |
logError(err); | |
return null; | |
} | |
} | |
// load a text file with links and convert to a PARAM_LINKS array | |
async function loadTextFileToArray(textFile) { | |
try { | |
const fm = FileManager.iCloud(); | |
const docDir = fm.documentsDirectory(); | |
const filePath = fm.joinPath(docDir, textFile); | |
if (fm.fileExists(filePath) && fm.isFileStoredIniCloud(filePath)) { | |
await fm.downloadFileFromiCloud(filePath); | |
var strTextFile = await fm.readString(filePath); | |
strTextFile = strTextFile.split(/\r?\n/) | |
if (strTextFile.length >= 1) { | |
const aLinks = new Array(); | |
for (iText = 0; iText < strTextFile.length; iText++) { | |
let aStrData = strTextFile[iText].split("|"); | |
if (aStrData.length == 1) { | |
if (aStrData[0].substring(0, 4) == "http") {aLinks.push([aStrData[0], "News"]);} | |
} else if (aStrData.length > 1) { | |
if (aStrData[0].substring(0, 4) == "http") {aLinks.push([aStrData[0], aStrData[1]]);} | |
} | |
} | |
return aLinks; | |
} else { | |
return null; | |
} | |
} else { | |
return null; | |
} | |
} catch(err) { | |
log(err); | |
return null; | |
} | |
} | |
// check if the url leads to a json file | |
async function isJSON(url) { | |
try { | |
let testJSON = await new Request(url).loadJSON(); | |
if (testJSON.reason == "Not Found") {return false;} | |
} catch(err) { | |
return false; | |
} | |
return true; | |
} | |
// get the featuredMedia image URL | |
async function getMediaURL(siteURL, featuredMedia, postID) { | |
let featuredMediaJSONURL = siteURL+"/wp-json/wp/v2/media/"+featuredMedia; | |
let loadedMediaJSON = await new Request(featuredMediaJSONURL).loadJSON(); | |
let mediaURL = loadedMediaJSON.source_url; | |
if (mediaURL == undefined || mediaURL == "undefined") { | |
// search for other images | |
featuredMediaJSONURL = siteURL+"/wp-json/wp/v2/posts/"+postID; | |
loadedMediaJSON = await new Request(featuredMediaJSONURL).loadJSON(); | |
mediaURL = loadedMediaJSON.jetpack_featured_media_url; | |
if (mediaURL == undefined || mediaURL == "undefined") { | |
return "none"; | |
} else { | |
mediaURL = mediaURL.match(/(http?s.*\.)(jpe?g|png|bmp)/i) | |
mediaURL = mediaURL[1]+""+mediaURL[2]; | |
return await encodeURI(mediaURL); | |
} | |
} else { | |
mediaURL = mediaURL.match(/(http?s.*\.)(jpe?g|png|bmp)/i) | |
mediaURL = mediaURL[1]+""+mediaURL[2]; | |
return await encodeURI(mediaURL); | |
} | |
return "none"; | |
} | |
// set the filename of the post image (site name + image id) | |
function getFileName(siteName, id) { | |
let widgetTitle = PARAM_WIDGET_TITLE.replace(/[^a-zA-Z1-9]+/g, "").toLowerCase(); | |
siteName = siteName.replace(/[^a-zA-Z1-9]+/g, "").toLowerCase(); | |
return widgetTitle+"-"+siteName+"-"+id; | |
} | |
// download the post image (if it doesn't already exist) | |
async function downloadPostImage(fileName, url, addBGImage) { | |
const fm = await FileManager.local(); | |
const cacheDir = await fm.cacheDirectory(); | |
const imgPath = await fm.joinPath(cacheDir+"/saudumm-news-widget-data/image-cache", fileName); | |
const tempDir = await fm.temporaryDirectory() | |
const tempPath = await fm.joinPath(tempDir, fileName); | |
// check if file already exists | |
if (!addBGImage && fm.fileExists(imgPath)) { | |
return imgPath; | |
} else if (!addBGImage && !fm.fileExists(imgPath)) { | |
// download, resize, crop and store image | |
let req = await new Request(url); | |
let loadedImage = await req.load(); | |
// write image and read again (it's smaller that way???) | |
await fm.write(tempPath, loadedImage); | |
loadedImage = await fm.readImage(tempPath); | |
loadedImage = await resizeImage(loadedImage, 150); | |
loadedImage = await cropImageToSquare(loadedImage); | |
await fm.writeImage(imgPath, loadedImage); | |
await fm.remove(tempPath); | |
return imgPath; | |
} | |
if (addBGImage) { | |
const imgPathBG = imgPath+"-bg" | |
const imgPathBGBlur = imgPath+"-bg-blur" | |
if (fm.fileExists(imgPath) && fm.fileExists(imgPathBG) && fm.fileExists(imgPathBGBlur)) { | |
return imgPath; | |
} else { | |
// download image | |
let req = await new Request(url); | |
let loadedImage = await req.load(); | |
// write image and read again (it's smaller that way???) | |
await fm.write(tempPath, loadedImage); | |
loadedImage = await fm.readImage(tempPath); | |
if (await Math.min(loadedImage.size.height, loadedImage.size.width) > 500) { | |
loadedImage = await resizeImage(loadedImage, 500); | |
} | |
// resize, crop and store image | |
if(!fm.fileExists(imgPath)) { | |
let loadedSmallImage = await resizeImage(loadedImage, 150); | |
loadedSmallImage = await cropImageToSquare(loadedSmallImage); | |
await fm.writeImage(imgPath, loadedSmallImage); | |
} | |
// store original image | |
if (!fm.fileExists(imgPathBG)) { | |
await fm.writeImage(imgPathBG, loadedImage); | |
} | |
// store blurred resized original image | |
if (!fm.fileExists(imgPathBGBlur)) { | |
let loadedImageBlur = await blurImage(loadedImage) | |
await fm.writeImage(imgPathBGBlur, loadedImageBlur); | |
} | |
await fm.remove(tempPath); | |
return imgPath; | |
} | |
} | |
return "none"; | |
} | |
// load post image from file path | |
async function loadLocalImage(imgPath) { | |
const fm = FileManager.local(); | |
if (fm.fileExists(imgPath)) { | |
const imgFile = await fm.readImage(imgPath); | |
return imgFile; | |
} | |
} | |
// search for and load a local (or iCloud) background image | |
async function loadBGImage(imageName, optBlur) { | |
const fm = FileManager.local(); | |
let fmiCloud; | |
try { | |
fmiCloud = FileManager.iCloud(); | |
} catch(err) { | |
// no iCloud, no BG Image | |
return "not found"; | |
} | |
const cacheDir = fm.cacheDirectory(); | |
const iCloudDocDir = fmiCloud.documentsDirectory(); | |
const bgIMGiCloudDocPath = fmiCloud.joinPath(iCloudDocDir, imageName); | |
const bgIMGiCloudWPPath = fmiCloud.joinPath(iCloudDocDir+"/wallpaper", imageName); | |
const bgIMGWPCachePath = fm.joinPath(cacheDir+"/saudumm-news-widget-data/wallpaper-cache", imageName); | |
if (optBlur == "true" && fm.fileExists(bgIMGWPCachePath+"-blur")) { | |
return await fm.readImage(bgIMGWPCachePath+"-blur"); | |
} else { | |
if (optBlur == "true") { | |
if (fmiCloud.fileExists(bgIMGiCloudDocPath)) { | |
if (fmiCloud.isFileStoredIniCloud(bgIMGiCloudDocPath)) {await fmiCloud.downloadFileFromiCloud(bgIMGiCloudDocPath);} | |
let imgToBlur = await fmiCloud.readImage(bgIMGiCloudDocPath); | |
imgToBlur = await resizeImage(imgToBlur, 300) | |
imgToBlur = await blurImage(imgToBlur); | |
await fm.writeImage(bgIMGWPCachePath+"-blur", imgToBlur); | |
return imgToBlur; | |
} else if (fmiCloud.fileExists(bgIMGiCloudWPPath)) { | |
if (fmiCloud.isFileStoredIniCloud(bgIMGiCloudWPPath)) {await fmiCloud.downloadFileFromiCloud(bgIMGiCloudWPPath);} | |
let imgToBlur = await fmiCloud.readImage(bgIMGiCloudWPPath); | |
imgToBlur = await resizeImage(imgToBlur, 300) | |
imgToBlur = await blurImage(imgToBlur); | |
await fm.writeImage(bgIMGWPCachePath+"-blur", imgToBlur); | |
return imgToBlur; | |
} else { | |
return "not found"; | |
} | |
} else { | |
if (fmiCloud.fileExists(bgIMGiCloudDocPath)) { | |
return await fmiCloud.readImage(bgIMGiCloudDocPath); | |
} else if (fmiCloud.fileExists(bgIMGiCloudWPPath)) { | |
return await fmiCloud.readImage(bgIMGiCloudWPPath); | |
} else { | |
return "not found"; | |
} | |
} | |
} | |
} | |
// check if all folders are available and create them if needed | |
function checkFileDirs() { | |
// Create new FileManager and set data dir | |
const fm = FileManager.local(); | |
const cacheDir = fm.cacheDirectory(); | |
const imgCacheDir = cacheDir+"/saudumm-news-widget-data/image-cache"; | |
const imgCacheDirWP = cacheDir+"/saudumm-news-widget-data/wallpaper-cache"; | |
if (!fm.fileExists(imgCacheDir)) {fm.createDirectory(imgCacheDir, true);} | |
if (!fm.fileExists(imgCacheDirWP)) {fm.createDirectory(imgCacheDirWP, true);} | |
return; | |
} | |
// cleanup post image files (if older than 7 days) | |
function cleanUpImages(aFileNames) { | |
const fm = FileManager.local(); | |
const cacheDir = fm.cacheDirectory(); | |
const imgCacheDir = cacheDir+"/saudumm-news-widget-data/image-cache"; | |
const aFiles = fm.listContents(imgCacheDir); | |
const site_id = PARAM_WIDGET_TITLE.replace(/[^a-zA-Z1-9]+/g, "").toLowerCase(); | |
let aFilesSite = new Array(); | |
for (i = 0; i < aFiles.length; i++) { | |
if (aFiles[i].substring(0, site_id.length) === site_id) {aFilesSite.push(aFiles[i]);} | |
} | |
for (i = 0; i < aFilesSite.length; i++) { | |
if (!aFileNames.includes(aFilesSite[i])) { | |
let path = fm.joinPath(imgCacheDir, aFilesSite[i]); | |
let fileDate = fm.creationDate(path); | |
let dateNow = Date.now(); | |
let dateDiffDays = Math.round((dateNow-fileDate)/1000/60/60/24); | |
if (Math.abs(dateDiffDays) > 7) { | |
fm.remove(path); | |
} | |
} | |
} | |
return; | |
} | |
// create a hash from a string | |
function hashCode (str){ | |
var hash = 0; | |
if (str.length == 0) return hash; | |
for (cHash = 0; cHash < str.length; cHash++) { | |
char = str.charCodeAt(cHash); | |
hash = ((hash<<5)-hash)+char; | |
hash = hash & hash; // Convert to 32bit integer | |
} | |
return hash; | |
} | |
// format the post title and replace all html entities with characters | |
function formatPostTitle(strHeadline) { | |
strHeadline = strHeadline.trim(); | |
strHeadline = strHeadline.replaceAll(""", '"'); | |
strHeadline = strHeadline.replaceAll("&", "&"); | |
strHeadline = strHeadline.replaceAll("<", "<"); | |
strHeadline = strHeadline.replaceAll(">", ">"); | |
strHeadline = strHeadline.replaceAll("'", "'"); | |
strHeadline = strHeadline.replaceAll(""", '"'); | |
strHeadline = strHeadline.replaceAll("&", "&"); | |
strHeadline = strHeadline.replaceAll("'", "'"); | |
strHeadline = strHeadline.replaceAll("<", "<"); | |
strHeadline = strHeadline.replaceAll(">", ">"); | |
strHeadline = strHeadline.replaceAll("Œ", "Œ"); | |
strHeadline = strHeadline.replaceAll("œ", "œ"); | |
strHeadline = strHeadline.replaceAll("Š", "Š"); | |
strHeadline = strHeadline.replaceAll("š", "š"); | |
strHeadline = strHeadline.replaceAll("Ÿ", "Ÿ"); | |
strHeadline = strHeadline.replaceAll("ˆ", "ˆ"); | |
strHeadline = strHeadline.replaceAll("˜", "˜"); | |
strHeadline = strHeadline.replaceAll("–", "–"); | |
strHeadline = strHeadline.replaceAll("—", "—"); | |
strHeadline = strHeadline.replaceAll("‘", "‘"); | |
strHeadline = strHeadline.replaceAll("’", "’"); | |
strHeadline = strHeadline.replaceAll("‚", "‚"); | |
strHeadline = strHeadline.replaceAll("“", "“"); | |
strHeadline = strHeadline.replaceAll("”", "”"); | |
strHeadline = strHeadline.replaceAll("„", "„"); | |
strHeadline = strHeadline.replaceAll("†", "†"); | |
strHeadline = strHeadline.replaceAll("‡", "‡"); | |
strHeadline = strHeadline.replaceAll("…", "…"); | |
strHeadline = strHeadline.replaceAll("‰", "‰"); | |
strHeadline = strHeadline.replaceAll("‹", "‹"); | |
strHeadline = strHeadline.replaceAll("›", "›"); | |
strHeadline = strHeadline.replaceAll("€", "€"); | |
strHeadline = strHeadline.replaceAll("<![CDATA[", ""); | |
strHeadline = strHeadline.replaceAll("]]>", ""); | |
return strHeadline; | |
} | |
// get the chosen font for widget texts | |
function loadFont(fontName, fontThickness, fontSize) { | |
let font = Font.boldSystemFont(fontSize); | |
if (fontName == "System") { | |
switch (fontThickness) { | |
case "ultralight": | |
font = Font.ultraLightSystemFont(fontSize); | |
break; | |
case "thin": | |
font = Font.thinSystemFont(fontSize); | |
break; | |
case "light": | |
font = Font.lightSystemFont(fontSize); | |
break; | |
case "regular": | |
font = Font.regularSystemFont(fontSize); | |
break; | |
case "medium": | |
font = Font.mediumSystemFont(fontSize); | |
break; | |
case "semibold": | |
font = Font.semiboldSystemFont(fontSize); | |
break; | |
case "bold": | |
font = Font.boldSystemFont(fontSize); | |
break; | |
case "heavy": | |
font = Font.heavySystemFont(fontSize); | |
break; | |
case "black": | |
font = Font.blackSystemFont(fontSize); | |
break; | |
} | |
} else { | |
font = new Font(fontName, fontSize); | |
} | |
return font; | |
} | |
// load the latest script version number from GitHub | |
async function loadGitHubVersion() { | |
try { | |
const latestVersion = await new Request("https://raw.githubusercontent.com/Saudumm/scriptable-News-Widget/main/version.txt").loadString(); | |
return latestVersion; | |
} catch(err) { | |
return CURRENT_VERSION; | |
} | |
} | |
// blurs an image | |
async function blurImage(img) { | |
/* | |
* A big THANK YOU to Mario Klingemann for the Blur Code and Max Zeryck for the WebView Code | |
* code taken and modified from: https://github.com/mzeryck/Widget-Blur | |
* Follow @mzeryck on Twitter: https://twitter.com/mzeryck | |
*/ | |
// defines the blur strength in relation to the image resolution | |
const blurStrength = Math.floor((img.size.height*img.size.width)/18000); | |
if (blurStrength == 0) {blurStrength = 1;} | |
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 { | |
imageData = context.getImageData(top_x, top_y, width, height); | |
} 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; | |
} | |
// Set up the canvas | |
const img = document.getElementById("blurImg"); | |
const canvas = document.getElementById("mainCanvas"); | |
const w = img.width; | |
const h = img.height; | |
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, w, h); | |
// Get the image data from the context | |
var imageData = context.getImageData(0,0,w,h); | |
// Draw over the old image | |
context.putImageData(imageData,0,0); | |
// Blur the image | |
stackBlurCanvasRGB("mainCanvas", 0, 0, w, h, ${blurStrength}); | |
// Return a base64 representation | |
canvas.toDataURL(); | |
`; | |
// Convert the images and create the HTML | |
let blurImgData = await 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 = await returnValue.slice(22); | |
let imageData = await Data.fromBase64String(imageDataString); | |
// Convert to image before returning | |
let imageFromData = await Image.fromData(imageData); | |
return imageFromData; | |
} | |
// resize the background image | |
async function resizeImage(img, maxShortSide) { | |
let imgHeight = await img.size.height; | |
let imgWidth = await img.size.width; | |
let imgShortSide = await Math.min(imgHeight, imgWidth); | |
let resizeFactor = await Math.round(imgShortSide/maxShortSide); | |
const js = ` | |
// Set up the canvas | |
const img = document.getElementById("resImg"); | |
const canvas = document.getElementById("mainCanvas"); | |
const w = img.width; | |
const h = img.height; | |
const maxW = Math.round(w / ${resizeFactor}); | |
const maxH = Math.round(h / ${resizeFactor}); | |
canvas.style.width = w + "px"; | |
canvas.style.height = h + "px"; | |
canvas.width = maxW; | |
canvas.height = maxH; | |
const context = canvas.getContext("2d"); | |
context.clearRect(0, 0, w, h); | |
context.drawImage(img, 0, 0, maxW, maxH); | |
// Get the image data from the context | |
var imageData = context.getImageData(0,0,w,h); | |
// Draw over the old image | |
context.putImageData(imageData,0,0); | |
// Return a base64 representation | |
canvas.toDataURL(); | |
`; | |
// Convert the images and create the HTML | |
let resImgData = await Data.fromPNG(img).toBase64String(); | |
let html = `<img id="resImg" src="data:image/png;base64,${resImgData}" /><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 = await returnValue.slice(22); | |
let imageData = await Data.fromBase64String(imageDataString); | |
// Convert to image before returning | |
let imageFromData = await Image.fromData(imageData); | |
return imageFromData; | |
} | |
// crop an image to a square | |
async function cropImageToSquare(img) { | |
const imgHeight = await img.size.height; | |
const imgWidth = await img.size.width; | |
let imgShortSide = await Math.min(imgHeight, imgWidth); | |
let imgLongSide = await Math.max(imgHeight, imgWidth); | |
if (imgShortSide != imgLongSide) { | |
let imgCropTotal = await (imgLongSide - imgShortSide); | |
let imgCropSide = await Math.floor(imgCropTotal / 2); | |
let rect; | |
switch (imgShortSide) { | |
case imgHeight: | |
rect = new Rect(imgCropSide, 0, imgShortSide, imgShortSide); | |
break; | |
case imgWidth: | |
rect = new Rect(0, imgCropSide, imgShortSide, imgShortSide); | |
break; | |
} | |
let draw = new DrawContext(); | |
draw.size = new Size(rect.width, rect.height); | |
draw.drawImageAtPoint(img, new Point(-rect.x, -rect.y)); | |
img = draw.getImage(); | |
} | |
return img; | |
} | |
/* ========== END OF SCRIPT ========== */ |
Thanks for the nice update but the images fetching not works. On the same feed using the old widget code I see the thumbs, with latest code I don't.
I just uploaded v1.1.3 - image fetching should be more reliable now.
Thanks for the nice update but the images fetching not works. On the same feed using the old widget code I see the thumbs, with latest code I don't.
I just uploaded v1.1.3 - image fetching should be more reliable now.
Great, thank you. I'm testing it and this update works fine. Now the widget is amazing.
I would suggest only another thing: the date is in US format, I mean Month/Day/Year, but if you disable the 12h you should live/use EU time/date, so display the date in Day/Month/Year would be more useful! Not a big trouble BTW. Thanks again!
Great, thank you. I'm testing it and this update works fine. Now the widget is amazing.
I would suggest only another thing: the date is in US format, I mean Month/Day/Year, but if you disable the 12h you should live/use EU time/date, so display the date in Day/Month/Year would be more useful! Not a big trouble BTW. Thanks again!
Thank you! :)
Currently the date is tied to your system language, but maybe I can add an option to set the locale via a parameter. I'll look into it.
since the last update of the Scrptable app I have problems to use your Skript .
Can you check if you need to update something ?
since the last update of the Scrptable app I have problems to use your Skript .
Can you check if you need to update something ?
What problems exactly?
Hi! Great work. Do you know why it does not work with “https://ilpost.it/feed” as source? Thank you.
Hi! That's an unfortunate side effect of the new code I wrote for parsing RSS feeds. I'm working on this problem right now.