Styx / Pichome Extender

// ==UserScript==
// @name         Pichome Extender
// @namespace    http://fanfics.me/user159153
// @version      0.0.3
// @description  Useful features for www.pichome.ru
// @author       Styx
// @copyright    2017, Styx (http://fanfics.me/user159153)
// @license      MIT
// @homepageURL  http://fanfics.me/index.php?section=blogs&search=%23phe
// @supportURL   http://fanfics.me/index.php?section=blogs&search=%23phe
// @updateURL    https://openuserjs.org/meta/Styx/Pichome_Extender.meta.js
// @include      http://www.pichome.ru/*
// @grant        none
// ==/UserScript==

/**
  Copyright 2017 Styx (http://fanfics.me/user159153)

  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
  to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
  and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/**
 * RELEASE NOTES:
 *
 * 0.0.3
 *   * Добавлена поддержка обоих дизайнов fanfics.me
 *
 * 0.0.2
 *   + Интеграция с Fanfics Extender
 *   + Слежение за действиями с картинками в других вкладках
 *
 * 0.0.1
 *   + Release
 */

(function (win, $) {
  'use strict';

  const VERSION = '0.0.3';

  const wlp = win.location.pathname;
  const ffme = wlp === '/faq' && window !== window.top;
  if (wlp !== '/' && !ffme) {
    return;
  }

  if (!ffme) {
    win.console.info(`Pichome.ru Extender [${VERSION}]`);
  }

  const CSS = `
    .clearfix:after {
      content: "";
      display: table;
      clear: both;
    }
    body.noscroll {
      overflow: hidden;
    }
    #phe-modal {
      position: fixed;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      z-index: 10000;
      background-color: rgba(0, 0, 0, 0.6);
    }
    #phe-modal img {
      display: block;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      max-height: 100%;
      cursor: zoom-out;
    }
    
    .phe-dropzone {
      margin: 20px;
      padding: 40px 0;
      height: 70px;
      line-height: 40px;
      font-size: 24px;
      color: #666;
      border: 2px dashed #666;
      border-radius: 10px;
      text-align: center;
      background: #f5f5f5;
    }
    .phe-dropzone a {
      font-size: 14px;
      cursor: pointer;
      color: #327FBE;
    }
    .phe-dropzone input {
      display:none;
    }
    .phe-dropzone.dragover {
      border-color: #388E3C;
      color: #388E3C;
      border-style: dotted;
      background: #e8f5e9;
    }

    #phe-switcher {
      padding: 5px 20px;
    }
    #phe-switcher span {
      width: 100px;
      font-size: 15px;
      text-align: center;
      display: inline-block;
      cursor: pointer;
      margin-right: 20px;
      background: #fff;
      line-height: 30px;
      padding: 0 10px;
      border-radius: 4px;
      border: 1px solid #ccc;
    }
    #phe-switcher span:hover {
      border-color: #999;
    }
    #phe-switcher span.active {
      background-color: #eee;
    }
    
    #phe-uploaded {
      margin: 0;
      padding: 10px 0x;
      display: flex;
      flex-wrap: wrap;
    }
    #phe-uploaded > div {
      width: 200px;
      height: 200px;
      vertical-align: middle;
      background-size: contain;
      background-position: center;
      display: inline-block;
      background-repeat: no-repeat;
      background-color: inherit;
      border-radius: 4px;
      box-shadow: 0 0 0 1px #eaeaea;
      display: flex;
      align-items: center;
      margin: 15px;
      position: relative;
      overflow: hidden;
      cursor: zoom-in;
    }
    #phe-uploaded > div.uploading {
      cursor: default;
    }
    #phe-uploaded.starred > div:not(.starred) {
      display: none;
    }
    #phe-uploaded > div:hover {
      box-shadow: 0 0 1px 1px #ccc;
    }
    #phe-uploaded > div.starred {
      box-shadow: 0 0 0 1px #ffc107;
    }
    #phe-uploaded > div.starred:hover {
      box-shadow: 0 0 1px 1px #ff9800;
    }
    #phe-uploaded > div progress, #wrap .uploading > progress {
      width: 100%;
      display: block;
    }
    #phe-uploaded > div span {
      font-size: 20px;
      position: absolute;
      cursor: pointer;
      display: block;
      line-height: 28px;
      width: 30px;
      height: 30px;
      text-align: center;
      background: #fff;
      border: 1px solid #ccc;
      box-sizing: border-box;
      color: #555459;
      transition: top ease-in-out 0.1s, bottom ease-in-out 0.1s, background-color ease-in-out 0.25s;
    }
    #phe-uploaded > div span.phe-top {
      top: -31px;
    }
    #phe-uploaded > div span.phe-bottom {
      bottom: -31px;
    }
    #phe-uploaded > div span.phe-left {
      left: -1px;
    }
    #phe-uploaded > div span.phe-right {
      right: -1px;
    }
    #phe-uploaded > div span.hl {
      background-color: #ffeb3b;
    }
    #phe-uploaded > div:hover span.phe-top {
      top: -1px;
    }
    #phe-uploaded > div:hover span.phe-bottom {
      bottom: -1px;
    }
    #phe-uploaded > div span i {
      line-height: inherit;
    }
    #phe-uploaded > div span a {
      color: inherit;
      text-decoration: none !important;
    }
    #phe-uploaded > div span.phe-star {
      color: #ffc107;
      border-bottom-right-radius: 4px;
    }
    #phe-uploaded > div span.phe-unstar {
      left: -31px;
      color: #ffc107;
      border-bottom-right-radius: 4px;
    }
    #phe-uploaded > div span.phe-copy {
      font-size: 15px;
      border-bottom-left-radius: 4px;
    }
    #phe-uploaded > div span.phe-view {
      border-top-right-radius: 4px;
    }
    #phe-uploaded > div span.phe-del {
      color: #e53935;
      border-top-left-radius: 4px;
    }
    #phe-uploaded > div span.phe-copy-text {
      top: -31px !important;
      font-size: 1px;
      overflow: hidden;
    }
    #phe-uploaded > div.starred span.phe-star {
      left: -31px;
    }
    #phe-uploaded > div.starred span.phe-unstar {
      left: -1px;
    }
    html.ffme, html.ffme body {
      background: transparent !important;
    }
    html.ffme .phe-dropzone {
      margin: 0;
    }
    html.ffme #phe-switcher {
      padding: 0 10px 5px;
    }
    html.ffme #phe-switcher span {
      margin-right: 10px;
      width: 86px;
      font-size: 12px;
      padding: 0 4px;
      line-height: 24px;
    }
    html.ffme #phe-uploaded {
      margin-bottom: 8px;
    }
    html.ffme #phe-uploaded > div {
      width: 130px;
      height: 130px;
      margin: 10px 0 0 10px;
      cursor: pointer;
    }
    html.ffme #phe-uploaded > div > span {
      display: none !important;
    }
    html.ffme #wrap #phe-switcher, html.ffme #wrap #phe-uploaded {
      display: none;
    }
    html.ffme #wrap.browse .phe-dropzone {
      display: none;
    }
    html.ffme #wrap.browse #phe-switcher {
      display: block;
    }
    html.ffme #wrap.browse #phe-uploaded {
      display: flex;
    }
  `;

  const CONF_IMAGES = 'images'
  ;

  const DEFAULTS = {};
  DEFAULTS[CONF_IMAGES] = [];

  let SETTINGS = {};

  const MAX_FILE_SIZE = 3 * 1024 * 1024; // 3MB

  const ROOT = 'http://www.pichome.ru';

  let ITEMS;

  try {
    addCSS();
    loadSettings();
    loadItems();
    addWindowMessageHandler();
    insertDropzone();
    insertItems();
    attachHandlers();
  } catch (err) {
    win.console.error('Pichome Extender:', err);
  }

  function addCSS() {
    const head = document.head || document.getElementsByTagName('head')[0];

    const flink = document.createElement('link');
    flink.rel = 'stylesheet';
    flink.setAttribute('crossorigin', 'anonymous');
    flink.integrity = 'sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN';
    flink.href = 'https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css';
    head.appendChild(flink);

    const style = document.createElement('style');
    style.type = 'text/css';
    if (style.styleSheet) {
      style.styleSheet.cssText = CSS;
    } else {
      style.appendChild(document.createTextNode(CSS));
    }
    head.appendChild(style);
  }

  function insertDropzone() {
    const $html = '<div class="phe-dropzone">Перетащите картинку сюда<br><a>Выбрать вручную</a><input type="file" accept="image/png, image/gif, image/jpeg" multiple></div>';
    const $body = $(ffme?'#wrap':'#content').html($html);
    const $dz = $body.find('.phe-dropzone');
    const dz = $dz.get(0);
    if (ffme) {
      $('html').addClass('ffme');
      $body.attr('style', null);
      $('#foot').remove();
    }
    dz.addEventListener('dragenter', function(e){
      e.preventDefault();
      e.stopPropagation();
      dz.classList.add('dragover');
    }, false);
    dz.addEventListener('dragover', function(e){
      e.preventDefault();
      e.stopPropagation();
      dz.classList.add('dragover');
    }, false);
    dz.addEventListener('dragleave', function(e){
      e.preventDefault();
      e.stopPropagation();
      dz.classList.remove('dragover');
    }, false);
    dz.addEventListener('drop', function(e){
      e.stopPropagation();
      e.preventDefault();
      processFiles(e.dataTransfer.files);
      dz.classList.remove('dragover');
      return false;
    }, false);
    $dz.find('a').click(function (e) {
      e.preventDefault();
      $dz.find('input').click();
    });
    $dz.find('input').on('change', function (e) {
      processFiles(e.target.files);
      dz.classList.remove('dragover');
      return false;
    });
  }

  function insertItems(rerender) {
    const $cont = $(ffme?'#wrap':'#content');
    let $div;
    if (rerender) {
      $div = $('#phe-uploaded');
      $div.empty();
    } else {
      $cont.append('<div id="phe-switcher"><span class="active">Все картинки</span><span data-starred="1">Избранное</span></div>');
      $div = $('<div id="phe-uploaded"></div>');
    }
    ITEMS.forEach(function (i) {
      renderItem(i, $div);
    });
    if (!rerender) {
      $cont.append($div);
    }
  }

  let ffmeOrigin = null;

  function _ffmePost(message) {
    if (!ffme) {
      return;
    }
    win.top.postMessage(JSON.stringify(message), ffmeOrigin || 'http://fanfics.me');
  }

  function addWindowMessageHandler() {
    win.addEventListener('storage', function (e) {
      if (e.key !== 'phe') {
        return;
      }
      _defer(function () {
        loadSettings();
        loadItems();
        insertItems(true);
      });
    }, false);
    if (!ffme) {
      return;
    }
    const testRE = /^http:\/\/(old\.)?fanfics\.me$/;
    win.addEventListener('message', function (event) {
      if (!testRE.test(event.origin)) {
        return;
      }
      if (ffmeOrigin == null) {
        ffmeOrigin = event.origin;
      }
      switch (event.data) {
        case 'ping':
          _ffmePost({ m: 'pong' });
          break;
        case 'upload':
          $('#wrap').removeClass('browse');
          break;
        case 'browse':
          $('#wrap').addClass('browse');
          break;
        default:
          break;
      }
    }, false);
  }

  function attachHandlers() {
    $('#phe-switcher span').live('click', function (e) {
      e.preventDefault();
      e.stopPropagation();
      const $e = $(e.target);
      const $p = $e.parent();
      $p.children('span').removeClass('active');
      $e.addClass('active');
      const star = $e.data('starred') != null;
      $('#phe-uploaded').toggleClass('starred', star);
    });
    if (ffme) {
      $('#phe-uploaded > div').live('click', function (e) {
        const $div = $(e.target).closest('div');
        const txt = $div.find('.phe-copy-text').text();
        _ffmePost({ m: 'one', u: txt });
      });
      return;
    }
    $('#phe-uploaded span.phe-star, #phe-uploaded span.phe-unstar').live('click', function (e) {
      e.preventDefault();
      e.stopPropagation();
      const $div = $(e.target).closest('div');
      $div.toggleClass('starred');
      const id = $div.data('id');
      const item = itemsGet(id);
      if (item != null) {
        item.s = 1 - (item.s|0);
        itemsSet(id, item);
      }
    });
    $('#phe-uploaded span.phe-del').live('click', function (e) {
      e.preventDefault();
      e.stopPropagation();
      if (!confirm("Вы действительно хотите удалить это изображение?\n\nЭто действие невозможно будет отменить!")) {
        return;
      }
      const $div = $(e.target).closest('div');
      const id = $div.data('id');
      const item = itemsGet(id);
      if (item != null) {
        $.getJSON(`${ROOT}/delete-confirm/image/${item.id}/${item.dh}`).success(function(data){
          if (data.status_code === 200) {
            itemsDel(item.id);
            $div.remove();
          }
        });
      }
    });
    $('#phe-uploaded span.phe-copy').live('click', function (e) {
      e.preventDefault();
      e.stopPropagation();
      const $div = $(e.target).closest('div');
      const txt = $div.find('.phe-copy-text').get(0);
      const sel = win.getSelection();
      const range = document.createRange();
      range.selectNodeContents(txt);
      sel.removeAllRanges();
      sel.addRange(range);
      document.execCommand('copy');
      sel.removeAllRanges();
      const icon = $div.find('.phe-copy').get(0);
      icon.classList.add('hl');
      setTimeout(function () {
        icon.classList.remove('hl');
      }, 300);
    });
    $('#phe-uploaded > div').live('click', function (e) {
      if (this.classList.contains('uploading') || e.target !== e.currentTarget) {
        return;
      }
      const $div = $(e.target).closest('div');
      const url = $div.find('.phe-copy-text').text();
      showModal(url);
    });
    $('#phe-modal').live('click', function () {
      hideModal();
    });
  }

  function renderItem(item, $div, uploaded) {
    if (item == null) {
      return;
    }
    let $cont = $div;
    if (!uploaded) {
      $cont = $('<div/>').appendTo($div);
    }
    const url = `${ROOT}${item.p}`;
    const html = [];
    html.push('<span title="Добавить в избранное" class="phe-star phe-top phe-left"><i class="fa fa-star-o"></i></span>');
    html.push('<span title="Убрать из избранного" class="phe-unstar phe-top phe-left"><i class="fa fa-star"></i></span>');
    html.push('<span title="Скопировать ссылку на изображение" class="phe-copy phe-top phe-right"><i class="fa fa-clipboard"></i></span>');
    html.push(`<span class="phe-copy-text">${url}</span>`);
    html.push(`<span title="Открыть предпросмотр" class="phe-view phe-bottom phe-left"><a href="${ROOT}/${item.id}" target="_blank"><i class="fa fa-eye"></i></a></span>`);
    html.push(`<span title="Удалить" class="phe-del phe-bottom phe-right"><i class="fa fa-trash-o"></i></span>`);
    $cont.data('id', item.id)
      .removeClass('uploading')
      .css({ 'background-image': `url(${url})` })
      .html(html.join(''));
    if (item.s) {
      $cont.addClass('starred');
    }
  }

  function showModal(url) {
    const $modal = $(`<div id="phe-modal"><img src="${url}"/></div>`);
    $('body').addClass('noscroll').append($modal);
  }

  function hideModal() {
    $('#phe-modal').remove();
    $('body').removeClass('noscroll');
  }

  const allowedTypes = ['image/png', 'image/gif', 'image/jpeg'];
  function processFiles(files) {
    const isMultiple = !ffme && files.length > 1;
    for(let idx = 0; idx < files.length; idx++) {
      const file = files[idx];
      if (allowedTypes.indexOf(file.type) === -1) {
        continue;
      }
      if (file.size > MAX_FILE_SIZE) {
        if (!isMultiple) {
          alert('Изображение превышает максимальный допустимый размер файла');
        }
        continue;
      }
      processFile(file);
      if (ffme) {
        break;
      }
    }
  }

  function processFile(file) {
    const fd = new FormData();
    fd.append('upload', file);

    let totalSize = file.size + 200;

    const $uploaded = $(`<div class="uploading"><progress max="${totalSize}" value="0"></progress></div>`);
    if (!ffme) {
      $uploaded.prependTo('#phe-uploaded');
    } else {
      $uploaded.appendTo('#wrap');
    }
    const progress = $uploaded.find('progress').get(0);

    const xhr = new XMLHttpRequest();
    xhr.open('POST', `${ROOT}/api.php`, true);
    xhr.addEventListener('load', function () {
      processUploaded($uploaded, xhr.responseText);
    });
    xhr.addEventListener('error', function (e) {
      $uploaded.remove();
      win.console.error(e);
      alert('Возникла ошибка при загрузке изображения');
    });
    xhr.addEventListener('abort', function () {
      $uploaded.remove();
    });
    const uploadProgress = function (e) {
      if (totalSize != null && e.lengthComputable) {
        progress.max = e.total;
        totalSize = null;
      }
      progress.value = e.loaded;
    };
    if (xhr.upload) {
      xhr.upload.addEventListener('progress', uploadProgress);
    } else {
      xhr.addEventListener('progress', uploadProgress);
    }
    xhr.send(fd);
  }

  function processUploaded($div, response) {
    let json;
    try {
      json = JSON.parse(response);
    } catch(err) {
      console.error('Something went wrong', err);
      alert('Возникла ошибка при загрузке изображения');
      $div.remove();
      return;
    }
    if (json.status_code !== 200 || json.status_txt !== 'OK') {
      alert('Возникла ошибка при загрузке изображения');
      $div.remove();
      return;
    }
    const data = {
      id: json.data.image_id_public,
      dh: json.data.image_delete_hash,
      ts: json.data.image_date,
      p: json.data.image_path,
      b: json.data.image_bytes,
    };

    itemsAdd(data);
    if (!ffme) {
      renderItem(data, $div, true);
    } else {
      _ffmePost({ m:'one', u: `${ROOT}${data.p}` });
    }
  }

  function _defer(func) {
    setTimeout(func, 1);
  }

  function _debounce(func, wait) {
    let timeout, args, context, lastCallTime;

    const later = function () {
      const last = new Date().getTime() - lastCallTime;

      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        timeout = null;
        func.apply(context, args);
        context = args = null;
      }
    };

    return function () {
      context = this;
      args = arguments;
      lastCallTime = new Date().getTime();
      if (!timeout) {
        timeout = setTimeout(later, wait);
      }
    };
  }

  function itemsGet(id) {
    return ITEMS.find(function (i) { return i != null && i.id === id; });
  }

  function itemsSet(id, item) {
    const idx = ITEMS.findIndex(function (i) { return i != null && i.id === id; });
    if (idx < 0) {
      return;
    }
    ITEMS[idx] = item;
    saveItems();
  }

  function itemsAdd(item) {
    ITEMS.unshift(item);
    saveItems();
  }

  function itemsDel(id) {
    const idx = ITEMS.findIndex(function (i) { return i != null && i.id === id; });
    if (idx < 0) {
      return;
    }
    ITEMS.splice(idx, 1);
    saveItems();
  }

  function loadItems() {
    ITEMS = settingsGet(CONF_IMAGES);
  }

  function saveItems() {
    ITEMS = ITEMS.filter(function (i) { return i != null; });
    settingsSet(CONF_IMAGES, ITEMS, true);
  }

  function settingsGet(key) {
    const val = SETTINGS[key];
    return val != null ? val : DEFAULTS[key];
  }

  function settingsSet(key, val, save) {
    SETTINGS[key] = val;
    if (save === true) {
      saveSettings();
    }
  }

  function loadSettings() {
    const key = `phe`;
    try {
      SETTINGS = JSON.parse(win.localStorage.getItem(key) || '{}');
    } catch(err) {
      // settings is corrupted
      win.localStorage.removeItem(key);
      SETTINGS = {};
    }
  }

  function saveSettings() {
    const key = `phe`;
    win.localStorage.setItem(key, JSON.stringify(SETTINGS));
  }

})(window, jQuery);