MyFigureCollection: 快速下载图片

快速下载MyFigureCollection的相册图片

目前為 2022-05-04 提交的版本,檢視 最新版本

// ==UserScript==
// @name        MyFigureCollection: 快速下载图片
// @namespace   Violentmonkey Scripts
// @match       https://myfigurecollection.net/picture/*
// @match       https://myfigurecollection.net/pictures.php
// @grant       GM_download
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @license     MIT
// @version     1.4.2
// @author      -
// @description 快速下载MyFigureCollection的相册图片
// ==/UserScript==
const TIME_OUT = 60 * 1000
const FILTER_URLS = [
  'https://static.myfigurecollection.net/ressources/nsfw.png',
  'https://static.myfigurecollection.net/ressources/spoiler.png'
]

function* getSequence() {
    let i = 0;
    while (true) {
        yield (i = i + 1);
    }
}
let sequence = getSequence();
function getId(name) {
    return `${name ? name : 'none'}.${new Date().getTime()}.${Math.floor(Math.random() * 10000)}.${sequence.next().value}`;
}
class Dep {
    constructor(name) {
        this.subs = [];
        this.id = getId(name);
    }
    delete() {
        if (this.subs.length < 1)
            return;
        this.notify();
        this.subs.forEach((sub) => {
            sub.removeDep(this);
        });
        this.subs.length = 0;
    }
    addSub(sub) {
        this.subs.push(sub);
    }
    depend() {
        Dep.target.addDep(this);
    }
    notify() {
        this.subs.forEach((sub) => {
            sub.update();
        });
    }
}
Dep.target = null;

const extend = (a, b) => {
    for (const key in b) {
        a[key] = b[key];
    }
    return a;
};
const isFunction = (val) => typeof val === 'function';
const NOOP = () => { };
const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);
const isPlainObject = (val) => toTypeString(val) === '[object Object]';
const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);
function toArray(nodes) {
    return [].slice.call(nodes);
}
const unique = (arr) => Array.from(new Set(arr));

function observe(value, vm) {
    if (!value || typeof value !== 'object')
        return value;
    if(value.__ob__)
        return value
    return new Observer(value, vm).proxy;
}
class Observer {
    constructor(data, vm) {
        Object.keys(data).forEach((key) => {
            data[key] = observe(data[key], vm);
        });
        this.dep = new Dep('data');
        Object.defineProperty(data, '__ob__', {
            configurable: false,
            enumerable: false,
            value:this
        })
        this.proxy = new Proxy(data, {
            get: (target, key, receiver) => {
                const result = Reflect.get(target, key, receiver)
                if (Dep.target){
                  if(isPlainObject(result) && result.__ob__)
                    result.__ob__.dep.depend()
                  else
                    this.dep.depend()
                }
                return result;
            },
            set: (target, key, newValue, receiver) => {
                const result = Reflect.set(target, key, observe(newValue), receiver);
                this.dep.notify();
                return result;
            },
            deleteProperty: (target, key) => {
                const childObj = target[key];
                let result = false;
                if (isPlainObject(childObj) && hasOwn(childObj, '__ob__')) {
                    let ob = childObj['__ob__'];
                    ob.dep.delete();
                    ob = null;
                    result = Reflect.deleteProperty(target, key);
                    this.dep.notify();
                }
                return result;
            },
        });
    }
}

class EventEmitter {
    constructor(scope) {
        this._events = new Map();
        if (scope)
            this._scope = scope;
    }
    has(eventName){
      return this._events.has(eventName)
    }
    on(eventName, callback) {
        if (!this._events.has(eventName))
            this._events.set(eventName, []);
        this._events.get(eventName).push(callback);
    }
    emit(eventName, value) {
        if (!this._events.has(eventName))
            return;
        this._events.get(eventName).forEach((callback) => {
            if (isFunction(callback))
                    callback(value);
        });
    }
    off(eventName, callback) {
        if (callback) {
            this._events.set(eventName, this._events.get(eventName).filter((cb) => {
                if (cb === callback || cb.originFunction === callback)
                    return false;
            }));
        }
        else {
            this._events.delete(eventName);
        }
    }
    once(eventName, callback) {
        const self = this;
        const onceCallback = function () {
            self.off(eventName, onceCallback);
            callback.apply(self, arguments);
        };
        onceCallback.originFunction = callback;
        this.on(eventName, onceCallback);
    }
}

var EventLoop;
(function (EventLoop) {
    const callbacks = [];
    const p = Promise.resolve();
    let pending = false;
    let useMacroTask = false;
    function flushCallbacks() {
        pending = false;
        const copies = callbacks.slice(0);
        callbacks.length = 0;
        copies.forEach((fn) => fn());
    }
    EventLoop.flushCallbacks = flushCallbacks;
    const macroTimerFunction = () => {
        setTimeout(flushCallbacks, 0);
    };
    const microTimerFunction = () => {
        p.then(flushCallbacks);
    };
    function withMacroTask(fn) {
        return (fn._withTask ||
            (fn._withTask = function () {
                useMacroTask = true;
                const res = fn.apply(null, arguments);
                useMacroTask = false;
                return res;
            }));
    }
    EventLoop.withMacroTask = withMacroTask;
    function nextTick(context, callback) {
        let _resolve;
        callbacks.push(() => {
            if (callback)
                callback.call(context);
            else if (_resolve)
                _resolve(context);
        });
        if (!pending) {
            pending = true;
            if (useMacroTask)
                macroTimerFunction();
            else
                microTimerFunction();
        }
        if (!callback)
            return new Promise((resolve) => {
                _resolve = resolve;
            });
    }
    EventLoop.nextTick = nextTick;
})(EventLoop || (EventLoop = {}));

function getApplyFunction(fn, scope) {
    const func = function () {
        fn.apply(scope, arguments);
    };
    return func;
}
const createVM = (options = {}) => new MVVM(extend(options, {
    element: options.element ? options.element : document.body
}));
class MVVM {
    constructor(options = {}) {
        this.$event = new EventEmitter(this);
        this.$children = {};
        this.$refs = {};
        this.$on = getApplyFunction(this.$event.on, this.$event);
        this.$emit = getApplyFunction(this.$event.emit, this.$event);
        this.$off = getApplyFunction(this.$event.off, this.$event);
        this.$once = getApplyFunction(this.$event.once, this.$event);
        this.$options = options;
        this.components = options.components;
        MVVM.cid += 1;
        this.cid = MVVM.cid;
        this._init();
        if (this.$options.element)
            this.compile(this.$options.element);
    }
    $watch(key, cb) {
        new Watcher(this, key, cb);
    }
    $nextTick(callback) {
        if (callback)
            return EventLoop.nextTick(this, callback);
        return EventLoop.nextTick(this);
    }
    use(fn) {
        fn.call(this, this);
        return this;
    }
    compile(element) {
        this.$compile = new Compile(element, this);
        this.$emit('mounted');
    }
    _init() {
        this._initMethods();
        this._initLifecycle();
        this.$emit('beforeCreate');
        this._initData();
        this._initComputed();
        this._initWatch();
        this.$emit('created');
    }
    _initMethods() {
        let methods = this.$options.methods;
        if (typeof methods !== 'object')
            return;
        Object.keys(methods).forEach((key) => {
            let object = methods[key];
            if (!isFunction(object))
                return;
            if (this[key])
                return;
            this[key] = object;
        });
    }
    _initLifecycle() {
        this.$options.beforeCreate &&
            this.$on('beforeCreate', this.$options.beforeCreate.bind(this));
        this.$options.created && this.$on('created', this.$options.created.bind(this));
        this.$options.beforeMount &&
            this.$on('beforeMount', this.$options.beforeMount);
        this.$options.mounted && this.$on('mounted', this.$options.mounted.bind(this));
        this.$options.beforeUpdate &&
            this.$on('beforeUpdate', this.$options.beforeUpdate);
        this.$options.updated && this.$on('updated', this.$options.updated.bind(this));
    }
    _initData() {
        const data = this.$options.data;
        this.$data = isFunction(data) ? data.call(this) : data;
        Object.keys(this.$data).forEach((key) => Object.defineProperty(this, key, {
            configurable: false,
            enumerable: true,
            get: () => {
                return this.$data[key];
            },
            set: (newVal) => {
                this.$data[key] = newVal;
            },
        }));
        this.$data = observe(this.$data, this);
    }
    _initComputed() {
        let computed = this.$options.computed;
        if (!isPlainObject(computed))
            return;
        Object.keys(computed).forEach((key) => {
            let object = computed[key];
            Object.defineProperty(this, key, {
                get: isFunction(object)
                    ? object
                    : 'get' in object
                        ? object.get
                        : NOOP,
                set: isFunction(object)
                    ? object
                    : 'set' in object
                        ? object.set
                        : NOOP,
            });
        });
    }
    _initWatch() {
        let watch = this.$options.watch;
        if (typeof watch !== 'object')
            return;
        Object.keys(watch).forEach((key) => {
            let object = watch[key];
            if (!isFunction(object))
                return;
            this.$watch(key, object);
        });
    }
}
MVVM.cid = 0;
function getVMVal(vm, exp) {
    let temp;
    exp.split('.').forEach((k, i) => {
        if (i === 0)
            temp = vm[k];
        else
            temp = temp[k];
    });
    return temp;
}
function setVMVal(vm, exp, value) {
    let temp;
    let exps = exp.split('.');
    if (exps.length === 1)
        vm[exps[0]] = value;
    else
        exps.forEach((k, i, exps) => {
            if (i === 0)
                temp = vm[k];
            else if (i < exps.length - 1)
                temp = temp[k];
            else if (i === exps.length - 1)
                temp[k] = value;
        });
}

