Commit 936cc9a1 authored by Vildravn's avatar Vildravn
Browse files

Initial commit

parents
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2020-12-10
### Added
* Initial version of the Today in WoW widget script.
* The widget displays:
* Three current Shadowlands Covenant calling quests
* The two active Torghast wings
* Current weekly Mythic+ affixes
* The current weekly Shadowlands world boss
* scriptable-require.js dependency
\ No newline at end of file
# Today in WoW - Widget for Scriptable for iOS
This is a script for [Scriptable](https://scriptable.app) for iOS that parses data from Wowhead's Today in WoW section and displays it on the home screen in a configurable widget.
* The widget displays:
* Three current Shadowlands Covenant calling quests
* The two active Torghast wings
* Current weekly Mythic+ affixes
* The current weekly Shadowlands world boss
The affix icons can be tapped to open the browser to read more information.
## Installation
1. Install [Scriptable](https://apps.apple.com/us/app/scriptable/id1405459188?uo=4)
1. Place `Today in WoW.js` and `scriptable-require.js` in the `Scriptable` folder on your iCloud Drive
1. (Optional) Edit the `Today in WoW.js` script to configure it (See the Configuration section below)
1. Add a Scriptable widget to the home screen and while still in edit mode, tap on the widget to configure it
1. In the widget configuration, select the `Today in WoW` script, the `When interacting` and `Parameter` fields aren't important
1. Tap away from the widget configuration modal
## Configuration
Edit the `Today in WoW.js` script to adjust the configuration of the widget. Currently supports region and faction, and defaults to EU Horde.
\ No newline at end of file
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-gray; icon-glyph: calendar-alt;
/*
Today in WoW - iOS Scriptable app widget that parses information from Wowhead's Today in WoW section
This widget requires 'scriptable-require.js' from the same repository to be next to the widget script itself.
The widget will also pull cheerio.js.
Scroll down for configuration, by default the widget is configured to pull information for EU Horde.
Version: 1.0.0
Get latest updates at: https://gitlab.com/vildravn/today-in-wow-widget
In case something breaks, open an issue at: https://gitlab.com/vildravn/today-in-wow-widget/-/issues
*/
/*
Widget Configuration
REGION - The region you want to show data of; Accepts 'EU' or 'US'
FACTION - What faction's data you want to show; Accepts 'alliance' or 'horde'
*/
const REGION = 'EU';
const FACTION = 'horde';
/*
Colors
*/
const colors = {
bg: new Color('#1C1C1C'),
header: new Color('#FFFFFF'),
textYellow: new Color('#FFD100'),
textGrey: new Color('#BBBBBB'),
faction: {
alliance: new Color('#4d6699'),
aorde: new Color('#8a2e2e'),
both: new Color('#333333')
}
}
/*
Text Style
*/
const textStyles = {
header: {
font: new Font('HelveticaNeue-Bold', 12),
color: colors.header
},
line: {
font: new Font('HelveticaNeue', 12),
color: colors.textYellow
},
lineBold: {
font: new Font('HelveticaNeue-Bold', 9),
color: colors.textGrey
}
}
// -----------------------------
const require = importModule('scriptable-require');
const cheerio = await require('cheerio', false);
if (config.runsInWidget) {
const size = config.widgetFamily;
const widget = await createWidget(size);
Script.setWidget(widget);
Script.complete();
} else {
// For debugging
//const size = 'small';
const size = 'medium'
//const size = 'large'
const widget = await createWidget(size);
if (size == 'small') {
widget.presentSmall();
} else if (size == 'medium') {
widget.presentMedium();
} else {
widget.presentLarge();
}
Script.complete();
}
async function createWidget(size) {
const widget = new ListWidget();
widget.setPadding(15, 15, 15, 15);
const whData = await fetchData("https://www.wowhead.com", "loadString");
const $ = cheerio.load(whData);
const callings = await parseTextGroup($, `#${REGION}-group-calling-quests`, `.tiw-region[data-region="${REGION}"]`);
const torghast = await parseTextGroup($, `#${REGION}-group-torghast-wings`, `.tiw-region[data-region="${REGION}"]`);
const affixes = await parseImageGroup($, `#${REGION}-group-mythicaffix`, `.tiw-region[data-region="${REGION}"]`);
const worldBoss = await parseTextGroup($, `#${REGION}-group-epiceliteworldsl`, `.tiw-region[data-region="${REGION}"]`);
widget.backgroundColor = colors.bg;
if (size == 'small') {
await buildTextDisplay(widget, "calling quest", callings);
widget.url = 'https://www.wowhead.com/#tiw-switcher-region';
}
else if (size == 'medium') {
const wrapperStack = widget.addStack();
wrapperStack.layoutHorizontally();
widget.addSpacer();
const leftColumn = wrapperStack.addStack();
leftColumn.layoutVertically();
await buildTextDisplay(leftColumn, "calling quests", callings);
leftColumn.addSpacer(20);
await buildTextDisplay(leftColumn, "torghast wings", torghast);
wrapperStack.addSpacer();
const rightColumn = wrapperStack.addStack();
rightColumn.layoutVertically();
await buildAffixDisplay(rightColumn, affixes);
rightColumn.addSpacer(20);
await buildTextDisplay(rightColumn, "world boss", worldBoss);
} else {
widget.setPadding(5, 0, 5, 0);
}
return widget;
}
async function buildAffixDisplay(parent, affixes) {
const levels = ["2+", "4+", "7+", "10+"];
for (let i = 0; i < affixes.length; i++) {
let affix = affixes[i];
affix.text = levels[i];
}
await buildImageDisplay(parent, "mythic+ affixes", affixes);
}
async function buildTextDisplay(parent, heading, data) {
const contentStack = parent.addStack();
contentStack.layoutVertically();
const header = contentStack.addText(heading.toUpperCase());
header.textColor = textStyles.header.color;
header.font = textStyles.header.font;
for (const item of data) {
const line = contentStack.addText(item);
line.textColor = textStyles.line.color;
line.font = textStyles.line.font;
}
}
async function buildImageDisplay(parent, heading, data) {
const contentStack = parent.addStack();
contentStack.layoutVertically();
const header = contentStack.addText(heading.toUpperCase());
header.textColor = textStyles.header.color;
header.font = textStyles.header.font;
const horizontalStack = contentStack.addStack();
horizontalStack.layoutHorizontally();
for (const item of data) {
const image = await fetchData(item.imageUrl, 'loadImage');
horizontalStack.addSpacer(2);
buildImageItem(horizontalStack, image, item);
horizontalStack.addSpacer(2);
}
}
function buildImageItem(parent, image, item) {
const contentStack = parent.addStack();
contentStack.layoutVertically()
const icon = contentStack.addImage(image);
icon.borderWidth = 4;
icon.cornerRadius = 4;
icon.imageSize = new Size(34, 34);
icon.borderColor = colors.faction[item.side || "both"];
if (item.urlFragment) {
icon.url = `https://wowhead.com${item.urlFragment}`;
}
if (item.text) {
const textStack = contentStack.addStack();
textStack.layoutHorizontally();
textStack.size = new Size(34, 0)
textStack.addSpacer();
const line = textStack.addText(item.text);
line.textColor = textStyles.lineBold.color;
line.font = textStyles.lineBold.font;
textStack.addSpacer();
}
}
async function parseImageGroup($, widgetSelector, context, side = "both") {
var returnData = [];
const widget = $(widgetSelector, context)[0];
if (!widget)
return null;
const widgetLines = $('.tiw-line-name', widget);
const widgetLineCount = $(widgetLines).length;
for (let i = 0; i < widgetLineCount; i++) {
let faction = $(widgetLines)[i].attribs['data-side'];
if (faction && faction != "both" && faction != side)
continue;
if (!faction)
faction = 'both';
const lineLink = $('a', widgetLines[i]);
const urlFragment = lineLink[0].attribs.href;
const imageUrl = $('img', lineLink[0])[0].attribs.src;
returnData.push({ imageUrl, urlFragment, faction });
}
return returnData;
}
async function parseTextGroup($, widgetSelector, context, side = "both") {
var returnData = [];
const widget = $(widgetSelector, context)[0];
if (!widget)
return null;
const widgetLines = $(`.tiw-line-name`, widget);
const widgetLineCount = $(widgetLines).length;
for (let i = 0; i < widgetLineCount; i++) {
let faction = $(widgetLines)[i].attribs['data-side'];
if (faction && faction != "both" && faction != side)
continue;
if (!faction)
faction = 'both';
returnData.push($(widgetLines[i]).text());
}
return returnData;
}
async function fetchData(url, type = 'loadJSON') {
const req = new Request(url);
const resp = await req[type]();
return resp;
}
\ No newline at end of file
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: red; icon-glyph: magic;
// I forgot where I found this.
// If you made this, let me know so I can at least credit your here.
// https://gist.github.com/supermamon/d66ec2357598a948d1561ac9e2aaa0c5
const fm = FileManager.iCloud();
const dir = fm.documentsDirectory();
const downloadIfNeeded = async (pkg, isAutoUpdateOn) => {
let name = getPackageName(pkg);
let filePath = fm.joinPath(dir, name + '.js');
let isInstalled = await isFound(filePath);
// If the package exists and autoupdate is off, stop checking further
if (isInstalled && !isAutoUpdateOn) {
console.log(`'${name}' is already installed, and autoupdate is disabled! Proceeding to import from disk...`);
return;
}
// Get the package information which satisfies the given semver range
let versionInfo = await getStatus(pkg);
let versions = versionInfo.satisfied;
let version = versionInfo.highest;
// Download the newer version if necessary
if (isInstalled && isAutoUpdateOn) {
let installedVersion = await getInstalledVersion(name);
// Check if the installed version satisfies the semver range
if (versions.includes(installedVersion)) {
console.log(`'${name}@${installedVersion}' satisfies the requested version. Good to go!`);
return;
} else {
console.log(`'${name}@${installedVersion}' doesn't match the version requested. Reinstalling '${version}' now...`);
}
} else {
console.log(`'${name}' was never installed previously. Downloading now...`);
}
// Download the package source and save to disk
let source = await getPackageSource(pkg);
savePackageToDisk(name, version, source);
};
const getInstalledVersion = async name => {
// Read the version from {package}.ver
let filePath = fm.joinPath(dir, name + '.ver');
let version;
if (isFound(filePath)) {
let content = fm.readString(filePath);
if (/^\d+\.\d+\.\d+$/g.test(content)) {
version = content;
}
}
console.log(`The installed version of '${name}' is ${version}.`);
return version;
};
const getPackageSource = async pkg => {
// Get the standalone package source from wzrd.in
let request = new Request(`https://wzrd.in/standalone/${encodeURIComponent(pkg)}`);
let response = await request.loadString();
return response;
};
const getPackageName = pkg => {
return pkg.split('@')[0];
};
const getStatus = async pkg => {
// Retrieve the information about the package
let request = new Request(`https://wzrd.in/status/${encodeURIComponent(pkg)}`);
let response = await request.loadJSON();
// Fail if the response is not good
if (response.statusCode >= 400 || response.ok === false) {
throw response.message;
}
// Fail if the semver did not satisfy any versions available on npm
// Otherwise, sort the versions in descending order
let versions = response.builds && Object.keys(response.builds);
if (versions.length < 1) {
throw `'${pkg}' did not satisfy any versions available on npm!`;
} else {
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
}
// Get all the satisfied versions and the highest version
let result = {
highest: versions[0],
satisfied: versions,
};
return result;
};
const isFound = async filePath => {
// Check if the package is already downloaded
if (fm.fileExists(filePath)) {
return true;
}
// Sync with iCloud and check again
await syncFileWithiCloud(filePath);
if (fm.fileExists(filePath)) {
return true;
}
return false;
};
const savePackageToDisk = (name, version, source) => {
// Write the package source and version info to disk
let filename = fm.joinPath(dir, name);
let jsFilePath = filename + '.js';
let versionFilePath = filename + '.ver';
let pkg = `${name}@${version}`;
tryWriteFile(jsFilePath, source, pkg);
tryWriteFile(versionFilePath, version, pkg);
console.log(`Successfully installed ${name}@${version}!`);
};
const syncFileWithiCloud = async filePath => {
// Try to sync with iCloud in case the package exists only on iCloud
try {
console.log(`Attempting to sync with iCloud just in case...`);
await fm.downloadFileFromiCloud(filePath);
console.log(`Finished syncing ${filePath}`);
} catch (err) {
console.log(`${filePath} does not exist on iCloud.`);
}
};
const tryWriteFile = (path, content, pkg) => {
// Sometimes wzrd.in is acting up and the file content is undefined.
// So, here is a little trick to let you know what's going on.
try {
console.log(`Saving ${pkg} to disk at ${path}...`);
fm.writeString(path, content);
} catch (err) {
throw `The package source from 'https://wzrd.in/standalone/${pkg}' is probably corrupted! Try with the different patch version.`;
}
};
module.exports = async (pkg, isAutoUpdateOn = false) => {
let name = getPackageName(pkg);
await downloadIfNeeded(pkg, isAutoUpdateOn);
return importModule(`${name}`);
};
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment