Ssieth / Storium Improver

// ==UserScript==
// @name        Storium Improver
// @namespace   4faa34747d826bd32beeeaae92bffb79
// @description Storium Page Improvements
// @include     https://storium.com/*
// @resource	  CSS_JUI https://code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css
// @resource	  CSS_Icons https://fonts.googleapis.com/icon?family=Material+Icons
// @resource    icon_email https://raw.githubusercontent.com/google/material-design-icons/master/png/communication/email/materialiconsround/48dp/2x/round_email_black_48dp.png
// @resource    icon_forums https://raw.githubusercontent.com/google/material-design-icons/master/png/communication/forum/materialiconsround/48dp/2x/round_forum_black_48dp.png
// @version     3.26.0
// @require     https://code.jquery.com/jquery-3.4.1.min.js
// @require     https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
// @require		  https://cabbit.org.uk/scripts/jquery.mark.min.js
// @require     https://cdn.jsdelivr.net/npm/marked/marked.min.js
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_xmlhttpRequest
// @grant       GM_setClipboard
// @grant       GM_getResourceURL
// @connect     cabbit.org.uk
// @oujs:author Ssieth
// @copyright   2022, Ssieth
// @license     GPL-3.0-or-later
// ==/UserScript==

/* Some stuff for JSHint */
/* globals $ */

/* Get this as early as possible to avoid rewrites of the url from storiums end stealing stuff */
var fullURL = window.location.href;

/* Version Info */
var scriptVerShort = GM_info.script.version;
var scriptVer = "script-name=ssi-si&script-ver=" + scriptVerShort;
// v3.26.0  Added support for settings tab
// v3.25.1  Profile page settings available
// v3.25.0  Bug fixes and imrpovements on profile page.
// v3.24.2  Replaced missing notification icons for forums and private messages
// v3.24.1  Improved speech styling a little.
// v3.24.0  Improved speech styling for multi-paragraph quotations
// v3.23.1  Tweak to settings - some now have larger textboxes
// v3.23.0  Add markdown to comments
// v3.22.1  Fixed some image search options, removed Yandex because Russia
// v3.21.2  Tweaked default settings for bookmarks.  Made them more rainbow-y
// v3.21.1  Slight bugfix around some timing issues in bookmarks
// v3.21.0  Gave Mark Last Read feature it's own config section in settings
// v3.20.4  More performance improvements.
// v3.20.3  Improved performance with comments box typing.
// v3.20.2  Fixed a bug impacting performance when typing a move.
// v3.20.1  First attempt at adding update support for reading marks.
// v3.20.0  First version of the moves reading mark
// v3.19.1  Sort options for default homepage style
// v3.19.0  Sort options for compact and semi-compact homepage styles
// v3.18.1  Corrected direct post link counting
// v3.18.0  Added export settings option.
//          Add option to link to individual posts.
// v3.17.3  Added option to change the colour of the comment refresh icon
// v3.17.2  Some tweaks to the auto-mege functionality
// v3.17.1  Changed forum thread for script udpates
// v3.17.0  Added option automatically click the box that tells storium to show you new moves on a game page.  Setting is on by default.
// v3.16.0  Added option to not condense notification lists.
// v3.15.0  Added an option to style though (text between tildas)
// v3.14.0  Rainbow option for active character marker.
// v3.12.0  Added tags to card lists for games.
// v3.11.1  Minor fix for loading CSS earlier
// v3.11.0  Added settings (with some default values) to game page settings that highlights links in game moves rather more than they are currently. By default the links are red and underlined, changing to blue when hovered over.
// v3.10.0  Clicking the maintenance warning makes it go away (until you refresh the page or visit a new one)
// v3.9.1   Fix for FF update
// v3.9.0   Added ability to click on images in cards to see a full size version
// v3.8.0   Added scroll to last entry for game page.
// v3.6.1   Added character notes
// v3.6.0   Added game notes
// v3.5.5   Improvde notifications page - improving identification of games
// v3.5.4   Slight improvements to logging
// v3.5.3   Cloud storage fixed (I hope)
// v3.5.1   Cloud storage disabled until I can debug it properly
// v3.5.0   Created basic functionality to store config settings in the cloud
// v3.4.0   Improvde notifications page adding some icons for forums and private messages and improving identification of games
// v3.3.0   Completed work started in 3.0.0, moving stored variables into the new config object.
// v3.2.1   Fixed spelling of "avetars" on settings page because my SO noticed the typo and was giving me ear-ache
// v3.2.0   Lexical diversity measure now changed from TTR to MATTR (https://quanteda.io/reference/textstat_lexdiv.html has a goood definition).  Window is set to 150 words
// v3.1.2   Image search now enabled on game edit card dialogues
// v3.1.1   Added unsplash to image searches in wild cards
// v3.1.0   Added image search to wild card creation in-game
// v3.0.6	Various bits of denbugging and tidy-up including:
//				Standardising on . notation for object properties rather than [" "] notation.
//				Tidying up potential duplicate variable declarations.
//				Tidying up undeclard local variables.
//				Tidying up double spaces where one is sufficient.
// v3.0.0   Replaced settings stuff with custom script rather than gm_config.
// Older version info at the end of the script now
/* - - */

/* Variables you can alter */
var blnColourPreview = true;
var blnShowExtraLinks = false;
var blnRemoveUpgradeLink = true;
var dispTimer = $("<div style='position: fixed; left: 5px; top: 75px; z-index: 999;'></div>");
var blnSnippets = true;
var config = {};
config.gameLastReads = {};
config.exNotes = {};
var config_display = {};
var blnUseCloud = GM_getValue("useCloud",false);
/* - - */

var lastAutoMerge = (new Date()).getTime();

/* What we're sending to console.log */
var logPerm = {};
logPerm.pageType = true;
logPerm.lfp = false;
logPerm.delay = true;
logPerm.error = false;
logPerm.scriptStart = false;
logPerm.scriptEnd = false;
logPerm.scriptVersion = true;
logPerm.atest = false;
logPerm.gameChars = false;
logPerm.exNote = false;
logPerm.sprefs = false;
logPerm.user = false;
logPerm.cloud = true;

log('Script start @ ' + new Date() + ' : ' + scriptVer,'scriptStart');
log(scriptVerShort,'scriptVersion');
/* - - */

/* Card Icons etc */
var imgs = {};
imgs.strength = 'https://d9wc4crcmzwt0.cloudfront.net/assets/cards-v2/card-type-icon-strength-ff0c3099b718420b2e4ec6ede7286cf33fcddd7e1efc7c93f536a5d855c0bd25.png';
imgs.weakness = 'https://d9wc4crcmzwt0.cloudfront.net/assets/cards-v2/card-type-icon-weakness-823ef4b5668e7f9c2c122e42425e97c0b4baa1fd5b884a65b17111984523f0a1.png';
imgs.subplot = 'https://d9wc4crcmzwt0.cloudfront.net/assets/cards-v2/card-type-icon-subplot-db010c67fd03ab68d68264115474eaafd8362f3dec79835950d41819903c19c4.png';
imgs.goal = '';
imgs.thing = '';

/* Loading delays for pages to allow for full loading of dynamic content */
var pageDelay = {};
pageDelay.game = 200;
pageDelay.character = 200;
pageDelay.home = 200;
pageDelay['forum-category'] = 200;
pageDelay['forum-thread'] = 200;
var pageDelayWaitFor = {};
pageDelayWaitFor.game = ".cast-characters";
pageDelayWaitFor.character = "div.card";
pageDelayWaitFor.home = ".feed-item";
pageDelayWaitFor['forum-category'] = "table.forum-threads";
pageDelayWaitFor['forum-thread'] = "p.by-line";
/* - - */

/* Some CSS Styling Suff */
var strCSSFakeLinks = ".fakelink {color:#086a87 !important;text-decoration:underline !important} .fakelink:hover, .fakelink:active {color:#04313f !important;text-decoration:underline !important; cursor: pointer !important; cursor: hand !important; } ";
var strCSSFakeLinksNoCol = ".fakelinkNoCol {cursor: pointer !important;} .fakelinkNoCol:hover, .fakelinkNoCol:active {color:#04313f !important;text-decoration:underline !important; cursor: pointer !important; cursor: hand !important; } ";
var strCSSAlert = ".noteAlert {color:red !important;} ";
var strCSSOddRow = ".oddRow {background-color: #eaeaea;} .oddRow2 {background-color: aliceblue;} ";
var strCSSGram = '';
var strCSSExtras = [
        ".wider-main .centered-main {width: auto; }",
		".censored {color: black; background-color: black;}",
        "div.gr_ver_2 {top: auto !important;}",
        ".ui-front {z-index: 999 !important;}",
        //"div.cast-characters {top: 80px !important; }",
        ""
    ].join('\n') + '\n';
var strCSSSnippetsMenu = [
        "ul.snip_menu, li.snip_menu { margin: 0; padding: 0; list-style: none; }",
        "img.snip_menu {}",
        "ul.snip_menu { display: none; border: 1px solid #1c1c1c; }",
        "ul.snip_menu li { background-color: light-gray; border: 1px solid gray; }",
        "ul.snip_menu li a {text-decoration:none; padding:10px; display:block; }",
        "ul.snip_menu li a:hover {padding:10px; font-weight:bold; color: #F00880; }"
    ].join('\n') + '\n';
var fontSubst = {};

var strCSSImageClean = [
        "  .face:hover > .type { display: none; }"
    ].join('\n') + '\n';

let strCSSProfile = [
      " .profile-table tr td:first-child { width: 7rem; }"
    ].join('\n') + '\n';

let strCSSProfileWide = [
      " .main-width, .main-width-if-narrow { width: auto; }",
      " .centered-main, .centered-main-if-narrow { width: auto; }"
    ].join('\n') + '\n';


var strCSSHomeGrid = [
        "#page {max-width: initial; }",
        "#page div.main { display: grid ; grid-template-columns: auto; grid-template-rows: auto; grid-template-areas: 'side1' 'middle' 'side2'; }",
        "#page > div.main > div {width: auto; float: none; margin: 0px; }",
        "#page > div.main > div.intro { grid-area: side1 }",
        "#page > div.main > div.game-list { grid-area: middle; max-width: 650px; justify-self: center; }",
        "#page > div.main > div.gm-rightcol { grid-area: side2 }",
        "@media only screen and (min-width: 1100px) {",
        "  #page div.main { display: grid ; grid-template-columns: auto 650px; grid-template-rows: auto; grid-template-areas: 'side1 middle' 'side2 middle'; grid-column-gap: 20px;}",
        "}",
        "@media only screen and (min-width: 1350px) {",
        "  #page div.main { display: grid ; grid-template-columns: auto 650px 320px; grid-template-rows: auto; grid-template-areas: 'side1 middle side2'; grid-column-gap: 10px;}",
        "}",
        "@media only screen and (min-width: 1500px) {",
        "  #page div.main { display: grid ; grid-template-columns: minmax(350px,600px) minmax(650px,auto) 350px; grid-template-rows: auto; grid-template-areas: 'side1 middle side2'; grid-column-gap: 20px;}",
        "}",
        "#header .header-stack > .content { max-width: initial; }"
    ].join('\n') + '\n';

fontSubst['Source Sans Pro'] = [
"@font-face {",
"  font-family: 'Quattrocento';",
"  font-style: normal;",
"  font-weight: 400;",
"  src: local('Quattrocento'), url(https://fonts.gstatic.com/s/quattrocento/v7/WZDISdyil4HsmirlOdBRFOgdm0LZdjqr5-oayXSOefg.woff2) format('woff2');",
"}",
"@font-face {",
"  font-family: 'Quattrocento';",
"  font-style: normal;",
"  font-weight: 700;",
"  src: local('Quattrocento Bold'), local('Quattrocento-Bold'), url(https://fonts.gstatic.com/s/quattrocento/v7/Uvi-cRwyvqFpl9j3oT2mqnNuWYKPzoeKl5tYj8yhly0.woff2) format('woff2');",
"}",
"@font-face {",
"  font-family: 'Merriweather';",
"  font-style: normal;",
"  font-weight: 300;",
"  src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(https://fonts.gstatic.com/s/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGOode0-EuMkY--TSyExeINg.woff2) format('woff2');",
"}",
"@font-face {",
"  font-family: 'Merriweather';",
"  font-style: normal;",
"  font-weight: 700;",
"  src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(https://fonts.gstatic.com/s/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGEo0As1BFRXtCDhS66znb_k.woff2) format('woff2');",
"}",
"@font-face {",
"  font-family: 'Merriweather';",
"  font-style: italic;",
"  font-weight: 300;",
"  src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightIt'), url(https://fonts.gstatic.com/s/sourcesanspro/v9/fpTVHK8qsXbIeTHTrnQH6MAjkyiewWYrWZc50I8hK7I.woff2) format('woff2');",
"}",
"@font-face {",
"  font-family: 'Merriweather';",
"  font-style: italic;",
"  font-weight: 700;",
"  src: local('Source Sans Pro Bold Italic'), local('SourceSansPro-BoldIt'), url(https://fonts.gstatic.com/s/sourcesanspro/v9/fpTVHK8qsXbIeTHTrnQH6Nnl6YROR5rHLkdLoHwoOWA.woff2) format('woff2');",
"}"].join('\n') + '\n';

fontSubst.Roboto = [
"@font-face {",
"  font-family: 'Quattrocento';",
"  font-style: normal;",
"  font-weight: 400;",
"  src: local('Quattrocento'), url(https://fonts.gstatic.com/s/quattrocento/v7/WZDISdyil4HsmirlOdBRFOgdm0LZdjqr5-oayXSOefg.woff2) format('woff2');",
"}",
"@font-face {",
"  font-family: 'Quattrocento';",
"  font-style: normal;",
"  font-weight: 700;",
"  src: local('Quattrocento Bold'), local('Quattrocento-Bold'), url(https://fonts.gstatic.com/s/quattrocento/v7/Uvi-cRwyvqFpl9j3oT2mqnNuWYKPzoeKl5tYj8yhly0.woff2) format('woff2');",
"}",
"@font-face {",
"  font-family: 'Merriweather';",
"  font-style: normal;",
"  font-weight: 300;",
"  src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/Hgo13k-tfSpn0qi1SFdUfVtXRa8TVwTICgirnJhmVJw.woff2) format('woff2');",
"}",
"@font-face {",
"  font-family: 'Merriweather';",
"  font-style: normal;",
"  font-weight: 700;",
"  src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/d-6IYplOFocCacKzxwXSOFtXRa8TVwTICgirnJhmVJw.woff2) format('woff2');",
"}",
"@font-face {",
"  font-family: 'Merriweather';",
"  font-style: italic;",
"  font-weight: 300;",
"  src: local('Roboto Light Italic'), local('Roboto-LightItalic'), url(https://fonts.gstatic.com/s/roboto/v15/7m8l7TlFO-S3VkhHuR0at44P5ICox8Kq3LLUNMylGO4.woff2) format('woff2');",
"}",
"@font-face {",
"  font-family: 'Merriweather';",
"  font-style: italic;",
"  font-weight: 700;",
"  src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(https://fonts.gstatic.com/s/roboto/v15/t6Nd4cfPRhZP44Q5QAjcC44P5ICox8Kq3LLUNMylGO4.woff2) format('woff2');",
"}"].join('\n') + '\n';

var fontSubstAvailable = ['None'];
for (var key in fontSubst) {
    fontSubstAvailable.push(key);
}

var styles = {};

styles.Dark = [
"body {",
"  background: black;",
"  color: white;",
"}",
".nu_banner > .details > .slide-wrapper > .details-content .attributes-item, .nu_banner > .details > .details-content .attributes-item,",
".card, .stack-size span.stack-bubble, .nu_banner > .details > .callout, .activity-summary table, table.forum-threads tr:nth-child(2n),",
".ngpopover {",
"background-color: #323232;",
"}",
".oddRow {",
"background-color: #323232 !important;",
"}",
"a.flip-button img {",
"-webkit-filter: invert(100%); filter: invert(100%);",
"}",
"#logo-name img {",
"-webkit-filter: invert(100%); filter: invert(100%);",
"}",
".entry-move .l-content, .nu_banner > .details, .character-popover, .character-popover .cp-name, .cast-character .bubble, .entry-move .l-tab,",
".entry-refresh .bubble, .entry-subplot .bubble, .narrator-shift .bubble, .action-box .l-content, .action-box .l-tab, .activity-summary,",
".lbox-entry-edit-form-wrapper .l-tab, .lbox-entry-edit-form-wrapper .entry-edit-form, .forum-post h5.thread-title, .well, .stacked-card,",
".card-stack-inner-2, .presence-toast {",
"background-color: #424242;",
"}",
".card .title {",
"color: #828282;",
"}",
".nu_banner > .details > .slide-wrapper > .details-content .nsfw-status .is-nsfw, .nu_banner > .details > .details-content .nsfw-status .is-nsfw,",
".stacked-card .title {",
"color: #fff;",
"}",
"div.cast-character .ngpopover .callout {",
"border-color: transparent rgba(207, 70, 70, 0.7) transparent transparent !important;",
"}",
"#header-outer {",
"background-color: #424242 !important;",
"}",
".cast-character .bubble.acting-as {",
"box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.3), 0px 0px 3px 3px rgba(0, 48, 255, 0.4) inset;",
"}",
"a:hover, #header-outer #header .header-nav .header-dropdown:hover {",
"color: #AA0000;",
"}"].join('\n') + '\n';

var cssRainbowActiveChar = ".cast-characters .cast-character .bubble.acting-as, .cast-characters .cast-character .bubble.acting-as.overrun { box-shadow: 0 0 0 1px red, 0 0 0 3px orange, 0 0 0 4px yellow, 0 0 0 6px green, 0 0 0 7px blue, 0 0 0 8px indigo, 0 0 0 10px violet; margin-top: 10px; margin-bottom: 15px !important}";

var stylesAvailable = ['None'];
for (var styleKey in styles) {
    stylesAvailable.push(styleKey);
}
/* -- */

/* Check if the browser is chrome-based */
var isChrome = /chrom(e|ium)/.test(navigator.userAgent.toLowerCase());

/* Locations for game info insert */
var $lastNote = {};
var $lastMove = {};

/* The page type that we're processing */
var pageType = "";

/* Don't alter these */
var gamePid = "";
var intNoteCount = 0;
var games = {};
var tasks =[];
var intNotes = 0;
var now = new Date();
var counter;
var pageLoadTime = new Date();
var lastCheck;
var blnPreviewHidden = false;
var usr;
var blAddingCardTags = false;
// From game page:
var chars = {}
var charPosts = {};
var scenes = [];
var gameScope = {};
var gamePageScope = {};
var gm_data = {};
var challenges = {}; /* Structure: challenges->challenge->cards->card->playedBy */
var users = {};
var adminUsers = ['ssieth'];
var chars_urls = {};
var intChars = 0;
var dispActiveChars;
var strModal = '<div id="charpop" title="title"></div>'
var refresh=0;
/* --- sPrefs --- */
var sprefs_prefs = {};
sprefs_prefs.status = "not loaded";
sprefs_prefs.data = {};
/* --- Looking for Players --- */
var strFirstLFP = "";
var lfpGamesNew = {};
var observerMWC = {};
var userNoteTypes = ['chat','flag','star','favorite','bookmark','warning','speaker_notes','music_note','thumb_up','thumb_down','bookmark_border','swap_vert','access_time','security','brightness_4'];
var intLFPCount=0;
var intMainRow=0; // Main row in notifications page table
var eventsData = {};
var searchData = [];
var lastSearchData;
var hashText = decodeURIComponent(window.location.hash.substring(1));
var taskPriorties = ["Need to act","Should act","Can act","Check"];

var processedEntries = []; // Which elements have already been processed

var charsForSlack = ""; // Characters for slack stuff
var objCharsForSlack = {};
var objTrello = false;
var strTrelloBoard = "";
var bookmarkCount = 0;
var lastSetReadMoves = 0;
var intCommentCounter = -1;

var strLastTab = "";

function setUser() {
    var scope = getScope();
    usr = {}
    if (scope && scope.authUser) {
        $.extend(usr,scope.authUser);
        usr.id = usr.slug;
        log(usr,"user");
    } else {
        log("Failed to get user by method 1, trying method 2","user");
        var $user = $("div#header").find("div.header-dropdown ul li:eq(1) a");
        if ($user.length > 0) {
            var aryPath = $user.attr("href").split("/");
            usr.id = aryPath[aryPath.length-1];
            log(usr,"user");
        } else {
            log("Failed to get user by method 2","user");
            usr.id = "unknown";
        }
    }
    usr.admin = adminUsers.includes(usr.id.toLowerCase());
}

setUser();

function log(logWhat,logType) {
    var caller = "";
    var strNow = (new Date()).toLocaleTimeString();
    if (logPerm[logType]) {
        if (log.caller !== undefined && log.caller !== null && log.caller.name !== undefined) {
            caller = ":" + log.caller.name;
        }
        if (typeof logWhat === "string") {
            console.log(strNow + ":" + logType + caller + ": " + logWhat);
        } else {
            console.log(":: " + strNow + ":" + logType + caller + " ::");
            console.log(logWhat);
        }
    }
}
/*
    The following function injects code into the parent page.
*/
function inject(source) {
  // Check for function input.
  if ('function' == typeof source) {
    // Execute this function with no arguments, by adding parentheses.
    // One set around the function, required for valid syntax, and a
    // second empty set calls the surrounded function.
    source = '(' + source + ')();'
  }

  // Create a script node holding this  source code.
  var script = document.createElement('script');
  script.setAttribute("type", "application/javascript");
  script.textContent = source;

  // Insert the script node into the page, so it will run, and immediately
  // remove it to clean up.
  document.body.appendChild(script);
  document.body.removeChild(script);
}

// Average an array
function getAvg(ary) {
  const total = ary.reduce((acc, c) => acc + c, 0);
  return total / ary.length;
}

/* The following function returns an object representing lexical diversity information for a given text */
function lexDiv(strText) {
	var window = 150;
    var regex = /\s+/gi;
    var aryWords = strText.trim().replace(regex, ' ').split(' ');
    var wordCount = aryWords.length;
    var objOut = {};
	var divs = [];

    /* Load the words into an object array with usage counts */
	if (wordCount < (window + 1)) {
		objOut.diversity = -1;
	} else {
		for (var w = 0; w < (wordCount - window); w++) {
			var objWords = {};
			var distinctCount = 0;
			for (var i = 0; i < window; i++) {
				var strWord = aryWords[i + w].toLowerCase();
				if (objWords[strWord] === undefined) {
					objWords[strWord] = 1;
					distinctCount += 1;
				} else {
					objWords[strWord] += 1;
				}
			}
            //console.log(distinctCount/window);
            divs.push(distinctCount/window);
		}
        objOut.diversity = getAvg(divs);
	}

    objOut.wordCount = wordCount;
    //objOut.distinctCount = distinctCount;
    objOut.diversityFixed = objOut.diversity.toFixed(2);
    return objOut;
}

function cleanLFP() {
    var oneDay = 24*60*60*1000; // hours*minutes*seconds*milliseconds
    var datNow = new Date();
    var datStored;
    var intDays = 0;

    for (var key in config.lfpGames) {
        datStored = new Date(config.lfpGames[key]);
        intDays = Math.round(Math.abs((datNow.getTime() - datStored.getTime())/(oneDay)));

        if (intDays > 30) {
            console.log("Deleting LFP: " + key + ": " + config.lfpGames[key] + ": " + intDays);
            delete config.lfpGames[key];
        }
    }
}

function processSpref(x) {
    var aryPrefs = x.split(":");
    if (sprefs_prefs.data[aryPrefs[1]] !== undefined) {
        return sprefs_prefs.data[aryPrefs[1]].scode;
    }
    return x;
}

function replaceSprefs(strText) {
    var regex = /sprefs:(\w*)/gi;
    var strReturn = strText;
    strReturn = strReturn.replace(regex, processSpref );
    return strReturn;
}

