[Scriptable] iOS Wanikani Leeches Widget

        {
            "name": "〜年来",
            "type": "vocabulary",
            "train_type": "meaning",
            "meaning_score": 20,
            "reading_score": 0.004829452884162952,
            "trained_at": "",
            "subject_id": 2829
        },

I take it a high score is bad?
I can never remember what 年来 means for some reason…

Yup, anything over a 1 is considered a leech. I haven’t done any new lessons in a long time because I have hundreds of leeches, so I’m just riding the wave of repeated reviews and trying to study from other sources at the moment.

If you do a GET request to the /user endpoint then you should get back your user profile. You can then PATCH to that same URL to update things. You’ll probably find that the quiz_size is 0, for some reason. I need to dig into why that it is, I came across the same problem last night when I got my local dev env running again (new laptop).

I updated the script to use both the new Shin Wanikani Leech Trainer API as well as the official API for grabbing the meanings/readings.

You can copy and paste it from below, or from the URLs in the initial post (the git repository has been updated, and so those links should have the latest version as well).

Umm, nevermind. There’s a large obvious mistake that needs fixing.

Okay here we go. Should be fixed now.

I updated the script to use both the new Shin Wanikani Leech Trainer API as well as the official API for grabbing the meanings/readings.

You can copy and paste it from below, or from the URLs in the initial post (the git repository has been updated, and so those links should have the latest version as well).

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: pink; icon-glyph: torii-gate;

/* 
  Leech Widget version 0.0.3
  Join the conversation on the Wanikani community forums!
  https://community.wanikani.com/t/scriptable-ios-wanikani-leeches-widget/53682
*/

/*
  You can hard code your API Token here if you'd like, though
  it would probably be better to add your token as the "Parameter"
  in the widget settings.
*/
const apiKey = args.widgetParameter || '';

// Change default font sizes!
const bigSize = 42;
const smallSize = 16;
const spacerSize = 5;

// Change default colors!
const vocabColor = "#aa00fe";
const vocabShadowColor = "#9300dd";
const vocabTextColor = "#ffffff";
const kanjiColor = "#ff01aa";
const kanjiShadowColor = "#dd0093";
const kanjiTextColor = "#ffffff";

/*
  +-------------------------------------------+
  |         ONLY EDIT THE CODE BELOW          |
  |      IF YOU KNOW WHAT YOU'RE DOING!       |
  +-------------------------------------------+
*/

let widget = await createWidget();

if (config.runsInWidget) {
  Script.setWidget(widget);
} else {
  widget.presentSmall();
}
Script.complete();

async function createWidget() {
  if (apiKey === '') {
    return makeApiTokenNotice();
  }

  let r = new Request(`https://wk-leeches-dev.pun7u6s23ol8m.eu-west-2.cs.amazonlightsail.com/leeches`);
  r.headers = { "Authorization": "Bearer " + apiKey };
  const json = await r.loadJSON()

  let listwidget = false
  let iterations = 0;
  do {
    if (iterations >= 1) {
      return makeErrorNotice();
    }

    listwidget = await attemptWidgetCreation(json)
    iterations++
  } while (listwidget === false)

  return listwidget;
}

async function attemptWidgetCreation(json) {
  try {
    let listwidget = new ListWidget();
    listwidget.url = "https://wanikani.com"

    if (json.leeches.length === 0) {
      return makeNoLeechNotice();
    }

    const index = Math.floor(Math.random() * (json.leeches.length - 1));
    const type = json.leeches[index].type

    switch (type) {
      case "kanji":
        listwidget.backgroundColor = new Color(kanjiColor);
        await buildLeechWidget(listwidget, type, json.leeches[index]);
        break;
      case "vocabulary":
        listwidget.backgroundColor = new Color(vocabColor);
        await buildLeechWidget(listwidget, type, json.leeches[index]);
        break;
      default:
    }

    return listwidget
  } catch (e) {
    console.log(e);
    throw e;
    return false;
  }
}

function makeApiTokenNotice() {
  let listwidget = new ListWidget();
  listwidget.backgroundColor = new Color("#ffaa01");

  let notice = listwidget.addText("API Token is not set.");
  notice.centerAlignText();
  notice.font = Font.blackRoundedSystemFont(smallSize)
  notice.textColor = new Color("#ffffff");
  shadow(notice, smallSize, "#dd9200");

  return listwidget;
}

