MyAnimeList (MAL) Track Missing Relations

Allows to find missing relations and entries with wrong chapter/episode count.

当前为 2016-01-15 提交的版本,查看 最新版本

// ==UserScript==
// @name        MyAnimeList (MAL) Track Missing Relations
// @namespace   https://greasyfork.org/users/7517
// @description Allows to find missing relations and entries with wrong chapter/episode count.
// @icon        http://i.imgur.com/b7Fw8oH.png
// @version     6.5
// @author      akarin
// @include     /^http:\/\/myanimelist\.net\/?$/
// @include     /^http:\/\/myanimelist\.net\/profile/
// @grant       none
// @noframes
// ==/UserScript==

(function($) {
  "use strict";
  
// Status codes used by MAL API
var USER_STATUS = {
    IN_PROCESS: 1, COMPLETED: 2, ON_HOLD: 3, DROPPED: 4, PLAN_TO: 6
  },
  SERIES_STATUS = {
    IN_PROCESS: 1, COMPLETED: 2, NOT_YET: 3
  };

// Main object
var mal = {
  name: '', // nickname of the user
  type: 'anime' // doesn't matter here
};

mal.settings = {
  // Script settings:
  cache: '4.0',
  ajax: { delay: 300, timeout: 10000 },
  
  // Interface settings:
  width: 670,
  height: 765,
  
  // Predefined data lists:
  availableRelations: [
    'Alternative Setting', 'Alternative Version', 'Character', 'Full Story', 'Other',
    'Parent Story', 'Prequel', 'Sequel', 'Side Story', 'Spin-off', 'Summary'
  ],
  availableStatus: {
    anime: ['Watching', 'Completed', 'On-Hold', 'Dropped', 'Plan to Watch'],
    manga: ['Reading', 'Completed', 'On-Hold', 'Dropped', 'Plan to Read']
  },
  
  // User settings:
  excludedRelations: [],
  excludedStatus: { anime: [], manga: [] },
  fullScan: { anime: false, manga: false },
  
  load: function() {
    mal.settings.excludedRelations = ['Adaptation'];
    $.each(mal.settings.availableRelations, function(i, val) {
      var id = 'mr_x' + mal.type[0] + '_' + val.toHtmlId();
      if (mal.loadSetting(id, 'false') !== 'false') {
        mal.settings.excludedRelations.push(val);
      }
    });
    
    $.each(['anime', 'manga'], function(i, status) {
      mal.settings.excludedStatus[status] = ['Empty Status'];
      $.each(mal.settings.availableStatus[status], function(i, val) {
        var id = 'mr_xs' + status[0] + '_' + val.toHtmlId();
        if (mal.loadSetting(id, 'false') !== 'false') {
          mal.settings.excludedStatus[status].push(val);
        }
      });
      
      mal.settings.fullScan[status] = mal.loadSetting('mr_xo' + status[0] + '_fullscan', 'false') !== 'false';
    });
  },
  
  reset: function() {
    $.each(mal.settings.availableRelations, function(i, val) {
      mal.saveSetting('mr_xa_' + val.toHtmlId(), 'false');
      mal.saveSetting('mr_xm_' + val.toHtmlId(), 'false');
    });
    
    $.each(['anime', 'manga'], function(i, status) {
      $.each(mal.settings.availableStatus[status], function(i, val) {
        mal.saveSetting('mr_xs' + status[0] + '_' + val.toHtmlId(), 'false');
      });
      
      mal.saveSetting('mr_xo' + status[0] + '_fullscan', 'false');
    });
  }
};

$.fn.myfancybox = function(onstart) {
  return $(this).click(function() {
    mal.fancybox.start(onstart);
  });
};

mal.fancybox = {
  body: $('<div id="mr_fancybox_inner"></div>'),
  outer: $('<div id="mr_fancybox_outer"></div>'),
  wrapper: $('<div id="mr_fancybox_wrapper"></div>'),
  
  init: function(el) {
    mal.fancybox.outer.hide()
      .append(mal.fancybox.body)
      .insertAfter(el);
    
    mal.fancybox.wrapper.hide()
      .insertAfter(el);
    
    mal.fancybox.wrapper.click(function() {
      mal.fancybox.close();
    });
  },
  
  start: function(onstart) {
    mal.fancybox.body.children().hide();
    if (onstart()) {
      mal.fancybox.wrapper.show();
      mal.fancybox.outer.show();
    } else {
      mal.fancybox.close();
    }
  },
  
  close: function() {
    mal.fancybox.outer.hide();
    mal.fancybox.wrapper.hide();
  }
};

function main() {
  if (document.URL.match(/^http:\/\/myanimelist\.net\/?$/)) {
    mal.name = $('#header-menu span.profile-name').text().trim();
  } else {
    mal.name = $('.icon-rss + div > a:first').prop('href').match(/&u=(.+)$/)[1].trim();
  }
  
  if (mal.name.length === 0) {
    return;
  }
  
  mal.settings.load();
  
  var header = $('<h2 id="mr_body_title">Missing Relations <span>' +
        '<a id="mr_version" href="http://greasyfork.org/scripts/9261" target="_blank">' +
        'v' + GM_info.script.version + '</a></span></h2>'),
    panel = $('<div id="mr_panel"></div>')
      .append('<span id="mr_profile"><a href="/profile/' + mal.name + '"><b>' + mal.name + '</b></a></span>')
      .append(mal.content.status)
      .prepend($('<div id="mr_links"></div>')
        .append($('<span id="mr_links_settings"></span>')
          .append($('<a href="javascript:void(0);" title="Switch lists" id="mr_link_switch">Manga</a>').click(function() {
             var type = mal.type === 'anime' ? 'manga' : 'anime';
             mal.fancybox.close();
             $('div.floatRightHeader > a[id^="mr_link_' + type + '"]').trigger('click');
             return true;
          }))
          .append(' - ')
          .append($('<a href="javascript:void(0);" title="Change calculation settings">Settings</a>').myfancybox(function() {
             mal.content.updateSettings();
             return true;
          }))
          .append(' - ')
          .append($('<a href="javascript:void(0);" title="Recalculate missing relations">Rescan</a>').click(function() {
            if (mal.entries.isUpdating()) {
              alert('Updating in process!');
            }
            else if (confirm('Recalculate missing relations?')) {
              mal.entries.update(mal.settings.fullScan[mal.type], false);
            }
          }))
          .append(' - ')
          .append($('<a href="javascript:void(0);" title="Update current missing relations">Update</a>').click(function() {
            if (mal.entries.isUpdating()) {
              alert('Updating in process!');
            }
            else if (confirm('Update current missing relations?')) {
              mal.entries.update(mal.settings.fullScan[mal.type], true);
            }
          }))
        )
      );
    
  mal.content.body
    .prepend(panel)
    .prepend(header)
    .append(mal.content.list);
    
  mal.content.settings
    .prepend(header.clone());
    
  mal.fancybox.init('#contentWrapper');
    
  mal.fancybox.body
    .append(mal.content.body)
    .append(mal.content.settings);
  
  var links = $('<div class="floatRightHeader"></div>')
    .append('<a href="javascript:void(0);" id="mr_link_anime" style="font-size: 11px; font-weight: normal;">Missing Anime Relations</a>')
    .append(' - ')
    .append('<a href="javascript:void(0);" id="mr_link_manga" style="font-size: 11px; font-weight: normal;">Missing Manga Relations</a>')
    .prependTo(document.URL.match(/^http:\/\/myanimelist\.net\/?$/)
      ? '.panel_introduction > div.spaceit'
      : '.container-right > #statistics > h2');
  
  $('a[id^="mr_link_"]', links).each(function() {
    var type = this.id.match(/^mr_link_(\w+)$/)[1];
    $(this).myfancybox(function() {
      if (type !== mal.type && mal.entries.isUpdating()) {
        alert('Updating in process!');
        return false;
      }
      
      var listType = $('#mr_list_type', mal.content.body);
      mal.type = type;
      $('#mr_links_settings > #mr_link_switch', mal.content.body)
        .text(mal.type === 'anime' ? 'Manga' : 'Anime');
      
      if (listType.length === 0 || listType.val() !== mal.type) {
        if (mal.entries.checkVersion()) {
          mal.entries.load();
        } else {
          mal.entries.clear();
        }
        mal.content.updateList(false);
      }
      
      mal.content.body.show();
      return true;
    });
  });
}

mal.entries = {
  // Cache data:
  left: {}, graph: {}, title: {}, wrong: {}, ignore: {},
  // Progress data:
  total: 0, done: 0, fail: 0, skip: 0,
      
  load: function() {
    mal.entries.clear();
    mal.entries.left = mal.loadValue('mal.entries.left', mal.entries.left);
    mal.entries.graph = mal.loadValue('mal.entries.graph', mal.entries.graph);
    mal.entries.title = mal.loadValue('mal.entries.title', mal.entries.title);
    mal.entries.wrong = mal.loadValue('mal.entries.wrong', mal.entries.wrong);
    mal.entries.ignore = mal.loadValue('mal.entries.ignore', mal.entries.ignore);
  },

  save: function() {
    mal.saveValue('mal.entries.version', mal.settings.cache);
    mal.saveValue('mal.entries.left', mal.entries.left);
    mal.saveValue('mal.entries.graph', mal.entries.graph);
    mal.saveValue('mal.entries.title', mal.entries.title);
    mal.saveValue('mal.entries.wrong', mal.entries.wrong);
    mal.entries.saveIgnore();
  },
  
  saveIgnore: function() {
    mal.saveValue('mal.entries.ignore', mal.entries.ignore);
  },

  clear: function() {
    mal.entries.total = 0;
    mal.entries.done = 0;
    mal.entries.fail = 0;
    mal.entries.skip = 0;
    mal.entries.left = {};
    mal.entries.graph = {};
    mal.entries.title = {};
    mal.entries.wrong = {};
  },
  
  clearIgnore: function() {
    mal.entries.ignore = {};
  },
  
  checkVersion: function() {
    var v = mal.loadValue('mal.entries.version', '').trim();
    return (v.length === 0 || v === mal.settings.cache);
  },
  
  isUpdating: function() {
    return (mal.entries.done + mal.entries.fail + mal.entries.skip) < mal.entries.total;
  },
  
  update: function(fullScan, onlyNew) {
    var userlist = [],
      right = {};
    
    if (!mal.entries.isUpdating()) {
      if (!onlyNew) {
        mal.entries.clear();
      }
      mal.settings.load();
      mal.content.status.text(' - Loading...');
      loadUserList();
    }
    
    function loadUserList() {
      $.get('/malappinfo.php?u=' + mal.name + '&status=all&type=' + mal.type, function(data) {
        $(mal.type, data).each(function() {
          var id = $('series_' + mal.type + 'db_id', this).text().trim(),
            title = $('series_title', this).text().toHtmlStr(),
            status = $('series_status', this).text().trim(),
            episodes = parseInt($('series_episodes', this).text().trim() || '0'),
            chapters = parseInt($('series_chapters', this).text().trim() || '0'),
            volumes = parseInt($('series_volumes', this).text().trim() || '0'),
            userStatus = $('my_status', this).text().trim(),
            userEpisodes = parseInt($('my_watched_episodes', this).text().trim() || '0'),
            userChapters = parseInt($('my_read_chapters', this).text().trim() || '0'),
            userVolumes = parseInt($('my_read_volumes', this).text().trim() || '0'),
            //my_rereadingg is not an error !!!
            rewatching = $(mal.type === 'anime' ? 'my_rewatching' : 'my_rereadingg', this).text().trim() || '0';
          
          userlist.push({
            id: parseInt(id),
            title: title,
            status: mal.settings.availableStatus[mal.type][
              userStatus - (userStatus == USER_STATUS.PLAN_TO ? 2 : 1)
            ],
            isCorrect: !(
              (userStatus != USER_STATUS.PLAN_TO && status == SERIES_STATUS.NOT_YET) ||
              (userStatus == USER_STATUS.COMPLETED && rewatching == '0' &&
                (status != SERIES_STATUS.COMPLETED || episodes != userEpisodes ||
                chapters != userChapters || volumes != userVolumes)) ||
              (userStatus != USER_STATUS.COMPLETED &&
                ((episodes > 0 && userEpisodes >= episodes) ||
                (volumes > 0 && userVolumes >= volumes) ||
                (chapters > 0 && userChapters >= chapters)))
            )
          });
        });
        
        loadContinue();
      });
    }
    
    function loadContinue() {
      if (mal.entries.isUpdating()) {
        return;
      }
      
      mal.entries.done = 0;
      mal.entries.fail = 0;
      mal.entries.skip = 0;
      mal.entries.total = userlist.length;
      mal.entries.wrong = {};
      right = {};
      
      mal.content.updateStatus(false, {});
      
      if (mal.entries.total === 0) {
        mal.content.updateList(true);
        return;
      }
      
      var index = 0,
        statusRe = new RegExp('^(' + mal.settings.excludedStatus[mal.type].join('|') + ')$', 'i');
        
      $.each(userlist, function(i, entry) {
        var id = entry.id;
        
        if (!entry.isCorrect) {
          mal.entries.wrong[id] = true;
        }
        
        mal.entries.title[id] = entry.title;
              
        if (mal.entries.left.hasOwnProperty(id) || entry.status.match(statusRe)) {
          mal.entries.skip++;
          mal.content.updateStatus(false, {});
          mal.content.updateList(true);
          return;
        }
        
        setTimeout(function() {
          $.ajax('/' + mal.type + '/' + id)
            .done(function(data) {
              mal.entries.left[id] = true;
              
              var rels = loadRelations(id, data);
              if (fullScan) {
                $.each(rels, function(i, id) {
                  right[id] = true;
                });
              }
              
              mal.entries.done++;
              mal.content.updateStatus(false, {});
              
              if (fullScan) {
                loadFull();
              } else {
                mal.content.updateList(true);
              }
            })
            .fail(function() {
              mal.entries.fail++;
              mal.content.updateStatus(false, {});
              console.log('Failed: /' + mal.type + '/' + id);
              
              if (fullScan) {
                loadFull();
              } else {
                mal.content.updateList(true);
              }
            });
        }, mal.settings.ajax.delay * (index++));
      });
    }
    
    function loadFull() {
      if (mal.entries.isUpdating()) {
        return;
      }
      
      $.each(right, function(id) {
        if (mal.entries.left.hasOwnProperty(id)) {
          delete right[id];
        }
      });
      
      var index = 0,
        opt = {
          done: mal.entries.done,
          fail: mal.entries.fail,
          skip: mal.entries.skip,
          total: mal.entries.total
        };
      
      mal.entries.done = 0;
      mal.entries.fail = 0;
      mal.entries.skip = 0;
      mal.entries.total = Object.keys(right).length;
      
      mal.content.updateStatus(true, opt);
      
      if (mal.entries.total === 0) {
        mal.content.updateList(true);
        return;
      }

      $.each(right, function(id) {
        setTimeout(function() {
          $.ajax('/' + mal.type + '/' + id)
            .done(function(data) {
              loadRelations(id, data);
              mal.entries.done++;
              mal.content.updateStatus(true, opt);
              mal.content.updateList(true);
            })
            .fail(function() {
              mal.entries.fail++;
              mal.content.updateStatus(true, opt);
              console.log('Failed: /' + mal.type + '/' + id);
              mal.content.updateList(true);
            });
        }, mal.settings.ajax.delay * (index++));
      });
    }
    
    function loadRelations(lId, data) {
      var result = [];

      $('h2 + table[class*="_detail_related_"] tr', data).each(function() {
        if ($('td:first-child', this).text().match(new RegExp('(' + mal.settings.excludedRelations.join('|') + ')', 'i'))) {
          return;
        }
        
        $('td:last-child a[href]', this).each(function() {
          var idData = $(this).prop('href').match(/\d+/);
          if (!idData) {
            console.log('Empty Relation: ' + lId);
            return;
          }
          var rId = parseInt(idData[0]);
          mal.entries.title[rId] = $(this).text().toHtmlStr();
          mal.entries.addRelation(lId, rId);
          result.push(rId);
        });
      });
      
      return result;
    }
  },
  
  addRelation: function(lId, rId) {
    if (!mal.entries.graph.hasOwnProperty(lId)) {
      mal.entries.graph[lId] = [];
    }
    if (!mal.entries.graph.hasOwnProperty(rId)) {
      mal.entries.graph[rId] = [];
    }
    if (mal.entries.graph[lId].indexOf(rId) < 0) {
      mal.entries.graph[lId].push(rId);
      mal.entries.graph[rId].push(lId);
    }
  },
  
  getTitle: function(id) {
    var result = mal.entries.title.hasOwnProperty(id) ? mal.entries.title[id] : '';
    return result.length === 0 ? '?' : result;
  },
  
  getComps: function() {
    var result = {}, used = {}, comp = [];
    
    function dfs(v) {
      used[v] = true;
      comp.push(v);
      
      if (!mal.entries.graph.hasOwnProperty(v)) {
        return;
      }
      
      $.each(mal.entries.graph[v], function(i, to) {
        if (!used.hasOwnProperty(to)) {
          dfs(to);
        }
      });
    }

    $.each(mal.entries.graph, function(v) {
      if (!used.hasOwnProperty(v)) {
        comp = [];
        dfs(v);
        if (comp.length > 1) {
          result[v] = comp;
        }
      }
    });
    
    return result;
  },
  
  comparator: function(a, b) {
    var aTitle = mal.entries.getTitle(a).toLowerCase(),
      bTitle = mal.entries.getTitle(b).toLowerCase();
    return aTitle.localeCompare(bTitle);
  }
};

mal.content = {
  body: $('<div id="mr_body" class="mr_body"></div>'),
  settings: $('<div id="mr_settings" class="mr_body"><div class="mr_list"></div></div>'),
  status: $('<span id="mr_status_msg"></span>'),
  list: $('<div class="mr_list"></div>'),
  
  updateStatus: function(extra, opt) {
    if (!opt.hasOwnProperty('done')) {
      opt.done = '';
    }
    if (!opt.hasOwnProperty('fail')) {
      opt.fail = '';
    }
    if (!opt.hasOwnProperty('skip')) {
      opt.skip = '';
    }
    if (!opt.hasOwnProperty('total')) {
      opt.total = '';
    }
    mal.content.status.html(' -' +
      ' Done: <b><span style="color: green;">' + (extra ? (opt.done + '+') : '') + mal.entries.done + '</span></b>' +
      ' Failed: <b><span style="color: #c32;">' + (extra ? (opt.fail + '+') : '') + mal.entries.fail + '</span></b>' +
      ' Skipped: <b><span style="color: gray;">' + (extra ? (opt.skip + '+') : '') + mal.entries.skip + '</span></b>' +
      ' Total: <b>' + (extra ? (opt.total + '+') : '') + mal.entries.total + '</b>'
    );
  },
  
  updateList: function(save) {
    if (mal.entries.isUpdating()) {
      return;
    }

    if (save) {
      mal.entries.save();
    }
    
    var title = $('#mr_body_title', mal.content.body),
      listType = $('<input type="hidden" id="mr_list_type" value="' + mal.type + '" />');
    
    title.html(title.html()
      .replace(/^[\s\S]*?\ </, 'Missing Relations – ' + (mal.type === 'anime' ? 'Anime' : 'Manga') + ' <'));
    
    var comps = mal.entries.getComps(),
      wrong = Object.keys(mal.entries.wrong);
    
    if (Object.keys(comps).length === 0 && wrong.length === 0) {
      mal.content.list.empty().append('<p id="mr_notfound">No missing relations found.</p>').append(listType);
      return;
    }
      
    var table = $('<table class="relTable" border="0" cellpadding="0" cellspacing="0" width="100%"><thead><tr><th>You\'ve ' + (mal.type === 'anime' ? 'watched' : 'read') + ' this…</th><th>…so you might want to check this:</th></tr></table>'),
        undel = $('<div id="mr_undelete"><p id="mr_undelete_msg" style="display: none;">There are <span id="mr_undelete_num" style="font-weight: bold;"></span> hidden relations. <a href="javascript:void(0);" title="Show ignored relations" onclick="window.showIgnoredRelations();">Show them</a></p></div>'),
      tfoot = $('<tfoot><tr><td class="mr_td_left"><div class="mr_count"><span></span></div></td><td class="mr_td_right"><div class="mr_count"><span></span></div></td></tr></tfoot>');
    
    // red relations
    if (wrong.length > 0) {      
      var ul = $('<ul></ul>'), size = 0;
      
      $.each(wrong.sort(mal.entries.comparator), function(i, id) {
        mal.content.getLiLeft(id).prop('id', 'mr_li_red_' + id).appendTo(ul);
        ++size;
      });
        
      var tbody_red = $('<tbody></tbody>'),
        tr_red = $('<tr class="mr_tr_data"></tr>')
          .append($('<td class="mr_td_left"></td>').append(ul))
          .append($('<td class="mr_td_right"></td>').append(mal.content.getEntryWarning()))
          .appendTo(tbody_red);
        
      if (size > 5) {
        tr_red.addClass('mr_tr_collapsed');
        $('<tr class="mr_tr_more"></tr>').append(mal.content.getMoreLink()).insertAfter(tr_red);
      }
      
      tbody_red.appendTo(table);
    }
    
    // normal relations
    $.each(Object.keys(comps).sort(mal.entries.comparator), function(i, key) {      
      var ulLeft = $('<ul></ul>'), ulRight = $('<ul></ul>'),
        lSize = 0, rSize = 0;
      
      $.each(comps[key].sort(mal.entries.comparator), function(i, id) {
        if (mal.entries.left.hasOwnProperty(id)) {
          mal.content.getLiLeft(id).prop('id', 'mr_li_' + id).appendTo(ulLeft);
          ++lSize;
        } else {
          mal.content.getLiRight(id).prop('id', 'mr_li_' + id).appendTo(ulRight);
          ++rSize;
        }
      });
      
      if (lSize > 0 && rSize > 0) {
        var tbody = $('<tbody></tbody>'),
          tr = $('<tr class="mr_tr_data"></tr>')
            .append($('<td class="mr_td_left"></td>').append(ulLeft))
            .append($('<td class="mr_td_right"></td>').append(ulRight))
            .appendTo(tbody);
          
        if (lSize > 5 || rSize > 5) {
          tr.addClass('mr_tr_collapsed');
          $('<tr class="mr_tr_more"></tr>').append(mal.content.getMoreLink()).insertAfter(tr);
        }
        
        tbody.appendTo(table);
      }
    });
    
    mal.content.list.empty()
      .append(undel)
      .append(table.append(tfoot))
      .append(listType);
    
    mal.hideIgnoredRelations(true);
    mal.content.updateLineCount();
  },
  
  updateSettings: function() {
    var title = $('#mr_body_title', mal.content.settings);    
    title.html(title.html().replace(/^[\s\S]*?\ </, 'Missing Relations – Settings <'));
    
    var list = $('.mr_list', mal.content.settings),
      tableExclude = $('<table class="relTable" border="0" cellpadding="0" cellspacing="0" width="100%"><thead><tr><th>Exclude Anime Relations:</th><th>Exclude Manga Relations:</th></tr><tbody></tbody></table>'),
      tableIgnore = $('<table class="relTable" border="0" cellpadding="0" cellspacing="0" width="100%"><thead><tr><th>Ignore Anime Entries:</th><th>Ignore Manga Entries:</th></tr><tbody></tbody></table>'),
      tableOther = $('<table class="relTable" border="0" cellpadding="0" cellspacing="0" width="100%"><thead><tr><th>Other Anime Settings:</th><th>Other Manga Settings:</th></tr><tbody></tbody></table>');
      
    var buttons = $('<div class="mr_buttons"></div>')
      .append(mal.content.getBtnSave())
      .append(mal.content.getBtnCancel())
      .append(mal.content.getBtnReset());
    
    // Add exclude relations
    var tbody = $('tbody', tableExclude);
    $.each(mal.settings.availableRelations, function(i, rel) {
      $('<tr class="mr_tr_data"></tr>')
        .append($('<td class="mr_td_left"></td>').append(mal.content.getCbSetting(rel, 'mr_xa_' + rel, false)))
        .append($('<td class="mr_td_right"></td>').append(mal.content.getCbSetting(rel, 'mr_xm_' + rel, false)))
        .appendTo(tbody);
    });
    
    // Add ignore status
    tbody = $('tbody', tableIgnore);
    for (var i = 0; i < mal.settings.availableStatus['anime'].length; ++i) {
      var statusA = mal.settings.availableStatus['anime'][i],
        statusM = mal.settings.availableStatus['manga'][i];
      $('<tr class="mr_tr_data"></tr>')
        .append($('<td class="mr_td_left"></td>').append(mal.content.getCbSetting(statusA, 'mr_xsa_' + statusA, false)))
        .append($('<td class="mr_td_right"></td>').append(mal.content.getCbSetting(statusM, 'mr_xsm_' + statusM, false)))
        .appendTo(tbody);
    }
    
    // Add other settings
    tbody = $('tbody', tableOther);
    $('<tr class="mr_tr_data"></tr>')
      .append($('<td class="mr_td_left"></td>').append(mal.content.getCbSetting('Deep Scan', 'mr_xoa_fullscan', false)))
      .append($('<td class="mr_td_right"></td>').append(mal.content.getCbSetting('Deep Scan', 'mr_xom_fullscan', false)))
      .appendTo(tbody);
      
    $('<tr class="mr_tr_data"></tr>')
      .append($('<td class="mr_td_center" colspan="2"></td>').append('<div class="mr_comment">Deep Scan allows to scan not only the entries from your list, but also their direct relations. This will result in finding more missing relations, but it will also require more time in comparison with the usual scan.<div>'))
      .appendTo(tbody);
      
    list.empty()
      .append(tableExclude)
      .append(tableIgnore)
      .append(tableOther);
    
    if (!mal.entries.isUpdating()) {
      list.append(buttons);
    } else {
      list.append(buttons.empty()
        .append('<div id="mr_warning">The settings can\'t be changed during relations calculation!</div>')
        .append(mal.content.getBtnCancel().val('OK'))
      );
    }

    mal.content.settings.show();
  },
  
  getLiLeft: function(id) {
    return $('<li></li>')
      .append(mal.content.getEntryLink(id, mal.entries.getTitle(id)));
  },
    
  getLiRight: function(id) {
    return $('<li></li>')
      .append(mal.content.getHideButton('window.ignoreRelation(' + id + ');', 'Hide this relation'))
      .append(mal.content.getEntryLink(id, mal.entries.getTitle(id)));
  },
  
  getCbSetting: function(str, id, state) {
    id = id.toHtmlId();
    state = state.toString();
    return $('<div class="mr_checkbox"></div>')
      .append($('<input name="' + id + '" id="' + id + '" type="checkbox" />').prop('checked', mal.loadSetting(id, state) !== 'false'))
      .append('<label for="' + id + '">' + str + '</label>');
  },
  
  getBtnSave: function() {
    return $('<input class="inputButton" value="Save" type="button" />').click(function() {
      $('input[type="checkbox"]', mal.content.settings).each(function() {
        mal.saveSetting(this.id, $(this).prop('checked').toString());
      });
      mal.settings.load();
      mal.fancybox.start(function() {
        mal.content.body.show();
        return true;
      });
    });
  },
  
  getBtnCancel: function() {
    return $('<input class="inputButton" value="Cancel" type="button" />').click(function() {
      mal.fancybox.start(function() {
        mal.content.body.show();
        return true;
      });
    });
  },
  
  getBtnReset: function() {
    return $('<input class="inputButton" value="Reset" type="button" />').click(function() {
        if (confirm('Reset all settings?')) {
          mal.settings.reset();
          mal.settings.load();
          mal.fancybox.start(function() {
            mal.content.body.show();
            return true;
          });
        }
    });
  },

  getEntryLink: function(id, title) {
    return $('<a title="' + title + '"href="' + '/' + mal.type + '/' + id + '" target="_blank">' + title + '</a>');
  },

  getEntryWarning: function() {
    return $('<div class="mr_warning"><span>Wrong status or ' + (mal.type === 'anime' ? 'episode' : 'vol./chap.') + ' count</span></div>');
  },

  getHideButton: function(func, title) {
    return $('<div class="mr_hide"><span><a href="javascript:void(0);" title="' + title + '" onclick="' + func + '">x</a></span></div>');
  },
  
  getMoreLink: function() {
    var result = $('<td colspan="2"></td>');
    $('<a class="mr_more" href="javascript:void(0);">show more</a>').click(function() {
      var tr = $(this).closest('tr');
      tr.prev('.mr_tr_data').removeClass('mr_tr_collapsed');
      tr.remove();
    }).appendTo(result);
    return result;
  },

  updateLineCount: function() {      
    $('tfoot td.mr_td_left .mr_count span', mal.content.list).text('Total: ' 
      + $('.relTable td.mr_td_left li', mal.content.list).length + ', Visible: ' 
      + $('.relTable tbody:not([style*="display: none"]) td.mr_td_left li', mal.content.list).length);
      
    $('tfoot td.mr_td_right .mr_count span', mal.content.list).text('Total: ' 
      + $('.relTable td.mr_td_right li', mal.content.list).length + ', Visible: ' 
      + $('.relTable td.mr_td_right li:not([style*="display: none"])', mal.content.list).length);
  }
};

mal.ignoreRelation = function(id, save) {
  var li = $('td.mr_td_right li[id="mr_li_' + id + '"]', mal.content.list);
  if (li.length === 0) {
    if (mal.entries.ignore.hasOwnProperty(id)) {
      delete mal.entries.ignore[id];
    }
    if (save) {
      mal.entries.saveIgnore();
    }
    return;
  }
  
  var row = li.hide().closest('tbody'),
    lSize = $('td.mr_td_left li', row).length,
    rSize = $('td.mr_td_right li:not([style*="display: none;"])', row).length;
    
  row.toggle(rSize > 0);  
  if (lSize <= 5 && rSize <= 5) {
    $('a.mr_more', row).trigger('click');
  }
  
  mal.entries.ignore[id] = true;  
  if (save) {
    mal.entries.saveIgnore();
    var count = Object.keys(mal.entries.ignore).length;
    $('#mr_undelete_num', mal.content.list).text(count);
    $('#mr_undelete_msg', mal.content.list).toggle(count > 0);
  }    
};
  
mal.hideIgnoredRelations = function(save) {
  mal.showIgnoredRelations(false);
  $.each(mal.entries.ignore, function(id) {
    mal.ignoreRelation(id, false);
  });
  
  var count = Object.keys(mal.entries.ignore).length;
  $('#mr_undelete_num', mal.content.list).text(count);
  $('#mr_undelete_msg', mal.content.list).toggle(count > 0);
  if (save) {
    mal.entries.saveIgnore();
  }
};
  
mal.showIgnoredRelations = function(save) {
  $('#mr_undelete_msg', mal.content.list).hide();
  $('li[id^="mr_li_"]', mal.content.list).show();
  $('tbody', mal.content.list).show();
  if (save) {
    mal.entries.clearIgnore();
    mal.entries.saveIgnore();
  }
};

mal.loadValue = function(key, value) {
  try {
    value = JSON.parse(localStorage.getItem(mal.name + '#' + mal.type + '#' + key)) || value;
  } catch (e) {}
  return value;
};

mal.saveValue = function(key, value) {
  localStorage.setItem(mal.name + '#' + mal.type + '#' + key, JSON.stringify(value));
};

mal.loadSetting = function(key, value) {
  try {
    value = JSON.parse(localStorage.getItem('global#' + key)) || value;
  } catch (e) {}
  return value;
};

mal.saveSetting = function(key, value) {
  localStorage.setItem('global#' + key, JSON.stringify(value));
};

window.ignoreRelation = function(id) {
  mal.ignoreRelation(id, true);
  mal.content.updateLineCount();
};

window.showIgnoredRelations = function() {
  mal.showIgnoredRelations(true);
  mal.content.updateLineCount();
};

String.prototype.toHtmlId = function() {
  return this.trim().toLowerCase().replace(/\s/g, '_').replace(/[^\w]/g, '_');
};

String.prototype.toHtmlStr = function() {
  return this.trim().replace(/"/g, '&quot;');
};

$('<style type="text/css" />').html(
  'div#mr_fancybox_wrapper { position: fixed; width: 100%; height: 100%; top: 0; left: 0; background: rgba(102, 102, 102, 0.3); z-index: 99990; }' +
  'div#mr_fancybox_inner { width: ' + mal.settings.width + 'px !important; height: ' + mal.settings.height + 'px !important; overflow: hidden; }' +
  'div#mr_fancybox_outer { position: absolute; display: block; width: auto; height: auto; padding: 10px; border-radius: 8px; top: 80px; left: 50%; margin-top: 0 !important; margin-left: ' + (-mal.settings.width/2) + 'px !important; background: #fff; box-shadow: 0 0 15px rgba(32, 32, 32, 0.4); z-index: 99991; }' +
  'div.mr_body { text-align: center; width: 100%; height: 100%; padding: 42px 0 0; box-sizing: border-box; }' +
  'div#mr_body { padding-top: 65px; }' +
  'div.mr_body a, div.mr_body a:visited { color: #1969cb; text-decoration: none; }' +
  'div.mr_body a:hover { color: #2d7de0; text-decoration: underline; }' +
  'div.mr_body #mr_body_title { position: absolute; top: 8px; left: 8px; width: ' + mal.settings.width + 'px; font-size: 16px; font-weight: normal; text-align: center; margin: 0; border: 0; }' +
  'div.mr_body #mr_body_title span { font-size: 12px; font-weight: normal; }' +
  'div.mr_body #mr_body_title:after { content: ""; position: absolute; left: 0; bottom: -14px; width: 100%; height: 8px; border-top: 1px solid #eee; background: center bottom no-repeat radial-gradient(#f6f6f6, #fff 70%); background-size: 100% 16px; }' +
  'div.mr_body #mr_panel { position: absolute; top: 50px; left: 8px; text-align: left; width: ' + mal.settings.width + 'px; height: 2em; margin: 0 0 1em; }' +
  'div.mr_body #mr_links { float: right; }' +
  'div.mr_body p#mr_notfound { margin: 10px 0; }' +
  'div.mr_body #mr_undelete { background-color: #fff; padding: 0; margin: 0; }' +
  'div.mr_body #mr_undelete_msg { margin: 10px 0; font-weight: normal; text-align: center; line-height: 20px; font-size: 13px; }' +
  'div.mr_body .mr_list { width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; margin: 0 auto; border: 1px solid #eee; box-sizing: border-box; }' +
  'div.mr_body .relTable { border: none; }' +
  'div.mr_body .relTable thead { background-color: #f5f5f5; }' +
  'div.mr_body .relTable th { background-color: transparent; width: 50%; padding: 5px 0 5px 6px; font-size: 12px; font-weight: bold; text-align: left; line-height: 20px !important; box-shadow: none; }' +
  'div.mr_body .relTable tbody { background-color: #fff; }' +
  'div#mr_body .relTable tbody:hover { background-color: #f5f5f5; }' +
  'div#mr_body .relTable tbody tr:first-of-type td { box-shadow: 0px 1em 1em -1em #ddd inset; }' +
  'div.mr_body .relTable td { background-color: transparent; width: 50%; padding: 5px 0 5px 6px; font-size: 13px; font-weight: normal; text-align: left; line-height: 20px !important; vertical-align: top; }' +
  'div.mr_body .relTable td.mr_td_center { padding: 5px 6px; }' +
  'div.mr_body .relTable td div span { line-height: 20px !important; }' +
  'div.mr_body .relTable td ul { list-style-type: none; margin: 0; padding: 0; }' +
  'div.mr_body .relTable tr.mr_tr_collapsed td ul { height: 100px; overflow-y: hidden; }' +
  'div.mr_body .relTable td ul li { width: 100%; padding: 0; margin: 0; }' +
  'div.mr_body .relTable td ul li > a { display: block; width: 295px !important; line-height: 20px !important; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }' +
  'div.mr_body .relTable td.mr_td_left ul li > a { width: 310px !important; }' +
  'div.mr_body .relTable tfoot td { border-top: 1px solid #f5f5f5; }' +
  'div.mr_body .relTable td .mr_count { color: #666; font-size: 11px; font-weight: normal; text-align: left; }' +
  'div.mr_body .relTable td .mr_warning { width: 260px; color: #e43; font-size: 12px; font-weight: bold; text-align: left; }' +
  'div.mr_body .relTable td .mr_hide { display: inline-block !important; width: 15px; float: right; text-align: left; font-size: 11px; }' +
  'div.mr_body .relTable td .mr_hide a { color: #888 !important; line-height: 20px !important; font-style: normal !important; text-decoration: none !important; }' +
  'div.mr_body .relTable tr.mr_tr_more td { padding: 0 0 2px 0; }' +
  'div.mr_body .relTable td .mr_more { display: block !important; text-align: center; color: #c0c0c0 !important; font-style: normal !important; font-size: 0.9em; text-decoration: none !important; }' +
  'div.mr_body .relTable .mr_checkbox > * { vertical-align: middle; }' +
  'div.mr_body .relTable .mr_comment { background-color: #f6f6f6; border: 1px solid #ebebeb; font-size: 11px; line-height: 16px; padding: 1px 4px; }' +
  'div.mr_body .mr_buttons { position: absolute; bottom: 13px; width: 100%; text-align: center; padding: 5px 10px; box-sizing: border-box; }' +
  'div.mr_body .mr_buttons > .inputButton { margin: 2px 5px !important; font-size: 12px; }' +
  'div#mr_settings .relTable { margin-bottom: 10px; }' +
  'div#mr_settings div#mr_warning { font-weight: bold; text-decoration: underline; margin-bottom: 2px; }'
).appendTo('head');

main(); 

})(jQuery);