node / whatcd gazelle toggle bookmarks

// ==UserScript==
// @name        whatcd gazelle toggle bookmarks
// @include     /https?://www\.empornium\.(me|sx)/torrents\.php.*/
// @include     /https?://www\.empornium\.(me|sx)/user\.php.*/
// @include     /https?://www\.empornium\.(me|sx)/top10\.php.*/
// @include     /https?://femdomcult\.org/torrents\.php.*/
// @include     /https?://femdomcult\.org/user\.php.*/
// @include     /https?://femdomcult\.org/top10\.php.*/
// @version     3
// @require     http://code.jquery.com/jquery-2.1.1.js
// @grant       GM_addStyle
// @grant       GM_unsafeWindow
// ==/UserScript==

"use strict";

// Changelog:
// * version 3
// - fixed missing non-breaking space on empty title
// * version 2
// - improved hover over download icon
// * version 1
// - initial version

GM_addStyle('' +
'.toggle-bookmarks {' +
'    position: absolute;' +
'    display: none;' +
'    padding-right: 5px;' +
'    z-index: 2;' +
'}' +
'.toggle-bookmarks .wrapper {' +
'    background: white;' +
'    border-radius: 3px;' +
'    border: 1px solid silver;' +
'}' +
'.toggle-bookmarks.bookmarked .add,' +
'.toggle-bookmarks:not(.bookmarked) .remove {' +
'    display: none;' +
'}' +
'.toggle-bookmarks svg {' +
'    width: 15px;' +
'    height: 15px;' +
'}' +
'');

var star_add = [
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 179.99999" height=',
    '"180" width="180"><path d="M37.048 171.56l52.9-38.433 52.9 38.434-20.204-62',
    '.187 52.9-38.435h-65.388L89.95 8.748 69.74 70.938H4.352l52.9 38.435z" fill-',
    'rule="evenodd" stroke="#000"/><g transform="matrix(1.278 0 0 1.278 -195.438',
    '-298.745)"><circle r="30" cy="337.362" cx="260" fill="#fff" stroke="#000" s',
    'troke-width="3.13"/><rect ry="5" y="317.283" x="255" height="40.159" width=',
    '"10" rx="5"/><rect ry="5" y="332.362" x="239.92" height="10" width="40.159"',
    'rx="5"/></g></svg>'
].join('');


var star_remove = [
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 179.99999" height=',
    '"180" width="180"><path d="M37.048 171.56l52.9-38.433 52.9 38.434-20.204-62',
    '.187 52.9-38.435h-65.388L89.95 8.748 69.74 70.938H4.352l52.9 38.435z" fill-',
    'rule="evenodd" stroke="#000"/><g transform="matrix(1.278 0 0 1.278 -195.438',
    '-298.745)"><circle r="30" cy="337.362" cx="260" fill="#fff" stroke="#000" s',
    'troke-width="3.13"/><rect ry="5" y="332.362" x="239.92" height="10" width="',
    '40.159" rx="5"/></g></svg>'
].join('');

this.jQuery = jQuery.noConflict(true);

function Ajax() {
    this.add    = jQuery.proxy(this.add,    this);
    this.remove = jQuery.proxy(this.remove, this);
}

Ajax.prototype.get = function (action, torrent_id) {
    return jQuery.get('bookmarks.php', {
        action: action,
        type: 'torrent',
        auth: unsafeWindow.authkey,
        id: torrent_id
    });
};

Ajax.prototype.add = function (torrent_id) {
    return this.get('add', torrent_id);
};

Ajax.prototype.remove = function (torrent_id) {
    return this.get('remove', torrent_id);
};


function ToggleBookmark() {
    this.el = this.create_el();
    this.id = null;
    this.ids = {};
    this.ajax = new Ajax();

    this.symbol_bookmark = '★';

    this.on_mouseenter_title   = jQuery.proxy(this.on_mouseenter_title, this);
    this.on_mouseenter_version = jQuery.proxy(this.on_mouseenter_version, this);
    this.on_mouseleave         = jQuery.proxy(this.on_mouseleave, this);

    this.add    = jQuery.proxy(this.add, this);
    this.remove = jQuery.proxy(this.remove, this);

    this.el.appendTo('body');
    this.attach_events();
}

ToggleBookmark.prototype.remove_char = function (value, index) {
    return value.slice(0, index) + value.slice(index + 1);
};

ToggleBookmark.prototype.remove_bookmark_symbol = function (title) {
    var bookmark_index = title.lastIndexOf(this.symbol_bookmark);
    if (bookmark_index === -1)
        return title;
    return this.remove_char(title, bookmark_index);
};

ToggleBookmark.prototype.create_el = function () {
    return jQuery([
        '<div class="toggle-bookmarks">',
        '    <div class="wrapper">',
        '        <a href="#" class="add">', star_add, '</a>',
        '        <a href="#" class="remove">', star_remove, '</a>',
        '    </div>',
        '</div>',
    ].join(''));
};

ToggleBookmark.prototype.attach_events = function () {
    var title_selector = [
        'td.cats_col  + td > a[href^="torrents.php?id"]',
        'td.cats_cols + td > a[href^="torrents.php?id"]'
    ].join(',');

    var version_selector = [
        'td.cats_col  + td .version',
        'td.cats_cols + td .version'
    ].join(',');

    var torrent_table = jQuery('.torrent_table');
    torrent_table.on('mouseenter', title_selector, this.on_mouseenter_title);
    torrent_table.on('mouseenter', version_selector, this.on_mouseenter_version);
    torrent_table.on('mouseleave', 'td', this.on_mouseleave);

    this.el.on('click', 'a.add', this.add);
    this.el.on('click', 'a.remove', this.remove);
};

ToggleBookmark.prototype.add = function (event) {
    event.preventDefault();
    var callback = jQuery.proxy(this.added, {
        self: this,
        title: this.title,
        id: this.id
    });
    this.ajax.add(this.id).done(callback);
};

ToggleBookmark.prototype.remove = function (event) {
    event.preventDefault();
    var callback = jQuery.proxy(this.removed, {
        self: this,
        title: this.title,
        id: this.id
    });
    this.ajax.remove(this.id).done(callback);
};

ToggleBookmark.prototype.on_toggle = function (id, value) {
    this.ids[id] = value;
    this.el.toggleClass('bookmarked', value);
};

ToggleBookmark.prototype.added = function () {
    var title = this.self.remove_bookmark_symbol(this.title.textContent).trim();
    if (title)
        this.title.textContent = title + '\u00a0' + this.self.symbol_bookmark;
    else
        this.title.textContent = this.self.symbol_bookmark;

    this.self.on_toggle(this.id, true);
};

ToggleBookmark.prototype.removed = function () {
    var title = this.self.remove_bookmark_symbol(this.title.textContent).trim();
    if (title)
        this.title.textContent = title;
    else
        this.title.textContent = '\u00a0';

    this.self.on_toggle(this.id, false);
};

ToggleBookmark.prototype.is_version_bookmarked = function (title) {
    return title.text().indexOf('★') !== -1;
};

ToggleBookmark.prototype.is_title_bookmarked = function (parent) {
    return parent.find('> span > img[alt="bookmarked"]').length > 0;
};

ToggleBookmark.prototype.get_id = function (title) {
    return title.attr('href').match(/id=(\d+)/)[1];
};

ToggleBookmark.prototype.rect = function (el) {
    var offset = el.offset();
    var width  = el.width();
    var height = el.height();
    var center = {
        top: offset.top + height / 2,
        left: offset.left + width / 2
    };
    return {
          left: offset.left,
         right: offset.left + width,
           top: offset.top,
        bottom: offset.top  + height,

        center: center,

         width: width,
        height: height
    };
};

ToggleBookmark.prototype.on_mouseenter = function (title, parent, is_bookmarked) {
    var id = this.get_id(title);
    var bookmarked = this.ids[id];

    if (bookmarked === undefined) {
        bookmarked = is_bookmarked.call(this);
    }

    this.id = id;
    this.title = title.contents()
        .filter(function () {return this.nodeType === 3})
        .filter(function () {return this.textContent.length > 0})
        .first()[0];

    this.el.toggleClass('bookmarked', bookmarked);
    this.el.prependTo(title);
    this.el.show();
}

ToggleBookmark.prototype.on_mouseenter_title = function (event) {
    var title = jQuery(event.currentTarget);
    var parent = title.parent();
    var is_bookmarked = function() {return this.is_title_bookmarked(parent)};

    this.on_mouseenter(title, parent, is_bookmarked);

    var rect_title = this.rect(title);
    this.set_offset({
         top: rect_title.center.top,
        left: rect_title.left
    });
};

ToggleBookmark.prototype.on_mouseenter_version = function (event) {
    var title = jQuery(event.currentTarget).find('.collapsed-title');
    var parent = title.parent();
    var is_bookmarked = function () {return this.is_version_bookmarked(title);}

    this.on_mouseenter(title, parent, is_bookmarked);

    var rect_title = this.rect(title);
    var rect_parent = this.rect(parent);
    this.set_offset({
         top: rect_title.center.top,
        left: rect_parent.left
    });
};

ToggleBookmark.prototype.on_mouseleave = function (event) {
    this.el.hide();
};

ToggleBookmark.prototype.set_offset = function (offset) {
    this.el.offset({
         top: offset.top - this.el.innerHeight() / 2,
        left: offset.left - this.el.innerWidth()
    });
};

new ToggleBookmark();