MyFigureCollection: 图片下载器

下载原始图片,相册下拉自动加载

// ==UserScript==
// @name        MyFigureCollection: 图片下载器
// @name:en     MyFigureCollection: Image Downloader
// @name:zh-CN  MyFigureCollection: 图片下载器
// @name:zh-TW  MyFigureCollection: 图片下载器
// @description 下载原始图片,相册下拉自动加载
// @description:en The original fullsize images downloader. The album pull down to load next page.
// @description:zh-CN 下载原始图片,相册下拉自动加载
// @description:zh-TW 下载原始图片,相册下拉自动加载
// @namespace   http://tampermonkey.net/
// @match       https://myfigurecollection.net/item/*
// @match       https://myfigurecollection.net/picture/*
// @match       https://myfigurecollection.net/pictures.php*
// @match       https://myfigurecollection.net/browse.v4.php*
// @grant       GM_download
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @require     https://greasyfork.org/scripts/444466-mini-mvvm/code/mini%20mvvm.js?version=1047822
// @license     GPL-3.0
// @compatible  Chrome
// @version     1.6.8
// @author      ayan0312
// ==/UserScript==
const TIME_OUT = 40 * 1000
const REQUEST_URL = GM_getValue('REQUEST_URL')
const FILTER_URLS = [
    'https://static.myfigurecollection.net/ressources/nsfw.png',
    'https://static.myfigurecollection.net/ressources/spoiler.png'
]
const LOCATION_ITEM_ID = (new URL(location.href)).searchParams.get('itemId') || location.href.split('/item/')[1] || ''

const logger = {
    info(...args) {
        console.log('[Image Downloader]:', ...args)
    },
    warn(...args) {
        console.log('%c[Image Downloader]:', "color: brown; font-weight: bold", ...args)
    },
    error(...args) {
        console.log('%c[Image Downloader]:', "color: red; font-weight: bold", ...args)
    },
    success(...args) {
        console.log('%c[Image Downloader]:', "color: green; font-weight: bold", ...args)
    },
}

function downloadImage(opts) {
    return new Promise((resolve, reject) => {
        if (!REQUEST_URL) {
            GM_download({
                url: opts.url,
                name: opts.name,
                timeout: TIME_OUT,
                onload: () => {
                    resolve(null)
                },
                onerror: (err) => reject({
                    status: 'error',
                    err,
                }),
                ontimeout: (err) => reject({
                    status: 'timeout',
                    err
                }),
            })
            return
        }

        GM_xmlhttpRequest({
            url: `${REQUEST_URL}?opts=${btoa(JSON.stringify(opts))}`,
            responseType: 'json',
            onload: ({ response }) => {
                if (response.success) {
                    resolve(response)
                    return
                }

                if (response.code === 3) {
                    reject({
                        status: 'timeout',
                        err: response.message
                    })
                    return
                }

                reject({
                    status: 'error',
                    err: response.message,
                })
            },
            onerror: (err) => reject({
                status: 'error',
                err,
            }),
            ontimeout: (err) => reject({
                status: 'timeout',
                err
            })
        })
    })
}

function download({ downloadButton, picture, group, count, origin }) {
	const pictureURL = picture.split('&')[0]
    const end = (status) => {
        downloadButton.downloadStatus = status
        GM_setValue(pictureURL, { origin, group, count, downloadStatus: status })
    }
    const [originName, fileType] = origin.split('/').pop().split('.')
    let name = `${group}_${count}_${pictureURL.split('/').pop()}`

    if (!REQUEST_URL)
        name = `${name}.${fileType}`

    downloadImage({
        url: origin,
        name,
		timeout:TIME_OUT * (downloadButton.timeoutTimes + 1)
    })
        .then((res) => {
            end('downloaded')
            logger.success(name,res)
        })
        .catch(({ status, err }) => {
            end(status)
            logger.warn(err)
        })
}

const downloadBtnVMs = {}
let waitRedownloadVMs = []

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

