code:
const SYS_DATA_SEPARATOR = '#/]';
const $status = $('#status');
const $inputBox = $('#input-code');

/**
 * Split an ALL-code into its constituent characters.
 * @param {string} code
 * @returns {{valid: boolean, version: string, characters: string[]}}
 */
function splitCode(code) {
    var startIdx = code.indexOf('***');
    var ret = {
        'valid': true,
        'version': '0',
        'characters': []
    };

    if (startIdx < 0) {
        ret.valid = false;
        return ret;
    }

    ret.version = code.substring(0, startIdx);

    var sysDataIdx = code.indexOf(SYS_DATA_SEPARATOR);
    if (sysDataIdx < 0) {
        sysDataIdx = code.length;
    }

    var charData = code.substring(startIdx+3, sysDataIdx);
    ret.characters = charData.split('*');

    return ret;
}

function update() {
    $status.empty();
    var inputCode = $inputBox.val();
    var splitData = splitCode(inputCode);

    $('.result-box').val('');

    if (!splitData.valid) {
        $status.text("This doesn't appear to be a valid ALL code.");
        return;
    }

    splitData.characters.forEach((value, idx) => {
        if (value !== '0') {
            let charCode = splitData.version + '**' + value;
            $('#result-'+idx).val(charCode);
        } else {
            $('#result-'+idx).val('');
        }
    });
}

$(function() {
    $('.result-box').focus(function() {
        $(this).select();
    });

    $inputBox.on('change', update);
})


code:
STATUSES = {
    "online": 40,
    "testing": 30,
    "offline": 20,
    "event": 15,
    "incomplete": 10,
    "duplicate": 5,
}

function display_creator(creator) {
    var wrapper = document.createElement('span');
    var username = document.createElement('span');
    var discriminator = document.createElement('span');

    wrapper.className = "creator";
    wrapper.setAttribute("title", creator["id"])

    username.className = "username";
    username.innerText = creator["username"];

    discriminator.className = "discriminator";
    discriminator.innerText = creator["discriminator"];

    wrapper.appendChild(username);
    wrapper.appendChild(discriminator);
    return wrapper;
}

function populate_character_row(character_id, status, creators) {
    var tr = document.createElement('tr');
    var th = document.createElement('th');

    th.setAttribute('scope', 'row');
    th.innerText = character_id;
    tr.className = "character-row";
    tr.appendChild(th);

    var status_elem = document.createElement('td');
    status_elem.innerText = status;
    tr.appendChild(status_elem);

    var creators_elem = document.createElement('td');
    creators_elem.className = "character-creators";

    if (creators && creators.length > 0) {
        creators.sort((a, b) => {
            return (a["username"].toLowerCase() <= b["username"].toLowerCase() ? -1 : 1);
        }).forEach(function (creator) {
            creators_elem.appendChild(display_creator(creator));
        });
    } else {
        tr.className = "character-row empty";
        creators_elem.setAttribute("title", "No users have been assigned permissions for updating this character.");
        creators_elem.innerText = "Unknown";
    }

    tr.appendChild(creators_elem);

    return tr;
}

function load_characters() {
    return Promise.all([
        fetch('/stats-api/roster/master').then((resp) => resp.json()),
        fetch('/attribution/creators').then((resp) => resp.json()),
    ]).then((data) => {
        var roster = data[0];
        var creators = data[1]["creators"];
        var tgt = document.getElementById('creator-character-rows');
        var characters = {};

        data[1]["errors"].forEach((msg) => console.log(msg));
        creators.forEach((creator) => {
            if (creator["username"].startsWith("Deleted User ")) return;

            creator["characters"].forEach((character) => {
                if (!characters[character]) characters[character] = [];
                characters[character].push(creator);
            });
        });

        for (let character of Object.keys(roster).sort().sort((a, b) => {
            var priorityA = STATUSES[roster[a]] || 0;
            var priorityB = STATUSES[roster[b]] || 0;

            return priorityB - priorityA;
        })) {
            tgt.appendChild(populate_character_row(character, roster[character], characters[character]));
        }
    });
}

$(load_characters);

repo_name: spnati/serve

license: mit


code:
const SITUATION_NAMES = {
    "selected": "Selected",
    "opponent_selected": "Opponent Selected",
    "game_start": "Game Start",
    "swap_cards": "Swapping Cards",
    "good_hand": "Hand (Good)",
    "okay_hand": "Hand (Okay)",
    "bad_hand": "Hand (Bad)",
    "hand": "Hand (Any)",
    "tie": "Tie",
    "must_strip_winning": "Self Must Strip (Winning)",
    "must_strip_normal": "Self Must Strip (Normal)",
    "must_strip_losing": "Self Must Strip (Losing)",
    "must_strip": "Self Must Strip (Any)",
    "must_masturbate_first": "Self Must Masturbate (First)",
    "must_masturbate": "Self Must Masturbate",
    "stripping": "Stripping",
    "stripped": "Stripped",
    "male_must_strip": "AI Opponent Must Strip (Male)",
    "female_must_strip": "AI Opponent Must Strip (Female)",
    "male_human_must_strip": "Player Must Strip (Male)",
    "female_human_must_strip": "Player Must Strip (Female)",
    "opponent_lost": "AI Opponent Lost (General)",
    "male_removing_accessory": "Male Removing Accessory",
    "male_removed_accessory": "Male Removed Accessory",
    "female_removing_accessory": "Female Removing Accessory",
    "female_removed_accessory": "Female Removed Accessory",
    "male_removing_minor": "Male Removing Minor Item",
    "male_removed_minor": "Male Removed Minor Item",
    "female_removing_minor": "Female Removing Minor Item",
    "female_removed_minor": "Female Removed Minor Item",
    "male_removing_major": "Male Removing Major Item",
    "male_removed_major": "Male Removed Major Item",
    "female_removing_major": "Female Removing Major Item",
    "female_removed_major": "Female Removed Major Item",
    "opponent_stripping": "AI Opponent Stripping (Anything)",
    "opponent_stripped": "AI Opponent Stripped (Anything)",
    "female_chest_will_be_visible": "Chest Will Be Visible (Female)",
    "female_small_chest_is_visible": "Chest Is Visible (Female, Small)",
    "female_medium_chest_is_visible": "Chest Is Visible (Female, Medium)",
    "female_large_chest_is_visible": "Chest Is Visible (Female, Large)",
    "female_chest_is_visible": "Chest Is Visible (Female, Any)",
    "male_chest_will_be_visible": "Chest Will Be Visible (Male)",
    "male_chest_is_visible": "Chest Is Visible (Male)",
    "opponent_chest_will_be_visible": "Chest Will Be Visible (Any)",
    "opponent_chest_is_visible": "Chest Is Visible (Any)",
    "male_crotch_will_be_visible": "Crotch Will Be Visible (Male)",
    "male_small_crotch_is_visible": "Crotch Is Visible (Male, Small)",
    "male_medium_crotch_is_visible": "Crotch Is Visible (Male, Medium)",
    "male_large_crotch_is_visible": "Crotch Is Visible (Male, Large)",
    "male_crotch_is_visible": "Crotch Is Visible (Male, Any)",
    "female_crotch_will_be_visible": "Crotch Will Be Visible (Female)",
    "female_crotch_is_visible": "Crotch Is Visible (Female)",
    "opponent_crotch_will_be_visible": "Crotch Will Be Visible (Any)",
    "opponent_crotch_is_visible": "Crotch Is Visible (Any)",
    "male_start_masturbating": "Start Masturbating (Male)",
    "female_start_masturbating": "Start Masturbating (Female)",
    "opponent_start_masturbating": "Start Masturbating (Any)",
    "male_masturbating": "Masturbation (Male)",
    "female_masturbating": "Masturbation (Female)",
    "opponent_masturbating": "Masturbation (Any)",
    "male_heavy_masturbating": "Heavy Masturbation (Male)",
    "female_heavy_masturbating": "Heavy Masturbation (Female)",
    "opponent_heavy_masturbating": "Heavy Masturbation (Any)",
    "male_finished_masturbating": "Finished Masturbating (Male)",
    "female_finished_masturbating": "Finished Masturbating (Female)",
    "opponent_finished_masturbating": "Finished Masturbating (Any)",
    "start_masturbating": "Start Masturbating (Self)",
    "masturbating": "Masturbation (Self)",
    "heavy_masturbating": "Heavy Masturbation (Self)",
    "finishing_masturbating": "Finishing Masturbating (Self)",
    "finished_masturbating": "Finished Masturbating (Self)",
    "after_masturbating": "After Masturbating (Self)",
    "game_over_victory": "Game Over (Victory)",
    "game_over_defeat": "Game Over (Defeat)",
    "global": "Global"
};

const STAGE_NAMES = {
    "early": "Early",
    "middle": "Middle",
    "late": "Late",
    "forfeit": "Forfeit",
    "post_forfeit": "Post-Forfeit",
};

const STATUS_TEXT = {
    "accepted": "Accepted",
    "rejected": "Rejected",
    "submitted": "Published",
    null: "Unreviewed",
};

function addSubelement(elem, type, opts) {
    var new_elem = $("<" + type + ">", opts);
    elem.append(new_elem);

    return new_elem;
}

/**
 * Utility function for formatting a string array as a comma-separated list.
 * @param {string[]} list
 * @returns {string}
 */
function joinList(list) {
    var s = "";
    for (var i = 0; i < list.length - 1; i++) {
        s += list[i] + ", ";
    }

    return s + list[list.length - 1];
}

function joinNumbers(list, separator) {
    var s = "";

	for (var i = 0; i < list.length; i++) {
		s += list[i];

		curIndex = i;
		for (var j = i + 1; j < list.length; j++) {
			if (parseInt(list[j]) == parseInt(list[curIndex]) + 1) {
				curIndex++;
			} else {
				if (curIndex != i) {
					s += "-" + list[curIndex];
				}

				if (curIndex != list.length - 1) {
					s += separator;
				}

				break;
			}
		}

		if (curIndex == list.length - 1 && curIndex != i) {
			s += "-" + list[curIndex];
		}

		i = curIndex;
	}

    return s;
}

/**
 * Makes a DOM subtree for a Recent Activity card.
 *
 * @param {Line} line
 * @param {boolean} moderationOptions
 */
function DialogueLineCard(line, moderationOptions) {
    var root = $("<div>", { "class": "recent-line card", "data-line-id": line.id });
    this.root = root;
    this.line = line;

    this.header = addSubelement(root, "h5", { "class": "line-id card-header", "text": line.id });
    this.metadata_container = addSubelement(root, "ul", { "class": "line-metadata list-group list-group-flush" });
    this.body = addSubelement(root, "div", { "class": "card-body" });
    this.footer = addSubelement(root, "div", { "class": "card-footer" });

    this.situation = addSubelement(this.metadata_container, "li", {
        "class": "line-situation list-group-item", "text": SITUATION_NAMES[line.situation]
    });

    /* var stages = Object.keys(line.stages)
        .filter(function (k) { return line.stages[k]; })
        .map(function (k) { return STAGE_NAMES[k] }); */

	var stages = Object.keys(line.stages)
        .filter(function (k) { return line.stages[k]; });

    this.stage = addSubelement(this.metadata_container, "li", {
        "class": "line-stages list-group-item", "text": joinNumbers(stages, ", ")
    });

    this.expression = addSubelement(this.metadata_container, "li", {
        "class": "line-expression list-group-item", "text": line.expression
    });

    if (line.targets.length > 0) {
        var targetList = joinList(line.targets.map(function (t) { return t.character; }));

        this.targets = addSubelement(this.metadata_container, "li", {
            "class": "line-targets list-group-item", "text": targetList
        });
    } else {
        this.targets = null;
    }

    this.dialogue = addSubelement(this.body, "div", {
        "class": "line-dialogue card-text", "text": line.text
    });

	if (line.notes && line.notes != "") {
		addSubelement(this.body, "hr", {});
		addSubelement(this.body, "b", {"text": "Notes:"});
		this.notes = addSubelement(this.body, "div", {
			"class": "line-notes card-text", "text": line.notes
		});
	} else {
		this.notes = null;
	}

    this.author = addSubelement(this.footer, "span", {
        "class": "line-author", "text": line.author
    });

    if (line.date) {
        var line_date = new Date(line.date);
        var now = new Date();

        var elapsed_time = now.getTime() - line_date.getTime();

        var format_str = '';
        if (elapsed_time < 5 * 60 * 1000) {
            // <5 minutes ago - display 'just now'
            format_str = 'just now';
        } else if (elapsed_time < 60 * 60 * 1000) {
            // < 1 hour ago - display minutes since last update
            format_str = Math.floor(elapsed_time / (60 * 1000)) + ' minutes ago';
        } else if (elapsed_time < 24 * 60 * 60 * 1000) {
            // < 1 day ago - display hours since last update
            var n_hours = Math.floor(elapsed_time / (60 * 60 * 1000));
            format_str = n_hours + (n_hours === 1 ? ' hour ago' : ' hours ago');
        } else {
            // otherwise just display days since last update
            var n_days = Math.floor(elapsed_time / (24 * 60 * 60 * 1000));
            format_str = n_days + (n_days === 1 ? ' day ago' : ' days ago');
        }

        this.date = addSubelement(this.footer, "span", {
            "class": "line-time text-muted", "text": format_str
        });
    } else {
        this.date = null;
    }

	this.optionsContainer = addSubelement(root, "div", { "class": "card-footer line-options" });
	this.status = addSubelement(this.optionsContainer, "span", { "class": "line-status", "text": STATUS_TEXT[line.status] });

    if (moderationOptions) {
		this.optionsButtonsContainer = addSubelement(this.optionsContainer, "div", { "class": "options-buttons" });
		this.optionsButtonsTopRow = addSubelement(this.optionsButtonsContainer, "div", { "class": "options-buttons" });
		this.optionsButtonsBottomRow = addSubelement(this.optionsButtonsContainer, "div", { "class": "options-buttons" });

		if (line.status !== "submitted") {
			this.acceptBtn = addSubelement(this.optionsButtonsTopRow, "button", { "type": "button", "class": "btn btn-sm line-accept" });
			this.rejectBtn = addSubelement(this.optionsButtonsTopRow, "button", { "type": "button", "class": "btn btn-sm line-reject" });
			this.editBtn = addSubelement(this.optionsButtonsBottomRow, "button", { "type": "button", "class": "btn btn-sm line-edit btn-primary", "text": "Edit"});
			// this.deleteBtn = addSubelement(this.optionsButtonsBottomRow, "button", { "type": "button", "class": "btn btn-sm line-delete btn-danger", "text": "Delete"});

			if (line.status === "accepted") {
				this.acceptBtn.addClass("btn-primary").text("Reset Status");
			} else {
				this.acceptBtn.addClass("btn-success").text("Accept");
			}

			if (line.status === "rejected") {
				this.rejectBtn.addClass("btn-primary").text("Reset Status");
			} else {
				this.rejectBtn.addClass("btn-danger").text("Reject");
			}

			this.acceptBtn.click(this.setStatus.bind(this, "accepted"));
			this.rejectBtn.click(this.setStatus.bind(this, "rejected"));
			this.editBtn.click(editLine.bind(this, this.line.id));
			// this.deleteBtn.click(deleteLine.bind(this, this.line.id));
		}
    } else if (isLoggedIn() && line.author === $("#author").val()) {
		this.optionsButtonsTopRow = null;
		this.optionsButtonsBottomRow = null;
        this.acceptBtn = null;
        this.rejectBtn = null;
		// this.deleteBtn = null;

		this.optionsButtonsContainer = addSubelement(this.optionsContainer, "div", { "class": "options-buttons" });

		// Don't allow accepted/rejected lines to be edited
		if (!line.status) {
			this.editBtn = addSubelement(this.optionsButtonsContainer, "button", { "type": "button", "class": "btn btn-sm line-edit btn-primary", "text": "Edit"});
			this.editBtn.click(editLine.bind(this, this.line.id));
		}
	} else {
		this.optionsButtonsContainer = null;
		this.optionsButtonsTopRow = null;
		this.optionsButtonsBottomRow = null;
        this.acceptBtn = null;
        this.rejectBtn = null;
		this.editBtn = null;
		// this.deleteBtn = null;
    }
}

/**
 *
 * @param {string} status
 */
DialogueLineCard.prototype.setStatus = function (status) {
    if (status === this.line.status) {
        status = null;
    }

    var payload = { "status": status };
    return fetch("/crowdsource/lines/" + this.line.id, {
        "method": "PATCH",
        'headers': {
            'Content-Type': 'application/json',
        },
        'body': JSON.stringify(payload),
        'mode': 'same-origin'
    }).then(function (resp) {
        if (resp.status >= 400) {
            return resp.text().then(function (text) {
                showAlert("danger", "Could not retrieve set status: error " + resp.status + ": " + text);
            });
        }

        this.line.status = status;
        this.acceptBtn.removeClass("btn-primary btn-success");
        this.rejectBtn.removeClass("btn-primary btn-danger");
        if (status === "accepted") {
            this.acceptBtn.addClass("btn-primary").text("Reset Status");
        } else {
            this.acceptBtn.addClass("btn-success").text("Accept");
        }

        if (status === "rejected") {
            this.rejectBtn.addClass("btn-primary").text("Reset Status");
        } else {
            this.rejectBtn.addClass("btn-danger").text("Reject");
        }

        this.status.text(STATUS_TEXT[status]);
    }.bind(this));
}


code:
var USER_DATA = null;
var EDITING_MODE = "simple";
var STAGE_MODE = "simple";
var CAN_SELECT_MULTIPLE_STAGES = true;
var CUR_LINE_ID = "";

/** @type {DialogueLineCard[]} */
var SUBMISSION_CARDS = [];

var SEEN_IDS = {};

const SITUATION_DESCS = {
    "selected": "The character has just been selected.",
    "game_start": "The game has just started.",
    "swap_cards": "The character is exchanging cards.",
    "good_hand": "The character has a good hand of cards.",
    "okay_hand": "The character has an okay hand of cards.",
    "bad_hand": "The character has a bad hand of cards.",
    "tie": "The current hand ends in an exact tie.",
    "must_strip_winning": "The character must strip, but has the most clothing left.",
    "must_strip_normal": "The character must strip.",
    "must_strip_losing": "The character must strip, and has the least clothing left.",
    "must_masturbate_first": "The character lost all clothing and must masturbate, and is the first to do so.",
    "must_masturbate": "The character lost all clothing and must masturbate.",
    "stripping": "The character is stripping.",
    "stripped": "The character has just finished taking off a piece of clothing.",
    "male_must_strip": "A male opponent lost the hand and must strip.",
    "female_must_strip": "A female opponent lost the hand and must strip.",
    "male_removing_accessory": "A male opponent is removing an accessory.",
    "male_removed_accessory": "A male opponent has just removed an accessory.",
    "female_removing_accessory": "A female opponent is removing an accessory.",
    "female_removed_accessory": "A female opponent has just removed an accessory.",
    "male_removing_minor": "A male opponent is removing a piece of clothing that does not reveal underwear.",
    "male_removed_minor": "A male opponent has just removed a piece of clothing that does not reveal underwear.",
    "female_removing_minor": "A female opponent is removing a piece of clothing that does not reveal underwear.",
    "female_removed_minor": "A female opponent has just removed a piece of clothing that does not reveal underwear.",
    "male_removing_major": "A male opponent is removing a piece of clothing that will reveal his underwear.",
    "male_removed_major": "A male opponent has just removed a piece of clothing, revealing his underwear.",
    "female_removing_major": "A female opponent is removing a piece of clothing that will reveal her underwear.",
    "female_removed_major": "A female opponent has just removed a piece of clothing, revealing her underwear.",
    "female_chest_will_be_visible": "A female opponent is removing the last piece of clothing covering her breasts.",
    "female_small_chest_is_visible": "A female opponent has just revealed her small breasts.",
    "female_medium_chest_is_visible": "A female opponent has just revealed her average-sized breasts.",
    "female_large_chest_is_visible": "A female opponent has just revealed her large breasts.",
    "male_chest_will_be_visible": "A male opponent is removing the last piece of clothing covering his chest.",
    "male_chest_is_visible": "A male opponent has just removed the last piece of clothing covering his chest.",
    "male_crotch_will_be_visible": "A male opponent is removing the last piece of clothing covering his crotch.",
    "male_small_crotch_is_visible": "A male opponent has just revealed his small penis.",
    "male_medium_crotch_is_visible": "A male opponent has just revealed his average-sized penis.",
    "male_large_crotch_is_visible": "A male opponent has just revealed his large penis.",
    "female_crotch_will_be_visible": "A female opponent is removing the last piece of clothing covering her crotch.",
    "female_crotch_is_visible": "A female opponent has just removed the last piece of clothing covering her crotch.",
    "male_must_masturbate": "A male opponent lost all clothing and must masturbate.",
    "female_must_masturbate": "A female opponent lost all clothing and must masturbate.",
    "male_start_masturbating": "A male opponent has just started masturbating.",
    "female_start_masturbating": "A female opponent has just started masturbating.",
    "male_masturbating": "A male opponent is currently masturbating.",
    "female_masturbating": "A female opponent is currently masturbating.",
    "male_heavy_masturbating": "A male opponent is currently masturbating, and is close to finishing.",
    "female_heavy_masturbating": "A female opponent is currently masturbating, and is close to finishing.",
    "male_finished_masturbating": "A male opponent has just had an orgasm.",
    "female_finished_masturbating": "A female opponent has just had an orgasm.",
    "start_masturbating": "The character has just started masturbating.",
    "masturbating": "The character is currently masturbating.",
    "heavy_masturbating": "The character is currently masturbating, and is close to finishing.",
    "finishing_masturbating": "The character is having an orgasm.",
    "finished_masturbating": "The character has just finished having an orgasm.",
    "after_masturbating": "The character is out of the game and others are still playing.",
    "game_over_victory": "The character won the game.",
    "game_over_defeat": "Someone else won the game."
};

const RANDOM_SITUATIONS = [
	"selected",
    "game_start",
    "swap_cards",
    "good_hand",
    "okay_hand",
    "bad_hand",
    "tie",
    "must_strip_winning",
    "must_strip_normal",
    "must_strip_losing",
    "must_masturbate_first",
    "must_masturbate",
    "stripping",
    "stripped",
    "male_must_strip",
    "female_must_strip",
    "male_removing_accessory",
    "male_removed_accessory",
    "female_removing_accessory",
    "female_removed_accessory",
    "male_removing_minor",
    "male_removed_minor",
    "female_removing_minor",
    "female_removed_minor",
    "male_removing_major",
    "male_removed_major",
    "female_removing_major",
    "female_removed_major",
    "female_chest_will_be_visible",
    "female_small_chest_is_visible",
    "female_medium_chest_is_visible",
    "female_large_chest_is_visible",
    "male_chest_will_be_visible",
    "male_chest_is_visible",
    "male_crotch_will_be_visible",
    "male_small_crotch_is_visible",
    "male_medium_crotch_is_visible",
    "male_large_crotch_is_visible",
    "female_crotch_will_be_visible",
    "female_crotch_is_visible",
    "male_must_masturbate",
    "female_must_masturbate",
    "male_start_masturbating",
    "female_start_masturbating",
    "male_masturbating",
    "female_masturbating",
    "male_heavy_masturbating",
    "female_heavy_masturbating",
    "male_finished_masturbating",
    "female_finished_masturbating",
    "start_masturbating",
    "masturbating",
    "heavy_masturbating",
    "finishing_masturbating",
    "finished_masturbating",
    "after_masturbating",
    "game_over_victory",
    "game_over_defeat"
];

const NON_VARIABLE_SITUATIONS = [
	"selected",
	"game_start",
	"stripping",
	"stripped",
	"must_masturbate_first",
	"must_masturbate",
	"start_masturbating",
	"masturbating",
	"heavy_masturbating",
	"finishing_masturbating",
	"finished_masturbating",
	"after_masturbating",
	"game_over_defeat"
];

const AVOID_REDUNDANCY_SITUATIONS = [
    "swap_cards",
    "good_hand",
    "okay_hand",
    "bad_hand",
    "must_strip_winning",
    "must_strip_normal",
    "must_strip_losing",
    "game_over_victory"
];

const RANDOM_STAGES = ["stage-early", "stage-middle", "stage-late", "stage-forfeit", "stage-post-forfeit"];

const BOWSETTE_LAYERS = ["Skirt", "Earrings", "Shoes", "Stockings", "Top", "Panties", "Bands"];
const NUM_BOWSETTE_LAYERS = 7;
const BOWSETTE_FIRST_MINOR_LOSS = 1; // Lost Skirt (technically Major...)
const BOWSETTE_FIRST_IMPORTANT_LOSS = 5; // Lost Top

const WIKI_LAYERS = ["Shoes", "Apron", "Dress", "Stockings", "Swimsuit Top", "Swimsuit"];
const NUM_WIKI_LAYERS = 6;
const WIKI_FIRST_MINOR_LOSS = 3; // Lost Dress (technically Major... also shoes are Minor but they're also Extra-ish)
const WIKI_FIRST_IMPORTANT_LOSS = 5; // Lost Swimsuit Top

/**
 * Get the currently selected character we're submitting lines for.
 * @returns {string}
 */
function currentlySelectedCharacter() {
    return $("#character-select").val();
}

/**
 * Get whether a Discord login is required to submit lines for the current
 * character.
 * @returns {boolean}
 */
function discordLoginRequired() {
    /* TODO: use /characters/<character_id> route to ascertain this */
    return currentlySelectedCharacter() !== "wikipe-tan";
}

/**
 * @returns {boolean}
 */
function isLoggedIn() {
    return USER_DATA && USER_DATA.user_data;
}

/**
 * @returns {boolean}
 */
function isModerator() {
    return (
        USER_DATA &&
        ((USER_DATA.user_data && USER_DATA.user_data.is_moderator) ||
            (USER_DATA.dev_mode))
    );
}

/**
 * @returns {boolean}
 */
function isDevServerMember() {
    return isLoggedIn() && USER_DATA.user_data.dev_server_member;
}

/**
 * @returns {Number?}
 */
function getDiscordID() {
    return isLoggedIn() && USER_DATA.user_data.id;
}

/**
 * Represents a line to be saved on the server.
 *
 * @param {Object} data
 */
function Line(data) {
    /** @type {string} */
    this.text = (data.text || "").trim();

    /** @type {string} */
    this.expression = (data.expression || "").trim();

    /** @type {string} */
    this.situation = (data.situation || "").trim();

    /** @type {ExactStages} */
    this.stages = data.stages;

    /** @type {TargetData[]} */
    this.targets = data.targets || [];

    /** @type {string} */
    this.marker = data.marker;
    if (this.marker) {
        this.marker = this.marker.trim();
    }

    /** @type {string} */
    this.notes = (data.notes || "").trim();

    /** @type {string} */
    this.author = (data.author || "").trim();
}

Line.prototype.serialize = function () {
    return {
        "text": this.text,
        "author": this.author,
        "expression": this.expression,
        "situation": this.situation,
        "marker": this.marker,
        "notes": this.notes,
        "stages": this.stages.serialize(),
        "targets": this.targets.map(function (t) { return t.serialize(); }),
    };
}

function readLineData() {
    var targetData = readTargetOptions();
    var apData = readAlsoPlayingOptions();
    var stages = readStageData();
    var targets = [];

    if (targetData.character) {
        targets.push(targetData);
    }

    if (apData.character) {
        targets.push(apData);
    }

    return new Line({
        "text": $("#dialogue").val(),
        "expression": $("#expression").val(),
        "situation": $("#situation").val(),
        "marker": $("#marker").val(),
        "notes": $("#notes").val(),
        "author": getUsername(),
        "stages": stages,
        "targets": targets,
    });
}

/**
 * Represents a rough approximation of what stages lines should go to.
 *
 * @param {Object} data
 */
function RoughStages(data) {
    this.early = !!data.early;      /* Early-game (fully clothed to accessories) */
    this.middle = !!data.middle;    /* Mid-game (minor clothing to major) */
    this.late = !!data.late;        /* Late-game (Underwear to naked) */
    this.forfeit = !!data.forfeit;  /* Forfeit lines */
    this.post_forfeit = !!data.post_forfeit;
}

RoughStages.prototype.serialize = function () {
    return {
        "early": this.early,
        "middle": this.middle,
        "late": this.late,
        "forfeit": this.forfeit,
        "post_forfeit": this.post_forfeit
    };
}

/* function readStageData() {
    return new RoughStages({
        "early": $("#stage-early").prop("checked"),
        "middle": $("#stage-middle").prop("checked"),
        "late": $("#stage-late").prop("checked"),
        "forfeit": $("#stage-forfeit").prop("checked"),
        "post_forfeit": $("#stage-post-forfeit").prop("checked")
    });
} */

/**
 * Represents an exact list of what stages lines should go to.
 *
 * @param {Object} data
 */
function ExactStages(data) {
    this.layer0 = !!data[0];
	this.layer1 = !!data[1];
	this.layer2 = !!data[2];
	this.layer3 = !!data[3];
	this.layer4 = !!data[4];
	this.layer5 = !!data[5];
	this.layer6 = !!data[6];
	this.layer7 = !!data[7];
	this.layer8 = !!data[8];
	this.layer9 = !!data[9];
	this.layer10 = !!data[10];
}

ExactStages.prototype.serialize = function () {
    return {
        0: this.layer0,
        1: this.layer1,
		2: this.layer2,
		3: this.layer3,
		4: this.layer4,
		5: this.layer5,
		6: this.layer6,
		7: this.layer7,
		8: this.layer8,
		9: this.layer9,
		10: this.layer10,
    };
}

function readStageData() {
	var layerz = [false, false, false, false, false, false, false, false, false, false, false];

	for (var i = 0; i <= NUM_WIKI_LAYERS; i++) {
		layerz[i] = $("#stage-" + i).prop("checked");
	}

	layerz[NUM_WIKI_LAYERS + 1] = $("#stage-f").prop("checked");
	layerz[NUM_WIKI_LAYERS + 2] = $("#stage-pf").prop("checked");

    return new ExactStages(layerz);
}

/**
 * Represents character targeting data.
 *
 * @param {string} character
 * @param {number} stage
 * @param {string} role
 * @param {string} marker
 */
function TargetData(character, stage, role, marker) {
    this.character = character;
    this.stage = parseInt(stage, 10) || 0;
    this.role = role;
    this.marker = marker;
}

TargetData.prototype.serialize = function () {
    return {
        "character": this.character,
        "stage": this.stage,
        "role": this.role,
        "marker": this.marker,
    };
}

function readTargetOptions() {
    return new TargetData(
        $("#target-id").val(),
        $("#target-stage").val(),
        "target",
        $("#target-marker").val(),
    );
}

function readAlsoPlayingOptions() {
    return new TargetData(
        $("#ap-id").val(),
        $("#ap-stage").val(),
        "alsoPlaying",
        $("#ap-marker").val(),
    );
}

function submitLine(lineID = "") {
	// can't handle other characters yet
	if (currentlySelectedCharacter() !== "wikipe-tan") {
		alert("wtf don't do that!");
		return;
	}

    var line = readLineData();

    /* data validation */
    if (discordLoginRequired()) {
        if (!isLoggedIn()) {
            showAlert("danger", "You need to log in via Discord before submitting lines for this character.");
            return;
        }

        if (!isDevServerMember()) {
            showAlert("danger", "You need to join the SPNATI Development Discord Server before you can submit lines for this character.");
            return;
        }
    }

    if (line.author.length === 0) {
        showAlert("danger", "Please set a username before submitting dialogue!");
        return;
    }

    if (line.text.length === 0) {
        showAlert("danger", "There is no dialogue to submit!");
        return;
    }

    if (line.expression.length === 0) {
        showAlert("danger", "You are missing an Expression for your dialogue.");
        return;
    }

    if (line.situation.length === 0) {
        showAlert("danger", "You are missing a Situation for your dialogue.");
        return;
    }

	// don't want to force precise, but the must-strip stages can't use Naked
	if (line.situation.includes("must_strip")) {
		line.stages[NUM_WIKI_LAYERS] = false;
	}

    var anyStageSet = false;
    Object.keys(line.stages).forEach(function (k) {
        anyStageSet = anyStageSet || line.stages[k];
    });

    if (!anyStageSet) {
        showAlert("danger", "You are missing stages for your dialogue.");
        return;
    }

    hideAlert();
    var serialized = JSON.stringify(line.serialize());

	if (lineID != "") {
		fetch("/crowdsource/lines/" + lineID, {
			"method": "PATCH",
			"headers": { "Content-Type": "application/json" },
			"body": serialized,
			"mode": "same-origin"
		}).then(function (resp) {
			var status = resp.status;
			if (status < 400) {
				var submitText = $("#edit-button > .text");
				submitText.text("Saved!");

				setTimeout(function () { submitText.text("Resubmit"); }, 3000);
				resetFormExceptAuthor();
				loadSubmissions(0);
			} else {
				resp.text().then(function (text) {
					showAlert("danger", "Could not submit line: error " + resp.status + ": " + text);
				});
			}
		});
	} else {
		fetch("/crowdsource/submissions/" + currentlySelectedCharacter(), {
			"method": "POST",
			"headers": { "Content-Type": "application/json" },
			"body": serialized,
			"mode": "same-origin"
		}).then(function (resp) {
			var status = resp.status;
			if (status < 400) {
				var submitText = $("#submit-button > .text");
				submitText.text("Saved!");

				setTimeout(function () { submitText.text("Submit"); }, 3000);
				resetFormExceptAuthor();
				loadSubmissions(0);
			} else {
				resp.text().then(function (text) {
					showAlert("danger", "Could not submit line: error " + resp.status + ": " + text);
				});
			}
		});
	}
}

function submitMergeRequest() {
    return fetch("/crowdsource/submissions/" + currentlySelectedCharacter() + "?limit=10000&status=accepted").then(function (resp) {
        if (resp.status >= 400) {
            resp.text().then(function (text) {
                showAlert("danger", "Could not retrieve lines: error " + resp.status + ": " + text);
            });

            return;
        }

        return resp.json();
    }).then(function (lines) {
		if (!lines || lines.length == 0) {
			showAlert("danger", "There are no lines to submit!");
			return;
		}

		// IMPORTANT: figure out how to add Notes later (right now I have to manually input them in the CE)
		return fetch("/opponents/" + currentlySelectedCharacter() + "/behaviour.xml", { method: "GET" }).then(function (resp) {
			if (resp.status < 200 || resp.status > 299) {
				throw new Error("Fetching " + url + " failed with error " + resp.status + ": " + resp.statusText);
			} else {
				return resp.text();
			}
		}).then(function (xml) {
			behaviour = xml.split("\n");
			behaviour.pop();
			behaviour.pop();
			behaviour.pop();

			for (var i = 0; i < lines.length; i++) {
				behaviour.push("        <trigger id=\"" + lines[i].situation + "\">");

				var stages = Object.keys(lines[i].stages)
					.filter(function (k) { return lines[i].stages[k]; })
					.map(function (k) { return parseInt(k.substring(k.length - 1, k.length)) });

				behaviour.push("            <case stage=\"" + joinNumbers(stages, " ") + "\">");

				if (lines[i].targets) {
					for (var j = 0; j < lines[i].targets.length; j++) {
						target = lines[i].targets[j];

						line = "                <condition role=\"" + target.role + "\" character=\"" + target.character + "\" ";

						if (target.stage) {
							line += "stage=\"" + target.stage + "\" ";
						}

						if (target.marker) {
							line += "sayingMarker=\"" + target.marker + "\" ";
						}

						behaviour.push(line + "/>");
					}
				}

				line = "                <state img=\"#-" + lines[i].expression + ".png\"";

				if (lines[i].marker) {
					line += " marker=\"" + lines[i].marker + "\"";
				}

				behaviour.push(line + ">" + lines[i].text + "</state>");
				behaviour.push("            </case>");
				behaviour.push("        </trigger>");
				behaviour.push("");

				fetch("/crowdsource/lines/" + lines[i].id, {
					"method": "PATCH",
					'headers': {
						'Content-Type': 'application/json',
					},
					'body': JSON.stringify({ "status": "submitted" }),
					'mode': 'same-origin'
				}).then(function (resp) {
					if (resp.status >= 400) {
						return resp.text().then(function (text) {
							showAlert("danger", "Could not retrieve set status: error " + resp.status + ": " + text);
						});
					}
				});
			}

			xml = "";

			for (var i = 0; i < behaviour.length; i++) {
				xml += behaviour[i] + "\n";
			}

			xml += "    </behaviour>\n"
			xml += "</opponent>\n";

			var file = new Blob([xml], {type: "text/xml"});

			// temp, replace with git operation
			var a = document.createElement("a"), url = URL.createObjectURL(file);
			a.href = url;
			a.download = "behaviour.xml";
			document.body.appendChild(a);
			a.click();
			setTimeout(function() {
				document.body.removeChild(a);
				window.URL.revokeObjectURL(url);
			}, 0);
		});
    });
}

function resetFormExceptMajor() {
	dialogue = $("#dialogue").val();
	notes = $("#notes").val();
	resetFormExceptAuthor();
	$("#dialogue").val(dialogue);
	$("#notes").val(notes);
}

function resetFormExceptAuthor() {
	author = $("#author").val();
	resetForm();
	$("#author").val(author);
}

function resetForm() {
	changeButtonState("mode-simple", false, false);
	changeButtonState("mode-advanced", false, false);
	changeButtonState("mode-expert", false, false);

	changeButtonState("mode-" + EDITING_MODE, true);

	CUR_LINE_ID = "";

    $("#dialogue").val("");
    $("#expression").val("");
    $("#situation").val("");
    $("#marker").val("");
    $("#notes").val("");
    $("#author").val("");

	$("#situation-desc").text("");

    clearStageSelection();
	updateImagePreview();
	$("#expression").prop("disabled", false);

    $("#target-id").val("");
    $("#target-stage").val(0);
    $("#target-marker").val("");

    $("#ap-id").val("");
    $("#ap-stage").val(0);
    $("#ap-marker").val("");

	$("#edit-button").prop("hidden", true);
	$("#submit-button").prop("hidden", false);

    hideAlert();

	if (EDITING_MODE === "simple") {
		createRandomSituation();
	}
}

function configureUsername(data) {
    USER_DATA = data;
    updateUsername();
}

function updateUsername() {
    if (isLoggedIn()) {
        var username = USER_DATA.user_data.username + "#" + USER_DATA.user_data.discriminator;
        $(".login-value").text(username).show();
        $("#author").val(username).hide();
        $("#mod-login").hide();
        $("#login-button").hide();
        $("#logout-button").show();
    } else if (discordLoginRequired()) {
        $(".login-value").text("<not logged in>").show();
        $("#author").val("").hide();
        $("#login-button").show();
        $("#logout-button").hide();
    } else {
        $(".login-value").hide();
        $("#logout-button").hide();
        $("#login-button").show();
        $("#author").show();
    }
}

function getUsername() {
    if (isLoggedIn()) {
        return USER_DATA.user_data.username + "#" + USER_DATA.user_data.discriminator;
    } else {
        return ($("#author").val() || "").trim();
    }
}

function updateSelectedCharacter() {
    if (discordLoginRequired()) {
        $("#discord-option > .option-label").text("Discord (Required)");
    } else {
        $("#discord-option > .option-label").text("Discord (Optional)");
    }

    updateUsername();

    SUBMISSION_CARDS = [];
    SEEN_IDS = {};
    loadNextSubmissions();
	setupStageNames();
}

function addCharacterAutofills() {
	fetch("/opponents/listing.xml", { method: "GET" }).then(function (resp) {
		if (resp.status < 200 || resp.status > 299) {
			throw new Error("Fetching " + url + " failed with error " + resp.status + ": " + resp.statusText);
		} else {
			return resp.text();
		}
	}).catch(function() {}).then(function (xml) { return $(xml); }).then(function (xml) {
		onlineOpponents = [];

		xml.find('>individuals>opponent').each(function () {
            var oppStatus = $(this).attr('status');
            var id = $(this).text();

            if (oppStatus === undefined || oppStatus === 'testing') {
                onlineOpponents.push(id);
            }
        });

		onlineOpponents.forEach(function (character) {
			$("#charList").append(new Option(character));
		});

		if (currentlySelectedCharacter() === "bowsette") {
			if (!onlineOpponents.includes("peach")) $("#charList").append(new Option("peach"));
			if (!onlineOpponents.includes("daisy")) $("#charList").append(new Option("daisy"));
			if (!onlineOpponents.includes("rosalina")) $("#charList").append(new Option("rosalina"));
		}
	});
}

function updateSituation() {
	if (EDITING_MODE === "advanced") {
		desc = SITUATION_DESCS[$("#situation").val()];

		if (currentlySelectedCharacter() === "wikipe-tan") {
			desc = desc.replace("The character", "Wikipe-tan");
		}

		$("#situation-desc").text(desc);
	}

	if (EDITING_MODE === "advanced" || EDITING_MODE === "expert") {
		switch ($("#situation").val()) {
		case "selected":
		case "opponent_selected":
		case "game_start":
			clearStageSelection(true);
			$("#stage-modes").prop("hidden", true);
			switchStageModes("precise");
			changeButtonState("stage-0", true);
			CAN_SELECT_MULTIPLE_STAGES = true; // WIKIPE-TAN WORKAROUND
			break;
		case "swap_cards":
		case "good_hand":
		case "okay_hand":
		case "bad_hand":
		case "hand":
		case "must_strip_winning":
		case "must_strip_normal":
		case "must_strip_losing":
		case "must_strip":
		case "game_over_victory":
			clearStageSelection();
			$("#stage-modes").prop("hidden", false);

			if(EDITING_MODE === "advanced") {
				switchStageModes("simple");
			}

			changeButtonState("stage-forfeit", false, true);
			changeButtonState("stage-f", false, true);
			changeButtonState("stage-post-forfeit", false, true);
			changeButtonState("stage-pf", false, true);
			CAN_SELECT_MULTIPLE_STAGES = true; // WIKIPE-TAN WORKAROUND
			break;
		case "stripping":
			clearStageSelection();
			$("#stage-modes").prop("hidden", true);
			switchStageModes("precise");

			changeButtonState("stage-" + NUM_WIKI_LAYERS, false, true);
			changeButtonState("stage-forfeit", false, true);
			changeButtonState("stage-f", false, true);
			changeButtonState("stage-post-forfeit", false, true);
			changeButtonState("stage-pf", false, true);
			CAN_SELECT_MULTIPLE_STAGES = false; // WIKIPE-TAN WORKAROUND
			break;
		case "stripped":
			clearStageSelection();
			$("#stage-modes").prop("hidden", true);
			switchStageModes("precise");

			changeButtonState("stage-0", false, true);
			changeButtonState("stage-forfeit", false, true);
			changeButtonState("stage-f", false, true);
			changeButtonState("stage-post-forfeit", false, true);
			changeButtonState("stage-pf", false, true);
			CAN_SELECT_MULTIPLE_STAGES = false; // WIKIPE-TAN WORKAROUND
			break;
		case "must_masturbate_first":
		case "must_masturbate":
		case "start_masturbating":
			clearStageSelection(true);
			$("#stage-modes").prop("hidden", true);
			switchStageModes("precise");
			changeButtonState("stage-" + NUM_WIKI_LAYERS, true);
			CAN_SELECT_MULTIPLE_STAGES = true; // WIKIPE-TAN WORKAROUND
			break;
		case "masturbating":
		case "heavy_masturbating":
		case "finishing_masturbating":
			clearStageSelection(true);
			$("#stage-modes").prop("hidden", false);

			if(EDITING_MODE === "advanced") {
				switchStageModes("simple");
			}

			changeButtonState("stage-forfeit", true);
			changeButtonState("stage-f", true);
			CAN_SELECT_MULTIPLE_STAGES = true; // WIKIPE-TAN WORKAROUND
			break;
		case "finished_masturbating":
		case "after_masturbating":
		case "game_over_defeat":
			clearStageSelection(true);
			$("#stage-modes").prop("hidden", false);

			if(EDITING_MODE === "advanced") {
				switchStageModes("simple");
			}

			changeButtonState("stage-post-forfeit", true);
			changeButtonState("stage-pf", true);
			CAN_SELECT_MULTIPLE_STAGES = true; // WIKIPE-TAN WORKAROUND
			break;
		default:
			clearStageSelection();
			$("#stage-modes").prop("hidden", false);

			if(EDITING_MODE === "advanced") {
				switchStageModes("simple");
			}

			CAN_SELECT_MULTIPLE_STAGES = true; // WIKIPE-TAN WORKAROUND
		}

		updateOtherHalfOfStages();
		updatePoseSelect($("#situation").val());
		updateImagePreview();
	}
}

function clearStageSelection(disable=false) {
	changeButtonState("stage-early", false, disable);
	changeButtonState("stage-middle", false, disable);
	changeButtonState("stage-late", false, disable);
	changeButtonState("stage-forfeit", false, disable);
	changeButtonState("stage-post-forfeit", false, disable);
	changeButtonState("stage-all", false, disable);

	// clear precise stages too
	tempmode = STAGE_MODE;
	STAGE_MODE = "simple";
	updateOtherHalfOfStages();
	STAGE_MODE = tempmode;
}

function loadSubmissions(page) {
    var start = page * 120;
    var endpoint = "/crowdsource/submissions/" + currentlySelectedCharacter() + "?start=" + start + "&limit=120&status=all";

    return fetch(endpoint).then(function (resp) {
        if (resp.status >= 400) {
            resp.text().then(function (text) {
                showAlert("danger", "Could not retrieve lines: error " + resp.status + ": " + text);
            });

            return;
        }

        return resp.json();
    }).then(function (lines) {
		if (!lines) {
			return;
		}

        var cards = lines.map(function (line) { return new DialogueLineCard(line, isModerator()); });

        Array.prototype.push.apply(SUBMISSION_CARDS, cards.filter(function (card) {
            return !SEEN_IDS[card.line.id];
        }));

        for (var i = 0; i < cards.length; i++) {
            SEEN_IDS[cards[i].line.id] = true;
        }

        updateSubmissionsView();
    });
}

function updateSubmissionsView() {
    var includeUnreviewed = $("#include-unreviewed").prop("checked");
    var includeAccepted = $("#include-accepted").prop("checked");
    var includeRejected = $("#include-rejected").prop("checked");
    var includeSubmitted = $("#include-submitted").prop("checked");

    var display_cards = SUBMISSION_CARDS;

    if (!includeUnreviewed) {
        display_cards = display_cards.filter(function (card) {
            return card.line.status; // !== null
        });
    }

    if (!includeAccepted) {
        display_cards = display_cards.filter(function (card) {
            return card.line.status !== "accepted";
        });
    }

    if (!includeRejected) {
        display_cards = display_cards.filter(function (card) {
            return card.line.status !== "rejected";
        });
    }

    if (!includeSubmitted) {
        display_cards = display_cards.filter(function (card) {
            return card.line.status !== "submitted";
        });
    }

    display_cards.sort(function (a, b) {
        var a_id = parseInt(a.line.id, 16) || 0;
        var b_id = parseInt(b.line.id, 16) || 0;
        return b_id - a_id; // sort reverse order
    });

    var listing = $(".listing-area");
    listing.children("div").detach();
    listing.append(display_cards.map(function (x) { return x.root; }));
}

function loadNextSubmissions() {
    var curPage = Math.floor(SUBMISSION_CARDS.length / 120);
    return loadSubmissions(curPage);
}

/**
 * Show the page alert notification div.
 *
 * @param {string} alertClass
 * @param {string | HTMLElement} contents
 */
function showAlert(alertClass, contents) {
    $(".alert-contents").empty().append(contents);
    $(".alert-notification")
        .attr("class", "alert-notification alert alert-" + alertClass)
        .show();

	$(".alert-notification")[0].scrollIntoView();
}

/**
 * Hide the page alert notification div.
 */
function hideAlert() {
    $(".alert-notification").hide();
}

function editLine(lineID) {
	// can't handle other characters yet
	if (currentlySelectedCharacter() !== "wikipe-tan") {
		alert("wtf don't do that!");
		return;
	}

    return fetch("/crowdsource/lines/" + lineID, {
			"method": "GET",
			"headers": { "Content-Type": "application/json" },
			"mode": "same-origin"
		}).then(function (resp) {
        if (resp.status >= 400) {
            resp.text().then(function (text) {
                showAlert("danger", "Could not retrieve line: error " + resp.status + ": " + text);
            });

            return;
        }

        return resp.json();
    }).then(function (line) {
        switchEditingModes("expert");

		changeButtonState("mode-simple", false, true);
		changeButtonState("mode-advanced", false, true);
		changeButtonState("mode-expert", true, true);

		switchStageModes("precise");

		$("#edit-button").prop("hidden", false);
		$("#submit-button").prop("hidden", true);

		CUR_LINE_ID = line.id;
		$("#dialogue").val(line.text);
		$("#expression").val(line.expression);
		$("#situation").val(line.situation);
		$("#marker").val(line.marker);
		$("#notes").val(line.notes);

		if (line.targets) {
			for (var i = 0; i < line.targets.length; i++) {
				target = line.targets[i];

				if (target.role === "target") {
					$("#target-id").val(target.character);
					$("#target-stage").val(target.stage);
					$("#target-marker").val(target.marker);
				} else if (target.role === "alsoPlaying") {
					$("#ap-id").val(target.character);
					$("#ap-stage").val(target.stage);
					$("#ap-marker").val(target.marker);
				}
			}
		}

		var layerz = line.stages;

		for (var i = 0; i <= NUM_WIKI_LAYERS; i++) {
			changeButtonState("stage-" + i, layerz[i]);
		}

		changeButtonState("stage-f", layerz[NUM_WIKI_LAYERS + 1]);
		changeButtonState("stage-pf", layerz[NUM_WIKI_LAYERS + 2]);

		updateOtherHalfOfStages();
		updatePoseSelect();
		$("#expression").val(line.expression);
		updateImagePreview();

		$(".mode-area")[0].scrollIntoView();
    });
}

function deleteLine(lineID) {
	fetch("/crowdsource/lines/" + lineID, {
        "method": "DELETE",
        'headers': {
            'Content-Type': 'application/json',
        },
        'mode': 'same-origin'
    }).then(function (resp) {
        if (resp.status >= 400) {
            return resp.text().then(function (text) {
                showAlert("danger", "Could not delete line: error " + resp.status + ": " + text);
            });
        }

		SUBMISSION_CARDS = SUBMISSION_CARDS.filter(function (card) {
            return card.line.id !== lineID;
        });

		updateSubmissionsView();
    });
}

function changeButtonState(b, activate, disable) {
	if (typeof disable === "undefined") {
		disable = $("#" + b).prop("disabled");
	}

	$("#" + b).prop("checked", activate);
	$("#" + b).prop("disabled", disable);

	if (activate) {
		$("#button-" + b).addClass("active");
	} else {
		$("#button-" + b).removeClass("active");
	}

	if (disable) {
		$("#button-" + b).addClass("disabled");
	} else {
		$("#button-" + b).removeClass("disabled");
	}

	// Really garbage workaround for bizarre lack of faded green
	if (activate && disable) {
		if ($("#button-" + b).hasClass("stage-input")) {
			$("#button-" + b).removeClass("stage-input");
			$("#button-" + b).removeClass("disabled");
			$("#button-" + b).addClass("stage-input-fadedgreen");
		} else if ($("#button-" + b).hasClass("mode-input")) {
			$("#button-" + b).removeClass("mode-input");
			$("#button-" + b).removeClass("disabled");
			$("#button-" + b).addClass("mode-input-fadedgreen");
		}
	} else {
		if ($("#button-" + b).hasClass("stage-input-fadedgreen")) {
			$("#button-" + b).removeClass("stage-input-fadedgreen");
			$("#button-" + b).addClass("stage-input");
		} else if ($("#button-" + b).hasClass("mode-input-fadedgreen")) {
			$("#button-" + b).removeClass("mode-input-fadedgreen");
			$("#button-" + b).addClass("mode-input");
		}
	}
}

function switchEditingModes(mode) {
	EDITING_MODE = mode;
	localStorage.setItem("editing_mode", mode);

	resetFormExceptMajor();

	if (mode === "simple") {
		createRandomSituation();

		$(".advanced-option").prop("hidden", true);
		$(".expert-option").prop("hidden", true);
		$(".simple-option").prop("hidden", false);
	} else if (mode === "advanced") {
		$(".simple-option").prop("hidden", true);
		$(".expert-option").prop("hidden", true);
		$(".advanced-option").prop("hidden", false);

		switchStageModes("simple");
	} else if (mode === "expert") {
		$(".simple-option").prop("hidden", true);
		$(".advanced-option").prop("hidden", true);
		$(".expert-option").prop("hidden", false);

		switchStageModes("precise");
	}
}

function switchStageModes(mode) {
	STAGE_MODE = mode;

	if(mode === "simple") {
		changeButtonState("stage-mode-simple", true);
		changeButtonState("stage-mode-precise", false);

        $("#stages-options").prop("hidden", false);
		$("#full-stages-options").prop("hidden", true);
	} else if (mode === "precise") {
		changeButtonState("stage-mode-precise", true);
		changeButtonState("stage-mode-simple", false);

        $("#stages-options").prop("hidden", true);
		$("#full-stages-options").prop("hidden", false);
	}
}

function setupStageNames() {
	// can't handle other characters yet
	if (currentlySelectedCharacter() !== "wikipe-tan") {
		alert("wtf don't do that!");
		return;
	}

	for (var i = 0; i < NUM_WIKI_LAYERS; i++) {
		lbl = $("#lbl" + (i+1));
		lbl.text("Lost " + WIKI_LAYERS[i]);

		if(i == NUM_WIKI_LAYERS - 1) {
			lbl.text(lbl.text() + " (Naked)");
		}
	}

	for (var i = NUM_WIKI_LAYERS + 1; i <= 8; i++) {
		$("#stage-area-" + i).prop("hidden", true);
	}
}

// WIKIPE-TAN WORKAROUND
function checkMultipleSelection(ev) {
	if (CAN_SELECT_MULTIPLE_STAGES) return;

	evId = ev.target.id;
	stage = evId.substring(evId.length - 1, evId.length);

	// if deselecting, no need to do anything
	if ($("#stage-" + stage).prop("checked"))
	{
		for (var i = 0; i <= NUM_WIKI_LAYERS; i++) {
			changeButtonState("stage-" + i, false);
		}

		changeButtonState("stage-" + stage, true);
	}

	updatePoseSelect($("#situation").val());
	updateImagePreview();
}

function updateOtherHalfOfStages() {
	// can't handle other characters yet
	if (currentlySelectedCharacter() !== "wikipe-tan") {
		alert("wtf don't do that!");
		return;
	}

	if (STAGE_MODE === "simple") {
		changeButtonState("stage-0", $("#stage-early").prop("checked"), $("#stage-early").prop("disabled"));

		for (var i = 1; i < WIKI_FIRST_MINOR_LOSS; i++) {
			changeButtonState("stage-" + i, $("#stage-early").prop("checked"), $("#stage-early").prop("disabled"));
		}

		for (var i = WIKI_FIRST_MINOR_LOSS; i < WIKI_FIRST_IMPORTANT_LOSS; i++) {
			changeButtonState("stage-" + i, $("#stage-middle").prop("checked"), $("#stage-middle").prop("disabled"));
		}

		for (var i = WIKI_FIRST_IMPORTANT_LOSS; i <= NUM_WIKI_LAYERS; i++) {
			changeButtonState("stage-" + i, $("#stage-late").prop("checked"), $("#stage-late").prop("disabled"));
		}

		changeButtonState("stage-f", $("#stage-forfeit").prop("checked"), $("#stage-forfeit").prop("disabled"));
		changeButtonState("stage-pf", $("#stage-post-forfeit").prop("checked"), $("#stage-post-forfeit").prop("disabled"));
		changeButtonState("stage-a", $("#stage-all").prop("checked"), $("#stage-all").prop("disabled"));
	} else if (STAGE_MODE === "precise") { // lossy conversion
		checked = false;
		for (var i = 0; i < WIKI_FIRST_MINOR_LOSS; i++) {
			if ($("#stage-" + i).prop("checked")) {
				checked = true;
				break;
			}
		}
		changeButtonState("stage-early", checked, $("#stage-0").prop("disabled"));

		checked = false;
		for (var i = WIKI_FIRST_MINOR_LOSS; i < WIKI_FIRST_IMPORTANT_LOSS; i++) {
			if ($("#stage-" + i).prop("checked")) {
				checked = true;
				break;
			}
		}
		changeButtonState("stage-middle", checked, $("#stage-" + WIKI_FIRST_MINOR_LOSS).prop("disabled"));

		checked = false;
		for (var i = WIKI_FIRST_IMPORTANT_LOSS; i <= NUM_WIKI_LAYERS; i++) {
			if ($("#stage-" + i).prop("checked")) {
				checked = true;
				break;
			}
		}
		changeButtonState("stage-late", checked, $("#stage-" + WIKI_FIRST_IMPORTANT_LOSS).prop("disabled"));

		changeButtonState("stage-forfeit", $("#stage-f").prop("checked"), $("#stage-f").prop("disabled"));
		changeButtonState("stage-post-forfeit", $("#stage-pf").prop("checked"), $("#stage-pf").prop("disabled"));
		changeButtonState("stage-all", $("#stage-a").prop("checked"), $("#stage-a").prop("disabled"));
	}
}

function toggleAllStages() {
	// can't handle other characters yet
	if (currentlySelectedCharacter() !== "wikipe-tan") {
		alert("wtf don't do that!");
		return;
	}

	if (STAGE_MODE === "simple") {
		changeTo = $("#stage-all").prop("checked");
		changeButtonState("stage-a", changeTo);
	} else { // "precise"
		changeTo = $("#stage-a").prop("checked");
	}

	for (var i = 0; i <= NUM_WIKI_LAYERS; i++) {
		if (!$("#stage-" + i).prop("disabled")) {
			changeButtonState("stage-" + i, changeTo);
		}
	}

	if (!$("#stage-f").prop("disabled")) {
		changeButtonState("stage-f", changeTo);
	}

	if (!$("#stage-pf").prop("disabled")) {
		changeButtonState("stage-pf", changeTo);
	}

	// change simple stages too
	tempmode = STAGE_MODE;
	STAGE_MODE = "precise";
	updateOtherHalfOfStages();
	STAGE_MODE = tempmode;
}

function checkForToggleAllButton() {
	// can't handle other characters yet
	if (currentlySelectedCharacter() !== "wikipe-tan") {
		alert("wtf don't do that!");
		return;
	}

	// WIKIPE-TAN WORKAROUND
	if (!CAN_SELECT_MULTIPLE_STAGES) {
		changeButtonState("stage-all", false, true);
		changeButtonState("stage-a", false, true);
		return;
	}

	allOn = true;

	for (var i = 0; i <= NUM_WIKI_LAYERS; i++) {
		if (!$("#stage-" + i).prop("disabled") && !$("#stage-" + i).prop("checked")) {
			allOn = false;
			break;
		}
	}

	if (!$("#stage-f").prop("disabled") && !$("#stage-f").prop("checked")) {
		allOn = false;
	}

	if (!$("#stage-pf").prop("disabled") && !$("#stage-pf").prop("checked")) {
		allOn = false;
	}

	if (allOn) {
		changeButtonState("stage-all", true);
		changeButtonState("stage-a", true);
	} else {
		changeButtonState("stage-all", false);
		changeButtonState("stage-a", false);
	}

	updateOtherHalfOfStages();
}

function updateImagePreview() {
	// can't handle other characters yet
	if (currentlySelectedCharacter() !== "wikipe-tan") {
		alert("wtf don't do that!");
		return;
	}

	pose = $("#expression").val();

	if (!pose || pose === "") {
		$("#preview-img").prop("src", "blank.png");
		return;
	}

	stage = getFirstSelectedStage();

	if (stage == -1) {
		$("#preview-img").prop("src", "blank.png");
		return;
	}

	newImg = "/opponents/" + currentlySelectedCharacter() + "/" + stage + "-" + pose + ".png";

	if (newImg == $("#preview-img").prop("src")) {
		return;
	}

	return fetch(newImg, { method: "GET" }).then(function (resp) {
		if (resp.status < 200 || resp.status > 299) {
			if (resp.status == 404) {
				$("#preview-img").prop("src", "blank.png");
			} else {
				throw new Error("Fetching " + url + " failed with error " + resp.status + ": " + resp.statusText);
			}
		} else {
			$("#preview-img").prop("src", newImg);
		}
	});
}

function getFirstSelectedStage() {
	// can't handle other characters yet
	if (currentlySelectedCharacter() !== "wikipe-tan") {
		alert("wtf don't do that!");
		return;
	}

	for (var i = 0; i <= NUM_WIKI_LAYERS; i++) {
		if ($("#stage-" + i).prop("checked")) {
			return i;
		}
	}

	if ($("#stage-f").prop("checked")) return NUM_WIKI_LAYERS + 1;
	if ($("#stage-pf").prop("checked")) return NUM_WIKI_LAYERS + 2;

	return -1;
}

function updatePoseSelect(situation) {
	$(".normal-pose").prop("hidden", true);
	$(".stripping-pose").prop("hidden", true);
	$(".stripped-pose").prop("hidden", true);
	$("#startmast-pose").prop("hidden", true);
	$(".heavy-pose").prop("hidden", true);
	$("#climax-pose").prop("hidden", true);
	$("#after-pose").prop("hidden", true);
	$("#expression").val("");
	$("#expression").prop("disabled", false);

	switch (situation) {
	case "stripping":
		// WIKIPE-TAN WORKAROUND
		curStage = getFirstSelectedStage();

		switch(curStage) {
		case 0:
		case 3:
			$("#stripping-pose-a").text("Stripping (Positive)");
			$("#stripping-pose-b").text("Stripping (Negative)");
			$("#stripping-pose-a").prop("hidden", false);
			$("#stripping-pose-b").prop("hidden", false);
			break;
		default: // stages 1, 2, 4, 5, and no selection
			$("#stripping-pose-a").text("Stripping");
			$("#stripping-pose-a").prop("hidden", false);
			$("#expression").val("z-stripping");
			$("#expression").prop("disabled", true);
		}

		// NON-WORKAROUND VERSION: $(".stripping-pose").prop("hidden", false);
		break;
	case "stripped":
		// WIKIPE-TAN WORKAROUND
		curStage = getFirstSelectedStage();

		switch(curStage) {
		case 2:
			$("#stripped-pose-a").text("Stripped (Overjoyed)");
			$("#stripped-pose-b").text("Stripped (Happy)");
			$("#stripped-pose-a").prop("hidden", false);
			$("#stripped-pose-b").prop("hidden", false);
			break;
		case 6:
			$("#stripped-pose-a").text("Stripped (Overjoyed)");
			$("#stripped-pose-b").text("Stripped (Smug)");
			$("#stripped-pose-a").prop("hidden", false);
			$("#stripped-pose-b").prop("hidden", false);
			break;
		default: // stages 1, 3, 4, 5, and no selection
			$("#stripped-pose-a").text("Stripped");
			$("#stripped-pose-a").prop("hidden", false);
			$("#expression").val("aa-stripped");
			$("#expression").prop("disabled", true);
		}

		// NON-WORKAROUND VERSION: $(".stripped-pose").prop("hidden", false);
		break;
	case "start_masturbating":
		$("#startmast-pose").prop("hidden", false);
		$("#expression").val("z-start");
		$("#expression").prop("disabled", true);
		break;
	case "heavy_masturbating":
		$(".heavy-pose").prop("hidden", false);
		break;
	case "finishing_masturbating":
		$("#climax-pose").prop("hidden", false);
		$("#expression").val("z-climax");
		$("#expression").prop("disabled", true);
		break;
	case "finished_masturbating":
		$("#after-pose").prop("hidden", false);
		$("#expression").val("afterglow");
		$("#expression").prop("disabled", true);
		break;
	default:
		$(".normal-pose").prop("hidden", false);
	}
}

function createRandomSituation() {
	// can't handle other characters yet
	if (currentlySelectedCharacter() !== "wikipe-tan") {
		alert("wtf don't do that!");
		return;
	}

	situation = RANDOM_SITUATIONS[Math.floor(Math.random() * RANDOM_SITUATIONS.length)];
	desc = SITUATION_DESCS[situation];

	if (currentlySelectedCharacter() === "wikipe-tan") {
		desc = desc.replace("The character", "Wikipe-tan");
	}

	$("#random-situation").text(desc);
	$("#situation").val(situation);

	switch (situation) {
	case "selected":
	case "game_start":
		stage = "stage-0";
		mode = "precise";
		break;
	case "must_masturbate_first":
	case "must_masturbate":
		stage = "stage-" + NUM_WIKI_LAYERS;
		mode = "precise";
		break;
	case "start_masturbating":
	case "masturbating":
	case "heavy_masturbating":
	case "finishing_masturbating":
		stage = "stage-forfeit";
		mode = "simple";
		break;
	case "finished_masturbating":
	case "after_masturbating":
	case "game_over_defeat":
		stage = "stage-post-forfeit";
		mode = "simple";
		break;
	case "stripping":
		stripLayer = Math.floor(Math.random() * NUM_WIKI_LAYERS);
		stage = "stage-" + stripLayer;
		mode = "precise";

		desc = "The character is removing their " + WIKI_LAYERS[stripLayer].toLowerCase() + ".";

		if (currentlySelectedCharacter() === "wikipe-tan") {
			desc = desc.replace("The character", "Wikipe-tan");
			desc = desc.replace("their", "her");
		}

		$("#random-situation").text(desc);
		break;
	case "stripped":
		stripLayer = Math.floor(Math.random() * NUM_WIKI_LAYERS);
		stage = "stage-" + (stripLayer + 1);
		mode = "precise";

		desc = "The character has just removed their " + WIKI_LAYERS[stripLayer].toLowerCase() + ".";

		if (currentlySelectedCharacter() === "wikipe-tan") {
			desc = desc.replace("The character", "Wikipe-tan");
			desc = desc.replace("their", "her");
		}

		$("#random-situation").text(desc);
		break;
	case "swap_cards":
	case "good_hand":
	case "okay_hand":
	case "bad_hand":
	case "must_strip_winning":
	case "must_strip_normal":
	case "must_strip_losing":
	case "game_over_victory":
		stage = RANDOM_STAGES[Math.floor(Math.random() * 3)];
		mode = "simple";
		break;
	default:
		stage = RANDOM_STAGES[Math.floor(Math.random() * RANDOM_STAGES.length)];
		mode = "simple";
	}

	clearStageSelection(true);
	switchStageModes(mode);
	$("#stages-options").prop("hidden", true);
	$("#full-stages-options").prop("hidden", true);

	changeButtonState(stage, true);
	updateOtherHalfOfStages();
	updatePoseSelect(situation);
	updateImagePreview();

	if (!NON_VARIABLE_SITUATIONS.includes(situation)) {
		switch (stage) {
		case "stage-early":
			stagedesc = "The character hasn't lost much clothing.";
			break;
		case "stage-middle":
			stagedesc = "The character has lost some clothing, but hasn't lost underwear.";
			break;
		case "stage-late":
			stagedesc = "The character is topless or naked.";
			break;
		case "stage-forfeit":
			stagedesc = "The character is masturbating.";
			break;
		case "stage-post-forfeit":
			stagedesc = "The character has already finished masturbating.";
			break;
		}

		if (currentlySelectedCharacter() === "wikipe-tan") {
			if (AVOID_REDUNDANCY_SITUATIONS.includes(situation)) {
				stagedesc = stagedesc.replace("The character", "She");
			} else {
				stagedesc = stagedesc.replace("The character", "Wikipe-tan");
			}
		} else {
			if (AVOID_REDUNDANCY_SITUATIONS.includes(situation)) {
				stagedesc = stagedesc.replace("The character", "They");
			}
		}

		$("#random-stage").text(stagedesc);
	} else {
		$("#random-stage").text("");
	}
}

$(function () {
	$("#mode-simple").click(function (ev) {
        switchEditingModes("simple");
    });

	$("#mode-advanced").click(function (ev) {
        switchEditingModes("advanced");
    });

	$("#mode-expert").click(function (ev) {
        switchEditingModes("expert");
    });

	$("#stage-mode-simple").click(function (ev) {
		switchStageModes("simple");
    });

	$("#stage-mode-precise").click(function (ev) {
		switchStageModes("precise");
    });

	$(".stage-input").click(function (ev) {
		// it happens twice per click, only do one of them
		if (ev.target.id.includes("button")) return;

		checkMultipleSelection(ev);	// WIKIPE-TAN WORKAROUND
		updateOtherHalfOfStages();
		checkForToggleAllButton();
		updateImagePreview();
    });

	$(".stage-all-input").click(function (ev) {
		// it happens twice per click, only do one of them
		if (ev.target.id.includes("button")) return;

		toggleAllStages();
		updateImagePreview();
    });

    $("#submit-button").click(function (ev) {
        ev.preventDefault();
        ev.stopPropagation();

        submitLine();
    });

	$("#edit-button").click(function (ev) {
        ev.preventDefault();
        ev.stopPropagation();

        submitLine(CUR_LINE_ID);
    });

	$("#reroll-button").click(function (ev) {
        ev.preventDefault();
        ev.stopPropagation();

        createRandomSituation();
    });

    $("#reset-button").click(function (ev) {
        ev.preventDefault();
        ev.stopPropagation();

        resetForm();
    });

	$("#mr-button").click(function (ev) {
        ev.preventDefault();
        ev.stopPropagation();

        submitMergeRequest();
    });

    $(".alert-notification .close-button").click(function (ev) {
        ev.preventDefault();
        ev.stopPropagation();

        hideAlert();
    });

	mode = localStorage.getItem("editing_mode");

	if (mode) {
		EDITING_MODE = mode;
	}

	switchStageModes(STAGE_MODE);
	switchEditingModes(EDITING_MODE);
	resetFormExceptAuthor();

	fetch("/auth/me").then(function (resp) {
        if (resp.status >= 400) {
            resp.text().then(function (text) {
                showAlert("danger", "Could not retrieve current user info: error " + resp.status + ": " + text);
            });
            return;
        }
        return resp.json();
    }).then(function (data) {
        configureUsername(data);
        loadNextSubmissions();
        setInterval(loadSubmissions.bind(null, 0), 30 * 1000);

        if (isModerator()) {
			$(".closed-notification").prop("hidden", true);
			$("#crowdsource-form").prop("hidden", false);

            $("#rejected-checkbox").prop("hidden", false);

			// Make this button visible for ALL mods once Git integration is finished
			if ($("#author").val() === "Kobrad#2197") {
				$("#mr-button").prop("hidden", false);
			}
        }
    });

    var loadMoreButton = $(".load-next-button");
    loadMoreButton.click(function (ev) {
        ev.preventDefault();
        ev.stopPropagation();

        loadMoreButton.children(".spinner").show();
        loadMoreButton.children(".text").text("Loading...");

        function onFinished() {
            loadMoreButton.children(".spinner").hide();
            loadMoreButton.children(".text").text("Load More");
        }

        loadNextSubmissions().then(onFinished, onFinished);
    });

    updateSelectedCharacter();
	addCharacterAutofills();

    $("#include-unreviewed").on("change", updateSubmissionsView);
    $("#include-accepted").on("change", updateSubmissionsView);
    $("#include-rejected").on("change", updateSubmissionsView);
    $("#include-submitted").on("change", updateSubmissionsView);
    $("#character-select").on("change", updateSelectedCharacter);
	$("#situation").on("change", updateSituation);
	$("#expression").on("change", updateImagePreview);
});


code:
function checkPendingRequest(request_id) {
    fetch('/kkl/pending/' + request_id, {
        'method': "GET",
        "mode": "same-origin",
        "redirect": "follow",
    }).then(function (resp) {
        var import_btn = $("#import-button");

        if (resp.status >= 400) {
            import_btn.children(".text").text("Import");
            import_btn.children(".spinner").hide();
            import_btn.removeAttr('disabled');
            $('#attach-upload-result').show().addClass("alert-danger").text("Failed to import (error " + resp.status + ")");
            return;
        }

        var type = resp.headers.get("Content-Type");
        if (type == "application/zip") {
            resp.blob().then(function (blob) {

                import_btn.children(".text").text("Import");
                import_btn.children(".spinner").hide();
                import_btn.removeAttr('disabled');

                var url = window.URL.createObjectURL(blob);
                $("#download-button").attr("href", url).show();
            });
        } else if (type == "application/json") {
            setTimeout(checkPendingRequest.bind(null, request_id), 1000);

            resp.json().then(function (data) {
                if (data.progress) {
                    pct = Math.floor((data.progress.current / data.progress.total) * 100);
                    import_btn.children(".text").text("Importing (" + pct + "%)...");
                }
            });
        }
    });
}

function doImport(ev) {
    var import_btn = $("#import-button");
    var download_btn = $("#download-button");

    var blush = parseInt($("#blush").val(), 10) || -1;
    var anger = parseInt($("#anger").val(), 10) || -1;
    var juice = parseInt($("#juice").val(), 10) || -1;
    var remove_motion = false;
    var kk_code = $('#code').val();

    if ($("#remove_motion").is(":checked")) {
        remove_motion = true;
    }

    import_btn.attr('disabled', 'disabled');
    import_btn.children(".text").text("Importing...");
    import_btn.children(".spinner").show();
    $('#attach-upload-result').removeClass("alert-success alert-danger").text("").hide();

    download_btn.hide();
    var prev_url = download_btn.attr("href");
    if (prev_url) {
        window.URL.revokeObjectURL(prev_url);
    }

    var payload = {
        'code': kk_code,
        'blush': blush,
        'anger': anger,
        'juice': juice,
        'remove_motion': remove_motion,
    };

    fetch('/kkl/disassemble', {
        'method': 'POST',
        'headers': {
            'Content-Type': 'application/json',
        },
        'body': JSON.stringify(payload),
        'mode': 'same-origin'
    }).then(function (resp) {
        return resp.json();
    }).then(function (data) {
        setTimeout(checkPendingRequest.bind(null, data.id), 1000);
    });
}

function doAttachmentUpload(ev) {
    var attach_btn = $("#attachment-button");
    var attach_input = $('#attachment-input');

    var fd = new FormData();
    fd.set('file', attach_input[0].files[0]);

    attach_btn.attr('disabled', 'disabled');
    attach_btn.children(".text").text("Uploading...");
    attach_btn.children(".spinner").show();
    $('#attach-upload-result').removeClass("alert-success alert-danger").text("").hide();

    fetch('/kkl/upload_attachment', {
        'method': 'POST',
        'body': fd,
        'mode': 'same-origin'
    }).then(function (resp) {
        attach_btn.children(".text").text("Upload Image Attachment...");
        attach_btn.children(".spinner").hide();
        attach_btn.removeAttr('disabled');

        if (resp.status < 400) {
            $('#attach-upload-result').show().addClass("alert-success").text("Attachment uploaded!");
        } else {
            $('#attach-upload-result').show().addClass("alert-danger").text("Attachment failed to upload (error " + resp.status + ")");
        }
    });
}

$(function () {
    var import_btn = $("#import-button");
    var attach_btn = $("#attachment-button");
    var attach_input = $('#attachment-input');

    attach_btn.click(function (ev) {
        ev.preventDefault();
        attach_input.trigger('click');
    });

    attach_input.change(doAttachmentUpload);
    import_btn.click(doImport);
});


code:
var img_btn = $('#img-button');
var img_input = $('#img-input');
var result_img = $('#result-image');
var status_text = $('#status');
var dropzone = document.body;

img_btn.click(function (ev) {
    ev.preventDefault();
    img_input.trigger('click');
});

function uploadFile(file) {
    img_btn.text("Uploading...").attr('disabled', 'disabled');
    $('#status').text("");

    fetch('/kkl/autocrop', {
        'method': 'POST',
        'body': file,
        'mode': 'same-origin'
    }).then(function (resp) {
        if (resp.status < 400) {
            return resp.blob();
        } else {
            throw new Error("Image failed to upload (error " + resp
                .status + ")");
        }
    }).then(function (blob) {
        var result_reader = new FileReader();
        result_reader.addEventListener('loadend', function () {
            result_img.attr('src', result_reader.result);
            status_text.text("Image cropped!");
        });
        result_reader.readAsDataURL(blob);
    }).catch(function (reason) {
        status_text.text(reason.message);
    }).then(function () {
        img_btn.text("Upload Image...").removeAttr('disabled');
    });
}

img_input.change(function (ev) {
    uploadFile(img_input[0].files[0]);
});

dropzone.addEventListener("dragenter", function (ev) {
    ev.stopPropagation();
    ev.preventDefault();
}, false);

dropzone.addEventListener("dragover", function (ev) {
    ev.stopPropagation();
    ev.preventDefault();
}, false);

dropzone.addEventListener("drop", function (ev) {
    ev.stopPropagation();
    ev.preventDefault();

    const dt = ev.dataTransfer;
    uploadFile(dt.files[0]);
}, false);