Multi Transfer Artifacts

Мультипередача артефактов

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name           Multi Transfer Artifacts
// @author         Neleus
// @namespace      Neleus
// @description    Мультипередача артефактов
// @version        1.3
// @match          https://www.heroeswm.ru/inventory.php*
// @match          https://mirror.heroeswm.ru/inventory.php*
// @match          https://lordswm.com/inventory.php*
// @match          https://my.lordswm.com/inventory.php*
// @grant          none
// @license        GNU GPLv3
// @run-at         document-end
// ==/UserScript==

;(function () {
  "use strict"

  // ==================== CONSTANTS ====================
  const DATA = document.documentElement.innerHTML

  // Find player ID
  let userID = null

  // Desktop: pl_hunter_stat link
  const idMatch = /pl_hunter_stat\.php\?id=(\d+)/.exec(DATA)
  if (idMatch) userID = idMatch[1]

  // Mobile fallback: cookie
  if (!userID) {
    const cookieMatch = document.cookie.match(/pl_id=(\d+)/)
    if (cookieMatch) userID = cookieMatch[1]
  }

  if (!userID) return

  const STORAGE_KEY = userID + "_translist"

  let IMG_LINK = "https://dcdn.heroeswm.ru/i/"
  if (/lordswm/.test(location.origin)) {
    IMG_LINK = "https://cfcdn.lordswm.com/i/"
  } else if (/mirror/.test(location.origin)) {
    IMG_LINK = "https://qcdn.heroeswm.ru/i/"
  }

  // ==================== STYLES ====================
  const styles = `
    .mtrans-container {
      width: 100%;
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 5px;
      box-sizing: border-box;
    }
    .mtrans-btn-anim {
      border-radius: 5px;
      animation: .5s linear infinite alternate mtrans-btn-anim;
    }
    @keyframes mtrans-btn-anim {
      from { outline: 4px dashed #aac7f500; }
      to { outline: 4px dashed #aac7f5ff; }
    }
    .mtrans-footer input, .mtrans-footer select { margin: 5px 0; }
    #btn_transfer { margin-top: 10px; padding: 4px; width: 100%; }
    .mtrans-header { width: 235px; text-align: center; }
    #art-name { height: 30px; font-weight: bold; word-wrap: break-word; font-size: 12px; }
    .mtrans-arts {
      width: 455px;
      height: 192px;
      overflow-y: scroll;
      padding: 2px;
      border: 1px dashed #aaa;
      box-sizing: border-box;
    }
    .mtrans-pool { font-size: 9pt; border: 1px dashed #aaa; }
    .mtrans-poolarts { padding: 0 4px; height: 156px; overflow-y: scroll; }
    .pool-element { display: grid; grid-template-columns: 1fr 45px 50px; }
    .pool-element > div { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .pool-header { text-align: center; padding: 2px; background: #dde; }
    .pool-selected { outline: 1px dotted #aaa; }
    .mtrans-item { display: inline-block; margin: 2px !important; }
    .mtrans-selected { outline: 1px solid #000; }
    .mtrans-dur { position: absolute; font-size: 90%; top: 1px; left: 1px; z-index: 2; background: #fff8; pointer-events: none; }
    .dur-warn { color: #fff; background: #f008; }
    .ppb-warn { border: 2px solid #f00; }
    .mtrans-chk { position: absolute; top: 1px; right: 1px; z-index: 2; }
    .mtrans-pbar { width: 100%; display: none; }
    .mtrans-badge { outline: 3px dashed #ACE; }
    .no-events { pointer-events: none; }
    #inv_menu_mtrans { display: none; }
    #inv_menu_mtrans img { filter: hue-rotate(90deg) saturate(2); }
    .thin-scrollbar::-webkit-scrollbar { width: 7px; }
    .thin-scrollbar::-webkit-scrollbar-thumb { background: #fff; border-radius: 5px; }
    .thin-scrollbar::-webkit-scrollbar-track { background: #5554; }

    @media (max-width: 600px) {
      .mtrans-container { grid-template-columns: 1fr; padding: 5px; }
      .mtrans-header { width: 100%; margin-bottom: 10px; }
      .mtrans-arts { width: 100%; height: 150px; }
      .mtrans-footer { width: 100%; }
      .mtrans-footer input[type="text"] { width: 50px !important; }
      #renter { width: 100% !important; max-width: 200px !important; }
      .mtrans-pool { width: 100%; }
      .pool-element { grid-template-columns: 1fr 40px 45px; font-size: 8pt; }
      .mtrans-poolarts { height: 120px; }
      #art-name { font-size: 11px; height: auto; min-height: 30px; }
    }
  `

  const styleElement = document.createElement("style")
  styleElement.textContent = styles
  document.head.appendChild(styleElement)

  // ==================== UTILITY FUNCTIONS ====================
  const $ = (selector, context = document) => context.querySelector(selector)

  const loadPage = async (url) => {
    const response = await fetch(url)
    const buffer = await response.arrayBuffer()
    return new TextDecoder("windows-1251").decode(buffer)
  }

  const getStorage = (key, defaultValue = null) => {
    try {
      const stored = localStorage.getItem(key)
      return stored ? JSON.parse(stored) : defaultValue
    } catch {
      return defaultValue
    }
  }

  const setStorage = (key, value) => {
    try {
      localStorage.setItem(key, JSON.stringify(value))
    } catch {}
  }

  const getTranslist = () => getStorage(STORAGE_KEY, {})
  const saveTranslist = (list) => setStorage(STORAGE_KEY, list)

  // ==================== ARTS EXTRACTION ====================
  // С версии движка инвентаря 50.x данные больше не лежат в глобальной
  // переменной `arts` (она спрятана в замыкании модуля HWM.Inventory).
  // Они приходят в вызове HWM.Inventory.bootstrap({...}) и декодируются
  // по карте имён art_fields — читаем их прямо из inline-скрипта.
  let INV_ARTS_OBJ = {}
  let SIGN = ""

  // Вырезает сбалансированный {...} начиная с позиции start, корректно
  // пропуская фигурные скобки внутри строковых литералов JSON.
  const sliceBalanced = (str, start) => {
    if (start < 0) return null
    let depth = 0, inStr = false, esc = false
    for (let i = start; i < str.length; i++) {
      const c = str[i]
      if (inStr) {
        if (esc) esc = false
        else if (c === "\\") esc = true
        else if (c === '"') inStr = false
      } else if (c === '"') inStr = true
      else if (c === "{") depth++
      else if (c === "}" && --depth === 0) return str.slice(start, i + 1)
    }
    return null
  }

  // Повторяет логику inv_decode_art_rows из inventory.js: позиционные поля
  // row[j] -> fields[j], затем «разреженный» хвост {"_":1,"<idx>":value,...}.
  const decodeArtRows = (rows, fields) => {
    if (!rows) return []
    if (!fields || !fields.length) return rows
    return rows.map((row) => {
      if (!Array.isArray(row)) return row
      const art = {}
      let len = row.length
      const tail = len ? row[len - 1] : null
      let sparse = null
      if (tail && typeof tail === "object" && !Array.isArray(tail) && tail["_"]) {
        sparse = tail
        len--
      }
      for (let j = 0; j < len && j < fields.length; j++) art[fields[j]] = row[j]
      if (sparse) {
        for (const key in sparse) {
          if (key === "_") continue
          const fi = parseInt(key, 10)
          if (fi >= 0 && fi < fields.length) art[fields[fi]] = sparse[key]
        }
      }
      return art
    })
  }

  const extractInventory = () => {
    let raw = ""
    for (const s of document.scripts) {
      if (s.textContent.includes("HWM.Inventory.bootstrap(")) { raw = s.textContent; break }
    }
    if (!raw) raw = DATA
    const m = /HWM\.Inventory\.bootstrap\(/.exec(raw)
    if (!m) return { arts: {}, sign: "" }
    const json = sliceBalanced(raw, raw.indexOf("{", m.index + m[0].length))
    if (!json) return { arts: {}, sign: "" }
    let data
    try { data = JSON.parse(json) } catch { return { arts: {}, sign: "" } }
    const decoded = decodeArtRows(data.arts || [], data.art_fields || [])
    const obj = {}
    decoded.forEach((art, idx) => { obj[idx] = art })
    return { arts: obj, sign: data.sign || "" }
  }

  // ==================== HELPER FUNCTIONS ====================
  const getFriendsList = async () => {
    try {
      const page = await loadPage("friends.php")
      if (typeof page !== "string") return ""
      return [...page.matchAll(/([\wа-яё\-\(\) ]+) \[/gi)].map(m => `<option value="${m[1]}">${m[1]}</option>`).join("")
    } catch {
      return ""
    }
  }

  const getIndex = (artId) => Object.keys(INV_ARTS_OBJ).find(t => INV_ARTS_OBJ[t].id == artId) || -1

  const parseSuffix = (mods) => {
    if (typeof mods !== "string" || !mods) return ""
    return [...mods.matchAll(/\w\d+/g)].map(m => `<img src="${IMG_LINK}mods_png/24/${m[0]}.png">`).join("")
  }

  const urlencode = (str) => {
    let result = ""
    for (let i = 0; i < str.length; i++) {
      let code = str.charCodeAt(i)
      if (code >= 1040 && code <= 1103) code -= 848
      if (code === 1025) code = 168
      if (code === 1105) code = 184
      result += /[A-Za-z0-9\-_.~]/.test(String.fromCharCode(code))
        ? String.fromCharCode(code)
        : "%" + code.toString(16).toUpperCase().padStart(2, "0")
    }
    return result
  }

  const setMTransferBadges = (translist, container) => {
    document.querySelectorAll(".mtrans-badge").forEach(el => el.classList.remove("mtrans-badge"))
    for (let artId in translist) {
      const idx = getIndex(artId)
      const el = $(`[art_idx="${idx}"]`, container)
      if (el) el.classList.add("mtrans-badge")
    }
  }

  // ==================== POOL FUNCTIONS ====================
  const poolToSession = (poolData) => sessionStorage.setItem("mtrans", JSON.stringify(poolData))

  const checkBattlesCount = (poolData) => {
    for (let artId in poolData.arts) {
      const el = $(`[data-id="${artId}"]`)
      if (!el) continue
      const durDiv = el.firstChild
      const warn = poolData.arts[artId].dur1 < poolData.battles && poolData.selected.includes(artId)
      durDiv.classList.toggle("dur-warn", warn)
    }
  }

  const checkPPB = (poolData, artId) => {
    const ppbInput = $("#ppb")
    if (!ppbInput || !poolData.arts[artId]) return
    const warn = poolData.battles > 0 && poolData.arts[artId].ppb === 0 && poolData.selected.includes(artId)
    ppbInput.classList.toggle("ppb-warn", warn)
  }

  const checkTransEnable = (poolData, button) => {
    const hasZeroPPB = poolData.battles > 0 && poolData.selected.some(id => poolData.arts[id]?.ppb === 0)
    button.disabled = poolData.selected.length === 0 || poolData.days === 0 || !poolData.renter || hasZeroPPB
  }

  const showPool = (poolData, selectedItem) => {
    let html = '<div class="pool-element pool-header"><div>Артефакт</div><div>Сумма</div><div>Комиссия</div></div><div class="mtrans-poolarts thin-scrollbar">'
    let num = 1, totalSum = 0, totalComm = 0

    for (let artId of poolData.selected) {
      const art = poolData.arts[artId]
      const sum = art.ppb * Math.min(poolData.battles, art.dur1)
      const comm = sum < 50 && sum > 0 ? 1 : Math.round(sum / 100)
      totalSum += sum
      totalComm += comm
      art.summ = sum
      const isSelected = selectedItem?.dataset.id === artId
      html += `<div class="pool-element${isSelected ? " pool-selected" : ""}"><div>${num++}. ${art.name} [${art.dur1}/${art.dur2}]</div><div>${sum}</div><div>${comm}</div></div>`
    }

    html += "</div>"
    $("#pool").innerHTML = html
    $("#summ").textContent = totalSum
    $("#comm").textContent = totalComm
  }

  const setSelected = (poolData, selectedItem) => {
    const ppbInput = $("#ppb"), artNameDiv = $("#art-name")
    const removeBtn = $("#btn_remove"), saveBtn = $("#btn_save")

    if (!selectedItem) {
      artNameDiv.textContent = "Перетащите артефакты на эту вкладку"
      ppbInput.value = 0
      ppbInput.disabled = removeBtn.disabled = saveBtn.disabled = true
      return
    }

    $(".mtrans-selected")?.classList.remove("mtrans-selected")
    selectedItem.classList.add("mtrans-selected")

    const artId = selectedItem.dataset.id
    const art = poolData.arts[artId]
    checkPPB(poolData, artId)
    artNameDiv.textContent = `${art.name} [${art.dur1}/${art.dur2}]`
    ppbInput.value = art.ppb
    ppbInput.disabled = removeBtn.disabled = saveBtn.disabled = false
  }

  // ==================== TRANSFER EXECUTION ====================
  const transferPool = async (poolData) => {
    const errorDiv = $("#mtrans-error"), progressBar = $(".mtrans-pbar")
    progressBar.style.display = "block"
    errorDiv.innerHTML = "<br>"

    const sign = SIGN
    const step = 100 / poolData.selected.length

    for (let artId of poolData.selected) {
      const response = await fetch("art_transfer.php", {
        method: "POST",
        redirect: "manual",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: `id=${artId}&nick=${urlencode(poolData.renter)}&gold=${poolData.arts[artId].summ}&sendtype=2&dtime=${poolData.days}&bcount=${poolData.battles}&rep_price=0&art_id=&sign=${sign}`,
      })

      if (response.ok) {
        progressBar.style.display = "none"
        const buffer = await response.arrayBuffer()
        const html = new TextDecoder("windows-1251").decode(buffer)
        const doc = new DOMParser().parseFromString(html, "text/html")
        errorDiv.append($("td>font", doc))
        $("#btn_transfer").disabled = false
        return
      }
      progressBar.value += step
    }

    sessionStorage.setItem("redirect", "true")
    location.reload()
  }

  // ==================== MULTI TRANSFER PANEL ====================
  const MTPanel = async (container, friendsOptions, translist) => {
    const poolData = { arts: {}, selected: [], days: 0, hours: 0, battles: 0, renter: "" }

    let html = '<div class="mtrans-container"><div class="mtrans-header"><div id="art-name"></div>'
    html += '<br>Стоимость боя <input id="ppb" type="text" maxlength="4" size="4" placeholder="0">'
    html += "<br><br><button id=btn_save>Сохранить цену</button><br><br><button id=btn_remove>Убрать артефакт</button>"
    html += '</div><div class="mtrans-arts thin-scrollbar">'

    const sortedIds = Object.keys(translist).sort((a, b) => getIndex(a) - getIndex(b))
    for (let artId of sortedIds) {
      const idx = getIndex(artId)
      if (idx === -1 || !INV_ARTS_OBJ[idx] || INV_ARTS_OBJ[idx].dressed || INV_ARTS_OBJ[idx].star_grouped === 1) continue

      const art = INV_ARTS_OBJ[idx]
      poolData.arts[artId] = { name: art.name + (art.suffix || ""), ppb: translist[artId], dur1: art.durability1, dur2: art.durability2 }

      // Берём картинку из уже отрисованного сайтом тайла — там полный путь
      // с подпапками (artifacts/events/...). Запасной вариант — по art_id.
      const realTile = $(`[art_idx="${idx}"]`, container)
      const imgSrc = realTile?.querySelector(".cre_mon_image2")?.getAttribute("src") || `${IMG_LINK}artifacts/${art.art_id}.png`
      const modsHtml = realTile?.querySelector(".art_mods")?.innerHTML || parseSuffix(art.suffix)

      html += `<div class="inventory_item_div mtrans-item" data-id="${artId}" art_idx="${idx}">`
      html += `<div class="mtrans-dur">${art.durability1}/${art.durability2}</div><input type="checkbox" class="mtrans-chk">`
      html += `<img src="${IMG_LINK}art_fon_100x100.png" height="100%">`
      html += `<img src="${imgSrc}" height="100%" class="cre_mon_image2">`
      html += `<div class="art_mods no-events">${modsHtml}</div></div>`
    }

    html += '</div><div class="mtrans-footer">'
    html += `<select style="width:100%" id="friends"><option selected disabled>Выбрать получателя</option>${friendsOptions}</select><br>`
    html += 'Получатель <input id="renter" type="text" style="width:155px"><br>Передать через:<br>'
    html += '<input style="width:42px" id="hours" type="text" maxlength="3" placeholder="0"> час.'
    html += ' <input style="width:42px" id="days" type="text" maxlength="3" placeholder="0"> дн.'
    html += ' <input style="width:24px" id="bcount" type="text" maxlength="2" placeholder="0"> боёв<br>'
    html += 'Стоимость: <span id="summ">0</span> Комиссия: <span id="comm">0</span>'
    html += '<button id="btn_transfer">Передать</button></div>'
    html += '<div id="pool" class="mtrans-pool"></div></div><progress class="mtrans-pbar" max="100" value="0"></progress><div id="mtrans-error"></div>'

    container.innerHTML = html

    const renterInput = $("#renter"), bcountInput = $("#bcount")
    const daysInput = $("#days"), hoursInput = $("#hours"), transferBtn = $("#btn_transfer")
    let currentItem = $(".mtrans-item")

    // Restore session
    const saved = sessionStorage.getItem("mtrans")
    if (saved) {
      const s = JSON.parse(saved)
      poolData.renter = renterInput.value = s.renter || ""
      poolData.battles = s.battles || 0
      poolData.days = s.days || 0
      poolData.hours = s.hours || 0
      bcountInput.value = poolData.battles || ""
      daysInput.value = poolData.days || ""
      hoursInput.value = poolData.hours || ""
      poolData.selected = s.selected?.filter(id => id in poolData.arts) || []
      poolData.selected.forEach(id => {
        const cb = $(`[data-id="${id}"] .mtrans-chk`)
        if (cb) cb.checked = true
      })
    }

    showPool(poolData, currentItem)
    poolToSession(poolData)
    setSelected(poolData, currentItem)
    checkTransEnable(poolData, transferBtn)
    checkBattlesCount(poolData)

    // Events
    transferBtn.onclick = () => { transferBtn.disabled = true; transferPool(poolData) }

    $(".mtrans-arts").onclick = (e) => {
      if (e.target.tagName === "IMG") {
        currentItem = e.target.parentNode
        setSelected(poolData, currentItem)
        showPool(poolData, currentItem)
      }
      if (e.target.tagName === "INPUT") {
        const artId = e.target.parentNode.dataset.id
        if (e.target.checked) poolData.selected.push(artId)
        else poolData.selected = poolData.selected.filter(id => id !== artId)
        currentItem = e.target.parentNode
        setSelected(poolData, currentItem)
        showPool(poolData, currentItem)
        poolToSession(poolData)
        checkTransEnable(poolData, transferBtn)
        checkBattlesCount(poolData)
      }
    }

    $("#btn_save").onclick = () => {
      const artId = currentItem.dataset.id
      poolData.arts[artId].ppb = translist[artId] = +$("#ppb").value
      saveTranslist(translist)
      showPool(poolData, currentItem)
      poolToSession(poolData)
      checkPPB(poolData, artId)
      checkTransEnable(poolData, transferBtn)
    }

    $("#btn_remove").onclick = () => {
      const artId = currentItem.dataset.id
      delete translist[artId]
      delete poolData.arts[artId]
      poolData.selected = poolData.selected.filter(id => id !== artId)
      currentItem.remove()
      saveTranslist(translist)
      currentItem = $(".mtrans-item")
      setSelected(poolData, currentItem)
      showPool(poolData, currentItem)
      poolToSession(poolData)
      checkTransEnable(poolData, transferBtn)
    }

    renterInput.oninput = () => {
      poolData.renter = renterInput.value.trim()
      poolToSession(poolData)
      checkTransEnable(poolData, transferBtn)
    }

    $("#friends").onchange = (e) => {
      poolData.renter = renterInput.value = e.target.value
      poolToSession(poolData)
      checkTransEnable(poolData, transferBtn)
    }

    daysInput.oninput = () => {
      poolData.days = Math.min(365, +daysInput.value) || 0
      poolData.hours = poolData.days * 24
      hoursInput.value = poolData.hours || ""
      poolToSession(poolData)
      checkTransEnable(poolData, transferBtn)
    }

    hoursInput.oninput = () => {
      const hours = +hoursInput.value
      if (!hours || hours < 0.1) return
      poolData.hours = hours
      poolData.days = +(hours / 24).toFixed(3)
      daysInput.value = poolData.days
      poolToSession(poolData)
      checkTransEnable(poolData, transferBtn)
    }

    bcountInput.oninput = () => {
      poolData.battles = +bcountInput.value || 0
      showPool(poolData, currentItem)
      poolToSession(poolData)
      checkTransEnable(poolData, transferBtn)
      checkBattlesCount(poolData)
      if (currentItem) checkPPB(poolData, currentItem.dataset.id)
    }
  }

  // ==================== MOBILE SUPPORT ====================
  const setupMobileSupport = (container) => {
    const invMenu = $("#inv_menu")
    if (!invMenu) return

    const buttonsContainer = $("#inv_item_buttons")
    if (!buttonsContainer || $("#inv_menu_mtrans")) return

    const btn = document.createElement("div")
    btn.className = "inv_item_select"
    btn.id = "inv_menu_mtrans"
    btn.innerHTML = `<img class="inv_item_select_img show_hint" hint="В мультипередачу" src="${IMG_LINK}inv_im/btn_art_transfer.png">`
    buttonsContainer.insertBefore(btn, buttonsContainer.lastElementChild)

    // Новое меню инвентаря закрывается по mouseup на document.body
    // (body_mouse_up). Поэтому вешаем обработчик на mouseup и гасим
    // всплытие, как это делают штатные кнопки меню (inv_menu_button).
    btn.onmouseup = (e) => {
      e.stopPropagation()
      const artIdx = invMenu.getAttribute("art_idx")
      if (!artIdx || !INV_ARTS_OBJ[artIdx]) return

      const artId = INV_ARTS_OBJ[artIdx].id
      if (!INV_ARTS_OBJ[artIdx].transfer_ok) return alert("Этот артефакт нельзя передать")

      const translist = getTranslist()
      if (artId in translist) return alert("Уже в списке мультипередачи")

      translist[artId] = 0
      saveTranslist(translist)
      setMTransferBadges(translist, container)

      const s = document.createElement("script")
      s.textContent = "if(window.HWM&&HWM.Inventory&&HWM.Inventory.menu&&HWM.Inventory.menu.hide){HWM.Inventory.menu.hide()}else if(typeof inv_menu_hide==='function'){inv_menu_hide()}"
      document.body.appendChild(s)
      s.remove()
    }

    const updateVisibility = () => {
      const artIdx = invMenu.getAttribute("art_idx")
      if (!artIdx || artIdx === "-1" || !INV_ARTS_OBJ[artIdx]) {
        btn.style.display = "none"
        return
      }
      const artId = INV_ARTS_OBJ[artIdx].id
      const canTransfer = !!INV_ARTS_OBJ[artIdx].transfer_ok
      btn.style.display = (canTransfer && !(artId in getTranslist())) ? "block" : "none"
    }

    new MutationObserver(updateVisibility).observe(invMenu, { attributes: true, attributeFilter: ["art_idx", "style"] })
  }

  // ==================== MAIN ====================
  const multiTransfer = async (container) => {
    const tabsBlock = $(".filter_tabs_block")
    if (!tabsBlock) return

    const mtransIcon = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2" stroke-linecap="round"><path d="M5 9h14l-4-4M5 15h14l-4 4"/></svg>')}`
    tabsBlock.insertAdjacentHTML("beforeend",
      `<div id="mtrans_btn" hint="mtrans" title="Мультипередача" style="background:url('${mtransIcon}') no-repeat center, #fff; background-size: 20px;" class="filter_tab filter_tab_for_hover"></div>`)

    const mtransBtn = $("#mtrans_btn")
    let translist = getTranslist()
    setMTransferBadges(translist, container)

    // Список друзей грузим в фоне: если friends.php не ответит, обработчики
    // вкладки и перетаскивания всё равно должны навеситься.
    let friendsOptions = ""
    getFriendsList().then(opts => { friendsOptions = opts }).catch(() => {})

    mtransBtn.onclick = () => {
      const active = $(".filter_tab_active")
      if (active !== mtransBtn) {
        active?.classList.replace("filter_tab_active", "filter_tab_for_hover")
        mtransBtn.classList.replace("filter_tab_for_hover", "filter_tab_active")
        $("#return_all_rents")?.style.setProperty("display", "none")
        translist = getTranslist()
        MTPanel(container, friendsOptions, translist)
      }
    }

    let draggedIndex = null, draggedArtId = null

    container.ondragstart = (e) => {
      let target = e.target
      while (target && !(draggedIndex = target.getAttribute("art_idx"))) target = target.parentNode
      if (!draggedIndex || !INV_ARTS_OBJ[draggedIndex]) return
      draggedArtId = INV_ARTS_OBJ[draggedIndex].id
      if (INV_ARTS_OBJ[draggedIndex].transfer_ok && !(draggedArtId in translist)) {
        mtransBtn.classList.add("mtrans-btn-anim")
      }
    }

    mtransBtn.ondragover = (e) => {
      if (draggedIndex && INV_ARTS_OBJ[draggedIndex]?.transfer_ok && !(draggedArtId in translist)) {
        e.preventDefault()
      }
    }

    document.ondragend = () => mtransBtn.classList.remove("mtrans-btn-anim")
    tabsBlock.ondrop = () => mtransBtn.classList.remove("mtrans-btn-anim")

    mtransBtn.ondrop = () => {
      if (draggedArtId && !(draggedArtId in translist)) {
        translist[draggedArtId] = 0
        saveTranslist(translist)
        setMTransferBadges(translist, container)
      }
    }

    if (sessionStorage.getItem("redirect")) {
      sessionStorage.removeItem("redirect")
      mtransBtn.click()
    }
  }

  // ==================== INIT ====================
  const init = async () => {
    if (document.readyState !== "complete") {
      await new Promise(r => window.addEventListener("load", r, { once: true }))
    }

    const inv = extractInventory()
    INV_ARTS_OBJ = inv.arts
    SIGN = inv.sign
    if (!Object.keys(INV_ARTS_OBJ).length) return

    const container = $("#inventory_block") || $(".inventory_items_block") || document.body

    setupMobileSupport(container)
    await multiTransfer(container)

    const invContainer = $("#inventory_block") || $(".inventory_items_block")
    if (invContainer) {
      new MutationObserver(() => {
        if ($(".filter_tab_active")?.getAttribute("hint") !== "mtrans") {
          setMTransferBadges(getTranslist(), invContainer)
        }
      }).observe(invContainer, { childList: true, subtree: true })
    }
  }

  init()
})()