Fixdit for Reddit Redesign

UIX enhancements for Reddit's 2018 redesign. We <3 the redesign. Filter content, view hover cards for subreddits, & more...

目前为 2018-05-27 提交的版本。查看 最新版本

// ==UserScript==
// @name         Fixdit for Reddit Redesign
// @namespace    http://tampermonkey.net/
// @version      0.6.3
// @description  UIX enhancements for Reddit's 2018 redesign. We <3 the redesign. Filter content, view hover cards for subreddits, & more...
// @author       scriptpost (u/postpics)
// @match        https://www.reddit.com/*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// @noframes
// ==/UserScript==

(function ($, undefined) {
  $(function () {
    if (!$('#hamburgers').length) return; // probably using old site.

    // TODO:
    // Add a UI tweak to force comments to reload the page.
    // Add UI tweak to make submission links easier to click?
    // Extend debug: add button to reset storage.
    // Remove ability to toggle required features and options.
    // Add notification each time settings are changed.
    // Add mod menu to menu_hover options.
    // Find more observers to add.
    // Remove setIntervals from observers?
    // User tagging and highlighting (highlight with symbol?)
    // Extend observers for no_prefixes.
    // Test dismiss dialog+settings, then bring it back and save. Error?

    class Feature {
      constructor(data) {
        this.loaded = JSON.parse(GM_getValue('features', '{}'));

        if (this.loaded.hasOwnProperty(data.id)) {
          data.enabled = this.loaded[data.id].enabled;
        }
        else {
          const db_entry = this.loaded;

          db_entry[data.id] = {
            enabled: data.enabled,
            options: {}
          };

          GM_setValue('features', JSON.stringify(db_entry));
          this.enabled = data.enabled;
        }

        this.feature = data;
        this.setting = Feature.add_setting(data);
        this.options = {};
      }

      static add_setting(data) {
        return new Setting(data);
      }

      add_option(option) {
        const loaded_options = this.loaded[this.feature.id].options;
        const is_stored = loaded_options.hasOwnProperty(option.id);

        if (is_stored) {
          // Modify passed object.
          option.enabled = loaded_options[option.id].enabled;

          if (loaded_options[option.id].hasOwnProperty('value')) {
            option.value = loaded_options[option.id].value;
          }
        }
        else {
          // Continue without modifying.
          const db_entry = this.loaded;
          db_entry[this.feature.id].options[option.id] = {};

          if (option.hasOwnProperty('value')) {
            db_entry[this.feature.id].options[option.id].value = option.value;
          }
          db_entry[this.feature.id].options[option.id].enabled = option.enabled;
          GM_setValue('features', JSON.stringify(db_entry));
          this.loaded = db_entry;
        }

        this.options[option.id] = new Option(option);
      }

      set toggle(fn) {
        this.setting.toggle = fn;
      }


      // arg: object
      set options_toggle(functions) {
        this.options_triggers = functions;
      }

      redraw() {
        // Finds all the changed options and updates their appearance.
        // Also calls any provided callback functions.
        this.setting.redraw();
        for (var oid in this.options) {
          const new_val = this.loaded.options[oid].enabled;
          const current_val = this.options[oid].enabled;

          if (new_val !== current_val) {
            const fid = this.feature.id;
            this.options[oid].enabled = new_val;

            const panel = '.fixd_options[data-id="' + fid + '"]';
            const dialog = '.fixd_dialog[data-id="' + oid + '"]';
            const $btn = $(panel + ' .fixd_option[data-id="' + oid + '"]');
            const $switch = $(dialog + ' .fixd_switch[data-id="' + oid + '"]');

            $btn.add($switch).each((idx, el) => {
              if (el.classList.contains('fixd_enabled')) {
                el.classList.remove('fixd_enabled');
              }
              else {
                el.classList.add('fixd_enabled');
              }
            });

            if (this.options_triggers.hasOwnProperty(oid) && this.setting.data.enabled) {
              this.options_triggers[oid](new_val);
            }
          }
        }
      }

      set update(data) {
        this.setting.data.enabled = data.enabled;
        this.loaded = data;
        this.redraw();
      }
    }

    class Setting {
      constructor(data) {
        this.data = data;

        this.button = $('<div>', {
          "text": data.label,
          "class": "fixd_option_btn",
          "data-click-id": "fixd_setting",
          "data-id": data.id
        })[0];

        if (data.enabled) {
          $(this.button).addClass('fixd_enabled');
        }
        if (!data.internal) {
          $('#fixd_settings .fixd_panel:not(.fixd_options)').append(this.button);
        }
      }

      redraw() {
        const el = $('.fixd_options .fixd_switch:not(.fixd_option)[data-id="' + this.data.id + '"]')[0];
        if (this.data.enabled) {
          $(el).add(this.button).addClass('fixd_enabled');
        }
        else {
          $(el).add(this.button).removeClass('fixd_enabled');
        }
        if (this.toggle) {
          this.toggle(this.data.enabled);
        }
      }
    }

    class Option {
      constructor(data) {
        this.enabled = data.enabled;
        this.data = data;
      }
    }

    /**
     * Creates an observer wrapper that can be extended from anywhere.
     * Each time an RO is extended, it disconnects the last observer
     * and adds a new one in its place with all the added functions.
     * When there's a mutation and a condition is met, it loops over
     * all the saved functions (from this.actions).
     */
    class Reddit_Observer {
      constructor(target) {
        this.target = target;
        this.actions = [];
      }

      set callback(callback) {
        this.basis = callback;
      }

      get records() {
        if (this.observer) {
          return this.observer.takeRecords();
        }
      }

      loop_all(mutations) {
        for (var i = 0; i < this.actions.length; i++) {
          this.actions[i](this, mutations);
        }
      }

      extend(fn) {
        this.actions.push(fn);
        if (this.target) {
          this.connect();
        }
      }

      connect(newTarget) {
        if (newTarget) {
          this.target = newTarget;
        }
        const self = this;
        const mutation = function (mutationsList) {
          self.basis(self, mutationsList);
        };
        if (this.observer) {
          this.observer.disconnect();
        }
        this.observer = new MutationObserver(mutation);
        this.observer.observe(this.target, { childList: true });
      }
    }

    const Utils = {
      format_date: {
        age: date => {
          // https://stackoverflow.com/a/23286781
          const diff_date = new Date(new Date() - date);
          let y = diff_date.toISOString().slice(0, 4) - 1970;
          let m = diff_date.getMonth() + 0;
          let d = diff_date.getDate();
          let result;
          if (y > 0) result = (y === 1) ? y + ' year' : y + ' years';
          else if (m > 0) result = (m === 1) ? m + ' month' : m + ' months';
          else result = (d === 1) ? d + ' day' : d + ' days';
          return result;
        }
      }
    };

    const get_reddit_data = function (kind, name) {
      return new Promise(function (resolve, reject) {
        let url;
        const key = kind + '_' + name;
        const cache = JSON.parse(GM_getValue('cache', '{}'));
        let ratelimit = JSON.parse(GM_getValue('ratelimit_get', '{}'));

        if (cache[key]) {
          resolve(cache[key]);
        }
        else if (!ratelimit.remaining || ratelimit.remaining > 150) {
          const req = new XMLHttpRequest();
          url = '/r/' + name + '/about.json';
          req.open('GET', url);

          req.onload = function () {
            if (req.status === 200) {
              const response = JSON.parse(this.response).data;
              let json_data = cache;

              ratelimit = {
                used: this.getResponseHeader('x-ratelimit-used'),
                remaining: this.getResponseHeader('x-ratelimit-remaining'),
                reset: this.getResponseHeader('x-ratelimit-reset')
              };

              json_data[key] = {
                name: response.display_name,
                title: response.title,
                subtitle: response.header_title,
                desc: response.public_description,
                created: response.created,
                subs: response.subscribers,
                subscriber: response.user_is_subscriber
              };

              GM_setValue('ratelimit_get', JSON.stringify(ratelimit));
              GM_setValue('cache', JSON.stringify(json_data));
              resolve(json_data[key]);
            }
            else {
              reject(Error(req.statusText));
            }
          };
          req.onerror = function () {
            reject(Error("Network Error"));
          };
          req.send();
        } else if (ratelimit.remaining) {
          reject(ratelimit.remaining + ' requests remaining.');
        }
      })
    };

    /**
     * Adds a visual indicator to show which mutations are occuring.
     */
    $('body').append($('<div id="fixd_indicators">').hide());
    class observer_indicator {
      constructor(name) {
        this.name = name;
        this.create();
      }

      create() {
        this.$el = $('<div class="fixd_observer_indicator" data-fixd-name="' + this.name + '">');
        $('#fixd_indicators').append(this.$el);
      }

      pulse() {
        this.$el.addClass('fixd_active');
        this.timeout = window.setTimeout(() => {
          this.$el.removeClass('fixd_active');
        }, 2000);
      }
    };

    /**
     * @param {string} html_tag Element to look inside.
     * @param {string} query Inner text to match against.
     * @param {Node} context Region to search.
     */
    var getElementByText = function (html_tag, query, context) {
      const result = document.evaluate(
        "//" + html_tag + "[contains(., '" + query + "')]",
        context, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
      return result;
    };

    const body_indicator = new observer_indicator('doc body');
    const viewframe_indicator = new observer_indicator('viewframe');
    const subreddit_indicator = new observer_indicator('subreddit');
    const overlay_indicator = new observer_indicator('overlay');
    const comment_indicator = new observer_indicator('comment');
    const comment_change_indicator = new observer_indicator('comment change');
    let post_indicator = new observer_indicator('post');

    /**
     * These special observers are defined according to Reddit's DOM behavior.
     * Each observer can be extended with .extend() and re-added with .connect().
     */
    const Body_Observer = new Reddit_Observer(document.body);
    const Viewframe_Observer = new Reddit_Observer($('#hamburgers + div')[0]);
    const Subreddit_Observer = new Reddit_Observer($('.Post').parent().parent().parent().parent().parent()[0]);
    const Overlay_Observer = new Reddit_Observer($('#hamburgers').parent().parent()[0]);
    const Comment_Observer = new Reddit_Observer($('.Comment').parent().parent().parent().parent()[0]);
    const Posts_Observer = new Reddit_Observer($('.Post').parent().parent().parent()[0]);

    const body_mutation_basis = function (self, mutations) {
      body_indicator.pulse();
      self.loop_all(mutations);
    };

    var viewframe_mutation_basis = function (self, mutations) {
      // When new data is fetched.
      viewframe_indicator.pulse();
      let ping = window.setInterval(() => {
        var $posts = $('.Post').parent().parent().parent();
        if ($posts.length) {
          window.clearInterval(ping);
          Subreddit_Observer.connect($posts.parent().parent()[0]);
          Posts_Observer.connect($posts[0]);
          Overlay_Observer.connect($('#hamburgers').parent().parent()[0]);
          self.loop_all(mutations);
        }
      }, 250);
    };

    var subreddit_mutation_basis = function (self, mutations) {
      // When loading from the cache.
      subreddit_indicator.pulse();
      let ping = window.setInterval(() => {
        var $posts = $('.Post').parent().parent().parent();
        if ($posts.length) {
          window.clearInterval(ping);
          Subreddit_Observer.connect($posts.parent()[0]);
          Posts_Observer.connect($posts[0]);
          self.loop_all(mutations);
        }
      }, 250);
    };

    const overlay_mutation_basis = function (self, mutationsList) {
      overlay_indicator.pulse();
      var ping = window.setInterval(() => {
        var $comments = $('#lightbox .Comment.top-level');
        if ($comments.length) {
          window.clearInterval(ping);
          Comment_Observer.connect($comments.parent().parent().parent().parent()[0]);
          self.loop_all(mutationsList);
        }
      }, 250);

      var timeout = window.setTimeout(() => {
        window.clearInterval(ping);
      }, 10000);
    };

    const comment_mutation_basis = function (self, mutations) {
      comment_indicator.pulse();
      self.loop_all(mutations);
    };

    var posts_mutation_basis = function (self, mutations) {
      post_indicator.pulse();
      self.loop_all(mutations);
    };

    Body_Observer.callback = body_mutation_basis;
    Viewframe_Observer.callback = viewframe_mutation_basis;
    Subreddit_Observer.callback = subreddit_mutation_basis;
    Overlay_Observer.callback = overlay_mutation_basis;
    Comment_Observer.callback = comment_mutation_basis;
    Posts_Observer.callback = posts_mutation_basis;

    const clear_cache = (() => {
      let ratelimit = JSON.parse(GM_getValue('ratelimit_get', '{}'));
      if (ratelimit.used !== undefined && ratelimit.used <= 1) {
        GM_setValue('cache', '{}');
      }
    })();

    const get_settings_form = function () {
      const layout = $('<div>', {
        "id": "fixd_settings"
      }).append($('<div>', {
        "class": "fixd_panel"
      }).append($('<h1>', {
        "text": "Fixdit Settings"
      })))[0];
      return layout;
    };

    const draw_options_panel = function (fid) {
      const settings = $('#fixd_settings')[0];
      const feature = FT[fid];

      const setting_toggle = $('<label>', {
        "html": '<input type="checkbox"> ' + feature.setting.data.label,
        "data-click-id": 'fixd_setting_toggle',
        "data-id": fid,
        "class": "fixd_switch"
      })[0];

      const panel = $('<div>', {
        "class": "fixd_panel fixd_options"
      })[0];

      const btn_back = $('<button>', {
        "data-click-id": "back",
        "class": "fixd_btn_back",
        "text": "Back"
      })[0];

      settings.appendChild(panel);

      panel.dataset.id = feature.setting.data.id;
      panel.appendChild(btn_back);
      panel.appendChild($('<h2>', {
        "text": 'Feature'
      })[0]);
      panel.appendChild(setting_toggle);

      if (feature.setting.data.enabled) {
        setting_toggle.querySelector('input').setAttribute('checked', 'checked');
        setting_toggle.classList.add('fixd_enabled');
      }

      for (var oid in feature.options) {
        draw_option_item(panel, feature.options[oid]);
      }
    };

    const draw_option_item = function (panel, option) {
      const label = document.createTextNode(option.data.label);
      const row = $('<label>', {
        "class": "fixd_option",
        "data-id": option.data.id
      })[0];

      if (option.data.hidden) return;

      // Will it be a checkbox or button?
      if (!option.data.type || option.data.type !== 'bool') {
        row.dataset.clickId = "fixd_option_btn";
        row.appendChild($('<div>', {
          "class": "fixd_option_btn",
          "text": option.data.label
        })[0]);
        row.classList.add('fixd_option_btn');
        row.innerText = option.data.label;
      }
      else {
        row.classList.add('fixd_switch');
        row.dataset.clickId = "fixd_option_toggle";
        row.innerHTML = '<input type="checkbox"> ' + option.data.label;
      }
      if (option.enabled) {
        let checkbox = row.querySelector('input');
        if (checkbox) {
          checkbox.setAttribute('checked', 'checked');
        }
        row.classList.add('fixd_enabled');
      }
      panel.appendChild(row);
    };

    const append_option_selector = function (el, data, saved_data) {
      var idx = 0;
      for (var key in data.choices) {
        const choice = data.choices[key];
        let value;
        if (data.type === 'select') {
          value = saved_data.value;
        }
        else {
          if (saved_data.value) {
            value = saved_data.value[idx];
          }
          else {
            value = data.value[idx];
          }
        }
        const item = $('<label>', {
          "html": '<input type="radio" name="choices" value="' + key + '"> ' + choice,
          "class": 'fixd_option_select'
        })[0];
        if (value && value === key) {
          item.querySelector('input').setAttribute('checked', 'checked');
          item.classList.add('fixd_enabled');
        }
        if (data.type === 'multi') {
          item.querySelector('input').setAttribute('type', 'checkbox');
        }
        $(el).append(item);
        idx++;
      };
    };

    const draw_option_dialog = function (oid) {
      const panel = $('#fixd_settings .fixd_options')[0];
      const fid = $(panel).data('id');
      const saved = JSON.parse(GM_getValue('features', '{}'));
      const data = FT[fid].options[oid].data;
      const saved_data = saved[fid].options[oid];

      if (data.type === 'enum') {
        saved_data.value.sort((a, b) => {
          return a.localeCompare(b, 'en', { 'sensitivity': 'base' });
        });
      }

      const dialog = panel.appendChild($('<div>', {
        "data-id": oid,
        "class": "fixd_dialog"
      })[0]);
      const dialog_content = dialog.appendChild($('<div>')[0]);
      const header = $('<div>', {
        "class": "fixd_settings_header"
      })[0];
      dialog_content.appendChild(header);
      header.appendChild($('<button>', {
        "data-click-id": "cancel",
        "class": "fixd_btn_cancel",
        "text": "Back"
      })[0]);
      header.appendChild($('<h3>', {
        "text": FT[fid].setting.data.label
      })[0]);
      const toggle = $('<label>', {
        "html": '<input type="checkbox"> ' + data.label,
        "data-click-id": 'fixd_option_toggle',
        "data-id": data.id,
        "class": "fixd_switch"
      })[0];
      dialog_content.appendChild(toggle);
      if (saved_data.enabled) {
        toggle.classList.add('fixd_enabled');
        toggle.querySelector('input').setAttribute('checked', 'checked');
      }
      if (data.description) {
        dialog_content.appendChild($('<div>', {
          "class": "fixd_description",
          "html": data.description
        })[0]);
      }
      if (data.type === 'enum') {
        dialog_content.appendChild($('<textarea>', {
          "data-click-id": "fixd_option_value",
          "text": saved_data.value.join('\n')
        })[0]);
      }
      else if (['select', 'multi'].includes(data.type)) {
        append_option_selector(dialog_content, data, saved_data);
      }
      const buttons = $('<div>', {
        "class": "fixd_settings_buttons"
      })[0];
      dialog_content.appendChild(buttons);
      buttons.appendChild($('<button>', {
        "data-click-id": "save",
        "class": "fixd_btn_save",
        "text": "Save"
      })[0]);
      $('#fixd_settings').addClass('fixd_expanded').append(dialog).css({});
    };

    const close_dialog = function () {
      $('#fixd_settings').removeClass('fixd_expanded');
      $('#fixd_settings .fixd_dialog').remove();
    };

    const handle_submit = function (ev) {
      const panel = $('#fixd_settings .fixd_options')[0];
      const fid = panel.dataset.id;
      const oid = document.getElementById('fixd_selected_option').dataset.id;
      const saved = JSON.parse(GM_getValue('features', '{}'));
      const feature = FT[fid];
      const option = feature.options[oid];
      let db_entry = saved;
      if (option.data.type === 'enum') {
        let value = $('#fixd_settings .fixd_dialog textarea').val();
        value = value.replace(/[^a-zA-Z\d\n#._-]/mg, "");
        db_entry[fid].options[oid].value = value.split('\n');
      }
      else if (['select', 'multi'].includes(option.data.type)) {
        const dialog = $('#fixd_settings .fixd_dialog')[0];
        const $inputs = $(dialog).find('input[name="choices"]');
        if (option.data.type === 'multi') {
          // Empty array before assigning new value.
          db_entry[fid].options[oid].value = [];
        }
        $inputs.each((idx, el) => {
          if (el.checked) {
            if (option.data.type === 'select') {
              db_entry[fid].options[oid].value = el.value;
              return false;
            }
            else {
              db_entry[fid].options[oid].value[idx] = el.value;
            }
          }
        });
      }
      GM_setValue('features', JSON.stringify(db_entry));
      close_dialog();
    };

    const handle_cancel = function (ev) {
      close_dialog();
    };

    const handle_back = function (ev) {
      $('#fixd_settings .fixd_options').remove();
      $('#fixd_settings .fixd_panel:not(.fixd_options)').show();
    };

    $('body').append(get_settings_form);

    document.addEventListener('click', ev => {
      const path = ev.composedPath();
      if (!path.includes($('#fixd_launch')[0]) && !path.includes($('#fixd_settings')[0])) {
        $('#fixd_settings, #fixd_launch').removeClass('fixd_active');
        return;
      }
      ev.stopImmediatePropagation();

      if (ev.target.dataset.clickId === 'fixd_launcher') {
        if ($(ev.target).hasClass('fixd_active')) {
          $('#fixd_launch, #fixd_settings').removeClass('fixd_active');
          close_dialog();
        }
        else {
          $('#fixd_launch, #fixd_settings').addClass('fixd_active');
          $('#fixd_settings .fixd_options').remove();
          $('#fixd_settings .fixd_panel:not(.fixd_options)').show();
        }
      }
      if (ev.target.dataset.clickId === 'fixd_setting') {
        $('#fixd_settings .fixd_panel:not(.fixd_options)').hide();
        draw_options_panel(ev.target.dataset.id);
      }
      if (ev.target.dataset.clickId === 'fixd_option_btn') {
        const oid = ev.target.dataset.id;
        let selected = document.getElementById('fixd_selected_option');
        if (selected) {
          selected.removeAttribute('id');
        }
        ev.target.id = 'fixd_selected_option';
        draw_option_dialog(oid);
      }
      if (ev.target.dataset.clickId === 'save') {
        handle_submit(ev);
      }
      if (ev.target.dataset.clickId === 'cancel') {
        handle_cancel(ev);
      }
      if (ev.target.dataset.clickId === 'back') {
        handle_back(ev);
      }
      if ($(ev.target).hasClass('fixd_switch') || $(ev.target.parentNode).hasClass('fixd_switch')) {
        let fid = $('#fixd_settings .fixd_options').data('id');
        let oid = ev.target.dataset.id || ev.target.parentNode.dataset.id;
        if (!ev.target.dataset.id) {
          // Checkbox has been clicked by user or label.
          handle_settings_switch(ev, fid, oid);
        }
      }
      if ($(ev.target).hasClass('fixd_option_select') ||
        $(ev.target.parentNode).hasClass('fixd_option_select')) {
        if (ev.target.tagName === 'INPUT') {
          if (ev.target.getAttribute('type') === 'radio') {
            $('#fixd_settings .fixd_dialog')
              .find('input[name="choices"]').parent().removeClass('fixd_enabled');
          }
          ev.target.parentNode.classList.toggle('fixd_enabled');
        }
      }
    });

    const handle_settings_switch = function (ev, fid, oid) {
      const saved = JSON.parse(GM_getValue('features', '{}'));
      const feature = FT[fid];
      const clicked = ev.target.dataset.clickId || ev.target.parentNode.dataset.clickId;

      let db_entry = saved;
      if (clicked === 'fixd_setting_toggle') {
        if (saved[fid].enabled) {
          db_entry[fid].enabled = false;
        }
        else {
          db_entry[fid].enabled = true;
        }
      }
      else {
        let option = feature.options[oid];
        if (saved[fid].options[oid].enabled) {
          db_entry[fid].options[oid].enabled = false;
        }
        else {
          db_entry[fid].options[oid].enabled = true;
        }
      }
      GM_setValue('features', JSON.stringify(db_entry));
      feature.update = db_entry[fid];
    }

    /**
     * Begin modules/features.
     */
    const FT = {};
    FT.ui_selectors = (() => {
      var feature = new Feature({
        id: "ui_selectors",
        label: "Add UI Selectors",
        enabled: true,
        internal: true
      });

      const do_subreddits_boxes = function (context) {
        const subscribers = getElementByText('p', 'subscribers', context);
        const targets = [];

        if (!subscribers) return;
        let target = subscribers.iterateNext();

        while (target) {
          if (!$('fixd--subreddits').find(target).length) {
            const item = target.parentNode.parentNode.parentNode;
            const img = item.querySelector('img');
            const svg = item.querySelector('svg');
            if (img || svg) {
              let $h3 = $(item).parent().parent().find('h3');
              if ($h3.length) {
                $(item).parent().parent().parent().parent().addClass('fixd--subreddits');
              }
            }
          }

          if (subscribers.invalidIteratorState) break;

          target = subscribers.iterateNext();
        }
      };

      var init = function (view) {
        let $content, $side;
        if (!view || view === 'normal') {
          $content = $($('.Post')[0]).parent().parent().parent().parent().parent();
          $side = $content.find('+ div');
          $('#hamburgers + div').addClass('fixd--page');
        }
        else if (view === 'overlay') {
          $content = $('#lightbox > div > div:nth-of-type(2)');
          $side = $content.find('> div:nth-of-type(2)');
        }

        $content.addClass('fixd--content');
        $side.addClass('fixd--side');

        if ($side.length) {
          const moderators_h3 = getElementByText('h3', 'Moderators', $side[0]).iterateNext();
          $(moderators_h3).parent().addClass('fixd--moderators');
          do_subreddits_boxes($side[0]);
        }

      };

      if (feature.setting.data.enabled) {
        init();

        Overlay_Observer.extend(function (self, mutations) {
          init('overlay');
        });
        Viewframe_Observer.extend(function (self, mutations) {
          init();
        });
        Subreddit_Observer.extend(function (self, mutations) {
          init();
        });
      }

      return feature;
    })();

    FT.ui_tweaks = (() => {
      var feature = new Feature({
        id: "ui_tweaks",
        label: "UI Tweaks",
        enabled: false
      });
      feature.add_option({
        id: "no_prefix",
        label: "No prefixes for subreddits, users",
        type: "bool",
        enabled: false
      });
      feature.add_option({
        id: "no_blanks",
        label: "All links can open in current tab",
        type: "bool",
        enabled: false
      });
      feature.add_option({
        id: "override_vote_icons",
        label: "Disable custom vote icons",
        enabled: false,
        type: "bool"
      });
      feature.add_option({
        id: "static_header",
        label: "Make the top bar stationary",
        enabled: false,
        type: "bool"
      });
      feature.add_option({
        id: "reduce_comment_spacing",
        label: "Reduce comment spacing",
        enabled: false,
        type: 'bool'
      });

      const init_override_vote_icon = function (el) {
        if (!el.childNodes.length) {
          const computed_style = window.getComputedStyle(el);
          const bg_value = computed_style.getPropertyValue('background-image');
          const class_list = ['fixd_no_icon'];

          if (bg_value.includes('IconInactive')) {
            class_list.push('fixd_inactive');
          }
          else if (bg_value.includes('IconActive')) {
            class_list.push('fixd_active');
          }
          $(el).addClass(class_list.join(' '));
        }
      };

      const init_override_vote_icons = function (context) {
        const selector = 'button[data-click-id="upvote"], button[data-click-id="downvote"]';
        $(context).find(selector).each((idx, el) => {
          init_override_vote_icon(el);
        });
      };

      var strip_prefixes = function (collection) {
        if (!collection) {
          collection = $('.Post');
        }
        $(collection).each((idx, el) => {
          var $a = $(el).find('a[data-click-id="user"]');
          $a.each((idx, el) => {
            if (!el.children.length) {
              el.innerText = el.innerText.replace("u/", "");
            }
          });
        });

        $(collection).each((idx, el) => {
          var $a = $(el).find('a[data-click-id="subreddit"]');
          $a.each((idx, el) => {
            if (!el.children.length) {
              el.innerText = el.innerText.replace("r/", "");
            }
          });
        });
      };

      var remove_blanks = function () {
        $('a[target="_blank"]').removeAttr('target');
      };

      const static_header = (enabled) => {
        if (enabled === undefined || enabled) {
          document.querySelector('body > div #header').style.position = 'absolute';
        } else {
          document.querySelector('body > div #header').style.position = '';
        }
      };

      const reduce_comment_spacing = (enabled) => {
        if (enabled) {
          document.body.classList.add('fixd_reduce_comment_spacing');
        }
        else {
          document.body.classList.remove('fixd_reduce_comment_spacing');
        }
      }

      feature.options_toggle = {
        'static_header': static_header,
        'reduce_comment_spacing': reduce_comment_spacing
      };

      if (feature.setting.data.enabled) {
        static_header(feature.options.static_header.data.enabled);
        reduce_comment_spacing(feature.options.reduce_comment_spacing.data.enabled);

        Body_Observer.extend(function (self, mutations) {
          if (feature.options.override_vote_icons.data.enabled) {
            init_override_vote_icons(document.body);
          }
          if (feature.options.no_blanks.data.enabled) {
            remove_blanks();
          }
          if (feature.options.no_prefix.data.enabled) {
            strip_prefixes();
          }
        });

        $(document).click(ev => {

          if (feature.options.override_vote_icons.data.enabled) {
            if (['upvote', 'downvote'].includes(ev.target.dataset.clickId)) {
              init_override_vote_icons(ev.target.parentNode);
            }
          }
        });

        Posts_Observer.extend(function (self, mutations) {
          var collection = [];

          for (var record in mutations) {
            let added_nodes = mutations[record].addedNodes;
            if (added_nodes.length === 1) {
              collection.push(added_nodes[0]);
            }
          }
          if (collection.length) {
            if (feature.options.no_prefix.data.enabled) {
              strip_prefixes();
            }
            if (feature.options.no_blanks.data.enabled) {
              remove_blanks();
            }
          }
        });
      }

      return feature;
    })();
    FT.filter_content = (function () {
      const feature = new Feature({
        id: "filter_content",
        label: "Filter Content",
        enabled: true
      });
      feature.add_option({
        id: "subreddits",
        label: "Posts by subreddit",
        type: "enum",
        enabled: true,
        description: "One subreddit name per line. No commas or slashes. Ignores search results.",
        value: []
      });
      feature.add_option({
        id: "users",
        label: "Posts by user",
        description: "One user name per line. No commas or slashes. Ignores search results.",
        type: "enum",
        enabled: false,
        value: []
      });
      feature.add_option({
        id: "comments",
        label: "Comments by user",
        description: "Hides comments and their replies if posted by one of these users \
        (click the + icon to unhide comments). One user name per line. No commas or slashes.",
        type: "enum",
        enabled: false,
        value: []
      });

      const blocked_subs = feature.options.subreddits.data;
      const blocked_submitters = feature.options.users.data;
      const blocked_comments = feature.options.comments.data;

      const regex = {
        url_subreddit: /.*\/r\//i,
        url_user: /.*\/user\//i
      };

      const is_match = function (query, list) {
        let result = list.findIndex(item => query.toUpperCase() === item.toUpperCase());
        if (result !== -1) {
          return true;
        }
      };

      const init_posts = function () {
        const $posts = $('.Post').parent().parent();

        const search_page = (() => {
          const h1 = $('.fixd--page h1')[0];
          const child = (h1) ? h1.firstChild : undefined;

          if (child && child.nodeValue) {
            return child.nodeValue.includes('Search results for');
          }
        })();

        if ($posts.length > 1 && !search_page) {
          // If length is 1, it's probably comments page.
          $posts.each((idx, el) => {
            filter_post(el);
          });
        }
      };

      const init_comments = function () {
        const $comments = $('.Comment').parent().parent().parent();

        $comments.each((idx, el) => {
          filter_comment(el);
        });
      };

      const filter_post = function (node) {
        let sub_link, user_link;
        if (feature.options.subreddits.data.enabled) {
          sub_link = $(node).find('.Post a[data-click-id="subreddit"]:first').attr('href');
        }
        if (feature.options.users.data.enabled) {
          user_link = $(node).find('.Post a[data-click-id="user"]:first').attr('href');
        }

        if (sub_link) {
          let sub_name = sub_link.replace(regex.url_subreddit, "");
          let name = sub_name.replace("/", "");

          if (is_match(name, blocked_subs.value)) {
            node.classList.add('fixd_hidden');
          }
        }
        if (!node.classList.contains('fixd_hidden') && user_link) {
          let user_name = user_link.replace(regex.url_user, "");
          let name = user_name.replace("/", "");

          if (is_match(name, blocked_submitters.value)) {
            node.classList.add('fixd_hidden');
          }
        }
      };

      const filter_comment = function (node) {
        let user_link = $(node).find('.Comment > div:nth-of-type(2) > div > div > a:first').attr('href');

        if (!user_link) return;

        let user_name = user_link.replace(regex.url_user, "");
        user_name = user_name.replace("/", "");

        if (is_match(user_name, blocked_comments.value)) {
          node = node.querySelector('.Comment');
          node.classList.forEach(function (name) {
            // Gets the unique comment ID and finds its threadline.
            var c_id;
            var match = /^t1_/.exec(name);
            if (match) {
              c_id = match.input;
              $(node).parent().find('> div:not(.Comment) .' + c_id).click();
              return false; // end loop.
            }
          });
        }
      };

      const handle_posts_mutation = function (mutation) {
        const nodes_array = Array.from(mutation.addedNodes);

        nodes_array.forEach((node) => {
          filter_post(node);
        });
      };

      const handle_comments_mutation = function (mutations) {
        mutations.forEach(function (mutation) {
          for (var i = 0; i < mutation.addedNodes.length; i++) {
            const nodes_array = Array.from(mutation.addedNodes);

            nodes_array.forEach((node) => {
              filter_comment(node);
            });
          }
        });
      };

      if (feature.setting.data.enabled) {
        if (feature.options.comments.data.enabled) {
          init_comments();

          Overlay_Observer.extend(function (self, mutations) {
            init_comments();
          });

          Comment_Observer.extend(function (self, mutations) {
            handle_comments_mutation(mutations);
          });
        }
        if (feature.options.subreddits.data.enabled || feature.options.users.data.enabled) {
          init_posts();

          Posts_Observer.extend(function (self, mutations) {
            mutations.forEach(function (mutation) {
              for (var i = 0; i < mutation.addedNodes.length; i++) {
                handle_posts_mutation(mutation);
              }
            });
          });

          Viewframe_Observer.extend(function (self, mutations) {
            let $posts = $('.Post').parent().parent().parent();
            init_posts();
          });

          Subreddit_Observer.extend(function (self, mutations) {
            let $posts = $('.Post').parent().parent().parent();
            init_posts();
          });
        }
      }

      return feature;
    })();
    FT.subreddit_info = (function () {
      const feature = new Feature({
        id: "subreddit_info",
        label: "Subreddit Info Box",
        enabled: true
      });
      feature.add_option({
        id: 'delay',
        label: 'Popup delay',
        enabled: true,
        required: true,
        choices: { short: 'Short', medium: 'Medium', long: 'Long' },
        value: 'medium',
        type: 'select'
      });

      const delay = {
        short: 150,
        medium: 300,
        long: 600
      }

      const create_popup = function (data, ev) {
        const classes = ['fixd_popup'];
        const filter_btn_classes = ["fixd_popup_filter"];
        const saved = JSON.parse(GM_getValue('features', '{}'));
        const filter_data = saved.filter_content.options.subreddits;
        if (data.subscriber) {
          classes.push('fixd_subscriber');
        }
        if (saved.filter_content.enabled && filter_data.enabled) {
          classes.push('fixd_filterable');
          if (filter_data.value.includes(data.name)) {
            filter_btn_classes.push('fixd_active');
          }
        }
        let document_width = $('html')[0].offsetWidth;
        let target_offset = $(ev.target).offset().left;
        let offset_left = target_offset;
        if ((document_width - target_offset) < (document_width / 2)) {
          offset_left -= 240;
        }
        const $box = $('<div>', {
          "id": "fixd_popup_subreddit",
          "class": classes.join(' '),
          "data-id": data.name
        }).css({
          top: $(ev.target).offset().top + ev.target.offsetHeight,
          left: offset_left
        })
          .append($('<div>')
            .append($('<h2>', {
              "class": "fixd_popup_name",
              "text": data.name
            }))
            .append($('<div>', {
              "class": "fixd_popup_created",
              "html": data.created
            }))
            .append($('<div>', {
              "class": "fixd_popup_subs",
              "html": '<span>' + data.subs + '</span> Subscribers'
            }))
            .append($('<button>', {
              "class": filter_btn_classes.join(' '),
              "text": 'Filter'
            }))
          )
          .append($('<div>')
            .append($('<div>', {
              "class": "fixd_popup_title",
              "text": data.title
            }))
            .append($('<div>', {
              "class": "fixd_popup_subtitle",
              "text": data.subtitle
            }))
            .append($('<div>', {
              "class": "fixd_popup_desc",
              "html": data.desc
            }))
          );
        return $box;
      };

      const display_popup = function (ev) {
        let name = ev.target.getAttribute('href').split('/')[2];
        get_reddit_data('t5', name).then((data) => {
          const date_created = new Date(data.created * 1000);
          const formatted = {
            name: data.name,
            title: data.title,
            subtitle: data.subtitle,
            created: Utils.format_date.age(date_created),
            subs: data.subs.toLocaleString(),
            subscriber: data.subscriber,
            desc: data.desc
          };
          const $box = create_popup(formatted, ev);
          $('body').append($box);
        }, function (error) {
          console.warn("Error retrieving subreddit info popup.", error);
        });
      };

      if (feature.setting.data.enabled) {
        let tmo_close;
        const init = ($el, open) => {
          let tmo_open;
          $el.on('mouseover', ev => {
            if (ev.target.tagName === 'A') {
              let a = ev.target;
              if ((a.dataset.clickId === 'subreddit' ||
                $(a).parents('p, .md, .fixd--subreddits').length) &&
                a.getAttribute('href').startsWith('/r/')) {
                tmo_open = window.setTimeout(() => { open(ev) }, 500);
              }
            }

          }).on('mouseout', ev => {
            window.clearTimeout(tmo_open);

            if ($('#fixd_popup_subreddit').length) {
              tmo_close = window.setTimeout(() => {
                $('.fixd_popup').detach();
              }, delay[feature.options.delay.data.value] || delay.medium);
            }

            $('#fixd_popup_subreddit').on('mouseover', ev => {
              window.clearTimeout(tmo_close);
            }).on('mouseout', ev => {
              const $popup = $('#fixd_popup_subreddit');
              if ($popup.length) {
                if (ev.target !== $popup[0] && !$(ev.target).parents($popup[0])) {
                  $('.fixd_popup').detach();
                }
              }
            });
          });
        };
        init($(document), (ev) => {
          display_popup(ev);
        });
        $(document).on('click', ev => {
          if (ev.target.classList.contains('fixd_popup_filter')) {
            const saved = JSON.parse(GM_getValue('features', '{}'));
            const name = $('#fixd_popup_subreddit').data('id');
            let db_entry = saved;
            const arr = saved.filter_content.options.subreddits.value;
            if (ev.target.classList.contains('fixd_active')) {
              const idx = arr.indexOf(name);
              db_entry.filter_content.options.subreddits.value.splice(idx);
              ev.target.classList.remove('fixd_active');
            }
            else {
              db_entry.filter_content.options.subreddits.value.push(name);
              ev.target.classList.add('fixd_active');
            }
            GM_setValue('features', JSON.stringify(db_entry));
          }
        });
      }

      return feature;
    })();
    FT.comments_collapse = (function () {
      const feature = new Feature({
        id: "comments_collapse",
        label: "Collapsible Child Comments",
        enabled: true
      });
      feature.add_option({
        id: 'auto',
        type: 'bool',
        label: 'Automatically collapse children',
        enabled: false
      });

      const auto_on = feature.options.auto.data.enabled;

      const init = function (is_mutate) {
        let comment = document.getElementsByClassName('Comment')[0];
        let comments_list;

        if (comment) {
          comments_list = comment.parentNode.parentNode.parentNode.parentNode.childNodes;

          const sort_picker = $('#CommentSort--SortPicker').parent()[0];
          const btn_all_classList = ['fixd_collapse_all'];

          if (auto_on && !is_mutate) {
            btn_all_classList.push('fixd_active');
          }

          const $btn_all = $('<button>', {
            "text": "children",
            "class": btn_all_classList.join(' ')
          });

          if (sort_picker) {
            $(sort_picker).append($btn_all);
          }

          for (var i = 0; i < comments_list.length; i++) {
            init_comment(comments_list[i], is_mutate);
          }
        }
      };

      const init_comment = function (item, is_mutate) {
        item.classList.add('fixd_comment_wrap');
        const is_comment = !!item.getElementsByClassName('Comment').length;
        const is_child = !item.getElementsByClassName('top-level').length;
        const is_thread = !!item.getElementsByClassName('threadline').length;
        const is_hidden = !!item.getElementsByClassName('icon-expand').length;

        if (is_comment && !is_child) {
          $(item).addClass('fixd_top-level');
          const $next = $(item).next();
          const next_is_child = $next.length && !$next[0].getElementsByClassName('top-level').length;
          const next_is_thread = $next.length && !!$next[0].getElementsByClassName('threadline').length;

          if (next_is_child && next_is_thread && !is_hidden && !$(item).find('.fixd_collapse').length) {
            const btn_classList = ['fixd_collapse'];

            if (auto_on && !is_mutate) {
              btn_classList.push('fixd_active');
            }

            const $btn = $('<button>', {
              "text": "children",
              "class": btn_classList.join(' ')
            });

            let $target = $(item).find('button:last');

            if (!$target.length) {
              $target = $(item).find('a:last');
            }

            $btn.insertAfter($target);
          }
        }
        if (auto_on && is_child && !is_mutate && !is_hidden) {
          item.classList.add('fixd_hidden');
        }
      };

      const toggle_visibility = function (clicked) {
        let wrap = $(clicked).parents('.fixd_comment_wrap')[0];
        let is_active = $(clicked).hasClass('fixd_active');

        const check_next = function ($next) {
          const next_is_child = !$next[0].getElementsByClassName('top-level').length;
          const next_is_thread = !!$next[0].getElementsByClassName('threadline').length;
          if (next_is_child && next_is_thread) {
            if (is_active) {
              $next.removeClass('fixd_hidden');
            }
            else {
              $next.addClass('fixd_hidden');
            }
            if ($next.next().length) {
              check_next($next.next());
            }
          }
        };

        check_next($(wrap).next());

        if (is_active) {
          $(clicked).removeClass('fixd_active');
        } else {
          $(clicked).addClass('fixd_active');
        }
      };

      const handle_click = function (ev) {
        if ($(ev.target).hasClass('fixd_collapse')) {
          ev.stopImmediatePropagation();
          toggle_visibility(ev.target);
        }
        else if ($(ev.target).hasClass('fixd_collapse_all')) {
          ev.stopImmediatePropagation();
          if ($(ev.target).hasClass('fixd_active')) {
            $('.fixd_collapse.fixd_active').click();
          }
          else {
            $('.fixd_collapse:not(.fixd_active)').click();
          }
          if ($('.fixd_collapse:not(.fixd_active)').length) {
            $(ev.target).removeClass('fixd_active');
          }
          else {
            $(ev.target).addClass('fixd_active');
          }
        }
      };

      const handle_mutation = function (mutation) {
        init_comment(mutation.previousSibling, true);
      };

      if (feature.setting.data.enabled) {
        init();
        $(document).on('click', handle_click);

        Overlay_Observer.extend(function (self, mutations) {
          for (var i = 0; i < mutations.length; i++) {
            if (mutations[i].addedNodes.length) {
              init(true);
            }
          }
        });

        Comment_Observer.extend(function (self, mutations) {
          for (var a = 0; a < mutations.length; a++) {
            handle_mutation(mutations[a]);
          }
        });
      }

      return feature;
    })();
    FT.menu_hover = (function () {
      var feature = new Feature({
        id: "menu_hover",
        label: "Hover to open menus",
        enabled: false
      });
      feature.add_option({
        id: 'menus',
        type: 'multi',
        label: "Choose menus",
        enabled: true,
        choices: { sortpicker: 'Sort Posts', user: 'User dropdown' },
        value: ["sortpicker", "user"]
      });
      feature.add_option({
        id: 'delay',
        type: 'select',
        label: "Delay",
        enabled: true,
        requires: "menus",
        choices: { short: 'Short', medium: 'Medium', long: 'Long' },
        value: 'medium'
      });

      const menus_val = feature.options.menus.data.value;
      const menus = {
        sortpicker: '#ListingSort--SortPicker',
        user: '#USER_DROPDOWN_ID'
      }

      const delay_val = feature.options.delay.data.value;
      const delay = {
        short: 150,
        medium: 300,
        long: 600
      }

      if (feature.setting.data.enabled) {
        if (feature.options.menus.data.enabled) {

          var add_menu_listeners = function (selector) {
            var commit_timeout;
            $(selector).on('mouseover', function (ev) {
              commit_timeout = window.setTimeout(() => {
                ev.target.click();
              }, delay[delay_val] || delay.medium);
            });
            $(selector).on('mouseout', function (ev) {
              window.clearTimeout(commit_timeout);
            });
          };

          for (var i = 0; i < menus_val.length; i++) {
            add_menu_listeners(menus[menus_val[i]]);
          }
        }
      }

      return feature;
    })();

    $('body').append('<div id="fixd_launch" data-click-id="fixd_launcher"></div>');

    FT.remindme = (function () {
      const feature = new Feature({
        id: "remindme",
        label: "Remind Me",
        enabled: false,
        internal: true
      });
      feature.add_option({
        id: "post",
        label: "Posts",
        enabled: false,
        type: "enum"
      });
      feature.add_option({
        id: "comment",
        label: "Comments",
        enabled: false,
        type: "enum"
      });
      const show_menu = function (ev, target, menu) {
        menu.display(ev, target, 'left');
      };

      return feature;
    })();
    FT.debug = (function () {
      var feature = new Feature({
        id: "debug",
        label: "Debug Fixdit",
        enabled: false
      });

      const init = (enabled) => {
        if (enabled) {
          $('#fixd_indicators').show();
          document.body.classList.add('fixd_debug');
        }
        else {
          $('#fixd_indicators').hide();
          document.body.classList.remove('fixd_debug');
        }
      };

      feature.toggle = init;
      init(feature.setting.data.enabled);
      return feature;
    })();

    const _stylesheet = '<style type="text/css" id="fixdit">#fixd_settings .fixd_option_select input,#fixd_settings .fixd_switch input,#fixd_settings button{vertical-align:middle}#fixd_launch{box-sizing:border-box;position:fixed;top:2px;right:2px;width:24px;height:24px;z-index:100;text-align:center;border-radius:50%;border:2px solid hsla(70,0%,100%,.5);border-left-color:hsla(70,0%,0%,1);border-right-color:hsla(70,0%,0%,1);background:0 0;cursor:pointer;-moz-user-select:none;user-select:none}#fixd_launch.fixd_active{border-color:hsla(70,0%,100%,.5);border-top-color:hsla(70,0%,0%,1);border-bottom-color:hsla(70,0%,0%,1)}#fixd_launch:not(.fixd_active):hover:before{content:"Fixdit settings...";display:inline-block;padding:6px 12px;font-size:80%;color:#fff;position:absolute;right:calc(100% + 12px);white-space:nowrap;background:hsla(0,0%,0%,.8);border-radius:2px;pointer-events:none}#fixd_settings{position:fixed;top:0;right:0;opacity:0;z-index:101;padding:24px 12px;background:hsla(180,2%,90%,.95);border-radius:3px;box-shadow:0 5px 10px hsla(0,0%,0%,.25),0 0 3px hsla(0,0%,0%,.25);font-size:80%;width:300px;visibility:hidden;overflow:hidden;transition:visibility .2s,height .2s,bottom .2s,left .2s,top .2s,right .2s,opacity .2s}#fixd_settings.fixd_active{visibility:visible;opacity:1;top:5px;right:24px}#fixd_settings.fixd_expanded{bottom:5px}.fixd_description{margin:12px 0 0;line-height:1.3}#fixd_settings h1{font-size:140%;font-weight:400;margin:0 0 .5em}#fixd_settings h2,#fixd_settings h3{font-size:120%;font-weight:400;display:inline-block;vertical-align:middle;margin-bottom:10px}#fixd_settings .fixd_option,#fixd_settings .fixd_option_btn,#fixd_settings .fixd_option_select,#fixd_settings .fixd_switch{display:block;margin:1px -12px 0;cursor:default;-moz-user-select:none;user-select:none;background:#e4e6e6}#fixd_settings .fixd_option_btn{margin:1px -12px;cursor:default}#fixd_settings .fixd_option_btn:after{content:" >";color:#999;font-weight:700;float:right}#fixd_settings .fixd_option_btn:hover:after{color:#000}#fixd_settings .fixd_option_btn:hover,#fixd_settings .fixd_setting:hover{outline:hsla(0,0%,0%,.3) solid 1px}#fixd_settings .fixd_option_btn,#fixd_settings .fixd_option_select,#fixd_settings .fixd_switch{padding:10px 16px}#fixd_settings .fixd_enabled{background:#fff}#fixd_settings .fixd_switch:not(.fixd_option){font-size:120%;margin-bottom:8px;background:#f2f2f2}#fixd_settings .fixd_switch.fixd_enabled:not(.fixd_option){color:#000;background:#fff}#fixd_settings .fixd_dialog,#fixd_settings .fixd_dialog>div{position:absolute;top:0;left:0;bottom:0;border-radius:4px;background:hsla(180,2%,90%,1);right:0}#fixd_settings .fixd_option span{padding-left:.3em}#fixd_settings .fixd_dialog{content:""}#fixd_settings .fixd_dialog>div{padding:24px 12px;display:flex;flex-direction:column}.fixd_dialog textarea{box-sizing:border-box;min-width:100%;max-width:100%;min-height:50px;max-height:100%;border:1px solid #fff;flex:1;margin-top:12px}.fixd_settings_buttons{margin-top:12px}#fixd_settings button{text-transform:uppercase;border:1px solid transparent;border-radius:2px}#fixd_settings button:hover{border:1px solid #666}#fixd_settings .fixd_btn_back,#fixd_settings .fixd_btn_cancel{font-weight:700;color:transparent;margin-right:12px;padding:0;overflow:hidden;width:30px;height:30px;line-height:30px;margin-bottom:10px;border:1px solid #ccc;background:0 0}.fixd_btn_back:before,.fixd_btn_cancel:before{color:#000;content:"< "}#fixd_settings .fixd_btn_save{color:#fff;padding:8px 24px;background:#0087cc}button.fixd_collapse,button.fixd_collapse_all{cursor:pointer;color:inherit;display:inline-block;background:0 0;outline:0;font-size:12px;font-weight:700}a+button.fixd_collapse{margin-left:10px}button.fixd_collapse_all{margin-left:20px;color:#a6a4a4;font-size:12px;font-weight:700;text-transform:uppercase}button.fixd_collapse:before,button.fixd_collapse_all:before{content:"Hide "}button.fixd_collapse:after,button.fixd_collapse_all:after{content:" <<"}button.fixd_collapse.fixd_active:before,button.fixd_collapse_all.fixd_active:before{content:"Show "}button.fixd_collapse.fixd_active:after,button.fixd_collapse_all.fixd_active:after{content:" >"}#fixd_indicators{position:fixed;top:40px;right:0;z-index:100}.fixd_observer_indicator{box-sizing:border-box;margin:5px 0 0;width:20px;height:15px;border:1px solid hsla(0,0%,0%,.2);background:hsla(0,0%,50%,.2);transition:all .3s}.fixd_observer_indicator.fixd_active{border-color:hsla(0,0%,0%,1);background:hsla(0,0%,100%,1)}.fixd_observer_indicator.fixd_active:before,.fixd_observer_indicator:hover:before{content:attr(data-fixd-name);display:block;position:absolute;margin-top:-1px;right:100%;white-space:nowrap;color:#fff;font-size:11px;padding:3px 6px;background:hsla(0,0%,0%,.4);pointer-events:none}.fixd_observer_indicator:hover:before{background:hsla(0,0%,0%,.8)}.fixd_popup{box-sizing:border-box;position:absolute;z-index:100;padding:12px;font-size:12px;border-radius:4px;border-top:4px solid #c1cfd6;color:#1c1c1c;background-color:#fff;box-shadow:rgba(0,0,0,.2) 0 1px 3px}#fixd_popup_subreddit.fixd_subscriber{border-top-color:#0076d1}.fixd_popup>div{float:left}.fixd_popup>div:nth-of-type(2){width:200px;margin-left:12px;padding-left:12px;border-left:1px solid #edeff1}.fixd_popup .fixd_popup_subs span,.fixd_popup h2{display:block;font-size:16px;font-weight:500;line-height:20px}.fixd_popup .fixd_popup_created,.fixd_popup .fixd_popup_subs{font-weight:500}.fixd_popup .fixd_popup_subs{margin-top:12px}.fixd_popup h2:before{content:"r/"}.fixd_popup .fixd_popup_subtitle{margin-bottom:.5em;color:#7f7f7f}.fixd_popup .fixd_popup_desc{color:#7f7f7f;line-height:1.2}.fixd_popup .fixd_popup_desc,.fixd_popup .fixd_popup_title{margin:0 0 .5em}.fixd_popup:not(.fixd_filterable) .fixd_popup_filter{display:none}.fixd_popup_filter{font-size:12px;text-transform:uppercase;padding:8px 12px;margin-top:12px;border-radius:2px;color:#fff;border:1px solid transparent;background:#0076d1;cursor:pointer}.fixd_popup_filter.fixd_active{color:#0076d1;border:1px solid;background:#fff}.fixd_popup_filter.fixd_active:before{content:"un"}.fixd_tooltip{pointer-events:none}body:not(.fixd_debug) .fixd_hidden{visibility:hidden;position:absolute}body.fixd_debug .fixd_hidden{opacity:.6}button.fixd_no_icon.fixd_active[data-click-id=upvote]{color:#f40!important}button.fixd_no_icon.fixd_active[data-click-id=downvote]{color:#7091ff!important}button.fixd_no_icon[data-click-id=upvote]:hover{color:#cc3600}button.fixd_no_icon[data-click-id=downvote]:hover{color:#5b75cc}button.fixd_no_icon[data-click-id=downvote]:before,button.fixd_no_icon[data-click-id=upvote]:before{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:redesignFont}button.fixd_no_icon[data-click-id=upvote]:before{content:"\F12A"}button.fixd_no_icon[data-click-id=downvote]:before{content:"\F107"}body.fixd_reduce_comment_spacing .Comment.top-level{margin-top:0}</style>';
    $('body').append(_stylesheet);

  });
})(window.jQuery.noConflict(true));