Единый форум поддержки

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » Единый форум поддержки » Новые возможности форумов » Скрипты от пользователей 3


Скрипты от пользователей 3

Сообщений 581 страница 586 из 586

Перевести1

Администрация MyBB не гарантирует работоспособность данных скриптов, вы ставите их на свой страх и риск!

Авторы скриптов, размещающие свои разработки на бесплатной и добровольной основе, оставляют за собой право в любой момент вносить в них изменения на своё усмотрение (в том числе: обновление с целью улучшения функционала и стабильности; изъятие из публичного пользования; включение рекомендательных интеграций, содержащих информацию о других разработках, относящихся к MyBB). Скрипты предоставляются авторами "как есть" и без каких-либо гарантий, высказанных или подразумеваемых. При этом размещаемый скрипт должен соответствовать условиям Пользовательского соглашения, не содержать вредоносного кода и не использоваться для рекламы и продвижения сторонних ресурсов, не относящихся к MyBB.

Пользователь, устанавливающий скрипт к себе на форум, имеет право сохранить копию скрипта, в том числе в файлы форума (если в описании к скрипту не указано иное) и использовать эту копию вместо оригинального файла. Однако следует понимать, что такая копия будет лишена каких-либо обновлений и может перестать работать вследствие изменений на сервисе. По этой причине рекомендуется использовать оригинальные версии скриптов, за исключением случаев крайней необходимости.

Данные правила относятся ко всем свободно распространяемым скриптам, размещаемым как в этой теме, так и в других темах раздела Новые возможности форумов.

Для тех кто выкладывает скрипты:
1) Дайте краткое описание скрипта, не будет лишним наглядно показать работу скрипта на скриншоте.
2) Обязательно укажите информацию по его установке: куда вставлять и как использовать.
3) Сам скрипт вставьте в bb-коды [сode] [/сode] или в [quotе] [/quotе]
4) Если при создании кода Вы используете нейросетевые технологий, пожалуйста, указывайте это на видном месте. В противном случае, будьте готовы отвечать за возможные ошибки или нагрузки на сервера сервиса, вызванные Вашим скриптом. Также будьте готовы к вопросам от пользователей по работоспособности скрипта.
5) Не перегружайте описание скрипта избыточной информацией, не имеющей к нему прямого отношения.

Администрация сервиса имеет право отредактировать ваше сообщение в соответствие с общими стандартами публикаций скриптов в этой теме.

В данной теме запрещено задавать вопросы и просить скрипты!
Для запросов по созданию новых скриптов есть актуальная тема Запросы по скриптам
При нарушение данного правила будет выдаваться предупреждение в профиль!

Если у вас есть вопросы по работе какого-либо скрипта, опубликованного в этой теме, пожалуйста, пишите свой вопрос в Общие вопросы от новичков (63) #3, ссылаясь на сообщение из этой темы.

Отредактировано Alex_63 (Пт, 14 Мар 2025 11:28:27)

+8

Перевести581

🖼️ Аватарка в цитате

Есть скрипт от Alex_63, он в  общем нормально работает: 

А вот скрипт, который для пользователей форума - по виду аналогичный, но делает это немного по-другому.


Итак, что такое  "аватарка в цитате"? Это вот такая красивая штука:

https://upforme.ru/uploads/0000/14/1c/38891/182673.webp

🔀Как подключить к себе на сайт (самый простой вариант): вставить следующие строчки в HTML верх или  в HTML низ (в данном случае нет разницы).

<!-- Аватарка в цитате -->
<script src="https://forumstatic.ru/files/001a/f0/7d/79702.js?v=49"></script>
<!-- Аватарка в цитате -->

Всё сразу прекрасно работает, Вы молодец :)

Для тех, кто хочет как-то поменять дизайн или добавить какой-то функционал, вот исходный код.