function parseGetter(exp) {
    return (vm) => getVMVal(vm, exp);
}
class Watcher {
    constructor(vm, expOrFn, callback,deep = false) {
        this.callback = callback;
        this.vm = vm;
        this.deep = deep
        this._depIds = {};
        if (isFunction(expOrFn))
            this._getter = expOrFn;
        else
            this._getter = parseGetter(expOrFn.trim());
        this.value = this.get();
    }
    update() {
        let newVal = this.get();
        let oldVal = this.value;
        if (newVal === oldVal && !(this.deep && isPlainObject(newVal) && isPlainObject(oldVal))) return
        this.value = newVal;
        this.callback.call(this.vm, newVal, oldVal);
    }
    removeDep(dep) {
        delete this._depIds[dep.id];
    }
    addDep(dep) {
        if (!hasOwn(this._depIds, dep.id)) {
            dep.addSub(this);
            this._depIds[dep.id] = dep;
        }
    }
    get() {
        Dep.target = this;
        let value = this._getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    }
}

class ElementUtility {
    static fragment(el) {
        let fragment = document.createDocumentFragment(), child;
        while ((child = el.firstChild))
            fragment.appendChild(child);
        return fragment;
    }
    static parseHTML(html) {
        const domParser = new DOMParser();
        let temp = domParser.parseFromString(html, 'text/html');
        return temp.body.children;
    }
    static isElementNode(node) {
        if (node instanceof Element)
            return node.nodeType == 1;
        return false;
    }
    static isTextNode(node) {
        if (node instanceof Text)
            return node.nodeType == 3;
        return false;
    }
    static text(node, value) {
        if (typeof value === 'number')
            value = String(value);
        node.textContent = value ? value : '';
    }
    static html(node, value) {
        if (typeof value === 'number')
            value = String(value);
        node.innerHTML = value ? value : '';
    }
    static class(node, value, oldValue) {
        let className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');
        let space = className && String(value) ? ' ' : '';
        node.className = className + space + value;
    }
    static model(node, newValue) {
        if (typeof newValue === 'number')
            newValue = String(newValue);
        node.value = newValue ? newValue : '';
    }
    static style(node, newValue, oldValue) {
        if (!oldValue)
            oldValue = {};
        if (!newValue)
            newValue = {};
        const keys = Object.keys(oldValue).concat(Object.keys(newValue));
        unique(keys).forEach((key) => {
            if (hasOwn(oldValue, key) && hasOwn(newValue, key)) {
                if (oldValue[key] != newValue[key])
                    node.style.setProperty(key, newValue[key]);
            }
            else if (hasOwn(newValue, key))
                node.style.setProperty(key, newValue[key]);
            else
                node.style.removeProperty(key);
        });
    }
    static display(node, newValue, oldValue) {
        let func = (val) => {
            return {
                display: val ? 'block' : 'none',
            };
        };
        ElementUtility.style(node, func(newValue), null);
    }
}

class MVVMComponent extends MVVM {
    constructor(options) {
        super(options);
        this.$template = options.template || '';
        if (options.parent)
            this.$parent = options.parent;
    }
    $mount(element) {
        this.compile(element);
    }
}

const parseAnyDirectiveFunction = (parseString) => {
    return (dir) => dir.indexOf(parseString) == 0;
};
const isDirective = parseAnyDirectiveFunction('v-');
const isEventDirective = parseAnyDirectiveFunction('on');
const isTextDirective = parseAnyDirectiveFunction('text');
const isHtmlDirective = parseAnyDirectiveFunction('html');
const isModelDirective = parseAnyDirectiveFunction('model');
const isClassDirective = parseAnyDirectiveFunction('class');
const isStyleDirective = parseAnyDirectiveFunction('style');
const isShowDirective = parseAnyDirectiveFunction('show');
const isRefDirective = parseAnyDirectiveFunction('ref');
const isForDirective = parseAnyDirectiveFunction('for');

function bindWatcher(node, vm, exp, updater) {
    let __for__ = node['__for__'];
    let val;
    if (__for__ && __for__[exp]) 
        val = __for__[exp]
    else
        val = getVMVal(vm, exp);
    updater && updater(node, val);
    new Watcher(vm, exp, (newValue, oldValue) => {
        if (newValue === oldValue)
            return;
        updater && updater(node, newValue, oldValue);
    });
}
function eventHandler(node, vm, exp, eventType) {
    let fn = vm.$options.methods && vm.$options.methods[exp];
    if (eventType && fn) {
        if(node instanceof MVVMComponent)
          node.$on(eventType,fn.bind(vm))
        else
          node.addEventListener(eventType, fn.bind(vm), false);
    }
}
function vFor(node, vm, exp, c) {
    let reg = /\((.*)\)/;
    let item, index, list;
    if (reg.test(exp)) {
        const arr = RegExp.$1.trim().split(',');
        item = arr[0];
        index = arr[1];
        let rightString = RegExp.rightContext.trim();
        let rarr = rightString.split(' ');
        list = rarr[1];
        if (rarr[0] !== 'in')
            return;
        let val = getVMVal(vm, list);
        let children = [];
        toArray(node.children).forEach((element) => {
            children.push(element.cloneNode(true));
            node.removeChild(element);
        });
        for (let i = 0; i < val.length; i++) {
            children.forEach((element) => {
                let newNode = element.cloneNode(true);
                newNode.__for__ = {
                    [item]: val[i],
                    [index]: i
                };
                node.appendChild(newNode);
                c.compileElement(node);
            });
        }
    }
}
function forHandler(node, vm, exp, c) {
    vFor(node, vm, exp, c);
    new Watcher(vm, exp, (newValue, oldValue) => {
        if (newValue === oldValue)
            return;
        vFor(node, vm, exp, c);
    });
}
class Compile {
    constructor(el, vm) {
        this.slotCallback = [];
        this.$vm = vm;
        this.$el = ElementUtility.isElementNode(el)
            ? el
            : document.querySelector(el);
        this._init();
    }
    _init() {
        if (this.$vm instanceof MVVMComponent) {
            this.$slot = ElementUtility.fragment(this.$el);
            this.$fragment = this.parseComponentTemplate(this.$vm.$template);
            this.$vm.$el = this.$el;
            this.$vm.$emit('beforeMount');
            this.compileElement(this.$fragment);
            this.$el.parentNode.replaceChild(this.$fragment, this.$el);
        }
        else {
            this.$fragment = ElementUtility.fragment(this.$el);
            this.$vm.$el = this.$el;
            this.$vm.$emit('beforeMount');
            this.compileElement(this.$fragment);
            this.$el.appendChild(this.$fragment);
        }
        Object.entries(this.$vm.$children).forEach(([key, child]) => {
            const slotCallback = child.$compile.slotCallback;
            if (slotCallback.length < 1)
                return;
            slotCallback.forEach((fn) => {
                fn(this);
            });
        });
    }
    isSlot(node) {
        if (node.tagName === 'SLOT')
            return true;
        return false;
    }
    compileSlotElement(slot) {
        if (!(this.$vm instanceof MVVMComponent))
            return;
        if (this.$slot.children.length === 0) {
            slot.parentNode.removeChild(slot);
            return;
        }
        this.slotCallback.push(c => {
            c.compileElement(this.$slot);
            slot.parentNode.replaceChild(this.$slot, slot);
        });
    }
    parseComponentTemplate(templateHTML) {
        let element = ElementUtility.parseHTML(templateHTML);
        const template = document.createElement('template');
        if (element.length) {
            if (element.length === 1) {
                if (element[0].tagName.toLowerCase() !== 'template')
                    template.appendChild(element[0]);
            }
            else
                toArray(element).forEach((child) => {
                    template.appendChild(child);
                });
        }
        return ElementUtility.fragment(template);
    }
    parseTemplate(leftString, rightString) {
        return (node, newValue, oldValue) => {
            const str = leftString + newValue + rightString;
            ElementUtility.text(node, str);
        };
    }
    compileElement(el) {
        let childNodes = [];
        // slice
        el.childNodes.forEach(node=>{
          childNodes.push(node)
        })
        childNodes.forEach((node) => {
            if (el['__for__'])
                node['__for__'] = el['__for__'];
            let reg = /\{\{(.*)\}\}/;
            if (ElementUtility.isElementNode(node)) {
                if (this.isComponent(node))
                  this.compileComponent(node.tagName.toLowerCase(), node);
                else if (this.isSlot(node)) 
                    this.compileSlotElement(node);
                else
                    this.compile(node);
            }
            else if (ElementUtility.isTextNode(node) &&
                reg.test(node.textContent))
                bindWatcher(node, this.$vm, RegExp.$1.trim(), this.parseTemplate(RegExp.leftContext, RegExp.rightContext));
            if (node.childNodes && node.childNodes.length)
                this.compileElement(node);
        });
    }
    compile(node) {
        let nodeAttrs = node.attributes;
        toArray(nodeAttrs).forEach((attr) => {
            let attrName = attr.name;
            if (!isDirective(attrName)){
              if (attrName.startsWith('[') && attrName.endsWith(']')) {
                node.removeAttribute(attrName)
                let realAttrName = attrName.replace('[','')
                realAttrName = realAttrName.replace(']','')
                bindWatcher(node,this.$vm,attr.value,(node,newVal,oldVal)=>{
                  node.setAttribute(realAttrName,newVal)
                })
              }
              return;
            }
            let dir = attrName.substring(2);
            let suffix = dir.split(':')[1];
            let exp = attr.value || suffix;
            if (isEventDirective(dir))
                eventHandler(node, this.$vm, exp, suffix);
            else if (isTextDirective(dir))
                bindWatcher(node, this.$vm, exp, ElementUtility.text);
            else if (isHtmlDirective(dir))
                bindWatcher(node, this.$vm, exp, ElementUtility.html);
            else if (isClassDirective(dir))
                bindWatcher(node, this.$vm, exp, ElementUtility.class);
            else if (isModelDirective(dir)) {
                bindWatcher(node, this.$vm, exp, ElementUtility.model);
                let val = getVMVal(this.$vm, exp);
                node.addEventListener('input', (e) => {
                    let target = e.target;
                    let newValue = target.value;
                    if (val === newValue)
                        return;
                    setVMVal(this.$vm, exp, newValue);
                    val = newValue;
                });
            }
            else if (isStyleDirective(dir))
                bindWatcher(node, this.$vm, exp, ElementUtility.style);
            else if (isShowDirective(dir))
                bindWatcher(node, this.$vm, exp, ElementUtility.display);
            else if (isRefDirective(dir))
                this.$vm.$refs[exp] = node;
            else if (isForDirective(dir))
                forHandler(node, this.$vm, exp, this);
            node.removeAttribute(attrName);
        });
    }
    isComponent(node) {
        const tagName = node.tagName.toLowerCase();
        if (!/^[(a-zA-Z)-]*$/.test(tagName))
            return false;
        if (this.$vm.components && hasOwn(this.$vm.components, tagName))
            return true;
        return false;
    }
    compileComponent(componentName, node) {
        const attributes = []
        toArray(node.attributes).forEach((attr) => {
          attributes.push(attr)
        })
        const componentOptions = this.$vm.components[componentName];
        const component = new MVVMComponent(extend(componentOptions, {
            parent: this.$vm
        }));
        component.$mount(node);
        this.$vm.$children[componentName] = component;
        attributes.forEach(attr=>{
          let attrName = attr.name;
          if (!isDirective(attrName))
            return;
          let dir = attrName.substring(2);
          let suffix = dir.split(':')[1];
          let exp = attr.value || suffix;
          if (isEventDirective(dir))
            eventHandler(component, this.$vm, exp, suffix);
          else if (isRefDirective(dir))
              this.$vm.$refs[exp] = component;
        })
    }
}

const globalState =observe({
  group:GM_getValue('groupCount',1),
  count:GM_getValue('picCount',1),
  componentDownloadStatus:{},
  downloadStates:{
    normal:0,
    loading:0,
    error:0,
    timeout:0,
    downloaded:0,
  }
})

new Watcher(null,()=>{
  return globalState.count
},(newVal)=>{
  GM_setValue('picCount',newVal)
})

new Watcher(null,()=>{
  return globalState.group
},(newVal)=>{
  GM_setValue('groupCount',newVal)
  globalState.count = 0
})

new Watcher(null,()=>{
  return globalState.componentDownloadStatus
},(newVal)=>{
  Object.assign(globalState.downloadStates,{
    normal:0,
    loading:0,
    error:0,
    timeout:0,
    downloaded:0,
  })
  const states = globalState.downloadStates
  Object.keys(newVal).forEach(key=>{
    const status = newVal[key]
    if(states[status] != null)
        states[status]+=1
  })
},true)

function beforeDownload(){
  if(GM_getValue('groupCount',1) != globalState.group){
    const curCount = GM_getValue('picCount',1) + 1
    globalState.group = GM_getValue('groupCount',1)
    globalState.count = curCount
  }else{
    globalState.group = GM_getValue('groupCount',1)
    globalState.count = GM_getValue('picCount',1) + 1
  }
  
  return {group:globalState.group,count:globalState.count}
}