window.onbeforeunload = (e) => {
    const { loading, error, timeout } = globalState.downloadStates
    if (loading || error || timeout)
        e.returnValue = '1'
}

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, {
        total: 0,
        normal: 0,
        loading: 0,
        error: 0,
        timeout: 0,
        downloaded: 0,
    })
    const states = globalState.downloadStates
    waitRedownloadVMs = []
    Object.keys(newVal).forEach(key => {
        const status = newVal[key]
        if (states[status] != null) {
            states[status] += 1
            states.total += 1
        }

        if (status === 'timeout' || status === 'error')
            waitRedownloadVMs.push(downloadBtnVMs[key])
    })
}, true)

const DownloadSequence = {
    template: `
        <span>Group: </span>
        <button v-on:click="decreaseGroup">-</button>
        <span style="margin:0 10px">{{global.group}}</span>
        <button v-on:click="increaseGroup">+</button>
        <span style="margin-left:10px">Item: </span>
        <button v-on:click="decreaseCount">-</button>
        <span style="margin:0 10px">{{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 DownloadButton = {
    template: `
        <button v-on:click="download" v-style="downloadBtnStyle">
            {{downloadedMsg}}
        </button>
    `,
    data() {
        return {
            oldStatus: 'normal',
            downloadStatus: 'normal', // 'normal' 'loading' 'error' 'timeout' 'downloaded'
            requestButtonStyles: {
                normal: {},
                loading: { background: 'white', color: 'black', cursor: 'wait' },
                error: { background: 'red', color: 'white' },
                timeout: { background: 'yellow', color: 'black' },
                downloaded: { background: 'green', color: 'white' }
            },
            group: globalState.group,
            count: globalState.count,
			timeoutTimes: 0
        }
    },
    computed: {
        downloadBtnStyle() {
            return this.requestButtonStyles[this.downloadStatus]
        },
        downloadedMsg() {
            const messages = {
                normal: 'Download',
                loading: 'Downloading...',
                error: 'Failed',
                timeout: 'Timeout',
                downloaded: 'Redownload'
            }
            return messages[this.downloadStatus]
        }
    },
    watch: {
        downloadStatus(newStatus, oldStatus) {
            this.oldStatus = oldStatus
			if(newStatus === 'timeout')
				this.timeoutTimes++
            globalState.componentDownloadStatus[this.cid] = newStatus
        },
    },
    created() {
        globalState.componentDownloadStatus[this.cid] = this.downloadStatus
        downloadBtnVMs[this.cid] = this
    },
    destoryed() {
        delete globalState.componentDownloadStatus[this.cid]
        delete downloadBtnVMs[this.cid]
    },
    methods: {
        download() {
            if (this.downloadStatus === 'loading') return
            this.downloadStatus = 'loading'

            if (LOCATION_ITEM_ID && !GM_getValue(LOCATION_ITEM_ID)) {
                GM_setValue(LOCATION_ITEM_ID, true)
                this.first = true
            }
            if (this.oldStatus !== 'error' && this.oldStatus !== 'timeout')
                this.refreshGroupAndCount()

            this.$emit('download', { group: this.group, count: this.count })
        },
        refreshGroupAndCount() {
            const newestGroup = GM_getValue('groupCount', 1)
            const newestCount = GM_getValue('picCount', 0)
            if (globalState.group !== newestGroup)
                globalState.group = newestGroup
            if (globalState.count !== newestCount)
                globalState.count = newestCount

            if (this.first && globalState.count > 0) {
                globalState.group += 1
                this.first = false
            }

            globalState.count += 1

            this.group = globalState.group
            this.count = globalState.count
        }
    }
}

const DownloadState = {
    template: `
        <div style="display:flex;flex-direction:row;padding:10px;flex-wrap:wrap;align-items:center;justify-content:center;width:100%;border-top: 1px dashed #ccc;">
            <div style="margin-right:15px;color:black">
                <span style="">Total: </span>
                <span>{{states.total}}</span>
            </div>
            <div style="margin-right:15px;color:green">
                <span style="">Downloaded: </span>
                <span>{{states.downloaded}}</span>
            </div>
            <div style="margin-right:15px;color:grey">
                <span style="">Downloading: </span>
                <span>{{states.loading}}</span>
            </div>
            <div style="margin-right:15px;color:brown">
                <span style="">Timeout: </span>
                <span>{{states.timeout}}</span>
            </div>
            <div style="color:red">
                <span>Failed: </span>
                <span>{{states.error}}</span>
            </div>
            <button style="margin-left:10px" v-show="waitCount" v-on:click="redownload">Redownload Timeout&Error</button>
        </div>
  `,
    data() {
        return {
            states: globalState.downloadStates
        }
    },
    computed: {
        waitCount() {
            return this.states.timeout + this.states.error > 0
        }
    },
    methods: {
        redownload() {
            waitRedownloadVMs.forEach(vm => vm.download())
        }
    }
}

const createPictureDownload = (origin) => {
    return {
        components: {
            'download-button': DownloadButton,
            'download-sequence': DownloadSequence,
        },
        template: `
            <div>
                <download-sequence></download-sequence>
                <span v-show:downloaded>{{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 `Group: ${this.group} Item: ${this.count}`
            }
        },
        mounted() {
            const value = GM_getValue(window.location.href.split('&')[0])
            if (!value) return
            this.$refs.downloadButton.downloadStatus = value.downloadStatus
            this.$refs.downloadButton.group = value.group
            this.$refs.downloadButton.count = value.count

            this.downloaded = true
            this.group = value.group
            this.count = value.count
        },
        methods: {
            download({ group, count }) {
                this.downloaded = true
                this.group = group
                this.count = count

                download({
                    group,
                    count,
                    origin,
                    picture: window.location.href,
                    downloadButton: this.$refs.downloadButton,
                })
            }
        }
    }
}