Исходный код "Аватарка в цитате"
Код:
(function () {
  "use strict";

  if (window.quoteAvatarInitialized) {
    console.warn('[QuoteAvatar] Already initialized. Skipping.');
    return;
  }
  window.quoteAvatarInitialized = true;

  const QV_CONFIG = {
    quoteImage: 'https://forumstatic.ru/files/0015/ec/20/41408.png',
    defaultAvatar: 'https://forumstatic.ru/files/0000/14/1c/20038.jpg',
    cacheKey: 'QuoteAvatarCache'
  };

  // === Стили ===
  (function injectStyles() {
    if (document.getElementById('quote-avatar-styles')) return;
    const style = document.createElement('style');
    style.id = 'quote-avatar-styles';
    style.textContent = `
      .quote-box.with-avatar { padding-left: 50px !important; }
      .author-avatar + span { margin-left: 0; }
      .author-avatar { float: left; width: 39px; margin-left: -40px; margin-top: 0; }
      .author-avatar > img { width: 28px; padding: 2px; border: 1px solid #c4cad4; border-radius: 4px; }
    `;
    document.head.appendChild(style);
  })();

  // === Получение имени по postId ===
  function getAuthorNameByPostId(postId) {
    const post = document.getElementById(`p${postId}`);
    if (post) {
      const authorEl = post.querySelector('.pa-author');
      if (authorEl) {
        const nodes = Array.from(authorEl.childNodes);
        for (let i = nodes.length - 1; i >= 0; i--) {
          if (nodes[i].nodeType === Node.TEXT_NODE) {
            const t = nodes[i].textContent.trim();
            if (t) return t;
          }
        }
        return authorEl.textContent.replace(/^Автор:\s*/i, '').trim() || 'Гость';
      }
    }
    return 'Гость';
  }

  function enhanceQuoteButtons() {
    document.querySelectorAll('.post[id]').forEach(post => {
      const quoteLink = post.querySelector('.pl-quote > a');
      if (!quoteLink || quoteLink.classList.contains('with-ava')) return;
      const postIdMatch = post.id?.match(/^p(\d+)$/);
      if (!postIdMatch) return;
      const postId = postIdMatch[1];
      const userId = post.dataset.userId;
      if (!userId) return;
      const qHref = quoteLink.getAttribute('href');
      if (qHref) {
        const newHref = qHref.replace(/quote\('([^']*)',\s*(\d+)\)/, `quote('${postId},${userId}', $2)`);
        quoteLink.setAttribute('href', newHref);
        quoteLink.classList.add('with-ava');
      }
    });
  }

  function setLinkAndAva(selector = 'body') {
    const context = typeof selector === 'string' ? document.querySelector(selector) || document : selector;
    const boxes = Array.from(context.querySelectorAll('.quote-box:not(.with-avatar)'));
    if (boxes.length === 0) return;

    boxes.forEach(box => {
      const cite = box.querySelector(':scope > cite');
      if (!cite) return;
      let text = cite.textContent.trim();
      if (text === 'HTML+ написал(а):' || text === 'HTML написал(а):') return;
      if (/^Скрытый\sтекст/.test(text)) return;

      // --- Случай 1: "Сообщение от 12345,678" ---
      const match = text.match(/(?:Сообщение\s+от\s+)?(\d+)\s*,\s*(\d+)/i);
      if (match) {
        const postId = match[1];
        const userId = match[2];
        if (/^\d+$/.test(postId) && /^\d+$/.test(userId)) {
          const username = getAuthorNameByPostId(postId);
          const href = document.querySelector(`#p${postId}.post`)
            ? `#p${postId}`
            : `/viewtopic.php?pid=${postId}#p${postId}`;

          const avatarEl = document.createElement('span');
          avatarEl.className = 'author-avatar';
          avatarEl.innerHTML = `<img src="${QV_CONFIG.defaultAvatar}" alt="${username}" title="${username}" loading="lazy"/>`;

          const nameEl = document.createElement('span');
          nameEl.innerHTML = `<span class="qcn">Сообщение от</span> <span class="qc-uname">${username}</span>`;

          const linkA = document.createElement('a');
          linkA.href = href;
          linkA.className = 'qc-post-link';
          linkA.innerHTML = `<img src="${QV_CONFIG.quoteImage}" title="Перейти к сообщению"/>`;

          const linkSpan = document.createElement('span');
          linkSpan.className = 'qc-post-link';
          linkSpan.appendChild(linkA);

          cite.innerHTML = '';
          cite.appendChild(avatarEl);
          cite.appendChild(nameEl);
          cite.appendChild(document.createTextNode('\u00A0')); //  
          cite.appendChild(linkSpan);

          box.classList.add('with-avatar');
          box.id = `u${userId}`;
          cite.id = `p${postId}`;
          return;
        }
      }

      // --- Случай 2: старые форматы ---
      let avatar = QV_CONFIG.defaultAvatar;
      let username = 'Гость';
      let link = '#';
      let hasLink = false;

      if (text.includes('http://')) {
        const match2 = text.match(/^(.*?),http:\/\/(.*?)\sнаписал/mi);
        if (match2) {
          username = match2[1];
          const urlPart = match2[2];
          const [url, avatarPart] = urlPart.includes('|') ? urlPart.split('|', 2) : [urlPart, ''];
          link = `http://${url}`;
          if (avatarPart) avatar = `/img/avatars/${avatarPart}`;
          hasLink = true;
        }
      } else if (text.includes('#p')) {
        const match2 = text.match(/^#p(\d+),(.*?)\sнаписал/mi);
        if (match2) {
          const postId = match2[1];
          const userPart = match2[2];
          const [name, avatarPart] = userPart.includes('|') ? userPart.split('|', 2) : [userPart, ''];
          username = name;
          link = `#p${postId}`;
          if (!document.querySelector(`#p${postId}.post`)) link = `/viewtopic.php?pid=${postId}#p${postId}`;
          if (avatarPart) avatar = `/img/avatars/${avatarPart}`;
          hasLink = true;
        }
      } else {
        username = text.replace(/\sнаписал.*$/i, '').trim();
        if (username.includes(',undefined')) username = 'Гость';
      }

      if (!box.classList.contains('with-avatar')) {
        const avatarEl = document.createElement('span');
        avatarEl.className = 'author-avatar';
        avatarEl.innerHTML = `<img src="${avatar}" alt="${username}" title="${username}" loading="lazy"/>`;

        const nameEl = document.createElement('span');
        nameEl.innerHTML = `<span class="qcn">Сообщение от</span> <span class="qc-uname">${username}</span>`;

        cite.innerHTML = '';
        cite.appendChild(avatarEl);
        cite.appendChild(nameEl);

        if (hasLink) {
          const linkA = document.createElement('a');
          linkA.href = link;
          linkA.className = 'qc-post-link';
          linkA.innerHTML = `<img src="${QV_CONFIG.quoteImage}" title="Перейти к сообщению"/>`;

          const linkSpan = document.createElement('span');
          linkSpan.className = 'qc-post-link';
          linkSpan.appendChild(linkA);

          cite.appendChild(document.createTextNode('\u00A0'));
          cite.appendChild(linkSpan);
        }

        box.classList.add('with-avatar');

        if (document.getElementById('pun-messages')) {
          box.classList.remove('with-avatar');
          const avatarSpan = cite.querySelector('.author-avatar');
          if (avatarSpan) avatarSpan.remove();
        }
      }
    });

    // === API-улучшение ===
    const userIDs = [...new Set(
      Array.from(document.querySelectorAll('.quote-box[id^="u"]'))
        .map(box => box.id.substring(1))
        .filter(id => /^\d+$/.test(id))
    )];

    if (userIDs.length === 0) {
      document.getElementById('Hide_qCite')?.remove();
      return;
    }

    // Обновляем существующие элементы без перезаписи ссылок
    function parseQuote(user_id, ava, username) {
      document.querySelectorAll(`.quote-box[id="u${user_id}"]`).forEach(box => {
        const cite = box.querySelector(':scope > cite');
        if (!cite) return;

        const avatarImg = cite.querySelector('.author-avatar img');
        const usernameEl = cite.querySelector('.qc-uname');

        if (avatarImg && ava) {
          avatarImg.src = ava;
          avatarImg.alt = avatarImg.title = username || usernameEl?.textContent || 'Гость';
        }

        if (usernameEl && username) {
          usernameEl.textContent = username;
          if (avatarImg) {
            avatarImg.alt = avatarImg.title = username;
          }
        }

        // Обновляем иконку в ссылке, если она есть
        const linkImg = cite.querySelector('.qc-post-link img');
        if (linkImg) {
          linkImg.src = QV_CONFIG.quoteImage;
          linkImg.title = "Перейти к сообщению";
        }
      });
    }

    // === Кэшированный API ===
    function getAPIdataFor(ids) {
      let cache = {};
      try {
        cache = JSON.parse(sessionStorage.getItem(QV_CONFIG.cacheKey) || '{}');
      } catch (e) {
        cache = {};
      }

      const stillMissing = [];
      ids.forEach(uid => {
        if (cache[uid]) {
          const { avatar, username } = cache[uid];
          parseQuote(uid, avatar, username);
        } else {
          stillMissing.push(uid);
        }
      });

      if (stillMissing.length === 0) {
        document.getElementById('Hide_qCite')?.remove();
        return;
      }

      const url = `/api.php?method=users.get&user_id=${encodeURIComponent(stillMissing.join(','))}&fields=user_id,username,avatar`;
      fetch(url)
        .then(res => res.json())
        .then(data => {
          document.getElementById('Hide_qCite')?.remove();
          const users = data?.response?.users || {};
          let curCache = {};
          try {
            curCache = JSON.parse(sessionStorage.getItem(QV_CONFIG.cacheKey) || '{}');
          } catch (e) {
            curCache = {};
          }

          for (const id in users) {
            const user = users[id];
            parseQuote(user.user_id, user.avatar, user.username);
            curCache[user.user_id] = {
              username: user.username,
              avatar: user.avatar || QV_CONFIG.defaultAvatar
            };
          }

          try {
            sessionStorage.setItem(QV_CONFIG.cacheKey, JSON.stringify(curCache));
          } catch (e) {}
        })
        .catch(() => {
          document.getElementById('Hide_qCite')?.remove();
        });
    }

    getAPIdataFor(userIDs);
  }

  function init() {
    if (document.querySelector('.quote-box, #post-preview, .post[id]')) {
      const hideStyle = document.createElement('style');
      hideStyle.id = 'Hide_qCite';
      hideStyle.textContent = '.quote-box > cite{display:none!important;}';
      document.head.appendChild(hideStyle);

      setLinkAndAva('body');
      enhanceQuoteButtons();

      const targets = [
        document.querySelector('#pun-viewtopic'),
        document.querySelector('#post-box'),
        document.body
      ].filter(Boolean);

      const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
          if (!mutation.addedNodes || mutation.addedNodes.length === 0) continue;
          let need = false;
          for (const node of mutation.addedNodes) {
            if (!node || node.nodeType !== 1) continue;
            if (
              (node.matches && node.matches('.quote-box, .post, .post.new-ajax, #post-preview')) ||
              (node.querySelector && node.querySelector('.quote-box, .post, .post.new-ajax, #post-preview')) ||
              (mutation.target && mutation.target.closest && (mutation.target.closest('.post') || mutation.target.id === 'post-preview'))
            ) {
              need = true;
              break;
            }
          }
          if (need) {
            setTimeout(() => {
              if (document.querySelector('.quote-box:not(.with-avatar)')) {
                setLinkAndAva('body');
                enhanceQuoteButtons();
              }
            }, 0);
            break;
          }
        }
      });

      targets.forEach(t => observer.observe(t, { childList: true, subtree: true }));
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

  const reapply = () => setTimeout(() => {
    if (document.querySelector('.quote-box:not(.with-avatar)')) setLinkAndAva('body');
  }, 0);
  ['pun_post', 'pun_edit', 'pun_preview', 'pun_main_ready'].forEach(evt =>
    document.addEventListener(evt, reapply)
  );

})();