const DownloadSequence = {
  template: `
    <span>当前组:</span>
    <button v-on:click="decreaseGroup">-</button>
    <span>{{global.group}}</span>
    <button v-on:click="increaseGroup">+</button>
    <span>当前图片:</span>
    <button v-on:click="decreaseCount">-</button>
    <span>{{global.count}}</span>
    <button v-on:click="increaseCount">+</button>
  `,
  data(){
    return {
      global:globalState,
    }
  },
  methods:{
    increaseCount(){
      this.global.count+=1
    },
    decreaseCount(){
      this.global.count-=1
    },
    increaseGroup(){
      this.global.group+=1
    },
    decreaseGroup(){
      this.global.group-=1
    }
  }
}

const REQEUST_BUTTON_STYLES = {
  normal:{},
  loading: { background: 'white',color:'black',cursor:'wait'},
  error:{background:'red',color:'white'},
  timeout:{background:'yellow',color:'black'},
  downloaded: {background:'green',color:'white'}
}

const DownloadButton = {
    template: `
      <button v-on:click="download" v-style="downloadBtnStyle">
        {{downloadedMsg}}
      </button>
    `,
    data(){
      return {
        oldStatus:'normal',
        downloadStatus:'normal' // 'normal' 'loading' 'error' 'timeout' 'downloaded'
      }
    },
    computed:{
      downloadBtnStyle(){
        return REQEUST_BUTTON_STYLES[this.downloadStatus]
      },
      downloadedMsg(){
        const messages = {
          normal:'下载原图' ,
          loading: '正在下载...',
          error:'下载失败',
          timeout:'下载超时',
          downloaded:  '重新下载'
        }
        return messages[this.downloadStatus]
      }
    },
    watch:{
      downloadStatus(newStatus,oldStatus){
        this.oldStatus = oldStatus
        globalState.componentDownloadStatus[this.cid] = newStatus
      }
    },
    created(){
      globalState.componentDownloadStatus[this.cid] = this.downloadStatus
    },
    destoryed(){
      delete globalState.componentDownloadStatus[this.cid]
    },
    methods:{
      download(){
        if(this.downloadStatus === 'loading') return
        this.downloadStatus = 'loading'
        if(this.oldStatus !== 'error' && this.oldStatus !== 'timeout')
          this.value = beforeDownload()
        this.$emit('download',this.value)
      },
    }
}

const DownloadState = {
  template:`
    <div style="display:flex;flex-direction:row;padding:5px">
      <div style="margin-right:5px;color:grey">
        <span style="">未下载:</span>
        <span>{{states.normal}}</span>
      </div>
      <div style="margin-right:5px;color:green">
        <span style="">已下载:</span>
        <span>{{states.downloaded}}</span>
      </div>
      <div style="margin-right:5px;color:black">
        <span style="">正在下载:</span>
        <span>{{states.loading}}</span>
      </div>
      <div style="margin-right:5px;color:brown">
        <span style="">下载超时:</span>
        <span>{{states.timeout}}</span>
      </div>
      <div style="color:red">
        <span>下载报错:</span>
        <span>{{states.error}}</span>
      </div>
    </div>
  `,
  data(){
    return {
      states:globalState.downloadStates
    }
  },
  methods:{
    download(value){
      this.$emit('download',value)
    }
  }
}