const createPicturePreview = ({ thumb, origin, picture }, preview) => {
    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;width:100%">
                        <img style="cursor:pointer;width:100%;min-width:140px;min-height:60px" v-on:click="openPicturePreview" [src]="thumb" />
                    </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>Group:</span>
                        <span >{{group}}</span>
                        <span>Item:</span>
                        <span >{{count}}</span>
                    </div>
                    <br />
                    <button v-on:click="openPicturePage">Open</button>
                    <br v-show:refresh />
                    <button v-show:refresh v-on:click="refreshOrigin" v-style="refreshBtnStyle">
                        {{refreshMsg}}
                    </button>
                </div>
            </div>
      `,
        data() {
            return {
                thumb,
                origin,
                refresh: FILTER_URLS.includes(origin),
                originalImage: false,
                group: 0,
                count: 0,
                downloaded: false,
                refreshStatus: 'normal',
                downloadStatus: 'normal',
                requestBorderStyle: {
                    normal: { border: '2px solid black' },
                    loading: { border: '2px solid grey' },
                    error: { border: '2px solid red' },
                    timeout: { border: '2px solid yellow' },
                    downloaded: { border: '2px dashed green' }
                },
                requestButtonStyles: {
                    normal: {},
                    loading: { background: 'white', color: 'black', cursor: 'wait' },
                    error: { background: 'red', color: 'white' },
                    timeout: { background: 'yellow', color: 'black' },
                    downloaded: { background: 'green', color: 'white' }
                },
                commonStyle: {
                    'margin': '10px 10px',
                    'padding': '10px',
                    'border-radius': '5px',
                    'background': '#fff',
                    'transition': 'all 0.5s',
                    'box-sizing': 'border-box',
                    'max-width': '300px'
                }
            }
        },
        computed: {
            containerStyle() {
                const borderStyle = this.requestBorderStyle[this.downloadStatus]
                return Object.assign({}, borderStyle, this.commonStyle)
            },
            refreshBtnStyle() {
                return this.requestButtonStyles[this.refreshStatus]
            },
            refreshMsg() {
                const messages = {
                    normal: 'Show Spoiler/NSFW',
                    loading: 'Showing...',
                    error: 'Failed',
                    timeout: 'Timeout',
                    downloaded: 'Reshow'
                }
                return messages[this.refreshStatus]
            }
        },
        mounted() {
            this.$watch(() => this.$refs.downloadButton.downloadStatus, (newVal) => {
                this.downloadStatus = newVal
            })

            const value = GM_getValue(picture.split('&')[0])
            if (!value) return
            this.$refs.downloadButton.downloadStatus = value.downloadStatus
            this.$refs.downloadButton.group = value.group
            this.$refs.downloadButton.count = value.count

            this.downloaded = true
            this.group = value.group
            this.count = value.count
        },
        methods: {
            download({ group, count }) {
                this.downloaded = true
                this.group = group
                this.count = count

                const params = {
                    group,
                    count,
                    origin: this.origin,
                    picture: picture,
                    downloadButton: this.$refs.downloadButton
                }

                if (this.refresh && this.refreshStatus !== 'downloaded') {
                    this.refreshOrigin()
                        .then(() => {
                            params.origin = this.origin
                            download(params)
                        })
                    return
                }

                download(params)
            },
            openPicturePage() {
                window.open(picture)
            },
            refreshOrigin() {
                if (this.refreshStatus === 'loading') return
                this.refreshStatus = 'loading'
                return new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        url: 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({
                                status: 'error',
                                err: data
                            })
                        },
                        onerror: (err) => {
                            this.refreshStatus = 'error'
                            reject({
                                status: 'error',
                                err,
                            })
                        },
                        ontimeout: (err) => {
                            this.refreshStatus = 'timeout'
                            reject({
                                status: 'timeout',
                                err,
                            })
                        }
                    })
                })
            },
            openPicturePreview() {
                if (this.refresh && this.refreshStatus !== 'downloaded') {
                    this.refreshOrigin()
                        .then(() => {
                            preview.open(this.origin)
                        })
                    return
                }
                preview.open(this.origin)
            }
        }
    }
}

const AutoloadMessageBox = {
    template: `
        <div style="width:100%;box-sizing:border-box;text-align:center;padding:15px">{{msg}}</div>
    `,
    data() {
        return {}
    },
    computed: {
        msg() {
            const messages = {
                normal: 'Pull Down',
                loading: 'Auto Loading...',
                error: 'Failed',
                timeout: 'Timeout',
                downloaded: 'Loaded',
                empty: 'Loaded All'
            }

            return messages[globalState.autoloadStatus]
        }
    },
}

const createItemTips = (itemId) => {
    return {
        template: `
            <span v-show="existItemId">Have been downloaded</span>
        `,
        data() {
            return {
            }
        },
        computed: {
            existItemId() {
                return !!GM_getValue(itemId)
            }
        }
    }
}

const Preview = {
    template: `
        <div v-show:show style="
            width:100%;
            height:100%;
            position:fixed;
            top:0;
            left:0;
            z-index: 11;
        ">
            <div v-on:click="close" style="
                display:flex;
                justify-content:center;
                align-items:center;
                height:100%;
                width:100%;
                background:rgba(0,0,0,0.5);
            ">
                <img [alt]="source" style="max-width:90%;max-height:90%" [src]="source" />
            </div>

        </div>
    `,
    data() {
        return {
            source: '',
            show: false
        }
    },
    methods: {
        open(source) {
            this.updateSource(source)
            this.show = true
        },
        close() {
            this.show = false
        },
        updateSource(source) {
            this.source = source
        }
    }
}

class DocumentUtility {
    static getElements(selector, doc = document) {
        const thumbs = []
        const pics = doc.querySelectorAll(selector) || []
        pics.forEach(thumb => {
            thumbs.push(thumb)
        })
        return thumbs
    }

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

class Renderer {
    render() {
        throw new Error('Not implemented')
    }

    disposeRenderError(err, name) {
        logger.error(err || 'Unknown error')
        logger.error('Fail to render extension of ' + name)
    }
}

class PictureRenderer extends Renderer {
    constructor() {
        super()
        this.objectMetaNode = document.querySelector('.object-meta')
        this.pictureNode = document.querySelector('.the-picture>a')
    }

    render() {
        if (this.objectMetaNode && this.pictureNode) {
            try {
                this.renderThePictureExtension()
            } catch (err) {
                this.disposeRenderError(err, 'picture')
            }
        }
    }

    renderThePictureExtension() {
        MVVMComponent.appendChild( createPictureDownload(this.pictureNode.href),this.objectMetaNode)
    }
}

class AutoloadListRenderer extends Renderer {
    constructor(listNode, callback) {
        super()
        this.listNode = listNode
        this.nextURL = this._getNextPageURL()
        this.callback = callback
        this.scrollingElement = document.scrollingElement
    }

    render() {
        if (this.listNode) {
            try {
                this.renderAutoloadListExtension()
            } catch (err) {
                this.disposeRenderError(err, 'autoloading list')
            }
        }
    }

    renderAutoloadListExtension() {
        let wait = false
        const complete = (status) => {
            wait = false
            globalState.autoloadStatus = status
        }

        if (!this.nextURL) {
            complete('empty')
            return
        }

        this._mountAutoloadMessageBox()
        window.onscroll = (e) => {
            const bottom = this.listNode.offsetTop + this.listNode.clientHeight + 50
            const top = this.scrollingElement.scrollTop + window.screen.height
            if (top < bottom || wait || !this.nextURL) return
            wait = true
            globalState.autoloadStatus = 'loading'
            GM_xmlhttpRequest({
                url: this.nextURL,
                responseType: 'document',
                timeout: TIME_OUT,
                onload: (data) => {
                    const doc = data.response
                    if (!doc) {
                        complete('error')
                        return
                    }

                    this.callback(doc)
                    this.nextURL = this._getNextPageURL(doc)
                    if (!this.nextURL) {
                        complete('empty')
                        return
                    }

                    complete('downloaded')
                },
                onerror: () => complete('error'),
                ontimeout: () => complete('timeout')
            })
        }
    }

    _getNextPageURL(doc = document) {
        const nextAnchor = doc.querySelector('.nav-next.nav-end')
        return nextAnchor ? nextAnchor.href : ''
    }

    _mountAutoloadMessageBox() {
        const countPages = document.querySelector('.listing-count-pages')
        if (countPages) {
            this.listNode.parentNode.insertBefore(countPages.cloneNode(true), this.listNode)
            MVVMComponent.mount(AutoloadMessageBox, countPages)
        }
    }
}

class PreviewRenderer extends Renderer {
    constructor() {
        super()
        this.body = document.body
    }

    render() {
        if (this.body) {
            try {
                this.renderPreviewExtension()
            } catch (err) {
                this.disposeRenderError(err, 'preview')
            }
        }
    }

    renderPreviewExtension() {
        this.vm = MVVMComponent.appendChild(Preview, this.body)
    }
}

class PicturesRenderer extends Renderer {
    constructor() {
        super()
        this.pictureNodes = DocumentUtility.getElements('.picture-icon.tbx-tooltip')
        this.autoloadList = new AutoloadListRenderer(document.querySelector('.listing-item'), (doc) => {
            const nextPictureNodes = DocumentUtility.getElements('.picture-icon.tbx-tooltip', doc)
            this._rewritePictureNodes(nextPictureNodes)
        })
        this.preview = new PreviewRenderer()
        this.tasks = []
        this._schedule = false
    }

    render() {
        if (this.pictureNodes.length > 0) {
            try {
                this.renderPicturesExtension()
            } catch (err) {
                this.disposeRenderError(err, 'pictures')
            }
        }
    }

    renderPicturesExtension() {
        this.preview.render()
        this.picturesParent = this.pictureNodes[0].parentNode
        this._initPicturesParent()
        this._rewritePictureNodes(this.pictureNodes)
        this.autoloadList.render()
    }

    _initPicturesParent() {
        this.picturesParent.innerHTML = ''
        this.picturesParent.style.setProperty('display', 'flex')
        this.picturesParent.style.setProperty('flex-direction', 'row')
        this.picturesParent.style.setProperty('flex-wrap', 'wrap')
        this.picturesParent.style.setProperty('justify-content', 'center')
        this.picturesParent.style.setProperty('align-items', 'center')
        const div = document.createElement('div')
        div.style.padding = '10px'
        this.picturesParent.parentNode.insertBefore(div, this.picturesParent)
        MVVMComponent.appendChild(DownloadSequence, div)
        this._renderDownloadState()
    }

    _renderDownloadState() {
        const div = document.createElement('div')
        div.style.position = 'fixed'
        div.style.bottom = 0
        div.style.right = 0
        div.style.width = '100%'
        div.style.backgroundColor = 'white'
        document.body.appendChild(div)
        MVVMComponent.appendChild(DownloadState, div)
    }

    _rewritePictureNodes(pictures) {
        pictures.forEach(picture_node => {
            this.tasks.push(() => {
                MVVMComponent.appendChild(this._createPicturePreview(picture_node), this.picturesParent)
            })
        })
        this._scheduler()
    }

    _scheduler() {
        if (this._schedule) return
        this._schedule = true
        const scheduler = () => {
            if (this.tasks.length === 0) {
                this._schedule = false
                return
            }
            this.tasks.shift()()
            requestAnimationFrame(scheduler)
        }
        requestAnimationFrame(scheduler)
    }

    _createPicturePreview(pictureNode) {
        return createPicturePreview(this._getImageURLs(pictureNode), this.preview.vm)
    }

    _getImageURLs(picture_node) {
        const picture = picture_node.querySelector('a').href
        const viewport = picture_node.querySelector('.viewport')
        const thumb = viewport.style.background.split('"')[1]
        const origin = FILTER_URLS.includes(thumb) ? thumb : this._parseOriginalImageURL(thumb)
        return { thumb, origin, picture }
    }

    _parseOriginalImageURL(thumb_url) {
        let url = 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
    }
}

class DiaporamasRenderer extends Renderer {
    constructor() {
        super()
        this.diaporamaNodes = DocumentUtility.getElements('.diaporama')
    }

    render() {
        if (this.diaporamaNodes.length > 0) {
            try {
                this.renderDiaporamasExtension()
            } catch (err) {
                this.disposeRenderError(err, 'diaporamas')
            }
        }
    }

    renderDiaporamasExtension() {
        this._updateDiaporamaNodes()
    }

    _updateDiaporamaNodes() {
        this.diaporamaNodes.forEach(diaporamaNode => {
            const a = diaporamaNode.querySelector('a')
            const itemId = a.href.split('/item/')[1]
            MVVMComponent.appendChild(createItemTips(itemId), a)
        })
    }
}

class ItemRenderer extends Renderer {
    constructor() {
        super()
        this.itemNode = document.querySelector('.split-left.righter')
    }

    render() {
        if (this.itemNode) {
            try {
                this.renderItemExtension()
            } catch (err) {
                this.disposeRenderError(err, 'item')
            }
        }
    }

    renderItemExtension() {
        this._moveRelatedItems()
        this._initItemNode()
        MVVMComponent.appendChild(createItemTips(LOCATION_ITEM_ID), this.itemNode)
        this._overwritePicturesNavigatingParameters()
    }

    _moveRelatedItems() {
        DocumentUtility.insertAfter(document.querySelector('.tbx-target-ITEMS'), document.querySelector('.tbx-target-LISTS'))
    }

    _initItemNode() {
        this.itemNode.style.display = 'flex'
        this.itemNode.style.flexDirection = 'column'
        this.itemNode.style.justifyContent = 'center'
        this.itemNode.style.alignItems = 'center'
    }

    _overwritePicturesNavigatingParameters() {
        const navigator = document.querySelector('.icon.icon-camera+a.count')
        navigator.href = `/pictures.php?itemId=${LOCATION_ITEM_ID}&sort=date&order=asc`
    }
}

class ExtensionRenderer extends Renderer {
    static renderer = new ExtensionRenderer()
    static rendered = false

    static render() {
        if (!this.rendered) {
            this.rendered = true
            this.renderer.render()
        }
    }

    constructor() {
        super()
        this.item = new ItemRenderer()
        this.picture = new PictureRenderer()
        this.pictures = new PicturesRenderer()
        this.diaporamas = new DiaporamasRenderer()
    }

    render() {
        this.item.render()
        this.picture.render()
        this.pictures.render()
        this.diaporamas.render()
    }

}

ExtensionRenderer.render()