update: 24.11.2025

Список изменений и исправлений

🔧 Что изменено:

    ✔ Полностью удалена перезапись innerHTML в parseQuote → теперь обновляются только src и textContent.
    ✔ Ссылка <a> всегда сохраняется и остаётся кликабельной.
    ✔ Конструирование DOM через createElement в setLinkAndAva — безопаснее, чем innerHTML.
    ✔ Иконка перехода корректно вложена в <a>, а не в <span> без обработки.


При разработке использовался ChatGPT и т.п. нейросети, затем код был проверен и протестирован на реальном форуме. (указано по требованию администрации сервиса)

Отредактировано Merlin777 (Пн, 24 Ноя 2025 20:37:32)

+3

Перевести582

⚡Мгновенные "Быстроплюсы" и ⚡Мгновенные "Сказали спасибо"

Предыстория
Есть скрипт "Быстроплюсы от Deff" https://forum.mybb.ru/viewtopic.php?id= … 29#p982958 , он позволяет ставить положительные "лайки" однократным нажатием на "сердечко".
И есть скрипт "Поблагодарили" от Reysler https://forum.mybb.ru/viewtopic.php?id= … 78#p995185 и "Список поддержавших пост" от Виплич https://forum.mybb.ru/viewtopic.php?id= … 29#p995548 . Смысл этих скриптов - вывести в сообщениях, под подписью, список поставивших "лайки".

Эти скрипты, "Быстроплюсы" и список их поставивших, органично дополняют друг друга.

В моём варианте список поставивших положительные "лайки" называется "Сказали спасибо", поэтому далее я буду называть эти функции "Быстроплюсы" и "Сказали спасибо".


Вышеприведённые скрипты выполняют свою работу, но у них есть свои функциональные ограничения:

● "Быстроплюсы" не работают при включении бесплатного скрипта "Живых тем" от Alex_63 https://forum.mybb.ru/viewtopic.php?id=41002#p993848
● "Сказали спасибо"  для отображения в обычном режиме  требуют перезагрузки страницы.
● "Сказали спасибо"  для отображения в режиме "Живые темы" тоже  требуют перезагрузки страницы.
● При  использовании платного дополнения "Мгновенные уведомления" от Alex_63 https://forum.mybb.ru/viewtopic.php?id=38567#p939931 ,  "Сказали спасибо"  тоже требуют перезагрузки страницы.

А чего мы хотим? Мы хотим, чтобы всё работало 🚀быстрее, чем лайки в Телеграме!


Далее будет приведён скрипт, при котором "Быстроплюсы" и "Сказали спасибо" отлично работают и отображаются мгновенно в ЛЮБОМ варианте работы форума!

"Быстроплюсы" и "Сказали спасибо" используют единое логическое ядро и непосредственно связаны друг с другом через вызов функции.

Кому не нужны "Сказали спасибо", а нужны лишь всегда быстро и чётко работающие сердечки "Быстроплюсов", для тех выложим сокращённую версию скрипта без "Сказали спасибо".


Подготовительная работа.
1. В панели администрирования форума Администрирование - Права  поставьте "Разрешить ajax для предпросмотра и отправки сообщения", затем нажмите кнопку "Сохранить".

Скриншот (кликабельно)

https://upforme.ru/uploads/0000/14/1c/38891/t684120.webp

2. В панели администрирования форума Администрирование - Настройки  в блоке "Система отношений" поставьте "Тип оценок - Только плюсы", затем нажмите кнопку "Сохранить".

Скриншот

https://upforme.ru/uploads/0000/14/1c/38891/545178.webp


🛠️Установка скрипта

🔀Самый простой способ:
В Администрирование - Формы - HTML низ вставить код:

Посмотреть код
Код:
<!-- ⚡Мгновенные" Быстроплюсы" и ⚡мгновенные "Сказали спасибо" -->
<link rel="stylesheet" href="https://forumstatic.ru/files/001a/f0/7d/33382.css?v=6">
<script src="https://forumstatic.ru/files/001a/f0/7d/21031.js?v=30"></script>
<!-- ⚡Мгновенные" Быстроплюсы" и ⚡мгновенные "Сказали спасибо" -->

Недостаток самого простого способа: картинка с "Сердечком" может не подходить под стиль Вашего форума (у меня стоит предустановленный "Mybb vBulletin mix").

🔀Рекомендуемый способ:
В Администрирование - Формы - HTML низ вставить код:

Посмотреть код
Код:
<!-- ⚡Мгновенные" Быстроплюсы" и ⚡мгновенные "Сказали спасибо" -->
<style>
div .post-rating p a {
    text-align: center;
    outline: 1px solid transparent;
    font-weight: 700;
    background: url(https://upforme.ru/uploads/001a/f0/7d/2/466540.png) no-repeat center;
     background-size:36px auto;
     position:relative;
    z-index:100;
    width: 36px;
    height: 36px;
    text-align:center;
    font-size: 12px !important;
    display: inline-block;
    vertical-align: middle;
    line-height: 34px;
    letter-spacing: -.1px;
}
.noNull:before{content: "+";
    display: inline-block;
    margin-left: -1px;
    font-size: 9px;
    letter-spacing: 0!important;
}
.punbb .post-rating p {
   padding: 0 !important;
}
</style>
<script src="https://forumstatic.ru/files/001a/f0/7d/21031.js?v=30"></script>
<!-- ⚡Мгновенные" Быстроплюсы" и ⚡мгновенные "Сказали спасибо" -->

Ссылку замените на файл c Вашей картинкой сердечка (с нужным Вам фоном или на прозрачном фоне).

🔀Способ для опытных администраторов:
Исходные коды CSS и JS вставляете в HTML Низ, а ещё лучше - сохраните коды в файлы (без строчек с <style> и </style> в файле с  CSS  и без <script> и </script> для файла с JS)  и подключите как  внешние ресурсы через link rel и script src.



🧩Исходные коды🧩

⚡Мгновенные "Быстроплюсы" и ⚡мгновенные "Сказали спасибо"

CSS (⚡Мгновенные" Быстроплюсы" и ⚡Мгновенные "Сказали спасибо")
Код:
<style>
/*Быстроплюсы для Топика!*/
div .post-rating p a {
    text-align: center;
    outline: 1px solid transparent;
    font-weight: 700;
    background: url(https://upforme.ru/uploads/001a/f0/7d/2/466540.png) no-repeat center;
     background-size:36px auto;
     position:relative;
    z-index:100;
    width: 36px;
    height: 36px;
    text-align:center;
    font-size: 12px !important;
    display: inline-block;
    vertical-align: middle;
    line-height: 34px;
    letter-spacing: -.1px;
}
.noNull:before{content: "+";
    display: inline-block;
    margin-left: -1px;
    font-size: 9px;
    letter-spacing: 0!important;
}
.punbb .post-rating p {
   padding: 0 !important;
}
</style>
JS (⚡Мгновенные" Быстроплюсы" и ⚡Мгновенные "Сказали спасибо")
Код:
<script>
    'use strict';

// --- ОБЩИЕ ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---

// Получает ID постов из указанного DOM-контекста
function getPostIds(scope = document) {
    const postElements = $(scope).find('div.post');
    return postElements.map(function() {
        const id = $(this).attr('id');
        if (id && id.startsWith('p')) {
            return id.slice(1);
        }
    }).get();
}

// --- ЧАСТЬ 1: БЫСТРОПЛЮСЫ ---

// Отслеживание уже обработанных постов (для избежания дублирования)
const processedPostsForPlus = new Set();

// Обновляет отображение цифры рейтинга и управляет классом noNull
function setDigit(th) {
    var d = parseInt(th.innerHTML);
    if (d > 0) $(th).addClass('noNull');
    else $(th).removeClass('noNull');
    th.innerHTML = d;
}

// Сохранение оригинального alert для восстановления после голосования
var Busy = window.alert;

// Обработка клика по кнопке "плюса": отправка запроса, обновление рейтинга и мини-профиля
function setPlus(sel) {
    window.alert = null;
    var a = sel.prop('href');
    var pid = a.match(/\?id=(\d+)/)[1];

    var uid = sel.parents('.post').find('.pl-email a[href*="profile.php?"]').prop('href');
    if (uid) uid = uid.match(/\?id=(\d+)$/)[1];
    var v = a.match(/&v=(\d+)/)[1] == 0 ? -1 : 1;
    $('#post-' + pid + '-vote').hide();

    $.get(a + '&format=json', function(data) {
        if (data.error && data.error.message) {
            // Ошибка обрабатывается без уведомлений — только внутренняя логика
        } else if (data.delta) {
            if(data.response !== undefined) {
                $('#p' + pid + ' .post-rating a').text(data.response);
                setDigit($('#p' + pid + ' .post-rating a')[0]);
            } else {
                var oldRating = parseInt($('#p' + pid + ' .post-rating a').text()) || 0;
                var newRating = oldRating + data.delta;
                $('#p' + pid + ' .post-rating a').text(newRating);
                setDigit($('#p' + pid + ' .post-rating a')[0]);
            }

            // Обновление мини-профиля автора и текущего пользователя
            var $res = $('.pl-email a[href$="profile.php?id=' + uid + '"]').parents('.post').find('.pa-respect');
            var $pos = $('.pl-email a[href$="profile.php?id=' + UserID + '"]').parents('.post').find('.pa-positive');

            function replaceRating(sel, v, revert) {
                var html = $(sel).html(),
                    delta = v;
                if (revert) delta = delta > 0 ? -1 : 1;
                if (v > 0) {
                    html = html.replace(/\[\+(\d+)\//g, function(str, p1) {
                        return '[+' + (parseInt(p1) + delta) + '/';
                    });
                } else {
                    html = html.replace(/\/-(\d+)\]/g, function(str, p1) {
                        return '/-' + (parseInt(p1) - delta) + ']';
                    });
                }
                $(sel).html(html);
            }
            if ($res.html().indexOf('[') != -1) {
                $res.each(function() { replaceRating(this, v); });
                $pos.each(function() { replaceRating(this, v); });
                if (Math.abs(data.delta) == 2) {
                    v = v > 0 ? -1 : 1;
                    $res.each(function() { replaceRating(this, v, 1); });
                    $pos.each(function() { replaceRating(this, v, 1); });
                }
            } else {
                var d0 = $res.find('span:not(.fld-name)').html(),
                    p0 = $pos.find('span:not(.fld-name)').html();
                var d1 = parseInt(d0) + v;
                if (p0) {
                    var p1 = parseInt(p0) + v;
                }
                if (d1 && d1 > 0) {
                    d1 = '+' + d1;
                }
                if (p1 && p1 > 0) {
                    p1 = '+' + p1;
                }
                $res.find('span:not(.fld-name)').html(d1);
                if (p0) $pos.find('span:not(.fld-name)').html(p1);
            }

            // Генерация события для обновления списка "Спасибо" при лайке
            if(v === 1) {
                document.dispatchEvent(new CustomEvent('vote:happened', {
                    detail: { pid: pid, type: 'thank' }
                }));
            }
        }
        setTimeout(function() {window.alert = Busy;},1300);
    })
    .fail(function(xhr, status, error) {
        setTimeout(function() {window.alert = Busy;},1300);
    });
}

// Обёртка для вызова setPlus из onclick
window.BR = function(th) {
    var lnk = $(th).parents('.post-box').find('.post-vote>p>a');
    setPlus(lnk);
};

// Скрывает все вновь добавленные элементы .post-vote
function hideAllNewPostVotes(scope = document) {
    $(scope).find('.post-vote[id^="post-"]').each(function() {
        var postId = this.id.replace('post-', '');
        var postRatingLink = $(`.post#p${postId} .post-rating p a`);
        if(postRatingLink.length) {
             $(this).hide();
        }
    });
}

// Назначает обработчики для кнопок "плюса" в указанной области
function applyPlusHandlers(scope = document) {
    $(scope).find('.post .post-rating p a').each(function() {
        if (!this.hasAttribute('data-plus-handler')) {
             this.setAttribute("onclick", "BR(this)");
             setDigit(this);

             var postVoteLinkSelector = $(this).parents('.post').attr('id');
             if(postVoteLinkSelector) {
                 var pid = postVoteLinkSelector.slice(1);
                 var postVoteLink = $('#post-' + pid + '-vote');
                 if(postVoteLink.length) {
                      postVoteLink.hide();
                 }
             }
             this.setAttribute('data-plus-handler', 'true');
        }
    });
    hideAllNewPostVotes(scope);
}

// --- ЧАСТЬ 2: СПИСОК ПОБЛАГОДАРИВШИХ (API-ЛОГИКА) ---

// Отслеживание постов, для которых уже запрашивались "спасибо"
const processedPostsForThanks = new Set();

// Отслеживание ID постов, для которых выполняется запрос (для дедупликации)
const pendingUpdates = new Set();

// Вставка списка благодаривших в пост
function insertThanksList(postId, thankersListHtml) {
    if (!thankersListHtml) {
        return;
    }

    var postBox = $('.post#p' + postId + ' .post-box');
    if (postBox.length && postBox.find('.postVoters').length === 0) {
        var html = '<div class="postVoters"><strong><i><font color ="#13355e">Сказали спасибо</font></i>:</strong> ' + thankersListHtml + '</div>';
        postBox.append(html);
        processedPostsForThanks.add(postId);
    } else {
        if(postBox.length === 0) {
            var postElement = $('.post#p' + postId);
            if(postElement.length && postElement.find('.postVoters').length === 0) {
                var html = '<div class="postVoters"><strong><i><font color ="#13355e">Сказали спасибо</font></i>:</strong> ' + thankersListHtml + '</div>';
                postElement.append(html);
                processedPostsForThanks.add(postId);
            }
        } else {
            postBox.find('.postVoters').html('<strong><i><font color ="#13355e">Сказали спасибо</font></i>:</strong> ' + thankersListHtml);
        }
    }
}

// Обработка ответа API: группировка и вставка благодаривших
function processVotes(data) {
    if (!data || !data.response) {
        const postIdsFromRequest = currentFetchRequestIds || [];
        postIdsFromRequest.forEach(id => processedPostsForThanks.add(id));
        currentFetchRequestIds = null;
        return;
    }

    const votesByPost = {};
    data.response.forEach(item => {
        const postId = item.post_id;
        if (item.votes && Array.isArray(item.votes)) {
            item.votes.forEach(vote => {
                if (vote.value === '1') {
                    if (!votesByPost[postId]) {
                        votesByPost[postId] = [];
                    }
                    votesByPost[postId].push(vote);
                }
            });
        }
    });

    Object.keys(votesByPost).forEach(postId => {
        const votesForThisPost = votesByPost[postId];
        let thankersHtml = '';
        votesForThisPost.forEach((vote, index) => {
            thankersHtml += '<a href="/profile.php?id=' + vote.user_id + '">' + vote.username + '</a>';
            if (index < votesForThisPost.length - 1) {
                thankersHtml += ', ';
            }
        });
        insertThanksList(postId, thankersHtml);
    });

    Object.keys(votesByPost).forEach(id => processedPostsForThanks.add(id));
    data.response.forEach(item => {
        if (!processedPostsForThanks.has(item.post_id)) {
            processedPostsForThanks.add(item.post_id);
        }
    });
}

// Выполнение API-запроса для получения списка благодаривших
let currentFetchRequestIds = null;

function fetchVotesForPosts(postIds) {
    if (!postIds || postIds.length === 0) {
        postIds.forEach(id => pendingUpdates.delete(id));
        return;
    }

    const apiParams = {
        method: 'post.getVotesByPosts',
        post_id: postIds.join(','),
        fields: 'post_id,user_id,username,value,datetime',
        sort_dir: 'desc'
    };

    currentFetchRequestIds = [...postIds];

    return $.get('/api.php', apiParams, function(data) {
        processVotes(data);
    }, 'json')
    .fail(function(xhr, status, error) {
        postIds.forEach(id => processedPostsForThanks.add(id));
        currentFetchRequestIds = null;
    });
}

// Планирование обновления списка "спасибо" для новых постов
function scheduleUpdate(scope = document) {
    requestAnimationFrame(() => {
        const newPostIds = getPostIds(scope).filter(id => !processedPostsForThanks.has(id));
        if (newPostIds.length > 0) {
            const idsToRequest = newPostIds.filter(id => !pendingUpdates.has(id));
            if (idsToRequest.length > 0) {
                idsToRequest.forEach(id => pendingUpdates.add(id));
                fetchVotesForPosts(idsToRequest).always(() => {
                    idsToRequest.forEach(id => pendingUpdates.delete(id));
                });
            }
        }
    });
}

// Объединённая функция резервного обновления (раз в секунду)
function combinedBackupUpdate() {
    applyPlusHandlers(document);
    scheduleUpdate(document);
}

// --- ИНИЦИАЛИЗАЦИЯ И ПОДПИСКИ НА СОБЫТИЯ ---

// Инициализация при загрузке DOM
$(function() {
    $('.post .post-rating p a').each(function() {
        this.setAttribute("onclick", "BR(this)");
        setDigit(this);
        var postVoteLinkSelector = $(this).parents('.post').attr('id');
        if(postVoteLinkSelector) {
            var pid = postVoteLinkSelector.slice(1);
            var postVoteLink = $('#post-' + pid + '-vote');
            if(postVoteLink.length && $(this).hasClass('noNull')) {
                postVoteLink.hide();
            }
        }
        this.setAttribute('data-plus-handler', 'true');
    });

    scheduleUpdate(document);
});

// Подписка на событие добавления нового поста (например, через AJAX)
document.addEventListener('pun_post', function(e) {
    var scope = e.detail?.post || document.body;
    applyPlusHandlers(scope);
    scheduleUpdate(scope);
});

// Подписка на собственное событие после успешного лайка
document.addEventListener('vote:happened', function(e) {
    if (e.detail && e.detail.pid && e.detail.type === 'thank') {
        var pid = e.detail.pid;
        var specificPostScope = $('#p' + pid);
        if(specificPostScope.length) {
            if (!pendingUpdates.has(pid)) {
                pendingUpdates.add(pid);
                fetchVotesForPosts([pid]).always(() => {
                    pendingUpdates.delete(pid);
                });
            }
        }
    }
});

// Подписка на глобальное AJAX-событие для отслеживания лайков от любых пользователей
$(document).on('ajaxSuccess', function(e, xhr, data) {
    if (data && data.url && /relation\.php.*[&?]v=(\d+)/.test(data.url)) {
        const match = data.url.match(/id=(\d+)/);
        const pid = match ? match[1] : null;
        const voteType = data.url.match(/v=(\d+)/)?.[1];

        if (pid && voteType === '1') {
            if (!pendingUpdates.has(pid)) {
                pendingUpdates.add(pid);
                fetchVotesForPosts([pid]).always(() => {
                    pendingUpdates.delete(pid);
                });
            }
        }
    }
});

// Наблюдатель за изменениями DOM для динамических постов
const observer = new MutationObserver(function(mutationsList) {
    let shouldUpdate = false;
    let relevantScope = document.body;

    for (let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            for (let node of mutation.addedNodes) {
                if (node.nodeType === 1) {
                    if (node.classList && node.classList.contains('post')) {
                        relevantScope = node;
                        shouldUpdate = true;
                        break;
                    } else if (node.querySelector && node.querySelector('div.post')) {
                        relevantScope = node;
                        shouldUpdate = true;
                        break;
                    } else {
                        const nestedPosts = node.querySelectorAll && node.querySelectorAll('div.post');
                        if (nestedPosts && nestedPosts.length > 0) {
                            relevantScope = node;
                            shouldUpdate = true;
                            break;
                        }
                    }
                }
            }
            if (shouldUpdate) break;
        }
    }

    if (shouldUpdate) {
        requestAnimationFrame(() => {
            applyPlusHandlers(relevantScope);
            hideAllNewPostVotes(relevantScope);
            scheduleUpdate(relevantScope);
        });
    }
});