const PictureDownload = {
  components:{
    'download-button':DownloadButton,
    'download-sequence':DownloadSequence,
  },
  template:`
    <div>
      <download-sequence></download-sequence>
      <span v-show:downloaded style="margin:0 10px">{{msg}}</span>
      <download-button v-ref="downloadButton" v-on:download="download"></download-button>
    </div>
  `,
  data(){
    return {
      group:0,
      count:0,
      downloaded:false
    }
  },
  computed:{
    msg(){
      return `${this.group}.${this.count}`
    }
  },
  mounted(){
      const value = GM_getValue(window.location.href)
      if(!value) return
      this.$refs.downloadButton.downloadStatus = value.downloadStatus
      this.$refs.downloadButton.value = {
        group: value.group,
        count: value.count
      }
      this.downloaded = true
      this.group = value.group
      this.count = value.count
  },
  methods:{
    download(value){
      this.downloaded = true
      this.group = value.group
      this.count = value.count
      this.$emit('download',value)
    }
  }
}

function insertAfter(targetNode, afterNode){
  const parentNode = afterNode.parentNode
  const beforeNode = afterNode.nextElementSibling
  if(beforeNode == null)
    parentNode.appendChild(targetNode)
  else
    parentNode.insertBefore(targetNode, beforeNode)
}

function mountVM(node,component){
  const vm = new MVVMComponent(component)
  vm.$mount(node)
  return vm
}

function download({vm,picture,group,count,origin}){
  const values = origin.split('/')
  const fileType = values[values.length - 1].split('.')[1]
  const name = `${group}.${count}.${fileType}`
  const end = (status)=>{
    return ()=>{
      vm.downloadStatus = status
      GM_setValue(picture,{origin,group,count,downloadStatus:status})
    }
  }
  
  GM_download({
    url:origin,
    name,
    timeout:TIME_OUT,
    onload:end('downloaded'),
    onerror:end('error'),
    ontimeout:end('timeout'),
  })
}

function renderPictureExtension(objectMeta,thePicture){
  const origin = thePicture.href
  const div = document.createElement('div')
  objectMeta.appendChild(div)
  const vm = mountVM(div,PictureDownload)
  vm.$on('download',(value)=>{
    download({
      ...value,
      origin,
      picture:window.location.href,
      vm:vm.$refs.downloadButton,
    })
  })
}

const createPictruePreview = ({thumb,origin,picture}) => {
  const commonStyle={
    'margin':'10px 10px',
    'border':'1px solid #000',
    'padding':'10px',
    'border-radius':'5px',
    'background':'#fff',
    'transition':'all 0.5s',
    'box-sizing': 'border-box'
  }
  
  return {
      components:{
        'download-button':DownloadButton
      },
      template: `
          <div v-style="containerStyle">
            <div style="margin-bottom:10px;display:flex;flex-direction:row;justify-content:center;align-items:center;width:100%;">
              <div style="margin:0 10px;flex-shrink:0"><img style="cursor:pointer" v-on:click="openPicturePage" [src]="thumb" /></div>
              <div style="flex-shrink:1" v-show="originalImage"><img style="width:100%" [src]="src" /></div>
            </div>
            <div style="display:flex;justify-content:center;align-items:center;flex-direction:column">
                <download-button v-ref="downloadButton" v-on:download="download"></download-button>
                <br v-show:downloaded />
                <div v-show:downloaded>
                  <span>组:</span>
                  <span >{{group}}</span>
                  <span>图片:</span>
                  <span >{{count}}</span>
                </div>
                <br />
                <button v-on:click="toggle">{{msg}}</button>
                <br />
                <button v-show:refresh v-on:click="refreshOrigin" v-style="refreshBtnStyle">
                  {{refreshMsg}}
                </button>
            </div>
          </div>
      `,
      data(){
        return {
          thumb,
          origin,
          picture,
          refresh:FILTER_URLS.includes(origin),
          originalImage:false,
          group:0,
          count:0,
          downloaded:false,
          refreshStatus:'normal' // 'normal' 'loading' 'error' 'timeout' 'downloaded'
        }
      },
      computed:{
        src(){
          return this.originalImage ? this.origin : this.thumb
        },
        msg(){
          return this.originalImage ? '关闭预览' : '预览原图'
        },
        containerStyle(){
          return Object.assign({},commonStyle,this.originalImage ? {
            width:'100%'
          }:{} ) 
        },
        refreshBtnStyle(){
          return REQEUST_BUTTON_STYLES[this.refreshStatus]
        },
        refreshMsg(){
          const messages = {
            normal:'获取隐藏图片' ,
            loading: '正在获取...',
            error:'获取失败',
            timeout:'获取超时',
            downloaded:  '重新获取'
          }
          return messages[this.refreshStatus]
        }
      },
      mounted(){
        const value = GM_getValue(picture)
        if(!value) return
        this.$refs.downloadButton.downloadStatus = value.downloadStatus
        this.$refs.downloadButton.value = {
          group: value.group,
          count: value.count
        }
        this.downloaded = true
        this.group = value.group
        this.count = value.count
      },
      methods:{
        download(value){
          this.downloaded = true
          this.group = value.group
          this.count = value.count
          
          if(this.refresh && this.refreshStatus !== 'downloaded')
            this.refreshOrigin()
              .then(()=>{
                this.$emit('download',{...value,origin:this.origin})
              })
          else
            this.$emit('download',{...value,origin:this.origin})
        },
        toggle(){
          if(this.refresh && !this.originalImage && this.refreshStatus !== 'downloaded')
            this.refreshOrigin()
          
          this.originalImage = !this.originalImage
        },
        refreshOrigin(){
          if(this.refreshStatus === 'loading') return
          this.refreshStatus = 'loading'
          return new Promise((resolve,reject)=>{
            GM_xmlhttpRequest({
              url:this.picture,
              responseType:'document',
              timeout:TIME_OUT,
              onload:(data)=>{
                const doc = data.response
                const a = doc.querySelector('.the-picture>a')
                if(a){
                  this.origin = a.href
                  const thumb = a.href.split('/')
                  thumb.splice(thumb.length - 1,0,'thumbnails')
                  this.thumb = thumb.join('/')
                  this.refreshStatus = 'downloaded'
                  resolve()
                  return                
                }
                this.refreshStatus = 'error'
                reject()
              },
              onerror:()=>{
                this.refreshStatus = 'error'
                reject()
              },
              ontimeout:()=>{
                this.refreshStatus = 'timeout'
                reject()
              }
            })
          })
        },
        openPicturePage(){
          window.open(picture)
        }
      }
  }
}

