bvffalo

dollchan

Apr 3rd, 2020
114
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name            Dollchan Extension Tools
  3. // @version         20.3.17.0
  4. // @namespace       http://www.freedollchan.org/scripts/*
  5. // @author          Sthephan Shinkufag @ FreeDollChan
  6. // @copyright       © Dollchan Extension Team. See the LICENSE file for license rights and limitations (MIT).
  7. // @description     Doing some profit for imageboards
  8. // @icon            https://raw.github.com/SthephanShinkufag/Dollchan-Extension-Tools/master/Icon.png
  9. // @updateURL       https://raw.github.com/SthephanShinkufag/Dollchan-Extension-Tools/master/Dollchan_Extension_Tools.meta.js
  10. // @nocompat        Chrome
  11. // @run-at          document-start
  12. // @grant           GM_getValue
  13. // @grant           GM_setValue
  14. // @grant           GM_deleteValue
  15. // @grant           GM_openInTab
  16. // @grant           GM_xmlhttpRequest
  17. // @grant           GM.getValue
  18. // @grant           GM.setValue
  19. // @grant           GM.deleteValue
  20. // @grant           GM.xmlHttpRequest
  21. // @grant           unsafeWindow
  22. // @include         *
  23. // ==/UserScript==
  24.  
  25. /* eslint indent: ["error", "tab", {
  26.     "flatTernaryExpressions": true,
  27.     "outerIIFEBody": 0 }] */
  28.  
  29. (function deMainFuncInner(deWindow, prestoStorage, FormData, scrollTo, localData) {
  30. 'use strict';
  31.  
  32. const version = '20.3.17.0';
  33. const commit = '772b6eb';
  34.  
  35. /* ==[ DefaultCfg.js ]========================================================================================
  36.                                                 DEFAULT CONFIG
  37. =========================================================================================================== */
  38.  
  39. const defaultCfg = {
  40.     disabled     : 0,    // Dollchan enabled by default
  41.     language     : 0,    // Dollchan language [0=ru, 1=en]
  42.     hideBySpell  : 1,    // hide posts by spells
  43.     spells       : null, // user defined spells
  44.     sortSpells   : 0,    // sort spells and remove duplicates
  45.     hideRefPsts  : 0,    // hide replies to hidden posts
  46.     nextPageThr  : 0,    // load threads from next pages instead of hidden
  47.     delHiddPost  : 0,    // remove placeholders [0=off, 1=all, 2=posts only, 3=threads only]
  48.     ajaxUpdThr   : 1,    // threads updater
  49.     updThrDelay  : 20,   //    update interval (sec)
  50.     updCount     : 1,    //    show countdown to thread update
  51.     favIcoBlink  : 0,    //    blink the favicon on new posts
  52.     desktNotif   : 0,    //    desktop notifications for new posts
  53.     noErrInTitle : 0,    //    don't show error code in title (except 404)
  54.     markNewPosts : 1,    //    highlight new posts with color
  55.     useDobrAPI   : 1,    //    dobrochan: use json API
  56.     markMyPosts  : 1,    // highlight my own posts
  57.     expandTrunc  : 0,    // auto-expand truncated posts
  58.     showHideBtn  : 1,    // show "Hide" buttons [0=off, 1=with menu, 2=no menu]
  59.     showRepBtn   : 1,    // show "Quick reply" buttons [0=off, 1=with menu, 2=no menu]
  60.     postBtnsCSS  : 1,    // post buttons style [0=simple, 1=gradient grey, 2=custom]
  61.     postBtnsBack : '#8c8c8c', //    custom background color
  62.     thrBtns      : 1,    /* additional buttons under threads
  63.         [0=off, 1=all, 2=all (on board), 3='New posts' on board] */
  64.     noSpoilers   : 1,    // text spoilers expansion [0=off, 1=grey, 2=native]
  65.     limitPostMsg : 2000, // limit text width in posts nessages
  66.     widePosts    : 0,    // stretch posts to screen width
  67.     noPostNames  : 0,    // hide poster names
  68.     correctTime  : 0,    // time correction in posts
  69.     timeOffset   : '+0', //    time offset (h)
  70.     timePattern  : '',   //    search pattern
  71.     timeRPattern : '',   //    replace pattern
  72.     expandImgs   : 2,    // expand images on click [0=off, 1=in post, 2=by center]
  73.     imgNavBtns   : 1,    //    add buttons to navigate images
  74.     imgInfoLink  : 1,    //    show name under expanded image
  75.     resizeDPI    : 0,    //    don't upscale images on high DPI displays
  76.     resizeImgs   : 1,    //    resize large images to fit screen [0=off', '1=by width', '2=width+height]
  77.     minImgSize   : 100,  //    minimal size for expanded images (px)
  78.     zoomFactor   : 25,   //    images zoom sensibility [1-100%]
  79.     webmControl  : 1,    //    show control bar for WebM
  80.     webmTitles   : 0,    //    load titles from WebM metadata
  81.     webmVolume   : 100,  //    default volume for WebM [0-100%]
  82.     minWebmWidth : 320,  //    minimal width for WebM (px)
  83.     preLoadImgs  : 0,    // preload images [0=off, 1=all, 2=non-WebM]
  84.     findImgFile  : 0,    //    detect embedded files in images
  85.     openImgs     : 0,    // replace thumbs with original images [0=off, 1=all, 2=GIFs only, 3=non-GIFs]
  86.     imgSrcBtns   : 1,    // add "Search" buttons for images
  87.     imgNames     : 0,    // image names in links [0=off, 1=original, 2=hide]
  88.     maskImgs     : 0,    // NSFW mode
  89.     maskVisib    : 7,    // image opacity in NSFW mode [0-100%]
  90.     linksNavig   : 1,    // posts navigation by >>links
  91.     linksOver    : 100,  //    delay appearance (ms)
  92.     linksOut     : 1500, //    delay disappearance (ms)
  93.     markViewed   : 0,    //    mark viewed posts
  94.     strikeHidd   : 0,    //    strike >>links to hidden posts
  95.     removeHidd   : 0,    //        also remove from reply maps
  96.     noNavigHidd  : 0,    //    don't show previews for hidden posts
  97.     markMyLinks  : 1,    // mark links to my posts with (You)
  98.     crossLinks   : 0,    // replace http:// with >>/b/links*
  99.     decodeLinks  : 0,    // decode %D0%A5%D1 in links
  100.     insertNum    : 1,    // insert >>link on №postnumber click*
  101.     addOPLink    : 0,    // insert >>link when replying to OP on board
  102.     addImgs      : 0,    // load images to jpg/png/gif links*
  103.     addMP3       : 1,    // embed mp3 links
  104.     addVocaroo   : 1,    // embed Vocaroo links
  105.     embedYTube   : 1,    // embed YouTube links [0=off, 1=preview+player, 2=onclick]
  106.     YTubeWidth   : 360,  //    player width (px)
  107.     YTubeHeigh   : 270,  //    player height (px)
  108.     YTubeTitles  : 0,    //    load titles for YouTube links
  109.     ytApiKey     : '',   //    YouTube API key
  110.     addVimeo     : 1,    //    embed Vimeo links
  111.     ajaxPosting  : 1,    // posting without refresh
  112.     postSameImg  : 1,    //    ability to post duplicate images
  113.     removeEXIF   : 1,    //    remove EXIF from JPEG
  114.     removeFName  : 0,    //    clear file names [0=off, 1=empty, 2=unixtime, 3=unixtime-random]
  115.     sendErrNotif : 1,    //    inform in title about post send error
  116.     scrAfterRep  : 0,    //    scroll to bottom after reply
  117.     fileInputs   : 2,    //    enhanced file attachment field  [0=off, 1=simple, 2=preview]
  118.     addPostForm  : 2,    // reply form display in thread [0=at top, 1=at bottom, 2=hidden]
  119.     spacedQuote  : 1,    // insert a space when quoting "> "
  120.     favOnReply   : 1,    // add thread to favorites after reply
  121.     warnSubjTrip : 0,    // warn about a tripcode in "Subject" field
  122.     addSageBtn   : 1,    // replace "Email" with Sage button
  123.     saveSage     : 1,    //    remember sage
  124.     sageReply    : 0,    //    reply with sage
  125.     altCaptcha   : 0,    // use alternative captcha (if available)
  126.     capUpdTime   : 300,  // captcha update interval (sec)
  127.     captchaLang  : 1,    // forced captcha input language [0=off, 1=en, 2=ru]
  128.     addTextBtns  : 1,    // text markup buttons [0=off, 1=graphics, 2=text, 3=usual]
  129.     txtBtnsLoc   : 1,    //    located at [0=top, 1=bottom]
  130.     userPassw    : 1,    // user password
  131.     passwValue   : '',   //    value
  132.     userName     : 0,    // user name
  133.     nameValue    : '',   //    value
  134.     noBoardRule  : 0,    // hide board rules
  135.     noPassword   : 1,    // hide form "Password" field
  136.     noName       : 0,    // hide form "Name" field
  137.     noSubj       : 0,    // hide form "Subject" field
  138.     scriptStyle  : 0,    /* Dollchan style
  139.         [0=Gradient darkblue, 1=gradient blue, 2=solid grey, 3=transparent blue, 4=square dark] */
  140.     userCSS      : 0,    // user CSS
  141.     userCSSTxt   : '',   //    css text
  142.     expandPanel  : 0,    // show full main panel
  143.     animation    : 1,    // CSS3 animation
  144.     hotKeys      : 1,    // hotkeys
  145.     loadPages    : 1,    //    number of pages that are loaded on F5
  146.     panelCounter : 1,    // panel counter for posts/images [0=off, 1=all posts, 2=except hidden]
  147.     hideReplies  : 0,    // show only op-posts in threads list
  148.     rePageTitle  : 1,    // show thread title in the page tab
  149.     inftyScroll  : 1,    // infinite scrolling for pages
  150.     scrollToTop  : 0,    // always scroll to top in the threads list
  151.     saveScroll   : 1,    // remember the scroll position in threads
  152.     favThrOrder  : 0,    /* threads sorting order in the Favorites window
  153.         [0=by opnum, 1=by opnum (desc), 2=by adding, 3=by adding (desc)] */
  154.     favWinOn     : 0,    // Always open the Favorites window
  155.     closePopups  : 0,    // close popups automatically
  156.     updDollchan  : 2,    // Check for Dollchan updates [0=off, 1=per day, 2=2days, 3=week, 4=2weeks, 5=month]
  157.     textaWidth   : 300,  // textarea width (px)
  158.     textaHeight  : 115,  // textarea height (px)
  159.     replyWinDrag : 0,          // draggable "Quick Reply" form
  160.     replyWinX    : 'right: 0', //    "Quick Reply" form X position
  161.     replyWinY    : 'top: 0',   //    "Quick Reply" form Y position
  162.     cfgTab       : 'filters',  // remembered tab in "Settings" window
  163.     cfgWinDrag   : 0,          // draggable "Settings" window
  164.     cfgWinX      : 'right: 0', //    "Settings" window X position
  165.     cfgWinY      : 'top: 0',   //    "Settings" window Y position
  166.     hidWinDrag   : 0,          // draggable "Hidden" window
  167.     hidWinX      : 'right: 0', //    "Hidden" window X position
  168.     hidWinY      : 'top: 0',   //    "Hidden" window Y position
  169.     favWinDrag   : 0,          // draggable "Favorites" window
  170.     favWinX      : 'right: 0', //    "Favorites" window X position
  171.     favWinY      : 'top: 0',   //    "Favorites" window Y position
  172.     favWinWidth  : 500,        //    "Favorites" window width (px)
  173.     vidWinDrag   : 0,          // draggable "Video" window
  174.     vidWinX      : 'right: 0', //    "Video" window X position
  175.     vidWinY      : 'top: 0'    //    "Video" window Y position
  176. };
  177.  
  178. /* ==[ Localization.js ]======================================================================================
  179.                                                  LOCALIZATION
  180. =========================================================================================================== */
  181.  
  182. const Lng = {
  183.     // "Settings" window: tab names
  184.     cfgTab: {
  185.         filters : ['Фильтры', 'Filters', 'Фільтри'],
  186.         posts   : ['Посты', 'Posts', 'Пости'],
  187.         images  : ['Картинки', 'Images', 'Зображ.'],
  188.         links   : ['Ссылки', 'Links', 'Посил.'],
  189.         form    : ['Форма', 'Form', 'Форма'],
  190.         common  : ['Общее', 'Common', 'Спільне'],
  191.         info    : ['Инфо', 'Info', 'Інфо']
  192.     },
  193.     // "Settings" window: options
  194.     cfg: {
  195.         language: {
  196.             sel : [['Ru', 'En', 'Ua'], ['Ru', 'En', 'Ua'], ['Ru', 'En', 'Ua']],
  197.             txt : ['', '', '']
  198.         },
  199.  
  200.         // "Filters" tab
  201.         hideBySpell: [
  202.             'Спеллы: ',
  203.             'Magic spells: ',
  204.             'Спелли: '],
  205.         sortSpells: [
  206.             'Сортировать спеллы и удалять дубликаты',
  207.             'Sort spells and remove duplicates',
  208.             'Сортувати спелли та видаляти дублікати'],
  209.         hideRefPsts: [
  210.             'Скрывать ответы на скрытые посты',
  211.             'Hide replies to hidden posts',
  212.             'Ховати відповіді на сховані пости'],
  213.         nextPageThr: [
  214.             'Скрытые треды - загружать со следующих страниц',
  215.             'Load threads from next pages instead of hidden',
  216.             'Сховані треди - брати з наступних сторінок'],
  217.         delHiddPost: {
  218.             sel: [
  219.                 ['Откл.', 'Всё', 'Только посты', 'Только треды'],
  220.                 ['Disable', 'All', 'Posts only', 'Threads only'],
  221.                 ['Вимк.', 'Все', 'Лише пости', 'Лише треди']],
  222.             txt: [
  223.                 'Удалять скрытое',
  224.                 'Remove placeholders',
  225.                 'Видаляти сховане']
  226.         },
  227.  
  228.         // "Posts" tab
  229.         ajaxUpdThr: [
  230.             'Апдейтер тредов ',
  231.             'Threads updater ',
  232.             'Оновлювач тредів '],
  233.         updThrDelay: [
  234.             '(сек)',
  235.             '(sec)',
  236.             '(сек)'],
  237.         updCount: [
  238.             'Обратный счетчик обновления треда',
  239.             'Show countdown to thread update',
  240.             'Зворотній відлік оновлення треду'],
  241.         favIcoBlink: [
  242.             'Мигать фавиконом при появлении новых постов',
  243.             'Blink the favicon on new posts',
  244.             'Блимати фавіконом в разі появи нових постів'],
  245.         desktNotif: [
  246.             'Уведомлять о новых постах на рабочем столе',
  247.             'Desktop notifications for new posts',
  248.             'Повідомляти про нові пости на стільниці'],
  249.         noErrInTitle: [
  250.             'Не показывать номер ошибки в заголовке',
  251.             'Donʼt show error code in pageʼs title',
  252.             'Не показувати номер помилки в заголовку'],
  253.         markNewPosts: [
  254.             'Выделять цветом новые посты',
  255.             'Highlight new posts with color',
  256.             'Виділяти кольором нові пости'],
  257.         useDobrAPI: [
  258.             'dobrochan: использовать JSON API',
  259.             'dobrochan: use JSON API',
  260.             'dobrochan: використовувати JSON API'],
  261.         markMyPosts: [
  262.             'Выделять цветом мои посты',
  263.             'Highlight my own posts',
  264.             'Виділяти кольором мої пости'],
  265.         expandTrunc: [
  266.             'Авторазворот сокращенных постов*',
  267.             'Autoexpand truncated posts*',
  268.             'Авторозгортання скорочених постів*'],
  269.         thrBtns: {
  270.             sel: [
  271.                 ['Откл.', 'Все', 'Все (на доске)', '"Новые посты" на доске'],
  272.                 ['Disable', 'All', 'All (on board)', '"New posts" on board'],
  273.                 ['Вимк.', 'Всі', 'Всі (на дошці)', '"Нові пости" на дошці']],
  274.             txt: [
  275.                 'Кнопки под тредами',
  276.                 'Buttons under threads',
  277.                 'Кнопки під тредами']
  278.         },
  279.         showHideBtn: {
  280.             sel: [
  281.                 ['Откл.', 'С меню', 'Без меню'],
  282.                 ['Disable', 'With menu', 'No menu'],
  283.                 ['Вимк.', 'Із меню', 'Без меню']],
  284.             txt: [
  285.                 'Кнопки "Скрыть пост/тред"',
  286.                 '"Hide post/thread" buttons',
  287.                 'Кнопки "Сховати пост/тред"']
  288.         },
  289.         showRepBtn: {
  290.             sel: [
  291.                 ['Откл.', 'С меню', 'Без меню'],
  292.                 ['Disable', 'With menu', 'No menu'],
  293.                 ['Вимк.', 'Із меню', 'Без меню']],
  294.             txt: [
  295.                 'Кнопки "Ответить на пост/тред"',
  296.                 '"Reply to post/thread" buttons',
  297.                 'Кнопки "Відповісти на пост/тред"']
  298.         },
  299.         postBtnsCSS: {
  300.             sel: [
  301.                 ['Упрощенные', 'Серый градиент', 'Настраиваемые'],
  302.                 ['Simple', 'Gradient grey', 'Custom'],
  303.                 ['Спрощені', 'Сірий градієнт', 'Користувацькі']],
  304.             txt: [
  305.                 'Кнопки постов ',
  306.                 'Post buttons ',
  307.                 'Кнопки постів ']
  308.         },
  309.         noSpoilers: {
  310.             sel: [
  311.                 ['Откл.', 'Серое', 'Родное'],
  312.                 ['Disable', 'Grey', 'Native'],
  313.                 ['Вимк.', 'Сіре', 'Рідне']],
  314.             txt: [
  315.                 'Раскрытие текстовых спойлеров',
  316.                 'Text spoilers expansion',
  317.                 'Розкриття текстових спойлерів']
  318.         },
  319.         limitPostMsg: [
  320.             'Ограничение ширины текста в постах (px)',
  321.             'Limit text width in posts messages (px)',
  322.             'Обмеження ширини тексту в постах (px)'
  323.         ],
  324.         widePosts: [
  325.             'Растягивать посты по ширине экрана',
  326.             'Stretch posts to page width',
  327.             'Розтягувати пости на ширину екрану'],
  328.         noPostNames: [
  329.             'Скрывать имена в постах',
  330.             'Hide poster names',
  331.             'Ховати імена в постах'],
  332.         correctTime: [
  333.             'Коррекция времени в постах* ',
  334.             'Time correction in posts* ',
  335.             'Корекція часу в постах* '],
  336.         timeOffset: [
  337.             'разница (ч) ',
  338.             'time offset (h) ',
  339.             'різниця (год) '],
  340.         timePattern: [
  341.             'Шаблон поиска',
  342.             'Search pattern',
  343.             'Шаблон пошуку'],
  344.         timeRPattern: [
  345.             'Шаблон замены',
  346.             'Replace pattern',
  347.             'Шаблон заміни'],
  348.  
  349.         // "Images" tab
  350.         expandImgs: {
  351.             sel: [
  352.                 ['Откл.', 'В посте', 'По центру'],
  353.                 ['Disable', 'In post', 'By center'],
  354.                 ['Вимк.', 'В пості', 'По центру']],
  355.             txt: [
  356.                 'Раскрывать картинки по клику',
  357.                 'Expand images on click',
  358.                 'Розгортати зображення по кліку']
  359.         },
  360.         imgNavBtns: [
  361.             'Добавлять кнопки навигации по картинкам',
  362.             'Add buttons to navigate images',
  363.             'Додавати кнопки навігації по зображеннях'],
  364.         imgInfoLink: [
  365.             'Имя файла под раскрытой картинкой',
  366.             'Show file name under expanded image',
  367.             'Імʼя файлу під розкритим зображенням'],
  368.         resizeDPI: [
  369.             'Не растягивать на дисплеях с высоким DPI',
  370.             'Donʼt upscale images on high DPI displays',
  371.             'Не розтягувати на дисплеях з високим DPI'],
  372.         resizeImgs: {
  373.             sel: [
  374.                 ['Откл.', 'По ширине', 'Шир.+выс.'],
  375.                 ['Disable', 'By width', 'Width+Height'],
  376.                 ['Вимк.', 'По ширині', 'Шир.+выс.']],
  377.             txt: [
  378.                 'Уменьшать при раскрытии в посте',
  379.                 'Fit to screen for expanding in post',
  380.                 'Зменшувати при розкритті в пості']
  381.         },
  382.         minImgSize: [
  383.             'Миним. размер раскрытых картинок (px)',
  384.             'Minimal size for expanded images (px)',
  385.             'Мінім. розмір розгорнутих зображень (px)'],
  386.         zoomFactor: [
  387.             'Чувствительность зума картинок [1-100%]',
  388.             'Images zoom sensibility [1-100%]',
  389.             'Чутливість зуму зображень [1-100%]'],
  390.         webmControl: [
  391.             'Показывать контрол-бар для WebM',
  392.             'Show control bar for WebM',
  393.             'Показувати смугу керування для WebM'],
  394.         webmTitles: [
  395.             'Получать названия WebM из метаданных',
  396.             'Load titles from WebM metadata',
  397.             'Отримувати назви WebM з метаданих'],
  398.         webmVolume: [
  399.             'Громкость WebM по умолчанию [0-100%]',
  400.             'Default volume for WebM [0-100%]',
  401.             'Гучність WebM по замовчуванню [0-100%]'],
  402.         minWebmWidth: [
  403.             'Минимальная ширина WebM (px)',
  404.             'Minimal width for WebM (px)',
  405.             'Мінімальна ширина WebM (px)'],
  406.         preLoadImgs: {
  407.             sel: [
  408.                 ['Откл.', 'Все', 'Без WebM'],
  409.                 ['Disable', 'All', 'Non-WebM'],
  410.                 ['Вимк.', 'Всі', 'Крім WebM']],
  411.             txt: [
  412.                 'Предварительно загружать картинки*',
  413.                 'Preload images*',
  414.                 'Наперед завантажувати зображення*']
  415.         },
  416.         findImgFile: [
  417.             'Распознавать файлы, встроенные в картинках*',
  418.             'Detect embedded files in images*',
  419.             'Розпізнавати файли, що вбудовані в зображення*'],
  420.         openImgs: {
  421.             sel: [
  422.                 ['Откл.', 'Все подряд', 'Только GIF', 'Кроме GIF'],
  423.                 ['Disable', 'All types', 'Only GIF', 'Non-GIF'],
  424.                 ['Вимк.', 'Всі', 'Лише GIF', 'Крім GIF']],
  425.             txt: [
  426.                 'Заменять картинки на оригиналы*',
  427.                 'Replace thumbnails with original images*',
  428.                 'Замінювати зображення на оригінали*']
  429.         },
  430.         imgSrcBtns: [
  431.             'Добавлять кнопки "Поиск" для картинок',
  432.             'Add "Search" buttons for images',
  433.             'Додавати кнопки "Пошук" для зображень'],
  434.         imgNames: {
  435.             sel: [
  436.                 ['Не изменять', 'Настоящие (сокр.)', 'Скрывать', 'Настоящие (полные)'],
  437.                 ['Don`t change', 'Original (trunc.)', 'Hide', 'Original (full)'],
  438.                 ['Не змінювати', 'Справжні (скороч.)', 'Ховати', 'Справжні (повні)']],
  439.             txt: [
  440.                 'имена картинок',
  441.                 'filenames',
  442.                 'імена зображень']
  443.         },
  444.         maskVisib: [
  445.             'Видимость для NSFW-картинок [0-100%]',
  446.             'Visibility for NSFW images [0-100%]',
  447.             'Видимість для NSFW-зображень [0-100%]'],
  448.  
  449.         // "Links" tab
  450.         linksNavig: [
  451.             'Навигация постов по >>ссылкам* ',
  452.             'Posts navigation by >>links* ',
  453.             'Навігація постів по >>посиланнях* '],
  454.         linksOver: [
  455.             'Появление ',
  456.             'Appearance ',
  457.             'Поява '],
  458.         linksOut: [
  459.             'Пропадание (мс)',
  460.             'Disappearance (ms)',
  461.             'Зникнення (мс)'],
  462.         markViewed: [
  463.             'Помечать просмотренные посты',
  464.             'Mark viewed posts',
  465.             'Позначати переглянуті пости'],
  466.         strikeHidd: [
  467.             'Зачеркивать >>ссылки на скрытые посты',
  468.             'Strike >>links to hidden posts',
  469.             'Закреслювати >>посилання на сховані пости'],
  470.         removeHidd: [
  471.             'Также удалять из обратных >>ссылок',
  472.             'Also remove from >>backlinks',
  473.             'Також видаляти із зворотніх >>посилань'],
  474.         noNavigHidd: [
  475.             'Не отображать превью для скрытых постов',
  476.             'Donʼt show previews for hidden posts',
  477.             'Не показувати превʼю до cхованих постів'],
  478.         markMyLinks: [
  479.             'Помечать ссылки на мои посты как (You)',
  480.             'Mark links to my posts with (You)',
  481.             'Позначати посилання на мої пости як (You)'],
  482.         crossLinks: [
  483.             'Заменять http:// на >>/b/ссылки*',
  484.             'Replace http:// with >>/b/links*',
  485.             'Замінювати https:// на >>/b/посилання*'],
  486.         decodeLinks: [
  487.             'Декодировать %D0%A5%D1 в ссылках*',
  488.             'Decode %D0%A5%D1 in links*',
  489.             'Декодувати %D0%A5%D1 в посиланнях*'],
  490.         insertNum: [
  491.             'Вставлять >>ссылку по клику на №поста*',
  492.             'Insert >>link on №postnumber click*',
  493.             'Вставляти >>посилання на клік по №посту*'],
  494.         addOPLink: [
  495.             '>>ссылка при ответе на OP в списке тредов',
  496.             'Insert >>link when replying to OP on threads list',
  497.             '>>посилання при відповіді на OP у списці тредів'],
  498.         addImgs: [
  499.             'Загружать картинки к jpg/png/gif ссылкам*',
  500.             'Load images for jpg/png/gif links*',
  501.             'Додавати зображення до jpg/png/gif посилань*'],
  502.         addMP3: [
  503.             'Плеер к mp3 ссылкам* ',
  504.             'Player for mp3 links* ',
  505.             'Плеєр до mp3 посилань* '],
  506.         addVocaroo: [
  507.             'к Vocaroo ссылкам*',
  508.             'for Vocaroo links*',
  509.             'до Vocaroo посилань*'],
  510.         addVimeo: [
  511.             'Добавлять плеер к Vimeo ссылкам*',
  512.             'Add player for Vimeo links*',
  513.             'Додавати плеєр до Vimeo посилань*'],
  514.         embedYTube: {
  515.             sel: [
  516.                 ['Ничего', 'Превью+плеер', 'Плеер по клику'],
  517.                 ['Nothing', 'Preview+player', 'On click player'],
  518.                 ['Нічого', 'Превʼю+плеєр', 'Плеєр по кліку']],
  519.             txt: [
  520.                 'к YouTube ссылкам* ',
  521.                 'for YouTube links* ',
  522.                 'до YouTube посилань* ']
  523.         },
  524.         YTubeTitles: [
  525.             'Загружать названия к YouTube ссылкам*',
  526.             'Load titles for YouTube links*',
  527.             'Отримувати назви до YouTube посилань*'],
  528.         ytApiKey: [
  529.             'Ключ YT API*',
  530.             'YT API Key*',
  531.             'Ключ YT API*'],
  532.  
  533.         // "Form" tab
  534.         ajaxPosting: [
  535.             'Отправка постов без перезагрузки*',
  536.             'Posting without page refresh*',
  537.             'Постування без оновлення сторінки*'],
  538.         postSameImg: [
  539.             'Возможность отправки одинаковых картинок',
  540.             'Ability to post duplicate images',
  541.             'Можливість надсилання однакових зображень'],
  542.         removeEXIF: [
  543.             'Удалять EXIF из JPEG ',
  544.             'Remove EXIF from JPEG ',
  545.             'Видаляти EXIF з JPEG '],
  546.         removeFName: {
  547.             sel: [
  548.                 ['Не изменять', 'Удалять', 'Unixtime', 'Unixtime-random'],
  549.                 ['Don`t change', 'Clear', 'Unixtime', 'Unixtime-random'],
  550.                 ['Не змінювати', 'Видаляти', 'Unixtime', 'Unixtime-random']],
  551.             txt: [
  552.                 'имена файлов',
  553.                 'file names',
  554.                 'імена файлів']
  555.         },
  556.         sendErrNotif: [
  557.             'Оповещать в заголовке об ошибке отправки',
  558.             'Inform in title about post send error',
  559.             'Сповіщати в заголовку про помилку надсилання'],
  560.         scrAfterRep: [
  561.             'Перемещаться в конец треда после отправки',
  562.             'Scroll to bottom after reply',
  563.             'Гортати в кінець треду після надсилання'],
  564.         fileInputs: {
  565.             sel: [
  566.                 ['Откл.', 'Упрощ.', 'Превью'],
  567.                 ['Disable', 'Simple', 'Preview'],
  568.                 ['Вимкн.', 'Спрощене', 'Превʼю']],
  569.             txt: [
  570.                 'Улучшенное поле добавления файлов',
  571.                 'Enhanced file attachment field',
  572.                 'Покращене поле додавання файлів']
  573.         },
  574.         addPostForm: {
  575.             sel: [
  576.                 ['Сверху', 'Внизу', 'Скрытая'],
  577.                 ['At top', 'At bottom', 'Hidden'],
  578.                 ['Вгорі', 'Знизу', 'Прихована']],
  579.             txt: [
  580.                 'Форма ответа в треде',
  581.                 'Reply form display in thread',
  582.                 'Форма відповіді в треді']
  583.         },
  584.         spacedQuote: [
  585.             'Вставлять пробел при цитировании "> "',
  586.             'Insert a space when quoting "> "',
  587.             'Вставляти пробіл при цитуванні "> "'],
  588.         favOnReply: [
  589.             'Добавлять тред в "Избранное" после ответа',
  590.             'Add thread to "Favorites" after reply',
  591.             'Додавати тред в "Вибране" після відповіді'],
  592.         warnSubjTrip: [
  593.             'Оповещать о трипкоде в поле "Тема"',
  594.             'Warn about a tripcode in "Subject" field',
  595.             'Сповіщувати про трипкод в полі "Тема"'],
  596.         addSageBtn: [
  597.             'Кнопка Sage вместо поля "Email" ',
  598.             'Replace "Email" with Sage button ',
  599.             'Кнопка Sage замість "E-mail" '],
  600.         saveSage: [
  601.             'Помнить сажу',
  602.             'Remember sage',
  603.             'Памʼятати сажу'],
  604.         altCaptcha: [
  605.             'Использовать альтернативную капчу',
  606.             'Use alternative captcha',
  607.             'Використовувати альтернативну капчу'],
  608.         capUpdTime: [
  609.             'Интервал обновления капчи (сек)',
  610.             'Captcha update interval (sec)',
  611.             'Інтервал оновлення капчі (сек)'],
  612.         captchaLang: {
  613.             sel: [
  614.                 ['Откл.', 'Eng', 'Rus'],
  615.                 ['Disable', 'Eng', 'Rus'],
  616.                 ['Вимк.', 'Eng', 'Ukr']],
  617.             txt: [
  618.                 'Принудительный язык ввода капчи',
  619.                 'Forced captcha input language',
  620.                 'Примусова мова вводу капчі']
  621.         },
  622.         addTextBtns: {
  623.             sel: [
  624.                 ['Откл.', 'Графические', 'Упрощённые', 'Стандартные'],
  625.                 ['Disable', 'As images', 'As text', 'Standard'],
  626.                 ['Вимк.', 'Графічні', 'Спрощені', 'Стандартні']],
  627.             txt: [
  628.                 'Кнопки разметки текста ',
  629.                 'Text markup buttons ',
  630.                 'Кнопки розмітки тексту ']
  631.         },
  632.         txtBtnsLoc: [
  633.             'Внизу',
  634.             'At bottom',
  635.             'Знизу'],
  636.         userPassw: [
  637.             'Постоянный пароль',
  638.             'Fixed password',
  639.             'Постійний пароль'],
  640.         userName: [
  641.             'Постоянное имя',
  642.             'Fixed name',
  643.             'Постійне імʼя'],
  644.         noBoardRule: [
  645.             'Правила ',
  646.             'Rules ',
  647.             'Правила '],
  648.         noPassword: [
  649.             'Пароль ',
  650.             'Password ',
  651.             'Пароль '],
  652.         noName: [
  653.             'Имя ',
  654.             'Name ',
  655.             'Імʼя '],
  656.         noSubj: [
  657.             'Тему',
  658.             'Subject',
  659.             'Тему'],
  660.  
  661.         // "Common" tab
  662.         scriptStyle: {
  663.             sel: [
  664.                 ['Gradient darkblue', 'Gradient blue', 'Solid grey', 'Transparent blue', 'Square dark'],
  665.                 ['Gradient darkblue', 'Gradient blue', 'Solid grey', 'Transparent blue', 'Square dark'],
  666.                 ['Gradient darkblue', 'Gradient blue', 'Solid grey', 'Transparent blue', 'Square dark']],
  667.             txt: [
  668.                 'Стиль Dollchan',
  669.                 'Dollchan style',
  670.                 'Стиль Dollchan']
  671.         },
  672.         userCSS: [
  673.             'Пользовательский CSS',
  674.             'User CSS',
  675.             'Користувацький CSS'],
  676.         animation: [
  677.             'CSS3 анимация',
  678.             'CSS3 animation',
  679.             'CSS3 анімація'],
  680.         hotKeys: [
  681.             'Горячие клавиши',
  682.             'Hotkeys',
  683.             'Гарячі клавіші'],
  684.         loadPages: [
  685.             'Количество страниц, загружаемых по F5',
  686.             'Number of pages that are loaded on F5 ',
  687.             'Кількість сторінок, що завантажуються по F5'],
  688.         panelCounter: {
  689.             sel: [
  690.                 ['Откл.', 'Все посты', 'Без скрытых'],
  691.                 ['Disabled', 'All posts', 'Except hidden'],
  692.                 ['Вимкн.', 'Усі пости', 'Крім схованих']],
  693.             txt: [
  694.                 'Счетчик постов/картинок в треде',
  695.                 'Сounter for posts/images in thread',
  696.                 'Лічильник постів/зображень в треді']
  697.         },
  698.         rePageTitle: [
  699.             'Название треда в заголовке вкладки*',
  700.             'Show thread title in the page tab*',
  701.             'Назва треду в заголовку вкладки*'],
  702.         inftyScroll: [
  703.             'Бесконечная прокрутка страниц',
  704.             'Infinite scrolling for pages',
  705.             'Нескінченна прокрутка сторінок'],
  706.         hideReplies: [
  707.             'Показывать только OP в списке тредов*',
  708.             'Show only OP in threads list*',
  709.             'Показувати лише OP в списку тредів*'],
  710.         scrollToTop: [
  711.             'Всегда перемещаться вверх в списке тредов',
  712.             'Always scroll to top in the threads list',
  713.             'Завжди гортати догори в списку тредів'],
  714.         saveScroll: [
  715.             'Запоминать позицию скролла в тредах',
  716.             'Remember the scroll position in threads',
  717.             'Пам`ятати позицію скролла в тредах'],
  718.         favThrOrder: {
  719.             sel: [
  720.                 ['По номеру', 'По номеру (убыв)', 'По добавлению', 'По добавлению (убыв)'],
  721.                 ['By number', 'By number (desc)', 'By adding', 'By adding (desc)'],
  722.                 ['За номером', 'За номером (зменш)', 'По додаванню', 'По додаванню (зменш)']],
  723.             txt: [
  724.                 'Сортировка в Избранном',
  725.                 'Sorting in Favorites',
  726.                 'Сортування в Вибраному']
  727.         },
  728.         favWinOn: [
  729.             'Всегда открывать окно Избранное',
  730.             'Always open the Favorites window',
  731.             'Завжди відкривати вікно Вибране'],
  732.         closePopups: [
  733.             'Автоматически закрывать уведомления',
  734.             'Close popups automatically',
  735.             'Автоматично закривати сповіщення'],
  736.         updDollchan: {
  737.             sel: [
  738.                 ['Откл.', 'Каждый день', 'Каждые 2 дня', 'Каждую неделю', 'Каждые 2 недели', 'Каждый месяц'],
  739.                 ['Disable', 'Every day', 'Every 2 days', 'Every week', 'Every 2 weeks', 'Every month'],
  740.                 ['Вимкн.', 'Щодня', 'Кожні 2 дні', 'Щотижня', 'Кожні 2 тижні', 'Щомісяця']],
  741.             txt: [
  742.                 'Проверять обновления Dollchan',
  743.                 'Check for Dollchan updates',
  744.                 'Перевіряти оновлення Dollchan']
  745.         }
  746.     },
  747.  
  748.     // Main panel buttons: tooltips
  749.     panelBtn: {
  750.         attach: [
  751.             'Прикрепить/Открепить панель',
  752.             'Attach/Detach panel',
  753.             'Закріпити/відкріпити панель'],
  754.         cfg: [
  755.             'Настройки',
  756.             'Settings',
  757.             'Налаштування'],
  758.         hid: [
  759.             'Скрытое',
  760.             'Hidden',
  761.             'Сховане'],
  762.         fav: [
  763.             'Избранное',
  764.             'Favorites',
  765.             'Вибране'],
  766.         vid: [
  767.             'Ссылки на видео',
  768.             'Video links',
  769.             'Посилання на відео'],
  770.         refresh: [
  771.             'Обновить',
  772.             'Refresh',
  773.             'Оновити'],
  774.         goback: [
  775.             'Назад на доску',
  776.             'Return to board',
  777.             'Назад до дошки'],
  778.         gonext: [
  779.             'На %s страницу',
  780.             'Go to page %s',
  781.             'До %s сторінки'],
  782.         goup: [
  783.             'В начало страницы',
  784.             'Scroll to top',
  785.             'Прогорнути догори'],
  786.         godown: [
  787.             'В конец страницы',
  788.             'Scroll to bottom',
  789.             'Прогорнути донизу'],
  790.         expimg: [
  791.             'Раскрыть все картинки',
  792.             'Expand all images',
  793.             'Розгорнути всі зображення'],
  794.         maskimg: [
  795.             'Режим NSFW',
  796.             'NSFW mode',
  797.             'Режим NSFW'],
  798.         preimg: [
  799.             'Предзагрузить картинки\r\n([Ctrl+Click] только для новых постов)',
  800.             'Preload images\r\n([Ctrl+Click] for new posts only)',
  801.             'Наперед завантажити зображення\r\n([Ctrl+Click] лише для нових постів)'],
  802.         savethr: [
  803.             'Сохранить на диск',
  804.             'Save to disk',
  805.             'Зберегти на диск'],
  806.         'upd-on': [
  807.             'Выключить автообновление треда',
  808.             'Disable thread updater',
  809.             'Вимкнути оновлювач треду'],
  810.         'upd-off': [
  811.             'Включить автообновление треда',
  812.             'Enable thread updater',
  813.             'Увімкнути оновлювач треду'],
  814.         'audio-off': [
  815.             'Звуковое оповещение о новых постах',
  816.             'Sound notification about new posts',
  817.             'Звукове сповіщення про нові пости'],
  818.         catalog: [
  819.             'Перейти в каталог',
  820.             'Go to catalog',
  821.             'Перейти до каталогу'],
  822.         enable: [
  823.             'Включить/выключить Dollchan',
  824.             'Turn on/off the Dollchan',
  825.             'Увімкнути/вимкнути Dollchan'],
  826.         pcount: [
  827.             'Постов в треде',
  828.             'Posts in thread',
  829.             'Постів у треді'],
  830.         pcountNotHid: [
  831.             'Постов в треде (без скрытых)',
  832.             'Posts in thread (without hidden)',
  833.             'Постів у треді (крім схованих)'],
  834.         imglen: [
  835.             'Картинок в треде',
  836.             'Images in thread',
  837.             'Зображень у треді'],
  838.         posters: [
  839.             'Постящих в треде',
  840.             'Posters in thread',
  841.             'Постувачів у треді']
  842.     },
  843.  
  844.     // Post buttons: tooltips
  845.     togglePost: [
  846.         'Скрыть/Раскрыть пост',
  847.         'Hide/Unhide post',
  848.         'Сховати/показати пост'],
  849.     toggleThr: [
  850.         'Скрыть/Раскрыть тред',
  851.         'Hide/Unhide thread',
  852.         'Сховати/показати тред'],
  853.     replyToPost: [
  854.         'Ответить на пост',
  855.         'Reply to post',
  856.         'Відповісти на пост'],
  857.     replyToThr: [
  858.         'Ответить в тред',
  859.         'Reply to thread',
  860.         'Відповісти в тред'],
  861.     expandThr: [
  862.         'Развернуть тред',
  863.         'Expand thread',
  864.         'Розгорнути тред'],
  865.     addFav: [
  866.         'Добавить тред в Избранное',
  867.         'Add thread to Favorites',
  868.         'Додати тред в Вибране'],
  869.     delFav: [
  870.         'Убрать тред из Избранного',
  871.         'Remove thread from Favorites',
  872.         'Прибрати тред з Вибраного'],
  873.     attachPview: [
  874.         'Закрепить превью',
  875.         'Attach preview',
  876.         'Закріпити превʼю'],
  877.  
  878.     // Windows buttons: tooltips
  879.     closeWindow: [
  880.         'Закрыть окно',
  881.         'Close window',
  882.         'Закрити вікно'],
  883.     closeReply: [
  884.         'Закрыть форму',
  885.         'Close form',
  886.         'Закрити форму'],
  887.     toPanel: [
  888.         'Закрепить на панели',
  889.         'Attach to panel',
  890.         'Закріпити на панелі'],
  891.     makeDrag: [
  892.         'Сделать перетаскиваемым окном',
  893.         'Make draggable window',
  894.         'Зробити перетягуваним вікном'],
  895.     underPost: [
  896.         'Разместить форму после поста',
  897.         'Move form under post',
  898.         'Розмістити форму після посту'],
  899.     clearForm: [
  900.         'Очистить форму',
  901.         'Clear form',
  902.         'Очистити форму'],
  903.  
  904.     // Markup buttons: tooltips
  905.     txtBtn: [
  906.         ['Жирный', 'Bold', 'Жирний'],
  907.         ['Курсив', 'Italic', 'Курсив'],
  908.         ['Подчеркнутый', 'Underlined', 'Підкреслений'],
  909.         ['Зачеркнутый', 'Strike', 'Закреслений'],
  910.         ['Спойлер', 'Spoiler', 'Спойлер'],
  911.         ['Код', 'Code', 'Код'],
  912.         ['Верхний индекс', 'Superscript', 'Верхній індекс'],
  913.         ['Нижний индекс', 'Subscript', 'Нижній індекс'],
  914.         ['Цитировать выделенное', 'Quote selected', 'Цитувати виділене']],
  915.  
  916.     // Drop-down menus: options
  917.     selHiderMenu: { // "Hide" post button
  918.         sel: [
  919.             'Скрывать выделенное',
  920.             'Hide selected text',
  921.             'Ховати виділене'],
  922.         name: [
  923.             'Скрывать по имени',
  924.             'Hide by name',
  925.             'Ховати по імені'],
  926.         trip: [
  927.             'Скрывать по трипкоду',
  928.             'Hide by tripcode',
  929.             'Ховати по тріпкоду'],
  930.         img: [
  931.             'Скрывать по размеру картинки',
  932.             'Hide by image size',
  933.             'Ховати по розміру зображення'],
  934.         imgn: [
  935.             'Скрывать по имени картинки',
  936.             'Hide by image name',
  937.             'Ховати по імені зображення'],
  938.         ihash: [
  939.             'Скрывать схожие картинки',
  940.             'Hide by similar images',
  941.             'Ховати подібні зображення'],
  942.         noimg: [
  943.             'Скрывать без картинок',
  944.             'Hide without images',
  945.             'Ховати без зображень'],
  946.         notext: [
  947.             'Скрывать без текста',
  948.             'Hide without text',
  949.             'Ховати без тексту'],
  950.         text: [
  951.             'Скрыть схожий текст',
  952.             'Hide similar text',
  953.             'Сховати схожий текст'],
  954.         refs: [
  955.             'Скрыть с ответами',
  956.             'Hide with replies',
  957.             'Сховати з відповідями'],
  958.         refsonly: [
  959.             'Скрывать ответы',
  960.             'Hide replies',
  961.             'Ховати відповіді']
  962.     },
  963.     selExpandThr: [ // "Expand thread" post button
  964.         ['+10 постов', 'Последние 30', 'Последние 50', 'Последние 100', 'Весь тред'],
  965.         ['+10 posts', 'Last 30 posts', 'Last 50 posts', 'Last 100 posts', 'Entire thread'],
  966.         ['+10 постів', 'Останні 30', 'Останні 50', 'Останні 100', 'Весь тред']],
  967.     selAjaxPages: [ // "Refresh" panel button
  968.         ['1 страница', '2 страницы', '3 страницы', '4 страницы', '5 страниц'],
  969.         ['1 page', '2 pages', '3 pages', '4 pages', '5 pages'],
  970.         ['1 сторінка', '2 сторінки', '3 сторінки', '4 сторінки', '5 сторінок']],
  971.     selSaveThr: [ // "Save to disk" panel button
  972.         ['Скачать весь тред', 'Скачать картинки'],
  973.         ['Download thread', 'Download images'],
  974.         ['Завантажити весь тред', 'Завантажити зображення']],
  975.     selAudioNotif: [ // "Sound notification" panel button
  976.         ['Каждые 30 сек.', 'Каждую минуту', 'Каждые 2 мин.', 'Каждые 5 мин.'],
  977.         ['Every 30 sec.', 'Every minute', 'Every 2 min.', 'Every 5 min.'],
  978.         ['Кожні 30 сек.', 'Щохвилини', 'Кожні 2 хв.', 'Кожні 5 хв.']],
  979.     reportPost: [
  980.         'Жалоба на пост',
  981.         'Report post',
  982.         'Скарга на пост'],
  983.     reportThr: [
  984.         'Жалоба на тред',
  985.         'Report thread',
  986.         'Скарга на тред'],
  987.     markMyPost: [
  988.         'Пометить пост как мой',
  989.         'Mark post as mine',
  990.         'Відмітити пост як мій'
  991.     ],
  992.     deleteMyPost: [
  993.         'Убрать из моих постов',
  994.         'Delete from my posts',
  995.         'Прибрати з моїх постів'
  996.     ],
  997.  
  998.     // Sauce search for images and video frames
  999.     searchIn: [
  1000.         'Искать в ',
  1001.         'Search in ',
  1002.         'Шукати в '],
  1003.     frameSearch: [
  1004.         'Поиск кадра в ',
  1005.         'Frame search in ',
  1006.         'Пошук кадру в '],
  1007.     gotoResults: [
  1008.         'Перейти к результатам поиска',
  1009.         'Go to search results',
  1010.         'Перейти до результатів пошуку'],
  1011.     getFrameLinks: [
  1012.         'Получить ссылки для поиска этого кадра',
  1013.         'Get links to search this frame',
  1014.         'Отримати посилання для пошуку цього кадру'],
  1015.     saveFrame: [
  1016.         'Сохранить полученный кадр',
  1017.         'Save the received frame',
  1018.         'Зберегти отриманий кадр'],
  1019.     errSaucenao: [
  1020.         'Ошибка: не могу загрузить на saucenao.com',
  1021.         'Error: can`t load to saucenao.com',
  1022.         'Помилка: не можу завантажити на saucenao.com'],
  1023.  
  1024.     // Hotkeys editor
  1025.     hotKeyEdit: [[
  1026.         // Ru
  1027.         '%l%i24 – предыдущая страница/картинка%/l',
  1028.         '%l%i217 – следующая страница/картинка%/l',
  1029.         '%l%i21 – тред (на доске)/пост (в треде) ниже%/l',
  1030.         '%l%i20 – тред (на доске)/пост (в треде) выше%/l',
  1031.         '%l%i31 – пост (на доске) ниже%/l',
  1032.         '%l%i30 – пост (на доске) выше%/l',
  1033.         '%l%i23 – скрыть пост/тред%/l',
  1034.         '%l%i32 – перейти в тред%/l',
  1035.         '%l%i33 – развернуть тред%/l',
  1036.         '%l%i211 – раскрыть картинку в посте%/l',
  1037.         '%l%i22 – быстрый ответ%/l',
  1038.         '%l%i25t – отправить пост%/l',
  1039.         '%l%i210 – открыть/закрыть "Настройки"%/l',
  1040.         '%l%i26 – открыть/закрыть "Избранное"%/l',
  1041.         '%l%i27 – открыть/закрыть "Скрытое"%/l',
  1042.         '%l%i218 – открыть/закрыть "Видео"%/l',
  1043.         '%l%i28 – открыть/закрыть панель%/l',
  1044.         '%l%i29 – вкл./выкл. режим NSFW%/l',
  1045.         '%l%i40 – обновить тред (в треде)%/l',
  1046.         '%l%i212t – жирный%/l',
  1047.         '%l%i213t – курсив%/l',
  1048.         '%l%i214t – зачеркнутый%/l',
  1049.         '%l%i215t – спойлер%/l',
  1050.         '%l%i216t – код%/l'], [
  1051.         // En
  1052.         '%l%i24 – previous page/image%/l',
  1053.         '%l%i217 – next page/image%/l',
  1054.         '%l%i21 – thread (on board)/post (in thread) below%/l',
  1055.         '%l%i20 – thread (on board)/post (in thread) above%/l',
  1056.         '%l%i31 – on board post below%/l',
  1057.         '%l%i30 – on board post above%/l',
  1058.         '%l%i23 – hide post/thread%/l',
  1059.         '%l%i32 – go to thread%/l',
  1060.         '%l%i33 – expand thread%/l',
  1061.         '%l%i211 – expand postʼs images%/l',
  1062.         '%l%i22 – quick reply%/l',
  1063.         '%l%i25t – send post%/l',
  1064.         '%l%i210 – open/close "Settings"%/l',
  1065.         '%l%i26 – open/close "Favorites"%/l',
  1066.         '%l%i27 – open/close "Hidden"%/l',
  1067.         '%l%i218 – open/close "Videos"%/l',
  1068.         '%l%i28 – open/close main panel%/l',
  1069.         '%l%i29 – toggle NSFW mode%/l',
  1070.         '%l%i40 – update thread%/l',
  1071.         '%l%i212t – bold%/l',
  1072.         '%l%i213t – italic%/l',
  1073.         '%l%i214t – strike%/l',
  1074.         '%l%i215t – spoiler%/l',
  1075.         '%l%i216t – code%/l'], [
  1076.         // Ua
  1077.         '%l%i24 – попередня сторінка/зображення%/l',
  1078.         '%l%i217 – наступна сторінка/зображення%/l',
  1079.         '%l%i21 – тред (на дошці)/пост (в треді) нижче%/l',
  1080.         '%l%i20 – тред (на дошці)/пост (в треді) вище%/l',
  1081.         '%l%i31 – пост (на дошці) нижче%/l',
  1082.         '%l%i30 – пост (на дошці) вище%/l',
  1083.         '%l%i23 – приховати пост/тред%/l',
  1084.         '%l%i32 – перейти в тред%/l',
  1085.         '%l%i33 – розгорнути тред%/l',
  1086.         '%l%i211 – розгорнути зображення в пості%/l',
  1087.         '%l%i22 – швидка відповідь%/l',
  1088.         '%l%i25t – відправити пост%/l',
  1089.         '%l%i210 – відкрити/закрити "Налаштування"%/l',
  1090.         '%l%i26 – відкрити/закрити "Вибране"%/l',
  1091.         '%l%i27 – відкрити/закрити "Сховане"%/l',
  1092.         '%l%i218 – відкрити/закрити "Посилання на відео"%/l',
  1093.         '%l%i28 – відкрити/закрити панель%/l',
  1094.         '%l%i29 – увімкнути/вимкнути режим NSFW%/l',
  1095.         '%l%i40 – оновити тред (в треді)%/l',
  1096.         '%l%i212t – жирний%/l',
  1097.         '%l%i213t – курсив%/l',
  1098.         '%l%i214t – закреслений%/l',
  1099.         '%l%i215t – спойлер%/l',
  1100.         '%l%i216t – код%/l']],
  1101.  
  1102.     // Time correction in posts
  1103.     cTimeError: [
  1104.         'Неправильные настройки времени',
  1105.         'Invalid time settings',
  1106.         'Неправильні налаштування часу'],
  1107.     month: [
  1108.         ['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'],
  1109.         ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
  1110.         ['січ', 'лют', 'бер', 'кві', 'тра', 'чер', 'лип', 'сер', 'вер', 'жов', 'лис', 'гру']],
  1111.     fullMonth: [
  1112.         ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
  1113.             'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'],
  1114.         ['January', 'February', 'March', 'April', 'May', 'June',
  1115.             'July', 'August', 'September', 'October', 'November', 'December'],
  1116.         ['січня', 'лютого', 'березня', 'квітня', 'травня', 'червня',
  1117.             'липня', 'серпня', 'вересня', 'жовтня', 'листопада', 'грудня']],
  1118.     week: [
  1119.         ['Вск', 'Пнд', 'Втр', 'Срд', 'Чтв', 'Птн', 'Сбт'],
  1120.         ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
  1121.         ['Нед', 'Пон', 'Вів', 'Сер', 'Чет', 'Птн', 'Сбт']],
  1122.     monthDict: {
  1123.         /* eslint-disable */
  1124.         янв: 0, фев: 1, мар: 2, апр: 3, май: 4, мая: 4, июн: 5, июл: 6, авг: 7, сен: 8, окт: 9, ноя: 10, дек: 11,
  1125.         jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
  1126.         січ: 0, лют: 1, бер: 2, кві: 3, тра: 4, чер: 5, лип: 6, сер: 7, вер: 8, жов: 9, лис: 10, гру: 11
  1127.         /* eslint-enable */
  1128.     },
  1129.  
  1130.     // Spells: popups
  1131.     seSyntaxErr: [
  1132.         'синтаксическая ошибка в аргументе спелла: %s',
  1133.         'syntax error in argument of spell: %s',
  1134.         'синтаксична помилка в аргументі спеллу: %s'],
  1135.     seUnknown: [
  1136.         'неизвестный спелл: %s',
  1137.         'unknown spell: %s',
  1138.         'невідомий спелл: %s'],
  1139.     seMissOp: [
  1140.         'пропущен оператор',
  1141.         'missing operator',
  1142.         'пропущено оператор'],
  1143.     seMissArg: [
  1144.         'пропущен аргумент спелла: %s',
  1145.         'missing argument of spell: %s',
  1146.         'пропущено аргумент спеллу: %s'],
  1147.     seMissSpell: [
  1148.         'пропущен спелл',
  1149.         'missing spell',
  1150.         'пропущено спелл'],
  1151.     seErrRegex: [
  1152.         'синтаксическая ошибка в регулярном выражении: %s',
  1153.         'syntax error in regular expression: %s',
  1154.         'синтаксична помилка в регулярному виразі: %s'],
  1155.     seUnexpChar: [
  1156.         'неожиданный символ: %s',
  1157.         'unexpected character: %s',
  1158.         'неочікуваний символ: %s'],
  1159.     seMissClBkt: [
  1160.         'пропущена закрывающая скобка',
  1161.         "missing ')' in expression",
  1162.         'пропущено закривну дужку'],
  1163.     seRepsInParens: [
  1164.         'спелл %s не должен располагаться в скобках',
  1165.         'spell %s shouldnʼt be inside parentheses',
  1166.         'спелл %s не може бути в дужках'],
  1167.     seOpInReps: [
  1168.         'недопустимо использовать оператор %s со спеллами #rep и #outrep',
  1169.         'donʼt use operator %s with spells #rep & #outrep',
  1170.         'неприпустимо використовувати оператор %s зі спеллами #rep и #outrep'],
  1171.     seRow: [
  1172.         ' (строка ',
  1173.         ' (row ',
  1174.         ' (рядок '],
  1175.     seCol: [
  1176.         ', столбец ',
  1177.         ', column ',
  1178.         ', стовпчик '],
  1179.  
  1180.     // Data editor
  1181.     editInTxt: [
  1182.         'Правка в текстовом формате',
  1183.         'Edit in text format',
  1184.         'Правка в текстовому форматі'],
  1185.     editor: {
  1186.         cfg: [
  1187.             'Редактирование настроек',
  1188.             'Edit settings',
  1189.             'Редагування налаштувань'],
  1190.         hidden: [
  1191.             'Редактирование скрытых тредов',
  1192.             'Edit hidden threads',
  1193.             'Редагування схованих тредів'],
  1194.         favor: [
  1195.             'Редактирование избранного',
  1196.             'Edit favorites',
  1197.             'Редагування вибраного'],
  1198.         css: [
  1199.             'Редактирование CSS',
  1200.             'Edit CSS',
  1201.             'Редагування CSS']
  1202.     },
  1203.  
  1204.     // Settings import/export/clearing
  1205.     fileImpExp: [
  1206.         'Импорт/экспорт настроек в файл',
  1207.         'Import/export config to file',
  1208.         'Імпорт/експорт налаштувань до файлу'],
  1209.     fileToData: [
  1210.         'Загрузить данные из файла',
  1211.         'Load data from a file',
  1212.         'Завантажити дані з файла'],
  1213.     dataToFile: [
  1214.         'Получить файл</a> с данными',
  1215.         'Get the file</a> with data',
  1216.         'Отримати файл</a> з даними'],
  1217.     globalCfg: [
  1218.         'Глобальные настройки',
  1219.         'Global config',
  1220.         'Глобальні налаштування'],
  1221.     loadGlobal: [
  1222.         'и применить к этому домену',
  1223.         'and apply to this domain',
  1224.         'і застосувати до цього домену'],
  1225.     saveGlobal: [
  1226.         'текущие настройки как глобальные',
  1227.         'current config as global',
  1228.         'поточні налаштування як глобальні'],
  1229.     descrGlobal: [
  1230.         'Глобальные настройки применяются по умолчанию<br>при первом посещении других доменов',
  1231.         'Global config is applied by default<br>on the first visit of other domains',
  1232.         'Глобальні налаштування застосовуються по замовчуванню<br>під час першого відвідання інших доменів'],
  1233.     resetCfg: [
  1234.         'Сбросить в настройки по умолчанию',
  1235.         'Reset config to defaults',
  1236.         'Скинути в налаштування по замовчуванню'],
  1237.     resetData: [
  1238.         'Очистить выбранные данные',
  1239.         'Reset selected data',
  1240.         'Очистити обрані дані'],
  1241.     allDomains: [
  1242.         'для всех доменов',
  1243.         'for all domains',
  1244.         'для всіх доменів'],
  1245.     delEntries: [
  1246.         'Удалить выбранные записи',
  1247.         'Delete selected entries',
  1248.         'Видалити обрані записи'],
  1249.     saveChanges: [
  1250.         'Сохранить внесенные изменения',
  1251.         'Save your changes',
  1252.         'Зберегти внесені зміни'],
  1253.     hidPostThr: [
  1254.         'Скрытые посты и треды',
  1255.         'Hidden posts and threads',
  1256.         'Сховані пости та треди'],
  1257.     myPosts: [
  1258.         'Мои посты',
  1259.         'My posts',
  1260.         'Мої пости'],
  1261.  
  1262.     // Settings window: Common/Info tab
  1263.     checkNow: [
  1264.         'Проверить сейчас',
  1265.         'Check now',
  1266.         'Перевірити зараз'],
  1267.     updAvail: [
  1268.         'Доступно обновление Dollchan: %s',
  1269.         'Dollchan update available: %s!',
  1270.         'Доступне оновлення Dollchan: %s'],
  1271.     newCommitsAvail: [
  1272.         'Обнаружены новые исправления: %s',
  1273.         'New fixes detected: %s',
  1274.         'Виявлено нові виправлення: %s'],
  1275.     changeLog: [
  1276.         'Список изменений',
  1277.         'List of changes',
  1278.         'Список змін'],
  1279.     haveLatestStable: [
  1280.         'Ваша версия %s является последней из стабильных.',
  1281.         'Your %s version is the latest from stable versions.',
  1282.         'Ваша версія %s є останньою зі стабільних.'],
  1283.     haveLatestCommit: [
  1284.         'Ваша версия %s содержит последние исправления.',
  1285.         'Your %s version contains all the latest fixes.',
  1286.         'Ваша версія %s містить всі останні виправлення.'],
  1287.     thrViewed: [
  1288.         'Тредов посещено',
  1289.         'Threads visited',
  1290.         'Тредів відвідано'],
  1291.     thrCreated: [
  1292.         'Тредов создано',
  1293.         'Threads created',
  1294.         'Тредів створено'],
  1295.     thrHidden: [
  1296.         'Тредов скрыто',
  1297.         'Threads hidden',
  1298.         'Тредів сховано'],
  1299.     postsSent: [
  1300.         'Постов отправлено',
  1301.         'Posts sent',
  1302.         'Постів надіслано'],
  1303.     total: [
  1304.         'Всего',
  1305.         'Total',
  1306.         'Всього'],
  1307.     debug: [
  1308.         'Отладка',
  1309.         'Debug',
  1310.         'Відлагодження'],
  1311.     infoDebug: [
  1312.         'Информация для отладки',
  1313.         'Information for debugging',
  1314.         'Інформація для відлагодження'],
  1315.  
  1316.     // Favorites window: tooltips
  1317.     infoCount: [
  1318.         'Обновить счетчики постов',
  1319.         'Refresh posts counters',
  1320.         'Оновити лічильники постів'],
  1321.     infoPage: [
  1322.         'Проверить положение тредов (до 10-й страницы)',
  1323.         'Check for threads position (up to 10th page)',
  1324.         'Перевірити актуальність тредів (до 10 сторінки)'],
  1325.     clrDeleted: [
  1326.         'Очистить недоступные (404) треды',
  1327.         'Clear inaccessible (404) threads',
  1328.         'Очистити недоступні (404) треди'],
  1329.     oldPosts: [
  1330.         'Постов при последнем посещении',
  1331.         'Posts at the last visit',
  1332.         'Постів під час останнього відвідування'],
  1333.     newPosts: [
  1334.         'Количество новых постов',
  1335.         'Number of new posts',
  1336.         'Кількість нових постів'],
  1337.     myPostsRep: [
  1338.         'Ответов на ваши посты',
  1339.         'Replies to your posts',
  1340.         'Відповідей на ваші пости'],
  1341.     thrPage: [
  1342.         'Тред на @странице',
  1343.         'Thread on @page',
  1344.         'Тред на @сторінці'],
  1345.     goToThread: [
  1346.         'Перейти к треду',
  1347.         'Go to the thread',
  1348.         'Перейти до треду'],
  1349.     goToBoard: [
  1350.         'Перейти к доске',
  1351.         'Go to the board',
  1352.         'Перейти до дошки'],
  1353.     toggleEntries: [
  1354.         'Скрыть/раскрыть записи',
  1355.         'Hide/expand entries',
  1356.         'Сховати/розкрити записи'],
  1357.  
  1358.     // Video links: tooltips
  1359.     hideLnkList: [
  1360.         'Скрыть/Показать список ссылок',
  1361.         'Hide/Unhide list of links',
  1362.         'Сховати/показати перелік посилань'],
  1363.     expandVideo: [
  1364.         'Развернуть/Свернуть видео',
  1365.         'Expand/Collapse video',
  1366.         'Розгорнути/згорнути відео'],
  1367.     prevVideo: [
  1368.         'Предыдущее видео',
  1369.         'Previous video',
  1370.         'Попереднє відео'],
  1371.     nextVideo: [
  1372.         'Следующее видео',
  1373.         'Next video',
  1374.         'Наступне відео'],
  1375.     duration: [
  1376.         'Продолжительность: ',
  1377.         'Duration: ',
  1378.         'Тривалість: '],
  1379.     published: [
  1380.         'опубликовано: ',
  1381.         'published: ',
  1382.         'опубліковано: '],
  1383.     author: [
  1384.         'Автор: ',
  1385.         'Author: ',
  1386.         'Автор: '],
  1387.     views: [
  1388.         'просмотров: ',
  1389.         'views: ',
  1390.         'переглядів: '],
  1391.  
  1392.     // Postform file inputs: tooltips
  1393.     pasteImage: [
  1394.         'Ctrl+V - вставить картинку из буфера',
  1395.         'Ctrl+V - paste an image from clipboard',
  1396.         'Ctrl+V - додати зображення з буферу'],
  1397.     dropFileHere: [
  1398.         'Бросьте сюда файл(ы) или ссылку',
  1399.         'Drop file(s) or link here',
  1400.         'Киньте сюди файл(и) чи посилання'],
  1401.     youCanDrag: [
  1402.         'Можно перетаскивать картинки и ссылки на файлы\r\nпрямо со страницы или других сайтов',
  1403.         'You can drag images and file links\r\ndirectly from the page or other sites',
  1404.         'Можна перетягувати зображення чи посилання на файли\r\nбезпосередньо зі сторінки чи інших сайтів'],
  1405.     removeFile: [
  1406.         'Удалить файл',
  1407.         'Remove file',
  1408.         'Видалити файл'],
  1409.     renameFile: [
  1410.         'Переименовать файл',
  1411.         'Rename file',
  1412.         'Перейменувати файл'],
  1413.     spoilFile: [
  1414.         'Спойлер',
  1415.         'Spoiler',
  1416.         'Спойлер'],
  1417.     addManually: [
  1418.         'Ввести ссылку на файл вручную',
  1419.         'Enter a link to the file manually',
  1420.         'Ввести посилання на файл вручну'],
  1421.     enterTheLink: [
  1422.         "Введите ссылку и нажмите '+'",
  1423.         "Enter the link and click '+'",
  1424.         "Введіть посилання та натисніть '+'"],
  1425.     helpAddFile: [
  1426.         'Встроить ogg/rar/zip/7z в картинку',
  1427.         'Embed ogg/rar/zip/7z into the image',
  1428.         'Вбудувати ogg/rar/zip/7z в зображення'],
  1429.  
  1430.     // Post images: tooltips
  1431.     expImgInline: [
  1432.         '[Click] открыть в посте, [Ctrl+Click] по центру',
  1433.         '[Click] expand in post, [Ctrl+Click] by center',
  1434.         '[Click] розгорнути в пості, [Ctrl+Click] в центрі'],
  1435.     expImgFull: [
  1436.         '[Click] открыть по центру, [Ctrl+Click] в посте',
  1437.         '[Click] expand by center, [Ctrl+Click] in post',
  1438.         '[Click] розгорнути в центрі, [Ctrl+Click] в пості'],
  1439.     nextImg: [
  1440.         'Следующая картинка',
  1441.         'Next image',
  1442.         'Наступне зображення'],
  1443.     prevImg: [
  1444.         'Предыдущая картинка',
  1445.         'Previous image',
  1446.         'Попереднє зображення'],
  1447.     rotateImg: [
  1448.         'Повернуть вправо',
  1449.         'Rotate right',
  1450.         'Повернути вправо'],
  1451.     autoPlayOn: [
  1452.         'Автоматически воспроизводить следующее видео',
  1453.         'Automatically play the next video',
  1454.         'Автоматично відтворювати наступне відео'],
  1455.     autoPlayOff: [
  1456.         'Отключить автовоспроизведение',
  1457.         'Disable autoplay',
  1458.         'Відключити автовідтворення'],
  1459.     downloadFile: [
  1460.         'Скачать содержащийся в картинке файл',
  1461.         'Download embedded file from the image',
  1462.         'Завантажити файл, що міститься в зображенні'],
  1463.     openOriginal: [
  1464.         'Открыть оригинал в новой вкладке',
  1465.         'Open the original image in new tab',
  1466.         'Відкрити оригінал в новій вкладці'],
  1467.  
  1468.     // Threads/images download: popups
  1469.     loadImage: [
  1470.         'Загружаются картинки',
  1471.         'Loading images',
  1472.         'Завантажуються зображення'],
  1473.     loadFile: [
  1474.         'Загружаются файлы',
  1475.         'Loading files',
  1476.         'Завантажуються файли'],
  1477.     cantLoad: [
  1478.         'Не могу загрузить',
  1479.         'Canʼt load',
  1480.         'Не можу завантажити'],
  1481.     willSavePview: [
  1482.         'Будет сохранено превью',
  1483.         'Thumbnail will be saved',
  1484.         'Буде збережено превʼю'],
  1485.     loadErrors: [
  1486.         'Во время загрузки произошли ошибки:',
  1487.         'An error occurred during the loading:',
  1488.         'Під час завантаження сталися помилки:'],
  1489.  
  1490.     // Ajax: popups
  1491.     succDeleted: [
  1492.         'Успешно удалено!',
  1493.         'Succesfully deleted!',
  1494.         'Успішно видалено!'],
  1495.     succReported: [
  1496.         'Жалоба успешно отправлена',
  1497.         'Succesfully reported',
  1498.         'Скарга успішно відправлена'],
  1499.     errDelete: [
  1500.         'Не могу удалить',
  1501.         'Canʼt delete',
  1502.         'Не можу видалити'],
  1503.     fileCorrupt: [
  1504.         'Файл повреждён',
  1505.         'File is corrupt',
  1506.         'Файл пошкоджено'],
  1507.     errCorruptData: [
  1508.         'Ошибка: сервер отправил повреждённые данные',
  1509.         'Error: server sent corrupted data',
  1510.         'Помилка: сервер надіслав пошкоджені дані'],
  1511.     noConnect: [
  1512.         'Ошибка подключения',
  1513.         'Connection failed',
  1514.         'Помилка зʼєднання'],
  1515.     thrNotFound: [
  1516.         'Тред недоступен',
  1517.         'Thread is unavailable',
  1518.         'Тред недоступний'],
  1519.     thrClosed: [
  1520.         'Тред закрыт',
  1521.         'Thread is closed',
  1522.         'Тред закрито'],
  1523.     thrArchived: [
  1524.         'Тред в архиве',
  1525.         'Thread is archived',
  1526.         'Тред заархівовано'],
  1527.  
  1528.     // Other warnings
  1529.     internalError: [
  1530.         'Внутренняя ошибка:\n',
  1531.         'Internal error:\n',
  1532.         'Внутрішня помилка:\n'],
  1533.     postNotFound: [
  1534.         'Пост не найден',
  1535.         'Post not found',
  1536.         'Пост не знайдено'],
  1537.     noHidThr: [
  1538.         'Нет скрытых тредов…',
  1539.         'No hidden threads…',
  1540.         'Немає схованих постів…'],
  1541.     noFavThr: [
  1542.         'Нет избранных тредов…',
  1543.         'Favorites is empty…',
  1544.         'Немає вибраних тредів…'],
  1545.     noVideoLinks: [
  1546.         'Нет ссылок на видео…',
  1547.         'No video links…',
  1548.         'Немає посилань на відео…'],
  1549.     invalidData: [
  1550.         'Некорректный формат данных',
  1551.         'Incorrect data format',
  1552.         'Некоректний формат даних'],
  1553.     noGlobalCfg: [
  1554.         'Глобальные настройки не найдены',
  1555.         'Global config not found',
  1556.         'Глобальні налаштування не знайдено'],
  1557.     subjHasTrip: [
  1558.         'Поле "Тема" содержит трипкод!',
  1559.         '"Subject" field contains a tripcode!',
  1560.         'Поле "Тема" містить трипкод!'],
  1561.     errMsEdgeWebm: [
  1562.         'Загрузите скрипт для воспроизведения WebM (VP9/Opus)',
  1563.         'Please load a script to play WebM (VP9/Opus)',
  1564.         'Завантажте скрипт для відтворення WebM (VP9/Opus)'],
  1565.     errFormLoad: [
  1566.         'Не удаётся загрузить форму ответа',
  1567.         'Can`t load the reply form',
  1568.         'Не вдалося завантажити форму відповіді'
  1569.     ],
  1570.  
  1571.     // Single words
  1572.     second    : ['с', 's', 'с'],
  1573.     sizeByte  : [' Байт', ' Byte', ' Байт'],
  1574.     sizeKByte : [' КБ', ' KB', ' КБ'],
  1575.     sizeMByte : [' МБ', ' MB', ' МБ'],
  1576.     sizeGByte : [' ГБ', ' GB', ' ГБ'],
  1577.     name      : ['Имя', 'Name', 'Імʼя'],
  1578.     subj      : ['Тема', 'Subject', 'Тема'],
  1579.     mail      : ['Почта', 'Email', 'Пошта'],
  1580.     video     : ['Видео', 'Video', 'Відео'],
  1581.     cap       : ['Капча', 'Captcha', 'Капча'],
  1582.     add       : ['Добавить', 'Add', 'Додати'],
  1583.     apply     : ['Применить', 'Apply', 'Застосувати'],
  1584.     cancel    : ['Отмена', 'Cancel', 'Скасувати'],
  1585.     clear     : ['Очистить', 'Clear', 'Очистити'],
  1586.     refresh   : ['Обновить', 'Refresh', 'Оновити'],
  1587.     save      : ['Сохранить', 'Save', 'Зберегти'],
  1588.     load      : ['Загрузить', 'Load', 'Завантажити'],
  1589.     edit      : ['Правка', 'Edit', 'Правка'],
  1590.     file      : ['Файл', 'File', 'Файл'],
  1591.     global    : ['Глобальные', 'Global', 'Глобальні'],
  1592.     reset     : ['Сброс', 'Reset', 'Скинути'],
  1593.     remove    : ['Удалить', 'Remove', 'Видалити'],
  1594.     change    : ['Сменить', 'Change', 'Змінити'],
  1595.     page      : ['Страница', 'Page', 'Сторінка'],
  1596.     reply     : ['Ответ', 'Reply', 'Відповідь'],
  1597.     replies   : ['Ответы:', 'Replies:', 'Відповіді:'],
  1598.     makeReply : ['Ответить', 'Reply', 'Відповісти'],
  1599.     error     : ['Ошибка', 'Error', 'Помилка'],
  1600.     loading   : ['Загрузка…', 'Loading…', 'Завантаження…'],
  1601.     sending   : ['Отправка…', 'Sending…', 'Надсилання…'],
  1602.     checking  : ['Проверка…', 'Checking…', 'Перевірка…'],
  1603.     updating  : ['Обновление…', 'Updating…', 'Оновлення…'],
  1604.     deleting  : ['Удаление…', 'Deleting…', 'Видалення…'],
  1605.     deleted   : ['удалён', 'deleted', 'видалено'],
  1606.     hide      : ['Скрыть: ', 'Hide: ', 'Сховати: '],
  1607.  
  1608.     // Miscellaneous
  1609.     hidePosts: [
  1610.         'Скрыть посты',
  1611.         'Hide posts',
  1612.         'Сховати пости'],
  1613.     showPosts: [
  1614.         'Показать посты',
  1615.         'Show posts',
  1616.         'Показати пости'],
  1617.     getNewPosts: [
  1618.         'Получить новые посты',
  1619.         'Get new posts',
  1620.         'Отримати нові пости'],
  1621.     makeThr: [
  1622.         'Создать тред',
  1623.         'Create thread',
  1624.         'Створити тред'],
  1625.     collapseThr: [
  1626.         'Свернуть тред',
  1627.         'Collapse thread',
  1628.         'Згорнути тред'],
  1629.     hiddenThr: [
  1630.         'Скрытый тред',
  1631.         'Hidden thread',
  1632.         'Схований тред'],
  1633.     hideForm: [
  1634.         'Скрыть форму',
  1635.         'Hide form',
  1636.         'Сховати форму'],
  1637.     noSage: [
  1638.         'Без сажи',
  1639.         'No sage',
  1640.         'Без сажі'],
  1641.     postsOmitted: [
  1642.         'Пропущено ответов: ',
  1643.         'Posts omitted: ',
  1644.         'Пропущено відповідей: '],
  1645.     newPost: [
  1646.         ['новый пост', 'новых поста', 'новых постов'],
  1647.         ['new post', 'new posts', 'new posts'],
  1648.         ['новий пост', 'нових пости', 'нових постів']],
  1649.     youReplies: [
  1650.         ['ответ Вам', 'ответа Вам', 'ответов Вам'],
  1651.         ['reply to You', 'replies to You', 'replies to You'],
  1652.         ['відповідь Вам', 'відповіді Вам', 'відповідей Вам']],
  1653.     latestPost: [
  1654.         'Последний пост',
  1655.         'Latest post',
  1656.         'Останній пост'],
  1657.     donateMsg: [
  1658.         '<b>Спасибо за использование Dollchan Extension!</b><br>Вы можете поддержать проект пожертвованием',
  1659.         '<b>Thank You for using Dollchan Extension!</b><br>You can support the project by donating',
  1660.         '<b>Дякуємо за використання Dollchan Extension!</b><br>Ви можете підтримати проект пожертвою'],
  1661.     firefoxAddon: [
  1662.         'Firefox аддон</a> доступен!',
  1663.         'Firefox add-on</a> is available!',
  1664.         'Firefox аддон</a> доступний!']
  1665. };
  1666.  
  1667. /* ==[ GlobalVars.js ]== */
  1668.  
  1669. const doc = deWindow.document;
  1670. const emptyFn = Function.prototype;
  1671. const aProto = Array.prototype;
  1672. const gitWiki = 'https://github.com/SthephanShinkufag/Dollchan-Extension-Tools/wiki/';
  1673. const gitRaw = 'https://raw.githubusercontent.com/SthephanShinkufag/Dollchan-Extension-Tools/master/';
  1674.  
  1675. let $each, aib, Cfg, docBody, dTime, dummy, isExpImg, isPreImg, lang, locStorage, nav, needScroll, pByEl,
  1676.     pByNum, pr, sesStorage, updater;
  1677. let quotetxt = '';
  1678. let visPosts = 2;
  1679. let topWinZ = 10;
  1680.  
  1681. /* ==[ Utils.js ]=============================================================================================
  1682.                                                     UTILS
  1683. =========================================================================================================== */
  1684.  
  1685. // DOM SEARCH
  1686.  
  1687. const $Q = (path, root = docBody) => root.querySelectorAll(path);
  1688.  
  1689. const $q = (path, root = docBody) => root.querySelector(path);
  1690.  
  1691. const $id = id => doc.getElementById(id);
  1692.  
  1693. function $parent(el, tagName) {
  1694.     do {
  1695.         el = el.parentElement;
  1696.     } while(el && el.tagName !== tagName);
  1697.     return el;
  1698. }
  1699.  
  1700. function $qParent(el, path) {
  1701.     do {
  1702.         el = el.parentElement;
  1703.     } while(el && !nav.matchesSelector(el, path));
  1704.     return el;
  1705. }
  1706.  
  1707. // DOM MODIFIERS
  1708.  
  1709. function $before(el, node) {
  1710.     el.parentNode.insertBefore(node, el);
  1711. }
  1712.  
  1713. function $after(el, node) {
  1714.     const nextEl = el.nextSibling;
  1715.     if(nextEl) {
  1716.         el.parentNode.insertBefore(node, nextEl);
  1717.     } else {
  1718.         el.parentNode.appendChild(node);
  1719.     }
  1720. }
  1721.  
  1722. function $bBegin(sibling, html) {
  1723.     sibling.insertAdjacentHTML('beforebegin', html);
  1724.     return sibling.previousSibling;
  1725. }
  1726.  
  1727. function $aBegin(parent, html) {
  1728.     parent.insertAdjacentHTML('afterbegin', html);
  1729.     return parent.firstChild;
  1730. }
  1731.  
  1732. function $bEnd(parent, html) {
  1733.     parent.insertAdjacentHTML('beforeend', html);
  1734.     return parent.lastChild;
  1735. }
  1736.  
  1737. function $aEnd(sibling, html) {
  1738.     sibling.insertAdjacentHTML('afterend', html);
  1739.     return sibling.nextSibling;
  1740. }
  1741.  
  1742. function $replace(origEl, newEl) {
  1743.     if(typeof newEl === 'string') {
  1744.         origEl.insertAdjacentHTML('afterend', newEl);
  1745.         origEl.remove();
  1746.     } else {
  1747.         origEl.parentNode.replaceChild(newEl, origEl);
  1748.     }
  1749. }
  1750.  
  1751. function $del(el) {
  1752.     if(el) {
  1753.         el.remove();
  1754.     }
  1755. }
  1756.  
  1757. function $delAll(path, root = docBody) {
  1758.     $each(root.querySelectorAll(path, root), el => el.remove());
  1759. }
  1760.  
  1761. function $add(html) {
  1762.     dummy.innerHTML = html;
  1763.     return dummy.firstElementChild;
  1764. }
  1765.  
  1766. const $txt = el => doc.createTextNode(el);
  1767.  
  1768. // TODO: Get rid of this function and paste buttons in html
  1769. function $btn(val, ttl, fn, className = 'de-button') {
  1770.     const el = doc.createElement('input');
  1771.     el.type = 'button';
  1772.     el.className = className;
  1773.     el.value = val;
  1774.     el.title = ttl;
  1775.     el.addEventListener('click', fn);
  1776.     return el;
  1777. }
  1778.  
  1779. function $script(text) {
  1780.     // We can't insert scripts directly as html
  1781.     const el = doc.createElement('script');
  1782.     el.type = 'text/javascript';
  1783.     el.textContent = text;
  1784.     doc.head.appendChild(el).remove();
  1785. }
  1786.  
  1787. function $css(text) {
  1788.     if(nav.isSafari && !('flex' in docBody.style)) {
  1789.         text = text.replace(/(transform|transition|flex|align-items)/g, ' -webkit-$1');
  1790.     }
  1791.     return $bEnd(doc.head, `<style type="text/css">${ text }</style>`);
  1792. }
  1793.  
  1794. function $DOM(html) {
  1795.     const myDoc = doc.implementation.createHTMLDocument('');
  1796.     myDoc.documentElement.innerHTML = html;
  1797.     return myDoc;
  1798. }
  1799.  
  1800. // CSS UTILS
  1801.  
  1802. function $toggle(el, needToShow = el.style.display) {
  1803.     if(needToShow) {
  1804.         el.style.removeProperty('display');
  1805.     } else {
  1806.         el.style.display = 'none';
  1807.     }
  1808. }
  1809.  
  1810. function $show(el) {
  1811.     el.style.removeProperty('display');
  1812. }
  1813.  
  1814. function $hide(el) {
  1815.     el.style.display = 'none';
  1816. }
  1817.  
  1818. function $animate(el, cName, isRemove = false) {
  1819.     el.addEventListener('animationend', function aEvent() {
  1820.         el.removeEventListener('animationend', aEvent);
  1821.         if(isRemove) {
  1822.             el.remove();
  1823.         } else {
  1824.             el.classList.remove(cName);
  1825.         }
  1826.     });
  1827.     el.classList.add(cName);
  1828. }
  1829.  
  1830. // Checks the validity of the user inputted color
  1831. function checkCSSColor(color) {
  1832.     if(!color || color === 'inherit' || color === 'currentColor') {
  1833.         return false;
  1834.     }
  1835.     if(color === 'transparent') {
  1836.         return true;
  1837.     }
  1838.     const image = doc.createElement('img');
  1839.     image.style.color = 'rgb(0, 0, 0)';
  1840.     image.style.color = color;
  1841.     if(image.style.color !== 'rgb(0, 0, 0)') {
  1842.         return true;
  1843.     }
  1844.     image.style.color = 'rgb(255, 255, 255)';
  1845.     image.style.color = color;
  1846.     return image.style.color !== 'rgb(255, 255, 255)';
  1847. }
  1848.  
  1849. // OTHER UTILS
  1850.  
  1851. const pad2 = i => (i < 10 ? '0' : '') + i;
  1852.  
  1853. const arrTags = (arr, start, end) => start + arr.join(end + start) + end;
  1854.  
  1855. const fixBrd = b => `/${ b }${ b ? '/' : '' }`;
  1856.  
  1857. const getAbsLink = url => (
  1858.     url[1] === '/' ? aib.prot :
  1859.     url[0] === '/' ? aib.prot + '//' + aib.host : '') + url;
  1860.  
  1861. // Prepares a string to be used as a new RegExp argument
  1862. const quoteReg = str => (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
  1863.  
  1864. // Converts a string to a regular expression
  1865. function toRegExp(str, noG) {
  1866.     const l = str.lastIndexOf('/');
  1867.     const flags = str.substr(l + 1);
  1868.     return new RegExp(str.substr(1, l - 1), noG ? flags.replace('g', '') : flags);
  1869. }
  1870.  
  1871. function toggleAttr(el, name, value, isAdd) {
  1872.     if(isAdd) {
  1873.         el.setAttribute(name, value);
  1874.     } else {
  1875.         el.removeAttribute(name);
  1876.     }
  1877. }
  1878.  
  1879. function $pd(e) {
  1880.     e.preventDefault();
  1881. }
  1882.  
  1883. function $isEmpty(obj) {
  1884.     for(const i in obj) {
  1885.         if(obj.hasOwnProperty(i)) {
  1886.             return false;
  1887.         }
  1888.     }
  1889.     return true;
  1890. }
  1891.  
  1892. function insertText(el, txt) {
  1893.     const scrtop = el.scrollTop;
  1894.     const start = el.selectionStart;
  1895.     el.value = el.value.substr(0, start) + txt + el.value.substr(el.selectionEnd);
  1896.     el.setSelectionRange(start + txt.length, start + txt.length);
  1897.     el.focus();
  1898.     el.scrollTop = scrtop;
  1899. }
  1900.  
  1901. // XXX: SVG events hack for Opera Presto
  1902. function fixEventEl(el) {
  1903.     if(el && nav.isPresto) {
  1904.         const svg = el.correspondingUseElement;
  1905.         if(svg) {
  1906.             el = svg.ownerSVGElement;
  1907.         }
  1908.     }
  1909.     return el;
  1910. }
  1911.  
  1912. // Allows to record the duration of code execution
  1913. const Logger = {
  1914.     finish() {
  1915.         this._finished = true;
  1916.         this._marks.push(['LoggerFinish', Date.now()]);
  1917.     },
  1918.     getLogData(isFull) {
  1919.         const marks = this._marks;
  1920.         const timeLog = [];
  1921.         let duration, i = 1;
  1922.         let lastExtra = 0;
  1923.         for(let len = marks.length - 1; i < len; ++i) {
  1924.             duration = marks[i][1] - marks[i - 1][1] + lastExtra;
  1925.             if(isFull || duration > 1) {
  1926.                 lastExtra = 0;
  1927.                 timeLog.push([marks[i][0], duration]);
  1928.             } else { // Ignore logs equal to 0ms
  1929.                 lastExtra = duration;
  1930.             }
  1931.         }
  1932.         timeLog.push([Lng.total[lang], marks[i][1] - marks[0][1]]);
  1933.         return timeLog;
  1934.     },
  1935.     initLogger() {
  1936.         this._marks.push(['LoggerInit', Date.now()]);
  1937.     },
  1938.     log(text) {
  1939.         if(!this._finished) {
  1940.             this._marks.push([text, Date.now()]);
  1941.         }
  1942.     },
  1943.  
  1944.     _finished : false,
  1945.     _marks    : []
  1946. };
  1947.  
  1948. // Some async operations should be cancelable, to ignore all the chaining callbacks of promises.
  1949. // Cancellation is supposed to flow through a graph of promise dependencies. When a promise is cancelled, it
  1950. // will propagate to the farthest pending promises and reject them with the cancel reason CancelError.
  1951. function CancelError() {}
  1952. class CancelablePromise {
  1953.     constructor(resolver, cancelFn) {
  1954.         this._promise = new Promise((resolve, reject) => {
  1955.             this._reject = reject;
  1956.             resolver(value => {
  1957.                 resolve(value);
  1958.                 this._isResolved = true;
  1959.             }, reason => {
  1960.                 reject(reason);
  1961.                 this._isResolved = true;
  1962.             });
  1963.         });
  1964.         this._cancelFn = cancelFn;
  1965.         this._isResolved = false;
  1966.     }
  1967.     static reject(val) {
  1968.         return new CancelablePromise((res, rej) => rej(val));
  1969.     }
  1970.     static resolve(val) {
  1971.         return new CancelablePromise(res => res(val));
  1972.     }
  1973.     cancelPromise() {
  1974.         this._reject(new CancelError());
  1975.         if(!this._isResolved && this._cancelFn) {
  1976.             this._cancelFn();
  1977.         }
  1978.     }
  1979.     catch(eb) {
  1980.         return this.then(void 0, eb);
  1981.     }
  1982.     then(cb, eb) {
  1983.         const children = [];
  1984.         const wrap = fn => (...args) => {
  1985.             const child = fn(...args);
  1986.             if(child instanceof CancelablePromise) {
  1987.                 children.push(child);
  1988.             }
  1989.             return child;
  1990.         };
  1991.         return new CancelablePromise(
  1992.             resolve => resolve(this._promise.then(cb && wrap(cb), eb && wrap(eb))), () => {
  1993.                 for(const child of children) {
  1994.                     child.cancelPromise();
  1995.                 }
  1996.                 this.cancelPromise();
  1997.             });
  1998.     }
  1999. }
  2000.  
  2001. class Maybe {
  2002.     constructor(Ctor/* , ...args */) {
  2003.         this._ctor = Ctor;
  2004.         // this._args = args;
  2005.         this.hasValue = false;
  2006.     }
  2007.     get value() {
  2008.         const Ctor = this._ctor;
  2009.         this.hasValue = !!Ctor;
  2010.         const value = Ctor ? new Ctor(/* ...this._args */) : null;
  2011.         Object.defineProperty(this, 'value', { value });
  2012.         return value;
  2013.     }
  2014. }
  2015.  
  2016. class TemporaryContent {
  2017.     constructor(key) {
  2018.         const oClass = /* new.target */this.constructor; // https://github.com/babel/babel/issues/1088
  2019.         if(oClass.purgeTO) {
  2020.             clearTimeout(oClass.purgeTO);
  2021.         }
  2022.         oClass.purgeTO = setTimeout(() => oClass.purge(), oClass.purgeSecs);
  2023.         if(oClass.data) {
  2024.             const rv = oClass.data.get(key);
  2025.             if(rv) {
  2026.                 return rv;
  2027.             }
  2028.         } else {
  2029.             oClass.data = new Map();
  2030.         }
  2031.         oClass.data.set(key, this);
  2032.     }
  2033.     static get(key) {
  2034.         return this.data ? this.data.get(key) : null;
  2035.     }
  2036.     static has(key) {
  2037.         return this.data ? this.data.has(key) : false;
  2038.     }
  2039.     static purge() {
  2040.         if(this.purgeTO) {
  2041.             clearTimeout(this.purgeTO);
  2042.             this.purgeTO = null;
  2043.         }
  2044.         this.data = null;
  2045.     }
  2046.     static removeTempData(key) {
  2047.         if(this.data) {
  2048.             this.data.delete(key);
  2049.         }
  2050.     }
  2051. }
  2052. TemporaryContent.purgeSecs = 6e4;
  2053.  
  2054. class TasksPool {
  2055.     constructor(tasksCount, taskFunc, endFn) {
  2056.         this.array = [];
  2057.         this.running = 0;
  2058.         this.num = 1;
  2059.         this.func = taskFunc;
  2060.         this.endFn = endFn;
  2061.         this.max = tasksCount;
  2062.         this.completed = this.paused = this.stopped = false;
  2063.     }
  2064.     completeTasks() {
  2065.         if(!this.stopped) {
  2066.             if(this.array.length === 0 && this.running === 0) {
  2067.                 this.endFn();
  2068.             } else {
  2069.                 this.completed = true;
  2070.             }
  2071.         }
  2072.     }
  2073.     pauseTasks() {
  2074.         this.paused = true;
  2075.     }
  2076.     runTask(data) {
  2077.         if(!this.stopped) {
  2078.             if(this.paused || this.running === this.max) {
  2079.                 this.array.push(data);
  2080.             } else {
  2081.                 this._runTask(data);
  2082.                 this.running++;
  2083.             }
  2084.         }
  2085.     }
  2086.     stopTasks() {
  2087.         this.stopped = true;
  2088.         this.endFn();
  2089.     }
  2090.  
  2091.     _continueTasks() {
  2092.         if(!this.stopped) {
  2093.             this.paused = false;
  2094.             if(this.array.length === 0) {
  2095.                 if(this.completed) {
  2096.                     this.endFn();
  2097.                 }
  2098.                 return;
  2099.             }
  2100.             while(this.array.length !== 0 && this.running !== this.max) {
  2101.                 this._runTask(this.array.shift());
  2102.                 this.running++;
  2103.             }
  2104.         }
  2105.     }
  2106.     _endTask() {
  2107.         if(!this.stopped) {
  2108.             if(!this.paused && this.array.length !== 0) {
  2109.                 this._runTask(this.array.shift());
  2110.                 return;
  2111.             }
  2112.             this.running--;
  2113.             if(!this.paused && this.completed && this.running === 0) {
  2114.                 this.endFn();
  2115.             }
  2116.         }
  2117.     }
  2118.     _runTask(data) {
  2119.         this.func(this.num++, data).then(() => this._endTask(), err => {
  2120.             if(err instanceof TasksPool.PauseError) {
  2121.                 this.pauseTasks();
  2122.                 if(err.duration !== -1) {
  2123.                     setTimeout(() => this._continueTasks(), err.duration);
  2124.                 }
  2125.             } else {
  2126.                 this._endTask();
  2127.                 throw err;
  2128.             }
  2129.         });
  2130.     }
  2131. }
  2132. TasksPool.PauseError = function(duration) {
  2133.     this.name = 'TasksPool.PauseError';
  2134.     this.duration = duration;
  2135. };
  2136.  
  2137. class WorkerPool {
  2138.     constructor(mReqs, wrkFn, errFn) {
  2139.         if(!nav.hasWorker) {
  2140.             this.runWorker = (data, transferObjs, fn) => fn(wrkFn(data));
  2141.             return;
  2142.         }
  2143.         const url = deWindow.URL.createObjectURL(new Blob([`self.onmessage = function(e) {
  2144.             var info = (${ String(wrkFn) })(e.data);
  2145.             if(info.data) {
  2146.                 self.postMessage(info, [info.data]);
  2147.             } else {
  2148.                 self.postMessage(info);
  2149.             }
  2150.         }`], { type: 'text/javascript' }));
  2151.         this._pool = new TasksPool(mReqs, (num, data) => this._createWorker(num, data), null);
  2152.         this._freeWorkers = [];
  2153.         this._url = url;
  2154.         this._errFn = errFn;
  2155.         while(mReqs--) {
  2156.             this._freeWorkers.push(new Worker(url));
  2157.         }
  2158.     }
  2159.     clearWorkers() {
  2160.         deWindow.URL.revokeObjectURL(this._url);
  2161.         this._freeWorkers.forEach(w => w.terminate());
  2162.         this._freeWorkers = [];
  2163.     }
  2164.     runWorker(data, transferObjs, fn) {
  2165.         this._pool.runTask([data, transferObjs, fn]);
  2166.     }
  2167.  
  2168.     _createWorker(num, data) {
  2169.         return new Promise(resolve => {
  2170.             const worker = this._freeWorkers.pop();
  2171.             const [sendData, transferObjs, fn] = data;
  2172.             worker.onmessage = e => {
  2173.                 fn(e.data);
  2174.                 this._freeWorkers.push(worker);
  2175.                 resolve();
  2176.             };
  2177.             worker.onerror = err => {
  2178.                 resolve();
  2179.                 this._freeWorkers.push(worker);
  2180.                 this._errFn(err);
  2181.             };
  2182.             worker.postMessage(sendData, transferObjs);
  2183.         });
  2184.     }
  2185. }
  2186.  
  2187. class TarBuilder {
  2188.     constructor() {
  2189.         this._data = [];
  2190.     }
  2191.     addFile(filepath, input) {
  2192.         let i, checksum = 0;
  2193.         const fileSize = input.length;
  2194.         const header = new Uint8Array(512);
  2195.         const nameLen = Math.min(filepath.length, 100);
  2196.         for(i = 0; i < nameLen; ++i) {
  2197.             header[i] = filepath.charCodeAt(i) & 0xFF;
  2198.         }
  2199.         TarBuilder._padSet(header, 100, '100777', 8); // fileMode
  2200.         TarBuilder._padSet(header, 108, '0', 8); // uid
  2201.         TarBuilder._padSet(header, 116, '0', 8); // gid
  2202.         TarBuilder._padSet(header, 124, fileSize.toString(8), 13); // fileSize
  2203.         TarBuilder._padSet(header, 136, Math.floor(Date.now() / 1e3).toString(8), 12); // mtime
  2204.         TarBuilder._padSet(header, 148, '        ', 8); // checksum
  2205.         // type ('0')
  2206.         header[156] = 0x30;
  2207.         for(i = 0; i < 157; ++i) {
  2208.             checksum += header[i];
  2209.         }
  2210.         // checksum
  2211.         TarBuilder._padSet(header, 148, checksum.toString(8), 8);
  2212.         this._data.push(header, input);
  2213.         if((i = Math.ceil(fileSize / 512) * 512 - fileSize) !== 0) {
  2214.             this._data.push(new Uint8Array(i));
  2215.         }
  2216.     }
  2217.     addString(filepath, str) {
  2218.         const sDat = unescape(encodeURIComponent(str));
  2219.         this.addFile(filepath, new Uint8Array(sDat.length).map((val, i) => sDat.charCodeAt(i) & 0xFF));
  2220.     }
  2221.     get() {
  2222.         this._data.push(new Uint8Array(1024));
  2223.         return new Blob(this._data, { type: 'application/x-tar' });
  2224.     }
  2225.  
  2226.     static _padSet(data, offset, num, len) {
  2227.         let i = 0;
  2228.         const nLen = num.length;
  2229.         len -= 2;
  2230.         while(nLen < len) {
  2231.             data[offset++] = 0x20; // ' '
  2232.             len--;
  2233.         }
  2234.         while(i < nLen) {
  2235.             data[offset++] = num.charCodeAt(i++);
  2236.         }
  2237.         data[offset] = 0x20; // ' '
  2238.     }
  2239. }
  2240.  
  2241. class WebmParser {
  2242.     constructor(data) {
  2243.         let offset = 0;
  2244.         const dv = nav.getUnsafeDataView(data);
  2245.         const len = dv.byteLength;
  2246.         const el = new WebmParser.Element(dv, len, 0);
  2247.         const voids = [];
  2248.         const EBMLId = 0x1A45DFA3;
  2249.         const segmentId = 0x18538067;
  2250.         const voidId = 0xEC;
  2251.         this.voidId = voidId;
  2252.         error: do {
  2253.             if(el.error || el.id !== EBMLId) {
  2254.                 break;
  2255.             }
  2256.             this.EBML = el;
  2257.             offset += el.headSize + el.size;
  2258.             while(true) {
  2259.                 const el = new WebmParser.Element(dv, len, offset);
  2260.                 if(el.error) {
  2261.                     break error;
  2262.                 }
  2263.                 if(el.id === segmentId) {
  2264.                     this.segment = el;
  2265.                     break; // Ignore everything after first segment
  2266.                 } else if(el.id === voidId) {
  2267.                     voids.push(el);
  2268.                 } else {
  2269.                     break error;
  2270.                 }
  2271.                 offset += el.headSize + el.size;
  2272.             }
  2273.             this.voids = voids;
  2274.             this.data = data;
  2275.             this.length = len;
  2276.             this.rv = [null];
  2277.             this.error = false;
  2278.             return;
  2279.         } while(false);
  2280.         this.error = true;
  2281.     }
  2282.     addWebmData(data) {
  2283.         if(this.error || !data) {
  2284.             return this;
  2285.         }
  2286.         const size = typeof data === 'string' ? data.length : data.byteLength;
  2287.         if(size > 127) {
  2288.             this.error = true;
  2289.             return;
  2290.         }
  2291.         this.rv.push(new Uint8Array([this.voidId, 0x80 | size]), data);
  2292.         return this;
  2293.     }
  2294.     getWebmData() {
  2295.         if(this.error) {
  2296.             return null;
  2297.         }
  2298.         this.rv[0] = nav.getUnsafeUint8Array(this.data, 0, this.segment.endOffset);
  2299.         return this.rv;
  2300.     }
  2301. }
  2302. WebmParser.Element = function(elData, dataLength, offset) {
  2303.     this.error = false;
  2304.     this.id = 0;
  2305.     if(offset + 4 >= dataLength) {
  2306.         return;
  2307.     }
  2308.     let num = elData.getUint32(offset);
  2309.     let leadZeroes = Math.clz32(num);
  2310.     if(leadZeroes > 3) {
  2311.         this.error = true;
  2312.         return;
  2313.     }
  2314.     offset += leadZeroes + 1;
  2315.     if(offset >= dataLength) {
  2316.         this.error = true;
  2317.         return;
  2318.     }
  2319.     this.id = num >>> (8 * (3 - leadZeroes));
  2320.     this.headSize = leadZeroes + 1;
  2321.     num = elData.getUint32(offset);
  2322.     leadZeroes = Math.clz32(num);
  2323.     let size = num & (0xFFFFFFFF >>> (leadZeroes + 1));
  2324.     if(leadZeroes > 3) {
  2325.         const shift = 8 * (7 - leadZeroes);
  2326.         if(size >>> shift !== 0 || offset + 4 > dataLength) {
  2327.             this.error = true;
  2328.             return; // We cannot handle webm-files with size greater than 4Gb :(
  2329.         }
  2330.         size = (size << (32 - shift)) | (elData.getUint32(offset + 4) >>> shift);
  2331.     } else {
  2332.         size >>>= 8 * (3 - leadZeroes);
  2333.     }
  2334.     this.headSize += leadZeroes + 1;
  2335.     offset += leadZeroes + 1;
  2336.     if(offset + size > dataLength) {
  2337.         this.error = true;
  2338.         return;
  2339.     }
  2340.     this.data = elData;
  2341.     this.offset = offset;
  2342.     this.endOffset = offset + size;
  2343.     this.size = size;
  2344. };
  2345.  
  2346. function getErrorMessage(err) {
  2347.     if(err instanceof AjaxError) {
  2348.         return err.toString();
  2349.     }
  2350.     if(typeof err === 'string') {
  2351.         return err;
  2352.     }
  2353.     const { stack, name, message } = err;
  2354.     return Lng.internalError[lang] + (
  2355.         !stack ? `${ name }: ${ message }` :
  2356.         nav.isWebkit ? stack : `${ name }: ${ message }\n${ !nav.isFirefox ? stack : stack.replace(
  2357.             /^([^@]*).*\/(.+)$/gm,
  2358.             (str, fName, line) => `    at ${ fName ? `${ fName } (${ line })` : line }`
  2359.         ) }`
  2360.     );
  2361. }
  2362.  
  2363. async function readFile(file, asText = false) {
  2364.     return new Promise(resolve => {
  2365.         const fr = new FileReader();
  2366.         // XXX: firefox hack to prevent 'XrayWrapper denied access to property "then"' errors
  2367.         fr.onload = e => resolve({ data: e.target.result });
  2368.         if(asText) {
  2369.             fr.readAsText(file);
  2370.         } else {
  2371.             fr.readAsArrayBuffer(file);
  2372.         }
  2373.     });
  2374. }
  2375.  
  2376. const prettifySize = val =>
  2377.     val > 512 * 1024 * 1024 ? (val / (1024 ** 3)).toFixed(2) + Lng.sizeGByte[lang] :
  2378.     val > 512 * 1024 ? (val / (1024 ** 2)).toFixed(2) + Lng.sizeMByte[lang] :
  2379.     val > 512 ? (val / 1024).toFixed(2) + Lng.sizeKByte[lang] :
  2380.     val.toFixed(2) + Lng.sizeByte[lang];
  2381.  
  2382. function getFileType(url) {
  2383.     const dotIdx = url.lastIndexOf('.') + 1;
  2384.     switch(dotIdx && url.substr(dotIdx).toLowerCase()) {
  2385.     case 'gif': return 'image/gif';
  2386.     case 'jpeg':
  2387.     case 'jpg': return 'image/jpeg';
  2388.     case 'mp4': return 'video/mp4';
  2389.     case 'ogv': return 'video/ogv';
  2390.     case 'png': return 'image/png';
  2391.     case 'webm': return 'video/webm';
  2392.     case 'webp': return 'image/webp';
  2393.     default: return '';
  2394.     }
  2395. }
  2396.  
  2397. function downloadBlob(blob, name) {
  2398.     const url = nav.isMsEdge ? navigator.msSaveOrOpenBlob(blob, name) : deWindow.URL.createObjectURL(blob);
  2399.     const link = $bEnd(docBody, `<a href="${ url }" download="${ name }"></a>`);
  2400.     link.click();
  2401.     setTimeout(() => {
  2402.         deWindow.URL.revokeObjectURL(url);
  2403.         link.remove();
  2404.     }, 2e5);
  2405. }
  2406.  
  2407. /* ==[ Storage.js ]===========================================================================================
  2408.                                                    STORAGE
  2409. =========================================================================================================== */
  2410.  
  2411. // Gets data from the global storage
  2412. async function getStored(id) {
  2413.     if(nav.hasNewGM) {
  2414.         const value = await GM.getValue(id);
  2415.         return value;
  2416.     } else if(nav.hasOldGM) {
  2417.         return GM_getValue(id);
  2418.     } else if(nav.hasWebStorage) {
  2419.         // Read storage.local first. If it not existed then read storage.sync
  2420.         const value = await new Promise(resolve => chrome.storage.local.get(id, obj => {
  2421.             if(Object.keys(obj).length) {
  2422.                 resolve(obj[id]);
  2423.             } else {
  2424.                 chrome.storage.sync.get(id, obj => resolve(obj[id]));
  2425.             }
  2426.         }));
  2427.         return value;
  2428.     } else if(nav.hasPrestoStorage) {
  2429.         return prestoStorage.getItem(id);
  2430.     }
  2431.     return locStorage[id];
  2432. }
  2433.  
  2434. // Saves data into the global storage
  2435. // FIXME: make async?
  2436. function setStored(id, value) {
  2437.     if(nav.hasNewGM) {
  2438.         return GM.setValue(id, value);
  2439.     } else if(nav.hasOldGM) {
  2440.         GM_setValue(id, value);
  2441.     } else if(nav.hasWebStorage) {
  2442.         const obj = {};
  2443.         obj[id] = value;
  2444.         chrome.storage.sync.set(obj, () => {
  2445.             if(chrome.runtime.lastError) {
  2446.                 // Store into storage.local if the storage.sync limit is exceeded
  2447.                 chrome.storage.local.set(obj, emptyFn);
  2448.                 chrome.storage.sync.remove(id, emptyFn);
  2449.             } else {
  2450.                 chrome.storage.local.remove(id, emptyFn);
  2451.             }
  2452.         });
  2453.     } else if(nav.hasPrestoStorage) {
  2454.         prestoStorage.setItem(id, value);
  2455.     } else {
  2456.         locStorage[id] = value;
  2457.     }
  2458. }
  2459.  
  2460. // Removes data from the global storage
  2461. // FIXME: make async?
  2462. function delStored(id) {
  2463.     if(nav.hasNewGM) {
  2464.         return GM.deleteValue(id);
  2465.     } else if(nav.hasOldGM) {
  2466.         GM_deleteValue(id);
  2467.     } else if(nav.hasWebStorage) {
  2468.         chrome.storage.sync.remove(id, emptyFn);
  2469.     } else if(nav.hasPrestoStorage) {
  2470.         prestoStorage.removeItem(id);
  2471.     } else {
  2472.         locStorage.removeItem(id);
  2473.     }
  2474. }
  2475.  
  2476. // Receives and parses JSON data into an object
  2477. async function getStoredObj(id) {
  2478.     return JSON.parse(await getStored(id) || '{}') || {};
  2479. }
  2480.  
  2481. // Replaces the domain config with an object. Removes the domain config, if there is no object.
  2482. function saveCfgObj(dm, obj) {
  2483.     getStoredObj('DESU_Config').then(val => {
  2484.         if(obj) {
  2485.             val[dm] = obj;
  2486.         } else {
  2487.             delete val[dm];
  2488.         }
  2489.         setStored('DESU_Config', JSON.stringify(val));
  2490.     });
  2491. }
  2492.  
  2493. // Saves the value for a particular config option
  2494. function saveCfg(id, val) {
  2495.     if(Cfg[id] !== val) {
  2496.         Cfg[id] = val;
  2497.         saveCfgObj(aib.dm, Cfg);
  2498.     }
  2499. }
  2500.  
  2501. // Toggles a particular config option (1|0)
  2502. function toggleCfg(id) {
  2503.     saveCfg(id, +!Cfg[id]);
  2504. }
  2505.  
  2506. function readData() {
  2507.     return Promise.all([readFavorites(), readCfg()]);
  2508. }
  2509.  
  2510. // Config initialization, checking for Dollchan update.
  2511. async function readCfg() {
  2512.     let obj;
  2513.     const val = await getStoredObj('DESU_Config');
  2514.     if(!(aib.dm in val) || $isEmpty(obj = val[aib.dm])) {
  2515.         const isGlobal = nav.hasGlobalStorage && !!val.global;
  2516.         obj = isGlobal ? val.global : {};
  2517.         if(isGlobal) {
  2518.             delete obj.correctTime;
  2519.             delete obj.captchaLang;
  2520.         }
  2521.     }
  2522.     defaultCfg.captchaLang = aib.capLang;
  2523.     defaultCfg.language = +!String(navigator.language).toLowerCase().startsWith('ru');
  2524.     Cfg = Object.assign(Object.create(defaultCfg), obj);
  2525.     if(!Cfg.timeOffset) {
  2526.         Cfg.timeOffset = '+0';
  2527.     }
  2528.     if(!Cfg.timePattern) {
  2529.         Cfg.timePattern = aib.timePattern;
  2530.     }
  2531.     if(aib.prot !== 'http:') { // Vocaroo doesn't support https
  2532.         Cfg.addVocaroo = 0;
  2533.     }
  2534.     if(aib.dobrochan && !Cfg.useDobrAPI) {
  2535.         aib.JsonBuilder = null;
  2536.     }
  2537.     if(!('FormData' in deWindow)) {
  2538.         Cfg.ajaxPosting = 0;
  2539.     }
  2540.     if(!Cfg.ajaxPosting) {
  2541.         Cfg.fileInputs = 0;
  2542.     }
  2543.     if(!('Notification' in deWindow)) {
  2544.         Cfg.desktNotif = 0;
  2545.     }
  2546.     if(nav.isPresto) {
  2547.         Cfg.preLoadImgs = 0;
  2548.         Cfg.findImgFile = 0;
  2549.         if(!nav.hasOldGM) {
  2550.             Cfg.updDollchan = 0;
  2551.         }
  2552.         Cfg.fileInputs = 0;
  2553.     }
  2554.     if(nav.scriptHandler === 'WebExtension') {
  2555.         Cfg.updDollchan = 0;
  2556.     }
  2557.     if(Cfg.updThrDelay < 10) {
  2558.         Cfg.updThrDelay = 10;
  2559.     }
  2560.     if(!Cfg.addSageBtn || !Cfg.saveSage) {
  2561.         Cfg.sageReply = 0;
  2562.     }
  2563.     if(!Cfg.passwValue) {
  2564.         Cfg.passwValue = Math.round(Math.random() * 1e12).toString(32);
  2565.     }
  2566.     if(!Cfg.stats) {
  2567.         Cfg.stats = { view: 0, op: 0, reply: 0 };
  2568.     }
  2569.     if(Cfg.addYouTube !== undefined) {
  2570.         Cfg.embedYTube = Cfg.addYouTube === 0 ? 0 : Cfg.addYouTube === 1 ? 2 : 1;
  2571.         delete Cfg.addYouTube;
  2572.     }
  2573.     lang = Cfg.language;
  2574.     if(val.commit !== commit && !localData) {
  2575.         const font = ' style="font: 13px monospace; color: green;"';
  2576.         const donateMsg = Lng.donateMsg[lang] + ':<br style="margin-bottom: 8px;">' +
  2577.             '<div class="de-logo"><svg><use xlink:href="#de-symbol-panel-logo"/></svg></div>' +
  2578.             '<div style="display: inline-block;"><b><i>Yandex.Money</i></b><br>' +
  2579.             `<span class="de-list de-depend"><i${
  2580.                 font }>410012122418236</i></span><br><b><i>WebMoney</i></b><br>` +
  2581.             `<span class="de-list de-depend">WMZ &ndash; <i${ font }>Z100197626370</i></span><br>` +
  2582.             `<span class="de-list de-depend">WMR &ndash; <i${ font }>R266614957054</i></span><br>` +
  2583.             `<span class="de-list de-depend">WMU &ndash; <i${ font }>U142375546253</i></span><br>` +
  2584.             `<b><i>Bitcoin</i></b><br><span class="de-list de-depend">P2PKH &ndash; <i${
  2585.                 font }>15xEo7BVQ3zjztJqKSRVhTq3tt3rNSHFpC</i></span><br>` +
  2586.             `<span class="de-list de-depend">P2SH &ndash; <i${
  2587.                 font }>3AhNPPpvtxQoFCLXk5e9Hzh6Ex9h7EoNzq</i></span></div>` +
  2588.             (nav.firefoxVer >= 56 && nav.scriptHandler !== 'WebExtension' ?
  2589.                 `<br><br>New: <a href="https://addons.mozilla.org/${ lang === 1 ? 'en-US' : 'ru' }` +
  2590.                 '/firefox/addon/dollchan-extension/" target="_blank">' + Lng.firefoxAddon[lang] : '');
  2591.         const popupFn = () => $popup('donate', donateMsg);
  2592.         if(doc.readyState === 'loading') {
  2593.             doc.addEventListener('DOMContentLoaded', () => setTimeout(popupFn, 1e3));
  2594.         } else {
  2595.             setTimeout(popupFn, 1e3);
  2596.         }
  2597.         val.commit = commit;
  2598.     }
  2599.     setStored('DESU_Config', JSON.stringify(val));
  2600.     if(Cfg.updDollchan && !localData) {
  2601.         checkForUpdates(false, val.lastUpd).then(html => {
  2602.             if(doc.readyState === 'loading') {
  2603.                 doc.addEventListener('DOMContentLoaded', () => $popup('updavail', html));
  2604.             } else {
  2605.                 $popup('updavail', html);
  2606.             }
  2607.         }, emptyFn);
  2608.     }
  2609. }
  2610.  
  2611. // Initialize of hidden and favorites. Run spells.
  2612. function readPostsData(firstPost, favObj) {
  2613.     let sVis = null;
  2614.     try {
  2615.         // Get hidden posts and threads that cached in current session
  2616.         const str = aib.t ? sesStorage['de-hidden-' + aib.b + aib.t] : null;
  2617.         if(str) {
  2618.             const json = JSON.parse(str);
  2619.             if(json.hash === (Cfg.hideBySpell ? Spells.hash : 0) &&
  2620.                 pByNum.has(json.lastNum) && pByNum.get(json.lastNum).count === json.lastCount
  2621.             ) {
  2622.                 sVis = json.data && json.data[0] instanceof Array ? json.data : null;
  2623.             }
  2624.         }
  2625.     } catch(err) {
  2626.         sesStorage['de-hidden-' + aib.b + aib.t] = null;
  2627.     }
  2628.     if(!firstPost) {
  2629.         return;
  2630.     }
  2631.     let updateFav = null;
  2632.     const favBrd = (aib.host in favObj) && (aib.b in favObj[aib.host]) ? favObj[aib.host][aib.b] : {};
  2633.     const spellsHide = Cfg.hideBySpell;
  2634.     const maybeSpells = new Maybe(SpellsRunner);
  2635.  
  2636.     // Search existed posts in stored data
  2637.     for(let post = firstPost; post; post = post.next) {
  2638.         const { num } = post;
  2639.         // Mark favorite threads, update favorites data
  2640.         if(post.isOp && (num in favBrd)) {
  2641.             const f = favBrd[num];
  2642.             const { thr } = post;
  2643.             post.toggleFavBtn(true);
  2644.             post.thr.isFav = true;
  2645.             if(aib.t) {
  2646.                 f.cnt = thr.pcount;
  2647.                 f.new = f.you = 0;
  2648.                 if(Cfg.markNewPosts && f.last) {
  2649.                     let lastPost = pByNum.get(+f.last.match(/\d+/));
  2650.                     if(lastPost) {
  2651.                         // Mark all new posts after last viewed post
  2652.                         while((lastPost = lastPost.next)) {
  2653.                             Post.addMark(lastPost.el, true);
  2654.                         }
  2655.                     }
  2656.                 }
  2657.                 f.last = aib.anchor + thr.last.num;
  2658.             } else {
  2659.                 f.new = thr.pcount - f.cnt;
  2660.             }
  2661.             updateFav = [aib.host, aib.b, aib.t, [thr.pcount, thr.last.num], 'update'];
  2662.         }
  2663.         if(HiddenPosts.has(num)) {
  2664.             HiddenPosts.hideHidden(post, num);
  2665.             continue;
  2666.         }
  2667.         let hideData;
  2668.         if(post.isOp) {
  2669.             if(HiddenThreads.has(num)) {
  2670.                 hideData = [true, null];
  2671.             } else if(spellsHide) {
  2672.                 hideData = sVis && sVis[post.count];
  2673.             }
  2674.         } else if(spellsHide) {
  2675.             hideData = sVis && sVis[post.count];
  2676.         } else {
  2677.             continue;
  2678.         }
  2679.         if(!hideData) {
  2680.             maybeSpells.value.runSpells(post); // Apply spells if posts not hidden
  2681.         } else if(hideData[0]) {
  2682.             if(post.isHidden) {
  2683.                 post.spellHidden = true;
  2684.             } else {
  2685.                 post.spellHide(hideData[1]);
  2686.             }
  2687.         }
  2688.     }
  2689.     if(maybeSpells.hasValue) {
  2690.         maybeSpells.value.endSpells();
  2691.     }
  2692.     if(aib.t && Cfg.panelCounter === 2) {
  2693.         $id('de-panel-info-pcount').textContent = Thread.first.pcount - Thread.first.hidCounter;
  2694.     }
  2695.     if(updateFav) {
  2696.         saveFavorites(favObj);
  2697.         sendStorageEvent('__de-favorites', updateFav);
  2698.     }
  2699.     // After following a link from Favorites, we need to open Favorites again.
  2700.     const hasFavWinKey = sesStorage['de-fav-win'] === '1';
  2701.     if(hasFavWinKey || Cfg.favWinOn) {
  2702.         toggleWindow('fav', !!$q('#de-win-fav.de-win-active'), null, true);
  2703.         if(hasFavWinKey) {
  2704.             sesStorage.removeItem('de-fav-win');
  2705.         }
  2706.     }
  2707.     let data = sesStorage['de-fav-newthr'];
  2708.     if(data) { // Detecting the created new thread and adding it to Favorites.
  2709.         data = JSON.parse(data);
  2710.         const isTimeOut = !data.num && (Date.now() - data.date > 2e4);
  2711.         if(data.num === firstPost.num || !firstPost.next && !isTimeOut) {
  2712.             firstPost.thr.toggleFavState(true);
  2713.             sesStorage.removeItem('de-fav-newthr');
  2714.         } else if(isTimeOut) {
  2715.             sesStorage.removeItem('de-fav-newthr');
  2716.         }
  2717.     }
  2718.     if(Cfg.nextPageThr && DelForm.first === DelForm.last) {
  2719.         const hidThrEls = $Q('.de-thr-hid', firstPost.thr.form.el);
  2720.         const hidThrLen = hidThrEls.length;
  2721.         if(hidThrLen) {
  2722.             Pages.addPage(hidThrLen);
  2723.         }
  2724.     }
  2725. }
  2726.  
  2727. function readFavorites() {
  2728.     return getStoredObj('DESU_Favorites');
  2729. }
  2730.  
  2731. function saveFavorites(data) {
  2732.     setStored('DESU_Favorites', JSON.stringify(data));
  2733. }
  2734.  
  2735. // Get posts that were read by posts previews
  2736. function readViewedPosts() {
  2737.     if(!Cfg.markViewed) {
  2738.         return;
  2739.     }
  2740.     const data = sesStorage['de-viewed'];
  2741.     if(data) {
  2742.         data.split(',').forEach(pNum => {
  2743.             const post = pByNum.get(+pNum);
  2744.             if(post) {
  2745.                 post.el.classList.add('de-viewed');
  2746.                 post.isViewed = true;
  2747.             }
  2748.         });
  2749.     }
  2750. }
  2751.  
  2752. // HIDDEN AND MY POSTS STORAGE
  2753.  
  2754. class PostsStorage {
  2755.     constructor() {
  2756.         this.storageName = '';
  2757.         this.__cachedTime = null;
  2758.         this._cachedStorage = null;
  2759.         this._cacheTO = null;
  2760.     }
  2761.     get(num) {
  2762.         const storage = this._readStorage()[aib.b];
  2763.         if(storage) {
  2764.             const val = storage[num];
  2765.             return val ? val[2] : null;
  2766.         }
  2767.         return null;
  2768.     }
  2769.     has(num) {
  2770.         const storage = this._readStorage()[aib.b];
  2771.         return storage ? storage.hasOwnProperty(num) : false;
  2772.     }
  2773.     purge() {
  2774.         this._cacheTO = this.__cachedTime = this._cachedStorage = null;
  2775.     }
  2776.     removeStorage(num, board = aib.b) {
  2777.         const storage = this._readStorage();
  2778.         const bStorage = storage[board];
  2779.         if(bStorage && bStorage.hasOwnProperty(num)) {
  2780.             delete bStorage[num];
  2781.             if($isEmpty(bStorage)) {
  2782.                 delete storage[board];
  2783.             }
  2784.             this._saveStorage();
  2785.         }
  2786.     }
  2787.     set(num, thrNum, data = true) {
  2788.         const storage = this._readStorage();
  2789.         if(storage && storage.$count > 5e3) {
  2790.             const minDate = Date.now() - 5 * 24 * 3600 * 1e3;
  2791.             for(const b in storage) {
  2792.                 if(storage.hasOwnProperty(b)) {
  2793.                     const data = storage[b];
  2794.                     for(const key in data) {
  2795.                         if(data.hasOwnProperty(key) && data[key][0] < minDate) {
  2796.                             delete data[key];
  2797.                         }
  2798.                     }
  2799.                 }
  2800.             }
  2801.         }
  2802.         (storage[aib.b] || (storage[aib.b] = {}))[num] = [this._cachedTime, thrNum, data];
  2803.         this._saveStorage();
  2804.     }
  2805.  
  2806.     static _migrateOld(newName, oldName) {
  2807.         if(locStorage.hasOwnProperty(oldName)) {
  2808.             locStorage[newName] = locStorage[oldName];
  2809.             locStorage.removeItem(oldName);
  2810.         }
  2811.     }
  2812.     get _cachedTime() {
  2813.         return this.__cachedTime || (this.__cachedTime = Date.now());
  2814.     }
  2815.     _readStorage() {
  2816.         if(this._cachedStorage) {
  2817.             return this._cachedStorage;
  2818.         }
  2819.         const data = locStorage[this.storageName];
  2820.         if(data) {
  2821.             try {
  2822.                 return (this._cachedStorage = JSON.parse(data));
  2823.             } catch(err) {}
  2824.         }
  2825.         return (this._cachedStorage = {});
  2826.     }
  2827.     _saveStorage() {
  2828.         if(this._cacheTO === null) {
  2829.             this._cacheTO = setTimeout(() => {
  2830.                 if(this._cachedStorage) {
  2831.                     locStorage[this.storageName] = JSON.stringify(this._cachedStorage);
  2832.                 }
  2833.                 this.purge();
  2834.             }, 0);
  2835.         }
  2836.     }
  2837. }
  2838.  
  2839. const HiddenPosts = new class HiddenPostsClass extends PostsStorage {
  2840.     constructor() {
  2841.         super();
  2842.         this.storageName = 'de-posts';
  2843.     }
  2844.     hideHidden(post, num) {
  2845.         const uHideData = HiddenPosts.get(num);
  2846.         if(!uHideData && post.isOp && HiddenThreads.has(num)) {
  2847.             post.setUserVisib(true);
  2848.         } else {
  2849.             post.setUserVisib(!!uHideData, false);
  2850.         }
  2851.     }
  2852.  
  2853.     _readStorage() {
  2854.         PostsStorage._migrateOld(this.storageName, 'de-threads-new'); // Old storage has wrong name
  2855.         return super._readStorage();
  2856.     }
  2857. }();
  2858.  
  2859. const HiddenThreads = new class HiddenThreadsClass extends PostsStorage {
  2860.     constructor() {
  2861.         super();
  2862.         this.storageName = 'de-threads';
  2863.     }
  2864.     getCount() {
  2865.         const storage = this._readStorage();
  2866.         let rv = 0;
  2867.         for(const b in storage) {
  2868.             rv += Object.keys(storage[b]).length;
  2869.         }
  2870.         return rv;
  2871.     }
  2872.     getRawData() {
  2873.         return this._readStorage();
  2874.     }
  2875.     saveRawData(data) {
  2876.         locStorage[this.storageName] = JSON.stringify(data);
  2877.         this.purge();
  2878.     }
  2879.  
  2880.     _readStorage() {
  2881.         PostsStorage._migrateOld(this.storageName, ''); // Old storage has wrong name
  2882.         return super._readStorage();
  2883.     }
  2884. }();
  2885.  
  2886. const MyPosts = new class MyPostsClass extends PostsStorage {
  2887.     constructor() {
  2888.         super();
  2889.         this.storageName = 'de-myposts';
  2890.         this._cachedData = null;
  2891.     }
  2892.     has(num) {
  2893.         return this._cachedData.has(num);
  2894.     }
  2895.     purge() {
  2896.         super.purge();
  2897.         this._cachedData = null;
  2898.         this._readStorage();
  2899.     }
  2900.     readStorage() {
  2901.         this._readStorage();
  2902.     }
  2903.     set(num, thrNum) {
  2904.         super.set(num, thrNum);
  2905.         this._cachedData.add(+num);
  2906.         sendStorageEvent('__de-mypost', 1);
  2907.     }
  2908.  
  2909.     _readStorage() {
  2910.         if(this._cachedData && this._cachedStorage) {
  2911.             return this._cachedStorage;
  2912.         }
  2913.         PostsStorage._migrateOld(this.storageName, 'de-myposts-new');
  2914.         const rv = super._readStorage();
  2915.         this._cachedData = rv[aib.b] ? new Set(Object.keys(rv[aib.b]).map(val => +val)) : new Set();
  2916.         return rv;
  2917.     }
  2918. }();
  2919.  
  2920. function sendStorageEvent(name, value) {
  2921.     locStorage[name] = typeof value === 'string' ? value : JSON.stringify(value);
  2922.     locStorage.removeItem(name);
  2923. }
  2924.  
  2925. function initStorageEvent() {
  2926.     doc.defaultView.addEventListener('storage', e => {
  2927.         let data, temp, val = e.newValue;
  2928.         if(!val) {
  2929.             return;
  2930.         }
  2931.         switch(e.key) {
  2932.         case '__de-favorites': {
  2933.             try {
  2934.                 data = JSON.parse(val);
  2935.             } catch(err) {
  2936.                 return;
  2937.             }
  2938.             updateFavWindow(...data);
  2939.             return;
  2940.         }
  2941.         case '__de-mypost': MyPosts.purge(); return;
  2942.         case '__de-webmvolume':
  2943.             val = +val || 0;
  2944.             Cfg.webmVolume = val;
  2945.             temp = $q('input[info="webmVolume"]');
  2946.             if(temp) {
  2947.                 temp.value = val;
  2948.             }
  2949.             return;
  2950.         case '__de-post':
  2951.             (() => {
  2952.                 try {
  2953.                     data = JSON.parse(val);
  2954.                 } catch(err) {
  2955.                     return;
  2956.                 }
  2957.                 HiddenThreads.purge();
  2958.                 HiddenPosts.purge();
  2959.                 if(data.brd === aib.b) {
  2960.                     let post = pByNum.get(data.num);
  2961.                     if(post && (post.isHidden ^ data.hide)) {
  2962.                         post.setUserVisib(data.hide, false);
  2963.                     } else if((post = pByNum.get(data.thrNum))) {
  2964.                         post.thr.userTouched.set(data.num, data.hide);
  2965.                     }
  2966.                 }
  2967.                 toggleWindow('hid', true);
  2968.             })();
  2969.             return;
  2970.         case 'de-threads':
  2971.             HiddenThreads.purge();
  2972.             Thread.first.updateHidden(HiddenThreads.getRawData()[aib.b]);
  2973.             toggleWindow('hid', true);
  2974.             return;
  2975.         case '__de-spells': (() => {
  2976.             try {
  2977.                 data = JSON.parse(val);
  2978.             } catch(err) {
  2979.                 return;
  2980.             }
  2981.             Cfg.hideBySpell = +data.hide;
  2982.             temp = $q('input[info="hideBySpell"]');
  2983.             if(temp) {
  2984.                 temp.checked = data.hide;
  2985.             }
  2986.             $hide(docBody);
  2987.             if(data.data) {
  2988.                 Spells.setSpells(data.data, false);
  2989.                 Cfg.spells = JSON.stringify(data.data);
  2990.                 temp = $id('de-spell-txt');
  2991.                 if(temp) {
  2992.                     temp.value = Spells.list;
  2993.                 }
  2994.             } else {
  2995.                 SpellsRunner.unhideAll();
  2996.                 Spells.disableSpells();
  2997.                 temp = $id('de-spell-txt');
  2998.                 if(temp) {
  2999.                     temp.value = '';
  3000.                 }
  3001.             }
  3002.             $show(docBody);
  3003.         })();
  3004.         }
  3005.     });
  3006. }
  3007.  
  3008. /* ==[ Panel.js ]=============================================================================================
  3009.                                                  MAIN PANEL
  3010. =========================================================================================================== */
  3011.  
  3012. const Panel = Object.create({
  3013.     isVidEnabled: false,
  3014.     initPanel(formEl) {
  3015.         const imgLen = $Q(aib.qPostImg, formEl).length;
  3016.         const isThr = aib.t;
  3017.         (pr && pr.pArea[0] || formEl).insertAdjacentHTML('beforebegin', `<div id="de-main">
  3018.             <div id="de-panel">
  3019.                 <div id="de-panel-logo" title="${ Lng.panelBtn.attach[lang] }">
  3020.                     <svg class="de-panel-logo-svg">
  3021.                         <use xlink:href="#de-symbol-panel-logo"/>
  3022.                     </svg>
  3023.                 </div>
  3024.                 <span id="de-panel-buttons"${ !Cfg.expandPanel ? ' style="display: none;"' : '' }>
  3025.                 ${ Cfg.disabled ? this._getButton('enable') : this._getButton('cfg') +
  3026.                     this._getButton('hid') +
  3027.                     this._getButton('fav') +
  3028.                     (Cfg.embedYTube ? this._getButton('vid') : '') +
  3029.                     (!localData ?
  3030.                         this._getButton('refresh') +
  3031.                         (isThr || aib.page !== aib.firstPage ? this._getButton('goback') : '') +
  3032.                         (!isThr && aib.page !== aib.lastPage ? this._getButton('gonext') : '') : '') +
  3033.                     this._getButton('goup') +
  3034.                     this._getButton('godown') +
  3035.                     (imgLen ? this._getButton('expimg') + this._getButton('maskimg') : '') +
  3036.                     (!localData && !nav.isPresto ?
  3037.                         (imgLen && !Cfg.preLoadImgs ? this._getButton('preimg') : '') +
  3038.                         (isThr ? this._getButton('savethr') : '') : '') +
  3039.                     (!localData && isThr ?
  3040.                         this._getButton(Cfg.ajaxUpdThr && !aib.isArchived ? 'upd-on' : 'upd-off') +
  3041.                         (!nav.isSafari ? this._getButton('audio-off') : '') : '') +
  3042.                     (aib.hasCatalog ? this._getButton('catalog') : '') +
  3043.                     this._getButton('enable') +
  3044.                     (isThr && Thread.first ? `<span id="de-panel-info">
  3045.                         <span id="de-panel-info-pcount" title="` +
  3046.                             `${ Lng.panelBtn[Cfg.panelCounter !== 2 ? 'pcount' : 'pcountNotHid'][lang] }">` +
  3047.                             `${ Thread.first.pcount }</span>
  3048.                         <span id="de-panel-info-icount" title="${ Lng.panelBtn.imglen[lang] }">${
  3049.                             imgLen }</span>
  3050.                         <span id="de-panel-info-acount" title="${ Lng.panelBtn.posters[lang] }"></span>
  3051.                     </span>` : '') }
  3052.                 </span>
  3053.             </div>
  3054.             ${ Cfg.disabled ? '' : '<div id="de-wrapper-popup"></div><hr style="clear: both;">' }
  3055.         </div>`);
  3056.         this._el = $id('de-panel');
  3057.         this._el.addEventListener('click', this, true);
  3058.         this._el.addEventListener('mouseover', this);
  3059.         this._el.addEventListener('mouseout', this);
  3060.         this._buttons = $id('de-panel-buttons');
  3061.         this.isNew = true;
  3062.     },
  3063.     removeMain() {
  3064.         this._el.removeEventListener('click', this, true);
  3065.         this._el.removeEventListener('mouseover', this);
  3066.         this._el.removeEventListener('mouseout', this);
  3067.         delete this._pcountEl;
  3068.         delete this._icountEl;
  3069.         delete this._acountEl;
  3070.         $id('de-main').remove();
  3071.     },
  3072.     handleEvent(e) {
  3073.         if('isTrusted' in e && !e.isTrusted) {
  3074.             return;
  3075.         }
  3076.         let el = fixEventEl(e.target);
  3077.         el = el.tagName.toLowerCase() === 'svg' ? el.parentNode : el;
  3078.         switch(e.type) {
  3079.         case 'click':
  3080.             switch(el.id) {
  3081.             case 'de-panel-logo':
  3082.                 if(Cfg.expandPanel && !$q('.de-win-active')) {
  3083.                     $hide(this._buttons);
  3084.                 }
  3085.                 toggleCfg('expandPanel');
  3086.                 return;
  3087.             case 'de-panel-cfg': toggleWindow('cfg', false); break;
  3088.             case 'de-panel-hid': toggleWindow('hid', false); break;
  3089.             case 'de-panel-fav': toggleWindow('fav', false); break;
  3090.             case 'de-panel-vid':
  3091.                 this.isVidEnabled = !this.isVidEnabled;
  3092.                 toggleWindow('vid', false);
  3093.                 break;
  3094.             case 'de-panel-refresh': deWindow.location.reload(); break;
  3095.             case 'de-panel-goup': scrollTo(0, 0); break;
  3096.             case 'de-panel-godown': scrollTo(0, docBody.scrollHeight || docBody.offsetHeight); break;
  3097.             case 'de-panel-expimg':
  3098.                 el.classList.toggle('de-panel-button-active');
  3099.                 isExpImg = !isExpImg;
  3100.                 $del($q('.de-fullimg-center'));
  3101.                 for(let post = Thread.first.op; post; post = post.next) {
  3102.                     post.toggleImages(isExpImg, false);
  3103.                 }
  3104.                 break;
  3105.             case 'de-panel-preimg':
  3106.                 el.classList.toggle('de-panel-button-active');
  3107.                 isPreImg = !isPreImg;
  3108.                 if(!e.ctrlKey) {
  3109.                     for(const { el } of DelForm) {
  3110.                         ContentLoader.preloadImages(el);
  3111.                     }
  3112.                 }
  3113.                 break;
  3114.             case 'de-panel-maskimg':
  3115.                 el.classList.toggle('de-panel-button-active');
  3116.                 toggleCfg('maskImgs');
  3117.                 updateCSS();
  3118.                 break;
  3119.             case 'de-panel-upd-on':
  3120.             case 'de-panel-upd-warn':
  3121.             case 'de-panel-upd-off':
  3122.                 updater.toggle();
  3123.                 break;
  3124.             case 'de-panel-audio-on':
  3125.             case 'de-panel-audio-off':
  3126.                 if(updater.toggleAudio(0)) {
  3127.                     updater.enableUpdater();
  3128.                     el.id = 'de-panel-audio-on';
  3129.                 } else {
  3130.                     el.id = 'de-panel-audio-off';
  3131.                 }
  3132.                 $del($q('.de-menu'));
  3133.                 break;
  3134.             case 'de-panel-savethr': break;
  3135.             case 'de-panel-enable':
  3136.                 toggleCfg('disabled');
  3137.                 deWindow.location.reload();
  3138.                 break;
  3139.             default: return;
  3140.             }
  3141.             $pd(e);
  3142.             return;
  3143.         case 'mouseover':
  3144.             if(!Cfg.expandPanel) {
  3145.                 clearTimeout(this._hideTO);
  3146.                 $show(this._buttons);
  3147.             }
  3148.             switch(el.id) {
  3149.             case 'de-panel-cfg': KeyEditListener.setTitle(el, 10); break;
  3150.             case 'de-panel-hid': KeyEditListener.setTitle(el, 7); break;
  3151.             case 'de-panel-fav': KeyEditListener.setTitle(el, 6); break;
  3152.             case 'de-panel-vid': KeyEditListener.setTitle(el, 18); break;
  3153.             case 'de-panel-goback': KeyEditListener.setTitle(el, 4); break;
  3154.             case 'de-panel-gonext': KeyEditListener.setTitle(el, 17); break;
  3155.             case 'de-panel-maskimg': KeyEditListener.setTitle(el, 9); break;
  3156.             case 'de-panel-refresh':
  3157.                 if(aib.t) {
  3158.                     return;
  3159.                 }
  3160.                 /* falls through */
  3161.             case 'de-panel-savethr':
  3162.             case 'de-panel-audio-off':
  3163.                 if(this._menu && this._menu.parentEl === el) {
  3164.                     return;
  3165.                 }
  3166.                 this._menuTO = setTimeout(() => {
  3167.                     this._menu = addMenu(el);
  3168.                     this._menu.onover = () => clearTimeout(this._hideTO);
  3169.                     this._menu.onout = () => this._prepareToHide(null);
  3170.                     this._menu.onremove = () => (this._menu = null);
  3171.                 }, Cfg.linksOver);
  3172.             }
  3173.             return;
  3174.         default: // mouseout
  3175.             this._prepareToHide(fixEventEl(e.relatedTarget));
  3176.             switch(el.id) {
  3177.             case 'de-panel-refresh':
  3178.             case 'de-panel-savethr':
  3179.             case 'de-panel-audio-off':
  3180.                 clearTimeout(this._menuTO);
  3181.                 this._menuTO = 0;
  3182.             }
  3183.         }
  3184.     },
  3185.     updateCounter(postCount, imgsCount, postersCount) {
  3186.         this._pcountEl.textContent = postCount;
  3187.         this._icountEl.textContent = imgsCount;
  3188.         this._acountEl.textContent = postersCount;
  3189.         this.isNew = false;
  3190.     },
  3191.  
  3192.     _el     : null,
  3193.     _hideTO : 0,
  3194.     _menu   : null,
  3195.     _menuTO : 0,
  3196.     get _acountEl() {
  3197.         const value = $id('de-panel-info-acount');
  3198.         Object.defineProperty(this, '_acountEl', { value, configurable: true });
  3199.         return value;
  3200.     },
  3201.     get _icountEl() {
  3202.         const value = $id('de-panel-info-icount');
  3203.         Object.defineProperty(this, '_icountEl', { value, configurable: true });
  3204.         return value;
  3205.     },
  3206.     get _pcountEl() {
  3207.         const value = $id('de-panel-info-pcount');
  3208.         Object.defineProperty(this, '_pcountEl', { value, configurable: true });
  3209.         return value;
  3210.     },
  3211.     _getButton(id) {
  3212.         let page, href, title, useId;
  3213.         switch(id) {
  3214.         case 'goback':
  3215.             page = Math.max(aib.page - 1, 0);
  3216.             href = aib.getPageUrl(aib.b, page);
  3217.             if(!aib.t) {
  3218.                 title = Lng.panelBtn.gonext[lang].replace('%s', page);
  3219.             }
  3220.             useId = 'arrow';
  3221.             break;
  3222.         case 'gonext':
  3223.             page = aib.page + 1;
  3224.             href = aib.getPageUrl(aib.b, page);
  3225.             title = Lng.panelBtn.gonext[lang].replace('%s', page);
  3226.             /* falls through */
  3227.         case 'goup':
  3228.         case 'godown':
  3229.             useId = 'arrow';
  3230.             break;
  3231.         case 'upd-on':
  3232.         case 'upd-off':
  3233.             useId = 'upd';
  3234.             break;
  3235.         case 'catalog':
  3236.             href = aib.catalogUrl;
  3237.         }
  3238.         // XXX Opera Presto: keep in sync with updMachine._setUpdateStatus
  3239.         return `<a id="de-panel-${ id }" class="de-abtn de-panel-button" title="${
  3240.             title || Lng.panelBtn[id][lang] }" href="${ href || '#' }">
  3241.             <svg class="de-panel-svg">
  3242.             ${ id !== 'audio-off' ? `
  3243.                 <use xlink:href="#de-symbol-panel-${ useId || id }"/>` : `
  3244.                 <use class="de-use-audio-off" xlink:href="#de-symbol-panel-audio-off"/>
  3245.                 <use class="de-use-audio-on" xlink:href="#de-symbol-panel-audio-on"/>` }
  3246.             </svg>
  3247.         </a>`;
  3248.     },
  3249.     _prepareToHide(rt) {
  3250.         if(!Cfg.expandPanel && !$q('.de-win-active') &&
  3251.             (!rt || !this._el.contains(rt.farthestViewportElement || rt))
  3252.         ) {
  3253.             this._hideTO = setTimeout(() => $hide(this._buttons), 500);
  3254.         }
  3255.     }
  3256. });
  3257.  
  3258. /* ==[ WindowUtils.js ]=======================================================================================
  3259.                                                WINDOW: UTILS
  3260. =========================================================================================================== */
  3261.  
  3262. function updateWinZ(style) {
  3263.     if(style.zIndex < topWinZ) {
  3264.         style.zIndex = ++topWinZ;
  3265.     }
  3266. }
  3267.  
  3268. function makeDraggable(name, win, head) {
  3269.     head.addEventListener('mousedown', {
  3270.         _oldX   : 0,
  3271.         _oldY   : 0,
  3272.         _win    : win,
  3273.         _wStyle : win.style,
  3274.         _X      : 0,
  3275.         _Y      : 0,
  3276.         _Z      : 0,
  3277.         handleEvent(e) {
  3278.             if(!Cfg[name + 'WinDrag']) {
  3279.                 return;
  3280.             }
  3281.             const { clientX: curX, clientY: curY } = e;
  3282.             switch(e.type) {
  3283.             case 'mousedown':
  3284.                 this._oldX = curX;
  3285.                 this._oldY = curY;
  3286.                 this._X = Cfg[name + 'WinX'];
  3287.                 this._Y = Cfg[name + 'WinY'];
  3288.                 if(this._Z < topWinZ) {
  3289.                     this._Z = this._wStyle.zIndex = ++topWinZ;
  3290.                 }
  3291.                 docBody.addEventListener('mouseleave', this);
  3292.                 docBody.addEventListener('mousemove', this);
  3293.                 docBody.addEventListener('mouseup', this);
  3294.                 $pd(e);
  3295.                 return;
  3296.             case 'mousemove': {
  3297.                 const maxX = Post.sizing.wWidth - this._win.offsetWidth;
  3298.                 const maxY = Post.sizing.wHeight - this._win.offsetHeight - 25;
  3299.                 const cr = this._win.getBoundingClientRect();
  3300.                 const x = cr.left + curX - this._oldX;
  3301.                 const y = cr.top + curY - this._oldY;
  3302.                 this._X = x >= maxX || curX > this._oldX && x > maxX - 20 ? 'right: 0' :
  3303.                     x < 0 || curX < this._oldX && x < 20 ? 'left: 0' :
  3304.                     `left: ${ x }px`;
  3305.                 this._Y = y >= maxY || curY > this._oldY && y > maxY - 20 ? 'bottom: 25px' :
  3306.                     y < 0 || curY < this._oldY && y < 20 ? 'top: 0' :
  3307.                     `top: ${ y }px`;
  3308.                 const { width } = this._wStyle;
  3309.                 this._win.setAttribute('style', `${ this._X }; ${ this._Y }; z-index: ${ this._Z }${
  3310.                     width ? '; width: ' + width : '' }`);
  3311.                 this._oldX = curX;
  3312.                 this._oldY = curY;
  3313.                 return;
  3314.             }
  3315.             case 'mouseleave':
  3316.             case 'mouseup':
  3317.                 docBody.removeEventListener('mouseleave', this);
  3318.                 docBody.removeEventListener('mousemove', this);
  3319.                 docBody.removeEventListener('mouseup', this);
  3320.                 saveCfg(name + 'WinX', this._X);
  3321.                 saveCfg(name + 'WinY', this._Y);
  3322.             }
  3323.         }
  3324.     });
  3325. }
  3326.  
  3327. class WinResizer {
  3328.     constructor(name, dir, cfgName, win, target) {
  3329.         this.name = name;
  3330.         this.dir = dir;
  3331.         this.cfgName = cfgName;
  3332.         this.vertical = dir === 'top' || dir === 'bottom';
  3333.         this.win = win;
  3334.         this.wStyle = this.win.style;
  3335.         this.tStyle = target.style;
  3336.         $q('.de-resizer-' + dir, win).addEventListener('mousedown', this);
  3337.     }
  3338.     handleEvent(e) {
  3339.         let val, x, y;
  3340.         const { wWidth: maxX, wHeight: maxY } = Post.sizing;
  3341.         const { width } = this.wStyle;
  3342.         const cr = this.win.getBoundingClientRect();
  3343.         const z = `; z-index: ${ this.wStyle.zIndex }${ width ? '; width:' + width : '' }`;
  3344.         switch(e.type) {
  3345.         case 'mousedown':
  3346.             if(this.win.classList.contains('de-win-fixed')) {
  3347.                 x = 'right: 0';
  3348.                 y = 'bottom: 25px';
  3349.             } else {
  3350.                 x = Cfg[this.name + 'WinX'];
  3351.                 y = Cfg[this.name + 'WinY'];
  3352.             }
  3353.             switch(this.dir) {
  3354.             case 'top': val = `${ x }; bottom: ${ maxY - cr.bottom }px${ z }`; break;
  3355.             case 'bottom': val = `${ x }; top: ${ cr.top }px${ z }`; break;
  3356.             case 'left': val = `right: ${ maxX - cr.right }px; ${ y + z }`; break;
  3357.             case 'right': val = `left: ${ cr.left }px; ${ y + z }`;
  3358.             }
  3359.             this.win.setAttribute('style', val);
  3360.             docBody.addEventListener('mousemove', this);
  3361.             docBody.addEventListener('mouseup', this);
  3362.             $pd(e);
  3363.             return;
  3364.         case 'mousemove':
  3365.             if(this.vertical) {
  3366.                 val = e.clientY;
  3367.                 this.tStyle.setProperty('height', Math.max(parseInt(this.tStyle.height, 10) + (
  3368.                     this.dir === 'top' ? cr.top - (val < 20 ? 0 : val) :
  3369.                     (val > maxY - 45 ? maxY - 25 : val) - cr.bottom
  3370.                 ), 90) + 'px', 'important');
  3371.             } else {
  3372.                 val = e.clientX;
  3373.                 this.tStyle.setProperty('width', Math.max(parseInt(this.tStyle.width, 10) + (
  3374.                     this.dir === 'left' ? cr.left - (val < 20 ? 0 : val) :
  3375.                     (val > maxX - 20 ? maxX : val) - cr.right
  3376.                 ), this.name === 'reply' ? 275 : 400) + 'px', 'important');
  3377.             }
  3378.             return;
  3379.         default: // mouseup
  3380.             docBody.removeEventListener('mousemove', this);
  3381.             docBody.removeEventListener('mouseup', this);
  3382.             saveCfg(this.cfgName, parseInt(this.vertical ? this.tStyle.height : this.tStyle.width, 10));
  3383.             if(this.win.classList.contains('de-win-fixed')) {
  3384.                 this.win.setAttribute('style', 'right: 0; bottom: 25px' + z);
  3385.                 return;
  3386.             }
  3387.             if(this.vertical) {
  3388.                 saveCfg(this.name + 'WinY', cr.top < 1 ? 'top: 0' :
  3389.                     cr.bottom > maxY - 26 ? 'bottom: 25px' : `top: ${ cr.top }px`);
  3390.             } else {
  3391.                 saveCfg(this.name + 'WinX', cr.left < 1 ? 'left: 0' :
  3392.                     cr.right > maxX - 1 ? 'right: 0' : `left: ${ cr.left }px`);
  3393.             }
  3394.             this.win.setAttribute('style', Cfg[this.name + 'WinX'] + '; ' + Cfg[this.name + 'WinY'] + z);
  3395.         }
  3396.     }
  3397. }
  3398.  
  3399. function toggleWindow(name, isUpdate, data, noAnim) {
  3400.     let el, win = $id('de-win-' + name);
  3401.     const isActive = win && win.classList.contains('de-win-active');
  3402.     if(isUpdate && !isActive) {
  3403.         return;
  3404.     }
  3405.     if(!win) {
  3406.         const winAttr = (Cfg[name + 'WinDrag'] ?
  3407.             `de-win" style="${ Cfg[name + 'WinX'] }; ${ Cfg[name + 'WinY'] }` :
  3408.             'de-win-fixed" style="right: 0; bottom: 25px'
  3409.         ) + (name !== 'fav' ? '' : `; width: ${ Cfg.favWinWidth }px; `);
  3410.         win = $aBegin($id('de-main'), `<div id="de-win-${ name }" class="${ winAttr }; display: none;">
  3411.             <div class="de-win-head">
  3412.                 <span class="de-win-title">
  3413.                     ${ name === 'cfg' ? 'Dollchan Extension Tools' : Lng.panelBtn[name][lang] }
  3414.                 </span>
  3415.                 <span class="de-win-buttons">
  3416.                     <svg class="de-win-btn-toggle"><use xlink:href="#de-symbol-win-arrow"/></svg>
  3417.                     <svg class="de-win-btn-close"><use xlink:href="#de-symbol-win-close"/></svg>
  3418.                 </span>
  3419.             </div>
  3420.             <div class="de-win-body"></div>
  3421.             ${ name !== 'fav' ? '' : `
  3422.                 <div class="de-resizer de-resizer-left"></div>
  3423.                 <div class="de-resizer de-resizer-right"></div>` }
  3424.         </div>`);
  3425.         const winBody = $q('.de-win-body', win);
  3426.         if(name === 'cfg') {
  3427.             winBody.className = 'de-win-body ' + aib.cReply;
  3428.         } else {
  3429.             setTimeout(() => {
  3430.                 const backColor = getComputedStyle(docBody).getPropertyValue('background-color');
  3431.                 winBody.style.backgroundColor = backColor !== 'transparent' ? backColor : '#EEE';
  3432.             }, 100);
  3433.         }
  3434.         if(name === 'fav') {
  3435.             new WinResizer('fav', 'left', 'favWinWidth', win, win);
  3436.             new WinResizer('fav', 'right', 'favWinWidth', win, win);
  3437.         }
  3438.         el = $q('.de-win-buttons', win);
  3439.         el.onmouseover = e => {
  3440.             const el = fixEventEl(e.target);
  3441.             const parent = el.parentNode;
  3442.             switch(el.classList[0]) {
  3443.             case 'de-win-btn-close': parent.title = Lng.closeWindow[lang]; break;
  3444.             case 'de-win-btn-toggle':
  3445.                 parent.title = Cfg[name + 'WinDrag'] ? Lng.toPanel[lang] : Lng.makeDrag[lang];
  3446.             }
  3447.         };
  3448.         el.lastElementChild.onclick = () => toggleWindow(name, false);
  3449.         $q('.de-win-btn-toggle', el).onclick = () => {
  3450.             toggleCfg(name + 'WinDrag');
  3451.             const isDrag = Cfg[name + 'WinDrag'];
  3452.             if(!isDrag) {
  3453.                 const temp = $q('.de-win-active.de-win-fixed', win.parentNode);
  3454.                 if(temp) {
  3455.                     toggleWindow(temp.id.substr(7), false);
  3456.                 }
  3457.             }
  3458.             win.classList.toggle('de-win', isDrag);
  3459.             win.classList.toggle('de-win-fixed', !isDrag);
  3460.             const { width } = win.style;
  3461.             win.style.cssText = `${ isDrag ? `${ Cfg[name + 'WinX'] }; ${ Cfg[name + 'WinY'] }` :
  3462.                 'right: 0; bottom: 25px' }${ width ? '; width: ' + width : '' }`;
  3463.             updateWinZ(win.style);
  3464.         };
  3465.         makeDraggable(name, win, $q('.de-win-head', win));
  3466.     }
  3467.     updateWinZ(win.style);
  3468.     let isRemove = !isUpdate && isActive;
  3469.     if(!isRemove && !win.classList.contains('de-win') &&
  3470.         (el = $q(`.de-win-active.de-win-fixed:not(#de-win-${ name })`, win.parentNode))
  3471.     ) {
  3472.         toggleWindow(el.id.substr(7), false);
  3473.     }
  3474.     const isAnim = !noAnim && !isUpdate && Cfg.animation;
  3475.     let body = $q('.de-win-body', win);
  3476.     if(isAnim && body.hasChildNodes()) {
  3477.         win.addEventListener('animationend', function aEvent(e) {
  3478.             e.target.removeEventListener('animationend', aEvent);
  3479.             showWindow(win, body, name, isRemove, data, Cfg.animation);
  3480.             win = body = name = isRemove = data = null;
  3481.         });
  3482.         win.classList.remove('de-win-open');
  3483.         win.classList.add('de-win-close');
  3484.     } else {
  3485.         showWindow(win, body, name, isRemove, data, isAnim);
  3486.     }
  3487. }
  3488.  
  3489. function showWindow(win, body, name, isRemove, data, isAnim) {
  3490.     body.innerHTML = '';
  3491.     win.classList.toggle('de-win-active', !isRemove);
  3492.     if(isRemove) {
  3493.         win.classList.remove('de-win-close');
  3494.         $hide(win);
  3495.         if(!Cfg.expandPanel && !$q('.de-win-active')) {
  3496.             $hide($id('de-panel-buttons'));
  3497.         }
  3498.         return;
  3499.     }
  3500.     if(!Cfg.expandPanel) {
  3501.         $show($id('de-panel-buttons'));
  3502.     }
  3503.     switch(name) {
  3504.     case 'fav':
  3505.         if(data) {
  3506.             showFavoritesWindow(body, data);
  3507.             break;
  3508.         }
  3509.         readFavorites().then(favObj => {
  3510.             showFavoritesWindow(body, favObj);
  3511.             $show(win);
  3512.             if(isAnim) {
  3513.                 win.classList.add('de-win-open');
  3514.             }
  3515.         });
  3516.         return;
  3517.     case 'cfg': CfgWindow.initCfgWindow(body); break;
  3518.     case 'hid': showHiddenWindow(body); break;
  3519.     case 'vid': showVideosWindow(body);
  3520.     }
  3521.     $show(win);
  3522.     if(isAnim) {
  3523.         win.classList.add('de-win-open');
  3524.     }
  3525. }
  3526.  
  3527. /* ==[ WindowVidHid.js ]======================================================================================
  3528.                                        WINDOW: VIDEOS, HIDDEN THREADS
  3529. =========================================================================================================== */
  3530.  
  3531. function showVideosWindow(body) {
  3532.     const els = $Q('.de-video-link');
  3533.     if(!els.length) {
  3534.         body.innerHTML = `<b>${ Lng.noVideoLinks[lang] }</b>`;
  3535.         return;
  3536.     }
  3537.     // EXCLUDED FROM FIREFOX EXTENSION - START
  3538.     if(!$id('de-ytube-api')) {
  3539.         // YouTube APT script. We can't insert scripts directly as html.
  3540.         const script = doc.createElement('script');
  3541.         script.type = 'text/javascript';
  3542.         script.src = aib.prot + '//www.youtube.com/player_api';
  3543.         doc.head.appendChild(script).id = 'de-ytube-api';
  3544.     }
  3545.     // EXCLUDED FROM FIREFOX EXTENSION - END
  3546.     body.innerHTML = `<div de-disableautoplay class="de-video-obj"></div>
  3547.     <div id="de-video-buttons">
  3548.         <a class="de-abtn" id="de-video-btn-prev" href="#" title="${ Lng.prevVideo[lang] }">&#x25C0;</a>
  3549.         <a class="de-abtn" id="de-video-btn-resize" href="#" title="${ Lng.expandVideo[lang] }"></a>
  3550.         <a class="de-abtn" id="de-video-btn-next" href="#" title="${ Lng.nextVideo[lang] }">&#x25B6;</a>
  3551.         <a class="de-abtn" id="de-video-btn-hide" href="#" title="${ Lng.hideLnkList[lang] }">&#x25B2;</a>
  3552.     </div>`;
  3553.     const linkList = $add(`<div id="de-video-list" style="max-width: ${
  3554.         +Cfg.YTubeWidth + 40 }px; max-height: ${
  3555.         nav.viewportHeight() * 0.92 - +Cfg.YTubeHeigh - 82 }px;"></div>`);
  3556.  
  3557.     // EXCLUDED FROM FIREFOX EXTENSION - START
  3558.     // A script to detect the end of current video playback, and auto play next. Uses YouTube API.
  3559.     // The first video should not start automatically!
  3560.     const script = doc.createElement('script');
  3561.     script.type = 'text/javascript';
  3562.     script.textContent = `(function() {
  3563.         if('YT' in window && 'Player' in window.YT) {
  3564.             onYouTubePlayerAPIReady();
  3565.         } else {
  3566.             window.onYouTubePlayerAPIReady = onYouTubePlayerAPIReady;
  3567.         }
  3568.         function onYouTubePlayerAPIReady() {
  3569.             window.de_addVideoEvents =
  3570.                 addEvents.bind(document.querySelector('#de-win-vid > .de-win-body > .de-video-obj'));
  3571.             window.de_addVideoEvents();
  3572.         }
  3573.         function addEvents() {
  3574.             var autoplay = true;
  3575.             if(this.hasAttribute('de-disableautoplay')) {
  3576.                 autoplay = false;
  3577.                 this.removeAttribute('de-disableautoplay');
  3578.             }
  3579.             new YT.Player(this.firstChild, { events: {
  3580.                 'onError': gotoNextVideo,
  3581.                 'onReady': autoplay ? function(e) {
  3582.                     e.target.playVideo();
  3583.                 } : Function.prototype,
  3584.                 'onStateChange': function(e) {
  3585.                     if(e.data === 0) {
  3586.                         gotoNextVideo();
  3587.                     }
  3588.                 }
  3589.             }});
  3590.         }
  3591.         function gotoNextVideo() {
  3592.             document.getElementById("de-video-btn-next").click();
  3593.         }
  3594.     })();`;
  3595.     body.appendChild(script);
  3596.     // EXCLUDED FROM FIREFOX EXTENSION - END
  3597.  
  3598.     // Events for control buttons
  3599.     body.addEventListener('click', {
  3600.         linkList,
  3601.         currentLink : null,
  3602.         listHidden  : false,
  3603.         player      : body.firstElementChild,
  3604.         playerInfo  : null,
  3605.         handleEvent(e) {
  3606.             const el = e.target;
  3607.             if(el.classList.contains('de-abtn')) {
  3608.                 let node;
  3609.                 switch(el.id) {
  3610.                 case 'de-video-btn-hide': { // Fold/unfold list of links
  3611.                     const isHide = this.listHidden = !this.listHidden;
  3612.                     $toggle(this.linkList, !isHide);
  3613.                     el.textContent = isHide ? '\u25BC' : '\u25B2';
  3614.                     break;
  3615.                 }
  3616.                 case 'de-video-btn-prev': // Play previous video
  3617.                     node = this.currentLink.parentNode;
  3618.                     node = node.previousElementSibling || node.parentNode.lastElementChild;
  3619.                     node.lastElementChild.click();
  3620.                     break;
  3621.                 case 'de-video-btn-next': // Play next video
  3622.                     node = this.currentLink.parentNode;
  3623.                     node = node.nextElementSibling || node.parentNode.firstElementChild;
  3624.                     node.lastElementChild.click();
  3625.                     break;
  3626.                 case 'de-video-btn-resize': { // Expand/collapse video player
  3627.                     const exp = this.player.className === 'de-video-obj';
  3628.                     this.player.className = exp ? 'de-video-obj de-video-expanded' : 'de-video-obj';
  3629.                     this.linkList.style.maxWidth = `${ exp ? 894 : +Cfg.YTubeWidth + 40 }px`;
  3630.                     this.linkList.style.maxHeight = `${ nav.viewportHeight() * 0.92 -
  3631.                         (exp ? 562 : +Cfg.YTubeHeigh + 82) }px`;
  3632.                 }
  3633.                 }
  3634.                 $pd(e);
  3635.                 return;
  3636.             } else if(!el.classList.contains('de-video-link')) { // Clicking on ">" before link
  3637.                 // Go to post that contains this link
  3638.                 pByNum.get(+el.getAttribute('de-num')).selectAndScrollTo();
  3639.                 return;
  3640.             }
  3641.             const info = el.videoInfo;
  3642.             if(this.playerInfo !== info) { // Prevents same link clicking
  3643.                 // Mark new link as a current and add player for it
  3644.                 if(this.currentLink) {
  3645.                     this.currentLink.classList.remove('de-current');
  3646.                 }
  3647.                 this.currentLink = el;
  3648.                 el.classList.add('de-current');
  3649.                 Videos.addPlayer(this, info, el.classList.contains('de-ytube'), true);
  3650.             }
  3651.             $pd(e);
  3652.         }
  3653.     }, true);
  3654.  
  3655.     // Copy all video links into videos list
  3656.     for(let i = 0, len = els.length; i < len; ++i) {
  3657.         updateVideoList(linkList, els[i], aib.getPostOfEl(els[i]).num);
  3658.     }
  3659.     body.appendChild(linkList);
  3660.     $q('.de-video-link', linkList).click();
  3661. }
  3662.  
  3663. function updateVideoList(parent, link, num) {
  3664.     const el = link.cloneNode(true);
  3665.     el.videoInfo = link.videoInfo;
  3666.     $bEnd(parent, `<div class="de-entry ${ aib.cReply }">
  3667.         <a class="de-video-refpost" title=">>${ num }" de-num="${ num }">&gt;&gt;</a>
  3668.     </div>`).appendChild(el).classList.remove('de-current');
  3669.     el.setAttribute('onclick', 'window.de_addVideoEvents && window.de_addVideoEvents();');
  3670. }
  3671.  
  3672. // HIDDEN THREADS WINDOW
  3673. function showHiddenWindow(body) {
  3674.     const hThr = HiddenThreads.getRawData();
  3675.     const hasThreads = !$isEmpty(hThr);
  3676.     if(hasThreads) {
  3677.         // Generate DOM for the list of hidden threads
  3678.         for(const b in hThr) {
  3679.             if($isEmpty(hThr[b])) {
  3680.                 continue;
  3681.             }
  3682.             const block = $bEnd(body,
  3683.                 `<div class="de-fold-block"><input type="checkbox"><b>/${ b }</b></div>`);
  3684.             block.firstChild.onclick =
  3685.                 e => $each($Q('.de-entry > input', block), el => (el.checked = e.target.checked));
  3686.             for(const tNum in hThr[b]) {
  3687.                 $bEnd(block, `<div class="de-entry ${ aib.cReply }" info="${ b };${ tNum }">
  3688.                     <input type="checkbox">
  3689.                     <a href="${ aib.getThrUrl(b, tNum) }" target="_blank">${ tNum }</a>
  3690.                     <div class="de-entry-title">- ${ hThr[b][tNum][2] }</div>
  3691.                 </div>`);
  3692.             }
  3693.         }
  3694.     }
  3695.     const btns = $bEnd(body, (!hasThreads ? `<center><b>${ Lng.noHidThr[lang] }</b></center>` : '') +
  3696.         '<div id="de-hid-buttons"></div>');
  3697.  
  3698.     // "Edit" button. Calls a popup with editor to edit Hidden in JSON.
  3699.     btns.appendChild(getEditButton('hidden', fn => fn(HiddenThreads.getRawData(), true, data => {
  3700.         HiddenThreads.saveRawData(data);
  3701.         Thread.first.updateHidden(data[aib.b]);
  3702.         toggleWindow('hid', true);
  3703.     })));
  3704.  
  3705.     // "Clear" button. Allows to clear 404'd threads.
  3706.     btns.appendChild($btn(Lng.clear[lang], Lng.clrDeleted[lang], async e => {
  3707.         // Sequentially load threads, and remove inaccessible
  3708.         const els = $Q('.de-entry[info]', e.target.parentNode.parentNode);
  3709.         for(let i = 0, len = els.length; i < len; ++i) {
  3710.             const [b, tNum] = els[i].getAttribute('info').split(';');
  3711.             await $ajax(aib.getThrUrl(b, tNum)).catch(err => {
  3712.                 if(err.code === 404) {
  3713.                     HiddenThreads.removeStorage(tNum, b);
  3714.                     HiddenPosts.removeStorage(tNum, b);
  3715.                 }
  3716.             });
  3717.         }
  3718.         toggleWindow('hid', true);
  3719.     }));
  3720.  
  3721.     // "Delete" button. Allows to delete selected threads
  3722.     btns.appendChild($btn(Lng.remove[lang], Lng.delEntries[lang], () => {
  3723.         $each($Q('.de-entry[info]', body), el => {
  3724.             if(!$q('input', el).checked) {
  3725.                 return;
  3726.             }
  3727.             const [brd, tNum] = el.getAttribute('info').split(';');
  3728.             const num = +tNum;
  3729.             if(pByNum.has(num)) {
  3730.                 pByNum.get(num).setUserVisib(false);
  3731.             } else {
  3732.                 sendStorageEvent('__de-post', { brd, num, hide: false, thrNum: num });
  3733.             }
  3734.             HiddenThreads.removeStorage(num, brd);
  3735.             HiddenPosts.set(num, num, false); // Actually unhide thread by its oppost
  3736.         });
  3737.         toggleWindow('hid', true);
  3738.     }));
  3739. }
  3740.  
  3741. /* ==[ WindowFavorites.js ]===================================================================================
  3742.                                               WINDOW: FAVORITES
  3743. =========================================================================================================== */
  3744.  
  3745. function saveRenewFavorites(favObj) {
  3746.     saveFavorites(favObj);
  3747.     toggleWindow('fav', true, favObj);
  3748. }
  3749.  
  3750. function removeFavEntry(favObj, h, b, num) {
  3751.     let f;
  3752.     if((h in favObj) && (b in favObj[h]) && (num in (f = favObj[h][b]))) {
  3753.         delete f[num];
  3754.         if(!(Object.keys(f).length - +f.hasOwnProperty('url') - +f.hasOwnProperty('hide'))) {
  3755.             delete favObj[h][b];
  3756.             if($isEmpty(favObj[h])) {
  3757.                 delete favObj[h];
  3758.             }
  3759.         }
  3760.     }
  3761. }
  3762.  
  3763. function toggleThrFavBtn(h, b, num, isEnable) {
  3764.     if(h === aib.host && b === aib.b && pByNum.has(num)) {
  3765.         const post = pByNum.get(num);
  3766.         post.toggleFavBtn(isEnable);
  3767.         post.thr.isFav = isEnable;
  3768.     }
  3769. }
  3770.  
  3771. function updateFavorites(num, value, mode) {
  3772.     readFavorites().then(favObj => {
  3773.         let isUpdate = false;
  3774.         let f = favObj[aib.host];
  3775.         if(!f || !f[aib.b] || !(f = f[aib.b][num])) {
  3776.             return;
  3777.         }
  3778.         switch(mode) {
  3779.         case 'error':
  3780.             if(f.err !== value) {
  3781.                 isUpdate = true;
  3782.             }
  3783.             f.err = value;
  3784.             break;
  3785.         case 'update':
  3786.             if(f.cnt !== value[0]) {
  3787.                 isUpdate = true;
  3788.             }
  3789.             f.cnt = value[0];
  3790.             f.new = f.you = 0;
  3791.             f.last = aib.anchor + value[1];
  3792.         }
  3793.         const data = [aib.host, aib.b, num, value, mode];
  3794.         if(isUpdate) {
  3795.             updateFavWindow(...data);
  3796.             saveFavorites(favObj);
  3797.             sendStorageEvent('__de-favorites', data);
  3798.         }
  3799.     });
  3800. }
  3801.  
  3802. function updateFavWindow(h, b, num, value, mode) {
  3803.     if(mode === 'add' || mode === 'delete') {
  3804.         toggleThrFavBtn(h, b, num, mode === 'add');
  3805.         toggleWindow('fav', true, value);
  3806.         return;
  3807.     }
  3808.     const winEl = $q('#de-win-fav > .de-win-body');
  3809.     if(!winEl || !winEl.hasChildNodes()) {
  3810.         return;
  3811.     }
  3812.     const el = $q(`.de-entry[de-host="${ h }"][de-board="${ b }"][de-num="${ num }"] > .de-fav-inf`, winEl);
  3813.     if(!el) {
  3814.         return;
  3815.     }
  3816.     const [iconEl, youEl, newEl, oldEl] = [...el.children];
  3817.     $hide(youEl);
  3818.     $hide(newEl);
  3819.     if(mode === 'error') {
  3820.         iconEl.firstElementChild.setAttribute('class', 'de-fav-inf-icon de-fav-unavail');
  3821.         iconEl.title = value;
  3822.         return;
  3823.     }
  3824.     youEl.textContent = 0;
  3825.     newEl.textContent = 0;
  3826.     oldEl.textContent = value[0];
  3827. }
  3828.  
  3829. // Delete previously marked entries from Favorites
  3830. function cleanFavorites() {
  3831.     const els = $Q('.de-entry[de-removed]');
  3832.     const len = els.length;
  3833.     if(!len) {
  3834.         return;
  3835.     }
  3836.     readFavorites().then(favObj => {
  3837.         for(let i = 0; i < len; ++i) {
  3838.             const el = els[i];
  3839.             const h = el.getAttribute('de-host');
  3840.             const b = el.getAttribute('de-board');
  3841.             const num = +el.getAttribute('de-num');
  3842.             removeFavEntry(favObj, h, b, num);
  3843.             toggleThrFavBtn(h, b, num, false);
  3844.         }
  3845.         saveRenewFavorites(favObj);
  3846.     });
  3847. }
  3848.  
  3849. function showFavoritesWindow(body, favObj) {
  3850.     let html = '';
  3851.     // Create the list of favorite threads
  3852.     for(const h in favObj) {
  3853.         for(const b in favObj[h]) {
  3854.             const f = favObj[h][b];
  3855.             const hb = `de-host="${ h }" de-board="${ b }"`;
  3856.             const delBtn = `<span class="de-fav-del-btn">
  3857.                 <svg><use xlink:href="#de-symbol-win-close"></use></svg>
  3858.             </span>`;
  3859.             let fArr, innerHtml = '';
  3860.             switch(Cfg.favThrOrder) {
  3861.             case 0: fArr = Object.entries(f); break;
  3862.             case 1: fArr = Object.entries(f).reverse(); break;
  3863.             case 2: fArr = Object.entries(f).sort((a, b) => (a[1].time || 0) - (b[1].time || 0)); break;
  3864.             case 3: fArr = Object.entries(f).sort((a, b) => (b[1].time || 0) - (a[1].time || 0));
  3865.             }
  3866.             for(let i = 0, len = fArr.length; i < len; ++i) {
  3867.                 const tNum = fArr[i][0];
  3868.                 if(tNum === 'url' || tNum === 'hide') {
  3869.                     continue;
  3870.                 }
  3871.                 const t = f[tNum];
  3872.                 if(!t.url.startsWith('http')) { // XXX: compatibility with older versions
  3873.                     t.url = (h === aib.host ? aib.prot + '//' : 'http://') + h + t.url;
  3874.                 }
  3875.                 // Generate DOM for separate entry
  3876.                 const favLinkHref = t.url + (
  3877.                     !t.last ? '' :
  3878.                     t.last.startsWith('#') ? t.last :
  3879.                     h === aib.host ? aib.anchor + t.last : '');
  3880.                 const favInfIwrapTitle = !t.err ? '' :
  3881.                     t.err === 'Closed' ? `title="${ Lng.thrClosed[lang] }"` : `title="${ t.err }"`;
  3882.                 const favInfIconClass = !t.err ? '' :
  3883.                     t.err === 'Closed' || t.err === 'Archived' ? 'de-fav-closed' : 'de-fav-unavail';
  3884.                 const favInfYouDisp = t.you ? '' : ' style="display: none;"';
  3885.                 const favInfNewDisp = t.new ? '' : ' style="display: none;"';
  3886.                 innerHtml += `<div class="de-entry ${ aib.cReply }" ${
  3887.                     hb } de-num="${ tNum }" de-url="${ t.url }">
  3888.                     ${ delBtn }
  3889.                     <a class="de-fav-link" title="${ Lng.goToThread[lang] }"` +
  3890.                         ` href="${ favLinkHref }" rel="noreferrer">${ tNum }</a>
  3891.                     <div class="de-entry-title">- ${ t.txt }</div>
  3892.                     <div class="de-fav-inf">
  3893.                         <span class="de-fav-inf-iwrap" ${ favInfIwrapTitle }>
  3894.                             <svg class="de-fav-inf-icon ${ favInfIconClass }">
  3895.                                 <use class="de-fav-closed-use" xlink:href="#de-symbol-closed"/>
  3896.                                 <use class="de-fav-unavail-use" xlink:href="#de-symbol-unavail"/>
  3897.                                 <use class="de-fav-wait-use" xlink:href="#de-symbol-wait"/>
  3898.                             </svg>
  3899.                         </span>
  3900.                         <span class="de-fav-inf-you" title="${ Lng.myPostsRep[lang] }"${ favInfYouDisp }>
  3901.                             ${ t.you || 0 }</span>
  3902.                         <span class="de-fav-inf-new" title="${ Lng.newPosts[lang] }"${ favInfNewDisp }>
  3903.                             ${ t.new || 0 }</span>
  3904.                         <span class="de-fav-inf-old" title="${ Lng.oldPosts[lang] }">${ t.cnt }</span>
  3905.                         <span class="de-fav-inf-page" title="${ Lng.thrPage[lang] }"></span>
  3906.                     </div>
  3907.                 </div>`;
  3908.             }
  3909.             if(!innerHtml) {
  3910.                 continue;
  3911.             }
  3912.             const isHide = f.hide === undefined ? h !== aib.host : f.hide;
  3913.             // Building a foldable block for specific board
  3914.             html += `<div class="de-fold-block${ h === aib.host && b === aib.b ? ' de-fav-current' : '' }">
  3915.                 <div class="de-fav-header">
  3916.                     ${ delBtn }
  3917.                     <a class="de-fav-header-link" title="${ Lng.goToBoard[lang] }"` +
  3918.                         ` href="${ f.url }" rel="noreferrer">${ h }/${ b }</a>
  3919.                     <a class="de-abtn de-fav-header-btn" title="${ Lng.toggleEntries[lang] }"` +
  3920.                         ` href="#">${ isHide ? '&#x25BC;' : '&#x25B2;' }</a>
  3921.                 </div>
  3922.                 <div class="de-fav-entries${ isHide ? ' de-fav-entries-hide' : '' }" ${ hb }>
  3923.                     ${ innerHtml }
  3924.                 </div>
  3925.             </div>`;
  3926.         }
  3927.     }
  3928.  
  3929.     // Appending DOM and events
  3930.     if(html) {
  3931.         $bEnd(body, `<div class="de-fav-table">${ html }</div>`).addEventListener('click', e => {
  3932.             let el = fixEventEl(e.target);
  3933.             let parentEl = el.parentNode;
  3934.             if(el.tagName.toLowerCase() === 'svg') {
  3935.                 el = parentEl;
  3936.                 parentEl = parentEl.parentNode;
  3937.             }
  3938.             switch(el.className) {
  3939.             case 'de-fav-link':
  3940.                 sesStorage['de-fav-win'] = '1'; // Favorites will open again after following a link
  3941.                 // We need to scroll to last seen post after following a link,
  3942.                 // remembering of scroll position is no longer needed
  3943.                 sesStorage.removeItem('de-scroll-' +
  3944.                     parentEl.getAttribute('de-board') + (parentEl.getAttribute('de-num') || ''));
  3945.                 break;
  3946.             case 'de-fav-del-btn': {
  3947.                 const wasChecked = el.getAttribute('de-checked') === '';
  3948.                 const toggleFn = btnEl => toggleAttr(btnEl, 'de-checked', '', !wasChecked);
  3949.                 toggleFn(el);
  3950.                 if(parentEl.className === 'de-fav-header') {
  3951.                     // Select/unselect all checkboxes in board block
  3952.                     const entriesEl = parentEl.nextElementSibling;
  3953.                     $each($Q('.de-fav-del-btn', entriesEl), toggleFn);
  3954.                     if(!wasChecked && entriesEl.classList.contains('de-fav-entries-hide')) {
  3955.                         entriesEl.classList.remove('de-fav-entries-hide');
  3956.                     }
  3957.                 }
  3958.                 const isShowDelBtns = !!$q('.de-entry > .de-fav-del-btn[de-checked]', body);
  3959.                 $toggle($id('de-fav-buttons'), !isShowDelBtns);
  3960.                 $toggle($id('de-fav-del-confirm'), isShowDelBtns);
  3961.                 break;
  3962.             }
  3963.             case 'de-abtn de-fav-header-btn': {
  3964.                 const entriesEl = parentEl.nextElementSibling;
  3965.                 const isHide = !entriesEl.classList.contains('de-fav-entries-hide');
  3966.                 el.innerHTML = isHide ? '&#x25BC' : '&#x25B2';
  3967.                 favObj[entriesEl.getAttribute('de-host')][entriesEl.getAttribute('de-board')].hide = isHide;
  3968.                 saveFavorites(favObj);
  3969.                 $pd(e);
  3970.                 entriesEl.classList.toggle('de-fav-entries-hide');
  3971.             }
  3972.             }
  3973.         });
  3974.     } else {
  3975.         $bEnd(body, `<center><b>${ Lng.noFavThr[lang] }</b></center>`);
  3976.     }
  3977.     const btns = $bEnd(body, '<div id="de-fav-buttons"></div>');
  3978.  
  3979.     // "Edit" button. Calls a popup with editor to edit Favorites in JSON.
  3980.     btns.appendChild(getEditButton('favor',
  3981.         fn => readFavorites().then(favObj => fn(favObj, true, saveRenewFavorites))));
  3982.  
  3983.     // "Refresh" button. Updates counters of new posts for each thread entry.
  3984.     btns.appendChild($btn(Lng.refresh[lang], Lng.infoCount[lang], async () => {
  3985.         const favObj = await readFavorites();
  3986.         if(!favObj[aib.host]) {
  3987.             return;
  3988.         }
  3989.         let isUpdate = false;
  3990.         let last404 = false;
  3991.         const myposts = JSON.parse(locStorage['de-myposts'] || '{}');
  3992.         const els = $Q('.de-entry');
  3993.         for(let i = 0, len = els.length; i < len; ++i) {
  3994.             const el = els[i];
  3995.             const host = el.getAttribute('de-host');
  3996.             const b = el.getAttribute('de-board');
  3997.             const num = el.getAttribute('de-num');
  3998.             const f = favObj[host][b][num];
  3999.             // Updating doesn't works for other domains because of different posts structure
  4000.             // Updating is not needed in closed threads
  4001.             if(host !== aib.host || f.err === 'Closed' || f.err === 'Archived') {
  4002.                 continue;
  4003.             }
  4004.             const [titleEl, youEl, countEl] = [...el.lastElementChild.children];
  4005.             const iconEl = titleEl.firstElementChild;
  4006.             // setAttribute for class is used because of SVG (for correct work in some browsers)
  4007.             iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-wait');
  4008.             titleEl.title = Lng.updating[lang];
  4009.             let form, isArchived;
  4010.             try {
  4011.                 if(!aib.iichan) {
  4012.                     form = await ajaxLoad(aib.getThrUrl(b, num));
  4013.                 } else {
  4014.                     [form, isArchived] = await ajaxLoad(aib.getThrUrl(b, num), true, false, true);
  4015.                 }
  4016.                 last404 = false;
  4017.             } catch(err) {
  4018.                 if((err instanceof AjaxError) && err.code === 404) { // Check for 404 error twice
  4019.                     if(last404) {
  4020.                         Thread.removeSavedData(b, num); // Doesn't work. Not done now.
  4021.                     } else {
  4022.                         last404 = true;
  4023.                         --i; // Repeat this cycle again
  4024.                         continue;
  4025.                     }
  4026.                 }
  4027.                 last404 = false;
  4028.                 $hide(countEl);
  4029.                 $hide(youEl);
  4030.                 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-unavail');
  4031.                 f.err = titleEl.title = getErrorMessage(err);
  4032.                 isUpdate = true;
  4033.                 continue;
  4034.             }
  4035.             if(aib.qClosed && $q(aib.qClosed, form)) { // Check for closed thread
  4036.                 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-closed');
  4037.                 titleEl.title = Lng.thrClosed[lang];
  4038.                 f.err = 'Closed';
  4039.                 isUpdate = true;
  4040.             } else if(isArchived) { // Moves archived threads into b/arch (iichan only)
  4041.                 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-closed');
  4042.                 titleEl.title = Lng.thrArchived[lang];
  4043.                 f.err = 'Archived';
  4044.                 const arch = b + '/arch';
  4045.                 const fo = favObj[host];
  4046.                 (fo[arch] || (fo[arch] = { url: favObj[host][b].url + 'arch/' }))[num] = Object.assign({}, f);
  4047.                 removeFavEntry(favObj, host, b, num);
  4048.                 isUpdate = true;
  4049.             } else {
  4050.                 // Thread is available and not closed
  4051.                 iconEl.setAttribute('class', 'de-fav-inf-icon');
  4052.                 titleEl.removeAttribute('title');
  4053.                 if(f.err) { // Cancel error status if existed
  4054.                     delete f.err;
  4055.                     isUpdate = true;
  4056.                 }
  4057.             }
  4058.             // Updating a counter of new posts
  4059.             const posts = $Q(aib.qRPost, form);
  4060.             const cnt = posts.length + 1 - f.cnt;
  4061.             countEl.textContent = cnt;
  4062.             if(cnt === 0) {
  4063.                 $hide(countEl); // Hide counter if no new posts
  4064.                 $hide(youEl);
  4065.             } else {
  4066.                 $show(countEl);
  4067.                 f.new = cnt;
  4068.                 isUpdate = true;
  4069.                 // Check for replies to my posts
  4070.                 if(myposts && myposts[b]) {
  4071.                     f.you = 0;
  4072.                     for(let j = 0; j < cnt; ++j) {
  4073.                         const links = $Q(aib.qPostMsg.split(', ').join(' a, ') + ' a',
  4074.                             posts[posts.length - 1 - j]);
  4075.                         for(let a = 0, len = links.length; a < len; ++a) {
  4076.                             const tc = links[a].textContent;
  4077.                             if(tc[0] === '>' && tc[1] === '>' && myposts[b][tc.substr(2)]) {
  4078.                                 f.you++;
  4079.                             }
  4080.                         }
  4081.                     }
  4082.                     if(f.you) {
  4083.                         youEl.textContent = f.you;
  4084.                         $show(youEl);
  4085.                     }
  4086.                 }
  4087.             }
  4088.         }
  4089.         AjaxCache.clearCache();
  4090.         if(isUpdate) {
  4091.             saveFavorites(favObj);
  4092.         }
  4093.     }));
  4094.  
  4095.     // "Page" button. Shows on which page every thread is existed.
  4096.     btns.appendChild($btn(Lng.page[lang], Lng.infoPage[lang], async () => {
  4097.         const els = $Q('.de-fav-current > .de-fav-entries > .de-entry');
  4098.         const len = els.length;
  4099.         if(!len) { // Cancel if no existed entries
  4100.             return;
  4101.         }
  4102.         $popup('load-pages', Lng.loading[lang], true);
  4103.         // Create indexed array of entries and "waiting" SVG icon for each entry
  4104.         const thrInfo = [];
  4105.         for(let i = 0; i < len; ++i) {
  4106.             const el = els[i];
  4107.             const iconEl = $q('.de-fav-inf-icon', el);
  4108.             const titleEl = iconEl.parentNode;
  4109.             thrInfo.push({
  4110.                 found     : false,
  4111.                 num       : +el.getAttribute('de-num'),
  4112.                 pageEl    : $q('.de-fav-inf-page', el),
  4113.                 iconClass : iconEl.getAttribute('class'),
  4114.                 iconEl,
  4115.                 iconTitle : titleEl.getAttribute('title'),
  4116.                 titleEl
  4117.             });
  4118.             iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-wait');
  4119.             titleEl.title = Lng.updating[lang];
  4120.         }
  4121.         // Sequentially load pages and search for favorites threads
  4122.         // We cannot know a count of pages while in the thread
  4123.         const endPage = (aib.lastPage || 10) + 1; // Check up to 10 page, if we don't know
  4124.         let infoLoaded = 0;
  4125.         const updateInf = (inf, page) => {
  4126.             inf.iconEl.setAttribute('class', inf.iconClass);
  4127.             toggleAttr(inf.titleEl, 'title', inf.iconTitle, inf.iconTitle);
  4128.             inf.pageEl.textContent = '@' + page;
  4129.         };
  4130.         for(let page = 0; page < endPage; ++page) {
  4131.             const tNums = new Set();
  4132.             try {
  4133.                 const form = await ajaxLoad(aib.getPageUrl(aib.b, page));
  4134.                 const els = DelForm.getThreads(form);
  4135.                 for(let i = 0, len = els.length; i < len; ++i) {
  4136.                     tNums.add(aib.getTNum(els[i]));
  4137.                 }
  4138.             } catch(err) {
  4139.                 continue;
  4140.             }
  4141.             // Search for threads on current page
  4142.             for(let i = 0; i < len; ++i) {
  4143.                 const inf = thrInfo[i];
  4144.                 if(tNums.has(inf.num)) {
  4145.                     updateInf(inf, page);
  4146.                     inf.found = true;
  4147.                     infoLoaded++;
  4148.                 }
  4149.             }
  4150.             if(infoLoaded === len) { // Stop pages loading when all favorite threads checked
  4151.                 break;
  4152.             }
  4153.         }
  4154.         // Process missed threads that not found
  4155.         for(let i = 0; i < len; ++i) {
  4156.             const inf = thrInfo[i];
  4157.             if(!inf.found) {
  4158.                 updateInf(inf, '?');
  4159.             }
  4160.         }
  4161.         closePopup('load-pages');
  4162.     }));
  4163.  
  4164.     // "Clear" button. Allows to clear 404'd threads.
  4165.     btns.appendChild($btn(Lng.clear[lang], Lng.clrDeleted[lang], async () => {
  4166.         // Sequentially load threads, and remove inaccessible
  4167.         let last404 = false;
  4168.         const els = $Q('.de-entry');
  4169.         const parent = $q('.de-fav-table');
  4170.         parent.classList.add('de-fav-table-unfold');
  4171.         for(let i = 0, len = els.length; i < len; ++i) {
  4172.             const el = els[i];
  4173.             const iconEl = $q('.de-fav-inf-icon', el);
  4174.             const titleEl = iconEl.parentNode;
  4175.             iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-wait');
  4176.             titleEl.title = Lng.updating[lang];
  4177.             await $ajax(el.getAttribute('de-url'), null, true).then(() => {
  4178.                 iconEl.setAttribute('class', 'de-fav-inf-icon');
  4179.                 titleEl.removeAttribute('title');
  4180.                 last404 = false;
  4181.             }).catch(err => {
  4182.                 if(err.code === 404) { // Check for 404 error twice
  4183.                     if(!last404) {
  4184.                         last404 = true;
  4185.                         --i; // Repeat this cycle again
  4186.                         return;
  4187.                     }
  4188.                     Thread.removeSavedData(el.getAttribute('de-board'), // Doesn't work. Not done now.
  4189.                         +el.getAttribute('de-num'));
  4190.                     el.setAttribute('de-removed', ''); // Mark an entry as deleted
  4191.                 }
  4192.                 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-unavail');
  4193.                 titleEl.title = getErrorMessage(err);
  4194.                 last404 = false;
  4195.             });
  4196.         }
  4197.         cleanFavorites(); // Delete marked entries
  4198.         parent.classList.remove('de-fav-table-unfold');
  4199.     }));
  4200.  
  4201.     // Deletion confirm/cancel buttons
  4202.     const delBtns = $bEnd(body, '<div id="de-fav-del-confirm" style="display: none;"></div>');
  4203.     delBtns.appendChild($btn(Lng.remove[lang], Lng.delEntries[lang], () => {
  4204.         $each($Q('.de-entry > .de-fav-del-btn[de-checked]', body),
  4205.             el => el.parentNode.setAttribute('de-removed', ''));
  4206.         cleanFavorites(); // Delete marked entries
  4207.         $show(btns);
  4208.         $hide(delBtns);
  4209.     }));
  4210.     delBtns.appendChild($btn(Lng.cancel[lang], '', () => {
  4211.         $each($Q('.de-fav-del-btn', body), el => el.removeAttribute('de-checked'));
  4212.         $show(btns);
  4213.         $hide(delBtns);
  4214.     }));
  4215. }
  4216.  
  4217. /* ==[ WindowSettings.js ]====================================================================================
  4218.                                                WINDOW: SETTINGS
  4219. =========================================================================================================== */
  4220.  
  4221. const CfgWindow = {
  4222.     initCfgWindow(body) {
  4223.         body.addEventListener('click', this);
  4224.         body.addEventListener('mouseover', this);
  4225.         body.addEventListener('mouseout', this);
  4226.         body.addEventListener('change', this);
  4227.         body.addEventListener('keyup', this);
  4228.         body.addEventListener('keydown', this);
  4229.         body.addEventListener('scroll', this);
  4230.  
  4231.         // Create tab bar and bottom buttons
  4232.         let div = $bEnd(body, `<div id="de-cfg-bar">${
  4233.             this._getTab('filters') +
  4234.             this._getTab('posts') +
  4235.             this._getTab('images') +
  4236.             this._getTab('links') +
  4237.             (pr.form || pr.oeForm ? this._getTab('form') : '') +
  4238.             this._getTab('common') +
  4239.             this._getTab('info')
  4240.         }</div><div id="de-cfg-buttons">${ this._getSel('language') }</div>`);
  4241.  
  4242.         // Open default or current tab
  4243.         this._clickTab(Cfg.cfgTab);
  4244.  
  4245.         // "Edit" button. Calls a popup with editor to edit Settings in JSON.
  4246.         div.appendChild(getEditButton('cfg', fn => fn(Cfg, true, data => {
  4247.             saveCfgObj(aib.dm, data);
  4248.             deWindow.location.reload();
  4249.         })));
  4250.  
  4251.         // "Global" button. Allows to save/load global settings.
  4252.         nav.hasGlobalStorage && div.appendChild($btn(Lng.global[lang], Lng.globalCfg[lang], () => {
  4253.             const el = $popup('cfg-global', `<b>${ Lng.globalCfg[lang] }:</b>`);
  4254.             // "Load" button. Applies global settings for current domain.
  4255.             $bEnd(el, `<div id="de-list"><input type="button" value="${
  4256.                 Lng.load[lang] }"> ${ Lng.loadGlobal[lang] }</div>`
  4257.             ).firstElementChild.onclick = () => getStoredObj('DESU_Config').then(data => {
  4258.                 if(data && ('global' in data) && !$isEmpty(data.global)) {
  4259.                     saveCfgObj(aib.dm, data.global);
  4260.                     deWindow.location.reload();
  4261.                 } else {
  4262.                     $popup('err-noglobalcfg', Lng.noGlobalCfg[lang]);
  4263.                 }
  4264.             });
  4265.             // "Save" button. Copies the domain settings into global.
  4266.             div = $bEnd(el, `<div id="de-list"><input type="button" value="${
  4267.                 Lng.save[lang] }"> ${ Lng.saveGlobal[lang] }</div>`
  4268.             ).firstElementChild.onclick = () => getStoredObj('DESU_Config').then(data => {
  4269.                 const obj = {};
  4270.                 const com = data[aib.dm];
  4271.                 for(const i in com) {
  4272.                     if(i !== 'correctTime' && i !== 'timePattern' && i !== 'userCSS' &&
  4273.                         i !== 'userCSSTxt' && i !== 'stats' && com[i] !== defaultCfg[i]
  4274.                     ) {
  4275.                         obj[i] = com[i];
  4276.                     }
  4277.                 }
  4278.                 data.global = obj;
  4279.                 saveCfgObj('global', data.global);
  4280.                 toggleWindow('cfg', true);
  4281.             });
  4282.             el.insertAdjacentHTML('beforeend', `<hr><small>${ Lng.descrGlobal[lang] }</small>`);
  4283.         }));
  4284.  
  4285.         // "File" button. Allows to save and load settings/favorites/hidden/etc from file.
  4286.         !nav.isPresto && div.appendChild($btn(Lng.file[lang], Lng.fileImpExp[lang], () => {
  4287.             const list = this._getList([
  4288.                 Lng.panelBtn.cfg[lang] + ' ' + Lng.allDomains[lang],
  4289.                 Lng.panelBtn.fav[lang],
  4290.                 Lng.hidPostThr[lang] + ` (${ aib.dm })`,
  4291.                 Lng.myPosts[lang] + ` (${ aib.dm })`
  4292.             ]);
  4293.             // Create popup with controls
  4294.             $popup('cfg-file', `<b>${ Lng.fileImpExp[lang] }:</b><hr><!--
  4295.                 --><div class="de-list">${ Lng.fileToData[lang] }:<div class="de-depend"><!--
  4296.                     --><input type="file" accept=".json" id="de-import-file"></div></div><hr><!--
  4297.                 --><div class="de-list"><a id="de-export-file" href="#">${ Lng.dataToFile[lang] }:<!--
  4298.                 --><div class="de-depend">${ list }</div></div>`);
  4299.             // Import data from a file to the storage
  4300.             $id('de-import-file').onchange = e => {
  4301.                 const file = e.target.files[0];
  4302.                 if(!file) {
  4303.                     return;
  4304.                 }
  4305.                 readFile(file, true).then(({ data }) => {
  4306.                     let obj;
  4307.                     try {
  4308.                         obj = JSON.parse(data);
  4309.                     } catch(err) {
  4310.                         $popup('err-invaliddata', Lng.invalidData[lang]);
  4311.                         return;
  4312.                     }
  4313.                     const cfgObj = obj.settings;
  4314.                     const favObj = obj.favorites;
  4315.                     const dmObj = obj[aib.dm];
  4316.                     const isOldCfg = !cfgObj && !favObj && !dmObj;
  4317.                     if(isOldCfg) {
  4318.                         setStored('DESU_Config', data);
  4319.                     }
  4320.                     if(cfgObj) {
  4321.                         try {
  4322.                             setStored('DESU_Config', JSON.stringify(cfgObj));
  4323.                             setStored('DESU_keys', JSON.stringify(obj.hotkeys));
  4324.                         } catch(err) {}
  4325.                     }
  4326.                     if(favObj) {
  4327.                         saveRenewFavorites(favObj);
  4328.                     }
  4329.                     if(dmObj) {
  4330.                         if(dmObj.posts) {
  4331.                             locStorage['de-posts'] = JSON.stringify(dmObj.posts);
  4332.                         }
  4333.                         if(dmObj.threads) {
  4334.                             locStorage['de-threads'] = JSON.stringify(dmObj.threads);
  4335.                         }
  4336.                         if(dmObj.myposts) {
  4337.                             locStorage['de-myposts'] = JSON.stringify(dmObj.myposts);
  4338.                         }
  4339.                     }
  4340.                     if(cfgObj || dmObj || isOldCfg) {
  4341.                         $popup('cfg-file', Lng.updating[lang], true);
  4342.                         deWindow.location.reload();
  4343.                         return;
  4344.                     }
  4345.                     closePopup('cfg-file');
  4346.                 });
  4347.             };
  4348.  
  4349.             // Export data from a storage to the file. The file will be named by date and type of storage.
  4350.             // For example, like "DE_20160727_1540_Cfg+Fav+domain.com(Hid+You).json".
  4351.             const expFile = $id('de-export-file');
  4352.             const els = $Q('input', expFile.nextElementSibling);
  4353.             els[0].checked = true;
  4354.             expFile.addEventListener('click', async e => {
  4355.                 const name = [], nameDm = [], d = new Date();
  4356.                 let val = [], valDm = [];
  4357.                 for(let i = 0, len = els.length; i < len; ++i) {
  4358.                     if(!els[i].checked) {
  4359.                         continue;
  4360.                     }
  4361.                     switch(i) {
  4362.                     case 0: name.push('Cfg'); {
  4363.                         const cfgData = await Promise.all([getStored('DESU_Config'), getStored('DESU_keys')]);
  4364.                         val.push(`"settings":${ cfgData[0] }`, `"hotkeys":${ cfgData[1] || '""' }`);
  4365.                         break;
  4366.                     }
  4367.                     case 1: name.push('Fav');
  4368.                         val.push(`"favorites":${ await getStored('DESU_Favorites') || '{}' }`);
  4369.                         break;
  4370.                     case 2: nameDm.push('Hid');
  4371.                         valDm.push(`"posts":${ locStorage['de-posts'] || '{}' }`,
  4372.                             `"threads":${ locStorage['de-threads'] || '{}' }`);
  4373.                         break;
  4374.                     case 3: nameDm.push('You');
  4375.                         valDm.push(`"myposts":${ locStorage['de-myposts'] || '{}' }`);
  4376.                     }
  4377.                 }
  4378.                 if((valDm = valDm.join(','))) {
  4379.                     val.push(`"${ aib.dm }":{${ valDm }}`);
  4380.                     name.push(`${ aib.dm } (${ nameDm.join('+') })`);
  4381.                 }
  4382.                 if((val = val.join(','))) {
  4383.                     downloadBlob(new Blob([`{${ val }}`], { type: 'application/json' }),
  4384.                         `DE_${ d.getFullYear() }${ pad2(d.getMonth() + 1) }${ pad2(d.getDate()) }_${
  4385.                             pad2(d.getHours()) }${ pad2(d.getMinutes()) }_${ name.join('+') }.json`);
  4386.                 }
  4387.                 $pd(e);
  4388.             }, true);
  4389.         }));
  4390.  
  4391.         // "Clear" button. Allows to clear settings/favorites/hidden/etc optionally.
  4392.         div.appendChild($btn(Lng.reset[lang] + '…', Lng.resetCfg[lang], () => $popup(
  4393.             'cfg-reset',
  4394.             `<b>${ Lng.resetData[lang] }:</b><hr>` +
  4395.             `<div class="de-list"><b>${ aib.dm }:</b>${
  4396.                 this._getList([Lng.panelBtn.cfg[lang], Lng.hidPostThr[lang], Lng.myPosts[lang]])
  4397.             }</div><hr>` +
  4398.             `<div class="de-list"><b>${ Lng.allDomains[lang] }:</b>${
  4399.                 this._getList([Lng.panelBtn.cfg[lang], Lng.panelBtn.fav[lang]])
  4400.             }</div><hr>`
  4401.         ).appendChild($btn(Lng.clear[lang], '', e => {
  4402.             const els = $Q('input[type="checkbox"]', e.target.parentNode);
  4403.             for(let i = 1, len = els.length; i < len; ++i) {
  4404.                 if(!els[i].checked) {
  4405.                     continue;
  4406.                 }
  4407.                 switch(i) {
  4408.                 case 1:
  4409.                     locStorage.removeItem('de-posts');
  4410.                     locStorage.removeItem('de-threads');
  4411.                     break;
  4412.                 case 2: locStorage.removeItem('de-myposts'); break;
  4413.                 case 4: delStored('DESU_Favorites');
  4414.                 }
  4415.             }
  4416.             if(els[3].checked) {
  4417.                 delStored('DESU_Config');
  4418.                 delStored('DESU_keys');
  4419.             } else if(els[0].checked) {
  4420.                 getStoredObj('DESU_Config').then(data => {
  4421.                     delete data[aib.dm];
  4422.                     setStored('DESU_Config', JSON.stringify(data));
  4423.                     $popup('cfg-reset', Lng.updating[lang], true);
  4424.                     deWindow.location.reload();
  4425.                 });
  4426.                 return;
  4427.             }
  4428.             $popup('cfg-reset', Lng.updating[lang], true);
  4429.             deWindow.location.reload();
  4430.         }))));
  4431.     },
  4432.  
  4433.     // Event handler for Setting window and its controls.
  4434.     handleEvent(e) {
  4435.         const { type, target: el } = e;
  4436.         const tag = el.tagName;
  4437.         if(type === 'click' && tag === 'DIV' && el.classList.contains('de-cfg-tab')) {
  4438.             const info = el.getAttribute('info');
  4439.             this._clickTab(info);
  4440.             saveCfg('cfgTab', info);
  4441.         }
  4442.         if(type === 'change' && tag === 'SELECT') {
  4443.             const info = el.getAttribute('info');
  4444.             saveCfg(info, el.selectedIndex);
  4445.             this._updateDependant();
  4446.             switch(info) {
  4447.             case 'language':
  4448.                 lang = el.selectedIndex;
  4449.                 Panel.removeMain();
  4450.                 if(pr.form) {
  4451.                     pr.addMarkupPanel();
  4452.                     pr.setPlaceholders();
  4453.                     pr.updateLanguage();
  4454.                     aib.updateSubmitBtn(pr.subm);
  4455.                     if(pr.files) {
  4456.                         $each($Q('.de-file-img, .de-file-txt-input', pr.form),
  4457.                             el => (el.title = Lng.youCanDrag[lang]));
  4458.                     }
  4459.                 }
  4460.                 this._updateCSS();
  4461.                 Panel.initPanel(DelForm.first.el);
  4462.                 toggleWindow('cfg', false);
  4463.                 break;
  4464.             case 'delHiddPost': {
  4465.                 const isHide = Cfg.delHiddPost === 1 || Cfg.delHiddPost === 2;
  4466.                 for(let post = Thread.first.op; post; post = post.next) {
  4467.                     if(post.isHidden && !post.isOp) {
  4468.                         post.wrap.classList.toggle('de-hidden', isHide);
  4469.                     }
  4470.                 }
  4471.                 updateCSS();
  4472.                 break;
  4473.             }
  4474.             case 'postBtnsCSS':
  4475.                 updateCSS();
  4476.                 if(nav.isPresto) {
  4477.                     $q('.de-svg-icons').remove();
  4478.                     addSVGIcons();
  4479.                 }
  4480.                 break;
  4481.             case 'thrBtns':
  4482.             case 'noSpoilers':
  4483.             case 'resizeImgs': updateCSS(); break;
  4484.             case 'expandImgs':
  4485.                 updateCSS();
  4486.                 AttachedImage.closeImg();
  4487.                 break;
  4488.             case 'imgNames':
  4489.                 if(Cfg.imgNames) {
  4490.                     for(const { el } of DelForm) {
  4491.                         processImgInfoLinks(el, 0, Cfg.imgNames);
  4492.                     }
  4493.                 } else {
  4494.                     $each($Q('.de-img-name'), el => (el.textContent = el.getAttribute('de-img-name-old')));
  4495.                 }
  4496.                 updateCSS();
  4497.                 break;
  4498.             case 'fileInputs':
  4499.                 pr.files.changeMode();
  4500.                 pr.setPlaceholders();
  4501.                 updateCSS();
  4502.                 break;
  4503.             case 'addPostForm':
  4504.                 pr.isBottom = Cfg.addPostForm === 1;
  4505.                 pr.setReply(false, !aib.t || Cfg.addPostForm > 1);
  4506.                 break;
  4507.             case 'addTextBtns': pr.addMarkupPanel();
  4508.                 /* falls through */
  4509.             case 'scriptStyle':
  4510.             case 'panelCounter': this._updateCSS(); break;
  4511.             case 'favThrOrder':
  4512.                 readFavorites().then(favObj => {
  4513.                     const body = $q('#de-win-fav > .de-win-body');
  4514.                     body.innerHTML = '';
  4515.                     showFavoritesWindow(body, favObj);
  4516.                 });
  4517.             }
  4518.             return;
  4519.         }
  4520.         if(type === 'click' && tag === 'INPUT' && el.type === 'checkbox') {
  4521.             const info = el.getAttribute('info');
  4522.             toggleCfg(info);
  4523.             this._updateDependant();
  4524.             switch(info) {
  4525.             case 'expandTrunc':
  4526.             case 'showHideBtn':
  4527.             case 'showRepBtn':
  4528.             case 'widePosts':
  4529.             case 'noPostNames':
  4530.             case 'imgNavBtns':
  4531.             case 'strikeHidd':
  4532.             case 'removeHidd':
  4533.             case 'noBoardRule':
  4534.             case 'userCSS': updateCSS(); break;
  4535.             case 'hideBySpell': Spells.toggle(); break;
  4536.             case 'sortSpells':
  4537.                 if(Cfg.sortSpells) {
  4538.                     Spells.toggle();
  4539.                 }
  4540.                 break;
  4541.             case 'hideRefPsts':
  4542.                 for(let post = Thread.first.op; post; post = post.next) {
  4543.                     if(!Cfg.hideRefPsts) {
  4544.                         post.ref.unhideRef();
  4545.                     } else if(post.isHidden) {
  4546.                         post.ref.hideRef();
  4547.                     }
  4548.                 }
  4549.                 break;
  4550.             case 'ajaxUpdThr':
  4551.                 if(aib.t) {
  4552.                     if(Cfg.ajaxUpdThr) {
  4553.                         updater.enableUpdater();
  4554.                     } else {
  4555.                         updater.disableUpdater();
  4556.                     }
  4557.                 }
  4558.                 break;
  4559.             case 'updCount': updater.toggleCounter(Cfg.updCount); break;
  4560.             case 'desktNotif':
  4561.                 if(Cfg.desktNotif) {
  4562.                     Notification.requestPermission();
  4563.                 }
  4564.                 break;
  4565.             case 'markNewPosts': Post.clearMarks(); break;
  4566.             case 'useDobrAPI': aib.JsonBuilder = Cfg.useDobrAPI ? DobrochanPostsBuilder : null; break;
  4567.             case 'markMyPosts':
  4568.             case 'markMyLinks':
  4569.                 if(!Cfg.markMyPosts && !Cfg.markMyLinks) {
  4570.                     locStorage.removeItem('de-myposts');
  4571.                     MyPosts.purge();
  4572.                 }
  4573.                 updateCSS();
  4574.                 break;
  4575.             case 'correctTime': DateTime.toggleSettings(el); break;
  4576.             case 'imgInfoLink': {
  4577.                 const img = $q('.de-fullimg-wrap');
  4578.                 if(img) {
  4579.                     img.click();
  4580.                 }
  4581.                 updateCSS();
  4582.                 break;
  4583.             }
  4584.             case 'imgSrcBtns':
  4585.                 if(Cfg.imgSrcBtns) {
  4586.                     for(const { el } of DelForm) {
  4587.                         processImgInfoLinks(el, 1, 0);
  4588.                         $each($Q('.de-img-embed'),
  4589.                             el => addImgSrcButtons(el.parentNode.nextSibling.nextSibling));
  4590.                     }
  4591.                 } else {
  4592.                     $delAll('.de-btn-src');
  4593.                 }
  4594.                 break;
  4595.             case 'addSageBtn':
  4596.                 PostForm.hideField($parent(pr.mail, 'LABEL') || pr.mail);
  4597.                 setTimeout(() => pr.toggleSage(), 0);
  4598.                 updateCSS();
  4599.                 break;
  4600.             case 'altCaptcha': pr.cap.initCapPromise(); break;
  4601.             case 'txtBtnsLoc':
  4602.                 pr.addMarkupPanel();
  4603.                 updateCSS();
  4604.                 break;
  4605.             case 'userPassw': PostForm.setUserPassw(); break;
  4606.             case 'userName': PostForm.setUserName(); break;
  4607.             case 'noPassword': $toggle($qParent(pr.passw, aib.qFormTr)); break;
  4608.             case 'noName': PostForm.hideField(pr.name); break;
  4609.             case 'noSubj': PostForm.hideField(pr.subj); break;
  4610.             case 'inftyScroll': toggleInfinityScroll(); break;
  4611.             case 'hotKeys':
  4612.                 if(Cfg.hotKeys) {
  4613.                     HotKeys.enableHotKeys();
  4614.                 } else {
  4615.                     HotKeys.disableHotKeys();
  4616.                 }
  4617.             }
  4618.             return;
  4619.         }
  4620.         if(type === 'click' && tag === 'INPUT' && el.type === 'button') {
  4621.             switch(el.id) {
  4622.             case 'de-cfg-button-pass':
  4623.                 $q('input[info="passwValue"]').value = Math.round(Math.random() * 1e12).toString(32);
  4624.                 PostForm.setUserPassw();
  4625.                 break;
  4626.             case 'de-cfg-button-keys':
  4627.                 $pd(e);
  4628.                 if($id('de-popup-edit-hotkeys')) {
  4629.                     return;
  4630.                 }
  4631.                 Promise.resolve(HotKeys.readKeys()).then(keys => {
  4632.                     const temp = KeyEditListener.getEditMarkup(keys);
  4633.                     const el = $popup('edit-hotkeys', temp[1]);
  4634.                     const fn = new KeyEditListener(el, keys, temp[0]);
  4635.                     el.addEventListener('focus', fn, true);
  4636.                     el.addEventListener('blur', fn, true);
  4637.                     el.addEventListener('click', fn, true);
  4638.                     el.addEventListener('keydown', fn, true);
  4639.                     el.addEventListener('keyup', fn, true);
  4640.                 });
  4641.                 break;
  4642.             case 'de-cfg-button-updnow':
  4643.                 $popup('updavail', Lng.loading[lang], true);
  4644.                 getStoredObj('DESU_Config')
  4645.                     .then(data => checkForUpdates(true, data.lastUpd))
  4646.                     .then(html => $popup('updavail', html), emptyFn);
  4647.                 break;
  4648.             case 'de-cfg-button-debug': {
  4649.                 const perf = {};
  4650.                 const arr = Logger.getLogData(true);
  4651.                 for(let i = 0, len = arr.length; i < len; ++i) {
  4652.                     perf[arr[i][0]] = arr[i][1];
  4653.                 }
  4654.                 $popup('cfg-debug', Lng.infoDebug[lang] + ':<textarea readonly class="de-editor"></textarea>'
  4655.                 ).firstElementChild.value = JSON.stringify({
  4656.                     version  : version + '.' + commit,
  4657.                     location : String(deWindow.location),
  4658.                     nav,
  4659.                     Cfg,
  4660.                     sSpells  : Spells.list.split('\n'),
  4661.                     oSpells  : sesStorage[`de-spells-${ aib.b }${ aib.t || '' }`],
  4662.                     perf
  4663.                 }, (key, value) => {
  4664.                     switch(key) {
  4665.                     case 'stats':
  4666.                     case 'nameValue':
  4667.                     case 'passwValue':
  4668.                     case 'ytApiKey': return void 0;
  4669.                     }
  4670.                     return key in defaultCfg && value === defaultCfg[key] ? void 0 : value;
  4671.                 }, '\t');
  4672.             }
  4673.             }
  4674.         }
  4675.         if(type === 'keyup' && tag === 'INPUT' && el.type === 'text') {
  4676.             const info = el.getAttribute('info');
  4677.             switch(info) {
  4678.             case 'postBtnsBack': {
  4679.                 const isCheck = checkCSSColor(el.value);
  4680.                 el.classList.toggle('de-input-error', !isCheck);
  4681.                 if(isCheck) {
  4682.                     saveCfg('postBtnsBack', el.value);
  4683.                     updateCSS();
  4684.                 }
  4685.                 break;
  4686.             }
  4687.             case 'limitPostMsg':
  4688.                 saveCfg('limitPostMsg', Math.max(+el.value || 0, 50));
  4689.                 updateCSS();
  4690.                 break;
  4691.             case 'minImgSize': saveCfg('minImgSize', Math.max(+el.value, 1)); break;
  4692.             case 'zoomFactor': saveCfg('zoomFactor', Math.min(Math.max(+el.value, 1), 100)); break;
  4693.             case 'webmVolume': {
  4694.                 const val = Math.min(+el.value || 0, 100);
  4695.                 saveCfg('webmVolume', val);
  4696.                 sendStorageEvent('__de-webmvolume', val);
  4697.                 break;
  4698.             }
  4699.             case 'minWebmWidth': saveCfg('minWebmWidth', Math.max(+el.value, Cfg.minImgSize)); break;
  4700.             case 'maskVisib':
  4701.                 saveCfg('maskVisib', Math.min(+el.value || 0, 100));
  4702.                 updateCSS();
  4703.                 break;
  4704.             case 'linksOver': saveCfg('linksOver', +el.value | 0); break;
  4705.             case 'linksOut': saveCfg('linksOut', +el.value | 0); break;
  4706.             case 'ytApiKey': saveCfg('ytApiKey', el.value.trim()); break;
  4707.             case 'passwValue': PostForm.setUserPassw(); break;
  4708.             case 'nameValue': PostForm.setUserName(); break;
  4709.             default: saveCfg(info, el.value);
  4710.             }
  4711.             return;
  4712.         }
  4713.         if(tag === 'A') {
  4714.             if(el.id === 'de-btn-spell-add') {
  4715.                 switch(e.type) {
  4716.                 case 'click': $pd(e); break;
  4717.                 case 'mouseover': el.odelay = setTimeout(() => addMenu(el), Cfg.linksOver); break;
  4718.                 case 'mouseout': clearTimeout(el.odelay);
  4719.                 }
  4720.                 return;
  4721.             }
  4722.             if(type === 'click') {
  4723.                 switch(el.id) {
  4724.                 case 'de-btn-spell-apply':
  4725.                     $pd(e);
  4726.                     saveCfg('hideBySpell', 1);
  4727.                     $q('input[info="hideBySpell"]').checked = true;
  4728.                     Spells.toggle();
  4729.                     break;
  4730.                 case 'de-btn-spell-clear':
  4731.                     $pd(e);
  4732.                     if(!confirm(Lng.clear[lang] + '?')) {
  4733.                         return;
  4734.                     }
  4735.                     $id('de-spell-txt').value = '';
  4736.                     Spells.toggle();
  4737.                 }
  4738.             }
  4739.             return;
  4740.         }
  4741.         if(tag === 'TEXTAREA' && el.id === 'de-spell-txt' && (type === 'keydown' || type === 'scroll')) {
  4742.             this._updateRowMeter(el);
  4743.         }
  4744.     },
  4745.  
  4746.     // Switch content in Settings by clicking on tab
  4747.     _clickTab(info) {
  4748.         const el = $q(`.de-cfg-tab[info="${ info }"]`);
  4749.         if(el.hasAttribute('selected')) {
  4750.             return;
  4751.         }
  4752.         const prefTab = $q('.de-cfg-body');
  4753.         if(prefTab) {
  4754.             prefTab.className = 'de-cfg-unvis';
  4755.             $q('.de-cfg-tab[selected]').removeAttribute('selected');
  4756.         }
  4757.         el.setAttribute('selected', '');
  4758.         const id = el.getAttribute('info');
  4759.         let newTab = $id('de-cfg-' + id);
  4760.         if(!newTab) {
  4761.             newTab = $aEnd($id('de-cfg-bar'),
  4762.                 id === 'filters' ? this._getCfgFilters() :
  4763.                 id === 'posts' ? this._getCfgPosts() :
  4764.                 id === 'images' ? this._getCfgImages() :
  4765.                 id === 'links' ? this._getCfgLinks() :
  4766.                 id === 'form' ? this._getCfgForm() :
  4767.                 id === 'common' ? this._getCfgCommon() :
  4768.                 this._getCfgInfo());
  4769.             if(id === 'filters') {
  4770.                 this._updateRowMeter($id('de-spell-txt'));
  4771.             }
  4772.             if(id === 'common') {
  4773.                 // XXX: remove and make insertion in this._getCfgCommon()
  4774.                 $after($q('input[info="userCSS"]').parentNode, getEditButton(
  4775.                     'css',
  4776.                     fn => fn(Cfg.userCSSTxt, false, function() {
  4777.                         saveCfg('userCSSTxt', this.value);
  4778.                         updateCSS();
  4779.                         toggleWindow('cfg', true);
  4780.                     }),
  4781.                     'de-cfg-button'
  4782.                 ));
  4783.             }
  4784.         }
  4785.         newTab.className = 'de-cfg-body';
  4786.         if(id === 'filters') {
  4787.             $id('de-spell-txt').value = Spells.list;
  4788.         }
  4789.         this._updateDependant();
  4790.  
  4791.         // Updates all inputs according to config
  4792.         const els = $Q('.de-cfg-chkbox, .de-cfg-inptxt, .de-cfg-select', newTab.parentNode);
  4793.         for(let i = 0, len = els.length; i < len; ++i) {
  4794.             const el = els[i];
  4795.             const info = el.getAttribute('info');
  4796.             if(el.tagName === 'INPUT') {
  4797.                 if(el.type === 'checkbox') {
  4798.                     el.checked = !!Cfg[info];
  4799.                 } else {
  4800.                     el.value = Cfg[info];
  4801.                 }
  4802.             } else {
  4803.                 el.selectedIndex = Cfg[info];
  4804.             }
  4805.         }
  4806.     },
  4807.  
  4808.     // "Filters" tab
  4809.     _getCfgFilters() {
  4810.         return `<div id="de-cfg-filters" class="de-cfg-unvis">
  4811.             <div id="de-spell-panel">
  4812.                 ${ this._getBox('hideBySpell') }
  4813.                 <a id="de-btn-spell-add" class="de-abtn de-spell-btn" href="#">${ Lng.add[lang] }</a>
  4814.                 <a id="de-btn-spell-apply" class="de-abtn de-spell-btn" href="#">${ Lng.apply[lang] }</a>
  4815.                 <a id="de-btn-spell-clear" class="de-abtn de-spell-btn" href="#">${ Lng.clear[lang] }</a>
  4816.                 <a class="de-abtn de-spell-btn" href="${ gitWiki }Spells-` +
  4817.                     `${ lang ? 'en' : 'ru' }" target="_blank">[?]</a>
  4818.             </div>
  4819.             <div id="de-spell-editor">
  4820.                 <div id="de-spell-rowmeter"></div>
  4821.                 <textarea id="de-spell-txt" wrap="off"></textarea>
  4822.             </div>
  4823.             ${ this._getBox('sortSpells') }<br>
  4824.             ${ this._getBox('hideRefPsts') }<br>
  4825.             ${ this._getBox('nextPageThr') }<br>
  4826.             ${ this._getSel('delHiddPost') }
  4827.         </div>`;
  4828.     },
  4829.  
  4830.     // "Posts" tab
  4831.     _getCfgPosts() {
  4832.         return `<div id="de-cfg-posts" class="de-cfg-unvis">
  4833.             ${ localData ? '' : `${ this._getBox('ajaxUpdThr') }
  4834.                 ${ this._getInp('updThrDelay') }
  4835.                 <div class="de-depend">
  4836.                     ${ this._getBox('updCount') }<br>
  4837.                     ${ this._getBox('favIcoBlink') }<br>
  4838.                     ${ 'Notification' in deWindow ? this._getBox('desktNotif') + '<br>' : '' }
  4839.                     ${ this._getBox('noErrInTitle') }<br>
  4840.                     ${ this._getBox('markNewPosts') }<br>
  4841.                     ${ aib.dobrochan ? this._getBox('useDobrAPI') : '' }
  4842.                 </div>` }
  4843.             ${ this._getBox('markMyPosts') }<br>
  4844.             ${ !localData ? `${ this._getBox('expandTrunc') }<br>` : '' }
  4845.             ${ this._getSel('showHideBtn') }<br>
  4846.             ${ !localData ? this._getSel('showRepBtn') : '' }<br>
  4847.             ${ this._getSel('postBtnsCSS') }
  4848.             ${ this._getInp('postBtnsBack', false, 8) }<br>
  4849.             ${ !localData ? this._getSel('thrBtns') : '' }<br>
  4850.             ${ this._getSel('noSpoilers') }<br>
  4851.             ${ this._getInp('limitPostMsg', true, 5) }<br>
  4852.             ${ this._getBox('widePosts') }<br>
  4853.             ${ this._getBox('noPostNames') }<br>
  4854.             ${ this._getBox('correctTime') }
  4855.             ${ this._getInp('timeOffset', true, 1) }
  4856.             <a class="de-abtn" target="_blank" href="${ gitWiki }Settings-time-` +
  4857.                 `${ lang ? 'en' : 'ru' }">[?]</a>
  4858.             <div class="de-depend">
  4859.                 ${ this._getInp('timePattern', true, 24) }<br>
  4860.                 ${ this._getInp('timeRPattern', true, 24) }
  4861.             </div>
  4862.         </div>`;
  4863.     },
  4864.  
  4865.     // "Images" tab
  4866.     _getCfgImages() {
  4867.         return `<div id="de-cfg-images" class="de-cfg-unvis">
  4868.             ${ this._getSel('expandImgs') }<br>
  4869.             <div class="de-depend">
  4870.                 ${ this._getBox('imgNavBtns') }<br>
  4871.                 ${ this._getBox('imgInfoLink') }<br>
  4872.                 ${ this._getSel('resizeImgs') }<br>
  4873.                 ${ Post.sizing.dPxRatio > 1 ? this._getBox('resizeDPI') + '<br>' : '' }
  4874.                 ${ this._getInp('minImgSize') }<br>
  4875.                 ${ this._getInp('zoomFactor') }<br>
  4876.                 ${ this._getBox('webmControl') }<br>
  4877.                 ${ this._getBox('webmTitles') }<br>
  4878.                 ${ this._getInp('webmVolume') }<br>
  4879.                 ${ this._getInp('minWebmWidth') }
  4880.             </div>
  4881.             ${ nav.isPresto ? '' : this._getSel('preLoadImgs') + '<br>' }
  4882.             ${ nav.isPresto || aib._4chan ? '' : `<div class="de-depend">
  4883.                 ${ this._getBox('findImgFile') }
  4884.             </div>` }
  4885.             ${ this._getSel('openImgs') }<br>
  4886.             ${ this._getBox('imgSrcBtns') }<br>
  4887.             ${ this._getSel('imgNames') }<br>
  4888.             ${ this._getInp('maskVisib') }
  4889.         </div>`;
  4890.     },
  4891.  
  4892.     // "Links" tab
  4893.     _getCfgLinks() {
  4894.         return `<div id="de-cfg-links" class="de-cfg-unvis">
  4895.             ${ this._getBox('linksNavig') }
  4896.             <div class="de-depend">
  4897.                 ${ this._getInp('linksOver') }
  4898.                 ${ this._getInp('linksOut') }<br>
  4899.                 ${ this._getBox('markViewed') }<br>
  4900.                 ${ this._getBox('strikeHidd') }
  4901.                 <div class="de-depend">${ this._getBox('removeHidd') }</div>
  4902.                 ${ this._getBox('noNavigHidd') }
  4903.             </div>
  4904.             ${ this._getBox('markMyLinks') }<br>
  4905.             ${ this._getBox('crossLinks') }<br>
  4906.             ${ this._getBox('decodeLinks') }<br>
  4907.             ${ this._getBox('insertNum') }<br>
  4908.             ${ !localData ? `${ this._getBox('addOPLink') }<br>
  4909.                 ${ this._getBox('addImgs') }<br>` : '' }
  4910.             <div>
  4911.                 ${ this._getBox('addMP3') }
  4912.                 ${ aib.prot === 'http:' ? this._getBox('addVocaroo') : '' }
  4913.             </div>
  4914.             ${ this._getSel('embedYTube') }
  4915.             <div class="de-depend">
  4916.                 ${ this._getInp('YTubeWidth', false) }\u00D7
  4917.                 ${ this._getInp('YTubeHeigh', false) }(px)<br>
  4918.                 ${ this._getBox('YTubeTitles') }<br>
  4919.                 ${ this._getInp('ytApiKey', true, 25) }<br>
  4920.                 ${ this._getBox('addVimeo') }
  4921.             </div>
  4922.         </div>`;
  4923.     },
  4924.  
  4925.     // "Form" tab
  4926.     _getCfgForm() {
  4927.         return `<div id="de-cfg-form" class="de-cfg-unvis">
  4928.             ${ this._getBox('ajaxPosting') }<br>
  4929.             ${ pr.form ? `<div class="de-depend">
  4930.                 ${ this._getBox('postSameImg') }<br>
  4931.                 ${ this._getBox('removeEXIF') }<br>
  4932.                 ${ this._getSel('removeFName') }<br>
  4933.                 ${ this._getBox('sendErrNotif') }<br>
  4934.                 ${ this._getBox('scrAfterRep') }<br>
  4935.                 ${ pr.files && !nav.isPresto ? this._getSel('fileInputs') : '' }
  4936.             </div>` : '' }
  4937.             ${ pr.form ? this._getSel('addPostForm') + '<br>' : '' }
  4938.             ${ pr.txta ? this._getBox('spacedQuote') + '<br>' : '' }
  4939.             ${ this._getBox('favOnReply') }<br>
  4940.             ${ pr.subj ? this._getBox('warnSubjTrip') + '<br>' : '' }
  4941.             ${ pr.mail ? `${ this._getBox('addSageBtn') }
  4942.                 ${ this._getBox('saveSage') }<br>` : '' }
  4943.             ${ pr.cap ? `${ aib.hasAltCaptcha ? `${ this._getBox('altCaptcha') }<br>` : '' }
  4944.                 ${ this._getInp('capUpdTime') }<br>
  4945.                 ${ this._getSel('captchaLang') }<br>` : '' }
  4946.             ${ pr.txta ? `${ this._getSel('addTextBtns') }
  4947.                 ${ !aib._4chan ? this._getBox('txtBtnsLoc') : '' }<br>` : '' }
  4948.             ${ pr.passw ? `${ this._getInp('passwValue', false, 9) }
  4949.                 ${ this._getBox('userPassw') }<input type="button"` +
  4950.                 ` id="de-cfg-button-pass" class="de-cfg-button" value="${ Lng.change[lang] }"><br>` : '' }
  4951.             ${ pr.name ? `${ this._getInp('nameValue', false, 9) }
  4952.                 ${ this._getBox('userName') }<br>` : '' }
  4953.             ${ pr.rules || pr.passw || pr.name ? Lng.hide[lang] +
  4954.                 (pr.rules ? this._getBox('noBoardRule') : '') +
  4955.                 (pr.passw ? this._getBox('noPassword') : '') +
  4956.                 (pr.name ? this._getBox('noName') : '') +
  4957.                 (pr.subj ? this._getBox('noSubj') : '') : '' }
  4958.         </div>`;
  4959.     },
  4960.  
  4961.     // "Common" tab
  4962.     _getCfgCommon() {
  4963.         return `<div id="de-cfg-common" class="de-cfg-unvis">
  4964.             ${ this._getSel('scriptStyle') }<br>
  4965.             ${ this._getBox('userCSS') }
  4966.             <a href="${ gitWiki }css-tricks" class="de-abtn" target="_blank">[?]</a><br>
  4967.             ${ 'animation' in docBody.style ? this._getBox('animation') + '<br>' : '' }
  4968.             ${ this._getBox('hotKeys') }
  4969.             <input type="button" id="de-cfg-button-keys" class="de-cfg-button" value="${ Lng.edit[lang] }">
  4970.             <div class="de-depend">${ this._getInp('loadPages') }</div>
  4971.             ${ this._getSel('panelCounter') }<br>
  4972.             ${ this._getBox('rePageTitle') }<br>
  4973.             ${ !localData ? `${ this._getBox('inftyScroll') }<br>
  4974.                 ${ this._getBox('hideReplies') }<br>
  4975.                 ${ this._getBox('scrollToTop') }<br>` : '' }
  4976.             ${ this._getBox('saveScroll') }<br>
  4977.             ${ this._getSel('favThrOrder') }<br>
  4978.             ${ this._getBox('favWinOn') }<br>
  4979.             ${ this._getBox('closePopups') }
  4980.         </div>`;
  4981.     },
  4982.  
  4983.     // "Info" tab
  4984.     _getCfgInfo() {
  4985.         const statsTable = this._getInfoTable([
  4986.             [Lng.thrViewed[lang], Cfg.stats.view],
  4987.             [Lng.thrCreated[lang], Cfg.stats.op],
  4988.             [Lng.thrHidden[lang], HiddenThreads.getCount()],
  4989.             [Lng.postsSent[lang], Cfg.stats.reply]
  4990.         ], false);
  4991.         return `<div id="de-cfg-info" class="de-cfg-unvis">
  4992.             <div style="padding-bottom: 10px;">
  4993.                 <a href="${ gitWiki }versions" target="_blank">v${ version }.${ commit }` +
  4994.                     `${ nav.isESNext ? '.es6' : '' }</a> |
  4995.                 <a href="https://dscript.me/" target="_blank">Homepage</a> |
  4996.                 <a href="${ gitWiki }${ lang === 1 ? 'home-en/' : '' }" target="_blank">Github</a> |
  4997.                 <input type="button" id="de-cfg-button-debug" value="` +
  4998.                     `${ Lng.debug[lang] }" title="${ Lng.infoDebug[lang] }">
  4999.             </div>
  5000.             <div id="de-info-table">
  5001.                 <div id="de-info-stats">${ statsTable }</div>
  5002.                 <div id="de-info-log">${ this._getInfoTable(Logger.getLogData(false), true) }</div>
  5003.             </div>
  5004.             ${ !nav.hasWebStorage && !nav.isPresto && !localData || nav.hasGMXHR ? `
  5005.                 <div style="margin-top: 3px; text-align: center;">&gt;&gt;
  5006.                     <input type="button" id="de-cfg-button-updnow" value="${ Lng.checkNow[lang] }">
  5007.                 &lt;&lt;</div>
  5008.                 ${ this._getSel('updDollchan') }` : '' }
  5009.         </div>`;
  5010.     },
  5011.  
  5012.     // Creates a label with checkbox for option switching
  5013.     _getBox: id => `<label class="de-cfg-label">
  5014.         <input class="de-cfg-chkbox" info="${ id }" type="checkbox"> ${ Lng.cfg[id][lang] }</label>`,
  5015.     // Creates a table for Info tab
  5016.     _getInfoTable: (data, needMs) => data.map(val => `<div class="de-info-row">
  5017.         <span class="de-info-name">${ val[0] }</span>
  5018.         <span>${ val[1] + (needMs ? 'ms' : '') }</span></div>`).join(''),
  5019.     // Creates a text input for text option values
  5020.     _getInp(id, addText = true, size = 2) {
  5021.         const el = doc.createElement('div');
  5022.         el.appendChild($txt(Cfg[id])); // Escape HTML
  5023.         return `<label class="de-cfg-label">
  5024.         <input class="de-cfg-inptxt" info="${ id }" type="text" size="${ size }" value="${
  5025.         el.innerHTML }">${ addText && Lng.cfg[id] ? Lng.cfg[id][lang] : '' }</label>`;
  5026.     },
  5027.     // Creates a menu with a list of checkboxes. Uses for popup window.
  5028.     _getList : arr => arrTags(arr, '<label class="de-block"><input type="checkbox"> ', '</label>'),
  5029.     // Creates a select for multiple option values
  5030.     _getSel  : id => `<label class="de-cfg-label"><select class="de-cfg-select" info="${ id }">${
  5031.         Lng.cfg[id].sel[lang].map((val, i) => `<option value="${ i }">${ val }</option>`).join('')
  5032.     }</select> ${ Lng.cfg[id].txt[lang] } </label>`,
  5033.     // Creates a tab for tab bar
  5034.     _getTab: id => `<div class="${ aib.cReply } de-cfg-tab" info="${ id }">${ Lng.cfgTab[id][lang] }</div>`,
  5035.     // Switching the dependent inputs according to their parents
  5036.     _toggleDependant(state, arr) {
  5037.         let i = arr.length;
  5038.         const nState = !state;
  5039.         while(i--) {
  5040.             const el = $q(arr[i]);
  5041.             if(el) {
  5042.                 el.disabled = nState;
  5043.             }
  5044.         }
  5045.     },
  5046.     _updateCSS() {
  5047.         $delAll('#de-css, #de-css-dynamic, #de-css-user', doc.head);
  5048.         scriptCSS();
  5049.     },
  5050.     _updateDependant() {
  5051.         const fn = this._toggleDependant;
  5052.         fn(Cfg.ajaxUpdThr, [
  5053.             'input[info="updThrDelay"]', 'input[info="updCount"]', 'input[info="favIcoBlink"]',
  5054.             'input[info="markNewPosts"]', 'input[info="desktNotif"]', 'input[info="noErrInTitle"]'
  5055.         ]);
  5056.         fn(Cfg.postBtnsCSS === 2, ['input[info="postBtnsBack"]']);
  5057.         fn(Cfg.expandImgs, [
  5058.             'input[info="imgNavBtns"]', 'input[info="imgInfoLink"]', 'input[info="resizeDPI"]',
  5059.             'select[info="resizeImgs"]', 'input[info="minImgSize"]', 'input[info="zoomFactor"]',
  5060.             'input[info="webmControl"]', 'input[info="webmTitles"]', 'input[info="webmVolume"]',
  5061.             'input[info="minWebmWidth"]'
  5062.         ]);
  5063.         fn(Cfg.preLoadImgs, ['input[info="findImgFile"]']);
  5064.         fn(Cfg.linksNavig, [
  5065.             'input[info="linksOver"]', 'input[info="linksOut"]', 'input[info="markViewed"]',
  5066.             'input[info="strikeHidd"]', 'input[info="noNavigHidd"]'
  5067.         ]);
  5068.         fn(Cfg.strikeHidd && Cfg.linksNavig, ['input[info="removeHidd"]']);
  5069.         fn(Cfg.embedYTube, [
  5070.             'input[info="YTubeWidth"]', 'input[info="YTubeHeigh"]', 'input[info="YTubeTitles"]',
  5071.             'input[info="ytApiKey"]', 'input[info="addVimeo"]'
  5072.         ]);
  5073.         fn(Cfg.YTubeTitles, ['input[info="ytApiKey"]']);
  5074.         fn(Cfg.ajaxPosting, [
  5075.             'input[info="postSameImg"]', 'input[info="removeEXIF"]', 'select[info="removeFName"]',
  5076.             'input[info="sendErrNotif"]', 'input[info="scrAfterRep"]', 'select[info="fileInputs"]'
  5077.         ]);
  5078.         fn(Cfg.addSageBtn, ['input[info="saveSage"]']);
  5079.         fn(Cfg.addTextBtns, ['input[info="txtBtnsLoc"]']);
  5080.         fn(Cfg.hotKeys, ['input[info="loadPages"]']);
  5081.     },
  5082.     // Updates row counter in spells editor
  5083.     _updateRowMeter(node) {
  5084.         const top = node.scrollTop;
  5085.         const el = node.previousElementSibling;
  5086.         let num = el.numLines || 1;
  5087.         let i = 19;
  5088.         if(num - i < ((top / 12) | 0 + 1)) {
  5089.             let str = '';
  5090.             while(i--) {
  5091.                 str += `${ num++ }<br>`;
  5092.             }
  5093.             el.insertAdjacentHTML('beforeend', str);
  5094.             el.numLines = num;
  5095.         }
  5096.         el.scrollTop = top;
  5097.     }
  5098. };
  5099.  
  5100. /* ==[ MenuPopups.js ]========================================================================================
  5101.                                                 POPUPS & MENU
  5102. =========================================================================================================== */
  5103.  
  5104. function closePopup(data) {
  5105.     const el = typeof data === 'string' ? $id('de-popup-' + data) : data;
  5106.     if(el) {
  5107.         el.closeTimeout = null;
  5108.         if(Cfg.animation) {
  5109.             $animate(el, 'de-close', true);
  5110.         } else {
  5111.             el.remove();
  5112.         }
  5113.     }
  5114. }
  5115.  
  5116. function $popup(id, txt, isWait = false) {
  5117.     let el = $id('de-popup-' + id);
  5118.     const buttonHTML = isWait ? '<svg class="de-wait"><use xlink:href="#de-symbol-wait"/></svg>' : '\u2716 ';
  5119.     if(el) {
  5120.         $q('div', el).innerHTML = txt.trim();
  5121.         $q('span', el).innerHTML = buttonHTML;
  5122.         if(!isWait && Cfg.animation) {
  5123.             $animate(el, 'de-blink');
  5124.         }
  5125.     } else {
  5126.         el = $bEnd($id('de-wrapper-popup'), `<div class="${ aib.cReply } de-popup" id="de-popup-${ id }">
  5127.             <span class="de-popup-btn">${ buttonHTML }</span>
  5128.             <div class="de-popup-msg">${ txt.trim() }</div>
  5129.         </div>`);
  5130.         el.onclick = e => {
  5131.             let el = fixEventEl(e.target);
  5132.             el = el.tagName.toLowerCase() === 'svg' ? el.parentNode : el;
  5133.             if(el.className === 'de-popup-btn') {
  5134.                 closePopup(el.parentNode);
  5135.             }
  5136.         };
  5137.         if(Cfg.animation) {
  5138.             $animate(el, 'de-open');
  5139.         }
  5140.     }
  5141.     if(Cfg.closePopups && !isWait && !id.includes('edit') && !id.includes('cfg')) {
  5142.         el.closeTimeout = setTimeout(closePopup, 6e3, el);
  5143.     }
  5144.     return el.lastElementChild;
  5145. }
  5146.  
  5147. // Adds button that calls a popup with the text editor. Useful to edit settings.
  5148. function getEditButton(name, getDataFn, className = 'de-button') {
  5149.     return $btn(Lng.edit[lang], Lng.editInTxt[lang], () => getDataFn((val, isJSON, saveFn) => {
  5150.         // Create popup window with textarea.
  5151.         const el = $popup('edit-' + name,
  5152.             `<b>${ Lng.editor[name][lang] }</b><textarea class="de-editor"></textarea>`);
  5153.         const ta = el.lastChild;
  5154.         ta.value = isJSON ? JSON.stringify(val, null, '\t') : val;
  5155.         // "Save" button. If there a JSON data, parses and saves on success.
  5156.         el.appendChild($btn(Lng.save[lang], Lng.saveChanges[lang], !isJSON ? saveFn.bind(ta) : () => {
  5157.             let data;
  5158.             try {
  5159.                 data = JSON.parse(ta.value.trim().replace(/[\n\r\t]/g, '') || '{}');
  5160.             } finally {
  5161.                 if(!data) {
  5162.                     $popup('err-invaliddata', Lng.invalidData[lang]);
  5163.                     return;
  5164.                 }
  5165.                 saveFn(data);
  5166.                 closePopup('edit-' + name);
  5167.                 closePopup('err-invaliddata');
  5168.             }
  5169.         }));
  5170.     }), className);
  5171. }
  5172.  
  5173. class Menu {
  5174.     constructor(parentEl, html, clickFn, isFixed = true) {
  5175.         this.onout = null;
  5176.         this.onover = null;
  5177.         this.onremove = null;
  5178.         this._closeTO = 0;
  5179.         const el = $bEnd(docBody, `<div class="${ aib.cReply } de-menu" style="position: ${
  5180.             isFixed ? 'fixed' : 'absolute' }; left: 0px; top: 0px; visibility: hidden;">${ html }</div>`);
  5181.         const cr = parentEl.getBoundingClientRect();
  5182.         const { style, offsetWidth: w, offsetHeight: h } = el;
  5183.         style.left = (isFixed ? 0 : deWindow.pageXOffset) +
  5184.             (cr.left + w < Post.sizing.wWidth ? cr.left : cr.right - w) + 'px';
  5185.         style.top = (isFixed ? 0 : deWindow.pageYOffset) +
  5186.             (cr.bottom + h < Post.sizing.wHeight ? cr.bottom - 0.5 : cr.top - h + 0.5) + 'px';
  5187.         style.removeProperty('visibility');
  5188.         this._clickFn = clickFn;
  5189.         this._el = el;
  5190.         this.parentEl = parentEl;
  5191.         el.addEventListener('mouseover', this, true);
  5192.         el.addEventListener('mouseout', this, true);
  5193.         el.addEventListener('click', this);
  5194.         parentEl.addEventListener('mouseout', this);
  5195.     }
  5196.     static getMenuImgSrc(data) {
  5197.         let p;
  5198.         if(typeof data === 'string') {
  5199.             p = encodeURIComponent(data) + '" target="_blank">' + Lng.frameSearch[lang];
  5200.         } else {
  5201.             const link = data.nextSibling;
  5202.             p = encodeURIComponent(data.getAttribute('de-href') || link.getAttribute('de-href') ||
  5203.                 link.href) + '" target="_blank">' + Lng.searchIn[lang];
  5204.         }
  5205.         return arrTags([
  5206.             `de-src-google" href="https://www.google.com/searchbyimage?image_url=${ p }Google`,
  5207.             `de-src-yandex" href="https://yandex.com/images/search?rpt=imageview&img_url=${ p }Yandex`,
  5208.             `de-src-tineye" href="https://tineye.com/search/?url=${ p }TinEye`,
  5209.             `de-src-saucenao" href="https://saucenao.com/search.php?url=${ p }SauceNAO`,
  5210.             `de-src-iqdb" href="https://iqdb.org/?url=${ p }IQDB`,
  5211.             `de-src-tracemoe" href="https://trace.moe/?auto&url=${ p }TraceMoe`
  5212.         ], '<a class="de-menu-item ', '</a>');
  5213.     }
  5214.     handleEvent(e) {
  5215.         let isOverEvent = false;
  5216.         switch(e.type) {
  5217.         case 'click':
  5218.             if(e.target.classList.contains('de-menu-item')) {
  5219.                 this.removeMenu();
  5220.                 this._clickFn(e.target);
  5221.                 if(!Cfg.expandPanel && !$q('.de-win-active')) {
  5222.                     $hide($id('de-panel-buttons'));
  5223.                 }
  5224.             }
  5225.             break;
  5226.         case 'mouseover': isOverEvent = true;
  5227.             /* falls through */
  5228.         case 'mouseout': {
  5229.             clearTimeout(this._closeTO);
  5230.             let rt = fixEventEl(e.relatedTarget);
  5231.             rt = rt && rt.farthestViewportElement || rt;
  5232.             if(!rt || (rt !== this._el && !this._el.contains(rt))) {
  5233.                 if(isOverEvent) {
  5234.                     if(this.onover) {
  5235.                         this.onover();
  5236.                     }
  5237.                 } else if(!rt || (rt !== this.parentEl && !this.parentEl.contains(rt))) {
  5238.                     this._closeTO = setTimeout(() => this.removeMenu(), 75);
  5239.                     if(this.onout) {
  5240.                         this.onout();
  5241.                     }
  5242.                 }
  5243.             }
  5244.         }
  5245.         }
  5246.     }
  5247.     removeMenu() {
  5248.         if(!this._el) {
  5249.             return;
  5250.         }
  5251.         if(this.onremove) {
  5252.             this.onremove();
  5253.         }
  5254.         this._el.removeEventListener('mouseover', this, true);
  5255.         this._el.removeEventListener('mouseout', this, true);
  5256.         this.parentEl.removeEventListener('mouseout', this);
  5257.         this._el.removeEventListener('click', this);
  5258.         this._el.remove();
  5259.         this._el = null;
  5260.     }
  5261. }
  5262.  
  5263. function addMenu(el) {
  5264.     const fn = a => arrTags(a, '<span class="de-menu-item">', '</span>');
  5265.     switch(el.id) {
  5266.     case 'de-btn-spell-add':
  5267.         return new Menu(el, `<div style="display: inline-block; border-right: 1px solid grey;">${
  5268.             fn('#words,#exp,#exph,#imgn,#ihash,#subj,#name,#trip,#img,#sage'.split(','))
  5269.         }</div><div style="display: inline-block;">${
  5270.             fn('#op,#tlen,#all,#video,#vauthor,#num,#wipe,#rep,#outrep,<br>'.split(',')) }</div>`,
  5271.         ({ textContent: s }) => insertText($id('de-spell-txt'), s +
  5272.             (!aib.t || s === '#op' || s === '#rep' || s === '#outrep' ? '' : `[${ aib.b },${ aib.t }]`) +
  5273.             (Spells.needArg[Spells.names.indexOf(s.substr(1))] ? '(' : '')));
  5274.     case 'de-panel-refresh':
  5275.         return new Menu(el, fn(Lng.selAjaxPages[lang]),
  5276.             el => Pages.loadPages(aProto.indexOf.call(el.parentNode.children, el) + 1));
  5277.     case 'de-panel-savethr':
  5278.         return new Menu(el, fn($q(aib.qPostImg, DelForm.first.el) ?
  5279.             Lng.selSaveThr[lang] : [Lng.selSaveThr[lang][0]]),
  5280.         el => {
  5281.             if($id('de-popup-savethr')) {
  5282.                 return;
  5283.             }
  5284.             const imgOnly = !!aProto.indexOf.call(el.parentNode.children, el);
  5285.             if(ContentLoader.isLoading) {
  5286.                 $popup('savethr', Lng.loading[lang], true);
  5287.                 ContentLoader.afterFn = () => ContentLoader.downloadThread(imgOnly);
  5288.                 ContentLoader.popupId = 'savethr';
  5289.             } else {
  5290.                 ContentLoader.downloadThread(imgOnly);
  5291.             }
  5292.         });
  5293.     case 'de-panel-audio-off':
  5294.         return new Menu(el, fn(Lng.selAudioNotif[lang]), el => {
  5295.             updater.enableUpdater();
  5296.             updater.toggleAudio([3e4, 6e4, 12e4, 3e5][aProto.indexOf.call(el.parentNode.children, el)]);
  5297.             $id('de-panel-audio-off').id = 'de-panel-audio-on';
  5298.         });
  5299.     }
  5300. }
  5301.  
  5302. /* ==[ Hotkeys.js ]===========================================================================================
  5303.                                                    HOTKEYS
  5304. =========================================================================================================== */
  5305.  
  5306. const HotKeys = {
  5307.     cPost          : null,
  5308.     enabled        : false,
  5309.     gKeys          : null,
  5310.     lastPageOffset : 0,
  5311.     ntKeys         : null,
  5312.     tKeys          : null,
  5313.     version        : 7,
  5314.     clearCPost() {
  5315.         this.cPost = null;
  5316.         this.lastPageOffset = 0;
  5317.     },
  5318.     disableHotKeys() {
  5319.         if(this.enabled) {
  5320.             this.enabled = false;
  5321.             if(this.cPost) {
  5322.                 this.cPost.unselect();
  5323.             }
  5324.             this.clearCPost();
  5325.             this.gKeys = this.ntKeys = this.tKeys = null;
  5326.             doc.removeEventListener('keydown', this, true);
  5327.         }
  5328.     },
  5329.     enableHotKeys() {
  5330.         if(!this.enabled) {
  5331.             this.enabled = true;
  5332.             this._paused = false;
  5333.             Promise.resolve(this.readKeys()).then(keys => {
  5334.                 if(this.enabled) {
  5335.                     [,, this.gKeys, this.ntKeys, this.tKeys] = keys;
  5336.                     doc.addEventListener('keydown', this, true);
  5337.                 }
  5338.             });
  5339.         }
  5340.     },
  5341.     getDefaultKeys: () => [HotKeys.version, nav.isFirefox, [
  5342.         // GLOBAL KEYS
  5343.         /* One post/thread above      */ 0x004B /* = K          */,
  5344.         /* One post/thread below      */ 0x004A /* = J          */,
  5345.         /* Reply or create thread     */ 0x0052 /* = R          */,
  5346.         /* Hide selected thread/post  */ 0x0048 /* = H          */,
  5347.         /* Open previous page/image   */ 0x1025 /* = Ctrl+Left  */,
  5348.         /* Send post (txt)            */ 0x900D /* = Ctrl+Enter */,
  5349.         /* Open/close "Favorites"     */ 0x4046 /* = Alt+F      */,
  5350.         /* Open/close "Hidden"        */ 0x4048 /* = Alt+H      */,
  5351.         /* Open/close panel           */ 0x0050 /* = P          */,
  5352.         /* Mask/unmask images         */ 0x0042 /* = B          */,
  5353.         /* Open/close "Settings"      */ 0x4053 /* = Alt+S      */,
  5354.         /* Expand current image       */ 0x0049 /* = I          */,
  5355.         /* Bold text                  */ 0xC042 /* = Alt+B      */,
  5356.         /* Italic text                */ 0xC049 /* = Alt+I      */,
  5357.         /* Strike text                */ 0xC054 /* = Alt+T      */,
  5358.         /* Spoiler text               */ 0xC050 /* = Alt+P      */,
  5359.         /* Code text                  */ 0xC043 /* = Alt+C      */,
  5360.         /* Open next page/image       */ 0x1027 /* = Ctrl+Right */,
  5361.         /* Open/close "Video"         */ 0x4056 /* = Alt+V      */
  5362.     ], [// NON-THREAD KEYS
  5363.         /* One post above */ 0x004D /* = M */,
  5364.         /* One post below */ 0x004E /* = N */,
  5365.         /* Open thread    */ 0x0056 /* = V */,
  5366.         /* Expand thread  */ 0x0045 /* = E */
  5367.     ], [// THREAD KEYS
  5368.         /* Update thread  */ 0x0055 /* = U */
  5369.     ]],
  5370.     handleEvent(e) {
  5371.         if(this._paused || e.metaKey) {
  5372.             return;
  5373.         }
  5374.         let idx;
  5375.         const isThr = aib.t;
  5376.         const el = e.target;
  5377.         const tag = el.tagName;
  5378.         const kc = e.keyCode |
  5379.             (e.ctrlKey ? 0x1000 : 0) |
  5380.             (e.shiftKey ? 0x2000 : 0) |
  5381.             (e.altKey ? 0x4000 : 0) |
  5382.             (tag === 'TEXTAREA' ||
  5383.                 tag === 'INPUT' && (el.type === 'text' || el.type === 'password') ? 0x8000 : 0);
  5384.         if(kc === 0x74 || kc === 0x8074) { // F5
  5385.             if(isThr || $id('de-popup-load-pages')) {
  5386.                 return;
  5387.             }
  5388.             AttachedImage.closeImg();
  5389.             Pages.loadPages(+Cfg.loadPages);
  5390.         } else if(kc === 0x1B) { // ESC
  5391.             if(AttachedImage.viewer) {
  5392.                 AttachedImage.closeImg();
  5393.                 return;
  5394.             }
  5395.             if(this.cPost) {
  5396.                 this.cPost.unselect();
  5397.                 this.cPost = null;
  5398.             }
  5399.             if(isThr) {
  5400.                 Post.clearMarks();
  5401.             }
  5402.             this.lastPageOffset = 0;
  5403.         } else if(kc === 0x801B) { // ESC (txt)
  5404.             el.blur();
  5405.         } else {
  5406.             let post;
  5407.             const globIdx = this.gKeys.indexOf(kc);
  5408.             switch(globIdx) {
  5409.             case 2: // Quick reply
  5410.                 if(pr.form) {
  5411.                     post = this.cPost || this._getFirstVisPost(false, true) || Thread.first.op;
  5412.                     this.cPost = post;
  5413.                     pr.showQuickReply(post, post.num, true, false);
  5414.                     post.select();
  5415.                 }
  5416.                 break;
  5417.             case 3: // Hide selected thread/post
  5418.                 post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
  5419.                 if(post) {
  5420.                     post.setUserVisib(!post.isHidden);
  5421.                     this._scroll(post, false, post.isOp);
  5422.                 }
  5423.                 break;
  5424.             case 4: // Open previous page/image
  5425.                 if(AttachedImage.viewer) {
  5426.                     AttachedImage.viewer.navigate(false);
  5427.                 } else if(isThr || aib.page !== aib.firstPage) {
  5428.                     deWindow.location.pathname = aib.getPageUrl(aib.b, isThr ? 0 : aib.page - 1);
  5429.                 }
  5430.                 break;
  5431.             case 5: // Send post (txt)
  5432.                 if(el !== pr.txta && el !== pr.cap.textEl) {
  5433.                     return;
  5434.                 }
  5435.                 pr.subm.click();
  5436.                 break;
  5437.             case 6: // Open/close "Favorites"
  5438.                 toggleWindow('fav', false);
  5439.                 break;
  5440.             case 7: // Open/close "Hidden"
  5441.                 toggleWindow('hid', false);
  5442.                 break;
  5443.             case 8: // Open/close panel
  5444.                 $toggle($id('de-panel-buttons'));
  5445.                 break;
  5446.             case 9: // Mask/unmask images
  5447.                 toggleCfg('maskImgs');
  5448.                 updateCSS();
  5449.                 break;
  5450.             case 10: // Open/close "Settings"
  5451.                 toggleWindow('cfg', false);
  5452.                 break;
  5453.             case 11: // Expand current image
  5454.                 post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
  5455.                 if(post) {
  5456.                     post.toggleImages();
  5457.                 }
  5458.                 break;
  5459.             case 12: // Bold text (txt)
  5460.                 if(el !== pr.txta) {
  5461.                     return;
  5462.                 }
  5463.                 $id('de-btn-bold').click();
  5464.                 break;
  5465.             case 13: // Italic text (txt)
  5466.                 if(el !== pr.txta) {
  5467.                     return;
  5468.                 }
  5469.                 $id('de-btn-italic').click();
  5470.                 break;
  5471.             case 14: // Strike text (txt)
  5472.                 if(el !== pr.txta) {
  5473.                     return;
  5474.                 }
  5475.                 $id('de-btn-strike').click();
  5476.                 break;
  5477.             case 15: // Spoiler text (txt)
  5478.                 if(el !== pr.txta) {
  5479.                     return;
  5480.                 }
  5481.                 $id('de-btn-spoil').click();
  5482.                 break;
  5483.             case 16: // Code text (txt)
  5484.                 if(el !== pr.txta) {
  5485.                     return;
  5486.                 }
  5487.                 $id('de-btn-code').click();
  5488.                 break;
  5489.             case 17: // Open next page/image
  5490.                 if(AttachedImage.viewer) {
  5491.                     AttachedImage.viewer.navigate(true);
  5492.                 } else if(!isThr) {
  5493.                     const pageNum = DelForm.last.pageNum + 1;
  5494.                     if(pageNum <= aib.lastPage) {
  5495.                         deWindow.location.pathname = aib.getPageUrl(aib.b, pageNum);
  5496.                     }
  5497.                 }
  5498.                 break;
  5499.             case 18: // Open/close "Videos"
  5500.                 toggleWindow('vid', false);
  5501.                 break;
  5502.             case -1:
  5503.                 if(isThr) {
  5504.                     idx = this.tKeys.indexOf(kc);
  5505.                     if(idx === 0) { // Update thread
  5506.                         updater.forceLoad(null);
  5507.                         break;
  5508.                     }
  5509.                     return;
  5510.                 }
  5511.                 idx = this.ntKeys.indexOf(kc);
  5512.                 if(idx === -1) {
  5513.                     return;
  5514.                 } else if(idx === 2) { // Open thread
  5515.                     post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
  5516.                     if(post) {
  5517.                         if(typeof GM_openInTab === 'function') {
  5518.                             GM_openInTab(aib.getThrUrl(aib.b, post.tNum), false, true);
  5519.                         } else {
  5520.                             deWindow.open(aib.getThrUrl(aib.b, post.tNum), '_blank');
  5521.                         }
  5522.                     }
  5523.                     break;
  5524.                 } else if(idx === 3) { // Expand/collapse thread
  5525.                     post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
  5526.                     if(post) {
  5527.                         if(post.thr.loadCount !== 0 && post.thr.op.next.count === 1) {
  5528.                             const nextThr = post.thr.nextNotHidden;
  5529.                             post.thr.loadPosts(visPosts, !!nextThr);
  5530.                             post = (nextThr || post.thr).op;
  5531.                         } else {
  5532.                             post.thr.loadPosts('all');
  5533.                             post = post.thr.op;
  5534.                         }
  5535.                         scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + post.top);
  5536.                         if(this.cPost && this.cPost !== post) {
  5537.                             this.cPost.unselect();
  5538.                             this.cPost = post;
  5539.                         }
  5540.                     }
  5541.                     break;
  5542.                 }
  5543.                 /* falls through */
  5544.             default: {
  5545.                 const scrollToThr = !isThr && (globIdx === 0 || globIdx === 1);
  5546.                 this._scroll(this._getFirstVisPost(scrollToThr, false),
  5547.                     globIdx === 0 || idx === 0, scrollToThr);
  5548.             }
  5549.             }
  5550.         }
  5551.         $pd(e);
  5552.         e.stopPropagation();
  5553.     },
  5554.     pauseHotKeys() {
  5555.         this._paused = true;
  5556.     },
  5557.     async readKeys() {
  5558.         const str = await getStored('DESU_keys');
  5559.         if(!str) {
  5560.             return this.getDefaultKeys();
  5561.         }
  5562.         let keys;
  5563.         try {
  5564.             keys = JSON.parse(str);
  5565.         } finally {
  5566.             if(!keys) {
  5567.                 return this.getDefaultKeys();
  5568.             }
  5569.             if(keys[0] !== this.version) {
  5570.                 const tKeys = this.getDefaultKeys();
  5571.                 switch(keys[0]) {
  5572.                 case 1:
  5573.                     keys[2][11] = tKeys[2][11];
  5574.                     keys[4] = tKeys[4];
  5575.                     /* falls through */
  5576.                 case 2:
  5577.                     keys[2][12] = tKeys[2][12];
  5578.                     keys[2][13] = tKeys[2][13];
  5579.                     keys[2][14] = tKeys[2][14];
  5580.                     keys[2][15] = tKeys[2][15];
  5581.                     keys[2][16] = tKeys[2][16];
  5582.                     /* falls through */
  5583.                 case 3:
  5584.                     keys[2][17] = keys[3][3];
  5585.                     keys[3][3] = keys[3].splice(4, 1)[0];
  5586.                     /* falls through */
  5587.                 case 4:
  5588.                 case 5:
  5589.                 case 6:
  5590.                     keys[2][18] = tKeys[2][18];
  5591.                 }
  5592.                 keys[0] = this.version;
  5593.                 setStored('DESU_keys', JSON.stringify(keys));
  5594.             }
  5595.             if(keys[1] ^ nav.isFirefox) {
  5596.                 const mapFunc = nav.isFirefox ?
  5597.                     key => key === 189 ? 173 : key === 187 ? 61 : key === 186 ? 59 : key :
  5598.                     key => key === 173 ? 189 : key === 61 ? 187 : key === 59 ? 186 : key;
  5599.                 keys[1] = nav.isFirefox;
  5600.                 keys[2] = keys[2].map(mapFunc);
  5601.                 keys[3] = keys[3].map(mapFunc);
  5602.                 setStored('DESU_keys', JSON.stringify(keys));
  5603.             }
  5604.             return keys;
  5605.         }
  5606.     },
  5607.     resume(keys) {
  5608.         [,, this.gKeys, this.ntKeys, this.tKeys] = keys;
  5609.         this._paused = false;
  5610.     },
  5611.  
  5612.     _paused: false,
  5613.     _getNextVisPost(cPost, isOp, toUp) {
  5614.         if(isOp) {
  5615.             const thr = cPost ? toUp ? cPost.thr.prevNotHidden : cPost.thr.nextNotHidden :
  5616.                 Thread.first.isHidden ? Thread.first.nextNotHidden : Thread.first;
  5617.             return thr ? thr.op : null;
  5618.         }
  5619.         return cPost ? cPost.getAdjacentVisPost(toUp) : Thread.first.isHidden ||
  5620.             Thread.first.op.isHidden ? Thread.first.op.getAdjacentVisPost(toUp) : Thread.first.op;
  5621.     },
  5622.     _getFirstVisPost(getThread, getFull) {
  5623.         if(this.lastPageOffset !== deWindow.pageYOffset) {
  5624.             let post = getThread ? Thread.first : Thread.first.op;
  5625.             while(post.top < 1) {
  5626.                 const tPost = post.next;
  5627.                 if(!tPost) {
  5628.                     break;
  5629.                 }
  5630.                 post = tPost;
  5631.             }
  5632.             if(this.cPost) {
  5633.                 this.cPost.unselect();
  5634.             }
  5635.             this.cPost = getThread ? getFull ? post.op : post.op.prev : getFull ? post : post.prev;
  5636.             this.lastPageOffset = deWindow.pageYOffset;
  5637.         }
  5638.         return this.cPost;
  5639.     },
  5640.     _scroll(post, toUp, toThread) {
  5641.         const next = this._getNextVisPost(post, toThread, toUp);
  5642.         if(!next) {
  5643.             if(!aib.t) {
  5644.                 const pageNum = toUp ? DelForm.first.pageNum - 1 : DelForm.last.pageNum + 1;
  5645.                 if(toUp ? pageNum >= aib.firstPage : pageNum <= aib.lastPage) {
  5646.                     deWindow.location.pathname = aib.getPageUrl(aib.b, pageNum);
  5647.                 }
  5648.             }
  5649.             return;
  5650.         }
  5651.         if(post) {
  5652.             post.unselect();
  5653.         }
  5654.         if(toThread) {
  5655.             next.el.scrollIntoView();
  5656.         } else {
  5657.             scrollTo(0, deWindow.pageYOffset + next.el.getBoundingClientRect().top -
  5658.                 Post.sizing.wHeight / 2 + next.el.clientHeight / 2);
  5659.         }
  5660.         this.lastPageOffset = deWindow.pageYOffset;
  5661.         next.select();
  5662.         this.cPost = next;
  5663.     }
  5664. };
  5665.  
  5666. class KeyEditListener {
  5667.     constructor(popupEl, keys, allKeys) {
  5668.         this.cEl = null;
  5669.         this.cKey = -1;
  5670.         this.errorInput = false;
  5671.         const aInputs = [...$Q('.de-input-key', popupEl)];
  5672.         for(let i = 0, len = allKeys.length; i < len; ++i) {
  5673.             const k = allKeys[i];
  5674.             if(k !== 0) {
  5675.                 for(let j = i + 1; j < len; ++j) {
  5676.                     if(k === allKeys[j]) {
  5677.                         aInputs[i].classList.add('de-input-error');
  5678.                         aInputs[j].classList.add('de-input-error');
  5679.                         break;
  5680.                     }
  5681.                 }
  5682.             }
  5683.         }
  5684.         this.popupEl = popupEl;
  5685.         this.keys = keys;
  5686.         this.initKeys = JSON.parse(JSON.stringify(keys));
  5687.         this.allKeys = allKeys;
  5688.         this.allInputs = aInputs;
  5689.         this.errCount = $Q('.de-input-error', popupEl).length;
  5690.         if(this.errCount !== 0) {
  5691.             this.saveButton.disabled = true;
  5692.         }
  5693.     }
  5694.     static getEditMarkup(keys) {
  5695.         const allKeys = [];
  5696.         return [allKeys, `${ Lng.hotKeyEdit[lang].join('')
  5697.             .replace(/%l/g, '<label class="de-block">')
  5698.             .replace(/%\/l/g, '</label>')
  5699.             .replace(/%i([2-4])([0-9]+)(t)?/g, (all, id1, id2, isText) => {
  5700.                 const key = keys[+id1][+id2];
  5701.                 allKeys.push(key);
  5702.                 return `<input class="de-input-key" type="text" de-id1="${ id1 }" de-id2="${ id2 }` +
  5703.                     `" size="16" value="${ KeyEditListener.getStrKey(key) }${
  5704.                         isText ? '" de-text' : '"' } readonly>`;
  5705.             }) }<input type="button" id="de-keys-save" class="de-button" value="${ Lng.save[lang] }">` +
  5706.             `<input type="button" id="de-keys-reset" class="de-button" value="${ Lng.reset[lang] }">`];
  5707.     }
  5708.     static getStrKey(key) {
  5709.         return (key & 0x1000 ? 'Ctrl+' : '') +
  5710.             (key & 0x2000 ? 'Shift+' : '') +
  5711.             (key & 0x4000 ? 'Alt+' : '') +
  5712.             KeyEditListener.keyCodes[key & 0xFFF];
  5713.     }
  5714.     static setTitle(el, idx) {
  5715.         let title = el.getAttribute('de-title');
  5716.         if(!title) {
  5717.             title = el.getAttribute('title');
  5718.             el.setAttribute('de-title', title);
  5719.         }
  5720.         if(HotKeys.enabled && idx !== -1) {
  5721.             title += ` [${ KeyEditListener.getStrKey(HotKeys.gKeys[idx]) }]`;
  5722.         }
  5723.         el.title = title;
  5724.     }
  5725.     get saveButton() {
  5726.         const value = $id('de-keys-save');
  5727.         Object.defineProperty(this, 'saveButton', { value, configurable: true });
  5728.         return value;
  5729.     }
  5730.     handleEvent(e) {
  5731.         let key, el = e.target;
  5732.         switch(e.type) {
  5733.         case 'blur':
  5734.             if(HotKeys.enabled && this.errCount === 0) {
  5735.                 HotKeys.resume(this.keys);
  5736.             }
  5737.             el.classList.remove('de-input-selected');
  5738.             this.cEl = null;
  5739.             return;
  5740.         case 'focus':
  5741.             if(HotKeys.enabled) {
  5742.                 HotKeys.pauseHotKeys();
  5743.             }
  5744.             el.classList.add('de-input-selected');
  5745.             this.cEl = el;
  5746.             return;
  5747.         case 'click': {
  5748.             let keys;
  5749.             if(el.id === 'de-keys-reset') {
  5750.                 this.keys = HotKeys.getDefaultKeys();
  5751.                 this.initKeys = HotKeys.getDefaultKeys();
  5752.                 if(HotKeys.enabled) {
  5753.                     HotKeys.resume(this.keys);
  5754.                 }
  5755.                 [this.allKeys, this.popupEl.innerHTML] = KeyEditListener.getEditMarkup(this.keys);
  5756.                 this.allInputs = [...$Q('.de-input-key', this.popupEl)];
  5757.                 this.errCount = 0;
  5758.                 delete this.saveButton;
  5759.                 break;
  5760.             } else if(el.id === 'de-keys-save') {
  5761.                 ({ keys } = this);
  5762.                 setStored('DESU_keys', JSON.stringify(keys));
  5763.             } else if(el.className === 'de-popup-btn') {
  5764.                 keys = this.initKeys;
  5765.             } else {
  5766.                 return;
  5767.             }
  5768.             if(HotKeys.enabled) {
  5769.                 HotKeys.resume(keys);
  5770.             }
  5771.             closePopup('edit-hotkeys');
  5772.             break;
  5773.         }
  5774.         case 'keydown': {
  5775.             if(!this.cEl) {
  5776.                 return;
  5777.             }
  5778.             key = e.keyCode;
  5779.             if(key === 0x1B || key === 0x2E) { // ESC, DEL
  5780.                 this.cEl.value = '';
  5781.                 this.cKey = 0;
  5782.                 this.errorInput = false;
  5783.                 break;
  5784.             }
  5785.             const keyStr = KeyEditListener.keyCodes[key];
  5786.             if(keyStr === undefined) {
  5787.                 this.cKey = -1;
  5788.                 return;
  5789.             }
  5790.             let str = '';
  5791.             if(e.ctrlKey) {
  5792.                 str += 'Ctrl+';
  5793.             }
  5794.             if(e.shiftKey) {
  5795.                 str += 'Shift+';
  5796.             }
  5797.             if(e.altKey) {
  5798.                 str += 'Alt+';
  5799.             }
  5800.             if(key === 16 || key === 17 || key === 18) {
  5801.                 this.errorInput = true;
  5802.                 this.cKey = 0;
  5803.             } else {
  5804.                 this.cKey = key | (e.ctrlKey ? 0x1000 : 0) | (e.shiftKey ? 0x2000 : 0) |
  5805.                     (e.altKey ? 0x4000 : 0) | (this.cEl.hasAttribute('de-text') ? 0x8000 : 0);
  5806.                 this.errorInput = false;
  5807.                 str += keyStr;
  5808.             }
  5809.             this.cEl.value = str;
  5810.             break;
  5811.         }
  5812.         case 'keyup': {
  5813.             el = this.cEl;
  5814.             key = this.cKey;
  5815.             if(!el || key === -1) {
  5816.                 return;
  5817.             }
  5818.             let rEl;
  5819.             const isError = el.classList.contains('de-input-error');
  5820.             if(!this.errorInput && key !== -1) {
  5821.                 let idx = this.allInputs.indexOf(el);
  5822.                 const oKey = this.allKeys[idx];
  5823.                 if(oKey === key) {
  5824.                     this.errorInput = false;
  5825.                     break;
  5826.                 }
  5827.                 const rIdx = key === 0 ? -1 : this.allKeys.indexOf(key);
  5828.                 this.allKeys[idx] = key;
  5829.                 if(isError) {
  5830.                     idx = this.allKeys.indexOf(oKey);
  5831.                     if(idx !== -1 && this.allKeys.indexOf(oKey, idx + 1) === -1) {
  5832.                         rEl = this.allInputs[idx];
  5833.                         if(rEl.classList.contains('de-input-error')) {
  5834.                             this.errCount--;
  5835.                             rEl.classList.remove('de-input-error');
  5836.                         }
  5837.                     }
  5838.                     if(rIdx === -1) {
  5839.                         this.errCount--;
  5840.                         el.classList.remove('de-input-error');
  5841.                     }
  5842.                 }
  5843.                 if(rIdx === -1) {
  5844.                     this.keys[+el.getAttribute('de-id1')][+el.getAttribute('de-id2')] = key;
  5845.                     if(this.errCount === 0) {
  5846.                         this.saveButton.disabled = false;
  5847.                     }
  5848.                     this.errorInput = false;
  5849.                     break;
  5850.                 }
  5851.                 rEl = this.allInputs[rIdx];
  5852.                 if(!rEl.classList.contains('de-input-error')) {
  5853.                     this.errCount++;
  5854.                     rEl.classList.add('de-input-error');
  5855.                 }
  5856.             }
  5857.             if(!isError) {
  5858.                 this.errCount++;
  5859.                 el.classList.add('de-input-error');
  5860.             }
  5861.             if(this.errCount !== 0) {
  5862.                 this.saveButton.disabled = true;
  5863.             }
  5864.         }
  5865.         }
  5866.         $pd(e);
  5867.     }
  5868. }
  5869. // Browsers have different codes for these keys (see HotKeys.readKeys):
  5870. //     Firefox - '-' - 173, '=' - 61, ';' - 59
  5871. //     Chrome/Opera: '-' - 189, '=' - 187, ';' - 186
  5872. /* eslint-disable comma-spacing, comma-style, no-sparse-arrays */
  5873. KeyEditListener.keyCodes = [
  5874.     '',,,,,,,,'Backspace','Tab',,,,'Enter',,,'Shift','Ctrl','Alt',/* Pause/Break */,/* Caps Lock */,,,,,,,
  5875.     /* Esc */,,,,,'Space',/* PgUp */,/* PgDn */,/* End */,/* Home */,'←','↑','→','↓',,,,,/* Insert */,
  5876.     /* Del */,,'0','1','2','3','4','5','6','7','8','9',,';',,'=',,,,'A','B','C','D','E','F','G','H','I','J',
  5877.     'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',/* Left WIN */,/* Right WIN */,
  5878.     /* Select */,,,'Num 0','Num 1','Num 2','Num 3','Num 4','Num 5','Num 6','Num 7','Num 8','Num 9','Num *',
  5879.     'Num +',,'Num -','Num .','Num /',/* F1 */,/* F2 */,/* F3 */,/* F4 */,/* F5 */,/* F6 */,/* F7 */,/* F8 */,
  5880.     /* F9 */,/* F10 */,/* F11 */,/* F12 */,,,,,,,,,,,,,,,,,,,,,/* Num Lock */,/* Scroll Lock */,,,,,,,,,,,,,,,
  5881.     ,,,,,,,,,,,,,'-',,,,,,,,,,,,,';','=',',','-','.','/','`',,,,,,,,,,,,,,,,,,,,,,,,,,,'[','\\',']',"'"
  5882. ];
  5883. /* eslint-enable comma-spacing, comma-style, no-sparse-arrays */
  5884.  
  5885. /* ==[ ContentLoad.js ]=======================================================================================
  5886.                                             CONTENT DOWNLOADING
  5887.                     images/video preloading, rarjpeg detecting, thread/images downloading
  5888. =========================================================================================================== */
  5889.  
  5890. const ContentLoader = {
  5891.     afterFn   : null,
  5892.     isLoading : false,
  5893.     popupId   : null,
  5894.     downloadThread(imgOnly) {
  5895.         let progress, counter, current = 1,
  5896.             warnings = '',
  5897.             tar = new TarBuilder();
  5898.         const dc = imgOnly ? doc : doc.documentElement.cloneNode(true);
  5899.         let els = [...$Q(aib.qPostImg, $q('[de-form]', dc))];
  5900.         let count = els.length;
  5901.         this._thrPool = new TasksPool(4, (num, data) => this.loadImgData(data[0]).then(imgData => {
  5902.             const [url, fName, el, imgLink] = data;
  5903.             let safeName = fName.replace(/[\\/:*?"<>|]/g, '_');
  5904.             progress.value = counter.innerHTML = current++;
  5905.             if(imgLink) {
  5906.                 let thumbName = safeName.replace(/\.[a-z]+$/, '.png');
  5907.                 if(imgOnly) {
  5908.                     thumbName = 'thumb-' + thumbName;
  5909.                 } else {
  5910.                     thumbName = 'thumbs/' + thumbName;
  5911.                     safeName = imgData ? 'images/' + safeName : thumbName;
  5912.                     imgLink.href = $q('a[de-href], ' + aib.qImgNameLink, aib.getImgWrap(el)).href = safeName;
  5913.                 }
  5914.                 if(imgData) {
  5915.                     tar.addFile(safeName, imgData);
  5916.                 } else {
  5917.                     warnings += `<br>${ Lng.cantLoad[lang] } <a href="${ url }">${ url }</a>` +
  5918.                         `<br>${ Lng.willSavePview[lang] }`;
  5919.                     $popup('err-files', Lng.loadErrors[lang] + warnings);
  5920.                     if(imgOnly) {
  5921.                         return this.getDataFromImg(el).then(data => tar.addFile(thumbName, data), emptyFn);
  5922.                     }
  5923.                 }
  5924.                 return imgOnly ? null : this.getDataFromImg(el).then(data => {
  5925.                     el.src = thumbName;
  5926.                     tar.addFile(thumbName, data);
  5927.                 }, () => (el.src = safeName));
  5928.             } else if(imgData && imgData.length > 0) {
  5929.                 tar.addFile(el.href = el.src = 'data/' + safeName, imgData);
  5930.             } else {
  5931.                 $del(el);
  5932.             }
  5933.         }), () => {
  5934.             const docName = `${ aib.dm }-${ aib.b.replace(/[\\/:*?"<>|]/g, '') }-${ aib.t }`;
  5935.             if(!imgOnly) {
  5936.                 $q('head', dc).insertAdjacentHTML('beforeend',
  5937.                     '<script type="text/javascript" src="data/dollscript.js" charset="utf-8"></script>');
  5938.                 const dcBody = $q('body', dc);
  5939.                 dcBody.classList.remove('de-runned');
  5940.                 dcBody.classList.add('de-mode-local');
  5941.                 $delAll('#de-css, #de-css-dynamic, #de-css-user', dc);
  5942.                 tar.addString('data/dollscript.js', `${ nav.isESNext ?
  5943.                     `(${ String(deMainFuncInner) })(window, null, null, (x, y) => window.scrollTo(x, y), ` :
  5944.                     `(${ String(/* global deMainFuncOuter */ deMainFuncOuter) })(`
  5945.                 }${ JSON.stringify({ dm: aib.dm, b: aib.b, t: aib.t }) });`);
  5946.                 const dt = doc.doctype;
  5947.                 tar.addString(docName + '.html', '<!DOCTYPE ' + dt.name +
  5948.                     (dt.publicId ? ` PUBLIC "${ dt.publicId }"` : dt.systemId ? ' SYSTEM' : '') +
  5949.                     (dt.systemId ? ` "${ dt.systemId }"` : '') + '>' + dc.outerHTML);
  5950.             }
  5951.             downloadBlob(tar.get(), docName + (imgOnly ? '-images.tar' : '.tar'));
  5952.             closePopup('load-files');
  5953.             this._thrPool = tar = warnings = count = current = imgOnly = progress = counter = null;
  5954.         });
  5955.         els.forEach(el => {
  5956.             const imgLink = $parent(el, 'A');
  5957.             if(imgLink) {
  5958.                 const url = imgLink.href;
  5959.                 this._thrPool.runTask([url, imgLink.getAttribute('download') ||
  5960.                     url.substring(url.lastIndexOf('/') + 1), el, imgLink]);
  5961.             }
  5962.         });
  5963.         if(!imgOnly) {
  5964.             $delAll('#de-main, .de-parea, .de-post-btns, .de-btn-src, .de-refmap, .de-thr-buttons, ' +
  5965.                 '.de-video-obj, #de-win-reply, link[rel="alternate stylesheet"], script, ' + aib.qForm, dc);
  5966.             $each($Q('a', dc), el => {
  5967.                 let num;
  5968.                 const tc = el.textContent;
  5969.                 if(tc[0] === '>' && tc[1] === '>' && (num = +tc.substr(2)) && pByNum.has(num)) {
  5970.                     el.href = aib.anchor + num;
  5971.                     if(!el.classList.contains('de-link-postref')) {
  5972.                         el.className = 'de-link-postref ' + el.className;
  5973.                     }
  5974.                 } else {
  5975.                     el.href = getAbsLink(el.href);
  5976.                 }
  5977.             });
  5978.             $each($Q(aib.qRPost, dc), (el, i) => el.setAttribute('de-num', i ? aib.getPNum(el) : aib.t));
  5979.             const files = [];
  5980.             const urlRegex = new RegExp(`^\\/\\/?|^https?:\\/\\/([^\\/]*\\.)?${
  5981.                 quoteReg(aib._4chan ? '4cdn.org' : aib.dm) }\\/`, 'i');
  5982.             $each($Q('link, *[src]', dc), el => {
  5983.                 if(els.indexOf(el) !== -1) {
  5984.                     return;
  5985.                 }
  5986.                 let url = el.tagName === 'LINK' ? el.href : el.src;
  5987.                 if(!urlRegex.test(url)) {
  5988.                     el.remove();
  5989.                     return;
  5990.                 }
  5991.                 let fName = url.substring(url.lastIndexOf('/') + 1)
  5992.                     .replace(/[\\/:*?"<>|]/g, '_').toLowerCase();
  5993.                 if(files.indexOf(fName) !== -1) {
  5994.                     let temp = url.lastIndexOf('.');
  5995.                     const ext = url.substring(temp);
  5996.                     url = url.substring(0, temp);
  5997.                     fName = fName.substring(0, fName.lastIndexOf('.'));
  5998.                     for(let i = 0; ; ++i) {
  5999.                         temp = `${ fName }(${ i })${ ext }`;
  6000.                         if(files.indexOf(temp) === -1) {
  6001.                             break;
  6002.                         }
  6003.                     }
  6004.                     fName = temp;
  6005.                 }
  6006.                 files.push(fName);
  6007.                 this._thrPool.runTask([url, fName, el, null]);
  6008.                 count++;
  6009.             });
  6010.         }
  6011.         $popup('load-files', `${ imgOnly ? Lng.loadImage[lang] : Lng.loadFile[lang] }:<br><progress ` +
  6012.             `id="de-loadprogress" value="0" max="${ count }"></progress> <span>1</span>/${ count }`, true);
  6013.         progress = $id('de-loadprogress');
  6014.         counter = progress.nextElementSibling;
  6015.         this._thrPool.completeTasks();
  6016.         els = null;
  6017.     },
  6018.     getDataFromImg(el) {
  6019.         try {
  6020.             const cnv = this._canvas || (this._canvas = doc.createElement('canvas'));
  6021.             cnv.width = el.width || el.videoWidth;
  6022.             cnv.height = el.height || el.videoHeight;
  6023.             cnv.getContext('2d').drawImage(el, 0, 0);
  6024.             return Promise.resolve(new Uint8Array(atob(cnv.toDataURL('image/png').split(',')[1])
  6025.                 .split('').map(a => a.charCodeAt())));
  6026.         } catch(err) {
  6027.             return this.loadImgData(el.src);
  6028.         }
  6029.     },
  6030.     loadImgData: (url, repeatOnError = true) => $ajax(
  6031.         url, { responseType: 'arraybuffer' }, !url.startsWith('blob')
  6032.     ).then(xhr => {
  6033.         if('response' in xhr) {
  6034.             try {
  6035.                 return nav.getUnsafeUint8Array(xhr.response);
  6036.             } catch(err) {}
  6037.         }
  6038.         const txt = xhr.responseText;
  6039.         return new Uint8Array(txt.length).map((val, i) => txt.charCodeAt(i) & 0xFF);
  6040.     }, err => err.code !== 404 && repeatOnError ? ContentLoader.loadImgData(url, false) : null),
  6041.     preloadImages(data) {
  6042.         if(!Cfg.preLoadImgs && !Cfg.openImgs && !isPreImg) {
  6043.             return;
  6044.         }
  6045.         let preloadPool;
  6046.         const isPost = data instanceof AbstractPost;
  6047.         const els = $Q(aib.qPostImg, isPost ? data.el : data);
  6048.         const len = els.length;
  6049.         if(isPreImg || Cfg.preLoadImgs) {
  6050.             let cImg = 1;
  6051.             const mReqs = isPost ? 1 : 4;
  6052.             const rarJpgFinder = (isPreImg || Cfg.findImgFile) && new WorkerPool(mReqs, this._detectImgFile,
  6053.                 err => console.error('File detector error:', `line: ${ err.lineno } - ${ err.message }`));
  6054.             preloadPool = new TasksPool(mReqs, (num, data) => this.loadImgData(data[0]).then(imageData => {
  6055.                 const [url, imgLink, iType, isRepToOrig, el, isVideo] = data;
  6056.                 if(imageData) {
  6057.                     const fName = url.substring(url.lastIndexOf('/') + 1);
  6058.                     const nameLink = $q(aib.qImgNameLink, aib.getImgWrap(el));
  6059.                     imgLink.setAttribute('download', fName);
  6060.                     if(!Cfg.imgNames) {
  6061.                         nameLink.setAttribute('download', fName);
  6062.                         nameLink.setAttribute('de-href', nameLink.href);
  6063.                     }
  6064.                     imgLink.href = nameLink.href =
  6065.                         deWindow.URL.createObjectURL(new Blob([imageData], { type: iType }));
  6066.                     if(isVideo) {
  6067.                         el.setAttribute('de-video', '');
  6068.                     }
  6069.                     if(isRepToOrig) {
  6070.                         el.src = imgLink.href;
  6071.                     }
  6072.                     if(rarJpgFinder) {
  6073.                         rarJpgFinder.runWorker(imageData.buffer, [imageData.buffer],
  6074.                             info => this._addImgFileIcon(nameLink, fName, info));
  6075.                     }
  6076.                 }
  6077.                 if(this.popupId) {
  6078.                     $popup(this.popupId, `${ Lng.loadImage[lang] }: ${ cImg }/${ len }`, true);
  6079.                 }
  6080.                 cImg++;
  6081.             }), () => {
  6082.                 this.isLoading = false;
  6083.                 if(this.afterFn) {
  6084.                     this.afterFn();
  6085.                     this.afterFn = this.popupId = null;
  6086.                 }
  6087.                 if(rarJpgFinder) {
  6088.                     rarJpgFinder.clearWorkers();
  6089.                 }
  6090.             });
  6091.             this.isLoading = true;
  6092.         }
  6093.         for(let i = 0; i < len; ++i) {
  6094.             const el = els[i];
  6095.             const imgLink = $parent(el, 'A');
  6096.             if(!imgLink) {
  6097.                 continue;
  6098.             }
  6099.             let isRepToOrig = !!Cfg.openImgs;
  6100.             const url = imgLink.href;
  6101.             const type = getFileType(url);
  6102.             const isVideo = type && (type === 'video/webm' || type === 'video/mp4' || type === 'video/ogv');
  6103.             if(!type || isVideo && Cfg.preLoadImgs === 2) {
  6104.                 continue;
  6105.             } else if(type === 'image/gif') {
  6106.                 isRepToOrig &= Cfg.openImgs !== 3;
  6107.             } else {
  6108.                 if(isVideo) {
  6109.                     isRepToOrig = false;
  6110.                 }
  6111.                 isRepToOrig &= Cfg.openImgs !== 2;
  6112.             }
  6113.             if(preloadPool) {
  6114.                 preloadPool.runTask([url, imgLink, type, isRepToOrig, el, isVideo]);
  6115.             } else if(isRepToOrig) {
  6116.                 el.src = url;
  6117.             }
  6118.         }
  6119.         if(preloadPool) {
  6120.             preloadPool.completeTasks();
  6121.         }
  6122.     },
  6123.  
  6124.     _canvas  : null,
  6125.     _thrPool : null,
  6126.     _addImgFileIcon(nameLink, fName, info) {
  6127.         const { type } = info;
  6128.         if(typeof type === 'undefined') {
  6129.             return;
  6130.         }
  6131.         const ext = ['7z', 'zip', 'rar', 'ogg', 'mp3'][type];
  6132.         nameLink.insertAdjacentHTML('afterend', `<a href="${ deWindow.URL.createObjectURL(
  6133.             new Blob([nav.getUnsafeUint8Array(info.data, info.idx)], {
  6134.                 type: [
  6135.                     'application/x-7z-compressed',
  6136.                     'application/zip',
  6137.                     'application/x-rar-compressed',
  6138.                     'audio/ogg',
  6139.                     'audio/mpeg'][type]
  6140.             })
  6141.         ) }" class="de-img-${ type > 2 ? 'audio' : 'arch' }" title="${ Lng.downloadFile[lang] }" download="${
  6142.             fName.substring(0, fName.lastIndexOf('.')) }.${ ext }">.${ ext }</a>`);
  6143.     },
  6144.     // Finds built-in files in jpg and png
  6145.     _detectImgFile: arrBuf => {
  6146.         let i, j;
  6147.         const dat = new Uint8Array(arrBuf);
  6148.         let len = dat.length;
  6149.         /* JPG [ff d8 ff e0] = [яШяа] */
  6150.         if(dat[0] === 0xFF && dat[1] === 0xD8) {
  6151.             for(i = 0, j = 0; i < len - 1; ++i) {
  6152.                 if(dat[i] === 0xFF) {
  6153.                     /* Built-in JPG */
  6154.                     if(dat[i + 1] === 0xD8) {
  6155.                         j++;
  6156.                     /* JPG end [ff d9] */
  6157.                     } else if(dat[i + 1] === 0xD9 && --j === 0) {
  6158.                         i += 2;
  6159.                         break;
  6160.                     }
  6161.                 }
  6162.             }
  6163.         /* PNG [89 50 4e 47] = [‰PNG] */
  6164.         } else if(dat[0] === 0x89 && dat[1] === 0x50) {
  6165.             for(i = 0; i < len - 7; ++i) {
  6166.                 /* PNG end [49 45 4e 44 ae 42 60 82] */
  6167.                 if(dat[i] === 0x49 && dat[i + 1] === 0x45 && dat[i + 2] === 0x4E && dat[i + 3] === 0x44) {
  6168.                     i += 8;
  6169.                     break;
  6170.                 }
  6171.             }
  6172.         } else {
  6173.             return {};
  6174.         }
  6175.         if(i === len || len - i <= 60) { // Ignore small files (<60 bytes)
  6176.             return {};
  6177.         }
  6178.         for(len = i + 90; i < len; ++i) {
  6179.             /* 7Z [37 7a bc af] = [7zјЇ] */
  6180.             if(dat[i] === 0x37 && dat[i + 1] === 0x7A && dat[i + 2] === 0xBC) {
  6181.                 return { type: 0, idx: i, data: arrBuf };
  6182.             /* ZIP [50 4b 03 04] = [PK..] */
  6183.             } else if(dat[i] === 0x50 && dat[i + 1] === 0x4B && dat[i + 2] === 0x03) {
  6184.                 return { type: 1, idx: i, data: arrBuf };
  6185.             /* RAR [52 61 72 21] = [Rar!] */
  6186.             } else if(dat[i] === 0x52 && dat[i + 1] === 0x61 && dat[i + 2] === 0x72) {
  6187.                 return { type: 2, idx: i, data: arrBuf };
  6188.             /* OGG [4f 67 67 53] = [OggS] */
  6189.             } else if(dat[i] === 0x4F && dat[i + 1] === 0x67 && dat[i + 2] === 0x67) {
  6190.                 return { type: 3, idx: i, data: arrBuf };
  6191.             /* MP3 [0x49 0x44 0x33] = [ID3] */
  6192.             } else if(dat[i] === 0x49 && dat[i + 1] === 0x44 && dat[i + 2] === 0x33) {
  6193.                 return { type: 4, idx: i, data: arrBuf };
  6194.             }
  6195.         }
  6196.         return {};
  6197.     }
  6198. };
  6199.  
  6200. /* ==[ TimeCorrection.js ]====================================================================================
  6201.                                               TIME CORRECTION
  6202. =========================================================================================================== */
  6203.  
  6204. class DateTime {
  6205.     constructor(pattern, rPattern, diff, dtLang, onRPat) {
  6206.         this.pad2 = pad2;
  6207.         this.genDateTime = null;
  6208.         this.onRPat = null;
  6209.         if(DateTime.checkPattern(pattern)) {
  6210.             this.disabled = true;
  6211.             return;
  6212.         }
  6213.         this.regex = pattern
  6214.             .replace(/(?:[sihdny]\?){2,}/g, str => `(?:${ str.replace(/\?/g, '') })?`)
  6215.             .replace(/-/g, '[^<]')
  6216.             .replace(/\+/g, '[^0-9<]')
  6217.             .replace(/([sihdny]+)/g, '($1)')
  6218.             .replace(/[sihdny]/g, '\\d')
  6219.             .replace(/m|w/g, '([a-zA--яА-Я]+)');
  6220.         this.pattern = pattern.replace(/[?\-+]+/g, '').replace(/([a-z])\1+/g, '$1');
  6221.         this.diff = parseInt(diff, 10);
  6222.         this.arrW = Lng.week[dtLang];
  6223.         this.arrM = Lng.month[dtLang];
  6224.         this.arrFM = Lng.fullMonth[dtLang];
  6225.         if(rPattern) {
  6226.             this.genDateTime = this.genRFunc(rPattern);
  6227.         } else {
  6228.             this.onRPat = onRPat;
  6229.         }
  6230.     }
  6231.     static checkPattern(val) {
  6232.         return !val.includes('i') || !val.includes('h') || !val.includes('d') ||
  6233.             !val.includes('y') || !(val.includes('n') || val.includes('m')) ||
  6234.             /[^?\-+sihdmwny]|mm|ww|\?\?|([ihdny]\?)\1+/.test(val);
  6235.     }
  6236.     static toggleSettings(el) {
  6237.         if(el.checked && (!/^[+-]\d{1,2}$/.test(Cfg.timeOffset) || DateTime.checkPattern(Cfg.timePattern))) {
  6238.             $popup('err-correcttime', Lng.cTimeError[lang]);
  6239.             saveCfg('correctTime', 0);
  6240.             el.checked = false;
  6241.         }
  6242.     }
  6243.     genRFunc(rPattern) {
  6244.         return dtime => rPattern.replace('_o', (this.diff < 0 ? '' : '+') + this.diff)
  6245.             .replace('_s', () => this.pad2(dtime.getSeconds()))
  6246.             .replace('_i', () => this.pad2(dtime.getMinutes()))
  6247.             .replace('_h', () => this.pad2(dtime.getHours()))
  6248.             .replace('_d', () => this.pad2(dtime.getDate()))
  6249.             .replace('_w', () => this.arrW[dtime.getDay()])
  6250.             .replace('_n', () => this.pad2(dtime.getMonth() + 1))
  6251.             .replace('_m', () => this.arrM[dtime.getMonth()])
  6252.             .replace('_M', () => this.arrFM[dtime.getMonth()])
  6253.             .replace('_y', () => ('' + dtime.getFullYear()).substring(2))
  6254.             .replace('_Y', () => dtime.getFullYear());
  6255.     }
  6256.     getRPattern(txt) {
  6257.         const m = txt.match(new RegExp(this.regex));
  6258.         if(!m) {
  6259.             this.disabled = true;
  6260.             return false;
  6261.         }
  6262.         let rPattern = '';
  6263.         for(let i = 1, len = m.length, j = 0, str = m[0]; i < len;) {
  6264.             const a = m[i++];
  6265.             if(!a) {
  6266.                 continue;
  6267.             }
  6268.             let p = this.pattern[i - 2];
  6269.             if((p === 'm' || p === 'y') && a.length > 3) {
  6270.                 p = p.toUpperCase();
  6271.             }
  6272.             const k = str.indexOf(a, j);
  6273.             rPattern += str.substring(j, k) + '_' + p;
  6274.             j = k + a.length;
  6275.         }
  6276.         if(this.onRPat) {
  6277.             this.onRPat(rPattern);
  6278.         }
  6279.         this.genDateTime = this.genRFunc(rPattern);
  6280.         return true;
  6281.     }
  6282.     fix(txt) {
  6283.         if(this.disabled || (!this.genDateTime && !this.getRPattern(txt))) {
  6284.             return txt;
  6285.         }
  6286.         return txt.replace(new RegExp(this.regex, 'g'), (str, ...args) => {
  6287.             let second, minute, hour, day, month, year;
  6288.             for(let i = 0; i < 7; ++i) {
  6289.                 const a = args[i];
  6290.                 switch(this.pattern[i]) {
  6291.                 case 's': second = a; break;
  6292.                 case 'i': minute = a; break;
  6293.                 case 'h': hour = a; break;
  6294.                 case 'd': day = a; break;
  6295.                 case 'n': month = a - 1; break;
  6296.                 case 'y': year = a; break;
  6297.                 case 'm': month = Lng.monthDict[a.slice(0, 3).toLowerCase()] || 0; break;
  6298.                 }
  6299.             }
  6300.             const dtime = new Date(year.length === 2 ? '20' + year :
  6301.                 year, month, day, hour, minute, second || 0);
  6302.             dtime.setHours(dtime.getHours() + this.diff);
  6303.             return this.genDateTime(dtime);
  6304.         });
  6305.     }
  6306. }
  6307.  
  6308. /* ==[ Players.js ]===========================================================================================
  6309.                                          PLAYERS / LINKS EMBEDDERS
  6310.                                youtube, vimeo, mp3, vocaroo embedding players
  6311. =========================================================================================================== */
  6312.  
  6313. class Videos {
  6314.     constructor(post, player = null, playerInfo = null) {
  6315.         this.currentLink = null;
  6316.         this.hasLinks = false;
  6317.         this.linksCount = 0;
  6318.         this.loadedLinksCount = 0;
  6319.         this.playerInfo = null;
  6320.         this.post = post;
  6321.         this.titleLoadFn = null;
  6322.         this.vData = [[], []];
  6323.         if(player && playerInfo) {
  6324.             Object.defineProperty(this, 'player', { value: player });
  6325.             this.playerInfo = playerInfo;
  6326.         }
  6327.     }
  6328.     static addPlayer(obj, m, isYtube, enableJsapi = false) {
  6329.         const el = obj.player;
  6330.         obj.playerInfo = m;
  6331.         let txt;
  6332.         if(isYtube) {
  6333.             const list = m[0].match(/list=[^&#]+/);
  6334.             txt = `<iframe class="de-video-player" src="https://www.youtube.com/embed/${ m[1] }?start=` +
  6335.                 ((m[2] ? m[2] * 3600 : 0) + (m[3] ? m[3] * 60 : 0) + (m[4] ? +m[4] : 0)) +
  6336.                 (enableJsapi ? '&enablejsapi=1' : Cfg.embedYTube === 1 ? '&autoplay=1' : '') +
  6337.                 (list ? '&' + list[0] : '') + '" frameborder="0" allowfullscreen></iframe>';
  6338.         } else {
  6339.             const id = m[1] + (m[2] ? m[2] : '');
  6340.             txt = `<iframe class="de-video-player" src="${ aib.prot }//player.vimeo.com/video/${ id }${
  6341.                 Cfg.embedYTube === 1 ? '?autoplay=1' : ''
  6342.             }" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>`;
  6343.         }
  6344.         el.innerHTML = txt + (enableJsapi ? '' :
  6345.             `<span class="de-video-resizer" title="${ Lng.expandVideo[lang] }"></span>`);
  6346.         $show(el);
  6347.         if(!enableJsapi) {
  6348.             el.lastChild.onclick = e => e.target.parentNode.classList.toggle('de-video-expanded');
  6349.         }
  6350.     }
  6351.     static setLinkData(link, data, isCloned = false) {
  6352.         const [title, author, views, publ, duration] = data;
  6353.         if(Panel.isVidEnabled && !isCloned) {
  6354.             const clonedLink = $q(`.de-entry > .de-video-link[href="${ link.href }"]:not(title)`);
  6355.             if(clonedLink) {
  6356.                 Videos.setLinkData(clonedLink, data, true);
  6357.             }
  6358.         }
  6359.         link.textContent = title;
  6360.         link.classList.add('de-video-title');
  6361.         link.setAttribute('de-author', author);
  6362.         link.title = (duration ? Lng.duration[lang] + duration : '') +
  6363.             (publ ? `, ${ Lng.published[lang] + publ }\n` : '') +
  6364.             Lng.author[lang] + author + (views ? ', ' + Lng.views[lang] + views : '');
  6365.     }
  6366.     get player() {
  6367.         const { post } = this;
  6368.         const value = aib.insertYtPlayer(post.msg, `<div class="de-video-obj${
  6369.             post.images.hasAttachments && !post.isOp ? ' de-video-obj-inline' : '' }"></div>`);
  6370.         Object.defineProperty(this, 'player', { value });
  6371.         return value;
  6372.     }
  6373.     addLink(m, loader, link, isYtube) {
  6374.         this.hasLinks = true;
  6375.         this.linksCount++;
  6376.         if(this.playerInfo === null) {
  6377.             if(Cfg.embedYTube === 1) {
  6378.                 this._addThumb(m, isYtube);
  6379.             }
  6380.         } else if(!link && $q(`.de-video-link[href*="${ m[1] }"]`, this.post.msg)) {
  6381.             return;
  6382.         }
  6383.         let dataObj;
  6384.         if(loader && (dataObj = Videos._global.vData[+!isYtube][m[1]])) {
  6385.             this.vData[+!isYtube].push(dataObj);
  6386.         }
  6387.         let time = '';
  6388.         [time, m[2], m[3], m[4]] = Videos._fixTime(m[4], m[3], m[2]);
  6389.         if(link) {
  6390.             link.href = link.href.replace(/^http:/, 'https:');
  6391.             if(time) {
  6392.                 link.setAttribute('de-time', time);
  6393.             }
  6394.             link.className = `de-video-link ${ isYtube ? 'de-ytube' : 'de-vimeo' }`;
  6395.         } else {
  6396.             const src = isYtube ?
  6397.                 `${ aib.prot }//www.youtube.com/watch?v=${ m[1] }${ time ? '#t=' + time : '' }` :
  6398.                 `${ aib.prot }//vimeo.com/${ m[1] }`;
  6399.             link = $bEnd(this.post.msg, `<p class="de-video-ext"><a class="de-video-link ${
  6400.                 isYtube ? 'de-ytube' : 'de-vimeo' }${ time ? '" de-time="' + time : ''
  6401.             }" href="${ src }">${ dataObj ? '' : src }</a></p>`).firstChild;
  6402.         }
  6403.         if(dataObj) {
  6404.             Videos.setLinkData(link, dataObj);
  6405.         }
  6406.         if(this.playerInfo === null || this.playerInfo === m) {
  6407.             this.currentLink = link;
  6408.         }
  6409.         link.videoInfo = m;
  6410.         let vidListEl;
  6411.         if(Panel.isVidEnabled && (vidListEl = $id('de-video-list'))) {
  6412.             updateVideoList(vidListEl, link, this.post.num);
  6413.         }
  6414.         if(loader && !dataObj) {
  6415.             loader.runTask([link, isYtube, this, m[1]]);
  6416.         }
  6417.     }
  6418.     clickLink(el, mode) {
  6419.         const m = el.videoInfo;
  6420.         if(this.playerInfo !== m) {
  6421.             this.currentLink.classList.remove('de-current');
  6422.             this.currentLink = el;
  6423.             if(mode === 1) {
  6424.                 this._addThumb(m, el.classList.contains('de-ytube'));
  6425.             } else {
  6426.                 el.classList.add('de-current');
  6427.                 this.setPlayer(m, el.classList.contains('de-ytube'));
  6428.             }
  6429.             return;
  6430.         }
  6431.         if(mode === 1) {
  6432.             if($q('.de-video-thumb', this.player)) {
  6433.                 el.classList.add('de-current');
  6434.                 this.setPlayer(m, el.classList.contains('de-ytube'));
  6435.             } else {
  6436.                 el.classList.remove('de-current');
  6437.                 this._addThumb(m, el.classList.contains('de-ytube'));
  6438.             }
  6439.         } else {
  6440.             el.classList.remove('de-current');
  6441.             $hide(this.player);
  6442.             this.player.innerHTML = '';
  6443.             this.playerInfo = null;
  6444.         }
  6445.     }
  6446.     setPlayer(m, isYtube) {
  6447.         Videos.addPlayer(this, m, isYtube);
  6448.     }
  6449.     toggleFloatedThumb(linkEl, isOutEvent) {
  6450.         let el = $id('de-video-thumb-floated');
  6451.         if(isOutEvent) {
  6452.             $del(el);
  6453.             return;
  6454.         }
  6455.         if(!el) {
  6456.             el = $bEnd(docBody, `<img id="de-video-thumb-floated" src="https://i.ytimg.com/vi/${
  6457.                 linkEl.videoInfo[1] }/0.jpg">`);
  6458.         }
  6459.         const cr = linkEl.getBoundingClientRect();
  6460.         const pvHeight = Cfg.YTubeHeigh;
  6461.         const isTop = cr.top + cr.height + pvHeight < nav.viewportHeight();
  6462.         el.style.cssText = `position: absolute; left: ${ deWindow.pageXOffset + cr.left }px; top: ${
  6463.             deWindow.pageYOffset + (isTop ? cr.top + cr.height : cr.top - pvHeight) }px; width: ${
  6464.             Cfg.YTubeWidth }px; height: ${ pvHeight }px; z-index: 9999;`;
  6465.     }
  6466.     updatePost(oldLinks, newLinks, cloned) {
  6467.         const loader = !cloned && Videos._getTitlesLoader();
  6468.         let j = 0;
  6469.         for(let i = 0, len = newLinks.length; i < len; ++i) {
  6470.             const el = newLinks[i];
  6471.             const link = oldLinks[j];
  6472.             if(link && link.classList.contains('de-current')) {
  6473.                 this.currentLink = el;
  6474.             }
  6475.             if(cloned) {
  6476.                 el.videoInfo = link.videoInfo;
  6477.                 j++;
  6478.             } else {
  6479.                 const m = el.href.match(Videos.ytReg);
  6480.                 if(m) {
  6481.                     this.addLink(m, loader, el, true);
  6482.                     j++;
  6483.                 }
  6484.             }
  6485.         }
  6486.         this.currentLink = this.currentLink || newLinks[0];
  6487.         if(loader) {
  6488.             loader.completeTasks();
  6489.         }
  6490.     }
  6491.  
  6492.     static _fixTime(seconds = 0, minutes = 0, hours = 0) {
  6493.         if(seconds >= 60) {
  6494.             minutes += Math.floor(seconds / 60);
  6495.             seconds %= 60;
  6496.         }
  6497.         if(minutes >= 60) {
  6498.             hours += Math.floor(seconds / 60);
  6499.             minutes %= 60;
  6500.         }
  6501.         return [
  6502.             (hours ? hours + 'h' : '') +
  6503.             (minutes ? minutes + 'm' : '') +
  6504.             (seconds ? seconds + 's' : ''),
  6505.             hours, minutes, seconds
  6506.         ];
  6507.     }
  6508.     static _getTitlesLoader() {
  6509.         return Cfg.YTubeTitles && new TasksPool(4, (num, info) => {
  6510.             const [, isYtube,, id] = info;
  6511.             if(isYtube) {
  6512.                 return Cfg.ytApiKey ? Videos._getYTInfoAPI(info, num, id) :
  6513.                     Videos._getYTInfoOembed(info, num, id);
  6514.             }
  6515.             return $ajax(`${ aib.prot }//vimeo.com/api/v2/video/${ id }.json`, null, true).then(xhr => {
  6516.                 const entry = JSON.parse(xhr.responseText)[0];
  6517.                 return Videos._titlesLoaderHelper(
  6518.                     info, num,
  6519.                     entry.title,
  6520.                     entry.user_name,
  6521.                     entry.stats_number_of_plays,
  6522.                     /(.*)\s(.*)?/.exec(entry.upload_date)[1],
  6523.                     Videos._fixTime(entry.duration)[0]);
  6524.             }).catch(() => Videos._titlesLoaderHelper(info, num));
  6525.         }, () => (sesStorage['de-videos-data2'] = JSON.stringify(Videos._global.vData)));
  6526.     }
  6527.     static _getYTInfoAPI(info, num, id) {
  6528.         return $ajax(
  6529.             `https://www.googleapis.com/youtube/v3/videos?key=${ Cfg.ytApiKey }&id=${ id }` +
  6530.             '&part=snippet,statistics,contentDetails&fields=items/snippet/title,items/snippet/publishedAt,' +
  6531.             'items/snippet/channelTitle,items/statistics/viewCount,items/contentDetails/duration',
  6532.             null, true
  6533.         ).then(xhr => {
  6534.             const items = JSON.parse(xhr.responseText).items[0];
  6535.             return Videos._titlesLoaderHelper(
  6536.                 info, num,
  6537.                 items.snippet.title,
  6538.                 items.snippet.channelTitle,
  6539.                 items.statistics.viewCount,
  6540.                 items.snippet.publishedAt.substr(0, 10),
  6541.                 items.contentDetails.duration.substr(2).toLowerCase());
  6542.         }).catch(() => Videos._getYTInfoOembed(info, num, id));
  6543.     }
  6544.     static _getYTInfoOembed(info, num, id) {
  6545.         const canSendCORS = nav.hasGMXHR || nav.canUseFetch;
  6546.         return (canSendCORS ?
  6547.             $ajax(`https://www.youtube.com/oembed?url=http%3A//youtube.com/watch%3Fv%3D${
  6548.                 id }&format=json`, null, true) :
  6549.             $ajax(`https://noembed.com/embed?url=http%3A//youtube.com/watch%3Fv%3D${ id }&callback=?`)
  6550.         ).then(xhr => {
  6551.             const res = xhr.responseText;
  6552.             const json = JSON.parse(canSendCORS ? res : res.replace(/^[^{]+|\)$/g, ''));
  6553.             return Videos._titlesLoaderHelper(info, num, json.title, json.author_name, null, null, null);
  6554.         }).catch(() => Videos._titlesLoaderHelper(info, num));
  6555.     }
  6556.     static _titlesLoaderHelper([link, isYtube, videoObj, id], num, ...data) {
  6557.         if(data.length !== 0) {
  6558.             Videos.setLinkData(link, data);
  6559.             Videos._global.vData[+!isYtube][id] = data;
  6560.             videoObj.vData[+!isYtube].push(data);
  6561.             if(videoObj.titleLoadFn) {
  6562.                 videoObj.titleLoadFn(data);
  6563.             }
  6564.         }
  6565.         videoObj.loadedLinksCount++;
  6566.         // Wait for 3 sec every 30 links
  6567.         if(num % 30 === 0) {
  6568.             return Promise.reject(new TasksPool.PauseError(3e3));
  6569.         }
  6570.         return new Promise(resolve => setTimeout(resolve, 250));
  6571.     }
  6572.     _addThumb(m, isYtube) {
  6573.         const el = this.player;
  6574.         this.playerInfo = m;
  6575.         el.classList.remove('de-video-expanded');
  6576.         $show(el);
  6577.         const str = `<a class="de-video-player" href="${ aib.prot }`;
  6578.         if(isYtube) {
  6579.             el.innerHTML = `${ str }//www.youtube.com/watch?v=${ m[1] }" target="_blank">` +
  6580.                 `<img class="de-video-thumb de-ytube" src="https://i.ytimg.com/vi/${ m[1] }/0.jpg"></a>`;
  6581.             return;
  6582.         }
  6583.         el.innerHTML = `${ str }//vimeo.com/${ m[1] }" target="_blank">` +
  6584.             '<img class="de-video-thumb de-vimeo" src=""></a>';
  6585.         $ajax(`${ aib.prot }//vimeo.com/api/v2/video/${ m[1] }.json`, null, true).then(xhr => {
  6586.             el.firstChild.firstChild.setAttribute('src', JSON.parse(xhr.responseText)[0].thumbnail_large);
  6587.         }).catch(emptyFn);
  6588.     }
  6589. }
  6590. Videos.ytReg =
  6591.     /^https?:\/\/(?:www\.|m\.)?youtu(?:be\.com\/(?:watch\?.*?v=|v\/|embed\/)|\.be\/)([a-zA-Z0-9-_]+).*?(?:t(?:ime)?=(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?)?$/;
  6592. Videos.vimReg =
  6593.     /^https?:\/\/(?:www\.)?vimeo\.com\/(?:[^?]+\?clip_id=|.*?\/)?(\d+).*?(#t=\d+)?$/;
  6594. Videos._global = {
  6595.     get vData() {
  6596.         let value;
  6597.         try {
  6598.             value = Cfg.YTubeTitles ? JSON.parse(sesStorage['de-videos-data2'] || '[{}, {}]') : [{}, {}];
  6599.         } catch(err) {
  6600.             value = [{}, {}];
  6601.         }
  6602.         Object.defineProperty(this, 'vData', { value });
  6603.         return value;
  6604.     }
  6605. };
  6606.  
  6607. class VideosParser {
  6608.     constructor() {
  6609.         this._loader = Videos._getTitlesLoader();
  6610.     }
  6611.     endParser() {
  6612.         if(this._loader) {
  6613.             this._loader.completeTasks();
  6614.         }
  6615.     }
  6616.     parse(data) {
  6617.         const isPost = data instanceof AbstractPost;
  6618.         const loader = this._loader;
  6619.         VideosParser._parserHelper('a[href*="youtu"]', data, loader, isPost, true, Videos.ytReg);
  6620.         if(Cfg.addVimeo) {
  6621.             VideosParser._parserHelper('a[href*="vimeo.com"]', data, loader, isPost, false, Videos.vimReg);
  6622.         }
  6623.         const vids = aib.fixVideo(isPost, data);
  6624.         for(let i = 0, len = vids.length; i < len; ++i) {
  6625.             const [post, m, isYtube] = vids[i];
  6626.             if(post) {
  6627.                 post.videos.addLink(m, loader, null, isYtube);
  6628.             }
  6629.         }
  6630.         return this;
  6631.     }
  6632.  
  6633.     static _parserHelper(qPath, data, loader, isPost, isYtube, reg) {
  6634.         const links = $Q(qPath, isPost ? data.el : data);
  6635.         for(let i = 0, len = links.length; i < len; ++i) {
  6636.             const link = links[i];
  6637.             const m = link.href.match(reg);
  6638.             if(m) {
  6639.                 const mPost = isPost ? data : aib.getPostOfEl(link);
  6640.                 if(mPost) {
  6641.                     mPost.videos.addLink(m, loader, link, isYtube);
  6642.                 }
  6643.             }
  6644.         }
  6645.     }
  6646. }
  6647.  
  6648. // Embed .mp3 and Vocaroo links
  6649. function embedAudioLinks(data) {
  6650.     const isPost = data instanceof AbstractPost;
  6651.     if(Cfg.addMP3) {
  6652.         const els = $Q('a[href*=".mp3"]', isPost ? data.el : data);
  6653.         for(let i = 0, len = els.length; i < len; ++i) {
  6654.             const link = els[i];
  6655.             if((link.target !== '_blank' && link.rel !== 'nofollow') || !link.pathname.includes('.mp3')) {
  6656.                 continue;
  6657.             }
  6658.             const src = link.href;
  6659.             const el = (isPost ? data : aib.getPostOfEl(link)).mp3Obj;
  6660.             if(nav.canPlayMP3) {
  6661.                 if(!$q(`audio[src="${ src }"]`, el)) {
  6662.                     el.insertAdjacentHTML('beforeend',
  6663.                         `<p><audio src="${ src }" preload="none" controls></audio></p>`);
  6664.                 }
  6665.             // Flash plugin for old browsers that not support HTML5 audio
  6666.             } else if(!$q(`object[FlashVars*="${ src }"]`, el)) {
  6667.                 el.insertAdjacentHTML('beforeend', '<object data="' +
  6668.                     'http://junglebook2007.narod.ru/audio/player.swf" type="application/x-shockwave-flash" ' +
  6669.                     'wmode="transparent" width="220" height="16" FlashVars="playerID=1&amp;' +
  6670.                     'bg=0x808080&amp;leftbg=0xB3B3B3&amp;lefticon=0x000000&amp;rightbg=0x808080&amp;' +
  6671.                     'rightbghover=0x999999&amp;rightcon=0x000000&amp;righticonhover=0xffffff&amp;' +
  6672.                     'text=0xffffff&amp;slider=0x222222&amp;track=0xf5f5dc&amp;border=0x666666&amp;' +
  6673.                     `loader=0x7fc7ff&amp;loop=yes&amp;autostart=no&amp;soundFile=${ src }"><br>`);
  6674.             }
  6675.         }
  6676.     }
  6677.     if(Cfg.addVocaroo) {
  6678.         const els = $Q('a[href*="vocaroo.com"]', isPost ? data.el : data);
  6679.         for(let i = 0, len = els.length; i < len; ++i) {
  6680.             const link = els[i];
  6681.             const el = link.previousSibling;
  6682.             if(!el || el.className !== 'de-vocaroo') { // Don't embed already embedded links
  6683.                 link.insertAdjacentHTML('beforebegin', `<div class="de-vocaroo">
  6684.                     <embed src="http://vocaroo.com/player.swf?playMediaID=${ link.href.split('/').pop() }` +
  6685.                         `" width="148" height="44" wmode="transparent" type="application/x-shockwave-flash">
  6686.                 </div>`);
  6687.             }
  6688.         }
  6689.     }
  6690. }
  6691.  
  6692. /* ==[ Ajax.js ]==============================================================================================
  6693.                                                AJAX FUNCTIONS
  6694. =========================================================================================================== */
  6695.  
  6696. // Main AJAX util
  6697. function $ajax(url, params = null, isCORS = false) {
  6698.     let resolve, reject, cancelFn;
  6699.     const needTO = params ? params.useTimeout : false;
  6700.     const WAITING_TIME = 5e3;
  6701.     if(((isCORS ? !nav.hasGMXHR : !nav.canUseNativeXHR) || aib.hasRefererErr && nav.canUseFetch) &&
  6702.         (nav.canUseFetchBlob || !url.startsWith('blob'))
  6703.     ) {
  6704.         if(!params) {
  6705.             params = {};
  6706.         }
  6707.         params.referrer =
  6708.             doc.referrer.startsWith(aib.prot + '//' + aib.host) ? doc.referrer : deWindow.location;
  6709.         if(params.data) {
  6710.             params.body = params.data;
  6711.             delete params.data;
  6712.         }
  6713.         if(isCORS) {
  6714.             params.mode = 'cors';
  6715.         }
  6716.         url = getAbsLink(url);
  6717.         // Chrome-extension: avoid CORS in content script. Sending data to background.js
  6718.         if(isCORS && nav.isChrome && nav.scriptHandler === 'WebExtension') {
  6719.             if(params.body) {
  6720.                 // Converting image as Uint8Array to text data for sending in POST request from background.js
  6721.                 let textData = '';
  6722.                 const arrData = params.body.arr;
  6723.                 for(let i = 0, len = arrData.length; i < len; ++i) {
  6724.                     textData += String.fromCharCode(arrData[i]);
  6725.                 }
  6726.                 params.body.arr = textData;
  6727.             }
  6728.             chrome.runtime.sendMessage({ 'de-messsage': 'corsRequest', url, params }, res => {
  6729.                 const { answer } = res;
  6730.                 if(res.isError || !aib.isAjaxStatusOK(res.status)) {
  6731.                     reject(res.statusText ?
  6732.                         new AjaxError(res.status, res.statusText) : getErrorMessage(answer));
  6733.                     return;
  6734.                 }
  6735.                 const obj = {};
  6736.                 switch(params.responseType) {
  6737.                 case 'arraybuffer':
  6738.                 case 'blob': { // Converting text data from the background.js response to arraybuffer/blob
  6739.                     const buf = new ArrayBuffer(answer.length);
  6740.                     const bufView = new Uint8Array(buf);
  6741.                     for(let i = 0, len = answer.length; i < len; ++i) {
  6742.                         bufView[i] = answer.charCodeAt(i);
  6743.                     }
  6744.                     obj.response = params.responseType === 'blob' ? new Blob([buf]) : buf;
  6745.                     break;
  6746.                 }
  6747.                 default: obj.responseText = answer;
  6748.                 }
  6749.                 resolve(obj);
  6750.             });
  6751.         } else {
  6752.             const controller = new AbortController();
  6753.             params.signal = controller.signal;
  6754.             const loadTO = needTO && setTimeout(() => {
  6755.                 reject(AjaxError.Timeout);
  6756.                 try {
  6757.                     controller.abort();
  6758.                 } catch(err) {}
  6759.             }, WAITING_TIME);
  6760.             cancelFn = () => {
  6761.                 if(needTO) {
  6762.                     clearTimeout(loadTO);
  6763.                 }
  6764.                 controller.abort();
  6765.             };
  6766.             fetch(url, params).then(async res => {
  6767.                 if(!aib.isAjaxStatusOK(res.status)) {
  6768.                     reject(new AjaxError(res.status, res.statusText));
  6769.                     return;
  6770.                 }
  6771.                 switch(params.responseType) {
  6772.                 case 'arraybuffer': res.response = await res.arrayBuffer(); break;
  6773.                 case 'blob': res.response = await res.blob(); break;
  6774.                 default: res.responseText = await res.text();
  6775.                 }
  6776.                 resolve(res);
  6777.             }).catch(err => reject(getErrorMessage(err)));
  6778.         }
  6779.     } else if((isCORS || !nav.canUseNativeXHR) && nav.hasGMXHR) {
  6780.         let gmxhr;
  6781.         const timeoutFn = () => {
  6782.             reject(AjaxError.Timeout);
  6783.             try {
  6784.                 gmxhr.abort();
  6785.             } catch(err) {}
  6786.         };
  6787.         let loadTO = needTO && setTimeout(timeoutFn, WAITING_TIME);
  6788.         const obj = {
  6789.             method : (params && params.method) || 'GET',
  6790.             url    : nav.fixLink(url),
  6791.             onreadystatechange(e) {
  6792.                 if(needTO) {
  6793.                     clearTimeout(loadTO);
  6794.                 }
  6795.                 if(e.readyState === 4) {
  6796.                     if(aib.isAjaxStatusOK(e.status)) {
  6797.                         resolve(e);
  6798.                     } else {
  6799.                         reject(new AjaxError(e.status, e.statusText));
  6800.                     }
  6801.                 } else if(needTO) {
  6802.                     loadTO = setTimeout(timeoutFn, WAITING_TIME);
  6803.                 }
  6804.             }
  6805.         };
  6806.         if(params) {
  6807.             if(params.onprogress) {
  6808.                 obj.upload = { onprogress: params.onprogress };
  6809.                 delete params.onprogress;
  6810.             }
  6811.             delete params.method;
  6812.             Object.assign(obj, params);
  6813.         }
  6814.         if(nav.hasNewGM) {
  6815.             GM.xmlHttpRequest(obj);
  6816.             cancelFn = emptyFn; // GreaseMonkey 4 cannot cancel xhr's
  6817.         } else {
  6818.             gmxhr = GM_xmlhttpRequest(obj);
  6819.             cancelFn = () => {
  6820.                 if(needTO) {
  6821.                     clearTimeout(loadTO);
  6822.                 }
  6823.                 try {
  6824.                     gmxhr.abort();
  6825.                 } catch(err) {}
  6826.             };
  6827.         }
  6828.     } else if(nav.canUseNativeXHR) {
  6829.         const xhr = new XMLHttpRequest();
  6830.         const timeoutFn = () => {
  6831.             reject(AjaxError.Timeout);
  6832.             xhr.abort();
  6833.         };
  6834.         let loadTO = needTO && setTimeout(timeoutFn, WAITING_TIME);
  6835.         if(params && params.onprogress) {
  6836.             xhr.upload.onprogress = params.onprogress;
  6837.         }
  6838.         xhr.onreadystatechange = ({ target }) => {
  6839.             if(needTO) {
  6840.                 clearTimeout(loadTO);
  6841.             }
  6842.             if(target.readyState === 4) {
  6843.                 if(aib.isAjaxStatusOK(target.status)) {
  6844.                     resolve(target);
  6845.                 } else {
  6846.                     reject(new AjaxError(target.status, target.statusText));
  6847.                 }
  6848.             } else if(needTO) {
  6849.                 loadTO = setTimeout(timeoutFn, WAITING_TIME);
  6850.             }
  6851.         };
  6852.         try {
  6853.             xhr.open((params && params.method) || 'GET', getAbsLink(url), true);
  6854.             if(params) {
  6855.                 if(params.responseType) {
  6856.                     xhr.responseType = params.responseType;
  6857.                 }
  6858.                 const { headers } = params;
  6859.                 if(headers) {
  6860.                     for(const h in headers) {
  6861.                         if(headers.hasOwnProperty(h)) {
  6862.                             xhr.setRequestHeader(h, headers[h]);
  6863.                         }
  6864.                     }
  6865.                 }
  6866.             }
  6867.             xhr.send(params && params.data || null);
  6868.             cancelFn = () => {
  6869.                 if(needTO) {
  6870.                     clearTimeout(loadTO);
  6871.                 }
  6872.                 xhr.abort();
  6873.             };
  6874.         } catch(err) {
  6875.             clearTimeout(loadTO);
  6876.             nav.canUseNativeXHR = false;
  6877.             return $ajax(url, params);
  6878.         }
  6879.     } else {
  6880.         reject(new AjaxError(0, 'Ajax error: Can`t send any type of request.'));
  6881.     }
  6882.     return new CancelablePromise((res, rej) => {
  6883.         resolve = res;
  6884.         reject = rej;
  6885.     }, cancelFn);
  6886. }
  6887.  
  6888. class AjaxError {
  6889.     constructor(code, message) {
  6890.         this.code = code;
  6891.         this.message = message;
  6892.     }
  6893.     toString() {
  6894.         return this.code <= 0 ?
  6895.             String(this.message || Lng.noConnect[lang]) :
  6896.             `HTTP [${ this.code }] ${ this.message }`;
  6897.     }
  6898. }
  6899. AjaxError.Success = new AjaxError(200, 'OK');
  6900. AjaxError.Locked = new AjaxError(-1, { toString: () => Lng.thrClosed[lang] });
  6901. AjaxError.Timeout = new AjaxError(0, { toString: () => Lng.noConnect[lang] + ' (timeout)' });
  6902.  
  6903. const AjaxCache = {
  6904.     clearCache() {
  6905.         this._data = new Map();
  6906.     },
  6907.     fixURL: url => `${ url }${ url.includes('?') ? '&' : '?' }nocache=${ Math.random() }`,
  6908.     runCachedAjax(url, useCache) {
  6909.         const { hasCacheControl, params } = this._data.get(url) || {};
  6910.         const ajaxURL = hasCacheControl === false ? this.fixURL(url) : url;
  6911.         return $ajax(ajaxURL, useCache && params || { useTimeout: true }, aib._4chan).then(xhr =>
  6912.             this.saveData(url, xhr) ? xhr : $ajax(this.fixURL(url), useCache && params, aib._4chan));
  6913.     },
  6914.     saveData(url, xhr) {
  6915.         let ETag = null;
  6916.         let LastModified = null;
  6917.         let i = 0;
  6918.         let hasCacheControl = false;
  6919.         let headers = 'getAllResponseHeaders' in xhr ? xhr.getAllResponseHeaders() : xhr.responseHeaders;
  6920.         headers = headers ? /* usual xhr */ headers.split('\r\n') : /* fetch */ xhr.headers;
  6921.         for(const idx in headers) {
  6922.             if(!headers.hasOwnProperty(idx)) {
  6923.                 continue;
  6924.             }
  6925.             let header = headers[idx];
  6926.             if(typeof header === 'string') { // usual xhr
  6927.                 const сIdx = header.indexOf(':');
  6928.                 if(сIdx === -1) {
  6929.                     continue;
  6930.                 }
  6931.                 const name = header.substring(0, сIdx);
  6932.                 const value = header.substring(сIdx + 2, header.length);
  6933.                 header = [name, value];
  6934.             }
  6935.             const hName = header[0].toLowerCase();
  6936.             let matched = true;
  6937.             switch(hName) {
  6938.             case 'cache-control': hasCacheControl = true; break;
  6939.             case 'last-modified': LastModified = header[1]; break;
  6940.             case 'etag': ETag = header[1]; break;
  6941.             default: matched = false;
  6942.             }
  6943.             if(matched && ++i === 3) {
  6944.                 break;
  6945.             }
  6946.         }
  6947.         headers = null;
  6948.         if(ETag || LastModified) {
  6949.             headers = {};
  6950.             if(ETag) {
  6951.                 headers['If-None-Match'] = ETag;
  6952.             }
  6953.             if(LastModified) {
  6954.                 headers['If-Modified-Since'] = LastModified;
  6955.             }
  6956.         }
  6957.         const hasUrl = this._data.has(url);
  6958.         this._data.set(url, {
  6959.             hasCacheControl,
  6960.             params: headers ? { headers, useTimeout: true } : { useTimeout: true }
  6961.         });
  6962.         return hasUrl || hasCacheControl;
  6963.     },
  6964.  
  6965.     _data: new Map()
  6966. };
  6967.  
  6968. function ajaxLoad(url, returnForm = true, useCache = false, checkArch = false) {
  6969.     return AjaxCache.runCachedAjax(url, useCache).then(xhr => {
  6970.         let el;
  6971.         const text = xhr.responseText;
  6972.         if(text.includes('</html>')) {
  6973.             el = returnForm ? $q(aib.qDForm, $DOM(text)) : $DOM(text);
  6974.         }
  6975.         return !el ? CancelablePromise.reject(new AjaxError(0, Lng.errCorruptData[lang])) :
  6976.             checkArch ? [el, (xhr.responseURL || '').includes('/arch/')] : el;
  6977.     }, err => err.code === 304 ? null : CancelablePromise.reject(err));
  6978. }
  6979.  
  6980. function ajaxPostsLoad(brd, tNum, useCache, useJson = true) {
  6981.     if(useJson && aib.JsonBuilder) {
  6982.         return AjaxCache.runCachedAjax(aib.getJsonApiUrl(brd, tNum), useCache).then(xhr => {
  6983.             try {
  6984.                 return new aib.JsonBuilder(JSON.parse(xhr.responseText), brd);
  6985.             } catch(err) {
  6986.                 if(err instanceof AjaxError) {
  6987.                     return CancelablePromise.reject(err);
  6988.                 }
  6989.                 console.warn(`API error: ${ err }. Switching to DOM parsing!`);
  6990.                 aib.JsonBuilder = null;
  6991.                 return ajaxPostsLoad(brd, tNum, useCache);
  6992.             }
  6993.         }, err => err.code === 304 ? null : CancelablePromise.reject(err));
  6994.     }
  6995.     return aib.iichan ?
  6996.         ajaxLoad(aib.getThrUrl(brd, tNum), true, useCache, true)
  6997.             .then(data => data && data[0] ? new DOMPostsBuilder(data[0], data[1]) : null) :
  6998.         ajaxLoad(aib.getThrUrl(brd, tNum), true, useCache)
  6999.             .then(form => form ? new DOMPostsBuilder(form) : null);
  7000. }
  7001.  
  7002. function infoLoadErrors(err, showError = true) {
  7003.     const isAjax = err instanceof AjaxError;
  7004.     const eCode = isAjax ? err.code : 0;
  7005.     if(eCode === 200) {
  7006.         closePopup('newposts');
  7007.     } else if(isAjax && eCode === 0) {
  7008.         $popup('newposts', err.message ? String(err.message) :
  7009.             `${ Lng.noConnect[lang] }: \n${ getErrorMessage(err) }`);
  7010.     } else {
  7011.         $popup('newposts', `${ Lng.thrNotFound[lang] } (№${ aib.t }): \n${ getErrorMessage(err) }`);
  7012.         if(showError) {
  7013.             doc.title = `{${ eCode }} ${ doc.title }`;
  7014.         }
  7015.     }
  7016. }
  7017.  
  7018. /* ==[ Pages.js ]=============================================================================================
  7019.                                                 PAGES LOADER
  7020. =========================================================================================================== */
  7021.  
  7022. const Pages = {
  7023.     addPage(needThreads = 0, pageNum = DelForm.last.pageNum + 1) {
  7024.         if(this._isAdding || pageNum > aib.lastPage || needThreads && pageNum > 4) {
  7025.             return;
  7026.         }
  7027.         this._isAdding = true;
  7028.         DelForm.last.el.insertAdjacentHTML('beforeend',
  7029.             `<div class="de-addpage-wait"><hr><center style="font-size: 1.5em"><svg class="de-wait">
  7030.                 <use xlink:href="#de-symbol-wait"/></svg>${ Lng.loading[lang] }</center></div>`);
  7031.         MyPosts.purge();
  7032.         this._addingPromise = ajaxLoad(aib.getPageUrl(aib.b, pageNum)).then(async formEl => {
  7033.             const newForm = this._addForm(formEl, pageNum);
  7034.             if(newForm.firstThr) {
  7035.                 if(!needThreads) {
  7036.                     return this._updateForms(DelForm.last);
  7037.                 }
  7038.                 $hide(newForm.el);
  7039.                 await this._updateForms(DelForm.last);
  7040.                 const firstForm = DelForm.first;
  7041.                 let thr = newForm.firstThr;
  7042.                 do {
  7043.                     if(thr.isHidden) {
  7044.                         DelForm.tNums.delete(thr.num);
  7045.                     } else {
  7046.                         const oldLastThr = firstForm.lastThr;
  7047.                         $after(oldLastThr.el, thr.el);
  7048.                         newForm.firstThr = thr.next;
  7049.                         thr.prev = oldLastThr;
  7050.                         thr.form = firstForm;
  7051.                         firstForm.lastThr = oldLastThr.next = thr;
  7052.                         needThreads--;
  7053.                     }
  7054.                     thr = thr.next;
  7055.                 } while(needThreads && thr);
  7056.                 DelForm.last = firstForm;
  7057.                 firstForm.next = firstForm.lastThr.next = null;
  7058.                 newForm.el.remove();
  7059.                 this._endAdding();
  7060.                 if(needThreads) {
  7061.                     this.addPage(needThreads, pageNum + 1);
  7062.                 }
  7063.                 return CancelablePromise.reject(new CancelError());
  7064.             }
  7065.             this._endAdding();
  7066.             this.addPage();
  7067.             return CancelablePromise.reject(new CancelError());
  7068.         }).then(() => this._endAdding()).catch(err => {
  7069.             if(!(err instanceof CancelError)) {
  7070.                 $popup('add-page', getErrorMessage(err));
  7071.                 this._endAdding();
  7072.             }
  7073.         });
  7074.     },
  7075.     async loadPages(count) {
  7076.         $popup('load-pages', Lng.loading[lang], true);
  7077.         if(this._addingPromise) {
  7078.             this._addingPromise.cancelPromise();
  7079.             this._endAdding();
  7080.         }
  7081.         PviewsCache.purge();
  7082.         isExpImg = false;
  7083.         pByEl = new Map();
  7084.         pByNum = new Map();
  7085.         Post.hiddenNums = new Set();
  7086.         AttachedImage.closeImg();
  7087.         if(pr.isQuick) {
  7088.             pr.clearForm();
  7089.         }
  7090.         DelForm.tNums = new Set();
  7091.         for(const form of DelForm) {
  7092.             $each($Q('a[href^="blob:"]', form.el), el => URL.revokeObjectURL(el.href));
  7093.             $hide(form.el);
  7094.             if(form === DelForm.last) {
  7095.                 break;
  7096.             }
  7097.             form.el.remove();
  7098.         }
  7099.         DelForm.first = DelForm.last;
  7100.         for(let i = aib.page, len = Math.min(aib.lastPage + 1, aib.page + count); i < len; ++i) {
  7101.             try {
  7102.                 this._addForm(await ajaxLoad(aib.getPageUrl(aib.b, i)), i);
  7103.             } catch(err) {
  7104.                 $popup('load-pages', getErrorMessage(err));
  7105.             }
  7106.         }
  7107.         const { first } = DelForm;
  7108.         if(first !== DelForm.last) {
  7109.             DelForm.first = first.next;
  7110.             first.el.remove();
  7111.             await this._updateForms(DelForm.first);
  7112.             closePopup('load-pages');
  7113.         }
  7114.     },
  7115.  
  7116.     _isAdding      : false,
  7117.     _addingPromise : null,
  7118.     _addForm(formEl, pageNum) {
  7119.         formEl = doc.adoptNode(formEl);
  7120.         $hide(formEl = aib.fixHTML(formEl));
  7121.         $after(DelForm.last.el, formEl);
  7122.         const form = new DelForm(formEl, +pageNum, DelForm.last);
  7123.         DelForm.last = form;
  7124.         form.addStuff();
  7125.         if(pageNum !== aib.page && form.firstThr) {
  7126.             formEl.insertAdjacentHTML('afterbegin', `<div class="de-page-num">
  7127.                 <center style="font-size: 2em">${ Lng.page[lang] } ${ pageNum }</center><hr></div>`);
  7128.         }
  7129.         $show(formEl);
  7130.         return form;
  7131.     },
  7132.     _endAdding() {
  7133.         $q('.de-addpage-wait').remove();
  7134.         this._isAdding = false;
  7135.         this._addingPromise = null;
  7136.     },
  7137.     async _updateForms(newForm) {
  7138.         readPostsData(newForm.firstThr.op, await readFavorites());
  7139.         if(pr.passw) {
  7140.             PostForm.setUserPassw();
  7141.         }
  7142.         embedPostMsgImages(newForm.el);
  7143.         if(HotKeys.enabled) {
  7144.             HotKeys.clearCPost();
  7145.         }
  7146.     }
  7147. };
  7148.  
  7149. function toggleInfinityScroll() {
  7150.     if(!aib.t) {
  7151.         const evtName = 'onwheel' in doc.defaultView ? 'wheel' : 'mousewheel';
  7152.         if(Cfg.inftyScroll) {
  7153.             doc.defaultView.addEventListener(evtName, toggleInfinityScroll.onwheel);
  7154.         } else {
  7155.             doc.defaultView.removeEventListener(evtName, toggleInfinityScroll.onwheel);
  7156.         }
  7157.     }
  7158. }
  7159. toggleInfinityScroll.onwheel = e => {
  7160.     if((e.type === 'wheel' ? e.deltaY : -('wheelDeltaY' in e ? e.wheelDeltaY : e.wheelDelta)) > 0) {
  7161.         deWindow.requestAnimationFrame(() => {
  7162.             if(Thread.last.bottom - 150 < Post.sizing.wHeight) {
  7163.                 Pages.addPage();
  7164.             }
  7165.         });
  7166.     }
  7167. };
  7168.  
  7169. /* ==[ Spells.js ]============================================================================================
  7170.                                                    SPELLS
  7171. =========================================================================================================== */
  7172.  
  7173. const Spells = Object.create({
  7174.     hash: null,
  7175.     get hiders() {
  7176.         this._initSpells();
  7177.         return this.hiders;
  7178.     },
  7179.     get list() {
  7180.         if(Cfg.spells === null) {
  7181.             return '#wipe(samelines,samewords,longwords,symbols,numbers,whitespace)';
  7182.         }
  7183.         let data;
  7184.         try {
  7185.             data = JSON.parse(Cfg.spells);
  7186.         } catch(err) {
  7187.             return '';
  7188.         }
  7189.         const [, s, reps, oreps] = data;
  7190.         let str = s ? this._decompileScope(s, '')[0].join('\n') : '';
  7191.         if(reps || oreps) {
  7192.             if(str) {
  7193.                 str += '\n\n';
  7194.             }
  7195.             if(reps) {
  7196.                 for(const rep of reps) {
  7197.                     str += this._decompileRep(rep, false) + '\n';
  7198.                 }
  7199.             }
  7200.             if(oreps) {
  7201.                 for(const orep of oreps) {
  7202.                     str += this._decompileRep(orep, true) + '\n';
  7203.                 }
  7204.             }
  7205.             str = str.substr(0, str.length - 1);
  7206.         }
  7207.         return str;
  7208.     },
  7209.     get names() {
  7210.         return [
  7211.             'words', 'exp', 'exph', 'imgn', 'ihash', 'subj', 'name', 'trip', 'img', 'sage', 'op', 'tlen',
  7212.             'all', 'video', 'wipe', 'num', 'vauthor'
  7213.         ];
  7214.     },
  7215.     get needArg() {
  7216.         return [
  7217.             /* words */ true, /* exp */ true, /* exph */ true, /* imgn */ true, /* ihash */ true,
  7218.             /* subj */ false, /* name */ true, /* trip */ false, /* img */ false, /* sage */ false,
  7219.             /* op */ false, /* tlen */ false, /* all */ false, /* video */ false, /* wipe */ false,
  7220.             /* num */ true, /* vauthor */ true
  7221.         ];
  7222.     },
  7223.     get outreps() {
  7224.         this._initSpells();
  7225.         return this.outreps;
  7226.     },
  7227.     get reps() {
  7228.         this._initSpells();
  7229.         return this.reps;
  7230.     },
  7231.     addSpell(type, arg, isNeg) {
  7232.         const fld = $id('de-spell-txt');
  7233.         const val = fld && fld.value;
  7234.         const chk = $q('input[info="hideBySpell"]');
  7235.         let spells = val && this.parseText(val);
  7236.         if(!val || spells) {
  7237.             if(!spells) {
  7238.                 try {
  7239.                     spells = JSON.parse(Cfg.spells);
  7240.                 } catch(err) {}
  7241.                 spells = spells || [Date.now(), [], null, null];
  7242.             }
  7243.             let idx, isAdded = true;
  7244.             const scope = aib.t ? [aib.b, aib.t] : null;
  7245.             if(spells[1]) {
  7246.                 const sScope = String(scope);
  7247.                 const sArg = String(arg);
  7248.                 spells[1].some(scope && isNeg ? (spell, i) => {
  7249.                     let data;
  7250.                     if(spell[0] === 0xFF &&
  7251.                         ((data = spell[1]) instanceof Array) &&
  7252.                         data.length === 2 &&
  7253.                         data[0][0] === 0x20C &&
  7254.                         data[1][0] === type &&
  7255.                         data[1][2] == null &&
  7256.                         String(data[1][1]) === sArg &&
  7257.                         String(data[0][2]) === sScope
  7258.                     ) {
  7259.                         idx = i;
  7260.                         return true;
  7261.                     }
  7262.                     return (spell[0] & 0x200) !== 0;
  7263.                 } : (spell, i) => {
  7264.                     if(spell[0] === type && String(spell[1]) === sArg && String(spell[2]) === sScope) {
  7265.                         idx = i;
  7266.                         return true;
  7267.                     }
  7268.                     return (spell[0] & 0x200) !== 0;
  7269.                 });
  7270.             } else {
  7271.                 spells[1] = [];
  7272.             }
  7273.             if(typeof idx === 'undefined') {
  7274.                 if(scope && isNeg) {
  7275.                     spells[1].unshift([0xFF, [[0x20C, '', scope], [type, arg, void 0]], void 0]);
  7276.                 } else {
  7277.                     spells[1].unshift([type, arg, scope]);
  7278.                 }
  7279.             } else if(Cfg.hideBySpell) {
  7280.                 if(spells[1].length === 1) {
  7281.                     spells[1] = null;
  7282.                 } else {
  7283.                     spells[1].splice(idx, 1);
  7284.                 }
  7285.                 isAdded = false;
  7286.             }
  7287.             if(isAdded) {
  7288.                 saveCfg('hideBySpell', 1);
  7289.                 if(chk) {
  7290.                     chk.checked = true;
  7291.                 }
  7292.             } else if(!spells[1] && !spells[2] && !spells[3]) {
  7293.                 saveCfg('hideBySpell', 0);
  7294.                 if(chk) {
  7295.                     chk.checked = false;
  7296.                 }
  7297.             }
  7298.             saveCfg('spells', JSON.stringify(spells));
  7299.             this.setSpells(spells, true);
  7300.             if(fld) {
  7301.                 fld.value = this.list;
  7302.             }
  7303.             Pview.updatePosition(true);
  7304.             return;
  7305.         }
  7306.         if(chk) {
  7307.             chk.checked = false;
  7308.         }
  7309.     },
  7310.     decompileSpell(type, neg, val, scope, wipeMsg = null) {
  7311.         let spell = (neg ? '!#' : '#') + this.names[type] +
  7312.             (scope ? `[${ scope[0] }${ scope[1] ? `,${ scope[1] === -1 ? '' : scope[1] }` : '' }]` : '');
  7313.         if(!val) {
  7314.             return spell;
  7315.         }
  7316.         // #img
  7317.         if(type === 8) {
  7318.             return spell + '(' + (val[0] === 2 ? '>' : val[0] === 1 ? '<' : '=') +
  7319.                 (val[1] ? val[1][0] + (val[1][1] === val[1][0] ? '' : '-' + val[1][1]) : '') +
  7320.                 (val[2] ? '@' + val[2][0] + (val[2][0] === val[2][1] ? '' : '-' + val[2][1]) + 'x' +
  7321.                 val[2][2] + (val[2][2] === val[2][3] ? '' : '-' + val[2][3]) : '') + ')';
  7322.         // #wipe
  7323.         } else if(type === 14) {
  7324.             if(val === 0x3F && !wipeMsg) {
  7325.                 return spell;
  7326.             }
  7327.             const [msgBit, msgData] = wipeMsg || [];
  7328.             const names = [];
  7329.             const bits = {
  7330.                 1  : 'samelines',
  7331.                 2  : 'samewords',
  7332.                 4  : 'longwords',
  7333.                 8  : 'symbols',
  7334.                 16 : 'capslock',
  7335.                 32 : 'numbers',
  7336.                 64 : 'whitespace'
  7337.             };
  7338.             for(const bit in bits) {
  7339.                 if(+bit !== msgBit && (val & +bit)) {
  7340.                     names.push(bits[bit]);
  7341.                 }
  7342.             }
  7343.             if(msgBit) {
  7344.                 names.push(bits[msgBit].toUpperCase() + (msgData ? ': ' + msgData : ''));
  7345.             }
  7346.             return `${ spell }(${ names.join(',') })`;
  7347.         // #num, #tlen
  7348.         } else if(type === 15 || type === 11) {
  7349.             let temp_, temp = val[1].length - 1;
  7350.             if(temp !== -1) {
  7351.                 for(temp_ = []; temp >= 0; --temp) {
  7352.                     temp_.push(val[1][temp][0] + '-' + val[1][temp][1]);
  7353.                 }
  7354.                 temp_.reverse();
  7355.             }
  7356.             spell += '(';
  7357.             if(val[0].length) {
  7358.                 spell += val[0].join(',') + (temp_ ? ',' : '');
  7359.             }
  7360.             if(temp_) {
  7361.                 spell += temp_.join(',');
  7362.             }
  7363.             return spell + ')';
  7364.         // #words, #name, #trip, #vauthor
  7365.         } else if(type === 0 || type === 6 || type === 7 || type === 16) {
  7366.             return `${ spell }(${ val.replace(/([)\\])/g, '\\$1').replace(/\n/g, '\\n') })`;
  7367.         } else {
  7368.             return `${ spell }(${ String(val) })`;
  7369.         }
  7370.     },
  7371.     disableSpells() {
  7372.         const value = null;
  7373.         const configurable = true;
  7374.         Object.defineProperties(this, {
  7375.             hiders  : { configurable, value },
  7376.             outreps : { configurable, value },
  7377.             reps    : { configurable, value }
  7378.         });
  7379.         saveCfg('hideBySpell', 0);
  7380.     },
  7381.     outReplace(txt) {
  7382.         for(const orep of this.outreps) {
  7383.             txt = txt.replace(orep[0], orep[1]);
  7384.         }
  7385.         return txt;
  7386.     },
  7387.     parseText(text) {
  7388.         const codeGen = new SpellsCodegen(text);
  7389.         const data = codeGen.generate();
  7390.         if(codeGen.hasError) {
  7391.             $popup('err-spell', Lng.error[lang] + ': ' + codeGen.errorSpell);
  7392.         } else if(data) {
  7393.             if(data[0] && Cfg.sortSpells) {
  7394.                 this._sort(data[0]);
  7395.             }
  7396.             return [Date.now(), ...data];
  7397.         }
  7398.         return null;
  7399.     },
  7400.     replace(txt) {
  7401.         for(const rep of this.reps) {
  7402.             txt = txt.replace(rep[0], rep[1]);
  7403.         }
  7404.         return txt;
  7405.     },
  7406.     setSpells(spells, sync) {
  7407.         if(sync) {
  7408.             this._sync(spells);
  7409.         }
  7410.         if(!Cfg.hideBySpell) {
  7411.             SpellsRunner.unhideAll();
  7412.             this.disableSpells();
  7413.             return;
  7414.         }
  7415.         this._optimize(spells);
  7416.         if(this.hiders) {
  7417.             const sRunner = new SpellsRunner();
  7418.             for(let post = Thread.first.op; post; post = post.next) {
  7419.                 sRunner.runSpells(post);
  7420.             }
  7421.             sRunner.endSpells();
  7422.         } else {
  7423.             SpellsRunner.unhideAll();
  7424.         }
  7425.     },
  7426.     toggle() {
  7427.         let spells;
  7428.         const fld = $id('de-spell-txt');
  7429.         const val = fld.value;
  7430.         if(val && (spells = this.parseText(val))) {
  7431.             closePopup('err-spell');
  7432.             this.setSpells(spells, true);
  7433.             saveCfg('spells', JSON.stringify(spells));
  7434.             fld.value = this.list;
  7435.         } else {
  7436.             if(!val) {
  7437.                 closePopup('err-spell');
  7438.                 SpellsRunner.unhideAll();
  7439.                 this.disableSpells();
  7440.                 saveCfg('spells', JSON.stringify([Date.now(), null, null, null]));
  7441.                 sendStorageEvent('__de-spells', '{ hide: false, data: null }');
  7442.             }
  7443.             $q('input[info="hideBySpell"]').checked = false;
  7444.         }
  7445.     },
  7446.  
  7447.     _decompileRep(rep, isOrep) {
  7448.         return (isOrep ? '#outrep' : '#rep') +
  7449.             (rep[0] ? `[${ rep[0] }${ rep[1] ? `,${ rep[1] === -1 ? '' : rep[1] }` : '' }]` : '') +
  7450.             `(${ rep[2] },${ rep[3].replace(/([)\\])/g, '\\$1').replace(/\n/g, '\\n') })`;
  7451.     },
  7452.     _decompileScope(scope, indent) {
  7453.         const dScope = [];
  7454.         let hScope = false;
  7455.         for(let i = 0, j = 0, len = scope.length; i < len; ++i, ++j) {
  7456.             const spell = scope[i];
  7457.             const type = spell[0] & 0xFF;
  7458.             if(type === 0xFF) {
  7459.                 hScope = true;
  7460.                 const temp = this._decompileScope(spell[1], indent + '    ');
  7461.                 if(temp[1]) {
  7462.                     const str = `${ spell[0] & 0x100 ? '!(\n' : '(\n' }${ indent }    ` +
  7463.                         `${ temp[0].join(`\n${ indent }    `) }\n${ indent })`;
  7464.                     if(j === 0) {
  7465.                         dScope[0] = str;
  7466.                     } else {
  7467.                         dScope[--j] += ' ' + str;
  7468.                     }
  7469.                 } else {
  7470.                     dScope[j] = `${ spell[0] & 0x100 ? '!(' : '(' }${ temp[0].join(' ') })`;
  7471.                 }
  7472.             } else {
  7473.                 dScope[j] = this.decompileSpell(type, spell[0] & 0x100, spell[1], spell[2]);
  7474.             }
  7475.             if(i !== len - 1) {
  7476.                 dScope[j] += spell[0] & 0x200 ? ' &' : ' |';
  7477.             }
  7478.         }
  7479.         return [dScope, dScope.length > 2 || hScope];
  7480.     },
  7481.     _initSpells() {
  7482.         if(!Cfg.hideBySpell) {
  7483.             const value = null;
  7484.             const configurable = true;
  7485.             Object.defineProperties(this, {
  7486.                 hiders  : { configurable, value },
  7487.                 outreps : { configurable, value },
  7488.                 reps    : { configurable, value }
  7489.             });
  7490.             return;
  7491.         }
  7492.         let spells, data;
  7493.         try {
  7494.             spells = JSON.parse(Cfg.spells);
  7495.             data = JSON.parse(sesStorage[`de-spells-${ aib.b }${ aib.t || '' }`]);
  7496.         } catch(err) {}
  7497.         if(data && spells && data[0] === spells[0]) {
  7498.             this.hash = data[0];
  7499.             this._setData(data[1], data[2], data[3]);
  7500.             return;
  7501.         }
  7502.         if(spells) {
  7503.             this._optimize(spells);
  7504.         } else {
  7505.             this.disableSpells();
  7506.         }
  7507.     },
  7508.     _initHiders(data) {
  7509.         if(data) {
  7510.             for(const item of data) {
  7511.                 const val = item[1];
  7512.                 if(val) {
  7513.                     switch(item[0] & 0xFF) {
  7514.                     case 1:
  7515.                     case 2:
  7516.                     case 3:
  7517.                     case 5:
  7518.                     case 13: item[1] = toRegExp(val, true); break;
  7519.                     case 0xFF: this._initHiders(val);
  7520.                     }
  7521.                 }
  7522.             }
  7523.         }
  7524.         return data;
  7525.     },
  7526.     _initReps(data) {
  7527.         if(data) {
  7528.             for(const item of data) {
  7529.                 item[0] = toRegExp(item[0], false);
  7530.             }
  7531.         }
  7532.         return data;
  7533.     },
  7534.     _optimize(data) {
  7535.         const arr = [
  7536.             data[1] ? this._optimizeSpells(data[1]) : null,
  7537.             data[2] ? this._optimizeReps(data[2]) : null,
  7538.             data[3] ? this._optimizeReps(data[3]) : null
  7539.         ];
  7540.         sesStorage[`de-spells-${ aib.b }${ aib.t || '' }`] = JSON.stringify([data[0], ...arr]);
  7541.         this.hash = data[0];
  7542.         this._setData(...arr);
  7543.     },
  7544.     _optimizeReps(data) {
  7545.         const rv = [];
  7546.         for(const [r0, r1, r2, r3] of data) {
  7547.             if(!r0 || (r0 === aib.b && (r1 === -1 ? !aib.t : !r1 || +r1 === aib.t))) {
  7548.                 rv.push([r2, r3]);
  7549.             }
  7550.         }
  7551.         return !rv.length ? null : rv;
  7552.     },
  7553.     _optimizeSpells(spells) {
  7554.         let neg, lastSpell = -1;
  7555.         let newSpells = [];
  7556.         for(let i = 0, len = spells.length; i < len; ++i) {
  7557.             let j;
  7558.             const spell = spells[i];
  7559.             let flags = spell[0];
  7560.             const type = flags & 0xFF;
  7561.             neg = (flags & 0x100) !== 0;
  7562.             if(type === 0xFF) {
  7563.                 const parensSpells = this._optimizeSpells(spell[1]);
  7564.                 if(parensSpells) {
  7565.                     if(parensSpells.length !== 1) {
  7566.                         newSpells.push([flags, parensSpells]);
  7567.                         lastSpell++;
  7568.                         continue;
  7569.                     } else if((parensSpells[0][0] & 0xFF) !== 12) {
  7570.                         newSpells.push([(parensSpells[0][0] | (flags & 0x200)) ^ (flags & 0x100),
  7571.                             parensSpells[0][1]]);
  7572.                         lastSpell++;
  7573.                         continue;
  7574.                     }
  7575.                     flags = parensSpells[0][0];
  7576.                     neg = !(neg ^ ((flags & 0x100) !== 0));
  7577.                 }
  7578.             } else {
  7579.                 const scope = spell[2];
  7580.                 if(!scope || (
  7581.                     scope[0] === aib.b &&
  7582.                     (scope[1] === -1 ? !aib.t : !scope[1] || +scope[1] === aib.t)
  7583.                 )) {
  7584.                     if(type === 12) {
  7585.                         neg = !neg;
  7586.                     } else {
  7587.                         newSpells.push([flags, spell[1]]);
  7588.                         lastSpell++;
  7589.                         continue;
  7590.                     }
  7591.                 }
  7592.             }
  7593.             for(j = lastSpell; j >= 0 && (((newSpells[j][0] & 0x200) !== 0) ^ neg); --j) /* empty */;
  7594.             if(j !== lastSpell) {
  7595.                 newSpells = newSpells.slice(0, j + 1);
  7596.                 lastSpell = j;
  7597.             }
  7598.             if(neg && j !== -1) {
  7599.                 newSpells[j][0] &= 0x1FF;
  7600.             }
  7601.             if(((flags & 0x200) !== 0) ^ neg) {
  7602.                 break;
  7603.             }
  7604.         }
  7605.         return lastSpell === -1 ? neg ? [[12, '']] : null : newSpells;
  7606.     },
  7607.     _setData(hiders, reps, outreps) {
  7608.         const configurable = true;
  7609.         Object.defineProperties(this, {
  7610.             hiders  : { configurable, value: this._initHiders(hiders) },
  7611.             outreps : { configurable, value: this._initReps(outreps) },
  7612.             reps    : { configurable, value: this._initReps(reps) }
  7613.         });
  7614.     },
  7615.     _sort(sp) {
  7616.         // Wraps AND-spells with brackets for proper sorting
  7617.         for(let i = 0, len = sp.length - 1; i < len; ++i) {
  7618.             if(sp[i][0] > 0x200) {
  7619.                 const temp = [0xFF, []];
  7620.                 do {
  7621.                     temp[1].push(sp.splice(i, 1)[0]);
  7622.                     len--;
  7623.                 } while(sp[i][0] > 0x200);
  7624.                 temp[1].push(sp.splice(i, 1)[0]);
  7625.                 sp.splice(i, 0, temp);
  7626.             }
  7627.         }
  7628.         sp = sp.sort();
  7629.         for(let i = 0, len = sp.length - 1; i < len; ++i) {
  7630.             // Removes duplicates and weaker spells
  7631.             const j = i + 1;
  7632.             if(sp[i][0] === sp[j][0] &&
  7633.                 sp[i][1] <= sp[j][1] &&
  7634.                 sp[i][1] >= sp[j][1] &&
  7635.                 (sp[i][2] === null || // Stronger spell with 3 parameters
  7636.                     sp[i][2] === undefined || // Equal spells with 2 parameters
  7637.                     (sp[i][2] <= sp[j][2] && sp[i][2] >= sp[j][2]))
  7638.             ) { // Equal spells with 3 parameters
  7639.                 sp.splice(j, 1);
  7640.                 i--;
  7641.                 len--;
  7642.             // Moves brackets to the end of the list
  7643.             } else if(sp[i][0] === 0xFF) {
  7644.                 sp.push(sp.splice(i, 1)[0]);
  7645.                 i--;
  7646.                 len--;
  7647.             }
  7648.         }
  7649.     },
  7650.     _sync(data) {
  7651.         sendStorageEvent('__de-spells', { hide: !!Cfg.hideBySpell, data });
  7652.     }
  7653. });
  7654.  
  7655. class SpellsCodegen {
  7656.     constructor(sList) {
  7657.         this.TYPE_UNKNOWN = 0;
  7658.         this.TYPE_ANDOR = 1;
  7659.         this.TYPE_NOT = 2;
  7660.         this.TYPE_SPELL = 3;
  7661.         this.TYPE_PARENTHESES = 4;
  7662.         this.TYPE_REPLACER = 5;
  7663.         this.hasError = false;
  7664.         this._col = 1;
  7665.         this._errMsg = '';
  7666.         this._errMsgArg = null;
  7667.         this._line = 1;
  7668.         this._sList = sList;
  7669.     }
  7670.     get errorSpell() {
  7671.         return !this.hasError ? '' :
  7672.             (this._errMsgArg ? this._errMsg.replace('%s', this._errMsgArg) : this._errMsg) +
  7673.             Lng.seRow[lang] + this._line + Lng.seCol[lang] + this._col + ')';
  7674.     }
  7675.     generate() {
  7676.         return this._sList ? this._generate(this._sList, false) : null;
  7677.     }
  7678.  
  7679.     static _getScope(str) {
  7680.         const m = str.match(/^\[([a-z0-9/]+)(?:(,)|,(\s*[0-9]+))?\]/);
  7681.         return m ? [m[0].length, [m[1], m[3] ? +m[3] : m[2] ? -1 : false]] : null;
  7682.     }
  7683.     static _getText(str, haveBracket) {
  7684.         if(haveBracket && (str[0] !== '(')) {
  7685.             return [0, ''];
  7686.         }
  7687.         let rv = '';
  7688.         for(let i = haveBracket ? 1 : 0, len = str.length; i < len; ++i) {
  7689.             const ch = str[i];
  7690.             if(ch === '\\') {
  7691.                 if(i === len - 1) {
  7692.                     return null;
  7693.                 }
  7694.                 switch(str[i + 1]) {
  7695.                 case 'n': rv += '\n'; break;
  7696.                 case '\\': rv += '\\'; break;
  7697.                 case ')': rv += ')'; break;
  7698.                 default: return null;
  7699.                 }
  7700.                 ++i;
  7701.             } else if(ch === ')') {
  7702.                 return [i + 1, rv];
  7703.             } else {
  7704.                 rv += ch;
  7705.             }
  7706.         }
  7707.         return null;
  7708.     }
  7709.     _generate(sList, inParens) {
  7710.         const spellsArr = [];
  7711.         let reps = [];
  7712.         let outreps = [];
  7713.         let lastType = this.TYPE_UNKNOWN;
  7714.         let hasReps = false;
  7715.         for(let i = 0, len = sList.length; i < len; i++, this._col++) {
  7716.             let res;
  7717.             switch(sList[i]) {
  7718.             case '\n':
  7719.                 this._line++;
  7720.                 this._col = 0;
  7721.                 /* falls through */
  7722.             case '\r':
  7723.             case ' ': continue;
  7724.             case '#': {
  7725.                 let name = '';
  7726.                 i++;
  7727.                 this._col++;
  7728.                 while((sList[i] >= 'a' && sList[i] <= 'z') || (sList[i] >= 'A' && sList[i] <= 'Z')) {
  7729.                     name += sList[i].toLowerCase();
  7730.                     i++;
  7731.                     this._col++;
  7732.                 }
  7733.                 if(name === 'rep' || name === 'outrep') {
  7734.                     if(!hasReps) {
  7735.                         if(inParens) {
  7736.                             this._col -= 1 + name.length;
  7737.                             this._setError(Lng.seRepsInParens[lang], '#' + name);
  7738.                             return null;
  7739.                         }
  7740.                         if(lastType === this.TYPE_ANDOR || lastType === this.TYPE_NOT) {
  7741.                             i -= 1 + name.length;
  7742.                             this._col -= 1 + name.length;
  7743.                             lookBack:
  7744.                             while(i >= 0) {
  7745.                                 switch(sList[i]) {
  7746.                                 case '\n':
  7747.                                 case '\r':
  7748.                                 case ' ':
  7749.                                     i--;
  7750.                                     this._col--;
  7751.                                     break;
  7752.                                 default:
  7753.                                     break lookBack;
  7754.                                 }
  7755.                             }
  7756.                             this._setError(Lng.seOpInReps[lang], sList[i]);
  7757.                             return null;
  7758.                         }
  7759.                         hasReps = true;
  7760.                     }
  7761.                     res = this._doRep(name, sList.substr(i));
  7762.                     if(!res) {
  7763.                         return null;
  7764.                     }
  7765.                     (name === 'rep' ? reps : outreps).push(res[1]);
  7766.                     i += res[0] - 1;
  7767.                     this._col += res[0] - 1;
  7768.                     lastType = this.TYPE_REPLACER;
  7769.                 } else {
  7770.                     if(lastType === this.TYPE_SPELL || lastType === this.TYPE_PARENTHESES) {
  7771.                         this._setError(Lng.seMissOp[lang], null);
  7772.                         return null;
  7773.                     }
  7774.                     res = this._doSpell(name, sList.substr(i), lastType === this.TYPE_NOT);
  7775.                     if(!res) {
  7776.                         return null;
  7777.                     }
  7778.                     i += res[0] - 1;
  7779.                     this._col += res[0] - 1;
  7780.                     spellsArr.push(res[1]);
  7781.                     lastType = this.TYPE_SPELL;
  7782.                 }
  7783.                 break;
  7784.             }
  7785.             case '(':
  7786.                 if(hasReps) {
  7787.                     this._setError(Lng.seUnexpChar[lang], '(');
  7788.                     return null;
  7789.                 }
  7790.                 if(lastType === this.TYPE_SPELL || lastType === this.TYPE_PARENTHESES) {
  7791.                     this._setError(Lng.seMissOp[lang], null);
  7792.                     return null;
  7793.                 }
  7794.                 res = this._generate(sList.substr(i + 1), true);
  7795.                 if(!res) {
  7796.                     return null;
  7797.                 }
  7798.                 i += res[0] + 1;
  7799.                 spellsArr.push([lastType === this.TYPE_NOT ? 0x1FF : 0xFF, res[1]]);
  7800.                 lastType = this.TYPE_PARENTHESES;
  7801.                 break;
  7802.             case '|':
  7803.             case '&':
  7804.                 if(hasReps) {
  7805.                     this._setError(Lng.seUnexpChar[lang], sList[i]);
  7806.                     return null;
  7807.                 }
  7808.                 if(lastType !== this.TYPE_SPELL && lastType !== this.TYPE_PARENTHESES) {
  7809.                     this._setError(Lng.seMissSpell[lang], null);
  7810.                     return null;
  7811.                 }
  7812.                 if(sList[i] === '&') {
  7813.                     spellsArr[spellsArr.length - 1][0] |= 0x200;
  7814.                 }
  7815.                 lastType = this.TYPE_ANDOR;
  7816.                 break;
  7817.             case '!':
  7818.                 if(hasReps) {
  7819.                     this._setError(Lng.seUnexpChar[lang], '!');
  7820.                     return null;
  7821.                 }
  7822.                 if(lastType !== this.TYPE_ANDOR && lastType !== this.TYPE_UNKNOWN) {
  7823.                     this._setError(Lng.seMissOp[lang], null);
  7824.                     return null;
  7825.                 }
  7826.                 lastType = this.TYPE_NOT;
  7827.                 break;
  7828.             case ')':
  7829.                 if(hasReps) {
  7830.                     this._setError(Lng.seUnexpChar[lang], ')');
  7831.                     return null;
  7832.                 }
  7833.                 if(lastType === this.TYPE_ANDOR || lastType === this.TYPE_NOT) {
  7834.                     this._setError(Lng.seMissSpell[lang], null);
  7835.                     return null;
  7836.                 }
  7837.                 if(inParens) {
  7838.                     return [i, spellsArr];
  7839.                 }
  7840.                 /* falls through */
  7841.             default:
  7842.                 this._setError(Lng.seUnexpChar[lang], sList[i]);
  7843.                 return null;
  7844.             }
  7845.         }
  7846.         if(inParens) {
  7847.             this._setError(Lng.seMissClBkt[lang], null);
  7848.             return null;
  7849.         }
  7850.         if(lastType !== this.TYPE_SPELL &&
  7851.             lastType !== this.TYPE_PARENTHESES &&
  7852.             lastType !== this.TYPE_REPLACER
  7853.         ) {
  7854.             this._setError(Lng.seMissSpell[lang], null);
  7855.             return null;
  7856.         }
  7857.         if(!reps.length) {
  7858.             reps = false;
  7859.         }
  7860.         if(!outreps.length) {
  7861.             outreps = false;
  7862.         }
  7863.         return [spellsArr, reps, outreps];
  7864.     }
  7865.     _getRegex(str, haveComma) {
  7866.         const m = str.match(/^\((\/.*?[^\\]\/[igm]*)(?:\)|\s*(,))/);
  7867.         if(!m || haveComma !== Boolean(m[2])) {
  7868.             return null;
  7869.         }
  7870.         const val = m[1];
  7871.         try {
  7872.             toRegExp(val, true);
  7873.         } catch(err) {
  7874.             this._setError(Lng.seErrRegex[lang], val);
  7875.             return null;
  7876.         }
  7877.         return [m[0].length, val];
  7878.     }
  7879.     _doRep(name, str) {
  7880.         let scope = SpellsCodegen._getScope(str);
  7881.         if(scope) {
  7882.             str = str.substring(scope[0]);
  7883.         } else {
  7884.             scope = [0, ['', '']];
  7885.         }
  7886.         const regex = this._getRegex(str, true);
  7887.         if(regex) {
  7888.             str = str.substring(regex[0]);
  7889.             if(str[0] === ')') {
  7890.                 return [regex[0] + scope[0] + 1, [scope[1][0], scope[1][1], regex[1], '']];
  7891.             }
  7892.             const val = SpellsCodegen._getText(str, false);
  7893.             if(val) {
  7894.                 return [val[0] + regex[0] + scope[0], [scope[1][0], scope[1][1], regex[1], val[1]]];
  7895.             }
  7896.         }
  7897.         this._setError(Lng.seSyntaxErr[lang], name);
  7898.         return null;
  7899.     }
  7900.     _doSpell(name, str, isNeg) {
  7901.         let m, val, scope = null, i = 0;
  7902.         const spellIdx = Spells.names.indexOf(name);
  7903.         if(spellIdx === -1) {
  7904.             this._setError(Lng.seUnknown[lang], name);
  7905.             return null;
  7906.         }
  7907.         let temp = SpellsCodegen._getScope(str);
  7908.         if(temp) {
  7909.             i += temp[0];
  7910.             str = str.substring(temp[0]);
  7911.             scope = temp[1];
  7912.         }
  7913.         const spellType = isNeg ? spellIdx | 0x100 : spellIdx;
  7914.         if(str[0] !== '(' || str[1] === ')') {
  7915.             if(Spells.needArg[spellIdx]) {
  7916.                 this._setError(Lng.seMissArg[lang], name);
  7917.                 return null;
  7918.             }
  7919.             return [str[0] === '(' ? i + 2 : i, [spellType, spellIdx === 14 ? 0x3F : '', scope]];
  7920.         }
  7921.         switch(spellIdx) {
  7922.         // #ihash
  7923.         case 4:
  7924.             m = str.match(/^\((\d+)\)/);
  7925.             if(!isNaN(+m[1])) {
  7926.                 return [i + m[0].length, [spellType, +m[1], scope]];
  7927.             }
  7928.             break;
  7929.         // #img
  7930.         case 8:
  7931.             m = str.match(/^\(([><=])(?:(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?)?(?:@(\d+)(?:-(\d+))?x(\d+)(?:-(\d+))?)?\)/);
  7932.             if(m && (m[2] || m[4])) {
  7933.                 return [i + m[0].length, [spellType, [
  7934.                     m[1] === '=' ? 0 : m[1] === '<' ? 1 : 2,
  7935.                     m[2] && [+m[2], m[3] ? +m[3] : +m[2]],
  7936.                     m[4] && [+m[4], m[5] ? +m[5] : +m[4], +m[6], m[7] ? +m[7] : +m[6]]
  7937.                 ], scope]];
  7938.             }
  7939.             break;
  7940.         // #wipe
  7941.         case 14:
  7942.             m = str.match(/^\(([a-z, ]+)\)/);
  7943.             if(m) {
  7944.                 let val = 0;
  7945.                 const arr = m[1].split(/, */);
  7946.                 for(let i = 0, len = arr.length; i < len; ++i) {
  7947.                     switch(arr[i]) {
  7948.                     case 'samelines': val |= 1; break;
  7949.                     case 'samewords': val |= 2; break;
  7950.                     case 'longwords': val |= 4; break;
  7951.                     case 'symbols': val |= 8; break;
  7952.                     case 'capslock': val |= 16; break;
  7953.                     case 'numbers': val |= 32; break;
  7954.                     case 'whitespace': val |= 64; break;
  7955.                     default: val = -1;
  7956.                     }
  7957.                 }
  7958.                 if(val !== -1) {
  7959.                     return [i + m[0].length, [spellType, val, scope]];
  7960.                 }
  7961.             }
  7962.             break;
  7963.         // #tlen, #num
  7964.         case 11:
  7965.         case 15:
  7966.             m = str.match(/^\(([\d-, ]+)\)/);
  7967.             if(m) {
  7968.                 m[1].split(/, */).forEach(function(v) {
  7969.                     if(v.includes('-')) {
  7970.                         const nums = v.split('-');
  7971.                         nums[0] = +nums[0];
  7972.                         nums[1] = +nums[1];
  7973.                         this[1].push(nums);
  7974.                     } else {
  7975.                         this[0].push(+v);
  7976.                     }
  7977.                 }, val = [[], []]);
  7978.                 return [i + m[0].length, [spellType, val, scope]];
  7979.             }
  7980.             break;
  7981.         // #exp, #exph, #imgn, #subj, #video
  7982.         case 1:
  7983.         case 2:
  7984.         case 3:
  7985.         case 5:
  7986.         case 13:
  7987.             temp = this._getRegex(str, false);
  7988.             if(temp) {
  7989.                 return [i + temp[0], [spellType, temp[1], scope]];
  7990.             }
  7991.             break;
  7992.         // #sage, #op, #all, #trip, #name, #words, #vauthor
  7993.         default:
  7994.             temp = SpellsCodegen._getText(str, true);
  7995.             if(temp) {
  7996.                 return [i + temp[0], [spellType, spellIdx === 0 ? temp[1].toLowerCase() : temp[1], scope]];
  7997.             }
  7998.         }
  7999.         this._setError(Lng.seSyntaxErr[lang], name);
  8000.         return null;
  8001.     }
  8002.     _setError(msg, arg) {
  8003.         this.hasError = true;
  8004.         this._errMsg = msg;
  8005.         this._errMsgArg = arg;
  8006.     }
  8007. }
  8008.  
  8009. class SpellsRunner {
  8010.     constructor() {
  8011.         this.hasNumSpell = false;
  8012.         this._endPromise = null;
  8013.         this._spells = Spells.hiders;
  8014.         if(!this._spells) {
  8015.             this.runSpells = SpellsRunner._unhidePost;
  8016.             SpellsRunner.cachedData = null;
  8017.         }
  8018.     }
  8019.     static unhideAll() {
  8020.         if(aib.t) {
  8021.             sesStorage['de-hidden-' + aib.b + aib.t] = null;
  8022.         }
  8023.         for(let post = Thread.first.op; post; post = post.next) {
  8024.             if(post.spellHidden) {
  8025.                 post.spellUnhide();
  8026.             }
  8027.         }
  8028.     }
  8029.     endSpells() {
  8030.         if(this._endPromise) {
  8031.             this._endPromise.then(() => this._savePostsHelper());
  8032.         } else {
  8033.             this._savePostsHelper();
  8034.         }
  8035.     }
  8036.     runSpells(post) {
  8037.         let res = (new SpellsInterpreter(post, this._spells)).runInterpreter();
  8038.         if(res instanceof Promise) {
  8039.             res = res.then(val => this._checkRes(post, val));
  8040.             this._endPromise = this._endPromise ? this._endPromise.then(() => res) : res;
  8041.             return 0;
  8042.         }
  8043.         return this._checkRes(post, res);
  8044.     }
  8045.  
  8046.     static _unhidePost(post) {
  8047.         if(post.spellHidden) {
  8048.             post.spellUnhide();
  8049.             if(SpellsRunner.cachedData && !post.isDeleted) {
  8050.                 SpellsRunner.cachedData[post.count] = [false, null];
  8051.             }
  8052.         }
  8053.         return 0;
  8054.     }
  8055.     _checkRes(post, [hasNumSpell, val, msg]) {
  8056.         this.hasNumSpell |= hasNumSpell;
  8057.         if(val) {
  8058.             post.spellHide(msg);
  8059.             if(SpellsRunner.cachedData && !post.isDeleted) {
  8060.                 SpellsRunner.cachedData[post.count] = [true, msg];
  8061.             }
  8062.             return 1;
  8063.         }
  8064.         return SpellsRunner._unhidePost(post);
  8065.     }
  8066.     _savePostsHelper() {
  8067.         if(this._spells) {
  8068.             if(aib.t) {
  8069.                 const lPost = Thread.first.lastNotDeleted;
  8070.                 let data = null;
  8071.                 if(Spells.hiders) {
  8072.                     if(SpellsRunner.cachedData) {
  8073.                         data = SpellsRunner.cachedData;
  8074.                     } else {
  8075.                         data = [];
  8076.                         for(let post = Thread.first.op; post; post = post.nextNotDeleted) {
  8077.                             data.push(post.spellHidden ? [true, Post.Note.text] : [false, null]);
  8078.                         }
  8079.                         SpellsRunner.cachedData = data;
  8080.                     }
  8081.                 }
  8082.                 sesStorage['de-hidden-' + aib.b + aib.t] = !data ? null : JSON.stringify({
  8083.                     hash      : Cfg.hideBySpell ? Spells.hash : 0,
  8084.                     lastCount : lPost.count,
  8085.                     lastNum   : lPost.num,
  8086.                     data
  8087.                 });
  8088.             }
  8089.             toggleWindow('hid', true);
  8090.         }
  8091.         ImagesHashStorage.endFn();
  8092.     }
  8093. }
  8094. SpellsRunner.cachedData = null;
  8095.  
  8096. class SpellsInterpreter {
  8097.     constructor(post, spells) {
  8098.         this.hasNumSpell = false;
  8099.         this._ctx = [spells.length, spells, 0, false];
  8100.         this._deep = 0;
  8101.         this._lastTSpells = [];
  8102.         this._post = post;
  8103.         this._triggeredSpellsStack = [this._lastTSpells];
  8104.         this._wipeMsg = null;
  8105.     }
  8106.     runInterpreter() {
  8107.         let rv, stopCheck;
  8108.         let isNegScope = this._ctx.pop();
  8109.         let i = this._ctx.pop();
  8110.         let scope = this._ctx.pop();
  8111.         let len = this._ctx.pop();
  8112.         while(true) {
  8113.             if(i < len) {
  8114.                 const type = scope[i][0] & 0xFF;
  8115.                 if(type === 0xFF) {
  8116.                     this._deep++;
  8117.                     this._ctx.push(len, scope, i, isNegScope);
  8118.                     isNegScope = !!(((scope[i][0] & 0x100) !== 0) ^ isNegScope);
  8119.                     scope = scope[i][1];
  8120.                     len = scope.length;
  8121.                     i = 0;
  8122.                     this._lastTSpells = [];
  8123.                     this._triggeredSpellsStack.push(this._lastTSpells);
  8124.                     continue;
  8125.                 }
  8126.                 const val = this._runSpell(type, scope[i][1]);
  8127.                 if(val instanceof Promise) {
  8128.                     this._ctx.push(len, scope, ++i, isNegScope);
  8129.                     return val.then(v => this._asyncContinue(v));
  8130.                 }
  8131.                 [rv, stopCheck] = this._checkRes(scope[i], val, isNegScope);
  8132.                 if(!stopCheck) {
  8133.                     i++;
  8134.                     continue;
  8135.                 }
  8136.             }
  8137.             if(this._deep !== 0) {
  8138.                 this._deep--;
  8139.                 isNegScope = this._ctx.pop();
  8140.                 i = this._ctx.pop();
  8141.                 scope = this._ctx.pop();
  8142.                 len = this._ctx.pop();
  8143.                 if(((scope[i][0] & 0x200) === 0) ^ rv) {
  8144.                     i++;
  8145.                     this._triggeredSpellsStack.pop();
  8146.                     this._lastTSpells = this._triggeredSpellsStack[this._triggeredSpellsStack.length - 1];
  8147.                     continue;
  8148.                 }
  8149.             }
  8150.             return [this.hasNumSpell, rv, rv ? this._getMsg() : null];
  8151.         }
  8152.     }
  8153.  
  8154.     static _tlenNum_helper(val, num) {
  8155.         for(let arr = val[0], i = arr.length - 1; i >= 0; --i) {
  8156.             if(arr[i] === num) {
  8157.                 return true;
  8158.             }
  8159.         }
  8160.         for(let arr = val[1], i = arr.length - 1; i >= 0; --i) {
  8161.             if(num >= arr[i][0] && num <= arr[i][1]) {
  8162.                 return true;
  8163.             }
  8164.         }
  8165.         return false;
  8166.     }
  8167.     _asyncContinue(val) {
  8168.         const cl = this._ctx.length;
  8169.         const spell = this._ctx[cl - 3][this._ctx[cl - 2] - 1];
  8170.         const [rv, stopCheck] = this._checkRes(spell, val, this._ctx[cl - 1]);
  8171.         return stopCheck ? [this.hasNumSpell, rv, rv ? this._getMsg() : null] : this.runInterpreter();
  8172.     }
  8173.     _checkRes(spell, val, isNegScope) {
  8174.         const flags = spell[0];
  8175.         const isAndSpell = ((flags & 0x200) !== 0) ^ isNegScope;
  8176.         const isNegSpell = ((flags & 0x100) !== 0) ^ isNegScope;
  8177.         if(isNegSpell ^ val) {
  8178.             this._lastTSpells.push([isNegSpell, spell, (spell[0] & 0xFF) === 14 ? this._wipeMsg : null]);
  8179.             return [true, !isAndSpell];
  8180.         }
  8181.         this._lastTSpells.length = 0;
  8182.         return [false, isAndSpell];
  8183.     }
  8184.     _getMsg() {
  8185.         const rv = [];
  8186.         for(const spellEls of this._triggeredSpellsStack) {
  8187.             for(const [isNeg, spell, wipeMsg] of spellEls) {
  8188.                 rv.push(Spells.decompileSpell(spell[0] & 0xFF, isNeg, spell[1], spell[2], wipeMsg));
  8189.             }
  8190.         }
  8191.         return rv.join(' & ');
  8192.     }
  8193.     _runSpell(spellId, val) {
  8194.         switch(spellId) {
  8195.         case 0: return this._words(val);
  8196.         case 1: return this._exp(val);
  8197.         case 2: return this._exph(val);
  8198.         case 3: return this._imgn(val);
  8199.         case 4: return this._ihash(val);
  8200.         case 5: return this._subj(val);
  8201.         case 6: return this._name(val);
  8202.         case 7: return this._trip(val);
  8203.         case 8: return this._img(val);
  8204.         case 9: return this._sage(val);
  8205.         case 10: return this._op(val);
  8206.         case 11: return this._tlen(val);
  8207.         case 12: return this._all(val);
  8208.         case 13: return this._video(val);
  8209.         case 14: return this._wipe(val);
  8210.         case 15:
  8211.             this.hasNumSpell = true;
  8212.             return this._num(val);
  8213.         case 16: return this._vauthor(val);
  8214.         }
  8215.     }
  8216.  
  8217.     _all() {
  8218.         return true;
  8219.     }
  8220.     _exp(val) {
  8221.         return val.test(this._post.text);
  8222.     }
  8223.     _exph(val) {
  8224.         return val.test(this._post.html);
  8225.     }
  8226.     async _ihash(val) {
  8227.         for(const image of this._post.images) {
  8228.             if((image instanceof AttachedImage) && await ImagesHashStorage.getHash(image) === val) {
  8229.                 return true;
  8230.             }
  8231.         }
  8232.         return false;
  8233.     }
  8234.     _img(val) {
  8235.         const { images } = this._post;
  8236.         const [compareRule, weightVals, sizeVals] = val;
  8237.         if(!val) {
  8238.             return images.hasAttachments;
  8239.         }
  8240.         for(const image of images) {
  8241.             if(!(image instanceof AttachedImage)) {
  8242.                 continue;
  8243.             }
  8244.             if(weightVals) {
  8245.                 const w = image.weight;
  8246.                 let isHide;
  8247.                 switch(compareRule) {
  8248.                 case 0: isHide = w >= weightVals[0] && w <= weightVals[1]; break;
  8249.                 case 1: isHide = w < weightVals[0]; break;
  8250.                 case 2: isHide = w > weightVals[0]; break;
  8251.                 }
  8252.                 if(!isHide) {
  8253.                     continue;
  8254.                 } else if(!sizeVals) {
  8255.                     return true;
  8256.                 }
  8257.             }
  8258.             if(sizeVals) {
  8259.                 const { height: h, width: w } = image;
  8260.                 switch(compareRule) {
  8261.                 case 0:
  8262.                     if(w >= sizeVals[0] && w <= sizeVals[1] && h >= sizeVals[2] && h <= sizeVals[3]) {
  8263.                         return true;
  8264.                     }
  8265.                     break;
  8266.                 case 1:
  8267.                     if(w < sizeVals[0] && h < sizeVals[3]) {
  8268.                         return true;
  8269.                     }
  8270.                     break;
  8271.                 case 2:
  8272.                     if(w > sizeVals[0] && h > sizeVals[3]) {
  8273.                         return true;
  8274.                     }
  8275.                 }
  8276.             }
  8277.         }
  8278.         return false;
  8279.     }
  8280.     _imgn(val) {
  8281.         for(const image of this._post.images) {
  8282.             if((image instanceof AttachedImage) && val.test(image.name)) {
  8283.                 return true;
  8284.             }
  8285.         }
  8286.         return false;
  8287.     }
  8288.     _name(val) {
  8289.         const pName = this._post.posterName;
  8290.         return pName ? !val || pName.includes(val) : false;
  8291.     }
  8292.     _num(val) {
  8293.         return SpellsInterpreter._tlenNum_helper(val, this._post.count + 1);
  8294.     }
  8295.     _op() {
  8296.         return this._post.isOp;
  8297.     }
  8298.     _sage() {
  8299.         return this._post.sage;
  8300.     }
  8301.     _subj(val) {
  8302.         const pSubj = this._post.subj;
  8303.         return pSubj ? !val || val.test(pSubj) : false;
  8304.     }
  8305.     _tlen(val) {
  8306.         const text = this._post.text.replace(/\s+(?=\s)|\n/g, '');
  8307.         return !val ? !!text : SpellsInterpreter._tlenNum_helper(val, text.length);
  8308.     }
  8309.     _trip(val) {
  8310.         const pTrip = this._post.posterTrip;
  8311.         return pTrip ? !val || pTrip.includes(val) : false;
  8312.     }
  8313.     _vauthor(val) {
  8314.         return this._videoVauthor(val, true);
  8315.     }
  8316.     _video(val) {
  8317.         return this._videoVauthor(val, false);
  8318.     }
  8319.     _videoVauthor(val, isAuthorSpell) {
  8320.         const { videos } = this._post;
  8321.         if(!val) {
  8322.             return !!videos.hasLinks;
  8323.         }
  8324.         if(!videos.hasLinks || !Cfg.YTubeTitles) {
  8325.             return false;
  8326.         }
  8327.         for(const siteData of videos.vData) {
  8328.             for(const data of siteData) {
  8329.                 if(isAuthorSpell ? val === data[1] : val.test(data[0])) {
  8330.                     return true;
  8331.                 }
  8332.             }
  8333.         }
  8334.         if(videos.linksCount === videos.loadedLinksCount) {
  8335.             return false;
  8336.         }
  8337.         return new Promise(resolve => (videos.titleLoadFn = data => {
  8338.             if(isAuthorSpell ? val === data[1] : val.test(data[0])) {
  8339.                 resolve(true);
  8340.             } else if(videos.linksCount === videos.loadedLinksCount) {
  8341.                 resolve(false);
  8342.             } else {
  8343.                 return;
  8344.             }
  8345.             videos.titleLoadFn = null;
  8346.         }));
  8347.     }
  8348.     _wipe(val) {
  8349.         let arr, len, x;
  8350.         const txt = this._post.text;
  8351.         // (1 << 0): samelines
  8352.         if(val & 1) {
  8353.             arr = txt.replace(/>/g, '').split(/\s*\n\s*/);
  8354.             if((len = arr.length) > 5) {
  8355.                 arr.sort();
  8356.                 for(let i = 0, n = len / 4; i < len;) {
  8357.                     x = arr[i];
  8358.                     let j = 0;
  8359.                     while(arr[i++] === x) {
  8360.                         j++;
  8361.                     }
  8362.                     if(j > 4 && j > n && x) {
  8363.                         this._wipeMsg = [1, `"${ x.substr(0, 20) }" x${ j + 1 }`];
  8364.                         return true;
  8365.                     }
  8366.                 }
  8367.             }
  8368.         }
  8369.         // (1 << 1): samewords
  8370.         if(val & 2) {
  8371.             arr = txt.replace(/[\s.?!,>]+/g, ' ').toUpperCase().split(' ');
  8372.             if((len = arr.length) > 3) {
  8373.                 arr.sort();
  8374.                 let keys = 0;
  8375.                 let pop = 0;
  8376.                 for(let i = 0, n = len / 4; i < len; keys++) {
  8377.                     x = arr[i];
  8378.                     let j = 0;
  8379.                     while(arr[i++] === x) {
  8380.                         j++;
  8381.                     }
  8382.                     if(len > 25) {
  8383.                         if(j > pop && x.length > 2) {
  8384.                             pop = j;
  8385.                         }
  8386.                         if(pop >= n) {
  8387.                             this._wipeMsg = [2, `same "${ x.substr(0, 20) }" x${ pop + 1 }`];
  8388.                             return true;
  8389.                         }
  8390.                     }
  8391.                 }
  8392.                 x = keys / len;
  8393.                 if(x < 0.25) {
  8394.                     this._wipeMsg = [2, `uniq ${ (x * 100).toFixed(0) }%`];
  8395.                     return true;
  8396.                 }
  8397.             }
  8398.         }
  8399.         // (1 << 2): longwords
  8400.         if(val & 4) {
  8401.             arr = txt.replace(/https*:\/\/.*?(\s|$)/g, '').replace(/[\s.?!,>:;-]+/g, ' ').split(' ');
  8402.             if(arr[0].length > 50 || ((len = arr.length) > 1 && arr.join('').length / len > 10)) {
  8403.                 this._wipeMsg = [4, null];
  8404.                 return true;
  8405.             }
  8406.         }
  8407.         // (1 << 3): symbols
  8408.         if(val & 8) {
  8409.             const _txt = txt.replace(/\s+/g, '');
  8410.             if((len = _txt.length) > 30 && (x = _txt.replace(/[0-9a-zа-я.?!,]/ig, '').length / len) > 0.4) {
  8411.                 this._wipeMsg = [8, `${ (x * 100).toFixed(0) }%`];
  8412.                 return true;
  8413.             }
  8414.         }
  8415.         // (1 << 4): capslock
  8416.         if(val & 16) {
  8417.             arr = txt.replace(/[\s.?!;,-]+/g, ' ').trim().split(' ');
  8418.             if((len = arr.length) > 4) {
  8419.                 let n = 0;
  8420.                 let capsw = 0;
  8421.                 let casew = 0;
  8422.                 for(let i = 0; i < len; ++i) {
  8423.                     x = arr[i];
  8424.                     if((x.match(/[a-zа-я]/ig) || []).length < 5) {
  8425.                         continue;
  8426.                     }
  8427.                     if((x.match(/[A-ZА-Я]/g) || []).length > 2) {
  8428.                         casew++;
  8429.                     }
  8430.                     if(x === x.toUpperCase()) {
  8431.                         capsw++;
  8432.                     }
  8433.                     n++;
  8434.                 }
  8435.                 if(capsw / n >= 0.3 && n > 4) {
  8436.                     this._wipeMsg = [16, `CAPS ${ capsw / arr.length * 100 }%`];
  8437.                     return true;
  8438.                 } else if(casew / n >= 0.3 && n > 8) {
  8439.                     this._wipeMsg = [16, `cAsE ${ casew / arr.length * 100 }%`];
  8440.                     return true;
  8441.                 }
  8442.             }
  8443.         }
  8444.         // (1 << 5): numbers
  8445.         if(val & 32) {
  8446.             const _txt = txt.replace(/\s+/g, ' ').replace(/>>\d+|https*:\/\/.*?(?: |$)/g, '');
  8447.             if((len = _txt.length) > 30 && (x = (len - _txt.replace(/\d/g, '').length) / len) > 0.4) {
  8448.                 this._wipeMsg = [32, `${ Math.round(x * 100) }%`];
  8449.                 return true;
  8450.             }
  8451.         }
  8452.         // (1 << 5): whitespace
  8453.         if(val & 64) {
  8454.             if(/(?:\n\s*){10}/i.test(txt)) {
  8455.                 this._wipeMsg = [64, null];
  8456.                 return true;
  8457.             }
  8458.         }
  8459.         return false;
  8460.     }
  8461.     _words(val) {
  8462.         return this._post.text.toLowerCase().includes(val) || this._post.subj.toLowerCase().includes(val);
  8463.     }
  8464. }
  8465.  
  8466. /* ==[ Form.js ]==============================================================================================
  8467.                                                   POSTFORM
  8468.                 postform improving, quick reply window, markup text panel, sage button, etc
  8469. =========================================================================================================== */
  8470.  
  8471. class PostForm {
  8472.     constructor(form, oeForm = null, ignoreForm = false) {
  8473.         this.isBottom = false;
  8474.         this.isHidden = false;
  8475.         this.isQuick = false;
  8476.         this.lastQuickPNum = -1;
  8477.         this.pArea = [];
  8478.         this.pForm = null;
  8479.         this.qArea = null;
  8480.         this._pBtn = [];
  8481.         const qOeForm = 'form[name="oeform"], form[action*="paint"]';
  8482.         this.oeForm = oeForm || $q(qOeForm);
  8483.         if(!ignoreForm && !form) {
  8484.             if(this.oeForm) {
  8485.                 ajaxLoad(aib.getThrUrl(aib.b, Thread.first.num), false).then(loadedDoc => {
  8486.                     const form = $q(aib.qForm, loadedDoc);
  8487.                     const oeForm = $q(qOeForm, loadedDoc);
  8488.                     pr = new PostForm(form && doc.adoptNode(form), oeForm && doc.adoptNode(oeForm), true);
  8489.                 }, () => (pr = new PostForm(null, null, true)));
  8490.             } else {
  8491.                 this.form = null;
  8492.             }
  8493.             return;
  8494.         }
  8495.         this.tNum = aib.t;
  8496.         this.form = form;
  8497.         this.files = null;
  8498.         this.txta = $q(aib.qFormTxta, form);
  8499.         this.subm = $q(aib.qFormSubm, form);
  8500.         this.name = $q(aib.qFormName, form);
  8501.         this.mail = $q(aib.qFormMail, form);
  8502.         this.subj = $q(aib.qFormSubj, form);
  8503.         this.passw = $q(aib.qFormPassw, form);
  8504.         this.rules = $q(aib.qFormRules, form);
  8505.         this.video = $q('tr input[name="video"], tr input[name="embed"]', form);
  8506.         this._initFileInputs();
  8507.         this._makeHideableContainer();
  8508.         this._makeWindow();
  8509.         if(!form || !this.txta) {
  8510.             return;
  8511.         }
  8512.         form.style.display = 'inline-block';
  8513.         form.style.textAlign = 'left';
  8514.         const { qArea, txta } = this;
  8515.         new WinResizer('reply', 'top', 'textaHeight', qArea, txta);
  8516.         new WinResizer('reply', 'left', 'textaWidth', qArea, txta);
  8517.         new WinResizer('reply', 'right', 'textaWidth', qArea, txta);
  8518.         new WinResizer('reply', 'bottom', 'textaHeight', qArea, txta);
  8519.         this._initTextarea();
  8520.         this.addMarkupPanel();
  8521.         this.setPlaceholders();
  8522.         this.updateLanguage();
  8523.         this._initCaptcha();
  8524.         this._initSubmit();
  8525.         if(Cfg.ajaxPosting) {
  8526.             this._initAjaxPosting();
  8527.         }
  8528.         if(Cfg.addSageBtn && this.mail) {
  8529.             PostForm.hideField($parent(this.mail, 'LABEL') || this.mail);
  8530.             setTimeout(() => this.toggleSage(), 0);
  8531.         }
  8532.         if(Cfg.noPassword && this.passw) {
  8533.             $hide($qParent(this.passw, aib.qFormTr));
  8534.         }
  8535.         if(Cfg.noName && this.name) {
  8536.             PostForm.hideField(this.name);
  8537.         }
  8538.         if(Cfg.noSubj && this.subj) {
  8539.             PostForm.hideField(this.subj);
  8540.         }
  8541.         if(Cfg.userName && this.name) {
  8542.             setTimeout(PostForm.setUserName, 0);
  8543.         }
  8544.         if(this.passw) {
  8545.             setTimeout(PostForm.setUserPassw, 0);
  8546.         }
  8547.     }
  8548.     static hideField(el) {
  8549.         const next = el.nextElementSibling;
  8550.         $toggle(next && (next.style.display !== 'none') ||
  8551.             el.previousElementSibling ? el : $qParent(el, aib.qFormTr));
  8552.     }
  8553.     static setUserName() {
  8554.         const el = $q('input[info="nameValue"]');
  8555.         if(el) {
  8556.             saveCfg('nameValue', el.value);
  8557.         }
  8558.         pr.name.value = Cfg.userName ? Cfg.nameValue : '';
  8559.     }
  8560.     static setUserPassw() {
  8561.         if(!Cfg.userPassw) {
  8562.             return;
  8563.         }
  8564.         const el = $q('input[info="passwValue"]');
  8565.         if(el) {
  8566.             saveCfg('passwValue', el.value);
  8567.         }
  8568.         const value = pr.passw.value = Cfg.passwValue;
  8569.         for(const { passEl } of DelForm) {
  8570.             if(passEl) {
  8571.                 passEl.value = value;
  8572.             }
  8573.         }
  8574.     }
  8575.     get isVisible() {
  8576.         if(!this.isHidden && this.isBottom && $q(':focus', this.pForm)) {
  8577.             const cr = this.pForm.getBoundingClientRect();
  8578.             return cr.bottom > 0 && cr.top < nav.viewportHeight();
  8579.         }
  8580.         return false;
  8581.     }
  8582.     get sageBtn() {
  8583.         const value = $aEnd(this.subm, '<span id="de-sagebtn"><svg class="de-btn-sage">' +
  8584.             '<use xlink:href="#de-symbol-post-sage"/></svg></span>');
  8585.         value.onclick = () => {
  8586.             toggleCfg('sageReply');
  8587.             this.toggleSage();
  8588.         };
  8589.         Object.defineProperty(this, 'sageBtn', { value });
  8590.         return value;
  8591.     }
  8592.     get top() {
  8593.         return this.pForm.getBoundingClientRect().top;
  8594.     }
  8595.     addMarkupPanel() {
  8596.         let el = $id('de-txt-panel');
  8597.         if(!Cfg.addTextBtns) {
  8598.             $del(el);
  8599.             return;
  8600.         }
  8601.         if(!el) {
  8602.             el = $add('<span id="de-txt-panel"></span>');
  8603.             el.addEventListener('click', this);
  8604.             el.addEventListener('mouseover', this);
  8605.         }
  8606.         el.style.cssFloat = Cfg.txtBtnsLoc ? 'none' : 'right';
  8607.         $after(Cfg.txtBtnsLoc ? $id('de-resizer-text') || this.txta : this.subm, el);
  8608.         const id = ['bold', 'italic', 'under', 'strike', 'spoil', 'code', 'sup', 'sub'];
  8609.         const val = ['B', 'i', 'U', 'S', '%', 'C', 'x\u00b2', 'x\u2082'];
  8610.         const mode = Cfg.addTextBtns;
  8611.         let html = '';
  8612.         for(let i = 0, len = aib.markupTags.length; i < len; ++i) {
  8613.             const tag = aib.markupTags[i];
  8614.             if(tag) {
  8615.                 html += `<div id="de-btn-${ id[i] }" de-title="${ Lng.txtBtn[i][lang] }" de-tag="${ tag }">${
  8616.                     mode === 2 ? `${ !html ? '[' : '' }&nbsp;<a class="de-abtn" href="#">${ val[i] }</a> /` :
  8617.                     mode === 3 ? `<button type="button" style="font-weight: bold;">${ val[i] }</button>` :
  8618.                     `<svg><use xlink:href="#de-symbol-markup-${ id[i] }"/></svg>`
  8619.                 }</div>`;
  8620.             }
  8621.         }
  8622.         el.innerHTML = `${ html }<div id="de-btn-quote" de-title="${ Lng.txtBtn[8][lang] }" de-tag="q">${
  8623.             mode === 2 ? '&nbsp;<a class="de-abtn" href="#">&gt;</a> ]' :
  8624.             mode === 3 ? '<button type="button" style="font-weight: bold;">&gt;</button>' :
  8625.             '<svg><use xlink:href="#de-symbol-markup-quote"/></svg>'
  8626.         }</span>`;
  8627.     }
  8628.     clearForm() {
  8629.         if(this.txta) {
  8630.             this.txta.value = '';
  8631.         }
  8632.         if(this.files) {
  8633.             this.files.clearInputs();
  8634.         }
  8635.         if(this.video) {
  8636.             this.video.value = '';
  8637.         }
  8638.     }
  8639.     closeReply() {
  8640.         if(this.isQuick) {
  8641.             this.isQuick = false;
  8642.             this.lastQuickPNum = -1;
  8643.             if(!aib.t) {
  8644.                 this._toggleQuickReply(false);
  8645.             }
  8646.             this.setReply(false, !aib.t || Cfg.addPostForm > 1);
  8647.         }
  8648.     }
  8649.     handleEvent(e) {
  8650.         let el = e.target;
  8651.         if(el.tagName !== 'DIV') {
  8652.             el = el.parentNode;
  8653.         }
  8654.         const { id } = el;
  8655.         if(!id.startsWith('de-btn')) {
  8656.             return;
  8657.         }
  8658.         if(e.type === 'mouseover') {
  8659.             if(id === 'de-btn-quote') {
  8660.                 quotetxt = deWindow.getSelection().toString();
  8661.             }
  8662.             let key = -1;
  8663.             if(HotKeys.enabled) {
  8664.                 switch(id.substr(7)) {
  8665.                 case 'bold': key = 12; break;
  8666.                 case 'italic': key = 13; break;
  8667.                 case 'strike': key = 14; break;
  8668.                 case 'spoil': key = 15; break;
  8669.                 case 'code': key = 16;
  8670.                 }
  8671.             }
  8672.             KeyEditListener.setTitle(el, key);
  8673.             return;
  8674.         }
  8675.         const txtaEl = pr.txta;
  8676.         const { selectionStart: start, selectionEnd: end } = txtaEl;
  8677.         const quote = Cfg.spacedQuote ? '> ' : '>';
  8678.         if(id === 'de-btn-quote') {
  8679.             insertText(txtaEl, quote + (start === end ? quotetxt : txtaEl.value.substring(start, end))
  8680.                 .replace(/\n/gm, '\n' + quote));
  8681.             quotetxt = '';
  8682.         } else {
  8683.             const { scrtop } = txtaEl;
  8684.             const val = PostForm._wrapText(el.getAttribute('de-tag'), txtaEl.value.substring(start, end));
  8685.             const len = start + val[0];
  8686.             txtaEl.value = txtaEl.value.substr(0, start) + val[1] + txtaEl.value.substr(end);
  8687.             txtaEl.setSelectionRange(len, len);
  8688.             txtaEl.focus();
  8689.             txtaEl.scrollTop = scrtop;
  8690.         }
  8691.         $pd(e);
  8692.         e.stopPropagation();
  8693.     }
  8694.     refreshCap(isErr = false) {
  8695.         if(this.cap) {
  8696.             this.cap.refreshCaptcha(isErr, isErr, this.tNum);
  8697.         }
  8698.     }
  8699.     setPlaceholders() {
  8700.         if(aib.kusaba || !aib.multiFile && Cfg.fileInputs === 2) {
  8701.             return;
  8702.         }
  8703.         this._setPlaceholder('name');
  8704.         this._setPlaceholder('subj');
  8705.         this._setPlaceholder('mail');
  8706.         this._setPlaceholder('video');
  8707.         if(this.cap) {
  8708.             this._setPlaceholder('cap');
  8709.         }
  8710.     }
  8711.     setReply(isQuick, needToHide) {
  8712.         if(isQuick) {
  8713.             $after(this.qArea.firstChild, this.pForm);
  8714.         } else {
  8715.             $after(this.pArea[+this.isBottom], this.qArea);
  8716.             $after(this._pBtn[+this.isBottom], this.pForm);
  8717.         }
  8718.         this.isHidden = needToHide;
  8719.         $toggle(this.qArea, isQuick);
  8720.         $toggle(this.pForm, !needToHide);
  8721.         this.updatePAreaBtns();
  8722.     }
  8723.     showMainReply(isBottom, e) {
  8724.         this.closeReply();
  8725.         if(!aib.t) {
  8726.             this.tNum = false;
  8727.             this.refreshCap();
  8728.         }
  8729.         if(this.isBottom === isBottom) {
  8730.             $toggle(this.pForm, this.isHidden);
  8731.             this.isHidden = !this.isHidden;
  8732.             this.updatePAreaBtns();
  8733.         } else {
  8734.             this.isBottom = isBottom;
  8735.             this.setReply(false, false);
  8736.         }
  8737.         if(e) {
  8738.             $pd(e);
  8739.         }
  8740.     }
  8741.     showQuickReply(post, pNum, isCloseReply, isNumClick, isNoLink = false) {
  8742.         if(!this.isQuick) {
  8743.             this.isQuick = true;
  8744.             this.setReply(true, false);
  8745.             $q('a', this._pBtn[+this.isBottom]).className =
  8746.                 `de-abtn de-parea-btn-${ aib.t ? 'reply' : 'thr' }`;
  8747.         } else if(isCloseReply && !quotetxt && post.wrap.nextElementSibling === this.qArea) {
  8748.             this.closeReply();
  8749.             return;
  8750.         }
  8751.         $after(post.wrap, this.qArea);
  8752.         if(this.qArea.classList.contains('de-win')) {
  8753.             updateWinZ(this.qArea.style);
  8754.         }
  8755.         const qNum = post.thr.num;
  8756.         if(!aib.t) {
  8757.             this._toggleQuickReply(qNum);
  8758.         }
  8759.         if(!this.form) {
  8760.             return;
  8761.         }
  8762.         if(!aib.t && this.tNum !== qNum) {
  8763.             this.tNum = qNum;
  8764.             this.refreshCap();
  8765.         }
  8766.         this.tNum = qNum;
  8767.         const txt = this.txta.value;
  8768.         const isOnNewLine = txt === '' || txt.slice(-1) === '\n';
  8769.         const link = isNoLink || post.isOp && !Cfg.addOPLink && !aib.t && !isNumClick ? '' :
  8770.             isNumClick ? `>>${ pNum }${ isOnNewLine ? '\n' : '' }` :
  8771.             (isOnNewLine ? '' : '\n') +
  8772.                 (this.lastQuickPNum === pNum && txt.includes('>>' + pNum) ? '' : `>>${ pNum }\n`);
  8773.         const quote = !quotetxt ? '' : `${ quotetxt.replace(/^\n|\n$/g, '')
  8774.             .replace(/(^|\n)(.)/gm, `$1>${ Cfg.spacedQuote ? ' ' : '' }$2`) }\n`;
  8775.         insertText(this.txta, link + quote);
  8776.         const winTitle = post.thr.op.title.trim();
  8777.         $q('.de-win-title', this.qArea).textContent =
  8778.             (winTitle.length < 28 ? winTitle : `${ winTitle.substr(0, 30) }\u2026`) || `#${ pNum }`;
  8779.         this.lastQuickPNum = pNum;
  8780.     }
  8781.     toggleSage() {
  8782.         if(!Cfg.addSageBtn || !this.mail) {
  8783.             return;
  8784.         }
  8785.         const isSage = Cfg.sageReply;
  8786.         this.sageBtn.style.opacity = isSage ? '1' : '.3';
  8787.         this.sageBtn.title = isSage ? 'SAGE!' : Lng.noSage[lang];
  8788.         if(this.mail.type === 'text') {
  8789.             this.mail.value = isSage ? 'sage' : aib._4chan ? 'noko' : '';
  8790.         } else {
  8791.             this.mail.checked = isSage;
  8792.         }
  8793.     }
  8794.     updateLanguage() {
  8795.         this.txta.title = Lng.pasteImage[lang];
  8796.         aib.updateSubmitBtn(this.subm);
  8797.     }
  8798.     updatePAreaBtns() {
  8799.         const txt = 'de-abtn de-parea-btn-';
  8800.         const rep = aib.t ? 'reply' : 'thr';
  8801.         $q('a', this._pBtn[+this.isBottom]).className = txt + (!this.pForm.style.display ? 'close' : rep);
  8802.         $q('a', this._pBtn[+!this.isBottom]).className = txt + rep;
  8803.     }
  8804.  
  8805.     static _wrapText(tag, text) {
  8806.         let isBB = aib.markupBB;
  8807.         if(tag.startsWith('[')) {
  8808.             tag = tag.substr(1);
  8809.             isBB = true;
  8810.         }
  8811.         if(isBB) {
  8812.             if(text.includes('\n')) {
  8813.                 const str = `[${ tag }]${ text }[/${ tag }]`;
  8814.                 return [str.length, str];
  8815.             }
  8816.             const m = text.match(/^(\s*)(.*?)(\s*)$/);
  8817.             const str = `${ m[1] }[${ tag }]${ m[2] }[/${ tag }]${ m[3] }`;
  8818.             return [!m[2].length ? m[1].length + tag.length + 2 : str.length, str];
  8819.         }
  8820.         let m, rv = '', i = 0;
  8821.         const arr = text.split('\n');
  8822.         for(let len = arr.length; i < len; ++i) {
  8823.             m = arr[i].match(/^(\s*)(.*?)(\s*)$/);
  8824.             rv += '\n' + m[1] + (tag === '^H' ? m[2] + '^H'.repeat(m[2].length) : tag + m[2] + tag) + m[3];
  8825.         }
  8826.         return [i === 1 && !m[2].length && tag !== '^H' ?
  8827.             m[1].length + tag.length :
  8828.             rv.length - 1, rv.slice(1)];
  8829.     }
  8830.     _initAjaxPosting() {
  8831.         let el;
  8832.         if(aib.qFormRedir && (el = $q(aib.qFormRedir, this.form))) {
  8833.             aib.disableRedirection(el);
  8834.         }
  8835.         this.form.onsubmit = e => {
  8836.             $pd(e);
  8837.             $popup('upload', Lng.sending[lang], true);
  8838.             html5Submit(this.form, this.subm, true).then(checkUpload)
  8839.                 .catch(err => $popup('upload', getErrorMessage(err)));
  8840.         };
  8841.     }
  8842.     _initCaptcha() {
  8843.         const capEl =
  8844.             $q('input[type="text"][name*="aptcha"], *[id*="captcha"], *[class*="captcha"]', this.form);
  8845.         if(!capEl) {
  8846.             this.cap = null;
  8847.             return;
  8848.         }
  8849.         this.cap = new Captcha(capEl, this.tNum);
  8850.         const updCapFn = () => {
  8851.             this.cap.addCaptcha();
  8852.             this.cap.updateOutdated();
  8853.         };
  8854.         this.txta.addEventListener('focus', updCapFn);
  8855.         if(this.files) {
  8856.             this.files.onchange = updCapFn;
  8857.         }
  8858.         this.form.addEventListener('click', () => this.cap.addCaptcha(), true);
  8859.     }
  8860.     _initFileInputs() {
  8861.         const fileEl = $q(aib.qFormFile, this.form);
  8862.         if(!fileEl) {
  8863.             return;
  8864.         }
  8865.         if(aib.fixFileInputs) {
  8866.             aib.fixFileInputs($qParent(fileEl, aib.qFormTd));
  8867.         }
  8868.         this.files = new Files(this, $q(aib.qFormFile, this.form));
  8869.         // We need to clear file inputs in case if session was restored.
  8870.         deWindow.addEventListener('load',
  8871.             () => setTimeout(() => !this.files.filesCount && this.files.clearInputs(), 0));
  8872.     }
  8873.     _initSubmit() {
  8874.         this.subm.addEventListener('click', e => {
  8875.             if(aib.makaba && !aib._2channel && !Cfg.altCaptcha) {
  8876.                 if(!this.cap.isSubmitWait) {
  8877.                     $pd(e);
  8878.                     $popup('upload', 'reCaptcha...', true);
  8879.                     this.cap.isSubmitWait = true;
  8880.                     this.refreshCap();
  8881.                     return;
  8882.                 }
  8883.                 this.cap.isSubmitWait = false;
  8884.             }
  8885.             if(Cfg.warnSubjTrip && this.subj && /#.|##./.test(this.subj.value)) {
  8886.                 $pd(e);
  8887.                 $popup('upload', Lng.subjHasTrip[lang]);
  8888.                 return;
  8889.             }
  8890.             let val = this.txta.value;
  8891.             if(Spells.outreps) {
  8892.                 val = Spells.outReplace(val);
  8893.             }
  8894.             if(this.tNum && pByNum.get(this.tNum).subj === 'Dollchan Extension Tools') {
  8895.                 const temp = `\n\n${ PostForm._wrapText(aib.markupTags[5],
  8896.                     `${ '-'.repeat(50) }\n${ nav.ua }\nv${ version }.${ commit }${
  8897.                         nav.isESNext ? '.es6' : '' } [${ nav.scriptHandler }]`
  8898.                 )[1] }`;
  8899.                 if(!val.includes(temp)) {
  8900.                     val += temp;
  8901.                 }
  8902.             }
  8903.             this.txta.value = val;
  8904.             this.toggleSage();
  8905.             if(Cfg.ajaxPosting) {
  8906.                 $popup('upload', Lng.checking[lang], true);
  8907.             }
  8908.             if(this.video && (val = this.video.value) && (val = val.match(Videos.ytReg))) {
  8909.                 this.video.value = 'http://www.youtube.com/watch?v=' + val[1];
  8910.             }
  8911.             if(this.isQuick) {
  8912.                 $hide(this.pForm);
  8913.                 $hide(this.qArea);
  8914.                 $after(this._pBtn[+this.isBottom], this.pForm);
  8915.             }
  8916.             updater.pauseUpdater();
  8917.         });
  8918.     }
  8919.     _initTextarea() {
  8920.         const el = this.txta;
  8921.         if(aib.dobrochan) {
  8922.             el.removeAttribute('id');
  8923.         }
  8924.         el.classList.add('de-textarea');
  8925.         const { style } = el;
  8926.         style.setProperty('width', Cfg.textaWidth + 'px', 'important');
  8927.         style.setProperty('height', Cfg.textaHeight + 'px', 'important');
  8928.         // Allow to scroll page on PgUp/PgDn
  8929.         el.addEventListener('keypress', e => {
  8930.             const code = e.charCode || e.keyCode;
  8931.             if((code === 33 /* PgUp */ || code === 34 /* PgDn */) && e.which === 0) {
  8932.                 e.target.blur();
  8933.                 deWindow.focus();
  8934.             }
  8935.         });
  8936.         // Add image from clipboard to file inputs on Ctrl+V
  8937.         el.addEventListener('paste', e => {
  8938.             if('clipboardData' in e) {
  8939.                 for(const item of e.clipboardData.items) {
  8940.                     if(item.kind === 'file') {
  8941.                         const inputs = this.files._inputs;
  8942.                         for(let i = 0, len = inputs.length; i < len; ++i) {
  8943.                             const input = inputs[i];
  8944.                             if(!input.hasFile) {
  8945.                                 const file = item.getAsFile();
  8946.                                 input._addUrlFile(URL.createObjectURL(file), file);
  8947.                                 break;
  8948.                             }
  8949.                         }
  8950.                     }
  8951.                 }
  8952.             }
  8953.         });
  8954.         // Make textarea resizer
  8955.         if(nav.isFirefox || nav.isWebkit) {
  8956.             el.addEventListener('mouseup', ({ target }) => {
  8957.                 const s = target.style;
  8958.                 const { width, height } = s;
  8959.                 s.setProperty('width', width + 'px', 'important');
  8960.                 s.setProperty('height', height + 'px', 'important');
  8961.                 saveCfg('textaWidth', parseInt(width, 10));
  8962.                 saveCfg('textaHeight', parseInt(height, 10));
  8963.             });
  8964.             return;
  8965.         }
  8966.         $aEnd(el, '<div id="de-resizer-text"></div>').addEventListener('mousedown', {
  8967.             _el      : el,
  8968.             _elStyle : style,
  8969.             handleEvent(e) {
  8970.                 switch(e.type) {
  8971.                 case 'mousedown':
  8972.                     docBody.addEventListener('mousemove', this);
  8973.                     docBody.addEventListener('mouseup', this);
  8974.                     $pd(e);
  8975.                     return;
  8976.                 case 'mousemove': {
  8977.                     const cr = this._el.getBoundingClientRect();
  8978.                     this._elStyle.setProperty('width', (e.clientX - cr.left) + 'px', 'important');
  8979.                     this._elStyle.setProperty('height', (e.clientY - cr.top) + 'px', 'important');
  8980.                     return;
  8981.                 }
  8982.                 default: // mouseup
  8983.                     docBody.removeEventListener('mousemove', this);
  8984.                     docBody.removeEventListener('mouseup', this);
  8985.                     saveCfg('textaWidth', parseInt(this._elStyle.width, 10));
  8986.                     saveCfg('textaHeight', parseInt(this._elStyle.height, 10));
  8987.                 }
  8988.             }
  8989.         });
  8990.     }
  8991.     _makeHideableContainer() {
  8992.         this.pForm = $add('<div id="de-pform" class="de-win-body"></div>');
  8993.         if(this.form) {
  8994.             this.pForm.appendChild(this.form);
  8995.         }
  8996.         if(this.oeForm) {
  8997.             this.pForm.appendChild(this.oeForm);
  8998.         }
  8999.         const html = '<div class="de-parea"><div>[<a href="#"></a>]</div><hr></div>';
  9000.         this.pArea = [
  9001.             $bBegin(DelForm.first.el, html),
  9002.             $aEnd(aib._4chan ? $q('.board', DelForm.first.el) : DelForm.first.el, html)
  9003.         ];
  9004.         this._pBtn = [this.pArea[0].firstChild, this.pArea[1].firstChild];
  9005.         this._pBtn[0].firstElementChild.onclick = e => this.showMainReply(false, e);
  9006.         this._pBtn[1].firstElementChild.onclick = e => this.showMainReply(true, e);
  9007.         this.qArea = $add(`<div style="display: none; ${ Cfg.replyWinX }; ${
  9008.             Cfg.replyWinY }; z-index: ${ ++topWinZ };" id="de-win-reply" class="${
  9009.             aib.cReply + (Cfg.replyWinDrag ? ' de-win' : ' de-win-inpost') }"></div>`);
  9010.         this.isBottom = Cfg.addPostForm === 1;
  9011.         this.setReply(false, !aib.t || Cfg.addPostForm > 1);
  9012.     }
  9013.     _makeWindow() {
  9014.         makeDraggable('reply', this.qArea, $aBegin(this.qArea, `<div class="de-win-head">
  9015.             <span class="de-win-title"></span>
  9016.             <span class="de-win-buttons">
  9017.                 <svg class="de-win-btn-clear"><use xlink:href="#de-symbol-unavail"/></svg>
  9018.                 <svg class="de-win-btn-toggle"><use xlink:href="#de-symbol-win-arrow"/></svg>
  9019.                 <svg class="de-win-btn-close"><use xlink:href="#de-symbol-win-close"/></svg>
  9020.             </span>
  9021.         </div>
  9022.         <div class="de-resizer de-resizer-top"></div>
  9023.         <div class="de-resizer de-resizer-left"></div>
  9024.         <div class="de-resizer de-resizer-right"></div>
  9025.         <div class="de-resizer de-resizer-bottom"></div>`));
  9026.         const buttons = $q('.de-win-buttons', this.qArea);
  9027.         buttons.onmouseover = ({ target }) => {
  9028.             const el = target.parentNode;
  9029.             switch(fixEventEl(target).classList[0]) {
  9030.             case 'de-win-btn-clear': el.title = Lng.clearForm[lang]; break;
  9031.             case 'de-win-btn-close': el.title = Lng.closeReply[lang]; break;
  9032.             case 'de-win-btn-toggle': el.title = Cfg.replyWinDrag ? Lng.underPost[lang] : Lng.makeDrag[lang];
  9033.             }
  9034.         };
  9035.         const [clearBtn, toggleBtn, closeBtn] = [...buttons.children];
  9036.         clearBtn.onclick = () => {
  9037.             saveCfg('sageReply', 0);
  9038.             this.toggleSage();
  9039.             this.files.clearInputs();
  9040.             [this.txta, this.name, this.mail, this.subj, this.video, this.cap && this.cap.textEl].forEach(
  9041.                 el => el && (el.value = ''));
  9042.         };
  9043.         toggleBtn.onclick = () => {
  9044.             toggleCfg('replyWinDrag');
  9045.             if(Cfg.replyWinDrag) {
  9046.                 this.qArea.className = aib.cReply + ' de-win';
  9047.                 updateWinZ(this.qArea.style);
  9048.             } else {
  9049.                 this.qArea.className = aib.cReply + ' de-win-inpost';
  9050.                 this.txta.focus();
  9051.             }
  9052.         };
  9053.         closeBtn.onclick = () => this.closeReply();
  9054.     }
  9055.     _setPlaceholder(val) {
  9056.         const el = val === 'cap' ? this.cap.textEl : this[val];
  9057.         if(el) {
  9058.             toggleAttr(el, 'placeholder', Lng[val][lang], aib.multiFile || Cfg.fileInputs !== 2);
  9059.         }
  9060.     }
  9061.     _toggleQuickReply(tNum) {
  9062.         if(this.oeForm) {
  9063.             $del($q('input[name="oek_parent"]', this.oeForm));
  9064.             if(tNum) {
  9065.                 this.oeForm.insertAdjacentHTML('afterbegin',
  9066.                     `<input type="hidden" value="${ tNum }" name="oek_parent">`);
  9067.             }
  9068.         }
  9069.         if(this.form) {
  9070.             if(aib.changeReplyMode && tNum !== this.tNum) {
  9071.                 aib.changeReplyMode(this.form, tNum);
  9072.             }
  9073.             $del($q(`input[name="${ aib.formParent }"]`, this.form));
  9074.             if(tNum) {
  9075.                 this.form.insertAdjacentHTML('afterbegin',
  9076.                     `<input type="hidden" name="${ aib.formParent }" value="${ tNum }">`);
  9077.             }
  9078.         }
  9079.     }
  9080. }
  9081.  
  9082. /* ==[ FormSubmit.js ]========================================================================================
  9083.                                                    SUBMIT
  9084.    postform/delform html5/iframe submit, images and webms parsing, duplicate files posting, EXIF clearing
  9085. =========================================================================================================== */
  9086.  
  9087. function getSubmitError(dc) {
  9088.     if(!dc.body.hasChildNodes() || $q(aib.qDForm, dc)) {
  9089.         return null;
  9090.     }
  9091.     const err = [...$Q(aib.qError, dc)].map(str => str.innerHTML + '\n').join('')
  9092.         .replace(/<a [^>]+>Назад.+|<br.+/, '') || dc.body.innerHTML;
  9093.     return aib.isIgnoreError(err) ? null : err;
  9094. }
  9095.  
  9096. function checkUpload(data) {
  9097.     let error = null;
  9098.     let postNum = null;
  9099.     const isDocument = data instanceof HTMLDocument;
  9100.     if(aib.getSubmitData) {
  9101.         if(aib.jsonSubmit) {
  9102.             const _data = (isDocument ? data.body.textContent : data).trim();
  9103.             try {
  9104.                 data = JSON.parse(_data);
  9105.             } catch(err) {
  9106.                 error = getSubmitError(_data);
  9107.             }
  9108.         }
  9109.         if(!error) {
  9110.             ({ error, postNum } = aib.getSubmitData(data));
  9111.         }
  9112.     } else {
  9113.         error = getSubmitError(data);
  9114.     }
  9115.     if(error) {
  9116.         if(pr.isQuick) {
  9117.             pr.setReply(true, false);
  9118.         }
  9119.         if(/[cf]aptch|капч|подтвер|verifi/i.test(error)) {
  9120.             pr.refreshCap(true);
  9121.         }
  9122.         $popup('upload', error.toString());
  9123.         updater.sendErrNotif();
  9124.         updater.continueUpdater();
  9125.         DollchanAPI.notify('submitform', { success: false, error });
  9126.         return;
  9127.     }
  9128.     const { tNum } = pr;
  9129.     if((Cfg.markMyPosts || Cfg.markMyLinks) && postNum) {
  9130.         MyPosts.set(postNum, tNum || postNum);
  9131.     }
  9132.     if(Cfg.favOnReply && !Cfg.sageReply) {
  9133.         if(tNum) {
  9134.             const { thr } = pByNum.get(tNum);
  9135.             if(!thr.isFav) {
  9136.                 thr.toggleFavState(true);
  9137.             }
  9138.         } else {
  9139.             sesStorage['de-fav-newthr'] = JSON.stringify({ num: postNum, date: Date.now() });
  9140.         }
  9141.     }
  9142.     pr.clearForm();
  9143.     DollchanAPI.notify('submitform', { success: true, num: postNum });
  9144.     Cfg.stats[tNum ? 'reply' : 'op']++;
  9145.     saveCfgObj(aib.dm, Cfg);
  9146.     if(!tNum) {
  9147.         if(postNum) {
  9148.             deWindow.location.assign(aib.getThrUrl(aib.b, postNum));
  9149.         } else if(isDocument) {
  9150.             const dForm = $q(aib.qDForm, data);
  9151.             if(dForm) {
  9152.                 deWindow.location.assign(aib.getThrUrl(aib.b, aib.getTNum(dForm)));
  9153.             }
  9154.         }
  9155.         return;
  9156.     }
  9157.     if(aib.t) {
  9158.         Post.clearMarks();
  9159.         Thread.first.loadNewPosts().then(() => AjaxError.Success, err => err).then(err => {
  9160.             infoLoadErrors(err);
  9161.             if(Cfg.scrAfterRep) {
  9162.                 scrollTo(0, deWindow.pageYOffset + Thread.first.last.el.getBoundingClientRect().top);
  9163.             }
  9164.             updater.continueUpdater(true);
  9165.             closePopup('upload');
  9166.         });
  9167.     } else {
  9168.         pByNum.get(tNum).thr.loadPosts('new', false, false).then(() => closePopup('upload'));
  9169.     }
  9170.     pr.closeReply();
  9171.     pr.refreshCap();
  9172. }
  9173.  
  9174. async function checkDelete(data) {
  9175.     const err = getSubmitError(data instanceof HTMLDocument ? data : $DOM(data));
  9176.     if(err) {
  9177.         $popup('delete', Lng.errDelete[lang] + ':\n' + err);
  9178.         updater.sendErrNotif();
  9179.         return;
  9180.     }
  9181.     const els = $Q(`[de-form] ${ aib.qRPost } input:checked`);
  9182.     const threads = new Set();
  9183.     const isThr = aib.t;
  9184.     for(let i = 0, len = els.length; i < len; ++i) {
  9185.         const el = els[i];
  9186.         el.checked = false;
  9187.         if(!isThr) {
  9188.             threads.add(aib.getPostOfEl(el).thr);
  9189.         }
  9190.     }
  9191.     if(isThr) {
  9192.         Post.clearMarks();
  9193.         await Thread.first.loadNewPosts().catch(err => infoLoadErrors(err));
  9194.     } else {
  9195.         await Promise.all([...threads].map(thr => thr.loadPosts('new', false, false)));
  9196.     }
  9197.     $popup('delete', Lng.succDeleted[lang]);
  9198. }
  9199.  
  9200. // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
  9201. function isFormElDisabled(el) {
  9202.     switch(el.tagName.toLowerCase()) {
  9203.     case 'button':
  9204.     case 'input':
  9205.     case 'select':
  9206.     case 'textarea':
  9207.         if(el.hasAttribute('disabled')) {
  9208.             return true;
  9209.         }
  9210.         /* falls through */
  9211.     default:
  9212.         if(nav.matchesSelector(el, 'fieldset[disabled] > :not(legend):not(:first-of-type) *')) {
  9213.             return true;
  9214.         }
  9215.     }
  9216.     return false;
  9217. }
  9218. // https://html.spec.whatwg.org/multipage/forms.html#constructing-form-data-set
  9219. function * getFormElements(form, submitter) {
  9220.     const controls = $Q('button, input, keygen, object, select, textarea', form);
  9221.     const fixName = name => name ? name.replace(/([^\r])\n|\r([^\n])/g, '$1\r\n$2') : '';
  9222.  
  9223.     constructSet:
  9224.     for(let i = 0, len = controls.length; i < len; ++i) {
  9225.         const field = controls[i];
  9226.         const tagName = field.tagName.toLowerCase();
  9227.         const type = field.getAttribute('type');
  9228.         const name = field.getAttribute('name');
  9229.         if($parent(field, 'DATALIST', form) || isFormElDisabled(field) ||
  9230.             field !== submitter && (
  9231.                 tagName === 'button' ||
  9232.                 tagName === 'input' && (type === 'submit' || type === 'reset' || type === 'button')
  9233.             ) ||
  9234.             tagName === 'input' && (
  9235.                 type === 'checkbox' && !field.checked ||
  9236.                 type === 'radio' && !field.checked ||
  9237.                 type === 'image' && !name
  9238.             ) ||
  9239.             tagName === 'object'
  9240.         ) {
  9241.             continue;
  9242.         }
  9243.         if(tagName === 'select') {
  9244.             const options = $Q('select > option, select > optgrout > option', field);
  9245.             for(let j = 0, jlen = options.length; j < jlen; ++j) {
  9246.                 const option = options[j];
  9247.                 if(option.selected && !isFormElDisabled(option)) {
  9248.                     yield { type, el: field, name: fixName(name), value: option.value };
  9249.                 }
  9250.             }
  9251.         } else if(tagName === 'input') {
  9252.             switch(type) {
  9253.             case 'image': throw new Error('input[type="image"] is not supported');
  9254.             case 'checkbox':
  9255.             case 'radio':
  9256.                 yield { type, el: field, name: fixName(name), value: field.value || 'on' };
  9257.                 continue constructSet;
  9258.             case 'file': {
  9259.                 let img;
  9260.                 if(field.files.length > 0) {
  9261.                     const { files } = field;
  9262.                     for(let j = 0, jlen = files.length; j < jlen; ++j) {
  9263.                         yield { name, type, el: field, value: files[j] };
  9264.                     }
  9265.                 } else if(field.obj && (img = field.obj.imgFile)) {
  9266.                     yield {
  9267.                         name,
  9268.                         type,
  9269.                         el    : field,
  9270.                         value : new File([img.data], img.name, { type: img.type })
  9271.                     };
  9272.                 } else {
  9273.                     yield {
  9274.                         el    : field,
  9275.                         name  : fixName(name),
  9276.                         type  : 'application/octet-stream',
  9277.                         value : new File([''], '')
  9278.                     };
  9279.                 }
  9280.                 continue constructSet;
  9281.             }
  9282.             }
  9283.         }
  9284.         if(type === 'textarea') {
  9285.             yield { type, el: field, name: name || '', value: field.value };
  9286.         } else {
  9287.             yield { type, el: field, name: fixName(name), value: field.value };
  9288.         }
  9289.         const dirname = field.getAttribute('dirname');
  9290.         if(dirname) {
  9291.             yield {
  9292.                 el    : field,
  9293.                 name  : fixName(dirname),
  9294.                 type  : 'direction',
  9295.                 value : nav.matchesSelector(field, ':dir(rtl)') ? 'rtl' : 'ltr'
  9296.             };
  9297.         }
  9298.     }
  9299. }
  9300.  
  9301. function getUploadFunc() {
  9302.     $popup('upload', Lng.sending[lang] +
  9303.         '<br><progress id="de-uploadprogress" value="0" max="1" style="display: none; width: 200px;">' +
  9304.         '</progress><div style="display: none; font: bold 12px arial;">' +
  9305.         '<span></span> / <span></span> (<span></span>)</div>', true);
  9306.     let isInited = false;
  9307.     const beginTime = Date.now();
  9308.     const progress = $id('de-uploadprogress');
  9309.     const counterWrap = progress.nextElementSibling;
  9310.     const [counterEl, totalEl, speedEl] = [...counterWrap.children];
  9311.     return ({ total, loaded: i }) => {
  9312.         if(!isInited) {
  9313.             progress.setAttribute('max', total);
  9314.             $show(progress);
  9315.             totalEl.textContent = prettifySize(total);
  9316.             $show(counterWrap);
  9317.             isInited = true;
  9318.         }
  9319.         progress.value = i;
  9320.         counterEl.textContent = prettifySize(i);
  9321.         speedEl.textContent = `${ prettifySize(1e3 * i / (Date.now() - beginTime)) }/${ Lng.second[lang] }`;
  9322.     };
  9323. }
  9324.  
  9325. async function html5Submit(form, submitter, needProgress = false) {
  9326.     const data = new FormData();
  9327.     let hasFiles = false;
  9328.     for(const { name, value, type, el } of getFormElements(form, submitter)) {
  9329.         let val = value;
  9330.         if(name === 'de-file-txt') {
  9331.             continue;
  9332.         }
  9333.         if(type === 'file') {
  9334.             hasFiles = true;
  9335.             const fileName = value.name;
  9336.             const fileExt = fileName.substring(fileName.lastIndexOf('.'));
  9337.             const newFileName =
  9338.                 !Cfg.removeFName || el.obj && el.obj.imgFile && el.obj.imgFile.isConstName ? fileName : (
  9339.                     Cfg.removeFName === 1 ? '' :
  9340.                     // 5 years = 5*365*24*60*60*1e3 = 15768e7
  9341.                     Date.now() - (Cfg.removeFName === 2 ? 0 : Math.round(Math.random() * 15768e7))
  9342.                 ) + fileExt;
  9343.             const mime = value.type;
  9344.             if((Cfg.postSameImg || Cfg.removeEXIF) && (
  9345.                 mime === 'image/jpeg' ||
  9346.                 mime === 'image/png' ||
  9347.                 mime === 'image/gif' ||
  9348.                 mime === 'video/webm' && !aib.makaba)
  9349.             ) {
  9350.                 const cleanData = cleanFile((await readFile(value)).data, el.obj ? el.obj.extraFile : null);
  9351.                 if(!cleanData) {
  9352.                     return Promise.reject(new Error(Lng.fileCorrupt[lang] + ': ' + fileName));
  9353.                 }
  9354.                 val = new File(cleanData, newFileName, { type: mime });
  9355.             } else if(Cfg.removeFName) {
  9356.                 val = new File([value], newFileName, { type: mime });
  9357.             }
  9358.         }
  9359.         data.append(name, val);
  9360.     }
  9361.     if(aib.sendHTML5Post) {
  9362.         return aib.sendHTML5Post(form, data, needProgress, hasFiles);
  9363.     }
  9364.     const ajaxParams = { data, method: 'POST' };
  9365.     if(needProgress && hasFiles) {
  9366.         ajaxParams.onprogress = getUploadFunc();
  9367.     }
  9368.     return $ajax(form.action, ajaxParams)
  9369.         .then(xhr => aib.jsonSubmit ? xhr.responseText : $DOM(xhr.responseText))
  9370.         .catch(err => Promise.reject(err));
  9371. }
  9372.  
  9373. function cleanFile(data, extraData) {
  9374.     const img = nav.getUnsafeUint8Array(data);
  9375.     const rand = Cfg.postSameImg && String(Math.round(Math.random() * 1e6));
  9376.     const rv = extraData ?
  9377.         rand ? [img, extraData, rand] : [img, extraData] :
  9378.         rand ? [img, rand] : [img];
  9379.     const rExif = !!Cfg.removeEXIF;
  9380.     if(!rand && !rExif && !extraData) {
  9381.         return rv;
  9382.     }
  9383.     let i, len, val, lIdx, jpgDat;
  9384.     const subarray = (begin, end) => nav.getUnsafeUint8Array(data, begin, end - begin);
  9385.     // JPG
  9386.     if(img[0] === 0xFF && img[1] === 0xD8) {
  9387.         let deep = 1;
  9388.         for(i = 2, len = img.length - 1, val = [null, null], lIdx = 2, jpgDat = null; i < len;) {
  9389.             if(img[i] === 0xFF) {
  9390.                 if(rExif) {
  9391.                     // Remove exif data
  9392.                     if(!jpgDat && deep === 1) {
  9393.                         if(img[i + 1] === 0xE1 && img[i + 4] === 0x45) {
  9394.                             jpgDat = readExif(data, i + 10, (img[i + 2] << 8) + img[i + 3]);
  9395.                         } else if(img[i + 1] === 0xE0 && img[i + 7] === 0x46 &&
  9396.                             (img[i + 2] !== 0 || img[i + 3] >= 0x0E || img[i + 15] !== 0xFF)
  9397.                         ) {
  9398.                             jpgDat = subarray(i + 11, i + 16);
  9399.                         }
  9400.                     }
  9401.                     if(((img[i + 1] >> 4) === 0xE && img[i + 1] !== 0xEE) || img[i + 1] === 0xFE) {
  9402.                         if(lIdx !== i) {
  9403.                             val.push(subarray(lIdx, i));
  9404.                         }
  9405.                         i += 2 + (img[i + 2] << 8) + img[i + 3];
  9406.                         lIdx = i;
  9407.                         continue;
  9408.                     }
  9409.                 } else if(img[i + 1] === 0xD8) { // Jpg start marker [0xFFD8]
  9410.                     deep++;
  9411.                     i++;
  9412.                     continue;
  9413.                 }
  9414.                 if(img[i + 1] === 0xD9 && --deep === 0) { // Jpg end marker [0xFFD9]
  9415.                     break;
  9416.                 }
  9417.             }
  9418.             i++;
  9419.         }
  9420.         i += 2;
  9421.         if(!extraData && len - i > 75) {
  9422.             i = len;
  9423.         }
  9424.         if(lIdx === 2) {
  9425.             // Remove data after the end marker
  9426.             if(i !== len) {
  9427.                 rv[0] = nav.getUnsafeUint8Array(data, 0, i);
  9428.             }
  9429.             return rv;
  9430.         }
  9431.         val[0] = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0, 0, 0x0E, 0x4A, 0x46, 0x49, 0x46, 0, 1, 1]);
  9432.         val[1] = jpgDat || new Uint8Array([0, 0, 1, 0, 1]);
  9433.         val.push(subarray(lIdx, i));
  9434.         if(extraData) {
  9435.             val.push(extraData);
  9436.         }
  9437.         if(rand) {
  9438.             val.push(rand);
  9439.         }
  9440.         return val;
  9441.     }
  9442.     // PNG
  9443.     if(img[0] === 0x89 && img[1] === 0x50) {
  9444.         // Search for end marker [0x49454e44]
  9445.         for(i = 0, len = img.length - 7; i < len && (
  9446.             img[i] !== 0x49 ||
  9447.             img[i + 1] !== 0x45 ||
  9448.             img[i + 2] !== 0x4E ||
  9449.             img[i + 3] !== 0x44
  9450.         ); ++i) /* empty */;
  9451.         i += 8;
  9452.         // Remove data after the end marker
  9453.         if(i !== len && (extraData || len - i <= 75)) {
  9454.             rv[0] = nav.getUnsafeUint8Array(data, 0, i);
  9455.         }
  9456.         return rv;
  9457.     }
  9458.     // GIF
  9459.     if(img[0] === 0x47 && img[1] === 0x49 && img[2] === 0x46) {
  9460.         // Search for last frame end marker [0x003B]
  9461.         i = len = img.length;
  9462.         while(i && img[--i - 1] !== 0x00 && img[i] !== 0x3B) /* empty */;
  9463.         // Remove data after the end marker
  9464.         if(++i !== len) {
  9465.             rv[0] = nav.getUnsafeUint8Array(data, 0, i);
  9466.         }
  9467.         return rv;
  9468.     }
  9469.     // WEBM
  9470.     if(img[0] === 0x1a && img[1] === 0x45 && img[2] === 0xDF && img[3] === 0xA3) {
  9471.         return new WebmParser(data).addWebmData(rand).getWebmData();
  9472.     }
  9473.     return null;
  9474. }
  9475.  
  9476. function readExif(data, off, len) {
  9477.     let xRes = 0;
  9478.     let yRes = 0;
  9479.     let resT = 0;
  9480.     const dv = nav.getUnsafeDataView(data, off);
  9481.     const le = String.fromCharCode(dv.getUint8(0), dv.getUint8(1)) !== 'MM';
  9482.     if(dv.getUint16(2, le) !== 0x2A) {
  9483.         return null;
  9484.     }
  9485.     const i = dv.getUint32(4, le);
  9486.     if(i > len) {
  9487.         return null;
  9488.     }
  9489.     for(let j = 0, tgLen = dv.getUint16(i, le); j < tgLen; ++j) {
  9490.         let dE = i + 2 + 12 * j;
  9491.         const tag = dv.getUint16(dE, le);
  9492.         if(tag === 0x0128) {
  9493.             resT = dv.getUint16(dE + 8, le) - 1;
  9494.         } else if(tag === 0x011A || tag === 0x011B) {
  9495.             dE = dv.getUint32(dE + 8, le);
  9496.             if(dE > len) {
  9497.                 return null;
  9498.             }
  9499.             if(tag === 0x11A) {
  9500.                 xRes = Math.round(dv.getUint32(dE, le) / dv.getUint32(dE + 4, le));
  9501.             } else {
  9502.                 yRes = Math.round(dv.getUint32(dE, le) / dv.getUint32(dE + 4, le));
  9503.             }
  9504.         }
  9505.     }
  9506.     xRes = xRes || yRes;
  9507.     yRes = yRes || xRes;
  9508.     return new Uint8Array([resT & 0xFF, xRes >> 8, xRes & 0xFF, yRes >> 8, yRes & 0xFF]);
  9509. }
  9510.  
  9511. /* ==[ FormFile.js ]==========================================================================================
  9512.                                                 FILE INPUTS
  9513.                 image/video files in postform: preview, adding by url, drag-n-drop, deleting
  9514. =========================================================================================================== */
  9515.  
  9516. class Files {
  9517.     constructor(form, fileEl) {
  9518.         this.filesCount = 0;
  9519.         this.fileTr = $qParent(fileEl, aib.qFormTr);
  9520.         this.onchange = null;
  9521.         this._form = form;
  9522.         this._inputs = [];
  9523.         const els = $Q('input[type="file"]', this.fileTr);
  9524.         for(let i = 0, len = els.length; i < len; ++i) {
  9525.             this._inputs.push(new FileInput(this, els[i]));
  9526.         }
  9527.         this._files = [];
  9528.         this.hideEmpty();
  9529.     }
  9530.     get rarInput() {
  9531.         const value = $bEnd(docBody, '<input type="file" style="display: none;">');
  9532.         Object.defineProperty(this, 'rarInput', { value });
  9533.         return value;
  9534.     }
  9535.     get thumbsEl() {
  9536.         let value;
  9537.         if(aib.multiFile) {
  9538.             value = $aEnd(this.fileTr, '<div id="de-file-area"></div>');
  9539.         } else {
  9540.             value = $qParent(this._form.txta, aib.qFormTd).previousElementSibling;
  9541.             value.innerHTML = `<div style="display: none;">${ value.innerHTML }</div><div></div>`;
  9542.             value = value.lastChild;
  9543.         }
  9544.         Object.defineProperty(this, 'thumbsEl', { value });
  9545.         return value;
  9546.     }
  9547.     changeMode() {
  9548.         const isThumbMode = Cfg.fileInputs === 2;
  9549.         for(const inp of this._inputs) {
  9550.             inp.changeMode(isThumbMode);
  9551.         }
  9552.         this.hideEmpty();
  9553.     }
  9554.     clearInputs() {
  9555.         for(const inp of this._inputs) {
  9556.             inp.clearInp();
  9557.         }
  9558.         this.hideEmpty();
  9559.     }
  9560.     hideEmpty() {
  9561.         for(let els = this._inputs, i = els.length - 1; i > 0; --i) {
  9562.             const inp = els[i];
  9563.             if(inp.hasFile) {
  9564.                 break;
  9565.             } else if(els[i - 1].hasFile) {
  9566.                 inp.showInp();
  9567.                 break;
  9568.             }
  9569.             inp.hideInp();
  9570.         }
  9571.     }
  9572. }
  9573.  
  9574. class FileInput {
  9575.     constructor(parent, el) {
  9576.         this.extraFile = null;
  9577.         this.hasFile = false;
  9578.         this.imgFile = null;
  9579.         this._input = el;
  9580.         this._isTxtEditable = false;
  9581.         this._isTxtEditName = false;
  9582.         this._mediaEl = null;
  9583.         this._parent = parent;
  9584.         this._rarMsg = null;
  9585.         this._spoilEl = $q(aib.qFormSpoiler, el.parentNode);
  9586.         this._thumb = null;
  9587.         this._utils = $add(`<div class="de-file-utils">
  9588.             <span class="de-file-btn-rar" title="${ Lng.helpAddFile[lang] }" style="display: none;">
  9589.                 <svg><use xlink:href="#de-symbol-file-rar"/></svg></span>
  9590.             <input class="de-file-spoil" type="checkbox" title="` +
  9591.                 `${ Lng.spoilFile[lang] }" style="display: none;">
  9592.             <span class="de-file-btn-txt" title="${ Lng.addManually[lang] }">
  9593.                 <svg><use xlink:href="#de-symbol-file-txt"/></svg></span>
  9594.             <span class="de-file-btn-ren" title="${ Lng.renameFile[lang] }" style="display: none;">
  9595.                 <svg><use xlink:href="#de-symbol-file-ren"/></svg></span>
  9596.             <span class="de-file-btn-del" title="${ Lng.removeFile[lang] }" style="display: none;">
  9597.                 <svg><use xlink:href="#de-symbol-file-del"/></svg></span>
  9598.         </div>`);
  9599.         [this._btnRar, this._btnSpoil, this._btnTxt, this._btnRen, this._btnDel] = [...this._utils.children];
  9600.         this._utils.addEventListener('click', this);
  9601.         this._txtWrap = $add(`<span class="de-file-txt-wrap">
  9602.             <input type="text" name="de-file-txt" class="de-file-txt-input de-file-txt-noedit" title="` +
  9603.                 `${ Lng.youCanDrag[lang] }" placeholder="${ Lng.dropFileHere[lang] }">
  9604.             <input type="button" class="de-file-txt-add" value="+" title="` +
  9605.                 `${ Lng.add[lang] }" style="display: none;"></span>`);
  9606.         [this._txtInput, this._txtAddBtn] = [...this._txtWrap.children];
  9607.         this._txtWrap.addEventListener('click', this);
  9608.         this._toggleDragEvents(this._txtWrap, true);
  9609.         el.obj = this;
  9610.         el.classList.add('de-file-input');
  9611.         el.addEventListener('change', this);
  9612.         if(el.files && el.files[0]) {
  9613.             this._removeFile();
  9614.         }
  9615.         if(Cfg.fileInputs) {
  9616.             $hide(el);
  9617.             if(aib.multiFile) {
  9618.                 this._input.setAttribute('multiple', true);
  9619.             }
  9620.         }
  9621.         if(FileInput._isThumbMode) {
  9622.             this._initThumbs();
  9623.         } else {
  9624.             $before(this._input, this._txtWrap);
  9625.             $after(this._input, this._utils);
  9626.         }
  9627.     }
  9628.     changeMode(showThumbs) {
  9629.         $toggle(this._input, !Cfg.fileInputs);
  9630.         toggleAttr(this._input, 'multiple', true, aib.multiFile && Cfg.fileInputs);
  9631.         $toggle(this._btnRen, Cfg.fileInputs && this.hasFile);
  9632.         if(!(showThumbs ^ !!this._thumb)) {
  9633.             return;
  9634.         }
  9635.         if(showThumbs) {
  9636.             this._initThumbs();
  9637.             return;
  9638.         }
  9639.         $before(this._input, this._txtWrap);
  9640.         $after(this._input, this._utils);
  9641.         $del($q('de-file-txt-area'));
  9642.         $show(this._parent.fileTr);
  9643.         $show(this._txtWrap);
  9644.         if(this._mediaEl) {
  9645.             deWindow.URL.revokeObjectURL(this._mediaEl.src);
  9646.         }
  9647.         this._toggleDragEvents(this._thumb, false);
  9648.         $del(this._thumb);
  9649.         this._thumb = this._mediaEl = null;
  9650.     }
  9651.     clearInp() {
  9652.         if(FileInput._isThumbMode) {
  9653.             this._thumb.classList.add('de-file-off');
  9654.             if(this._mediaEl) {
  9655.                 deWindow.URL.revokeObjectURL(this._mediaEl.src);
  9656.                 this._mediaEl.parentNode.title = Lng.youCanDrag[lang];
  9657.                 this._mediaEl.remove();
  9658.                 this._mediaEl = null;
  9659.             }
  9660.         }
  9661.         if(this._btnDel) {
  9662.             this._toggleDelBtn(false);
  9663.             $hide(this._btnSpoil);
  9664.             if(this._spoilEl) {
  9665.                 this._spoilEl.checked = this._btnSpoil.checked = false;
  9666.             }
  9667.             $hide(this._btnRar);
  9668.             $hide(this._txtAddBtn);
  9669.             $del(this._rarMsg);
  9670.             if(FileInput._isThumbMode) {
  9671.                 $hide(this._txtWrap);
  9672.             }
  9673.             this._txtInput.value = '';
  9674.             this._txtInput.classList.add('de-file-txt-noedit');
  9675.             this._txtInput.placeholder = Lng.dropFileHere[lang];
  9676.         }
  9677.         this.extraFile = this.imgFile = null;
  9678.         this._isTxtEditable = this._isTxtEditName = false;
  9679.         this._changeFilesCount(-1);
  9680.         this._removeFile();
  9681.     }
  9682.     handleEvent(e) {
  9683.         const el = e.target;
  9684.         const thumb = this._thumb;
  9685.         const isThumb = el === thumb || el.className === 'de-file-img';
  9686.         switch(e.type) {
  9687.         case 'change': {
  9688.             const inpArray = this._parent._inputs;
  9689.             const curInpIdx = inpArray.indexOf(this);
  9690.             const filesLen = el.files.length;
  9691.             if(filesLen > 1) {
  9692.                 const allowedLen = Math.min(filesLen, inpArray.length - curInpIdx);
  9693.                 let j = allowedLen;
  9694.                 for(let i = 0; i < allowedLen; ++i) {
  9695.                     FileInput._readDroppedFile(inpArray[curInpIdx + i], el.files[i]).then(() => {
  9696.                         if(!--j) { // Clear original file input after all allowed files will be read.
  9697.                             this._removeFileHelper();
  9698.                         }
  9699.                     });
  9700.                     this._parent._files[curInpIdx + i] = el.files[i];
  9701.                 }
  9702.             } else {
  9703.                 if(filesLen > 0) {
  9704.                     setTimeout(() => this._onFileChange(false), 20);
  9705.                     this._parent._files[curInpIdx] = el.files[0];
  9706.                 } else {
  9707.                     this.clearInp();
  9708.                     delete this._parent._files[curInpIdx];
  9709.                 }
  9710.             }
  9711.             DollchanAPI.notify('filechange', this._parent._files);
  9712.             return;
  9713.         }
  9714.         case 'click': {
  9715.             const parent = el.parentNode;
  9716.             if(isThumb) {
  9717.                 this._input.click();
  9718.             } else if(parent === this._btnDel) {
  9719.                 this.clearInp();
  9720.                 this._parent.hideEmpty();
  9721.                 delete this._parent._files[this._parent._inputs.indexOf(this)];
  9722.                 DollchanAPI.notify('filechange', this._parent._files);
  9723.             } else if(parent === this._btnRar) {
  9724.                 this._addRarJpeg();
  9725.             } else if(parent === this._btnRen) {
  9726.                 const isShow = this._isTxtEditName = !this._isTxtEditName;
  9727.                 this._isTxtEditable = !this._isTxtEditable;
  9728.                 if(FileInput._isThumbMode) {
  9729.                     $toggle(this._txtWrap, isShow);
  9730.                 }
  9731.                 $toggle(this._txtAddBtn, isShow);
  9732.                 this._txtInput.classList.toggle('de-file-txt-noedit', !isShow);
  9733.                 if(isShow) {
  9734.                     this._txtInput.focus();
  9735.                 }
  9736.             } else if(parent === this._btnTxt) {
  9737.                 this._toggleDelBtn(this._isTxtEditable = true);
  9738.                 $show(this._txtAddBtn);
  9739.                 if(FileInput._isThumbMode) {
  9740.                     $toggle(this._txtWrap);
  9741.                 }
  9742.                 this._txtInput.classList.remove('de-file-txt-noedit');
  9743.                 this._txtInput.placeholder = Lng.enterTheLink[lang];
  9744.                 this._txtInput.focus();
  9745.             } else if(el === this._btnSpoil) {
  9746.                 this._spoilEl.checked = this._btnSpoil.checked;
  9747.                 return;
  9748.             } else if(el === this._txtAddBtn) {
  9749.                 if(this._isTxtEditName) {
  9750.                     if(FileInput._isThumbMode) {
  9751.                         $hide(this._txtWrap);
  9752.                     }
  9753.                     $hide(this._txtAddBtn);
  9754.                     this._txtInput.classList.add('de-file-txt-noedit');
  9755.                     this._isTxtEditable = this._isTxtEditName = false;
  9756.                     const newName = this._txtInput.value;
  9757.                     if(!newName) {
  9758.                         this._txtInput.value = this.imgFile ? this.imgFile.name : this._input.files[0].name;
  9759.                         return;
  9760.                     }
  9761.                     if(this.imgFile) {
  9762.                         this.imgFile.isConstName = true;
  9763.                         this.imgFile.name = newName;
  9764.                         if(FileInput._isThumbMode) {
  9765.                             this._addThumbTitle(newName, this.imgFile.data.byteLength);
  9766.                         }
  9767.                         return;
  9768.                     }
  9769.                     const file = this._input.files[0];
  9770.                     readFile(file).then(({ data }) => {
  9771.                         this.imgFile = { data, name: newName, type: file.type, isConstName: true };
  9772.                         this._removeFileHelper(); // Clear the original file
  9773.                         if(FileInput._isThumbMode) {
  9774.                             this._addThumbTitle(newName, data.byteLength);
  9775.                         }
  9776.                     });
  9777.                     return;
  9778.                 } else {
  9779.                     this._addUrlFile(this._txtInput.value);
  9780.                 }
  9781.             } else if(el === this._txtInput && !this._isTxtEditable) {
  9782.                 this._input.click();
  9783.                 this._txtInput.blur();
  9784.             }
  9785.             $pd(e);
  9786.             e.stopPropagation();
  9787.             return;
  9788.         }
  9789.         case 'dragenter':
  9790.             if(isThumb) {
  9791.                 thumb.classList.add('de-file-drag');
  9792.             }
  9793.             return;
  9794.         case 'dragleave':
  9795.             if(isThumb && el.classList.contains('de-file-img')) {
  9796.                 thumb.classList.remove('de-file-drag');
  9797.             }
  9798.             return;
  9799.         case 'drop': {
  9800.             const dt = e.dataTransfer;
  9801.             if(!isThumb && el !== this._txtInput) {
  9802.                 return;
  9803.             }
  9804.             const filesLen = dt.files.length;
  9805.             if(filesLen) {
  9806.                 const inpArray = this._parent._inputs;
  9807.                 const inpLen = inpArray.length;
  9808.                 for(let i = inpArray.indexOf(this), j = 0; i < inpLen && j < filesLen; ++i, ++j) {
  9809.                     FileInput._readDroppedFile(inpArray[i], dt.files[j]);
  9810.                     this._parent._files[i] = dt.files[j];
  9811.                 }
  9812.                 DollchanAPI.notify('filechange', this._parent._files);
  9813.             } else {
  9814.                 this._addUrlFile(dt.getData('text/plain'));
  9815.             }
  9816.             if(FileInput._isThumbMode) {
  9817.                 setTimeout(() => thumb.classList.remove('de-file-drag'), 10);
  9818.             }
  9819.             $pd(e);
  9820.             e.stopPropagation();
  9821.         }
  9822.         }
  9823.     }
  9824.     hideInp() {
  9825.         if(FileInput._isThumbMode) {
  9826.             this._toggleDelBtn(false);
  9827.             $hide(this._thumb);
  9828.             $hide(this._txtWrap);
  9829.         }
  9830.         $hide(this._wrap);
  9831.     }
  9832.     showInp() {
  9833.         if(FileInput._isThumbMode) {
  9834.             $show(this._thumb);
  9835.         }
  9836.         $show(this._wrap);
  9837.     }
  9838.  
  9839.     static get _isThumbMode() {
  9840.         return Cfg.fileInputs === 2;
  9841.     }
  9842.     static _readDroppedFile(inputObj, file) {
  9843.         return readFile(file).then(({ data }) => {
  9844.             inputObj.imgFile = { data, name: file.name, type: file.type };
  9845.             inputObj.showInp();
  9846.             inputObj._onFileChange(true);
  9847.         });
  9848.     }
  9849.     get _wrap() {
  9850.         return aib.multiFile ? this._input.parentNode : this._input;
  9851.     }
  9852.     _addNewThumb(fileData, fileName, fileType, fileSize) {
  9853.         let el = this._thumb;
  9854.         el.classList.remove('de-file-off');
  9855.         el = el.firstChild.firstChild;
  9856.         el.title = `${ fileName }, ${ (fileSize / 1024).toFixed(2) }KB`;
  9857.         this._mediaEl = el = $aBegin(el, fileType.startsWith('video/') ?
  9858.             '<video class="de-file-img" loop autoplay muted src=""></video>' :
  9859.             '<img class="de-file-img" src="">');
  9860.         el.src = deWindow.URL.createObjectURL(new Blob([fileData]));
  9861.         if((el = el.nextSibling)) {
  9862.             deWindow.URL.revokeObjectURL(el.src);
  9863.             el.remove();
  9864.         }
  9865.     }
  9866.     _addRarJpeg() {
  9867.         const el = this._parent.rarInput;
  9868.         el.onchange = e => {
  9869.             $hide(this._btnRar);
  9870.             const myBtn = this._rarMsg = $aBegin(this._utils,
  9871.                 '<span><svg class="de-wait"><use xlink:href="#de-symbol-wait"/></svg></span>');
  9872.             const file = e.target.files[0];
  9873.             readFile(file).then(({ data }) => {
  9874.                 if(this._rarMsg === myBtn) {
  9875.                     myBtn.className = 'de-file-rarmsg';
  9876.                     const origFileName = this.imgFile ? this.imgFile.name : this._input.files[0].name;
  9877.                     myBtn.title = origFileName + ' + ' + file.name;
  9878.                     myBtn.textContent = origFileName.split('.').pop() + ' + ' + file.name.split('.').pop();
  9879.                     this.extraFile = data;
  9880.                 }
  9881.             });
  9882.         };
  9883.         el.click();
  9884.     }
  9885.     _addThumbTitle(name, size) {
  9886.         this._thumb.firstChild.firstChild.title = `${ name }, ${ (size / 1024).toFixed(2) }KB`;
  9887.     }
  9888.     _addUrlFile(url, file = null) {
  9889.         if(!url) {
  9890.             return Promise.reject(new Error('URL is null'));
  9891.         }
  9892.         $popup('file-loading', Lng.loading[lang], true);
  9893.         return ContentLoader.loadImgData(url, false).then(data => {
  9894.             if(file) {
  9895.                 deWindow.URL.revokeObjectURL(url);
  9896.             }
  9897.             if(!data) {
  9898.                 $popup('file-loading', Lng.cantLoad[lang] + ' URL: ' + url);
  9899.                 return;
  9900.             }
  9901.             closePopup('file-loading');
  9902.             this._isTxtEditable = this._isTxtEditName = false;
  9903.             let name = file ? file.name : url.split('/').pop();
  9904.             const type = file && file.type || getFileType(name);
  9905.             if(!type || name.includes('?')) {
  9906.                 let ext;
  9907.                 switch((data[0] << 8) | data[1]) {
  9908.                 case 0xFFD8: ext = 'jpg'; break;
  9909.                 case 0x8950: ext = 'png'; break;
  9910.                 case 0x4749: ext = 'gif'; break;
  9911.                 case 0x1A45: ext = 'webm'; break;
  9912.                 default: ext = '';
  9913.                 }
  9914.                 if(ext) {
  9915.                     name = name.split('?').shift() + '.' + ext;
  9916.                 }
  9917.             }
  9918.             this.imgFile = { data: data.buffer, name, type: type || getFileType(name) };
  9919.             if(!file) {
  9920.                 file = new Blob([data], { type: this.imgFile.type });
  9921.                 file.name = name;
  9922.             }
  9923.             this._parent._files[this._parent._inputs.indexOf(this)] = file;
  9924.             DollchanAPI.notify('filechange', this._parent._files);
  9925.             if(FileInput._isThumbMode) {
  9926.                 $hide(this._txtWrap);
  9927.             }
  9928.             this._onFileChange(true);
  9929.         });
  9930.     }
  9931.     _changeFilesCount(val) {
  9932.         this._parent.filesCount = Math.max(this._parent.filesCount + val, 0);
  9933.         if(aib.dobrochan) {
  9934.             $id('post_files_count').value = this._parent.filesCount + 1;
  9935.         }
  9936.     }
  9937.     _initThumbs() {
  9938.         const { fileTr } = this._parent;
  9939.         $hide(fileTr);
  9940.         $hide(this._txtWrap);
  9941.         const isTr = fileTr.tagName === 'TR';
  9942.         const txtArea = $q('.de-file-txt-area') || $bBegin(fileTr, isTr ?
  9943.             '<tr class="de-file-txt-area"><td class="postblock"></td><td></td></tr>' :
  9944.             '<div class="de-file-txt-area"></div>');
  9945.         (isTr ? txtArea.lastChild : txtArea).appendChild(this._txtWrap);
  9946.         this._thumb = $bEnd(this._parent.thumbsEl,
  9947.             `<div class="de-file de-file-off"><div class="de-file-img"><div class="de-file-img" title="${
  9948.                 Lng.youCanDrag[lang] }"></div></div></div>`);
  9949.         this._thumb.addEventListener('click', this);
  9950.         this._thumb.addEventListener('dragenter', this);
  9951.         this._thumb.appendChild(this._utils);
  9952.         this._toggleDragEvents(this._thumb, true);
  9953.         if(this.hasFile) {
  9954.             this._showFileThumb();
  9955.         }
  9956.     }
  9957.     _onFileChange(hasImgFile) {
  9958.         this._txtInput.value = hasImgFile ? this.imgFile.name : this._input.files[0].name;
  9959.         if(!hasImgFile) {
  9960.             this.imgFile = null;
  9961.         }
  9962.         if(this._parent.onchange) {
  9963.             this._parent.onchange();
  9964.         }
  9965.         if(FileInput._isThumbMode) {
  9966.             this._showFileThumb();
  9967.         }
  9968.         if(this.hasFile) {
  9969.             this.extraFile = null;
  9970.         } else {
  9971.             this.hasFile = true;
  9972.             this._changeFilesCount(+1);
  9973.             this._toggleDelBtn(true);
  9974.             $hide(this._txtAddBtn);
  9975.             if(FileInput._isThumbMode) {
  9976.                 $hide(this._txtWrap);
  9977.             }
  9978.             if(this._spoilEl) {
  9979.                 this._btnSpoil.checked = this._spoilEl.checked;
  9980.                 $show(this._btnSpoil);
  9981.             }
  9982.             this._txtInput.classList.add('de-file-txt-noedit');
  9983.             this._txtInput.placeholder = Lng.dropFileHere[lang];
  9984.         }
  9985.         this._parent.hideEmpty();
  9986.         if(!nav.isPresto && !aib._4chan &&
  9987.             /^image\/(?:png|jpeg)$/.test(hasImgFile ? this.imgFile.type : this._input.files[0].type)
  9988.         ) {
  9989.             $del(this._rarMsg);
  9990.             $show(this._btnRar);
  9991.         }
  9992.     }
  9993.     _removeFile() {
  9994.         this._removeFileHelper();
  9995.         this.hasFile = false;
  9996.         if(this._parent._files) {
  9997.             delete this._parent._files[this._parent._inputs.indexOf(this)];
  9998.         }
  9999.     }
  10000.     _removeFileHelper() {
  10001.         const oldEl = this._input;
  10002.         const newEl = $aEnd(oldEl, oldEl.outerHTML);
  10003.         oldEl.removeEventListener('change', this);
  10004.         newEl.addEventListener('change', this);
  10005.         newEl.obj = this;
  10006.         this._input = newEl;
  10007.         oldEl.remove();
  10008.     }
  10009.     _showFileThumb() {
  10010.         const { imgFile } = this;
  10011.         if(imgFile) {
  10012.             this._addNewThumb(imgFile.data, imgFile.name, imgFile.type, imgFile.data.byteLength);
  10013.             return;
  10014.         }
  10015.         const file = this._input.files[0];
  10016.         if(file) {
  10017.             readFile(file).then(({ data }) => {
  10018.                 if(this._input.files[0] === file) {
  10019.                     this._addNewThumb(data, file.name, file.type, file.size);
  10020.                 }
  10021.             });
  10022.         }
  10023.     }
  10024.     _toggleDelBtn(isShow) {
  10025.         $toggle(this._btnDel, isShow);
  10026.         $toggle(this._btnRen, Cfg.fileInputs && isShow && this.hasFile);
  10027.         $toggle(this._btnTxt, !isShow);
  10028.     }
  10029.     _toggleDragEvents(el, isAdd) {
  10030.         const name = isAdd ? 'addEventListener' : 'removeEventListener';
  10031.         el[name]('dragover', $pd);
  10032.         el[name]('dragenter', this);
  10033.         el[name]('dragleave', this);
  10034.         el[name]('drop', this);
  10035.     }
  10036. }
  10037.  
  10038. /* ==[ FormCaptcha.js ]=======================================================================================
  10039.                                                    CAPTCHA
  10040. =========================================================================================================== */
  10041.  
  10042. class Captcha {
  10043.     constructor(el, initNum) {
  10044.         this.hasCaptcha = true;
  10045.         this.textEl = null;
  10046.         this.tNum = initNum;
  10047.         this.parentEl = nav.matchesSelector(el, aib.qFormTr) ? el : aib.getCapParent(el);
  10048.         this.isAdded = false;
  10049.         this.isSubmitWait = false;
  10050.         this._isRecap = !aib._02ch && !!$q('[id*="recaptcha"], [class*="recaptcha"]', this.parentEl);
  10051.         this._lastUpdate = null;
  10052.         this.originHTML = this.parentEl.innerHTML;
  10053.         $hide(this.parentEl);
  10054.         if(!this._isRecap) {
  10055.             this.parentEl.innerHTML = '';
  10056.         }
  10057.     }
  10058.     addCaptcha() {
  10059.         if(this.isAdded) { // Run this function only once
  10060.             return;
  10061.         }
  10062.         this.isAdded = true;
  10063.         if(!this._isRecap) {
  10064.             this.parentEl.innerHTML = this.originHTML;
  10065.             this.textEl = $q('input[type="text"][name*="aptcha"]', this.parentEl);
  10066.         } else {
  10067.             const el = $q('#g-recaptcha, .g-recaptcha');
  10068.             $replace(el, `<div id="g-recaptcha" class="g-recaptcha" data-sitekey="${
  10069.                 el.getAttribute('data-sitekey') }"></div>`);
  10070.         }
  10071.         this.initCapPromise();
  10072.     }
  10073.     handleEvent(e) {
  10074.         switch(e.type) {
  10075.         case 'keypress': {
  10076.             if(!Cfg.captchaLang || e.which === 0) {
  10077.                 return;
  10078.             }
  10079.             const ruUa = 'йцукенгшщзхъїфыівапролджэєячсмитьбюёґ';
  10080.             const en = "qwertyuiop[]]assdfghjkl;''zxcvbnm,.`\\";
  10081.             const code = e.charCode || e.keyCode;
  10082.             let i, chr = String.fromCharCode(code).toLowerCase();
  10083.             if(Cfg.captchaLang === 1) {
  10084.                 if(code < 0x0410 || code > 0x04FF || (i = ruUa.indexOf(chr)) === -1) {
  10085.                     return;
  10086.                 }
  10087.                 chr = en[i];
  10088.             } else {
  10089.                 if(code < 0x0021 || code > 0x007A || (i = en.indexOf(chr)) === -1) {
  10090.                     return;
  10091.                 }
  10092.                 chr = ruUa[i];
  10093.             }
  10094.             insertText(e.target, chr);
  10095.             break;
  10096.         }
  10097.         case 'focus': this.updateOutdated();
  10098.         }
  10099.         $pd(e);
  10100.         e.stopPropagation();
  10101.     }
  10102.     initCapPromise() {
  10103.         const initPromise = aib.initCaptcha ? aib.initCaptcha(this) : null;
  10104.         if(initPromise) {
  10105.             initPromise.then(() => this.showCaptcha(), err => {
  10106.                 if(err instanceof AjaxError) {
  10107.                     this._setUpdateError(err);
  10108.                 } else {
  10109.                     this.hasCaptcha = false;
  10110.                 }
  10111.             });
  10112.         } else if(this.hasCaptcha) {
  10113.             this.showCaptcha(true);
  10114.         }
  10115.     }
  10116.     initImage(img) {
  10117.         img.title = Lng.refresh[lang];
  10118.         img.alt = Lng.loading[lang];
  10119.         img.style.cssText = 'vertical-align: text-bottom; border: none; cursor: pointer;';
  10120.         img.onclick = () => this.refreshCaptcha(true);
  10121.     }
  10122.     initTextEl() {
  10123.         this.textEl.autocomplete = 'off';
  10124.         if(!aib.kusaba && (aib.multiFile || Cfg.fileInputs !== 2)) {
  10125.             this.textEl.placeholder = Lng.cap[lang];
  10126.         }
  10127.         this.textEl.addEventListener('keypress', this);
  10128.         this.textEl.onkeypress = null;
  10129.         this.textEl.addEventListener('focus', this);
  10130.         this.textEl.onfocus = null;
  10131.     }
  10132.     showCaptcha(isUpdateImage = false) {
  10133.         if(!this.textEl) {
  10134.             $show(this.parentEl);
  10135.             if(aib.updateCaptcha) {
  10136.                 aib.updateCaptcha(this, false);
  10137.             } else if(this._isRecap) {
  10138.                 this._updateRecap();
  10139.             }
  10140.             return;
  10141.         }
  10142.         this.initTextEl();
  10143.         let img;
  10144.         if(this._isRecap || !(img = $q('img', this.parentEl))) {
  10145.             $show(this.parentEl);
  10146.             return;
  10147.         }
  10148.         this.initImage(img);
  10149.         const a = img.parentNode;
  10150.         if(a.tagName === 'A') {
  10151.             $replace(a, img);
  10152.         }
  10153.         if(isUpdateImage) {
  10154.             this.refreshCaptcha(false);
  10155.         } else {
  10156.             this._lastUpdate = Date.now();
  10157.         }
  10158.         $show(this.parentEl);
  10159.     }
  10160.     refreshCaptcha(isFocus, isErr = false, tNum = this.tNum) {
  10161.         if(!this.isAdded || tNum !== this.tNum) {
  10162.             this.tNum = tNum;
  10163.             this.isAdded = false;
  10164.             this.hasCaptcha = true;
  10165.             this.textEl = null;
  10166.             $hide(this.parentEl);
  10167.             this.addCaptcha();
  10168.             return;
  10169.         } else if(!this.hasCaptcha && !isErr) {
  10170.             return;
  10171.         }
  10172.         this._lastUpdate = Date.now();
  10173.         if(aib.updateCaptcha) {
  10174.             const updatePromise = aib.updateCaptcha(this, isErr);
  10175.             if(updatePromise) {
  10176.                 updatePromise.then(() => this._updateTextEl(isFocus), err => this._setUpdateError(err));
  10177.             }
  10178.         } else if(this._isRecap) {
  10179.             this._updateRecap();
  10180.         } else if(this.textEl) {
  10181.             this._updateTextEl(isFocus);
  10182.             const img = $q('img', this.parentEl);
  10183.             if(!img) {
  10184.                 return;
  10185.             }
  10186.             if(aib.getCaptchaSrc) {
  10187.                 const src = img.getAttribute('src');
  10188.                 if(src) {
  10189.                     img.src = '';
  10190.                     img.src = aib.getCaptchaSrc(src, tNum);
  10191.                 }
  10192.             } else {
  10193.                 img.click();
  10194.             }
  10195.         }
  10196.     }
  10197.     updateHelper(url, fn) {
  10198.         if(aib._capUpdPromise) {
  10199.             aib._capUpdPromise.cancelPromise();
  10200.         }
  10201.         return (aib._capUpdPromise = $ajax(url).then(xhr => {
  10202.             aib._capUpdPromise = null;
  10203.             fn(xhr);
  10204.         }, err => {
  10205.             if(!(err instanceof CancelError)) {
  10206.                 aib._capUpdPromise = null;
  10207.                 return CancelablePromise.reject(err);
  10208.             }
  10209.         }));
  10210.     }
  10211.     updateOutdated() {
  10212.         if(this._lastUpdate && (Date.now() - this._lastUpdate > Cfg.capUpdTime * 1e3)) {
  10213.             this.refreshCaptcha(false);
  10214.         }
  10215.     }
  10216.  
  10217.     _setUpdateError(e) {
  10218.         if(e) {
  10219.             this.parentEl = e.toString();
  10220.             this.isAdded = false;
  10221.             this.parentEl.onclick = () => {
  10222.                 this.parentEl.onclick = null;
  10223.                 this.addCaptcha();
  10224.             };
  10225.             $show(this.parentEl);
  10226.         }
  10227.     }
  10228.     _updateRecap() {
  10229.         // EXCLUDED FROM FIREFOX EXTENSION - START
  10230.         const script = doc.createElement('script');
  10231.         script.type = 'text/javascript';
  10232.         script.src = aib.prot + '//www.google.com/recaptcha/api.js';
  10233.         doc.head.appendChild(script);
  10234.         setTimeout(() => script.remove(), 1e5);
  10235.         // EXCLUDED FROM FIREFOX EXTENSION - END
  10236.     }
  10237.     _updateTextEl(isFocus) {
  10238.         if(this.textEl) {
  10239.             this.textEl.value = '';
  10240.             if(isFocus) {
  10241.                 this.textEl.focus();
  10242.             }
  10243.         }
  10244.     }
  10245. }
  10246.  
  10247. /* ==[ Posts.js ]=============================================================================================
  10248.                                                    POSTS
  10249. =========================================================================================================== */
  10250.  
  10251. class AbstractPost {
  10252.     constructor(thr, num, isOp) {
  10253.         this.isOp = isOp;
  10254.         this.kid = null;
  10255.         this.num = num;
  10256.         this.ref = new RefMap(this);
  10257.         this.thr = thr;
  10258.         this._hasEvents = false;
  10259.         this._linkDelay = 0;
  10260.         this._menu = null;
  10261.         this._menuDelay = 0;
  10262.     }
  10263.     get btnFav() {
  10264.         const value = $q('.de-btn-fav, .de-btn-fav-sel', this.btns);
  10265.         Object.defineProperty(this, 'btnFav', { value });
  10266.         return value;
  10267.     }
  10268.     get btnHide() {
  10269.         const value = this.btns.firstChild;
  10270.         Object.defineProperty(this, 'btnHide', { value });
  10271.         return value;
  10272.     }
  10273.     get images() {
  10274.         const value = new PostImages(this);
  10275.         Object.defineProperty(this, 'images', { value });
  10276.         return value;
  10277.     }
  10278.     get mp3Obj() {
  10279.         const value = $bBegin(this.msg, '<div class="de-mp3"></div>');
  10280.         Object.defineProperty(this, 'mp3Obj', { value });
  10281.         return value;
  10282.     }
  10283.     * refLinks() {
  10284.         const links = $Q('a', this.msg);
  10285.         for(let lNum, i = 0, len = links.length; i < len; ++i) {
  10286.             const link = links[i];
  10287.             const tc = link.textContent;
  10288.             if(tc[0] !== '>' || tc[1] !== '>' || !(lNum = parseInt(tc.substr(2), 10))) {
  10289.                 continue;
  10290.             }
  10291.             yield [link, lNum];
  10292.         }
  10293.     }
  10294.     get msg() {
  10295.         const value = $q(aib.qPostMsg, this.el);
  10296.         Object.defineProperty(this, 'msg', { value, configurable: true });
  10297.         return value;
  10298.     }
  10299.     get trunc() {
  10300.         let value = null;
  10301.         const el = aib.qTrunc && $q(aib.qTrunc, this.el);
  10302.         if(el && /long|full comment|gekürzt|слишком|длинн|мног|полн/i.test(el.textContent)) {
  10303.             value = el;
  10304.         }
  10305.         Object.defineProperty(this, 'trunc', { value, configurable: true });
  10306.         return value;
  10307.     }
  10308.     get videos() {
  10309.         const value = Cfg.embedYTube ? new Videos(this) : null;
  10310.         Object.defineProperty(this, 'videos', { value });
  10311.         return value;
  10312.     }
  10313.     addFuncs() {
  10314.         RefMap.updateRefMap(this, true);
  10315.         embedAudioLinks(this);
  10316.     }
  10317.     handleEvent(e) {
  10318.         let temp, el = fixEventEl(e.target);
  10319.         const { type } = e;
  10320.         const isOutEvent = type === 'mouseout';
  10321.         const isPview = this instanceof Pview;
  10322.         if(type === 'click') {
  10323.             switch(e.button) {
  10324.             case 0: break;
  10325.             case 1: e.stopPropagation(); // Skip the click on wheel button
  10326.                 /* falls through */
  10327.             default: return;
  10328.             }
  10329.             if(this._menu) { // Hide the dropdown menu after the click on its option
  10330.                 this._menu.removeMenu();
  10331.                 this._menu = null;
  10332.             }
  10333.             switch(el.tagName) {
  10334.             case 'A':
  10335.                 // Click on YouTube link - show/hide player or thumbnail
  10336.                 if(el.classList.contains('de-video-link')) {
  10337.                     this.videos.clickLink(el, Cfg.embedYTube);
  10338.                     $pd(e);
  10339.                     return;
  10340.                 }
  10341.                 // Check if the link is not an image container
  10342.                 if(!(temp = el.firstElementChild) || temp.tagName !== 'IMG') {
  10343.                     temp = el.parentNode;
  10344.                     if(temp === this.trunc) { // Click on "truncated message" link
  10345.                         this._getFullMsg(temp, false);
  10346.                         $pd(e);
  10347.                         e.stopPropagation();
  10348.                     } else if(Cfg.insertNum && pr.form && (this._pref === temp || this._pref === el) &&
  10349.                         !/Reply|Ответ/.test(el.textContent)
  10350.                     ) { // Click on post number link - show quick reply or redirect with an #anchor
  10351.                         $pd(e);
  10352.                         e.stopPropagation();
  10353.                         if(!Cfg.showRepBtn) {
  10354.                             quotetxt = deWindow.getSelection().toString();
  10355.                             pr.showQuickReply(isPview ? Pview.topParent : this, this.num, !isPview, false);
  10356.                             quotetxt = '';
  10357.                         } else if(pr.isQuick || (aib.t && pr.isHidden)) {
  10358.                             pr.showQuickReply(isPview ? Pview.topParent : this, this.num, false, true);
  10359.                         } else if(aib.t) {
  10360.                             const formText = pr.txta.value;
  10361.                             const isOnNewLine = formText === '' || formText.slice(-1) === '\n';
  10362.                             insertText(pr.txta, `>>${ this.num }${ isOnNewLine ? '\n' : '' }`);
  10363.                         } else {
  10364.                             deWindow.location.assign(el.href.replace(/#i/, '#'));
  10365.                         }
  10366.                     } else if((temp = el.textContent)[0] === '>' &&
  10367.                         temp[1] === '>' && !temp[2].includes('/')
  10368.                     ) { // Click on >>link - scroll to the referenced post
  10369.                         const post = pByNum.get(+temp.match(/\d+/));
  10370.                         if(post) {
  10371.                             post.selectAndScrollTo();
  10372.                         }
  10373.                     }
  10374.                     return;
  10375.                 }
  10376.                 el = temp; // The link is an image container
  10377.                 /* falls through */
  10378.             case 'IMG': // Click on attached image - expand/collapse
  10379.                 if(el.classList.contains('de-video-thumb')) {
  10380.                     if(Cfg.embedYTube === 1) {
  10381.                         const { videos } = this;
  10382.                         videos.currentLink.classList.add('de-current');
  10383.                         videos.setPlayer(videos.playerInfo, el.classList.contains('de-ytube'));
  10384.                         $pd(e);
  10385.                     }
  10386.                 } else if(Cfg.expandImgs !== 0) {
  10387.                     this._clickImage(el, e);
  10388.                 }
  10389.                 return;
  10390.             case 'OBJECT':
  10391.             case 'VIDEO': // Click on attached video - expand/collapse
  10392.                 if(Cfg.expandImgs !== 0 && !ExpandableImage.isControlClick(e)) {
  10393.                     this._clickImage(el, e);
  10394.                 }
  10395.                 return;
  10396.             }
  10397.             if(aib.makaba) {
  10398.                 // Makaba: Click on like/dislike elements
  10399.                 let c = el.classList;
  10400.                 if(c.contains('post__rate') || c[0] === 'like-div' || c[0] === 'dislike-div' ||
  10401.                     (temp = el.parentNode) && (
  10402.                         (c = temp.classList).contains('post__rate') ||
  10403.                         c[0] === 'like-div' ||
  10404.                         c[0] === 'dislike-div') ||
  10405.                     (temp = temp.parentNode) && (
  10406.                         (c = temp.className) === 'like-div' ||
  10407.                         c === 'dislike-div')
  10408.                 ) {
  10409.                     const task = temp.id.split('-')[0];
  10410.                     const num = +temp.id.match(/\d+/);
  10411.                     $ajax(`/makaba/likes.fcgi?task=${ task }&board=${ aib.b }&num=${ num }`).then(xhr => {
  10412.                         const data = JSON.parse(xhr.responseText);
  10413.                         if(data.Status !== 'OK') {
  10414.                             $popup('err-2chlike', data.Reason);
  10415.                             return;
  10416.                         }
  10417.                         temp.classList.add(`${ task }-div-checked`, `post__rate_${ task }d`);
  10418.                         const countEl = $q(`.${ task }-count, #${ task }-count${ num }`, temp);
  10419.                         countEl.textContent = +countEl.textContent + 1;
  10420.                     }, () => $popup('err-2chlike', Lng.noConnect[lang]));
  10421.                 }
  10422.                 // Makaba: Click on "truncated message" link
  10423.                 if(el.classList.contains('expand-large-comment')) {
  10424.                     this._getFullMsg(el, false);
  10425.                     $pd(e);
  10426.                     e.stopPropagation();
  10427.                 }
  10428.             }
  10429.             // Click on post buttons
  10430.             switch(el.classList[0]) {
  10431.             case 'de-btn-expthr': this.thr.loadPosts('all'); return;
  10432.             case 'de-btn-fav': this.thr.toggleFavState(true, isPview ? this : null); return;
  10433.             case 'de-btn-fav-sel': this.thr.toggleFavState(false, isPview ? this : null); return;
  10434.             case 'de-btn-hide':
  10435.             case 'de-btn-hide-user':
  10436.             case 'de-btn-unhide':
  10437.             case 'de-btn-unhide-user': this.setUserVisib(!this.isHidden); return;
  10438.             case 'de-btn-reply':
  10439.                 pr.showQuickReply(isPview ? Pview.topParent : this, this.num, !isPview, false);
  10440.                 quotetxt = '';
  10441.                 return;
  10442.             case 'de-btn-sage': Spells.addSpell(9, '', false); return;
  10443.             case 'de-btn-stick': this.toggleSticky(true); return;
  10444.             case 'de-btn-stick-on': this.toggleSticky(false); return;
  10445.             }
  10446.             return;
  10447.         }
  10448.         if(!this._hasEvents) {
  10449.             this._hasEvents = true;
  10450.             this.el.addEventListener('click', this, true);
  10451.             this.el.addEventListener('mouseout', this, true);
  10452.         }
  10453.         // Mouseover/mouseout on YouTube links
  10454.         if(el.classList.contains('de-video-link')) {
  10455.             if(aib.makaba && !el.videoInfo) {
  10456.                 const origMsg = this.msg.firstChild;
  10457.                 this.videos.updatePost($Q('.de-video-link', origMsg),
  10458.                     $Q('.de-video-link', origMsg.nextSibling), true);
  10459.             }
  10460.             if(Cfg.embedYTube === 2) {
  10461.                 this.videos.toggleFloatedThumb(el, isOutEvent);
  10462.             }
  10463.         }
  10464.         // Mouseover/mouseout on attached images/videos - update title
  10465.         if(!isOutEvent && Cfg.expandImgs && el.tagName === 'IMG' && !el.classList.contains('de-fullimg') &&
  10466.             (temp = this.images.getImageByEl(el)) && (temp.isImage || temp.isVideo)
  10467.         ) {
  10468.             el.title = Cfg.expandImgs === 1 ? Lng.expImgInline[lang] : Lng.expImgFull[lang];
  10469.         }
  10470.         // Mouseover/mouseout on post buttons - update title, add/delete dropdown menu
  10471.         switch(el.classList[0]) {
  10472.         case 'de-post-btns': el.removeAttribute('title'); return;
  10473.         case 'de-btn-reply': {
  10474.             const title = this.btns.title = this.isOp ? Lng.replyToThr[lang] : Lng.replyToPost[lang];
  10475.             if(Cfg.showRepBtn === 1) {
  10476.                 if(!isOutEvent) {
  10477.                     quotetxt = deWindow.getSelection().toString();
  10478.                 }
  10479.                 this._addMenu(el, isOutEvent,
  10480.                     `<span class="de-menu-item" info="post-reply">${ title }</span>` +
  10481.                     (aib.reportForm ? `<span class="de-menu-item" info="post-report">${
  10482.                         this.num === this.thr.num ? Lng.reportThr[lang] : Lng.reportPost[lang] }</span>` : ''
  10483.                     ) +
  10484.                     (Cfg.markMyPosts || Cfg.markMyLinks ? `<span class="de-menu-item" info="post-markmy">${
  10485.                         MyPosts.has(this.num) ? Lng.deleteMyPost[lang] : Lng.markMyPost[lang] }</span>` : ''
  10486.                     ));
  10487.             }
  10488.             return;
  10489.         }
  10490.         case 'de-btn-hide':
  10491.         case 'de-btn-hide-user':
  10492.         case 'de-btn-unhide':
  10493.         case 'de-btn-unhide-user':
  10494.             this.btns.title = this.isOp ? Lng.toggleThr[lang] : Lng.togglePost[lang];
  10495.             if(Cfg.showHideBtn === 1) {
  10496.                 this._addMenu(el, isOutEvent,
  10497.                     (this instanceof Pview ? pByNum.get(this.num) : this)._getMenuHide());
  10498.             }
  10499.             return;
  10500.         case 'de-btn-expthr':
  10501.             this.btns.title = Lng.expandThr[lang];
  10502.             this._addMenu(el, isOutEvent, arrTags(Lng.selExpandThr[lang],
  10503.                 '<span class="de-menu-item" info="thr-exp">', '</span>'));
  10504.             return;
  10505.         case 'de-btn-fav': this.btns.title = Lng.addFav[lang]; return;
  10506.         case 'de-btn-fav-sel': this.btns.title = Lng.delFav[lang]; return;
  10507.         case 'de-btn-sage': this.btns.title = 'SAGE'; return;
  10508.         case 'de-btn-stick': this.btns.title = Lng.attachPview[lang]; return;
  10509.         case 'de-btn-src':
  10510.             if(el.parentNode.className !== 'de-fullimg-info') {
  10511.                 this._addMenu(el, isOutEvent, Menu.getMenuImgSrc(el));
  10512.             }
  10513.             return;
  10514.         // Mouseover/mouseout on >>links - show/delete post previews
  10515.         default:
  10516.             if(!Cfg.linksNavig || el.tagName !== 'A' || el.isNotRefLink) {
  10517.                 return;
  10518.             }
  10519.             if(!el.textContent.startsWith('>>')) {
  10520.                 el.isNotRefLink = true;
  10521.                 return;
  10522.             }
  10523.             // Don't use classList here, 'de-link-postref ' should be first
  10524.             el.className = 'de-link-postref ' + el.className;
  10525.             /* falls through */
  10526.         case 'de-link-backref':
  10527.         case 'de-link-postref':
  10528.             if(!Cfg.linksNavig) {
  10529.                 return;
  10530.             }
  10531.             if(isOutEvent) { // Mouseout - We need to delete previews
  10532.                 clearTimeout(this._linkDelay);
  10533.                 if(!(aib.getPostOfEl(fixEventEl(e.relatedTarget)) instanceof Pview) && Pview.top) {
  10534.                     Pview.top.markToDel(); // If cursor is not over one of previews - delete all previews
  10535.                 } else if(this.kid) {
  10536.                     this.kid.markToDel(); // If cursor is over any preview - delete its kids
  10537.                 }
  10538.             } else { // Mouseover - we need to show a preview for this link
  10539.                 this._linkDelay = setTimeout(() => (this.kid = Pview.showPview(this, el)), Cfg.linksOver);
  10540.             }
  10541.             $pd(e);
  10542.             e.stopPropagation();
  10543.         }
  10544.     }
  10545.     toggleFavBtn(isEnable) {
  10546.         const elClass = isEnable ? 'de-btn-fav-sel' : 'de-btn-fav';
  10547.         if(this.btnFav) {
  10548.             this.btnFav.setAttribute('class', elClass);
  10549.         }
  10550.         if(this.thr.btnFav) {
  10551.             this.thr.btnFav.setAttribute('class', elClass);
  10552.         }
  10553.     }
  10554.     updateMsg(newMsg, sRunner) {
  10555.         let videoExt, videoLinks;
  10556.         const origMsg = aib.dobrochan ? this.msg.firstElementChild : this.msg;
  10557.         if(Cfg.embedYTube) {
  10558.             videoExt = $q('.de-video-ext', origMsg);
  10559.             videoLinks = $Q(':not(.de-video-ext) > .de-video-link', origMsg);
  10560.         }
  10561.         $replace(origMsg, newMsg);
  10562.         Object.defineProperties(this, {
  10563.             msg   : { configurable: true, value: newMsg },
  10564.             trunc : { configurable: true, value: null }
  10565.         });
  10566.         Post.Сontent.removeTempData(this);
  10567.         if(Cfg.embedYTube) {
  10568.             this.videos.updatePost(videoLinks, $Q('a[href*="youtu"], a[href*="vimeo.com"]', newMsg), false);
  10569.             if(videoExt) {
  10570.                 newMsg.appendChild(videoExt);
  10571.             }
  10572.         }
  10573.         this.addFuncs();
  10574.         sRunner.runSpells(this);
  10575.         embedPostMsgImages(this.el);
  10576.         if(this.isHidden) {
  10577.             this.hideContent(this.isHidden);
  10578.         }
  10579.         closePopup('load-fullmsg');
  10580.     }
  10581.  
  10582.     _addMenu(el, isOutEvent, html) {
  10583.         if(!this.menu || this.menu.parentEl !== el) {
  10584.             if(isOutEvent) {
  10585.                 clearTimeout(this._menuDelay);
  10586.             } else {
  10587.                 this._menuDelay = setTimeout(() => this._showMenu(el, html), Cfg.linksOver);
  10588.             }
  10589.         }
  10590.     }
  10591.     _clickImage(el, e) {
  10592.         const image = this.images.getImageByEl(el);
  10593.         if(!image || (!image.isImage && !image.isVideo)) {
  10594.             return;
  10595.         }
  10596.         image.expandImg((Cfg.expandImgs === 1) ^ e.ctrlKey, e);
  10597.         $pd(e);
  10598.         e.stopPropagation();
  10599.     }
  10600.     _getFullMsg(truncEl, isInit) {
  10601.         if(aib.deleteTruncMsg) {
  10602.             aib.deleteTruncMsg(this, truncEl, isInit);
  10603.             return;
  10604.         }
  10605.         if(!isInit) {
  10606.             $popup('load-fullmsg', Lng.loading[lang], true);
  10607.         }
  10608.         ajaxLoad(aib.getThrUrl(aib.b, this.tNum)).then(form => {
  10609.             let sourceEl;
  10610.             const maybeSpells = new Maybe(SpellsRunner);
  10611.             if(this.isOp) {
  10612.                 sourceEl = form;
  10613.             } else {
  10614.                 const posts = $Q(aib.qRPost, form);
  10615.                 for(let i = 0, len = posts.length; i < len; ++i) {
  10616.                     const post = posts[i];
  10617.                     if(this.num === aib.getPNum(post)) {
  10618.                         sourceEl = post;
  10619.                         break;
  10620.                     }
  10621.                 }
  10622.             }
  10623.             if(sourceEl) {
  10624.                 this.updateMsg(aib.fixHTML(doc.adoptNode($q(aib.qPostMsg, sourceEl))), maybeSpells.value);
  10625.                 truncEl.remove();
  10626.             }
  10627.             if(maybeSpells.hasValue) {
  10628.                 maybeSpells.value.endSpells();
  10629.             }
  10630.         }, emptyFn);
  10631.     }
  10632.     _showMenu(el, html) {
  10633.         if(this._menu) {
  10634.             this._menu.removeMenu();
  10635.         }
  10636.         this._menu = new Menu(el, html,
  10637.             el => (this instanceof Pview ? pByNum.get(this.num) : this)._clickMenu(el), false);
  10638.         this._menu.onremove = () => (this._menu = null);
  10639.     }
  10640. }
  10641.  
  10642. class Post extends AbstractPost {
  10643.     constructor(el, thr, num, count, isOp, prev) {
  10644.         super(thr, num, isOp);
  10645.         this.count = count;
  10646.         this.el = el;
  10647.         this.isDeleted = false;
  10648.         this.isHidden = false;
  10649.         this.isOmitted = false;
  10650.         this.isViewed = false;
  10651.         this.next = null;
  10652.         this.prev = prev;
  10653.         this.spellHidden = false;
  10654.         this.userToggled = false;
  10655.         this._selRange = null;
  10656.         this._selText = '';
  10657.         if(prev) {
  10658.             prev.next = this;
  10659.         }
  10660.         pByEl.set(el, this);
  10661.         pByNum.set(num, this);
  10662.         let isMyPost = MyPosts.has(num);
  10663.         if(isMyPost) {
  10664.             this.el.classList.add('de-mypost');
  10665.         } else if(localData && this.el.classList.contains('de-mypost')) {
  10666.             MyPosts.set(num, thr.num);
  10667.             isMyPost = true;
  10668.         }
  10669.         el.classList.add(isOp ? 'de-oppost' : 'de-reply');
  10670.         this.sage = aib.getSage(el);
  10671.         this.btns = $aEnd(this._pref = $q(aib.qPostRef, el),
  10672.             '<span class="de-post-btns">' + Post.getPostBtns(isOp, aib.t) +
  10673.             (this.sage ? '<svg class="de-btn-sage"><use xlink:href="#de-symbol-post-sage"/></svg>' : '') +
  10674.             (isOp ? '' : `<span class="de-post-counter">${ count + 1 }</span>`) +
  10675.             (isMyPost ? '<span class="de-post-counter-you">(You)</span>' : '') + '</span>');
  10676.         this.counterEl = isOp ? null : $q('.de-post-counter', this.btns);
  10677.         if(Cfg.expandTrunc && this.trunc) {
  10678.             this._getFullMsg(this.trunc, true);
  10679.         }
  10680.         el.addEventListener('mouseover', this, true);
  10681.     }
  10682.     static addMark(postEl, forced) {
  10683.         if(!doc.hidden && !forced) {
  10684.             Post.clearMarks();
  10685.         } else {
  10686.             if(!Post.hasNew) {
  10687.                 Post.hasNew = true;
  10688.                 doc.addEventListener('click', Post.clearMarks, true);
  10689.             }
  10690.             postEl.classList.add('de-new-post');
  10691.         }
  10692.     }
  10693.     static clearMarks() {
  10694.         if(Post.hasNew) {
  10695.             Post.hasNew = false;
  10696.             $each($Q('.de-new-post'), el => el.classList.remove('de-new-post'));
  10697.             doc.removeEventListener('click', Post.clearMarks, true);
  10698.         }
  10699.     }
  10700.     static getPostBtns(isOp, noExpThr) {
  10701.         return '<svg class="de-btn-hide"><use class="de-btn-hide-use" xlink:href="#de-symbol-post-hide"/>' +
  10702.             '<use class="de-btn-unhide-use" xlink:href="#de-symbol-post-unhide"/></svg>' +
  10703.             '<svg class="de-btn-reply"><use xlink:href="#de-symbol-post-reply"/></svg>' + (isOp ?
  10704.             (noExpThr ? '' : '<svg class="de-btn-expthr"><use xlink:href="#de-symbol-post-expthr"/></svg>') +
  10705.                 '<svg class="de-btn-fav"><use xlink:href="#de-symbol-post-fav"/></svg>' : '');
  10706.     }
  10707.     static findSameText(pNum, isHidden, words, curPost) {
  10708.         const curWords = Post.getWrds(curPost.text);
  10709.         const len = curWords.length;
  10710.         let i = words.length;
  10711.         const olen = i;
  10712.         let _olen = i;
  10713.         let n = 0;
  10714.         if(len < olen * 0.4 || len > olen * 3) {
  10715.             return;
  10716.         }
  10717.         while(i--) {
  10718.             if(olen > 6 && words[i].length < 3) {
  10719.                 _olen--;
  10720.                 continue;
  10721.             }
  10722.             let j = len;
  10723.             while(j--) {
  10724.                 if(curWords[j] === words[i] || words[i].match(/>>\d+/) && curWords[j].match(/>>\d+/)) {
  10725.                     n++;
  10726.                 }
  10727.             }
  10728.         }
  10729.         if(n < _olen * 0.4 || len > _olen * 3) {
  10730.             return;
  10731.         }
  10732.         if(isHidden) {
  10733.             if(curPost.spellHidden) {
  10734.                 Post.Note.reset();
  10735.             } else {
  10736.                 curPost.setVisib(false);
  10737.             }
  10738.             if(curPost.userToggled) {
  10739.                 HiddenPosts.removeStorage(curPost.num);
  10740.                 curPost.userToggled = false;
  10741.             }
  10742.         } else {
  10743.             curPost.setUserVisib(true, true, 'similar to >>' + pNum);
  10744.         }
  10745.         return false;
  10746.     }
  10747.     static getWrds(text) {
  10748.         return text.replace(/\s+/g, ' ').replace(/[^a-zа-яё ]/ig, '').trim().substring(0, 800).split(' ');
  10749.     }
  10750.     static hideContent(headerEl, btnHide, isUser, isHide) {
  10751.         if(!isHide) {
  10752.             btnHide.setAttribute('class', isUser ? 'de-btn-hide-user' : 'de-btn-hide');
  10753.             $each($Q('.de-post-hiddencontent', headerEl.parentNode),
  10754.                 el => el.classList.remove('de-post-hiddencontent'));
  10755.             return;
  10756.         }
  10757.         if(aib.t) {
  10758.             Thread.first.hidCounter++;
  10759.         }
  10760.         btnHide.setAttribute('class', isUser ? 'de-btn-unhide-user' : 'de-btn-unhide');
  10761.         if(headerEl) {
  10762.             for(let el = headerEl.nextElementSibling; el; el = el.nextElementSibling) {
  10763.                 el.classList.add('de-post-hiddencontent');
  10764.             }
  10765.         }
  10766.     }
  10767.     get banned() {
  10768.         const value = aib.getBanId(this.el);
  10769.         Object.defineProperty(this, 'banned', { value, writable: true });
  10770.         return value;
  10771.     }
  10772.     get bottom() {
  10773.         return (this.isOp && this.isHidden ? this.thr.el.previousElementSibling : this.el)
  10774.             .getBoundingClientRect().bottom;
  10775.     }
  10776.     get headerEl() {
  10777.         return new Post.Сontent(this).headerEl;
  10778.     }
  10779.     get html() {
  10780.         return new Post.Сontent(this).html;
  10781.     }
  10782.     get nextInThread() {
  10783.         const post = this.next;
  10784.         return !post || post.count === 0 ? null : post;
  10785.     }
  10786.     get nextNotDeleted() {
  10787.         let post = this.nextInThread;
  10788.         while(post && post.isDeleted) {
  10789.             post = post.nextInThread;
  10790.         }
  10791.         return post;
  10792.     }
  10793.     get note() {
  10794.         const value = new Post.Note(this);
  10795.         Object.defineProperty(this, 'note', { value });
  10796.         return value;
  10797.     }
  10798.     get posterName() {
  10799.         return new Post.Сontent(this).posterName;
  10800.     }
  10801.     get posterTrip() {
  10802.         return new Post.Сontent(this).posterTrip;
  10803.     }
  10804.     get subj() {
  10805.         return new Post.Сontent(this).subj;
  10806.     }
  10807.     get text() {
  10808.         return new Post.Сontent(this).text;
  10809.     }
  10810.     get title() {
  10811.         return new Post.Сontent(this).title;
  10812.     }
  10813.     get tNum() {
  10814.         return this.thr.num;
  10815.     }
  10816.     get top() {
  10817.         return (this.isOp && this.isHidden ? this.thr.el.previousElementSibling : this.el)
  10818.             .getBoundingClientRect().top;
  10819.     }
  10820.     get wrap() {
  10821.         return new Post.Сontent(this).wrap;
  10822.     }
  10823.     addFuncs() {
  10824.         super.addFuncs();
  10825.         if(isExpImg) {
  10826.             this.toggleImages(true, false);
  10827.         }
  10828.     }
  10829.     deleteCounter() {
  10830.         this.isDeleted = true;
  10831.         this.counterEl.textContent = Lng.deleted[lang];
  10832.         this.counterEl.classList.add('de-post-counter-deleted');
  10833.         this.el.classList.add('de-post-removed');
  10834.         this.wrap.classList.add('de-wrap-removed');
  10835.     }
  10836.     deletePost(isRemovePost) {
  10837.         if(isRemovePost) {
  10838.             this.wrap.remove();
  10839.             pByEl.delete(this.el);
  10840.             pByNum.delete(this.num);
  10841.             if(this.isHidden) {
  10842.                 this.ref.unhideRef();
  10843.             }
  10844.             RefMap.updateRefMap(this, false);
  10845.             if((this.prev.next = this.next)) {
  10846.                 this.next.prev = this.prev;
  10847.             }
  10848.             return;
  10849.         }
  10850.         this.deleteCounter();
  10851.         ($q('input[type="checkbox"]', this.el) || {}).disabled = true;
  10852.     }
  10853.     getAdjacentVisPost(toUp) {
  10854.         let post = toUp ? this.prev : this.next;
  10855.         while(post) {
  10856.             if(post.thr.isHidden) {
  10857.                 post = toUp ? post.thr.op.prev : post.thr.last.next;
  10858.             } else if(post.isHidden || post.isOmitted) {
  10859.                 post = toUp ? post.prev : post.next;
  10860.             } else {
  10861.                 return post;
  10862.             }
  10863.         }
  10864.         return null;
  10865.     }
  10866.     hideContent(needToHide) {
  10867.         if(this.isOp) {
  10868.             if(!aib.t) {
  10869.                 $toggle(this.thr.el, !needToHide);
  10870.                 $toggle(this.thr.btns, !needToHide);
  10871.             }
  10872.         } else {
  10873.             Post.hideContent(this.headerEl, this.btnHide, this.userToggled, needToHide);
  10874.         }
  10875.     }
  10876.     select() {
  10877.         if(this.isOp) {
  10878.             if(this.isHidden) {
  10879.                 this.thr.el.previousElementSibling.classList.add('de-selected');
  10880.             }
  10881.             this.thr.el.classList.add('de-selected');
  10882.         } else {
  10883.             this.el.classList.add('de-selected');
  10884.         }
  10885.     }
  10886.     selectAndScrollTo(scrollNode = this.el) {
  10887.         scrollTo(0, deWindow.pageYOffset + scrollNode.getBoundingClientRect().top -
  10888.             Post.sizing.wHeight / 2 + scrollNode.clientHeight / 2);
  10889.         if(HotKeys.enabled) {
  10890.             if(HotKeys.cPost) {
  10891.                 HotKeys.cPost.unselect();
  10892.             }
  10893.             HotKeys.cPost = this;
  10894.             HotKeys.lastPageOffset = deWindow.pageYOffset;
  10895.         } else {
  10896.             const el = $q('.de-selected');
  10897.             if(el) {
  10898.                 el.unselect();
  10899.             }
  10900.         }
  10901.         this.select();
  10902.     }
  10903.     setUserVisib(isHide, isSave = true, note = null) {
  10904.         this.userToggled = true;
  10905.         this.setVisib(isHide, note);
  10906.         if(this.isOp || this.isHidden === isHide) {
  10907.             const hideClass = isHide ? 'de-btn-unhide-user' : 'de-btn-hide-user';
  10908.             this.btnHide.setAttribute('class', hideClass);
  10909.             if(this.isOp) {
  10910.                 this.thr.btnHide.setAttribute('class', hideClass);
  10911.             }
  10912.         }
  10913.         if(isSave) {
  10914.             const { num } = this;
  10915.             HiddenPosts.set(num, this.thr.num, isHide);
  10916.             if(this.isOp) {
  10917.                 if(isHide) {
  10918.                     HiddenThreads.set(num, num, this.title);
  10919.                 } else {
  10920.                     HiddenThreads.removeStorage(num);
  10921.                 }
  10922.             }
  10923.             sendStorageEvent('__de-post', {
  10924.                 hide   : isHide,
  10925.                 brd    : aib.b,
  10926.                 num,
  10927.                 thrNum : this.thr.num,
  10928.                 title  : this.isOp ? this.title : ''
  10929.             });
  10930.         }
  10931.         this.ref.toggleRef(isHide, false);
  10932.     }
  10933.     setVisib(isHide, note = null) {
  10934.         if(this.isHidden === isHide) {
  10935.             if(isHide && note) {
  10936.                 this.note.set(note);
  10937.             }
  10938.             return;
  10939.         }
  10940.         if(this.isOp) {
  10941.             this.thr.isHidden = isHide;
  10942.         } else {
  10943.             if(Cfg.delHiddPost === 1 || Cfg.delHiddPost === 2) {
  10944.                 this.wrap.classList.toggle('de-hidden', isHide);
  10945.             } else {
  10946.                 this._pref.onmouseover = this._pref.onmouseout = !isHide ? null : e => {
  10947.                     const yOffset = deWindow.pageYOffset;
  10948.                     this.hideContent(e.type === 'mouseout');
  10949.                     scrollTo(deWindow.pageXOffset, yOffset);
  10950.                 };
  10951.             }
  10952.         }
  10953.         if(Cfg.strikeHidd) {
  10954.             setTimeout(() => this._strikePostNum(isHide), 50);
  10955.         }
  10956.         if(isHide) {
  10957.             this.note.set(note);
  10958.         } else {
  10959.             this.note.hideNote();
  10960.         }
  10961.         this.hideContent(this.isHidden = isHide);
  10962.     }
  10963.     spellHide(note) {
  10964.         this.spellHidden = true;
  10965.         if(!this.userToggled) {
  10966.             this.setVisib(true, note);
  10967.             this.ref.hideRef();
  10968.         }
  10969.     }
  10970.     spellUnhide() {
  10971.         this.spellHidden = false;
  10972.         if(!this.userToggled) {
  10973.             this.setVisib(false);
  10974.             this.ref.unhideRef();
  10975.         }
  10976.     }
  10977.     toggleImages(isExpand = !this.images.expanded, isExpandVideos = true) {
  10978.         for(const image of this.images) {
  10979.             if((image.isImage || isExpandVideos && image.isVideo) && (image.expanded ^ isExpand)) {
  10980.                 if(isExpand) {
  10981.                     image.expandImg(true, null);
  10982.                 } else {
  10983.                     image.collapseImg(null);
  10984.                 }
  10985.             }
  10986.         }
  10987.     }
  10988.     unselect() {
  10989.         if(this.isOp) {
  10990.             const el = $id('de-thr-hid-' + this.num);
  10991.             if(el) {
  10992.                 el.classList.remove('de-selected');
  10993.             }
  10994.             this.thr.el.classList.remove('de-selected');
  10995.         } else {
  10996.             this.el.classList.remove('de-selected');
  10997.         }
  10998.     }
  10999.  
  11000.     _clickMenu(el) {
  11001.         const isHide = !this.isHidden;
  11002.         const isPview = this instanceof Pview;
  11003.         const { num } = this;
  11004.         switch(el.getAttribute('info')) {
  11005.         case 'hide-sel': {
  11006.             let { startContainer: start, endContainer: end } = this._selRange;
  11007.             if(start.nodeType === 3) {
  11008.                 start = start.parentNode;
  11009.             }
  11010.             if(end.nodeType === 3) {
  11011.                 end = end.parentNode;
  11012.             }
  11013.             const inMsgSel = `${ aib.qPostMsg }, ${ aib.qPostMsg } *`;
  11014.             if((nav.matchesSelector(start, inMsgSel) && nav.matchesSelector(end, inMsgSel)) || (
  11015.                 nav.matchesSelector(start, aib.qPostSubj) &&
  11016.                 nav.matchesSelector(end, aib.qPostSubj)
  11017.             )) {
  11018.                 if(this._selText.includes('\n')) {
  11019.                     Spells.addSpell(1 /* #exp */,
  11020.                         `/${ quoteReg(this._selText).replace(/\r?\n/g, '\\n') }/`, false);
  11021.                 } else {
  11022.                     Spells.addSpell(0 /* #words */, this._selText.toLowerCase(), false);
  11023.                 }
  11024.             } else {
  11025.                 dummy.innerHTML = '';
  11026.                 dummy.appendChild(this._selRange.cloneContents());
  11027.                 Spells.addSpell(2 /* #exph */,
  11028.                     `/${ quoteReg(dummy.innerHTML.replace(/^<[^>]+>|<[^>]+>$/g, '')) }/`, false);
  11029.             }
  11030.             return;
  11031.         }
  11032.         case 'hide-name': Spells.addSpell(6 /* #name */, this.posterName, false); return;
  11033.         case 'hide-trip': Spells.addSpell(7 /* #trip */, this.posterTrip, false); return;
  11034.         case 'hide-img': {
  11035.             const { weight: w, width: wi, height: h } = this.images.firstAttach;
  11036.             Spells.addSpell(8 /* #img */, [0, [w, w], [wi, wi, h, h]], false);
  11037.             return;
  11038.         }
  11039.         case 'hide-imgn':
  11040.             Spells.addSpell(3 /* #imgn */, `/${ quoteReg(this.images.firstAttach.name) }/`, false);
  11041.             return;
  11042.         case 'hide-ihash':
  11043.             ImagesHashStorage.getHash(this.images.firstAttach).then(hash => {
  11044.                 if(hash !== -1) {
  11045.                     Spells.addSpell(4 /* #ihash */, hash, false);
  11046.                 }
  11047.             });
  11048.             return;
  11049.         case 'hide-noimg': Spells.addSpell(0x108 /* (#all & !#img) */, '', true); return;
  11050.         case 'hide-text': {
  11051.             const words = Post.getWrds(this.text);
  11052.             for(let post = Thread.first.op; post; post = post.next) {
  11053.                 Post.findSameText(num, !isHide, words, post);
  11054.             }
  11055.             return;
  11056.         }
  11057.         case 'hide-notext': Spells.addSpell(0x10B /* (#all & !#tlen) */, '', true); return;
  11058.         case 'hide-refs':
  11059.             this.ref.toggleRef(isHide, true);
  11060.             this.setUserVisib(isHide);
  11061.             return;
  11062.         case 'hide-refsonly': Spells.addSpell(0 /* #words */, '>>' + num, false); return;
  11063.         case 'post-markmy': {
  11064.             const isAdd = !MyPosts.has(num);
  11065.             if(isAdd) {
  11066.                 MyPosts.set(num, this.thr.num);
  11067.             } else {
  11068.                 MyPosts.removeStorage(num);
  11069.             }
  11070.             this.el.classList.toggle('de-mypost', isAdd);
  11071.             $each($Q(`[de-form] ${ aib.qPostMsg } a[href$="${ aib.anchor + num }"]`), el => {
  11072.                 const post = aib.getPostOfEl(el);
  11073.                 if(post.el !== this.el) {
  11074.                     el.classList.toggle('de-ref-you', isAdd);
  11075.                     post.el.classList.toggle('de-mypost-reply', isAdd);
  11076.                 }
  11077.             });
  11078.             return;
  11079.         }
  11080.         case 'post-reply':
  11081.             pr.showQuickReply(isPview ? Pview.topParent : this, num, !isPview, false);
  11082.             quotetxt = '';
  11083.             return;
  11084.         case 'post-report': aib.reportForm(num, this.thr.num); return;
  11085.         case 'thr-exp': {
  11086.             const task = +el.textContent.match(/\d+/);
  11087.             this.thr.loadPosts(!task ? 'all' : task === 10 ? 'more' : task);
  11088.         }
  11089.         }
  11090.     }
  11091.     _getMenuHide() {
  11092.         const item = name => `<span info="hide-${ name }" class="de-menu-item">${
  11093.             Lng.selHiderMenu[name][lang] }</span>`;
  11094.         const sel = deWindow.getSelection();
  11095.         const ssel = sel.toString().trim();
  11096.         if(ssel) {
  11097.             this._selText = ssel;
  11098.             this._selRange = sel.getRangeAt(0);
  11099.         }
  11100.         return `${ ssel ? item('sel') : '' }${
  11101.             this.posterName ? item('name') : '' }${
  11102.             this.posterTrip ? item('trip') : '' }${
  11103.             this.images.hasAttachments ? item('img') + item('imgn') + item('ihash') : item('noimg') }${
  11104.             this.text ? item('text') : item('notext') }${
  11105.             !Cfg.hideRefPsts && this.ref.hasMap ? item('refs') : '' }${
  11106.             item('refsonly') }`;
  11107.     }
  11108.     _strikePostNum(isHide) {
  11109.         const { num } = this;
  11110.         if(isHide) {
  11111.             Post.hiddenNums.add(+num);
  11112.         } else {
  11113.             Post.hiddenNums.delete(+num);
  11114.         }
  11115.         $each($Q(`[de-form] a[href$="${ aib.anchor + num }"]`), el => {
  11116.             el.classList.toggle('de-link-hid', isHide);
  11117.             if(Cfg.removeHidd && el.classList.contains('de-link-backref')) {
  11118.                 const refMapEl = el.parentNode;
  11119.                 if(isHide === !$q('.de-link-backref:not(.de-link-hid)', refMapEl)) {
  11120.                     $toggle(refMapEl, !isHide);
  11121.                 }
  11122.             }
  11123.         });
  11124.     }
  11125. }
  11126. Post.hasNew = false;
  11127. Post.hiddenNums = new Set();
  11128. Post.Сontent = class PostContent extends TemporaryContent {
  11129.     constructor(post) {
  11130.         super(post);
  11131.         if(this._isInited) {
  11132.             return;
  11133.         }
  11134.         this._isInited = true;
  11135.         this.el = post.el;
  11136.         this.post = post;
  11137.     }
  11138.     get headerEl() {
  11139.         const value = $q(aib.qPostHeader, this.el);
  11140.         Object.defineProperty(this, 'headerEl', { value });
  11141.         return value;
  11142.     }
  11143.     get html() {
  11144.         const value = this.el.outerHTML;
  11145.         Object.defineProperty(this, 'html', { value });
  11146.         return value;
  11147.     }
  11148.     get posterName() {
  11149.         const pName = $q(aib.qPostName, this.el);
  11150.         const value = pName ? pName.textContent.trim().replace(/\s/g, ' ') : '';
  11151.         Object.defineProperty(this, 'posterName', { value });
  11152.         return value;
  11153.     }
  11154.     get posterTrip() {
  11155.         const pTrip = $q(aib.qPostTrip, this.el);
  11156.         const value = pTrip ? pTrip.textContent : '';
  11157.         Object.defineProperty(this, 'posterTrip', { value });
  11158.         return value;
  11159.     }
  11160.     get subj() {
  11161.         const subj = $q(aib.qPostSubj, this.el);
  11162.         const value = subj ? subj.textContent : '';
  11163.         Object.defineProperty(this, 'subj', { value });
  11164.         return value;
  11165.     }
  11166.     get text() {
  11167.         const value = this.post.msg.innerHTML
  11168.             .replace(/<\/?(?:br|p|li)[^>]*?>/gi, '\n')
  11169.             .replace(/<[^>]+?>/g, '')
  11170.             .replace(/&gt;/g, '>')
  11171.             .replace(/&lt;/g, '<')
  11172.             .replace(/&nbsp;/g, '\u00A0').trim();
  11173.         Object.defineProperty(this, 'text', { value });
  11174.         return value;
  11175.     }
  11176.     get title() {
  11177.         const value = this.subj || this.text.substring(0, 70).replace(/\s+/g, ' ');
  11178.         Object.defineProperty(this, 'title', { value });
  11179.         return value;
  11180.     }
  11181.     get wrap() {
  11182.         const value = aib.getPostWrap(this.el, this.post.isOp);
  11183.         Object.defineProperty(this, 'wrap', { value });
  11184.         return value;
  11185.     }
  11186. };
  11187. Post.Note = class PostNote {
  11188.     constructor(post) {
  11189.         this.text = null;
  11190.         this._post = post;
  11191.         this.isHideThr = this._post.isOp && !aib.t; // Hide threads only on board
  11192.         if(!this.isHideThr) {
  11193.             // Create usual post note
  11194.             this._noteEl = this.textEl = $bEnd(post.btns, '<span class="de-post-note"></span>');
  11195.             return;
  11196.         }
  11197.         // Create a stub before the thread, that also hides thread by CSS
  11198.         this._noteEl = $bBegin(post.thr.el, `<div class="${ aib.cReply } de-thr-hid" id="de-thr-hid-${
  11199.             post.num }">${ Lng.hiddenThr[lang] }: <a href="#">№${ post.num }</a>
  11200.             <span class="de-thread-note"></span>
  11201.         </div>`);
  11202.         this._aEl = $q('a', this._noteEl);
  11203.         this.textEl = this._aEl.nextElementSibling;
  11204.     }
  11205.     hideNote() {
  11206.         if(this.isHideThr) {
  11207.             this._aEl.onmouseover = this._aEl.onmouseout = this._aEl.onclick = null;
  11208.         }
  11209.         $hide(this._noteEl);
  11210.     }
  11211.     reset() {
  11212.         this.text = null;
  11213.         if(this.isHideThr) {
  11214.             this.set(null);
  11215.         } else {
  11216.             this.hideNote();
  11217.         }
  11218.     }
  11219.     set(note) {
  11220.         this.text = note;
  11221.         let text;
  11222.         if(this.isHideThr) {
  11223.             this._aEl.onmouseover = this._aEl.onmouseout = e => this._post.hideContent(e.type === 'mouseout');
  11224.             this._aEl.onclick = e => {
  11225.                 $pd(e);
  11226.                 this._post.setUserVisib(!this._post.isHidden);
  11227.             };
  11228.             text = (this._post.title ? `(${ this._post.title }) ` : '') +
  11229.                 (note ? `[autohide: ${ note }]` : '');
  11230.         } else {
  11231.             text = note ? `autohide: ${ note }` : '';
  11232.         }
  11233.         this.textEl.textContent = text;
  11234.         $show(this._noteEl);
  11235.     }
  11236. };
  11237. Post.sizing = {
  11238.     get dPxRatio() {
  11239.         const value = deWindow.devicePixelRatio || 1;
  11240.         Object.defineProperty(this, 'dPxRatio', { value });
  11241.         return value;
  11242.     },
  11243.     get wHeight() {
  11244.         const value = nav.viewportHeight();
  11245.         if(!this._enabled) {
  11246.             doc.defaultView.addEventListener('resize', this);
  11247.             this._enabled = true;
  11248.         }
  11249.         Object.defineProperties(this, {
  11250.             wHeight : { writable: true, configurable: true, value },
  11251.             wWidth  : { writable: true, configurable: true, value: nav.viewportWidth() }
  11252.         });
  11253.         return value;
  11254.     },
  11255.     get wWidth() {
  11256.         const value = nav.viewportWidth();
  11257.         if(!this._enabled) {
  11258.             doc.defaultView.addEventListener('resize', this);
  11259.             this._enabled = true;
  11260.         }
  11261.         Object.defineProperties(this, {
  11262.             wHeight : { writable: true, configurable: true, value: nav.viewportHeight() },
  11263.             wWidth  : { writable: true, configurable: true, value }
  11264.         });
  11265.         return value;
  11266.     },
  11267.     handleEvent() {
  11268.         this.wHeight = nav.viewportHeight();
  11269.         this.wWidth = nav.viewportWidth();
  11270.     },
  11271.  
  11272.     _enabled: false
  11273. };
  11274.  
  11275. /* ==[ PostPreviews.js ]======================================================================================
  11276.                                                POST PREVIEWS
  11277. =========================================================================================================== */
  11278.  
  11279. class Pview extends AbstractPost {
  11280.     constructor(parent, link, pNum, tNum) {
  11281.         super(parent.thr, pNum, pNum === tNum);
  11282.         this.isSticky = false;
  11283.         this.parent = parent;
  11284.         this.remoteThr = null;
  11285.         this.tNum = tNum;
  11286.         this._isCached = false;
  11287.         this._isLeft = false;
  11288.         this._isTop = false;
  11289.         this._link = link;
  11290.         this._newPos = null;
  11291.         this._offsetTop = 0;
  11292.         this._readDelay = 0;
  11293.         let post = pByNum.get(pNum);
  11294.         if(post && (!post.isOp || !(parent instanceof Pview) || !parent._isCached)) {
  11295.             this._buildPview(post);
  11296.             return;
  11297.         }
  11298.         this._isCached = true;
  11299.         this.brd = link.pathname.match(/^\/?(.+\/)/)[1].replace(aib.res, '').replace(/\/$/, '');
  11300.         if(PviewsCache.has(this.brd + tNum)) {
  11301.             post = PviewsCache.get(this.brd + tNum).getPost(pNum);
  11302.             if(post) {
  11303.                 this._buildPview(post);
  11304.             } else {
  11305.                 this._showPview(this.el = $add(`<div class="${ aib.cReply } de-pview-info de-pview">
  11306.                     ${ Lng.postNotFound[lang] }</div>`));
  11307.             }
  11308.             return;
  11309.         }
  11310.         this._showPview(this.el = $add(`<div class="${ aib.cReply } de-pview-info de-pview">
  11311.             <svg class="de-wait"><use xlink:href="#de-symbol-wait"/></svg>${ Lng.loading[lang] }</div>`));
  11312.  
  11313.         // Get post preview via ajax. Always use DOM parsing.
  11314.         this._loadPromise = ajaxPostsLoad(this.brd, tNum, false, false)
  11315.             .then(pBuilder => this._onload(pBuilder), err => this._onerror(err));
  11316.     }
  11317.     static get topParent() {
  11318.         return Pview.top ? Pview.top.parent : null;
  11319.     }
  11320.     static showPview(parent, link) {
  11321.         const tNum = +(link.pathname.match(/.+?\/[^\d]*(\d+)/) || [0, aib.getPostOfEl(link).tNum])[1];
  11322.         let pNum = link.textContent.match(/\d+/g);
  11323.         pNum = pNum ? +pNum.pop() : tNum;
  11324.         const isTop = !(parent instanceof Pview);
  11325.         let pv = isTop ? Pview.top : parent.kid;
  11326.         clearTimeout(Pview._delTO);
  11327.         if(pv && pv.num === pNum) {
  11328.             if(pv.kid) {
  11329.                 pv.kid.deletePview();
  11330.             }
  11331.             if(pv._link !== link) {
  11332.                 // If cursor hovers new link with the same number - move old preview here
  11333.                 pv._setPosition(link, Cfg.animation);
  11334.                 pv._link.classList.remove('de-link-parent');
  11335.                 link.classList.add('de-link-parent');
  11336.                 pv._link = link;
  11337.                 if(pv.parent.num !== parent.num) {
  11338.                     $each($Q('.de-link-pview', pv.el), el => el.classList.remove('de-link-pview'));
  11339.                     Pview._markLink(pv.el, parent.num);
  11340.                 }
  11341.             }
  11342.             pv.parent = parent;
  11343.         } else if(!Cfg.noNavigHidd || !pByNum.has(pNum) || !pByNum.get(pNum).hidden) {
  11344.             // Show new preview under new link
  11345.             if(pv) {
  11346.                 pv.deletePview();
  11347.             }
  11348.             pv = new Pview(parent, link, pNum, tNum);
  11349.             if(isTop) {
  11350.                 Pview.top = pv;
  11351.             }
  11352.         } else {
  11353.             return null;
  11354.         }
  11355.         return pv;
  11356.     }
  11357.     static updatePosition(scroll) {
  11358.         let pv = Pview.top;
  11359.         if(!pv) {
  11360.             return;
  11361.         }
  11362.         const { parent } = pv;
  11363.         if(parent.isOmitted) {
  11364.             pv.deletePview();
  11365.             return;
  11366.         }
  11367.         if(parent.thr.loadCount === 1 && !parent.el.contains(pv._link)) {
  11368.             const el = parent.ref.getElByNum(pv.num);
  11369.             if(!el) {
  11370.                 pv.deletePview();
  11371.                 return;
  11372.             }
  11373.             pv._link = el;
  11374.         }
  11375.         const cr = parent.isHidden ? parent : pv._link.getBoundingClientRect();
  11376.         const diff = pv._isTop ?
  11377.             pv._offsetTop - deWindow.pageYOffset - cr.bottom :
  11378.             pv._offsetTop + pv.el.offsetHeight - deWindow.pageYOffset - cr.top;
  11379.         if(Math.abs(diff) > 1) {
  11380.             if(scroll) {
  11381.                 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset - diff);
  11382.             }
  11383.             do {
  11384.                 pv._offsetTop -= diff;
  11385.                 pv.el.style.top = Math.max(pv._offsetTop, 0) + 'px';
  11386.             } while((pv = pv.kid));
  11387.         }
  11388.     }
  11389.     get stickBtn() {
  11390.         const value = $q('.de-btn-stick', this.el);
  11391.         Object.defineProperty(this, 'stickBtn', { value });
  11392.         return value;
  11393.     }
  11394.     deletePview() {
  11395.         this.parent.kid = null;
  11396.         this._link.classList.remove('de-link-parent');
  11397.         if(Pview.top === this) {
  11398.             Pview.top = null;
  11399.         }
  11400.         if(this._loadPromise) {
  11401.             this._loadPromise.cancelPromise();
  11402.             this._loadPromise = null;
  11403.         }
  11404.         let vPost = AttachedImage.viewer && AttachedImage.viewer.data.post;
  11405.         let pv = this;
  11406.         do {
  11407.             clearTimeout(pv._readDelay);
  11408.             if(vPost === pv) {
  11409.                 AttachedImage.closeImg();
  11410.                 vPost = null;
  11411.             }
  11412.             const { el } = pv;
  11413.             pByEl.delete(el);
  11414.             if(Cfg.animation) {
  11415.                 $animate(el, 'de-pview-anim', true);
  11416.                 el.style.animationName =
  11417.                     `de-post-close-${ this._isTop ? 't' : 'b' }${ this._isLeft ? 'l' : 'r' }`;
  11418.             } else {
  11419.                 el.remove();
  11420.             }
  11421.         } while((pv = pv.kid));
  11422.     }
  11423.     deleteNonSticky() {
  11424.         let lastSticky = null, pv = this;
  11425.         do {
  11426.             if(pv.isSticky) {
  11427.                 lastSticky = pv;
  11428.             }
  11429.         } while((pv = pv.kid));
  11430.         if(!lastSticky) {
  11431.             this.deletePview();
  11432.         } else if(lastSticky.kid) {
  11433.             lastSticky.kid.deletePview();
  11434.         }
  11435.     }
  11436.     handleEvent(e) {
  11437.         const pv = e.target;
  11438.         if(e.type === 'animationend' && pv.style.animationName) {
  11439.             pv.classList.remove('de-pview-anim');
  11440.             pv.style.cssText = this._newPos;
  11441.             this._newPos = null;
  11442.             $delAll('.de-css-move', doc.head);
  11443.             pv.removeEventListener('animationend', this);
  11444.             return;
  11445.         }
  11446.         let isOverEvent = false;
  11447.         checkMouse: do {
  11448.             switch(e.type) {
  11449.             case 'mouseover': isOverEvent = true; break;
  11450.             case 'mouseout': break;
  11451.             default: break checkMouse;
  11452.             }
  11453.             const el = fixEventEl(e.relatedTarget);
  11454.             if(!el ||
  11455.                 isOverEvent && (el.tagName !== 'A' || el.isNotRefLink) ||
  11456.                 el !== this.el && !this.el.contains(el)
  11457.             ) {
  11458.                 if(isOverEvent) {
  11459.                     this.mouseEnter();
  11460.                 } else if(Pview.top) {
  11461.                     Pview.top.markToDel();
  11462.                 }
  11463.             }
  11464.         } while(false);
  11465.         if(!this.loading) {
  11466.             super.handleEvent(e);
  11467.         }
  11468.     }
  11469.     markToDel() {
  11470.         clearTimeout(Pview._delTO);
  11471.         Pview._delTO = setTimeout(() => this.deleteNonSticky(), Cfg.linksOut);
  11472.     }
  11473.     mouseEnter() {
  11474.         if(this.kid) {
  11475.             this.kid.markToDel();
  11476.         } else {
  11477.             clearTimeout(Pview._delTO);
  11478.         }
  11479.     }
  11480.     setUserVisib() {
  11481.         const post = pByNum.get(this.num);
  11482.         const isHide = post.isHidden;
  11483.         post.setUserVisib(!isHide);
  11484.         Pview.updatePosition(true);
  11485.         $each($Q(`.de-btn-pview-hide[de-num="${ this.num }"]`), el => {
  11486.             el.setAttribute('class',
  11487.                 `${ isHide ? 'de-btn-hide-user' : 'de-btn-unhide-user' } de-btn-pview-hide`);
  11488.             el.parentNode.classList.toggle('de-post-hide', !isHide);
  11489.         });
  11490.     }
  11491.     toggleSticky(isEnabled) {
  11492.         this.stickBtn.setAttribute('class', isEnabled ? 'de-btn-stick-on' : 'de-btn-stick');
  11493.         this.isSticky = isEnabled;
  11494.     }
  11495.  
  11496.     static _markLink(el, num) {
  11497.         $each($Q(`a[href*="${ num }"]`, el),
  11498.             el => el.textContent.startsWith('>>' + num) && el.classList.add('de-link-pview'));
  11499.     }
  11500.     async _buildPview(post) {
  11501.         $del(this.el);
  11502.         const { num } = this;
  11503.         const pv = this.el = post.el.cloneNode(true);
  11504.         pByEl.set(pv, this);
  11505.         const isMyPost = MyPosts.has(num);
  11506.         pv.className = `${ aib.cReply } de-pview${
  11507.             post.isViewed ? ' de-viewed' : '' }${ isMyPost ? ' de-mypost' : '' }` +
  11508.             `${ post.el.classList.contains('de-mypost-reply') ? ' de-mypost-reply' : '' }`;
  11509.         $show(pv);
  11510.         $each($Q('.de-post-hiddencontent', pv), el => el.classList.remove('de-post-hiddencontent'));
  11511.         if(Cfg.linksNavig) {
  11512.             Pview._markLink(pv, this.parent.num);
  11513.         }
  11514.         this._pref = $q(aib.qPostRef, pv);
  11515.         this._link.classList.add('de-link-parent');
  11516.         const { isOp } = this;
  11517.         let f;
  11518.         const isFav = isOp && (post.thr.isFav ||
  11519.             ((f = (await readFavorites())[aib.host]) && (f = f[this.brd]) && (num in f)));
  11520.         const isCached = post instanceof CacheItem;
  11521.         const pCountHtml = (post.isDeleted ? ` de-post-counter-deleted">${ Lng.deleted[lang] }</span>` :
  11522.             `">${ isOp ? '(OP)' : post.count + +!(aib.JsonBuilder && isCached) }</span>`) +
  11523.             (isMyPost ? '<span class="de-post-counter-you">(You)</span>' : '');
  11524.         const pText = '<svg class="de-btn-reply"><use xlink:href="#de-symbol-post-reply"/></svg>' +
  11525.             (isOp ? `<svg class="${ isFav ? 'de-btn-fav-sel' : 'de-btn-fav' }">` +
  11526.                 '<use xlink:href="#de-symbol-post-fav"></use></svg>' : '') +
  11527.             (post.sage ? '<svg class="de-btn-sage"><use xlink:href="#de-symbol-post-sage"/></svg>' : '') +
  11528.             '<svg class="de-btn-stick"><use xlink:href="#de-symbol-post-stick"/></svg>' +
  11529.             '<span class="de-post-counter' + pCountHtml;
  11530.         if(isCached) {
  11531.             if(isOp) {
  11532.                 this.remoteThr = post.thr;
  11533.             }
  11534.             this.btns = $aEnd(this._pref, `<span class="de-post-btns">${ pText }</span>`);
  11535.             embedAudioLinks(this);
  11536.             if(Cfg.embedYTube) {
  11537.                 new VideosParser().parse(this).endParser();
  11538.             }
  11539.             embedPostMsgImages(pv);
  11540.             processImgInfoLinks(this);
  11541.         } else {
  11542.             const btnsEl = this.btns = $q('.de-post-btns', pv);
  11543.             $del($q('.de-post-counter', btnsEl));
  11544.             if(post.isHidden) {
  11545.                 btnsEl.classList.add('de-post-hide');
  11546.             }
  11547.             btnsEl.innerHTML = `<svg class="de-btn-${ post.isHidden ? 'unhide' : 'hide' }${
  11548.                 post.userToggled ? '-user' : '' } de-btn-pview-hide" de-num="${ num }"><!--
  11549.                 --><use class="de-btn-hide-use" xlink:href="#de-symbol-post-hide"/><!--
  11550.                 --><use class="de-btn-unhide-use" xlink:href="#de-symbol-post-unhide"/></svg>${ pText }`;
  11551.             $delAll(`${ !aib.t && isOp ? aib.qOmitted + ', ' : '' }.de-fullimg-wrap, .de-fullimg-after`, pv);
  11552.             $each($Q(aib.qPostImg, pv), el => $show(el.parentNode));
  11553.             const link = $q('.de-link-parent', pv);
  11554.             if(link) {
  11555.                 link.classList.remove('de-link-parent');
  11556.             }
  11557.             if(Cfg.embedYTube && post.videos.hasLinks) {
  11558.                 if(post.videos.playerInfo !== null) {
  11559.                     Object.defineProperty(this, 'videos',
  11560.                         { value: new Videos(this, $q('.de-video-obj', pv), post.videos.playerInfo) });
  11561.                 }
  11562.                 this.videos.updatePost($Q('.de-video-link', post.el), $Q('.de-video-link', pv), true);
  11563.             }
  11564.             if(Cfg.addImgs) {
  11565.                 $each($Q('.de-img-embed', pv), $show);
  11566.             }
  11567.             if(Cfg.markViewed) {
  11568.                 this._readDelay = setTimeout(post => {
  11569.                     if(!post.isViewed) {
  11570.                         post.el.classList.add('de-viewed');
  11571.                         post.isViewed = true;
  11572.                     }
  11573.                     const arr = (sesStorage['de-viewed'] || '').split(',');
  11574.                     arr.push(post.num);
  11575.                     sesStorage['de-viewed'] = arr;
  11576.                 }, post.text.length > 100 ? 2e3 : 500, post);
  11577.             }
  11578.         }
  11579.         pv.addEventListener('click', this, true);
  11580.         this._showPview(pv);
  11581.     }
  11582.     _onerror(err) {
  11583.         if(!(err instanceof CancelError)) {
  11584.             this.el.innerHTML = (err instanceof AjaxError) && err.code === 404 ?
  11585.                 Lng.postNotFound[lang] : getErrorMessage(err);
  11586.         }
  11587.     }
  11588.     _onload(pBuilder) {
  11589.         const b = this.brd;
  11590.         const { num } = this.parent;
  11591.         const post = new PviewsCache(pBuilder, b, this.tNum).getPost(this.num);
  11592.         if(post && (aib.b !== b || !post.ref.hasMap || !post.ref.has(num))) {
  11593.             (post.ref.hasMap ? $q('.de-refmap', post.el) : $aEnd(post.msg, '<div class="de-refmap"></div>'))
  11594.                 .insertAdjacentHTML('afterbegin', `<a class="de-link-backref" href="${
  11595.                     aib.getThrUrl(b, this.parent.tNum) + aib.anchor + num }">&gt;&gt;${
  11596.                     aib.b === b ? '' : `/${ aib.b }/` }${ num }</a><span class="de-refcomma">, </span>`);
  11597.         }
  11598.         if(post) {
  11599.             this._buildPview(post);
  11600.         } else {
  11601.             this.el.innerHTML = Lng.postNotFound[lang];
  11602.         }
  11603.     }
  11604.     _setPosition(link, isAnim) {
  11605.         let oldCSS;
  11606.         const cr = link.getBoundingClientRect();
  11607.         const offX = cr.left + deWindow.pageXOffset + cr.width / 2;
  11608.         const offY = cr.top;
  11609.         const bWidth = nav.viewportWidth();
  11610.         const isLeft = offX < bWidth / 2;
  11611.         const pv = this.el;
  11612.         const temp = isLeft ? offX : offX - Math.min(parseInt(pv.offsetWidth, 10), offX - 10);
  11613.         const lmw = `max-width:${ bWidth - temp - 10 }px; left:${ temp }px;`;
  11614.         const { style } = pv;
  11615.         if(isAnim) {
  11616.             oldCSS = style.cssText;
  11617.         }
  11618.         style.cssText = (isAnim ? 'opacity: 0; ' : '') + lmw;
  11619.         let top = pv.offsetHeight;
  11620.         const isTop = offY + top + cr.height < nav.viewportHeight() || offY - top < 5;
  11621.         top = deWindow.pageYOffset + (isTop ? offY + cr.height : offY - top);
  11622.         this._offsetTop = top;
  11623.         this._isLeft = isLeft;
  11624.         this._isTop = isTop;
  11625.         if(!isAnim) {
  11626.             style.top = top + 'px';
  11627.             return;
  11628.         }
  11629.         const uId = 'de-movecss-' + Math.round(Math.random() * 1e3);
  11630.         $css(`@keyframes ${ uId } { to { ${ lmw } top:${ top }px; } }`).className = 'de-css-move';
  11631.         if(this._newPos) {
  11632.             style.cssText = this._newPos;
  11633.             pv.removeEventListener('animationend', this);
  11634.         } else {
  11635.             style.cssText = oldCSS;
  11636.         }
  11637.         this._newPos = `${ lmw } top:${ top }px;`;
  11638.         pv.addEventListener('animationend', this);
  11639.         pv.classList.add('de-pview-anim');
  11640.         style.animationName = uId;
  11641.     }
  11642.     _showMenu(el, html) {
  11643.         super._showMenu(el, html);
  11644.         this._menu.onover = () => this.mouseEnter();
  11645.         this._menu.onout = () => Pview.top.markToDel();
  11646.     }
  11647.     _showPview(el) {
  11648.         el.addEventListener('mouseover', this, true);
  11649.         el.addEventListener('mouseout', this, true);
  11650.         this.thr.form.el.appendChild(el);
  11651.         this._setPosition(this._link, false);
  11652.         if(Cfg.animation) {
  11653.             el.addEventListener('animationend', function aEvent() {
  11654.                 el.removeEventListener('animationend', aEvent);
  11655.                 el.classList.remove('de-pview-anim');
  11656.                 el.style.animationName = '';
  11657.             });
  11658.             el.classList.add('de-pview-anim');
  11659.             el.style.animationName = `de-post-open-${ this._isTop ? 't' : 'b' }${ this._isLeft ? 'l' : 'r' }`;
  11660.         }
  11661.     }
  11662. }
  11663. Pview.top = null;
  11664. Pview._delTO = null;
  11665.  
  11666. class CacheItem {
  11667.     constructor(pBuilder, thrUrl, count) {
  11668.         this._pBuilder = pBuilder;
  11669.         this._thrUrl = thrUrl;
  11670.         this.count = count;
  11671.         this.isDeleted = false;
  11672.         this.isInited = false;
  11673.         this.isOp = count === 0;
  11674.         this.isViewed = false;
  11675.     }
  11676.     * refLinks() {
  11677.         yield * this._pBuilder.getRefLinks(this.count, this._thrUrl);
  11678.     }
  11679.     get msg() {
  11680.         const value = $q(aib.qPostMsg, this.el);
  11681.         Object.defineProperty(this, 'msg', { value });
  11682.         return value;
  11683.     }
  11684.     get ref() {
  11685.         const value = new RefMap(this);
  11686.         Object.defineProperty(this, 'ref', { value });
  11687.         return value;
  11688.     }
  11689.     get sage() {
  11690.         const value = aib.getSage(this.el);
  11691.         Object.defineProperty(this, 'sage', { value });
  11692.         return value;
  11693.     }
  11694.     get title() {
  11695.         return new Post.Сontent(this).title;
  11696.     }
  11697.     get el() {
  11698.         const value = this.isOp ? this._pBuilder.getOpEl() : this._pBuilder.getPostEl(this.count - 1);
  11699.         Object.defineProperty(this, 'el', { value: doc.adoptNode(value) });
  11700.         return value;
  11701.     }
  11702.     get thr() {
  11703.         let value = null;
  11704.         if(this.isOp) {
  11705.             const pcount = this._pBuilder.length;
  11706.             value = { lastNum: this._pBuilder.getPNum(pcount - 1), pcount };
  11707.             Object.defineProperty(value, 'title', { get: () => this.title });
  11708.         }
  11709.         Object.defineProperty(this, 'thr', { value });
  11710.         return value;
  11711.     }
  11712. }
  11713.  
  11714. class PviewsCache extends TemporaryContent {
  11715.     constructor(pBuilder, b, tNum) {
  11716.         super(b + tNum);
  11717.         if(this._isInited) {
  11718.             return;
  11719.         }
  11720.         this._isInited = true;
  11721.         const lPByNum = new Map();
  11722.         const thrUrl = aib.getThrUrl(b, tNum);
  11723.         lPByNum.set(tNum, new CacheItem(pBuilder, thrUrl, 0));
  11724.         for(let i = 0; i < pBuilder.length; ++i) {
  11725.             lPByNum.set(pBuilder.getPNum(i), new CacheItem(pBuilder, thrUrl, i + 1));
  11726.         }
  11727.         DelForm.tNums.add(tNum);
  11728.         this._b = b;
  11729.         this._posts = lPByNum;
  11730.         if(Cfg.linksNavig) {
  11731.             RefMap.gen(lPByNum);
  11732.         }
  11733.     }
  11734.     getPost(num) {
  11735.         const post = this._posts.get(num);
  11736.         if(post && !post.isInited) {
  11737.             if(this._b === aib.b && pByNum.has(num)) {
  11738.                 post.ref.makeUnion(pByNum.get(num).ref);
  11739.             }
  11740.             if(post.ref.hasMap) {
  11741.                 post.ref.initPostRef(post._thrUrl,
  11742.                     Cfg.strikeHidd && Post.hiddenNums.size ? Post.hiddenNums : null);
  11743.             }
  11744.             post.isInited = true;
  11745.         }
  11746.         return post;
  11747.     }
  11748. }
  11749. PviewsCache.purgeSecs = 3e5;
  11750.  
  11751. /* ==[ PostImages.js ]========================================================================================
  11752.                                                    IMAGES
  11753.               images expanding (in post / by center), navigate buttons, image-links embedding
  11754. =========================================================================================================== */
  11755.  
  11756. // Navigation buttons for expanding of images/videos by center
  11757. class ImagesNavigBtns {
  11758.     constructor(viewerObj) {
  11759.         const btns = $bEnd(docBody, `<div style="display: none;">
  11760.             <div id="de-img-btn-prev" class="de-img-btn" de-title="${ Lng.prevImg[lang] }">
  11761.                 <svg><use xlink:href="#de-symbol-img-btn-arrow"/></svg></div>
  11762.             <div id="de-img-btn-next" class="de-img-btn" de-title="${ Lng.nextImg[lang] }">
  11763.                 <svg><use xlink:href="#de-symbol-img-btn-arrow"/></svg></div>
  11764.             <div id="de-img-btn-auto" class="de-img-btn de-img-btn-none" title="${ Lng.autoPlayOn[lang] }">
  11765.                 <svg><use xlink:href="#de-symbol-img-btn-auto"/></svg></div>
  11766.             <div id="de-img-btn-rotate" class="de-img-btn" title="${ Lng.rotateImg[lang] }">
  11767.                 <svg><use xlink:href="#de-symbol-img-btn-rotate"/></svg></div></div>`);
  11768.         [this.prevBtn, this.nextBtn, this.autoBtn] = [...btns.children];
  11769.         this._btns = btns;
  11770.         this._btnsStyle = btns.style;
  11771.         this._hideTmt = 0;
  11772.         this._isHidden = true;
  11773.         this._oldX = -1;
  11774.         this._oldY = -1;
  11775.         this._viewer = viewerObj;
  11776.         doc.defaultView.addEventListener('mousemove', this);
  11777.         btns.addEventListener('mouseover', this);
  11778.     }
  11779.     handleEvent(e) {
  11780.         switch(e.type) {
  11781.         case 'mousemove': {
  11782.             const { clientX: curX, clientY: curY } = e;
  11783.             if(this._oldX !== curX || this._oldY !== curY) {
  11784.                 this._oldX = curX;
  11785.                 this._oldY = curY;
  11786.                 this.showBtns();
  11787.             }
  11788.             return;
  11789.         }
  11790.         case 'mouseover':
  11791.             if(!this.hasEvents) {
  11792.                 this.hasEvents = true;
  11793.                 this._btns.addEventListener('mouseout', this);
  11794.                 this._btns.addEventListener('click', this);
  11795.             }
  11796.             if(!this._isHidden) {
  11797.                 clearTimeout(this._hideTmt);
  11798.                 KeyEditListener.setTitle(this.prevBtn, 4);
  11799.                 KeyEditListener.setTitle(this.nextBtn, 17);
  11800.             }
  11801.             return;
  11802.         case 'mouseout': this._setHideTmt(); return;
  11803.         case 'click': {
  11804.             const parent = e.target.parentNode;
  11805.             const viewer = this._viewer;
  11806.             switch(parent.id) {
  11807.             case 'de-img-btn-next': viewer.navigate(true); return;
  11808.             case 'de-img-btn-prev': viewer.navigate(false); return;
  11809.             case 'de-img-btn-rotate': viewer.rotateView(true); return;
  11810.             case 'de-img-btn-auto':
  11811.                 this.autoBtn.title = (viewer.isAutoPlay = !viewer.isAutoPlay) ?
  11812.                     Lng.autoPlayOff[lang] : Lng.autoPlayOn[lang];
  11813.                 viewer.toggleVideoLoop();
  11814.                 parent.classList.toggle('de-img-btn-auto-on');
  11815.             }
  11816.         }
  11817.         }
  11818.     }
  11819.     hideBtns() {
  11820.         this._btnsStyle.display = 'none';
  11821.         this._isHidden = true;
  11822.         this._oldX = this._oldY = -1;
  11823.     }
  11824.     removeBtns() {
  11825.         this._btns.remove();
  11826.         doc.defaultView.removeEventListener('mousemove', this);
  11827.         clearTimeout(this._hideTmt);
  11828.     }
  11829.     showBtns() {
  11830.         if(this._isHidden) {
  11831.             this._btnsStyle.removeProperty('display');
  11832.             this._isHidden = false;
  11833.             this._setHideTmt();
  11834.         }
  11835.     }
  11836.  
  11837.     _setHideTmt() {
  11838.         clearTimeout(this._hideTmt);
  11839.         this._hideTmt = setTimeout(() => this.hideBtns(), 2e3);
  11840.     }
  11841. }
  11842.  
  11843. // Expanding of images/videos BY CENTER: resizing, moving, opening, closing
  11844. class ImagesViewer {
  11845.     constructor(data) {
  11846.         this.data = null;
  11847.         this.isAutoPlay = false;
  11848.         this._data = null;
  11849.         this._elStyle = null;
  11850.         this._fullEl = null;
  11851.         this._height = 0;
  11852.         this._minSize = 0;
  11853.         this._moved = false;
  11854.         this._oldL = 0;
  11855.         this._oldT = 0;
  11856.         this._oldX = 0;
  11857.         this._oldY = 0;
  11858.         this._parentEl = null;
  11859.         this._width = 0;
  11860.         this._showFullImg(data);
  11861.     }
  11862.     closeImgViewer(e) {
  11863.         if(this.hasOwnProperty('_btns')) {
  11864.             this._btns.removeBtns();
  11865.         }
  11866.         this._removeFullImg(e);
  11867.     }
  11868.     handleEvent(e) {
  11869.         switch(e.type) {
  11870.         case 'mousedown':
  11871.             if(this.data.isVideo && ExpandableImage.isControlClick(e)) {
  11872.                 return;
  11873.             }
  11874.             this._oldX = e.clientX;
  11875.             this._oldY = e.clientY;
  11876.             docBody.addEventListener('mousemove', this, true);
  11877.             docBody.addEventListener('mouseup', this, true);
  11878.             break;
  11879.         case 'mousemove': {
  11880.             const { clientX: curX, clientY: curY } = e;
  11881.             if(curX !== this._oldX || curY !== this._oldY) {
  11882.                 this._oldL = parseInt(this._elStyle.left, 10) + curX - this._oldX;
  11883.                 this._elStyle.left = this._oldL + 'px';
  11884.                 this._oldT = parseInt(this._elStyle.top, 10) + curY - this._oldY;
  11885.                 this._elStyle.top = this._oldT + 'px';
  11886.                 this._oldX = curX;
  11887.                 this._oldY = curY;
  11888.                 this._moved = true;
  11889.             }
  11890.             return;
  11891.         }
  11892.         case 'mouseup':
  11893.             docBody.removeEventListener('mousemove', this, true);
  11894.             docBody.removeEventListener('mouseup', this, true);
  11895.             return;
  11896.         case 'click': {
  11897.             const el = e.target;
  11898.             if(this.data.isVideo && ExpandableImage.isControlClick(e) ||
  11899.                 el.tagName !== 'IMG' &&
  11900.                 el.tagName !== 'VIDEO' &&
  11901.                 !el.classList.contains('de-fullimg-wrap') &&
  11902.                 !el.classList.contains('de-fullimg-wrap-link') &&
  11903.                 !el.classList.contains('de-fullimg-video-hack') &&
  11904.                 el.className !== 'de-fullimg-load'
  11905.             ) {
  11906.                 return;
  11907.             }
  11908.             if(e.button === 0) {
  11909.                 if(this._moved) {
  11910.                     this._moved = false;
  11911.                 } else {
  11912.                     this.closeImgViewer(e);
  11913.                     AttachedImage.viewer = null;
  11914.                 }
  11915.                 e.stopPropagation();
  11916.                 break;
  11917.             }
  11918.             return;
  11919.         }
  11920.         case 'mousewheel':
  11921.             this._handleWheelEvent(e.clientX, e.clientY,
  11922.                 -1 / 40 * ('wheelDeltaY' in e ? e.wheelDeltaY : e.wheelDelta));
  11923.             break;
  11924.         default: // 'wheel' event
  11925.             this._handleWheelEvent(e.clientX, e.clientY, e.deltaY);
  11926.         }
  11927.         $pd(e);
  11928.     }
  11929.     navigate(isForward, isVideoOnly = false) {
  11930.         let { data } = this;
  11931.         data.cancelWebmLoad(this._fullEl);
  11932.         do {
  11933.             data = data.getFollowImg(isForward);
  11934.         } while(data && !data.isVideo && !data.isImage || isVideoOnly && data.isImage);
  11935.         if(data) {
  11936.             this.updateImgViewer(data, true, null);
  11937.             data.post.selectAndScrollTo(data.post.images.first.el);
  11938.         }
  11939.     }
  11940.     rotateView(isNextAngle) {
  11941.         if(isNextAngle) {
  11942.             this.data.rotate += this.data.rotate === 270 ? -270 : 90;
  11943.         }
  11944.         const angle = this.data.rotate;
  11945.         const isVert = angle === 90 || angle === 270;
  11946.         const img = $q('img, video', this._fullEl);
  11947.         img.style.transform = `rotate(${ angle }deg)${
  11948.             angle === 90 ? ' translateY(-100%)' : angle === 270 ? ' translateX(-100%)' : '' }`;
  11949.         img.classList.toggle('de-fullimg-rotated', isVert);
  11950.         img.style.height = `${ (isVert ? this._height / this._width : 1) * 100 }%`;
  11951.         if(this.data.isVideo && nav.firefoxVer >= 59) {
  11952.             img.previousElementSibling.style =
  11953.                 (isVert ? 'width: calc(100% - 40px); height: 100%; ' : '') +
  11954.                 (angle === 90 ? 'right: 0; ' : '') +
  11955.                 (angle === 180 ? 'bottom: 0;' : '');
  11956.         }
  11957.         if(isNextAngle || angle !== 180) {
  11958.             this._rotateFullImg(this._fullEl);
  11959.         }
  11960.     }
  11961.     toggleVideoLoop() {
  11962.         if(this.data.isVideo) {
  11963.             toggleAttr($q('video', this._fullEl), 'loop', '', !this.isAutoPlay);
  11964.         }
  11965.     }
  11966.     updateImgViewer(data, showButtons, e) {
  11967.         this._removeFullImg(e);
  11968.         this._showFullImg(data, showButtons);
  11969.     }
  11970.  
  11971.     get _btns() {
  11972.         const value = new ImagesNavigBtns(this);
  11973.         Object.defineProperty(this, '_btns', { value });
  11974.         return value;
  11975.     }
  11976.     get _zoomFactor() {
  11977.         const value = 1 + (Cfg.zoomFactor / 100);
  11978.         Object.defineProperty(this, '_zoomFactor', { value });
  11979.         return value;
  11980.     }
  11981.     _handleWheelEvent(clientX, clientY, delta) {
  11982.         if(delta === 0) {
  11983.             return;
  11984.         }
  11985.         let width, height;
  11986.         const { _width: oldW, _height: oldH } = this;
  11987.         if(delta > 0) {
  11988.             width = oldW / this._zoomFactor;
  11989.             height = oldH / this._zoomFactor;
  11990.             if(width <= this._minSize && height <= this._minSize) {
  11991.                 return;
  11992.             }
  11993.         } else {
  11994.             width = oldW * this._zoomFactor;
  11995.             height = oldH * this._zoomFactor;
  11996.         }
  11997.         this._width = width;
  11998.         this._height = height;
  11999.         this._elStyle.width = width + 'px';
  12000.         this._elStyle.height = height + 'px';
  12001.         this._oldL = parseInt(clientX - (width / oldW) * (clientX - this._oldL), 10);
  12002.         this._elStyle.left = this._oldL + 'px';
  12003.         this._oldT = parseInt(clientY - (height / oldH) * (clientY - this._oldT), 10);
  12004.         this._elStyle.top = this._oldT + 'px';
  12005.     }
  12006.     _removeFullImg(e) {
  12007.         const { data } = this;
  12008.         data.cancelWebmLoad(this._fullEl);
  12009.         if(data.inPview && data.post.isSticky) {
  12010.             data.post.toggleSticky(false);
  12011.         }
  12012.         this._parentEl.remove();
  12013.         if(e && data.inPview) {
  12014.             data.sendCloseEvent(e, false);
  12015.         }
  12016.     }
  12017.     _resizeFullImg(el) {
  12018.         if(el !== this._fullEl) {
  12019.             return;
  12020.         }
  12021.         let [width, height, minSize] = this.data.computeFullSize();
  12022.         this._minSize = minSize ? minSize / this._zoomFactor : Cfg.minImgSize;
  12023.         if(Post.sizing.wWidth - this._oldL - this._width < 5 ||
  12024.             Post.sizing.wHeight - this._oldT - this._height < 5
  12025.         ) {
  12026.             return;
  12027.         }
  12028.         const cPointX = this._oldL + this._width / 2;
  12029.         const cPointY = this._oldT + this._height / 2;
  12030.         const maxWidth = (Post.sizing.wWidth - cPointX - 2) * 2;
  12031.         const maxHeight = (Post.sizing.wHeight - cPointY - 2) * 2;
  12032.         if(width > maxWidth || height > maxHeight) {
  12033.             const ar = width / height;
  12034.             if(ar > maxWidth / maxHeight) {
  12035.                 width = maxWidth;
  12036.                 height = width / ar;
  12037.             } else {
  12038.                 height = maxHeight;
  12039.                 width = height * ar;
  12040.             }
  12041.             if(minSize && width < minSize || height < minSize) {
  12042.                 this._minSize = Math.max(width, height);
  12043.             }
  12044.         }
  12045.         this._width = width;
  12046.         this._height = height;
  12047.         this._elStyle.width = width + 'px';
  12048.         this._elStyle.height = height + 'px';
  12049.         this._elStyle.left = `${ this._oldL = parseInt(cPointX - width / 2, 10) }px`;
  12050.         this._elStyle.top = `${ this._oldT = parseInt(cPointY - height / 2, 10) }px`;
  12051.     }
  12052.     _rotateFullImg(el) {
  12053.         if(el !== this._fullEl) {
  12054.             return;
  12055.         }
  12056.         const { _width, _height } = this;
  12057.         this._width = _height;
  12058.         this._height = _width;
  12059.         this._elStyle.width = _height + 'px';
  12060.         this._elStyle.height = _width + 'px';
  12061.         const halfWidth = _width / 2;
  12062.         const halfHeight = _height / 2;
  12063.         this._elStyle.left = `${ this._oldL = parseInt(this._oldL + halfWidth - halfHeight, 10) }px`;
  12064.         this._elStyle.top = `${ this._oldT = parseInt(this._oldT + halfHeight - halfWidth, 10) }px`;
  12065.     }
  12066.     _showFullImg(data) {
  12067.         const [width, height, minSize] = data.computeFullSize();
  12068.         this._fullEl = data.getFullImg(false, el => this._resizeFullImg(el), el => this._rotateFullImg(el));
  12069.         this._width = width;
  12070.         this._height = height;
  12071.         this._minSize = minSize ? minSize / this._zoomFactor : Cfg.minImgSize;
  12072.         this._oldL = (Post.sizing.wWidth - width) / 2 - 1;
  12073.         this._oldT = (Post.sizing.wHeight - height) / 2 - 1;
  12074.         const el = $add(`<div class="de-fullimg-center${
  12075.             data.isVideo ? ' de-fullimg-center-video' : '' }" style="top:${ this._oldT -
  12076.             (Cfg.imgInfoLink ? 11 : 0) - (nav.firefoxVer >= 59 && data.isVideo ? 10 : 0) }px; left:${
  12077.             this._oldL }px; width:${ width }px; height:${ height }px; display: block"></div>`);
  12078.         el.appendChild(this._fullEl);
  12079.         if(data.isImage) {
  12080.             $aBegin(this._fullEl, `<a class="de-fullimg-wrap-link" href="${ data.src }"></a>`)
  12081.                 .appendChild($q('img', this._fullEl));
  12082.         }
  12083.         this._elStyle = el.style;
  12084.         this.data = data;
  12085.         this._parentEl = el;
  12086.         el.addEventListener('onwheel' in el ? 'wheel' : 'mousewheel', this, true);
  12087.         el.addEventListener('mousedown', this, true);
  12088.         el.addEventListener('click', this, true);
  12089.         data.srcBtnEvents(this);
  12090.         if(data.inPview && !data.post.isSticky) {
  12091.             data.post.toggleSticky(true);
  12092.         }
  12093.         const btns = this._btns;
  12094.         if(!data.inPview) {
  12095.             btns.showBtns();
  12096.             btns.autoBtn.classList.toggle('de-img-btn-none', !data.isVideo);
  12097.         } else if(this.hasOwnProperty('_btns')) {
  12098.             btns.hideBtns();
  12099.         }
  12100.         data.post.thr.form.el.appendChild(el);
  12101.         this.toggleVideoLoop();
  12102.         if(this.data.rotate) {
  12103.             this.rotateView(false);
  12104.         }
  12105.         data.checkForRedirect(this._fullEl);
  12106.     }
  12107. }
  12108.  
  12109. // Post image/video main initialization
  12110. class ExpandableImage {
  12111.     constructor(post, el, prev) {
  12112.         this.el = el;
  12113.         this.expanded = false;
  12114.         this.next = null;
  12115.         this.post = post;
  12116.         this.prev = prev;
  12117.         this.redirected = false;
  12118.         this.rotate = 0;
  12119.         this._fullEl = null;
  12120.         this._webmTitleLoad = null;
  12121.         if(prev) {
  12122.             prev.next = this;
  12123.         }
  12124.     }
  12125.     static isControlClick(e) {
  12126.         return Cfg.webmControl && e.clientY > (e.target.getBoundingClientRect().bottom - 40);
  12127.     }
  12128.     get height() {
  12129.         return (this._size || [-1, -1])[1];
  12130.     }
  12131.     get inPview() {
  12132.         const value = this.post instanceof Pview;
  12133.         Object.defineProperty(this, 'inPview', { value });
  12134.         return value;
  12135.     }
  12136.     get isImage() {
  12137.         const value = /(jpe?g|png|gif|webp)$/i.test(this.src) ||
  12138.             (this.src.startsWith('blob:') && !this.el.hasAttribute('de-video'));
  12139.         Object.defineProperty(this, 'isImage', { value });
  12140.         return value;
  12141.     }
  12142.     get isVideo() {
  12143.         const value = /(webm|mp4|ogv)(&|$)/i.test(this.src) ||
  12144.             (this.src.startsWith('blob:') && this.el.hasAttribute('de-video'));
  12145.         Object.defineProperty(this, 'isVideo', { value });
  12146.         return value;
  12147.     }
  12148.     get src() {
  12149.         const value = this._getImageSrc();
  12150.         Object.defineProperty(this, 'src', { value, configurable: true });
  12151.         return value;
  12152.     }
  12153.     get width() {
  12154.         return (this._size || [-1, -1])[0];
  12155.     }
  12156.     cancelWebmLoad(fullEl) {
  12157.         if(this.isVideo) {
  12158.             const videoEl = $q('video', fullEl);
  12159.             videoEl.pause();
  12160.             videoEl.removeAttribute('src');
  12161.             videoEl.load();
  12162.         }
  12163.         if(this._webmTitleLoad) {
  12164.             this._webmTitleLoad.cancelPromise();
  12165.             this._webmTitleLoad = null;
  12166.         }
  12167.     }
  12168.     checkForRedirect(fullEl) {
  12169.         if(!aib.getImgRedirectSrc || this.redirected) {
  12170.             return;
  12171.         }
  12172.         aib.getImgRedirectSrc(this.src).then(newSrc => {
  12173.             this.redirected = true;
  12174.             Object.defineProperty(this, 'src', { value: newSrc });
  12175.             $q('img, video', fullEl).src = this.el.src =
  12176.                 this.el.parentNode.href = $q(aib.qImgNameLink, aib.getImgWrap(this.el)).href = newSrc;
  12177.             if(!this.isVideo) {
  12178.                 $q('a', fullEl).href = newSrc;
  12179.             }
  12180.         });
  12181.     }
  12182.     collapseImg(e) { // Collapse an image that expanded in post
  12183.         if(e && this.isVideo && ExpandableImage.isControlClick(e)) {
  12184.             return;
  12185.         }
  12186.         let fullImgTop;
  12187.         if(e) {
  12188.             fullImgTop = e.target.getBoundingClientRect().top;
  12189.         }
  12190.         this.cancelWebmLoad(this._fullEl);
  12191.         this.expanded = false;
  12192.         this._fullEl.remove();
  12193.         this._fullEl = null;
  12194.         $show(this.el.parentNode);
  12195.         (aib.hasPicWrap ? this._getImageParent : this.el.parentNode).nextSibling.remove();
  12196.         if(e) {
  12197.             $pd(e);
  12198.             if(this.inPview) {
  12199.                 this.sendCloseEvent(e, true);
  12200.             }
  12201.             const origImgTop = this.el.getBoundingClientRect().top;
  12202.             if(fullImgTop < 0 || origImgTop < 0) {
  12203.                 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + origImgTop);
  12204.             }
  12205.         }
  12206.     }
  12207.     computeFullSize() {
  12208.         if(!this._size) {
  12209.             if(this.isVideo) {
  12210.                 return [0, 0, null];
  12211.             }
  12212.             const el = new Image();
  12213.             el.src = this.el.src;
  12214.             return [el.width, el.height, null];
  12215.         }
  12216.         let [width, height] = this._size;
  12217.         if(Cfg.resizeDPI) {
  12218.             width /= Post.sizing.dPxRatio;
  12219.             height /= Post.sizing.dPxRatio;
  12220.         }
  12221.         const minSize = this.isVideo ? Math.max(Cfg.minImgSize, Cfg.minWebmWidth) : Cfg.minImgSize;
  12222.         if(width < minSize && height < minSize) {
  12223.             const ar = width / height;
  12224.             if(width > height) {
  12225.                 width = minSize;
  12226.                 height = width / ar;
  12227.             } else {
  12228.                 height = minSize;
  12229.                 width = this.isVideo ? minSize : height * ar;
  12230.             }
  12231.         }
  12232.         const maxWidth = Post.sizing.wWidth - 2;
  12233.         const maxHeight = Post.sizing.wHeight -
  12234.             (Cfg.imgInfoLink ? 24 : 2) - (nav.firefoxVer >= 59 && this.isVideo ? 19 : 0);
  12235.         if(width > maxWidth || height > maxHeight) {
  12236.             const ar = width / height;
  12237.             if(ar > maxWidth / maxHeight) {
  12238.                 width = maxWidth;
  12239.                 height = width / ar;
  12240.             } else {
  12241.                 height = maxHeight;
  12242.                 width = height * ar;
  12243.             }
  12244.             if(width < minSize || height < minSize) {
  12245.                 return [width, height, Math.max(width, height)];
  12246.             }
  12247.         }
  12248.         return [width, height, null];
  12249.     }
  12250.     expandImg(inPost, e) {
  12251.         if(e && !e.bubbles) {
  12252.             return;
  12253.         }
  12254.         if(!inPost) {
  12255.             const { viewer } = AttachedImage;
  12256.             if(!viewer) {
  12257.                 AttachedImage.viewer = new ImagesViewer(this);
  12258.                 return;
  12259.             }
  12260.             if(viewer.data === this) {
  12261.                 viewer.closeImgViewer(e);
  12262.                 AttachedImage.viewer = null;
  12263.                 return;
  12264.             }
  12265.             viewer.updateImgViewer(this, e);
  12266.             return;
  12267.         }
  12268.         let origImgTop;
  12269.         if(e) {
  12270.             origImgTop = e.target.getBoundingClientRect().top;
  12271.         }
  12272.         this.expanded = true;
  12273.         const { el } = this;
  12274.         (aib.hasPicWrap ? this._getImageParent : el.parentNode).insertAdjacentHTML('afterend',
  12275.             '<div class="de-fullimg-after"></div>');
  12276.         this._fullEl = this.getFullImg(true, null, null);
  12277.         this._fullEl.addEventListener('click', e => this.collapseImg(e), true);
  12278.         this.srcBtnEvents(this);
  12279.         $hide(el.parentNode);
  12280.         $after(el.parentNode, this._fullEl);
  12281.         this.checkForRedirect(this._fullEl);
  12282.         if(e) {
  12283.             const fullImgTop = this._fullEl.getBoundingClientRect().top;
  12284.             if(fullImgTop < 0 || origImgTop < 0) {
  12285.                 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + fullImgTop);
  12286.             }
  12287.         }
  12288.     }
  12289.     getFollowImg(isForward) {
  12290.         const nImage = isForward ? this.next : this.prev;
  12291.         if(nImage) {
  12292.             return nImage;
  12293.         }
  12294.         let imgs, { post } = this;
  12295.         do {
  12296.             post = post.getAdjacentVisPost(!isForward);
  12297.             if(!post) {
  12298.                 post = isForward ? Thread.first.op : Thread.last.last;
  12299.                 if(post.isHidden || post.thr.isHidden) {
  12300.                     post = post.getAdjacentVisPost(!isForward);
  12301.                     if(!post) {
  12302.                         return null;
  12303.                     }
  12304.                 }
  12305.             }
  12306.             imgs = post.images;
  12307.         } while(imgs.first === null);
  12308.         return isForward ? imgs.first : imgs.last;
  12309.     }
  12310.     getFullImg(inPost, onsizechange, onrotate) {
  12311.         let wrapEl, name, origSrc;
  12312.         const src = this._getImageSrc();
  12313.         const parent = this._getImageParent;
  12314.         if(this.el.className !== 'de-img-embed') {
  12315.             const nameEl = $q(aib.qImgNameLink, parent) || $q('a', parent);
  12316.             origSrc = nameEl.getAttribute('de-href') || nameEl.href;
  12317.             ({ name } = this);
  12318.         } else {
  12319.             origSrc = parent.href;
  12320.             name = origSrc.split('/').pop();
  12321.         }
  12322.         const imgNameEl = (Cfg.imgSrcBtns ?
  12323.             '<svg class="de-btn-src"><use xlink:href="#de-symbol-post-src"></use></svg>' : '') +
  12324.             `<a class="de-fullimg-link" target="_blank" title="${
  12325.                 Lng.openOriginal[lang] }" href="${ origSrc }">${ name }`;
  12326.         const wrapClass = `${ inPost ? ' de-fullimg-wrap-inpost' : ` de-fullimg-wrap-center${
  12327.             this._size ? '' : ' de-fullimg-wrap-nosize' }` }${
  12328.             this.isVideo ? ' de-fullimg-video' : '' }`;
  12329.         // Expand images: JPG, PNG, GIF, WEBP
  12330.         if(!this.isVideo) {
  12331.             const waitEl = !aib.getImgRedirectSrc && this._size ? '' :
  12332.                 '<svg class="de-fullimg-load"><use xlink:href="#de-symbol-wait"/></svg>';
  12333.             wrapEl = $add(`<div class="de-fullimg-wrap${ wrapClass }">
  12334.                 ${ waitEl }
  12335.                 <img class="de-fullimg" src="${ src }" alt="${ src }">
  12336.                 <div class="de-fullimg-info">${ imgNameEl }</a></div>
  12337.             </div>`);
  12338.             const imgEl = $q('.de-fullimg', wrapEl);
  12339.             imgEl.onload = imgEl.onerror = ({ target: img }) => {
  12340.                 if(!(img.naturalHeight + img.naturalWidth)) {
  12341.                     if(!img.onceLoaded) {
  12342.                         img.src = img.src;
  12343.                         img.onceLoaded = true;
  12344.                     }
  12345.                     return;
  12346.                 }
  12347.                 const { naturalWidth: newW, naturalHeight: newH } = img;
  12348.                 const ar = this._size ? this._size[1] / this._size[0] : newH / newW;
  12349.                 const isRotated = !img.scrollWidth ? false :
  12350.                     img.scrollHeight / img.scrollWidth > 1 ? ar < 1 : ar > 1;
  12351.                 if(!this._size || isRotated) {
  12352.                     this._size = isRotated ? [newH, newW] : [newW, newH];
  12353.                 }
  12354.                 const parentEl = img.parentNode.parentNode;
  12355.                 const waitEl = $q('.de-fullimg-load', parentEl);
  12356.                 if(waitEl) {
  12357.                     $hide(waitEl);
  12358.                     parentEl.classList.remove('de-fullimg-wrap-nosize');
  12359.                     if(onsizechange) {
  12360.                         onsizechange(parentEl);
  12361.                     }
  12362.                 } else if(isRotated && onrotate) {
  12363.                     onrotate(parentEl);
  12364.                 }
  12365.             };
  12366.             DollchanAPI.notify('expandmedia', src);
  12367.             return wrapEl;
  12368.         }
  12369.  
  12370.         // Expand videos: WEBM, MP4
  12371.         // FIXME: handle null size videos
  12372.         const isWebm = origSrc.split('.').pop() === 'webm';
  12373.         const needTitle = isWebm && Cfg.webmTitles;
  12374.         let inPostSize = '';
  12375.         if(inPost) {
  12376.             const [width, height] = this.computeFullSize();
  12377.             inPostSize = ` style="width: ${ width }px; height: ${ height }px;"`;
  12378.         }
  12379.         const hasTitle = needTitle && this.el.hasAttribute('de-metatitle');
  12380.         const title = hasTitle ? this.el.getAttribute('de-metatitle') : '';
  12381.         wrapEl = $add(`<div class="de-fullimg-wrap${ wrapClass }"${ inPostSize }>${
  12382.             nav.firefoxVer < 59 ? '' : '<div class="de-fullimg-video-hack"></div>' }
  12383.             <video src="${ src }" ` +
  12384.                 `${ hasTitle && title ? `title="${ title }" ` : '' }loop autoplay ` +
  12385.                 `${ Cfg.webmControl ? 'controls ' : '' }` +
  12386.                 `${ Cfg.webmVolume === 0 ? 'muted ' : '' }></video>
  12387.             <div class="de-fullimg-info">
  12388.                 ${ imgNameEl }${ hasTitle && title ? ` - ${ title }` : '' }</a>
  12389.                 ${ needTitle && !hasTitle ? `<svg class="de-wait">
  12390.                     <use xlink:href="#de-symbol-wait"/></svg>` : '' }
  12391.             </div>
  12392.         </div>`);
  12393.         const videoEl = $q('video', wrapEl);
  12394.         videoEl.volume = Cfg.webmVolume / 100;
  12395.         videoEl.addEventListener('ended', () => AttachedImage.viewer.navigate(true, true));
  12396.         videoEl.addEventListener('error', ({ target: el }) => {
  12397.             if(!el.onceLoaded) {
  12398.                 el.load();
  12399.                 el.onceLoaded = true;
  12400.             }
  12401.         });
  12402.         if(!this._size) {
  12403.             videoEl.addEventListener('loadedmetadata', ({ target: el }) => {
  12404.                 this._size = [el.videoWidth, el.videoHeight];
  12405.                 onsizechange(wrapEl);
  12406.             });
  12407.         }
  12408.         // Sync webm volume on all browser tabs
  12409.         setTimeout(() => videoEl.dispatchEvent(new CustomEvent('volumechange')), 150);
  12410.         videoEl.addEventListener('volumechange', ({ target: el, isTrusted }) => {
  12411.             const val = el.muted ? 0 : Math.round(el.volume * 100);
  12412.             if(isTrusted && val !== Cfg.webmVolume) {
  12413.                 saveCfg('webmVolume', val);
  12414.                 sendStorageEvent('__de-webmvolume', val);
  12415.             }
  12416.         });
  12417.         // MS Edge needs an external app with DollchanAPI to play webms
  12418.         if(nav.isMsEdge && isWebm && !DollchanAPI.hasListener('expandmedia')) {
  12419.             const href = 'https://github.com/Kagami/webmify/';
  12420.             $popup('err-expandmedia', `${ Lng.errMsEdgeWebm[lang] }:\n<a href="${
  12421.                 href }" target="_blank">${ href }</a>`, false);
  12422.         }
  12423.         // Get webm title: load file and parse its metadata
  12424.         if(needTitle && !hasTitle) {
  12425.             this._webmTitleLoad = ContentLoader.loadImgData(videoEl.src, false).then(data => {
  12426.                 $hide($q('.de-wait', wrapEl));
  12427.                 if(!data) {
  12428.                     return;
  12429.                 }
  12430.                 let str = '', d = (new WebmParser(data.buffer)).getWebmData();
  12431.                 if(!d) {
  12432.                     return;
  12433.                 }
  12434.                 d = d[0];
  12435.                 for(let i = 0, len = d.length; i < len; ++i) {
  12436.                     // Segment Info = 0x1549A966, segment title = 0x7BA9[length | 0x80]
  12437.                     if(d[i] === 0x49 && d[i + 1] === 0xA9 && d[i + 2] === 0x66 &&
  12438.                         d[i + 18] === 0x7B && d[i + 19] === 0xA9
  12439.                     ) {
  12440.                         i += 20;
  12441.                         for(let end = (d[i++] & 0x7F) + i; i < end; ++i) {
  12442.                             str += String.fromCharCode(d[i]);
  12443.                         }
  12444.                         break;
  12445.                     }
  12446.                 }
  12447.                 const loadedTitle = decodeURIComponent(escape(str));
  12448.                 this.el.setAttribute('de-metatitle', loadedTitle);
  12449.                 if(str) {
  12450.                     $q('.de-fullimg-link', wrapEl).textContent +=
  12451.                         ` - ${ videoEl.title = loadedTitle.replace(/\./g, ' ') }`;
  12452.                 }
  12453.             });
  12454.         }
  12455.         DollchanAPI.notify('expandmedia', src);
  12456.         return wrapEl;
  12457.     }
  12458.     sendCloseEvent(e, inPost) {
  12459.         let { post } = this;
  12460.         let cr = post.el.getBoundingClientRect();
  12461.         const x = e.pageX - deWindow.pageXOffset;
  12462.         const y = e.pageY - deWindow.pageYOffset;
  12463.         if(!inPost) {
  12464.             while(x > cr.right || x < cr.left || y > cr.bottom || y < cr.top) {
  12465.                 post = post.parent;
  12466.                 if(post && (post instanceof Pview)) {
  12467.                     cr = post.el.getBoundingClientRect();
  12468.                 } else {
  12469.                     if(Pview.top) {
  12470.                         Pview.top.markToDel();
  12471.                     }
  12472.                     return;
  12473.                 }
  12474.             }
  12475.             post.mouseEnter();
  12476.         } else if(x > cr.right || y > cr.bottom && Pview.top) {
  12477.             Pview.top.markToDel();
  12478.         }
  12479.     }
  12480.     srcBtnEvents({ _fullEl }) {
  12481.         if(!Cfg.imgSrcBtns) {
  12482.             return;
  12483.         }
  12484.         const srcBtnEl = $q('.de-btn-src', _fullEl);
  12485.         srcBtnEl.addEventListener('mouseover', () => (srcBtnEl.odelay = setTimeout(() => {
  12486.             const menuHtml = !this.isVideo ? Menu.getMenuImgSrc(srcBtnEl) :
  12487.                 `<span class="de-menu-item">${ Lng.getFrameLinks[lang] }</span>`;
  12488.             new Menu(srcBtnEl, menuHtml, !this.isVideo ? emptyFn : optiontEl => {
  12489.                 ContentLoader.getDataFromImg($q('video', _fullEl)).then(arr => {
  12490.                     $popup('upload', Lng.sending[lang], true);
  12491.                     const name = this.name.substring(0, this.name.lastIndexOf('.')) + '.png';
  12492.                     const blob = new Blob([arr], { type: 'image/png' });
  12493.                     let formData;
  12494.                     if(!nav.isChrome || nav.scriptHandler !== 'WebExtension') {
  12495.                         formData = new FormData();
  12496.                         formData.append('file', blob, name);
  12497.                     }
  12498.                     const ajaxParams = { data: formData || { arr, name }, method: 'POST' };
  12499.                     const frameLinkHtml = `<a class="de-menu-item de-list" href="${
  12500.                         deWindow.URL.createObjectURL(blob) }" download="${ name }" target="_blank">${
  12501.                         Lng.saveFrame[lang] }</a>`;
  12502.                     $ajax('https://tmp.saucenao.com/', ajaxParams, true).then(xhr => {
  12503.                         let hostUrl, errMsg = Lng.errSaucenao[lang];
  12504.                         try {
  12505.                             const res = JSON.parse(xhr.responseText);
  12506.                             if(res.status === 'success') {
  12507.                                 hostUrl = res.url ? Menu.getMenuImgSrc(res.url) : '';
  12508.                             } else {
  12509.                                 errMsg += ':<br>' + res.error_message;
  12510.                             }
  12511.                         } catch(e) {}
  12512.                         $popup('upload', (hostUrl || errMsg) + frameLinkHtml);
  12513.                     }, () => $popup('upload', Lng.errSaucenao[lang] + frameLinkHtml));
  12514.                 }, emptyFn);
  12515.             });
  12516.         }, Cfg.linksOver)));
  12517.         srcBtnEl.addEventListener('mouseout', e => clearTimeout(e.target.odelay));
  12518.     }
  12519.  
  12520.     get _size() {
  12521.         const value = this._getImageSize();
  12522.         Object.defineProperty(this, '_size', { value, writable: true });
  12523.         return value;
  12524.     }
  12525. }
  12526.  
  12527. // Initialization of embedded image that added to the link in post message
  12528. class EmbeddedImage extends ExpandableImage {
  12529.     get _getImageParent() {
  12530.         const value = this.el.parentNode;
  12531.         Object.defineProperty(this, '_getImageParent', { value });
  12532.         return value;
  12533.     }
  12534.     _getImageSize() {
  12535.         return [this.el.naturalWidth, this.el.naturalHeight];
  12536.     }
  12537.     _getImageSrc() {
  12538.         return this.el.src;
  12539.     }
  12540. }
  12541.  
  12542. // Initialization of image/video that attached to the post
  12543. class AttachedImage extends ExpandableImage {
  12544.     static closeImg() {
  12545.         const { viewer } = AttachedImage;
  12546.         if(viewer) {
  12547.             viewer.closeImgViewer(null);
  12548.             AttachedImage.viewer = null;
  12549.         }
  12550.     }
  12551.     get info() {
  12552.         const value = aib.getImgInfo(this._getImageParent);
  12553.         Object.defineProperty(this, 'info', { value });
  12554.         return value;
  12555.     }
  12556.     get name() {
  12557.         const value = aib.getImgRealName(this._getImageParent).trim();
  12558.         Object.defineProperty(this, 'name', { value });
  12559.         return value;
  12560.     }
  12561.     get nameLink() {
  12562.         const value = $q(aib.qImgNameLink, this._getImageParent);
  12563.         Object.defineProperty(this, 'nameLink', { value });
  12564.         return value;
  12565.     }
  12566.     get weight() {
  12567.         let value = 0;
  12568.         if(this.info) {
  12569.             const w = this.info.match(/(\d+(?:[.,]\d+)?)\s*([mмkк])?i?[bб]/i);
  12570.             const w1 = w[1].replace(',', '.');
  12571.             value = w[2] === 'M' ? (w1 * 1e3) | 0 : !w[2] ? Math.round(w1 / 1e3) : w1;
  12572.         }
  12573.         Object.defineProperty(this, 'weight', { value });
  12574.         return value;
  12575.     }
  12576.  
  12577.     get _getImageParent() {
  12578.         const value = aib.getImgWrap(this.el);
  12579.         Object.defineProperty(this, '_getImageParent', { value });
  12580.         return value;
  12581.     }
  12582.     _getImageSize() {
  12583.         if(this.info) {
  12584.             const size = this.info.match(/(?:[\s(]|^)(\d+)\s?[x\u00D7]\s?(\d+)(?:[)\s,]|$)/);
  12585.             return size ? [size[1], size[2]] : null;
  12586.         }
  12587.         return null;
  12588.     }
  12589.     _getImageSrc() {
  12590.         // XXX: DON'T USE aib.getImgSrcLink(this.el).href
  12591.         // If #ihash spells enabled, Chrome reads href in ajaxed posts as empty -> image can't be expanded!
  12592.         return aib.getImgSrcLink(this.el).getAttribute('href');
  12593.     }
  12594. }
  12595. AttachedImage.viewer = null;
  12596.  
  12597. // A class that finds a set of images in a post
  12598. class PostImages {
  12599.     constructor(post) {
  12600.         let first = null, last = null, els = $Q(aib.qPostImg, post.el);
  12601.         let hasAttachments = false;
  12602.         const filesMap = new Map();
  12603.         for(let i = 0, len = els.length; i < len; ++i) {
  12604.             const el = els[i];
  12605.             last = new AttachedImage(post, el, last);
  12606.             filesMap.set(el, last);
  12607.             hasAttachments = true;
  12608.             if(!first) {
  12609.                 first = last;
  12610.             }
  12611.         }
  12612.         if(Cfg.addImgs || localData) {
  12613.             els = $Q('.de-img-embed', post.el);
  12614.             for(let i = 0, len = els.length; i < len; ++i) {
  12615.                 const el = els[i];
  12616.                 last = new EmbeddedImage(post, el, last);
  12617.                 filesMap.set(el, last);
  12618.                 if(!first) {
  12619.                     first = last;
  12620.                 }
  12621.             }
  12622.         }
  12623.         this.first = first;
  12624.         this.last = last;
  12625.         this.hasAttachments = hasAttachments;
  12626.         this._map = filesMap;
  12627.     }
  12628.     get expanded() {
  12629.         for(let img = this.first; img; img = img.next) {
  12630.             if(img.expanded) {
  12631.                 return true;
  12632.             }
  12633.         }
  12634.         return false;
  12635.     }
  12636.     get firstAttach() {
  12637.         return this.hasAttachments ? this.first : null;
  12638.     }
  12639.     getImageByEl(el) {
  12640.         return this._map.get(el);
  12641.     }
  12642.     [Symbol.iterator]() {
  12643.         return {
  12644.             _img: this.first,
  12645.             next() {
  12646.                 const value = this._img;
  12647.                 if(value) {
  12648.                     this._img = value.next;
  12649.                     return { value, done: false };
  12650.                 }
  12651.                 return { done: true };
  12652.             }
  12653.         };
  12654.     }
  12655. }
  12656.  
  12657. const ImagesHashStorage = Object.create({
  12658.     get getHash() {
  12659.         const value = this._getHashHelper.bind(this);
  12660.         Object.defineProperty(this, 'getHash', { value });
  12661.         return value;
  12662.     },
  12663.     endFn() {
  12664.         if(this.hasOwnProperty('_storage')) {
  12665.             sesStorage['de-imageshash'] = JSON.stringify(this._storage);
  12666.         }
  12667.         if(this.hasOwnProperty('_workers')) {
  12668.             this._workers.clearWorkers();
  12669.             delete this._workers;
  12670.         }
  12671.     },
  12672.  
  12673.     get _canvas() {
  12674.         const value = doc.createElement('canvas');
  12675.         Object.defineProperty(this, '_canvas', { value });
  12676.         return value;
  12677.     },
  12678.     get _storage() {
  12679.         let value = null;
  12680.         try {
  12681.             value = JSON.parse(sesStorage['de-imageshash']);
  12682.         } finally {
  12683.             if(!value) {
  12684.                 value = {};
  12685.             }
  12686.             Object.defineProperty(this, '_storage', { value });
  12687.             return value;
  12688.         }
  12689.     },
  12690.     get _workers() {
  12691.         const value = new WorkerPool(4, this._genImgHash, emptyFn);
  12692.         Object.defineProperty(this, '_workers', { value, configurable: true });
  12693.         return value;
  12694.     },
  12695.     _genImgHash: ([arrBuf, oldw, oldh]) => {
  12696.         const buf = new Uint8Array(arrBuf);
  12697.         const size = oldw * oldh;
  12698.         for(let i = 0, j = 0; i < size; i++, j += 4) {
  12699.             buf[i] = buf[j] * 0.3 + buf[j + 1] * 0.59 + buf[j + 2] * 0.11;
  12700.         }
  12701.         const newh = 8;
  12702.         const neww = 8;
  12703.         const levels = 3;
  12704.         const areas = 256 / levels;
  12705.         const values = 256 / (levels - 1);
  12706.         let hash = 0;
  12707.         for(let i = 0; i < newh; ++i) {
  12708.             for(let j = 0; j < neww; ++j) {
  12709.                 let temp = i / (newh - 1) * (oldh - 1);
  12710.                 const l = Math.min(temp | 0, oldh - 2);
  12711.                 const u = temp - l;
  12712.                 temp = j / (neww - 1) * (oldw - 1);
  12713.                 const c = Math.min(temp | 0, oldw - 2);
  12714.                 const t = temp - c;
  12715.                 hash = (hash << 4) + Math.min(values * (((buf[l * oldw + c] * ((1 - t) * (1 - u)) +
  12716.                     buf[l * oldw + c + 1] * (t * (1 - u)) +
  12717.                     buf[(l + 1) * oldw + c + 1] * (t * u) +
  12718.                     buf[(l + 1) * oldw + c] * ((1 - t) * u)) / areas) | 0), 255);
  12719.                 const g = hash & 0xF0000000;
  12720.                 if(g) {
  12721.                     hash ^= g >>> 24;
  12722.                 }
  12723.                 hash &= ~g;
  12724.             }
  12725.         }
  12726.         return { hash };
  12727.     },
  12728.     async _getHashHelper({ el, src }) {
  12729.         if(src in this._storage) {
  12730.             return this._storage[src];
  12731.         }
  12732.         if(!el.complete) {
  12733.             await new Promise(resolve => el.addEventListener('load', () => resolve()));
  12734.         }
  12735.         if(el.naturalWidth + el.naturalHeight === 0) {
  12736.             return -1;
  12737.         }
  12738.         let data, buffer, val = -1;
  12739.         const { naturalWidth: w, naturalHeight: h } = el;
  12740.         if(aib._4chan) {
  12741.             const imgData = await ContentLoader.loadImgData(el.src);
  12742.             if(imgData) {
  12743.                 ({ buffer } = imgData);
  12744.             }
  12745.         } else {
  12746.             const cnv = this._canvas;
  12747.             cnv.width = w;
  12748.             cnv.height = h;
  12749.             const ctx = cnv.getContext('2d');
  12750.             ctx.drawImage(el, 0, 0);
  12751.             ({ buffer } = ctx.getImageData(0, 0, w, h).data);
  12752.         }
  12753.         if(buffer) {
  12754.             data = await new Promise(resolve =>
  12755.                 this._workers.runWorker([buffer, w, h], [buffer], val => resolve(val)));
  12756.             if(data && ('hash' in data)) {
  12757.                 val = data.hash;
  12758.             }
  12759.         }
  12760.         this._storage[src] = val;
  12761.         return val;
  12762.     }
  12763. });
  12764.  
  12765. function addImgSrcButtons(link, src) {
  12766.     link.insertAdjacentHTML('beforebegin', `<svg class="de-btn-src"${
  12767.         src ? ` de-href="${ src }"` : '' }><use xlink:href="#de-symbol-post-src"/></svg>`);
  12768. }
  12769.  
  12770. // Adding features for info links of images
  12771. function processImgInfoLinks(parent, addSrc = Cfg.imgSrcBtns, imgNames = Cfg.imgNames) {
  12772.     if(addSrc || imgNames) {
  12773.         if(parent instanceof AbstractPost) {
  12774.             processPostImgInfoLinks(parent, addSrc, imgNames);
  12775.         } else {
  12776.             const posts = $Q(aib.qRPost + ', ' + aib.qOPost + ', .de-oppost', parent);
  12777.             for(let i = 0, len = posts.length; i < len; ++i) {
  12778.                 processPostImgInfoLinks(pByEl.get(posts[i]), addSrc, imgNames);
  12779.             }
  12780.         }
  12781.     }
  12782. }
  12783.  
  12784. function processPostImgInfoLinks(post, addSrc, imgNames) {
  12785.     if(!post) {
  12786.         return;
  12787.     }
  12788.     for(const image of post.images) {
  12789.         const link = image.nameLink;
  12790.         if(!link) {
  12791.             return;
  12792.         }
  12793.         if(addSrc) {
  12794.             addImgSrcButtons(link, image.isVideo ? image.el.src : null);
  12795.         }
  12796.         const { name } = image;
  12797.         if(!link.classList.contains('de-img-name')) {
  12798.             link.classList.add('de-img-name');
  12799.             link.title = name;
  12800.             link.setAttribute('download', name);
  12801.             link.setAttribute('de-href', link.href);
  12802.         }
  12803.         if(imgNames) {
  12804.             let ext;
  12805.             if(!(ext = link.getAttribute('de-img-ext'))) {
  12806.                 ext = name.split('.').pop() || link.href.split('/').pop().split('.').pop();
  12807.                 link.setAttribute('de-img-ext', ext);
  12808.                 link.setAttribute('de-img-name-old', link.textContent);
  12809.             }
  12810.             link.textContent = imgNames === 2 ? ext : name;
  12811.         }
  12812.     }
  12813. }
  12814.  
  12815. // Adding image previews before links in post message
  12816. function embedPostMsgImages(el) {
  12817.     if(!Cfg.addImgs || localData) {
  12818.         return;
  12819.     }
  12820.     const els = $Q(aib.qMsgImgLink, el);
  12821.     for(let i = 0, len = els.length; i < len; ++i) {
  12822.         const link = els[i];
  12823.         const url = link.href;
  12824.         if(url.includes('?') || aib.getPostOfEl(link).hidden) {
  12825.             continue;
  12826.         }
  12827.         $bBegin(link, `<a href="${
  12828.             link.href }" target="_blank"><img class="de-img-embed" src="${ url }"></a><br>`);
  12829.         if(Cfg.imgSrcBtns) {
  12830.             addImgSrcButtons(link);
  12831.         }
  12832.     }
  12833. }
  12834.  
  12835. /* ==[ PostBuilders.js ]======================================================================================
  12836.                                           BUILDERS FOR LOADED POSTS
  12837. =========================================================================================================== */
  12838.  
  12839. class DOMPostsBuilder {
  12840.     constructor(form, isArchived) {
  12841.         this._form = form;
  12842.         this._posts = $Q(aib.qRPost, form);
  12843.         this.length = this._posts.length;
  12844.         this.postersCount = '';
  12845.         this._isArchived = isArchived;
  12846.     }
  12847.     get isClosed() {
  12848.         return aib.qClosed && !!$q(aib.qClosed, this._form) || this._isArchived;
  12849.     }
  12850.     getOpMessage() {
  12851.         return aib.fixHTML(doc.adoptNode($q(aib.qPostMsg, this._form)));
  12852.     }
  12853.     getPNum(i) {
  12854.         return aib.getPNum(this._posts[i]);
  12855.     }
  12856.     getOpEl() {
  12857.         return aib.fixHTML(aib.getOp($q(aib.qThread, this._form) || this._form));
  12858.     }
  12859.     getPostEl(i) {
  12860.         return aib.fixHTML(this._posts[i]);
  12861.     }
  12862.     * getRefLinks(i, thrUrl) { // i === 0 - OP-post
  12863.         const msg = i === 0 ? $q(aib.qPostMsg, this._form) : $q(aib.qPostMsg, this._posts[i - 1]);
  12864.         const links = $Q('a', msg);
  12865.         for(let i = 0, len = links.length; i < len; ++i) {
  12866.             const link = links[i];
  12867.             const tc = link.textContent;
  12868.             if(tc[0] === '>' && tc[1] === '>') {
  12869.                 const lNum = parseInt(tc.substr(2), 10);
  12870.                 if(lNum) {
  12871.                     yield [link, lNum];
  12872.                     const url = link.getAttribute('href');
  12873.                     if(url[0] === '#') {
  12874.                         link.setAttribute('href', thrUrl + url);
  12875.                     }
  12876.                 }
  12877.             }
  12878.         }
  12879.     }
  12880.     * bannedPostsData() {
  12881.         const banEls = $Q(aib.qBan, this._form);
  12882.         for(let i = 0, len = banEls.length; i < len; ++i) {
  12883.             const banEl = banEls[i];
  12884.             const postEl = aib.getPostElOfEl(banEl);
  12885.             yield [1, postEl ? aib.getPNum(postEl) : null, doc.adoptNode(banEl)];
  12886.         }
  12887.     }
  12888. }
  12889.  
  12890. class _4chanPostsBuilder {
  12891.     constructor(json, brd) {
  12892.         this._posts = json.posts;
  12893.         this._brd = brd;
  12894.         this.length = json.posts.length - 1;
  12895.         this.postersCount = this._posts[0].unique_ips;
  12896.     }
  12897.     static fixFileName(name, maxLength) {
  12898.         const decodedName = name.replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&#039;/g, "'")
  12899.             .replace(/&lt;/g, '<').replace(/&gt;/g, '>');
  12900.         return decodedName.length <= maxLength ? { isFixed: false, name } : {
  12901.             isFixed : true,
  12902.             name    : decodedName.slice(0, 25).replace(/&/g, '&amp;').replace(/"/g, '&quot;')
  12903.                 .replace(/'/g, '&#039;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  12904.         };
  12905.     }
  12906.     get isClosed() {
  12907.         return !!(this._posts[0].closed || this._posts[0].archived);
  12908.     }
  12909.     getOpMessage() {
  12910.         const { no, com } = this._posts[0];
  12911.         return $add(aib.fixHTML(`<blockquote class="postMessage" id="m${ no }"> ${ com }</blockquote>`));
  12912.     }
  12913.     getPNum(i) {
  12914.         return this._posts[i + 1].no;
  12915.     }
  12916.     getOpEl() {
  12917.         return this.getPostEl(-1);
  12918.     }
  12919.     getPostEl(i) {
  12920.         return $add(aib.fixHTML(this.getPostHTML(i))).lastElementChild;
  12921.     }
  12922.     getPostHTML(i) {
  12923.         const data = this._posts[i + 1];
  12924.         const num = data.no;
  12925.         const brd = this._brd;
  12926.         const _icon = id => `//s.4cdn.org/image/${ id }${
  12927.             deWindow.devicePixelRatio < 2 ? '.gif' : '@2x.gif' }`;
  12928.  
  12929.         // --- FILE ---
  12930.         let fileHTML = '';
  12931.         if(data.filedeleted) {
  12932.             fileHTML = `<div id="f${ num }" class="file"><span class="fileThumb">
  12933.                 <img src="${ _icon('filedeleted-res') }" class="fileDeletedRes" alt="File deleted.">
  12934.             </span></div>`;
  12935.         } else if(typeof data.filename === 'string') {
  12936.             let { name, isFixed: needTitle } = _4chanPostsBuilder.fixFileName(data.filename, 30);
  12937.             name += data.ext;
  12938.             if(!data.tn_w && !data.tn_h && data.ext === '.gif') {
  12939.                 data.tn_w = data.w;
  12940.                 data.tn_h = data.h;
  12941.             }
  12942.             const isSpoiler = data.spoiler;
  12943.             if(isSpoiler) {
  12944.                 name = 'Spoiler Image';
  12945.                 data.tn_w = data.tn_h = 100;
  12946.                 needTitle = false;
  12947.             }
  12948.             const size = prettifySize(data.fsize);
  12949.             const fileTextTitle = isSpoiler ? ` title="${ data.filename + data.ext }"` : '';
  12950.             const aHref = needTitle ? `title="${ data.filename + data.ext }"` : '';
  12951.             const imgSrc = isSpoiler ? '//s.4cdn.org/image/spoiler.png' :
  12952.                 `//i.4cdn.org/${ brd }/${ data.tim }s.jpg`;
  12953.             fileHTML = `<div class="file" id="f${ num }">
  12954.                 <div class="fileText" id="fT${ num }"${ fileTextTitle }>File:
  12955.                     <a href="//i.4cdn.org/${ brd }/${ data.tim +
  12956.                         data.ext }" ${ aHref } target="_blank">${ name }</a>
  12957.                     (${ size }, ${ data.ext === '.pdf' ? 'PDF' : data.w + 'x' + data.h })
  12958.                 </div>
  12959.                 <a class="fileThumb ${ isSpoiler ? 'imgspoiler' : '' }" href="//i.4cdn.org/${ brd }/` +
  12960.                     `${ data.tim + data.ext }" target="_blank">
  12961.                     <img src="${ imgSrc }" alt="${ size }" data-md5="` +
  12962.                         `${ data.md5 }" style="height: ${ data.tn_h }px; width: ${ data.tn_w }px;">
  12963.                     <div data-tip="" data-tip-cb="mShowFull" class="mFileInfo mobile">
  12964.                         ${ size } ${ data.ext.substr(1).toUpperCase() }
  12965.                     </div>
  12966.                 </a>
  12967.             </div>`;
  12968.         }
  12969.  
  12970.         // --- CAPCODE ---
  12971.         let highlight = '', ccBy = '';
  12972.         let cc = data.capcode;
  12973.         switch(cc) {
  12974.         case 'admin_highlight':
  12975.             highlight = ' highlightPost';
  12976.             cc = 'admin';
  12977.             /* falls through */
  12978.         case 'admin': ccBy = 'Administrators'; break;
  12979.         case 'mod': ccBy = 'Moderators'; break;
  12980.         case 'developer': ccBy = 'Developers'; break;
  12981.         case 'manager': ccBy = 'Managers'; break;
  12982.         case 'founder': ccBy = 'Founder';
  12983.         }
  12984.         let ccName = '', ccText = '', ccImg = '', ccClass = '';
  12985.         if(cc) {
  12986.             ccName = cc[0].toUpperCase() + cc.slice(1);
  12987.             ccText = `<strong class="capcode hand id_${ cc === 'founder' ? 'admin' : cc }` +
  12988.                 `" title="Highlight posts by ${ ccBy }">## ${ ccName }</strong>`;
  12989.             ccImg = `<img src="${ _icon(cc + 'icon') }" alt="${
  12990.                 ccName } Icon." title="This user is 4chan ${ ccName }." class="identityIcon">`;
  12991.             ccClass = 'capcode' + (cc === 'founder' ? 'Admin' : ccName);
  12992.         }
  12993.  
  12994.         // --- POST ---
  12995.         const { name = '' } = data;
  12996.         const nameEl = `<span class="name">${ name }</span>`;
  12997.         const mobNameEl = name.length <= 30 ? nameEl :
  12998.             `<span class="name" data-tip data-tip-cb="mShowFull">${ name.substring(30) }()</span>`;
  12999.         const tripEl = `${ data.trip ? `<span class="postertrip">${ data.trip }</span>` : '' }`;
  13000.         const posteruidEl = data.id && !data.capcode ? `<span class="posteruid id_${ data.id }` +
  13001.             `">(ID: <span class="hand" title="Highlight posts by this ID">${ data.id }</span>)</span>` : '';
  13002.         const flagEl = data.country ? `<span title="${ data.country_name }" class="flag flag-${
  13003.             data.country.toLowerCase() }"></span>` : '';
  13004.         const emailEl = data.email ? `<a href="mailto:${
  13005.             data.email.replace(/ /g, '%20') }" class="useremail">` : '';
  13006.         const replyEl = `<a href="#p${ num }" title="Link to this post">No.</a><a href="javascript:quote('${
  13007.             num }');" title="Reply to this post">${ num }</a>`;
  13008.         const subjEl = `<span class="subject">${ data.sub || '' }</span>`;
  13009.         return `<div class="postContainer replyContainer" id="pc${ num }">
  13010.             <div class="sideArrows" id="sa${ num }">&gt;&gt;</div>
  13011.             <div id="p${ num }" class="post ${ i === -1 ? 'op' : 'reply' } ${ highlight }">
  13012.                 <div class="postInfoM mobile" id="pim${ num }">
  13013.                     <span class="nameBlock ${ ccClass }">
  13014.                         ${ mobNameEl }
  13015.                         ${ tripEl }
  13016.                         ${ ccText }
  13017.                         ${ ccImg }
  13018.                         ${ posteruidEl }
  13019.                         ${ flagEl }<br>
  13020.                         ${ subjEl }
  13021.                     </span>
  13022.                     <span class="dateTime postNum" data-utc="${ data.time }">${ data.now } ${ replyEl }</span>
  13023.                 </div>
  13024.                 <div class="postInfo desktop" id="pi${ num }">
  13025.                     <input name="${ num }" value="delete" type="checkbox">
  13026.                     ${ subjEl }
  13027.                     <span class="nameBlock ${ ccClass }">
  13028.                         ${ emailEl }
  13029.                             ${ nameEl }
  13030.                             ${ tripEl }
  13031.                             ${ ccText }
  13032.                         ${ data.email ? '</a>' : '' }
  13033.                         ${ ccImg }
  13034.                         ${ posteruidEl }
  13035.                         ${ flagEl }
  13036.                     </span>
  13037.                     <span class="dateTime" data-utc="${ data.time }">${ data.now }</span>
  13038.                     <span class="postNum desktop">${ replyEl }</span>
  13039.                 </div>
  13040.                 ${ fileHTML }
  13041.                 <blockquote class="postMessage" id="m${ num }"> ${ data.com || '' }</blockquote>
  13042.             </div>
  13043.         </div>`;
  13044.     }
  13045.     * bannedPostsData() {}
  13046. }
  13047. _4chanPostsBuilder._customSpoiler = new Map();
  13048.  
  13049. class DobrochanPostsBuilder {
  13050.     constructor(json, brd) {
  13051.         if(json.error) {
  13052.             throw new AjaxError(0, `API error: ${ json.error.message }`);
  13053.         }
  13054.         this._json = json.result;
  13055.         this._brd = brd;
  13056.         this._posts = json.result.threads[0].posts;
  13057.         this.length = this._posts.length - 1;
  13058.         this.postersCount = '';
  13059.     }
  13060.     get isClosed() {
  13061.         return !!this._json.threads[0].archived;
  13062.     }
  13063.     getOpMessage() {
  13064.         return $add(aib.fixHTML(`<div class="postbody"> ${ this._posts[0].message_html }</div>`));
  13065.     }
  13066.     getPNum(i) {
  13067.         return this._posts[i + 1].display_id;
  13068.     }
  13069.     getOpEl() {
  13070.         return this.getPostEl(-1);
  13071.     }
  13072.     getPostEl(i) {
  13073.         const el = $add(aib.fixHTML(this.getPostHTML(i)));
  13074.         if(i === -1) {
  13075.             return el;
  13076.         }
  13077.         return el.firstElementChild.firstElementChild.lastElementChild;
  13078.     }
  13079.     getPostHTML(i) {
  13080.         const data = this._posts[i + 1];
  13081.         const num = data.display_id;
  13082.         const brd = this._brd;
  13083.         const multiFile = data.files.length > 1;
  13084.  
  13085.         // --- FILE ---
  13086.         let filesHTML = '';
  13087.         for(const { file_id, metadata, rating, size, src, thumb, thumb_height, thumb_width } of data.files) {
  13088.             let fileName, fullFileName, th = thumb;
  13089.             let thumbW = 200;
  13090.             let thumbH = 200;
  13091.             const ext = src.split('.').pop();
  13092.             if(brd === 'b' || brd === 'rf') {
  13093.                 fileName = fullFileName = th.split('/').pop();
  13094.             } else {
  13095.                 fileName = fullFileName = src.split('/').pop();
  13096.                 if(multiFile && fileName.length > 20) {
  13097.                     fileName = fileName.substr(0, 20 - ext.length) + '(…)' + ext;
  13098.                 }
  13099.             }
  13100.             const maxRating = 'r15'; // FIXME: read from settings
  13101.             if(rating === 'r-18g' && maxRating !== 'r-18g') {
  13102.                 th = 'images/r-18g.png';
  13103.             } else if(rating === 'r-18' && (maxRating !== 'r-18g' || maxRating !== 'r-18')) {
  13104.                 th = 'images/r-18.png';
  13105.             } else if(rating === 'r-15' && maxRating === 'sfw') {
  13106.                 th = 'images/r-15.png';
  13107.             } else if(rating === 'illegal') {
  13108.                 th = 'images/illegal.png';
  13109.             } else {
  13110.                 thumbW = thumb_width;
  13111.                 thumbH = thumb_height;
  13112.             }
  13113.             const fileInfo = `<div class="fileinfo${ multiFile ? ' limited' : '' }">Файл:
  13114.                 <a href="/${ src }" title="${ fullFileName }" target="_blank">${ fileName }</a><br>
  13115.                 <em>${ ext }, ${ prettifySize(size) }, ${ metadata.width }x${ metadata.height }
  13116.                 </em>${ multiFile ? '' : ' - Нажмите на картинку для увеличения' }<br>
  13117.                 <a class="edit_ icon" href="/utils/image/edit/${ file_id }/${ num }">
  13118.                     <img title="edit" alt="edit" src="/images/blank.png">
  13119.                 </a>
  13120.             </div>`;
  13121.             filesHTML += `${ multiFile ? '' : fileInfo }
  13122.             <div id="file_${ num }_${ file_id }" class="file">${ multiFile ? fileInfo : '' }
  13123.                 <a href="/${ src }" target="_blank">
  13124.                     <img class="thumb" src="/${ th }" width="${ thumbW }" height="${ thumbH }">
  13125.                 </a>
  13126.             </div>`;
  13127.         }
  13128.  
  13129.         // --- POST ---
  13130.         const date = data.date.replace(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/,
  13131.             (all, y, mo, d, h, m, s) => {
  13132.                 const dt = new Date(y, +mo - 1, d, h, m, s);
  13133.                 return `${ pad2(dt.getDate()) } ${ Lng.fullMonth[1][dt.getMonth()] } ${ dt.getFullYear()
  13134.                 } (${ Lng.week[1][dt.getDay()] }) ${ pad2(dt.getHours()) }:${ pad2(dt.getMinutes()) }`;
  13135.             });
  13136.         const isOp = i === -1;
  13137.         return `${ isOp ? `<div id="post_${ num }" class="oppost post">` :
  13138.             `<table id="post_${ num }" class="replypost post"><tbody><tr>
  13139.             <td class="doubledash">&gt;&gt;</td>
  13140.             <td class="reply" id="reply${ num }">` }
  13141.                 <a name="i${ num }"></a>
  13142.                 <label>
  13143.                     <input name="${ num }" value="${ data.thread_id }" ` +
  13144.                         `class="delete_checkbox" id="delbox_${ num }" type="checkbox">
  13145.                     ${ data.subject ? `<span class="replytitle">${ data.subject }</span>` : '' }
  13146.                     <span class="postername">${ data.name || 'Анонимус' }</span> ${ date }
  13147.                 </label>
  13148.                 <span class="reflink">
  13149.                     <a href="/${ brd }/res/${ data.thread_id }.xhtml#i${ num }"> No.${ num }</a>
  13150.                 </span><br>
  13151.                 ${ filesHTML }
  13152.                 ${ multiFile ? '<div style="clear: both;"></div>' : '' }
  13153.                 <div class="postbody"> ${ data.message_html }</div>
  13154.             ${ isOp ? '</div>' : '</td></tr></tbody></table>' }`;
  13155.     }
  13156.     * bannedPostsData() {}
  13157. }
  13158.  
  13159. class MakabaPostsBuilder {
  13160.     constructor(json, brd) {
  13161.         if(json.Error) {
  13162.             throw new AjaxError(0, `API error: ${ json.Error } (${ json.Code })`);
  13163.         }
  13164.         this._json = json;
  13165.         this._brd = brd;
  13166.         this._posts = json.threads[0].posts;
  13167.         this.length = aib._2channel ? json.counter_posts - 1 : json.posts_count;
  13168.         this.postersCount = json.unique_posters;
  13169.     }
  13170.     get isClosed() {
  13171.         return this._json.is_closed;
  13172.     }
  13173.     getOpMessage() {
  13174.         return $add(aib.fixHTML(this._getPostMsg(this._posts[0])));
  13175.     }
  13176.     getPNum(i) {
  13177.         return this._posts[i + 1].num;
  13178.     }
  13179.     getOpEl() {
  13180.         return this.getPostEl(-1);
  13181.     }
  13182.     getPostEl(i) {
  13183.         return $add(aib.fixHTML(this.getPostHTML(i))).firstElementChild;
  13184.     }
  13185.     getPostHTML(i) {
  13186.         const data = this._posts[i + 1];
  13187.         const { num } = data;
  13188.         const brd = this._brd;
  13189.         const isNew = this._isNew;
  13190.         const p = isNew ? 'post__' : '';
  13191.         const _switch = (val, obj) => val in obj ? obj[val] : obj['@@default'];
  13192.  
  13193.         // --- FILE ---
  13194.         let filesHTML = '';
  13195.         if(data.files && data.files.length !== 0) {
  13196.             filesHTML = `<div class="${ isNew ? 'post__images post__images_type_' : 'images images-' }${
  13197.                 data.files.length === 1 ? 'single' : 'multi' }">`;
  13198.             for(const file of data.files) {
  13199.                 const imgId = num + '-' + file.md5;
  13200.                 const { fullname = file.name, displayname: dispName = file.name } = file;
  13201.                 const isVideo = file.type === 6 || file.type === 10;
  13202.                 const imgClass = isNew ?
  13203.                     `post__file-preview${ isVideo ? ' post__file-webm' : '' }${
  13204.                         data.nsfw ? ' post__file-nsfw' : '' }` :
  13205.                     `img preview${ isVideo ? ' webm-file' : '' }`;
  13206.                 filesHTML += `<figure class="${ p }image">
  13207.                     <figcaption class="${ p }file-attr">
  13208.                         <a id="title-${ imgId }" class="desktop" target="_blank" href="` +
  13209.                             `${ file.type === 100 /* is sticker */ ? file.install : file.path }"` +
  13210.                             `${ dispName === fullname ? '' : ` title="${ fullname }"` }>${ dispName }</a>
  13211.                         <span class="${ isNew ? 'post__filezise' : 'filesize' }">(${ file.size }Кб, ` +
  13212.                             `${ file.width }x${ file.height }${ isVideo ? ', ' + file.duration : '' })</span>
  13213.                     </figcaption>
  13214.                     <div id="exlink-${ imgId }"${ isNew ? '' : 'class="image-link"' }>
  13215.                         <a ${ isNew ? 'class="post__image-link" ' : '' }href="${ file.path }">
  13216.                             <img class="${ imgClass }" src="${ file.thumbnail }" alt="${ file.width }x` +
  13217.                                 `${ file.height }" width="${ file.tn_width }" height="${ file.tn_height }">
  13218.                         </a>
  13219.                     </div>
  13220.                 </figure>`;
  13221.             }
  13222.             filesHTML += '</div>';
  13223.         }
  13224.  
  13225.         // --- POST ---
  13226.         const emailEl = data.email ?
  13227.             `<a href="${ data.email }" class="${ isNew ? 'post__' : 'post-' }email">${ data.name }</a>` :
  13228.             `<span class="${ isNew ? 'post__anon' : 'ananimas' }">${ data.name }</span>`;
  13229.         const tripEl = !data.trip ? '' : `<span class="${ _switch(data.trip, {
  13230.             '!!%adm%!!'        : `${ p }adm">## ${ aib._2channel ? 'Admin' : 'Abu' } ##`,
  13231.             '!!%mod%!!'        : `${ p }mod">## Mod ##`,
  13232.             '!!%Inquisitor%!!' : `${ p }inquisitor">## Applejack ##`,
  13233.             '!!%coder%!!'      : `${ p }mod">## Кодер ##`,
  13234.             '!!%curunir%!!'    : `${ p }mod">## Curunir ##`,
  13235.             '@@default'        :
  13236.                 `${ data.trip_style ? data.trip_style : isNew ? 'post__trip' : 'postertrip' }">` + data.trip
  13237.         }) }</span>`;
  13238.         const refHref = `/${ brd }/res/${ parseInt(data.parent) || num }.html#${ num }`;
  13239.         let rate = '';
  13240.         if(this._hasLikes) {
  13241.             const likes = `<div id="like-div${ num }" class="${ isNew ?
  13242.                 `post__detailpart post__rate post__rate_type_like" title="Мне это нравится">
  13243.                     <svg xmlns="http://www.w3.org/2000/svg" class="post__rate-icon icon">
  13244.                         <use xlink:href="#icon__thunder"></use></svg>` :
  13245.                 'like-div"> <span class="like-icon"> <i class="fa fa-bolt"></i></span>'
  13246.             } <span id="like-count${ num }"${ isNew ? '' : 'class="like-count"' }>`;
  13247.             const dislikes = likes.replace(/like/g, 'dislike').replace('icon__thunder', 'icon__thumbdown');
  13248.             rate = `${ likes }${ data.likes || 0 }</span></div>
  13249.                 ${ dislikes }${ data.dislikes || 0 }</span></div>`;
  13250.         }
  13251.         const isOp =  i === -1;
  13252.         const wrapClass = !isNew ? 'post-wrapper' : isOp ? 'thread__oppost' : 'thread__post';
  13253.         const timeReflink = `<span class="${ isNew ? 'post__time' : 'posttime' }">${ data.date }</span>
  13254.             <span class="${ isNew ? 'post__detailpart' : 'reflink' }">` +
  13255.                 `<a id="${ num }" ${ isNew ? 'class="post__reflink" ' : '' }href="${ refHref }">` +
  13256.                     `${ aib._2channel ? 'No.' : '№' }</a>` +
  13257.                 `<a class="${ isNew ? 'post__reflink ' : '' }postbtn-reply-href" href="${ refHref }"` +
  13258.                     ` name="${ num }">${ num }</a>
  13259.             </span>`;
  13260.         return `<div id="post-${ num }" class="${ wrapClass }">
  13261.             <div class="post ${ isNew ? 'post_type_' : '' }${ isOp ? 'oppost' : 'reply' }` +
  13262.                 `${ filesHTML ? ' post_withimg' : '' }" id="post-body-${ num }" data-num="${ num }">
  13263.                 <div id="post-details-${ num }" class="${ isNew ? 'post__details' : 'post-details' }">
  13264.                     <input type="checkbox" name="delete" value="${ num }">
  13265.                     ${ !data.subject ? '' : `<span class="${ isNew ? 'post__' : 'post-' }title">` +
  13266.                         `${ data.subject + (data.tags ? ` /${ data.tags }/` : '') }</span>` }
  13267.                     ${ emailEl }
  13268.                     ${ data.icon ? `<span class="${ isNew ? 'post__' : 'post-' }icon">` +
  13269.                         `${ data.icon }</span>` : '' }
  13270.                     ${ tripEl }
  13271.                     ${ data.op === 1 ? `<span class="${ p }ophui"># OP</span>&nbsp;` : '' }
  13272.                     ${ isNew ? timeReflink : `<span class="posttime-reflink">
  13273.                         ${ timeReflink }
  13274.                     </span>` }
  13275.                     ${ rate }
  13276.                 </div>
  13277.                 ${ filesHTML }
  13278.                 ${ this._getPostMsg(data) }
  13279.             </div>
  13280.         </div>`;
  13281.     }
  13282.     * bannedPostsData() {
  13283.         const p = this._isNew ? 'post__' : '';
  13284.         for(const { banned, num } of this._posts) {
  13285.             switch(banned) {
  13286.             case 1:
  13287.                 yield [1, num, $add(`<span class="${ p }pomyanem">(Автор этого поста был забанен.)</span>`)];
  13288.                 break;
  13289.             case 2:
  13290.                 yield [2, num, $add(`<span class="${ p }pomyanem">` +
  13291.                     '(Автор этого поста был предупрежден.)</span>')];
  13292.                 break;
  13293.             }
  13294.         }
  13295.     }
  13296.  
  13297.     get _hasLikes() {
  13298.         const value = !!$q('.like-div, .post__rate');
  13299.         Object.defineProperty(this, '_hasLikes', { value });
  13300.         return value;
  13301.     }
  13302.     get _isNew() {
  13303.         const value = !!$q('.post_type_oppost');
  13304.         Object.defineProperty(this, '_isNew', { value });
  13305.         return value;
  13306.     }
  13307.     _getPostMsg(data) {
  13308.         const _switch = (val, obj) => val in obj ? obj[val] : obj['@@default'];
  13309.         const comment = data.comment.replace(/<script /ig, '<!--<textarea ')
  13310.             .replace(/<\/script>/ig, '</textarea>-->');
  13311.         const p = this._isNew ? 'post__' : '';
  13312.         return `<blockquote id="m${ data.num }" class="${ this._isNew ? 'post__' : 'post-' }message">` +
  13313.             `${ comment }${ _switch(data.banned, {
  13314.                 1           : `<br><span class="${ p }pomyanem">(Автор этого поста был забанен.)</span>`,
  13315.                 2           : `<br><span class="${ p }pomyanem">(Автор этого поста был предупрежден.)</span>`,
  13316.                 '@@default' : ''
  13317.             }) }</blockquote>`;
  13318.     }
  13319. }
  13320.  
  13321. /* ==[ RefMap.js ]============================================================================================
  13322.                                              REFERENCE LINKS MAP
  13323. =========================================================================================================== */
  13324.  
  13325. class RefMap {
  13326.     constructor(post) {
  13327.         this.hasMap = false;
  13328.         this._isHidden = false;
  13329.         this._isInited = false;
  13330.         this._post = post;
  13331.         this._set = new Set();
  13332.     }
  13333.     static gen(posts) {
  13334.         const { tNums } = DelForm;
  13335.         for(const [pNum, post] of posts) {
  13336.             for(const [link, lNum] of post.refLinks()) { // link might be from another document
  13337.                 if(MyPosts.has(lNum)) {
  13338.                     link.classList.add('de-ref-you');
  13339.                     if(!MyPosts.has(pNum) && (post instanceof AbstractPost)) {
  13340.                         post.el.classList.add('de-mypost-reply');
  13341.                     }
  13342.                 }
  13343.                 if(!aib.hasOPNum && tNums.has(lNum)) {
  13344.                     link.classList.add('de-ref-op');
  13345.                 }
  13346.                 if(!posts.has(lNum)) {
  13347.                     continue;
  13348.                 }
  13349.                 const { ref } = posts.get(lNum);
  13350.                 if(ref._isInited) {
  13351.                     ref.addRefNum(post, pNum);
  13352.                 } else {
  13353.                     ref._set.add(pNum);
  13354.                     ref.hasMap = true;
  13355.                 }
  13356.             }
  13357.         }
  13358.     }
  13359.     static initRefMap(form) {
  13360.         let post = form.firstThr && form.firstThr.op;
  13361.         if(post && Cfg.linksNavig) {
  13362.             this.gen(pByNum);
  13363.             const strNums = Cfg.strikeHidd && Post.hiddenNums.size ? Post.hiddenNums : null;
  13364.             for(; post; post = post.next) {
  13365.                 if(post.ref.hasMap) {
  13366.                     post.ref.initPostRef('', strNums);
  13367.                 }
  13368.             }
  13369.         }
  13370.     }
  13371.     static updateRefMap(post, isAdd) {
  13372.         const pNum = post.num;
  13373.         const strNums = isAdd && Cfg.strikeHidd && Post.hiddenNums.size ? Post.hiddenNums : null;
  13374.         const links = $Q('a', post.msg);
  13375.         for(let lNum, i = 0, len = links.length; i < len; ++i) {
  13376.             const link = links[i];
  13377.             const tc = link.textContent;
  13378.             if(tc[0] !== '>' || tc[1] !== '>' || !(lNum = parseInt(tc.substr(2), 10))) {
  13379.                 continue;
  13380.             }
  13381.             if(isAdd && MyPosts.has(lNum)) {
  13382.                 link.classList.add('de-ref-you');
  13383.                 if(!MyPosts.has(pNum)) {
  13384.                     const postClass = post.el.classList;
  13385.                     if(!postClass.contains('de-mypost-reply')) {
  13386.                         postClass.add('de-mypost-reply');
  13387.                         updater.refToYou(pNum);
  13388.                     }
  13389.                 }
  13390.             }
  13391.             if(!pByNum.has(lNum)) {
  13392.                 continue;
  13393.             }
  13394.             const lPost = pByNum.get(lNum);
  13395.             if(!aib.t) {
  13396.                 link.href = `#${ aib._4chan ? 'p' : '' }${ lNum }`;
  13397.             }
  13398.             if(!isAdd) {
  13399.                 lPost.ref.removeLink(pNum);
  13400.                 return;
  13401.             }
  13402.             if(strNums && strNums.has(lNum)) {
  13403.                 link.classList.add('de-link-hid');
  13404.             }
  13405.             if(!aib.hasOPNum && DelForm.tNums.has(lNum)) {
  13406.                 link.classList.add('de-ref-op');
  13407.             }
  13408.             lPost.ref.hasMap = true;
  13409.             lPost.ref.addRefNum(post, pNum, strNums && strNums.has(pNum));
  13410.         }
  13411.     }
  13412.     addRefNum(post, num, isHidden = null) {
  13413.         if(isHidden === null) {
  13414.             const strNums = Cfg.strikeHidd && Post.hiddenNums.size ? Post.hiddenNums : null;
  13415.             isHidden = strNums ? strNums.has(+num) : false;
  13416.         }
  13417.         if(!this._set.has(num)) {
  13418.             this._set.add(num);
  13419.             this._el.insertAdjacentHTML('beforeend', this._getHTML(num, '', isHidden));
  13420.             if(Cfg.hideRefPsts && this._post.isHidden && (post instanceof Post)) {
  13421.                 post.setVisib(true, 'reference to >>' + num);
  13422.                 post.ref.hideRef();
  13423.             }
  13424.         }
  13425.     }
  13426.     getElByNum(num) {
  13427.         return $q(`a[href$="${ num }"]`, this._el);
  13428.     }
  13429.     has(num) {
  13430.         return this._set.has(num);
  13431.     }
  13432.     hideRef(isForced = false) {
  13433.         if(!isForced && !Cfg.hideRefPsts || !this.hasMap || this._isHidden) {
  13434.             return;
  13435.         }
  13436.         this._isHidden = true;
  13437.         for(const num of this._set) {
  13438.             const post = pByNum.get(num);
  13439.             if(post && !post.isHidden) {
  13440.                 if(isForced) {
  13441.                     post.setUserVisib(true, true, 'reference to >>' + this._post.num);
  13442.                     post.ref.hideRef(true);
  13443.                 } else if(!post.userToggled) {
  13444.                     post.setVisib(true, 'reference to >>' + this._post.num);
  13445.                     post.ref.hideRef();
  13446.                 }
  13447.             }
  13448.         }
  13449.     }
  13450.     initPostRef(tUrl, strNums) {
  13451.         let html = '';
  13452.         for(const num of this._set) {
  13453.             html += this._getHTML(num, tUrl, strNums && strNums.has(num));
  13454.         }
  13455.         this._createEl(html, false);
  13456.         this._isInited = true;
  13457.     }
  13458.     makeUnion(oRef) {
  13459.         this._set = new Set([...this._set, ...oRef._set].sort((a, b) => a - b));
  13460.     }
  13461.     removeLink(num) {
  13462.         this._set.delete(num);
  13463.         if(!this._set.size) {
  13464.             this.removeMap();
  13465.         } else {
  13466.             const el = this.getElByNum(num);
  13467.             if(el) {
  13468.                 $del(el.nextSibling);
  13469.                 el.remove();
  13470.             }
  13471.         }
  13472.     }
  13473.     removeMap() {
  13474.         this._set = new Set();
  13475.         this._el.remove();
  13476.         delete this._el;
  13477.         this.hasMap = false;
  13478.     }
  13479.     toggleRef(isHide, isForced) {
  13480.         if(isHide) {
  13481.             this.hideRef(isForced);
  13482.         } else {
  13483.             this.unhideRef(isForced);
  13484.         }
  13485.     }
  13486.     unhideRef(isForced = false) {
  13487.         if(this._isHidden && !this.hasMap) {
  13488.             return;
  13489.         }
  13490.         this._isHidden = false;
  13491.         for(const num of this._set) {
  13492.             const post = pByNum.get(num);
  13493.             if(post && post.isHidden && !post.spellHidden) {
  13494.                 if(isForced) {
  13495.                     post.setUserVisib(false);
  13496.                     post.ref.unhideRef(true);
  13497.                 } else if(!post.userToggled) {
  13498.                     post.setVisib(false);
  13499.                     post.ref.unhideRef();
  13500.                 }
  13501.             }
  13502.         }
  13503.     }
  13504.  
  13505.     get _el() {
  13506.         let value = $q('.de-refmap', this._post.el);
  13507.         if(!value) {
  13508.             this._createEl('', this._post.isHidden);
  13509.             value = $q('.de-refmap', this._post.el);
  13510.         }
  13511.         Object.defineProperty(this, '_el', { value, configurable: true });
  13512.         return value;
  13513.     }
  13514.     _createEl(innerHTML, isHidden) {
  13515.         let el;
  13516.         const { msg } = this._post;
  13517.         const html = `<div class="de-refmap${
  13518.             isHidden ? ' de-post-hiddencontent' : '' }">${ innerHTML }</div>`;
  13519.         if(aib.dobrochan && (el = msg.nextElementSibling)) {
  13520.             el.insertAdjacentHTML('beforeend', html);
  13521.         } else {
  13522.             msg.insertAdjacentHTML('afterend', html);
  13523.         }
  13524.     }
  13525.     _getHTML(num, tUrl, isHidden) {
  13526.         return `<a href="${ tUrl }${ aib.anchor }${ num }" class="de-link-backref${
  13527.             isHidden ? ' de-link-hid' : '' }${ MyPosts.has(num) ? ' de-ref-you' : ''
  13528.         }">&gt;&gt;${ num }</a><span class="de-refcomma">, </span>`;
  13529.     }
  13530. }
  13531.  
  13532. /* ==[ Threads.js ]===========================================================================================
  13533.                                                    THREADS
  13534. =========================================================================================================== */
  13535.  
  13536. class Thread {
  13537.     constructor(el, num, prev, form) {
  13538.         this.hasNew = false;
  13539.         this.hidCounter = 0;
  13540.         this.isFav = false;
  13541.         this.isHidden = false;
  13542.         this.loadCount = 0;
  13543.         this.next = null;
  13544.         this.num = num;
  13545.         const els = $Q(aib.qRPost, el);
  13546.         const len = els.length;
  13547.         const omt = aib.t ? 1 : aib.getOmitted($q(aib.qOmitted, el), len);
  13548.         this.pcount = omt + len;
  13549.         this.el = el;
  13550.         this.prev = prev;
  13551.         this.form = form;
  13552.         this._lastModified = '';
  13553.         if(prev) {
  13554.             prev.next = this;
  13555.         }
  13556.         let lastPost = this.op = new Post(aib.getOp(el), this, num, 0, true, prev ? prev.last : null);
  13557.         pByEl.set(el, lastPost);
  13558.         for(let i = 0; i < len; ++i) {
  13559.             const pEl = els[i];
  13560.             lastPost = new Post(pEl, this, aib.getPNum(pEl), omt + i, false, lastPost);
  13561.         }
  13562.         this.last = lastPost;
  13563.         el.setAttribute('de-thread', null);
  13564.         visPosts = Math.max(visPosts, len);
  13565.         if(localData) {
  13566.             return;
  13567.         }
  13568.         this.btns = $bEnd(el, `<div class="de-thr-buttons">${ Post.getPostBtns(true, true) }
  13569.             <span class="de-thr-updater">[<a class="de-thr-updater-link de-abtn" href="#"></a>` +
  13570.             (!aib.t ? ']</span>' : '<span id="de-updater-count" style="display: none;"></span>]</span>') +
  13571.             '</div>');
  13572.         this.btns.addEventListener('click', this);
  13573.         this.btns.addEventListener('mouseover', this);
  13574.         [this.btnHide,, this.btnFav, this.btnUpd] = [...this.btns.children];
  13575.         if(!aib.t && Cfg.hideReplies) {
  13576.             this.btnReplies = $bEnd(this.btns,
  13577.                 ' <span class="de-btn-replies">[<a class="de-abtn" href="#"></a>]</span>');
  13578.             this._toggleReplies();
  13579.         }
  13580.     }
  13581.     static get first() {
  13582.         return DelForm.first.firstThr;
  13583.     }
  13584.     static get last() {
  13585.         return DelForm.last.lastThr;
  13586.     }
  13587.     static removeSavedData() {
  13588.         // TODO: remove relevant spells, hidden posts and user posts
  13589.     }
  13590.     get bottom() {
  13591.         return this.isHidden || Cfg.hideReplies ? this.op.bottom : this.last.bottom;
  13592.     }
  13593.     get lastNotDeleted() {
  13594.         let post = this.last;
  13595.         while(post.isDeleted) {
  13596.             post = post.prev;
  13597.         }
  13598.         return post;
  13599.     }
  13600.     get nextNotHidden() {
  13601.         let thr;
  13602.         for(thr = this.next; thr && thr.isHidden; thr = thr.next) /* empty */;
  13603.         return thr;
  13604.     }
  13605.     get prevNotHidden() {
  13606.         let thr;
  13607.         for(thr = this.prev; thr && thr.isHidden; thr = thr.prev) /* empty */;
  13608.         return thr;
  13609.     }
  13610.     get top() {
  13611.         return this.op.top;
  13612.     }
  13613.     get userTouched() {
  13614.         const value = new Map();
  13615.         Object.defineProperty(this, 'userTouched', { value });
  13616.         return value;
  13617.     }
  13618.     deletePosts(post, delAll, isRemovePost) {
  13619.         SpellsRunner.cachedData = null;
  13620.         let count = 0;
  13621.         do {
  13622.             if(isRemovePost && this.last === post) {
  13623.                 this.last = post.prev;
  13624.             }
  13625.             post.deletePost(isRemovePost);
  13626.             post = post.nextNotDeleted;
  13627.             count++;
  13628.         } while(delAll && post);
  13629.         for(let tPost = post; tPost; tPost = tPost.nextInThread) {
  13630.             if(!tPost.isDeleted) {
  13631.                 tPost.count -= count;
  13632.                 tPost.counterEl.textContent = tPost.count + 1;
  13633.             }
  13634.         }
  13635.         this.pcount -= count;
  13636.         return post;
  13637.     }
  13638.     handleEvent(e) {
  13639.         $pd(e);
  13640.         const el = fixEventEl(e.target);
  13641.         const elClass = el.classList[0];
  13642.         const nextThr = this.next;
  13643.         let oldCoord = false;
  13644.         if(e.type === 'click') {
  13645.             switch(elClass) {
  13646.             case 'de-btn-fav': this.toggleFavState(true); break;
  13647.             case 'de-btn-fav-sel': this.toggleFavState(false); break;
  13648.             case 'de-btn-hide':
  13649.             case 'de-btn-hide-user':
  13650.             case 'de-btn-unhide-user':
  13651.                 oldCoord = nextThr && nextThr.top;
  13652.                 this.op.setUserVisib(!this.isHidden);
  13653.                 break;
  13654.             case 'de-btn-reply': pr.showQuickReply(this.last, this.num, false, false, true); break;
  13655.             case 'de-btn-replies':
  13656.             case 'de-replies-show':
  13657.             case 'de-replies-hide':
  13658.                 oldCoord = !nextThr || this.last.isOmitted ? null : nextThr.top;
  13659.                 this._toggleReplies();
  13660.                 break;
  13661.             case 'de-thr-collapse':
  13662.             case 'de-thr-collapse-link': this.loadPosts(visPosts, true); break;
  13663.             case 'de-thr-updater':
  13664.             case 'de-thr-updater-link':
  13665.                 if(aib.t) {
  13666.                     updater.forceLoad();
  13667.                 } else {
  13668.                     this.loadPosts('new');
  13669.                 }
  13670.             }
  13671.             if(oldCoord) {
  13672.                 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + nextThr.top - oldCoord);
  13673.             }
  13674.         } else if(e.type === 'mouseover') {
  13675.             switch(el.classList[0]) {
  13676.             case 'de-btn-reply':
  13677.                 this.btns.title = Lng.replyToThr[lang];
  13678.                 quotetxt = deWindow.getSelection().toString();
  13679.                 return;
  13680.             case 'de-btn-hide':
  13681.             case 'de-btn-hide-user':
  13682.             case 'de-btn-unhide':
  13683.             case 'de-btn-unhide-user': this.btns.title = Lng.toggleThr[lang]; return;
  13684.             case 'de-btn-fav': this.btns.title = Lng.addFav[lang]; return;
  13685.             case 'de-btn-fav-sel': this.btns.title = Lng.delFav[lang]; return;
  13686.             default: this.btns.removeAttribute('title');
  13687.             }
  13688.         }
  13689.     }
  13690.     /*
  13691.     * Thread loading via ajax.
  13692.     *   Calls from the list of threads, not in a thread.
  13693.     *   Adds posts to current thread accoring to task:
  13694.     * @param {String|Number} task
  13695.     *   'new'    - get new posts,
  13696.     *   'all'    - get all posts,
  13697.     *   'more'   - show 10 omitted posts + get new posts
  13698.     *   {number} - get last N posts
  13699.     * @param {Boolean} isSmartScroll - keeps the scroll position relative to the next thread.
  13700.     * @param {Boolean} isInformUser - shows a popup with waiting animation
  13701.     * @returns {Promise} - resolves with null, to chain with function when loading ends
  13702.     */
  13703.     loadPosts(task, isSmartScroll = false, isInformUser = true) {
  13704.         if(isInformUser) {
  13705.             $popup('load-thr', Lng.loading[lang], true);
  13706.         }
  13707.         return ajaxPostsLoad(aib.b, this.num, false).then(
  13708.             pBuilder => this._loadFromBuilder(task, isSmartScroll, pBuilder),
  13709.             err => $popup('load-thr', getErrorMessage(err)));
  13710.     }
  13711.     /*
  13712.     * New posts loading via ajax.
  13713.     *  Calls by thread updater, by clicking on >>[Get new posts] button, and after sending a reply.
  13714.     *  Adds new posts to the end of current thread.
  13715.     *  @returns {Promise} - resolves with Object, { newCount: Number, locked: Boolean }
  13716.     */
  13717.     loadNewPosts() {
  13718.         return ajaxPostsLoad(aib.b, this.num, true)
  13719.             .then(pBuilder => pBuilder ? this._loadNewFromBuilder(pBuilder) : { newCount: 0, locked: false });
  13720.     }
  13721.     toggleFavState(isEnable, preview = null) {
  13722.         let h, b, num, cnt, txt, last;
  13723.         if(preview) {
  13724.             preview.toggleFavBtn(isEnable);
  13725.         }
  13726.         if(!preview || preview.num === this.num) { // Oppost or usual preview
  13727.             this.op.toggleFavBtn(isEnable);
  13728.             this.isFav = isEnable;
  13729.             ({ host: h, b } = aib);
  13730.             ({ num } = this);
  13731.             cnt = this.pcount;
  13732.             txt = this.op.title;
  13733.             last = aib.anchor + this.last.num;
  13734.         } else { // Loaded preview for oppost in remote thread
  13735.             h = aib.host;
  13736.             ({ brd: b, num } = preview);
  13737.             cnt = preview.remoteThr.pcount;
  13738.             txt = preview.remoteThr.title;
  13739.             last = aib.anchor + preview.remoteThr.lastNum;
  13740.         }
  13741.         readFavorites().then(favObj => {
  13742.             if(isEnable) {
  13743.                 let f = favObj[h] || (favObj[h] = {});
  13744.                 f = f[b] || (f[b] = {});
  13745.                 f.url = aib.prot + '//' + aib.host + aib.getPageUrl(b, 0);
  13746.                 f[num] = { cnt, new: 0, you: 0, txt, url: aib.getThrUrl(b, num), last, time: Date.now() };
  13747.             } else {
  13748.                 removeFavEntry(favObj, h, b, num);
  13749.             }
  13750.             sendStorageEvent('__de-favorites', [h, b, num, favObj, isEnable ? 'add' : 'delete']);
  13751.             saveRenewFavorites(favObj);
  13752.         });
  13753.     }
  13754.     updateHidden(data) {
  13755.         let thr = this;
  13756.         do {
  13757.             const realHid = data ? data.hasOwnProperty(thr.num) : false;
  13758.             if(thr.isHidden ^ realHid) {
  13759.                 if(realHid) {
  13760.                     thr.op.setUserVisib(true, false);
  13761.                     data[thr.num] = thr.op.title;
  13762.                 } else if(thr.isHidden) {
  13763.                     thr.op.setUserVisib(false, false);
  13764.                 }
  13765.             }
  13766.         } while((thr = thr.next));
  13767.     }
  13768.  
  13769.     _addPost(parent, el, i, prev, maybeVParser) {
  13770.         const num = aib.getPNum(el);
  13771.         const wrap = doc.adoptNode(aib.getPostWrap(el, false));
  13772.         const post = new Post(el, this, num, i, false, prev);
  13773.         parent.appendChild(wrap);
  13774.         if(aib.t && !doc.hidden && Cfg.animation) {
  13775.             $animate(el, 'de-post-new');
  13776.         }
  13777.         if(this.userTouched.has(num)) {
  13778.             post.setUserVisib(this.userTouched.get(num), false);
  13779.             this.userTouched.delete(num);
  13780.         } else if(HiddenPosts.has(num)) {
  13781.             HiddenPosts.hideHidden(post, num);
  13782.         }
  13783.         if(maybeVParser.value) {
  13784.             maybeVParser.value.parse(post);
  13785.         }
  13786.         processImgInfoLinks(post);
  13787.         post.addFuncs();
  13788.         ContentLoader.preloadImages(post);
  13789.         if(aib.t && Cfg.markNewPosts) {
  13790.             Post.addMark(el, false);
  13791.         }
  13792.         return post;
  13793.     }
  13794.     _checkBans(pBuilder) {
  13795.         if(!aib.qBan) {
  13796.             return;
  13797.         }
  13798.         for(const [banId, bNum, bEl] of pBuilder.bannedPostsData()) {
  13799.             const post = bNum ? pByNum.get(bNum) : this.op;
  13800.             if(post && post.banned !== banId) {
  13801.                 $del($q(aib.qBan, post.el));
  13802.                 post.msg.appendChild(bEl);
  13803.                 post.banned = banId;
  13804.             }
  13805.         }
  13806.     }
  13807.     _importPosts(last, pBuilder, begin, end, maybeVParser, maybeSpells) {
  13808.         const nums = [];
  13809.         const newCount = end - begin;
  13810.         let newVisCount = newCount;
  13811.         let fragm;
  13812.         if(aib.JsonBuilder && nav.hasTemplate) {
  13813.             const html = [];
  13814.             for(let i = begin; i < end; ++i) {
  13815.                 html.push(pBuilder.getPostHTML(i));
  13816.                 nums.push(pBuilder.getPNum(i));
  13817.             }
  13818.             const temp = doc.createElement('template');
  13819.             temp.innerHTML = aib.fixHTML(html.join(''));
  13820.             fragm = temp.content;
  13821.             const posts = $Q(aib.qRPost, fragm);
  13822.             for(let i = 0, len = posts.length; i < len; ++i) {
  13823.                 last = this._addPost(fragm, posts[i], begin + i + 1, last, maybeVParser);
  13824.                 newVisCount -= maybeSpells.value.runSpells(last);
  13825.                 embedPostMsgImages(last.el);
  13826.             }
  13827.         } else {
  13828.             fragm = doc.createDocumentFragment();
  13829.             for(; begin < end; ++begin) {
  13830.                 last = this._addPost(fragm, pBuilder.getPostEl(begin), begin + 1, last, maybeVParser);
  13831.                 nums.push(last.num);
  13832.                 newVisCount -= maybeSpells.value.runSpells(last);
  13833.                 embedPostMsgImages(last.el);
  13834.             }
  13835.         }
  13836.         return [newCount, newVisCount, fragm, last, nums];
  13837.     }
  13838.     _loadFromBuilder(last, smartScroll, pBuilder) {
  13839.         let nextCoord;
  13840.         const maybeSpells = new Maybe(SpellsRunner);
  13841.         if(smartScroll) {
  13842.             if(this.next) {
  13843.                 nextCoord = this.next.top;
  13844.             } else {
  13845.                 smartScroll = false;
  13846.             }
  13847.         }
  13848.         pr.closeReply();
  13849.         const { op, el: thrEl } = this;
  13850.         $del($q(aib.qOmitted + ', .de-omitted', thrEl));
  13851.         if(this.loadCount === 0) {
  13852.             if(op.trunc) {
  13853.                 op.updateMsg(pBuilder.getOpMessage(), maybeSpells.value);
  13854.             }
  13855.             op.ref.removeMap();
  13856.         }
  13857.         this.loadCount++;
  13858.         this._parsePosts(pBuilder);
  13859.         let needToHide, needToOmit, needToShow;
  13860.         let post = op.next;
  13861.         let needRMUpdate = false;
  13862.         let existed = this.pcount === 1 ? 0 : this.pcount - post.count;
  13863.         switch(last) {
  13864.         case 'new': // get new posts
  13865.             needToHide = $Q('.de-hidden', thrEl).length;
  13866.             needToOmit = needToHide + post.count - 1;
  13867.             needToShow = pBuilder.length - needToOmit;
  13868.             break;
  13869.         case 'all': // get all posts
  13870.             needToHide = needToOmit = 0;
  13871.             needToShow = pBuilder.length;
  13872.             break;
  13873.         case 'more': // show 10 omitted posts + get new posts
  13874.             needToHide = $Q('.de-hidden', thrEl).length - 10;
  13875.             needToOmit = Math.max(needToHide + post.count - 1, 0);
  13876.             needToHide = Math.max(needToHide, 0);
  13877.             needToShow = pBuilder.length - needToOmit;
  13878.             break;
  13879.         default: // get last posts
  13880.             needToHide = Math.max(existed - last, 0);
  13881.             needToOmit = Math.max(pBuilder.length - last, 0);
  13882.             needToShow = last;
  13883.         }
  13884.         if(needToHide) {
  13885.             while(existed-- !== needToShow) {
  13886.                 post.wrap.classList.add('de-hidden');
  13887.                 post.isOmitted = true;
  13888.                 post = post.next;
  13889.             }
  13890.         } else {
  13891.             const nonExisted = pBuilder.length - existed;
  13892.             const maybeVParser = new Maybe(Cfg.embedYTube ? VideosParser : null);
  13893.             const [,, fragm, last, nums] = this._importPosts(
  13894.                 op, pBuilder,
  13895.                 Math.max(0, nonExisted + existed - needToShow),
  13896.                 nonExisted,
  13897.                 maybeVParser,
  13898.                 maybeSpells);
  13899.             if(maybeVParser.hasValue) {
  13900.                 maybeVParser.value.endParser();
  13901.             }
  13902.             $after(op.wrap, fragm);
  13903.             DollchanAPI.notify('newpost', nums);
  13904.             last.next = post;
  13905.             if(post) {
  13906.                 post.prev = last;
  13907.             }
  13908.             needRMUpdate = true;
  13909.             needToShow = Math.min(nonExisted + existed, needToShow);
  13910.         }
  13911.         while(existed-- !== 0) {
  13912.             if(post.trunc) {
  13913.                 const newMsg = doc.adoptNode($q(aib.qPostMsg, pBuilder.getPostEl(post.count - 1)));
  13914.                 post.updateMsg(aib.fixHTML(newMsg), maybeSpells.value);
  13915.             }
  13916.             if(post.isOmitted) {
  13917.                 post.wrap.classList.remove('de-hidden');
  13918.                 post.isOmitted = false;
  13919.             }
  13920.             if(needRMUpdate) {
  13921.                 RefMap.updateRefMap(post, true);
  13922.             }
  13923.             post = post.next;
  13924.         }
  13925.         if(maybeSpells.hasValue) {
  13926.             maybeSpells.value.endSpells();
  13927.         }
  13928.         const btns = this._moveBtnsToEnd();
  13929.         if(!$q('.de-thr-collapse', btns)) {
  13930.             $bEnd(btns, `<span class="de-thr-collapse"> [<a class="de-thr-collapse-link de-abtn" href="${
  13931.                 aib.getThrUrl(aib.b, this.num) }"></a>]</span>`);
  13932.         }
  13933.         if(needToShow > visPosts) {
  13934.             thrNavPanel.addThr(this);
  13935.             btns.lastChild.style.display = 'initial';
  13936.         } else {
  13937.             thrNavPanel.removeThr(this);
  13938.             $hide(btns.lastChild);
  13939.         }
  13940.         if(needToOmit > 0) {
  13941.             op.el.insertAdjacentHTML('afterend', `<div class="de-omitted">${ needToOmit }</div>`);
  13942.         }
  13943.         if(smartScroll) {
  13944.             scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + this.next.top - nextCoord);
  13945.         }
  13946.         Pview.updatePosition(false);
  13947.         if(Cfg.hideReplies) {
  13948.             this.btnReplies.firstElementChild.className = 'de-replies-hide de-abtn';
  13949.             if(Cfg.updThrBtns) {
  13950.                 $show(this.btnUpd);
  13951.             }
  13952.         }
  13953.         closePopup('load-thr');
  13954.     }
  13955.     _loadNewFromBuilder(pBuilder) {
  13956.         const lastOffset = pr.isVisible ? pr.top : null;
  13957.         const [newPosts, newVisPosts] = this._parsePosts(pBuilder);
  13958.         this._moveBtnsToEnd();
  13959.         if(lastOffset !== null) {
  13960.             scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + pr.top - lastOffset);
  13961.         }
  13962.         if(newPosts !== 0 || Panel.isNew) {
  13963.             Panel.updateCounter(
  13964.                 pBuilder.length + 1 - (Cfg.panelCounter === 2 ? this.hidCounter : 0),
  13965.                 $Q(`.de-reply:not(.de-post-removed) ${
  13966.                     aib.qPostImg }, .de-oppost ${ aib.qPostImg }`, this.el).length,
  13967.                 pBuilder.postersCount);
  13968.             Pview.updatePosition(true);
  13969.         }
  13970.         if(pBuilder.isClosed) {
  13971.             AjaxCache.clearCache();
  13972.         }
  13973.         return { newCount: newVisPosts, locked: pBuilder.isClosed };
  13974.     }
  13975.     _moveBtnsToEnd() {
  13976.         const { btns, el } = this;
  13977.         if(btns !== el.lastChild) {
  13978.             el.appendChild(btns);
  13979.         }
  13980.         return btns;
  13981.     }
  13982.     _parsePosts(pBuilder) {
  13983.         this._checkBans(pBuilder);
  13984.         let newPosts = 0;
  13985.         let newVisPosts = 0;
  13986.         let post = this.lastNotDeleted;
  13987.         const len = pBuilder.length;
  13988.         const maybeSpells = new Maybe(SpellsRunner);
  13989.         const maybeVParser = new Maybe(Cfg.embedYTube ? VideosParser : null);
  13990.         const { count } = post;
  13991.         if(count !== 0 && (aib.dobrochan || count > len || pBuilder.getPNum(count - 1) !== post.num)) {
  13992.             post = this.op.nextNotDeleted;
  13993.             let i = post.count - 1;
  13994.             let firstChangedPost = null;
  13995.             for(; i < len && post;) {
  13996.                 const { num, prev } = post;
  13997.                 const iNum = pBuilder.getPNum(i);
  13998.                 if(num === iNum) {
  13999.                     i++;
  14000.                     post = post.nextNotDeleted;
  14001.                     continue;
  14002.                 }
  14003.                 if(num <= iNum) {
  14004.                     if(!firstChangedPost) {
  14005.                         firstChangedPost = post;
  14006.                     }
  14007.                     post = this.deletePosts(post, false, !aib.t);
  14008.                     continue;
  14009.                 }
  14010.                 if(!firstChangedPost) {
  14011.                     firstChangedPost = prev;
  14012.                 }
  14013.                 let cnt = 0;
  14014.                 do {
  14015.                     cnt++;
  14016.                     i++;
  14017.                 } while(pBuilder.getPNum(i) < num);
  14018.                 const res = this._importPosts(prev, pBuilder, i - cnt, i, maybeVParser, maybeSpells);
  14019.                 newPosts += res[0];
  14020.                 this.pcount += res[0];
  14021.                 newVisPosts += res[1];
  14022.                 $after(prev.wrap, res[2]);
  14023.                 res[3].next = post;
  14024.                 post.prev = res[3];
  14025.                 DollchanAPI.notify('newpost', res[4]);
  14026.                 for(let temp = post; temp; temp = temp.nextInThread) {
  14027.                     temp.count += cnt;
  14028.                 }
  14029.             }
  14030.             if(i === len && post) {
  14031.                 this.deletePosts(post, true, !aib.t);
  14032.             }
  14033.             if(firstChangedPost && maybeSpells.hasValue && maybeSpells.value.hasNumSpell) {
  14034.                 for(post = firstChangedPost.nextInThread; post; post = post.nextInThread) {
  14035.                     maybeSpells.value.runSpells(post);
  14036.                 }
  14037.             }
  14038.             if(newPosts !== 0) {
  14039.                 for(post = firstChangedPost; post; post = post.nextInThread) {
  14040.                     RefMap.updateRefMap(post, true);
  14041.                 }
  14042.             }
  14043.         }
  14044.         if(len + 1 > this.pcount) {
  14045.             const res = this._importPosts(this.last, pBuilder, this.lastNotDeleted.count,
  14046.                 len, maybeVParser, maybeSpells);
  14047.             newPosts += res[0];
  14048.             newVisPosts += res[1];
  14049.             (aib.qPostsParent ? $q(aib.qPostsParent, this.el) : this.el).appendChild(res[2]);
  14050.             this.last = res[3];
  14051.             DollchanAPI.notify('newpost', res[4]);
  14052.             this.pcount = len + 1;
  14053.         }
  14054.         updateFavorites(this.op.num, [this.pcount, this.last.num], 'update');
  14055.         if(maybeVParser.hasValue) {
  14056.             maybeVParser.value.endParser();
  14057.         }
  14058.         if(maybeSpells.hasValue) {
  14059.             maybeSpells.value.endSpells();
  14060.         }
  14061.         return [newPosts, newVisPosts];
  14062.     }
  14063.     _toggleReplies() {
  14064.         const isHide = !this.last.isOmitted;
  14065.         let post = this.op;
  14066.         let i = 0;
  14067.         for(; post !== this.last; ++i) {
  14068.             (post = post.next).isOmitted = isHide;
  14069.             post.wrap.classList.toggle('de-hidden', isHide);
  14070.         }
  14071.         this.btnReplies.firstElementChild.className =
  14072.             `${ isHide ? 'de-replies-show' : 'de-replies-hide' } de-abtn`;
  14073.         $each(this.btns.children, el => el !== this.btnReplies && $toggle(el, !isHide));
  14074.         $del($q(aib.qOmitted + ', .de-omitted', this.el));
  14075.         i = this.pcount - 1 - (isHide ? 0 : i);
  14076.         if(i) {
  14077.             this.op.el.insertAdjacentHTML('afterend', `<span class="de-omitted">${ i }</span> `);
  14078.         }
  14079.     }
  14080. }
  14081.  
  14082. const thrNavPanel = {
  14083.     addThr(thr) {
  14084.         this._thrs.add(thr.el);
  14085.         if(this._thrs.size === 1) {
  14086.             doc.defaultView.addEventListener('scroll', this);
  14087.         }
  14088.         if(!this._visible) {
  14089.             this._checkThreads();
  14090.         }
  14091.     },
  14092.     handleEvent(e) {
  14093.         switch(e.type) {
  14094.         case 'scroll': deWindow.requestAnimationFrame(() => this._checkThreads()); break;
  14095.         case 'mouseover': this._expandCollapse(true, fixEventEl(e.relatedTarget)); break;
  14096.         case 'mouseout': this._expandCollapse(false, fixEventEl(e.relatedTarget)); break;
  14097.         case 'click': this._handleClick(e); break;
  14098.         }
  14099.     },
  14100.     initThrNav() {
  14101.         const el = $bEnd(docBody, `
  14102.         <div id="de-thr-navpanel" class="de-thr-navpanel-hidden" style="display: none;">
  14103.             <svg id="de-thr-navarrow"><use xlink:href="#de-symbol-thr-nav-arrow"/></svg>
  14104.             <div id="de-thr-navup">
  14105.                 <svg viewBox="0 0 24 24"><use xlink:href="#de-symbol-thr-nav-up"/></svg>
  14106.             </div>
  14107.             <div id="de-thr-navdown">
  14108.                 <svg viewBox="0 0 24 24"><use xlink:href="#de-symbol-thr-nav-down"/></svg>
  14109.             </div>
  14110.         </div>`);
  14111.         el.addEventListener('mouseover', this, true);
  14112.         el.addEventListener('mouseout', this, true);
  14113.         el.addEventListener('click', this, true);
  14114.         this._el = el;
  14115.         this._thrs = new Set();
  14116.     },
  14117.     removeThr(thr) {
  14118.         this._thrs.delete(thr.el);
  14119.         if(!this._thrs.size) {
  14120.             $hide(this._el);
  14121.             this._currentThr = null;
  14122.             this._visible = false;
  14123.             doc.defaultView.removeEventListener('scroll', this);
  14124.         }
  14125.     },
  14126.  
  14127.     _currentThr : null,
  14128.     _el         : null,
  14129.     _toggleTO   : 0,
  14130.     _thrs       : null,
  14131.     _visible    : false,
  14132.     _checkThreads() {
  14133.         const el = this._findCurrentThread();
  14134.         if(el) {
  14135.             if(!this._visible) {
  14136.                 this._toggleNavPanel(false);
  14137.             }
  14138.             this._currentThr = el;
  14139.         } else if(this._visible) {
  14140.             this._toggleNavPanel(true);
  14141.         }
  14142.     },
  14143.     _expandCollapse(isExpand, rt) {
  14144.         if(!rt || !this._el.contains(rt.farthestViewportElement || rt)) {
  14145.             clearTimeout(this._toggleTO);
  14146.             this._toggleTO = setTimeout(() => this._el.classList.toggle('de-thr-navpanel-hidden', !isExpand),
  14147.                 Cfg.linksOver);
  14148.         }
  14149.     },
  14150.     _findCurrentThread() {
  14151.         Object.defineProperty(this, '_findCurrentThread', {
  14152.             value: 'elementsFromPoint' in doc ?
  14153.                 () => doc.elementsFromPoint(Post.sizing.wWidth / 2, Post.sizing.wHeight / 2)
  14154.                     .find(el => this._thrs.has(el)) :
  14155.                 () => {
  14156.                     let el = doc.elementFromPoint(Post.sizing.wWidth / 2, Post.sizing.wHeight / 2);
  14157.                     while(el) {
  14158.                         if(this._thrs.has(el)) {
  14159.                             return el;
  14160.                         }
  14161.                         el = el.parentElement;
  14162.                     }
  14163.                     return undefined;
  14164.                 }
  14165.         });
  14166.         return this._findCurrentThread();
  14167.     },
  14168.     _handleClick(e) {
  14169.         const el = fixEventEl(e.target);
  14170.         switch((el.tagName.toLowerCase() === 'svg' ? el.parentNode : el).id) {
  14171.         case 'de-thr-navup':
  14172.             scrollTo(deWindow.pageXOffset, deWindow.pageYOffset +
  14173.                 this._currentThr.getBoundingClientRect().top - 50);
  14174.             break;
  14175.         case 'de-thr-navdown':
  14176.             scrollTo(deWindow.pageXOffset, deWindow.pageYOffset +
  14177.                 this._currentThr.getBoundingClientRect().bottom - Post.sizing.wHeight + 50);
  14178.             break;
  14179.         }
  14180.     },
  14181.     _toggleNavPanel(isHide) {
  14182.         this._el.style.display = isHide ? 'none' : 'initial';
  14183.         this._visible = !isHide;
  14184.     }
  14185. };
  14186.  
  14187. /* ==[ ThreadUpdater.js ]=====================================================================================
  14188.                                                 THREAD UPDATER
  14189. =========================================================================================================== */
  14190.  
  14191. function initThreadUpdater(title, enableUpdate) {
  14192.     let focusLoadTime, disabledByUser = true;
  14193.     let enabled = false;
  14194.     let repliesToYou = new Set();
  14195.     let lastECode = 200;
  14196.     let newPosts = 0;
  14197.     let paused = false;
  14198.     let sendError = false;
  14199.     const storageName = `de-lastpcount-${ aib.b }-${ aib.t }`;
  14200.  
  14201.     const audio = {
  14202.         enabled  : false,
  14203.         repeatMS : 0,
  14204.         disableAudio() {
  14205.             this.stopAudio();
  14206.             this.enabled = false;
  14207.             const btn = $id('de-panel-audio-on');
  14208.             if(btn) {
  14209.                 btn.id = 'de-panel-audio-off';
  14210.             }
  14211.         },
  14212.         playAudio() {
  14213.             this.stopAudio();
  14214.             if(this.repeatMS === 0) {
  14215.                 this._el.play();
  14216.                 return;
  14217.             }
  14218.             this._playInterval = setInterval(() => this._el.play(), this.repeatMS);
  14219.         },
  14220.         stopAudio() {
  14221.             if(this._playInterval) {
  14222.                 clearInterval(this._playInterval);
  14223.                 this._playInterval = null;
  14224.             }
  14225.         },
  14226.  
  14227.         get _el() {
  14228.             const value = doc.createElement('audio');
  14229.             value.setAttribute('preload', 'auto');
  14230.             value.src = gitRaw + 'signal.ogg';
  14231.             Object.defineProperty(this, '_el', { value });
  14232.             return value;
  14233.         }
  14234.     };
  14235.  
  14236.     const counter = {
  14237.         count(delayMS, useCounter, callback) {
  14238.             if(!this._enabled || !useCounter) {
  14239.                 this._countingTO = setTimeout(() => {
  14240.                     this._countingTO = null;
  14241.                     callback();
  14242.                 }, delayMS);
  14243.                 return;
  14244.             }
  14245.             let seconds = delayMS / 1e3;
  14246.             this._set(seconds);
  14247.             this._countingIV = setInterval(() => {
  14248.                 seconds--;
  14249.                 if(seconds === 0) {
  14250.                     this._stopCounter();
  14251.                     callback();
  14252.                 } else {
  14253.                     this._set(seconds);
  14254.                 }
  14255.             }, 1e3);
  14256.         },
  14257.         disableCounter() {
  14258.             this._enabled = false;
  14259.             this._stopCounter();
  14260.             $hide(this._el);
  14261.         },
  14262.         enableCounter() {
  14263.             this._enabled = true;
  14264.             $show(this._el);
  14265.         },
  14266.         setWait() {
  14267.             this._stopCounter();
  14268.             if(this._enabled) {
  14269.                 this._el.innerHTML = '<svg class="de-wait"><use xlink:href="#de-symbol-wait"/></svg>';
  14270.             }
  14271.         },
  14272.  
  14273.         _countingTO : null,
  14274.         _countingIV : null,
  14275.         _enabled    : false,
  14276.         get _el() {
  14277.             const value = $id('de-updater-count');
  14278.             Object.defineProperty(this, '_el', { value });
  14279.             return value;
  14280.         },
  14281.         _set(seconds) {
  14282.             this._el.innerHTML = seconds;
  14283.         },
  14284.         _stopCounter() {
  14285.             if(this._countingIV) {
  14286.                 clearInterval(this._countingIV);
  14287.                 this._countingIV = null;
  14288.             }
  14289.             if(this._countingTO) {
  14290.                 clearTimeout(this._countingTO);
  14291.                 this._countingTO = null;
  14292.             }
  14293.         }
  14294.     };
  14295.  
  14296.     const favicon = {
  14297.         get canBlink() {
  14298.             return Cfg.favIcoBlink && !!this.originalIcon;
  14299.         },
  14300.         get originalIcon() {
  14301.             return this._iconEl ? this._iconEl.href : null;
  14302.         },
  14303.         initIcons() {
  14304.             if(this._isInited) {
  14305.                 return;
  14306.             }
  14307.             this._isInited = true;
  14308.             const icon = new Image();
  14309.             icon.onload = e => {
  14310.                 try {
  14311.                     this._initIconsHelper(e.target);
  14312.                 } catch(err) {
  14313.                     console.warn('Icon error:', err);
  14314.                 }
  14315.             };
  14316.             if(aib._4chan) {
  14317.                 // Due to CORS we cannot apply href to icon.src directly
  14318.                 $ajax(this._iconEl.href, { responseType: 'blob' }, true).then(xhr => {
  14319.                     icon.src = 'response' in xhr ?
  14320.                         deWindow.URL.createObjectURL(xhr.response) : '/favicon.ico';
  14321.                 });
  14322.                 return;
  14323.             }
  14324.             icon.src = this._iconEl.href;
  14325.         },
  14326.         startBlink(isError) {
  14327.             const iconUrl = !this._hasIcons ? this._emptyIcon :
  14328.                 isError ? this._iconError :
  14329.                 repliesToYou.size ? this._getIconYou(newPosts) : this._getIconNew(newPosts);
  14330.             if(this._blinkInterv) {
  14331.                 if(this._currentIcon === iconUrl) {
  14332.                     return;
  14333.                 }
  14334.                 clearInterval(this._blinkInterv);
  14335.             }
  14336.             this._currentIcon = iconUrl;
  14337.             this._blinkInterv = setInterval(() => this._setIcon((this._isOrigIcon = !this._isOrigIcon) ?
  14338.                 this.originalIcon : this._currentIcon), this._blinkMS);
  14339.         },
  14340.         stopBlink() {
  14341.             if(this._blinkInterv) {
  14342.                 clearInterval(this._blinkInterv);
  14343.                 this._blinkInterv = null;
  14344.             }
  14345.             if(!this._isOrigIcon) {
  14346.                 this._setIcon(this.originalIcon);
  14347.                 this._isOrigIcon = true;
  14348.             }
  14349.         },
  14350.         updateIcon(isError) {
  14351.             if(!isError && !newPosts) {
  14352.                 this._setIcon(this.originalIcon);
  14353.             } else if(this._hasIcons) {
  14354.                 this._setIcon(isError ? this._iconError :
  14355.                     repliesToYou.size ? this._getIconYou(newPosts) : this._getIconNew(newPosts));
  14356.             }
  14357.         },
  14358.  
  14359.         _blinkInterv : null,
  14360.         _blinkMS     : 800,
  14361.         _currentIcon : null,
  14362.         _emptyIcon   : 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==',
  14363.         _getIconNew  : newPosts => null,
  14364.         _getIconYou  : newPosts => null,
  14365.         _hasIcons    : false,
  14366.         _iconError   : null,
  14367.         _iconsNew    : [],
  14368.         _iconsYou    : [],
  14369.         _isInited    : false,
  14370.         _isOrigIcon  : true,
  14371.         get _iconEl() {
  14372.             const el = $q('link[rel="shortcut icon"]', doc.head) ||
  14373.                 $bEnd(doc.head, '<link href="/favicon.ico" rel="shortcut icon"/>');
  14374.             Object.defineProperties(this, {
  14375.                 _iconEl      : { value: el, writable: true },
  14376.                 originalIcon : { value: el.href }
  14377.             });
  14378.             return el;
  14379.         },
  14380.         _drawCanvCircle(ctx, strokeColor, fillColor, scale) {
  14381.             ctx.beginPath();
  14382.             ctx.arc(10.5 * scale, 10.5 * scale, 5 * scale, 0, 2 * Math.PI);
  14383.             ctx.fillStyle = fillColor;
  14384.             ctx.fill();
  14385.             ctx.lineWidth = 1;
  14386.             ctx.strokeStyle = strokeColor;
  14387.             ctx.stroke();
  14388.         },
  14389.         _drawCanvLines(ctx, line1, line2, color, width, scale) {
  14390.             ctx.beginPath();
  14391.             ctx.strokeStyle = color;
  14392.             ctx.lineWidth = width * scale;
  14393.             ctx.moveTo(line1[0] * scale, line1[1] * scale);
  14394.             ctx.lineTo(line1[2] * scale, line1[3] * scale);
  14395.             ctx.moveTo(line2[0] * scale, line2[1] * scale);
  14396.             ctx.lineTo(line2[2] * scale, line2[3] * scale);
  14397.             ctx.stroke();
  14398.         },
  14399.         _drawIconsNewYou(ctx, canvas, id, iconCircle, scale) {
  14400.             ctx.putImageData(iconCircle, 0, 0);
  14401.             ctx.fillStyle = '#fff';
  14402.             if(id) {
  14403.                 ctx.font = `bold ${ 12 * scale }px Arial`;
  14404.                 ctx.fillText(id, 7 * scale, 15 * scale);
  14405.             } else {
  14406.                 ctx.fillRect(6 * scale, 9 * scale, 2 * scale, 3 * scale);
  14407.                 ctx.fillRect(9.5 * scale, 9 * scale, 2 * scale, 3 * scale);
  14408.                 ctx.fillRect(13 * scale, 9 * scale, 2 * scale, 3 * scale);
  14409.             }
  14410.             return canvas.toDataURL('image/png');
  14411.         },
  14412.         _initIconsHelper(icon) {
  14413.             const canvas = doc.createElement('canvas');
  14414.             const ctx = canvas.getContext('2d');
  14415.             const wh = Math.max(icon.naturalHeight, 16 * (deWindow.devicePixelRatio || 1));
  14416.             const scale = wh / 16;
  14417.             canvas.width = canvas.height = wh;
  14418.             ctx.drawImage(icon, 0, 0, wh, wh);
  14419.             const original = ctx.getImageData(0, 0, wh, wh);
  14420.             // Error (red cross)
  14421.             this._drawCanvLines(ctx, [15, 15, 7, 7], [7, 15, 15, 7], '#780000', 3, scale);
  14422.             this._drawCanvLines(ctx, [14.5, 14.5, 7.5, 7.5], [7.5, 14.5, 14.5, 7.5], '#fa2020', 1.5, scale);
  14423.             this._iconError = canvas.toDataURL('image/png');
  14424.             // New posts (green circle)
  14425.             ctx.putImageData(original, 0, 0);
  14426.             this._drawCanvCircle(ctx, '#174f1d', '#00a000', scale);
  14427.             const iconNewCircle = ctx.getImageData(0, 0, wh, wh);
  14428.             // Replies to you (blue circle)
  14429.             ctx.putImageData(original, 0, 0);
  14430.             this._drawCanvCircle(ctx, '#122091', '#1b6df5', scale);
  14431.             const iconYouCircle = ctx.getImageData(0, 0, wh, wh);
  14432.             this._getIconNew = newPosts => {
  14433.                 const id = newPosts < 10 ? newPosts : 0;
  14434.                 return this._iconsNew[id] || (this._iconsNew[id] =
  14435.                     this._drawIconsNewYou(ctx, canvas, id, iconNewCircle, scale));
  14436.             };
  14437.             this._getIconYou = newPosts => {
  14438.                 const id = newPosts < 10 ? newPosts : 0;
  14439.                 return this._iconsYou[id] || (this._iconsYou[id] =
  14440.                     this._drawIconsNewYou(ctx, canvas, id, iconYouCircle, scale));
  14441.             };
  14442.             this._hasIcons = true;
  14443.         },
  14444.         _setIcon(iconUrl) {
  14445.             this._iconEl.remove();
  14446.             this._iconEl = $aBegin(doc.head, `<link rel="shortcut icon" href="${ iconUrl }">`);
  14447.         }
  14448.     };
  14449.  
  14450.     const notification = {
  14451.         get canShow() {
  14452.             return Cfg.desktNotif && this._granted;
  14453.         },
  14454.         checkPermission() {
  14455.             if(Cfg.desktNotif && ('permission' in Notification)) {
  14456.                 switch(Notification.permission.toLowerCase()) {
  14457.                 case 'default': this._requestPermission(); break;
  14458.                 case 'denied': saveCfg('desktNotif', 0);
  14459.                 }
  14460.             }
  14461.         },
  14462.         closeNotif() {
  14463.             if(this._notifEl) {
  14464.                 this._notifEl.close();
  14465.                 this._notifEl = null;
  14466.             }
  14467.         },
  14468.         showNotif() {
  14469.             const lngQuantity = num => {
  14470.                 const new10 = num % 10;
  14471.                 return lang === 1 ? +(num !== 1) :
  14472.                     new10 > 4 || new10 === 0 || (((num % 100) / 10) | 0) === 1 ? 2 :
  14473.                     new10 === 1 ? 0 : 1;
  14474.             };
  14475.             const post = Thread.first.last;
  14476.             const toYou = repliesToYou.size;
  14477.             const notif = new Notification(`${ aib.dm }/${ aib.b }/${ aib.t }: ${ newPosts } ${
  14478.                 Lng.newPost[lang][lngQuantity(newPosts)] }. ${
  14479.                 toYou ? `${ toYou } ${ Lng.youReplies[lang][lngQuantity(toYou)] }.` : '' }`,
  14480.             {
  14481.                 body : Lng.latestPost[lang] + ':\n' + post.text.substring(0, 250).replace(/\s+/g, ' '),
  14482.                 icon : post.images.firstAttach ? post.images.firstAttach.src : favicon.originalIcon,
  14483.                 tag  : aib.dm + aib.b + aib.t
  14484.             });
  14485.             notif.onshow = () => setTimeout(() => notif === this._notifEl && this.closeNotif(), 12e3);
  14486.             notif.onclick = () => deWindow.focus();
  14487.             notif.onerror = () => {
  14488.                 deWindow.focus();
  14489.                 this._requestPermission();
  14490.             };
  14491.             this._notifEl = notif;
  14492.         },
  14493.  
  14494.         _closeTO : null,
  14495.         _granted : true,
  14496.         _notifEl : null,
  14497.         _requestPermission() {
  14498.             this._granted = false;
  14499.             Notification.requestPermission(state => {
  14500.                 if(state.toLowerCase() === 'denied') {
  14501.                     saveCfg('desktNotif', 0);
  14502.                 } else {
  14503.                     this._granted = true;
  14504.                 }
  14505.             });
  14506.         }
  14507.     };
  14508.  
  14509.     const updMachine = {
  14510.         start(needSleep = false, loadOnce = false) {
  14511.             if(this._state !== -1) {
  14512.                 this.stopUpdater(false);
  14513.             }
  14514.             this._state = 0;
  14515.             this._loadOnce = loadOnce;
  14516.             this._delay = this._initDelay = Cfg.updThrDelay * 1e3;
  14517.             if(!loadOnce) {
  14518.                 this._setUpdateStatus('on');
  14519.             }
  14520.             this._makeStep(needSleep);
  14521.         },
  14522.         stopUpdater(updateStatus = true) {
  14523.             if(this._state !== -1) {
  14524.                 this._state = -1;
  14525.                 if(this._loadPromise) {
  14526.                     this._loadPromise.cancelPromise();
  14527.                     this._loadPromise = null;
  14528.                 }
  14529.                 counter.setWait();
  14530.                 if(updateStatus) {
  14531.                     this._setUpdateStatus('off');
  14532.                 }
  14533.             }
  14534.         },
  14535.  
  14536.         _delay       : 0,
  14537.         _initDelay   : 0,
  14538.         _loadOnce    : false,
  14539.         _loadPromise : null,
  14540.         _seconds     : 0,
  14541.         _state       : -1,
  14542.         get _panelButton() {
  14543.             const value = $q('a[id^="de-panel-upd"]');
  14544.             if(value) {
  14545.                 Object.defineProperty(this, '_panelButton', { value });
  14546.             }
  14547.             return value;
  14548.         },
  14549.         _handleNewPosts(lPosts, err) {
  14550.             if(err instanceof CancelError) {
  14551.                 return;
  14552.             }
  14553.             infoLoadErrors(err, false);
  14554.             const eCode = err instanceof AjaxError ? err.code : 0;
  14555.             if(eCode !== 200 && eCode !== 304) {
  14556.                 if(doc.hidden && favicon.canBlink) {
  14557.                     favicon.startBlink(true);
  14558.                 }
  14559.                 if(eCode === -1 || (eCode === 404 && lastECode === 404)) {
  14560.                     Thread.removeSavedData(aib.b, aib.t);
  14561.                     updateTitle(eCode);
  14562.                     disableUpdater();
  14563.                 } else {
  14564.                     this._setUpdateStatus('warn');
  14565.                     if(!Cfg.noErrInTitle) {
  14566.                         updateTitle(eCode);
  14567.                     }
  14568.                     this._makeStep();
  14569.                 }
  14570.                 lastECode = eCode;
  14571.                 updateFavorites(aib.t, getErrorMessage(err), 'error');
  14572.                 return;
  14573.             }
  14574.             if(lastECode !== 200) {
  14575.                 favicon.stopBlink();
  14576.                 this._setUpdateStatus('on');
  14577.                 if(!Cfg.noErrInTitle) {
  14578.                     updateTitle(eCode);
  14579.                 }
  14580.             }
  14581.             lastECode = eCode;
  14582.             if(doc.hidden) {
  14583.                 if(lPosts !== 0) {
  14584.                     newPosts += lPosts;
  14585.                     updateTitle();
  14586.                     if(favicon.canBlink) {
  14587.                         favicon.startBlink(false);
  14588.                     }
  14589.                     if(notification.canShow) {
  14590.                         notification.showNotif();
  14591.                     }
  14592.                     if(audio.enabled) {
  14593.                         audio.playAudio();
  14594.                     }
  14595.                     sesStorage[storageName] = Thread.first.pcount;
  14596.                     this._delay = this._initDelay;
  14597.                 } else if(this._delay !== 12e4) {
  14598.                     this._delay = Math.min(this._delay + this._initDelay, 12e4);
  14599.                 }
  14600.             }
  14601.             this._makeStep();
  14602.         },
  14603.         _makeStep(needSleep = true) {
  14604.             while(true) {
  14605.                 switch(this._state) {
  14606.                 case 0:
  14607.                     if(needSleep) {
  14608.                         this._state = 1;
  14609.                         counter.count(this._delay, !doc.hidden, () => this._makeStep());
  14610.                         return;
  14611.                     }
  14612.                     /* falls through */
  14613.                 case 1:
  14614.                     counter.setWait();
  14615.                     this._state = 2;
  14616.                     this._loadPromise = Thread.first.loadNewPosts().then(
  14617.                         ({ newCount, locked }) =>
  14618.                             this._handleNewPosts(newCount, locked ? AjaxError.Locked : AjaxError.Success),
  14619.                         err => this._handleNewPosts(0, err));
  14620.                     return;
  14621.                 case 2:
  14622.                     this._loadPromise = null;
  14623.                     if(this._loadOnce) {
  14624.                         this._state = -1;
  14625.                         return;
  14626.                     }
  14627.                     this._state = 0;
  14628.                     break;
  14629.                 default:
  14630.                     console.error('Invalid thread updater state:', this._state, new Error().stack);
  14631.                     return;
  14632.                 }
  14633.             }
  14634.         },
  14635.         _setUpdateStatus(status) {
  14636.             if(this._panelButton) {
  14637.                 this._panelButton.id = 'de-panel-upd-' + status;
  14638.                 this._panelButton.title = Lng.panelBtn[`upd-${ status === 'off' ? 'off' : 'on' }`][lang];
  14639.                 if(nav.isPresto) {
  14640.                     this._panelButton.innerHTML =
  14641.                         '<svg class="de-panel-svg"><use xlink:href="#de-symbol-panel-upd"/></svg>';
  14642.                 }
  14643.             }
  14644.         }
  14645.     };
  14646.  
  14647.     function enableUpdater() {
  14648.         enabled = true;
  14649.         disabledByUser = paused = false;
  14650.         repliesToYou = new Set();
  14651.         newPosts = 0;
  14652.         focusLoadTime = -1e4;
  14653.         notification.checkPermission();
  14654.         if(Cfg.updCount) {
  14655.             counter.enableCounter();
  14656.         }
  14657.         favicon.initIcons();
  14658.     }
  14659.  
  14660.     function disableUpdater() {
  14661.         if(enabled) {
  14662.             audio.disableAudio();
  14663.             counter.disableCounter();
  14664.             updMachine.stopUpdater();
  14665.             enabled = false;
  14666.         }
  14667.     }
  14668.  
  14669.     function forceLoadPosts() {
  14670.         if(enabled && paused) {
  14671.             return;
  14672.         }
  14673.         if(!enabled && !disabledByUser) {
  14674.             enableUpdater();
  14675.         }
  14676.         updMachine.start(false, !enabled);
  14677.     }
  14678.  
  14679.     function updateTitle(eCode = lastECode) {
  14680.         doc.title = (sendError === true ? `{${ Lng.error[lang] }} ` : '') +
  14681.             (eCode <= 0 || eCode === 200 ? '' : `{${ eCode }} `) +
  14682.             (newPosts ? `[${ newPosts }] ` : '') + title;
  14683.         favicon.updateIcon(eCode !== 200 && eCode !== 304);
  14684.     }
  14685.  
  14686.     doc.addEventListener('visibilitychange', e => {
  14687.         if(!doc.hidden) {
  14688.             const focusTime = e.timeStamp;
  14689.             favicon.stopBlink();
  14690.             audio.stopAudio();
  14691.             notification.closeNotif();
  14692.             newPosts = 0;
  14693.             repliesToYou = new Set();
  14694.             sendError = false;
  14695.             setTimeout(() => {
  14696.                 updateTitle();
  14697.                 if(enabled && focusTime - focusLoadTime > 1e4) {
  14698.                     focusLoadTime = focusTime;
  14699.                     forceLoadPosts();
  14700.                 }
  14701.             }, 200);
  14702.         } else if(Thread.first) {
  14703.             Post.clearMarks();
  14704.         }
  14705.     });
  14706.     if(enableUpdate) {
  14707.         enableUpdater();
  14708.         updMachine.start(true);
  14709.     }
  14710.  
  14711.     return {
  14712.         continueUpdater(needSleep = false) {
  14713.             if(enabled && paused) {
  14714.                 updMachine.start(needSleep);
  14715.                 paused = false;
  14716.             }
  14717.         },
  14718.         disableUpdater() {
  14719.             disabledByUser = true;
  14720.             disableUpdater();
  14721.         },
  14722.         enableUpdater() {
  14723.             if(!enabled) {
  14724.                 enableUpdater();
  14725.                 updMachine.start();
  14726.             }
  14727.         },
  14728.         forceLoad(e) {
  14729.             if(e) {
  14730.                 $pd(e);
  14731.             }
  14732.             Post.clearMarks();
  14733.             if(enabled && paused) {
  14734.                 return;
  14735.             }
  14736.             $popup('newposts', Lng.loading[lang], true);
  14737.             forceLoadPosts();
  14738.         },
  14739.         pauseUpdater() {
  14740.             if(enabled && !paused) {
  14741.                 updMachine.stopUpdater();
  14742.                 paused = true;
  14743.             }
  14744.         },
  14745.         refToYou(pNum) {
  14746.             if(doc.hidden) {
  14747.                 repliesToYou.add(pNum);
  14748.             }
  14749.         },
  14750.         toggle() {
  14751.             if(enabled) {
  14752.                 this.disableUpdater();
  14753.             } else {
  14754.                 this.enableUpdater();
  14755.             }
  14756.         },
  14757.         toggleAudio(repeatMS) {
  14758.             if(audio.enabled) {
  14759.                 audio.stopAudio();
  14760.                 return (audio.enabled = false);
  14761.             }
  14762.             audio.repeatMS = repeatMS;
  14763.             return (audio.enabled = true);
  14764.         },
  14765.         toggleCounter(enableCnt) {
  14766.             if(enableCnt) {
  14767.                 counter.enableCounter();
  14768.                 counter.setWait();
  14769.             } else {
  14770.                 counter.disableCounter();
  14771.             }
  14772.             forceLoadPosts();
  14773.         },
  14774.         sendErrNotif() {
  14775.             if(Cfg.sendErrNotif && doc.hidden) {
  14776.                 sendError = true;
  14777.                 updateTitle();
  14778.             }
  14779.         }
  14780.     };
  14781. }
  14782.  
  14783. /* ==[ DelForm.js ]===========================================================================================
  14784.                                                    DELFORM
  14785. =========================================================================================================== */
  14786.  
  14787. class DelForm {
  14788.     constructor(formEl, pageNum, prev) {
  14789.         let thr = null;
  14790.         this.el = formEl;
  14791.         this.firstThr = null;
  14792.         this.lastThr = null;
  14793.         this.next = null;
  14794.         this.pageNum = pageNum;
  14795.         this.prev = prev;
  14796.         if(prev) {
  14797.             prev.next = this;
  14798.             thr = prev.lastThr;
  14799.         }
  14800.         formEl.setAttribute('de-form', '');
  14801.         formEl.removeAttribute('id');
  14802.         $delAll('script', this.el);
  14803.         const threads = DelForm.getThreads(this.el);
  14804.         for(let i = 0, len = threads.length; i < len; ++i) {
  14805.             const num = aib.getTNum(threads[i]);
  14806.             if(!DelForm.tNums.has(num)) {
  14807.                 DelForm.tNums.add(num);
  14808.                 thr = new Thread(threads[i], num, thr, this);
  14809.                 if(this.firstThr === null) {
  14810.                     this.firstThr = thr;
  14811.                 }
  14812.                 continue;
  14813.             }
  14814.             const el = threads[i];
  14815.             const thrNext = threads[i + 1];
  14816.             let elNext = el.nextSibling;
  14817.             while(elNext && elNext !== thrNext) {
  14818.                 elNext.remove();
  14819.                 elNext = el.nextSibling;
  14820.             }
  14821.             el.remove();
  14822.             console.log('Repeated thread: ' + num);
  14823.         }
  14824.         if(this.firstThr === null) {
  14825.             if(prev) {
  14826.                 this.lastThr = prev.lastThr;
  14827.             }
  14828.             return;
  14829.         }
  14830.         this.lastThr = thr;
  14831.     }
  14832.     static getThreads(formEl) {
  14833.         let threads = $Q(aib.qThread, formEl);
  14834.         let len = threads.length;
  14835.         if(len === 0) {
  14836.             if(localData) {
  14837.                 threads = $Q('div[de-thread]');
  14838.                 len = threads.length;
  14839.             }
  14840.             if(len === 0) {
  14841.                 threads = DelForm._parseClasslessThreads(formEl);
  14842.             }
  14843.         }
  14844.         return threads;
  14845.     }
  14846.     static [Symbol.iterator]() {
  14847.         return {
  14848.             _data: this.first,
  14849.             next() {
  14850.                 const value = this._data;
  14851.                 if(value) {
  14852.                     this._data = value.next;
  14853.                     return { value, done: false };
  14854.                 }
  14855.                 return { done: true };
  14856.             }
  14857.         };
  14858.     }
  14859.  
  14860.     static _parseClasslessThreads(formEl) {
  14861.         let i, len, cThr = doc.createElement('div');
  14862.         const threads = [];
  14863.         const fNodes = [...formEl.childNodes];
  14864.         for(i = 0, len = fNodes.length - 1; i < len; ++i) {
  14865.             const el = fNodes[i];
  14866.             if(el.tagName === 'HR') {
  14867.                 formEl.insertBefore(cThr, el);
  14868.                 const lastEl = cThr.lastElementChild;
  14869.                 if(lastEl.tagName === 'BR') {
  14870.                     formEl.insertBefore(lastEl, el);
  14871.                 }
  14872.                 try {
  14873.                     aib.getTNum(cThr);
  14874.                     threads.push(cThr);
  14875.                 } catch(err) {}
  14876.                 cThr = doc.createElement('div');
  14877.             } else {
  14878.                 cThr.appendChild(el);
  14879.             }
  14880.         }
  14881.         cThr.appendChild(fNodes[i]);
  14882.         formEl.appendChild(cThr);
  14883.         return threads;
  14884.     }
  14885.     get passEl() {
  14886.         const value = aib.qDelPassw ? $q(aib.qDelPassw, this.el) : null;
  14887.         Object.defineProperty(this, 'passEl', { value });
  14888.         return value;
  14889.     }
  14890.     addStuff() {
  14891.         const { el } = this;
  14892.         if(Cfg.ajaxPosting && !localData) {
  14893.             const delBtn = aib.qDelBut ? $q(aib.qDelBut, el) : null;
  14894.             if(delBtn) {
  14895.                 el.onsubmit = $pd;
  14896.                 delBtn.onclick = e => {
  14897.                     $pd(e);
  14898.                     pr.closeReply();
  14899.                     $popup('delete', Lng.deleting[lang], true);
  14900.                     html5Submit(el, e.target).then(checkDelete)
  14901.                         .catch(err => $popup('delete', getErrorMessage(err)));
  14902.                 };
  14903.             }
  14904.             Logger.log('Init AJAX');
  14905.         }
  14906.         ContentLoader.preloadImages(el);
  14907.         Logger.log('Preload images');
  14908.         embedAudioLinks(el);
  14909.         Logger.log('Audio links');
  14910.         if(Cfg.embedYTube) {
  14911.             new VideosParser().parse(el).endParser();
  14912.             Logger.log('Video links');
  14913.         }
  14914.         processImgInfoLinks(el);
  14915.         Logger.log('Image names');
  14916.         RefMap.initRefMap(this);
  14917.         Logger.log('Reflinks map');
  14918.     }
  14919. }
  14920. DelForm.tNums = new Set();
  14921.  
  14922. /* ==[ Browser.js ]===========================================================================================
  14923.                                       BROWSER DETECTORS AND DEPENDENCIES
  14924. =========================================================================================================== */
  14925.  
  14926. function checkStorage() {
  14927.     try {
  14928.         locStorage = deWindow.localStorage;
  14929.         sesStorage = deWindow.sessionStorage;
  14930.         sesStorage['de-test'] = 1;
  14931.     } catch(err) {
  14932.         if(typeof unsafeWindow !== 'undefined') {
  14933.             locStorage = unsafeWindow.localStorage;
  14934.             sesStorage = unsafeWindow.sessionStorage;
  14935.         }
  14936.     }
  14937.     if(!(locStorage && (typeof locStorage === 'object') && sesStorage)) {
  14938.         console.error('Webstorage error: please, enable webstorage!');
  14939.         return false;
  14940.     }
  14941.     return true;
  14942. }
  14943.  
  14944. // Browser identification and browser-specific hacks
  14945. function initNavFuncs() {
  14946.     const ua = navigator.userAgent;
  14947.     const isFirefox = ua.includes('Gecko/');
  14948.     const isWebkit = ua.includes('WebKit/');
  14949.     const isChrome = isWebkit && ua.includes('Chrome/');
  14950.     const isSafari = isWebkit && !isChrome;
  14951.     const hasPrestoStorage = !!prestoStorage && !ua.includes('Opera Mobi');
  14952.     const hasNewGM = /* global GM */ typeof GM !== 'undefined' && typeof GM.xmlHttpRequest === 'function';
  14953.     const canUseFetch = 'AbortController' in deWindow; // Firefox 57+, Chrome 66+, Safari 11.1+
  14954.     let scriptHandler, hasWebStorage = false;
  14955.     let hasOldGM = false;
  14956.     if(hasNewGM) {
  14957.         scriptHandler = GM.info ? `${ GM.info.scriptHandler } ${ GM.info.version }` : 'Greasemonkey';
  14958.     } else {
  14959.         try {
  14960.             hasOldGM = (typeof GM_setValue === 'function') &&
  14961.                 (!isChrome || !GM_setValue.toString().includes('not supported'));
  14962.         } catch(err) {
  14963.             hasOldGM = err.message === 'Permission denied to access property "toString"'; // Chrome
  14964.         }
  14965.         hasWebStorage = !hasOldGM && (isFirefox || ('chrome' in deWindow)) &&
  14966.             (typeof chrome === 'object') && !!chrome && !!chrome.storage;
  14967.         scriptHandler = hasWebStorage ? 'WebExtension' :
  14968.             typeof GM_info === 'undefined' ? isFirefox ? 'Scriptish' : 'Unknown' :
  14969.             GM_info.scriptHandler ? `${ GM_info.scriptHandler } ${ GM_info.version }` :
  14970.             isFirefox ? 'Greasemonkey' : 'Unknown';
  14971.     }
  14972.     if(!('requestAnimationFrame' in deWindow)) { // XXX: Opera Presto
  14973.         deWindow.requestAnimationFrame = fn => setTimeout(fn, 0);
  14974.     }
  14975.     if(!('remove' in Element.prototype)) { // XXX: Opera Presto
  14976.         Element.prototype.remove = function() {
  14977.             const el = this.parentNode;
  14978.             if(el) {
  14979.                 el.removeChild(this);
  14980.             }
  14981.         };
  14982.     }
  14983.     const nlProto = NodeList.prototype;
  14984.     $each = 'forEach' in nlProto ?
  14985.         (els, cb) => nlProto.forEach.call(els, cb) :
  14986.         (els, cb) => aProto.forEach.call(els, cb);
  14987.     let needFileHack = false;
  14988.     try {
  14989.         new File([''], '');
  14990.         if(isFirefox || isSafari) {
  14991.             needFileHack = !FormData.prototype.get;
  14992.         }
  14993.     } catch(err) {
  14994.         needFileHack = true;
  14995.     }
  14996.     if(needFileHack && FormData) { // XXX: Firefox < 39, Chrome < 50, Safari < 11
  14997.         const OrigFormData = FormData;
  14998.         const origAppend = FormData.prototype.append;
  14999.         FormData = function FormData(form) {
  15000.             const rv = form ? new OrigFormData(form) : new OrigFormData();
  15001.             rv.append = function append(name, value, fileName = null) {
  15002.                 if(value instanceof Blob && 'name' in value && fileName === null) {
  15003.                     return origAppend.call(this, name, value, value.name);
  15004.                 }
  15005.                 return origAppend.apply(this, arguments);
  15006.             };
  15007.             return rv;
  15008.         };
  15009.         deWindow.File = function File(arr, name) {
  15010.             const rv = new Blob(arr);
  15011.             rv.name = name;
  15012.             return rv;
  15013.         };
  15014.     }
  15015.     nav = {
  15016.         cssMatches: (leftSel, ...rules) => leftSel.split(', ').map(
  15017.             val => val + rules.join(', ' + val)
  15018.         ).join(', '),
  15019.         canUseFetch,
  15020.         canUseFetchBlob  : canUseFetch && !(isChrome && scriptHandler === 'WebExtension'),
  15021.         canUseNativeXHR  : true,
  15022.         firefoxVer       : isFirefox ? +(ua.match(/Firefox\/(\d+)/) || [0, 0])[1] : 0,
  15023.         fixLink          : isSafari ? getAbsLink : url => url,
  15024.         hasGlobalStorage : hasOldGM || hasNewGM || hasWebStorage || hasPrestoStorage,
  15025.         hasGMXHR         : (typeof GM_xmlhttpRequest === 'function') ||
  15026.             hasNewGM && (typeof GM.xmlHttpRequest === 'function'),
  15027.         hasNewGM,
  15028.         hasOldGM,
  15029.         hasPrestoStorage,
  15030.         hasWebStorage,
  15031.         isChrome,
  15032.         isESNext : typeof deMainFuncOuter === 'undefined',
  15033.         isFirefox,
  15034.         isMsEdge : ua.includes('Edge/'),
  15035.         isPresto : !!deWindow.opera,
  15036.         isSafari,
  15037.         isWebkit,
  15038.         scriptHandler,
  15039.         ua       : navigator.userAgent + (isFirefox ? ` [${ navigator.buildID }]` : ''),
  15040.  
  15041.         get canPlayMP3() {
  15042.             const value = !!new Audio().canPlayType('audio/mpeg;');
  15043.             Object.defineProperty(this, 'canPlayMP3', { value });
  15044.             return value;
  15045.         },
  15046.         get hasTemplate() {
  15047.             const value = 'content' in doc.createElement('template');
  15048.             Object.defineProperty(this, 'hasTemplate', { value });
  15049.             return value;
  15050.         },
  15051.         get hasWorker() {
  15052.             let value = false;
  15053.             try {
  15054.                 value = 'Worker' in deWindow && 'URL' in deWindow;
  15055.             } catch(err) {}
  15056.             if(value && this.isFirefox) {
  15057.                 value = this.firefoxVer >= 40;
  15058.             }
  15059.             Object.defineProperty(this, 'hasWorker', { value });
  15060.             return value;
  15061.         },
  15062.         get matchesSelector() {
  15063.             const dE = doc.documentElement;
  15064.             const func = dE.matches || dE.mozMatchesSelector ||
  15065.                 dE.webkitMatchesSelector || dE.oMatchesSelector;
  15066.             const value = (el, sel) => func.call(el, sel);
  15067.             Object.defineProperty(this, 'matchesSelector', { value });
  15068.             return value;
  15069.         },
  15070.         get viewportHeight() {
  15071.             const value = doc.compatMode && doc.compatMode === 'CSS1Compat' ?
  15072.                 () => doc.documentElement.clientHeight : () => docBody.clientHeight;
  15073.             Object.defineProperty(this, 'viewportHeight', { value });
  15074.             return value;
  15075.         },
  15076.         get viewportWidth() {
  15077.             const value = doc.compatMode && doc.compatMode === 'CSS1Compat' ?
  15078.                 () => doc.documentElement.clientWidth : () => docBody.clientWidth;
  15079.             Object.defineProperty(this, 'viewportWidth', { value });
  15080.             return value;
  15081.         },
  15082.         getUnsafeUint8Array(data, i, len) { // XXX: Old Greasemonkeys
  15083.             let Ctor = Uint8Array;
  15084.             if(nav.isFirefox && nav.hasOldGM) {
  15085.                 try {
  15086.                     if(!(new Uint8Array(data) instanceof Uint8Array)) {
  15087.                         Ctor = unsafeWindow.Uint8Array;
  15088.                     }
  15089.                 } catch(err) {
  15090.                     Ctor = unsafeWindow.Uint8Array;
  15091.                 }
  15092.             }
  15093.             switch(arguments.length) {
  15094.             case 1: return new Ctor(data);
  15095.             case 2: return new Ctor(data, i);
  15096.             case 3: return new Ctor(data, i, len);
  15097.             }
  15098.             throw new Error();
  15099.         },
  15100.         getUnsafeDataView(data, offset) { // XXX: Old Greasemonkeys
  15101.             const value = new DataView(data, offset || 0);
  15102.             return !nav.isFirefox || !nav.hasOldGM || (value instanceof DataView) ? value :
  15103.                 new unsafeWindow.DataView(data, offset || 0);
  15104.         }
  15105.     };
  15106. }
  15107.  
  15108. /* ==[ BoardDefaults.js ]=====================================================================================
  15109.                                              IMAGEBOARD DEFAULTS
  15110. =========================================================================================================== */
  15111.  
  15112. class BaseBoard {
  15113.     constructor(prot, dm) {
  15114.         // Query paths
  15115.         this.cReply = 'reply';
  15116.         this.qBan = null;
  15117.         this.qClosed = null;
  15118.         this.qDelBut = 'input[type="submit"]';
  15119.         this.qDelPassw = 'input[type="password"], input[name="password"]';
  15120.         this.qDForm = '#delform, form[name="delform"]';
  15121.         this.qError = 'h1, h2, font[size="5"]';
  15122.         this.qForm = '#postform';
  15123.         this.qFormFile = 'tr input[type="file"]';
  15124.         this.qFormPassw = 'tr input[type="password"]';
  15125.         this.qFormRedir = 'input[name="postredir"][value="1"]';
  15126.         this.qFormRules = '.rules, #rules';
  15127.         this.qFormSpoiler = 'input[type="checkbox"][name="spoiler"]'; // Ernstchan
  15128.         this.qFormSubm = 'tr input[type="submit"]';
  15129.         this.qFormTd = 'td';
  15130.         this.qFormTr = 'tr';
  15131.         this.qFormTxta = 'tr:not([style*="none"]) textarea:not([style*="display:none"])'; // Makaba
  15132.         this.qImgInfo = '.filesize';
  15133.         this.qOmitted = '.omittedposts';
  15134.         this.qOPost = '.oppost';
  15135.         this.qPages = 'table[border="1"] > tbody > tr > td:nth-child(2) > a:last-of-type';
  15136.         this.qPostHeader = '.de-post-btns';
  15137.         this.qPostImg = '.thumb, .ca_thumb, img[src*="thumb"], img[src*="/spoiler"], img[src^="blob:"]';
  15138.         this.qPostMsg = 'blockquote';
  15139.         this.qPostName = '.postername, .commentpostername';
  15140.         this.qPostSubj = '.filetitle';
  15141.         this.qPostTrip = '.postertrip';
  15142.         this.qPostRef = '.reflink';
  15143.         this.qPostsParent = null;
  15144.         this.qRPost = '.reply';
  15145.         this.qTrunc = '.abbrev, .abbr, .shortened';
  15146.         this._qOPostEnd = 'form > table, div > table, div[id^="repl"]';
  15147.  
  15148.         // Other propertioes
  15149.         this.anchor = '#';
  15150.         this.b = '';
  15151.         this.dm = dm;
  15152.         this.docExt = null;
  15153.         this.firstPage = 0;
  15154.         this.formParent = 'parent';
  15155.         this.hasAltCaptcha = false;
  15156.         this.hasCatalog = false;
  15157.         this.hasOPNum = false;
  15158.         this.hasPicWrap = false;
  15159.         this.hasRefererErr = false;
  15160.         this.hasTextLinks = false;
  15161.         this.host = deWindow.location.hostname;
  15162.         this.JsonBuilder = null;
  15163.         this.jsonSubmit = false;
  15164.         this.markupBB = false;
  15165.         this.multiFile = false;
  15166.         this.page = 0;
  15167.         this.prot = prot;
  15168.         this.res = 'res/';
  15169.         this.ru = false;
  15170.         this.t = false;
  15171.         this.timePattern = 'w+dd+m+yyyy+hh+ii+ss';
  15172.  
  15173.         // Imageboard-specific booleans
  15174.         this._02ch = false;
  15175.         this._2channel = false;
  15176.         this._4chan = false;
  15177.         this.dobrochan = false;
  15178.         this.iichan = false;
  15179.         this.makaba = false;
  15180.     }
  15181.     get qFormMail() {
  15182.         return nav.cssMatches('tr:not([style*="none"]) input:not([type="hidden"]):not([style*="none"])',
  15183.             '[name="email"]', '[name="em"]', '[name="field2"]', '[name="sage"]');
  15184.     }
  15185.     get qFormName() {
  15186.         return nav.cssMatches('tr:not([style*="none"]) input:not([type="hidden"]):not([style*="none"])',
  15187.             '[name="name"]', '[name="field1"]');
  15188.     }
  15189.     get qFormSubj() {
  15190.         return nav.cssMatches('tr:not([style*="none"]) input:not([type="hidden"]):not([style*="none"])',
  15191.             '[name="subject"]', '[name="field3"]');
  15192.     }
  15193.     get qImgNameLink() {
  15194.         const value = nav.cssMatches(this.qImgInfo.split(', ').join(' a, ') + ' a',
  15195.             '[href$=".jpg"]', '[href$=".jpeg"]', '[href$=".png"]', '[href$=".gif"]', '[href$=".webm"]',
  15196.             '[href$=".webp"]', '[href$=".mp4"]', '[href$=".ogv"]', '[href$=".apng"]', ', [href^="blob:"]');
  15197.         Object.defineProperty(this, 'qImgNameLink', { value });
  15198.         return value;
  15199.     }
  15200.     get qMsgImgLink() { // Sets here only
  15201.         const value = nav.cssMatches(this.qPostMsg.split(', ').join(' a, ') + ' a',
  15202.             '[href$=".jpg"]', '[href$=".jpeg"]', '[href$=".png"]', '[href$=".gif"]');
  15203.         Object.defineProperty(this, 'qMsgImgLink', { value });
  15204.         return value;
  15205.     }
  15206.     get qThread() {
  15207.         const value = $q('.thread') ? '.thread' : '[id^="thread"]';
  15208.         Object.defineProperty(this, 'qThread', { value });
  15209.         return value;
  15210.     }
  15211.     get capLang() { // _410chan
  15212.         return this.ru ? 2 : 1;
  15213.     }
  15214.     get catalogUrl() { // Iichan
  15215.         return `${ this.prot }//${ this.host }/${ this.b }/catalog.html`;
  15216.     }
  15217.     get changeReplyMode() {
  15218.         return null;
  15219.     }
  15220.     get css() {
  15221.         return '';
  15222.     }
  15223.     get deleteTruncMsg() {
  15224.         return null;
  15225.     }
  15226.     get fixDeadLinks() { // _4chan
  15227.         return null;
  15228.     }
  15229.     get fixHTMLHelper() {
  15230.         return null;
  15231.     }
  15232.     get fixFileInputs() {
  15233.         return null;
  15234.     }
  15235.     get getImgRedirectSrc() { // Archived
  15236.         return null;
  15237.     }
  15238.     get getSubmitData() {
  15239.         return null;
  15240.     }
  15241.     get initCaptcha() {
  15242.         return null;
  15243.     }
  15244.     get isArchived() {
  15245.         return false;
  15246.     }
  15247.     get lastPage() { // Makaba
  15248.         const el = $q(this.qPages);
  15249.         let value = el && +aProto.pop.call(el.textContent.match(/\d+/g) || []) || 0;
  15250.         if(this.page === value + 1) {
  15251.             value++;
  15252.         }
  15253.         Object.defineProperty(this, 'lastPage', { value });
  15254.         return value;
  15255.     }
  15256.     get markupTags() {
  15257.         return this.markupBB ? ['b', 'i', 'u', 's', 'spoiler', 'code'] : ['**', '*', '', '^H', '%%', '`'];
  15258.     }
  15259.     get observeContent() { // Makaba
  15260.         return null;
  15261.     }
  15262.     get reCrossLinks() { // Sets here only
  15263.         const value = new RegExp(`>https?:\\/\\/[^\\/]*${ this.dm }\\/([a-z0-9]+)\\/${
  15264.             quoteReg(this.res) }(\\d+)(?:[^#<]+)?(?:#i?(\\d+))?<`, 'g');
  15265.         Object.defineProperty(this, 'reCrossLinks', { value });
  15266.         return value;
  15267.     }
  15268.     get reportForm() {
  15269.         return null;
  15270.     }
  15271.     get sendHTML5Post() { // Lynxchan
  15272.         return null;
  15273.     }
  15274.     get updateCaptcha() {
  15275.         return null;
  15276.     }
  15277.     disableRedirection(el) { // Dobrochan
  15278.         $hide($qParent(el, aib.qFormTr));
  15279.         el.checked = true;
  15280.     }
  15281.     fixHTML(data, isForm = false) {
  15282.         if(!(dTime || Spells.reps || Cfg.crossLinks || Cfg.decodeLinks ||
  15283.             this.fixHTMLHelper || this.fixDeadLinks || this.hasTextLinks)
  15284.         ) {
  15285.             return data;
  15286.         }
  15287.         let str;
  15288.         if(typeof data === 'string') {
  15289.             str = data;
  15290.         } else if(isForm) {
  15291.             data.id = 'de-dform-old';
  15292.             str = data.outerHTML;
  15293.         } else {
  15294.             str = data.innerHTML;
  15295.         }
  15296.         if(dTime) {
  15297.             str = dTime.fix(str);
  15298.         }
  15299.         if(this.fixHTMLHelper) {
  15300.             str = this.fixHTMLHelper(str);
  15301.         }
  15302.         if(this.fixDeadLinks) {
  15303.             str = this.fixDeadLinks(str);
  15304.         }
  15305.         if(this.hasTextLinks) {
  15306.             str = str.replace(/(^|>|\s|&gt;)(https*:\/\/[^"<>]*?)(<\/a>)?(?=$|<|\s)/ig,
  15307.                 (x, a, b, c) => c ? x : `${ a }<a rel="noreferrer" href="${ b }">${ b }</a>`);
  15308.         }
  15309.         if(Spells.reps) {
  15310.             str = Spells.replace(str);
  15311.         }
  15312.         if(Cfg.crossLinks) {
  15313.             str = str.replace(aib.reCrossLinks, (_, b, tNum, pNum) => `>&gt;&gt;/${ b }/${ pNum || tNum }<`);
  15314.         }
  15315.         if(Cfg.decodeLinks) {
  15316.             str = str.replace(/>https?:\/\/[^<]+</ig, match => {
  15317.                 try {
  15318.                     return decodeURI(match);
  15319.                 } catch(err) {}
  15320.                 return match;
  15321.             });
  15322.         }
  15323.         if(typeof data === 'string') {
  15324.             return str;
  15325.         }
  15326.         if(isForm) {
  15327.             const newForm = $bBegin(data, str);
  15328.             $hide(data);
  15329.             deWindow.addEventListener('load', () => $del($id('de-dform-old')));
  15330.             return newForm;
  15331.         }
  15332.         data.innerHTML = str;
  15333.         return data;
  15334.     }
  15335.     fixVideo(isPost, data) {
  15336.         const videos = [];
  15337.         const els = $Q('embed, object, iframe', isPost ? data.el : data);
  15338.         for(let i = 0, len = els.length; i < len; ++i) {
  15339.             const el = els[i];
  15340.             const src = el.src || el.data;
  15341.             if(!src) {
  15342.                 continue;
  15343.             }
  15344.             let m = src.match(Videos.ytReg);
  15345.             if(m) {
  15346.                 videos.push([isPost ? data : this.getPostOfEl(el), m, true]);
  15347.                 $del(el);
  15348.             }
  15349.             if(Cfg.addVimeo && (m = src.match(Videos.vimReg))) {
  15350.                 videos.push([isPost ? data : this.getPostOfEl(el), m, false]);
  15351.                 $del(el);
  15352.             }
  15353.         }
  15354.         return videos;
  15355.     }
  15356.     getBanId(postEl) { // Makaba
  15357.         return this.qBan && $q(this.qBan, postEl) ? 1 : 0;
  15358.     }
  15359.     getCapParent(el) {
  15360.         return $qParent(el, this.qFormTr);
  15361.     }
  15362.     getCaptchaSrc(src, tNum) {
  15363.         const temp = src.replace(/pl$/, 'pl?key=mainpage&amp;dummy=')
  15364.             .replace(/dummy=[\d.]*/, 'dummy=' + Math.random());
  15365.         return tNum ? temp.replace(/mainpage|res\d+/, 'res' + tNum) : temp.replace(/res\d+/, 'mainpage');
  15366.     }
  15367.     getImgInfo(wrap) {
  15368.         const el = $q(this.qImgInfo, wrap);
  15369.         return el ? el.textContent : '';
  15370.     }
  15371.     getImgRealName(wrap) {
  15372.         const el = $q(this.qImgNameLink, wrap);
  15373.         return el ? el.title || el.textContent : '';
  15374.     }
  15375.     getImgSrcLink(img) {
  15376.         return $parent(img, 'A');
  15377.     }
  15378.     getImgWrap(img) {
  15379.         return ($parent(img, 'A') || img).parentNode;
  15380.     }
  15381.     getJsonApiUrl() {}
  15382.     getOmitted(el) {
  15383.         return +(el && (el.textContent || '').match(/\d+/)) + 1;
  15384.     }
  15385.     getOp(thr) { // Arhivach
  15386.         let op = localData ? $q('.de-oppost', thr) : $q(this.qOPost, thr);
  15387.         if(op) {
  15388.             return op;
  15389.         }
  15390.         op = thr.ownerDocument.createElement('div');
  15391.         op.classList.add('de-oppost');
  15392.         let el;
  15393.         const opEnd = $q(this._qOPostEnd, thr);
  15394.         while((el = thr.firstChild) && (el !== opEnd)) {
  15395.             op.appendChild(el);
  15396.         }
  15397.         if(thr.hasChildNodes()) {
  15398.             thr.insertBefore(op, thr.firstChild);
  15399.         } else {
  15400.             thr.appendChild(op);
  15401.         }
  15402.         return op;
  15403.     }
  15404.     getPageUrl(b, p) {
  15405.         return fixBrd(b) + (p > 0 ? p + this.docExt : '');
  15406.     }
  15407.     getPNum(post) {
  15408.         return +post.id.match(/\d+/);
  15409.     }
  15410.     getPostElOfEl(el) {
  15411.         const sel = this.qRPost + ', [de-thread], .de-pview';
  15412.         while(el && !nav.matchesSelector(el, sel)) {
  15413.             el = el.parentElement;
  15414.         }
  15415.         return el;
  15416.     }
  15417.     getPostOfEl(el) { // Sets here only
  15418.         return pByEl.get(this.getPostElOfEl(el));
  15419.     }
  15420.     getPostWrap(el, isOp) {
  15421.         if(isOp) {
  15422.             return el;
  15423.         }
  15424.         Object.defineProperty(this, 'getPostWrap',
  15425.             { value: el.tagName === 'TD' ? (el, isOp) => isOp ? el : $parent(el, 'TABLE') : el => el });
  15426.         return this.getPostWrap(el, isOp);
  15427.     }
  15428.     getSage(post) {
  15429.         if($q('.sage', post)) {
  15430.             return true;
  15431.         }
  15432.         const el = $q('a[href^="mailto:"], a[href="sage"]', post);
  15433.         return !!el && /sage/i.test(el.href);
  15434.     }
  15435.     getThrUrl(b, tNum) { // Arhivach
  15436.         return this.prot + '//' + this.host + fixBrd(b) + this.res + tNum + this.docExt;
  15437.     }
  15438.     getTNum(thr) {
  15439.         return +$q('input[type="checkbox"]', thr).value;
  15440.     }
  15441.     insertYtPlayer(msg, playerHtml) { // Dobrochan
  15442.         return $bBegin(msg, playerHtml);
  15443.     }
  15444.     isAjaxStatusOK(status) {
  15445.         return status === 200 || status === 206;
  15446.     }
  15447.     isIgnoreError(txt) { // Lynxchan
  15448.         return /successful|uploaded|updating|post deleted|post created|обновл|удален[о.]/i.test(txt);
  15449.     }
  15450.     parseURL() {
  15451.         const url = (deWindow.location.pathname || '').replace(/^[/]+/, '').replace(/[/]+/g, '/');
  15452.         if(url.match(this.res)) { // We are in thread
  15453.             const temp = url.split(this.res);
  15454.             this.b = temp[0].replace(/\/$/, '');
  15455.             this.t = +temp[1].match(/^[^\d]?\d+/)[0];
  15456.             this.page = this.firstPage;
  15457.         } else { // We are on board
  15458.             const temp = url.match(/\/?(\d+)[^/]*?$/);
  15459.             this.page = temp && +temp[1] || this.firstPage;
  15460.             this.b = url.replace(temp && this.page ? temp[0] : /\/(?:[^/]+\.[a-z]+)?$/, '');
  15461.         }
  15462.         if(this.docExt === null) {
  15463.             this.docExt = (url.match(/\.[a-z]+$/) || ['.html'])[0];
  15464.         }
  15465.     }
  15466.     updateSubmitBtn(el) {
  15467.         el.value = Lng.reply[lang];
  15468.     }
  15469. }
  15470.  
  15471. /* ==[ BoardDetector.js ]=====================================================================================
  15472.                                              IMAGEBOARD DETECTOR
  15473. =========================================================================================================== */
  15474.  
  15475. function getImageBoard(checkDomains, checkEngines) {
  15476.     const ibDomains = {};
  15477.     const ibEngines = [];
  15478.  
  15479.     // ENGINES
  15480.     ibEngines.push(['form[action$="wakaba.pl"]', BaseBoard]);
  15481.  
  15482.     class Kusaba extends BaseBoard {
  15483.         constructor(prot, dm) {
  15484.             super(prot, dm);
  15485.             this.kusaba = true;
  15486.  
  15487.             this.qError = 'h1, h2, div[style*="1.25em"]';
  15488.             this.qFormRedir = 'input[name="redirecttothread"][value="1"]';
  15489.  
  15490.             this.formParent = 'replythread';
  15491.             this.markupBB = true;
  15492.         }
  15493.         get css() {
  15494.             return `.extrabtns > a, .extrabtns > span, #newposts_get, .replymode,
  15495.                     .ui-resizable-handle, blockquote + a { display: none !important; }
  15496.                 .ui-wrapper { display: inline-block; width: auto !important;
  15497.                     height: auto !important; padding: 0 !important; }`;
  15498.         }
  15499.         getCaptchaSrc(src) {
  15500.             return src.replace(/\?[^?]+$|$/, '?' + Math.random());
  15501.         }
  15502.         getImgRealName(wrap) {
  15503.             const el = $q('.filesize', wrap);
  15504.             if(el) {
  15505.                 const info = el.textContent.split(',');
  15506.                 if(info.length > 2) {
  15507.                     return info.pop().replace(')', '');
  15508.                 }
  15509.             }
  15510.             return super.getImgRealName(wrap);
  15511.         }
  15512.         init() {
  15513.             const el = $id('posttypeindicator');
  15514.             if(el) {
  15515.                 [el.previousSibling, el.nextSibling, el].forEach($del);
  15516.             }
  15517.             return false;
  15518.         }
  15519.     }
  15520.     ibEngines.push(['script[src*="kusaba"]', Kusaba], ['form#delform[action$="/board.php"]', Kusaba]);
  15521.  
  15522.     class Tinyboard extends BaseBoard {
  15523.         constructor(prot, dm) {
  15524.             super(prot, dm);
  15525.  
  15526.             this.cReply = 'post reply';
  15527.             this.qClosed = '.fa-lock';
  15528.             this.qDForm = 'form[name*="postcontrols"]';
  15529.             this.qForm = 'form[name="post"]';
  15530.             this.qFormPassw = 'input[name="password"]:not([type="hidden"])';
  15531.             this.qFormRedir = null;
  15532.             this.qImgInfo = '.fileinfo';
  15533.             this.qOmitted = '.omitted';
  15534.             this.qPages = '.pages';
  15535.             this.qPostHeader = '.intro';
  15536.             this.qPostMsg = '.body';
  15537.             this.qPostName = '.name';
  15538.             this.qPostRef = '.post_no + a';
  15539.             this.qPostSubj = '.subject';
  15540.             this.qPostTrip = '.trip';
  15541.             this.qTrunc = '.toolong';
  15542.             this._origInputs = null;
  15543.             this._qOPostEnd = '.post.reply';
  15544.  
  15545.             this.firstPage = 1;
  15546.             this.formParent = 'thread';
  15547.             this.hasCatalog = true;
  15548.             this.hasRefererErr = true;
  15549.             this.jsonSubmit = true;
  15550.             this.timePattern = 'nn+dd+yy++w++hh+ii+ss';
  15551.         }
  15552.         get qImgNameLink() {
  15553.             return 'p.fileinfo > a:first-of-type';
  15554.         }
  15555.         get css() {
  15556.             return `.banner, .hide-thread-link, .mentioned,
  15557.                     .post-hover { display: none !important; }
  15558.                 div.post.reply:not(.de-entry):not(.de-cfg-tab):not(.de-win-body) {
  15559.                     float: left !important; clear: left; display: block; }
  15560.                 ${ Cfg.imgNames ? `.postfilename, .unimportant > a[download] { display: none }
  15561.                     .fileinfo > .unimportant { white-space: nowrap; }` : '' }`;
  15562.         }
  15563.         get markupTags() {
  15564.             return ["'''", "''", '__', '~~', '**', '[code'];
  15565.         }
  15566.         async changeReplyMode(form, tNum) {
  15567.             if(!this._origInputs && !$q('input[name="hash"]', form)) {
  15568.                 // Board without antibot protection
  15569.                 pr.subm.value = Lng.reply[lang];
  15570.                 const pageInp = $q('input[name="page"]', form);
  15571.                 if(tNum) {
  15572.                     $del(pageInp);
  15573.                 } else if(!pageInp) {
  15574.                     form.insertAdjacentHTML('beforeend', '<input name="page" value="1" type="hidden">');
  15575.                 }
  15576.                 return;
  15577.             }
  15578.             const query = 'div[style="display:none"], input[style="display:none"], ' +
  15579.                 'span[style="display:none"], textarea[style="display:none"], ' +
  15580.                 'input[type="hidden"]:not(.de-input-hidden)';
  15581.             if(!$q('input[name="thread"]', form)) {
  15582.                 // Switching from the thread creation to post reply mode occurs. Saving the original fields.
  15583.                 this._origInputs = [doc.createElement('div'), pr.subm.value];
  15584.                 $each($Q(query, form), el => this._origInputs[0].appendChild(el));
  15585.             } else if(!tNum) {
  15586.                 // Switching from the post reply to thread creation occurs. Restoring the original fields.
  15587.                 pr.subm.value = this._origInputs[1];
  15588.                 $delAll(query, form);
  15589.                 form.insertAdjacentHTML('beforeend', this._origInputs[0].innerHTML);
  15590.                 this._origInputs = null;
  15591.                 return;
  15592.             }
  15593.             // Post reply mode. Loading a thread with a form that contains the correct hidden fields.
  15594.             const errFn = () => {
  15595.                 $popup('load-form', Lng.errFormLoad[lang]);
  15596.                 pr.closeReply();
  15597.             };
  15598.             $popup('load-form', Lng.loading[lang], true);
  15599.             await ajaxLoad(aib.getThrUrl(this.b, tNum), false).then(loadedDoc => {
  15600.                 const loadedForm = $q(this.qForm, loadedDoc);
  15601.                 if(!loadedForm) {
  15602.                     errFn();
  15603.                     return;
  15604.                 }
  15605.                 pr.subm.value = $q(this.qFormSubm, loadedDoc).value;
  15606.                 $delAll(query, form);
  15607.                 $each($Q(query, loadedForm), el => form.appendChild(doc.adoptNode(el)));
  15608.                 closePopup('load-form');
  15609.             }, errFn);
  15610.         }
  15611.         fixHTML(data, isForm) {
  15612.             const formEl = super.fixHTML(data, isForm);
  15613.             $each($Q('br.clear', formEl), brEl => {
  15614.                 const hr = brEl.nextElementSibling;
  15615.                 if(hr && hr.tagName === 'HR') {
  15616.                     $after(brEl.parentNode, hr);
  15617.                 }
  15618.                 brEl.remove();
  15619.             });
  15620.             return formEl;
  15621.         }
  15622.         fixVideo(isPost, data) {
  15623.             return Array.from($Q('.video-container, #ytplayer', isPost ? data.el : data), el => {
  15624.                 const value = [isPost ? data : this.getPostOfEl(el), el.id === 'ytplayer' ?
  15625.                     el.src.match(Videos.ytReg) : ['', el.getAttribute('data-video')], true];
  15626.                 el.remove();
  15627.                 return value;
  15628.             });
  15629.         }
  15630.         getImgRealName(wrap) {
  15631.             const el = $q('.postfilename', wrap) ||
  15632.                 $q('.unimportant > a[download]', wrap) || $q(this.qImgNameLink, wrap);
  15633.             return el.title || el.textContent;
  15634.         }
  15635.         getPageUrl(b, p) {
  15636.             return p > 1 ? fixBrd(b) + p + this.docExt : fixBrd(b);
  15637.         }
  15638.         getSubmitData({ error, id }) {
  15639.             return { error, postNum: id && +id };
  15640.         }
  15641.         getTNum(thr) {
  15642.             return +$q('input[type="checkbox"]', thr).name.match(/\d+/);
  15643.         }
  15644.         init() {
  15645.             $script('window.FormData = void 0');
  15646.             const formEl = $q('form[name="post"]');
  15647.             if(formEl) {
  15648.                 formEl.insertAdjacentHTML('beforeend',
  15649.                     '<input class="de-input-hidden" name="json_response" value="1" type="hidden">');
  15650.             }
  15651.             return false;
  15652.         }
  15653.         isAjaxStatusOK(status) {
  15654.             return status === 200 || status === 206 || status === 400;
  15655.         }
  15656.         updateSubmitBtn() {}
  15657.     }
  15658.     ibEngines.push(['form[name*="postcontrols"]', Tinyboard]);
  15659.  
  15660.     class Vichan extends Tinyboard {
  15661.         constructor(prot, dm) {
  15662.             super(prot, dm);
  15663.  
  15664.             this.qDelPassw = '#password';
  15665.             this.qPostImg = '.post-image[alt]:not(.deleted)';
  15666.  
  15667.             this.multiFile = true;
  15668.         }
  15669.         get css() {
  15670.             return `${ super.css }
  15671.                 #expand-all-images, #expand-all-images + .unimportant, .fileinfo > span[style*="nowrap;"],
  15672.                     .post-btn, small, .watchThread { display: none !important; }
  15673.                 body { padding: 0 5px !important; }
  15674.                 .boardlist { z-index: 1 !important; }
  15675.                 .fileinfo { width: 240px; }
  15676.                 .multifile { width: auto !important; }`;
  15677.         }
  15678.         fixFileInputs(el) {
  15679.             el.innerHTML = Array.from({ length: 5 }, (val, i) =>
  15680.                 `<div${ i ? ' style="display: none;"' : '' }>` +
  15681.                 `<input type="file" name="file${ i ? i + 1 : '' }"></div>`
  15682.             ).join('');
  15683.         }
  15684.         fixHTMLHelper(str) {
  15685.             return str.replace(/"\/player\.php\?v=([^&]+)&[^"]+"/g, '"$1"');
  15686.         }
  15687.         init() {
  15688.             super.init();
  15689.             if(locStorage.file_dragdrop !== 'false') {
  15690.                 locStorage.file_dragdrop = false;
  15691.                 deWindow.location.reload();
  15692.                 return true;
  15693.             }
  15694.             $script('highlightReply = Function.prototype');
  15695.             setTimeout(() => $del($id('updater')), 0);
  15696.             const textarea = $id('body');
  15697.             if(textarea) {
  15698.                 textarea.removeAttribute('id');
  15699.             }
  15700.             return false;
  15701.         }
  15702.     }
  15703.     ibEngines.push(['tr#upload', Vichan]);
  15704.  
  15705.     class TinyIB extends BaseBoard {
  15706.         constructor(prot, dm) {
  15707.             super(prot, dm);
  15708.  
  15709.             this.qError = 'body[align=center] div, div[style="margin-top: 50px;"]';
  15710.             this.qPostImg = 'img.thumb, video.thumb';
  15711.             this.qPostMsg = '.message';
  15712.         }
  15713.         get css() {
  15714.             return '.replymode { display: none; }';
  15715.         }
  15716.         fixHTMLHelper(str) {
  15717.             return str.replace(/="\.\.\//g, `="/${ this.b }/`);
  15718.         }
  15719.         getCaptchaSrc(src) {
  15720.             return src.replace(/\?[^?]+$|$/, '?' + Math.random());
  15721.         }
  15722.         getImgWrap(img) {
  15723.             return img.parentNode.parentNode.parentNode;
  15724.         }
  15725.         getImgRealName(wrap) {
  15726.             const el = $q('.filesize', wrap);
  15727.             if(el) {
  15728.                 const info = el.textContent.split(',');
  15729.                 if(info.length > 2) {
  15730.                     return info.pop().replace(')', '');
  15731.                 }
  15732.             }
  15733.             return super.getImgRealName(wrap);
  15734.         }
  15735.         init() {
  15736.             defaultCfg.addTextBtns = 0;
  15737.             $each($Q('.message > .omittedposts'),
  15738.                 el => $replace(el, '<span class="abbrev">Post too long. <a href="#">Click to view.</a>'));
  15739.             return false;
  15740.         }
  15741.     }
  15742.     ibEngines.push(['form[action$="imgboard.php?delete"]', TinyIB]);
  15743.  
  15744.     class newTinyIB extends TinyIB {
  15745.         constructor(prot, dm) {
  15746.             super(prot, dm);
  15747.  
  15748.             this.hasCatalog = true;
  15749.             this.markupBB = true;
  15750.             this.multiFile = true;
  15751.             this.timePattern = 'yy+nn+dd+w+hh+ii+ss';
  15752.         }
  15753.         get fixHTMLHelper() {
  15754.             return null;
  15755.         }
  15756.         fixFileInputs(el) {
  15757.             const str = '><input type="file" name="file[]"></div>';
  15758.             el.innerHTML = '<div' + str + ('<div style="display: none;"' + str).repeat(3);
  15759.         }
  15760.         getImgRealName(wrap) {
  15761.             return $q('.filesize > a', wrap).textContent;
  15762.         }
  15763.         init() {
  15764.             return false;
  15765.         }
  15766.     }
  15767.     ibEngines.push(['body.tinyib', newTinyIB]);
  15768.  
  15769.     class Lynxchan extends BaseBoard {
  15770.         constructor(prot, dm) {
  15771.             super(prot, dm);
  15772.  
  15773.             this.cReply = 'innerPost';
  15774.             this.qDForm = 'form[action$="contentActions.js"]';
  15775.             this.qDelBut = '#deleteFormButton';
  15776.             this.qError = '#errorLabel, #labelMessage';
  15777.             this.qForm = '.form-post, form[action$="newThread.js"], form[action$="replyThread.js"]';
  15778.             this.qFormPassw = 'input[name="password"]';
  15779.             this.qFormRules = '.form-post > .small';
  15780.             this.qFormSubm = '#formButton, #de-postform-submit';
  15781.             this.qImgInfo = '.uploadDetails';
  15782.             this.qOmitted = '.labelOmission';
  15783.             this.qOPost = '.innerOP';
  15784.             this.qPages = '#divPages';
  15785.             this.qPostHeader = '.postInfo, .de-post-btns';
  15786.             this.qPostImg = '.imgLink > img, img[src*="/.media/"]';
  15787.             this.qPostMsg = '.divMessage';
  15788.             this.qPostRef = '.linkQuote';
  15789.             this.qPostSubj = '.labelSubject';
  15790.             this.qPostsParent = '.divPosts';
  15791.             this.qRPost = '.innerPost';
  15792.             this.qTrunc = '.contentOmissionIndicator';
  15793.             this._qOPostEnd = '.divPosts';
  15794.  
  15795.             this.firstPage = 1;
  15796.             this.formParent = 'threadId';
  15797.             this.hasCatalog = true;
  15798.             this.jsonSubmit = true;
  15799.             this.multiFile = true;
  15800.  
  15801.             this._hasNewAPI = false;
  15802.         }
  15803.         get qImgNameLink() {
  15804.             return '.originalNameLink';
  15805.         }
  15806.         get qThread() {
  15807.             return '.opCell';
  15808.         }
  15809.         get css() {
  15810.             return `.de-video-link + div[style="display: inline;"] > .embedButton, .de-parea > hr,
  15811.                     .divRefresh, #jsButton, .hideButton, .nameLink, #newPostFieldset, .panelBacklinks,
  15812.                     .quoteTooltip, body > div[style^="display: inline;"] { display: none !important; }
  15813.                 .divPosts { margin: 0 0; }
  15814.                 #formButton { display: initial !important; }
  15815.                 .form-post button, .form-post input, .form-post img { width: initial; }`;
  15816.         }
  15817.         get markupTags() {
  15818.             return ["'''", "''", '__', '~~', '**', '[code'];
  15819.         }
  15820.         changeReplyMode(form, tNum) {
  15821.             const action = form.getAttribute('action');
  15822.             form.setAttribute('action', tNum ? action.replace('newThread', 'replyThread') :
  15823.                 action.replace('replyThread', 'newThread'));
  15824.         }
  15825.         fixFileInputs(el) {
  15826.             const str = '><input name="files" type="file"></div>';
  15827.             const maxEl = $id('labelMaxFiles');
  15828.             el.innerHTML = '<div' + str +
  15829.                 ('<div style="display: none;"' + str).repeat((maxEl ? +maxEl.textContent : 3) - 1);
  15830.         }
  15831.         getCapParent(el) {
  15832.             return $id('captchaDiv');
  15833.         }
  15834.         getImgRealName(wrap) {
  15835.             return $q('.originalNameLink', wrap).textContent;
  15836.         }
  15837.         getImgSrcLink(img) {
  15838.             const el = img.parentNode;
  15839.             return el.tagName === 'A' ? el : $q('.originalNameLink', el.parentNode);
  15840.         }
  15841.         getImgWrap(img) {
  15842.             return $parent(img, 'FIGURE');
  15843.         }
  15844.         getPageUrl(b, p) {
  15845.             return fixBrd(b) + (p > 1 ? p + this.docExt : 'index.html');
  15846.         }
  15847.         getPNum(post) {
  15848.             return +$q('.deletionCheckBox', post).name.split('-')[2];
  15849.         }
  15850.         getPostWrap(el, isOp) {
  15851.             return isOp ? el : el.parentNode;
  15852.         }
  15853.         getSubmitData({ status, data }) {
  15854.             return {
  15855.                 error   : status === 'error' ? data : null,
  15856.                 postNum : status === 'ok' ? +data : null
  15857.             };
  15858.         }
  15859.         getTNum(thr) {
  15860.             return +$q('.deletionCheckBox', thr).name.split('-')[1];
  15861.         }
  15862.         init() {
  15863.             const submEl = $id('formButton');
  15864.             if(submEl && submEl.type === 'button') {
  15865.                 this._hasNewAPI = true;
  15866.                 $replace(submEl, `<button id="de-postform-submit" type="submit">${
  15867.                     submEl.innerHTML }</button>`);
  15868.             }
  15869.             $script(`if("autoRefresh" in window) {
  15870.                     clearInterval(refreshTimer);
  15871.                 }
  15872.                 if("thread" in window && thread.refreshTimer) {
  15873.                     clearInterval(thread.refreshTimer);
  15874.                     Object.defineProperty(thread, "startTimer",
  15875.                         { value: Function.prototype, writable: false, configurable: false });
  15876.                 }`);
  15877.             const el = $q(this.qForm);
  15878.             if(el && !$q('td', el)) {
  15879.                 const table = $aBegin($q(this.qForm), '<table><tbody></tbody></table>').firstChild;
  15880.                 const els = $Q('#captchaDiv, #divUpload, #fieldEmail, #fieldMessage, #fieldName,' +
  15881.                     ' #fieldPostingPassword, #fieldSubject');
  15882.                 for(let i = 0, len = els.length; i < len; ++i) {
  15883.                     $bEnd(table, '<tr><th></th><td></td></tr>').lastChild.appendChild(els[i]);
  15884.                 }
  15885.             }
  15886.             return false;
  15887.         }
  15888.         isAjaxStatusOK(status) {
  15889.             return status === 200 || status === 206 || status === 400 || status === 500;
  15890.         }
  15891.         isIgnoreError(txt) {
  15892.             try {
  15893.                 const obj = JSON.parse(txt);
  15894.                 if(obj.status === 'ok' && obj.data && (obj.data.removedThreads || obj.data.removedPosts)) {
  15895.                     return true;
  15896.                 }
  15897.             } catch(err) {}
  15898.             return false;
  15899.         }
  15900.         async sendHTML5Post(form, data, needProgress, hasFiles) {
  15901.             let ajaxParams;
  15902.             if(this._hasNewAPI) {
  15903.                 ajaxParams = { data, method: 'POST' };
  15904.             } else {
  15905.                 const getBase64 = async file => new Promise((resolve, reject) => {
  15906.                     const reader = new FileReader();
  15907.                     reader.readAsDataURL(file);
  15908.                     reader.onload = () => resolve(reader.result);
  15909.                     reader.onerror = err => reject(err);
  15910.                 });
  15911.                 const getCookies = () => {
  15912.                     const obj = {};
  15913.                     const cookies = doc.cookie.split(';');
  15914.                     for(let i = 0, len = cookies.length; i < len; ++i) {
  15915.                         const parts = cookies[i].split('=');
  15916.                         obj[parts.shift().trim()] = decodeURI(parts.join('='));
  15917.                     }
  15918.                     return obj;
  15919.                 };
  15920.                 const dataObj = { files: [] };
  15921.                 const files = [];
  15922.                 data.forEach(async (value, key) => {
  15923.                     if(key !== 'files') {
  15924.                         dataObj[key] = value;
  15925.                     } else {
  15926.                         files.push(value);
  15927.                     }
  15928.                 });
  15929.                 for(let i = 0, len = files.length; i < len; ++i) {
  15930.                     const file = files[i];
  15931.                     if(file.type) {
  15932.                         dataObj.files.push({
  15933.                             content: `data:${ file.type };base64,${
  15934.                                 await getBase64(file).then(data => data.split(',')[1]) }`,
  15935.                             name    : file.name,
  15936.                             spoiler : false
  15937.                         });
  15938.                     }
  15939.                 }
  15940.                 const cookieObj = getCookies();
  15941.                 ajaxParams = {
  15942.                     data: JSON.stringify({
  15943.                         captchaId  : cookieObj.captchaid,
  15944.                         bypassId   : cookieObj.bypass,
  15945.                         parameters : dataObj,
  15946.                         auth       : { login: cookieObj.login, hash: cookieObj.hash }
  15947.                     }),
  15948.                     headers : { 'Content-Type': 'application/x-www-form-urlencoded' },
  15949.                     method  : 'POST'
  15950.                 };
  15951.             }
  15952.             if(needProgress && hasFiles) {
  15953.                 ajaxParams.onprogress = getUploadFunc();
  15954.             }
  15955.             const task = form.action.split('/').pop();
  15956.             const url = this._hasNewAPI ? `/${ task }?json=1` : '/.api/' + task.replace('.js', '');
  15957.             return $ajax(url, ajaxParams).then(xhr => xhr.responseText).catch(err => Promise.reject(err));
  15958.         }
  15959.         updateCaptcha() {
  15960.             $script('reloadCaptcha();');
  15961.             return null;
  15962.         }
  15963.         updateSubmitBtn(el) {
  15964.             el.textContent = Lng.reply[lang];
  15965.         }
  15966.     }
  15967.     ibEngines.push(['form[action$="contentActions.js"]', Lynxchan]);
  15968.  
  15969.     class FoolFuuka extends BaseBoard {
  15970.         constructor(prot, dm) {
  15971.             super(prot, dm);
  15972.  
  15973.             this.cReply = 'post_wrapper';
  15974.             this.qDForm = '#main';
  15975.             this.qImgInfo = '.post_file_metadata, .thread_image_box > .post_file';
  15976.             this.qOmitted = '.omitted_text';
  15977.             this.qPages = '.paginate > ul > li:nth-last-child(3)';
  15978.             this.qPostHeader = 'header';
  15979.             this.qPostImg = '.post_image, .thread_image';
  15980.             this.qPostMsg = '.text';
  15981.             this.qPostRef = '.post_data > a[data-function="quote"]';
  15982.             this.qPostSubj = '.post_title';
  15983.             this.qPostsParent = '.posts';
  15984.             this.qRPost = '.post[id]';
  15985.             this._qOPostEnd = '.posts';
  15986.  
  15987.             this.docExt = '';
  15988.             this.firstPage = 1;
  15989.             this.res = 'thread/';
  15990.         }
  15991.         get qImgNameLink() {
  15992.             return '.post_file_filename';
  15993.         }
  15994.         get qThread() {
  15995.             return '.thread[id]';
  15996.         }
  15997.         get css() {
  15998.             return `.backlink_list { display: none !important; }
  15999.                 .de-oppost > .thread_image_box { float: left; margin: 0 20px 10px 15px; text-align: center;
  16000.                     color: #bfbfbf; font-size: .8em; line-height: 150%; }`;
  16001.         }
  16002.         get isArchived() {
  16003.             return true;
  16004.         }
  16005.         fixHTMLHelper(str) {
  16006.             return str.replace(/\/#(\d+)"/g, '#$1"').replace(/\/post\/(\d+)\/"/g, '/#$1"');
  16007.         }
  16008.         getImgWrap(img) {
  16009.             return img.parentNode.parentNode.parentNode;
  16010.         }
  16011.         getPageUrl(b, p) {
  16012.             return fixBrd(b) + (p > 1 ? `page/${ p }/` : '');
  16013.         }
  16014.         getTNum(thr) {
  16015.             return +thr.getAttribute('data-thread-num');
  16016.         }
  16017.         init() {
  16018.             defaultCfg.ajaxUpdThr = 0;
  16019.             const el = $q('.search_box');
  16020.             if(el) {
  16021.                 docBody.appendChild(el);
  16022.             }
  16023.             return false;
  16024.         }
  16025.         parseURL() {
  16026.             super.parseURL();
  16027.             this.page = +(this.b.match(/\/page\/(\d+)/) || [1, 1])[1];
  16028.             this.b = this.b.replace(/\/page\/\d+/, '');
  16029.         }
  16030.     }
  16031.     ibEngines.push(['meta[name="generator"][content^="FoolFuuka"]', FoolFuuka]);
  16032.  
  16033.     // DOMAINS
  16034.     class _02ch extends Kusaba {
  16035.         constructor(prot, dm) {
  16036.             super(prot, dm);
  16037.             this._02ch = true;
  16038.  
  16039.             this.hasCatalog = true;
  16040.             this._capUpdPromise = null;
  16041.         }
  16042.         updateCaptcha(cap) {
  16043.             return cap.updateHelper('/captcha_update.php', xhr => {
  16044.                 cap.parentEl.innerHTML = xhr.responseText;
  16045.                 cap.textEl = $id('recaptcha_response_field');
  16046.                 cap.initImage($q('img', cap.parentEl));
  16047.                 cap.initTextEl();
  16048.             });
  16049.         }
  16050.     }
  16051.     ibDomains['02ch.su'] = _02ch;
  16052.  
  16053.     class _2__ch extends BaseBoard {
  16054.         constructor(prot, dm) {
  16055.             super(prot, dm);
  16056.  
  16057.             this.qPages = 'table[border="1"] td > a:last-of-type';
  16058.             this.qPostImg = 'img.thumb';
  16059.             this._qOPostEnd = 'table:not(.postfiles)';
  16060.  
  16061.             this.docExt = '.html';
  16062.             this.hasPicWrap = true;
  16063.             this.jsonSubmit = true;
  16064.             this.markupBB = true;
  16065.             this.multiFile = true;
  16066.             this.ru = true;
  16067.         }
  16068.         get qThread() {
  16069.             return '.threadz';
  16070.         }
  16071.         get css() {
  16072.             return 'span[id$="_display"], #fastload { display: none; }';
  16073.         }
  16074.         get initCaptcha() {
  16075.             $id('captchadiv').innerHTML =
  16076.                 `<img src="${ this.getCaptchaSrc() }" style="vertical-align: bottom;" id="imgcaptcha">`;
  16077.             return null;
  16078.         }
  16079.         fixFileInputs(el) {
  16080.             const str = '><input type="file" name="file"></div>';
  16081.             el.innerHTML = '<div' + str + ('<div style="display: none;"' + str).repeat(3);
  16082.         }
  16083.         fixHTMLHelper(str) {
  16084.             return str.replace(/data-original="\//g, 'src="/');
  16085.         }
  16086.         getCaptchaSrc() {
  16087.             return `/${ this.b }/captcha.fpl?${ Math.random() }`;
  16088.         }
  16089.         getImgWrap(img) {
  16090.             return img.parentNode.parentNode.parentNode;
  16091.         }
  16092.         getOmitted(el, len) {
  16093.             let txt;
  16094.             return el && (txt = el.textContent) ? +txt.match(/\d+/) - len : 1;
  16095.         }
  16096.         getPageUrl(b, p) {
  16097.             return `${ fixBrd(b) }${ p > 0 ? p : 0 }.memhtml`;
  16098.         }
  16099.         getSubmitData(json) {
  16100.             let error = null;
  16101.             let postNum = null;
  16102.             if(json.post) {
  16103.                 postNum = +json.post;
  16104.             } else {
  16105.                 error = Lng.error[lang];
  16106.                 if(json.error) {
  16107.                     error += ': ' + json.error.text;
  16108.                 }
  16109.             }
  16110.             return { error, postNum };
  16111.         }
  16112.         init() {
  16113.             const btnEl = $q('#postform input[type="button"]');
  16114.             if(btnEl) {
  16115.                 $replace(btnEl, '<input type="submit" value="Отправить">');
  16116.             }
  16117.             const dFormEl = $q(this.qDForm);
  16118.             $delAll('input[type="hidden"]', dFormEl);
  16119.             dFormEl.appendChild($q('.userdelete'));
  16120.             return false;
  16121.         }
  16122.     }
  16123.     ibDomains['2--ch.ru'] = _2__ch;
  16124.     ibDomains['2-ch.su'] = _2__ch;
  16125.  
  16126.     class /* _2ch */ Makaba extends BaseBoard {
  16127.         constructor(prot, dm) {
  16128.             super(prot, dm);
  16129.             this.makaba = true;
  16130.  
  16131.             this.cReply = 'de-reply-class';
  16132.             this.qBan = '.post__pomyanem';
  16133.             this.qClosed = '.sticky-img[src$="locked.png"]';
  16134.             this.qDForm = '#posts-form';
  16135.             this.qFormFile = '.postform__raw.filer input[type="file"]';
  16136.             this.qFormRedir = null;
  16137.             this.qFormRules = '.rules';
  16138.             this.qFormSubm = '#submit';
  16139.             this.qFormTd = '.postform__raw';
  16140.             this.qFormTr = '.postform__raw';
  16141.             this.qFormTxta = '#shampoo';
  16142.             this.qImgInfo = '.post__file-attr';
  16143.             this.qOmitted = '.thread__missed';
  16144.             this.qOPost = '.post_type_oppost';
  16145.             this.qPostHeader = '.post__details';
  16146.             this.qPostImg = '.post__file-preview';
  16147.             this.qPostMsg = '.post__message';
  16148.             this.qPostName = '.post__anon, .post__email';
  16149.             this.qPostRef = '.post__reflink:nth-child(2)';
  16150.             this.qPostSubj = '.post__title';
  16151.             this.qRPost = '.post_type_reply[data-num]';
  16152.             this.qTrunc = null;
  16153.  
  16154.             this.formParent = 'thread';
  16155.             this.hasAltCaptcha = true;
  16156.             this.hasCatalog = true;
  16157.             this.hasOPNum = true;
  16158.             this.hasPicWrap = true;
  16159.             this.JsonBuilder = MakabaPostsBuilder;
  16160.             this.jsonSubmit = true;
  16161.             this.markupBB = true;
  16162.             this.multiFile = true;
  16163.             this.timePattern = 'dd+nn+yy+w+hh+ii+ss';
  16164.             this._capUpdPromise = null;
  16165.         }
  16166.         get qFormMail() {
  16167.             return 'input[name="email"]';
  16168.         }
  16169.         get qFormName() {
  16170.             return 'input[name="name"]';
  16171.         }
  16172.         get qFormSubj() {
  16173.             return 'input[name="subject"]';
  16174.         }
  16175.         get qImgNameLink() {
  16176.             return '.file-attr > .desktop, .post__file-attr > .desktop';
  16177.         }
  16178.         get css() {
  16179.             return `#alert-undefined, .cntnt__header > hr, .cntnt__right > hr, #CommentToolbar,
  16180.                     .de-btn-src + a + a, #de-win-reply #tags, #down-nav-arrow, .media-expand-button,
  16181.                     .media-thumbnail, .newpost, .post__btn:not(.icon_type_active), .post__message .icon,
  16182.                     .post__number, .post__panel, .post__refmap, .postform__len, .postform-hr,
  16183.                     .thread-nav > :not(.search), #up-nav-arrow { display: none !important; }
  16184.                 .captcha { overflow: hidden; max-width: 300px; }
  16185.                 .captcha > img { display: block; width: 364px; margin: -45px 0 -22px 0; }
  16186.                 .de-btn-src + a { display: inline-flex; }
  16187.                 .de-pview > .post__details { margin-left: 4px; }
  16188.                 .de-reply-class { background: var(--theme_default_postbg);
  16189.                     border: 1px solid var(--theme_default_border); border-radius: 3px; }
  16190.                 .oekaki-height, .oekaki-width { width: 36px !important; }
  16191.                 .post_type_reply { max-width: 100%; }
  16192.                 .postform { width: auto; }
  16193.                 .postform__sticker-btn, .postform__sticker-prev { bottom: ` +
  16194.                     `${ !Cfg.txtBtnsLoc || !Cfg.addTextBtns ? 3 :
  16195.                     Cfg.addTextBtns === 1 ? 28 : Cfg.addTextBtns === 2 ? 19 : 25 }px !important; }
  16196.                 ${ Cfg.addSageBtn ? `.options__box[onclick="ToggleSage()"]
  16197.                     { display: none !important; }` : '' }
  16198.                 ${ Cfg.expandTrunc ? `.expand-large-comment,
  16199.                     div[id^="shrinked-post"] { display: none !important; }
  16200.                     div[id^="original-post"] { display: block !important; }` : '' }
  16201.                 ${ Cfg.imgNames === 2 ? `.post__filezise { display: inline !important; }
  16202.                     .post__file-attr { margin-bottom: 1px; }` : '' }
  16203.                 ${ Cfg.noSpoilers ? '.spoiler::after { width: 0; }' : '' }`;
  16204.         }
  16205.         get isArchived() {
  16206.             return this.b.includes('/arch');
  16207.         }
  16208.         get lastPage() {
  16209.             const els = $Q('.pager > a:not([class])');
  16210.             const value = els ? els.length : 1;
  16211.             Object.defineProperty(this, 'lastPage', { value });
  16212.             return value;
  16213.         }
  16214.         get markupTags() {
  16215.             return ['B', 'I', 'U', 'S', 'SPOILER', '', 'SUP', 'SUB'];
  16216.         }
  16217.         get reportForm() {
  16218.             const value = (pNum, tNum) => ($q('input[type="button"]', $popup(
  16219.                 'edit-report',
  16220.                 `<input name="comment" value="" placeholder="${
  16221.                     pNum === tNum ? Lng.reportThr[lang] : Lng.reportPost[lang]
  16222.                 }" type="text"> <input value="OK" type="button">`)
  16223.             ).onclick = e => {
  16224.                 const inpEl = e.target.previousElementSibling;
  16225.                 if(!inpEl.value) {
  16226.                     inpEl.classList.add('de-input-error');
  16227.                     return;
  16228.                 }
  16229.                 const formData = new FormData();
  16230.                 formData.append('task', 'report');
  16231.                 formData.append('board', this.b);
  16232.                 formData.append('thread', tNum);
  16233.                 formData.append('posts', pNum);
  16234.                 formData.append('comment', inpEl.value);
  16235.                 closePopup('edit-report');
  16236.                 $popup('report', Lng.sending[lang], true);
  16237.                 $ajax('/makaba/makaba.fcgi?json=1', { method: 'POST', data: formData }).then(xhr => {
  16238.                     let obj;
  16239.                     try {
  16240.                         obj = JSON.parse(xhr.responseText);
  16241.                     } catch(e) {}
  16242.                     $popup('report', !obj ? Lng.error[lang] + ': ' + xhr.responseText :
  16243.                         (obj.message || Lng.succReported[lang]) + ': ' + obj.message_title);
  16244.                 });
  16245.             });
  16246.             Object.defineProperty(this, 'reportForm', { value });
  16247.             return value;
  16248.         }
  16249.         deleteTruncMsg(post, el) {
  16250.             el.previousSibling.remove();
  16251.             $show(el.previousSibling);
  16252.             el.remove();
  16253.         }
  16254.         fixFileInputs(el) {
  16255.             el.innerHTML = Array.from({ length: 8 }, (val, i) =>
  16256.                 `<div${ i ? ' style="display: none;"' : '' }><input type="file" name="formimages[]"></div>`
  16257.             ).join('');
  16258.         }
  16259.         getBanId(postEl) {
  16260.             const el = $q(this.qBan, postEl);
  16261.             return !el ? 0 : el.textContent.includes('предупрежден') ? 2 : 1;
  16262.         }
  16263.         getImgWrap(img) {
  16264.             return $parent(img, 'FIGURE');
  16265.         }
  16266.         getJsonApiUrl(brd, tNum) {
  16267.             return `/${ brd }/res/${ tNum }.json`;
  16268.         }
  16269.         getPNum(post) {
  16270.             return +post.getAttribute('data-num');
  16271.         }
  16272.         getPostWrap(el) {
  16273.             return el.parentNode;
  16274.         }
  16275.         getSage(post) {
  16276.             this.getSage = !$q('span[id^="id_tag_"]') ? super.getSage : post => {
  16277.                 return !$q('span[id^="id_tag_"], .post__ophui', post);
  16278.             };
  16279.             return this.getSage(post);
  16280.         }
  16281.         getSubmitData(json) {
  16282.             let error = null;
  16283.             let postNum = null;
  16284.             if(json.Status === 'OK') {
  16285.                 postNum = +json.Num;
  16286.             } else if(json.Status === 'Redirect') {
  16287.                 postNum = +json.Target;
  16288.             } else {
  16289.                 error = Lng.error[lang] + ': ' + json.Reason;
  16290.             }
  16291.             return { error, postNum };
  16292.         }
  16293.         init() {
  16294.             if($q('section.posts')) { // Old Makaba engine
  16295.                 this.cReply = 'post reply';
  16296.                 this.qBan = '.pomyanem';
  16297.                 this.qFormFile = 'tr input[type="file"]';
  16298.                 this.qFormRules = '.rules-area';
  16299.                 this.qFormTd = 'td';
  16300.                 this.qFormTr = 'tr';
  16301.                 this.qImgInfo = '.file-attr';
  16302.                 this.qOmitted = '.mess-post';
  16303.                 this.qOPost = '.oppost';
  16304.                 this.qPostHeader = '.post-details';
  16305.                 this.qPostImg = '.preview';
  16306.                 this.qPostMsg = '.post-message';
  16307.                 this.qPostName = '.ananimas, .post-email';
  16308.                 this.qPostRef = '.reflink';
  16309.                 this.qPostSubj = '.post-title';
  16310.                 this.qRPost = '.post.reply[data-num]';
  16311.                 const { css } = this;
  16312.                 Object.defineProperty(this, 'css', {
  16313.                     configurable : true,
  16314.                     get          : () => `${ css }
  16315.                         #ABU-alert-wait, .ABU-refmap, .fa-media-icon, .kupi-passcode-suka, .logo + hr,
  16316.                         .media-expand-button, #media-thumbnail, .message-byte-len, .nav-arrows, .norm-reply,
  16317.                         .postform-hr, .postpanel > :not(img), .posts > hr, .reflink::before,
  16318.                         .thread-nav > :not(.search), .toolbar-area { display: none !important; }
  16319.                         ${ Cfg.addSageBtn ? `.box[onclick="ToggleSage()"] {
  16320.                             display: none !important; }` : '' }
  16321.                         ${ Cfg.imgNames === 2 ? `.filesize { display: inline !important; }
  16322.                             .file-attr { margin-bottom: 1px; }` : '' }`
  16323.                 });
  16324.             }
  16325.             $script(`(function() {
  16326.                 function fixGlobalFunc(name) {
  16327.                     Object.defineProperty(window, name,
  16328.                         { value: Function.prototype, writable: false, configurable: false });
  16329.                 }
  16330.                 fixGlobalFunc("$alert");
  16331.                 fixGlobalFunc("autorefresh_start"); // Old makaba only
  16332.                 fixGlobalFunc("linkremover");
  16333.                 fixGlobalFunc("Media");
  16334.                 fixGlobalFunc("MExpandMedia");
  16335.                 window.FormData = void 0;
  16336.                 $(function() { $(window).off(); });
  16337.             })();`);
  16338.             $each($Q('.autorefresh'), el => {
  16339.                 const inpEl = $q('input', el);
  16340.                 if(inpEl.checked) {
  16341.                     inpEl.click();
  16342.                 }
  16343.                 el.remove();
  16344.             });
  16345.             const el = $id('shampoo');
  16346.             if(el) {
  16347.                 el.tabIndex = 1;
  16348.             }
  16349.             return false;
  16350.         }
  16351.         initCaptcha(cap) {
  16352.             const box = $q('.captcha-box, .captcha');
  16353.             if(!Cfg.altCaptcha) {
  16354.                 box.innerHTML = `<div id="captcha-widget-main"></div>
  16355.                     <input name="captcha_type" value="invisible_recaptcha" type="hidden">`;
  16356.                 return null;
  16357.             }
  16358.             const img = box.firstChild;
  16359.             if(!img || img.tagName !== 'IMG') {
  16360.                 box.innerHTML = `<img>
  16361.                     <input name="2chaptcha_value" maxlength="6" type="text">
  16362.                     <input name="captcha_type" value="2chaptcha" type="hidden">
  16363.                     <input name="2chaptcha_id" type="hidden">`;
  16364.                 const [img, inp] = [...box.children];
  16365.                 img.onclick = () => this.updateCaptcha(cap);
  16366.                 inp.tabIndex = 999;
  16367.                 cap.textEl = inp;
  16368.             }
  16369.             return null;
  16370.         }
  16371.         observeContent(checkDomains, dataPromise) {
  16372.             if($q('#posts-form > .thread, form[de-form] > .thread')) {
  16373.                 return true;
  16374.             }
  16375.             const initObserver = new MutationObserver(mutations => {
  16376.                 const el = mutations[0].addedNodes[0];
  16377.                 if(el && el.className === 'thread') {
  16378.                     initObserver.disconnect();
  16379.                     runMain(checkDomains, dataPromise);
  16380.                 }
  16381.             });
  16382.             const el = $q('#posts-form, form[de-form]');
  16383.             if(el) {
  16384.                 initObserver.observe(el, { childList: true });
  16385.             }
  16386.             return false;
  16387.         }
  16388.         updateCaptcha(cap) {
  16389.             const url = Cfg.altCaptcha ? `/api/captcha/2chaptcha/id?board=${ this.b }&thread=` + pr.tNum :
  16390.                 '/api/captcha/invisible_recaptcha/id';
  16391.             return cap.updateHelper(url, xhr => {
  16392.                 const box = $q('.captcha-box, .captcha');
  16393.                 let data = xhr.responseText;
  16394.                 try {
  16395.                     data = JSON.parse(data);
  16396.                 } catch(err) {}
  16397.                 if(cap.isSubmitWait && data.result !== 1) {
  16398.                     pr.subm.click();
  16399.                 }
  16400.                 switch(data.result) {
  16401.                 case 0: box.textContent = 'Пасскод недействителен. Перелогиньтесь.'; break;
  16402.                 case 2: box.textContent = 'Вы - пасскодобоярин.'; break;
  16403.                 case 3: return CancelablePromise.reject(new CancelError()); // Captcha is disabled
  16404.                 case 1: // Captcha is enabled
  16405.                     if(data.type === 'invisible_recaptcha') {
  16406.                         if(!cap.isSubmitWait) {
  16407.                             break;
  16408.                         }
  16409.                         $q('.captcha__key').value = data.id;
  16410.                         $script($id('captcha-widget').hasChildNodes() ?
  16411.                             `grecaptcha.reset(deCapWidget);
  16412.                             grecaptcha.execute(deCapWidget);` :
  16413.                             `deCapWidget = grecaptcha.render('captcha-widget', {
  16414.                                 sitekey : '${ data.id }',
  16415.                                 theme   : 'light',
  16416.                                 size    : 'invisible',
  16417.                                 callback: function() {
  16418.                                     var el = document.getElementById('captcha-widget-main');
  16419.                                     el.innerHTML = '<input type="hidden" name="g-recaptcha-response">';
  16420.                                     el.firstChild.value = grecaptcha.getResponse();
  16421.                                     document.getElementById('submit').click();
  16422.                                 }
  16423.                             });
  16424.                             grecaptcha.execute(deCapWidget);`);
  16425.                         break;
  16426.                     } else if(data.type === '2chaptcha') {
  16427.                         const img = box.firstChild;
  16428.                         img.src = '';
  16429.                         img.src = `/api/captcha/2chaptcha/image/${ data.id }`;
  16430.                         box.lastChild.value = data.id;
  16431.                         break;
  16432.                     }
  16433.                     /* falls through */
  16434.                 default: box.innerHTML = data;
  16435.                 }
  16436.             });
  16437.         }
  16438.     }
  16439.     ibDomains['2ch.hk'] = Makaba;
  16440.     ibDomains['2ch.pm'] = Makaba;
  16441.     ibDomains['2ch.re'] = Makaba;
  16442.     ibDomains['2ch.wf'] = Makaba;
  16443.  
  16444.     class _2chan extends BaseBoard {
  16445.         constructor(prot, dm) {
  16446.             super(prot, dm);
  16447.  
  16448.             this.qDForm = 'form:not([enctype])';
  16449.             this.qForm = '#fm';
  16450.             this.qFormRedir = null;
  16451.             this.qFormRules = '.chui';
  16452.             this.qOmitted = 'font[color="#707070"]';
  16453.             this.qPostImg = 'a[href$=".jpg"] > img, a[href$=".png"] > img, a[href$=".gif"] > img';
  16454.             this.qPostRef = '.del';
  16455.             this.qRPost = 'td:nth-child(2)';
  16456.  
  16457.             this.docExt = '.htm';
  16458.             this.formParent = 'resto';
  16459.         }
  16460.         get qImgNameLink() {
  16461.             return 'a[href$=".jpg"], a[href$=".png"], a[href$=".gif"]';
  16462.         }
  16463.         get qThread() {
  16464.             return '.thre';
  16465.         }
  16466.         get css() {
  16467.             return `.ftbl { width: auto; margin: 0; }
  16468.                 .reply { background: #f0e0d6; }
  16469.                 span { font-size: inherit; }`;
  16470.         }
  16471.         getPageUrl(b, p) {
  16472.             return fixBrd(b) + (p > 0 ? p + this.docExt : 'futaba.htm');
  16473.         }
  16474.         getPNum(post) {
  16475.             return +$q('input', post).name;
  16476.         }
  16477.         getPostElOfEl(el) {
  16478.             while(el && el.tagName !== 'TD' && !el.hasAttribute('de-thread')) {
  16479.                 el = el.parentElement;
  16480.             }
  16481.             return el;
  16482.         }
  16483.         getTNum(thr) {
  16484.             return +$q('input[type="checkbox"]', thr).name.match(/\d+/);
  16485.         }
  16486.         init() {
  16487.             $del($q('base', doc.head)); // <base> is not compartible with SVG
  16488.             return false;
  16489.         }
  16490.     }
  16491.     ibDomains['2chan.net'] = _2chan;
  16492.  
  16493.     class _2channel extends Makaba {
  16494.         constructor(prot, dm) {
  16495.             super(prot, dm);
  16496.             this._2channel = true;
  16497.  
  16498.             this.hasAltCaptcha = false;
  16499.         }
  16500.         get reportForm() {
  16501.             return null;
  16502.         }
  16503.         fixFileInputs(el) {
  16504.             el.innerHTML = Array.from({ length: 4 }, (val, i) =>
  16505.                 `<div${ i ? ' style="display: none;"' : '' }><input type="file" name="formimages[]"></div>`
  16506.             ).join('');
  16507.         }
  16508.         fixHTMLHelper(str) {
  16509.             return str.replace(/src="[^>]+" data-src="/g, 'src="');
  16510.         }
  16511.         getCapParent(el) {
  16512.             return $q('.captcha');
  16513.         }
  16514.         init() {
  16515.             super.init();
  16516.             this.qFormFile = 'input[name="formimages[]"]';
  16517.             this.qFormTd = 'div[class^="freply__"]';
  16518.             this.qFormTr = 'div[class^="freply__"]';
  16519.             const { css } = this;
  16520.             Object.defineProperty(this, 'css', {
  16521.                 configurable : true,
  16522.                 get          : () => `${ css }
  16523.                     #AlertBox, .postform__checkbox.first, .postform__header, .refmap, #youtube-thumb-float
  16524.                         { display: none !important; }
  16525.                     .de-win-open:not(#de-win-cfg) > .de-win-body { background-color: #eee !important; }
  16526.                     .preview.lazy { opacity: 1; }`
  16527.             });
  16528.             let el = $q('.captcha');
  16529.             if(el) {
  16530.                 $before($q('.freply__files-and-captcha'), el);
  16531.             }
  16532.             el = $id('postform');
  16533.             if(el) {
  16534.                 el.setAttribute('action', el.getAttribute('action') + '?json=1');
  16535.             }
  16536.             return false;
  16537.         }
  16538.         initCaptcha(cap) {
  16539.             return this.updateCaptcha(cap);
  16540.         }
  16541.         updateCaptcha(cap) {
  16542.             const url = `/api/captcha/service_id?board=${ this.b }&thread=` + pr.tNum;
  16543.             return cap.updateHelper(url, xhr => {
  16544.                 const box = $q('.captcha');
  16545.                 let data = xhr.responseText;
  16546.                 try {
  16547.                     data = JSON.parse(data);
  16548.                 } catch(err) {}
  16549.                 switch(data.result) {
  16550.                 case 1: { // Captcha is enabled
  16551.                     const el = $q('.captcha__image');
  16552.                     const img = $q('img', el) || $aBegin(el, '<img>');
  16553.                     img.src = '';
  16554.                     img.src = `/api/captcha/image/${ data.id }`;
  16555.                     $q('input[name="captcha_id"]').value = data.id;
  16556.                     break;
  16557.                 }
  16558.                 case 2: return CancelablePromise.reject(new CancelError()); // Captcha is disabled
  16559.                 case 3: box.innerHTML = 'Вам больше не нужно вводить капчу.'; break;
  16560.                 default: box.innerHTML = data;
  16561.                 }
  16562.                 $show(box);
  16563.                 box.removeAttribute('hidden');
  16564.                 cap.textEl.tabIndex = 999;
  16565.             });
  16566.         }
  16567.     }
  16568.     ibDomains['2channel.moe'] = _2channel;
  16569.     ibDomains['2channel5xx5xchx.onion'] = _2channel;
  16570.  
  16571.     class _2chRip extends BaseBoard {
  16572.         constructor(prot, dm) {
  16573.             super(prot, dm);
  16574.  
  16575.             this.jsonSubmit = true;
  16576.             this.ru = true;
  16577.  
  16578.             this._capUpdPromise = null;
  16579.         }
  16580.         get css() {
  16581.             return `small[id^="rfmap_"], #submit_button, .qreply_btn { display: none; }
  16582.                 #subject + div { display: inline !important; }
  16583.                 .replypage .reply .reflink::before { content: "" }`;
  16584.         }
  16585.         getSubmitData(json) {
  16586.             return {
  16587.                 error   : json.message ? json.message_title + ': ' + json.message : null,
  16588.                 postNum : json.num ? +json.num : null
  16589.             };
  16590.         }
  16591.         init() {
  16592.             $script('postFormSubmit = Function.prototype;');
  16593.             $bEnd($id('postform'), '<input type="hidden" name="json" value="1">');
  16594.             return false;
  16595.         }
  16596.         updateCaptcha(cap) {
  16597.             return cap.updateHelper('/cgi/captcha?task=get_id', ({ responseText: id }) => {
  16598.                 $id('imgcaptcha').src = '/cgi/captcha?task=get_image&id=' + id;
  16599.                 $id('captchaid').value = id;
  16600.             });
  16601.         }
  16602.     }
  16603.     ibDomains['2ch.rip'] = _2chRip;
  16604.     ibDomains['dva-ch.net'] = _2chRip;
  16605.  
  16606.     class _410chan extends Kusaba {
  16607.         constructor(prot, dm) {
  16608.             super(prot, dm);
  16609.  
  16610.             this.qFormRedir = 'input#noko';
  16611.             this.qPages = '.pgstbl > table > tbody > tr > td:nth-child(2)';
  16612.  
  16613.             this.ru = true;
  16614.             this.hasCatalog = true;
  16615.             this.markupBB = false;
  16616.             this.timePattern = 'dd+nn+yyyy++w++hh+ii+ss';
  16617.             this._capUpdPromise = null;
  16618.         }
  16619.         get capLang() {
  16620.             return 0;
  16621.         }
  16622.         get css() {
  16623.             return `${ super.css }
  16624.                 #resizer { display: none; }
  16625.                 form > span { margin-top: 5px; }
  16626.                 .de-thr-hid { display: inherit; }
  16627.                 .reflink::after { content: none !important; }
  16628.                 .spoiler-image:hover::after { content: none !important; }
  16629.                 .topmenu { z-index: 1; }`;
  16630.         }
  16631.         get markupTags() {
  16632.             return ['**', '*', '__', '^^', '%%', '`'];
  16633.         }
  16634.         getCaptchaSrc(src) {
  16635.             return src.replace(/\?[^?]+$|$/, `?board=${ aib.b }&${ Math.random() }`);
  16636.         }
  16637.         getSage(post) {
  16638.             const el = $q('.filetitle', post);
  16639.             return !!el && el.textContent.includes('\u21E9');
  16640.         }
  16641.         updateCaptcha(cap) {
  16642.             return cap.updateHelper(`/api_adaptive.php?board=${ this.b }`, xhr => {
  16643.                 if(xhr.responseText === '1') {
  16644.                     cap.textEl.disabled = true;
  16645.                     setTimeout(() => (cap.textEl.value = 'проезд оплачен'), 0);
  16646.                     return;
  16647.                 }
  16648.                 cap.textEl.disabled = false;
  16649.                 cap.textEl.value = '';
  16650.                 const img = $q('img', cap.parentEl);
  16651.                 const src = img.getAttribute('src');
  16652.                 img.src = '';
  16653.                 img.src = this.getCaptchaSrc(src);
  16654.             });
  16655.         }
  16656.     }
  16657.     ibDomains['410chan.org'] = _410chan;
  16658.  
  16659.     class _4chan extends BaseBoard {
  16660.         constructor(prot, dm) {
  16661.             super(prot, dm);
  16662.             this._4chan = true;
  16663.  
  16664.             this.cReply = 'post reply';
  16665.             this.qBan = 'strong[style="color: red;"]';
  16666.             this.qClosed = '.archivedIcon';
  16667.             this.qDelBut = '.deleteform > input[type="submit"]';
  16668.             this.qError = '#errmsg';
  16669.             this.qForm = 'form[name="post"]';
  16670.             this.qFormRedir = null;
  16671.             this.qImgInfo = '.fileText';
  16672.             this.qOmitted = '.summary.desktop';
  16673.             this.qOPost = '.op';
  16674.             this.qPages = '.pagelist > .pages:not(.cataloglink) > a:last-of-type';
  16675.             this.qPostHeader = '.postInfo';
  16676.             this.qPostImg = '.fileThumb > img:not(.fileDeletedRes)';
  16677.             this.qPostName = '.name';
  16678.             this.qPostRef = '.postInfo > .postNum';
  16679.             this.qPostSubj = '.subject';
  16680.             this._qOPostEnd = '.replyContainer';
  16681.  
  16682.             this.anchor = '#p';
  16683.             this.docExt = '';
  16684.             this.firstPage = 1;
  16685.             this.formParent = 'resto';
  16686.             this.hasAltCaptcha = true;
  16687.             this.hasCatalog = true;
  16688.             this.hasTextLinks = true;
  16689.             this.JsonBuilder = _4chanPostsBuilder;
  16690.             this.res = 'thread/';
  16691.             this.timePattern = 'nn+dd+yy+w+hh+ii-?s?s?';
  16692.         }
  16693.         get qFormSubj() {
  16694.             return 'input[name="sub"]';
  16695.         }
  16696.         get qImgNameLink() {
  16697.             return '.fileText > a';
  16698.         }
  16699.         get css() {
  16700.             return `.backlink, #blotter, .de-file-utils + .desktop, .extButton, hr.desktop, .navLinks,
  16701.                     .postMenuBtn, #togglePostFormLink { display: none !important; }
  16702.                 #bottomReportBtn { display: initial !important; }
  16703.                 #g-recaptcha { height: initial; }
  16704.                 .postForm { display: table !important; width: auto !important; }
  16705.                 textarea { margin-right: 0 !important; }
  16706.                 ${ Cfg.widePosts ? '.sideArrows { display: none; }' : '' }`;
  16707.         }
  16708.         get markupTags() {
  16709.             return ['', '', '', '', $q('input[type="checkbox"][name="spoiler"]') ? '[spoiler' : '',
  16710.                 this.b === 'g' ? '[code' : ''];
  16711.         }
  16712.         get updateCaptcha() {
  16713.             let value = null;
  16714.             const tr = $id('captchaFormPart');
  16715.             if(tr) {
  16716.                 value = () => {
  16717.                     if(Cfg.altCaptcha) {
  16718.                         $id('g-recaptcha').innerHTML = $q('noscript', tr).innerHTML;
  16719.                     } else {
  16720.                         $replace($id('g-recaptcha'), '<div id="g-recaptcha"></div>');
  16721.                         $script('initRecaptcha();');
  16722.                     }
  16723.                     tr.removeAttribute('onclick');
  16724.                     return null;
  16725.                 };
  16726.             }
  16727.             Object.defineProperty(this, 'updateCaptcha', { value });
  16728.             return value;
  16729.         }
  16730.         fixDeadLinks(str) {
  16731.             return str.replace(/<span class="deadlink">&gt;&gt;(\d+)<\/span>/g,
  16732.                 '<a class="de-ref-del deadlink" href="#p$1">&gt;&gt;$1</a>');
  16733.         }
  16734.         fixHTMLHelper(str) {
  16735.             return str.replace(/<span>([^<]+)(?:<\/?wbr>)?([^<]+)<\/span> \[<a [^>]+>Embed<\/a>\]/g, '$1$2')
  16736.                 .replace(/<\/?wbr>/g, '').replace(/( \(OP\)| →)<\/a/g, '</a');
  16737.         }
  16738.         fixVideo(isPost, data) {
  16739.             return [];
  16740.         }
  16741.         getImgInfo(wrap) {
  16742.             const el = $q(this.qImgInfo, wrap);
  16743.             return el ? el.lastChild.textContent : '';
  16744.         }
  16745.         getImgRealName(wrap) {
  16746.             const el = $q(this.qImgNameLink, wrap);
  16747.             return el ? el.title || el.parentNode.title || el.textContent : '';
  16748.         }
  16749.         getJsonApiUrl(brd, tNum) {
  16750.             return `//a.4cdn.org/${ brd }/thread/${ tNum }.json`;
  16751.         }
  16752.         getImgWrap(img) {
  16753.             return img.parentNode.parentNode;
  16754.         }
  16755.         getPageUrl(b, p) {
  16756.             return fixBrd(b) + (p > 1 ? p : '');
  16757.         }
  16758.         getPostWrap(el) {
  16759.             return el.parentNode;
  16760.         }
  16761.         getSage(post) {
  16762.             return !!$q('.id_Heaven, .useremail[href^="mailto:sage"]', post);
  16763.         }
  16764.         getSubmitData(dc) {
  16765.             let error = null;
  16766.             let postNum = null;
  16767.             const errEl = $q('#errmsg', dc);
  16768.             if(errEl) {
  16769.                 error = errEl.innerHTML;
  16770.             } else {
  16771.                 try {
  16772.                     postNum = +$q('h1', dc).nextSibling.textContent.match(/no:(\d+)/)[1];
  16773.                 } catch(err) {}
  16774.             }
  16775.             return { error, postNum };
  16776.         }
  16777.         getTNum(thr) {
  16778.             return +$q('input[type="checkbox"]', thr).name.match(/\d+/);
  16779.         }
  16780.         init() {
  16781.             Cfg.findImgFile = 0;
  16782.             Cfg.txtBtnsLoc = 0;
  16783.             const el = $id('styleSelector');
  16784.             if(el) {
  16785.                 el.setAttribute('onchange', 'setActiveStyleSheet(this.value);');
  16786.             }
  16787.             return false;
  16788.         }
  16789.     }
  16790.     ibDomains['4chan.org'] = _4chan;
  16791.     ibDomains['4channel.org'] = _4chan;
  16792.  
  16793.     class _55chan extends Vichan {
  16794.         constructor(prot, dm) {
  16795.             super(prot, dm);
  16796.  
  16797.             this.qFormRules = '.regras';
  16798.  
  16799.             this._capUpdPromise = null;
  16800.         }
  16801.         get qImgNameLink() {
  16802.             return '.fileinfo > a:last-of-type';
  16803.         }
  16804.         get qThread() {
  16805.             return 'div[data-board]';
  16806.         }
  16807.         get css() {
  16808.             return `${ super.css }
  16809.                 input.delete, #post-moderation-fields { display: initial !important; }`;
  16810.         }
  16811.         initCaptcha(cap) {
  16812.             $q('td', cap.parentEl).innerHTML = `<input placeholder="${ Lng.cap[lang] }" class="captcha_text` +
  16813.                 `" type="text" name="captcha_text" size="25" maxlength="8" autocomplete="off">
  16814.                 <input class="captcha_cookie de-input-hidden" name="captcha_cookie" type="hidden">
  16815.                 <div class="captcha_html"></div>`;
  16816.             cap.textEl = $q('.captcha_text', cap.parentEl);
  16817.             return this.updateCaptcha(cap, true);
  16818.         }
  16819.         updateCaptcha(cap) {
  16820.             return cap.updateHelper(
  16821.                 '/8chan-captcha/entrypoint.php?mode=get&extra=abcdefghijklmnopqrstuvwxyz',
  16822.                 xhr => {
  16823.                     const obj = JSON.parse(xhr.responseText);
  16824.                     $q('.captcha_cookie', cap.parentEl).value = obj.cookie;
  16825.                     $q('.captcha_html', cap.parentEl).innerHTML = obj.captchahtml;
  16826.                     const img = $q('img', cap.parentEl);
  16827.                     if(img) {
  16828.                         cap.initImage(img);
  16829.                     }
  16830.                 });
  16831.         }
  16832.     }
  16833.     ibDomains['55chan.org'] = _55chan;
  16834.  
  16835.     class Archived extends FoolFuuka {
  16836.         getImgRedirectSrc(url) {
  16837.             return $ajax(url).then(xhr => xhr.responseText.match(/<meta[^>]+url=([^"]+)">/)[1]);
  16838.         }
  16839.     }
  16840.     ibDomains['archived.moe'] = Archived;
  16841.  
  16842.     class Arhivach extends BaseBoard {
  16843.         constructor(prot, dm) {
  16844.             super(prot, dm);
  16845.  
  16846.             this.cReply = 'post';
  16847.             this.qDelBut = null;
  16848.             this.qDelPassw = null;
  16849.             this.qDForm = 'body > .container-fluid';
  16850.             this.qPostHeader = '.post_head';
  16851.             this.qPostImg = '.post_image > img';
  16852.             this.qPostMsg = '.post_comment_body';
  16853.             this.qPostRef = '.post_id, .post_head > b';
  16854.             this.qPostSubj = '.post_subject';
  16855.             this.qRPost = '.post[postid]:not([postid=""])';
  16856.  
  16857.             this.docExt = '';
  16858.             this.hasOPNum = true;
  16859.             this.res = 'thread/';
  16860.         }
  16861.         get qImgNameLink() {
  16862.             return '.img_filename';
  16863.         }
  16864.         get qThread() {
  16865.             return '.thread_inner';
  16866.         }
  16867.         get css() {
  16868.             return `.media-expand-button, .post_replies, .post_num, .poster_sage { display: none !important; }
  16869.                 .navbar-fixed-top, .thread_header_fixed { z-index: 5 !important; }
  16870.                 .post { overflow-x: auto !important; }
  16871.                 .thread_inner img.de-fullimg { max-width: 100% !important; max-height: 100% !important; }`;
  16872.         }
  16873.         get isArchived() {
  16874.             return true;
  16875.         }
  16876.         fixHTML(data, isForm) {
  16877.             const formEl = super.fixHTML(data, isForm);
  16878.             const links = $Q('.expand_image', formEl);
  16879.             for(let i = 0, len = links.length; i < len; ++i) {
  16880.                 const link = links[i];
  16881.                 link.href = link.getAttribute('onclick').match(/https?:\/[^']+/)[0];
  16882.                 link.removeAttribute('onclick');
  16883.             }
  16884.             return formEl;
  16885.         }
  16886.         getImgInfo(wrap) {
  16887.             return wrap.title;
  16888.         }
  16889.         getImgWrap(img) {
  16890.             return $parent(img, 'A').parentNode;
  16891.         }
  16892.         getOp(el) {
  16893.             return $q('.post:first-child', el);
  16894.         }
  16895.         getPNum(post) {
  16896.             return +post.getAttribute('postid');
  16897.         }
  16898.         getSage(post) {
  16899.             return !!$q('.poster_sage', post);
  16900.         }
  16901.         getThrUrl() {
  16902.             return $q('link[rel="canonical"]', doc.head).href;
  16903.         }
  16904.         getTNum(thr) {
  16905.             return this.getPNum(this.getOp(thr));
  16906.         }
  16907.         init() {
  16908.             defaultCfg.ajaxUpdThr = 0;
  16909.             setTimeout(() => {
  16910.                 const delPosts = $Q('.post_deleted');
  16911.                 for(let i = 0, len = delPosts.length; i < len; ++i) {
  16912.                     const post = pByNum.get(this.getPNum(delPosts[i]));
  16913.                     if(post) {
  16914.                         post.thr.deletePosts(post, false, false);
  16915.                     }
  16916.                 }
  16917.                 $css(`.post { background-color: ${
  16918.                     getComputedStyle($q('.post')).getPropertyValue('background-color') } !important; }`);
  16919.             }, 500);
  16920.             return false;
  16921.         }
  16922.     }
  16923.     ibDomains['arhivach.ng'] = Arhivach;
  16924.     ibDomains['arhivachovtj2jrp.onion'] = Arhivach;
  16925.  
  16926.     class Animach extends Vichan {
  16927.         constructor(prot, dm) {
  16928.             super(prot, dm);
  16929.  
  16930.             this.hasRefererErr = false;
  16931.             this.markupBB = true;
  16932.         }
  16933.         get css() {
  16934.             return `${ super.css }\r\n\t${
  16935.                 Cfg.noSpoilers ? `span.spoiler, span.spoiler:hover { ${
  16936.                     Cfg.noSpoilers === 1 ? 'color: #F5F5F5 !important; background-color: #888 !important' :
  16937.                     'color: inherit !important' }; transition: none !important; }` : '' }
  16938.                 #thread-interactions { display: none; }
  16939.                 .reflink::after { content: "" !important; }`;
  16940.         }
  16941.         get markupTags() {
  16942.             return ['b', 'i', 'u', 's', 'spoiler', ''];
  16943.         }
  16944.         getImgWrap(img) {
  16945.             return img.parentNode.parentNode;
  16946.         }
  16947.     }
  16948.     ibDomains['animach.pw'] = Animach;
  16949.  
  16950.     ibDomains['desuchan.moe'] = BaseBoard;
  16951.     ibDomains['desuchan.net'] = BaseBoard;
  16952.  
  16953.     class CrystalCafe extends Tinyboard {
  16954.         constructor(prot, dm) {
  16955.             super(prot, dm);
  16956.  
  16957.             this.qRPost = '.post.reply';
  16958.         }
  16959.         get qImgNameLink() {
  16960.             return '.fileinfo > a[title]';
  16961.         }
  16962.         getImgInfo(wrap) {
  16963.             return $q(this.qImgNameLink, wrap).title;
  16964.         }
  16965.         getTNum(thr) {
  16966.             return +thr.id.match(/\d+/);
  16967.         }
  16968.     }
  16969.     ibDomains['crystal.cafe'] = CrystalCafe;
  16970.  
  16971.     class Diochan extends Kusaba {
  16972.         constructor(prot, dm) {
  16973.             super(prot, dm);
  16974.  
  16975.             this.qImgInfo = '.filesize, .fileinfo';
  16976.  
  16977.             this.multiFile = true;
  16978.         }
  16979.         get qImgNameLink() {
  16980.             return '.filesize > a, .file_reply > a';
  16981.         }
  16982.         get css() {
  16983.             return `${ super.css }
  16984.                 .resize, .backlink, .postblock, .sage { display: none; }`;
  16985.         }
  16986.         fixFileInputs(el) {
  16987.             const str = `><input type="file" name="imagefile[]">${ $q('#spoiler') ?
  16988.                 '<input type="checkbox" name="spoiler" style="display: none;">' : '' }</div>`;
  16989.             el.innerHTML = '<div' + str + ('<div style="display: none;"' + str).repeat(2);
  16990.             $delAll('.file2, .file3, .fileurl1, .fileurl2, .fileurl3');
  16991.         }
  16992.     }
  16993.     ibDomains['diochan.com'] = Diochan;
  16994.  
  16995.     class Dobrochan extends BaseBoard {
  16996.         constructor(prot, dm) {
  16997.             super(prot, dm);
  16998.             this.dobrochan = true;
  16999.  
  17000.             this.qClosed = 'img[src="/images/locked.png"]';
  17001.             this.qDForm = 'form[action*="delete"]';
  17002.             this.qError = '.post-error, h2';
  17003.             this.qFormRedir = 'select[name="goto"]';
  17004.             this.qImgInfo = '.fileinfo';
  17005.             this.qOmitted = '.abbrev > span:last-of-type';
  17006.             this.qPages = '.pages > tbody > tr > td';
  17007.             this.qPostMsg = '.postbody';
  17008.             this.qPostSubj = '.replytitle';
  17009.             this.qTrunc = '.abbrev > span:first-of-type';
  17010.  
  17011.             this.anchor = '#i';
  17012.             this.formParent = 'thread_id';
  17013.             this.hasPicWrap = true;
  17014.             this.JsonBuilder = DobrochanPostsBuilder;
  17015.             this.multiFile = true;
  17016.             this.ru = true;
  17017.             this.timePattern = 'dd+m+?+?+?+?+?+yyyy++w++hh+ii-?s?s?';
  17018.         }
  17019.         get css() {
  17020.             return `.de-video-obj-inline { margin-left: 5px; }
  17021.                 .delete > img, .popup, .reply_, .search_google, .search_iqdb { display: none; }
  17022.                 .delete { background: none; }
  17023.                 .delete_checkbox { position: static !important; }`;
  17024.         }
  17025.         deleteTruncMsg(post, el, isInit) {
  17026.             [el.previousSibling, el.nextSibling, el].forEach($del);
  17027.             if(isInit) {
  17028.                 $replace(post.msg.firstElementChild, $q('.alternate > div', post.el));
  17029.             } else {
  17030.                 const sRunner = new SpellsRunner();
  17031.                 post.updateMsg($q('.alternate > div', post.el), sRunner);
  17032.                 sRunner.endSpells();
  17033.             }
  17034.         }
  17035.         disableRedirection(el) {
  17036.             $hide($parent(el, 'TR'));
  17037.             el.selectedIndex = 1;
  17038.         }
  17039.         fixFileInputs(el) {
  17040.             $each($Q('input[type="file"]', el), el => el.removeAttribute('onchange'));
  17041.             el.firstElementChild.value = 1;
  17042.         }
  17043.         getImgSrcLink(img) {
  17044.             // There can be a censored <img> without <a> parent
  17045.             const el = img.parentNode;
  17046.             return el.tagName === 'A' ? el :
  17047.                 $q('.fileinfo > a', img.previousElementSibling ? el : el.parentNode);
  17048.         }
  17049.         getImgWrap(img) {
  17050.             const el = img.parentNode;
  17051.             return el.tagName === 'A' ? (el.previousElementSibling ? el : el.parentNode).parentNode :
  17052.                 img.previousElementSibling ? el : el.parentNode;
  17053.         }
  17054.         getJsonApiUrl(brd, tNum) {
  17055.             return `/api/thread/${ brd }/${ tNum }/all.json?new_format&message_html&board`;
  17056.         }
  17057.         getOmitted(el) {
  17058.             while(el) {
  17059.                 const m = el.textContent.match(/(\d+) posts are omitted/);
  17060.                 if(m) {
  17061.                     return +m[1] + 1;
  17062.                 }
  17063.                 el = el.previousElementSibling;
  17064.             }
  17065.             return 1;
  17066.         }
  17067.         getPageUrl(b, p) {
  17068.             return fixBrd(b) + (p > 0 ? p + this.docExt : 'index.xhtml');
  17069.         }
  17070.         getTNum(thr) {
  17071.             return +$q('a[name]', thr).name.match(/\d+/);
  17072.         }
  17073.         init() {
  17074.             if(deWindow.location.pathname === '/settings') {
  17075.                 $q('input[type="button"]').addEventListener('click',
  17076.                     () => readCfg().then(() => saveCfg('__hanarating', $id('rating').value)));
  17077.                 return true;
  17078.             }
  17079.             $script('UploadProgress = Function.prototype;');
  17080.             const el = $id('postform');
  17081.             if(el) {
  17082.                 el.appendChild($q('.rules'));
  17083.             }
  17084.             return false;
  17085.         }
  17086.         initCaptcha(cap) {
  17087.             if(!cap.textEl) {
  17088.                 $hide($q('img', cap.parentEl));
  17089.                 $show(cap.parentEl);
  17090.             }
  17091.             return null;
  17092.         }
  17093.         insertYtPlayer(msg, playerHtml) {
  17094.             const prev = msg.previousElementSibling;
  17095.             return $bBegin(prev.tagName === 'BR' ? prev : msg, playerHtml);
  17096.         }
  17097.         updateCaptcha(cap, isErr) {
  17098.             const img = $q('img', cap.parentEl);
  17099.             if(!img) {
  17100.                 return null;
  17101.             }
  17102.             if(cap.textEl) {
  17103.                 const src = img.getAttribute('src').split('/').slice(0, -1).join('/') +
  17104.                     `/${ Date.now() }.png`;
  17105.                 img.src = '';
  17106.                 img.src = src;
  17107.                 cap.textEl.value = '';
  17108.             } else if(isErr) {
  17109.                 const el = img.parentNode;
  17110.                 el.innerHTML = '';
  17111.                 el.appendChild(img);
  17112.                 img.insertAdjacentHTML('afterend', '<br><input placeholder="Капча" autocomplete="off"' +
  17113.                     ' id="captcha" name="captcha" size="35" type="text">');
  17114.                 $show(img);
  17115.                 cap.isAdded = false;
  17116.                 cap.originHTML = cap.parentEl.innerHTML;
  17117.                 cap.addCaptcha();
  17118.             }
  17119.             return null;
  17120.         }
  17121.     }
  17122.     ibDomains['dobrochan.com'] = Dobrochan;
  17123.     ibDomains['dobrochan.net'] = Dobrochan;
  17124.     ibDomains['dobrochan.org'] = Dobrochan;
  17125.     ibDomains['dobrochan.ru'] = Dobrochan;
  17126.  
  17127.     class Endchan extends Lynxchan {
  17128.         constructor(prot, dm) {
  17129.             super(prot, dm);
  17130.  
  17131.             this.qTrunc = '.contentOmissionIndicator > p';
  17132.         }
  17133.         get css() {
  17134.             return `${ super.css }
  17135.                 .bottomNav, .delLink, #expandAll, .hidePost, .hideThread, .linkLast50,
  17136.                     .linkPreview, #modeBanner, .watchButton { display: none !important; }
  17137.                 #de-main, .de-pview { font-size: 75%; }`;
  17138.         }
  17139.         init() {
  17140.             super.init();
  17141.             $each($Q('.imgLink > img[src^="/.youtube/"]'), el => $del($parent(el, 'FIGURE')));
  17142.             $each($Q('.youtube_wrapper'), el => {
  17143.                 const src = $q('a', el).href;
  17144.                 $del($bBegin(el, `<a href="${ src }">${ src }</a>`).nextSibling);
  17145.             });
  17146.             return false;
  17147.         }
  17148.     }
  17149.     ibDomains['endchan.net'] = Endchan;
  17150.  
  17151.     class Ernstchan extends BaseBoard {
  17152.         constructor(prot, dm) {
  17153.             super(prot, dm);
  17154.  
  17155.             this.cReply = 'post';
  17156.             this.qError = '.error > .info';
  17157.             this.qFormRedir = 'input[name="gb2"][value="thread"]';
  17158.             this.qFormSpoiler = 'input[type="checkbox"][name="spoilered"]';
  17159.             this.qOPost = '.thread_OP';
  17160.             this.qPages = '.pagelist > li:nth-last-child(2)';
  17161.             this.qPostHeader = '.post_head';
  17162.             this.qPostMsg = '.text';
  17163.             this.qPostSubj = '.subject';
  17164.             this.qPostTrip = '.tripcode';
  17165.             this.qRPost = '.thread_reply';
  17166.             this.qTrunc = '.tldr';
  17167.             this.docExt = '';
  17168.             this.firstPage = 1;
  17169.             this.markupBB = true;
  17170.             this.multiFile = true;
  17171.             this.res = 'thread/';
  17172.         }
  17173.         get qImgNameLink() {
  17174.             return '.filename > a';
  17175.         }
  17176.         get css() {
  17177.             return `.content > hr, .de-parea > hr, .de-pview > .doubledash, .sage { display: none !important }
  17178.                 .de-pview > .post { margin-left: 0; border: none; }
  17179.                 #de-win-reply { float:left; margin-left:2em }`;
  17180.         }
  17181.         fixFileInputs(el) {
  17182.             const str = `><input name="file" type="file">
  17183.                 <input type="hidden" name="spoilered" value="0">
  17184.                 <input type="checkbox" name="spoilered" value="1"></div>`;
  17185.             el.innerHTML = '<div' + str + ('<div style="display: none;"' + str).repeat(3);
  17186.         }
  17187.         getImgWrap(img) {
  17188.             return img.parentNode.parentNode.parentNode;
  17189.         }
  17190.         getPageUrl(b, p) {
  17191.             return p > 1 ? fixBrd(b) + 'page/' + p : fixBrd(b);
  17192.         }
  17193.         getPostElOfEl(el) {
  17194.             while(el && !nav.matchesSelector(el, '.post')) {
  17195.                 el = el.parentElement;
  17196.             }
  17197.             return el.parentNode;
  17198.         }
  17199.     }
  17200.     ibDomains['ernstchan.xyz'] = Ernstchan;
  17201.  
  17202.     class Gensokyo extends Kusaba {
  17203.         constructor(prot, dm) {
  17204.             super(prot, dm);
  17205.  
  17206.             this.hasRefererErr = true;
  17207.         }
  17208.     }
  17209.     ibDomains['gensokyo.4otaku.org'] = Gensokyo;
  17210.  
  17211.     class Iichan extends BaseBoard {
  17212.         constructor(prot, dm) {
  17213.             super(prot, dm);
  17214.             this.iichan = true;
  17215.  
  17216.             this.hasCatalog = true;
  17217.         }
  17218.         get qFormMail() {
  17219.             return 'input[name="nya2"]';
  17220.         }
  17221.         get qFormName() {
  17222.             return 'td > input[name="nya1"]';
  17223.         }
  17224.         get qFormSubj() {
  17225.             return 'input[name="nya3"]';
  17226.         }
  17227.         get catalogUrl() {
  17228.             return `${ this.prot }//${ this.host }/${ this.b }/catalogue.html`;
  17229.         }
  17230.         get css() {
  17231.             return `${ !this.t ? '' : 'hr + #de-main { margin-top: -32px; } .logo { margin-bottom: 14px; }' }
  17232.             .iichan-hide-thread-btn, .iichan-quick-reply-btn, .postnum { display: none; }
  17233.             .replypage div[id^="thread"] span.reflink::after { content: none; }`;
  17234.         }
  17235.         get isArchived() {
  17236.             return this.b.includes('/arch');
  17237.         }
  17238.         getImgRealName(wrap) {
  17239.             return $q('.filesize > em', wrap).textContent.split(',')[2] || super.getImgRealName(wrap);
  17240.         }
  17241.         init() {
  17242.             defaultCfg.addSageBtn = 0;
  17243.             $script('highlight = Function.prototype;');
  17244.             let el = $q(this.qFormSpoiler);
  17245.             if(el) {
  17246.                 $hide(el = el.parentNode);
  17247.                 $del(el.previousSibling);
  17248.             }
  17249.             return false;
  17250.         }
  17251.     }
  17252.     ibDomains['iichan.hk'] = Iichan;
  17253.  
  17254.     class Kohlchan extends Lynxchan {
  17255.         constructor(prot, dm) {
  17256.             super(prot, dm);
  17257.  
  17258.             this.qFormRules = '#rules_row';
  17259.  
  17260.             this.hasTextLinks = true;
  17261.             this.timePattern = 'yyyy+nn+dd+hh+ii+ss';
  17262.         }
  17263.         get css() {
  17264.             return `${ super.css }
  17265.                 #postingForm, .sage { display: none; }`;
  17266.         }
  17267.         getImgRealName(wrap) {
  17268.             return $q('.originalNameLink', wrap).title;
  17269.         }
  17270.         getSage(post) {
  17271.             return !!$q('.sage', post).hasChildNodes();
  17272.         }
  17273.         init() {
  17274.             if(!this.host.includes('nocsp.')) {
  17275.                 deWindow.location.assign(deWindow.location.href
  17276.                     .replace(/(www\.)?kohlchan\.net/, 'nocsp.kohlchan.net'));
  17277.                 return true;
  17278.             }
  17279.             return super.init();
  17280.         }
  17281.     }
  17282.     ibDomains['kohlchan.net'] = Kohlchan;
  17283.  
  17284.     class Kropyvach extends Vichan {
  17285.         constructor(prot, dm) {
  17286.             super(prot, dm);
  17287.  
  17288.             this.markupBB = true;
  17289.         }
  17290.         get css() {
  17291.             return super.css + (this.t ? '' : '\r\n.de-btn-reply { display: none !important; }');
  17292.         }
  17293.         get markupTags() {
  17294.             return ['b', 'i', 'u', 's', 'spoiler', 'code'];
  17295.         }
  17296.     }
  17297.     ibDomains['kropyva.ch'] = Kropyvach;
  17298.  
  17299.     class Lainchan extends Vichan {
  17300.         constructor(prot, dm) {
  17301.             super(prot, dm);
  17302.  
  17303.             this.qOPost = '.op';
  17304.         }
  17305.         get css() {
  17306.             return `${ super.css }
  17307.                 .sidearrows { display: none !important; }
  17308.                 .bar { z-index: 1; }
  17309.                 ${ Cfg.imgNames ? '.details > a { display: none; }' : '' }`;
  17310.         }
  17311.         getImgRealName(wrap) {
  17312.             return $q('.details > a, .postfilename', wrap).textContent;
  17313.         }
  17314.         init() {
  17315.             super.init();
  17316.             $each($Q('.files + .post.op'), el => el.insertBefore(el.previousElementSibling, el.firstChild));
  17317.             return false;
  17318.         }
  17319.     }
  17320.     ibDomains['lainchan.org'] = Lainchan;
  17321.  
  17322.     class Niuchan extends Kusaba {
  17323.         get css() {
  17324.             return `${ super.css }
  17325.                 .replybacklinks, .resize { display: none; }`;
  17326.         }
  17327.     }
  17328.     ibDomains['niuchan.org'] = Niuchan;
  17329.  
  17330.     class Nowere extends BaseBoard {
  17331.         get markupTags() {
  17332.             return ['**', '***', '', '^H', '', ''];
  17333.         }
  17334.         init() {
  17335.             $script('highlight = Function.prototype;');
  17336.             return false;
  17337.         }
  17338.     }
  17339.     ibDomains['nowere.net'] = Nowere;
  17340.  
  17341.     class Ponyach extends BaseBoard {
  17342.         constructor(prot, dm) {
  17343.             super(prot, dm);
  17344.  
  17345.             this.qBan = 'font[color="#FF0000"]';
  17346.             this.qImgInfo = '.filesize[style="display: inline;"]';
  17347.  
  17348.             this.formParent = 'replythread';
  17349.             this.jsonSubmit = true;
  17350.             this.multiFile = true;
  17351.         }
  17352.         get qImgNameLink() {
  17353.             return 'a:first-of-type';
  17354.         }
  17355.         getImgInfo(wrap) {
  17356.             return wrap.textContent;
  17357.         }
  17358.         getImgRealName(wrap) {
  17359.             return $q('.mobile_filename_hide', wrap).textContent;
  17360.         }
  17361.         getImgWrap(img) {
  17362.             return $id('fs_' + img.alt);
  17363.         }
  17364.         getPNum(post) {
  17365.             return +post.getAttribute('data-num');
  17366.         }
  17367.         getSubmitData({ error, id }) {
  17368.             return { error, postNum: id && +id };
  17369.         }
  17370.         init() {
  17371.             const el = $id('postform');
  17372.             if(el) {
  17373.                 el.setAttribute('action', el.getAttribute('action') + '?json=1');
  17374.             }
  17375.             defaultCfg.postSameImg = 0;
  17376.             defaultCfg.removeEXIF = 0;
  17377.             return false;
  17378.         }
  17379.     }
  17380.     ibDomains['ponyach.ru'] = Ponyach;
  17381.  
  17382.     class Ponychan extends Tinyboard {
  17383.         constructor(prot, dm) {
  17384.             super(prot, dm);
  17385.  
  17386.             this.qOPost = '.opContainer';
  17387.  
  17388.             this.jsonSubmit = false;
  17389.         }
  17390.         get css() {
  17391.             return `${ super.css }
  17392.                 .mature_thread { display: block !important; }
  17393.                 .mature_warning { display: none; }
  17394.                 ${ Cfg.imgNames ? '.post-filename { display: none; }' : '' }`;
  17395.         }
  17396.         getImgRealName(wrap) {
  17397.             return $q('.post-filename', wrap).textContent;
  17398.         }
  17399.         init() {
  17400.             super.init();
  17401.             $each($Q('img[data-mature-src]'), el => (el.src = el.getAttribute('data-mature-src')));
  17402.             return false;
  17403.         }
  17404.     }
  17405.     ibDomains['ponychan.net'] = Ponychan;
  17406.  
  17407.     class Synch extends Tinyboard {
  17408.         constructor(prot, dm) {
  17409.             super(prot, dm);
  17410.  
  17411.             this.qImgInfo = '.unimportant';
  17412.             this.qPages = '.pagination';
  17413.  
  17414.             this.markupBB = true;
  17415.         }
  17416.         get qImgNameLink() {
  17417.             return '.file-info > a';
  17418.         }
  17419.         get css() {
  17420.             return `${ super.css }
  17421.                 .fa-sort { display: none; }
  17422.                 time::after { content: none; }`;
  17423.         }
  17424.         get markupTags() {
  17425.             return ['b', 'i', 'u', 's', 'spoiler', 'code', 'sup', 'sub'];
  17426.         }
  17427.         init() {
  17428.             const val = '{ "simpleNavbar": true }';
  17429.             if(locStorage.settings !== val) {
  17430.                 locStorage.settings = val;
  17431.                 deWindow.location.reload();
  17432.                 return true;
  17433.             }
  17434.             super.init();
  17435.             defaultCfg.timePattern = 'w+dd+m+yyyy+hh+ii+ss';
  17436.             defaultCfg.timeOffset = 4;
  17437.             defaultCfg.correctTime = 1;
  17438.             return false;
  17439.         }
  17440.         fixHTML(data, isForm) {
  17441.             const formEl = super.fixHTML(data, isForm);
  17442.             const els = $Q('.btn-group', formEl);
  17443.             for(let i = 0, len = els.length; i < len; ++i) {
  17444.                 $replace(els[i], $q('a', els[i]));
  17445.             }
  17446.             return formEl;
  17447.         }
  17448.     }
  17449.     ibDomains['syn-ch.ru'] = Synch;
  17450.     ibDomains['syn-ch.com'] = Synch;
  17451.     ibDomains['syn-ch.org'] = Synch;
  17452.  
  17453.     class Warosu extends BaseBoard {
  17454.         constructor(prot, dm) {
  17455.             super(prot, dm);
  17456.  
  17457.             this.qDForm = '.content';
  17458.             this.qForm = '.subreply';
  17459.             this.qPostRef = '.js';
  17460.             this.qImgInfo = 'span';
  17461.             this.qOPost = 'div[itemscope]';
  17462.  
  17463.             this.res = 'thread/';
  17464.         }
  17465.         get css() {
  17466.             return '.quoted-by { display: none !important; }';
  17467.         }
  17468.         getTNum(thr) {
  17469.             return +$q('div[itemscope]', thr).id.match(/\d+/);
  17470.         }
  17471.         fixHTMLHelper(str) {
  17472.             return str.replace(/\/post\/(\d+)"/g, '/#$1"');
  17473.         }
  17474.     }
  17475.     ibDomains['warosu.org'] = Warosu;
  17476.  
  17477.     const wLoc = deWindow.location;
  17478.     const prot = wLoc.protocol;
  17479.     let dm = localData && localData.dm;
  17480.     if(checkDomains) {
  17481.         if(!dm) {
  17482.             const ibKeys = Object.keys(ibDomains);
  17483.             let i = ibKeys.length;
  17484.             const host = wLoc.hostname.toLowerCase();
  17485.             while(i--) {
  17486.                 dm = ibKeys[i];
  17487.                 if(host === dm || host.endsWith('.' + dm)) {
  17488.                     return new ibDomains[dm](prot, dm);
  17489.                 }
  17490.             }
  17491.         } else if(dm in ibDomains) {
  17492.             return new ibDomains[dm](prot, dm);
  17493.         }
  17494.     }
  17495.     if(!dm) {
  17496.         dm = wLoc.hostname;
  17497.     }
  17498.     if(!dm || !checkEngines) {
  17499.         return null;
  17500.     }
  17501.     dm = dm.match(/(?:(?:[^.]+\.)(?=org\.|net\.|com\.))?[^.]+\.[^.]+$|^\d+\.\d+\.\d+\.\d+$|localhost/)[0];
  17502.     for(let i = ibEngines.length - 1; i >= 0; --i) {
  17503.         const [path, Ctor] = ibEngines[i];
  17504.         if($q(path, doc)) {
  17505.             return new Ctor(prot, dm);
  17506.         }
  17507.     }
  17508.     return null;
  17509. }
  17510.  
  17511. /* ==[ Misc.js ]==============================================================================================
  17512.                                                 MISCELLANEOUS
  17513. =========================================================================================================== */
  17514.  
  17515. // You can use Dollchan API listeners in Your external scripts and apps
  17516. // More info: https://github.com/SthephanShinkufag/Dollchan-Extension-Tools/wiki/dollchan-api
  17517. const DollchanAPI = {
  17518.     initAPI() {
  17519.         this.hasListeners = false;
  17520.         if(!('MessageChannel' in deWindow)) {
  17521.             return;
  17522.         }
  17523.         const channel = new MessageChannel();
  17524.         this.port = channel.port1;
  17525.         this.port.onmessage = this._handleMessage;
  17526.         this.activeListeners = new Set();
  17527.         const port = channel.port2;
  17528.         doc.defaultView.addEventListener('message', e => {
  17529.             if(e.data === 'de-request-api-message') {
  17530.                 this.hasListeners = true;
  17531.                 doc.defaultView.postMessage('de-answer-api-message', '*', [port]);
  17532.             }
  17533.         });
  17534.     },
  17535.     hasListener: name => DollchanAPI.hasListeners && DollchanAPI.activeListeners.has(name),
  17536.     notify(name, data) {
  17537.         if(this.hasListener(name)) {
  17538.             this.port.postMessage({ name, data });
  17539.         }
  17540.     },
  17541.  
  17542.     _handleMessage({ data: arg }) {
  17543.         if(!arg || !arg.name) {
  17544.             return;
  17545.         }
  17546.         let rv = null;
  17547.         const { name, data } = arg;
  17548.         switch(name.toLowerCase()) {
  17549.         case 'registerapi':
  17550.             if(data) {
  17551.                 rv = {};
  17552.                 for(const aName of data) {
  17553.                     rv[aName] = DollchanAPI._register(aName.toLowerCase());
  17554.                 }
  17555.             }
  17556.             break;
  17557.         }
  17558.         DollchanAPI.port.postMessage({ name, data: rv });
  17559.     },
  17560.     _register(name) {
  17561.         switch(name) {
  17562.         case 'expandmedia':
  17563.         case 'filechange':
  17564.         case 'newpost':
  17565.         case 'submitform': break;
  17566.         default: return false;
  17567.         }
  17568.         this.activeListeners.add(name);
  17569.         return true;
  17570.     }
  17571. };
  17572.  
  17573. // Checking for Dollchan updates from github
  17574. function checkForUpdates(isManual, lastUpdateTime) {
  17575.     if(!isManual) {
  17576.         if(Date.now() - +lastUpdateTime < [0, 1, 2, 7, 14, 30][Cfg.updDollchan] * 1e3 * 60 * 60 * 24) {
  17577.             return Promise.reject();
  17578.         }
  17579.     }
  17580.     return $ajax(
  17581.         gitRaw + 'src/modules/Wrap.js', { 'Content-Type': 'text/plain' }, true
  17582.     ).then(({ responseText }) => {
  17583.         const v = responseText.match(/const version = '([0-9.]+)';/);
  17584.         const remoteVer = v && v[1] ? v[1].split('.') : null;
  17585.         if(!remoteVer) {
  17586.             return Promise.reject();
  17587.         }
  17588.         const currentVer = version.split('.');
  17589.         const src = `${ gitRaw }${ nav.isESNext ? 'src/' : '' }Dollchan_Extension_Tools.${
  17590.             nav.isESNext ? 'es6.' : '' }user.js`;
  17591.         saveCfgObj('lastUpd', Date.now());
  17592.         const link = `<a style="color: blue; font-weight: bold;" href="${ src }">`;
  17593.         const chLogLink = `<a target="_blank" href="${ gitWiki }${
  17594.             lang === 1 ? 'versions-en' : 'versions' }">\r\n${ Lng.changeLog[lang] }<a>`;
  17595.         for(let i = 0, len = Math.max(currentVer.length, remoteVer.length); i < len; ++i) {
  17596.             if((+remoteVer[i] || 0) > (+currentVer[i] || 0)) {
  17597.                 return `${ link }${ Lng.updAvail[lang].replace('%s', v[1]) }</a>${ chLogLink }`;
  17598.             } else if((+remoteVer[i] || 0) < (+currentVer[i] || 0)) {
  17599.                 break;
  17600.             }
  17601.         }
  17602.         if(isManual) {
  17603.             const c = responseText.match(/const commit = '([0-9abcdef]+)';/)[1];
  17604.             const vc = version + '.' + c;
  17605.             return c === commit ? Lng.haveLatestCommit[lang].replace('%s', vc) :
  17606.                 `${ Lng.haveLatestStable[lang].replace('%s', version) }\r\n${
  17607.                     Lng.newCommitsAvail[lang].replace('%s', `${ link }${ vc }</a>${ chLogLink }`) }`;
  17608.         }
  17609.         return Promise.reject();
  17610.     }, () => !isManual ?
  17611.         Promise.reject() : `<div style="color: red; font-weigth: bold;">${ Lng.noConnect[lang] }</div>`
  17612.     );
  17613. }
  17614.  
  17615. function initPage() {
  17616.     if(aib.t) {
  17617.         if(Cfg.rePageTitle && Thread.first) {
  17618.             doc.title = `/${ aib.b } - ${ Thread.first.op.title }`;
  17619.         }
  17620.         if(!localData) {
  17621.             Cfg.stats.view++;
  17622.             saveCfgObj(aib.dm, Cfg);
  17623.         }
  17624.     } else {
  17625.         thrNavPanel.initThrNav();
  17626.     }
  17627.     if(!localData) {
  17628.         updater = initThreadUpdater(doc.title, aib.t && Cfg.ajaxUpdThr && !aib.isArchived);
  17629.     }
  17630. }
  17631.  
  17632. function scrollPage() {
  17633.     if(!aib.t && Cfg.scrollToTop) {
  17634.         scrollTo(0, 1);
  17635.         return;
  17636.     }
  17637.     setTimeout(() => {
  17638.         let post, num;
  17639.         const { hash } = deWindow.location;
  17640.         if(hash && (num = hash.match(/#[ip]?(\d+)$/)) &&
  17641.             (num = +num[1]) && (post = pByNum.get(num)) && !post.isOp
  17642.         ) {
  17643.             post.selectAndScrollTo();
  17644.             return;
  17645.         }
  17646.         const id = 'de-scroll-' + aib.b + (aib.t || '');
  17647.         const val = +sesStorage[id];
  17648.         if(val && needScroll && Cfg.saveScroll) {
  17649.             scrollTo(0, val);
  17650.             sesStorage.removeItem(id);
  17651.         }
  17652.     }, 0);
  17653. }
  17654.  
  17655. /* ==[ SvgIcons.js ]==========================================================================================
  17656.                                                   SVG ICONS
  17657. =========================================================================================================== */
  17658.  
  17659. /* eslint-disable max-len */
  17660.  
  17661. function addSVGIcons() {
  17662.     docBody.insertAdjacentHTML('beforeend', `
  17663.     <div id="de-svg-icons">
  17664.     <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  17665.     <defs>
  17666.         <linearGradient id="de-btn-back-gradient" x1="50%" y1="0%" y2="100%" x2="50%">
  17667.             <stop offset="0%" stop-color="#A0A0A0"/>
  17668.             <stop offset="50%" stop-color="#505050"/>
  17669.             <stop offset="100%" stop-color="#A0A0A0"/>
  17670.         </linearGradient>
  17671.         <linearGradient id="de-file-del-gradient" x1="50%" y1="10%" x2="50%" y2="90%">
  17672.             <stop offset="0" stop-color="#fbd"/>
  17673.             <stop offset="50%" stop-color="#f30"/>
  17674.         </linearGradient>
  17675.     </defs>
  17676.  
  17677.     <!-- POST ICONS -->
  17678.     <symbol viewBox="0 0 16 16" id="de-symbol-post-back">
  17679.         <path class="de-post-btns-back" d="M4 1Q1 1 1 4v8q0 3 3 3h8q3 0 3-3V4q0-3-3-3z"/>
  17680.     </symbol>
  17681.     <symbol viewBox="0 0 16 16" id="de-symbol-post-hide">
  17682.         <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
  17683.         <path class="de-svg-stroke" stroke-width="2.5" d="M4.5 11.5l7-7M11.5 11.5l-7-7"/>
  17684.     </symbol>
  17685.     <symbol viewBox="0 0 16 16" id="de-symbol-post-unhide">
  17686.         <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
  17687.         <path class="de-svg-stroke" stroke-width="2" d="M8 4v8M4 8h8"/>
  17688.     </symbol>
  17689.     <symbol viewBox="0 0 16 16" id="de-symbol-post-reply">
  17690.         <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
  17691.         <path class="de-svg-fill" d="M5 11c0 .8.6 1.2 1.3.7l5-3c.6-.4.6-1 0-1.5l-5-3C5.6 4 5 4.3 5 5v6z"/>
  17692.     </symbol>
  17693.     <symbol viewBox="0 0 16 16" id="de-symbol-post-expthr">
  17694.         <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
  17695.         <path class="de-svg-fill" d="M4.5 6L8 3l3.5 3H9.25v4h2.25L8 13l-3.5-3h2.25V6z"/>
  17696.     </symbol>
  17697.     <symbol viewBox="0 0 16 16" id="de-symbol-post-fav">
  17698.         <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
  17699.         <path class="de-svg-fill" d="M8 3l1.5 3 3.5.5-2.5 2.2 1 3.8-3.5-2-3.5 2 1-3.8L3 6.5 6.5 6 8 3z"/>
  17700.     </symbol>
  17701.     <symbol viewBox="0 0 16 16" id="de-symbol-post-stick">
  17702.         <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
  17703.         <path class="de-svg-fill" d="M5 5h6v6H5z"/>
  17704.     </symbol>
  17705.     <symbol viewBox="0 0 16 16" id="de-symbol-post-sage">
  17706.         <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
  17707.         <path class="de-svg-fill" d="M4 9h8l-4 4.5zm2-6h4v1H6zm0 2h4v1H6zm0 2h4v1H6z"/>
  17708.     </symbol>
  17709.     <symbol viewBox="0 0 16 16" id="de-symbol-post-src">
  17710.         <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
  17711.         <circle class="de-svg-stroke" stroke-width="2" cx="7" cy="7" r="2.5"/>
  17712.         <path class="de-svg-stroke" stroke-width="2" d="M9 9l3 3"/>
  17713.     </symbol>
  17714.  
  17715.     <!-- FILE ICONS -->
  17716.     <symbol viewBox="0 0 16 16" id="de-symbol-file-del">
  17717.         <path fill="url(#de-file-del-gradient)" stroke="#ca2900" d="M4 1.3l4 4 4-4L14.8 4l-4 4 4 4-2.8 2.8-4-4-4 4L1.3 12l4-4-4-4L4 1.3z"/>
  17718.     </symbol>
  17719.     <symbol viewBox="0 0 16 16" width="16" height="16" id="de-symbol-file-rar">
  17720.         <path stroke="#07ac07" stroke-width="2" d="M3 13h13"/>
  17721.         <path stroke="#03043f" stroke-width="4" d="M3 10h13"/>
  17722.         <path stroke="#cc5dc1" stroke-width="2" d="M3 7h13"/>
  17723.         <path fill="#ccd0db" d="M3 14l-3-3V3l3 3v8z"/>
  17724.         <path fill="#666" d="M3 5L0 2v1l3 3V5zm0 3L0 5v1l3 3V8zm0 3L0 8v1l3 3v-1zm0 3l-3-3v1l3 3v-1z"/>
  17725.         <path stroke="#103cef" stroke-width="2" d="M3 10h13"/>
  17726.         <path stroke="#294f1d" d="M3 14.5h13"/>
  17727.         <path fill="#994a95" d="M13 2H0l3 3h13l-3-3z"/>
  17728.         <path stroke="#7C467a" d="M3 5.5h13"/>
  17729.         <path stroke="#513400" stroke-width="2" d="M9.5 15V5"/>
  17730.         <path fill="#513400" d="M10.5 5l-3-3h-2l3 3h2z"/>
  17731.         <path stroke="#ceab00" stroke-width="4" d="M7 10h5"/>
  17732.         <path fill="none" stroke="#222" d="M8.5 9v1.5h2V9"/>
  17733.     </symbol>
  17734.     <symbol viewBox="0 0 16 16" id="de-symbol-file-ren">
  17735.         <circle fill="#ffe888" stroke="#333" stroke-width=".75" cx="6" cy="14" r="1.5"/>
  17736.         <circle fill="#ffe888" stroke="#333" stroke-width=".75" cx="10" cy="14" r="1.5"/>
  17737.         <circle fill="#ffe888" stroke="#333" stroke-width=".75" cx="14" cy="14" r="1.5"/>
  17738.         <path fill="#fcb45e" stroke="#3a2200" stroke-width=".75" d="M2 8L9.5.5l1.8 1.8-7.5 7.5L2 8z"/>
  17739.         <path fill="#ff8a33" stroke="#3a2200" stroke-width=".75" d="M3.8 9.8l7.5-7.5L13 4l-7.5 7.5-1.7-1.7z"/>
  17740.         <path fill="#ffe888" stroke="#333" stroke-width=".75" d="M2 8l-.5.5L1 9v3.5h3.5l1-1-1.7-1.7L2 8z"/>
  17741.         <path stroke="#333" d="M1 12.5L2.5 11"/>
  17742.     </symbol>
  17743.     <symbol viewBox="0 0 16 16" id="de-symbol-file-txt">
  17744.         <circle fill="#2cabe1" cx="8" cy="8" r="7.5"/>
  17745.         <line stroke="#fff" stroke-width="2" x1="8" y1="3" x2="8" y2="13"/>
  17746.         <line stroke="#fff" stroke-width="2" x1="3" y1="8" x2="13" y2="8"/>
  17747.     </symbol>
  17748.  
  17749.     <!-- WINDOW ICONS -->
  17750.     <symbol viewBox="0 0 16 16" id="de-symbol-win-arrow">
  17751.         <path class="de-svg-stroke" stroke-width="3.5" d="M8 13V6"/>
  17752.         <path class="de-svg-fill"  d="M3.5 7h9L8 2.5 3.5 7z"/>
  17753.     </symbol>
  17754.     <symbol viewBox="0 0 16 16" id="de-symbol-win-close">
  17755.         <path class="de-svg-stroke" stroke-width="2.5" d="M3.5 3.5l9 9m-9 0l9-9"/>
  17756.     </symbol>
  17757.  
  17758.     <!-- THREAD NAVIGATION ICONS -->
  17759.     <symbol viewBox="0 0 7 7" id="de-symbol-thr-nav-arrow">
  17760.         <path class="de-svg-fill" d="M6 3.5L2 0v7z"/>
  17761.     </symbol>
  17762.     <symbol viewBox="0 0 24 24" id="de-symbol-thr-nav-up">
  17763.         <path class="de-svg-stroke" stroke-width="3" d="M3 22.5l9-9 9 9M3 13.5l9-9 9 9"/>
  17764.     </symbol>
  17765.     <symbol viewBox="0 0 24 24" id="de-symbol-thr-nav-down">
  17766.         <path class="de-svg-stroke" stroke-width="3" d="M3 11.5l9 9 9-9M3 2.5l9 9 9-9"/>
  17767.     </symbol>
  17768.  
  17769.     <!-- IMAGE BUTTON ICONS -->
  17770.     <symbol viewBox="0 0 32 32" id="de-symbol-img-btn-arrow">
  17771.         <path class="de-svg-stroke" stroke-width="8" d="M0 16h20"/>
  17772.         <path class="de-svg-stroke" stroke-width="9" d="M13 3l16 16M13 29l16-16"/>
  17773.     </symbol>
  17774.     <symbol viewBox="0 0 32 32" id="de-symbol-img-btn-auto">
  17775.         <path class="de-svg-fill" d="M13.2 26.6c-3.1 2.4-5.9.5-5.9-3.3V8.7c0-3.8 2.8-5.6 6.1-3.3l12.5 7.1c3.1 1.9 3.1 5.2 0 7.1 0-.1-12.7 7-12.7 7z"/>
  17776.     </symbol>
  17777.     <symbol viewBox="0 0 32 32" id="de-symbol-img-btn-rotate">
  17778.         <path class="de-svg-stroke" stroke-width="7" d="M16 4c6.6 0 12 5.4 12 12s-5.4 12-12 12S4 22.6 4 16"/>
  17779.         <path class="de-svg-fill" d="M13.5 19.2L0 27V11.4z"/>
  17780.     </symbol>
  17781.  
  17782.     <!-- MAIN PANEL -->
  17783.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-logo">
  17784.         <path class="de-svg-fill" d="M22 5h-10v16h4v-14h6z"/>
  17785.         <path class="de-svg-stroke" stroke-width="3" d="M22 20.5H12c-2.8 0-5.7 0-5.7-4s2.8-4 5.7-4H21"/>
  17786.     </symbol>
  17787.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-cfg">
  17788.         <circle class="de-svg-stroke" stroke-width="3" cx="12.5" cy="12.5" r="6"/>
  17789.         <path class="de-svg-stroke" stroke-width="3" d="M12.5 6.5v-3M18.5 12.5h3M12.5 18.5v3M6.5 12.5h-3M16.7 8.3L19 6M16.7 16.7L19 19M8.3 16.7L6 19M8.3 8.3L6 6"/>
  17790.     </symbol>
  17791.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-hid">
  17792.         <path class="de-svg-stroke" stroke-width="4" d="M6 19L19 6M6 6l13 13"/>
  17793.     </symbol>
  17794.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-fav">
  17795.         <path class="de-svg-fill" d="M12.5 3.5l2.5 6 6.5.5-5 4.2 2 6.8-6-4-6 4 2-6.8-5-4.2 6.5-.5 2.5-6z"/>
  17796.     </symbol>
  17797.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-vid">
  17798.         <path class="de-svg-fill" d="M12.5 4a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zm-1 13c-1.3 1-2.5.2-2.5-1.4V9.4C9 7.8 10.2 7 11.6 8l5.3 3c1.3.8 1.3 2.2 0 3l-5.4 3z"/>
  17799.     </symbol>
  17800.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-refresh">
  17801.         <path class="de-svg-fill" d="M14 4v4.3a4.5 4.5 0 1 1-3 0V4a8.5 8.5 0 1 0 3 0z"/>
  17802.         <path class="de-svg-fill" d="M13 11V4h7"/>
  17803.     </symbol>
  17804.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-arrow">
  17805.         <path class="de-svg-stroke" stroke-width="5" d="M4 12.5h12"/>
  17806.         <path class="de-svg-fill" d="M14 19V6l7 6.5"/>
  17807.     </symbol>
  17808.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-expimg">
  17809.         <path class="de-svg-stroke" stroke-width="2" d="M20 18c0 1-1 2-2 2H7c-1 0-2-1-2-2V7c0-1 1-2 2-2h11c1 0 2 1 2 2v11z"/>
  17810.         <path stroke-width="3" d="M8 12.5h9"/>
  17811.         <path d="M10 8v9l-5-4.5M15 17V8l5 4.5"/>
  17812.     </symbol>
  17813.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-maskimg">
  17814.         <path class="de-svg-stroke" stroke-width="2" d="M20 18c0 1-1 2-2 2H7c-1 0-2-1-2-2V7c0-1 1-2 2-2h11c1 0 2 1 2 2v11z"/>
  17815.         <path d="M5 20L20 5M5 15.5L15.5 5M5 11l6-6M20 9.5L9.5 20M20 14l-6 6"/>
  17816.     </symbol>
  17817.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-preimg">
  17818.         <path class="de-svg-stroke" stroke-width="2" d="M20 18c0 1-1 2-2 2H7c-1 0-2-1-2-2V7c0-1 1-2 2-2h11c1 0 2 1 2 2v11z"/>
  17819.         <path stroke-width="3" d="M12.5 17V9"/>
  17820.         <path d="M8 15h9l-4.5 5"/>
  17821.     </symbol>
  17822.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-savethr">
  17823.         <path class="de-svg-fill" d="M18 4h-1v6H8V4H6C5 4 4 5 4 6v13c0 1 1 2 2 2h13c1 0 2-1 2-2V7l-3-3zM6 20v-8h13v8H6z"/>
  17824.         <path class="de-svg-stroke" stroke-width="3" d="M13.5 9V4"/>
  17825.     </symbol>
  17826.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-upd">
  17827.         <circle cx="12.5" cy="10.8" r="4"/>
  17828.         <path class="de-svg-stroke" stroke-width="2" stroke-linejoin="round" d="M4.5 12q8-10,16 0q-8 10,-16 0z"/>
  17829.         <path class="de-svg-stroke" d="M11 7L9.8 5M14 7l1.2-2M11 17l-1.2 2m4.2-2l1.2 2M7 8.5L5.3 6.8M7 15.5l-1.7 1.7M18 8.5l1.7-1.7M18 15.5l1.7 1.7"/>
  17830.     </symbol>
  17831.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-audio-off">
  17832.         <path class="de-svg-fill" d="M13 21V4L8 9H4v7h4l5 5z"/>
  17833.         <path class="de-svg-stroke" stroke-width="2" d="M15 9.5l6 6m0-6l-6 6"/>
  17834.     </symbol>
  17835.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-audio-on">
  17836.         <path class="de-svg-fill" d="M13 21V4L8 9H4v7h4z"/>
  17837.         <path class="de-svg-stroke" stroke-width="2" d="M15.5 7.5c1.7 3.3 1.7 6.7 0 10m3-12.5c3 5 3 10 0 15"/>
  17838.     </symbol>
  17839.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-catalog">
  17840.         <path class="de-svg-fill" d="M5 5h3v3H5zm12 0h3v3h-3zm-4 0h3v3h-3zM9 5h3v3H9zM5 9h3v3H5zm12 0h3v3h-3zm-4 0h3v3h-3zM9 9h3v3H9zm-4 4h3v3H5zm12 0h3v3h-3zm-4 0h3v3h-3zm-4 0h3v3H9zm-4 4h3v3H5zm12 0h3v3h-3zm-4 0h3v3h-3zm-4 0h3v3H9z"/>
  17841.     </symbol>
  17842.     <symbol viewBox="0 0 25 25" id="de-symbol-panel-enable">
  17843.         <path class="de-svg-stroke" stroke-width="3" d="M12.5 4v8"/>
  17844.         <path class="de-svg-fill" d="M16 4.8v4a5 5 0 0 1-3.5 8.7A5 5 0 0 1 9 9V4.7a8.5 8.5 0 1 0 7 0z"/>
  17845.     </symbol>
  17846.  
  17847.     <!-- MARKUP BUTTONS -->
  17848.     <symbol viewBox="0 0 23 22" id="de-symbol-markup-back">
  17849.         <path class="de-markup-back" stroke-width="2" d="M6 1q-5 0,-5 5v10q0 5,5 5h11q5 0,5 -5v-10q0 -5,-5-5z"/>
  17850.     </symbol>
  17851.     <symbol viewBox="0 0 23 22" id="de-symbol-markup-bold">
  17852.         <use xlink:href="#de-symbol-markup-back"/>
  17853.         <text x="5.5" y="17" style="font-family: sans-serif; font-size: 17px; font-weight: 800;">B</text>
  17854.     </symbol>
  17855.     <symbol viewBox="0 0 23 22" id="de-symbol-markup-italic">
  17856.         <use xlink:href="#de-symbol-markup-back"/>
  17857.         <text x="8" y="17" style="font-family: sans-serif; font-size: 17px; font-weight: 600; font-style: italic;">i</text>
  17858.     </symbol>
  17859.     <symbol viewBox="0 0 23 22" id="de-symbol-markup-under">
  17860.         <use xlink:href="#de-symbol-markup-back"/>
  17861.         <text x="6" y="15" width="20" style="font-family: sans-serif; font-size: 17px; font-weight: 600;">u</text>
  17862.         <path stroke="#444" stroke-width="1.5" d="M6 17H17.5"/>
  17863.     </symbol>
  17864.     <symbol viewBox="0 0 23 22" id="de-symbol-markup-strike">
  17865.         <use xlink:href="#de-symbol-markup-back"/>
  17866.         <text x="4" y="17" style="font-family: sans-serif; font-size: 22px; font-weight: 600; font-style: italic;">s</text>
  17867.         <path stroke="#444" d="M4 11H19"/>
  17868.     </symbol>
  17869.     <symbol viewBox="0 0 23 22" id="de-symbol-markup-spoil">
  17870.         <use xlink:href="#de-symbol-markup-back"/>
  17871.         <path stroke="#666" stroke-width="10" d="M4 11H19"/>
  17872.     </symbol>
  17873.     <symbol viewBox="0 0 23 22" id="de-symbol-markup-code">
  17874.         <use xlink:href="#de-symbol-markup-back"/>
  17875.         <text x="5" y="17" style="font-family: 'Lucida Console', monospace; font-size: 18px; font-weight: 600;">C</text>
  17876.     </symbol>
  17877.     <symbol viewBox="0 0 23 22" id="de-symbol-markup-sup">
  17878.         <use xlink:href="#de-symbol-markup-back"/>
  17879.         <text x="4" y="15" style="font-family: sans-serif; font-size: 16px; font-weight: 600;">x</text>
  17880.         <text x="14" y="10" style="font-family: sans-serif; font-size: 8px; font-weight: 600;">2</text>
  17881.     </symbol>
  17882.     <symbol viewBox="0 0 23 22" id="de-symbol-markup-sub">
  17883.         <use xlink:href="#de-symbol-markup-back"/>
  17884.         <text x="4" y="15" style="font-family: sans-serif; font-size: 16px; font-weight: 600;">x</text>
  17885.         <text x="14" y="17" style="font-family: sans-serif; font-size: 8px; font-weight: 600;">2</text>
  17886.     </symbol>
  17887.     <symbol viewBox="0 0 23 22" id="de-symbol-markup-quote">
  17888.         <use xlink:href="#de-symbol-markup-back"/>
  17889.         <text x="6" y="18" style="font-family: sans-serif; font-size: 20px; font-weight: 600;">&gt;</text>
  17890.     </symbol>
  17891.  
  17892.     <!-- OTHER -->
  17893.     <symbol viewBox="0 0 16 16" id="de-symbol-wait">
  17894.         <circle fill="#929087" cx="8" cy="2" r="2"/>
  17895.         <circle fill="#C5C2BA" cx="8" cy="14" r="2"/>
  17896.         <circle fill="#ACAAA0" cx="2" cy="8" r="2"/>
  17897.         <circle fill="#79766C" cx="14" cy="8" r="2"/>
  17898.         <circle fill="#D2CFC6" cx="12.25" cy="12.25" r="2"/>
  17899.         <circle fill="#9F9C93" cx="3.75" cy="3.75" r="2"/>
  17900.         <circle fill="#B9B6AE" cx="3.75" cy="12.25" r="2"/>
  17901.         <circle fill="#868379" cx="12.25" cy="3.75" r="2"/>
  17902.     </symbol>
  17903.     <symbol viewBox="0 0 16 16" id="de-symbol-closed">
  17904.         <image display="inline" width="16" height="16" xlink:href="data:image/gif;base64,R0lGODlhEAAQAKIAAP3rqPPOd+y6V+WmN+Dg4M7OzmZmZv///yH5BAEAAAcALAAAAAAQABAAAANCeLrWvZARUqqJkjiLj9FMcWHf6IldGZqM4zqRAcw0zXpAoO/6LfeNnS8XcAhjAIHSoFwim0wockCtUodWq+/1UiQAADs="/>
  17905.     </symbol>
  17906.     <symbol viewBox="0 0 16 16" id="de-symbol-unavail">
  17907.         <circle class="de-svg-stroke" fill="none" stroke-width="2" cx="8" cy="8" r="5"/>
  17908.         <path class="de-svg-stroke" stroke-width="2" d="M4 4l8 8"/>
  17909.     </symbol>
  17910.     </svg>
  17911.     </div>`);
  17912. }
  17913.  
  17914. /* eslint-enable max-len */
  17915.  
  17916. /* ==[ Css.js ]===============================================================================================
  17917.                                                      CSS
  17918. =========================================================================================================== */
  17919.  
  17920. /* eslint-disable max-len */
  17921.  
  17922. function scriptCSS() {
  17923.     const cont = (id, src) => `${ id }::before { content: ""; display: inline-block; vertical-align: -3px; padding: 16px 16px 0 0; margin-right: 4px; background: url(${ src }) no-repeat center; background-size: contain; }`;
  17924.  
  17925.     let x = `
  17926.     /* Main panel */
  17927.     #de-panel { position: fixed; right: 0; bottom: 0; z-index: 9999; border-radius: 15px 0 0 0; cursor: default; display: flex; min-height: 25px; color: #F5F5F5; }
  17928.     #de-panel-logo { flex: none; margin: auto 3px auto 0; cursor: pointer; }
  17929.     #de-panel-buttons { flex: 0 1 auto; display: flex; flex-flow: row wrap; align-items: center; padding: 0 0 0 2px; margin: 0; border-left: 1px solid #616b86; }
  17930.     .de-panel-button { display: block; flex: none; margin: 0 1px; padding: 0; transition: all .3s ease; }
  17931.     .de-panel-button-active { stroke: #32ff32 !important; fill: #32ff32 !important; }
  17932.     a.de-panel-button, a.de-panel-button:hover { color: inherit !important; }
  17933.     .de-panel-svg, #de-panel-logo, .de-panel-logo-svg, .de-panel-button { width: 25px; height: 25px; }
  17934.     #de-panel-expimg, #de-panel-maskimg, #de-panel-preimg { stroke: currentColor; fill: currentColor; }
  17935.     #de-panel-goback { transform: rotate(180deg); will-change: transform; }
  17936.     #de-panel-godown { transform: rotate(90deg); will-change: transform; }
  17937.     #de-panel-goup { transform: rotate(-90deg); will-change: transform; }
  17938.     #de-panel-upd-on { fill: #32ff32; }
  17939.     #de-panel-upd-warn { fill: #fff441; }
  17940.     #de-panel-upd-off { fill: #ff3232; }
  17941.     #de-panel-audio-on > .de-panel-svg > .de-use-audio-off, #de-panel-audio-off > .de-panel-svg > .de-use-audio-on { display: none; }
  17942.     #de-panel-info { display: flex; flex: none; padding: 0 6px; margin-left: 2px; border-left: 1px solid #616b86; font: 18px serif; }
  17943.     #de-panel-info-icount::before, #de-panel-info-acount:not(:empty)::before { content: "/"; }
  17944.     #de-svg-icons, #de-svg-icons > svg { height: 0; width: 0; position: fixed; }
  17945.     .de-svg-fill { stroke: none; fill: currentColor; }
  17946.     .de-svg-stroke { stroke: currentColor; fill: none; }
  17947.     use { fill: inherit; pointer-events: none; }
  17948.  
  17949.     /* Panel theme */
  17950.     .de-img-btn, #de-panel, .de-win-head ${ [
  17951.         /* Gradient darkblue */
  17952.         '{ background: linear-gradient(to bottom, #7b849b, #616b86 8%, #3a414f 52%, rgba(0,0,0,0) 52%), linear-gradient(to bottom, rgba(0,0,0,0) 48%, #121212 52%, #1f2740 100%); }',
  17953.         /* Gradient blue */
  17954.         `{ background: linear-gradient(to bottom, #4b90df, #3d77be 20%, #376cb0 28%, #295591 52%, rgba(0,0,0,0) 52%), linear-gradient(to bottom, rgba(0,0,0,0) 48%, #183d77 52%, #1f4485 72%, #264c90 80%, #325f9e 100%); }
  17955.         #de-panel-buttons, #de-panel-info { border-color: #8fbbed; }`,
  17956.         /* Solid grey */
  17957.         `{ background-color: #777; }
  17958.         #de-panel-buttons, #de-panel-info { border-color: #ccc; }
  17959.         .de-panel-svg:hover { border: 2px solid #444; border-radius: 5px; box-sizing: border-box; transition: none; }`,
  17960.         /* Transparent blue */
  17961.         '{ background-color: rgba(0,20,80,.72); }',
  17962.         /* Square dark */
  17963.         `{ background: none; background-color: #333; border-radius: 0 !important; }
  17964.         #de-win-reply.de-win { border-radius: 0 !important; }
  17965.         #de-panel-buttons, #de-panel-info { border-color: #666; }`
  17966.     ][Cfg.scriptStyle] }
  17967.     .de-logo { background: linear-gradient(to bottom, #7b849b, #616b86 8%, #3a414f 52%, rgba(0,0,0,0) 52%), linear-gradient(to bottom, rgba(0,0,0,0) 48%, #121212 52%, #1f2740 100%); }
  17968.     ${ Cfg.scriptStyle === 2 ?
  17969.         '.de-panel-svg:hover { border: 2px solid #444; border-radius: 5px; box-sizing: border-box; transition: none; }' :
  17970.         '.de-panel-button:hover { background-color: rgba(255,255,255,.15); box-shadow: 0 0 3px rgba(143,187,237,.5); }' }\r\n`;
  17971.  
  17972.     if(Cfg.disabled) {
  17973.         $css(x).id = 'de-css';
  17974.         return;
  17975.     }
  17976.  
  17977.     x += `
  17978.     /* Windows */
  17979.     .de-win .de-win-btn-toggle { transform: rotate(180deg); }
  17980.     .de-resizer { position: absolute; }
  17981.     .de-resizer-bottom { height: 6px; bottom: -3px; left: 0; right: 0; cursor: ns-resize; }
  17982.     .de-resizer-left { width: 6px; top: 0px; bottom: 0px; left: -3px; cursor: ew-resize; }
  17983.     .de-resizer-right { width: 6px; top: 0px; bottom: 0px; right: -3px; cursor: ew-resize; }
  17984.     .de-resizer-top { height: 6px; top: -3px; left: 0; right: 0; cursor: ns-resize; }
  17985.     .de-win > .de-win-head { cursor: move; }
  17986.     .de-win-buttons { position: absolute; right: 0; margin: 0 2px 0 0; font-size: 0; cursor: pointer; }
  17987.     .de-win-buttons > svg { transition: background .3s ease, box-shadow .3s ease; }
  17988.     .de-win-buttons > svg:hover { background-color: rgba(255,255,255,.2); box-shadow: 0 0 2px rgba(255,255,255,.4); }
  17989.     .de-win-inpost > .de-win-head > .de-win-buttons > svg:hover { background-color: rgba(64,64,64,.15); box-shadow: 0 0 2px rgba(64,64,64,.3); }
  17990.     #de-win-cfg { width: 355px; }
  17991.     #de-win-cfg, #de-win-fav, #de-win-hid, #de-win-vid { position: fixed; max-height: 92%; overflow-x: hidden; overflow-y: auto; }
  17992.     #de-win-cfg > .de-win-body { float: none; display: block; width: auto; min-width: 0; max-width: 100% !important; padding: 0 !important; margin: 0 !important; border: none; }
  17993.     #de-win-fav > .de-win-body, #de-win-hid > .de-win-body, #de-win-vid > .de-win-body { padding: 6px; border: 1px solid gray; }
  17994.     #de-win-hid { max-width: 60%; }
  17995.     #de-win-vid > .de-win-body { display: flex; flex-direction: column; align-items: center; }
  17996.     #de-win-vid .de-entry { white-space: normal; }
  17997.     .de-win-head { position: relative; padding: 2px; border-radius: 10px 10px 0 0; color: #F5F5F5; font: bold 14px/16px arial; text-align: center; cursor: default; }
  17998.  
  17999.     /* Settings window */
  18000.     .de-block { display: block; }
  18001.     #de-btn-spell-add { margin-left: auto; }
  18002.     #de-cfg-bar { display: flex; margin: 0; padding: 0; }
  18003.     .de-cfg-body { min-height: 353px; padding: 9px 7px 7px; margin-top: -1px; font: 13px/15px arial !important; -moz-box-sizing: content-box; box-sizing: content-box; }
  18004.     .de-cfg-body, #de-cfg-buttons { border: 1px solid #183d77; border-top: none; }
  18005.     .de-cfg-button { padding: 0 ${ nav.isFirefox ? '2' : '4' }px !important; margin: 0 4px; height: 21px; font: 12px arial !important; }
  18006.     #de-cfg-button-debug { padding: 0 2px; font: 13px/15px arial; }
  18007.     #de-cfg-buttons { display: flex; align-items: center; padding: 3px; }
  18008.     #de-cfg-buttons > label { flex: 1 0 auto; }
  18009.     .de-cfg-chkbox { ${ nav.isPresto ? '' : 'vertical-align: -1px !important; ' }margin: 2px 1px !important; }
  18010.     #de-cfg-info { display: flex; flex-direction: column; }
  18011.     input[type="text"].de-cfg-inptxt { width: auto; height: auto; min-height: 0; padding: 0 2px !important; margin: 1px 4px 1px 0 !important; font: 13px arial !important; border-width: 1px; }
  18012.     .de-cfg-inptxt, .de-cfg-label, .de-cfg-select { display: inline; width: auto; height: auto !important; font: 13px/15px arial !important; }
  18013.     .de-cfg-label { padding: 0; margin: 0; }
  18014.     .de-cfg-select { padding: 0 2px; margin: 1px 0; font: 13px arial !important; float: none; }
  18015.     .de-cfg-tab { flex: 1 0 auto; display: block !important; margin: 0 !important; float: none !important; width: auto !important; min-width: 0 !important; padding: 4px 0 !important; box-shadow: none !important; border: 1px solid #444 !important; border-radius: 4px 4px 0 0 !important; opacity: 1; font: bold 12px arial; text-align: center; cursor: default; background-image: linear-gradient(to bottom, rgba(132,132,132,.35) 0%, rgba(79,79,79,.35) 50%, rgba(40,40,40,.35) 50%, rgba(80,80,80,.35) 100%) !important; }
  18016.     .de-cfg-tab:hover { background-image: linear-gradient(to top, rgba(132,132,132,.35) 0%, rgba(79,79,79,.35) 50%, rgba(40,40,40,.35) 50%, rgba(80,80,80,.35) 100%) !important; }
  18017.     .de-cfg-tab[selected], .de-cfg-tab[selected]:hover { background-image: none !important; border-bottom: none !important; }
  18018.     .de-cfg-tab::${ nav.isFirefox ? '-moz-' : '' }selection { background: transparent; }
  18019.     .de-cfg-unvis { display: none !important; }
  18020.     .de-depend { padding-left: 17px; }
  18021.     #de-info-log, #de-info-stats { width: 100%; padding: 0px 7px; }
  18022.     #de-info-log { overflow-y: auto; border-left: 1px solid grey; }
  18023.     .de-info-name { flex: 1 0 auto; }
  18024.     .de-info-row { display: flex; }
  18025.     #de-info-table { display: flex; flex: 1 0 auto; }
  18026.     .de-spell-btn { padding: 0 4px; }
  18027.     #de-spell-editor { display: flex; align-items: stretch; height: 258px; padding: 2px 0; }
  18028.     #de-spell-panel { display: flex; }
  18029.     #de-spell-txt { padding: 2px !important; margin: 0; width: 100%; min-width: 0; border: none !important; outline: none !important; font: 12px courier new; ${ nav.isPresto ? '' : 'resize: none !important; ' }}
  18030.     #de-spell-rowmeter { padding: 2px 3px 0 0; overflow: hidden; min-width: 2em; background-color: #616b86; text-align: right; color: #fff; font: 12px courier new; }
  18031.     #de-win-cfg.de-win-fixed { z-index: 10001 !important; }
  18032.  
  18033.     /* Settings window theme */
  18034.     ${ [/* Gradient darkblue */
  18035.         `#de-cfg-bar { background-color: #1f2740; }
  18036.         .de-cfg-tab { border-color: #121421 !important; }`,
  18037.         /* Gradient blue */
  18038.         `#de-cfg-bar { background-color: #325f9e; }
  18039.         .de-cfg-tab { border-color: #183d77 !important; }`,
  18040.         /* Solid grey */
  18041.         `#de-cfg-bar, #de-spell-rowmeter { background-color: #777; }
  18042.         .de-cfg-body, #de-cfg-buttons { border-color: #444; }`,
  18043.         /* Transparent blue */
  18044.         `#de-cfg-bar { background-color: rgba(0,20,80,.72); }
  18045.         .de-cfg-tab { border-color: #001450 !important; }`,
  18046.         /* Square dark */
  18047.         `#de-cfg-bar { background-color: #222; }
  18048.         .de-cfg-body, #de-cfg-buttons { border-color: #666; }`
  18049.     ][Cfg.scriptStyle] }
  18050.  
  18051.     /* Favorites window */
  18052.     .de-entry { display: flex !important; align-items: center; float: none !important; padding: 0 !important; margin: 1px 0 !important; min-width: 0 !important; border: none !important; font-size: 13px; overflow: hidden !important; white-space: nowrap; }
  18053.     .de-entry-title { flex: auto; padding-left: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  18054.     #de-fav-buttons, #de-hid-buttons { padding-top:6px; }
  18055.     .de-fav-entries { border-top: 1px solid rgba(80,80,80,.3); }
  18056.     .de-fav-entries-hide, .de-fav-inf-icon:not(.de-fav-closed):not(.de-fav-unavail):not(.de-fav-wait), .de-fav-closed > .de-fav-unavail-use, .de-fav-closed > .de-fav-wait-use, .de-fav-unavail > .de-fav-closed-use, .de-fav-unavail > .de-fav-wait-use, .de-fav-wait > .de-fav-closed-use, .de-fav-wait > .de-fav-unavail-use { display: none; }
  18057.     .de-fav-del-btn { margin-left: 2px; cursor: pointer; }
  18058.     .de-fav-del-btn > svg { width: 12px; height: 12px; opacity: 0.65; vertical-align: -2px; }
  18059.     .de-fav-del-btn[de-checked] > svg { color: red; background-color: rgba(255,0,0,.2); border-radius: 7px; opacity: 1; }
  18060.     .de-fav-header { display: flex; cursor: pointer; font-size: 13px; }
  18061.     .de-fav-header-btn { flex: 1 0 auto; margin-right: 2px; font-size: 11px; color: inherit; text-align: right; opacity: 0.65; }
  18062.     .de-fav-header-link { margin-left: 2px; color: inherit; font-weight: bold; text-decoration: none; outline: none; }
  18063.     .de-fav-inf { flex: none; padding: 0 4px 0 10px; font: bold 14px serif; cursor: default; }
  18064.     .de-fav-inf-icon, .de-fav-inf-iwrap  { width: 16px; height: 16px; }
  18065.     .de-fav-inf-icon { margin-bottom: -3px; }
  18066.     .de-fav-inf-new { color: #424f79; }
  18067.     .de-fav-inf-new::after { content: " +"; }
  18068.     .de-fav-inf-old { color: #4f7942; }
  18069.     .de-fav-inf-you { padding: 0 4px; margin-right: 4px; border-radius: 3px; color: #fff; background-color: #424f79; opacity: 0.65; }
  18070.     .de-fav-link { flex: none; margin-left: 2px; text-decoration: none; border: none; }
  18071.     .de-fav-table-unfold > .de-fold-block > .de-fav-entries { display: initial !important; }
  18072.     .de-fav-unavail { color: #cf4436; }
  18073.     .de-fold-block { border: 1px solid rgba(120,120,120,.8); border-radius: 2px; }
  18074.     .de-fold-block:not(:first-child) { border-top: none; }
  18075.  
  18076.     /* Post panel */
  18077.     .de-btn-hide > .de-btn-unhide-use, .de-btn-hide-user > .de-btn-unhide-use, .de-btn-unhide > .de-btn-hide-use, .de-btn-unhide-user > .de-btn-hide-use { display: none; }
  18078.     .de-btn-expthr, .de-btn-fav, .de-btn-fav-sel, .de-btn-hide, .de-btn-hide-user, .de-btn-reply, .de-btn-sage, .de-btn-src, .de-btn-stick, .de-btn-stick-on, .de-btn-unhide, .de-btn-unhide-user, .de-win-btn-clear, .de-win-btn-close, .de-win-btn-toggle { margin: 0 2px -3px 0 !important; cursor: pointer; width: 16px; height: 16px; }${
  18079.     !pr.form && !pr.oeForm ? '.de-btn-reply { display: none; }' : '' }
  18080.     .de-post-btns { margin-left: 4px; }
  18081.     .de-post-btns-back { fill: inherit; stroke: none; }
  18082.     .de-post-note:not(:empty) { color: inherit; margin: 0 4px; vertical-align: 1px; font: italic bold 12px serif; }
  18083.     .de-thread-note { font-style: italic; }
  18084.  
  18085.     /* Sauce buttons */
  18086.     ${ cont('.de-src-google', 'data:image/gif;base64,R0lGODlhEAAQAMQAAIy0+tHh/gJc8Qlh8UyM9H2r9/3///7//x+OfACSJy+mTZHQos3Te////f///v3HAP+uAPzWjvWTWeUTAPSdl/79/f////39/f///wAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABgALAAAAAAQABAAAAVuICaKh2Eax6hih+W+bqoaLjXdE+UaY2vhwInrInLhdBYbDEOL3GBQS4X2gEiiUBoEAhMIBl6CpaHlvrxocaO1XUQBgsLYxUgkot7AGONS2N0WCwgCYhZFfXaJCQguDiMvC34JCoCOKlgvK49QKyEAOw==') }
  18087.     ${ cont('.de-src-yandex', 'data:image/gif;base64,R0lGODlhEAAQAMQAAP////v7+/r5+fb29vHx8eLi4tnZ2dTU1NDQ0MvLy8fHx8TExPzJv/immvlXRvq0re4UEdeGhtbFxcnJyby8vKampm5ubv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABcALAAAAAAQABAAAAVx4CWOZCkOQKqq5uoCQhnMQuPcc2CQuc3YuQBJQHQ8BA8HkUgYDZ6Qx+ABeVoVIoL2RmhAtODmpUD23iDkdEFkaBsiEAfE3a6IDngJJALH4ycjCIJyCXCCgiQJhXuLigl2IwqSk5QUJRQLmZqaFiaeniEAOw==') }
  18088.     ${ cont('.de-src-tineye', 'data:image/gif;base64,R0lGODlhEAAQAMQAAP///wAAAAwOEhg4UDZ7skGNxkuf3Vycxx4nLerw9DlSX2FnanO21Epxg4LO62KOnpXj+ZGcmb7CvbZ6RfxmAIxBCzsGAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABcALAAAAAAQABAAAAWlYGIUZGGc6HgkxzSRozIgdHMUx0NR5hH8v8VjeFsoDAyFpcKcQByMnIGgYDAikmxkOLQWqNAGQhAQLBC/BgSXC0QivwgAgFbjHLQ5XDKXBBRrBA80WAABRgkJPwxfDw2HCYYBfHABDSQMDgoIEgs/ZgoPDmuYEFEHmQ6jT5ckBKirEE8HCgEWJQe5ZwJjQBYRBwQMsk8RFgJkP04sVrHGNAEVEAAhADs=') }
  18089.     ${ cont('.de-src-saucenao', 'data:image/gif;base64,R0lGODlhEAAQAIAAAP///wAAACwAAAAAEAAQAAACJ4yPacDtvpQCkU1KT0P75i49mbSAZACd6HN2pmbBI7pe9K1+4q5KBQA7') }
  18090.     ${ cont('.de-src-iqdb', 'data:image/gif;base64,R0lGODlhEAAQAMQAAP//////AP8A//8AAAD//wD/AAAA/wAAANx/hV1ISW9YWvLOd/u0T+WlTNaqcKdtMv/r1mxML7OCVoxtUbmmlfPRuKKGeHpcTvK3nEEvKGpRTCcbGU48OYVua3tkYv///yH5BAEAAB8ALAAAAAAQABAAAAW4INV5ZDcmibM0iVJRnukpmnJJDdNwnDWSk4vCNMnpGJXfwqHReDQJTuTxmEhmqoTFEtRsNhltRTFZTBKUl2IzAmeE2csFA8kcKADLl5NgSDgaGRYAFVYQABkRgAxnGhcdEJEVhxYSDhwNEQkaEhSRGBgIGBUOGBwXERkcExyeFaGjr4E8qgcHDpKgkQpRGhwZYA4Vw6CvPBOpwD0ODlMSoxcJOQ8ZyhccBxkSkRRFDw4SD1/jF5MQIQA7') }
  18091.     ${ cont('.de-src-tracemoe', 'data:image/gif;base64,R0lGODlhEAAPALMAAAAAAP///9fY18HBwTg4ODw1No6PjoFnZhoBAXNGRf/V1KmRkf///wAAAAAAAAAAACH5BAEAAAwALAAAAAAQAA8AAAQ5EMhJq7046w0I9hMhBAIoiSQ4BiS1tgQrg7EceIM8UDm7S4aBwRIcYgoFjmSxQHASCkWCc6gelJkIADs=') }
  18092.  
  18093.     /* Posts counter */
  18094.     .de-post-counter { margin: 0 4px 0 2px; vertical-align: 1px; font: bold 11px tahoma; color: #4f7942; cursor: default; }
  18095.     .de-post-counter-deleted { color: #727579; }
  18096.     .de-post-counter-you { vertical-align: 1px; font: bold 11px tahoma; color: #505a7a; cursor: default; }
  18097.  
  18098.     /* Text markup buttons */
  18099.     .de-markup-back { fill: #f0f0f0; stroke: #808080; }
  18100.     #de-txt-panel { display: block; font-weight: bold; cursor: pointer; }
  18101.     #de-txt-panel > div { display: inline-block; }
  18102.     #de-txt-panel > div > svg { width: 23px; height: 22px; margin: 0 2px; }\r\n`;
  18103.  
  18104.     if('animation' in docBody.style) {
  18105.         x += `
  18106.         /* Show/hide animation */
  18107.         @keyframes de-open { 0% { transform: translateY(-100%); } 100% { transform: translateY(0); } }
  18108.         @keyframes de-close { 0% { transform: translateY(0); } 100% { transform: translateY(-100%); } }
  18109.         @keyframes de-blink {
  18110.             0%, 100% { transform: translateX(0); }
  18111.             10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
  18112.             20%, 40%, 60%, 80% { transform: translateX(10px); }
  18113.         }
  18114.         @keyframes de-post-open-tl { from { transform: translate(-50%,-50%) scale(0); opacity: 0; } }
  18115.         @keyframes de-post-open-bl { from { transform: translate(-50%,50%) scale(0); opacity: 0; } }
  18116.         @keyframes de-post-open-tr { from { transform: translate(50%,-50%) scale(0); opacity: 0; } }
  18117.         @keyframes de-post-open-br { from { transform: translate(50%,50%) scale(0); opacity: 0; } }
  18118.         @keyframes de-post-close-tl { to { transform: translate(-50%,-50%) scale(0); opacity: 0; } }
  18119.         @keyframes de-post-close-bl { to { transform: translate(-50%,50%) scale(0); opacity: 0; } }
  18120.         @keyframes de-post-close-tr { to { transform: translate(50%,-50%) scale(0); opacity: 0; } }
  18121.         @keyframes de-post-close-br { to { transform: translate(50%,50%) scale(0); opacity: 0; } }
  18122.         @keyframes de-post-new { from { transform: translate(0,-50%) scaleY(0); opacity: 0; } }
  18123.         @keyframes de-win-open { from { transform: translate(0,50%) scaleY(0); opacity: 0; } }
  18124.         @keyframes de-win-close { to { transform: translate(0,50%) scaleY(0); opacity: 0; } }
  18125.         .de-pview-anim { animation-duration: .2s; animation-timing-function: ease-in-out; animation-fill-mode: both; }
  18126.         .de-open { animation: de-open .15s ease-out both; }
  18127.         .de-close { animation: de-close .15s ease-in both; }
  18128.         .de-blink { animation: de-blink .7s ease-in-out both; }
  18129.         .de-post-new { animation: de-post-new .2s ease-out both; }
  18130.         .de-win-open { animation: de-win-open .2s ease-out backwards; }
  18131.         .de-win-close { animation: de-win-close .2s ease-in both; }\r\n`;
  18132.     } else {
  18133.         Cfg.animation = 0;
  18134.     }
  18135.  
  18136.     let p = Math.max(Cfg.minImgSize || 0, 50);
  18137.     x += `
  18138.     /* Full images */
  18139.     .de-img-embed, .de-fullimg { border: none; outline: none; cursor: pointer; image-orientation: from-image; }
  18140.     .de-img-embed { max-width: 200px; max-height: 200px; }
  18141.     .de-fullimg { display: block; }
  18142.     .de-fullimg, .de-fullimg-wrap-link { flex: 0 0 auto; transition: none !important; max-width: none; max-height: none; }
  18143.     .de-fullimg-after { clear: left; }
  18144.     .de-fullimg-center { position: fixed; margin: 0 !important; z-index: 9999; background-color: #ccc; border: 1px solid black !important; -moz-box-sizing: content-box; box-sizing: content-box; }
  18145.     .de-fullimg-info { position: absolute; bottom: -22px; left: 50%; padding: 1px 4px; transform: translateX(-50%); background-color: rgba(64,64,64,.8); white-space: nowrap; line-height: 17px; }
  18146.     .de-fullimg-info > .de-btn-src { color: #fff; }
  18147.     .de-fullimg-link { float: none !important; display: inline-block; font: bold 12px tahoma; color: #fff !important; text-decoration: none; outline: none; }
  18148.     .de-fullimg-link:hover { color: #fff !important; background: rgba(64,64,64,.6); }
  18149.     .de-fullimg-load { position: absolute; z-index: 2; width: 50px; height: 50px; top: 50%; left: 50%; margin: -25px; }
  18150.     .de-fullimg-rotated { transform-origin: top left; width: auto !important; max-width: none !important; }
  18151.     .de-fullimg-video-hack { width: 100%; height: calc(100% - 40px); position: absolute; z-index: 1; cursor: pointer; }
  18152.     .de-fullimg-wrap { position: relative; margin-bottom: 24px; }
  18153.     .de-fullimg-wrap-center, .de-fullimg-wrap-link, .de-fullimg-video > video { width: 100%; height: 100%; max-height: 100%; }
  18154.     .de-fullimg-wrap-center > .de-fullimg-wrap-link > .de-fullimg { height: 100%; }
  18155.     .de-fullimg-wrap-inpost { min-width: ${ p }px; min-height: ${ p }px; float: left; ${ aib.multiFile ? '' : 'margin: 2px 5px; -moz-box-sizing: border-box; box-sizing: border-box; ' } }
  18156.     .de-fullimg-wrap-nosize > .de-fullimg-wrap-link > .de-fullimg { opacity: 0.3; }
  18157.     .de-img-btn { position: fixed; top: 50%; z-index: 10000; height: 36px; width: 36px; border-radius: 10px 0 0 10px; color: #f0f0f0; cursor: pointer; }
  18158.     .de-img-btn > svg { height: 32px; width: 32px; margin: 2px; }
  18159.     #de-img-btn-auto { right: 0; margin-top: 58px; }
  18160.     .de-img-btn-auto-on { color: #ffe100; }
  18161.     #de-img-btn-next { right: 0; margin-top: -18px; }
  18162.     .de-img-btn-none { display: none; }
  18163.     #de-img-btn-prev { left: 0; margin-top: -18px; transform: scaleX(-1); }
  18164.     #de-img-btn-rotate { right: 0; margin-top: 20px; }
  18165.  
  18166.     /* Embedders */
  18167.     ${ cont('.de-video-link.de-ytube', 'https://youtube.com/favicon.ico') }
  18168.     ${ cont('.de-video-link.de-vimeo', 'https://vimeo.com/favicon.ico') }
  18169.     ${ cont('.de-img-arch', 'data:image/gif;base64,R0lGODlhEAAQALMAAF82SsxdwQMEP6+zzRA872NmZQesBylPHYBBHP///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAAAQABAAQARTMMlJaxqjiL2L51sGjCOCkGiBGWyLtC0KmPIoqUOg78i+ZwOCUOgpDIW3g3KJWC4t0ElBRqtdMr6AKRsA1qYy3JGgMR4xGpAAoRYkVDDWKx6NRgAAOw==') }
  18170.     ${ cont('.de-img-audio', 'data:image/gif;base64,R0lGODlhEAAQAKIAAGya4wFLukKG4oq3802i7Bqy9P///wAAACH5BAEAAAYALAAAAAAQABAAQANBaLrcHsMN4QQYhE01OoCcQIyOYQGooKpV1GwNuAwAa9RkqTPpWqGj0YTSELg0RIYM+TjOkgba0sOaAEbGBW7HTQAAOw==') }
  18171.     .de-current::after { content: " \u25CF"; }
  18172.     .de-img-arch, .de-img-audio { margin-left: 4px; color: inherit; text-decoration: none; font-weight: bold; }
  18173.     .de-mp3 { margin: 5px 20px; }
  18174.     .de-video-obj { margin: 5px 20px; white-space: nowrap; }
  18175.     .de-video-obj-inline { display: inline-block; }
  18176.     #de-video-btn-resize { padding: 0 14px 8px 0; margin: 0 8px; border: 2px solid; border-radius: 2px; }
  18177.     #de-video-btn-hide, #de-video-btn-prev { margin-left: auto; }
  18178.     #de-video-buttons { display: flex; margin-bottom: 2px; align-items: center; width: 100%; line-height: 16px; }
  18179.     #de-video-buttons > a:not(:hover) { color: inherit; }
  18180.     .de-video-expanded { width: 854px !important; height: 480px !important; }
  18181.     #de-video-list { padding: 0 0 4px; overflow-y: auto; width: 100%; }
  18182.     .de-video-refpost { margin: 0 3px; color: inherit; text-decoration: none; cursor: pointer; }
  18183.     .de-video-resizer::after { content: "\u2795"; margin: 0 -15px 0 3px; vertical-align: 6px; color: #000; font-size: 12px; cursor: pointer; }
  18184.     .de-video-player, .de-video-thumb { width: 100%; height: 100%; }
  18185.     a.de-video-player { display: inline-block; position: relative; border-spacing: 0; border: none; }
  18186.     a.de-video-player::after { content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAWCAQAAACMYb/JAAAArklEQVR4AYXSr05CYRjA4cPGxjRosTijdvNJzmD1CrwAvQWugASNwGg0MoErOIVCPCMx0hmBMaAA4mPX8/2rT/i+9/1lPu0M3MtCN1OAvS+NEFkDmHqoJwcAbHzUkb9n7C5FqLynCAzdpAhLrynCRc9VnEDpKUWYpUmZIlt5nBQeY889amvGPj33HBvdt45WbAELeWyNP/qu/8dwBrDyVp9UBRi5DYXZdTLxEs77F5bCVAHlDJ1UAAAAAElFTkSuQmCC"); position: absolute;top: 50%; left: 50%; padding: 12px 24px; margin: -22px 0 0 -32px; background-color: rgba(255,0,0,.4); border-radius: 8px; line-height: 0; }
  18187.     a.de-video-player:hover::after { background-color: rgba(255,0,0,.7); }
  18188.     .de-video-title[de-time]::after { content: " [" attr(de-time) "]"; color: red; }
  18189.     .de-video-title[de-time].de-current::after { content: " [" attr(de-time) "] \u25CF"; color: red; }
  18190.     .de-vocaroo > embed { display: inline-block; }
  18191.     video { background: black; }
  18192.  
  18193.     /* File inputs */
  18194.     .de-file { display: inline-block; vertical-align: top; margin: 1px; height: ${ p = aib.multiFile ? 90 : 130 }px; width: ${ p }px; text-align: center; background-color: rgba(96,96,96,.15); border: 1px dashed grey; }
  18195.     .de-file > .de-file-img > div { display: flex; justify-content: center; align-items: center; height: ${ p }px; cursor: pointer; }
  18196.     .de-file > .de-file-utils { display: none; height: 18px; margin-top: -20px; padding: 1px 0; background: rgba(64,64,64,.6); position: relative; -moz-box-sizing: initial; box-sizing: initial; }
  18197.     .de-file > .de-file-utils > .de-file-rarmsg { display: block; position: absolute; bottom: 20px; width: 100%; margin: 0; background: rgba(64,64,64,.6); color: #fff; }
  18198.     #de-file-area { margin-top: 1px; width: 275px; min-width: 100%; max-width: 100%; overflow-x: auto; overflow-y: hidden; white-space: nowrap; }
  18199.     .de-file-drag { background: rgba(96,96,96,.8); border: 1px solid grey; opacity: .7; }
  18200.     .de-file:hover:not(.de-file-drag) > .de-file-utils { display: block !important; }
  18201.     img.de-file-img, video.de-file-img { max-width: ${ p }px; max-height: ${ p }px; }
  18202.     .de-file-input { max-width: 300px; }
  18203.     .de-file-input + .de-file-utils { margin-left: 4px; }
  18204.     .de-file-off > .de-file-img > div::after { content: "${ Lng.dropFileHere[lang] }"; display: block; width: 80px; margin: 0 auto; font: 11px arial; opacity: .8; white-space: initial; }
  18205.     .de-file-rarmsg { margin: 0 2px; vertical-align: 4px; font: bold 11px tahoma; cursor: default; }
  18206.     .de-file-btn-del, .de-file-btn-rar, .de-file-btn-ren, .de-file-btn-txt { margin: 0 1px; cursor: pointer; }
  18207.     .de-file-btn-del > svg, .de-file-btn-rar > svg, .de-file-btn-ren > svg, .de-file-btn-txt > svg { width: 16px; height: 16px; }
  18208.     .de-file-spoil { margin: 0 3px; vertical-align: 1px; }
  18209.     .de-file-txt-add { font-weight: bold; width: 21px; padding: 0 !important; }
  18210.     .de-file-txt-input { border: 1px solid #9c9c9c; padding: 2px; font: 12px/16px sans-serif; }
  18211.     .de-file-txt-noedit { background: rgba(255,255,255,.5); cursor: pointer; }
  18212.     .de-file-utils { display: inline-block; float: none; vertical-align: -3px; }
  18213.  
  18214.     /* Reply form */
  18215.     .de-parea { text-align: center; }
  18216.     .de-parea-btn-close::after { content: "${ Lng.hideForm[lang] }"; }
  18217.     .de-parea-btn-thr::after { content: "${ Lng.makeThr[lang] }"; }
  18218.     .de-parea-btn-reply::after { content: "${ Lng.makeReply[lang] }"; }
  18219.     #de-pform > form { padding: 0; margin: 0; border: none; }
  18220.     #de-pform input[type="text"], #de-pform input[type="file"] { width: 200px; }
  18221.     #de-resizer-text { display: inline-block !important; float: none !important; padding: 5px; margin: ${ nav.isPresto ? '-2px -10px' : '0 0 -2px -10px' }; border-bottom: 2px solid #666; border-right: 2px solid #666; cursor: se-resize; }
  18222.     .de-win-inpost { float: none; clear: left; display: inline-block; width: auto; padding: 3px; margin: 2px 0; }
  18223.     .de-win-inpost > .de-resizer { display: none; }
  18224.     .de-win-inpost > .de-win-head { background: none; color: inherit; }
  18225.     #de-win-reply { width: auto !important; min-width: 0; padding: 0 !important; border: none !important; }
  18226.     #de-win-reply.de-win { position: fixed !important; padding: 0 !important; margin: 0 !important; border-radius: 10px 10px 0 0; }
  18227.     #de-win-reply.de-win > .de-win-body { padding: 2px 2px 0 1px; border: 1px solid gray; }
  18228.     #de-win-reply.de-win .de-textarea { min-width: 98% !important; resize: none !important; }
  18229.     #de-win-reply.de-win #de-resizer-text { display: none !important; }
  18230.     #de-sagebtn { display: inline-block; margin: 3px 4px 0 4px !important; cursor: pointer; }
  18231.     .de-textarea { display: inline-block; padding: 3px !important; min-width: 275px !important; min-height: 90px !important; resize: both; transition: none !important; }
  18232.  
  18233.     /* Thread navigation */
  18234.     #de-thr-navarrow { display: none; position: absolute; top: 50%; left: 34px; transform: translateY(-50%); width: 7px; height: 7px; }
  18235.     #de-thr-navpanel { color: #F5F5F5; height: 98px; width: 41px; position: fixed; top: 50%; left: 0px; padding: 0; margin: -49px 0 0; background: #777; border: 1px solid #525252; border-left: none; border-radius: 0 5px 5px 0; cursor: pointer; z-index: 1000; }
  18236.     .de-thr-navpanel-hidden { opacity: .7; margin-left: -34px !important; }
  18237.     .de-thr-navpanel-hidden > #de-thr-navarrow { display: initial; }
  18238.     #de-thr-navup { padding: 12px 9px 13px 8px; border-radius: 0 5px 0 0; }
  18239.     #de-thr-navdown { padding: 13px 9px 12px 8px; border-radius: 0 0 5px 0; }
  18240.     #de-thr-navup, #de-thr-navdown { width: 41px; height: 49px; -moz-box-sizing: border-box; box-sizing: border-box; }
  18241.     :not(.de-thr-navpanel-hidden) > #de-thr-navup:hover, :not(.de-thr-navpanel-hidden) > #de-thr-navdown:hover { background: #555; }
  18242.  
  18243.     /* Other */
  18244.     .de-abtn { text-decoration: none !important; outline: none; }
  18245.     .de-button { flex: none; padding: 0 ${ nav.isFirefox ? 2 : 4 }px !important; margin: 1px 2px; height: 24px; font: 13px arial; }
  18246.     .de-editor { display: block; font: 12px courier new; width: 619px; height: 337px; tab-size: 4; -moz-tab-size: 4; -o-tab-size: 4; }
  18247.     .de-hidden { float: left; overflow: hidden !important; margin: 0 !important; padding: 0 !important; border: none !important; width: 0 !important; height: 0 !important; display: inline !important; }
  18248.     .de-input-key { padding: 0 2px !important; margin: 0 !important; font: 13px/15px arial !important; }
  18249.     input[type="text"].de-input-selected { background: rgba(255,255,150,0.4) !important }
  18250.     .de-link-backref { text-decoration: none; }
  18251.     .de-link-parent { outline: 1px dotted !important; }
  18252.     .de-link-pview { font-weight: bold; }
  18253.     .de-list { padding-top: 4px; }
  18254.     .de-list::before { content: "\u25CF"; margin-right: 4px; }
  18255.     .de-logo { display: inline-block; margin-right: 10px; fill: inherit; color: #F5F5F5; border-radius: 80px 0 0 0; }
  18256.     .de-logo > svg { width: 144px; height: 144px; }
  18257.     .de-menu { padding: 0 !important; margin: 0 !important; width: auto !important; min-width: 0 !important; z-index: 10002; border: 1px solid grey !important; text-align: left; }
  18258.     .de-menu-item { display: block; padding: 3px 10px; color: inherit; text-decoration: none; font: 13px arial; white-space: nowrap; cursor: pointer; }
  18259.     .de-menu-item:hover { background-color: #222; color: #fff; }
  18260.     .de-omitted { color: grey; }
  18261.     .de-omitted::before { content: "${ Lng.postsOmitted[lang] }"; }
  18262.     .de-popup { overflow: visible !important; clear: both !important; width: auto !important; min-width: 0pt !important; padding: 8px !important; margin: 1px !important; border: 1px solid grey !important; display: block !important; float: right !important; max-width: initial !important; }
  18263.     .de-popup-btn { display: inline-block; vertical-align: top; color: green; cursor: pointer; line-height: 1.15; }
  18264.     .de-popup-msg { display: inline-block; white-space: pre-wrap; }
  18265.     .de-popup-msg > hr { margin: 0 !important; }
  18266.     .de-post-hiddencontent { display: none !important; }
  18267.     .de-pview { position: absolute; width: auto; min-width: 0; z-index: 9999; border: 1px solid grey !important; margin: 0 !important; display: block !important; }
  18268.     .de-pview-info { padding: 3px 6px !important; }
  18269.     .de-ref-del::after { content: " (Del)"; }
  18270.     .de-ref-op::after { content: " (OP)"; }
  18271.     .de-refcomma:last-child { display: none; }
  18272.     .de-refmap { margin: 10px 4px 4px 4px; font-size: 75%; font-style: italic; }
  18273.     .de-refmap::before { content: "${ Lng.replies[lang] } "; }
  18274.     .de-replies-hide::after { content: "${ Lng.hidePosts[lang] }"; }
  18275.     .de-replies-show::after { content: "${ Lng.showPosts[lang] }"; }
  18276.     .de-thr-buttons { clear: left; margin-top: 5px; }
  18277.     ${ aib.t ? '.de-thr-buttons > .de-btn-reply { display: none; }' : '' }
  18278.     .de-thr-collapse-link::after { content: "${ Lng.collapseThr[lang] }"; }
  18279.     .de-thr-hid { display: block; padding: 2px; }
  18280.     .de-thr-updater-link::after { content: "${ Lng.getNewPosts[lang] }"; }
  18281.     #de-updater-count::before { content: ": "; }
  18282.     .de-viewed { color: #747488 !important; }
  18283.     .de-wait, .de-fav-wait , .de-fullimg-load { animation: de-wait-anim 1s linear infinite; }
  18284.     .de-wait { margin: 0 2px -3px 0 !important; width: 16px; height: 16px; }
  18285.     #de-wrapper-popup { overflow-x: hidden !important; overflow-y: auto !important; -moz-box-sizing: border-box; box-sizing: border-box; max-height: 100vh; position: fixed; right: 0; top: 0; z-index: 9999; font: 14px arial; cursor: default; }
  18286.     @keyframes de-wait-anim { to { transform: rotate(360deg); } }
  18287.     form > hr { clear: both }`;
  18288.  
  18289.     $css(x).id = 'de-css';
  18290.     $css('').id = 'de-css-dynamic';
  18291.     $css('').id = 'de-css-user';
  18292.     updateCSS();
  18293. }
  18294.  
  18295. function updateCSS() {
  18296.     const x = `
  18297.     .de-video-obj { width: ${ Cfg.YTubeWidth }px; height: ${ Cfg.YTubeHeigh }px; }
  18298.     .de-new-post { ${ nav.isPresto ?
  18299.         'border-left: 4px solid rgba(107,134,97,.7); border-right: 4px solid rgba(107,134,97,.7)' :
  18300.         'box-shadow: 6px 0 2px -2px rgba(107,134,97,.8), -6px 0 2px -2px rgba(107,134,97,.8)' } !important; }
  18301.     .de-selected, .de-input-error { ${ nav.isPresto ?
  18302.         'border-left: 4px solid rgba(220,0,0,.7); border-right: 4px solid rgba(220,0,0,.7)' :
  18303.         'box-shadow: 6px 0 2px -2px rgba(220,0,0,.8), -6px 0 2px -2px rgba(220,0,0,.8)' } !important; }
  18304.     ${ Cfg.markMyPosts ?
  18305.         `.de-mypost { ${ nav.isPresto ?
  18306.             'border-left: 4px solid rgba(97,107,134,.7); border-right: 4px solid rgba(97,107,134,.7)' :
  18307.             'box-shadow: 6px 0 2px -2px rgba(97,107,134,.8), -6px 0 2px -2px rgba(97,107,134,.8)'
  18308.         } !important; }
  18309.         .de-mypost-reply:not(.de-pview) { position: relative; }
  18310.         .de-mypost-reply::before { content: ""; position: absolute; top: -0; bottom: 0; left: -1px; border-left: 5px dotted rgba(97,107,134,.8) !important; }` : '' }
  18311.     ${ Cfg.markMyLinks ?
  18312.         `.de-ref-del.de-ref-you::after { content: " (Del)(You)"; }
  18313.             .de-ref-op.de-ref-you::after { content: " (OP)(You)"; }
  18314.             .de-ref-you::after { content: " (You)"; }` :
  18315.         '.de-post-counter-you { display: none; }' }
  18316.     ${ Cfg.postBtnsCSS === 0 ?
  18317.         `.de-btn-expthr, .de-btn-fav, .de-btn-hide, .de-btn-reply, .de-btn-src, .de-btn-stick, .de-btn-unhide { fill: rgba(0,0,0,0); color: currentColor; }
  18318.             .de-btn-fav-sel, .de-btn-hide-user, .de-btn-sage, .de-btn-stick-on, .de-btn-unhide-user { fill: rgba(0,0,0,0); color: #F00; }` :
  18319.         `.de-btn-expthr, .de-btn-fav, .de-btn-hide, .de-btn-reply, .de-btn-sage, .de-btn-src, .de-btn-stick, .de-btn-unhide { color: #F5F5F5; }
  18320.             .de-btn-expthr, .de-btn-fav, .de-btn-fav-sel, .de-btn-hide, .de-btn-hide-user, .de-btn-reply, .de-btn-src, .de-btn-stick, .de-btn-stick-on, .de-btn-unhide, .de-btn-unhide-user { fill: ${ Cfg.postBtnsCSS === 1 && !nav.isPresto ? 'url(#de-btn-back-gradient)' : Cfg.postBtnsBack }; }
  18321.             .de-btn-fav-sel { color: #FFE100; }
  18322.             .de-btn-hide-user { color: #BFFFBF; }
  18323.             .de-btn-sage { fill: #4B4B4B; }
  18324.             .de-btn-stick-on { color: #BFFFBF; }
  18325.             .de-btn-unhide-user { color: #FFBFBF; }` }
  18326.     .de-fullimg-wrap-inpost > .de-fullimg { ${ Cfg.resizeImgs ?
  18327.         `max-width: 100%;${ Cfg.resizeImgs === 2 ? ' max-height: 96vh' : '' }` :
  18328.         'width: auto' }; }
  18329.     ${ Cfg.maskImgs ?
  18330.         `${ aib.qPostImg }, .de-img-embed, .de-video-obj { opacity: ${ Cfg.maskVisib / 100 } !important; }
  18331.             ${ aib.qPostImg.split(', ').join(':hover, ') }:hover, .de-img-embed:hover, .de-video-obj:hover { opacity: 1 !important; }
  18332.             .de-video-obj:not(.de-video-obj-inline) { clear: both; }` : '' }
  18333.     ${ Cfg.imgNames === 1 ? '.de-img-name { max-width: 165px; overflow: hidden; }' : '' }
  18334.     ${ Cfg.imgNames === 1 || Cfg.imgNames === 3 ?
  18335.         '.de-img-name { display: inline-block; white-space: nowrap; vertical-align: bottom; text-overflow: ellipsis; }' :
  18336.         Cfg.imgNames === 2 ? '.de-img-name { text-decoration: none !important; text-transform: capitalize; }' : '' }
  18337.     ${ Cfg.widePosts ? '.de-reply { float: none; width: 99.9%; margin-left: 0; }' : '' }
  18338.     ${ aib.qPostMsg } { max-width: ${ Cfg.limitPostMsg }px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; }
  18339.     ${ Cfg.strikeHidd ? '.de-link-hid { text-decoration: line-through !important; }' : '' }
  18340.     ${ Cfg.noSpoilers === 1 ?
  18341.         `.spoiler, s { color: #F5F5F5 !important; background-color: #888 !important; }
  18342.             .spoiler > a, s > a:not(:hover) { color: #F5F5F5 !important; background-color: #888 !important; }` : '' }
  18343.     ${ Cfg.noSpoilers === 2 ?
  18344.         `.spoiler, s { color: inherit !important; }
  18345.             .spoiler > a, s > a:not(:hover) { color: inherit !important; }` : '' }
  18346.     ${ Cfg.addSageBtn ? '' : '#de-sagebtn, ' }
  18347.     ${ Cfg.delHiddPost === 1 || Cfg.delHiddPost === 3 ?
  18348.         '.de-thr-hid, .de-thr-hid + div + br, .de-thr-hid + div + hr, .de-thr-hid + div + br + hr, .de-thr-hid + div + div + hr, ' :
  18349.         '.de-thr-hid:not([style="display: none;"]) + div + br, ' }
  18350.     ${ Cfg.imgNavBtns ? '' : '.de-img-btn, ' }
  18351.     ${ Cfg.imgInfoLink ? '' : '.de-fullimg-info, ' }
  18352.     ${ Cfg.noPostNames ? `${ aib.qPostName }, ${ aib.qPostTrip }, ` : '' }
  18353.     ${ Cfg.noBoardRule ? `${ aib.qFormRules }, ` : '' }
  18354.     ${ Cfg.panelCounter ? '' : '#de-panel-info, ' }
  18355.     ${ Cfg.removeHidd ? '.de-link-backref.de-link-hid, .de-link-backref.de-link-hid + .de-refcomma, ' : '' }
  18356.     ${ Cfg.showHideBtn ? '' : '.de-btn-hide, ' }
  18357.     ${ Cfg.showRepBtn ? '' : '.de-btn-reply, ' }
  18358.     ${ Cfg.thrBtns || aib.t ? '' : '.de-thr-updater, ' }
  18359.     ${ Cfg.thrBtns === 1 || Cfg.thrBtns === 2 && !aib.t ? '' : '.de-thr-buttons > svg, ' }
  18360.     ${ Cfg.ajaxPosting ? '' : '.de-file-btn-rar, .de-file-btn-txt, ' }
  18361.     ${ Cfg.fileInputs ? '' : '.de-file-txt-wrap, .de-file-btn-txt, ' }
  18362.     ${ !aib.kusaba && (aib.multiFile || Cfg.fileInputs !== 2) ?
  18363.         '#de-pform form > table > tbody > tr > td:not([colspan]):first-child, #de-pform form > table > tbody > tr > th:first-child, ' : '' }body > hr, .postarea, .theader { display: none !important; }\r\n`;
  18364.     $id('de-css-dynamic').textContent = (x + aib.css).replace(/[\r\n\t]+/g, '\r\n\t');
  18365.     $id('de-css-user').textContent = Cfg.userCSS ? Cfg.userCSSTxt : '';
  18366. }
  18367.  
  18368. /* eslint-enable max-len */
  18369.  
  18370. /* ==[ Main.js ]==============================================================================================
  18371.                                                      MAIN
  18372. =========================================================================================================== */
  18373.  
  18374. function runFrames() {
  18375.     let inf;
  18376.     if(typeof GM !== 'undefined') {
  18377.         inf = GM.info;
  18378.     } else {
  18379.         if(typeof GM_info === 'undefined') {
  18380.             return;
  18381.         }
  18382.         inf = GM_info;
  18383.     }
  18384.     if(!inf || inf.scriptHandler !== 'Greasemonkey' || !deWindow.frames[0]) {
  18385.         return;
  18386.     }
  18387.     const deMainFuncFrame = frameEl => {
  18388.         const fDoc = frameEl.contentDocument;
  18389.         if(fDoc) {
  18390.             const deWindow = fDoc.defaultView;
  18391.             deMainFuncInner(
  18392.                 deWindow,
  18393.                 deWindow.opera && deWindow.opera.scriptStorage,
  18394.                 deWindow.FormData,
  18395.                 (x, y) => deWindow.scrollTo(x, y),
  18396.                 typeof localData === 'object' ? localData : null
  18397.             );
  18398.         }
  18399.     };
  18400.     for(let i = 0, len = deWindow.length; i < len; ++i) {
  18401.         const frameEl = deWindow.frames[i].frameElement;
  18402.         const fDoc = frameEl.contentDocument;
  18403.         if(fDoc) {
  18404.             if(String(fDoc.defaultView.location) === 'about:blank') {
  18405.                 frameEl.onload = () => deMainFuncFrame(frameEl);
  18406.             } else if(fDoc.readyState === 'loading') {
  18407.                 fDoc.addEventListener('DOMContentLoaded', () => deMainFuncFrame(frameEl));
  18408.             } else {
  18409.                 deMainFuncFrame(frameEl);
  18410.             }
  18411.         }
  18412.     }
  18413. }
  18414.  
  18415. async function runMain(checkDomains, dataPromise) {
  18416.     Logger.initLogger();
  18417.     if(!(docBody = doc.body) || !aib && !(aib = getImageBoard(checkDomains, true))) {
  18418.         return;
  18419.     }
  18420.     let formEl = $q(aib.qDForm + ', form[de-form]');
  18421.     if(!formEl) {
  18422.         runFrames();
  18423.         return;
  18424.     }
  18425.     if(docBody.classList.contains('de-runned') ||
  18426.         aib.observeContent && !aib.observeContent(checkDomains, dataPromise)
  18427.     ) {
  18428.         return;
  18429.     }
  18430.     Logger.log('Imageboard check');
  18431.     if(!locStorage) {
  18432.         if(!checkStorage()) {
  18433.             return;
  18434.         }
  18435.         initNavFuncs();
  18436.     }
  18437.     const [favObj] = await (dataPromise || readData());
  18438.     if(!Cfg.disabled && aib.init && aib.init() || !localData && docBody.classList.contains('de-mode-local')) {
  18439.         return;
  18440.     }
  18441.     docBody.classList.add('de-runned');
  18442.     Logger.log('Storage loading');
  18443.     addSVGIcons();
  18444.     if(Cfg.disabled) {
  18445.         Panel.initPanel(formEl);
  18446.         scriptCSS();
  18447.         return;
  18448.     }
  18449.     if('toJSON' in aProto) {
  18450.         delete aProto.toJSON;
  18451.     }
  18452.     initStorageEvent();
  18453.     DollchanAPI.initAPI();
  18454.     if(localData) {
  18455.         aib.prot = 'http:';
  18456.         aib.host = aib.dm;
  18457.         aib.b = localData.b;
  18458.         aib.t = localData.t;
  18459.         aib.docExt = '.html';
  18460.     } else {
  18461.         aib.parseURL();
  18462.     }
  18463.     if(aib.t || !Cfg.scrollToTop) {
  18464.         doc.defaultView.addEventListener('beforeunload', () => {
  18465.             sesStorage['de-scroll-' + aib.b + (aib.t || '')] = deWindow.pageYOffset;
  18466.         });
  18467.     }
  18468.     Logger.log('Init');
  18469.     if(Cfg.correctTime) {
  18470.         dTime = new DateTime(Cfg.timePattern, Cfg.timeRPattern, Cfg.timeOffset, lang,
  18471.             rp => saveCfg('timeRPattern', rp));
  18472.         Logger.log('Time correction');
  18473.     }
  18474.     MyPosts.readStorage();
  18475.     Logger.log('Read my posts');
  18476.     $hide(docBody);
  18477.     dummy = doc.createElement('div');
  18478.     formEl = aib.fixHTML(formEl, true);
  18479.     Logger.log('Replace delform');
  18480.     pByEl = new Map();
  18481.     pByNum = new Map();
  18482.     try {
  18483.         DelForm.last = DelForm.first = new DelForm(formEl, aib.page, null);
  18484.         if(!Thread.first) {
  18485.             console.error('No threads detected!');
  18486.         }
  18487.     } catch(err) {
  18488.         console.error('Delform parsing error:', getErrorMessage(err));
  18489.         $show(docBody);
  18490.         return;
  18491.     }
  18492.     Logger.log('Parse delform');
  18493.     const storageName = `de-lastpcount-${ aib.b }-${ aib.t }`;
  18494.     if(aib.t && !!sesStorage[storageName] && (sesStorage[storageName] > Thread.first.pcount)) {
  18495.         sesStorage.removeItem(storageName);
  18496.         deWindow.location.reload();
  18497.     }
  18498.     pr = new PostForm($q(aib.qForm));
  18499.     Logger.log('Parse postform');
  18500.     if(Cfg.hotKeys) {
  18501.         HotKeys.enableHotKeys();
  18502.         Logger.log('Init keybinds');
  18503.     }
  18504.     initPage();
  18505.     Logger.log('Init page');
  18506.     Panel.initPanel(formEl);
  18507.     Logger.log('Add panel');
  18508.     embedPostMsgImages(DelForm.first.el);
  18509.     Logger.log('Image-links');
  18510.     DelForm.first.addStuff();
  18511.     readViewedPosts();
  18512.     scriptCSS();
  18513.     Logger.log('Apply CSS');
  18514.     $show(docBody);
  18515.     Logger.log('Display page');
  18516.     toggleInfinityScroll();
  18517.     Logger.log('Infinity scroll');
  18518.     const { firstThr } = DelForm.first;
  18519.     if(firstThr) {
  18520.         readPostsData(firstThr.op, favObj);
  18521.     }
  18522.     Logger.log('Hide posts');
  18523.     scrollPage();
  18524.     Logger.log('Scroll page');
  18525.     if(localData) {
  18526.         $each($Q('.de-post-removed'), el => {
  18527.             const post = pByEl.get(el);
  18528.             if(post) {
  18529.                 post.deletePost(false);
  18530.             }
  18531.         });
  18532.         Logger.log('Local changings');
  18533.     }
  18534.     Logger.finish();
  18535. }
  18536.  
  18537. function initMain() {
  18538.     if(doc.readyState !== 'loading') {
  18539.         needScroll = false;
  18540.         runMain(true, null);
  18541.         return;
  18542.     }
  18543.     let dataPromise = null;
  18544.     if((aib = getImageBoard(true, false))) {
  18545.         if(!checkStorage()) {
  18546.             return;
  18547.         }
  18548.         initNavFuncs();
  18549.         dataPromise = readData();
  18550.     }
  18551.     needScroll = true;
  18552.     doc.addEventListener('onwheel' in doc.defaultView ? 'wheel' : 'mousewheel', function wFunc(e) {
  18553.         needScroll = false;
  18554.         doc.removeEventListener(e.type, wFunc);
  18555.     });
  18556.     doc.addEventListener('DOMContentLoaded', () => runMain(false, dataPromise));
  18557. }
  18558.  
  18559. initMain();
  18560.  
  18561. /* ==[ Tail ]== */
  18562. }(
  18563.     window,
  18564.     window.opera && window.opera.scriptStorage,
  18565.     window.FormData,
  18566.     (x, y) => window.scrollTo(x, y),
  18567.     /* global localData */ typeof localData === 'object' ? localData : null
  18568. ));
Add Comment
Please, Sign In to add comment