observer.observe(document.body, { childList: true, subtree: true });

// Резервный таймер для обновления раз в секунду
const backupInterval = setInterval(combinedBackupUpdate, 1000);
</script>

Если вам нужны лишь "Быстроплюсы", то вы берёте вышеприведённый CSS, а JS будет такой:

JS (⚡Мгновенные "Быстроплюсы")
Код:
<script>
'use strict';

// --- ОБЩИЕ ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---

// Получает ID постов из указанного DOM-контекста
function getPostIds(scope = document) {
    const postElements = $(scope).find('div.post');
    return postElements.map(function() {
        const id = $(this).attr('id');
        if (id && id.startsWith('p')) {
            return id.slice(1);
        }
    }).get();
}

// --- БЫСТРОПЛЮСЫ ---

// Отслеживание уже обработанных постов (для избежания дублирования)
const processedPostsForPlus = new Set();

// Обновляет отображение цифры рейтинга и управляет классом noNull
function setDigit(th) {
    var d = parseInt(th.innerHTML);
    if (d > 0) $(th).addClass('noNull');
    else $(th).removeClass('noNull');
    th.innerHTML = d;
}

// Сохранение оригинального alert для восстановления после голосования
var Busy = window.alert;

// Обработка клика по кнопке "плюса": отправка запроса, обновление рейтинга и мини-профиля
function setPlus(sel) {
    window.alert = null;
    var a = sel.prop('href');
    var pid = a.match(/\?id=(\d+)/)[1];

    var uid = sel.parents('.post').find('.pl-email a[href*="profile.php?"]').prop('href');
    if (uid) uid = uid.match(/\?id=(\d+)$/)[1];
    var v = a.match(/&v=(\d+)/)[1] == 0 ? -1 : 1;
    $('#post-' + pid + '-vote').hide();

    $.get(a + '&format=json', function(data) {
        if (data.error && data.error.message) {
            // Ошибка — без уведомлений
        } else if (data.delta) {
            if(data.response !== undefined) {
                $('#p' + pid + ' .post-rating a').text(data.response);
                setDigit($('#p' + pid + ' .post-rating a')[0]);
            } else {
                var oldRating = parseInt($('#p' + pid + ' .post-rating a').text()) || 0;
                var newRating = oldRating + data.delta;
                $('#p' + pid + ' .post-rating a').text(newRating);
                setDigit($('#p' + pid + ' .post-rating a')[0]);
            }

            // Обновление мини-профиля автора и текущего пользователя
            var $res = $('.pl-email a[href$="profile.php?id=' + uid + '"]').parents('.post').find('.pa-respect');
            var $pos = $('.pl-email a[href$="profile.php?id=' + UserID + '"]').parents('.post').find('.pa-positive');

            function replaceRating(sel, v, revert) {
                var html = $(sel).html(),
                    delta = v;
                if (revert) delta = delta > 0 ? -1 : 1;
                if (v > 0) {
                    html = html.replace(/\[\+(\d+)\//g, function(str, p1) {
                        return '[+' + (parseInt(p1) + delta) + '/';
                    });
                } else {
                    html = html.replace(/\/-(\d+)\]/g, function(str, p1) {
                        return '/-' + (parseInt(p1) - delta) + ']';
                    });
                }
                $(sel).html(html);
            }
            if ($res.html().indexOf('[') != -1) {
                $res.each(function() { replaceRating(this, v); });
                $pos.each(function() { replaceRating(this, v); });
                if (Math.abs(data.delta) == 2) {
                    v = v > 0 ? -1 : 1;
                    $res.each(function() { replaceRating(this, v, 1); });
                    $pos.each(function() { replaceRating(this, v, 1); });
                }
            } else {
                var d0 = $res.find('span:not(.fld-name)').html(),
                    p0 = $pos.find('span:not(.fld-name)').html();
                var d1 = parseInt(d0) + v;
                if (p0) {
                    var p1 = parseInt(p0) + v;
                }
                if (d1 && d1 > 0) {
                    d1 = '+' + d1;
                }
                if (p1 && p1 > 0) {
                    p1 = '+' + p1;
                }
                $res.find('span:not(.fld-name)').html(d1);
                if (p0) $pos.find('span:not(.fld-name)').html(p1);
            }
        }
        setTimeout(function() { window.alert = Busy; }, 1300);
    })
    .fail(function(xhr, status, error) {
        setTimeout(function() { window.alert = Busy; }, 1300);
    });
}