function parseOriginalImageURL(thumb_url){
  if(thumb_url.indexOf('thumbnails/') > -1){
    url = thumb_url.split('thumbnails/').join('')
  } else {
    const paths = thumb_url.split('pictures/')[1].split('/')
    if(paths.length > 2){
      paths.splice(3,1)
      url = [thumb_url.split('pictures/')[0],paths.join('/')].join('pictures/')
    }
  }

  return url
}

function getImageURLs(node){
  const picture = node.querySelector('a').href
  const viewport = node.querySelector('.viewport')
  const thumb = viewport.style.background.split('"')[1]
  let origin = thumb
  if(FILTER_URLS.includes(origin))
    origin = thumb
  else
    origin = parseOriginalImageURL(origin)
  
  return {thumb,origin,picture}
}

function renderPicturesExtension(thumbs){
  function _initParentNode(parentNode){
    parentNode.innerHTML = ''
    parentNode.style.setProperty('display','flex')
    parentNode.style.setProperty('flex-direction','row')
    parentNode.style.setProperty('flex-wrap','wrap')
    parentNode.style.setProperty('justify-content','center')
    parentNode.style.setProperty('align-items','center')
    const div1 = document.createElement('div')
    parentNode.parentNode.insertBefore(div1,parentNode)
    mountVM(div1,DownloadSequence)
    const div2 = document.createElement('div')
    const pageCount = document.querySelector('.listing-count-pages') || parentNode
    pageCount.parentNode.insertBefore(div2,pageCount)
    mountVM(div2,DownloadState)
  }
  
  const parentNode = thumbs[0].parentNode
  _initParentNode(parentNode)
  thumbs.forEach(thumb_node=>{
    const imageURLs = getImageURLs(thumb_node)
    const preview = createPictruePreview(imageURLs)
    const div3 = document.createElement('div')
    parentNode.appendChild(div3)
    const previewVM = mountVM(div3,preview)
    previewVM.$on('download',(value)=>{
      download({
        ...value,
        picture:imageURLs.picture,
        vm:previewVM.$refs.downloadButton,
      })
    })
  })
}

function render(){
  const objectMeta = document.querySelector('.object-meta')
  const thePicture = document.querySelector('.the-picture>a')
  if(objectMeta && thePicture)
    renderPictureExtension(objectMeta,thePicture)
  
  const thumbs = []
  document.querySelectorAll('.picture-icon.tbx-tooltip').forEach(thumb=>{
    thumbs.push(thumb)  
  })
  if(thumbs.length > 0)
    renderPicturesExtension(thumbs.reverse())
}

render()