function getLFP() {
    var strURL = "https://storium.com/games/open?" + scriptVer + "getlfp";
    var aURL;
    var blDone = false;
    var intLFPGameIDOrdinal=4;
    $.get(strURL, function(data) {
        log("LFP: Checking","lfp");
        log("LFP-data: " + data,"lfp");
        blDone = false;
        intLFPCount=0;
        $(data).find('td.lower-button-cell a').each(function() {
            aURL = $(this).prop("href").split("/");
            log("LFP-found: " + aURL,"lfp");
            log('LFP: ' + aURL[intLFPGameIDOrdinal],'lfp');
            if (aURL[intLFPGameIDOrdinal] in config.lfpGames) {
                config.lfpGames[aURL[intLFPGameIDOrdinal]] = new Date();
                blDone = true;
            } else {
                lfpGamesNew[aURL[intLFPGameIDOrdinal]] = new Date();
                intLFPCount++;
            }
        });
        strFirstLFP = $(data).find('td.lower-button-cell:first a').prop("href").split("/")[intLFPGameIDOrdinal];
        setLFPIndicator();
    });
}

function getCardIcon(cardType) {
    var iconURL = "";
    var strToFind = "script[src*='cloudfront.net']:first";
    var ary = $(strToFind).prop("src").split("/");
    iconURL = "<img src='" + cardIconForNamespace(cardType) + "' alt='Card icon: " + cardType + "' />";
    return iconURL;
}

String.prototype.replaceAll = function(search, replacement) {
    var target = this;
    return target.replace(new RegExp(search, 'g'), replacement);
};

Date.prototype.addDays = function(days) {
    var dat = new Date(this.valueOf());
    dat.setDate(dat.getDate() + days);
    return dat;
}

/**
 * Convert an image
 * to a base64 string
 * @param  {String}   url
 * @param  {Function} callback
 * @param  {String}   [outputFormat=image/png]
 */
function convertImgToBase64(url, callback, outputFormat){
    var canvas = document.createElement('CANVAS'),
        ctx = canvas.getContext('2d'),
        img = new Image;
    img.crossOrigin = 'Anonymous';
    img.onload = function(){
        var dataURL;
        canvas.height = img.height;
        canvas.width = img.width;
        ctx.drawImage(img, 0, 0);
        dataURL = canvas.toDataURL(outputFormat);
        callback.call(this, dataURL);
        canvas = null;
    };
    img.src = url;
}

function storeCharImg(charID,imgURL) {
    var blnUpdate = false;
    var datNow = new Date();
    if (config.charimgcache[charID] === undefined) {
        blnUpdate = true;
    } else {
        var charimg = config.charimgcache[charID];
        var datWhen = new Date(charimg.when);
        var datUpdate = datWhen.addDays(1);
        if (datUpdate < datNow) {
            blnUpdate = true;
        }
    }
    if (blnUpdate) {
        convertImgToBase64(imgURL, function(base64Img){
            var charimg = {}
            charimg.id = charID;
            charimg.img = base64Img;
            charimg.when = datNow;
            config.charimgcache[charID] = charimg;
			saveConfig();
            $('img#charimg-' & charID).attr("src",base64Img);
        });
    }
}

/* ----- Tasks  ----- */
function SetTaskCount() {
	var $taskLink = $('#gm-tasks-nav');
	if (config.tasks.length > 0) {
		$taskLink.text("Tasks (" + config.tasks.length + ")");
	} else {
		$taskLink.text("Tasks");
	}
}

function showAddTask(gameName,gameID, taskID) {
	var strBody = "";
	var strGameName = "";
	var strGameURL = "";
	var intPriority = 0;
	var strModalTitle = "Add Task";
	if (taskID) {
		var task = config.tasks[taskID-1];
		//console.log(task);
		strGameName = task.title;
		strGameURL = task.url;
		intPriority = task.piority;
		strModalTitle = "Edit Task";
	} else {
		if (gameName) {
			strGameName = gameName;
		}
		if (gameID && gameID != '') {
			strGameURL = "https://storium.com/game/" + gameID;
		} else {
			switch (pageType) {
				case "game":
				case "green-room":
                case "cards":
					strGameURL = fullURL;
					strGameName = strGameURL.split("/")[4];
					strGameName = $("div.top-banner-content:eq(0) td.title-cell h2").text();
					break;
			}
		}
	}
	strBody += "<input type='hidden' name='addTaskID' id='addTaskID' value='"
	if (taskID) {
		strBody += (taskID - 1);
	} else {
		strBody += "-1"
	}
	strBody += "' />"
	strBody += "<table style='width: 100%'>";
	strBody += "<tr><th style='text-align: left; width: 5em;'>Title:</th><td><input type='text' name='addTaskTitle' id='addTaskTitle' value='" + strGameName + "' style='width: 100%' /></td></tr>";
	strBody += "<tr><th style='text-align: left; width: 5em;'>URL:</th><td><input type='text' name='addTaskURL' id='addTaskURL' value='" + strGameURL + "' style='width: 100%' /></td></tr>";
	strBody += "<tr><th style='text-align: left; width: 5em;'>Priority:</th>";
	strBody += "<td><select name='addTaskPriority' id='addTaskPriority'>";
	for (var i = 0; i < taskPriorties.length; i++) {
		if (i==intPriority) {
			strBody += "<option value='" + i + "'>" + taskPriorties[i] + "</option>";
		} else {
			strBody += "<option value='" + i + "' selected='selected'>" + taskPriorties[i] + "</option>";
		}
	}
	strBody += "</select></td></tr>";
	strBody += "<tr><td colspan='2'>";
	if (taskID) {
		strBody += "<button value='Edit Task' name='addTaskAdd' id='addTaskAdd'>Edit Task</button></td></tr>";
	} else {
		strBody += "<button value='Add Task' name='addTaskAdd' id='addTaskAdd'>Add Task</button></td></tr>";
	}
	strBody += "</table>";

	throwModal(strModalTitle,strBody);
	$('#charpop #addTaskAdd').click(function(e) {
		e.preventDefault();
		//console.log($('#charpop #addTaskID').val());
		if ($('#charpop #addTaskID').val() == "-1") {
			addTask(
				$('#charpop #addTaskTitle').val(),
				parseInt($('#charpop #addTaskPriority').val()),
				$('#charpop #addTaskURL').val(),
				//$('#charpop #addTaskDetails').val()
				''
			);
		} else {
			var objTask = config.tasks[parseInt($('#charpop #addTaskID').val())];
			objTask.title = $('#charpop #addTaskTitle').val();
			//console.log(parseInt($('#charpop #addTaskPriority').val()));
			objTask.priority = parseInt($('#charpop #addTaskPriority').val());
			objTask.url = $('#charpop #addTaskURL').val();
			saveTasks();
		}
		showTasks();
	});
}

function showTasks() {
	var strBody = "";
	strBody += "<table id='showTasksTable' style='width: 100%'>";
	strBody += "<tr><th style='text-align: left;'>Priority</th><th style='text-align: left;'>Created</th><th style='text-align: left;'>Title</th><th style='text-align: left;'>Actions</th></tr>"
	for (var i = 0; i < config.tasks.length; i++) {
		var task = config.tasks[i];
		var datCreated = new Date(task.created);
		strBody += "<tr>";
		strBody += "<td>" + taskPriorties[task.priority] + "</td>";
		strBody += "<td>" + datCreated.toLocaleString() + "</td>";
		strBody += "<td>";
		if (task.url && task.url != '') {
			strBody += "<a href='" + task.url + "'>";
			strBody += task.title;
			strBody += "</a>";
		} else {
			strBody += task.title;
		}
		strBody += "</td>";
		strBody += "<td><button id='editTask-" + i + "' class='btnEditTask'>Edit</button> <button id='deleteTask-" + i + "' class='btnDeleteTask'>Delete</button></td>";
		strBody += "</tr>";
	}

	strBody += "</table>";

	strBody += "<button id='addTaskButton'>Add Task</button>";
	throwModal("Tasks", strBody);
	$('#charpop .btnDeleteTask').click(function(e) {
		e.preventDefault();
		var intDelete = parseInt($(this).attr("id").replace("deleteTask-",""));
		config.tasks.splice(intDelete,1);
		saveTasks();
		showTasks();
	});
	$('#charpop .btnEditTask').click(function(e) {
		e.preventDefault();
		var intDelete = parseInt($(this).attr("id").replace("editTask-",""));
		showAddTask("","",intDelete+1);
	});
	$('#charpop #addTaskButton').click(function(e) {
		e.preventDefault();
		showAddTask();
	});
}


function saveTasks() {
	config.tasks.sort(function(t1,t2) { return ('' + t1.priority + t1.title) > ('' + t2.priority + t2.title) } );
	saveConfig();
	SetTaskCount();
}

function addTask(title,priority,url,details) {
	var task = {};
	task.created = new Date();
	task.title = title;
	task.priority = priority;
	task.details = details;
	task.url = url;
	config.tasks.push(task);
	saveTasks();
}
/* ----- Tasks  ----- */


//Finds y value of given object
function findPos(obj) {
    var curtop = 0;
    if (obj.offsetParent) {
        do {
            curtop += obj.offsetTop;
        } while (obj = obj.offsetParent);
    return [curtop];
    } else {
        log("Erk",'error');
    }
}

function getBanner($ele) {
    var banner = {};
	var homeScope = unsafeWindow.homeScope;
    banner.url = $ele.find("td.lower-button-cell a.banner-default-btn").prop("href");
    banner.id = banner.url.split("/")[4];
    banner.name = $ele.find("td.title-cell h2").text().trim();
	banner.charURL = "";
	banner.bannerURL = $ele.find("div.bg img").prop("src");
    if ($ele.find("h4 span").length==0) {
        banner.char = "";
        banner.charimg = "";
        banner.charimgurl = "";
        banner.charimgslack = "";
    } else {
        banner.char = $ele.find("h4 span").text().trim().replace("As ","");
        banner.charimg = "";
        banner.charimgurl = "";
        banner.charimgslack = "";
		banner.charURL = banner.url.replace("/game/","/character/" + homeScope.helpers.slugifyString(banner.char.replace(" and 1 other","")) + "/in/");
        if ($ele.find("td.avatar-cell").length > 0) {
            banner.charimgslack = $ele.find("td.avatar-cell").find("img").attr("src");
            banner.charimgurl = banner.charimgslack.replace("w_150,h_150","w_60,h_60");
            storeCharImg(banner.id,banner.charimgurl);
            banner.charimg = "<td rowspan='2' style='padding: 5px;'>" + $ele.find("td.avatar-cell").html().replace("w_150,h_150","w_60,h_60") + "</td>";
        }
    }
    return banner;
}


function compactBannerGroup($yourplaying, groupNo) {
    var banners = {};
    var url = "";
    //var $yourplaying = $('div.with-character').parent();
    var strRow = "";
    $yourplaying.find('div.nu_banner').each(function() {
        var banner = getBanner($(this));
        banners[banner.id] = banner;
    });
    $yourplaying.find('div.nu_banner').remove();
    switch (config.Homepage.sortStyleCode) {
      case "S":
        banners = sortArray(banners,sortBy,"name",false);
        break;
      case "A":
        banners = sortArray(banners,sortBy,"name",true);
        break;
      case "N":
      default:
        // no sorting to be done
        break;
    }

    $yourplaying.append("<table id='gm-compact-yourplaying-" + groupNo + "'><tbody></tbody></table>");
    for (var key in banners) {
        var banner = banners[key];
        var id = banner.id;
        strRow = "<tr class='gm-compact-game-row' id='gm-compact-game-row-" + banner.id + "'>"
        if (config.Homepage.compactType == 1) {
            if (banner.charimgurl == "") {
                strRow += "<td rowspan='2' style='padding: 5px;'><img src='" + config.charimgcache["Anon-65"].img + "'></td>"; //banner["charimg"];
            } else {
                strRow += "<td rowspan='2' style='padding: 5px;'><img src='" + config.charimgcache[id].img + "'></td>"; //banner["charimg"];
            }
        }
        strRow += "<td colspan='2' style='padding: 5px;'>"
        strRow += "<a href='" + banner.url + "'>" + banner.name + "</a>"
        if (banner.char != "") {
            strRow += " As " + banner.char;
        }
        strRow += "</td>"
        strRow += "</tr>"
        if (config.Homepage.showYourLastMoves) {
            strRow += "<tr class='gm-compact-game-row2' id='gm-compact-game-row2-" + banner.id + "'>"
            strRow += "<td id='gm-compact-lastnote-" + banner.id + "' style='padding: 5px;'><span style='color: #aaaaaa !important;'>...</span></td>"
            strRow += "<td id='gm-compact-lastmove-" + banner.id + "' style='padding: 5px;'><span style='color: #aaaaaa !important;'>...</span></td>"
        } else {
            strRow += "<tr class='gm-compact-game-row2' id='gm-compact-game-row2-" + banner.id + "'>"
            strRow += "<td colspan='2' id='gm-compact-lastnote-" + banner.id + "' style='padding: 5px;'><span style='color: #aaaaaa !important;'>...</span></td>"
        }
        strRow += "</tr>"
        $yourplaying.find("#gm-compact-yourplaying-" + groupNo + " tbody").append(strRow)
        $lastNote[banner.id] = $("#gm-compact-yourplaying-" + groupNo + " tbody").find("#gm-compact-lastnote-" + banner.id);
        $lastMove[banner.id] = $("#gm-compact-yourplaying-" + groupNo + " tbody").find("#gm-compact-lastmove-" + banner.id);
    }
    $("#gm-compact-yourplaying-" + groupNo + " tbody tr:nth-child(4n)").addClass("oddRow")
        .prev().addClass("oddRow");
}

function compactView() {
    var intGroup = 0;
    $(".banner-group, .non-banner-group").each(function() {
        compactBannerGroup($(this),intGroup);
        intGroup++;
    });
}

function showCharsForSlack() {
	var strBody = "<textarea style='width: 100%; height: 300px;'>" + charsForSlack + "</textarea>";
	strBody = "<select name='slack_chars' multiple style='height: 18em; width: 40%;'>";
	for (var i = 0; i < objCharsForSlack.lines.length; i++) {
		var objLine = objCharsForSlack.lines[i];
		strBody += "<option value='" + objLine.id + "'>" + objLine.char.replaceAll('"','') + " in " + objLine.name.replaceAll('"','') + "</option>";
	}
	strBody += "</select>";
	throwModal("Characters for Slack",strBody);
}

function getCharsForSlack() {
	var strOut = "";
	var banners = [];
	var bannersNoChar = [];
	var objOut = {fullText: "", lines: [], gmLines: []};
    $(".banner-group, .non-banner-group").each(function() {
		$(this).find('div.nu_banner').each(function() {
			var banner = getBanner($(this));
			var urlGame = banner.url;
			var urlChar = banner.charURL;
			if (urlChar != "" && banner.char && banner.char.indexOf("Hosted by") === -1) {
				banners.push(banner);
				//console.log("*<" + banner.charURL + "|" + banner.char + ">* (_<" + urlGame + "|" + banner.name + ">_)\r");
			} else {
				bannersNoChar.push(banner);
			}
		});
    });
	banners.sort(function(o1, o2) {
		return (o1.char > o2.char);
	});
	for (var i = 0; i < banners.length; i++) {
		banners[i].cabbitLine = "*<" + banners[i].charURL + "|" + banners[i].char.replace(" and 1 other","") + ">* (_<" + banners[i].url + "|" + banners[i].name + ">_)";
        if (banners[i].charimgslack != "") {
            banners[i].cabbitLine = banners[i].cabbitLine + "||" + banners[i].charimgslack;
        }
        banners[i].cabbitLine = banners[i].cabbitLine + "\n";
		objOut.lines.push(banners[i]);
		strOut += banners[i].cabbitLine;
	}
	for (var i2 = 0; i2 < bannersNoChar.length; i2++) {
		objOut.gmLines.push(bannersNoChar[i2]);
	}
	objOut.fullText = strOut;
	return objOut;
}

function showNotes(game,title) {
    var strBody = "<textarea id='gameNotes' style='width: calc(100% - 20px); margin-right: 10px; min-height: 10rem; height: calc(100% - 20px);'>"
    if (config.gameNotes[game]) {
        strBody += config.gameNotes[game];
    }
    strBody += "</textarea>";
    throwModal(title, strBody);
    $( "#charpop" ).on( "dialogclose", function( event, ui ) {
        var strNotes = $("#gameNotes").val().trim();
        if (strNotes === "") {
            if (config.gameNotes[game]) {
                delete config.gameNotes[game];
            }
        } else {
            config.gameNotes[game] = strNotes;
        }
        saveConfig();
        $( "#charpop" ).unbind( "dialogclose" );
    });
}

function toggleFocusMode() {
    let strHide = '.column-right-content, .column-cast';
    if (config.Game.focusModeActive) {
        config.Game.focusModeActive = false;
        $('#btnFocusMode').text('visibility_off');
        $(strHide).show();
    } else {
        config.Game.focusModeActive = true;
        $('#btnFocusMode').text('visibility');
        $(strHide).hide();
    }
}

function dbFormatComments($target) {
  if (intCommentCounter == -1) {
    intCommentCounter = setTimeout(function() { formatComments($target) }, 500);
  } else {
    clearTimeout(intCommentCounter);
    intCommentCounter = setTimeout(function() { formatComments($target) }, 500);
  }
}

function formatCommentsCSS() {
  if (config.Comments.format) {
    // Basic Header formatting for comment author etc
    let styleComment = ".comment-meta {\n";
    styleComment += config.Comments.authorCSS;
    styleComment += "}\n"
    styleComment += ".comment-meta-narrator {\n";
    styleComment += config.Comments.authorCSSNarrator;
    styleComment += "}\n"
    styleComment += ".comment-meta-host {\n";
    styleComment += config.Comments.authorCSSHost;
    styleComment += "}\n"
    styleComment += ".comment-meta-you {\n";
    styleComment += config.Comments.authorCSSYou;
    styleComment += "}\n"
    GM_addStyle(styleComment);
  }
}

function formatComments($target) {
  if (config.Comments.format) {
    // Conditional header formatting
    $(".comment-meta").each(function () {
      let $meta = $(this);
      $meta.find(".ng-hide").remove();
      let txtMeta = $meta.text();
      if (txtMeta.includes("(narrator)")) {
        $meta.addClass("comment-meta-narrator");
      } else if (txtMeta.includes("(host)")) {
        $meta.addClass("comment-meta-host");
      } else if (txtMeta.includes("(You)")) {
        $meta.addClass("comment-meta-you");
      }
    });

    // Comment content formatting - markdown
    if (config.Comments.markdown) {
      $(".comment-content").each(function() {
        let $comment = $(this);
        let $commentDel = $comment.find(".ng-hide");
        $commentDel.remove();
        let commentContent = $comment.text().trim();
        $comment.html(marked.parse(commentContent));
      });
    }
  }
}

function initMenu() {
    var strLinks = "";
    if (blnRemoveUpgradeLink) {
        $("div.header-nav a[href*='header_upgrade']").parent().remove();
    }
    strLinks = "<li><a href='/help#settings' id='gm-settlings-nav'>Settings</a></li>";
	if (config.Game.getCards && pageType == "game") {
		strLinks += "<li><a href='#' id='gm-get-cards'>Get Cards</a></li>";
	}
	if (config.root.tasks) {
		strLinks += "<li><a href='#' id='gm-tasks-nav'>Tasks</a></li>";
	}
    //console.log($('ul.nav-links').html());
    $('ul.nav-links').prepend(strLinks);
	SetTaskCount();
    $('#gm-get-cards').click(function(e){
		e.preventDefault();
		getCardData();
    });

	if (config.root.tasks) {
		$('#gm-tasks-nav').click(function(e){
		   e.preventDefault();
		   showTasks();
		});
	}
    $('#gm-historical-nav').click(function(e){
       e.preventDefault();
       displayHistorical();
    });

    if (config.Game.focusMode && (pageType === "game")) {
        var $focusLinks;
        if (config.Game.focusModeActive) {
            $focusLinks = $("<li><a href='#' id='gm-focus-nav' style=''><i id='btnFocusMode' class='material-icons' style='cursor: pointer' title='Focus'>visibility</i></a></li>");
        } else {
            $focusLinks = $("<li><a href='#' id='gm-focus-nav' style=''><i id='btnFocusMode' class='material-icons' style='cursor: pointer' title='Focus'>visibility_off</i></a></li>");
        }
		$('ul.nav-links').prepend($focusLinks);
		$focusLinks.click(function(e){
		   e.preventDefault();
           toggleFocusMode();
		});
    }
    if (config.Game.lastEntryButton && (pageType === "game")) {
        var $btnLastEntry;
        $btnLastEntry = $("<li style='padding: 0 0 0 10px;'><a href='#' id='gm-lastentry-nav' style=''><i id='btnLastEntry' class='material-icons' style='cursor: pointer' title='Focus'>arrow_downwards</i></a></li>");
		$('ul.nav-links').prepend($btnLastEntry);
		$btnLastEntry.click(function(e){
		   e.preventDefault();
           $('div.entry:last')[0].scrollIntoView(false);
		});
    }

    if (config.Bookmarks.markLastRead && (pageType === "game")) {
        var $btnLastRead;
        $btnLastRead = $("<li style='padding: 0 0 0 10px;'><a href='#' id='gm-lastread-nav' style=''><i id='btnLastRead' class='material-icons' style='cursor: pointer' title='Focus'>local_library</i></a></li>");
		$('ul.nav-links').prepend($btnLastRead);
		$btnLastRead.click(function(e){
		   e.preventDefault();
           $('div#gm-readline')[0].scrollIntoView(false);
		});
    }

    if (config.Notes.gameNotes && (pageType === "game" || pageType === "green-room" || pageType === "cards" )) {
        var $gameLinks = $("<li><a href='#' id='gm-gamenotes-nav'>Game Notes</a></li>");
		$('ul.nav-links').prepend($gameLinks);
        if (config.gameNotes[gameSlugFromURL()]) {
            $gameLinks.find("a").css("color","red");
        }
		$gameLinks.click(function(e){
		   e.preventDefault();
           showNotes(gameSlugFromURL(),"Game Notes");
		});
    }

    if (config.Notes.charNotes && (pageType === "character")) {
        var $charLinks = $("<li><a href='#' id='gm-gamenotes-nav'>Character Notes</a></li>");
		$('ul.nav-links').prepend($charLinks);
        if (config.gameNotes[charAndGameSlugFromURL()]) {
            $charLinks.find("a").css("color","red");
        }
		$charLinks.click(function(e){
		   e.preventDefault();
           showNotes(charAndGameSlugFromURL(),"Character Notes");
		});
    }

	if (config.Homepage.showSlackChars && pageType == "home") {
		strLinks = "<li><a href='#' id='gm-slackchars-nav'>Slack Chars</a></li>";
		//console.log($('ul.nav-links').html());
		$('ul.nav-links').prepend(strLinks);
		$('#gm-slackchars-nav').click(function(e){
		   e.preventDefault();
		   //showCharsForSlack();
		   $('#frmSlackChars').submit();
		   //alert(charsForSlack);
		});

		strLinks = "<li><a href='#' id='gm-slackchars-tmi-nav'>Slack Chars (TMI)</a></li>";
		//console.log($('ul.nav-links').html());
		$('ul.nav-links').prepend(strLinks);
		$('#gm-slackchars-tmi-nav').click(function(e){
		   e.preventDefault();
		   //showCharsForSlack();
		   $('#frmSlackChars-tmi').submit();
		   //alert(charsForSlack);
		});
	}
}

function gameSlugFromURL() {
    var strPage = fullURL.split(window.location.host)[1];
    return strPage.split("/")[2].toLowerCase();
}

function charAndGameSlugFromURL(url) {
    if (!url) {
        url = fullURL;
    }
    var strPage = url.split(window.location.host)[1];
    var aryPage = strPage.split("/");
    //console.log(aryPage[4].toLowerCase() + "/" + aryPage[2].toLowerCase());
    return aryPage[4].toLowerCase() + "/" + aryPage[2].toLowerCase();
}

