mini mvvm

自用,bug较多,if和for指令不能使用

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

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/444466/1047821/mini%20mvvm.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

作者
ayan0312
版本
0.0.2
建立日期
2022-05-04
更新日期
2022-05-07
尺寸
23.6 KB
授權條款
未知

指令

<div v-on:click="myClick"></div>
<div v-style="myStyle"></div>
<div v-show="display"></div>
<div v-show:display></div>
<div v-class="myClass"></div>
<div v-model="myModel"></div>
<div v-ref="myRef"></div>
<div v-text="myText"></div>
<div v-html="myHTML"></div>

全局

const state=observe({
  count:GM_getValue('count',1),
})

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

const Counter = {
  template: `
    <button v-on:click="decrease">-</button>
    <span>{{global.count}}</span>
    <button v-on:click="increase">+</button>
  `,
  data(){
    return {
      global:state,
    }
  },
  methods:{
    increase(){
      this.global.count+=1
    },
    decrease(){
      this.global.count-=1
    },
  }
}

用例

// ==UserScript==
// @name        MyFigureCollection: 图片下载器
// @name:en     MyFigureCollection: Image Downloader
// @name:zh-CN  MyFigureCollection: 图片下载器
// @name:zh-TW  MyFigureCollection: 图片下载器
// @description 下载原始图片,相册下拉自动加载
// @description:en The original fullsize images downloader. Album pull down to autoload.
// @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=1047420
// @license     GPL-3.0
// @compatible  Chrome
// @version     1.5.9
// @author      ayan0312
// ==/UserScript==
const TIME_OUT = 30 * 1000
const 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 end = (status) => {
        downloadButton.downloadStatus = status
        GM_setValue(picture.split('&')[0], { origin, group, count, downloadStatus: status })
    }
    const [originName,fileType] = origin.split('/').pop().split('.')
    let name = `${group}_${count}_${originName}`

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

    downloadImage({
        url: origin,
        name,
    })
        .then(() => {
            end('downloaded')
        })
        .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,
        }
    },
    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
            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

            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">
      <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 createPictrueComponent = ({ thumb, origin, picture }) => {
    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>Group:</span>
                  <span >{{group}}</span>
                  <span>Item:</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,
                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'
                }
            }
        },
        computed: {
            src() {
                return this.originalImage ? this.origin : this.thumb
            },
            msg() {
                return this.originalImage ? 'Close Preview' : 'Preview'
            },
            containerStyle() {
                const borderStyle = this.requestBorderStyle[this.downloadStatus]
                return Object.assign({}, borderStyle, this.commonStyle, this.originalImage ? {
                    width: '100%'
                } : {})
            },
            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)
            },
            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: 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,
                            })
                        }
                    })
                })
            },
            openPicturePage() {
                window.open(picture)
            }
        }
    }
}

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)
            }
        }
    }
}

class DocumentUtility {
    static getElements(doc, selector) {
        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() {
        const div = document.createElement('div')
        this.objectMetaNode.appendChild(div)
        mountComponent(div, createPictureDownload(this.pictureNode.href))
    }
}

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
                    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)
            mountComponent(countPages, AutoloadMessageBox)
        }
    }
}

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

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

    renderPicturesExtension() {
        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 div1 = document.createElement('div')
        const div2 = document.createElement('div')
        div1.appendChild(div2)
        div1.style.padding = '10px'
        this.picturesParent.parentNode.insertBefore(div1, this.picturesParent)
        mountComponent(div2, DownloadSequence)
        this._renderDownloadState()
    }

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

    _rewritePictureNodes(pictures) {
        pictures.forEach(picture_node => {
            this._mountPictureComponent(this._createPictureComponent(picture_node))
        })
    }

    _createPictureComponent(pictureNode) {
        return createPictrueComponent(this._getImageURLs(pictureNode))
    }

    _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
    }

    _mountPictureComponent(component) {
        const div = document.createElement('div')
        this.picturesParent.appendChild(div)
        return mountComponent(div, component)
    }
}

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

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

    renderDiaporamasExtension() {
        this._updateDiaporamaNodes()
    }

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

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()
        this._mountPicturesNavigator()
        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'
    }

    _mountPicturesNavigator() {
        const div = document.createElement('div')
        this.itemNode.appendChild(div)
        mountComponent(div, createItemTips(LOCATION_ITEM_ID))
    }

    _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()