// Обёртка для вызова setPlus из onclick
window.BR = function(th) {
    var lnk = $(th).parents('.post-box').find('.post-vote>p>a');
    setPlus(lnk);
};

// Назначает обработчики для кнопок "плюса" в указанной области
function applyPlusHandlers(scope = document) {
    $(scope).find('.post .post-rating p a').each(function() {
        if (!this.hasAttribute('data-plus-handler')) {
            this.setAttribute("onclick", "BR(this)");
            setDigit(this);

            var postVoteLinkSelector = $(this).parents('.post').attr('id');
            if (postVoteLinkSelector) {
                var pid = postVoteLinkSelector.slice(1);
                var postVoteLink = $('#post-' + pid + '-vote');
                if (postVoteLink.length) {
                    postVoteLink.hide();
                }
            }
            this.setAttribute('data-plus-handler', 'true');
        }
    });
}

// --- ИНИЦИАЛИЗАЦИЯ ---

$(function() {
    applyPlusHandlers(document);
});

// Подписка на событие добавления нового поста (например, через AJAX)
document.addEventListener('pun_post', function(e) {
    var scope = e.detail?.post || document.body;
    applyPlusHandlers(scope);
});

// Наблюдатель за изменениями DOM для динамических постов
const observer = new MutationObserver(function(mutationsList) {
    let shouldUpdate = false;
    let relevantScope = document.body;

    for (let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            for (let node of mutation.addedNodes) {
                if (node.nodeType === 1) {
                    if (node.classList && node.classList.contains('post')) {
                        relevantScope = node;
                        shouldUpdate = true;
                        break;
                    } else if (node.querySelector && node.querySelector('div.post')) {
                        relevantScope = node;
                        shouldUpdate = true;
                        break;
                    } else {
                        const nestedPosts = node.querySelectorAll && node.querySelectorAll('div.post');
                        if (nestedPosts && nestedPosts.length > 0) {
                            relevantScope = node;
                            shouldUpdate = true;
                            break;
                        }
                    }
                }
            }
            if (shouldUpdate) break;
        }
    }

    if (shouldUpdate) {
        requestAnimationFrame(() => {
            applyPlusHandlers(relevantScope);
        });
    }
});

observer.observe(document.body, { childList: true, subtree: true });
</script>

При разработке использовался ChatGPT и т.п. нейросети, затем код был проверен и протестирован, сейчас всё работает на реальных форумах.
(указано по требованию администрации сервиса)

Отредактировано Merlin777 (Пн, 1 Дек 2025 22:48:20)

+4

Перевести583

Мокаем данные на страницах (пока мокается число ЛС и статистика)
За доп функционалом можно пнуть меня в тг - ya4aika - или тут

Кому оно вообще надо? - верстальщикам, чтобы можно было не менять руками разметку и/или посмотреть на нужные данные, например, с телефона
Как это юзать? - собственно, под катом пример использования. "Логины пользаков" обрезаются до 25 символов, как это делает сервис (точнее он просто не даст запилить логин длиннее 25 символов)

Дополнительно:
Ссылка в await import('https://cdn.jsdelivr.net/gh/Embery/mybb-scripts@HEAD/mock.js') ведет на cdn, раздающий файлы с гитхаба, если есть сомнения, что я упорюсь и решу снести/закрыть репу/эта cdn тоже решит лечь - можете утащить файл к себе в файлы форума и заменить ссылку. В текущем виде автоматом будут подсасываться обновления, обещаю не ломать совместимость по возможности)

Пруф работоспособности
Ставим в низ
Код:
<script>
    (async () => {
        try {
            const {Mock} = await import('https://cdn.jsdelivr.net/gh/Embery/mybb-scripts@HEAD/mock.js');
            Mock.pmNumber(10, Number(new Date()));
            Mock.statistics({
                postsAmount: 99999,
                topicsAmount: 1,
                usersAmount: 100500,
                lastRegisterLogin: 'Ололо азазза триста тыщ символов все равно обрежется',
                activeUsers: [{
                    id: 3,
                    login: 'Ololo asdsdcsdcsdcsdcsdcsававава',
                    groupId: 4,
                }, {}],
                activeRecordAmount: 100500,
                activeGuestsAmount: 100500,
            });
        } catch (e) {
            console.error(e);
        }
    })();
</script>

+4

Перевести584

⚡Ударные гласные под формой ответа⚡

📜Описание:
Этот код добавляет под форму ответа разворачивающийся список ударных гласных.
В свёрнутом виде не мешает, в мобильном виде ведёт себя прилично (хотя его можно скрыть, если вам не нужен) , в тёмных темах выглядит достойно.

Как выглядит:

В свёрнутом виде

https://upforme.ru/uploads/0000/14/1c/38891/588508.webp

В развёрнутом виде

https://upforme.ru/uploads/0000/14/1c/38891/820662.webp

💡Как использовать: развернуть список ударных букв и нажать на букву - она вставится в форму ответа.

➡️ Как установить: вставляем код в Администрирование - Формы - HTML в форме ответа

Код
Код:
<!--//Начало//Ударные гласные-->
<style>
@media screen and (min-width: 540px) {
    body
details{
    float: right;
  display:block;
  background: #afc2d4;
  width:300px;
  box-shadow: 0 10px 15px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  border-radius: 8px;
  overflow:hidden;
  margin-bottom: 1.5rem;
}
summary::-webkit-details-marker{display:none;}
summary::-moz-list-bullet{list-style-type:none;}
summary::marker{display:none;} 
summary {
   display:block;
   padding: .3em 1em .3em .9em;
   border-bottom: 1px solid #e2e8f0;
   font-size:1.1em;
   cursor: pointer;
   position: relative;
}
summary:before {  
  top: .4em;
  right: .3em;
  color: transparent;
  background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTYuNTkgOC41OUwxMiAxMy4xNyA3LjQxIDguNTkgNiAxMGw2IDYgNi02eiIvPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48L3N2Zz4=") no-repeat 50% 50% / 1em 1em;
  width: 1em;
  height: 1em;  
  content: "";
  position: absolute;
  transition: transform .5s;
}
details[open] > summary:before {
  transform: scale(1,-1);
}
summary ~ * {
   padding: 0 1em 10px 1.4em;
}
details[open] summary ~ *{ 
  animation: sweep .5s ease-in-out;
}
@keyframes sweep {
  0%    {opacity: 0;}
  100%  {opacity: 1;}
}
summary:focus {
  outline:0;
  box-shadow: inset 0 0 1px rgba(0,0,0,0.3), inset 0 0 2px rgba(0,0,0,0.3);
}
}
</style>


<details>
    <summary>Вставить ударные гласные</summary>
<center>
<input type="button" value="А́" name="H" title="А́" onclick="bbcode('А́','')" />
<input type="button" value="а́" name="H" title="а́" onclick="bbcode('а́','')" />
<input type="button" value="Е́" name="H" title="Е́" onclick="bbcode('Е́','')" />
<input type="button" value="е́" name="H" title="е́" onclick="bbcode('е́','')" />
<input type="button" value="И́" name="H" title="И́" onclick="bbcode('И́','')" />
<input type="button" value="и́" name="H" title="и́" onclick="bbcode('и́','')" />
<input type="button" value="О́" name="H" title="О́" onclick="bbcode('О́','')" />
<input type="button" value="о́" name="H" title="о́" onclick="bbcode('о́','')" />
<input type="button" value="У́" name="H" title="У́" onclick="bbcode('У́','')" />
<input type="button" value="у́" name="H" title="у́" onclick="bbcode('у́','')" />
<input type="button" value="Ы́" name="H" title="Ы́" onclick="bbcode('Ы́','')" />
<input type="button" value="ы́" name="H" title="ы́" onclick="bbcode('ы́','')" />
<input type="button" value="Э́" name="H" title="Э́" onclick="bbcode('Э́','')" />
<input type="button" value="э́" name="H" title="э́" onclick="bbcode('э́','')" />
<input type="button" value="Ю́" name="H" title="Ю́" onclick="bbcode('Ю́','')" />
<input type="button" value="ю́" name="H" title="ю́" onclick="bbcode('ю́','')" />
<input type="button" value="Я́" name="H" title="Я́" onclick="bbcode('Я́','')" />
<input type="button" value="я́" name="H" title="я́" onclick="bbcode('я́','')" />
</center>
</details>
<!--//Конец//Ударные гласные-->

P.S. Это не скрипт, а "стиль", но я не совсем точно понимаю, в какую тему его нужно писать, поэтому написал сюда :)

Отредактировано Merlin777 (Сб, 20 Дек 2025 19:21:36)

+4

Перевести585

🤖 ИИ-собеседник

Представляем вашему вниманию ИИ-собеседника для форумов на платформе mybb.ru.

Стоимость: 100% бесплатно.

Как это выглядит: добавляется надпись "🤖 AI" в строчку перед кнопками "Удалить", "Редактировать", "Цитировать".

Посмотреть скриншот с кнопкой

https://upforme.ru/uploads/001a/f0/7d/2/941744.webp

После нажатия на кнопку появляется модальное окно для чата с Искусственным Интеллектом.

Посмотреть скриншот с окном для ИИ

https://upforme.ru/uploads/001a/f0/7d/2/62340.webp

🔀Установка скрипта: вставляем код в Администрирование - Формы - HTML низ (или HTML верх, нет разницы).

Код для вставки на форум
Код:
<!-- AI Assistant Modal — бесплатная Llama для *.mybb.ru -->
<script src="https://forumstatic.ru/files/001a/f0/7d/98921.js?v=5"></script>
<!-- AI Assistant Modal — бесплатная Llama для *.mybb.ru -->

🛠️Технические подробности
Скрипт использует бесплатные лимиты ресурсов на сервисах OpenRouter и Cloudflare Workers для доступа к бесплатной нейросети Llama-3.2 3B.

Как это работает и почему это совершенно безопасно

Ваш запрос проходит путь:

  → Браузер
  → Cloudflare Workers (наш прокси)
  → OpenRouter
  → Llama-3.2 3B (на инференс-серверах OpenRouter)
  → OpenRouter
  → Cloudflare Workers
  → Браузер

Проще говоря, Браузер ⇄ Cloudflare Worker ⇄ OpenRouter ⇄ Llama-3.2 3B
(где ⇄ означает двусторонний обмен)

Почему это безопасно:
✅Llama-3.2 3B не «звонит» сама никуда — она работает внутри OpenRouter.
✅OpenRouter выступает как единый API-шлюз, скрывающий всю инференс-инфраструктуру.
✅Cloudflare Worker — наш безопасный прокси, который:

● Прячет API-ключ,
● Обходит CORS,
● Контролирует доступ.

На сколько хватит бесплатных мощностей? (на много)

У Cloudflare Workers на бесплатном тарифе (Free Plan)  лимит составляет:
✅ 100 000 запросов в день.
🔸 Это ~3 миллиона запросов в месяц (при равномерной нагрузке).
⚠️ Важно: лимит ежедневный, а не месячный. Если исчерпан лимит сегодня — Cloudflare вернёт "Ошибку 1101" (доступ запрещён), но завтра он обновится автоматически.

🚨Особенности и ограничения.

❗Скрипт "ИИ собеседник" работает только на сайтах типа *.mybb.ru, например, имя сайта vasya.mybb.ru допустимо, а имя vasya.ru нет.
✔️Это бесплатная модель, соответственно, ресурсы на неё выделяются по остаточному принципу, поэтому до ответа нейросети приходится ждать примерно 10 секунд, при сложных вопросах - больше.
✔️Если выдаётся сообщение типа "AI provider error", то это значит, что бесплатных ресурсов на данный момент нет, закройте окошко и попробуйте через 1-2 минуты :)
✔️Лимиты для выходного количества символов написаны в окошке ИИ-чата - если в ответе будет выдача больше максимального количества символов, то сообщение будет обрезаться.
Но для коротких ответов и "болталки" этого вполне достаточно :)

P.S. Как запустить свой персональный AI чат на форуме, и подключить к нему платные и бесплатные модели - возможно, будет отдельная тема, а пока что в наличии вот такой бесплатный, но всё равно прикольный вариант:)


25.12.2025 - сейчас вместо 1 бесплатной нейросети можно выбрать любую из 6-ти :) Чем мощнее -  тем медленнее, конечно.
Уточнение насчёт бесплатных ресурсов: на Openrouter при балансе от 10$ доступно 1000 бесплатных запросов, при балансе до 10$ - всего 50. Сейчас там баланс 0$, но вы сейчас и 50 запросов не вычерпываете :)

Отредактировано Merlin777 (Чт, 25 Дек 2025 05:21:30)

+2

Перевести586

По приколу дружим штрудель с форумом:

В вверх

<script>
    $(document).on('custom_tag', (e) => {
        if (e.tag !== 'strudel') {
            return;
        }
        e.sender[0].code = e.sender[0].innerHTML.substring(3,e.sender[0].innerHTML.length - 4).replaceAll('<br>', '\n');
        e.sender[0].innerHTML = '';
    })
</script>
<script defer src="https://unpkg.com/@strudel/repl@1.0.2"></script>

В пользовательские теги:
strudel[strudel-editor]:e
Получится в постах такая штука
https://i5.imageban.ru/out/2025/12/25/6d1361d74e1aca06d8696f12226ffc54.png
Вставляется при этом такой код

Код:
[strudel]//$: note("C#4").sound("piano")
//$: note("E4").sound("piano")
//$: note("A3").sound("piano")[/strudel]

Она работает, запускается по ctrl + enter и мое уважение тем, кто это сделал

Можно по приколу пнуть меня в тг и я таки бахну туда кнопку старт-стоп и обновления, как в доке штруделя, если это кому-то интересно :D
Добавлено спустя 4 минуты 40 секунд: буду ультра благодарна тому, кто перевезет пост в скрипты от пользаков, idk как я уже второй раз вместо той темы закидываю сюда

Добавлено спустя 20 часов 32 минуты: Я таки подружила его с кнопками!

Если нужны кнопки start и stop пихаем в верх это

<script>
    $(document).on('custom_tag', (e) => {
        if (e.tag !== 'strudel') {
            return;
        }
        const el = e.sender[0];
        el.code = el.innerHTML.substring(3, el.innerHTML.length - 4).replaceAll('<br>', '\n');
        el.innerHTML = '';
        const start = document.createElement('button');
        start.innerText = 'start';
        start.addEventListener('click', async () => {
            const p = await el.editor.repl.evaluate(el.code);
            el.editor.repl.start();
        });
        el.insertAdjacentElement('afterEnd', start);
       
        const stop = document.createElement('button');
        stop.innerText = 'stop';
        stop.addEventListener('click', () => el.editor.repl.stop());
        el.insertAdjacentElement('afterEnd', stop);
    })
</script>
<script defer src="https://unpkg.com/@strudel/repl@1.0.2"></script>

Отредактировано Амираль (Пт, 26 Дек 2025 13:53:23)

+2


Вы здесь » Единый форум поддержки » Новые возможности форумов » Скрипты от пользователей 3