Last active
January 6, 2024 02:55
-
-
Save iiKurt/0c6d11d11590a781ea75ae2757fc13c3 to your computer and use it in GitHub Desktop.
OUTDATED, see: https://github.com/iiKurt/Wikipedia-Top-Read-Widget
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: brown; icon-glyph: bookmark; | |
// Useful links | |
// https://en.wikipedia.org/api/rest_v1/#/Feed/aggregatedFeed | |
// https://design.wikimedia.org/blog/2021/04/26/bringing-wikipedia-to-the-homescreen-on-ios.html | |
// https://design.wikimedia.org/blog/assets/uploads/wikipedia-widget/wikipedia-top-read-widget.png | |
const PREFS = { | |
// Number of articles to show when running in app | |
// API only returns about 50 articles I think | |
maximumArticles: 25, | |
// Whether to show the app view in fullscreen or not | |
fullscreen: false, | |
// Is automatically determined based on widget size | |
// Set to 4 when running script directly in debug mode | |
maximumWidgetArticles: 4, | |
background: Color.dynamic(new Color("#FFF"), new Color("#000")), | |
foregroundPrimary: Color.dynamic(new Color("#00"), new Color("#FF")), | |
foregroundSecondary: Color.dynamic(new Color("#8A898D"), new Color("#8E8D93")), | |
foregroundTertiary: Color.dynamic(new Color("#EEE"), new Color("#222")), | |
statsBackground: Color.dynamic(new Color("#F8F7F9"), new Color("#1F1F1F")), | |
statsLines: Color.dynamic(new Color("#EBEBEB"), new Color("#373737")), | |
statsForeground: Color.dynamic(new Color("#4DAA8C"), new Color("#4DAA8C")), | |
// Not actually used at the moment | |
font: { | |
name: "San Francisco", | |
size: 16, | |
}, | |
rankColors: [ | |
new Color("#3F64C7"), | |
new Color("#3E75B5"), | |
new Color("#4188A7"), | |
new Color("#479B99") | |
], | |
debugMode: false | |
} | |
// Preview widget | |
if (PREFS.debugMode) { | |
let widget = await createWidget(); | |
await widget.presentLarge(); | |
} | |
// Running in widget | |
else if (config.runsInWidget) { | |
let widget = await createWidget(); | |
Script.setWidget(widget); | |
} | |
// Not running in widget/being run directly | |
else { | |
let app = await createApp(); | |
await QuickLook.present(app, PREFS.fullscreen); | |
} | |
Script.complete(); | |
async function createApp() { | |
let topRead = await getTopRead(PREFS.maximumArticles); | |
const table = new UITable() | |
for (let index = 0; index < topRead.length; index++) { | |
let article = await topRead[index]; | |
let row = new UITableRow(); | |
let rankCell = row.addText("" + (index + 1)); | |
rankCell.titleFont = Font.boldSystemFont(16); | |
let textCell = row.addText(article.normalizedtitle, article.description); | |
textCell.subtitleColor = PREFS.foregroundSecondary; | |
let viewsCell = row.addText(formatNumber(article.views)); | |
viewsCell.titleFont = Font.footnote(); | |
viewsCell.titleColor = PREFS.statsForeground; | |
let imageCell = row.addImageAtURL(article.thumbnail?.source); | |
rankCell.widthWeight = 10; | |
textCell.widthWeight = 100; | |
viewsCell.widthWeight = 20; | |
imageCell.widthWeight = 10; | |
row.height = 60; | |
row.cellSpacing = 10; | |
row.onSelect = () => { | |
Safari.open(article.content_urls.desktop.page); | |
} | |
row.dismissOnSelect = false; | |
table.addRow(row); | |
} | |
return table | |
} | |
async function createWidget() { | |
let widget = new ListWidget(); | |
let nextRefresh = Date.now() + 1000*60*60*8; // 8 hours | |
widget.refreshAfterDate = new Date(nextRefresh); | |
// Light color first, dark color second | |
widget.backgroundColor = PREFS.background; | |
widget.setPadding(15, 15, 0, 15); | |
// (top, leading, bottom, trailing) | |
const line = widget.addText("Top read"); | |
line.font = Font.boldSystemFont(18); | |
line.textColor = PREFS.foregroundPrimary; | |
// widget.addSpacer(); | |
const listStack = widget.addStack(); | |
listStack.layoutVertically(); | |
listStack.setPadding(7.5, 0, 0, 0); | |
// Determine how many articles to show | |
if (config.widgetFamily == "small") { | |
PREFS.maximumWidgetArticles = 1; | |
} | |
else if (config.widgetFamily == "medium") { | |
PREFS.maximumWidgetArticles = 2; | |
} | |
else if (config.widgetFamily == "large") { | |
PREFS.maximumWidgetArticles = 4; | |
} | |
// Create the article list | |
let topRead = await getTopRead(PREFS.maximumWidgetArticles); | |
for (let index = 0; index < topRead.length; index++) { | |
let article = topRead[index]; | |
await listItem( | |
listStack, | |
index + 1, | |
article.normalizedtitle, | |
article.description, | |
article.content_urls.desktop.page, | |
article.views, | |
await loadThumbnail(article.thumbnail?.source), | |
config.widgetFamily | |
); | |
}; | |
// Small widgets only support one tap target - set it to the top article's URL | |
if (config.widgetFamily == "small") { | |
widget.url = topRead[0].content_urls.desktop.page; | |
} | |
// await listItem(listStack, 1, "Octavia E. Butler", "American science fiction writer", 10000, "https://upload.wikimedia.org/wikipedia/commons/thumb/e/eb/Mexico_City_New_Years_2013%21_%288333128248%29.jpg/320px-Mexico_City_New_Years_2013%21_%288333128248%29.jpg"); | |
// await listItem(listStack, 2, "Octavia E. Butler"); | |
// await listItem(listStack, 3, "Octavia E. Butler"); | |
// await listItem(listStack, 4, "Octavia E. Butler"); | |
widget.addSpacer(); | |
return widget; | |
} | |
async function listItem(listStack, rank, title, description = "Some description", url, views = 0, thumbnail = undefined, size = "large") { | |
// Item | |
const itemStack = listStack.addStack(); | |
itemStack.layoutHorizontally(); | |
itemStack.centerAlignContent(); | |
itemStack.url = url; | |
// fixes the spacing so all the items aren't at the top when there isn't much content | |
listStack.addSpacer(); | |
// Ranking | |
if (size != "small") { | |
let colorIndex = rank; | |
let scaling = PREFS.rankColors.length/PREFS.maximumWidgetArticles; | |
colorIndex *= scaling; | |
colorIndex -= 1; | |
const rankSymbol = SFSymbol.named(rank + ".circle"); | |
rankSymbol.applyFont(Font.thinSystemFont(24)) | |
const rankImage = rankSymbol.image; | |
const rankWidgetImage = itemStack.addImage(rankImage); | |
rankWidgetImage.tintColor = PREFS.rankColors[colorIndex]; | |
rankWidgetImage.resizable = false; | |
} | |
// Title and description | |
const infoStack = itemStack.addStack(); | |
infoStack.layoutVertically(); | |
if (size == "small") { | |
infoStack.setPadding(4, 0, 4, 0); | |
} | |
else { | |
infoStack.setPadding(4, 15, 4, 0); | |
} | |
infoStack.spacing = 4; | |
// Title | |
const itemTitle = infoStack.addText(title); | |
itemTitle.font = Font.semiboldSystemFont(16); | |
itemTitle.textColor = PREFS.foregroundPrimary; | |
// Description | |
const itemDescription = infoStack.addText(description); | |
itemDescription.font = Font.regularSystemFont(14); | |
itemDescription.textColor = PREFS.foregroundSecondary; | |
// View count | |
if (size == "large" || size == "small") { | |
const statsStack = infoStack.addStack(); | |
statsStack.backgroundColor = PREFS.statsBackground; | |
statsStack.cornerRadius = 4; | |
statsStack.size = new Size(0, 18); | |
statsStack.centerAlignContent(); | |
statsStack.setPadding(4, 4, 4, 4); | |
// Graph would go here... | |
//statsStack.addSpacer(32); | |
const itemViews = statsStack.addText(formatNumber(views) + ""); | |
itemViews.font = Font.regularSystemFont(12); | |
itemViews.textColor = PREFS.statsForeground; | |
} | |
// Thumbnail image | |
if (size != "small") { | |
itemStack.addSpacer(); | |
let spacing = 60; | |
if (size == "large") { | |
spacing = 60; | |
} | |
else if (size == "medium") { | |
spacing = 40; | |
} | |
// Some articles don't have a thumbnail provided | |
if (thumbnail === undefined) { | |
// Empty spot where thumbnail would usually be | |
//itemStack.addSpacer(spacing); | |
// SFSymbol.named("square.slash").image | |
const missingThumbnailStack = itemStack.addStack(); | |
missingThumbnailStack.backgroundColor = PREFS.foregroundTertiary; | |
missingThumbnailStack.cornerRadius = 8; | |
missingThumbnailStack.size = new Size(spacing, spacing); | |
} | |
else { | |
const thumbnailImage = itemStack.addImage(thumbnail); | |
thumbnailImage.cornerRadius = 8; | |
thumbnailImage.applyFillingContentMode(); | |
thumbnailImage.imageSize = new Size(spacing, spacing); | |
} | |
} | |
} | |
async function loadThumbnail(url) { | |
if (url === undefined) { | |
return undefined; | |
} | |
let request = new Request(url); | |
return request.loadImage(); | |
} | |
async function getTopRead(maximum) { | |
const apiUrl = 'https://en.wikipedia.org/api/rest_v1/feed/featured/'; | |
// Get current UTC date | |
const currentDate = new Date().toISOString().split('T')[0].replace(/-/g, '/'); | |
// Append the current date to the API URL | |
const fullApiUrl = `${apiUrl}${currentDate}`; | |
const request = new Request(fullApiUrl); | |
const response = await request.loadJSON(fullApiUrl) | |
const articles = response.mostread.articles; | |
// Sort articles by views in descending order | |
articles.sort((a, b) => b.views - a.views); | |
// Get the top articles up until maximum | |
const topArticles = articles.slice(0, maximum); | |
return topArticles; | |
} | |
function formatNumber(n) { | |
const ranges = [ | |
{ divider: 1e6 , suffix: 'M' }, | |
{ divider: 1e3 , suffix: 'k' } | |
]; | |
for (let i = 0; i < ranges.length; i++) { | |
if (n >= ranges[i].divider) { | |
n = (n / ranges[i].divider); | |
n = Math.round(n * 10) / 10; | |
return n.toString() + ranges[i].suffix; | |
} | |
} | |
return n.toString(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
OUTDATED, see: https://github.com/iiKurt/Wikipedia-Top-Read-Widget