function makeNoLeechNotice() {
  const color = "#008343";
  const shadowColor = "#006333";
  let listwidget = new ListWidget();
  listwidget.backgroundColor = new Color(color);

  let notice = listwidget.addText("Good job genius! ❤︎ You have no leeches!");
  notice.centerAlignText();
  notice.font = Font.blackRoundedSystemFont(smallSize)
  notice.textColor = new Color("#ffffff");
  shadow(notice, smallSize, shadowColor);

  return listwidget;
}

function makeErrorNotice() {
  let listwidget = new ListWidget();
  listwidget.backgroundColor = new Color("#ee2201");

  let notice = listwidget.addText("Oops! Something went wrong...");
  notice.centerAlignText();
  notice.font = Font.blackRoundedSystemFont(smallSize)
  notice.textColor = new Color("#ffffff");
  shadow(notice, smallSize, "#922201");

  return listwidget;
}

async function buildLeechWidget(listwidget, type, json) {
  const subject_id = json.subject_id
  const { reading, meaning } = await getSubjectInfo(type, subject_id);
  let textColor = vocabTextColor;
  let shadowColor = vocabShadowColor;
  if (type === "kanji") {
    textColor = kanjiTextColor;
    shadowColor = kanjiShadowColor;
  }

  let fSize = bigSize;
  if (json.name.length !== 1) fSize = bigSize - ((json.name.length) * 4);
  if (fSize < smallSize) fSize = smallSize;
  let target = listwidget.addText(json.name);
  target.centerAlignText();
  target.font = Font.blackRoundedSystemFont(fSize)
  target.textColor = new Color(textColor);
  shadow(target, fSize, shadowColor);

  listwidget.addSpacer(spacerSize);

  let rt = listwidget.addText(reading);
  rt.centerAlignText();
  rt.font = Font.lightSystemFont(smallSize);
  rt.textColor = new Color(textColor);
  let mt = listwidget.addText(meaning);
  mt.centerAlignText();
  mt.font = Font.lightSystemFont(smallSize);
  mt.textColor = new Color(textColor);
  shadow(mt, smallSize, shadowColor);
  shadow(rt, smallSize, shadowColor);
}

async function getSubjectInfo(type, subject_id) {
  const url = 'https://api.wanikani.com/v2/subjects/' + subject_id;

  let r = new Request(url);
  r.headers = { "Authorization": "Bearer " + apiKey };
  const result = await r.loadJSON()

  const meaning = result.data.meanings.filter(a => a.primary)[0].meaning;
  const reading = result.data.readings.filter(a => a.primary)[0].reading;

  return {
    meaning,
    reading
  }
}

function shadow(target, size, color) {
  p = size * (3 / 32);

  target.shadowColor = new Color(color);
  target.shadowOffset = new Point(p, p);
  target.shadowRadius = 0.1;
}
2 Likes

Readings and meanings are now returned alongside the lessons.

1 Like

Awesome I’ll give it a look later.

This is just wonderful, thank you! I just added it to my phone and I love it!

I love this! WaniKani and Scriptable are a match made in heaven.

Sorry to double-post but I just started getting this error today:

What can I do to fix it?

The most likely culprit is an incorrectly entered API token. Can you double check that the token is valid and that it has been entered correctly?

For example, perhaps an unnecessary space was copied in the end or something.

I double-checked it and it’s all correct. Am I the only one having this issue?

Seems to still work well on my end.

It randomly started working again without me touching it. :thinking: Well, all’s well that ends well. :slight_smile:

1 Like

I’m glad things worked out for you.

I really wish we could’ve found the cause though.

I’m getting this error now too. I checked the API key and it’s all good. I’ll let it sit and hopefully corrects itself?

Just an update that it suddenly started working a few minutes ago with no changes from me since my last comment. :smiley:

@MichaelCharles Are you hitting the leeches API that’s hosted on Lightsail? If so, would you mind swapping out the server for a new one at https://wk-leeches.herokuapp.com, please? There’s been a lot of updates over the last couple of weeks, and I’m moving it all back to heroku to cut down on monthy bills. Thanks!

Same endpoint, just different domain?

I can update the script. Unfortunately one downside to Scriptable is there’s no way to like… deploy an update. I can change the script that’s on Github though. I’ll give it a thorough look later when I get the chance. Thanks for the heads up.

That’s fine. I figure that so long as an update is available Ihen I can turn off the old one, people will come here to post a bug, and hopefully learn about the update.