function getPageType() {
    var strPage = fullURL.split(window.location.host)[1];
    var strType = strPage.split("/")[1].toLowerCase();
    var strHash = "";
	if (strType.indexOf("#") != -1) {
        strHash = strType.split("#")[1];
		strType = strType.split("#")[0];
	}
    switch (strType) {
        case "":
            return "home";
            break;
        case "game":
            if (strPage.indexOf("/green-room") > -1) {
                if (strPage.indexOf("/cards") > -1) {
                    return "cards";
                }
                return "green-room";
            } else {
                return "game";
            }
            break;
        case "forums#":
            if (strPage.indexOf("/thread/") > -1) {
                return "forum-thread";
            } else {
                //console.log("for: " + strPage);
                return "forum-category";
            }
            break;
        case "help":
            if (strHash == "settings") {
                return "gm-settings";
            }
            //console.log("Helpy!");
            return "help";
        default:
            return strType;
            break;
    }
}

function setUpNotificationDivs() {
    $("div.gm_gamelast").remove();
    $("td.lower-button-cell a[href*='/game/']").each(function() {
        var game = $(this).prop("href");
        var aGame = game.split("/");
        game = aGame[4];
        var $loc;
        $lastNote[game] = $("<div class='gm_gamelast' id='gamelast-" + game + "' style='font-size: 65%;'></div>");
        $lastMove[game] = $("<div class='gm_gamelast_our' id='gamelast-our-" + game + "' style='font-size: 65%;'></div>");
        $loc = $(this).parent().parent().prev().find("span.title-caps");
        $loc.append($lastNote[game]);
        $loc.append($lastMove[game]);
    });
}

function sleepPlay(millis, aryGames) {
    setTimeout(function() { doWeLastPlayed(aryGames); }, millis);
}

function doWeLastPlayed(aryGames) {
    var $game;
    var $gamePage;
    var ourLastMove;
    var url;
    var game;
    var aGame;
    if (aryGames.length > 0) {
        $game = aryGames[0];
        aryGames = aryGames.slice(1);
        if (config.Homepage.isCompact) {
            game = $game.prop("id").replace("gm-compact-game-row2-","");
            if (game == "in") {
                doWeLastPlayed(aryGames);
            } else {
                url = "https://storium.com/game/" + game + "?" + scriptVer + "displayLastWePlayed";
                $.get(url,function(data){
                    $gamePage = $(data);
                    ourLastMove = "";
                    $gamePage.find("div.entry-move h5.l-title:contains('(you)')").last().each(function() {
                        ourLastMove = $(this).text();
                        ourLastMove = ourLastMove.split("•")[1];
                        ourLastMove = ourLastMove.trim();
                        var strHTML = "You last moved: " + ourLastMove;
                        $lastMove[game].html(strHTML);
                    });
                }).always(function() {
                    sleepPlay(1000, aryGames);
                });
            }
        } else {
           url = $game.prop("href") + "?" + scriptVer + "displayLastWePlayed";
           game = $game.prop("href");
           aGame = game.split("/");
           game = aGame[4];
            // Find out when we last moved
            $.get(url,function(data){
                $gamePage = $(data);
                ourLastMove = "";
                $gamePage.find("div.entry-move h5.l-title:contains('(you)')").last().each(function() {
                    ourLastMove = $(this).text();
                    ourLastMove = ourLastMove.split("•")[1];
                    ourLastMove = ourLastMove.trim();
                    var strHTML = "You last moved: " + ourLastMove;
                    $lastMove[game].html(strHTML);
                });
            }).always(function() {
                sleepPlay(1000, aryGames);
            });
        }
    }
}

function displayWeLastPlayed() {
    var $gamePage;
    var ourLastMove = "";
    var aryGames = [];
    if (config.Homepage.isCompact) {
        $("tr.gm-compact-game-row2").each(function() {
            aryGames.push($(this));
        });
    } else {
        $("td.lower-button-cell a[href*='/game/']").each(function() {
            aryGames.push($(this));
        });
    }
    doWeLastPlayed(aryGames);
}

function logMyGame(game) {
    var thisGame = {};
    thisGame.id = game.id;
    thisGame.name = game.name;
    thisGame.url = game.url;
    thisGame.lastactivity = game.approxwhen.toLocaleDateString();
    config.myGames[game.id] = thisGame;
}

function doDisplayOnBanners() {
    /* Now apply those to the current page */
    if (config.Homepage.isCompact) {
        $("tr.gm-compact-game-row2").each(function() {
            var game = $(this).prop("id").replace("gm-compact-game-row2-","");
            if (games[game] === undefined) {
              // Nope
            } else if(game!="edit") {
               if (games[game].displayed) {
               } else {
                   games[game].displayed = true;
                   var strHTML = games[game].when + " " + games[game].shortwhat;
                   $("#gm-compact-lastnote-" + game).html(strHTML);
                   logMyGame(games[game]);
               }
            }
        });
    } else {
        $("td.lower-button-cell a[href*='/game/']").each(function() {
           var url = $(this).prop("href");
           var game = $(this).prop("href");
           var aGame = game.split("/");
           game = aGame[4];
           var $gamePage;
           var ourLastMove = "";
           if (games[game] === undefined) {
              // Nope
           } else if(game!="edit") {
               if (games[game].displayed) {
               } else {
                   games[game].displayed = true;

                   var strHTML = games[game].when + " " + games[game].shortwhat;
                   $lastNote[game].html(strHTML);
                   logMyGame(games[game]);
               }
           }
        });
    }
	saveConfig();
}

function getGame($href) {
    var strURL = $href.prop("href");
    var astrGame = strURL.split("/");
    var strID = "";
    var strWhen = "";
    var now;
    var intTime;
    var strText;
    var game={};
    strText = $href.text();

    // Work out what type of link this is....
    var td = $href.parent().text();
    if (td.includes("continued the scene") || td.includes("made a move in") || td.includes("made a new comment in") || td.includes("posted in") ||
        td.includes("mentioned you in a comment") || td.includes("created a new scene") || td.includes("edited their move") || td.includes("The host has unsuspended") || td.includes("The host has suspended") ) {
        game.type = "game";
		strID = $href.text();
    } else if (td.includes("edited") || td.includes("retired") || td.includes("edited") || td.includes("has suspended") || td.includes("revised their") || td.includes("requested revisions") || td.includes("ended the scene") || td.includes("created a new character")) {
        game.type = "game";
        var aryID = td.split(" in ");
        strID = aryID[aryID.length-1].trim().replace(/Chapter \d*\, Scene \d* of /g,'');
        //console.log(strID);
    } else if (td.includes("new private message")) {
        game.type = "privatemsg";
        strID = "privmessage";
    } else if (td.includes("forum") ) {
        game.type = "forum";
		strID = $href.text();
    } else {
        game.type = "unknown";
        console.log("Unknown link type");
        console.log(td);
        strID = "unknown";
    }

    game.id = unsafeWindow.homeScope.helpers.slugifyString(strID);
    game.name = strText;
    strWhen = $href.parent().parent().find("td:first").text().trim();
    game.when = strWhen;
    game.shown = false;
    /* Work out the approximate time this happened */
    now = new Date();
    intTime = strWhen.substring(0,strWhen.indexOf(" "));
    game.approxwhen = now;
    if (strWhen.indexOf("ess than") > -1) {
        game.approxwhen = now-1000;
    } else if (strWhen.indexOf("minute") > -1) {
        intTime = parseInt(intTime);
        game.approxwhen = new Date(now.getTime() - (60*1000*intTime));
    } else if (strWhen.indexOf("hour") > -1) {
        intTime = parseInt(intTime);
        game.approxwhen = new Date(now.getTime() - (60*60*1000*intTime));
        //alert(game["approxwhen"]);
    } else if (strWhen.indexOf("day") > -1) {
        intTime = parseInt(intTime);
        game.approxwhen = new Date(now.getTime() - (24*60*60*1000*intTime));
    } else if (strWhen.indexOf("month") > -1) {
        intTime = parseInt(intTime);
        // arbitrarily use 30 days per month because...
        game.approxwhen = new Date(now.getTime() - (30*24*60*60*1000*intTime));
    }
    game.what = $href.parent().html().trim();
    if (game.type == "forum") {
       game.shortwhat = game.what;
    } else {
       game.shortwhat = game.what.substring(0,game.what.lastIndexOf(" in "));
    }
    game.displayed = false;
    game.url = strURL;
    game.ordinal = 0;
    game.others = new Array();
    return game;
}

function toggleMore($this) {
    var strID = $this.prop("id").replace("gm-more-","");
    if (games[strID] === undefined) {
       // Nope
    } else {
       if (games[strID].shown) {
           games[strID].shown = false;
           $("div.feed-item-sub-" + strID).hide();
       } else {
           games[strID].shown = true;
           $("div.feed-item-sub-" + strID).show();
       }
    }
}

function extraCSS() {
    var strCSS = "<style>";
    strCSS += strCSSFakeLinks;
	strCSS += strCSSFakeLinksNoCol;
    strCSS += strCSSAlert;
    strCSS += strCSSOddRow;
    strCSS += strCSSGram;
    strCSS += strCSSExtras;
    strCSS += strCSSSnippetsMenu;
    if (config.Game.cleanImages) {
		strCSS += strCSSImageClean;
	}
    if (config.Homepage.showAsGrid && pageType=='home') {
        //console.log("Applying Homepage Grid");
		//console.log(config);
        // Get rid of old narrow/wide only stuff
        $(".narrow-only").remove();
        $(".wide-only").removeClass("wide-only");
        // Create right column and move stuff over
        $("#page > div.main").append("<div class='gm-rightcol' id='gm-rightcol'></div>");
        $("#page > div.main > div.intro > div > div.well").appendTo("#gm-rightcol");
        $("#page > div:eq(2)").css("max-width","initial");
        $("div.well").css("margin-top","initial");
        strCSS += strCSSHomeGrid;
    }
    strCSS += "</style>";
    return strCSS;
}

function doNotifications() {
     /* Clear down front page notifications */
     games = {};
     now = new Date();
     $("div.feed-notifs > div").html("<div id='lastUpdateTime' style='font-size: 80%; margin-bottom: 5px;'>" + now.toLocaleString() + " (<span class='fakelink' id='gm_markread'>Mark Read</span>)</div>");
     $("#gm_markread").click(function(){
        setLast();
     });
     intNotes = 0;
    intMainRow=0;
     processPage(1);
     if (config.Homepage.refreshSeconds>0) {
         refresh = config.Homepage.refreshSeconds;
         counter = setInterval(updateTimer, 1000); //1000 will  run it every 1 second
     }
}

/* For notifications page */
var toType = function(obj) {
  return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
}

function updateTimer() {
  refresh=refresh-1;
  if (refresh <= 0) {
    switch (pageType) {
        case "notifications":
            pageLoadTime = new Date();
            setLastIndicator();
            games = {};
            $("table tr").remove();
            getLFP();
            intMainRow=0;
           processPage(1);
            refresh = config.Notifications.refreshSeconds;
            return;
            break;
        case "home":
            clearInterval(counter);
            getLFP();
            doNotifications()
            return;
            break;
     }
  }

  switch (pageType) {
    case "notifications":
        if (!config.Notifications.hideTimerNotes) {
            $(dispTimer).text("" + refresh + " secs");
        }
        break;
    case "home":
        if (!config.Homepage.hideTimer) {
            $(dispTimer).text("" + refresh + " secs");
        }
        break;
  }
}

function showHideGame($this) {
    var gameID = $this.prop("id").replace("more-","");
    var $rows = $("tr.row-expand-" + gameID);
    var shown = games[gameID].shown;
    if (shown) {
        $rows.hide();
    } else {
        $rows.show();
    }
    games[gameID].shown = !shown;
}

function excludeGame(gameID) {
    if (gameID in config.exNotes) {
    } else {
        config.exNotes[gameID] = new Date();
        saveExNotes();
    }
}


function trelloStatusDropdown(objCardList) {
    var strOut = "<select class='trelloStatusDropdown' id='trelloStatusSelect-" + objCardList.card.id + "' cardId='" + objCardList.card.id + "'>";
    for (var i = 0; i < unsafeWindow.aryTrelLists.length; i++) {
        var objList = unsafeWindow.aryTrelLists[i];
        strOut += "<option value='" + objList.id + "'";
        if (objList.id == objCardList.list.id) {
            strOut += " selected='SELECTED'";
        }
        strOut += ">" + objList.name + "</option>";
    }
    strOut += "</select>";
    return strOut;
}

function processGame($a) {
    var game=getGame($a);
    var strAlert = "";
    var strOdd = "";
    var charImgCell = "";
    var id;
    if (game.id in config.exNotes) {
        // Ignore this game as we have chosen to exclude it
    } else {
        if (games[game.id] === undefined) {
            id = game.id;
            games[game.id]=game;
            if (game.approxwhen > lastCheck) {
                strAlert = " noteAlert"
            }
            switch (pageType) {
                case "notifications":
                    intMainRow++;
                    if (intMainRow % 2 == 0) {
                        strOdd = " oddRow";
                    } else {
                        strOdd = "";
                    }

                    if (config.Notifications.showAvatars) {
                        if (id === "privmessage") {
                            charImgCell = "<td style='padding: 5px;'><img style='width: 64px; height: 64px;' src='" + GM_getResourceURL('icon_email') + "'></td>"; //banner["charimg"];
                        } else if (game.type === "forum") {
                            charImgCell = "<td style='padding: 5px;'><img style='width: 64px; height: 64px;' src='" + GM_getResourceURL('icon_forums') + "'></td>"; //banner["charimg"];
                        } else {
                            if (config.charimgcache[id] === undefined) {
                                var blGotImg = false;
                                for (var i=0;i<=5;i++) {
                                    if (config.charimgcache[id + "--" + i] !== undefined) {
                                        charImgCell = "<td style='padding: 5px;'><img src='" + config.charimgcache[id + "--" + i].img + "'></td>"; //banner["charimg"];
                                        blGotImg = true;
                                        break;
                                    }
                                }
                                if (!blGotImg) {
                                    charImgCell = "<td style='padding: 5px;'><img src='" + config.charimgcache["Anon-65"].img + "'></td>"; //banner["charimg"];
                                }
                            } else {
                                charImgCell = "<td style='padding: 5px;'><img src='" + config.charimgcache[id].img + "'></td>"; //banner["charimg"];
                            }
                        }
                    }

					var strAdd = "<tr id='row-" + game.id + "' class='masterRow row-" + game.id + strOdd + "' game=\"" + game.name + "\">" + charImgCell + "<td class='ago" + strAlert + "'>" + game.when + "</td><td class='what'>" +
                                      game.what + "</td><td id='more-" + game.id + "' class='more fakelink' aria-game='" + game.id +
                                      "'><span class='more'>" + game.others.length + " more</span></td><td id='exclude-" + game.id + "' class='exclude fakelink' aria-game='" + game.id +
                                      "'><span class='exclude'>Exclude</span></td>"

					if (config.root.tasks) {
						strAdd += "<td id='addTask-" + game.id + "' class='addTaskLink fakelink' aria-game='" + game.id + "'><span class='addTaskLink'>Add Task</span></td></tr>"
					}

					if (config.Trello.integrateTrello && config.Trello.statusNote) {
						var strGame = game.name.trim();
						var objCardList = unsafeWindow.objCardLists[strGame];
						var strList;
						if (objCardList) {
							if (objCardList.card.closed) {
                                console.log("Card closed! " + strGame);
								strList = "-";
							} else {
								strList = trelloStatusDropdown(objCardList); //"<a target='_blank' class='fakelink' href='" + unsafeWindow.strTrelURL + "'>" + objCardList.list.name + "</a>";
							}
						} else {
                            console.log("No card! " + strGame);
							strList = "-"
						}
						strAdd += "<td id='trelloStatus-" + game.id + "' class='trelloStatus'><span style='color: #086a87;'>" + strList + "</span></td>";
                        var objAdd = $(strAdd);
                        objAdd.find(".trelloStatusDropdown").change(function() {
                            console.log("tsdd Change");
                            console.log($(this).attr("cardId"));
                            console.log($(this).val());
                            unsafeWindow.trelloUpdateCardsList($(this).attr("cardId"),$(this).val());
                        });
                        $("div.centered-main table").append(objAdd);
					} else {
                        $("div.centered-main table").append(strAdd);
                    }




                    /* Hook up the click to show-hide child notifications */
                    $("#more-" + game.id).click(function() {
                        showHideGame($(this));
                    });
                    $("#exclude-" + game.id).click(function() {
                        var r = confirm("Exclude " + game.id + " from notifications?");
                        if (r == true) {
                            excludeGame(game.id);
                            $(this).parent().remove();
                        }
                    });
                    $("#addTask-" + game.id).click(function() {
						showAddTask(game.name,game.id);
                    });
                    break;
                case "home":
                  if (intNotes < config.Homepage.maxNotes) {
                     intNotes++;
                     $("div.feed-notifs > div").append("<div id='feed-item-" + game.id + "' class='feed-item feed-item-" + game.id +
                        "'>" + game.what + "<br><div class='ago" + strAlert + "'>" + game.when + " <span id='gm-more-" + game.id +
                        "'>(<span class='fakelink'>0 more</span>)</span></div></div>");
                     $("#gm-more-" + game.id).click(function() {
                        toggleMore($(this));
                     });
                  }
                  break;
            }
        } else {
            game.ordinal = games[game.id].others.length + 1;
            games[game.id].others.push(game);
            switch (pageType) {
                case "notifications":
                    if (games[game.id].others.length % 2 == 0) {
                        strOdd = "";
                    } else {
                        strOdd = " oddRow2";
                    }
                    $row = $("<tr class='row-expand-" + game.id + " row-" + game.id + strOdd + "' id='row-" + game.id + "-" + game.ordinal +
                             "'><td class='more'>&nbsp;</td><td colspan='2' class='what'>" + game.shortwhat +
                             " (<span style='color:gray'>" + game.when + "</span>)</td></tr>");
                    $("tr.row-" + game.id + ":last").after($row);
                    $row.hide();
                    $("#row-" + game.id + " td.more span.more").html(game.ordinal + " more");
                    break;
                case "home":
                    var strShown;
                    if (games[game.id].shown) {
                        strShown = " style='margin-left: 10px;background-color:#f2f2f2;'";
                    } else {
                        strShown = " style='display:none;margin-left: 10px;background-color:#f2f2f2;'";
                    }
                    $("#gm-more-" + game.id).html("(<span class='fakelink'>" + games[game.id].others.length + " more</span>)");
                    var $row = $("<div class='feed-item feed-item-" + game.id + " feed-item-sub-" + game.id + "'" + strShown + ">" + game.what + "<br><div class='ago'>" + game.when + "</div></div>");
                    $("div.feed-item-" + game.id + ":last").after($row);
                    break;
            }
        }
    }
}

function processTable($table) {
    $table.find("a[href*='storium.com/game/'],a[href*='storium.com/forums'],a[href*='storium.com/character/'],a[href*='storium.com/invitation/'],a[href*='/messages'],a[href*='/notif']").each(function() {
       processGame($(this));
    });
}

function processPage(intPage) {
    var intMax;
    switch (pageType) {
        case "notifications":
            intMax = config.Notifications.maxPages;
            break;
        case "home":
            intMax = config.Homepage.maxPages;
            break;
    }
    if (intPage <= intMax) {
        $.get("https://storium.com/notifications?page=" + intPage + "&" + scriptVer + "displayLastWePlayed",function(data) {
            processTable($(data).find("table"));
            if (pageType=="home") {
                doDisplayOnBanners();
            }
            processPage(intPage+1);
        });
    }
    if (config.root.userNotes) {
        displayUserNotes();
    }
}

function setLastIndicator() {
    $("#markRead").html("Mark Read <span>(" + pageLoadTime.toLocaleString() + ")</span>");
}

function setLFPIndicator() {
    var $ele = $('#gm-LFP-text');
    if (intLFPCount == 0) {
        $ele.removeClass("noteAlert");
    } else {
        $ele.addClass("noteAlert");
    }
    $ele.prop("style", config.root.LFPStyle);
    $ele.html("LFP: " + intLFPCount + " games");
    $ele.prop("title","Last: " + strFirstLFP)
}

function setLast() {
    /* Log the time of the last check */
	config.lastCheck = pageLoadTime.toISOString();
    for (var key in lfpGamesNew) {
        config.lfpGames[key] = lfpGamesNew[key];
    }
	saveConfig();
    lastCheck = pageLoadTime;
    setLastIndicator();
    intLFPCount = 0;
    setLFPIndicator();
    $(".noteAlert").removeClass("noteAlert");
}

function saveExNotes() {
	saveConfig();
}

function unexcludeNote(el) {
    var strID;
    var index;
    strID = el.prop("id");
    strID = strID.replace("gm-unexclude-","");
    delete config.exNotes[strID];
    el.parent().parent().remove();
    saveExNotes();
}

/* Game Page Code */
function throwModal(strTitle, strBody, strWidth) {
    var intHeight;
    intHeight = Math.floor($( window ).height() * 0.8);

    if (!strWidth) {
        strWidth = "720px";
    }

    if ($("#charpop").length == 0) {
        $('body').append($(strModal));
    }
    $('#charpop').html(strBody).on("dialogopen", function(event,ui) {
        $('#charpop .gm-del-historical').each(function() {
            $(this).click(function() {
                removeHistorical($(this));
            });
        });
        $('#charpop .gm-unexclude').each(function() {
            $(this).click(function(e) {
                unexcludeNote($(this));
                e.preventDefault();
            });
        });
        $('#gm-historical-showHidden').change(function() {
            if ($(this).is(":checked")) {
                $('.gm-historical-hidden').show();
            } else {
                $('.gm-historical-hidden').hide();
            }
        });
    });
    $('#charpop').dialog({title: strTitle, width: strWidth, maxHeight: intHeight, resizable: true});
}

/* Tests for sorting */
function sortBy(obj1,obj2,sortType) {
    switch (sortType.trim().toLowerCase()) {
        case "last move":
            return obj1.movepos > obj2.movepos;
            break;
        case "last move (desc)":
            return obj1.movepos < obj2.movepos;
            break;
        case "name":
            return obj1.name > obj2.name;
            break;
        case "name (desc)":
            return obj1.name < obj2.name;
            break;
    }
}

/* For removing articles when searching */
function removeArticles(str) {
  words = str.split(" ");
  if(words.length <= 1) return str;
  if( words[0] == 'a' || words[0] == 'the' || words[0] == 'an' )
    return words.splice(1).join(" ");
  return str;
}

/* Function for sorting associative arrays */
function sortArray(obj, comparator, comparatorParam, withoutArticles) {
    var array = [];
    for (var i in obj) {
        array.push([obj[i], i]);
    }
    if (withoutArticles) {
        array.sort(function(o1, o2) {
          var aTitle = o1[0][comparatorParam].toLowerCase();
          var bTitle = o2[0][comparatorParam].toLowerCase();

          aTitle = removeArticles(aTitle);
          bTitle = removeArticles(bTitle);

          if (aTitle > bTitle) return 1;
          if (aTitle < bTitle) return -1;
          return 0;
        });

    } else {
        array.sort(function(o1, o2) {
          return comparator(o1[0], o2[0], comparatorParam);
        });
    }
    var newObj = {};
    for (var i2 = 0; i2 < array.length; i2++) {
        newObj[array[i2][1]] = array[i2][0];
    }
    return newObj;
}

function cleanCard(strIn) {
    var strOut = strIn;
    strOut = strOut.replace(/\<\!--/g,"");
    strOut = strOut.replace(/\--\>/g,"");
    strOut = strOut.trim();
    return strOut;
}


function GetSelectedText() {
    var range;
    if (window.getSelection) { // all browsers, except IE before version 9
        range = window.getSelection ();
        return range.toString();
    } else {
        if (document.selection.createRange) { // Internet Explorer
            range = document.selection.createRange ();
            return range.text;
        }
    }
}

function moveCaretToEnd(el) {
    if (typeof el.selectionStart == "number") {
        el.selectionStart = el.selectionEnd = el.value.length;
    } else if (typeof el.createTextRange != "undefined") {
        el.focus();
        var range = el.createTextRange();
        range.collapse(false);
        range.select();
    }
}

/* For doing bold, italics etc */
function addTag(tag_s,tag_e,moveToEnd, strTextArea, blReplaceSelected, blStartOfLine) {
    var textArea;
    if (strTextArea === undefined) {
        textArea = $('textarea.description-input');
    } else {
        textArea = $(strTextArea);
    }
    if (textArea.length > 0) {
        var start = textArea[0].selectionStart;
        var end = textArea[0].selectionEnd;
        var replacement = "";
        if (!blReplaceSelected) {
            replacement = tag_s + textArea.val().substring(start, end) + tag_e;
        } else {
            replacement = tag_s + tag_e;
        }
        if (blStartOfLine) {
            replacement = replacement.replace(/\r?\n/g, '\r\n' + tag_s)
        }
        textArea.val(textArea.val().substring(0, start) + replacement + textArea.val().substring(end, textArea.val().length));
        if (moveToEnd) {
           moveCaretToEnd(textArea[0]);
        }
    }
    unsafeWindow.updateDescBoxData();
}

function showHideChars() {
    $("#gm_display div.gm_char").slideToggle();
    $("#gm_display #gameTitle").slideToggle();
}

function killPreview() {
    if (blnPreviewHidden) {
        $("button:contains('Preview')").hide();
    } else {
        $("button:contains('Preview')").show();
    }
    setTimeout(function(){killPreview();}, 1000);
}

function removeHistorical(el) {
    var strID;
    strID = el.prop("id");
    strID = strID.replace("gm-del-historical-","");
    if (el.html() == "Remove") {
        el.html("Restore");
        if ($('#gm-historical-showHidden').is(":checked")) {
            $("#gm-historical-row-" + strID).addClass("gm-historical-hidden").show();
        } else {
            $("#gm-historical-row-" + strID).addClass("gm-historical-hidden").hide();
        }
        config.myGames[strID].hidden = 1;
    } else {
        el.html("Remove");
        $("#gm-historical-row-" + strID).removeClass("gm-historical-hidden").show();
        delete config.myGames[strID].hidden;
    }
    saveConfig();
}

function displayExcluded() {
    var strBody = "";
    var game;
    strBody = "<p>Please note that games un-excluded below will only reappear if you refresh the notifications page.</p>"
    strBody += "<table>"
    for (var key in config.exNotes) {
        game = config.myGames[key];
        strBody += "<tr id='gm-excluded-row-" + key + "'>"
        if (game === undefined) {
            strBody += "<td style='padding: 5px;'>" + key + "</td>"
        } else {
            strBody += "<td style='padding: 5px;'>" + game.name + "</td>"
        }
        strBody += "<td style='padding: 5px;'><a href='#' class='gm-unexclude fakelink' id='gm-unexclude-" + key + "'>Unexclude</a></td>"
        strBody += "</tr>"
    }
    strBody += "</table>"
    throwModal("Games Excluded from Notifications",strBody);
}


function displayHistorical() {
    var strBody;
    var game;
    config.myGames = sortArray(config.myGames,sortBy,"name");
    strBody = "<div>"
    strBody += "<input type='checkbox' name='gm-historical-showHidden' id='gm-historical-showHidden'>: Show Hidden?"
    strBody += "</div>"
    strBody += "<table>"
    for (var key in config.myGames) {
        game = config.myGames[key];
        if (game.hidden === undefined) {
            strBody += "<tr id='gm-historical-row-"+ game.id + "'>"
            strBody += "<td style='padding: 5px;'><a class='fakelink' href='" + game.url + "'>" + game.name + "</a></td>"
            strBody += "<td style='padding: 5px;'>" + game.lastactivity + "</td>"
            strBody += "<td style='padding: 5px;'><a href='#' class='gm-del-historical fakelink' id='gm-del-historical-" + game.id + "'>Remove</a></td>"
            strBody += "</tr>"
        } else {
            strBody += "<tr id='gm-historical-row-"+ game.id + "' class='gm-historical-hidden' style='display: none'>"
            strBody += "<td style='padding: 5px;'><a class='fakelink' href='" + game.url + "'>" + game.name + "</a></td>"
            strBody += "<td style='padding: 5px;'>" + game.lastactivity + "</td>"
            strBody += "<td style='padding: 5px;'><a href='#' class='gm-del-historical fakelink' id='gm-del-historical-" + game.id + "'>Restore</a></td>"
            strBody += "</tr>"
        }
    }
    strBody += "</table>"
    throwModal("Historically Active Games",strBody);
}

function addRetiredDiv() {
    $("div.game-list-inner").append("<div class='well' style='margin-bottom: 10px' id='gm-retired'><p class='fakelink'>Click here to see list of historically active games</p></div>");
    $("#gm-retired").click(function() {
        displayHistorical();
    });
}

function getWordCount(obj) {
    var regex = /\s+/gi;
    var txt = obj.text();
    var wordCount = txt.trim().replace(regex, ' ').split(' ').length;
    return wordCount;
}

function getWordCountTB(obj) {
    var regex = /\s+/gi;
    var txt = obj.val();
    var wordCount = txt.trim().replace(regex, ' ').split(' ').length;
    return wordCount;
}

function getWordCountText(txt) {
    var regex = /\s+/gi;
    var wordCount = txt.trim().replace(regex, ' ').split(' ').length;
    return wordCount;
}

function charAvgWordCount(char) {
    var num;
    if (char.moves == 0) {
        num=0;
    } else {
        num = char.wordcount / char.moves;
    }
    return Math.round(num * 100) / 100;
}

function loadNarratorChar() {
    /* Add Narrator 'character' */
    var char = {};
    char.name = "Narrator";
    char.url = "";
    char.movepos = 0;
    char.retired = false;
    char.strengths = {};
    char.played = 0;
    char.username = "Narrator";
    char.userurl = "";
    $('div.column-main-content div:first div:first a').each(function() {
        var userURL = $(this).prop('href');
        if (userURL.indexOf('https://storium.com/user') > -1) {
            char.userurl = userURL;
        }
    });

    char.loadedUser = false;
    char.loadedChar = false;
    char.strStrength = "";
    char.strWeakness = "";
    char.strAsset = "";
    char.strGoal = "";
    char.strSubplot = "";
    char.moves = [];
    char.wordcount = 0;
    chars[char.name] = char;
}

function initStyle() {
    var strCSS = "<style>";
    if (config.root.fontSubst != "None") {
        strCSS += fontSubst[config.root.fontSubst];
    }
    if (config.root.theme != "None"){
        strCSS += styles[config.root.theme ];
    }
    strCSS += "</style>";
    $('head').append(strCSS);
}

async function initCSS() {
    /* Add style for fake links */
	GM_addStyle(extraCSS());
    // Move links
    GM_addStyle("div.entry-description a { " + config.Game.moveLinkCSS + "}");
    GM_addStyle("div.entry-description .fakelink { " + config.Game.moveLinkCSS + "}");
    GM_addStyle("div.entry-description a:hover { " + config.Game.moveLinkHoverCSS + "}");
	GM_addStyle(GM_getResourceText("CSS_JUI"));
    /* Google Material icons */
	GM_addStyle(GM_getResourceText("CSS_Icons"));
    if (config.root.BGCol && config.root.BGCol.trim().length > 0) {
        GM_addStyle("body {background-color: " + config.root.BGCol + "}");
    }
    if (config.root.IconCol && config.root.IconCol.trim().length > 0) {
        GM_addStyle(".comments .comments-refresh-link {color: " + config.root.IconCol + "}");
    }
    if (config.Game.rainbowActiveChar) {
        // console.log("Rainbow! " + cssRainbowActiveChar);
        GM_addStyle(cssRainbowActiveChar);
    }
}

async function init() {
    pageType = getPageType();
    scriptVer += "&script-origin=" + pageType + "&script-detail=";
    log("Page type: " + pageType,"pageType");
    await initConfig();
    await initCSS();
    if (pageType == "sprefs") {
        // sprefs
    } else {
        cleanLFP();
		saveConfig();
        initMenu();
        initStyle();
		if (config.Trello.integrateTrello && (pageType=="home" || pageType=="notifications")) {
			initTrello();
		}
    }
}

function charCount() {
    var intChars = 0;
    for (var key in chars) {
        intChars++;
    }
    return intChars;
}

/* No longer seems to be needed but keep it in case it is later */
function checkActivityTable() {
    var $table = $("div.the-activity-summary table:first");
    var intSize = charCount() + 1;

    if ($table.find("tr").length > (intSize * 2)) {
        //alert("bork");
    }
    setTimeout(function() { checkActivityTable(); }, 1000);
}

function getCharBySeq(seqID) {
    var char = {};
    for (var key in chars) {
        if (chars[key].seq == seqID) {
            char = chars[key];
            break;
        }
    }
    if (char.name === undefined) {
        console.log("Unknown char seq: " + seqID);
    }
    return char;
}

function cardsPlayedTable(challenge) {
    var strCards = "";
    var cardsPlayed = challenge.cardsplayed;
    if (cardsPlayed === undefined) {
    } else {
        var arrayLength = cardsPlayed.length;
        for (var i = 0; i < arrayLength; i++) {
            var pCard = cardsPlayed[i];
            strCards += "<tr>";
            // 1.17.1: Switch player and card icon columns around
            strCards += "<td><a href='" + pCard.playedBy.url + "'>" + pCard.playedBy.name + "</a></td>";
            strCards += "<td>" + getCardIcon(pCard.namespace) + " " + pCard.name + "</td>";
            strCards += "</tr>";
        }
    }
    /* If we have any output then wrap it in a table */
    if (strCards != "") {
        strCards = "<table style='width: auto;'><tbody>" + strCards + "</tbody></table>";
    } else {
        strCards = "<div>No cards played yet.</div>"
    }

    return strCards;
}

function getChallengeByName(strName) {
    var challenge = {};
    for (var key in challenges) {
        if (challenges[key].name.trim() == strName.trim()) {
            challenge = challenges[key];
            break;
        }
    }
    if (challenge.name === undefined) {
        console.log("Unknown challenge name: " + strName);
    }
    return challenge;
}

function convertUKDates(pageType) {
    var strPre="";
    var strPost="";
    var strText="";
    var aryText;
    var strHTML = "";
    var strNew = "";
    switch(pageType) {
        case "game":
            $("span.dateline").each(function() {
                strText = $(this).text();
                if (strText.indexOf(" ") >= 0) {
                    aryText = strText.split(" ");
                    strPre = aryText[0];
                    strPost = aryText[1];
                } else {
                    strPre="";
                    strPost = strText;
                }
                aryText = strPost.split("/");
                if (aryText.length == 3) {
                    strHTML = $(this).html();
                    strNew = aryText[1] + "/" + aryText[0] + "/" + aryText[2];
                    strHTML = strHTML.replace(strPost,strNew);
                    $(this).html(strHTML);
                }
            });
            break;
        default:
            break;
    }
}

function moveWordCount(moveElement) {
    var intWC;
    var $p = moveElement.parent();
    var $wc;
    if (moveElement.val().trim() == "") {
        intWC = 0;
    } else {
        intWC = getWordCountTB(moveElement);
    }
    if ($p.find("span#wordcount").length == 0) {
        moveElement.before("<span id='wordcount' atyle='font-size: 11px; line-height: 13px; margin-bottom: 5px;'></div>");
    }
    $wc = $p.find("span#wordcount");
    $wc.text("Word Count: " + intWC);
}

function moveWordCountInit(moveElement) {
    moveElement.keypress(function(event) {
        moveWordCount($(this));
    });
    moveWordCount(moveElement);
}

function handleEntry($e) {
    for (var i = 0; i < $e.length; i++) {
        var $ele = $($e[i]);
        var strHTML;
        if (!$ele.hasClass("gm_processed")) {
            /* Style up location tags */
            if (config.Game.processLocationTags) {
               $ele.html($ele.html().replace(/\<strong\>\[/g,"<strong style='" + config.Game.tagStyle + "'>"));
               $ele.html($ele.html().replace(/\]\<\/strong\>/g,"</strong>"));
               /* And once for [<strong>...</strong>] */
               $ele.html($ele.html().replace(/\[\<strong\>/g,"<strong style='" + config.Game.tagStyle + "'>"));
               $ele.html($ele.html().replace(/\<\/strong\>\]/g,"</strong>"));
            }

            if (config.Game.processCensorTags) {
               $ele.html($ele.html().replace(/\+\[/g,"<span class='censored'>"));
               $ele.html($ele.html().replace(/\]\+/g,"</span>"));
            }

            /* Style up speech */
            if (config.Speech.styleSpeech) {
               strHTML = $ele.html();
               strHTML = speechStyle(strHTML,config.Speech.CSS, config.Speech.incQuotes);
               $ele.html(strHTML);
			   setCensorClear();
            }

            if (config.Speech.styleThought) {
               strHTML = $ele.html();
               strHTML = thoughtStyle(strHTML,config.Speech.CSSThought, config.Speech.incQuotes);
               $ele.html(strHTML);
			   setCensorClear();
            }

            /* Add word counts to the end of entries */
            if (config.Game.entryWC) {
                var objStats = lexDiv($ele.text());
                $ele.append("<hr /><span class='wordCount' style='color: gray;'>(" + objStats.wordCount + " words" + (objStats.diversityFixed == -1 ? '' : (", " + objStats.diversityFixed) + " diversity") + ")</span>");
            }
            if (config.Game.directLinks) {
              $ele.append("<span class='gameBookmark' style='float: right; display: inline;'><a href='" + fullURL.split('#')[0] + "#" + (parseInt(bookmarkCount) - 1) + "'>Direct Link</a></span>");
              bookmarkCount++;
            }

			/* Make edits minor by default */
			if (config.Game.minorEditDefault) {
                //console.log("Minor set");
				$('input#minor_edit').prop('checked', true);
			}

            $ele.addClass("gm_processed");
            //processedEntries.push($ele.text());
            //console.log(processedEntries);
        }
    }
}

function randomInteger(lowerBound, upperBound) {
    return Math.floor((Math.random() * upperBound) + lowerBound);
}

function setCensorClear() {
	$("span.censored").click(function(e) {
		$(this).removeClass("censored");
	});
}

function handleChangesMoveBox($e) {
    var $btn;
    var strTextArea;
    var strFontSizeIcon = "; font-size: 24px; ";
    var strFontSizeSep = " font-size: 20px;";
    if (config.Game.moveWC) {
        moveWordCountInit($e);
    }
	/* Make edits minor by default */
	if (true) {
		$('input#minor_edit').prop('checked', true);
	}
    if ($e.hasClass("addedButtons") || $('body').find('i#btnFormatBold').length > 0) {
        return;
    } else {
        //console.log("Adding class");
        $e.addClass("addedButtons");
    }
    if ($e.hasClass('description-input')) {
        strTextArea = 'textarea.description-input';
    } else if ($e.prop('id') == 'newPostBodyInput') {
        strTextArea = 'textarea#newPostBodyInput';
    } else {
        strTextArea = 'textarea.ng-isolate-scope';
        strFontSizeIcon = "; font-size: 18px; ";
        strFontSizeSep = " font-size: 16px;";
    }
    if (config.Game.formatButtons) {
        /* Insert the bold button and hook it up */
        //console.log("Adding buttons!");
        $btn = $("<span class='formatButtons' style='clear: left; display: block;'></span>");
        $btn.append("<i id='btnFormatBold' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Bold'>format_bold</i>")
        $btn.append("<i id='btnFormatItalic' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Italic'>format_italic</i>")
        $btn.append("<i id='btnFormatStrikethrough' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Strikethrough'>format_strikethrough</i>")
        $btn.append("<i id='btnFormatBullet' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Bullet Point'>format_list_bulleted</i>")
        $btn.append("<i id='btnFormatLink' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Link'>link</i>")
        $btn.append("<i id='btnFormatLocTag' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Location Tag'>add_location</i>")

        $btn.append("<span style='border-right: 3px solid rgba(0, 0, 0, 0.25); margin-right:5px; margin-left: 5px; font-size: 20px;" + strFontSizeSep + " font-weight: bold; vertical-align: top;' title='Separator'></span>")

        $btn.append("<i id='btnFormatQuote' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Block Quote'>format_quote</i>")
        $btn.append("<i id='btnFormatConsole' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Console Text'>code</i>")
        $btn.append("<span id='btnFormatH1' style='cursor: pointer" + strFontSizeIcon + "; font-weight: bold; vertical-align: top;' title='Header 1'>H</span")
        $btn.append("<i id='btnFormatHR' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Horizontal Rule'>border_horizontal</i>")
        //$btn.append("<i id='btnFormatCharQ' class='material-icons' style='cursor: pointer' title='Character Question'>question_answer</i>")


        if (blnSnippets) {
            $btn.append("<span style='border-right: 3px solid rgba(0, 0, 0, 0.25); margin-right:5px; margin-left: 5px;" + strFontSizeSep + " font-weight: bold; vertical-align: top;' title='Separator'></span>")
            $btn.append("<i id='btnFormatSnippets' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Snippets'>content_paste</i>")
        }
        $btn.append("<i id='btnFormatHelp' class='material-icons' style='cursor: pointer" + strFontSizeIcon + "' title='Formatting Help'>help</i>")

        $e.before($btn);
        displaySnippets();



        var $textArea = $(strTextArea);

        $('#btnFormatBold').click(function () {
            addTag('**','**',false,strTextArea);
            $textArea.focus();
        });

        $('#btnFormatItalic').click(function () {
            addTag('*','*',false,strTextArea);
            $textArea.focus();
        });
        $('#btnFormatStrikethrough').click(function () {
            addTag('~~','~~',false,strTextArea);
            $textArea.focus();
        });
        $('#btnFormatLocTag').click(function () {
            addTag('**[',']**',false,strTextArea);
            $textArea.focus();
        });
        $('#btnFormatHR').click(function () {
            addTag('','\n***\n',false,strTextArea);
            $textArea.focus();
        });
        $('#btnFormatSnippets').click(function () {
            $('ul#ulSnip').slideToggle('medium');
            $textArea.focus();
        });

        $('#btnFormatLink').click(function () {
            var start = $textArea[0].selectionStart;
            var end = $textArea[0].selectionEnd;
            var replacement = "";
            replacement = $textArea.val().substring(start, end);

            frmInsertLink(strTextArea, replacement);
            $textArea.focus();
        });
        $('#btnFormatH1').click(function () {
            addTag('#','',false,strTextArea);
            $textArea.focus();
        });
        $('#btnFormatConsole').click(function () {
            addTag('    ','',false,strTextArea);
            $textArea.focus();
        });
        $('#btnFormatQuote').click(function () {
            addTag('>','',false,strTextArea);
            $textArea.focus();
        });
        $('#btnFormatBullet').click(function () {
            addTag('* ','',false,strTextArea, false, true);
            $textArea.focus();
        });
        $('#btnFormatHelp').click(function () {
            var url = 'https://docs.google.com/document/d/1nbgt9DkiIR1wNQkT_6xnFHrf0y5cvAVyZ4XPh5B62cM/edit';
            window.open(url, '_blank').focus();
        });
    }
}

function speechStyle(strToCheck, strStyle, blnIncludeQuotes) {
    var strOut = '';
    if (blnIncludeQuotes) {
        strOut = strToCheck.replace(/“.*?(?:”|“|(<\/p>))/g,function lambdaReplace(x) { return '<span style="' + strStyle + '">' + x + '</span>';});
    } else {
        strOut = strToCheck.replace(/“.*?(?:”|“|(<\/p>))/g,function lambdaReplace(x) {
          let strEndQ = "";
          if (x.slice(-1) === "”") { strEndQ = "”" }
          return '“<span style="' + strStyle + '">' + x.replace(/\”/g,'').replace(/\“/g,'') + '</span>' + strEndQ;
        });
    }
    return strOut;
}

function thoughtStyle(strToCheck, strStyle, blnIncludeQuotes) {
    var strCheck = strToCheck;
    var strOut = '';
    if (blnIncludeQuotes) {
        strOut = strCheck.replace(/([~])(?:(?=(\\?))\2.)*?\1/g,function lambdaReplace(x) { return '<span style="' + strStyle + '">~' + x.replace(/\~/g,'') + '~</span>';});
    } else {
        strOut = strCheck.replace(/([~])(?:(?=(\\?))\2.)*?\1/g,function lambdaReplace(x) { return '~<span style="' + strStyle + '">' + x.replace(/\~/g,'') + '</span>~';});
    }
    return strOut;
}


/* =========================== */
/* Snippets                    */
/* =========================== */
function cleanSnippets() {
    log("functiontrace","Start Function");
    var key;
    for (key in config.snippets) {
        var snippet = config.snippets[key];
        if (snippet.body.trim() === "") {
            delete config.snippets[key];
        }
    }
}

function sortedSnippetKeys() {
    var keys = [];
    for (key in config.snippets) {
        keys.push(key);
    }
    keys.sort();
    return keys;
}

function setSnippet() {
    log("functiontrace","Start Function");
    var strName = $('#snippetName').val();
    var strBody = $('#snippetBody').val();
    var strID = strName.replace(/ /g,"-");
    var snippet = {};

    snippet.id = strID;
    snippet.body = strBody;
    snippet.name = strName;
    config.snippets[strID] = snippet;

    displaySnippets();
    //$('#modalpop').dialog( "close" );
    cleanSnippets();
    saveSnippets();
    return false;
}

function frmSnippetBody() {
    log("functiontrace","Start Function");
    var strBody = "";
    var key;
    strBody = "<p><strong>Add Snippet</strong></p>";
    strBody += "<table style='width: 100%;'>";
    strBody += "<tr>";
    strBody += " <th style='vertical-align: top; text-align: right;'>Name:</th>";
    strBody += " <td><input type='text' id='snippetName' style='width: 95%;'></td>";
    strBody += "</tr>";
    strBody += "<tr>";
    strBody += " <th style='vertical-align: top; text-align: right;'>Snippet:</th>";
    strBody += " <td><textarea id='snippetBody' rows='3' style='width: 95%;'></textarea></td>";
    strBody += "</tr>";
    strBody += "<tr>";
    strBody += " <td colspan='2'><center><button value='Set' id='setSnippet'>Set</button></center></td>";
    strBody += "</tr>";
    strBody += "</table>";
    strBody += "<p><strong>Current Snippets</strong></p>";
    strBody += "<table>";
    strBody += "<tr>";
    strBody += " <th style='text-align: left;'>Name</th>";
    strBody += " <th style='text-align: left;'>Actions</th>";
    strBody += "</tr>";
    for (key in config.snippets) {
        var snippet = config.snippets[key];
        if (snippet.body !== "") {
            strBody += "<tr id='snipEdit_row_" + snippet.id + "'>";
            strBody += " <td style='padding: 5px;'>" + snippet.name.replace("'","&#39;") + "</td>";
            strBody += " <td style='padding: 5px;'>";
            strBody += "<button type='button' id='snipEdit_use_" + snippet.id + "' class='snipEdit_usebutton' value='Use'>Use</button> ";
            strBody += "<button type='button' id='snipEdit_update_" + snippet.id + "' class='snipEdit_updatebutton' value='Edit'>Edit</button> ";
            strBody += "<button type='button' id='snipEdit_delete_" + snippet.id + "' class='snipEdit_deletebutton' value='Delete'>Delete</button> ";
            strBody += "</td>";
            strBody += "</tr>";
        }
    }
    return strBody;
}

function frmSnippetButtons() {
    log("functiontrace","Start Function");
    var strBody;
    var strID;
    var snippet;
    $('button#setSnippet').click( function(e) {
        e.preventDefault();
        setSnippet();
        strBody = frmSnippetBody();
        $("#charpop").html(strBody);
        frmSnippetButtons();
    });
    $('#charpop button.snipEdit_updatebutton').click( function(e) {
        strID = $(this).attr("id");
        strID = strID.replace("snipEdit_update_","");
        snippet = config.snippets[strID];
        $("#charpop #snippetName").val(snippet.name);
        $("#charpop #snippetBody").val(snippet.body);
    });
    $('#charpop button.snipEdit_usebutton').click( function(e) {
        strID = $(this).attr("id");
        strID = strID.replace("snipEdit_use_","");
        snippet = config.snippets[strID];
        addTag(config.snippets[strID].body,"",false);
    });
    $('#charpop button.snipEdit_deletebutton').click( function(e) {
        strID = $(this).attr("id");
        strID = strID.replace("snipEdit_delete_","");
        delete config.snippets[strID];
        saveSnippets();
        displaySnippets();
        strBody = frmSnippetBody();
        $("#charpop").html(strBody);
        frmSnippetButtons();
    });
}

function frmSnippet() {
    log("functiontrace","Start Function");
    var strBody = frmSnippetBody;
    var strID = "";
    throwModal("Add Snippet",strBody);
    frmSnippetButtons();
}

function saveSnippets() {
	saveConfig();
}

function pasteSnippet($this) {
    log("functiontrace","Start Function");
    var strID = $this.attr("id").replace("snip-","");
    addTag(config.snippets[strID].body,"",false);
    return false;
}

function displaySnippets() {
    log("functiontrace","Start Function");
    $("li#button_snip ul").remove();
    var $copyTo = $('i#btnFormatSnippets');
    var key;
    if ($copyTo.length > 0) {
        var $menuQ = $('ul#ulSnip');
        var newMenu = false;
        if ($menuQ.length === 0) {
            $menuQ = $("<ul id='ulSnip' class='snip_menu'></ul>");
            newMenu = true;
        }
        $menuQ.html("<li id='snip_menu'><a class='firstlevel snip_menu' href='#'><span class='firstlevel'>-- Manage Snippets --</span></a></li>");
        $menuQ.find('a.firstlevel').click(function (e) {
            e.preventDefault();
            frmSnippet();
        });
        var counter;
        var keys = sortedSnippetKeys();
        for (counter=0; counter<keys.length; counter++) {
            key = keys[counter];
            var snippet = config.snippets[key];
            if (snippet.body !== "") {
                $menuQ.append("<li class='snip_menu'><a href='javascript:void(0);' class='snip_menu snippet_link_outer' id='snip-" + snippet.id + "'><span class='snippet_link'>" + snippet.name + "</span></a></li>");
                $menuQ.find('#snip-' + snippet.id).click(function (e) {
                    pasteSnippet($(this));
                    e.preventDefault();
                    return false;
                });
            }
        }
        if (newMenu) {
            //console.log($menuQ.html() );
            $copyTo.after($menuQ);
        }
    }
}

/* =========================== */

/* =========================== */
/* Link Insertion              */
/* =========================== */

function frmInsertLinkBody() {
    log("functiontrace","Start Function");
    var strBody = "<p><strong>Insert Link</strong></p>";
    strBody += "<table style='width: 100%;'>";
    strBody += "<tr>";
    strBody += " <th style='vertical-align: top; text-align: right;'>URL:</th>";
    strBody += " <td><input name='insertLinkURL' id='insertLinkURL' type='text' /></td>";
    strBody += "</tr>";
    strBody += "<tr>";
    strBody += " <th style='vertical-align: top; text-align: right;'>Text:</th>";
    strBody += " <td><input name='insertLinkName' id='insertLinkName' type='text' "
    strBody += " /></td>";
    strBody += "</tr>";
    strBody += "<tr>";
    strBody += " <td colspan='2'><center><button value='Set' id='insertLinkButton'>Insert</button></center></td>";
    strBody += "</tr>";
    strBody += "</table>";
    return strBody;
}

function frmInsertLinkButtons(strTextArea) {
    log("functiontrace","Start Function");
    $('button#insertLinkButton').click( function(e) {
        e.preventDefault();
        addTag('','[' + $('#insertLinkName').val() + '](' + $('#insertLinkURL').val() + ')',false,strTextArea, true);
        $('#charpop').dialog( "close" )
    });
}

function frmInsertLink(strTextBox, strLinkText) {
    log("functiontrace","Start Function");
    var strBody = frmInsertLinkBody();
    throwModal("Insert Link",strBody);
    if (strLinkText) {
        $('#insertLinkName').val(strLinkText);
    }
    frmInsertLinkButtons(strTextBox);
}
/* =========================== */

/* =========================== */
/* User Notes                  */
/* =========================== */
function displayUserNote($this) {
    intNoteCount++;
    var strSlug;
    var strName;
    var aryURL = $this.prop("href").split("/");
    var objNote;
    var strNoteIcon = "bookmark_border";
    var strNoteText = "User Notes";
    strSlug = aryURL[aryURL.length-1];
    if (strSlug=="") {
        strSlug = aryURL[aryURL.length-2];
    }
    strSlug = strSlug.trim().toLowerCase();
    strName = $this.text();
    objNote = config.userNotes[strSlug];
    if (objNote !== undefined) {
        strNoteText = objNote.note;
        switch (objNote.noteType) {
            case "note":
                strNoteIcon = 'chat';
                break;
            default:
                strNoteIcon = objNote.noteType;
        }
    }
    var $btn = $("<i id='btnUserNotes-" + intNoteCount + "-" + strSlug + "' class='material-icons btnUserNotes' style='cursor: pointer; font-size: 16px;' title='" + htmlEncode(strNoteText) + "'>" + strNoteIcon + "</i>");
    $btn.click(function() {
        frmUserNote(strSlug, strName, strNoteIcon);
    });
    $this.after($btn);
}

function displayUserNotes() {
    $('i.btnUserNotes').remove();
    $("a").each(function() {
        var $href = $(this).prop("href");
        if ($href.indexOf('user/') > -1 && $href.indexOf('/me') === -1) {
            if ($(this).parent().parent().prop("class") == 'header-dropdown-menu') {
                // Not in the menus
            } else {
                // Don't place after image links
                if ($(this).find("img").length == 0) {
                    displayUserNote($(this));
                }
            }
        } else {
        }
    });
}

function saveUserNotes() {
	saveConfig();
}

function frmUserNotesBody(strSlug,strName, strType) {
    log("functiontrace","Start Function");
    var strNote;
    var strNoteType;
    var objNote = config.userNotes[strSlug];
    if (objNote === undefined) {
        strNote = "";
        strNoteType = "";
    } else {
        strNote = objNote.note;
        strNoteType = objNote.noteType;
    }
    var strBody = "<p><strong>User Notes: " + strName + "</strong></p>";
    strBody += "<table style='width: 100%;'>";
    strBody += "<tr>";
    strBody += " <th style='vertical-align: top; text-align: right;'>Note type:</th>";
    strBody += " <td>"
    for (counter=0; counter<userNoteTypes.length; counter++) {
        strBody += "<label for='noteType" + userNoteTypes[counter] + "' class='material-icons' style='font-size: 16px;'>" + userNoteTypes[counter] + "</label>";
         strBody += "<input type='radio' name='noteType' class='noteType' id='noteType" + userNoteTypes[counter] + "' value='" + userNoteTypes[counter] + "'";
        if (strType == userNoteTypes[counter]) {
            strBody += " checked='checked'";
        }
        strBody += "> ";
    }
    strBody += "</td>";
    strBody += "</tr>";
    strBody += "<tr>";
    strBody += " <th style='vertical-align: top; text-align: right;'>Notes:</th>";
    strBody += " <td><textarea id='snippetBody' rows='3' style='width: 95%;'>"
    strBody += strNote;
    strBody += "</textarea></td>";
    strBody += "</tr>";
    strBody += "<tr>";
    strBody += " <td colspan='2'><center><button value='Set' id='setUserNote'>Set</button> | <button value='Delete' id='deleteUserNote'>Delete</button></center></td>";
    strBody += "</tr>";
    strBody += "</table>";
    return strBody;
}

function frmUserNoteButtons(strSlug,strName) {
    log("functiontrace","Start Function");
    $('button#setUserNote').click( function(e) {
        e.preventDefault();
        setUserNote(strSlug,strName,$('#snippetBody').val(),$('input.noteType:checked').val());
        $('#charpop').dialog( "close" )
    });
    $('button#deleteUserNote').click( function(e) {
        e.preventDefault();
        deleteUserNote(strSlug);
        $('#charpop').dialog( "close" )
    });
}

function deleteUserNote(strSlug) {
    delete config.userNotes[strSlug];
    saveUserNotes();
    displayUserNotes();
}

function setUserNote(strSlug,strName,strBody, strType) {
    var note = {};
    note.note = strBody;
    note.noteType = strType;
    config.userNotes[strSlug] = note;
    saveUserNotes();
    displayUserNotes();
}

function frmUserNote(strSlug,strName, strType) {
    log("functiontrace","Start Function");
    var strBody = frmUserNotesBody(strSlug,strName, strType);
    var strID = "";
    throwModal("User Note for " + strName,strBody);
    frmUserNoteButtons(strSlug,strName);
}

function htmlEncode(value){
  //create a in-memory div, set it's inner text(which jQuery automatically encodes)
  //then grab the encoded contents back out.  The div never exists on the page.
  var strValue;
  strValue = $('<div/>').text(value).html();
  strValue = strValue.replace("'","&apos;");
  return strValue;
}

function htmlDecode(value){
  return $('<div/>').html(value).text();
}
/* =========================== */

function bannnerInfoClick() {
	if (config.Game.bannerLink) {
		var $bg = $(".bg img");
		var bg = $bg[0];
		var strLinks = "<li style='padding-left: 10px;'><a href='" + bg.src + "' id='gm-slackchars-nav'><i class='material-icons'>image</i></a></li>";
		$('ul.nav-links').prepend(strLinks);
	}
}

function hideFavButtons() {
	if (config.root.hideFavButtons) {
		$("div.fav-button").remove();
	}
}

function buildCardString(thisChar,cardType) {
	var strReturn = "";
	var cardList = thisChar["v_" + cardType + "Cards"];
	var aryCards = [];
	if (cardList.length > 0) {
		for (var i = 0; i < charCount; i++) {
			if (typeof cardList[i] != 'undefined') {
				aryCards.push(cardList[i].name + " " + cardList[i].v_playsRemaining);
				//strReturn += cardList[i].name + " " + cardList[i].v_playsRemaining + ", ";
			}
		}
		if (aryCards.length > 0) {
			strReturn = "<span style='margin-bottom: 5px; display: block;'>iconReplaceMe " + aryCards.join(", ") + "</span>";
		}
	}
	return strReturn;
}

function getObjectUp(baseObject,objectName) {
		if (baseObject.hasOwnProperty(objectName)) {
			return baseObject[objectName];
		} else if (baseObject.$parent.hasOwnProperty(objectName)) {
			return baseObject.$parent[objectName];
		} else if (baseObject.$parent.$parent.hasOwnProperty(objectName)) {
			return baseObject.$parent.$parent[objectName];
		}
}

// Helper functions
function cardIconForNamespace(strNamespace) {
    return unsafeWindow.angular.element("body").scope().helpers.cardIconForNamespace(strNamespace);
}
// =================

// Config Settings

// Returns true if a setting hsa been set, false otherwise
function updateConfig(controlID) {
	var $control = $("div#page").find("#" + controlID);
	var aID = controlID.split("-");
	var catID = aID[3];
	var settingID = aID[4];
	if ($control.hasClass("gm-settings-control-bool")) {
		config[catID][settingID] = $control[0].checked;
	} else if ($control.hasClass("gm-settings-control-int")) {
        var intVal = parseInt($control.val());
        if ($.isNumeric("" + intVal)) {
            if (config_display[catID][settingID].hasOwnProperty("min") && intVal < config_display[catID][settingID].min) {
                console.log("There's a minimum and " + intVal + " < " + config_display[catID][settingID].min);
                return false;
            }
            if (config_display[catID][settingID].hasOwnProperty("max") && intVal > config_display[catID][settingID].max) {
                console.log("There's a maximum and " + intVal + " > " + config_display[catID][settingID].max);
                return false;
            }
            config[catID][settingID] = intVal;
        } else {
            console.log("Not an integer: " + $control.val())
            return false;
        }
	} else {
		config[catID][settingID] = $control.val();
	}
    return true;
}

function editConfig_cabbit($page) {
    var $newcat = $("<div class='gm-settings-cat well' id='gm-settings-cat-cabbit'></div>");
    $newcat.append("<h3 class='gm-settings-cat-title'>Cloud Storage</h3>");
    var $newSettings = $("<div class='gm-settings-cat-settings'></div>");
    var $newSetting = $("<div class='gm-settings-setting'></div>");
    $newSetting.append("<label class='gm-settings-setting-label' for='gm-settings-value-useCloud'>User cloud to store settings?</label>");
    $newSetting.append("<span class='gm-settings-setting-value' style='display: inline' ><input type='checkbox' class='gm-settings-control gm-settings-control-bool' id='gm-settings-value-useCloud'" + ((blnUseCloud) ? ' checked' : '') + "></span>");
    $newSetting.find("#gm-settings-value-useCloud").change($.debounce(500, function(e) {
        blnUseCloud = this.checked;
        GM_setValue("useCloud",blnUseCloud);
        if (blnUseCloud) {
            loadConfig(saveConfig);
        }
	}));
    $newSettings.append($newSetting);
    $newcat.append($newSettings);
    $page.append($newcat);
}

function frmExportConfigBody() {
    log("functiontrace","Start Function");
    var strBody = "<p><span style='color: red; font-weight: bold;'>Warning</span> - this is a perilous section... it's handy for moving settings from one browser to another or backing them up but it has potential to mess your settings up.  Proceed with caution.</p>";
	strBody += "<textarea id='txtExportConfig' style='width: 100%; height: 10em;'>"
	strBody += JSON.stringify(config);
    strBody += "</textarea>";
    strBody += "<button id='exportConfig'>Export to Clipboard</button> | <button id='importConfig'>Import from TextBox</button>"
    return strBody;
}

function frmExportConfigButtons() {
    log("functiontrace","Start Function");
    $('button#importConfig').click( function(e) {
        e.preventDefault();
		    config = JSON.parse($('textarea#txtExportConfig').val());
        saveConfig(function() {
          location.reload();
        })
    });
    $('button#exportConfig').click( function(e) {
        e.preventDefault();
		    GM_setClipboard(JSON.stringify(config),'text');
		    alert("Copied config to clipboard");
    });
}

function frmExportConfig() {
    log("functiontrace","Start Function");
    var strBody = frmExportConfigBody();
    throwModal("Import / Export Settings",strBody);
    frmExportConfigButtons();
}

function editConfig() {
    GM_addStyle(".gm-settings-cat { float: left; display: block; border: thin solid black; padding: 10px; margin: 10px; background-color: #c5c5c5}");
    GM_addStyle(".gm-settings-cat-settings { margin-left: 10px; }");
    GM_addStyle(".gm-settings-setting { margin-bottom: 15px; border-bottom: thin solid gray; width: auto; padding-bottom: 5px; verical-align: top;}");
    GM_addStyle(".gm-settings-setting-label { margin-right: 10px; font-weight: bold; max-width: 15rem; display: inline-block;}");
    GM_addStyle(".gm-settings-control-int { width: 4rem; }");
    var $page = $("div#page");

    $page.css("max-width","initial");
	var $title = $("<h2>Script Settings (v" + scriptVerShort + ") <button id='importExportConfig'>Import / Export</button></h2>");
    $page.html($title);
	$('button#importExportConfig').click(function() {
		frmExportConfig()
	});
    for (var key in config_display) {
        var confd = config_display[key];
        var $newcat = $("<div class='gm-settings-cat well' id='gm-settings-cat-" + key + "'></div>");
        $newcat.append("<h3 class='gm-settings-cat-title'>" + confd.displayName + "</h3>");
        var $newSettings = $("<div class='gm-settings-cat-settings'></div>");
        for (var key2 in confd) {
            var $newSetting = $("<div class='gm-settings-setting'></div>");
            var setting = confd[key2];
            var val = config[key][key2];
            if (setting.text) {
                $newSetting.append("<label class='gm-settings-setting-label' for='gm-settings-value-" + key + "-" + key2 + "'>" + setting.text + "</label>");
                switch (setting.type) {
                    case "bool":
                        $newSetting.append("<span class='gm-settings-setting-value' style='display: inline' ><input type='checkbox' class='gm-settings-control gm-settings-control-bool' id='gm-settings-value-" + key + "-" + key2 + "'" + ((val) ? ' checked' : '') + "></span>");
                        break;
                    case "int":
                        var min = '';
                        if (setting.hasOwnProperty('min')) {
                            min = " min='" + setting.min + "'";
                        }
                        var max = '';
                        if (setting.hasOwnProperty('max')) {
                            max = " max='" + setting.max + "'";
                        }
                        $newSetting.append("<span class='gm-settings-setting-value'><input type='number' class='gm-settings-control gm-settings-control-int' id='gm-settings-value-" + key + "-" + key2 + "' value='" + val + "'" + min + max + "></span>");
                        break;
                    case "select":
                        var $select;
                        $select = $("<select class='gm-settings-control gm-settings-control-select' id='gm-settings-value-" + key + "-" + key2 + "'>");
                        for (var i = 0; i < setting.select.length; i++) {
                            var selKey = setting.select[i];
                            $select.append("<option value='" + selKey + "'" + ((val === selKey) ? " selected" : "") + ">" + selKey + "</option>");
                        }
                        $newSetting.append($select);
                        break;
                    case "textbox":
                        $newSetting.append("<textarea class='gm-settings-control gm-settings-control-text' id='gm-settings-value-" + key + "-" + key2 + "'>" + val + "</textarea>");
                        break;
                    case "text":
                    default:
                        $newSetting.append("<span class='gm-settings-setting-value'><input type='text' class='gm-settings-control gm-settings-control-textbox' id='gm-settings-value-" + key + "-" + key2 + "' value='" + val + "'></span>");
                        break;
                }
                $newSettings.append($newSetting);
            }
        }
        $newcat.append($newSettings);
		$page.append($newcat);
    }
	$page.find(".gm-settings-control").change($.debounce(500, function(e) {
		if (updateConfig(e.target.id)) {
            saveConfig();
        };
	}));
	$page.find(".gm-settings-control-text, .gm-settings-control-int, .gm-settings-control-textbox").keyup($.debounce(500, function(e) {
		if (updateConfig(e.target.id)) {
            saveConfig();
        };
	}));
    editConfig_cabbit($page);
}

function simpleHash(str) {
    var hash = 0;
    if (str.length == 0) {
        return hash;
    }
    for (var i = 0; i < str.length; i++) {
        var char = str.charCodeAt(i);
        hash = ((hash<<5)-hash)+char;
        hash = hash & hash; // Convert to 32bit integer
    }
    return hash;
}

async function saveConfig(andThen) {
	config.version = GM_info.script.version;
	config.savedWhen = new Date();
    // Always save locally, even if we're saving to the cloud.
    GM_setValue("config",JSON.stringify(config));
    // Cloud stuff
    if (blnUseCloud) {
        var strData = JSON.stringify(config);
        var strURL = "https://cabbit.org.uk/eli/?site=storium&user=" + usr.id + "|auto&hash=" + simpleHash(usr.id + "|auto");
        await GM_xmlhttpRequest({
            method: "POST",
            url: strURL,
            data: strData,
            headers: {
                "Content-Type": "application/json"
            },
            onload: function (response) {
                log("Saved to cloud","cloud");
                if (andThen) {
                    andThen();
                }
            }
        });
    } else {
        if (andThen) andThen();
    }
}

function getScope() {
    var scope;
    if (unsafeWindow.angular.element("game-page").length > 0) {
        scope = unsafeWindow.angular.element("game-page").scope();
    } else if (unsafeWindow.angular.element("div#page").length > 0) {
        scope = unsafeWindow.angular.element("div#page").scope();
    } else {
        scope = unsafeWindow.angular.element("div#page").scope();
    }
    return scope;
}

async function loadConfig(andThen) {
    // Load from local storage in case cloud loading fails
    var strConf = GM_getValue("config","");
    if (strConf === "") {
        config = {};
    } else {
        config = JSON.parse(strConf);
    }
    // Cloud stuff
    if (blnUseCloud) {
        var strURL = "https://cabbit.org.uk/eli/index.php?user=" + usr.id + "|auto&site=storium&hash=" + simpleHash(usr.id + "|auto")
        await GM_xmlhttpRequest({
            method: "GET",
            url: strURL,
            headers: {
                "Content-Type": "application/json"
            },
            onload: function (response) {
                var data = JSON.parse(response.responseText);
                if (data.status === "ok") {

                    config = JSON.parse(data.settings);
                    log("Loaded from cloud: " + strURL,"cloud");
                    if (andThen) {
                        andThen();
                    }
                } else {
                    console.log("An error occurred" + data.errorMsg);
                }
            }
        });
    } else {
        if (andThen) andThen();
    }
}

function initConfigCategory(configCat, catDisplayName) {
    if (!config[configCat]) {
        config[configCat] = {
            loaded: new Date()
        };
    }
    config_display[configCat] = {
        displayName: catDisplayName,
        loaded: new Date()
    }
}

function initConfigItem(itemCat, itemName, defaultValue, displaySettings) {
    if (config[itemCat]) {
        if (!config[itemCat].hasOwnProperty(itemName)) {
            config[itemCat][itemName] = defaultValue;
        }
    }
    config_display[itemCat][itemName] = displaySettings;
}


async function initConfig(andThen) {
    config = {};
    config_display = {};
    await loadConfig()
        // Settings categories
        initConfigCategory("root","General");
        initConfigCategory("Homepage","Homepage");
        initConfigCategory("Notifications","Notifications Page");
        initConfigCategory("Game","Game Page");
        initConfigCategory("Profile","Profile Page");
        initConfigCategory("Speech","Speech Auto-Styling");
        initConfigCategory("Trello","Trello");
        initConfigCategory("Img","Image Search");
        initConfigCategory("Notes","Notes");
        initConfigCategory("GreenRoom","Green Room");
        initConfigCategory("Bookmarks","Last Read Markers");
        initConfigCategory("Comments","Comment Formatting");
        initConfigCategory("SettingInfo","Setting Information");

        // Root-level settings
        initConfigItem("root","tasks", false, {text: "Tasks?", type: "bool" });
        initConfigItem("root","userNotes", false, {text: "User Notes?", type: "bool" });
        initConfigItem("root","hideFavButtons",false, {text: "Hide favourite buttons?", type: "bool" });
        initConfigItem("root","fontSubst","None", {text: "Replace main font with:", type: "select", select: fontSubstAvailable});
        initConfigItem("root","theme",'None', {text: "Use theme:", type: "select", select: stylesAvailable });
        initConfigItem("root","LFPStyle", "", {text: "LFP indicator styling", type: "text" });
        initConfigItem("root","BGCol", "", {text: "Custom background colour", type: "text" });
        initConfigItem("root","IconCol", "", {text: "Custom comment refresh icon colour", type: "text" });

        // Green room settings
        initConfigItem("GreenRoom","cardTags",false, {text: "Show card tags?", type: "bool" });

        // Setting Info settings
        initConfigItem("SettingInfo","on",false, {text: "Show setting info?", type: "bool" });

        // Homepage settings
        initConfigItem("Homepage","showAsGrid", true, {text: "Reformat Homepage using CSS Grid?", type: "bool" });
        initConfigItem("Homepage","maxPages", 5, {text: "Max notification pages to load", type: "int", min: 1, max: 20 });
        initConfigItem("Homepage","maxNotes", 20, {text: "Max notes on homepage", type: "int", min: 1, max: 99 });
        initConfigItem("Homepage","refreshMinutes", 0, {text: "No of minutes for refresh (0 = no refresh, min of 10 otherwise)", type: "int", min: 0, max: 999 });
        config.Homepage.refreshSeconds = (parseInt(config.Homepage.refreshMinutes)*60)
        if (config.Homepage.refreshSeconds>0) {
            config.Homepage.refreshSeconds = Math.max(config.Homepage.refreshSeconds,600);
        }
        initConfigItem("Homepage","hideTimer", false, {text: "Hide timer from homepage?", type: "bool" });
        //initConfigItem("Homepage","removeIntro", false, {text: "Remove intro text from homepage?", type: "bool" });
        initConfigItem("Homepage","hideOfficialResources", false, {text: "Remove official resources from homepage?", type: "bool" });
        initConfigItem("Homepage","hideCommunityResources", false, {text: "Remove community resources from homepage?", type: "bool" });
        initConfigItem("Homepage","hideActiveGames", false, {text: "Remove active games from homepage?", type: "bool" });
        initConfigItem("Homepage","hideLatestDiscussions", false, {text: "Remove latest discussions from homepage?", type: "bool" });
        //initConfigItem("Homepage","hideQuotes", false, {text: "Remove quotes from homepage?", type: "bool" });
        // ToDo - needs fixing
        //initConfigItem("Homepage","showYourLastMoves", true, {text: "Show your last moves on homepage?", type: "bool" });
        initConfigItem("Homepage","compact",'Original', {text: "Homepage style:", type: "select", select: ['Original','Semi-Compact','Compact']});
        switch (config.Homepage.compact) {
            case "Compact":
                config.Homepage.isCompact = true;
                config.Homepage.compactType = 0;
                break;
            case "Semi-Compact":
                config.Homepage.isCompact = true;
                config.Homepage.compactType = 1;
                break;
            case "Original":
            default:
                config.Homepage.isCompact = false;
                config.Homepage.compactType = 0;
                break;
        }
        initConfigItem("Homepage","sortStyle",'Original', {text: "Game sorting:", type: "select", select: ['Standard','Without Articles','None']});
        switch (config.Homepage.sortStyle) {
            case "None":
                config.Homepage.sortStyleCode = "N";
                break;
            case "Without Articles":
                config.Homepage.sortStyleCode = "A";
                break;
            case "Standard":
            default:
                config.Homepage.sortStyleCode = "S";
                break;
        }
        initConfigItem("Homepage","showSlackChars", false, {text: "Show slack characters link?", type: "bool" });
        initConfigItem("Homepage","condenseNotifications", true, {text: "Condense notification list?", type: "bool" });
        config.Homepage.removeUpgradeLink = false;

        // Notifications page settings
        initConfigItem("Notifications","condense", true, {text: "Condense notifications", type: "bool" });
        initConfigItem("Notifications","maxPages", 5, {text: "Max pages of notifications to load", type: "int", min: 0, max: 20 });
        initConfigItem("Notifications","refreshMinutes", 0, {text: "No of minutes for refresh (0 = no refresh, min of 10 otherwise)", type: "int", min: 0, max: 999 });
        config.Notifications.refreshSeconds = (parseInt(config.Notifications.refreshMinutes)*60)
        if (config.Notifications.refreshSeconds > 0) {
            config.Notifications.refreshSeconds = Math.max(config.Notifications.refreshSeconds,600);
        }
        initConfigItem("Notifications","hideTimerNotes", false, {text: "Hide timer from notifications page?", type: "bool" });
        initConfigItem("Notifications","showAvatars", true, {text: "Show avatars on notifications list?", type: "bool" });

        // Game page settings
        initConfigItem("Game","processLocationTags", true, {text: "Process location tags (**[...]**)?", type: "bool" });
        initConfigItem("Game","processCensorTags", true, {text: "Process censor tags (+[...]+)?", type: "bool" });
        initConfigItem("Game","tagStyle", "font-size: 120%; color: blue;", {text: "Style to use when processing location tags:", type: "textbox" });
        initConfigItem("Game","hidePreviewButton", false, {text: "Hide preview button?", type: "bool" });
        blnPreviewHidden = config.Game.hidePreviewButton;
        initConfigItem("Game","sortChars", "Default", {text: "Sort Characters?", type: "select", select: ['Default','Last Move','Last Move (Desc)','Name','Name (Desc)']});
        initConfigItem("Game","addTOCTo", "On Page", {text: "Table of Contents?", type: "select", select: ['None','Game Info','On Page']});
        initConfigItem("Game","signalPreview", true, {text: "Signal Preview?", type: "bool" });
        initConfigItem("Game","signalPreviewColour", 'thistle', {text: "Colour to signal preview?", type: "text" });
        initConfigItem("Game","hideGameImages", false, {text: "Hide images?", type: "bool" });
        initConfigItem("Game","moveWC", true, {text: "Word count for moves?", type: "bool" });
        initConfigItem("Game","entryWC", true, {text: "Word count for entries?", type: "bool" });
        initConfigItem("Game","gameUKDates", false, {text: "Convert dates to UK format?", type: "bool" });
        initConfigItem("Game","formatButtons", true, {text: "Formatting buttons?", type: "bool" });
        initConfigItem("Game","biggerPopoverAvatars", true, {text: "Bigger character avatars?", type: "bool" });
        initConfigItem("Game","cleanImages", true, {text: "Clean images on hover?", type: "bool" });
        initConfigItem("Game","bannerLink", false, {text: "Add banner link to menu?", type: "bool" });
        initConfigItem("Game","getCards", false, {text: "Enable get cards?", type: "bool" });
        initConfigItem("Game","showGamePid", false, {text: "Show game ID?", type: "bool" });
        initConfigItem("Game","linkGamePid", false, {text: "Link game ID?", type: "bool" });
        initConfigItem("Game","minorEditDefault", false, {text: "Make minor edit the default?", type: "bool" });
        initConfigItem("Game","focusMode", false, {text: "Show focus mode?", type: "bool" });
        initConfigItem("Game","lastEntryButton", false, {text: "Show last entry button?", type: "bool" });
        initConfigItem("Game","imgPopCard",false, {text: "Click to pop up full sized images from cards?", type: "bool" });
        initConfigItem("Game","moveLinkCSS","color: red; text-decoration: underline;", {text: "CSS for links in moves:", type: "textbox" });
        initConfigItem("Game","moveLinkHoverCSS","color: blue;", {text: "CSS for links in moves (on hover):", type: "textbox" });
        initConfigItem("Game","rainbowActiveChar", false, {text: "Rainbow border for active char?", type: "bool" });
        initConfigItem("Game","autoMerge", true, {text: "Automerge on new moves?", type: "bool" });
        initConfigItem("Game","directLinks", true, {text: "Show move direct links?", type: "bool" });

        // Game page settings
        initConfigItem("Profile","wide", true, {text: "Widen profile table?", type: "bool" });
        initConfigItem("Profile","showAge", true, {text: "Show storium age?", type: "bool" });

  // Speech Auto-Styling Settings
        initConfigItem("Speech","styleSpeech", true, {text: "Automatically style speech?", type: "bool" });
        initConfigItem("Speech","incQuotes", true, {text: "Restyle the quotes as well?", type: "bool" });
        initConfigItem("Speech","CSS", 'color: blue;', {text: "Style to apply for speech?", type: "textbox" });
        initConfigItem("Speech","styleThought", false, {text: "Automatically style thought (using tildas)?", type: "bool" });
        initConfigItem("Speech","CSSThought", 'color: purple; font-style: italic;', {text: "Style to apply for thought?", type: "textbox" });

        // Trello settings
        initConfigItem("Trello","integrateTrello", false, {text: "Integrate Trello?", type: "bool" });
        initConfigItem("Trello","boardName", "Storium", {text: "Board Name", type: "text" });
        initConfigItem("Trello","statusHome", true, {text: "Trello status on front page?", type: "bool" });
        initConfigItem("Trello","statusNote", true, {text: "Trello status on notifications page?", type: "bool" });

        // Image search options
        initConfigItem("Img","imgSearch", true, {text: "Image Search Links?", type: "bool" });
        initConfigItem("Img","imgSearchGoogle", true, {text: "Google?", type: "bool" });
        // Fuck Russia
        // initConfigItem("Img","imgSearchYandex", true, {text: "Yandex?", type: "bool" });
        initConfigItem("Img","imgSearchPixabay", true, {text: "Pixabay?", type: "bool" });
        initConfigItem("Img","imgSearchPin", true, {text: "Pinterest?", type: "bool" });
        initConfigItem("Img","imgSearchUnsplash", true, {text: "Unsplash?", type: "bool" });

        // Notes options
        initConfigItem("Notes","gameNotes", true, {text: "Game notes?", type: "bool" });
        initConfigItem("Notes","charNotes", true, {text: "Character notes?", type: "bool" });

        // Mark last read
        initConfigItem("Bookmarks","markLastRead", true, {text: "Enabled?", type: "bool" });
        initConfigItem("Bookmarks","CSS","border-radius: 25px; background: linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet, red);;width:100%; height: 2em;  margin-top: 5px; vertical-align: middle; text-align: center; color: white; font-size: 120%; line-height: 2em;", {text: "CSS for bookmark:", type: "textbox" });
        initConfigItem("Bookmarks","Text","<---- Read to Here ---->", {text: "Text for bookmark:", type: "text" });

        // Comment Formatting
        initConfigItem("Comments","format",false, {text: "Format Comments?", type: "bool" });
        initConfigItem("Comments","markdown",true, {text: "Process markdown?", type: "bool" });
        initConfigItem("Comments","authorCSS","display: block !important; width: 100%; text-align: center; background-color: beige; margin-bottom: 5px !important;", {text: "Author CSS", type: "textbox" });
        initConfigItem("Comments","authorCSSNarrator","background-color: aliceblue;", {text: "Author CSS (Narrator)", type: "textbox" });
        initConfigItem("Comments","authorCSSHost","background-color: aliceblue;", {text: "Author CSS (Host)", type: "textbox" });
        initConfigItem("Comments","authorCSSYou","display: block !important; width: 100%; text-align: center; border-radius: 25px; background: linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet, red); vertical-align: middle; color: white;", {text: "Author CSS (You)", type: "textbox" });


        if (!config.gameNotes) {
            config.gameNotes = {};
        }

        if (!config.exNotes) {
            config.exNotes = {};
        }

        if (!config.userNotes) {
            config.userNotes = {};
        }

        if (!config.snippets) {
            config.snippets = {};
        }

        if (!config.lfpGames) {
            config.lfpGames = {};
        }

        if (!config.charimgcache) {
            config.charimgcache = {};
        }

        if (!config.myGames) {
            config.myGames = {};
        }

        if (!config.tasks) {
            config.tasks = [];
        }

        if (!config.lastCheck) {
            config.lastCheck = (new Date()).toISOString();
        }
        lastCheck = new Date(config.lastCheck);

        saveConfig();
}

// =================

// Update signalling
function signalUpdate() {
	if (config.prevVersion !== config.version) {
		var $updateBanner = $("<div style=\"background-color: #FFFF99; width: 100%; padding: 10px\" id='gm-config-update-banner'></div>");
		$updateBanner.append("<span>The Storium Improver script has been updated to v" + config.version + "</span>, ");
		$updateBanner.append("<a href='/forums#/category/advice/thread/8v28jy'>click here</a> to view notes on the release. ");
		//var $closeButton = $("<span style='float: right;margin-right: 20px;font-weight: bold;' id='gm-close-update-notification' class='fakelinkNoCol'>X</span>");
		//$updateBanner.append($closeButton);
		$updateBanner.click(function(e) {
			config.prevVersion = config.version;
			saveConfig();
			$("#gm-config-update-banner").remove();
		});
		$("div#header").append($updateBanner);
	}
}
// =================

function gotoEntry(entryNo) {
      $([document.documentElement, document.body]).animate({
          scrollTop: $("div.entry:eq(" + entryNo + ")").offset().top
      }, 500);
}

function gotoGameBookmark() {
  var idx = fullURL.indexOf("#");
  if (idx != -1) {
    var intBM = parseInt(fullURL.split('#')[1]);
      setTimeout(function() {
      console.log($("div.entry:eq(" + intBM + ")"));
      gotoEntry(intBM);
      console.log("Bookmark to entry #" + intBM);
    }, 3000);
  }
}

// Sorting of banner groups for homepage
function sortBannerGroups() {
    var $grps = $('div.banner-group');
    $grps.each(function() {
      var grp = $(this);
      var games = grp.children('div');
      if (config.Homepage.sortStyleCode != "N") {
        games.detach().sort(function(a,b) {
          var aname = $(a).find("h2").text().toLowerCase();
          var bname = $(b).find("h2").text().toLowerCase();
          var asort, bsort;
          switch (config.Homepage.sortStyleCode) {
            case "A":
              asort = removeArticles(aname);
              bsort = removeArticles(bname);
              break;
            default:
              asort = aname;
              bsort = bname;
          }
          return (asort > bsort);
        });
        grp.append(games);
      }
    });
}

// Place button for editing setting info onto the host tools buttons on green room
function insertSettingInfoButton() {
  if (!config.SettingInfo.on) {
    return false;
  }
  let pid = document.URL.split("/")[4];
  let filehash = pid + "-" + Math.abs(simpleHash(pid));
  let $hostMgtBtns = $("div.host-tool-btns");
  if ($hostMgtBtns.length > 0 ) {
    let $siButton = $hostMgtBtns.find("#ssiSiButton");
    if ($siButton.length <= 0) {
      $siButton = $("<span id='ssiSiButton'><button class='flat-btn bg3'>Setting Info</button></span>");
      $siButton.click(function(e) {
        var win = window.open('https://cabbit.org.uk/storium-settinginfo/#' + filehash, '_blank');
      });
      $hostMgtBtns.append($siButton);
    }
  } else {
//    setTimeout(function() {
//      insertSettingInfoButton();
//    },500);
  }
}

// Place setting info tab 
function insertSettingTab() {
  if (!config.SettingInfo.on) {
    return false;
  }
  let settingData = ""
  let $tabsUL = $("div.gameplay-green-room-tabs ul");
  if ($tabsUL.length > 0) {
    let $li = $("<li role='presentation' id='ssiGameSettingTab'></li>")
    let $a = $("<a id='tab_ssiGameSetting' role='tab' class='tab' style='' href='#'>Setting Info</a>")
    let $banner = $("#panel_game div.nu_banner").clone();
    let pid = document.URL.split("/")[4];
    let filehash = pid + "-" + Math.abs(simpleHash(pid));
    $banner.find(".details").remove();
    $banner.find(".upper-button-cell").remove();

    $li.append($a);
    $("#tab_greenRoom").click(function() {
      $("#panel_ssiGameSetting").remove();
      if (strLastTab == "setting") {
        if (window.location.href.indexOf("/green-room") > -1) {
          location.reload();
        } else {
          //console.log("URL: " + document.URL);
        }
      }
      strLastTab = "green";
    });
    $("#tab_game").click(function() {
      $("#panel_ssiGameSetting").remove();
      $("#panel_game").removeClass("ng-hide");
      strLastTab = "game";
    });

    $tabsUL.append($li)

    $a.click(function(e) {
      e.preventDefault();
      strLastTab = "setting";
      $("div#panel_game").addClass("ng-hide");
      $("div#panel_greenRoom").remove();
      $("div#panel_ssiGameSetting").remove();

      let $settingDiv = $("<div id='panel_ssiGameSetting' role='tabpanel' ></div>");
      $settingDiv.append($banner);
      $("div.gameplay-green-room-tabs").after($settingDiv);

      GM_xmlhttpRequest({
          method: "GET",
          url: "https://cabbit.org.uk/storium-settinginfo/data/" + filehash,
          headers: {
              "Content-Type": "application/json"
          },
          onload: function (response) {
              if (response.status == 200) {
                settingData = response.responseText;
                $setting = $(settingData);
                // Deal with img height and width attributes
                $setting.find("img").each(function() {
                  console.log("=== img ===")
                  if ($(this).attr("width")) {
                    $(this).css("width",$(this).attr("width") + "px");
                  }
                  if ($(this).attr("height")) {
                    $(this).css("height",$(this).attr("height") + "px");
                  }
                });
                $settingDiv.append($setting);
              } else {
                $settingDiv.append("<p>The host has not provided setting information for this game</p>");
              }
          },
          onerror: function() {
            $settingDiv.append("<p>Error getting information to load</p>");
          }
      });

    });
    insertSettingInfoButton();
    setInterval(function() {
      insertSettingInfoButton();
    }, 2000);
  } else {
    setTimeout(function() {
      insertSettingTab();
    }, 500);
  }
}

// The main body of what we are doing.  Pulled together from the former init() function and the main (document).ready
function main() {
	signalUpdate()
   switch (pageType) {
       case "gm-settings":
           // This is out settings page for the config
           editConfig();
           break;
        case "home":
			hideFavButtons();
			objCharsForSlack = getCharsForSlack();
			if (config.Trello.integrateTrello) {
				// This is where we build up our cards
				ifTrelloReady(function() {
					for (var i = 0; i < objCharsForSlack.lines.length; i++) {
						var obj = objCharsForSlack.lines[i];
						var blCreateCard = true;
						for (var j = 0; j < unsafeWindow.aryTrelCards.length; j++) {
							var objCard = unsafeWindow.aryTrelCards[j];
							if (objCard.name == obj.name) {
								blCreateCard = false;
								break;
							}
						}
						if (blCreateCard) {
							unsafeWindow.addCard(obj.name,obj.url,obj.bannerURL);
						}
					}
					for (var i2 = 0; i2 < objCharsForSlack.gmLines.length; i2++) {
						var obj2 = objCharsForSlack.gmLines[i2];
						var blCreateCard2 = true;
						for (var j2 = 0; j2 < unsafeWindow.aryTrelCards.length; j2++) {
							var objCard2 = unsafeWindow.aryTrelCards[j2];
							if (objCard2.name == obj2.name) {
								blCreateCard2 = false;
								break;
							}
						}
						if (blCreateCard2) {
							unsafeWindow.addCard(obj2.name,obj2.url,obj2.bannerURL);
						}
					}
				});
			}
			charsForSlack = objCharsForSlack.fullText;
			var userID = usr.id;
			$('body').append("<form method='post' target='_blank' id='frmSlackChars' action='https://cabbit.org.uk/pad/storium_slack_characters_" + userID + "'><textarea name='text' style='display: none'>" + charsForSlack + "</textarea></form>")
			$('body').append("<form method='post' target='_blank' id='frmSlackChars-tmi' action='https://cabbit.org.uk/pad/storium_slack_characters_" + userID + "_tmi'><textarea name='text' style='display: none'>" + charsForSlack + "</textarea></form>")
            if (config.Homepage.isCompact) {
                compactView();
				if (config.Trello.integrateTrello && config.Trello.statusHome) {
					ifTrelloReady(function() {
						$("div.banner-group table tbody tr td a").each(function() {
							var strGame = $(this).text().trim();
							var objCardList = unsafeWindow.objCardLists[strGame];
							if (objCardList) {
								var strList;
								if (objCardList.card.closed) {
									strList = "-";
								} else {
									strList = objCardList.list.name;
								}
								$(this).parent().parent().append("<td style='padding: 5px;'>" + strList + "</td>");
								$(this).parent().parent().next().append("<td>&nbsp;</td>");
							}
						});
					});
				}
            } else {
                sortBannerGroups();
                setUpNotificationDivs();
				if (config.Trello.integrateTrello && config.Trello.statusHome) {
					ifTrelloReady(function() {
						$("div.banner-group table tbody tr td h2 span").each(function() {
							var strGame = $(this).text().trim();
							var objCardList = unsafeWindow.objCardLists[strGame];
							if (objCardList) {
								var strList;
								if (objCardList.card.closed) {
									strList = "-";
								} else {
									strList = objCardList.list.name;
								}
								var $td = $(this).parent().parent();
								var $h4 = $td.find("h4 span");
								if ($h4.length > 0) {
									$h4.html($h4.html() + ", <span style='font-weight: bold'>" + strList + "</span>");
								} else {
									$td.append("<h4><span style='font-weight: bold'>" + strList + "</span></h4>")
								}
							}
						});
					});
				}
            }
            refresh = config.Homepage.refreshSeconds;
            /* Put the refresh timer on the page */
            if (config.Homepage.refreshSeconds > 0 && !config.Homepage.hideTimer) {
               $('body').append(dispTimer);
            }
           if (config.Homepage.hideOfficialResources) {
				$("div.intro div.well h4:contains('Official resources')").parent().hide();
				$("div#gm-rightcol div.well h4:contains('Official resources')").parent().hide();
           }
           if (config.Homepage.hideCommunityResources) {
				$("div.intro div.well h4:contains('Community resources')").parent().hide();
				$("div#gm-rightcol div.well h4:contains('Community resources')").parent().hide();
           }
           if (config.Homepage.hideActiveGames) {
				$("div.intro div.well h4:contains('Latest active games')").parent().hide();
				$("div#gm-rightcol div.well h4:contains('Latest active games')").parent().hide();
           }
           if (config.Homepage.hideLatestDiscussions) {
				$("div.intro div.well h4:contains('Latest discussions')").parent().hide();
				$("div#gm-rightcol div.well h4:contains('Latest discussions')").parent().hide();
           }
            $("div.feed-notifs h4").after("<span id='gm-LFP' style='margin-bottom: 5px;'><a id='gm-LFP-text' href='/games/open'>LFP: Checking...</a></span>");
            addRetiredDiv();
            break;
	   case "green-room":
       case "cards":
    			hideFavButtons();
          insertSettingTab();
		   break;
		case "user":
		case "games":
			hideFavButtons();
			break;
        case "game":
		        bannnerInfoClick();
			      hideFavButtons();
            if (config.Game.hideGameImages) {	removeAllImages(); }
            /* For game page entries */
            if (config.Game.entryWC || config.Speech.styleSpeech || config.Speech.styleThought || config.Game.processLocationTags) {
                var $entries = $('div.entry-description div');
                if ($entries.length > 0) {
                    handleEntry($entries);
                }
            }
            gotoGameBookmark();
            insertSettingTab();
            break;
        case "notifications":
            refresh = config.Notifications.refreshSeconds;
            /* Put the refresh timer on the page */
            if (refresh > 0) {
               if (!config.Notifications.hideTimerNotes) {
                    $('body').append(dispTimer);
               }
               counter = setInterval(updateTimer, 1000); //1000 will  run it every 1 second
            }
            break;
    }

    switch (pageType) {
        case "user":
            var intTD = 0;
            /* User Profile Page */
            $('td').each(function () {
                intTD++;
                var $this = $(this);
                // Do we have a ul in here?
                var intUL = $this.find("li").length;
                if ( intUL > 0) {
                    // Is it the play prefs area?
                    if ($this.text().indexOf("Narration style") === -1) {
                        $this.append("<div style='margin-top: 5px; font-weight: bold;'>Total: " + intUL + "</div>");
                    }
                }
            });
            // Storium age
            if (config.Profile.showAge) {
              let $ageTable = $('table.profile-table:eq(0)');
              let $ageTitleSpan = $ageTable.find("span.smallcap:contains('Joined')")
              let $ageRow = $ageTitleSpan.parent().parent().parent();
              let $ageCell = $ageRow.find("td:eq(1)");
              let datJoined = Date.parse($ageCell.text());
              let datNow = Date.now();
              let diff = new Date(datNow -  datJoined); // in ms
              let diffY = diff.getUTCFullYear() - 1970; // Gives difference as year
              let diffM = diff.getUTCMonth(); // Gives month count of difference
              let diffD = diff.getUTCDate() - 1; // Gives day count of difference
              let strDiff = "(" + diffY + " years, " + diffM + " months, " + diffD + " days)";
              $ageCell.html($ageCell.html() + " " + strDiff);
            }
            GM_addStyle(strCSSProfile);
            if (config.Profile.wide) {
              GM_addStyle(strCSSProfileWide);
            }
            break;
        case "home":
            /* Remove intro stuff */
            if (config.Homepage.removeIntro) {
                //$("div.intro-inner h2:first").remove();
                $("div.wide-only p:first").remove();
                $("div.wide-only p:first").remove();
                $("div.wide-only img:first").remove();
            }
            getLFP();
            if (config.Homepage.condenseNotifications) {
                doNotifications();
            }
            // Put notifications in a well
            $(".feed-notifs").addClass("well").css("padding","19px");
            if (config.Homepage.showYourLastMoves) {
                // Load Issue
                /* BUG */
				/* ToDo */
                // Not working since Gamma - save the effort and don't run until we fix
               //displayWeLastPlayed();
            }
            break;
        case "notifications":
            var $table = $($("table").html());
            getLFP();
            if (config.Notifications.condense) {
                /* Clear the current rows out */
                $("table tr").remove();
                intMainRow=0;
                processTable($table);
                processPage(2);

                $("div.centered-main").prepend($("<div id='markRead' class='fakelink' style='float:left;'>Mark Read <span>(" + lastCheck.toLocaleString() + ")</span></div> <div id='showExcluded' class='fakelink' style='float:left; margin-left: 10px;'>Show Excluded</div>" +
                                                 "<div style='float: right' id='gm-LFP'><a id='gm-LFP-text' href='/games/open'>LFP: Checking...</a></div>" +
                                                 "<div id='gm-clearer' style='clear: both;'></div>").click(function(){
                    setLast();
                }));
                $("#showExcluded").click(function() {
                    displayExcluded();
                });
            }
            break;
        case "character":
            inject("function updateDescBoxData() { var thisScope = angular.element($('.ng-isolate-scope')).scope(); thisScope.wc.descriptionInput = $('.ng-isolate-scope').val(); }");
            break;
        case "forms":
        case "forum-thread":
            inject("function updateDescBoxData() { var thisScope = angular.element($('textarea#newPostBodyInput')).scope(); thisScope.newPost.body = $('textarea#newPostBodyInput').val(); }");
            break;
        case "game":
            formatCommentsCSS();
            inject("function updateDescBoxData() { var thisScope = angular.element($('.description-input')).scope(); thisScope.wc.descriptionInput = $('.description-input').val(); }");

            gamePageScope = unsafeWindow.angular.element("div#page").scope();
            var myScope = unsafeWindow.angular.element(".cast-characters").scope();
            gameScope = myScope; /* So we can pass this up to GM */
            if (config.Bookmarks.markLastRead) {
              setTimeout(function(){ setReadMoves(); }, 3000);
              setInterval(function(){ setReadMoves(); }, 60000);
            }

			var focusedScene = myScope.focusedScene;
			if (focusedScene) {
				var posts = focusedScene.entries;
				var postCount = posts.length;
				var thisPost = {};
                var post;
				for (var intPost in posts) {
					thisPost = posts[intPost];
					if (typeof charPosts[thisPost.role] != 'undefined') {
						post = charPosts[thisPost.role];
					} else {
						post = {};
						post.role = thisPost.role;
						post.wordcount = 0;
						post.postcount = 0;
						post.cardsplayed = 0;
						post.seq = 0;
					}
					post.id = thisPost.entrySeqId;
				   if (thisPost.description != null) {
					   if (thisPost.isDraft == "new") {
					   } else {
							post.wordcount += getWordCountText(thisPost.description);
							post.postcount += 1;
							post.seq = thisPost.entrySeqId;
							if (typeof thisPost.plays.onChallenge != 'undefined') {
								 post.cardsplayed += thisPost.plays.cards.length;
							}
						}
					}
					charPosts[post.role] = post;
				}
				posts = focusedScene.entries;
				postCount = posts.length;
				thisPost = {};
				for (var intPost2 in posts) {
                    var post2;
					thisPost = posts[intPost2];
					if (typeof charPosts[thisPost.role] != 'undefined') {
						post2 = charPosts[thisPost.role];
					} else {
						post2 = {};
						post2.role = thisPost.role;
						post2.wordcount = 0;
						post2.postcount = 0;
						post2.cardsplayed = 0;
						post2.seq =0;
					}
					post2.id = thisPost.entrySeqId;
				   if (thisPost.description != null) {
					   if (thisPost.isDraft == "new") {
					   } else {
							post2.wordcount += getWordCountText(thisPost.description);
							post2.postcount += 1;
							post2.seq = thisPost.entrySeqId;
							if (typeof thisPost.plays.onChallenge != 'undefined') {
								 post2.cardsplayed += thisPost.plays.cards.length;
							}
						}
					}
					charPosts[post2.role] = post2;
				}
			}

            var charCast = myScope.castCharacters;
            var charCount = charCast.length;
            var thisChar = {};
            for (var i = 0; i < charCount; i++) {
                thisChar = charCast[i];
                $("img.cp-avatar[src='" + thisChar.v_avatarUrlPopover + "']" ).prop("src",thisChar.v_avatarUrlMedium);
                var char = {};
                if (typeof charPosts["character:" + thisChar.characterSeqId] != 'undefined') {
                    var charPost = charPosts["character:" + thisChar.characterSeqId];
                    char.moves = charPost.postcount;
                    char.wordcount = charPost.wordcount;
                    char.played = charPost.cardsplayed;
                    char.movepos = charPost.seq;
                } else {
                    char.moves = [];
                    char.wordcount = 0;
                    char.played = 0;
                    char.movepos = 0;
                }
                // Need to cope with characters with spaces in their name
                char.name = thisChar.name.trim();
                char.seq = thisChar.characterSeqId;
                char.url = thisChar.v_showUrl;
				char.retired = thisChar.v_retired;
				char.strengths = {};
				char.username = "user";
				char.userurl = "/";
				char.loadedUser = false;
				char.loadedChar = false;
				char.strStrength = buildCardString(thisChar,"strength");
				char.strWeakness = buildCardString(thisChar,"weakness");
				char.strAsset = buildCardString(thisChar,"thing");
				char.strGoal = buildCardString(thisChar,"goal");
				char.strSubplot = buildCardString(thisChar,"subplot");
				char.slug = thisChar.slug;
				char.imageAssetId = thisChar.imageAssetId;
				chars[char.name] = char;
			}

           /* Work out what chellenges we have and who has played what on them */
           var scene = gameScope.focusedScene;

           // Grab the list of challenges from the cards on the entries
		   if (scene) {
			   for (var intEntry in scene.entries) {
					var thisEntry = scene.entries[intEntry];
					var challengeCards = thisEntry.v_challengeCards;
					if (challengeCards === undefined) {
					} else {
						if (challengeCards.length > 0) {
							var arrayLength = challengeCards.length;
							for (var i2 = 0; i2 < arrayLength; i2++) {
								var cCard = challengeCards[i2];
								challenges[cCard.v_playGuid] = {};
								challenges[cCard.v_playGuid].name = cCard.name;
								challenges[cCard.v_playGuid].v_playGuid = cCard.v_playGuid;
								challenges[cCard.v_playGuid].cardsplayed = [];
							}
						}
					}
			   }

			   // Work out plays on those challenges
			   for (var intEntry2 in scene.entries) {
					var thisEntry2 = scene.entries[intEntry2];
					var plays = thisEntry2.plays;
					var playsOnChallenge = plays.onChallenge;
					if (playsOnChallenge === undefined) {
					} else {
						// This player has played on a challenge
						var challengeID = playsOnChallenge.refPlayGuid;
						var cardsPlayed = thisEntry2.v_cardsPlayedOnChallenge;
						var arrayLength2 = cardsPlayed.length;
						for (var i3 = 0; i3 < arrayLength2; i3++) {
							var pCard = cardsPlayed[i3];
							var pChar = getCharBySeq(thisEntry2.characterSeqId);
							pCard.playedBy = pChar;
							challenges[challengeID].cardsplayed.push(pCard);
						}
					}
			   }

           /* ---------------------------------------------------------------- */

           //charPosts = unsafeWindow.charPosts;
           log(JSON.stringify(chars),"gameChars");
           loadNarratorChar();
           var charN = chars.Narrator;
            charN.moves = charPosts.narrator.postcount;
            charN.wordcount = charPosts.narrator.wordcount;
            charN.played = charPosts.narrator.cardsplayed;
            charN.movepos = charPosts.narrator.seq;
            charN.imageAssetId = "we-dont-have-one";
            charN.slug = "narrator";

            /* Sort the characters as per the user's prefs */
            if (config.Game.sortChars.trim().toLowerCase() != "default") {
                /* Sort the array of chars */
                chars = sortArray(chars,sortBy,config.Game.sortChars.trim().toLowerCase(),false);
                /* Grab the row data and remove the table row for each non-narrator row */
                $("div.the-activity-summary table:first tr").each(function(index, value) {
                    var charName = $(this).find("td:first a:first").text().trim();
                    if (charName=='') {
                        // This is the narrator row - leave it be
                    } else {
                        var rowHTML = $(this)[0].outerHTML;
                        chars[charName].rowHTML = rowHTML;
                        $(this).remove();
                    }
                });
                /* Add the removed rows back in, in the new order */
                for (var strName in chars) {
                    var thisChar2 = chars[strName];
                    if (strName != "Narrator") {
                        $("div.the-activity-summary table:first tr:last").after(thisChar2.rowHTML);
                    }
                }
            }

            /* Locate the challenges in the new activity summary */
            $("div.the-activity-summary table:last tr").each(function(index, value) {
                if (index > 0) {
                    var challengeName = $(this).find("td:first").text().trim();
                    var challenge = getChallengeByName(challengeName);

                    /* Set up toggle switch for each row */
                    $(this).find("td:last").append("<span style='float: right;'><a href='#' class='gm_toggle2' id='gm_toggle2-" + challenge.v_playGuid + "'>+</a></span>");
                    $('#gm_toggle2-' + challenge.v_playGuid).click(function(e) {
                        e.preventDefault();
                        if ($(this).text() == "+") {
                            $(this).text("-");
                        } else {
                            $(this).text("+");
                        }
                        $("#gm-insert-actrow2-" + challenge.v_playGuid).toggle();
                    });

                    var $insert = $("<tr class='gm-insert-actrow2' id='gm-insert-actrow2-" + challenge.v_playGuid + "' style='display:none;'></tr>");
                    $insert.append("<td colspan='4' style='padding-bottom: 2em; padding-top: 0.5em; padding-left: 1em;' id='gm-cardsPlayed-" + challenge.v_playGuid + "'>" + cardsPlayedTable(challenge) + "</td>");
                    $(this).after($insert);
                }
            });

            /* Locate the characters in the new activity summary */
            $("div.the-activity-summary table:first tr").each(function(index, value) {
                if (index>0) {
                    var charName = $(this).find("td:first a:first").text().trim();
                    if (charName=='') {
                        charName = 'Narrator';
                    }
                    var thisChar = chars[charName];
                    if (thisChar === undefined) {
                        console.log("unfound char: " + charName);
                        console.log(JSON.stringify(chars));
                    }
                    var charSlug = thisChar.slug;
                    var $insert = "";

                    /* Set up toggle switch for each row */
                    $(this).find("td:last").append("<span style='float: right;'><a href='#' class='gm_toggle' id='gm_toggle-" + charSlug + "'>+</a></span>");
                    $('#gm_toggle-' + charSlug).click(function(e) {
                        e.preventDefault();
                        if ($(this).text() == "+") {
                            $(this).text("-");
                        } else {
                            $(this).text("+");
                        }
                        $("#gm-insert-actrow-" + charSlug).toggle();
                    });

                    /* Insert the row containing extra stats */
                    $insert = $("<tr class='gm-insert-actrow' id='gm-insert-actrow-" + charSlug + "' style='display:none;'></tr>");
                    $insert.append("<td colspan='1' style='padding-bottom: 2em; padding-top: 0.5em; padding-left: 1em;' id='gm-charWordCount-" + charSlug + "'>" + thisChar.wordcount + " words, " + charAvgWordCount(thisChar) + "/post</td>");
                    $insert.append("<td colspan='3' style='padding-bottom: 2em; padding-top: 0.5em;' id='gm-charHand-" + charSlug + "'>" +
                        "<div>" +
                        thisChar.strStrength.replace("iconReplaceMe",getCardIcon("strength")) +
                        thisChar.strWeakness.replace("iconReplaceMe",getCardIcon("weakness")) +
                        thisChar.strAsset.replace("iconReplaceMe",getCardIcon("thing")) +
                        thisChar.strGoal.replace("iconReplaceMe",getCardIcon("goal")) +
                        thisChar.strSubplot.replace("iconReplaceMe",getCardIcon("subplot")) +
                        "</div>" +
                        "</td>");

                    $(this).after($insert);
                    /* Link up tab images to character pages */
                    $("div.entry div.l-tab img[ng-src*='" + thisChar.imageAssetId + "']").each(function(index) {
                        var strHTML = $(this).parent().html().trim();
                        strHTML = "<a href='" + thisChar.url + "'>" + strHTML + "</a>";
                        $(this).parent().html(strHTML);
                    });
                    $("div.entry h5.l-title:contains('" + charName + " (')").each(function(index) {
                        if ($(this).find("a").length > 0) {
                            // v1.16.1 - Don't add links if we have links (edit, revision request etc) here already.
                        } else {
                            var strHTML = $(this).html().trim();
                            strHTML = strHTML.replace(charName,"<a href='" + thisChar.url + "'>" + charName + "</a>");
                            var userID = $(this).text().trim().match(/\((.*)\)/)[1].substr(3);
                            thisChar.userID = userID;
                            thisChar.userURL = "/user/" + userID;
                            strHTML = strHTML.replace(userID,"<a href='" + thisChar.userURL + "'>" + userID + "</a>");
                            $(this).html(strHTML);
                        }
                    });
                } else {
                    /* Row 0 - add a open-all and a close all */
                    $(this).find("th:last").append("<span style='float: right;'><a href='#' id='gm_show-all'>+</a> | <a href='#' id='gm_hide-all'>-</a></span>");
                    $('#gm_show-all').click(function(e) {
                        e.preventDefault();
                        $("tr.gm-insert-actrow").show();
                        $("a.gm_toggle").text("-");
                    });
                    $('#gm_hide-all').click(function(e) {
                        e.preventDefault();
                        $("tr.gm-insert-actrow").hide();
                        $("a.gm_toggle").text("+");
                    });
                }
            });
			}

            setGamePid();


            /* Set Up Tagging Key on CTRL+\, bold on CTRL+B etc */
            $(document).keypress(function(event) {
                var bStop = false;
                if (event.ctrlKey) {
                    var code = (event.keyCode ? event.keyCode : event.which);
                    switch (code) {
                        case 15:
                        case 111:
                            addTag('**[',']**',false);
                            bStop = true;
                            break;
                        case 2:
                        case 98:
                            addTag('**','**',false);
                            bStop = true;
                            break;
                        case 9:
                        case 105:
                            addTag('*','*',false);
                            bStop = true;
                            break;
                        case 12:
                        case 108:
                            var objLast = $("div.entry-description").last().get(0);
                            window.scroll(0,findPos(objLast));
                            bStop = true;
                            break;
                        case 8:
                        case 104:
                            blnPreviewHidden = !blnPreviewHidden;
                            bStop = true;
                            break;
                        case 17:
                        case 113:
                            addTag(GetSelectedText(),'\n\n',true);
                            bStop = true;
                            break;
                    }
                }
                if (bStop) {
                    event.preventDefault();
                    event.stopPropagation();
                    return false;
                }
            });
            /* No longer seems to be needed but keep it in case it is later */
            // checkActivityTable();

            killPreview();

            addTOC();


          //getSearchData();
			doHighlight();
			window.setInterval(function() { doHighlight(); }, 1000*30 )
    }

    if (config.Game.gameUKDates) {
        convertUKDates(pageType);
    }

    if (config.root.userNotes) {
        displayUserNotes();
    }

    storeCharImg("Anon-65","https://cloudinary-a.akamaihd.net/protagonistlabs/image/upload/a_exif//w_65,h_65,c_fill/e_sharpen:50/character-avatar-default-960_bd6tul.jpg");

    // Allow click-to-kill maintenance message
    $("div.maintenance-warning").click(function() {
        $(this).remove();
    });
}

function fireMainWhen() {
    if (pageDelay[pageType]) {
        if ($(pageDelayWaitFor[pageType]).length) {
            main();
        } else {
            setTimeout(fireMainWhen,pageDelay[pageType]);
        }
    } else {
		if (pageType == "notifications" && config.Trello.integrateTrello && config.Trello.statusNote) {
			ifTrelloReady(function(){
				main();
			});
		} else {
			main();
		}
    }
}

/* =========================== */
/* Removing images			   */
/* =========================== */
function blankImg(objImg) {
	objImg.prop("width",objImg.width());
	objImg.prop("height",objImg.height());
	objImg.prop("src","http://stisrv13.epfl.ch/img/white_pixel.jpg");
}

function removeAllImages() {
	log("functiontrace","Start Function");
	var counter;
	var aryToRemove = ['div.bg img','div.entry-img img','div.art img'];
	for (counter=0; counter<aryToRemove.length; counter++) {
		$(aryToRemove[counter]).each(function() {
			blankImg($(this));
		});
	}
	var aryAvatar = ['img.avatar-small', 'div.action-box div.l-tab img'];
	for (counter=0; counter<aryAvatar.length; counter++) {
        // eslint-disable-next-line no-loop-func
		$(aryAvatar[counter]).each(function() {
			this.src = config.charimgcache["Anon-65"].img; //"https://cloudinary-a.akamaihd.net/protagonistlabs/image/upload/a_exif//w_65,h_65,c_fill/e_sharpen:50/character-avatar-default-960_bd6tul.jpg";
		});
	}
}
/* =========================== */

function addImgSearchLaunch($lnk,url) {
    $lnk.click(function(e) {
        if ($('#name-input').val() && $('#name-input').val().trim().length > 0) {
            var win = window.open(url.split("%q%").join($('#name-input').val()), '_blank');
            win.focus();
            e.preventDefault();
        }
    });
}

function watcherImgSearch($e) {
    if (config.Img.imgSearch) {
        if ($.find("div.img-mgmt-controls").length > 0 || $.find("div.image-source-choice-buttons").length > 0 ) {
          const myTimeout = setTimeout(function() {
            if ($.find("div.ng-modal-footer").length > 0) {
                // we have a footer lets see if it has has a tip section
                console.log($.find("div.ng-modal-footer"));
                $.find("div.ng-modal-footer").forEach(function(val){
                  $m = $(val);
                  if ($m.find("div.modal-footer-tip").length === 0) {
                    $m.prepend("<div class='modal-footer-tip'></div>");
                  }
                });
                var $tips = $.find("div.modal-footer-tip");
              /*
                if ($tips.length == 0) {
                  $tips = $("<div class='modal-footer-tip'></div>");
                  $.find("div.ng-modal-footer").each(function(){
                    .prepend($tips);
                  });
                  console.log($e);
                  $tips = $.find("div.modal-footer-tip");
                  console.log($tips);
                }
                */
                $tips.forEach(function(tip){
                  console.log(tip);
                  var $tip = $(tip);
                  $tip.css("text-align", "left");
                  if ($tip.find(".gm-img-links").length == 0) {
                      var $imgLnk = $("<div class='gm-img-links' style='margin-top:5px;margin-bottom:5px;'><i class='material-icons' style='vertical-align: sub; font-size: 1.25em;'>search</i> Image Search: </div>");
                      var aryLnks = [];
                      if (config.Img.imgSearchGoogle) {
                          var $imgLnkGoogle = $("<a target='_blank' href='https://www.google.co.uk/imghp?hl=en&tab=wi0&authuser=0&ogbl'>Google</a>");
                          addImgSearchLaunch($imgLnkGoogle,'https://www.google.co.uk/search?authuser=0&tbm=isch&q=%q%&oq=%q%');
                          aryLnks.push($imgLnkGoogle);
                      }
                      // Fuck Russia
                      /*
                      if (config.Img.imgSearchYandex) {
                          $imgLnk.append(" | ");
                          var $imgLnkYandex = $("<a target='_blank' href='https://yandex.com/images/'>Yandex</a>");
                          $imgLnk.append($imgLnkYandex);
                          addImgSearchLaunch($imgLnkYandex,'https://yandex.com/images/search?text=%q%');
                      }
                      */
                      if (config.Img.imgSearchPixabay) {
                          var $imgLnkPixabay = $("<a target='_blank' href='https://pixabay.com/'>Pixabay</a>");
                          addImgSearchLaunch($imgLnkPixabay,'https://pixabay.com/images/search/%q%/');
                          aryLnks.push($imgLnkPixabay);
                      }

                      if (config.Img.imgSearchPin) {
                          var $imgLnkPin = $("<a target='_blank' href='https://www.pinterest.com/'>Pinterest</a>");
                          addImgSearchLaunch($imgLnkPin,'https://www.pinterest.com/search/pins/?q=%q%');
                          aryLnks.push($imgLnkPin);
                      }

                      if (config.Img.imgSearchUnsplash) {
                          var $imgLnkUn = $("<a target='_blank' href='https://unsplash.com/'>Unsplash</a>");
                          addImgSearchLaunch($imgLnkUn,'https://unsplash.com/s/photos/%q%');
                          aryLnks.push($imgLnkUn);
                      }

                      aryLnks.forEach(function($l) {
                        if ($l !== aryLnks[0]) {
                          $imgLnk.append(" | ");
                        }
                        $imgLnk.append($l);
                      });

                      $tip.append($imgLnk);
                  }
                });
            }

          }, 500);
        }
    }
}

function doImgPop(event) {
    var src = event.target.src;
    var srcOrig;
    var arySrc = src.split('/');
    arySrc.splice(8,2);
    srcOrig = arySrc.join('/');
    console.log(srcOrig);
    throwModal("Original Image","<img src='" + srcOrig + "'>","auto");
    //console.log(event.target.src);
}

function addImgPop() {
    var arySel = ["div.art img"];
    if (config.Game.imgPopCard) {
        for (var i = 0; i < arySel.length; i++) {
            $( "body" ).off("click",arySel[i],doImgPop);
            $( "body" ).on("click",arySel[i],doImgPop);
        }
    }
}

function addCardTags($e) {
    var cardType = $e.find("h4").text().split("(")[0].trim();
    if (!blAddingCardTags && $e.find("span#gm-cardtags-" + cardType).length === 0 ) {
        $e.find("span.gm-cardtags").remove();
        blAddingCardTags = true;
        var cardTags = ["[All]"];
        console.log("=== Add Card Tags for " + cardType + " ===");
        $(".description").each(function() {
            if ($(this).text().indexOf("[") > -1) {
                var matches = $(this).text().match(/\[[^\]]*\]/g);
                for (var i = 0; i < matches.length; i++) {
                    if (!cardTags.includes(matches[i])) {
                        cardTags.push(matches[i]);
                    }
                }
            }
        });
        $('#gm-cardtags-' + cardType).remove();
        let $cardTags = $("<span class='gm-cardtags' id='gm-cardtags-" + cardType + "'></span>");
        for (var i = 0; i < cardTags.length; i++) {
            let cardTag = cardTags[i];
            let $cardTag = $("<span class='gm-cardtag fakelink' style='font-size: 80%; margin-left: 5px; color: #086a87'>" + cardTag + "</span>");
            $cardTag.click(function() {
                if (cardTag === "[All]") {
                    $("div.deck-cards-browser-section .description").parent().parent().parent().parent().parent().show();
                } else {
                    $("div.deck-cards-browser-section .description:not(:contains(" + cardTag + "))").parent().parent().parent().parent().parent().hide();
                }
            });
            $cardTags.append($cardTag);
        }
        $e.find("h4").append($cardTags);
        blAddingCardTags = false;
    }
}



function addMutationWatchers() {
    observerMWC = new MutationObserver(function(mutations) {
        if (config.GreenRoom.cardTags) {
            var $eleCardNodeEntries = $('div.deck-cards-browser-section');
            if ($eleCardNodeEntries.length > 0) {
                addCardTags($eleCardNodeEntries);
            }
        }
        mutations.forEach(function(mutation) {
            if (mutation.addedNodes.length > 0) {
                if (mutation.addedNodes[0].outerHTML) {
                    switch (pageType) {
                        case "home":
                            break;
                        case "notifications":
                            break;
                        case "forums":
                        case "forum-thread":
                            if (config.Game.formatButtons) {
                                var $ele = $(mutation.addedNodes[0]);
                                var $entries = $ele.find('textarea#newPostBodyInput');
                                if ($entries.length > 0) {
                                    handleChangesMoveBox($entries);
                                }
                            }
                            break;
                        case "character":
                            if (config.Game.formatButtons) {
                                var $ele2 = $(mutation.addedNodes[0]);
                                var $entries2 = $ele2.find('textarea.ng-isolate-scope');
                                if ($entries2.length > 0) {
                                    handleChangesMoveBox($entries2);
                                }
                            }
                            break;
                        case "green-room":
                        case "cards":
                            addImgPop();
                            var $elegr = $(mutation.addedNodes[0]);
                            watcherImgSearch($elegr);
                            break;
                        case "game":
                            addImgPop();
                            var $ele3 = $(mutation.addedNodes[0]);
                            if ($ele3.find(".comment-meta").length > 0) {
                              dbFormatComments();
                            }
                            var $entry = $ele3.find("div.entry");

                            if (config.Bookmarks.markLastRead) {
                              if ($entry.length > 0) {
                                setReadMoves($entry);
                              }
                            }


                            // Need to split these two out
                            if (config.Game.autoMerge) {
                                // Let's not spam things
                                if (lastAutoMerge + 500 < (new Date).getTime()) {
                                    lastAutoMerge = (new Date).getTime();
                                    if ($(".poll-toast").length > 0) {
                                        // Work out what sort of toast we have
                                        if ($(".poll-toast").text().indexOf("paused") >= 0) {
                                            // It's updates-paused toast!
                                            console.log("Paused!")
                                            // Unpause
                                            unsafeWindow.angular.element(".poll-toast").scope().userTriggeredPoll();
                                            // Ask for updates in a little bit
                                            setTimeout(function () {
                                                if ($(".poll-toast").length > 0) {
                                                    unsafeWindow.angular.element(".poll-toast").scope().userAskedForMerge();
                                                }
                                            }, 2000);
                                        } else {
                                            // It's activity toast!
                                            //console.log("Activity!")
                                            // Ask for updates
                                            unsafeWindow.angular.element(".poll-toast").scope().userAskedForMerge();
                                        }
                                    };
                                }
                            }

							/* Image links for upload image dialogue */
                            watcherImgSearch($ele3);

                            /* For move box */
                            if (config.Game.moveWC || config.Game.formatButtons) {
                                var $entries3 = $ele3.find('textarea.description-input');
                                if ($entries3.length > 0) {
                                    handleChangesMoveBox($entries3);
                                }
                            }

							if (config.Game.hideGameImages) {
								removeAllImages();
							}

                            /* For game page entries */
                            if ($entry.length > 0 && (config.Game.entryWC || config.Speech.styleSpeech || config.Speech.styleThought || config.Game.processLocationTags)) {
                                handleEntry($entry.find('div.entry-description div'));
                                /*
                                var $entries4 = $('div.entry-description div');
                                if ($entries4.length > 0) {
                                    handleEntry($entries4);
                                }
                                */
                            }

                            /* Bugger character avatars on character popover */
                            if (config.Game.biggerPopoverAvatars) {
                                var $entries5 = $ele3.find("img.cp-avatar");
                                if ($entries5.length > 0) {
                                    $entries5.each(function() {
                                        var src = $(this).prop("src");
                                        src = src.replaceAll("w_100,h_100","w_150,h_150");
                                        $(this).prop("src",src);
                                        $(this).css("width","150px");
                                        $(this).css("height","150px");
                                    });
                                }
                            }

                            if (config.Notes.charNotes) {
                                var $entCharNotes = $ele3.find("div.character-popover");
                                if ($entCharNotes.length > 0) {
                                    if ($entCharNotes.find("#gm-charnote-link").length === 0) {
                                        var $charLinks = $("<p><a href='#' id='gm-charnote-link'>Character Notes</a></p>");
                                        var charurl = $entCharNotes.find("div.cp-name a").attr("href");
                                        if (charurl) {
                                            charurl = "https://storium.com" + charurl;
                                            if (config.gameNotes[charAndGameSlugFromURL(charurl)]) {
                                                $charLinks.find("a").css("color","red");
                                            }
                                            $charLinks.click(function(e){
                                                e.preventDefault();
                                                showNotes(charAndGameSlugFromURL(charurl),"Character Notes");
                                            });
                                            $entCharNotes.find("img.cp-avatar").parent().parent().after($charLinks);
                                        }
                                    }
                                }
                            }

                            if (config.Game.signalPreview) {
                                if ($(document.body).find("div.entry-preview").length > 0) {
                                    $(document.body).css("background-color",config.Game.signalPreviewColour);
                                } else {
                                    $(document.body).css("background-color","");
                                }
                            }
                            break;
                        default:
                            break;
                    }
                }
            }
        });
    });

    // Notify me of everything!
    var observerConfig = {
        attributes: false,
        childList: true,
        subtree: true,
        characterData: false
    };

    // Node, config
    // In this case we'll listen to all changes to body and child nodes
    observerMWC.observe(document.body, observerConfig);
}

/* Game page read moves */
function getGamePid() {
  return unsafeWindow.angular.element("div#page").scope().gameplay.game.gamePid;
}

function getGameChapSceneID() {
  var strID = getGamePid();
  if (unsafeWindow.angular.element("div#page").scope().v_actNumber) {
    strID += "-" + unsafeWindow.angular.element("div#page").scope().focusedScene.v_actNumber;
  } else {
    strID += "-" + unsafeWindow.angular.element("div#page").scope().focusedScene.v_chapterNumber;
  }
  strID += "-" + unsafeWindow.angular.element("div#page").scope().focusedScene.v_sceneNumber;
  return strID;
}

function addMarkRead($e) {
  if ($e) {
//    console.log("amr-e");
//    console.log($e);
    $e.find("span.gm-markEntryRead").remove();
    let $markRead = $('<span class="gm-markEntryRead fakelink" style="float: right; display: inline; margin-right: 5px;">Mark Read</span>');
    let index = $e.index("div.entry")
//    console.log("idx: " + index);
    $markRead.click(function() {
      if (!config.gameLastReads) {
        config.gameLastReads = {};
      }
      config.gameLastReads[getGameChapSceneID()] = index+1;
      saveConfig();
      setReadMoves();
    });
    $e.find("div.entry-description div").append($markRead);
  } else {
    $("span.gm-markEntryRead").remove();
    $("div#panel_game div.entry").each(function(index) {
      let $markRead = $('<span class="gm-markEntryRead fakelink" style="float: right; display: inline; margin-right: 5px;">Mark Read</span>');
      $markRead.click(function() {
        if (!config.gameLastReads) {
          config.gameLastReads = {};
        }
        config.gameLastReads[getGameChapSceneID()] = index+1;
        saveConfig();
        setReadMoves();
      });
      $(this).find("div.entry-description div").append($markRead);
    })
  }
}

function setReadMoves($e) {
  if ($e) {
    addMarkRead($e);
  } else {
    //console.log("**** Blork ! ****");
    if (Date.now() - lastSetReadMoves < 300) {
      return;
    }
    var gcsID = getGameChapSceneID();
    var lastRead = 0;
    if (config.gameLastReads && config.gameLastReads[gcsID]) {
      lastRead = config.gameLastReads[gcsID];
    }
    if (lastRead > 0) {
      $("div#gm-readline").remove();
      var $lrEntry = $("div#panel_game div.entry:eq(" + (lastRead - 1) + ")").parent().parent();
      $lrEntry.after("<div id='gm-readline' class='gm-readline' style='" + config.Bookmarks.CSS + " '>" + config.Bookmarks.Text + "</div>");
    }
    addMarkRead();
    lastSetReadMoves = Date.now();
  }
}

/* Search */
function sceneFromSeq(seqId, toc) {
	var intSeq = parseInt(seqId);
	for (var i = toc.scenes.length - 1; i >= 0 ; i--) {
		if (parseInt(toc.scenes[i].firstSeqId) < intSeq) {
			return toc.scenes[i];
		}
	}
	return {};
}

function clip(text) {
    var copyElement = document.createElement('input');
    copyElement.setAttribute('type', 'text');
    copyElement.setAttribute('value', text);
    copyElement = document.body.appendChild(copyElement);
    copyElement.select();
    document.execCommand('copy');
    copyElement.remove();
}

function getCardData() {
    var gamePlay = gamePageScope.gameplay;
    var userInfo = gamePageScope.userInfo;
//    console.info(gamePageScope.userInfo.name("6nwest"));
    var gamePid = gamePlay.game.gamePid;
    var thisChar = gamePageScope.actingCharacter;
	var cacheURL = "https://storium.com/ngapi/v0.1/game/cache?cache_key=cards&game_pid=" + gamePid + "&" + scriptVer + "get-card-data";
	var strOut = "";
	$.ajax({
		url: cacheURL,
		async: false,
		success: function(data) {
			var objCache = JSON.parse(data.cache);
			for (var key in objCache) {
				var objCard = objCache[key];
                if (objCard.name !== "Wild") {
                    strOut += "Author: " + userInfo.name(objCard.authorUserPid);
                    strOut += "\nType: " + gamePageScope.helpers.prettyNamespace(objCard.namespace);
                    if (objCard.imageAssetId) {
                        strOut += "\nImage: https://cloudinary-a.akamaihd.net/protagonistlabs/image/upload/a_exif/" + objCard.imageAssetId + ".jpg";
                    }
                    strOut += "\nName: " + objCard.name;
                    strOut += "\nDescription: " + objCard.description;
                    strOut += "\n-----------------------\n\n";
                }
            }
		}
    });
	var strBody = "<textarea style='width: 100%; height: 300px;'>" + strOut + "</textarea>";
	throwModal("Card Data",strBody);

	return strOut;
}

function setGamePid() {
    var gamePlay = gamePageScope.gameplay;
    var $pid = "";
    gamePid = gamePlay.game.gamePid;
    if (config.Game.showGamePid) {
        if (config.Game.linkGamePid) {
            $pid = $("<span class='gm-gpid' id='gm-gpid' style='font-size: 50%'> (<a target='_blank' style='text-decoration: underline;' href='https://storium.com/ngapi/v0.1/game/events?game_pid=" + gamePid + "&since_seq_id=" + gamePageScope.focusedScene.firstSeqId + "'>" + gamePid + "</a>)</span>");
        } else {
            $pid = $("<span class='gm-gpid' id='gm-gpid' style='font-size: 50%'> (" + gamePid + ")</span>");
        }
        $("td.title-cell h2 span").after($pid);
    }
}

function getSearchData(callBack) {
    //var gamePageScope = unsafeWindow.gamePageScope;
    var gamePlay = gamePageScope.gameplay;
    var gamePid = gamePlay.game.gamePid;
    var toc = JSON.parse(gamePlay.gsm.stash.toc);
	var firstSeq = toc.scenes[0].firstSeqId;
	var cacheURL = "https://storium.com/ngapi/v0.1/game/all_scene_caches?game_pid=" + gamePid + "&" + scriptVer + "get-search-data";
	var intScenes = gamePlay.game.v_numScenes;
	var dat30mins = new Date();
	dat30mins = new Date(dat30mins - 30*60000);

	if (!lastSearchData || lastSearchData < dat30mins) {
		$.get(cacheURL, function(data) {
			var newSearchData = [];
			for (var s = 1; s <= intScenes; s++) {
				var sceneData;
				var commentData;
				if (data.caches["scene" + s]) {
					sceneData = JSON.parse(data.caches["scene" + s]);
				}
				if (data.caches["sceneComments" + s]) {
					commentData = JSON.parse(data.caches["sceneComments" + s]);
				}
                var eventData;
                var searchDataItem;
				if (sceneData && sceneData.entries) {
					for (var key in sceneData.entries) {
						eventData = sceneData.entries[key];
						searchDataItem = {};
						searchDataItem.body = eventData.description;
						searchDataItem.type = "Move";
						searchDataItem.sceneId = s;
						newSearchData.push(searchDataItem);
					}
				}
				if (commentData) {
					for (var c = 1; c < commentData.length; c++) {
						eventData = commentData[c];
						searchDataItem = {};
						searchDataItem.body = eventData.body;
						searchDataItem.type = "Comment";
						searchDataItem.sceneId = s;
						newSearchData.push(searchDataItem);
					}
				}
			}
			searchData = newSearchData;
			lastSearchData = new Date();
			if (callBack) {
				callBack();
			}
		});
	} else {
		if (callBack) {
			callBack();
		}
	}
}

function doSearch(str) {
	var results = [];
	for (var i = 0; i < searchData.length; i++) {
		var item = searchData[i];
		if (item.body.toLowerCase().indexOf(str.toLowerCase()) !== -1) {
			results.push(item);
		}
	}
	return results;
}

function doHighlight() {
    $('div.entry-description').unmark();
	$('div.comment-content').unmark();
	if (hashText && hashText !== "") {
		$('div.entry-description').mark(hashText, {
			"separateWordSearch": false
		});
		$('div.comment-content').mark(hashText, {
			"separateWordSearch": false
		});
	} else {
		// Nothing now...
	}
}

/* TOC */
function currentScenes() {
	var toc = JSON.parse(gamePageScope.gameplay.gsm.stash.toc);
    var arrayLength = toc.scenes.length;
    var chapters = {};
    for (var i = 0; i < arrayLength; i++) {
        var scene = {};
        var sceneData = toc.scenes[i];
        var intNum = -1;

        if (sceneData.actNumber) {
            intNum = sceneData.actNumber;
            if (!chapters[intNum]) {
                chapters[intNum] = {};
                chapters[intNum].scenes = {};
            }
            chapters[intNum].desc = "Act " + intNum;
            chapters[intNum].url = "https://storium.com/game/" + gamePageScope.gameplay.game.slug + "/act-" + intNum + "/";
        } else {
            intNum = sceneData.chapterNumber;
            if (!chapters[intNum]) {
                chapters[intNum] = {};
                chapters[intNum].scenes = {};
            }
            chapters[intNum].desc = "Chapter " + intNum;;
            chapters[intNum].url = "https://storium.com/game/" + gamePageScope.gameplay.game.slug + "/chapter-" + intNum + "/";
        }
        scene.url = chapters[intNum].url + "scene-" + sceneData.sceneNumber;
        scene.desc = "Scene " + sceneData.sceneNumber;
		scene.id = sceneData.sceneId;
        chapters[intNum].scenes[sceneData.sceneNumber] = scene;
    }
    return chapters;
}

function performSearch() {
	var searchText = $('#gm_search_text').val();
	hashText = searchText;
	if (searchText && searchText !== "") {
		$('ul.gmTOCscenesList').css("display","block");
		$('i.gm_search_marker').remove();
		for (var i = 0; i < searchData.length; i++) {
			if (searchData[i].body.toLowerCase().indexOf(searchText.toLowerCase()) !== -1) {
                var $icon;
				switch (searchData[i].type) {
					case "Move":
						$icon = $("<i class='material-icons gm_search_marker gm_search_marker_move' style='vertical-align: sub; font-size: 110%;' title='Bold'>theaters</i>")
						if ($("#gmTOC_scene_" + searchData[i].sceneId).find("i.gm_search_marker_move").length == 0) {
							$("#gmTOC_scene_" + searchData[i].sceneId).append($icon);
						}
						break;
					case "Comment":
						$icon = $("<i class='material-icons gm_search_marker gm_search_marker_comment' style='vertical-align: sub;; font-size: 110%;' title='Bold'>chat</i>")
						if ($("#gmTOC_scene_" + searchData[i].sceneId).find("i.gm_search_marker_comment").length == 0) {
							$("#gmTOC_scene_" + searchData[i].sceneId).append($icon);
						}
						break;
				}
				var $a = $("#gmTOC_scene_" + searchData[i].sceneId + " a");
				var url = $a.attr("href");
				url = url.split("#")[0] + "#" + searchText;
				$a.attr("href",url);
			}
		}
	}
	$("#gm_search_text").removeAttr("disabled");
	$("#gm_btnSearch").css("color", "black");
	doHighlight();
}

function searchClick() {
	$("#gm_search_text").attr("disabled", "disabled");
	$("#gm_btnSearch").css("color", "lightgray");
	getSearchData(performSearch);
}

function tocHTML() {
    var gameURL = "https://storium.com/game/" + gamePageScope.game.slug + "/";
    var $out = $("<div class='gameTOC' style='border: thin solid black; background-color: cornsilk; margin-top: 40px; border-radius: 10px'></div>");
    var $list = $("<ul></ul>");
	$out.append("<div class='gm_search' style='float: right; margin-right: 10px; margin-top: 10px;'><input name='gm_search_text' id='gm_search_text' type='text' style='width: 10em;' /> <i id='gm_btnSearch' class='material-icons' style='cursor: pointer; vertical-align: sub;' title='Bold'>search</i></div>");
    var chaps = currentScenes();
    for (var key in chaps) {
        var chap = chaps[key];
        var $chap = $("<li><span class='fakelink gmTOCchapList' style='font-weight: bold'>" + chap.desc + "</span></li>");
        var $scenes = $("<ul class='gmTOCscenesList' id='gm-toc-scenes-" + key + "' style='display: none;'></ul>");
        for (var sceneKey in chap.scenes) {
            var scene = chap.scenes[sceneKey];
            $scenes.append("<li id='gmTOC_scene_" + scene.id + "'><a href='" + scene.url + "'>" + scene.desc + "</a></li>");
        }
        $chap.find("span.gmTOCchapList").click(function() {
            $(this).parent().find("ul").each(function() {
                if ($(this).css("display") === "none") {
                    $(this).css("display","block");
                } else {
                    $(this).css("display","none");
                }
            });
        });
        $chap.append($scenes);
        $list.append($chap);
        //$list.append("<li><a href='" + scene.url + "' style='text-transform: capitalize;'>" + scene.name + "</a></li>");
    }
    $out.append("<p style='padding: 10px 0px 0px 10px;'><strong>Table of Contents</strong></p>");
    $out.append($list);
	$out.find("#gm_btnSearch").click(function() {
		searchClick();
	});
	$out.on("keypress","#gm_search_text",function(e) {
		if (e.keyCode==13) {
			searchClick();
		}
	});
    return $out;
}

function addTOC() {
    var $toc = tocHTML();
    switch (config.Game.addTOCTo) {
        case "Game Info":
            $("div#page div.game-columns:eq(0) div.column-main div.details div.details-content").append($toc);
            break;
        case "On Page":
            $("div#page div.game-columns:eq(1) div.column-main div.mbGBC:eq(0)").after($toc.css("margin-bottom", "40px"));
            break;
        default:
            break;
    }
}
/* --------------------- */

/* --------------------- */
/* Trello Integration    */
/* --------------------- */
function ifTrelloReady(fireThis) {
	// Wait for Trello object to be ready in context of GM script
    if (unsafeWindow.trelLoaded()) {
		fireThis();
    } else {
		setTimeout(function() { ifTrelloReady(fireThis) },500);
    }
}

function fireTrelloWhen(fireThis) {
	// Wait for main page to have the trello object ready :)
    if (unsafeWindow.strTrelBoard !== '') {
		objTrello = unsafeWindow.trel;
		strTrelloBoard = unsafeWindow.strTrelBoard;
    } else {
		setTimeout(fireTrelloWhen,500);
    }
}

// Disable eslint for the Trello stuff. It's got crazy-assed injection and so will throw errors and warnings all over the place
/*eslint-disable */
function initTrello() {
	inject("var trel={}; var strTrelBoard=''; var aryTrelCards = []; aryTrelLists = []; var strTrelloBoardName = '" + config.Trello.boardName + "'; var objCardLists = {}; var strTrelURL = '';");
	inject("var trelCardsLoaded = false; var trelListsLoaded = false; var trelCardListsLoaded = false;");
	inject("var updateCards; var addCard; var updateCardLists; var trelloUpdateCardsList;");

	inject(function() {
		var trelloAuthenticationSuccess = function() {
			trel = window.Trello;
			trel.get("members/me/boards", { fields: "id,name,url"}, function(boards) {
				for (var i = 0; i < boards.length; i++) {
					if (boards[i].name== strTrelloBoardName) {
						strTrelBoard=boards[i].id;
						strTrelURL = boards[i].url;
					}
				}
				if (strTrelBoard == "") {
					trel.post("boards", {name: strTrelloBoardName, desc: "Storium Trello"}, function(board) {
						strTrelBoard = board.id;
						strTrelURL = boards.url;
					});
				}
				updateLists();
				updateCards();
			})
		};

        trelloUpdateCardsList = function(cardId,listId) {
            trel.put("cards/" + cardId, { idList: listId }, function() {
            }, function(err) {
                console.log(err);
            });
        }

		updateCardLists = function () {
			for (var i = 0; i < aryTrelCards.length; i++) {
				var card = aryTrelCards[i];
				var objCardList;
				objCardLists[card.name] = {};
				objCardList = objCardLists[card.name];
				objCardList.card = card;
				for (var j = 0; j < aryTrelLists.length; j++) {
					var list = aryTrelLists[j];
					if (list.id == card.idList) {
						objCardList.list = list;
						objCardList.listNo = j;
						break;
					}
				}
			}
			trelCardListsLoaded = true;
		}

		updateLists = function() {
			trel.get("boards/" + strTrelBoard + "/lists",function(lists) {
				aryTrelLists=lists;
				trelListsLoaded = true;
				if (trelCardsLoaded) {
					updateCardLists();
				}
			});
		}

		updateCards = function () {
			trel.get("boards/" + strTrelBoard + "/cards/all",function(cards) {
				aryTrelCards=cards;
				trelCardsLoaded = true;
				if (trelListsLoaded) {
					updateCardLists();
				}
			});
		}

		trelLoaded = function() {
			return (trelCardsLoaded && trelListsLoaded && trelCardListsLoaded);
		}

		addCard = function (strName, strDesc, urlImg) {
			trel.post("cards", {name: strName, desc: strDesc, idList: aryTrelLists[0].id} ,function(card) {
				var objAdd = {name: 'banner image', url: urlImg};
				trel.post("cards/" + card.id + "/attachments", objAdd ,function(att) {
				});
				updateCards();
			});
		}

		var trelloAuthenticationFailure = function() {
		  console.log('Trello failed authentication');
		};

		$.getScript("https://api.trello.com/1/client.js?key=713342fe542cf4ec04da4ffc9fa4cb44").done(function() {
			window.Trello.authorize({
			  type: 'popup',
			  name: 'Storium Trello Integration',
			  scope: {
				read: 'true',
				write: 'true' },
			  expiration: 'never',
			  success: trelloAuthenticationSuccess,
			  error: trelloAuthenticationFailure
			});
		}).fail(function( jqxhr, settings, exception ) {
			console.log("Failed script load - trello");
		});
	});
	fireTrelloWhen();
}
/*eslint-enable */

/* --------------------- */
async function asyncStartup() {
    await init();
	inject("var homeScope = angular.element(document.body).scope();");
	fireMainWhen();
	addMutationWatchers();
}

$(document).ready(function() {
    asyncStartup();
});


/*
 * jQuery throttle / debounce - v1.1 - 3/7/2010
 * http://benalman.com/projects/jquery-throttle-debounce-plugin/
 *
 * Copyright (c) 2010 "Cowboy" Ben Alman
 * Dual licensed under the MIT and GPL licenses.
 * http://benalman.com/about/license/
 */
(function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this);

/* Old Version Info - moved down here since v3.0.0 */
// v2.12.0  Front page grid
// v2.11.0  Added optrion for hiding new homepage elements
// v2.10.0  Added option to have game pid exposed on game page banner.
// v2.9.1   Tweak on grouping notifications
// v2.9.0   Quick fix for notifcations changes
// v2.8.1   Omit wild cards from get cards
// v2.8.0   First version of get cards
// v2.7.0   Make use of extended tags in slack character file exports
// v2.6.0   Add CSS as an option for removing card indicators when you hover over card art
// v2.5.1   Add in CSS fix for Grammarly plugin
// v2.5.0   Allow trello cards to be updated from updates page
// v2.4.2	Fixed frontpage trello statuses for hosted games when in non-compact mode
// v2.4.1	Notification page trello status now link to Trello
// v2.4.0	Trello list staus on notifications page
// v2.3.1	Minor bugfix on notifications listing
// v2.3.0	Trello list status on homepage
// v2.2.0	Some trello inetgration.
// v2.1.1  	Fixed search
// v2.1.0  	Updated the slack chars link to go straight to cabbitpad.
// v2.0.0  	Removed a lot of injection and done some curative stuff for game page ToC etc
// Version info pre v2.0.0 is removed for brevity
/* - - */