// ==UserScript==
// @name Dollchan Extension Tools
// @version 23.9.19.0
// @namespace http://www.freedollchan.org/scripts/*
// @author Sthephan Shinkufag @ FreeDollChan
// @copyright © Dollchan Extension Team. See the LICENSE file for license rights and limitations (MIT).
// @description Doing some profit for imageboards
// @icon https://raw.github.com/SthephanShinkufag/Dollchan-Extension-Tools/master/Icon.png
// @updateURL https://raw.github.com/SthephanShinkufag/Dollchan-Extension-Tools/master/Dollchan_Extension_Tools.meta.js
// @nocompat Chrome
// @run-at document-start
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.xmlHttpRequest
// @grant unsafeWindow
// @include *
// ==/UserScript==
/* eslint indent: ["error", "tab", { "flatTernaryExpressions": true, "outerIIFEBody": 0 }] */
(function deMainFuncInner(deWindow, prestoStorage, FormData, scrollTo, localData) {
'use strict';
const version = '23.9.19.0';
const commit = '335b491';
/* ==[ GlobalVars.js ]== */
const doc = deWindow.document;
const gitWiki = 'https://github.com/SthephanShinkufag/Dollchan-Extension-Tools/wiki/';
const gitRaw = 'https://raw.githubusercontent.com/SthephanShinkufag/Dollchan-Extension-Tools/master/';
let aib, Cfg, dTime, dummy, isExpImg, isPreImg, lang, locStorage, nav, needScroll, pByEl, pByNum, postform,
sesStorage, updater;
let topWinZ = 10;
/* ==[ DefaultCfg.js ]========================================================================================
DEFAULT CONFIG
=========================================================================================================== */
const defaultCfg = {
disabled : 0, // Dollchan enabled by default
language : 0, // Dollchan language [0=ru, 1=en, 2=ua]
// FILTERS
hideBySpell : 1, // hide posts by spells
spells : null, // user defined spells
sortSpells : 0, // sort spells and remove duplicates
hideRefPsts : 0, // hide replies to hidden posts
nextPageThr : 0, // load threads from next pages instead of hidden
delHiddPost : 0, // remove placeholders [0=off, 1=all, 2=posts only, 3=threads only]
// POSTS
ajaxUpdThr : 1, // threads updater
updThrDelay : 20, // update interval (sec)
updCount : 1, // show countdown to thread update
favIcoBlink : 1, // blink the favicon on new posts
desktNotif : 0, // desktop notifications for new posts
markNewPosts : 1, // highlight new posts with color
markMyPosts : 1, // highlight my own posts
expandTrunc : 0, // auto-expand truncated posts
widePosts : 0, // stretch posts to screen width
limitPostMsg : 2000, // limit text width in posts nessages
showHideBtn : 1, // show "Hide" buttons [0=off, 1=with menu, 2=no menu]
showRepBtn : 1, // show "Quick reply" buttons [0=off, 1=with menu, 2=no menu]
postBtnsCSS : 2, // post buttons style [0=simple, 1=gradient grey, 2=custom]
postBtnsBack : '#8c8c8c', // custom background color
thrBtns : 1, // buttons under threads [0=off, 1=all, 2=all (on board), 3='New posts' on board]
noSpoilers : 0, // text spoilers expansion [0=off, 1=grey, 2=native]
noPostNames : 0, // hide poster names
correctTime : 0, // time correction in posts
timeOffset : '+0', // time offset (h)
timePattern : '', // search pattern
timeRPattern : '', // replace pattern
// IMAGES
expandImgs : 2, // expand images on click [0=off, 1=in post, 2=by center]
imgNavBtns : 1, // add buttons to navigate images
imgInfoLink : 1, // show name under expanded image
resizeDPI : 0, // donʼt upscale images on high DPI displays
resizeImgs : 1, // resize large images to fit screen [0=off', '1=by width', '2=width+height]
minImgSize : 100, // minimal size for expanded images (px)
maxImgSize : 2000, // maximum size for expanded images (px)
zoomFactor : 20, // images zoom sensibility [1-100%]
webmControl : 1, // show control bar for WebM
webmTitles : 1, // load titles from WebM metadata
webmVolume : 100, // default volume for WebM [0-100%]
minWebmWidth : 320, // minimal width for WebM (px)
preLoadImgs : 0, // preload images [0=off, 1=all, 2=non-WebM]
findImgFile : 0, // detect embedded files in images
openImgs : 0, // replace thumbs with original images [0=off, 1=all, 2=GIFs only, 3=non-GIFs]
imgSrcBtns : 1, // add "Search" buttons for images
imgNames : 0, // image names in links [0=off, 1=original, 2=hide]
maskImgs : 0, // NSFW mode
maskVisib : 7, // image opacity in NSFW mode [0-100%]
// LINKS
linksNavig : 1, // posts navigation by >>links
linksOver : 100, // delay appearance (ms)
linksOut : 1500, // delay disappearance (ms)
markViewed : 0, // mark viewed posts
strikeHidd : 0, // strike >>links to hidden posts
removeHidd : 0, // also remove from reply maps
noNavigHidd : 0, // donʼt show previews for hidden posts
markMyLinks : 1, // mark links to my posts with (You)
crossLinks : 1, // replace http:// with >>/b/links*
decodeLinks : 1, // decode %D0%A5%D1 in links
insertNum : 1, // insert >>link on №postnumber click*
addOPLink : 1, // insert >>link when replying to OP on board
addImgs : 0, // load images to jpg/png/gif links*
addMP3 : 1, // embed mp3 links
addVocaroo : 1, // embed Vocaroo links
embedYTube : 1, // embed YouTube links [0=off, 1=preview+player, 2=onclick]
YTubeWidth : 360, // player width (px)
YTubeHeigh : 270, // player height (px)
YTubeTitles : 1, // load titles for YouTube links
ytApiKey : '', // YouTube API key
addVimeo : 1, // embed Vimeo links
// POSTFORM
ajaxPosting : 1, // posting without refresh
postSameImg : 1, // ability to post duplicate images
removeEXIF : 0, // remove EXIF from JPEG
removeFName : 0, // clear file names [0=off, 1=empty, 2=unixtime, 3=unixtime-random]
sendErrNotif : 1, // inform in title about post send error
scrAfterRep : 0, // scroll to bottom after reply
fileInputs : 2, // enhanced file attachment field [0=off, 1=simple, 2=preview]
addPostForm : 2, // reply form display in thread [0=at top, 1=at bottom, 2=hidden]
spacedQuote : 1, // insert a space when quoting "> "
favOnReply : 1, // add thread to Favorites after reply
warnSubjTrip : 0, // warn about a tripcode in "Subject" field
addSageBtn : 1, // replace "Email" with Sage button
saveSage : 1, // remember sage
sageReply : 0, // reply with sage
altCaptcha : 0, // use alternative captcha (if available)
capUpdTime : 300, // captcha update interval (sec)
captchaLang : 1, // forced captcha input language [0=off, 1=en, 2=ru]
addTextBtns : 1, // text markup buttons [0=off, 1=graphics, 2=text, 3=usual]
txtBtnsLoc : 1, // located at [0=top, 1=bottom]
userPassw : 1, // user password
passwValue : '', // value
userName : 0, // user name
nameValue : '', // value
noBoardRule : 0, // hide board rules
noPassword : 1, // hide form "Password" field
noName : 0, // hide form "Name" field
noSubj : 0, // hide form "Subject" field
// COMMON
scriptStyle : 0, /* Dollchan style [
0=gradient darkblue, 1=gradient blue, 2=solid grey, 3=transparent blue, 4=square dark,
5=gradient pink] */
userCSS : 0, // user CSS
userCSSTxt : '', // css text
expandPanel : 0, // show full main panel
animation : 1, // CSS3 animation
hotKeys : 1, // hotkeys
loadPages : 1, // number of pages that are loaded on F5
panelCounter : 1, // panel counter for posts/images [0=off, 1=all posts, 2=except hidden]
rePageTitle : 1, // show thread title in the page tab
inftyScroll : 1, // infinite scrolling for pages
hideReplies : 0, // show only op-posts in threads list
scrollToTop : 0, // always scroll to top in the threads list
saveScroll : 1, // remember the scroll position in threads
favFolders : 1, // boards folders in the Favorites Window
favThrOrder : 0, /* threads sorting order in the Favorites window
[0=by opnum, 1=by opnum (desc), 2=by adding, 3=by adding (desc)] */
favWinOn : 0, // always open the Favorites window
closePopups : 0, // close popups automatically
updDollchan : 2, // Check for Dollchan updates [0=off, 1=per day, 2=2days, 3=week, 4=2weeks, 5=month]
// WINDOWS
textaWidth : 300, // textarea width (px)
textaHeight : 115, // textarea height (px)
replyWinDrag : 0, // draggable "Quick Reply" form
replyWinX : 'right: 0', // "Quick Reply" form X position
replyWinY : 'top: 0', // "Quick Reply" form Y position
cfgTab : 'filters', // remembered tab in "Settings" window
cfgWinDrag : 0, // draggable "Settings" window
cfgWinX : 'right: 0', // "Settings" window X position
cfgWinY : 'top: 0', // "Settings" window Y position
hidWinDrag : 0, // draggable "Hidden" window
hidWinX : 'right: 0', // "Hidden" window X position
hidWinY : 'top: 0', // "Hidden" window Y position
favWinDrag : 0, // draggable "Favorites" window
favWinX : 'right: 0', // "Favorites" window X position
favWinY : 'top: 0', // "Favorites" window Y position
favWinWidth : 500, // "Favorites" window width (px)
vidWinDrag : 0, // draggable "Video" window
vidWinX : 'right: 0', // "Video" window X position
vidWinY : 'top: 0' // "Video" window Y position
};
/* ==[ Localization.js ]======================================================================================
LOCALIZATION
=========================================================================================================== */
const Lng = {
// Settings window: tooltips
cfgNeedReload: [
'Для применения необходима перезагрузка',
'Reboot required to apply',
'Для застосування необхідне перезавантаження'],
// Settings window: tab names
cfgTab: {
filters : ['Фильтры', 'Filters', 'Фільтри'],
posts : ['Посты', 'Posts', 'Дописи'],
images : ['Картинки', 'Images', 'Зображ.'],
links : ['Ссылки', 'Links', 'Посил.'],
form : ['Форма', 'Form', 'Форма'],
common : ['Общее', 'Common', 'Спільне'],
info : ['Инфо', 'Info', 'Інфо']
},
// Settings window: options
cfg: {
language: {
sel : [['Ru', 'En', 'Ua'], ['Ru', 'En', 'Ua'], ['Ru', 'En', 'Ua']],
txt : ['', '', '']
},
// "Filters" tab
hideBySpell: [
'Спеллы: ',
'Magic spells: ',
'Спелли: '],
sortSpells: [
'Сортировать спеллы и удалять дубликаты',
'Sort spells and remove duplicates',
'Сортувати спелли та видаляти дублікати'],
hideRefPsts: [
'Скрывать ответы на скрытые посты',
'Hide replies to hidden posts',
'Ховати відповіді на сховані дописи'],
nextPageThr: [
'Скрытые треды - загружать со следующих страниц',
'Load threads from next pages instead of hidden',
'Сховані треди - брати з наступних сторінок'],
delHiddPost: {
sel: [
['Откл.', 'Всё', 'Только посты', 'Только треды'],
['Disable', 'All', 'Posts only', 'Threads only'],
['Вимк.', 'Все', 'Лише дописи', 'Лише треди']],
txt: [
'Удалять скрытое',
'Remove placeholders',
'Видаляти сховане']
},
// "Posts" tab
ajaxUpdThr: [
'Апдейтер тредов ',
'Threads updater ',
'Оновлювач тредів '],
updThrDelay: [
'(сек)',
'(sec)',
'(сек)'],
updCount: [
'Обратный счетчик обновления треда',
'Show countdown to thread update',
'Зворотній відлік оновлення треду'],
favIcoBlink: [
'Мигать фавиконом при появлении новых постов',
'Blink the favicon on new posts',
'Блимати фавіконом в разі появи нових дописів'],
desktNotif: [
'Уведомлять о новых постах на рабочем столе',
'Desktop notifications for new posts',
'Повідомляти про нові дописи на стільниці'],
markNewPosts: [
'Выделять цветом новые посты',
'Highlight new posts with color',
'Виділяти кольором нові дописи'],
markMyPosts: [
'Выделять цветом мои посты',
'Highlight my own posts',
'Виділяти кольором мої дописи'],
expandTrunc: [
'Авторазворот сокращенных постов',
'Autoexpand truncated posts',
'Авторозгортання скорочених дописів'],
widePosts: [
'Растягивать посты по ширине экрана',
'Stretch posts to page width',
'Розтягувати дописи на ширину екрану'],
limitPostMsg: [
'Ограничение ширины текста в постах (px)',
'Limit text width in posts messages (px)',
'Обмеження ширини тексту в дописах (px)'
],
thrBtns: {
sel: [
['Откл.', 'Все', 'Все (на доске)', '"Новые посты" на доске'],
['Disable', 'All', 'All (on board)', '"New posts" on board'],
['Вимк.', 'Всі', 'Всі (на дошці)', '"Нові дописи" на дошці']],
txt: [
'Кнопки под тредами',
'Buttons under threads',
'Кнопки під тредами']
},
showHideBtn: {
sel: [
['Откл.', 'С меню', 'Без меню'],
['Disable', 'With menu', 'No menu'],
['Вимк.', 'Із меню', 'Без меню']],
txt: [
'Кнопки "Скрыть пост/тред"',
'"Hide post/thread" buttons',
'Кнопки "Сховати допис/тред"']
},
showRepBtn: {
sel: [
['Откл.', 'С меню', 'Без меню'],
['Disable', 'With menu', 'No menu'],
['Вимк.', 'Із меню', 'Без меню']],
txt: [
'Кнопки "Ответить на пост/тред"',
'"Reply to post/thread" buttons',
'Кнопки "Відповісти на допис/тред"']
},
postBtnsCSS: {
sel: [
['Упрощенные', 'Серый градиент', 'Настраиваемые'],
['Simple', 'Gradient grey', 'Custom'],
['Спрощені', 'Сірий градієнт', 'Користувацькі']],
txt: [
'Кнопки постов ',
'Post buttons ',
'Кнопки дописів ']
},
noSpoilers: {
sel: [
['Откл.', 'Серое', 'Родное'],
['Disable', 'Grey', 'Native'],
['Вимк.', 'Сіре', 'Рідне']],
txt: [
'Раскрытие текстовых спойлеров',
'Text spoilers expansion',
'Розкриття текстових спойлерів']
},
noPostNames: [
'Скрывать имена в постах',
'Hide poster names',
'Ховати імена в дописах'],
correctTime: [
'Коррекция времени в постах',
'Time correction in posts',
'Корекція часу в дописах'],
timeOffset: [
'разница (ч) ',
'time offset (h) ',
'різниця (год) '],
timePattern: [
'Шаблон поиска',
'Search pattern',
'Шаблон пошуку'],
timeRPattern: [
'Шаблон замены',
'Replace pattern',
'Шаблон заміни'],
// "Images" tab
expandImgs: {
sel: [
['Откл.', 'В посте', 'По центру'],
['Disable', 'In post', 'By center'],
['Вимк.', 'В дописі', 'По центру']],
txt: [
'Раскрывать картинки по клику',
'Expand images on click',
'Розгортати зображення по кліку']
},
imgNavBtns: [
'Добавлять кнопки навигации по картинкам',
'Add buttons to navigate images',
'Додавати кнопки навігації по зображеннях'],
imgInfoLink: [
'Имя файла под раскрытой картинкой',
'Show file name under expanded image',
'Імʼя файлу під розкритим зображенням'],
resizeDPI: [
'Не растягивать на дисплеях с высоким DPI',
'Donʼt upscale images on high DPI displays',
'Не розтягувати на дисплеях з високим DPI'],
resizeImgs: {
sel: [
['Откл.', 'По ширине', 'Шир.+выс.'],
['Disable', 'By width', 'Width+Height'],
['Вимк.', 'По ширині', 'Шир.+выс.']],
txt: [
'Уменьшать при раскрытии в посте',
'Fit to screen for expanding in post',
'Зменшувати при розкритті в дописі']
},
minImgSize: [
'мин.',
'min',
'мін.'],
maxImgSize: [
'макс. размер раскрытия (px)',
'max expansion size (px)',
'макс. розмір розгортання (px)'],
zoomFactor: [
'Чувствительность зума картинок [1-100%]',
'Images zoom sensibility [1-100%]',
'Чутливість зуму зображень [1-100%]'],
webmControl: [
'Показывать контрол-бар для WebM',
'Show control bar for WebM',
'Показувати смугу керування для WebM'],
webmTitles: [
'Получать названия WebM из метаданных',
'Load titles from WebM metadata',
'Отримувати назви WebM з метаданих'],
webmVolume: [
'Громкость WebM по умолчанию [0-100%]',
'Default volume for WebM [0-100%]',
'Гучність WebM по замовчуванню [0-100%]'],
minWebmWidth: [
'Минимальная ширина WebM (px)',
'Minimal width for WebM (px)',
'Мінімальна ширина WebM (px)'],
preLoadImgs: {
sel: [
['Откл.', 'Все', 'Без WebM'],
['Disable', 'All', 'Non-WebM'],
['Вимк.', 'Всі', 'Крім WebM']],
txt: [
'Предварительно загружать картинки',
'Preload images',
'Наперед завантажувати зображення']
},
findImgFile: [
'Распознавать файлы, встроенные в картинках',
'Detect embedded files in images',
'Розпізнавати файли, що вбудовані в зображення'],
openImgs: {
sel: [
['Откл.', 'Все подряд', 'Только GIF', 'Кроме GIF'],
['Disable', 'All types', 'Only GIF', 'Non-GIF'],
['Вимк.', 'Всі', 'Лише GIF', 'Крім GIF']],
txt: [
'Заменять тамбнейлы на оригиналы',
'Replace thumbnails with original images',
'Замінювати зображення на оригінали']
},
imgSrcBtns: [
'Добавлять кнопки "Поиск" для картинок',
'Add "Search" buttons for images',
'Додавати кнопки "Пошук" для зображень'],
imgNames: {
sel: [
['Не изменять', 'Настоящие (сокр.)', 'Скрывать', 'Настоящие (полные)'],
['Donʼt change', 'Original (trunc.)', 'Hide', 'Original (full)'],
['Не змінювати', 'Справжні (скороч.)', 'Ховати', 'Справжні (повні)']],
txt: [
'имена картинок',
'filenames',
'імена зображень']
},
maskVisib: [
'Видимость для NSFW-картинок [0-100%]',
'Visibility for NSFW images [0-100%]',
'Видимість для NSFW-зображень [0-100%]'],
// "Links" tab
linksNavig: [
'Навигация постов по >>ссылкам',
'Posts navigation by >>links',
'Навігація дописів по >>посиланнях'],
linksOver: [
'Появление ',
'Appearance ',
'Поява '],
linksOut: [
'Пропадание (мс)',
'Disappearance (ms)',
'Зникнення (мс)'],
markViewed: [
'Помечать просмотренные посты',
'Mark viewed posts',
'Позначати переглянуті дописи'],
strikeHidd: [
'Зачеркивать >>ссылки на скрытые посты',
'Strike >>links to hidden posts',
'Закреслювати >>посилання на сховані дописи'],
removeHidd: [
'Также удалять из обратных >>ссылок',
'Also remove from >>backlinks',
'Також видаляти із зворотніх >>посилань'],
noNavigHidd: [
'Не отображать превью для скрытых постов',
'Donʼt show previews for hidden posts',
'Не показувати превʼю до cхованих дописів'],
markMyLinks: [
'Помечать ссылки на мои посты как (You)',
'Mark links to my posts with (You)',
'Позначати посилання на мої дописи як (You)'],
crossLinks: [
'Заменять http:// на >>/b/ссылки',
'Replace http:// with >>/b/links',
'Замінювати https:// на >>/b/посилання'],
decodeLinks: [
'Декодировать %D0%A5%D1 в ссылках',
'Decode %D0%A5%D1 in links',
'Декодувати %D0%A5%D1 в посиланнях'],
insertNum: [
'Вставлять >>ссылку по клику на №поста',
'Insert >>link on №postnumber click',
'Вставляти >>посилання на клік по №допису'],
addOPLink: [
'>>ссылка при ответе на OP в списке тредов',
'Insert >>link when replying to OP on threads list',
'>>посилання при відповіді на OP у списці тредів'],
addImgs: [
'Загружать картинки к jpg/png/gif ссылкам',
'Load images for jpg/png/gif links',
'Додавати зображення до jpg/png/gif посилань'],
addMP3: [
'Плеер к mp3 ссылкам',
'Player for mp3 links',
'Плеєр до mp3 посилань'],
addVocaroo: [
'к Vocaroo ссылкам',
'for Vocaroo links',
'до Vocaroo посилань'],
addVimeo: [
'Добавлять плеер к Vimeo ссылкам',
'Add player for Vimeo links',
'Додавати плеєр до Vimeo посилань'],
embedYTube: {
sel: [
['Ничего', 'Превью+плеер', 'Плеер по клику'],
['Nothing', 'Preview+player', 'On click player'],
['Нічого', 'Превʼю+плеєр', 'Плеєр по кліку']],
txt: [
'к YouTube ссылкам',
'for YouTube links',
'до YouTube посилань']
},
YTubeTitles: [
'Загружать названия к YouTube ссылкам',
'Load titles for YouTube links',
'Отримувати назви до YouTube посилань'],
ytApiKey: [
'Ключ YT API*',
'YT API Key*',
'Ключ YT API*'],
// "Form" tab
ajaxPosting: [
'Отправка постов без перезагрузки',
'Posting without page refresh',
'Дописування без оновлення сторінки'],
postSameImg: [
'Возможность отправки одинаковых картинок',
'Ability to post duplicate images',
'Можливість надсилання однакових зображень'],
removeEXIF: [
'Удалять EXIF из JPEG ',
'Remove EXIF from JPEG ',
'Видаляти EXIF з JPEG '],
removeFName: {
sel: [
['Не изменять', 'Удалять', 'Unixtime', 'Unixtime-random'],
['Donʼt change', 'Clear', 'Unixtime', 'Unixtime-random'],
['Не змінювати', 'Видаляти', 'Unixtime', 'Unixtime-random']],
txt: [
'имена файлов',
'file names',
'імена файлів']
},
sendErrNotif: [
'Оповещать в заголовке об ошибке отправки',
'Inform in title about post send error',
'Сповіщати в заголовку про помилку надсилання'],
scrAfterRep: [
'Перемещаться в конец треда после отправки',
'Scroll to bottom after reply',
'Гортати в кінець треду після надсилання'],
fileInputs: {
sel: [
['Откл.', 'Упрощ.', 'Превью'],
['Disable', 'Simple', 'Preview'],
['Вимкн.', 'Спрощене', 'Превʼю']],
txt: [
'Улучшенное поле добавления файлов',
'Enhanced file attachment field',
'Покращене поле додавання файлів']
},
addPostForm: {
sel: [
['Сверху', 'Внизу', 'Скрытая'],
['At top', 'At bottom', 'Hidden'],
['Вгорі', 'Знизу', 'Прихована']],
txt: [
'Форма ответа в треде',
'Reply form display in thread',
'Форма відповіді в треді']
},
spacedQuote: [
'Вставлять пробел при цитировании "> "',
'Insert a space when quoting "> "',
'Вставляти пробіл при цитуванні "> "'],
favOnReply: [
'Добавлять тред в Избранное после ответа',
'Add thread to Favorites after reply',
'Додавати тред в Вибране після відповіді'],
warnSubjTrip: [
'Оповещать о трипкоде в поле "Тема"',
'Warn about a tripcode in "Subject" field',
'Сповіщувати про трипкод в полі "Тема"'],
addSageBtn: [
'Кнопка Sage вместо поля "Email" ',
'Replace "Email" with Sage button ',
'Кнопка Sage замість "E-mail" '],
saveSage: [
'Помнить сажу',
'Remember sage',
'Памʼятати сажу'],
altCaptcha: [
'Использовать альтернативную капчу',
'Use alternative captcha',
'Використовувати альтернативну капчу'],
capUpdTime: [
'Интервал обновления капчи (сек)',
'Captcha update interval (sec)',
'Інтервал оновлення капчі (сек)'],
captchaLang: {
sel: [
['Откл.', 'Eng', 'Rus'],
['Disable', 'Eng', 'Rus'],
['Вимк.', 'Eng', 'Ukr']],
txt: [
'Принудительный язык ввода капчи',
'Forced captcha input language',
'Примусова мова вводу капчі']
},
addTextBtns: {
sel: [
['Откл.', 'Графические', 'Упрощённые', 'Стандартные'],
['Disable', 'As images', 'As text', 'Standard'],
['Вимк.', 'Графічні', 'Спрощені', 'Стандартні']],
txt: [
'Кнопки разметки текста ',
'Text markup buttons ',
'Кнопки розмітки тексту ']
},
txtBtnsLoc: [
'Внизу',
'At bottom',
'Знизу'],
userPassw: [
'Постоянный пароль',
'Fixed password',
'Постійний пароль'],
userName: [
'Постоянное имя',
'Fixed name',
'Постійне імʼя'],
noBoardRule: [
'Правила ',
'Rules ',
'Правила '],
noPassword: [
'Пароль ',
'Password ',
'Пароль '],
noName: [
'Имя ',
'Name ',
'Імʼя '],
noSubj: [
'Тему',
'Subject',
'Тему'],
// "Common" tab
scriptStyle: {
sel: [
['Gradient darkblue', 'Gradient blue', 'Solid grey', 'Transparent blue', 'Square dark',
'Gradient pink'],
['Gradient darkblue', 'Gradient blue', 'Solid grey', 'Transparent blue', 'Square dark',
'Gradient pink'],
['Gradient darkblue', 'Gradient blue', 'Solid grey', 'Transparent blue', 'Square dark',
'Gradient pink']],
txt: [
'Стиль Dollchan',
'Dollchan style',
'Стиль Dollchan']
},
userCSS: [
'Пользовательский CSS',
'User CSS',
'Користувацький CSS'],
animation: [
'CSS3 анимация',
'CSS3 animation',
'CSS3 анімація'],
hotKeys: [
'Горячие клавиши',
'Hotkeys',
'Гарячі клавіші'],
loadPages: [
'Количество страниц, загружаемых по F5',
'Number of pages that are loaded on F5 ',
'Кількість сторінок, що завантажуються по F5'],
panelCounter: {
sel: [
['Откл.', 'Все посты', 'Без скрытых'],
['Disabled', 'All posts', 'Except hidden'],
['Вимкн.', 'Всі дописи', 'Крім схованих']],
txt: [
'Счетчик постов/картинок в треде',
'Сounter for posts/images in thread',
'Лічильник дописів/зображ. в треді']
},
rePageTitle: [
'Название треда в заголовке вкладки',
'Show thread title in the page tab',
'Назва треду в заголовку вкладки'],
inftyScroll: [
'Бесконечная прокрутка страниц',
'Infinite scrolling for pages',
'Нескінченна прокрутка сторінок'],
hideReplies: [
'Показывать только OP в списке тредов',
'Show only OP in threads list',
'Показувати лише OP в списку тредів'],
scrollToTop: [
'Всегда перемещаться вверх в списке тредов',
'Always scroll to top in the threads list',
'Завжди гортати догори в списку тредів'],
saveScroll: [
'Запоминать позицию скролла в тредах',
'Remember the scroll position in threads',
'Пам`ятати позицію скролла в тредах'],
favFolders: [
'Папки досок в окне Избранного',
'Boards folders in the Favorites window',
'Папки дошок в вікні Вибраного'],
favThrOrder: {
sel: [
['По номеру', 'По номеру (убыв)', 'По добавлению', 'По добавлению (убыв)'],
['By number', 'By number (desc)', 'By adding', 'By adding (desc)'],
['За номером', 'За номером (зменш)', 'По додаванню', 'По додаванню (зменш)']],
txt: [
'Сортировка в Избранном',
'Sorting in Favorites',
'Сортування в Вибраному']
},
favWinOn: [
'Всегда открывать окно Избранное',
'Always open the Favorites window',
'Завжди відкривати вікно Вибране'],
closePopups: [
'Автоматически закрывать уведомления',
'Close popups automatically',
'Автоматично закривати сповіщення'],
updDollchan: {
sel: [
['Откл.', 'Каждый день', 'Каждые 2 дня', 'Каждую неделю', 'Каждые 2 недели', 'Каждый месяц'],
['Disable', 'Every day', 'Every 2 days', 'Every week', 'Every 2 weeks', 'Every month'],
['Вимкн.', 'Щодня', 'Кожні 2 дні', 'Щотижня', 'Кожні 2 тижні', 'Щомісяця']],
txt: [
'Проверять обновления Dollchan',
'Check for Dollchan updates',
'Перевіряти оновлення Dollchan']
}
},
// Main panel buttons: tooltips
panelBtn: {
attach: [
'Прикрепить/Открепить панель',
'Attach/Detach panel',
'Закріпити/відкріпити панель'],
cfg: [
'Настройки',
'Settings',
'Налаштування'],
hid: [
'Скрытое',
'Hidden',
'Сховане'],
fav: [
'Избранное',
'Favorites',
'Вибране'],
vid: [
'Ссылки на видео',
'Video links',
'Посилання на відео'],
refresh: [
'Обновить',
'Refresh',
'Оновити'],
goback: [
'Назад на доску',
'Return to board',
'Назад до дошки'],
gonext: [
'На %s страницу',
'Go to page %s',
'До %s сторінки'],
goup: [
'В начало страницы',
'Scroll to top',
'Прогорнути догори'],
godown: [
'В конец страницы',
'Scroll to bottom',
'Прогорнути донизу'],
expimg: [
'Раскрыть все картинки',
'Expand all images',
'Розгорнути всі зображення'],
maskimg: [
'Режим NSFW',
'NSFW mode',
'Режим NSFW'],
preimg: [
'Предзагрузить картинки\r\n([Ctrl+Click] только для новых постов)',
'Preload images\r\n([Ctrl+Click] for new posts only)',
'Наперед завантажити зображення\r\n([Ctrl+Click] лише для нових дописів)'],
savethr: [
'Сохранить на диск',
'Save to disk',
'Зберегти на диск'],
'upd-on': [
'Выключить автообновление треда',
'Disable thread updater',
'Вимкнути оновлювач треду'],
'upd-off': [
'Включить автообновление треда',
'Enable thread updater',
'Увімкнути оновлювач треду'],
'audio-off': [
'Звуковое оповещение о новых постах',
'Sound notification about new posts',
'Звукове сповіщення про нові дописи'],
catalog: [
'Перейти в каталог',
'Go to catalog',
'Перейти до каталогу'],
enable: [
'Включить/выключить Dollchan',
'Turn on/off the Dollchan',
'Увімкнути/вимкнути Dollchan'],
postsCount: [
'Постов в треде',
'Posts in thread',
'Дописів у треді'],
postsNotHid: [
'Постов в треде (без скрытых)',
'Posts in thread (without hidden)',
'Дописів у треді (крім схованих)'],
filesCount: [
'Картинок и видео в треде',
'Images and videos in thread',
'Зображень та відео у треді'],
postersCount: [
'Постящих в треде',
'Posters in thread',
'Дописувачів у треді']
},
// Post buttons: tooltips
togglePost: [
'Скрыть/Раскрыть пост',
'Hide/Unhide post',
'Сховати/показати допис'],
toggleThr: [
'Скрыть/Раскрыть тред',
'Hide/Unhide thread',
'Сховати/показати тред'],
replyToPost: [
'Ответить на пост',
'Reply to post',
'Відповісти на допис'],
replyToThr: [
'Ответить в тред',
'Reply to thread',
'Відповісти в тред'],
expandThr: [
'Развернуть тред',
'Expand thread',
'Розгорнути тред'],
addFav: [
'Добавить тред в Избранное',
'Add thread to Favorites',
'Додати тред в Вибране'],
delFav: [
'Убрать тред из Избранного',
'Remove thread from Favorites',
'Прибрати тред з Вибраного'],
attachPview: [
'Закрепить превью',
'Attach preview',
'Закріпити превʼю'],
// Windows buttons: tooltips
closeWindow: [
'Закрыть окно',
'Close window',
'Закрити вікно'],
closeReply: [
'Закрыть форму',
'Close form',
'Закрити форму'],
toPanel: [
'Закрепить на панели',
'Attach to panel',
'Закріпити на панелі'],
makeDrag: [
'Сделать перетаскиваемым окном',
'Make draggable window',
'Зробити перетягуваним вікном'],
underPost: [
'Разместить форму после поста',
'Move form under post',
'Розмістити форму після допису'],
clearForm: [
'Очистить форму',
'Clear form',
'Очистити форму'],
// Markup buttons: tooltips
txtBtn: [
['Жирный', 'Bold', 'Жирний'],
['Курсив', 'Italic', 'Курсив'],
['Подчеркнутый', 'Underlined', 'Підкреслений'],
['Зачеркнутый', 'Strike', 'Закреслений'],
['Спойлер', 'Spoiler', 'Спойлер'],
['Код', 'Code', 'Код'],
['Верхний индекс', 'Superscript', 'Верхній індекс'],
['Нижний индекс', 'Subscript', 'Нижній індекс'],
['Цитировать выделенное', 'Quote selected', 'Цитувати виділене']],
// Drop-down menus: options
selHiderMenu: { // "Hide" post button
sel: [
'Скрывать выделенное',
'Hide selected text',
'Ховати виділене'],
name: [
'Скрывать по имени',
'Hide by name',
'Ховати по імені'],
trip: [
'Скрывать по трипкоду',
'Hide by tripcode',
'Ховати по тріпкоду'],
img: [
'Скрывать по размеру картинки',
'Hide by image size',
'Ховати по розміру зображення'],
imgn: [
'Скрывать по имени картинки',
'Hide by image name',
'Ховати по імені зображення'],
ihash: [
'Скрывать схожие картинки',
'Hide by similar images',
'Ховати подібні зображення'],
noimg: [
'Скрывать без картинок',
'Hide without images',
'Ховати без зображень'],
notext: [
'Скрывать без текста',
'Hide without text',
'Ховати без тексту'],
text: [
'Скрыть схожий текст',
'Hide similar text',
'Сховати схожий текст'],
refs: [
'Скрыть с ответами',
'Hide with replies',
'Сховати з відповідями'],
refsonly: [
'Скрывать ответы',
'Hide replies',
'Ховати відповіді']
},
selExpandThr: [ // "Expand thread" post button
['+10 постов', 'Последние 30', 'Последние 50', 'Последние 100', 'Весь тред'],
['+10 posts', 'Last 30 posts', 'Last 50 posts', 'Last 100 posts', 'Entire thread'],
['+10 дописів', 'Останні 30', 'Останні 50', 'Останні 100', 'Весь тред']],
selAjaxPages: [ // "Refresh" panel button
['1 страница', '2 страницы', '3 страницы', '4 страницы', '5 страниц'],
['1 page', '2 pages', '3 pages', '4 pages', '5 pages'],
['1 сторінка', '2 сторінки', '3 сторінки', '4 сторінки', '5 сторінок']],
selSaveThr: [ // "Save to disk" panel button
['Скачать весь тред', 'Скачать картинки'],
['Download thread', 'Download images'],
['Завантажити весь тред', 'Завантажити зображення']],
selAudioNotif: [ // "Sound notification" panel button
['Каждые 30 сек.', 'Каждую минуту', 'Каждые 2 мин.', 'Каждые 5 мин.'],
['Every 30 sec.', 'Every minute', 'Every 2 min.', 'Every 5 min.'],
['Кожні 30 сек.', 'Щохвилини', 'Кожні 2 хв.', 'Кожні 5 хв.']],
reportPost: [
'Жалоба на пост',
'Report a post',
'Скарга на допис'],
reportThr: [
'Жалоба на тред',
'Report a thread',
'Скарга на тред'],
markMyPost: [
'Пометить как мой пост',
'Mark as my post',
'Відмітити як мій допис'
],
deleteMyPost: [
'Убрать из моих постов',
'Delete from my posts',
'Прибрати з моїх дописів'
],
// Sauce search for images and video frames
saveAs: [
'Сохр. как ',
'Save as ',
'Збер. як '],
origName: [
'Оригинальное имя',
'Original name',
'Оригінальне імʼя'],
metaName: [
'Имя из метаданных',
'Name from metadata',
'Імʼя з метаданих'],
boardName: [
'Имя, присвоенное доской',
'Name assigned by the board',
'Імʼя, присвоєне дошкою'],
searchIn: [
'Искать в ',
'Search in ',
'Шукати в '],
frameSearch: [
'Поиск кадра в ',
'Frame search in ',
'Пошук кадру в '],
gotoResults: [
'Перейти к результатам поиска',
'Go to search results',
'Перейти до результатів пошуку'],
getFrameLinks: [
'Получить ссылки для поиска этого кадра',
'Get links to search this frame',
'Отримати посилання для пошуку цього кадру'],
saveFrame: [
'Сохранить полученный кадр',
'Save the received frame',
'Зберегти отриманий кадр'],
errSaucenao: [
'Ошибка: не могу загрузить на saucenao.com',
'Error: canʼt load to saucenao.com',
'Помилка: не можу завантажити на saucenao.com'],
// Hotkeys editor
hotKeyEdit: [[
// Ru
'%l%i24 – предыдущая страница/картинка%/l',
'%l%i217 – следующая страница/картинка%/l',
'%l%i21 – тред (на доске)/пост (в треде) ниже%/l',
'%l%i20 – тред (на доске)/пост (в треде) выше%/l',
'%l%i31 – пост (на доске) ниже%/l',
'%l%i30 – пост (на доске) выше%/l',
'%l%i23 – скрыть пост/тред%/l',
'%l%i32 – перейти в тред%/l',
'%l%i33 – развернуть тред%/l',
'%l%i211 – раскрыть картинку в посте%/l',
'%l%i22 – быстрый ответ%/l',
'%l%i25t – отправить пост%/l',
'%l%i210 – открыть/закрыть "Настройки"%/l',
'%l%i26 – открыть/закрыть "Избранное"%/l',
'%l%i27 – открыть/закрыть "Скрытое"%/l',
'%l%i218 – открыть/закрыть "Видео"%/l',
'%l%i28 – открыть/закрыть панель%/l',
'%l%i29 – вкл./выкл. режим NSFW%/l',
'%l%i40 – обновить тред (в треде)%/l',
'%l%i212t – жирный%/l',
'%l%i213t – курсив%/l',
'%l%i214t – зачеркнутый%/l',
'%l%i215t – спойлер%/l',
'%l%i216t – код%/l'], [
// En
'%l%i24 – previous page/image%/l',
'%l%i217 – next page/image%/l',
'%l%i21 – thread (on board)/post (in thread) below%/l',
'%l%i20 – thread (on board)/post (in thread) above%/l',
'%l%i31 – on board post below%/l',
'%l%i30 – on board post above%/l',
'%l%i23 – hide post/thread%/l',
'%l%i32 – go to thread%/l',
'%l%i33 – expand thread%/l',
'%l%i211 – expand postʼs images%/l',
'%l%i22 – quick reply%/l',
'%l%i25t – send post%/l',
'%l%i210 – open/close "Settings"%/l',
'%l%i26 – open/close "Favorites"%/l',
'%l%i27 – open/close "Hidden"%/l',
'%l%i218 – open/close "Videos"%/l',
'%l%i28 – open/close main panel%/l',
'%l%i29 – toggle NSFW mode%/l',
'%l%i40 – update thread%/l',
'%l%i212t – bold%/l',
'%l%i213t – italic%/l',
'%l%i214t – strike%/l',
'%l%i215t – spoiler%/l',
'%l%i216t – code%/l'], [
// Ua
'%l%i24 – попередня сторінка/зображення%/l',
'%l%i217 – наступна сторінка/зображення%/l',
'%l%i21 – тред (на дошці)/допис (в треді) нижче%/l',
'%l%i20 – тред (на дошці)/допис (в треді) вище%/l',
'%l%i31 – допис (на дошці) нижче%/l',
'%l%i30 – допис (на дошці) вище%/l',
'%l%i23 – приховати допис/тред%/l',
'%l%i32 – перейти в тред%/l',
'%l%i33 – розгорнути тред%/l',
'%l%i211 – розгорнути зображення в дописі%/l',
'%l%i22 – швидка відповідь%/l',
'%l%i25t – відправити допис%/l',
'%l%i210 – відкрити/закрити "Налаштування"%/l',
'%l%i26 – відкрити/закрити "Вибране"%/l',
'%l%i27 – відкрити/закрити "Сховане"%/l',
'%l%i218 – відкрити/закрити "Посилання на відео"%/l',
'%l%i28 – відкрити/закрити панель%/l',
'%l%i29 – увімкнути/вимкнути режим NSFW%/l',
'%l%i40 – оновити тред (в треді)%/l',
'%l%i212t – жирний%/l',
'%l%i213t – курсив%/l',
'%l%i214t – закреслений%/l',
'%l%i215t – спойлер%/l',
'%l%i216t – код%/l']],
// Time correction in posts
cTimeError: [
'Неправильные настройки времени',
'Invalid time settings',
'Неправильні налаштування часу'],
month: [
['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'],
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
['січ', 'лют', 'бер', 'кві', 'тра', 'чер', 'лип', 'сер', 'вер', 'жов', 'лис', 'гру']],
fullMonth: [
['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'],
['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'],
['січня', 'лютого', 'березня', 'квітня', 'травня', 'червня',
'липня', 'серпня', 'вересня', 'жовтня', 'листопада', 'грудня']],
week: [
['Вск', 'Пнд', 'Втр', 'Срд', 'Чтв', 'Птн', 'Сбт'],
['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
['Нед', 'Пон', 'Вів', 'Сер', 'Чет', 'Птн', 'Сбт']],
monthDict: {
/* eslint-disable key-spacing, max-len, object-property-newline */
янв: 0, фев: 1, мар: 2, апр: 3, май: 4, мая: 4, июн: 5, июл: 6, авг: 7, сен: 8, окт: 9, ноя: 10, дек: 11,
jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
січ: 0, лют: 1, бер: 2, кві: 3, тра: 4, чер: 5, лип: 6, сер: 7, вер: 8, жов: 9, лис: 10, гру: 11
/* eslint-enable key-spacing, max-len, object-property-newline */
},
// Spells: popups
seSyntaxErr: [
'синтаксическая ошибка в аргументе спелла: #%s',
'syntax error in argument of spell: #%s',
'синтаксична помилка в аргументі спеллу: #%s'],
seUnknown: [
'неизвестный спелл: #%s',
'unknown spell: #%s',
'невідомий спелл: #%s'],
seMissOp: [
'пропущен оператор',
'missing operator',
'пропущено оператор'],
seMissArg: [
'пропущен аргумент спелла: #%s',
'missing argument of spell: #%s',
'пропущено аргумент спеллу: #%s'],
seMissSpell: [
'пропущен спелл',
'missing spell',
'пропущено спелл'],
seErrRegex: [
'синтаксическая ошибка в регулярном выражении: %s',
'syntax error in regular expression: %s',
'синтаксична помилка в регулярному виразі: %s'],
seUnexpChar: [
'неожиданный символ: %s',
'unexpected character: %s',
'неочікуваний символ: %s'],
seMissClBkt: [
'пропущена закрывающая скобка',
'missing \')\' in expression',
'пропущено закривну дужку'],
seRepsInParens: [
'спелл #%s не должен располагаться в скобках',
'spell #%s shouldnʼt be inside parentheses',
'спелл #%s не може бути в дужках'],
seOpInReps: [
'недопустимо использовать оператор %s со спеллами #rep и #outrep',
'donʼt use operator %s with spells #rep & #outrep',
'неприпустимо використовувати оператор %s зі спеллами #rep и #outrep'],
seRow: [
' (строка ',
' (row ',
' (рядок '],
seCol: [
', столбец ',
', column ',
', стовпчик '],
// Data editor
editInTxt: [
'Правка в текстовом формате',
'Edit in text format',
'Правка в текстовому форматі'],
editor: {
cfg: [
'Редактирование настроек',
'Edit settings',
'Редагування налаштувань'],
hidden: [
'Редактирование скрытых тредов',
'Edit hidden threads',
'Редагування схованих тредів'],
favor: [
'Редактирование избранного',
'Edit favorites',
'Редагування вибраного'],
css: [
'Редактирование CSS',
'Edit CSS',
'Редагування CSS']
},
// Settings import/export/clearing
fileImpExp: [
'Импорт/экспорт настроек в файл',
'Import/export config to file',
'Імпорт/експорт налаштувань до файлу'],
fileToData: [
'Загрузить данные из файла',
'Load data from a file',
'Завантажити дані з файла'],
dataToFile: [
'Получить файл с данными',
'Get the file with data',
'Отримати файл з даними'],
globalCfg: [
'Глобальные настройки',
'Global config',
'Глобальні налаштування'],
loadGlobal: [
'и применить к этому домену',
'and apply to this domain',
'і застосувати до цього домену'],
saveGlobal: [
'текущие настройки как глобальные',
'current config as global',
'поточні налаштування як глобальні'],
descrGlobal: [
'Глобальные настройки применяются по умолчанию при первом посещении других доменов',
'Global config is applied by default on the first visit of other domains',
'Глобальні налаштування застосовуються по замовчуванню під час першого відвідання інших доменів'],
resetCfg: [
'Сбросить в настройки по умолчанию',
'Reset config to defaults',
'Скинути в налаштування по замовчуванню'],
resetData: [
'Очистить выбранные данные',
'Reset selected data',
'Очистити обрані дані'],
allDomains: [
'для всех доменов',
'for all domains',
'для всіх доменів'],
delEntries: [
'Удалить выбранные записи',
'Delete selected entries',
'Видалити обрані записи'],
saveChanges: [
'Сохранить внесенные изменения',
'Save your changes',
'Зберегти внесені зміни'],
hidPostThr: [
'Скрытые посты и треды',
'Hidden posts and threads',
'Сховані дописи та треди'],
myPosts: [
'Мои посты',
'My posts',
'Мої дописи'],
// Settings window: Common/Info tab
checkNow: [
'Проверить сейчас',
'Check now',
'Перевірити зараз'],
updAvail: [
'Доступно обновление Dollchan: %s',
'Dollchan update available: %s!',
'Доступне оновлення Dollchan: %s'],
newCommitsAvail: [
'Обнаружены новые исправления: %s',
'New fixes detected: %s',
'Виявлено нові виправлення: %s'],
changeLog: [
'Список изменений',
'List of changes',
'Список змін'],
haveLatestStable: [
'Ваша версия %s является последней из стабильных.',
'Your %s version is the latest from stable versions.',
'Ваша версія %s є останньою зі стабільних.'],
haveLatestCommit: [
'Ваша версия %s содержит последние исправления.',
'Your %s version contains all the latest fixes.',
'Ваша версія %s містить всі останні виправлення.'],
thrViewed: [
'Тредов посещено',
'Threads visited',
'Тредів відвідано'],
thrCreated: [
'Тредов создано',
'Threads created',
'Тредів створено'],
thrHidden: [
'Тредов скрыто',
'Threads hidden',
'Тредів сховано'],
postsSent: [
'Постов отправлено',
'Posts sent',
'дописів надіслано'],
total: [
'Всего',
'Total',
'Всього'],
debug: [
'Отладка',
'Debug',
'Відлагодження'],
infoDebug: [
'Информация для отладки',
'Information for debugging',
'Інформація для відлагодження'],
// Favorites window: tooltips
refreshCounters: [
'Обновить счетчики постов',
'Refresh posts counters',
'Оновити лічильники дописів'],
refreshClear404: [
'Обновить счетчики и очистить недоступные (404) треды',
'Refresh counters and clear inaccessible (404) threads',
'Оновити лічильники та очистити недоступні (404) треди'],
clear404: [
'Очистить недоступные (404) треды',
'Clear inaccessible (404) threads',
'Очистити недоступні (404) треди'],
infoPage: [
'Проверить положение тредов (до 10-й страницы)',
'Check for threads position (up to 10th page)',
'Перевірити актуальність тредів (до 10 сторінки)'],
totalPosts: [
'Всего постов в треде',
'Total posts in thread',
'Всього дописів в треді'],
newPosts: [
'Количество новых постов',
'Number of new posts',
'Кількість нових дописів'],
myPostsRep: [
'Ответов на ваши посты',
'Replies to your posts',
'Відповідей на ваші дописи'],
thrPage: [
'На какой странице сейчас тред',
'What page is the thread on now',
'На якій сторінці зараз тред'],
goToThread: [
'Перейти к треду',
'Go to the thread',
'Перейти до треду'],
goToBoard: [
'Перейти к доске',
'Go to the board',
'Перейти до дошки'],
toggleEntries: [
'Скрыть/раскрыть записи',
'Hide/expand entries',
'Сховати/розкрити записи'],
// Video links: tooltips
hideLnkList: [
'Скрыть/Показать список ссылок',
'Hide/Unhide list of links',
'Сховати/показати перелік посилань'],
expandVideo: [
'Развернуть/Свернуть видео',
'Expand/Collapse video',
'Розгорнути/згорнути відео'],
prevVideo: [
'Предыдущее видео',
'Previous video',
'Попереднє відео'],
nextVideo: [
'Следующее видео',
'Next video',
'Наступне відео'],
duration: [
'Продолжительность: ',
'Duration: ',
'Тривалість: '],
published: [
'опубликовано: ',
'published: ',
'опубліковано: '],
author: [
'Автор: ',
'Author: ',
'Автор: '],
views: [
'просмотров: ',
'views: ',
'переглядів: '],
// Postform file inputs: tooltips
dropFileHere: [
'Бросьте сюда файл(ы) или ссылку',
'Drop file(s) or link here',
'Киньте сюди файл(и) чи посилання'],
youCanDrag: [
'Можно перетаскивать картинки и ссылки на файлы\r\nпрямо со страницы или других сайтов',
'You can drag images and file links\r\ndirectly from the page or other sites',
'Можна перетягувати зображення чи посилання на файли\r\nбезпосередньо зі сторінки чи інших сайтів'],
removeFile: [
'Удалить файл',
'Remove file',
'Видалити файл'],
renameFile: [
'Переименовать файл',
'Rename file',
'Перейменувати файл'],
spoilFile: [
'Спойлер',
'Spoiler',
'Спойлер'],
addManually: [
'Ввести ссылку на файл вручную',
'Enter a link to the file manually',
'Ввести посилання на файл вручну'],
enterTheLink: [
'Введите ссылку и нажмите \'+\'',
'Enter the link and click \'+\'',
'Введіть посилання та натисніть \'+\''],
helpAddFile: [
'Встроить ogg/rar/zip/7z в картинку',
'Embed ogg/rar/zip/7z into the image',
'Вбудувати ogg/rar/zip/7z в зображення'],
// Post images: tooltips
expImgInline: [
'[Click] открыть в посте, [Ctrl+Click] по центру',
'[Click] expand in post, [Ctrl+Click] by center',
'[Click] розгорнути в дописі, [Ctrl+Click] в центрі'],
expImgFull: [
'[Click] открыть по центру, [Ctrl+Click] в посте',
'[Click] expand by center, [Ctrl+Click] in post',
'[Click] розгорнути в центрі, [Ctrl+Click] в дописі'],
nextImg: [
'Следующая картинка',
'Next image',
'Наступне зображення'],
prevImg: [
'Предыдущая картинка',
'Previous image',
'Попереднє зображення'],
rotateImg: [
'Повернуть вправо',
'Rotate right',
'Повернути вправо'],
autoPlayOn: [
'Автоматически воспроизводить следующее видео',
'Automatically play the next video',
'Автоматично відтворювати наступне відео'],
autoPlayOff: [
'Отключить автовоспроизведение',
'Disable autoplay',
'Відключити автовідтворення'],
downloadFile: [
'Скачать содержащийся в картинке файл',
'Download embedded file from the image',
'Завантажити файл, що міститься в зображенні'],
openOriginal: [
'Открыть оригинал в новой вкладке',
'Open the original image in new tab',
'Відкрити оригінал в новій вкладці'],
// Threads/images download: popups
loadImage: [
'Загружаются картинки',
'Loading images',
'Завантажуються зображення'],
loadFile: [
'Загружаются файлы',
'Loading files',
'Завантажуються файли'],
cantLoad: [
'Не могу загрузить',
'Canʼt load',
'Не можу завантажити'],
willSavePview: [
'Будет сохранено превью',
'Thumbnail will be saved',
'Буде збережено превʼю'],
loadErrors: [
'Во время загрузки произошли ошибки:',
'An error occurred during the loading:',
'Під час завантаження сталися помилки:'],
// Ajax: popups
succDeleted: [
'Успешно удалено!',
'Succesfully deleted!',
'Успішно видалено!'],
succReported: [
'Жалоба успешно отправлена',
'Succesfully reported',
'Скарга успішно відправлена'],
errDelete: [
'Не могу удалить',
'Canʼt delete',
'Не можу видалити'],
fileCorrupt: [
'Файл повреждён',
'File is corrupt',
'Файл пошкоджено'],
errCorruptData: [
'Ошибка: сервер отправил повреждённые данные',
'Error: server sent corrupted data',
'Помилка: сервер надіслав пошкоджені дані'],
noConnect: [
'Ошибка подключения',
'Connection failed',
'Помилка зʼєднання'],
thrNotFound: [
'Тред недоступен',
'Thread is unavailable',
'Тред недоступний'],
thrClosed: [
'Тред закрыт',
'Thread is closed',
'Тред закрито'],
thrArchived: [
'Тред в архиве',
'Thread is archived',
'Тред заархівовано'],
stormWallCheck: [
'Проверка StormWall защиты от DDoS атак...',
'Checking for the StormWall DDoS protection...',
'Перевірка StormWall захисту від DDoS атак...'],
stormWallErr: [
'Пожалуйста, решите капчу StormWall защиты',
'Please resolve the StormWall protection captcha',
'Будь ласка, вирішіть капчу StormWall захисту'],
// Other warnings
internalError: [
'Внутренняя ошибка:\n',
'Internal error:\n',
'Внутрішня помилка:\n'],
postNotFound: [
'Пост не найден',
'Post not found',
'Допис не знайдено'],
noHidThr: [
'Нет скрытых тредов…',
'No hidden threads…',
'Немає схованих дописів…'],
noFavThr: [
'Нет избранных тредов…',
'Favorites is empty…',
'Немає вибраних тредів…'],
noVideoLinks: [
'Нет ссылок на видео…',
'No video links…',
'Немає посилань на відео…'],
invalidData: [
'Некорректный формат данных',
'Incorrect data format',
'Некоректний формат даних'],
noGlobalCfg: [
'Глобальные настройки не найдены',
'Global config not found',
'Глобальні налаштування не знайдено'],
subjHasTrip: [
'Поле "Тема" содержит трипкод!',
'"Subject" field contains a tripcode!',
'Поле "Тема" містить трипкод!'],
errMsEdgeWebm: [
'Загрузите скрипт для воспроизведения WebM (VP9/Opus)',
'Please load a script to play WebM (VP9/Opus)',
'Завантажте скрипт для відтворення WebM (VP9/Opus)'],
errFormLoad: [
'Не удаётся загрузить форму ответа',
'Canʼt load the reply form',
'Не вдалося завантажити форму відповіді'
],
// Single words
second : ['с', 's', 'с'],
sizeByte : [' Байт', ' Byte', ' Байт'],
sizeKByte : [' КБ', ' KB', ' КБ'],
sizeMByte : [' МБ', ' MB', ' МБ'],
sizeGByte : [' ГБ', ' GB', ' ГБ'],
name : ['Имя', 'Name', 'Імʼя'],
subj : ['Тема', 'Subject', 'Тема'],
mail : ['Почта', 'Email', 'Пошта'],
video : ['Видео', 'Video', 'Відео'],
cap : ['Капча', 'Captcha', 'Капча'],
add : ['Добавить', 'Add', 'Додати'],
apply : ['Применить', 'Apply', 'Застосувати'],
cancel : ['Отмена', 'Cancel', 'Скасувати'],
clear : ['Очистить', 'Clear', 'Очистити'],
refresh : ['Обновить', 'Refresh', 'Оновити'],
save : ['Сохранить', 'Save', 'Зберегти'],
load : ['Загрузить', 'Load', 'Завантажити'],
edit : ['Правка', 'Edit', 'Правка'],
file : ['Файл', 'File', 'Файл'],
global : ['Глобальные', 'Global', 'Глобальні'],
reset : ['Сброс', 'Reset', 'Скинути'],
remove : ['Удалить', 'Remove', 'Видалити'],
change : ['Сменить', 'Change', 'Змінити'],
page : ['Страница', 'Page', 'Сторінка'],
reply : ['Ответ', 'Reply', 'Відповідь'],
replies : ['Ответы:', 'Replies:', 'Відповіді:'],
makeReply : ['Ответить', 'Reply', 'Відповісти'],
error : ['Ошибка', 'Error', 'Помилка'],
loading : ['Загрузка…', 'Loading…', 'Завантаження…'],
sending : ['Отправка…', 'Sending…', 'Надсилання…'],
checking : ['Проверка…', 'Checking…', 'Перевірка…'],
updating : ['Обновление…', 'Updating…', 'Оновлення…'],
deleting : ['Удаление…', 'Deleting…', 'Видалення…'],
deleted : ['удалён', 'deleted', 'видалено'],
hide : ['Скрыть: ', 'Hide: ', 'Сховати: '],
// Miscellaneous
hidePosts: [
'Скрыть посты',
'Hide posts',
'Сховати дописи'],
showPosts: [
'Показать посты',
'Show posts',
'Показати дописи'],
getNewPosts: [
'Получить новые посты',
'Get new posts',
'Отримати нові дописи'],
makeThr: [
'Создать тред',
'Create thread',
'Створити тред'],
collapseThr: [
'Свернуть тред',
'Collapse thread',
'Згорнути тред'],
hiddenThr: [
'Скрытый тред',
'Hidden thread',
'Схований тред'],
hideForm: [
'Скрыть форму',
'Hide form',
'Сховати форму'],
enableSage: [
'Нажмите, чтобы включить сажу',
'Click to enable sage',
'Натисніть, щоб увімкнути сажу'],
disableSage: [
'САЖА включена! Нажмите, чтобы отключить',
'SAGE enabled! Click to disable',
'САЖА ввімкнена! Натисніть, щоб вимкнути'],
postsOmitted: [
'Пропущено ответов: ',
'Posts omitted: ',
'Пропущено відповідей: '],
newPost: [
['новый пост', 'новых поста', 'новых постов'],
['new post', 'new posts', 'new posts'],
['новий допис', 'нових дописи', 'нових дописів']],
youReplies: [
['ответ Вам', 'ответа Вам', 'ответов Вам'],
['reply to You', 'replies to You', 'replies to You'],
['відповідь Вам', 'відповіді Вам', 'відповідей Вам']],
latestPost: [
'Последний пост',
'Latest post',
'Останній допис'],
donateMsg: [
'Спасибо за использование Dollchan Extension! Вы можете поддержать проект пожертвованием',
'Thank You for using Dollchan Extension! You can support the project by donating',
'Дякуємо за використання Dollchan Extension! Ви можете підтримати проект пожертвою'],
donateOnline: [
'Онлайн донат (грн)',
'Donate online (UAH)',
'Онлайн донат (грн)'
],
firefoxAddon: [
'Firefox аддон доступен!',
'Firefox add-on is available!',
'Firefox аддон доступний!']
};
/* ==[ Utils.js ]=============================================================================================
UTILS
=========================================================================================================== */
// DOM SEARCH
function $id(id) {
return doc.getElementById(id);
}
function $q(path, rootEl = doc.body) {
return rootEl.querySelector(path);
}
function $Q(path, rootEl = doc.body) {
return rootEl.querySelectorAll(path);
}
function $match(parentStr, ...rules) {
return parentStr.split(', ').map(val => val + rules.join(', ' + val)).join(', ');
}
// DOM MODIFIERS
function $bBegin(siblingEl, html) {
siblingEl.insertAdjacentHTML('beforebegin', html);
return siblingEl.previousSibling;
}
function $aBegin(parentEl, html) {
parentEl.insertAdjacentHTML('afterbegin', html);
return parentEl.firstChild;
}
function $bEnd(parentEl, html) {
parentEl.insertAdjacentHTML('beforeend', html);
return parentEl.lastChild;
}
function $aEnd(siblingEl, html) {
siblingEl.insertAdjacentHTML('afterend', html);
return siblingEl.nextSibling;
}
function $delAll(path, rootEl = doc.body) {
rootEl.querySelectorAll(path, rootEl).forEach(el => el.remove());
}
function $add(html) {
dummy.innerHTML = html;
return dummy.firstElementChild;
}
function $button(value, title, fn, className = 'de-button') {
const el = $add(` `);
el.addEventListener('click', fn);
return el;
}
function $script(text) {
try {
const el = doc.createElement('script');
el.type = 'text/javascript';
el.textContent = text;
doc.head.append(el);
el.remove();
} catch(err) {}
}
function $css(text) {
return $bEnd(doc.head, ``);
}
function $createDoc(html) {
const myDoc = doc.implementation.createHTMLDocument('');
myDoc.documentElement.innerHTML = html;
return myDoc;
}
// CSS AND ATTRIBUTES
function $show(el) {
el.style.removeProperty('display');
}
function $hide(el) {
el.style.display = 'none';
}
function $toggle(el, needToShow = el.style.display) {
if(needToShow) {
el.style.removeProperty('display');
} else {
el.style.display = 'none';
}
}
function $animate(el, cName, isRemove = false) {
el.addEventListener('animationend', function aEvent() {
el.removeEventListener('animationend', aEvent);
if(isRemove) {
el.remove();
} else {
el.classList.remove(cName);
}
});
el.classList.add(cName);
}
// OBJECT
function $hasProp(obj, i) {
return Object.prototype.hasOwnProperty.call(obj, i);
}
function $isEmpty(obj) {
for(const i in obj) {
if($hasProp(obj, i)) {
return false;
}
}
return true;
}
// REGEXP
// Prepares a string to be used as a new RegExp argument
function escapeRegExp(str) {
return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
}
// Converts a string into regular expression
function strToRegExp(str, notGlobal) {
const l = str.lastIndexOf('/');
const flags = str.substr(l + 1);
return new RegExp(str.substr(1, l - 1), notGlobal ? flags.replace('g', '') : flags);
}
// OTHER UTILS
function pad2(i) {
return i < 10 ? '0' + i : i;
}
function arrTags(arr, start, end) {
return start + arr.join(end + start) + end;
}
function fixBoardName(board) {
return `/${ board ? board + '/' : '' }`;
}
function getFileName(url) {
return url.substring(url.lastIndexOf('/') + 1);
}
function getFileExt(url) {
return url.substring(url.lastIndexOf('.') + 1);
}
function cutFileExt(fileName) {
return fileName.substring(0, fileName.lastIndexOf('.'));
}
// Converts bytes into KB/MB/GB
function prettifySize(val) {
return val > 512 * 1024 * 1024 ? (val / (1024 ** 3)).toFixed(2) + Lng.sizeGByte[lang] :
val > 512 * 1024 ? (val / (1024 ** 2)).toFixed(2) + Lng.sizeMByte[lang] :
val > 512 ? (val / 1024).toFixed(2) + Lng.sizeKByte[lang] :
val.toFixed(2) + Lng.sizeByte[lang];
}
// Inserts the text at the cursor into an input field
function insertText(el, txt) {
const { scrollTop, selectionStart: start, value } = el;
el.value = value.substr(0, start) + txt + value.substr(el.selectionEnd);
el.setSelectionRange(start + txt.length, start + txt.length);
el.focus();
el.scrollTop = scrollTop;
}
// Gets the error stack trace
function getErrorMessage(err) {
if(err instanceof AjaxError) {
return err.toString();
}
if(typeof err === 'string') {
return err;
}
const { stack, name, message } = err;
return Lng.internalError[lang] + (
!stack ? `${ name }: ${ message }` :
nav.isWebkit ? stack : `${ name }: ${ message }\n${ !nav.isFirefox ? stack : stack.replace(
/^([^@]*).*\/(.+)$/gm,
(str, fName, line) => ` at ${ fName ? `${ fName } (${ line })` : line }`
) }`
);
}
// Read cookies
function getCookies() {
const obj = {};
const cookies = doc.cookie.split(';');
for(let i = 0, len = cookies.length; i < len; ++i) {
const parts = cookies[i].split('=');
obj[parts.shift().trim()] = decodeURI(parts.join('='));
}
return obj;
}
// Reads File into data
async function readFile(file, asText) {
return new Promise(resolve => {
const fr = new FileReader();
fr.onload = e => resolve({ data: e.target.result });
if(asText) {
fr.readAsText(file);
} else {
fr.readAsArrayBuffer(file);
}
});
}
// Gets mime type depending on file name
function getFileMime(url) {
const dotIdx = url.lastIndexOf('.') + 1;
switch(dotIdx && url.substr(dotIdx).toLowerCase()) {
case 'gif': return 'image/gif';
case 'jfif':
case 'jpeg':
case 'jpg': return 'image/jpeg';
case 'mov': return 'video/quicktime';
case 'mp4':
case 'm4v': return 'video/mp4';
case 'ogv': return 'video/ogv';
case 'png': return 'image/png';
case 'webm': return 'video/webm';
case 'webp': return 'image/webp';
default: return '';
}
}
// Downloads files stored in a Blob
function downloadBlob(blob, name) {
const url = nav.isMsEdge ? navigator.msSaveOrOpenBlob(blob, name) : deWindow.URL.createObjectURL(blob);
const link = $bEnd(doc.body, ` `);
link.click();
setTimeout(() => {
deWindow.URL.revokeObjectURL(url);
link.remove();
}, 2e5);
}
// Allows to record the duration of code execution
const Logger = {
finish() {
this._finished = true;
this._marks.push(['LoggerFinish', Date.now()]);
},
getLogData(isFull) {
const marks = this._marks;
const timeLog = [];
let duration;
let i = 1;
let lastExtra = 0;
for(let len = marks.length - 1; i < len; ++i) {
duration = marks[i][1] - marks[i - 1][1] + lastExtra;
if(isFull || duration > 1) {
lastExtra = 0;
timeLog.push([marks[i][0], duration]);
} else { // Ignore logs equal to 0ms
lastExtra = duration;
}
}
timeLog.push([Lng.total[lang], marks[i][1] - marks[0][1]]);
return timeLog;
},
initLogger() {
this._marks.push(['LoggerInit', Date.now()]);
},
log(text) {
if(!this._finished) {
this._marks.push([text, Date.now()]);
}
},
_finished : false,
_marks : []
};
// Some async operations should be cancelable, to ignore all the chaining callbacks of promises.
// Cancellation is supposed to flow through a graph of promise dependencies. When a promise is cancelled, it
// will propagate to the farthest pending promises and reject them with the cancel reason CancelError.
function CancelError() {}
class CancelablePromise {
constructor(resolver, cancelFn) {
this._promise = new Promise((resolve, reject) => {
this._reject = reject;
resolver(value => {
resolve(value);
this._isResolved = true;
}, reason => {
reject(reason);
this._isResolved = true;
});
});
this._cancelFn = cancelFn;
this._isResolved = false;
}
static reject(val) {
return new CancelablePromise((res, rej) => rej(val));
}
static resolve(val) {
return new CancelablePromise(res => res(val));
}
cancelPromise() {
this._reject(new CancelError());
if(!this._isResolved && this._cancelFn) {
this._cancelFn();
}
}
catch(eb) {
return this.then(undefined, eb);
}
then(cb, eb) {
const children = [];
const wrap = fn => (...args) => {
const child = fn(...args);
if(child instanceof CancelablePromise) {
children.push(child);
}
return child;
};
return new CancelablePromise(
resolve => resolve(this._promise.then(cb && wrap(cb), eb && wrap(eb))), () => {
for(const child of children) {
child.cancelPromise();
}
this.cancelPromise();
});
}
}
class Maybe {
constructor(Ctor/* , ...args */) {
this._ctor = Ctor;
// this._args = args;
this.hasValue = false;
}
get value() {
const Ctor = this._ctor;
this.hasValue = !!Ctor;
const value = Ctor ? new Ctor(/* ...this._args */) : null;
Object.defineProperty(this, 'value', { value });
return value;
}
}
class TemporaryContent {
constructor(key) {
const oClass = /* new.target */ this.constructor; // https://github.com/babel/babel/issues/1088
if(oClass.purgeTO) {
clearTimeout(oClass.purgeTO);
}
oClass.purgeTO = setTimeout(() => oClass.purge(), oClass.purgeSecs);
if(oClass.data) {
const rv = oClass.data.get(key);
if(rv) {
return rv;
}
} else {
oClass.data = new Map();
}
oClass.data.set(key, this);
}
static get(key) {
return this.data ? this.data.get(key) : null;
}
static has(key) {
return this.data ? this.data.has(key) : false;
}
static purge() {
if(this.purgeTO) {
clearTimeout(this.purgeTO);
this.purgeTO = null;
}
this.data = null;
}
static removeTempData(key) {
if(this.data) {
this.data.delete(key);
}
}
}
TemporaryContent.purgeSecs = 6e4;
class TasksPool {
constructor(tasksCount, taskFunc, endFn) {
this.array = [];
this.running = 0;
this.num = 1;
this.func = taskFunc;
this.endFn = endFn;
this.max = tasksCount;
this.completed = this.paused = this.stopped = false;
}
completeTasks() {
if(!this.stopped) {
if(!this.array.length && this.running === 0) {
this.endFn();
} else {
this.completed = true;
}
}
}
pauseTasks() {
this.paused = true;
}
runTask(data) {
if(!this.stopped) {
if(this.paused || this.running === this.max) {
this.array.push(data);
} else {
this._runTask(data);
this.running++;
}
}
}
stopTasks() {
this.stopped = true;
this.endFn();
}
_continueTasks() {
if(!this.stopped) {
this.paused = false;
if(!this.array.length) {
if(this.completed) {
this.endFn();
}
return;
}
while(this.array.length && this.running !== this.max) {
this._runTask(this.array.shift());
this.running++;
}
}
}
_endTask() {
if(!this.stopped) {
if(!this.paused && this.array.length) {
this._runTask(this.array.shift());
return;
}
this.running--;
if(!this.paused && this.completed && this.running === 0) {
this.endFn();
}
}
}
_runTask(data) {
this.func(this.num++, data).then(() => this._endTask(), err => {
if(err instanceof TasksPool.PauseError) {
this.pauseTasks();
if(err.duration !== -1) {
setTimeout(() => this._continueTasks(), err.duration);
}
} else {
this._endTask();
throw err;
}
});
}
}
TasksPool.PauseError = function(duration) {
this.name = 'TasksPool.PauseError';
this.duration = duration;
};
class WorkerPool {
constructor(mReqs, wrkFn, errFn) {
if(!nav.hasWorker) {
this.runWorker = (data, transferObjs, fn) => fn(wrkFn(data));
return;
}
const url = deWindow.URL.createObjectURL(new Blob([`self.onmessage = function(e) {
var info = (${ String(wrkFn) })(e.data);
if(info.data) {
self.postMessage(info, [info.data]);
} else {
self.postMessage(info);
}
}`], { type: 'text/javascript' }));
this._pool = new TasksPool(mReqs, (num, data) => this._createWorker(num, data), null);
this._freeWorkers = [];
this._url = url;
this._errFn = errFn;
while(mReqs--) {
this._freeWorkers.push(new Worker(url));
}
}
clearWorkers() {
deWindow.URL.revokeObjectURL(this._url);
this._freeWorkers.forEach(w => w.terminate());
this._freeWorkers = [];
}
runWorker(data, transferObjs, fn) {
this._pool.runTask([data, transferObjs, fn]);
}
_createWorker(num, data) {
return new Promise(resolve => {
const worker = this._freeWorkers.pop();
const [sendData, transferObjs, fn] = data;
worker.onmessage = e => {
fn(e.data);
this._freeWorkers.push(worker);
resolve();
};
worker.onerror = err => {
resolve();
this._freeWorkers.push(worker);
this._errFn(err);
};
worker.postMessage(sendData, transferObjs);
});
}
}
class TarBuilder {
constructor() {
this._data = [];
}
addFile(filepath, input) {
let i;
let checksum = 0;
const fileSize = input.length;
const header = new Uint8Array(512);
const nameLen = Math.min(filepath.length, 100);
for(i = 0; i < nameLen; ++i) {
header[i] = filepath.charCodeAt(i) & 0xFF;
}
TarBuilder._padSet(header, 100, '100777', 8); // fileMode
TarBuilder._padSet(header, 108, '0', 8); // uid
TarBuilder._padSet(header, 116, '0', 8); // gid
TarBuilder._padSet(header, 124, fileSize.toString(8), 13); // fileSize
TarBuilder._padSet(header, 136, Math.floor(Date.now() / 1e3).toString(8), 12); // mtime
TarBuilder._padSet(header, 148, ' ', 8); // checksum
// type ('0')
header[156] = 0x30;
for(i = 0; i < 157; ++i) {
checksum += header[i];
}
// checksum
TarBuilder._padSet(header, 148, checksum.toString(8), 8);
this._data.push(header, input);
if((i = Math.ceil(fileSize / 512) * 512 - fileSize) !== 0) {
this._data.push(new Uint8Array(i));
}
}
addString(filepath, str) {
const sDat = unescape(encodeURIComponent(str));
this.addFile(filepath, new Uint8Array(sDat.length).map((val, i) => sDat.charCodeAt(i) & 0xFF));
}
get() {
this._data.push(new Uint8Array(1024));
return new Blob(this._data, { type: 'application/x-tar' });
}
static _padSet(data, offset, num, len) {
let i = 0;
const nLen = num.length;
len -= 2;
while(nLen < len) {
data[offset++] = 0x20; // ' '
len--;
}
while(i < nLen) {
data[offset++] = num.charCodeAt(i++);
}
data[offset] = 0x20; // ' '
}
}
class WebmParser {
constructor(data) {
let offset = 0;
const dv = nav.getUnsafeDataView(data);
const len = dv.byteLength;
const el = new WebmParser.Element(dv, len, 0);
const voids = [];
const EBMLId = 0x1A45DFA3;
const segmentId = 0x18538067;
const voidId = 0xEC;
this.voidId = voidId;
error: do {
if(el.error || el.id !== EBMLId) {
break;
}
this.EBML = el;
offset += el.headSize + el.size;
while(true) {
const el = new WebmParser.Element(dv, len, offset);
if(el.error) {
break error;
}
if(el.id === segmentId) {
this.segment = el;
break; // Ignore everything after first segment
} else if(el.id === voidId) {
voids.push(el);
} else {
break error;
}
offset += el.headSize + el.size;
}
this.voids = voids;
this.data = data;
this.length = len;
this.rv = [null];
this.error = false;
return;
} while(false);
this.error = true;
}
addWebmData(data) {
if(this.error || !data) {
return this;
}
const size = typeof data === 'string' ? data.length : data.byteLength;
if(size > 127) {
this.error = true;
return;
}
this.rv.push(new Uint8Array([this.voidId, 0x80 | size]), data);
return this;
}
getWebmData() {
if(this.error) {
return null;
}
this.rv[0] = nav.getUnsafeUint8Array(this.data, 0, this.segment.endOffset);
return this.rv;
}
}
WebmParser.Element = function(elData, dataLength, offset) {
this.error = false;
this.id = 0;
if(offset + 4 >= dataLength) {
return;
}
let num = elData.getUint32(offset);
let leadZeroes = Math.clz32(num);
if(leadZeroes > 3) {
this.error = true;
return;
}
offset += leadZeroes + 1;
if(offset >= dataLength) {
this.error = true;
return;
}
this.id = num >>> (8 * (3 - leadZeroes));
this.headSize = leadZeroes + 1;
num = elData.getUint32(offset);
leadZeroes = Math.clz32(num);
let size = num & (0xFFFFFFFF >>> (leadZeroes + 1));
if(leadZeroes > 3) {
const shift = 8 * (7 - leadZeroes);
if(size >>> shift !== 0 || offset + 4 > dataLength) {
this.error = true;
return; // We cannot handle webm-files with size greater than 4Gb :(
}
size = (size << (32 - shift)) | (elData.getUint32(offset + 4) >>> shift);
} else {
size >>>= 8 * (3 - leadZeroes);
}
this.headSize += leadZeroes + 1;
offset += leadZeroes + 1;
if(offset + size > dataLength) {
this.error = true;
return;
}
this.data = elData;
this.offset = offset;
this.endOffset = offset + size;
this.size = size;
};
/* ==[ Storage.js ]===========================================================================================
STORAGE
=========================================================================================================== */
// Gets data from the global storage
async function getStored(id) {
if(nav.hasNewGM) {
const value = await GM.getValue(id);
return value;
} else if(nav.hasOldGM) {
return GM_getValue(id);
} else if(nav.hasWebStorage) {
// Read storage.local first. If it not existed then read storage.sync
return new Promise(resolve => chrome.storage.local.get(id, obj => {
if(Object.keys(obj).length) {
resolve(obj[id]);
} else {
chrome.storage.sync.get(id, obj => resolve(obj[id]));
}
}));
} else if(nav.hasPrestoStorage) {
return prestoStorage.getItem(id);
}
return locStorage[id];
}
// Saves data into the global storage
// FIXME: make async?
function setStored(id, value) {
if(nav.hasNewGM) {
return GM.setValue(id, value);
} else if(nav.hasOldGM) {
GM_setValue(id, value);
} else if(nav.hasWebStorage) {
return new Promise(resolve => {
const obj = {};
obj[id] = value;
chrome.storage.sync.set(obj, () => {
if(chrome.runtime.lastError) {
// Store into storage.local if the storage.sync limit is exceeded
chrome.storage.local.set(obj, Function.prototype);
chrome.storage.sync.remove(id, Function.prototype);
} else {
chrome.storage.local.remove(id, Function.prototype);
}
resolve();
});
});
} else if(nav.hasPrestoStorage) {
prestoStorage.setItem(id, value);
} else {
locStorage[id] = value;
}
return null;
}
// Removes data from the global storage
// FIXME: make async?
function delStored(id) {
if(nav.hasNewGM) {
return GM.deleteValue(id);
} else if(nav.hasOldGM) {
GM_deleteValue(id);
} else if(nav.hasWebStorage) {
chrome.storage.sync.remove(id, Function.prototype);
} else if(nav.hasPrestoStorage) {
prestoStorage.removeItem(id);
} else {
locStorage.removeItem(id);
}
}
// Receives and parses JSON data into an object
async function getStoredObj(id) {
return JSON.parse(await getStored(id) || '{}') || {};
}
// == CONFIG DATA ============================================================================================
// Asynchronous saving of config. Fixes a race condition when saving from different browser tabs.
const CfgSaver = {
// Saves enumerated options and values
async save(...args) {
let isChanged = false;
for(let i = 0; i < args.length; i += 2) {
const id = args[i];
const val = args[i + 1];
if(Cfg[id] !== val) {
Cfg[id] = val;
isChanged = true;
}
}
if(isChanged) {
await this.saveObj(aib.domain, loadedCfg => {
for(let i = 0; i < args.length; i += 2) {
loadedCfg[args[i]] = args[i + 1];
}
return loadedCfg;
});
}
},
// Saves all domain options as an object
async saveObj(domain, fn) {
if(this._isBusy) {
await new Promise((resolve, reject) => {
this._queue.push([domain, fn, resolve, reject]);
});
return;
}
this._isBusy = true;
await this.saveObjHelper(domain, fn);
if(this._queue.length > 0) {
while(this._queue.length > 0) {
const [[qDomain, qFn, resolve, reject]] = this._queue.splice(0, 1);
try {
await this.saveObjHelper(qDomain, qFn);
resolve();
} catch(err) {
reject(err);
}
}
}
this._isBusy = false;
},
async saveObjHelper(domain, fn) {
const val = await getStoredObj('DESU_Config');
const res = fn(val[domain]);
if(res) {
val[domain] = res;
} else {
delete val[domain];
}
const rv = setStored('DESU_Config', JSON.stringify(val));
if(rv) {
await rv;
}
},
_isBusy : false,
_queue : []
};
// Toggles a particular config option (1|0)
async function toggleCfg(id) {
await CfgSaver.save(id, +!Cfg[id]);
}
// Config initialization, checking for Dollchan update.
async function readCfg() {
let obj;
const val = await getStoredObj('DESU_Config');
if(!(aib.domain in val) || $isEmpty(obj = val[aib.domain])) {
const isGlobal = nav.hasGlobalStorage && !!val.global;
obj = isGlobal ? val.global : {};
if(isGlobal) {
delete obj.correctTime;
delete obj.captchaLang;
}
}
defaultCfg.captchaLang = aib.captchaLang;
const browserLang = String(navigator.language).toLowerCase();
defaultCfg.language =
browserLang.startsWith('ru') ? 0 :
browserLang.startsWith('en') ? 1 :
browserLang.startsWith('uk') ? 2 : defaultCfg.language;
Cfg = Object.assign(Object.create(defaultCfg), obj);
if(!Cfg.timeOffset) {
Cfg.timeOffset = '+0';
}
if(!Cfg.timePattern) {
Cfg.timePattern = aib.timePattern;
}
if(!('FormData' in deWindow)) {
Cfg.ajaxPosting = 0;
}
if(!Cfg.ajaxPosting) {
Cfg.fileInputs = 0;
}
if(!('Notification' in deWindow)) {
Cfg.desktNotif = 0;
}
if(nav.isPresto) {
Cfg.preLoadImgs = 0;
Cfg.findImgFile = 0;
if(!nav.hasOldGM) {
Cfg.updDollchan = 0;
}
Cfg.fileInputs = 0;
}
if(nav.scriptHandler === 'WebExtension') {
Cfg.updDollchan = 0;
}
if(Cfg.updThrDelay < 10) {
Cfg.updThrDelay = 10;
}
if(!Cfg.addSageBtn || !Cfg.saveSage) {
Cfg.sageReply = 0;
}
if(!Cfg.passwValue) {
Cfg.passwValue = Math.round(Math.random() * 1e12).toString(32);
}
if(!Cfg.stats) {
Cfg.stats = { view: 0, op: 0, reply: 0 };
}
lang = Cfg.language;
val[aib.domain] = Cfg;
if(val.commit !== commit && !localData) {
if(doc.readyState === 'loading') {
doc.addEventListener('DOMContentLoaded', () => setTimeout(showDonateMsg, 1e3));
} else {
setTimeout(showDonateMsg, 1e3);
}
val.commit = commit;
}
setStored('DESU_Config', JSON.stringify(val));
if(Cfg.updDollchan && !localData) {
checkForUpdates(false, val.lastUpd).then(html => {
if(doc.readyState === 'loading') {
doc.addEventListener('DOMContentLoaded', () => $popup('updavail', html));
} else {
$popup('updavail', html);
}
}, Function.prototype);
}
}
// == POSTS DATA =============================================================================================
// Initialization of hidden and favorites. Run spells.
function readPostsData(firstPost, favObj) {
let sVis = null;
try {
// Get hidden posts and threads from current session
const str = aib.t ? sesStorage['de-hidden-' + aib.b + aib.t] : null;
if(str) {
const json = JSON.parse(str);
if(json.hash === (Cfg.hideBySpell ? Spells.hash : 0) &&
pByNum.has(json.lastNum) && pByNum.get(json.lastNum).count === json.lastCount
) {
sVis = json.data?.[0] instanceof Array ? json.data : null;
}
}
} catch(err) {
sesStorage['de-hidden-' + aib.b + aib.t] = null;
}
if(!firstPost) {
return;
}
let updatedFav = null;
const favBoardObj = favObj[aib.host]?.[aib.b] || {};
const spellsHide = Cfg.hideBySpell;
const maybeSpells = new Maybe(SpellsRunner);
for(let post = firstPost; post; post = post.next) {
const { num } = post;
// Mark favorite threads, update favorites data
if(post.isOp && (num in favBoardObj)) {
let newCount = 0;
let youCount = 0;
post.toggleFavBtn(true);
const { thr } = post;
thr.isFav = true;
const isThrActive = aib.t && !doc.hidden;
const entry = favBoardObj[num];
let _post = pByNum.get(+entry.last.match(/\d+/));
if(_post) {
while((_post = _post.nextInThread)) {
if(Cfg.markNewPosts) {
Post.addMark(_post.el, true);
}
if(!isThrActive) {
newCount++;
if(isPostRefToYou(_post.el)) {
youCount++;
}
}
}
} else if(!aib.t) {
newCount = entry.new + thr.postsCount - entry.cnt;
_post = post;
while((_post = _post.nextInThread)) {
if(Cfg.markNewPosts) {
Post.addMark(_post.el, true);
}
if(isPostRefToYou(_post.el)) {
youCount++;
}
}
}
if(isThrActive) {
entry.last = aib.anchor + thr.last.num;
}
updatedFav = [aib.host, aib.b, aib.t, [
entry.cnt = thr.postsCount,
entry.new = newCount,
entry.you = youCount,
thr.last.num
], 'update'];
}
// Search existed posts in hidden posts data and apply spells
if(HiddenPosts.has(num)) {
HiddenPosts.hideHidden(post, num);
continue;
}
let hideData;
if(post.isOp) {
if(HiddenThreads.has(num)) {
hideData = [true, null];
} else if(spellsHide) {
hideData = sVis?.[post.count];
}
} else if(spellsHide) {
hideData = sVis?.[post.count];
} else {
continue;
}
if(!hideData) {
maybeSpells.value.runSpells(post);
} else if(hideData[0]) {
if(post.isHidden) {
post.spellHidden = true;
} else {
post.spellHide(hideData[1]);
}
}
}
if(maybeSpells.hasValue) {
maybeSpells.value.endSpells();
}
if(aib.t && Cfg.panelCounter === 2) {
$id('de-panel-info-posts').textContent = Thread.first.postsCount - Thread.first.hiddenCount;
}
if(updatedFav) {
saveFavorites(favObj);
// Updating Favorites: page is loaded
sendStorageEvent('__de-favorites', updatedFav);
}
// After following a link from Favorites, we need to open Favorites again.
const hasFavWinKey = sesStorage['de-fav-win'] === '1';
if(hasFavWinKey || Cfg.favWinOn) {
toggleWindow('fav', !!$q('#de-win-fav.de-win-active'), null, true);
if(hasFavWinKey) {
sesStorage.removeItem('de-fav-win');
}
}
let data = sesStorage['de-fav-newthr'];
if(data) { // Detecting the new created thread and adding it to Favorites.
data = JSON.parse(data);
const isTimeOut = !data.num && (Date.now() - data.date > 2e4);
if(data.num === firstPost.num || !firstPost.next && !isTimeOut) {
firstPost.thr.toggleFavState(true);
sesStorage.removeItem('de-fav-newthr');
} else if(isTimeOut) {
sesStorage.removeItem('de-fav-newthr');
}
}
if(Cfg.nextPageThr && DelForm.first === DelForm.last) {
const hidThrLen = $Q('.de-thr-hid', firstPost.thr.form.el).length;
if(hidThrLen) {
Pages.addPage(hidThrLen);
}
}
}
function readFavorites() {
return getStoredObj('DESU_Favorites');
}
function saveFavorites(data) {
setStored('DESU_Favorites', JSON.stringify(data));
}
// Get posts that were read by posts previews
function readViewedPosts() {
if(!Cfg.markViewed) {
return;
}
const data = sesStorage['de-viewed'];
if(data) {
data.split(',').forEach(pNum => {
const post = pByNum.get(+pNum);
if(post) {
post.el.classList.add('de-viewed');
post.isViewed = true;
}
});
}
}
class PostsStorage {
constructor() {
this.storageName = '';
this.__cachedTime = null;
this._cachedStorage = null;
this._cacheTO = null;
this._onReadNew = null;
this._onAfterSave = null;
}
get(num) {
const storage = this._readStorage()[aib.b];
if(storage) {
const val = storage[num];
return val ? val[2] : null;
}
return null;
}
has(num) {
const storage = this._readStorage()[aib.b];
return storage ? $hasProp(storage, num) : false;
}
purge() {
this._cacheTO = this.__cachedTime = this._cachedStorage = null;
}
removeStorage(num, board = aib.b) {
const storage = this._readStorage(true);
const bStorage = storage[board];
if(bStorage && $hasProp(bStorage, num)) {
delete bStorage[num];
if($isEmpty(bStorage)) {
delete storage[board];
}
this._saveStorage();
}
}
set(num, thrNum, data = true) {
const storage = this._readStorage(true);
this._removeOldItems(storage);
(storage[aib.b] || (storage[aib.b] = {}))[num] = [this._cachedTime, thrNum, data];
this._saveStorage();
}
_removeOldItems(storage) {
if(storage && storage.$count > 5e3) {
const minDate = Date.now() - 5 * 24 * 3600 * 1e3;
for(const board in storage) {
if($hasProp(storage, board)) {
const data = storage[board];
for(const key in data) {
if($hasProp(data, key) && data[key][0] < minDate) {
delete data[key];
}
}
}
}
}
}
get _cachedTime() {
return this.__cachedTime || (this.__cachedTime = Date.now());
}
_readStorage(ignoreCache = false) {
if(!ignoreCache && this._cachedStorage) {
return this._cachedStorage;
}
const data = locStorage[this.storageName];
let rv = {};
if(data) {
try {
rv = this._cachedStorage = JSON.parse(data);
} catch(err) {}
}
this._cachedStorage = rv;
if(this._onReadNew) {
this._onReadNew(rv);
}
return rv;
}
_saveStorage() {
if(this._cacheTO === null) {
this._cacheTO = setTimeout(() => {
if(this._cachedStorage) {
locStorage[this.storageName] = JSON.stringify(this._cachedStorage);
}
this.purge();
if(this._onAfterSave) {
this._onAfterSave();
}
}, 0);
}
}
}
const HiddenPosts = new class HiddenPostsClass extends PostsStorage {
constructor() {
super();
this.storageName = 'de-posts';
}
hideHidden(post, num) {
const uHideData = HiddenPosts.get(num);
if(!uHideData && post.isOp && HiddenThreads.has(num)) {
post.setUserVisib(true);
} else {
post.setUserVisib(!!uHideData, false);
}
}
}();
const HiddenThreads = new class HiddenThreadsClass extends PostsStorage {
constructor() {
super();
this.storageName = 'de-threads';
}
getCount() {
const storage = this._readStorage();
let rv = 0;
for(const board in storage) {
if($hasProp(storage, board)) {
rv += Object.keys(storage[board]).length;
}
}
return rv;
}
getRawData() {
return this._readStorage();
}
saveRawData(data) {
locStorage[this.storageName] = JSON.stringify(data);
this.purge();
}
}();
const MyPosts = new class MyPostsClass extends PostsStorage {
constructor() {
super();
this.storageName = 'de-myposts';
this._cachedData = null;
this._onReadNew = newStorage => {
this._cachedData = newStorage[aib.b] ?
new Set(Object.keys(newStorage[aib.b]).map(val => +val)) : new Set();
};
this._onAfterSave = () => sendStorageEvent('__de-mypost', 1);
}
has(num) {
return this._cachedData.has(num);
}
update() {
this.purge();
for(const num of this._cachedData) {
pByNum[num]?.changeMyMark(true);
}
}
purge() {
super.purge();
this._cachedData = null;
this._readStorage();
}
readStorage() {
this._readStorage();
}
set(num, thrNum) {
super.set(num, thrNum);
this._cachedData.add(+num);
}
}();
function sendStorageEvent(name, value) {
locStorage[name] = typeof value === 'string' ? value : JSON.stringify(value);
locStorage.removeItem(name);
}
function initStorageEvent() {
doc.defaultView.addEventListener('storage', e => {
let data, temp;
let val = e.newValue;
if(!val) {
return;
}
switch(e.key) {
case '__de-favorites': {
try {
data = JSON.parse(val);
} catch(err) {
return;
}
// Updating Favorites: keep in sync with other tab
updateFavWindow(...data);
return;
}
case '__de-mypost': MyPosts.update(); return;
case '__de-webmvolume':
val = +val || 0;
Cfg.webmVolume = val;
temp = $q('input[info="webmVolume"]');
if(temp) {
temp.value = val;
}
return;
case '__de-post':
(() => {
try {
data = JSON.parse(val);
} catch(err) {
return;
}
HiddenThreads.purge();
HiddenPosts.purge();
if(data.brd === aib.b) {
let post = pByNum.get(data.num);
if(post && (post.isHidden ^ data.hide)) {
post.setUserVisib(data.hide, false);
} else if((post = pByNum.get(data.thrNum))) {
post.thr.userTouched.set(data.num, data.hide);
}
}
toggleWindow('hid', true);
})();
return;
case 'de-threads':
HiddenThreads.purge();
Thread.first.updateHidden(HiddenThreads.getRawData()[aib.b]);
toggleWindow('hid', true);
return;
case '__de-spells': (async () => {
try {
data = JSON.parse(val);
} catch(err) {
return;
}
Cfg.hideBySpell = +data.hide;
temp = $q('input[info="hideBySpell"]');
if(temp) {
temp.checked = data.hide;
}
$hide(doc.body);
if(data.data) {
await Spells.setSpells(data.data, false);
Cfg.spells = JSON.stringify(data.data);
temp = $id('de-spell-txt');
if(temp) {
temp.value = Spells.list;
}
} else {
SpellsRunner.unhideAll();
await Spells.disableSpells();
temp = $id('de-spell-txt');
if(temp) {
temp.value = '';
}
}
$show(doc.body);
})();
}
}, false);
}
/* ==[ Panel.js ]=============================================================================================
MAIN PANEL
=========================================================================================================== */
const Panel = Object.create({
isVidEnabled: false,
initPanel(formEl) {
const filesCount = $Q(aib.qPostImg, formEl).length;
const isThr = aib.t;
(postform?.pArea[0] || formEl).insertAdjacentHTML('beforebegin', `
${ Cfg.disabled ? this._getButton('enable') : this._getButton('cfg') +
this._getButton('hid') +
this._getButton('fav') +
(Cfg.embedYTube ? this._getButton('vid') : '') +
(!localData ?
this._getButton('refresh') +
(isThr || aib.page !== aib.firstPage ? this._getButton('goback') : '') +
(!isThr && aib.page !== aib.lastPage ? this._getButton('gonext') : '') : '') +
this._getButton('goup') +
this._getButton('godown') +
(filesCount ? this._getButton('expimg') + this._getButton('maskimg') : '') +
(!localData && !nav.isPresto ?
(filesCount && !Cfg.preLoadImgs ? this._getButton('preimg') : '') +
(isThr ? this._getButton('savethr') : '') : '') +
(!localData && isThr ?
this._getButton(Cfg.ajaxUpdThr && !aib.isArchived ? 'upd-on' : 'upd-off') +
(!nav.isSafari ? this._getButton('audio-off') : '') : '') +
(aib.hasCatalog ? this._getButton('catalog') : '') +
this._getButton('enable') +
(isThr && Thread.first ? `
${ Thread.first.postsCount }
${
filesCount }
${
aib.postersCount }
` : '') }
${ Cfg.disabled ? '' : '
' }
`);
this._el = $id('de-panel');
this._el.addEventListener('click', this, true);
['mouseover', 'mouseout'].forEach(e => this._el.addEventListener(e, this));
this._buttons = $id('de-panel-buttons');
},
removeMain() {
this._el.removeEventListener('click', this, true);
['mouseover', 'mouseout'].forEach(e => this._el.removeEventListener(e, this));
delete this._postsCountEl;
delete this._filesCountEl;
delete this._postersCountEl;
$id('de-main').remove();
},
async handleEvent(e) {
if('isTrusted' in e && !e.isTrusted) {
return;
}
let el = nav.fixEventEl(e.target);
el = el.tagName.toLowerCase() === 'svg' ? el.parentNode : el;
switch(e.type) {
case 'click':
if(el.tagName.toLowerCase() === 'a') {
return;
}
e.preventDefault();
switch(el.id) {
case 'de-panel-logo':
if(Cfg.expandPanel && !$q('.de-win-active')) {
$hide(this._buttons);
}
await toggleCfg('expandPanel');
return;
case 'de-panel-cfg': toggleWindow('cfg', false); return;
case 'de-panel-hid': toggleWindow('hid', false); return;
case 'de-panel-fav': toggleWindow('fav', false); return;
case 'de-panel-vid':
this.isVidEnabled = !this.isVidEnabled;
toggleWindow('vid', false);
return;
case 'de-panel-refresh': deWindow.location.reload(); return;
case 'de-panel-goup': scrollTo(0, 0); return;
case 'de-panel-godown': scrollTo(0, doc.body.scrollHeight || doc.body.offsetHeight); return;
case 'de-panel-expimg':
el.classList.toggle('de-panel-button-active');
isExpImg = !isExpImg;
$q('.de-fullimg-center')?.remove();
for(let post = Thread.first.op; post; post = post.next) {
post.toggleImages(isExpImg, false);
}
return;
case 'de-panel-preimg':
el.classList.toggle('de-panel-button-active');
isPreImg = !isPreImg;
if(!e.ctrlKey) {
for(const { el } of DelForm) {
ContentLoader.preloadImages(el);
}
}
return;
case 'de-panel-maskimg':
el.classList.toggle('de-panel-button-active');
await toggleCfg('maskImgs');
updateCSS();
return;
case 'de-panel-upd-on':
case 'de-panel-upd-warn':
case 'de-panel-upd-off':
updater.toggle();
return;
case 'de-panel-audio-on':
case 'de-panel-audio-off':
if(updater.toggleAudio(0)) {
updater.enableUpdater();
el.id = 'de-panel-audio-on';
} else {
el.id = 'de-panel-audio-off';
}
$q('.de-menu')?.remove();
return;
case 'de-panel-savethr': return;
case 'de-panel-enable':
await toggleCfg('disabled');
deWindow.location.reload();
return;
default: return;
}
case 'mouseover':
if(!Cfg.expandPanel) {
clearTimeout(this._hideTO);
$show(this._buttons);
}
switch(el.id) {
case 'de-panel-cfg': KeyEditListener.setTitle(el, 10); break;
case 'de-panel-hid': KeyEditListener.setTitle(el, 7); break;
case 'de-panel-fav': KeyEditListener.setTitle(el, 6); break;
case 'de-panel-vid': KeyEditListener.setTitle(el, 18); break;
case 'de-panel-goback': KeyEditListener.setTitle(el, 4); break;
case 'de-panel-gonext': KeyEditListener.setTitle(el, 17); break;
case 'de-panel-maskimg': KeyEditListener.setTitle(el, 9); break;
case 'de-panel-refresh':
if(aib.t) {
return;
}
/* falls through */
case 'de-panel-savethr':
case 'de-panel-audio-off':
if(this._menu?.parentEl === el) {
return;
}
this._menuTO = setTimeout(() => {
this._menu = addMenu(el);
this._menu.onover = () => clearTimeout(this._hideTO);
this._menu.onout = () => this._prepareToHide(null);
this._menu.onremove = () => (this._menu = null);
}, Cfg.linksOver);
}
return;
default: // mouseout
this._prepareToHide(nav.fixEventEl(e.relatedTarget));
switch(el.id) {
case 'de-panel-refresh':
case 'de-panel-savethr':
case 'de-panel-audio-off':
clearTimeout(this._menuTO);
this._menuTO = 0;
}
}
},
updateCounter(postCount, filesCount, postersCount) {
this._postsCountEl.textContent = postCount;
this._filesCountEl.textContent = filesCount;
this._postersCountEl.textContent = postersCount;
if(aib.makaba) {
$Q('span[title="Всего постов в треде"]').forEach(
el => el.innerHTML = el.innerHTML.replace(/\d+$/, postCount));
$Q('span[title="Всего файлов в треде"]').forEach(
el => el.innerHTML = el.innerHTML.replace(/\d+$/, filesCount));
$Q('span[title="Постеры"]').forEach(
el => el.innerHTML = el.innerHTML.replace(/\d+$/, postersCount));
}
},
_el : null,
_hideTO : 0,
_menu : null,
_menuTO : 0,
get _filesCountEl() {
const value = $id('de-panel-info-files');
Object.defineProperty(this, '_filesCountEl', { value, configurable: true });
return value;
},
get _postersCountEl() {
const value = $id('de-panel-info-posters');
Object.defineProperty(this, '_postersCountEl', { value, configurable: true });
return value;
},
get _postsCountEl() {
const value = $id('de-panel-info-posts');
Object.defineProperty(this, '_postsCountEl', { value, configurable: true });
return value;
},
_getButton(id) {
let page, href, title, useId;
let tag = 'button';
switch(id) {
case 'goback':
tag = 'a';
page = Math.max(aib.page - 1, 0);
href = aib.getPageUrl(aib.b, page);
if(!aib.t) {
title = Lng.panelBtn.gonext[lang].replace('%s', page);
}
useId = 'arrow';
break;
case 'gonext':
tag = 'a';
page = aib.page + 1;
href = aib.getPageUrl(aib.b, page);
title = Lng.panelBtn.gonext[lang].replace('%s', page);
/* falls through */
case 'goup':
case 'godown':
useId = 'arrow';
break;
case 'upd-on':
case 'upd-off':
useId = 'upd';
break;
case 'catalog':
tag = 'a';
href = aib.catalogUrl;
}
return `<${ tag } id="de-panel-${ id }" class="de-abtn de-panel-button"
title="${ title || Lng.panelBtn[id][lang] }" ${ href ? 'href="' + href + '"': '' }>
${ id !== 'audio-off' ? `
` : `
` }
${ tag }>`;
},
_prepareToHide(rt) {
if(!Cfg.expandPanel && !$q('.de-win-active') &&
(!rt || !this._el.contains(rt.farthestViewportElement || rt))
) {
this._hideTO = setTimeout(() => $hide(this._buttons), 500);
}
}
});
/* ==[ WindowUtils.js ]=======================================================================================
WINDOW: UTILS
=========================================================================================================== */
function updateWinZ(winEl) {
const { style } = winEl;
if(style.zIndex < topWinZ) {
style.zIndex = ++topWinZ;
}
}
function makeDraggable(name, winEl, headEl) {
headEl.addEventListener('mousedown', {
_oldX : 0,
_oldY : 0,
_win : winEl,
_wStyle : winEl.style,
_X : 0,
_Y : 0,
_Z : 0,
async handleEvent(e) {
if(!Cfg[name + 'WinDrag']) {
return;
}
const { clientX: curX, clientY: curY } = e;
switch(e.type) {
case 'mousedown':
this._oldX = curX;
this._oldY = curY;
this._X = Cfg[name + 'WinX'];
this._Y = Cfg[name + 'WinY'];
if(this._Z < topWinZ) {
this._Z = this._wStyle.zIndex = ++topWinZ;
}
['mouseleave', 'mousemove', 'mouseup'].forEach(e => doc.body.addEventListener(e, this));
e.preventDefault();
return;
case 'mousemove': {
const maxX = Post.sizing.wWidth - this._win.offsetWidth;
const maxY = Post.sizing.wHeight - this._win.offsetHeight - 25;
const cr = this._win.getBoundingClientRect();
const x = cr.left + curX - this._oldX;
const y = cr.top + curY - this._oldY;
this._X = x >= maxX || curX > this._oldX && x > maxX - 20 ? 'right: 0' :
x < 0 || curX < this._oldX && x < 20 ? 'left: 0' :
`left: ${ x }px`;
this._Y = y >= maxY || curY > this._oldY && y > maxY - 20 ? 'bottom: 25px' :
y < 0 || curY < this._oldY && y < 20 ? 'top: 0' :
`top: ${ y }px`;
const { width } = this._wStyle;
this._win.setAttribute('style', `${ this._X }; ${ this._Y }; z-index: ${ this._Z }${
width ? '; width: ' + width : '' }`);
this._oldX = curX;
this._oldY = curY;
return;
}
case 'mouseleave':
case 'mouseup':
['mouseleave', 'mousemove', 'mouseup'].forEach(e => doc.body.removeEventListener(e, this));
await CfgSaver.save(name + 'WinX', this._X, name + 'WinY', this._Y);
}
}
});
}
class WinResizer {
constructor(name, direction, cfgName, winEl, targetEl) {
this.name = name;
this.direction = direction;
this.cfgName = cfgName;
this.vertical = direction === 'top' || direction === 'bottom';
this.winEl = winEl;
this.wStyle = this.winEl.style;
this.tStyle = targetEl.style;
$q('.de-resizer-' + direction, winEl).addEventListener('mousedown', this);
}
async handleEvent(e) {
let val, x, y;
const { wWidth: maxX, wHeight: maxY } = Post.sizing;
const { width } = this.wStyle;
const cr = this.winEl.getBoundingClientRect();
const z = `; z-index: ${ this.wStyle.zIndex }${ width ? '; width:' + width : '' }`;
switch(e.type) {
case 'mousedown':
if(this.winEl.classList.contains('de-win-fixed')) {
x = 'right: 0';
y = 'bottom: 25px';
} else {
x = Cfg[this.name + 'WinX'];
y = Cfg[this.name + 'WinY'];
}
switch(this.direction) {
case 'top': val = `${ x }; bottom: ${ maxY - cr.bottom }px${ z }`; break;
case 'bottom': val = `${ x }; top: ${ cr.top }px${ z }`; break;
case 'left': val = `right: ${ maxX - cr.right }px; ${ y + z }`; break;
case 'right': val = `left: ${ cr.left }px; ${ y + z }`;
}
this.winEl.setAttribute('style', val);
['mousemove', 'mouseup'].forEach(e => doc.body.addEventListener(e, this));
e.preventDefault();
return;
case 'mousemove':
if(this.vertical) {
val = e.clientY;
this.tStyle.setProperty('height', Math.max(parseInt(this.tStyle.height, 10) + (
this.direction === 'top' ? cr.top - (val < 20 ? 0 : val) :
(val > maxY - 45 ? maxY - 25 : val) - cr.bottom
), 90) + 'px', 'important');
} else {
val = e.clientX;
this.tStyle.setProperty('width', Math.max(parseInt(this.tStyle.width, 10) + (
this.direction === 'left' ? cr.left - (val < 20 ? 0 : val) :
(val > maxX - 20 ? maxX : val) - cr.right
), this.name === 'reply' ? 275 : 400) + 'px', 'important');
}
return;
default: // mouseup
['mousemove', 'mouseup'].forEach(e => doc.body.removeEventListener(e, this));
await CfgSaver.save(this.cfgName,
parseInt(this.vertical ? this.tStyle.height : this.tStyle.width, 10));
if(this.winEl.classList.contains('de-win-fixed')) {
this.winEl.setAttribute('style', 'right: 0; bottom: 25px' + z);
return;
}
if(this.vertical) {
await CfgSaver.save(this.name + 'WinY', cr.top < 1 ? 'top: 0' :
cr.bottom > maxY - 26 ? 'bottom: 25px' : `top: ${ cr.top }px`);
} else {
await CfgSaver.save(this.name + 'WinX', cr.left < 1 ? 'left: 0' :
cr.right > maxX - 1 ? 'right: 0' : `left: ${ cr.left }px`);
}
this.winEl.setAttribute('style', Cfg[this.name + 'WinX'] + '; ' + Cfg[this.name + 'WinY'] + z);
}
}
}
function toggleWindow(name, isUpdate, data, noAnim) {
let el;
let winEl = $id('de-win-' + name);
const isActive = winEl?.classList.contains('de-win-active');
if(isUpdate && !isActive) {
return;
}
if(!winEl) {
const winAttr = (Cfg[name + 'WinDrag'] ?
`de-win" style="${ Cfg[name + 'WinX'] }; ${ Cfg[name + 'WinY'] }` :
'de-win-fixed" style="right: 0; bottom: 25px'
) + (name !== 'fav' ? '' : `; width: ${ Cfg.favWinWidth }px; `);
winEl = $aBegin($id('de-main'), `
${ name === 'cfg' ? 'Dollchan Extension Tools' : Lng.panelBtn[name][lang] }
${ name !== 'fav' ? '' : `
` }
`);
const winBody = $q('.de-win-body', winEl);
if(name === 'cfg') {
winBody.className = 'de-win-body ' + aib.cReply;
} else {
setTimeout(() => {
const backColor = getComputedStyle(doc.body).getPropertyValue('background-color');
winBody.style.backgroundColor = backColor !== 'transparent' ? backColor : '#EEE';
}, 100);
}
if(name === 'fav') {
new WinResizer('fav', 'left', 'favWinWidth', winEl, winEl);
new WinResizer('fav', 'right', 'favWinWidth', winEl, winEl);
}
el = $q('.de-win-buttons', winEl);
el.onmouseover = e => {
const el = nav.fixEventEl(e.target);
const parent = el.parentNode;
switch(el.classList[0]) {
case 'de-win-btn-close': parent.title = Lng.closeWindow[lang]; break;
case 'de-win-btn-toggle':
parent.title = Cfg[name + 'WinDrag'] ? Lng.toPanel[lang] : Lng.makeDrag[lang];
}
};
el.lastElementChild.onclick = () => toggleWindow(name, false);
$q('.de-win-btn-toggle', el).onclick = async () => {
await toggleCfg(name + 'WinDrag');
const isDrag = Cfg[name + 'WinDrag'];
if(!isDrag) {
const temp = $q('.de-win-active.de-win-fixed', winEl.parentNode);
if(temp) {
toggleWindow(temp.id.substr(7), false);
}
}
winEl.classList.toggle('de-win', isDrag);
winEl.classList.toggle('de-win-fixed', !isDrag);
const { width } = winEl.style;
winEl.style.cssText = `${ isDrag ? `${ Cfg[name + 'WinX'] }; ${ Cfg[name + 'WinY'] }` :
'right: 0; bottom: 25px' }${ width ? '; width: ' + width : '' }`;
updateWinZ(winEl);
};
makeDraggable(name, winEl, $q('.de-win-head', winEl));
}
updateWinZ(winEl);
let isRemove = !isUpdate && isActive;
if(!isRemove && !winEl.classList.contains('de-win') &&
(el = $q(`.de-win-active.de-win-fixed:not(#de-win-${ name })`, winEl.parentNode))
) {
toggleWindow(el.id.substr(7), false);
}
const isAnim = !noAnim && !isUpdate && Cfg.animation;
let winBody = $q('.de-win-body', winEl);
if(isAnim && winBody.hasChildNodes()) {
winEl.addEventListener('animationend', function aEvent(e) {
e.target.removeEventListener('animationend', aEvent);
showWindow(winEl, winBody, name, isRemove, data, Cfg.animation);
winEl = winBody = name = isRemove = data = null;
});
winEl.classList.remove('de-win-open');
winEl.classList.add('de-win-close');
} else {
showWindow(winEl, winBody, name, isRemove, data, isAnim);
}
}
function showWindow(winEl, winBody, name, isRemove, data, isAnim) {
winBody.innerHTML = '';
winEl.classList.toggle('de-win-active', !isRemove);
if(isRemove) {
winEl.classList.remove('de-win-close');
$hide(winEl);
if(!Cfg.expandPanel && !$q('.de-win-active')) {
$hide($id('de-panel-buttons'));
}
return;
}
if(!Cfg.expandPanel) {
$show($id('de-panel-buttons'));
}
switch(name) {
case 'fav':
if(data) {
showFavoritesWindow(winBody, data);
break;
}
readFavorites().then(favObj => {
showFavoritesWindow(winBody, favObj);
$show(winEl);
if(isAnim) {
winEl.classList.add('de-win-open');
}
});
return;
case 'cfg': CfgWindow.initCfgWindow(winBody); break;
case 'hid': showHiddenWindow(winBody); break;
case 'vid': showVideosWindow(winBody);
}
$show(winEl);
if(isAnim) {
winEl.classList.add('de-win-open');
}
}
/* ==[ WindowVidHid.js ]======================================================================================
WINDOW: VIDEOS, HIDDEN THREADS
=========================================================================================================== */
function showVideosWindow(winBody) {
const els = $Q('.de-video-link');
if(!els.length) {
winBody.innerHTML = `${ Lng.noVideoLinks[lang] } `;
return;
}
//
if(!$id('de-ytube-api')) {
// YouTube APT script. We canʼt insert scripts directly as html.
const script = doc.createElement('script');
script.type = 'text/javascript';
script.src = aib.protocol + '//www.youtube.com/player_api';
script.id = 'de-ytube-api';
doc.head.append(script);
}
//
winBody.innerHTML = `
`;
const linkList = $add(`
`);
//
// A script to detect the end of current video playback, and auto play next. Uses YouTube API.
// The first video should not start automatically!
const script = doc.createElement('script');
script.type = 'text/javascript';
script.textContent = `(function() {
if('YT' in window && 'Player' in window.YT) {
onYouTubePlayerAPIReady();
} else {
window.onYouTubePlayerAPIReady = onYouTubePlayerAPIReady;
}
function onYouTubePlayerAPIReady() {
window.de_addVideoEvents =
addEvents.bind(document.querySelector('#de-win-vid > .de-win-body > .de-video-obj'));
window.de_addVideoEvents();
}
function addEvents() {
var autoplay = true;
if(this.hasAttribute('de-disableautoplay')) {
autoplay = false;
this.removeAttribute('de-disableautoplay');
}
new YT.Player(this.firstChild, { events: {
'onError': gotoNextVideo,
'onReady': autoplay ? function(e) {
e.target.playVideo();
} : Function.prototype,
'onStateChange': function(e) {
if(e.data === 0) {
gotoNextVideo();
}
}
}});
}
function gotoNextVideo() {
document.getElementById("de-video-btn-next").click();
}
})();`;
winBody.append(script);
//
// Events for control buttons
winBody.addEventListener('click', {
linkList,
currentLink : null,
listHidden : false,
player : winBody.firstElementChild,
playerInfo : null,
handleEvent(e) {
const el = e.target;
if(el.classList.contains('de-abtn')) {
let node;
switch(el.id) {
case 'de-video-btn-hide': { // Fold/unfold list of links
const isHide = this.listHidden = !this.listHidden;
$toggle(this.linkList, !isHide);
el.textContent = isHide ? '\u25BC' : '\u25B2';
break;
}
case 'de-video-btn-prev': // Play previous video
node = this.currentLink.parentNode;
node = node.previousElementSibling || node.parentNode.lastElementChild;
node.lastElementChild.click();
break;
case 'de-video-btn-next': // Play next video
node = this.currentLink.parentNode;
node = node.nextElementSibling || node.parentNode.firstElementChild;
node.lastElementChild.click();
break;
case 'de-video-btn-resize': { // Expand/collapse video player
const exp = this.player.className === 'de-video-obj';
this.player.className = exp ? 'de-video-obj de-video-expanded' : 'de-video-obj';
this.linkList.style.maxWidth = `${ exp ? 894 : +Cfg.YTubeWidth + 40 }px`;
this.linkList.style.maxHeight = `${ nav.viewportHeight() * 0.92 -
(exp ? 562 : +Cfg.YTubeHeigh + 82) }px`;
}
}
e.preventDefault();
return;
} else if(!el.classList.contains('de-video-link')) { // Clicking on ">" before link
// Go to post that contains this link
pByNum.get(+el.getAttribute('de-num')).selectAndScrollTo();
return;
}
const info = el.videoInfo;
if(this.playerInfo !== info) { // Prevents same link clicking
// Mark new link as a current and add player for it
if(this.currentLink) {
this.currentLink.classList.remove('de-current');
}
this.currentLink = el;
el.classList.add('de-current');
Videos.addPlayer(this, info, el.classList.contains('de-ytube'), true);
}
e.preventDefault();
}
}, true);
// Copy all video links into videos list
for(let i = 0, len = els.length; i < len; ++i) {
updateVideoList(linkList, els[i], aib.getPostOfEl(els[i]).num);
}
winBody.append(linkList);
$q('.de-video-link', linkList).click();
}
function updateVideoList(parent, link, num) {
const el = link.cloneNode(true);
el.videoInfo = link.videoInfo;
el.classList.remove('de-current');
el.setAttribute('onclick', 'window.de_addVideoEvents && window.de_addVideoEvents();');
$bEnd(parent, ``).append(el);
}
// HIDDEN THREADS WINDOW
function showHiddenWindow(winBody) {
const boards = HiddenThreads.getRawData();
const hasThreads = !$isEmpty(boards);
if(hasThreads) {
// Generate DOM for the list of hidden threads
for(const board in boards) {
if(!$hasProp(boards, board)) {
continue;
}
const threads = boards[board];
if($isEmpty(threads)) {
continue;
}
const block = $bEnd(winBody,
`/${ board }
`);
block.firstChild.onclick =
e => $Q('.de-entry > input', block).forEach(el => (el.checked = e.target.checked));
for(const tNum in threads) {
if($hasProp(threads, tNum)) {
block.insertAdjacentHTML('beforeend',
``);
}
}
}
}
$bEnd(winBody, (!hasThreads ? `${ Lng.noHidThr[lang] } ` : '') +
'
'
).append(
// "Edit" button. Calls a popup with editor to edit Hidden in JSON.
getEditButton('hidden', fn => fn(HiddenThreads.getRawData(), true, data => {
HiddenThreads.saveRawData(data);
Thread.first.updateHidden(data[aib.b]);
toggleWindow('hid', true);
})),
// "Clear" button. Allows to clear 404'd threads.
$button(Lng.clear[lang], Lng.clear404[lang], async e => {
// Sequentially load threads, and remove inaccessible
const els = $Q('.de-entry[info]', e.target.parentNode.parentNode);
for(let i = 0, len = els.length; i < len; ++i) {
const [board, tNum] = els[i].getAttribute('info').split(';');
await $ajax(aib.getThrUrl(board, tNum)).catch(err => {
if(err.code === 404) {
HiddenThreads.removeStorage(tNum, board);
HiddenPosts.removeStorage(tNum, board);
}
});
}
toggleWindow('hid', true);
}),
// "Delete" button. Allows to delete selected threads
$button(Lng.remove[lang], Lng.delEntries[lang], () => {
$Q('.de-entry[info]', winBody).forEach(el => {
if(!$q('input', el).checked) {
return;
}
const [board, tNum] = el.getAttribute('info').split(';');
const num = +tNum;
if(pByNum.has(num)) {
pByNum.get(num).setUserVisib(false);
} else {
sendStorageEvent('__de-post', { brd: board, num, hide: false, thrNum: num });
}
HiddenThreads.removeStorage(num, board);
HiddenPosts.set(num, num, false); // Actually unhide thread by its oppost
});
toggleWindow('hid', true);
})
);
}
/* ==[ WindowFavorites.js ]===================================================================================
WINDOW: FAVORITES
=========================================================================================================== */
// Saving favorites and renewing the Favorites window if it is open
function saveRenewFavorites(favObj) {
saveFavorites(favObj);
toggleWindow('fav', true, favObj);
}
// Removing an entry from hte favorites object
function removeFavEntry(favObj, host, board, num) {
const entry = favObj[host]?.[board];
if(entry?.[num]) {
delete entry[num];
if(!(Object.keys(entry).length - +$hasProp(entry, 'url') - +$hasProp(entry, 'hide'))) {
delete favObj[host][board];
if($isEmpty(favObj[host])) {
delete favObj[host];
}
}
}
}
// Toggling a favorites button in thread if it is available on page
function toggleThrFavBtn(host, board, num, isEnable) {
if(host === aib.host && board === aib.b && pByNum.has(num)) {
const post = pByNum.get(num);
post.toggleFavBtn(isEnable);
post.thr.isFav = isEnable;
}
}
// Updating Favorites on successed/failed thread loading, or on visiting a previously inactive page
function updateFavorites(num, value, mode) {
readFavorites().then(favObj => {
const entry = favObj[aib.host]?.[aib.b]?.[num];
if(!entry) {
return;
}
let isUpdate = false;
switch(mode) {
case 'error':
if(entry.err !== value) {
entry.err = value;
isUpdate = true;
}
break;
case 'update':
if(entry.last !== aib.anchor + value[3]) {
if(doc.hidden) {
value[1] += entry.new;
} else {
value[1] = value[2] = 0;
entry.last = aib.anchor + value[3];
}
if(entry.err) {
delete entry.err;
}
[entry.cnt, entry.new, entry.you] = value;
isUpdate = true;
}
}
if(isUpdate) {
const data = [aib.host, aib.b, num, value, mode];
updateFavWindow(...data);
saveFavorites(favObj);
sendStorageEvent('__de-favorites', data);
}
});
}
// Updating the Favorites window if it is open
function updateFavWindow(host, board, num, value, mode) {
if(mode === 'add' || mode === 'delete') {
toggleThrFavBtn(host, board, num, mode === 'add');
toggleWindow('fav', true, value);
return;
}
const winEl = $q('#de-win-fav > .de-win-body');
if(!winEl?.hasChildNodes()) {
return;
}
const el = $q(`.de-entry[de-host="${
host }"][de-board="${ board }"][de-num="${ num }"] > .de-fav-inf`, winEl);
if(!el) {
return;
}
const [iconEl, youEl, newEl, oldEl] = [...el.children];
$toggle(newEl, value[1]);
$toggle(youEl, value[2]);
if(mode === 'error') {
iconEl.firstElementChild.setAttribute('class', 'de-fav-inf-icon de-fav-unavail');
iconEl.title = value;
return;
} else if(mode === 'update') {
iconEl.firstElementChild.setAttribute('class', 'de-fav-inf-icon');
iconEl.removeAttribute('title');
}
oldEl.textContent = value[0];
newEl.textContent = value[1];
youEl.textContent = value[2];
}
// Removing previously marked entries from Favorites
async function remove404Favorites(favObj) {
const els = $Q('.de-entry[de-removed]');
const len = els.length;
if(!len) {
return;
}
if(!favObj) {
favObj = await readFavorites();
}
for(let i = 0; i < len; ++i) {
const el = els[i];
const host = el.getAttribute('de-host');
const board = el.getAttribute('de-board');
const num = +el.getAttribute('de-num');
removeFavEntry(favObj, host, board, num);
toggleThrFavBtn(host, board, num, false);
}
saveRenewFavorites(favObj);
}
// Checking if post contains reply links to my posts
function isPostRefToYou(post, myPosts) {
if(Cfg.markMyPosts && (myPosts || MyPosts)) {
const isMatch = myPosts ? num => myPosts[num] : num => MyPosts.has(num);
const links = $Q(aib.qPostMsg.split(', ').join(' a, ') + ' a', post);
for(let a = 0, linksLen = links.length; a < linksLen; ++a) {
const tc = links[a].textContent;
if(tc[0] === '>' && tc[1] === '>' && isMatch(parseInt(tc.substr(2), 10))) {
return true;
}
}
}
return false;
}
// Checking threads for availability and new posts
async function refreshFavorites(needClear404) {
let isUpdate = false;
let isLast404 = false;
const favObj = await readFavorites();
const myPosts = JSON.parse(locStorage['de-myposts'] || '{}');
const parentEl = $q('.de-fav-table');
const entryEls = $Q('.de-entry');
for(let i = 0, len = entryEls.length; i < len; ++i) {
const entryEl = entryEls[i];
const [titleEl, youEl, newEl, totalEl] = [...entryEl.lastElementChild.children];
const iconEl = titleEl.firstElementChild;
const host = entryEl.getAttribute('de-host');
const board = entryEl.getAttribute('de-board');
const num = entryEl.getAttribute('de-num');
const url = entryEl.getAttribute('de-url');
const entry = favObj[host][board][num];
if(entry.err === 'Archived') {
continue;
}
if(host !== aib.host || entry.err === 'Closed') {
if(needClear404) {
parentEl.classList.add('de-fav-table-unfold');
const oldClassName = iconEl.getAttribute('class');
const oldTitle = titleEl.title;
// setAttribute for class is used for correct SVG work in old browsers
iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-wait');
titleEl.title = Lng.updating[lang];
try {
await $ajax(url, null, true);
iconEl.setAttribute('class', oldClassName);
if(oldTitle) {
titleEl.title = oldTitle;
} else {
titleEl.removeAttribute('title');
}
isLast404 = false;
if(entry.err && entry.err !== 'Closed') {
delete entry.err;
isUpdate = true;
}
} catch(err) {
if((err instanceof AjaxError) && err.code === 404) { // Check for 404 error twice
if(!isLast404) {
isLast404 = true;
--i; // Repeat this cycle again
continue;
}
Thread.removeSavedData(board, num); // Not working yet
}
entryEl.setAttribute('de-removed', ''); // Mark an entry as deleted
iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-unavail');
titleEl.title = entry.err = getErrorMessage(err);
isLast404 = false;
isUpdate = true;
}
}
continue;
}
let formEl, isArchived;
iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-wait');
titleEl.title = Lng.updating[lang];
try {
if(aib.hasArchive) {
[formEl, isArchived] = await ajaxLoad(url, true, false, true);
} else {
formEl = await ajaxLoad(url);
}
isLast404 = false;
} catch(err) {
if((err instanceof AjaxError) && err.code === 404) {
if(!isLast404) {
isLast404 = true;
--i;
continue;
}
Thread.removeSavedData(board, num);
}
$hide(newEl);
$hide(youEl);
entryEl.setAttribute('de-removed', '');
iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-unavail');
titleEl.title = entry.err = getErrorMessage(err);
isLast404 = false;
isUpdate = true;
continue;
}
if(aib.qClosed && $q(aib.qClosed, formEl)) {
// Thread is closed
iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-closed');
titleEl.title = Lng.thrClosed[lang];
entry.err = 'Closed';
isUpdate = true;
} else if(isArchived) {
// Thread is archived
iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-closed');
titleEl.title = Lng.thrArchived[lang];
entry.err = 'Archived';
isUpdate = true;
} else {
// Thread is available and not closed
iconEl.setAttribute('class', 'de-fav-inf-icon');
titleEl.removeAttribute('title');
if(entry.err) { // Cancel error status if existed
delete entry.err;
isUpdate = true;
}
}
// Updating the posts counters
let newCount = 0;
let youCount = 0;
const lastNum = entry.last.match(/\d+$/)?.[0] || 0;
const posts = $Q(aib.qPost, formEl);
const postsLen = posts.length;
for(let j = 0; j < postsLen; ++j) {
const post = posts[j];
if(lastNum >= aib.getPNum(post)) {
continue;
}
newCount++;
if(isPostRefToYou(post, myPosts[board])) {
youCount++;
}
}
if(newCount !== entry.new || entry.cnt !== postsLen + 1) {
isUpdate = true;
}
totalEl.textContent = entry.cnt = postsLen + 1;
if(newCount) {
newEl.textContent = entry.new = newCount;
$show(newEl);
if(youCount) {
youEl.textContent = entry.you = youCount;
$show(youEl);
}
} else {
$hide(newEl);
$hide(youEl);
}
}
AjaxCache.clearCache();
if(needClear404) {
if(isUpdate) {
remove404Favorites(favObj);
}
parentEl.classList.remove('de-fav-table-unfold');
} else if(isUpdate) {
saveFavorites(favObj);
}
}
function showFavoritesWindow(winBody, favObj) {
let html = '';
// Create the list of favorite threads
for(const host in favObj) {
if(!$hasProp(favObj, host)) {
continue;
}
const boards = favObj[host];
for(const board in boards) {
if(!$hasProp(boards, board)) {
continue;
}
const threads = boards[board];
const hb = `de-host="${ host }" de-board="${ board }"`;
const delBtn = `
`;
let tNums;
const tArr = Object.entries(threads);
switch(Cfg.favThrOrder) {
case 0: tNums = tArr; break;
case 1: tNums = tArr.reverse(); break;
case 2: tNums = tArr.sort((a, b) => (a[1].time || 0) - (b[1].time || 0)); break;
case 3: tNums = tArr.sort((a, b) => (b[1].time || 0) - (a[1].time || 0));
}
let innerHtml = '';
for(let i = 0, len = tNums.length; i < len; ++i) {
const tNum = tNums[i][0];
if(tNum === 'url' || tNum === 'hide') {
continue;
}
const entry = threads[tNum];
// Generate DOM for separate entry
const favLinkHref = entry.url + (
!entry.last ? '' :
entry.last.startsWith('#') ? entry.last :
host === aib.host ? aib.anchor + entry.last : '');
const favInfIwrapTitle = !entry.err ? '' :
entry.err === 'Closed' ? `title="${ Lng.thrClosed[lang] }"` : `title="${ entry.err }"`;
const favInfIconClass = !entry.err ? '' :
entry.err === 'Closed' || entry.err === 'Archived' ? 'de-fav-closed' : 'de-fav-unavail';
const favInfYouDisp = entry.you ? '' : ' style="display: none;"';
const favInfNewDisp = entry.new ? '' : ' style="display: none;"';
innerHtml += `
${ delBtn }
${ tNum }
- ${ entry.txt }
${ entry.you || 0 }
${ entry.new || 0 }
${ entry.cnt }
`;
}
if(!innerHtml) {
continue;
}
const isHide = threads.hide === undefined ? host !== aib.host : threads.hide;
// Building a foldable block for specific board
html += ``;
}
}
// Appending DOM and events
if(html) {
$bEnd(winBody, `${ html }
`).addEventListener('click', e => {
let el = nav.fixEventEl(e.target);
let parentEl = el.parentNode;
if(el.tagName.toLowerCase() === 'svg') {
el = parentEl;
parentEl = parentEl.parentNode;
}
switch(el.className) {
case 'de-fav-link':
sesStorage['de-fav-win'] = '1'; // Favorites will open again after following a link
// We need to scroll to last seen post after following a link,
// remembering of scroll position is no longer needed
sesStorage.removeItem('de-scroll-' +
parentEl.getAttribute('de-board') + (parentEl.getAttribute('de-num') || ''));
break;
case 'de-fav-del-btn': {
const wasChecked = el.hasAttribute('de-checked');
const toggleFn = btnEl => btnEl.toggleAttribute('de-checked', !wasChecked);
toggleFn(el);
if(parentEl.className === 'de-fav-header') {
// Select/unselect all checkboxes in board block
const entriesEl = parentEl.nextElementSibling;
$Q('.de-fav-del-btn', entriesEl).forEach(toggleFn);
if(!wasChecked && entriesEl.classList.contains('de-fav-entries-hide')) {
entriesEl.classList.remove('de-fav-entries-hide');
}
}
const isShowDelBtns = !!$q('.de-entry > .de-fav-del-btn[de-checked]', winBody);
$toggle($id('de-fav-buttons'), !isShowDelBtns);
$toggle($id('de-fav-del-confirm'), isShowDelBtns);
break;
}
case 'de-abtn de-fav-header-btn': {
const entriesEl = parentEl.nextElementSibling;
const isHide = !entriesEl.classList.contains('de-fav-entries-hide');
el.innerHTML = isHide ? '▼' : '▲';
favObj[entriesEl.getAttribute('de-host')][entriesEl.getAttribute('de-board')].hide = isHide;
saveFavorites(favObj);
e.preventDefault();
entriesEl.classList.toggle('de-fav-entries-hide');
}
}
});
} else {
winBody.insertAdjacentHTML('beforeend', `${ Lng.noFavThr[lang] } `);
}
const btns = $bEnd(winBody, '
');
btns.append(
// "Edit" button. Calls a popup with editor to edit Favorites in JSON.
getEditButton('favor', fn => readFavorites().then(favObj => fn(favObj, true, saveRenewFavorites))),
// "Refresh" button. Updates counters of new posts for each thread entry.
$button(Lng.refresh[lang], Lng.refreshCounters[lang], () => refreshFavorites(false)),
// "Clear" button. Updates counters of new posts and clears 404 threads.
$button(Lng.clear[lang], Lng.refreshClear404[lang], () => refreshFavorites(true)),
// "Page" button. Shows on which page every thread is existed.
$button(Lng.page[lang], Lng.infoPage[lang], async () => {
const els = $Q('.de-fav-current > .de-fav-entries > .de-entry');
const len = els.length;
if(!len) { // Cancel if no existed entries
return;
}
$popup('load-pages', Lng.loading[lang], true);
// Create indexed array of entries and "waiting" SVG icon for each entry
const thrInfo = [];
for(let i = 0; i < len; ++i) {
const el = els[i];
const iconEl = $q('.de-fav-inf-icon', el);
const titleEl = iconEl.parentNode;
thrInfo.push({
found : false,
num : +el.getAttribute('de-num'),
pageEl : $q('.de-fav-inf-page', el),
iconClass : iconEl.getAttribute('class'),
iconEl,
iconTitle : titleEl.getAttribute('title'),
titleEl
});
iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-wait');
titleEl.title = Lng.updating[lang];
}
// Sequentially load pages and search for favorites threads
// We cannot know a count of pages while in the thread
const endPage = (aib.lastPage || 10) + 1; // Check up to 10 page, if we donʼt know
let infoLoaded = 0;
const updateInf = (inf, page) => {
inf.iconEl.setAttribute('class', inf.iconClass);
if(inf.iconTitle) {
inf.titleEl.title = inf.iconTitle;
} else {
inf.titleEl.removeAttribute('title');
}
inf.pageEl.textContent = '@' + page;
};
for(let page = 0; page < endPage; ++page) {
const tNums = new Set();
try {
const form = await ajaxLoad(aib.getPageUrl(aib.b, page));
const els = DelForm.getThreads(form);
for(let i = 0, len = els.length; i < len; ++i) {
tNums.add(aib.getTNum(els[i]));
}
} catch(err) {
continue;
}
// Search for threads on current page
for(let i = 0; i < len; ++i) {
const inf = thrInfo[i];
if(tNums.has(inf.num)) {
updateInf(inf, page);
inf.found = true;
infoLoaded++;
}
}
if(infoLoaded === len) { // Stop pages loading when all favorite threads checked
break;
}
}
// Process missed threads that not found
for(let i = 0; i < len; ++i) {
const inf = thrInfo[i];
if(!inf.found) {
updateInf(inf, '?');
}
}
closePopup('load-pages');
})
);
// Deletion of confirm/cancel buttons
const delBtns = $bEnd(winBody, '
');
delBtns.append(
$button(Lng.remove[lang], Lng.delEntries[lang], () => {
$Q('.de-entry > .de-fav-del-btn[de-checked]', winBody).forEach(
el => el.parentNode.setAttribute('de-removed', ''));
remove404Favorites();
$show(btns);
$hide(delBtns);
}),
$button(Lng.cancel[lang], '', () => {
$Q('.de-fav-del-btn', winBody).forEach(el => el.removeAttribute('de-checked'));
$show(btns);
$hide(delBtns);
})
);
}
/* ==[ WindowSettings.js ]====================================================================================
WINDOW: SETTINGS
=========================================================================================================== */
const CfgWindow = {
initCfgWindow(winBody) {
['click', 'mouseover', 'mouseout', 'change', 'keyup', 'keydown', 'scroll'].forEach(
e => winBody.addEventListener(e, this));
// Create tab bar and bottom buttons
let div = $bEnd(winBody, `${
this._getTab('filters') +
this._getTab('posts') +
this._getTab('images') +
this._getTab('links') +
(postform.form || postform.oeForm ? this._getTab('form') : '') +
this._getTab('common') +
this._getTab('info')
}
${ this._getSel('language') }
`);
// Open default or current tab
this._clickTab(Cfg.cfgTab);
div.append(
// "Edit" button. Calls a popup with editor to edit Settings in JSON.
getEditButton('cfg', fn => fn(Cfg, true, async data => {
await CfgSaver.saveObj(aib.domain, () => data);
deWindow.location.reload();
})),
// "Global" button. Allows to save/load global settings.
nav.hasGlobalStorage ? $button(Lng.global[lang], Lng.globalCfg[lang], () => {
const el = $popup('cfg-global', `${ Lng.globalCfg[lang] }: `);
// "Load" button. Applies global settings for current domain.
$bEnd(el, ` ${ Lng.loadGlobal[lang] }
`
).firstElementChild.onclick = async () => {
const data = await getStoredObj('DESU_Config');
if(data && ('global' in data) && !$isEmpty(data.global)) {
await CfgSaver.saveObj(aib.domain, () => data.global);
deWindow.location.reload();
} else {
$popup('err-noglobalcfg', Lng.noGlobalCfg[lang]);
}
};
// "Save" button. Copies the domain settings into global.
div = $bEnd(el, ` ${ Lng.saveGlobal[lang] }
`
).firstElementChild.onclick = async () => {
const data = await getStoredObj('DESU_Config');
const obj = {};
const com = data[aib.domain];
for(const i in com) {
if(i !== 'correctTime' && i !== 'timePattern' && i !== 'userCSS' &&
i !== 'userCSSTxt' && i !== 'stats' && com[i] !== defaultCfg[i]
) {
obj[i] = com[i];
}
}
data.global = obj;
await CfgSaver.saveObj('global', () => data.global);
toggleWindow('cfg', true);
};
el.insertAdjacentHTML('beforeend', `${ Lng.descrGlobal[lang] } `);
}) : '',
// "File" button. Allows to save and load settings/favorites/hidden/etc from file.
!nav.isPresto ? $button(Lng.file[lang], Lng.fileImpExp[lang], () => {
const list = this._getList([
Lng.panelBtn.cfg[lang] + ' ' + Lng.allDomains[lang],
Lng.panelBtn.fav[lang],
Lng.hidPostThr[lang] + ` (${ aib.domain })`,
Lng.myPosts[lang] + ` (${ aib.domain })`
]);
// Create popup with controls
$popup('cfg-file', `${ Lng.fileImpExp[lang] }: `);
// Import data from a file to the storage
$id('de-import-file').onchange = e => {
const file = e.target.files[0];
if(!file) {
return;
}
readFile(file, true).then(({ data }) => {
let obj;
try {
obj = JSON.parse(data);
} catch(err) {
$popup('err-invaliddata', Lng.invalidData[lang]);
return;
}
const { settings: cfgObj, favorites: favObj, [aib.domain]: domainObj } = obj;
const isOldCfg = !cfgObj && !favObj && !domainObj;
if(isOldCfg) {
setStored('DESU_Config', data);
}
if(cfgObj) {
try {
setStored('DESU_Config', JSON.stringify(cfgObj));
setStored('DESU_keys', JSON.stringify(obj.hotkeys));
} catch(err) {}
}
if(favObj) {
saveRenewFavorites(favObj);
}
if(domainObj) {
if(domainObj.posts) {
locStorage['de-posts'] = JSON.stringify(domainObj.posts);
}
if(domainObj.threads) {
locStorage['de-threads'] = JSON.stringify(domainObj.threads);
}
if(domainObj.myposts) {
locStorage['de-myposts'] = JSON.stringify(domainObj.myposts);
}
}
if(cfgObj || domainObj || isOldCfg) {
$popup('cfg-file', Lng.updating[lang], true);
deWindow.location.reload();
return;
}
closePopup('cfg-file');
});
};
// Export data from a storage to the file. The file will be named by date and type of storage.
// For example, like "DE_20160727_1540_Cfg+Fav+domain.com(Hid+You).json".
const expFile = $id('de-export-file');
const els = $Q('input', expFile.nextElementSibling);
els[0].checked = true;
expFile.addEventListener('click', async e => {
const name = [];
const nameDomain = [];
const d = new Date();
let val = [];
let valDomain = [];
for(let i = 0, len = els.length; i < len; ++i) {
if(!els[i].checked) {
continue;
}
switch(i) {
case 0: name.push('Cfg'); {
const cfgData = await Promise.all(
[getStored('DESU_Config'), getStored('DESU_keys')]);
val.push(`"settings":${ cfgData[0] }`, `"hotkeys":${ cfgData[1] || '""' }`);
break;
}
case 1: name.push('Fav');
val.push(`"favorites":${ await getStored('DESU_Favorites') || '{}' }`);
break;
case 2: nameDomain.push('Hid');
valDomain.push(`"posts":${ locStorage['de-posts'] || '{}' }`,
`"threads":${ locStorage['de-threads'] || '{}' }`);
break;
case 3: nameDomain.push('You');
valDomain.push(`"myposts":${ locStorage['de-myposts'] || '{}' }`);
}
}
if((valDomain = valDomain.join(','))) {
val.push(`"${ aib.domain }":{${ valDomain }}`);
name.push(`${ aib.domain } (${ nameDomain.join('+') })`);
}
if((val = val.join(','))) {
downloadBlob(new Blob([`{${ val }}`], { type: 'application/json' }),
`DE_${ d.getFullYear() }${ pad2(d.getMonth() + 1) }${ pad2(d.getDate()) }_${
pad2(d.getHours()) }${ pad2(d.getMinutes()) }_${ name.join('+') }.json`);
}
e.preventDefault();
}, true);
}) : '',
// "Clear" button. Allows to clear settings/favorites/hidden/etc optionally.
$button(Lng.reset[lang] + '…', Lng.resetCfg[lang], () => $popup(
'cfg-reset',
`${ Lng.resetData[lang] }: ` +
`${ aib.domain }: ${
this._getList([Lng.panelBtn.cfg[lang], Lng.hidPostThr[lang], Lng.myPosts[lang]])
}
` +
`${ Lng.allDomains[lang] }: ${
this._getList([Lng.panelBtn.cfg[lang], Lng.panelBtn.fav[lang]])
}
`
).append($button(Lng.clear[lang], '', e => {
const els = $Q('input[type="checkbox"]', e.target.parentNode);
for(let i = 1, len = els.length; i < len; ++i) {
if(!els[i].checked) {
continue;
}
switch(i) {
case 1:
locStorage.removeItem('de-posts');
locStorage.removeItem('de-threads');
break;
case 2: locStorage.removeItem('de-myposts'); break;
case 4: delStored('DESU_Favorites');
}
}
if(els[3].checked) {
delStored('DESU_Config');
delStored('DESU_keys');
} else if(els[0].checked) {
getStoredObj('DESU_Config').then(data => {
delete data[aib.domain];
setStored('DESU_Config', JSON.stringify(data));
$popup('cfg-reset', Lng.updating[lang], true);
deWindow.location.reload();
});
return;
}
$popup('cfg-reset', Lng.updating[lang], true);
deWindow.location.reload();
})))
);
},
// Event handler for Setting window and its controls.
async handleEvent(e) {
const { type, target: el } = e;
const tag = el.tagName.toLowerCase();
const { classList } = el;
if(type === 'mouseover' && classList.contains('de-cfg-needreload') && !el.title) {
el.title = Lng.cfgNeedReload[lang];
}
if(type === 'click' && tag === 'div' && classList.contains('de-cfg-tab')) {
const info = el.getAttribute('info');
this._clickTab(info);
await CfgSaver.save('cfgTab', info);
}
if(type === 'change' && tag === 'select') {
const info = el.getAttribute('info');
await CfgSaver.save(info, el.selectedIndex);
this._updateDependant();
switch(info) {
case 'language':
lang = el.selectedIndex;
Panel.removeMain();
if(postform.form) {
postform.addMarkupPanel();
postform.setPlaceholders();
aib.updateSubmitBtn(postform.subm);
if(postform.files) {
$Q('.de-file-img, .de-file-txt-input', postform.form).forEach(
el => (el.title = Lng.youCanDrag[lang]));
}
}
this._updateCSS();
Panel.initPanel(DelForm.first.el);
toggleWindow('cfg', false);
break;
case 'delHiddPost': {
const isHide = Cfg.delHiddPost === 1 || Cfg.delHiddPost === 2;
for(let post = Thread.first.op; post; post = post.next) {
if(post.isHidden && !post.isOp) {
post.wrap.classList.toggle('de-hidden', isHide);
}
}
updateCSS();
break;
}
case 'postBtnsCSS':
updateCSS();
if(nav.isPresto) {
$q('.de-svg-icons').remove();
addSVGIcons();
}
break;
case 'thrBtns':
case 'noSpoilers':
case 'resizeImgs': updateCSS(); break;
case 'expandImgs':
updateCSS();
AttachedImage.closeImg();
break;
case 'imgNames':
if(Cfg.imgNames) {
for(const { el } of DelForm) {
processImgInfoLinks(el, 0, Cfg.imgNames);
}
} else {
$Q('.de-img-name').forEach(el => (el.textContent = el.getAttribute('de-img-name-old')));
}
updateCSS();
break;
case 'fileInputs':
postform.files.changeMode();
postform.setPlaceholders();
updateCSS();
break;
case 'addPostForm':
postform.isBottom = Cfg.addPostForm === 1;
postform.setReply(false, !aib.t || Cfg.addPostForm > 1);
break;
case 'addTextBtns': postform.addMarkupPanel();
/* falls through */
case 'scriptStyle':
case 'panelCounter': this._updateCSS(); break;
case 'favThrOrder':
readFavorites().then(favObj => {
const winBody = $q('#de-win-fav > .de-win-body');
winBody.innerHTML = '';
showFavoritesWindow(winBody, favObj);
});
}
return;
}
if(type === 'click' && tag === 'input' && el.type === 'checkbox') {
const info = el.getAttribute('info');
await toggleCfg(info);
this._updateDependant();
switch(info) {
case 'expandTrunc':
case 'widePosts':
case 'showHideBtn':
case 'showRepBtn':
case 'noPostNames':
case 'imgNavBtns':
case 'strikeHidd':
case 'removeHidd':
case 'noBoardRule':
case 'favFolders':
case 'userCSS': updateCSS(); break;
case 'hideBySpell': await Spells.toggle(); break;
case 'sortSpells':
if(Cfg.sortSpells) {
await Spells.toggle();
}
break;
case 'hideRefPsts':
for(let post = Thread.first.op; post; post = post.next) {
if(!Cfg.hideRefPsts) {
post.ref.unhideRef();
} else if(post.isHidden) {
post.ref.hideRef();
}
}
break;
case 'ajaxUpdThr':
if(aib.t) {
if(Cfg.ajaxUpdThr) {
updater.enableUpdater();
} else {
updater.disableUpdater();
}
}
break;
case 'updCount': updater.toggleCounter(Cfg.updCount); break;
case 'desktNotif':
if(Cfg.desktNotif) {
Notification.requestPermission();
}
break;
case 'markNewPosts': Post.clearMarks(); break;
case 'markMyPosts':
case 'markMyLinks':
if(!Cfg.markMyPosts && !Cfg.markMyLinks) {
locStorage.removeItem('de-myposts');
MyPosts.purge();
}
updateCSS();
break;
case 'correctTime': await DateTime.toggleSettings(el); break;
case 'imgInfoLink': {
const img = $q('.de-fullimg-wrap');
if(img) {
img.click();
}
updateCSS();
break;
}
case 'imgSrcBtns':
if(Cfg.imgSrcBtns) {
for(const { el } of DelForm) {
processImgInfoLinks(el, 1, 0);
$Q('.de-img-embed').forEach(
el => addImgButtons(el.parentNode.nextSibling.nextSibling));
}
} else {
$delAll('.de-btn-img');
}
break;
case 'addSageBtn':
PostForm.hideField(postform.mail.closest('label') || postform.mail);
setTimeout(() => postform.toggleSage(), 0);
updateCSS();
break;
case 'altCaptcha': postform.cap.initCapPromise(); break;
case 'txtBtnsLoc':
postform.addMarkupPanel();
updateCSS();
break;
case 'userPassw': await PostForm.setUserPassw(); break;
case 'userName': await PostForm.setUserName(); break;
case 'noPassword': $toggle(postform.passw.closest(aib.qFormTr)); break;
case 'noName': PostForm.hideField(postform.name); break;
case 'noSubj': PostForm.hideField(postform.subj); break;
case 'inftyScroll': toggleInfinityScroll(); break;
case 'hotKeys':
if(Cfg.hotKeys) {
HotKeys.enableHotKeys();
} else {
HotKeys.disableHotKeys();
}
}
return;
}
if(type === 'click' && tag === 'input' && el.type === 'button') {
switch(el.id) {
case 'de-cfg-button-pass':
$q('input[info="passwValue"]').value = Math.round(Math.random() * 1e12).toString(32);
await PostForm.setUserPassw();
break;
case 'de-cfg-button-keys':
e.preventDefault();
if($id('de-popup-edit-hotkeys')) {
return;
}
Promise.resolve(HotKeys.readKeys()).then(keys => {
const temp = KeyEditListener.getEditMarkup(keys);
const el = $popup('edit-hotkeys', temp[1]);
const fn = new KeyEditListener(el, keys, temp[0]);
['focus', 'blur', 'click', 'keydown', 'keyup'].forEach(
e => el.addEventListener(e, fn, true));
});
break;
case 'de-cfg-button-updnow':
$popup('updavail', Lng.loading[lang], true);
getStoredObj('DESU_Config')
.then(data => checkForUpdates(true, data.lastUpd))
.then(html => $popup('updavail', html), Function.prototype);
break;
case 'de-cfg-button-donate': showDonateMsg(); break;
case 'de-cfg-button-debug': {
const perf = {};
const arr = Logger.getLogData(true);
for(let i = 0, len = arr.length; i < len; ++i) {
perf[arr[i][0]] = arr[i][1];
}
$popup('cfg-debug', Lng.infoDebug[lang] + ':'
).firstElementChild.value = JSON.stringify({
version : version + '.' + commit,
location : String(deWindow.location),
nav,
Cfg,
sSpells : Spells.list.split('\n'),
oSpells : sesStorage[`de-spells-${ aib.b }${ aib.t || '' }`],
perf
}, (key, value) => {
switch(key) {
case 'stats':
case 'nameValue':
case 'passwValue':
case 'ytApiKey': return undefined;
}
return key in defaultCfg && value === defaultCfg[key] ? undefined : value;
}, '\t');
}
}
}
if(type === 'keyup' && tag === 'input' && el.type === 'text') {
const info = el.getAttribute('info');
switch(info) {
case 'postBtnsBack': {
let isValidColor = false;
const color = el.value;
if(color === 'transparent') {
isValidColor = true;
} else if(color && color !== 'inherit' && color !== 'currentColor') {
const image = doc.createElement('img');
image.style.color = 'rgb(0, 0, 0)';
image.style.color = color;
if(image.style.color !== 'rgb(0, 0, 0)') {
isValidColor = true;
}
image.style.color = 'rgb(255, 255, 255)';
image.style.color = color;
isValidColor = image.style.color !== 'rgb(255, 255, 255)';
}
classList.toggle('de-input-error', !isValidColor);
if(isValidColor) {
await CfgSaver.save('postBtnsBack', el.value);
updateCSS();
}
break;
}
case 'limitPostMsg':
await CfgSaver.save('limitPostMsg', Math.max(+el.value || 0, 50));
updateCSS();
break;
case 'minImgSize':
await CfgSaver.save('minImgSize', Math.min(Math.max(+el.value, 1)), Cfg.maxImgSize);
break;
case 'maxImgSize': await CfgSaver.save('maxImgSize', Math.max(+el.value, Cfg.minImgSize)); break;
case 'zoomFactor':
await CfgSaver.save('zoomFactor', Math.min(Math.max(+el.value, 1), 100));
break;
case 'webmVolume': {
const val = Math.min(+el.value || 0, 100);
await CfgSaver.save('webmVolume', val);
sendStorageEvent('__de-webmvolume', val);
break;
}
case 'minWebmWidth':
await CfgSaver.save('minWebmWidth', Math.max(+el.value, Cfg.minImgSize));
break;
case 'maskVisib':
await CfgSaver.save('maskVisib', Math.min(+el.value || 0, 100));
updateCSS();
break;
case 'linksOver': await CfgSaver.save('linksOver', +el.value | 0); break;
case 'linksOut': await CfgSaver.save('linksOut', +el.value | 0); break;
case 'ytApiKey': await CfgSaver.save('ytApiKey', el.value.trim()); break;
case 'passwValue': await PostForm.setUserPassw(); break;
case 'nameValue': await PostForm.setUserName(); break;
default: await CfgSaver.save(info, el.value);
}
return;
}
if(tag === 'a') {
if(el.id === 'de-btn-spell-add') {
switch(e.type) {
case 'click': e.preventDefault(); break;
case 'mouseover': el.odelay = setTimeout(() => addMenu(el), Cfg.linksOver); break;
case 'mouseout': clearTimeout(el.odelay);
}
return;
}
if(type === 'click') {
switch(el.id) {
case 'de-btn-spell-apply':
e.preventDefault();
await CfgSaver.save('hideBySpell', 1);
$q('input[info="hideBySpell"]').checked = true;
await Spells.toggle();
break;
case 'de-btn-spell-clear':
e.preventDefault();
if(!confirm(Lng.clear[lang] + '?')) {
return;
}
$id('de-spell-txt').value = '';
await Spells.toggle();
}
}
return;
}
if(tag === 'textarea' && el.id === 'de-spell-txt' && (type === 'keydown' || type === 'scroll')) {
this._updateRowMeter(el);
}
},
// Switch content in Settings by clicking on tab
_clickTab(info) {
const el = $q(`.de-cfg-tab[info="${ info }"]`);
if(el.hasAttribute('selected')) {
return;
}
const prefTab = $q('.de-cfg-body');
if(prefTab) {
prefTab.className = 'de-cfg-unvis';
$q('.de-cfg-tab[selected]').removeAttribute('selected');
}
el.setAttribute('selected', '');
const id = el.getAttribute('info');
let newTab = $id('de-cfg-' + id);
if(!newTab) {
newTab = $aEnd($id('de-cfg-bar'),
id === 'filters' ? this._getCfgFilters() :
id === 'posts' ? this._getCfgPosts() :
id === 'images' ? this._getCfgImages() :
id === 'links' ? this._getCfgLinks() :
id === 'form' ? this._getCfgForm() :
id === 'common' ? this._getCfgCommon() :
this._getCfgInfo());
if(id === 'filters') {
this._updateRowMeter($id('de-spell-txt'));
}
if(id === 'common') {
// XXX: remove and make insertion in this._getCfgCommon()
$q('input[info="userCSS"]').parentNode.after(getEditButton(
'css',
fn => fn(Cfg.userCSSTxt, false, async inputEl => {
await CfgSaver.save('userCSSTxt', inputEl.value);
updateCSS();
toggleWindow('cfg', true);
}),
'de-cfg-button'
));
}
}
newTab.className = 'de-cfg-body';
if(id === 'filters') {
$id('de-spell-txt').value = Spells.list;
}
this._updateDependant();
// Updates all inputs according to config
const els = $Q('.de-cfg-chkbox, .de-cfg-inptxt, .de-cfg-select', newTab.parentNode);
for(let i = 0, len = els.length; i < len; ++i) {
const el = els[i];
const info = el.getAttribute('info');
if(el.tagName.toLowerCase() === 'input') {
if(el.type === 'checkbox') {
el.checked = !!Cfg[info];
} else {
el.value = Cfg[info];
}
} else {
el.selectedIndex = Cfg[info];
}
}
},
// "Filters" tab
_getCfgFilters() {
return `
${ this._getBox('sortSpells') }
${ this._getBox('hideRefPsts') }
${ this._getBox('nextPageThr') }
${ this._getSel('delHiddPost') }
`;
},
// "Posts" tab
_getCfgPosts() {
return `
${ localData ? '' : `${ this._getBox('ajaxUpdThr') }
${ this._getInp('updThrDelay') }
${ this._getBox('updCount') }
${ this._getBox('favIcoBlink') }
${ 'Notification' in deWindow ? this._getBox('desktNotif') + ' ' : '' }
${ this._getBox('markNewPosts') }
` }
${ this._getBox('markMyPosts') }
${ !localData ? `${ this._getBox('expandTrunc', true) }
` : '' }
${ this._getBox('widePosts') }
${ this._getInp('limitPostMsg', true, 5) }
${ this._getSel('showHideBtn') }
${ !localData ? this._getSel('showRepBtn') : '' }
${ this._getSel('postBtnsCSS') }
${ this._getInp('postBtnsBack', false, 8) }
${ !localData ? this._getSel('thrBtns') : '' }
${ this._getSel('noSpoilers') }
${ this._getBox('noPostNames') }
${ this._getBox('correctTime', true) }
${ this._getInp('timeOffset', true, 1) }
[?]
${ this._getInp('timePattern', true, 24) }
${ this._getInp('timeRPattern', true, 24) }
`;
},
// "Images" tab
_getCfgImages() {
return `
${ this._getSel('expandImgs') }
${ this._getBox('imgNavBtns') }
${ this._getBox('imgInfoLink') }
${ this._getSel('resizeImgs') }
${ Post.sizing.dPxRatio > 1 ? this._getBox('resizeDPI') + ' ' : '' }
${ this._getInp('minImgSize') }${ this._getInp('maxImgSize') }
${ this._getInp('zoomFactor') }
${ this._getBox('webmControl') }
${ this._getBox('webmTitles') }
${ this._getInp('webmVolume') }
${ this._getInp('minWebmWidth') }
${ nav.isPresto ? '' : this._getSel('preLoadImgs', true) + '
' }
${ nav.isPresto || aib._4chan ? '' : `
${ this._getBox('findImgFile', true) }
` }
${ this._getSel('openImgs', true) }
${ this._getBox('imgSrcBtns') }
${ this._getSel('imgNames') }
${ this._getInp('maskVisib') }
`;
},
// "Links" tab
_getCfgLinks() {
return `
${ this._getBox('linksNavig', true) }
${ this._getInp('linksOver') }
${ this._getInp('linksOut') }
${ this._getBox('markViewed') }
${ this._getBox('strikeHidd') }
${ this._getBox('removeHidd') }
${ this._getBox('noNavigHidd') }
${ this._getBox('markMyLinks') }
${ this._getBox('crossLinks', true) }
${ this._getBox('decodeLinks', true) }
${ this._getBox('insertNum') }
${ !localData ? `${ this._getBox('addOPLink') }
${ this._getBox('addImgs', true) }
` : '' }
${ this._getBox('addMP3', true) }
${ this._getBox('addVocaroo', true) }
${ this._getSel('embedYTube', true) }
${ this._getInp('YTubeWidth', false) }\u00D7
${ this._getInp('YTubeHeigh', false) }(px)
${ this._getBox('YTubeTitles', true) }
${ this._getInp('ytApiKey', true, 25) }
${ this._getBox('addVimeo', true) }
`;
},
// "Form" tab
_getCfgForm() {
return `
${ this._getBox('ajaxPosting', true) }
${ postform.form ? `
${ this._getBox('postSameImg') }
${ this._getBox('removeEXIF') }
${ this._getSel('removeFName') }
${ this._getBox('sendErrNotif') }
${ this._getBox('scrAfterRep') }
${ postform.files && !nav.isPresto ? this._getSel('fileInputs') : '' }
` : '' }
${ postform.form ? this._getSel('addPostForm') + '
' : '' }
${ postform.txta ? this._getBox('spacedQuote') + '
' : '' }
${ this._getBox('favOnReply') }
${ postform.subj ? this._getBox('warnSubjTrip') + '
' : '' }
${ postform.mail ? `${ this._getBox('addSageBtn') }
${ this._getBox('saveSage') }
` : '' }
${ postform.cap ? `${ aib.hasAltCaptcha ? `${ this._getBox('altCaptcha') }
` : '' }
${ !aib.makaba ? `${ this._getInp('capUpdTime') }
` : '' }
${ this._getSel('captchaLang') }
` : '' }
${ postform.txta ? `${ this._getSel('addTextBtns') }
${ !aib._4chan ? this._getBox('txtBtnsLoc') : '' }
` : '' }
${ postform.passw ? `${ this._getInp('passwValue', false, 9) }
${ this._getBox('userPassw') }
` : '' }
${ postform.name ? `${ this._getInp('nameValue', false, 9) }
${ this._getBox('userName') }
` : '' }
${ postform.rules || postform.passw || postform.name ? Lng.hide[lang] +
(postform.rules ? this._getBox('noBoardRule') : '') +
(postform.passw ? this._getBox('noPassword') : '') +
(postform.name ? this._getBox('noName') : '') +
(postform.subj ? this._getBox('noSubj') : '') : '' }
`;
},
// "Common" tab
_getCfgCommon() {
return `
${ this._getSel('scriptStyle') }
${ this._getBox('userCSS') }
[?]
${ 'animation' in doc.body.style ? this._getBox('animation') + '
' : '' }
${ this._getBox('hotKeys') }
${ this._getInp('loadPages') }
${ this._getSel('panelCounter') }
${ this._getBox('rePageTitle', true) }
${ !localData ? `${ this._getBox('inftyScroll') }
${ this._getBox('hideReplies', true) }
${ this._getBox('scrollToTop') }
` : '' }
${ this._getBox('saveScroll') }
${ this._getBox('favFolders') }
${ this._getSel('favThrOrder') }
${ this._getBox('favWinOn') }
${ this._getBox('closePopups') }
`;
},
// "Info" tab
_getCfgInfo() {
const statsTable = this._getInfoTable([
[Lng.thrViewed[lang], Cfg.stats.view],
[Lng.thrCreated[lang], Cfg.stats.op],
[Lng.thrHidden[lang], HiddenThreads.getCount()],
[Lng.postsSent[lang], Cfg.stats.reply]
], false);
return ``;
},
// Creates a label with checkbox for option switching
_getBox: (id, needReload) => `
${ Lng.cfg[id][lang] } `,
// Creates a table for Info tab
_getInfoTable: (data, needMs) => data.map(val => `
${ val[0] }
${ val[1] + (needMs ? 'ms' : '') }
`).join(''),
// Creates a text input for text option values
_getInp(id, addText = true, size = 2) {
const el = doc.createElement('div');
el.append(Cfg[id]); // Escape HTML
return `
${ addText && Lng.cfg[id] ? Lng.cfg[id][lang] : '' } `;
},
// Creates a menu with a list of checkboxes. Uses for popup window.
_getList : arr => arrTags(arr, ' ', ' '),
// Creates a select for multiple option values
_getSel : (id, needReload) => `
${ Lng.cfg[id].sel[lang].map((val, i) =>
`${ val } `).join('') } ${ Lng.cfg[id].txt[lang] } `,
// Creates a tab for tab bar
_getTab: id => `${ Lng.cfgTab[id][lang] }
`,
// Switching the dependent inputs according to their parents
_toggleDependant(state, arr) {
let i = arr.length;
const nState = !state;
while(i--) {
const el = $q(arr[i]);
if(el) {
el.disabled = nState;
}
}
},
_updateCSS() {
$delAll('#de-css, #de-css-dynamic, #de-css-user', doc.head);
scriptCSS();
},
_updateDependant() {
const fn = this._toggleDependant;
fn(Cfg.ajaxUpdThr, [
'input[info="updThrDelay"]', 'input[info="updCount"]', 'input[info="favIcoBlink"]',
'input[info="markNewPosts"]', 'input[info="desktNotif"]'
]);
fn(Cfg.postBtnsCSS === 2, ['input[info="postBtnsBack"]']);
fn(Cfg.expandImgs, [
'input[info="imgNavBtns"]', 'input[info="imgInfoLink"]', 'input[info="resizeDPI"]',
'select[info="resizeImgs"]', 'input[info="minImgSize"]', 'input[info="maxImgSize"]',
'input[info="zoomFactor"]', 'input[info="webmControl"]', 'input[info="webmTitles"]',
'input[info="webmVolume"]', 'input[info="minWebmWidth"]'
]);
fn(Cfg.preLoadImgs, ['input[info="findImgFile"]']);
fn(Cfg.linksNavig, [
'input[info="linksOver"]', 'input[info="linksOut"]', 'input[info="markViewed"]',
'input[info="strikeHidd"]', 'input[info="noNavigHidd"]'
]);
fn(Cfg.strikeHidd && Cfg.linksNavig, ['input[info="removeHidd"]']);
fn(Cfg.embedYTube, [
'input[info="YTubeWidth"]', 'input[info="YTubeHeigh"]', 'input[info="YTubeTitles"]',
'input[info="ytApiKey"]', 'input[info="addVimeo"]'
]);
fn(Cfg.YTubeTitles, ['input[info="ytApiKey"]']);
fn(Cfg.ajaxPosting, [
'input[info="postSameImg"]', 'input[info="removeEXIF"]', 'select[info="removeFName"]',
'input[info="sendErrNotif"]', 'input[info="scrAfterRep"]', 'select[info="fileInputs"]'
]);
fn(Cfg.addSageBtn, ['input[info="saveSage"]']);
fn(Cfg.addTextBtns, ['input[info="txtBtnsLoc"]']);
fn(Cfg.hotKeys, ['input[info="loadPages"]']);
},
// Updates row counter in spells editor
_updateRowMeter(node) {
const top = node.scrollTop;
const el = node.previousElementSibling;
let num = el.numLines || 1;
let i = 19;
if(num - i < ((top / 12) | 0 + 1)) {
let str = '';
while(i--) {
str += `${ num++ } `;
}
el.insertAdjacentHTML('beforeend', str);
el.numLines = num;
}
el.scrollTop = top;
}
};
/* ==[ MenuPopups.js ]========================================================================================
POPUPS & MENU
=========================================================================================================== */
function closePopup(data) {
const el = typeof data === 'string' ? $id('de-popup-' + data) : data;
if(el) {
el.closeTimeout = null;
if(Cfg.animation) {
$animate(el, 'de-close', true);
} else {
el.remove();
}
}
}
function $popup(id, txt, isWait = false) {
let el = $id('de-popup-' + id);
const buttonHTML = isWait ? ' ' : '\u2716 ';
if(el) {
$q('div', el).innerHTML = txt.trim();
$q('span', el).innerHTML = buttonHTML;
if(!isWait && Cfg.animation) {
$animate(el, 'de-blink');
}
} else {
el = $bEnd($id('de-wrapper-popup'), ``);
el.onclick = e => {
let el = nav.fixEventEl(e.target);
el = el.tagName.toLowerCase() === 'svg' ? el.parentNode : el;
if(el.className === 'de-popup-btn') {
closePopup(el.parentNode);
}
};
if(Cfg.animation) {
$animate(el, 'de-open');
}
}
if(Cfg.closePopups && !isWait && !id.includes('edit') && !id.includes('cfg')) {
el.closeTimeout = setTimeout(closePopup, 6e3, el);
}
return el.lastElementChild;
}
// Adds button that calls a popup with the text editor. Useful to edit settings.
function getEditButton(name, getDataFn, className = 'de-button') {
return $button(Lng.edit[lang], Lng.editInTxt[lang], () => getDataFn((val, isJSON, saveFn) => {
// Create popup window with textarea.
const el = $popup('edit-' + name,
`${ Lng.editor[name][lang] } `);
const inputEl = el.lastChild;
inputEl.value = isJSON ? JSON.stringify(val, null, '\t') : val;
// "Save" button. If there a JSON data, parses and saves on success.
el.append($button(Lng.save[lang], Lng.saveChanges[lang], !isJSON ? () => saveFn(inputEl) : () => {
let data;
try {
data = JSON.parse(inputEl.value.trim().replace(/[\n\r\t]/g, '') || '{}');
} catch(err) {}
if(!data) {
$popup('err-invaliddata', Lng.invalidData[lang]);
return;
}
saveFn(data);
closePopup('edit-' + name);
closePopup('err-invaliddata');
}));
}), className);
}
class Menu {
constructor(parentEl, html, clickFn, isFixed = true) {
this.onout = null;
this.onover = null;
this.onremove = null;
this._closeTO = 0;
const el = $bEnd(doc.body, ``);
const cr = parentEl.getBoundingClientRect();
const { style, offsetWidth: w, offsetHeight: h } = el;
style.left = (isFixed ? 0 : deWindow.pageXOffset) +
(cr.left + w < Post.sizing.wWidth ? cr.left : cr.right - w) + 'px';
style.top = (isFixed ? 0 : deWindow.pageYOffset) +
(cr.bottom + h < Post.sizing.wHeight ? cr.bottom - 0.5 : cr.top - h + 0.5) + 'px';
style.removeProperty('visibility');
this._clickFn = clickFn;
this._el = el;
this.parentEl = parentEl;
['mouseover', 'mouseout'].forEach(e => el.addEventListener(e, this, true));
el.addEventListener('click', this);
parentEl.addEventListener('mouseout', this);
}
static getMenuImg(data, isDlOnly = false) {
let p;
let dlLinks = '';
if(typeof data === 'string') {
p = encodeURIComponent(data) + '" target="_blank">' + Lng.frameSearch[lang];
} else {
const link = data.nextSibling;
const { href } = link;
const origSrc = link.getAttribute('de-href') || href;
p = encodeURIComponent(origSrc) + '" target="_blank">' + Lng.searchIn[lang];
const getDlLnk = (href, name, title, isAddExt) => {
let ext;
if(isAddExt) {
ext = getFileExt(href);
name += '.' + ext;
} else {
ext = getFileExt(name);
}
let nameShort = name;
if(name.length > 20) {
nameShort = name.substr(0, 20 - ext.length) + '\u2026' + ext;
}
const info = aib.domain !== href.match(/^(?:(?:blob:)?https?:\/\/)([^/]+)/)[1] ?
' info="img-load"' : '';
return ``;
};
const name = decodeURIComponent(getFileName(origSrc));
const isFullImg = link.classList.contains('de-fullimg-link');
const realName = isFullImg ? link.textContent :
link.classList.contains('de-img-name') ? aib.getImgRealName(aib.getImgWrap(data)) : name;
if(name !== realName) {
dlLinks += getDlLnk(href, realName, Lng.origName[lang], false);
}
let webmTitle;
if(isFullImg && (webmTitle = $q('.de-webm-title', link.parentNode)?.textContent)) {
dlLinks += getDlLnk(href, webmTitle, Lng.metaName[lang], true);
}
dlLinks += getDlLnk(href, name, Lng.boardName[lang], false);
}
if(aib.kohlchan) {
p = p.replace('kohlchanagb7ih5g.onion', 'kohlchan.net')
.replace('kohlchanvwpfx6hthoti5fvqsjxgcwm3tmddvpduph5fqntv5affzfqd.onion', 'kohlchan.net');
}
return dlLinks + (isDlOnly ? '' : arrTags([
`de-src-google" href="https://lens.google.com/uploadbyurl?url=${ p }Google`,
`de-src-yandex" href="https://yandex.com/images/search?rpt=imageview&url=${ p }Yandex`,
`de-src-tineye" href="https://tineye.com/search/?url=${ p }TinEye`,
`de-src-saucenao" href="https://saucenao.com/search.php?url=${ p }SauceNAO`,
`de-src-iqdb" href="https://iqdb.org/?url=${ p }IQDB`,
`de-src-tracemoe" href="https://trace.moe/?auto&url=${ p }TraceMoe`
], '', '');
switch(el.id) {
case 'de-btn-spell-add':
return new Menu(el, `${
fn('#words,#exp,#exph,#imgn,#ihash,#subj,#name,#trip,#img,#sage'.split(','))
}
${
fn('#op,#tlen,#all,#video,#vauthor,#num,#wipe,#rep,#outrep, '.split(',')) }
`,
({ textContent: s }) => insertText($id('de-spell-txt'), s +
(!aib.t || s === '#op' || s === '#rep' || s === '#outrep' ? '' : `[${ aib.b },${ aib.t }]`) +
(Spells.needArg[Spells.names.indexOf(s.substr(1))] ? '(' : '')));
case 'de-panel-refresh':
return new Menu(el, fn(Lng.selAjaxPages[lang]),
el => Pages.loadPages(Array.prototype.indexOf.call(el.parentNode.children, el) + 1));
case 'de-panel-savethr':
return new Menu(el, fn($q(aib.qPostImg, DelForm.first.el) ?
Lng.selSaveThr[lang] : [Lng.selSaveThr[lang][0]]),
el => {
if($id('de-popup-savethr')) {
return;
}
const imgOnly = !!Array.prototype.indexOf.call(el.parentNode.children, el);
if(ContentLoader.isLoading) {
$popup('savethr', Lng.loading[lang], true);
ContentLoader.afterFn = () => ContentLoader.downloadThread(imgOnly);
ContentLoader.popupId = 'savethr';
} else {
ContentLoader.downloadThread(imgOnly);
}
});
case 'de-panel-audio-off':
return new Menu(el, fn(Lng.selAudioNotif[lang]), el => {
updater.enableUpdater();
updater.toggleAudio(
[3e4, 6e4, 12e4, 3e5][Array.prototype.indexOf.call(el.parentNode.children, el)]);
$id('de-panel-audio-off').id = 'de-panel-audio-on';
});
}
}
/* ==[ Hotkeys.js ]===========================================================================================
HOTKEYS
=========================================================================================================== */
const HotKeys = {
cPost : null,
enabled : false,
gKeys : null,
lastPageOffset : 0,
ntKeys : null,
tKeys : null,
version : 7,
clearCPost() {
this.cPost = null;
this.lastPageOffset = 0;
},
disableHotKeys() {
if(this.enabled) {
this.enabled = false;
if(this.cPost) {
this.cPost.unselect();
}
this.clearCPost();
this.gKeys = this.ntKeys = this.tKeys = null;
doc.removeEventListener('keydown', this, true);
}
},
enableHotKeys() {
if(!this.enabled) {
this.enabled = true;
this._paused = false;
Promise.resolve(this.readKeys()).then(keys => {
if(this.enabled) {
[,, this.gKeys, this.ntKeys, this.tKeys] = keys;
doc.addEventListener('keydown', this, true);
}
});
}
},
getDefaultKeys: () => [HotKeys.version, nav.isFirefox, [
// GLOBAL KEYS
/* One post/thread above */ 0x004B /* = K */,
/* One post/thread below */ 0x004A /* = J */,
/* Reply or create thread */ 0x0052 /* = R */,
/* Hide selected thread/post */ 0x0048 /* = H */,
/* Open previous page/image */ 0x1025 /* = Ctrl+Left */,
/* Send post (txt) */ 0x900D /* = Ctrl+Enter */,
/* Open/close "Favorites" */ 0x4046 /* = Alt+F */,
/* Open/close "Hidden" */ 0x4048 /* = Alt+H */,
/* Open/close panel */ 0x0050 /* = P */,
/* Mask/unmask images */ 0x0042 /* = B */,
/* Open/close "Settings" */ 0x4053 /* = Alt+S */,
/* Expand current image */ 0x0049 /* = I */,
/* Bold text */ 0xC042 /* = Alt+B */,
/* Italic text */ 0xC049 /* = Alt+I */,
/* Strike text */ 0xC054 /* = Alt+T */,
/* Spoiler text */ 0xC050 /* = Alt+P */,
/* Code text */ 0xC043 /* = Alt+C */,
/* Open next page/image */ 0x1027 /* = Ctrl+Right */,
/* Open/close "Video" */ 0x4056 /* = Alt+V */
], [// NON-THREAD KEYS
/* One post above */ 0x004D /* = M */,
/* One post below */ 0x004E /* = N */,
/* Open thread */ 0x0056 /* = V */,
/* Expand thread */ 0x0045 /* = E */
], [// THREAD KEYS
/* Update thread */ 0x0055 /* = U */
]],
handleEvent(e) {
if(this._paused || e.metaKey) {
return;
}
let idx;
const isThr = aib.t;
const el = e.target;
const tag = el.tagName.toLowerCase();
const kc = e.keyCode |
(e.ctrlKey ? 0x1000 : 0) |
(e.shiftKey ? 0x2000 : 0) |
(e.altKey ? 0x4000 : 0) |
(tag === 'textarea' ||
tag === 'input' && (el.type === 'text' || el.type === 'password') ? 0x8000 : 0);
if(kc === 0x74 || kc === 0x8074) { // F5
if(isThr || $id('de-popup-load-pages')) {
return;
}
AttachedImage.closeImg();
Pages.loadPages(+Cfg.loadPages);
} else if(kc === 0x1B) { // ESC
if(AttachedImage.viewer) {
AttachedImage.closeImg();
return;
}
if(this.cPost) {
this.cPost.unselect();
this.cPost = null;
}
if(isThr) {
Post.clearMarks();
}
this.lastPageOffset = 0;
} else if(kc === 0x801B) { // ESC (txt)
el.blur();
} else {
let post;
const globIdx = this.gKeys.indexOf(kc);
switch(globIdx) {
case 2: // Quick reply
if(postform.form) {
post = this.cPost || this._getFirstVisPost(false, true) || Thread.first.op;
this.cPost = post;
postform.showQuickReply(post, post.num, true, false);
post.select();
}
break;
case 3: // Hide selected thread/post
post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
if(post) {
post.setUserVisib(!post.isHidden);
this._scroll(post, false, post.isOp);
}
break;
case 4: // Open previous page/image
if(AttachedImage.viewer) {
AttachedImage.viewer.navigate(false);
} else if(isThr || aib.page !== aib.firstPage) {
deWindow.location.pathname = aib.getPageUrl(aib.b, isThr ? 0 : aib.page - 1);
}
break;
case 5: // Send post (txt)
if(el !== postform.txta && el !== postform.cap.textEl) {
return;
}
postform.subm.click();
break;
case 6: // Open/close "Favorites"
toggleWindow('fav', false);
break;
case 7: // Open/close "Hidden"
toggleWindow('hid', false);
break;
case 8: // Open/close panel
$toggle($id('de-panel-buttons'));
break;
case 9: // Mask/unmask images
toggleCfg('maskImgs').then(() => updateCSS());
break;
case 10: // Open/close "Settings"
toggleWindow('cfg', false);
break;
case 11: // Expand current image
post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
if(post) {
post.toggleImages();
}
break;
case 12: // Bold text (txt)
if(el !== postform.txta) {
return;
}
$id('de-btn-bold').click();
break;
case 13: // Italic text (txt)
if(el !== postform.txta) {
return;
}
$id('de-btn-italic').click();
break;
case 14: // Strike text (txt)
if(el !== postform.txta) {
return;
}
$id('de-btn-strike').click();
break;
case 15: // Spoiler text (txt)
if(el !== postform.txta) {
return;
}
$id('de-btn-spoil').click();
break;
case 16: // Code text (txt)
if(el !== postform.txta) {
return;
}
$id('de-btn-code').click();
break;
case 17: // Open next page/image
if(AttachedImage.viewer) {
AttachedImage.viewer.navigate(true);
} else if(!isThr) {
const pageNum = DelForm.last.pageNum + 1;
if(pageNum <= aib.lastPage) {
deWindow.location.pathname = aib.getPageUrl(aib.b, pageNum);
}
}
break;
case 18: // Open/close "Videos"
toggleWindow('vid', false);
break;
case -1:
if(isThr) {
idx = this.tKeys.indexOf(kc);
if(idx === 0) { // Update thread
updater.forceLoad(null);
break;
}
return;
}
idx = this.ntKeys.indexOf(kc);
if(idx === -1) {
return;
} else if(idx === 2) { // Open thread
post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
if(post) {
if(typeof GM_openInTab === 'function') {
GM_openInTab(aib.getThrUrl(aib.b, post.tNum), false, true);
} else {
deWindow.open(aib.getThrUrl(aib.b, post.tNum), '_blank');
}
}
break;
} else if(idx === 3) { // Expand/collapse thread
post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
if(post) {
if(post.thr.loadCount !== 0 && post.thr.op.next.count === 1) {
const nextThr = post.thr.nextNotHidden;
post.thr.loadPosts(Thread.visPosts, !!nextThr);
post = (nextThr || post.thr).op;
} else {
post.thr.loadPosts('all');
post = post.thr.op;
}
scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + post.top);
if(this.cPost && this.cPost !== post) {
this.cPost.unselect();
this.cPost = post;
}
}
break;
}
/* falls through */
default: {
const scrollToThr = !isThr && (globIdx === 0 || globIdx === 1);
this._scroll(this._getFirstVisPost(scrollToThr, false),
globIdx === 0 || idx === 0, scrollToThr);
}
}
}
e.preventDefault();
e.stopPropagation();
},
pauseHotKeys() {
this._paused = true;
},
async readKeys() {
const str = await getStored('DESU_keys');
if(!str) {
return this.getDefaultKeys();
}
let keys;
try {
keys = JSON.parse(str);
} catch(err) {}
if(!keys) {
return this.getDefaultKeys();
}
if(keys[0] !== this.version) {
const tKeys = this.getDefaultKeys();
switch(keys[0]) {
case 1:
keys[2][11] = tKeys[2][11];
keys[4] = tKeys[4];
/* falls through */
case 2:
keys[2][12] = tKeys[2][12];
keys[2][13] = tKeys[2][13];
keys[2][14] = tKeys[2][14];
keys[2][15] = tKeys[2][15];
keys[2][16] = tKeys[2][16];
/* falls through */
case 3:
keys[2][17] = keys[3][3];
keys[3][3] = keys[3].splice(4, 1)[0];
/* falls through */
case 4:
case 5:
case 6:
keys[2][18] = tKeys[2][18];
}
keys[0] = this.version;
setStored('DESU_keys', JSON.stringify(keys));
}
if(keys[1] ^ nav.isFirefox) {
const mapFunc = nav.isFirefox ?
key => key === 189 ? 173 : key === 187 ? 61 : key === 186 ? 59 : key :
key => key === 173 ? 189 : key === 61 ? 187 : key === 59 ? 186 : key;
keys[1] = nav.isFirefox;
keys[2] = keys[2].map(mapFunc);
keys[3] = keys[3].map(mapFunc);
setStored('DESU_keys', JSON.stringify(keys));
}
return keys;
},
resume(keys) {
[,, this.gKeys, this.ntKeys, this.tKeys] = keys;
this._paused = false;
},
_paused: false,
_getNextVisPost(cPost, isOp, toUp) {
if(isOp) {
const thr = cPost ? toUp ? cPost.thr.prevNotHidden : cPost.thr.nextNotHidden :
Thread.first.isHidden ? Thread.first.nextNotHidden : Thread.first;
return thr ? thr.op : null;
}
return cPost ? cPost.getAdjacentVisPost(toUp) : Thread.first.isHidden ||
Thread.first.op.isHidden ? Thread.first.op.getAdjacentVisPost(toUp) : Thread.first.op;
},
_getFirstVisPost(getThread, getFull) {
if(this.lastPageOffset !== deWindow.pageYOffset) {
let post = getThread ? Thread.first : Thread.first.op;
while(post.top < 1) {
const tPost = post.next;
if(!tPost) {
break;
}
post = tPost;
}
if(this.cPost) {
this.cPost.unselect();
}
this.cPost = getThread ? getFull ? post.op : post.op.prev : getFull ? post : post.prev;
this.lastPageOffset = deWindow.pageYOffset;
}
return this.cPost;
},
_scroll(post, toUp, toThread) {
const next = this._getNextVisPost(post, toThread, toUp);
if(!next) {
if(!aib.t) {
const pageNum = toUp ? DelForm.first.pageNum - 1 : DelForm.last.pageNum + 1;
if(toUp ? pageNum >= aib.firstPage : pageNum <= aib.lastPage) {
deWindow.location.pathname = aib.getPageUrl(aib.b, pageNum);
}
}
return;
}
if(post) {
post.unselect();
}
if(toThread) {
next.el.scrollIntoView();
} else {
scrollTo(0, deWindow.pageYOffset + next.el.getBoundingClientRect().top -
Post.sizing.wHeight / 2 + next.el.clientHeight / 2);
}
this.lastPageOffset = deWindow.pageYOffset;
next.select();
this.cPost = next;
}
};
class KeyEditListener {
constructor(popupEl, keys, allKeys) {
this.cEl = null;
this.cKey = -1;
this.errorInput = false;
const aInputs = [...$Q('.de-input-key', popupEl)];
for(let i = 0, len = allKeys.length; i < len; ++i) {
const k = allKeys[i];
if(k !== 0) {
for(let j = i + 1; j < len; ++j) {
if(k === allKeys[j]) {
aInputs[i].classList.add('de-input-error');
aInputs[j].classList.add('de-input-error');
break;
}
}
}
}
this.popupEl = popupEl;
this.keys = keys;
this.initKeys = JSON.parse(JSON.stringify(keys));
this.allKeys = allKeys;
this.allInputs = aInputs;
this.errCount = $Q('.de-input-error', popupEl).length;
if(this.errCount !== 0) {
this.saveButton.disabled = true;
}
}
static getEditMarkup(keys) {
const allKeys = [];
return [allKeys, `${ Lng.hotKeyEdit[lang].join('')
.replace(/%l/g, '')
.replace(/%\/l/g, ' ')
.replace(/%i([2-4])([0-9]+)(t)?/g, (all, id1, id2, isText) => {
const key = keys[+id1][+id2];
allKeys.push(key);
return ` `;
}) } ` +
` `];
}
static getStrKey(key) {
return (key & 0x1000 ? 'Ctrl+' : '') +
(key & 0x2000 ? 'Shift+' : '') +
(key & 0x4000 ? 'Alt+' : '') +
KeyEditListener.keyCodes[key & 0xFFF];
}
static setTitle(el, idx) {
let title = el.getAttribute('de-title');
if(!title) {
title = el.getAttribute('title');
el.setAttribute('de-title', title);
}
if(HotKeys.enabled && idx !== -1) {
title += ` [${ KeyEditListener.getStrKey(HotKeys.gKeys[idx]) }]`;
}
el.title = title;
}
get saveButton() {
const value = $id('de-keys-save');
Object.defineProperty(this, 'saveButton', { value, configurable: true });
return value;
}
handleEvent(e) {
let key;
let el = e.target;
switch(e.type) {
case 'blur':
if(HotKeys.enabled && this.errCount === 0) {
HotKeys.resume(this.keys);
}
el.classList.remove('de-input-selected');
this.cEl = null;
return;
case 'focus':
if(HotKeys.enabled) {
HotKeys.pauseHotKeys();
}
el.classList.add('de-input-selected');
this.cEl = el;
return;
case 'click': {
let keys;
if(el.id === 'de-keys-reset') {
this.keys = HotKeys.getDefaultKeys();
this.initKeys = HotKeys.getDefaultKeys();
if(HotKeys.enabled) {
HotKeys.resume(this.keys);
}
[this.allKeys, this.popupEl.innerHTML] = KeyEditListener.getEditMarkup(this.keys);
this.allInputs = [...$Q('.de-input-key', this.popupEl)];
this.errCount = 0;
delete this.saveButton;
break;
} else if(el.id === 'de-keys-save') {
({ keys } = this);
setStored('DESU_keys', JSON.stringify(keys));
} else if(el.className === 'de-popup-btn') {
keys = this.initKeys;
} else {
return;
}
if(HotKeys.enabled) {
HotKeys.resume(keys);
}
closePopup('edit-hotkeys');
break;
}
case 'keydown': {
if(!this.cEl) {
return;
}
key = e.keyCode;
if(key === 0x1B || key === 0x2E) { // ESC, DEL
this.cEl.value = '';
this.cKey = 0;
this.errorInput = false;
break;
}
const keyStr = KeyEditListener.keyCodes[key];
if(typeof keyStr === 'undefined') {
this.cKey = -1;
return;
}
let str = '';
if(e.ctrlKey) {
str += 'Ctrl+';
}
if(e.shiftKey) {
str += 'Shift+';
}
if(e.altKey) {
str += 'Alt+';
}
if(key === 16 || key === 17 || key === 18) {
this.errorInput = true;
this.cKey = 0;
} else {
this.cKey = key | (e.ctrlKey ? 0x1000 : 0) | (e.shiftKey ? 0x2000 : 0) |
(e.altKey ? 0x4000 : 0) | (this.cEl.hasAttribute('de-text') ? 0x8000 : 0);
this.errorInput = false;
str += keyStr;
}
this.cEl.value = str;
break;
}
case 'keyup': {
el = this.cEl;
key = this.cKey;
if(!el || key === -1) {
return;
}
let rEl;
const isError = el.classList.contains('de-input-error');
if(!this.errorInput && key !== -1) {
let idx = this.allInputs.indexOf(el);
const oKey = this.allKeys[idx];
if(oKey === key) {
this.errorInput = false;
break;
}
const rIdx = key === 0 ? -1 : this.allKeys.indexOf(key);
this.allKeys[idx] = key;
if(isError) {
idx = this.allKeys.indexOf(oKey);
if(idx !== -1 && this.allKeys.indexOf(oKey, idx + 1) === -1) {
rEl = this.allInputs[idx];
if(rEl.classList.contains('de-input-error')) {
this.errCount--;
rEl.classList.remove('de-input-error');
}
}
if(rIdx === -1) {
this.errCount--;
el.classList.remove('de-input-error');
}
}
if(rIdx === -1) {
this.keys[+el.getAttribute('de-id1')][+el.getAttribute('de-id2')] = key;
if(this.errCount === 0) {
this.saveButton.disabled = false;
}
this.errorInput = false;
break;
}
rEl = this.allInputs[rIdx];
if(!rEl.classList.contains('de-input-error')) {
this.errCount++;
rEl.classList.add('de-input-error');
}
}
if(!isError) {
this.errCount++;
el.classList.add('de-input-error');
}
if(this.errCount !== 0) {
this.saveButton.disabled = true;
}
}
}
e.preventDefault();
}
}
// Browsers have different codes for these keys (see HotKeys.readKeys):
// Firefox - '-' - 173, '=' - 61, ';' - 59
// Chrome/Opera: '-' - 189, '=' - 187, ';' - 186
/* eslint-disable comma-spacing, comma-style, no-sparse-arrays */
KeyEditListener.keyCodes = [
'',,,,,,,,'Backspace','Tab',,,,'Enter',,,'Shift','Ctrl','Alt',/* Pause/Break */,/* Caps Lock */,,,,,,,
/* Esc */,,,,,'Space',/* PgUp */,/* PgDn */,/* End */,/* Home */,'←','↑','→','↓',,,,,/* Insert */,
/* Del */,,'0','1','2','3','4','5','6','7','8','9',,';',,'=',,,,'A','B','C','D','E','F','G','H','I','J',
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',/* Left WIN */,/* Right WIN */,
/* Select */,,,'Num 0','Num 1','Num 2','Num 3','Num 4','Num 5','Num 6','Num 7','Num 8','Num 9','Num *',
'Num +',,'Num -','Num .','Num /',/* F1 */,/* F2 */,/* F3 */,/* F4 */,/* F5 */,/* F6 */,/* F7 */,/* F8 */,
/* F9 */,/* F10 */,/* F11 */,/* F12 */,,,,,,,,,,,,,,,,,,,,,/* Num Lock */,/* Scroll Lock */,,,,,,,,,,,,,,,
,,,,,,,,,,,,,'-',,,,,,,,,,,,,';','=',',','-','.','/','`',,,,,,,,,,,,,,,,,,,,,,,,,,,'[','\\',']','\''
];
/* eslint-enable comma-spacing, comma-style, no-sparse-arrays */
/* ==[ ContentLoad.js ]=======================================================================================
CONTENT DOWNLOADING
images/video preloading, rarjpeg detecting, thread/images downloading
=========================================================================================================== */
const ContentLoader = {
afterFn : null,
isLoading : false,
popupId : null,
downloadThread(imgOnly) {
let progress, counter;
let current = 1;
let warnings = '';
let tar = new TarBuilder();
const dc = imgOnly ? doc : doc.documentElement.cloneNode(true);
let els = [...$Q(aib.qPostImg, $q('[de-form]', dc))];
let count = els.length;
const delSymbols = (str, r = '') => str.replace(/[\\/:*?"<>|]/g, r);
this._thrPool = new TasksPool(4, (num, data) => this.loadImgData(data[0]).then(imgData => {
const [url, fName, el, parentLink] = data;
let safeName = delSymbols(fName, '_');
progress.value = counter.innerHTML = current++;
if(parentLink) {
let thumbName = safeName.replace(/\.[a-z]+$/, '.png');
if(imgOnly) {
thumbName = 'thumb-' + thumbName;
} else {
thumbName = 'thumbs/' + thumbName;
safeName = imgData ? 'images/' + safeName : thumbName;
parentLink.href = getImgNameLink(el).href = safeName;
}
if(imgData) {
tar.addFile(safeName, imgData);
} else {
warnings += ` ${ Lng.cantLoad[lang] } ${ url } ` +
` ${ Lng.willSavePview[lang] }`;
$popup('err-files', Lng.loadErrors[lang] + warnings);
if(imgOnly) {
return this.getDataFromImg(el).then(data =>
tar.addFile(thumbName, data), Function.prototype);
}
}
return imgOnly ? null : this.getDataFromImg(el).then(data => {
el.src = thumbName;
tar.addFile(thumbName, data);
}, () => (el.src = safeName));
} else if(imgData?.length) {
tar.addFile(el.href = el.src = 'data/' + safeName, imgData);
} else {
el.remove();
}
}), () => {
const docName = `${ aib.domain }-${ delSymbols(aib.b) }-${ aib.t }`;
if(!imgOnly) {
$q('head', dc).insertAdjacentHTML('beforeend',
'');
const dcBody = $q('body', dc);
dcBody.classList.remove('de-runned');
dcBody.classList.add('de-mode-local');
$delAll('#de-css, #de-css-dynamic, #de-css-user', dc);
tar.addString('data/dollscript.js', `${ nav.isESNext ?
`(${ String(deMainFuncInner) })(window, null, null, (x, y) => window.scrollTo(x, y), ` :
`(${ String(/* global deMainFuncOuter */ deMainFuncOuter) })(`
}${ JSON.stringify({ domain: aib.domain, b: aib.b, t: aib.t }) });`);
const dt = doc.doctype;
tar.addString(docName + '.html', '' + dc.outerHTML);
}
const title = delSymbols(Thread.first.op.title.trim());
downloadBlob(tar.get(), `${ docName }${ imgOnly ? '-images' : '' }${
title ? ' - ' + title : '' }.tar`);
closePopup('load-files');
this._thrPool = tar = warnings = count = current = imgOnly = progress = counter = null;
});
els.forEach(el => {
const parentLink = el.closest('a');
if(parentLink) {
const url = parentLink.href;
this._thrPool.runTask(
[url, parentLink.getAttribute('download') || getFileName(url), el, parentLink]);
}
});
if(!imgOnly) {
$delAll('.de-btn-img, #de-main, .de-parea, .de-post-btns, .de-refmap, .de-thr-buttons, ' +
'.de-video-obj, #de-win-reply, link[rel="alternate stylesheet"], script, ' + aib.qForm, dc);
$Q('a', dc).forEach(el => {
let num;
const tc = el.textContent;
if(tc[0] === '>' && tc[1] === '>' && (num = parseInt(tc.substr(2), 10)) && pByNum.has(num)) {
el.href = aib.anchor + num;
if(!el.classList.contains('de-link-postref')) {
el.className = 'de-link-postref ' + el.className;
}
} else {
el.href = aib.getAbsLink(el.href);
}
});
$Q(aib.qPost, dc).forEach((el, i) => el.setAttribute('de-num', i ? aib.getPNum(el) : aib.t));
const files = [];
const urlRegex = new RegExp(`^\\/\\/?|^https?:\\/\\/([^\\/]*\\.)?${
escapeRegExp(aib._4chan ? '4cdn.org' : aib.domain) }\\/`, 'i');
$Q('link, *[src]', dc).forEach(el => {
if(els.indexOf(el) !== -1) {
return;
}
let url = el.tagName.toLowerCase() === 'link' ? el.href : el.src;
if(!urlRegex.test(url)) {
el.remove();
return;
}
let fName = delSymbols(getFileName(url).replace(/(#|\?).*?$/, ''), '_').toLowerCase();
if(files.indexOf(fName) !== -1) {
let temp = url.lastIndexOf('.');
const ext = url.substring(temp);
url = url.substring(0, temp);
fName = cutFileExt(fName);
for(let i = 0; ; ++i) {
temp = `${ fName }(${ i })${ ext }`;
if(files.indexOf(temp) === -1) {
break;
}
}
fName = temp;
}
files.push(fName);
this._thrPool.runTask([url, fName, el, null]);
count++;
});
}
$popup('load-files', `${ imgOnly ? Lng.loadImage[lang] : Lng.loadFile[lang] }: 1 /${ count }`, true);
progress = $id('de-loadprogress');
counter = progress.nextElementSibling;
this._thrPool.completeTasks();
els = null;
},
getDataFromCanvas: el =>
new Uint8Array(atob(el.toDataURL('image/png').split(',')[1]).split('').map(a => a.charCodeAt())),
getDataFromImg(el) {
if(el.getAttribute('loading') === 'lazy') {
return this.loadImgData(el.src);
}
try {
const cnv = this._canvas || (this._canvas = doc.createElement('canvas'));
cnv.width = el.width || el.videoWidth;
cnv.height = el.height || el.videoHeight;
cnv.getContext('2d').drawImage(el, 0, 0);
return Promise.resolve(this.getDataFromCanvas(cnv));
} catch(err) {
return this.loadImgData(el.src);
}
},
loadImgData: (url, repeatOnError = true) => $ajax(
url, { responseType: 'arraybuffer' }, !url.startsWith('blob')
).then(xhr => {
if('response' in xhr) {
try {
return nav.getUnsafeUint8Array(xhr.response);
} catch(err) {}
}
const txt = xhr.responseText;
return new Uint8Array(txt.length).map((val, i) => txt.charCodeAt(i) & 0xFF);
}, err => err.code !== 404 && repeatOnError ? ContentLoader.loadImgData(url, false) : null),
preloadImages(data) {
if(!Cfg.preLoadImgs && !Cfg.openImgs && !isPreImg) {
return;
}
let preloadPool;
const isPost = data instanceof AbstractPost;
const els = $Q(aib.qPostImg, isPost ? data.el : data);
const len = els.length;
if(isPreImg || Cfg.preLoadImgs) {
let cImg = 1;
const mReqs = isPost ? 1 : 4;
const rarJpgFinder = (isPreImg || Cfg.findImgFile) && new WorkerPool(mReqs, this._detectImgFile,
err => console.error('File detector error:', `line: ${ err.lineno } - ${ err.message }`));
preloadPool = new TasksPool(mReqs, (num, data) => this.loadImgData(data[0]).then(imageData => {
const [url, parentLink, iType, isRepToOrig, el, isVideo] = data;
if(imageData) {
const fName = decodeURIComponent(getFileName(url));
const nameLink = getImgNameLink(el);
parentLink.setAttribute('download', fName);
if(!Cfg.imgNames) {
nameLink.setAttribute('download', fName);
nameLink.setAttribute('de-href', nameLink.href);
}
parentLink.href = nameLink.href =
deWindow.URL.createObjectURL(new Blob([imageData], { type: iType }));
if(isVideo) {
el.setAttribute('de-video', '');
}
if(isRepToOrig) {
el.src = parentLink.href;
}
if(rarJpgFinder) {
rarJpgFinder.runWorker(imageData.buffer, [imageData.buffer],
info => this._addImgFileIcon(nameLink, fName, info));
}
}
if(this.popupId) {
$popup(this.popupId, `${ Lng.loadImage[lang] }: ${ cImg }/${ len }`, true);
}
cImg++;
}), () => {
this.isLoading = false;
if(this.afterFn) {
this.afterFn();
this.afterFn = this.popupId = null;
}
if(rarJpgFinder) {
rarJpgFinder.clearWorkers();
}
});
this.isLoading = true;
}
for(let i = 0; i < len; ++i) {
const imgEl = els[i];
const parentLink = imgEl.closest('a');
if(!parentLink) {
continue;
}
let isRepToOrig = !!Cfg.openImgs;
const url = aib.getImgSrcLink(imgEl).getAttribute('href');
const type = getFileMime(url);
const isVideo = type && (type === 'video/webm' || type === 'video/mp4' ||
type === 'video/quicktime' || type === 'video/ogv');
if(!type || isVideo && Cfg.preLoadImgs === 2) {
continue;
} else if($q('img[src*="/spoiler"]', parentLink)) {
isRepToOrig = false;
} else if(type === 'image/gif') {
isRepToOrig &= Cfg.openImgs !== 3;
} else {
if(isVideo) {
isRepToOrig = false;
}
isRepToOrig &= Cfg.openImgs !== 2;
}
if(preloadPool) {
preloadPool.runTask([url, parentLink, type, isRepToOrig, imgEl, isVideo]);
} else if(isRepToOrig) {
imgEl.src = url;
}
}
if(preloadPool) {
preloadPool.completeTasks();
}
},
_canvas : null,
_thrPool : null,
_addImgFileIcon(nameLink, fName, info) {
const { type } = info;
if(typeof type === 'undefined') {
return;
}
const ext = ['7z', 'zip', 'rar', 'ogg', 'mp3'][type];
nameLink.insertAdjacentHTML('afterend', `.${ ext } `);
},
// Finds built-in files in jpg and png
_detectImgFile: arrBuf => {
let i, j;
const dat = new Uint8Array(arrBuf);
let len = dat.length;
/* JPG [ff d8 ff e0] = [яШяа] */
if(dat[0] === 0xFF && dat[1] === 0xD8) {
for(i = 0, j = 0; i < len - 1; ++i) {
if(dat[i] === 0xFF) {
/* Built-in JPG */
if(dat[i + 1] === 0xD8) {
j++;
/* JPG end [ff d9] */
} else if(dat[i + 1] === 0xD9 && --j === 0) {
i += 2;
break;
}
}
}
/* PNG [89 50 4e 47] = [‰PNG] */
} else if(dat[0] === 0x89 && dat[1] === 0x50) {
for(i = 0; i < len - 7; ++i) {
/* PNG end [49 45 4e 44 ae 42 60 82] */
if(dat[i] === 0x49 && dat[i + 1] === 0x45 && dat[i + 2] === 0x4E && dat[i + 3] === 0x44) {
i += 8;
break;
}
}
} else {
return {};
}
if(i === len || len - i <= 60) { // Ignore small files (<60 bytes)
return {};
}
for(len = i + 90; i < len; ++i) {
/* 7Z [37 7a bc af] = [7zјЇ] */
if(dat[i] === 0x37 && dat[i + 1] === 0x7A && dat[i + 2] === 0xBC) {
return { type: 0, idx: i, data: arrBuf };
/* ZIP [50 4b 03 04] = [PK..] */
} else if(dat[i] === 0x50 && dat[i + 1] === 0x4B && dat[i + 2] === 0x03) {
return { type: 1, idx: i, data: arrBuf };
/* RAR [52 61 72 21] = [Rar!] */
} else if(dat[i] === 0x52 && dat[i + 1] === 0x61 && dat[i + 2] === 0x72) {
return { type: 2, idx: i, data: arrBuf };
/* OGG [4f 67 67 53] = [OggS] */
} else if(dat[i] === 0x4F && dat[i + 1] === 0x67 && dat[i + 2] === 0x67) {
return { type: 3, idx: i, data: arrBuf };
/* MP3 [0x49 0x44 0x33] = [ID3] */
} else if(dat[i] === 0x49 && dat[i + 1] === 0x44 && dat[i + 2] === 0x33) {
return { type: 4, idx: i, data: arrBuf };
}
}
return {};
}
};
/* ==[ TimeCorrection.js ]====================================================================================
TIME CORRECTION
=========================================================================================================== */
class DateTime {
constructor(pattern, rPattern, diff, dtLang, onRPat) {
this.pad2 = pad2;
this.genDateTime = null;
this.onRPat = null;
if(DateTime.checkPattern(pattern)) {
this.disabled = true;
return;
}
this.regex = pattern
.replace(/(?:[sihdny]\?){2,}/g, str => `(?:${ str.replace(/\?/g, '') })?`)
.replace(/-/g, '[^<]')
.replace(/\+/g, '[^0-9<]')
.replace(/([sihdny]+)/g, '($1)')
.replace(/[sihdny]/g, '\\d')
.replace(/m|w/g, '([a-zA-Zа-яА-Я]+)');
this.pattern = pattern.replace(/[?\-+]+/g, '').replace(/([a-z])\1+/g, '$1');
this.diff = parseInt(diff, 10);
this.arrW = Lng.week[dtLang];
this.arrM = Lng.month[dtLang];
this.arrFM = Lng.fullMonth[dtLang];
if(rPattern) {
this.genDateTime = this.genRFunc(rPattern);
} else {
this.onRPat = onRPat;
}
}
static checkPattern(val) {
return !val.includes('i') || !val.includes('h') || !val.includes('d') ||
!val.includes('y') || !(val.includes('n') || val.includes('m')) ||
/[^?\-+sihdmwny]|mm|ww|\?\?|([ihdny]\?)\1+/.test(val);
}
static async toggleSettings(el) {
if(el.checked && (!/^[+-]\d{1,2}$/.test(Cfg.timeOffset) || DateTime.checkPattern(Cfg.timePattern))) {
$popup('err-correcttime', Lng.cTimeError[lang]);
await CfgSaver.save('correctTime', 0);
el.checked = false;
}
}
genRFunc(rPattern) {
return dtime => rPattern.replace('_o', (this.diff < 0 ? '' : '+') + this.diff)
.replace('_s', () => this.pad2(dtime.getSeconds()))
.replace('_i', () => this.pad2(dtime.getMinutes()))
.replace('_h', () => this.pad2(dtime.getHours()))
.replace('_d', () => this.pad2(dtime.getDate()))
.replace('_w', () => this.arrW[dtime.getDay()])
.replace('_n', () => this.pad2(dtime.getMonth() + 1))
.replace('_m', () => this.arrM[dtime.getMonth()])
.replace('_M', () => this.arrFM[dtime.getMonth()])
.replace('_y', () => ('' + dtime.getFullYear()).substring(2))
.replace('_Y', () => dtime.getFullYear());
}
getRPattern(txt) {
const m = txt.match(new RegExp(this.regex));
if(!m) {
this.disabled = true;
return false;
}
let rPattern = '';
for(let i = 1, len = m.length, j = 0, str = m[0]; i < len;) {
const a = m[i++];
if(!a) {
continue;
}
let p = this.pattern[i - 2];
if((p === 'm' || p === 'y') && a.length > 3) {
p = p.toUpperCase();
}
const k = str.indexOf(a, j);
rPattern += str.substring(j, k) + '_' + p;
j = k + a.length;
}
if(this.onRPat) {
this.onRPat(rPattern);
}
this.genDateTime = this.genRFunc(rPattern);
return true;
}
fix(txt) {
if(this.disabled || (!this.genDateTime && !this.getRPattern(txt))) {
return txt;
}
return txt.replace(new RegExp(this.regex, 'g'), (str, ...args) => {
let second, minute, hour, day, month, year;
for(let i = 0; i < 7; ++i) {
const a = args[i];
switch(this.pattern[i]) {
case 's': second = a; break;
case 'i': minute = a; break;
case 'h': hour = a; break;
case 'd': day = a; break;
case 'n': month = a - 1; break;
case 'y': year = a; break;
case 'm': month = Lng.monthDict[a.slice(0, 3).toLowerCase()] || 0; break;
}
}
const dtime = new Date(year.length === 2 ? '20' + year :
year, month, day, hour, minute, second || 0);
dtime.setHours(dtime.getHours() + this.diff);
return this.genDateTime(dtime);
});
}
}
/* ==[ Players.js ]===========================================================================================
PLAYERS / LINKS EMBEDDERS
youtube, vimeo, mp3, vocaroo embedding players
=========================================================================================================== */
class Videos {
constructor(post, player = null, playerInfo = null) {
this.currentLink = null;
this.hasLinks = false;
this.linksCount = 0;
this.loadedLinksCount = 0;
this.playerInfo = null;
this.post = post;
this.titleLoadFn = null;
this.vData = [[], []];
if(player && playerInfo) {
Object.defineProperty(this, 'player', { value: player });
this.playerInfo = playerInfo;
}
}
static addPlayer(obj, m, isYtube, enableJsapi = false) {
const el = obj.player;
obj.playerInfo = m;
let txt;
if(isYtube) {
const list = m[0].match(/list=[^]+/);
txt = `VIDEO ';
} else {
const id = m[1] + (m[2] ? m[2] : '');
txt = ``;
}
el.innerHTML = txt + (enableJsapi ? '' :
` `);
$show(el);
if(!enableJsapi) {
el.lastChild.onclick = e => e.target.parentNode.classList.toggle('de-video-expanded');
}
}
static setLinkData(link, data, isCloned = false) {
const [title, author, views, publ, duration] = data;
if(Panel.isVidEnabled && !isCloned) {
const clonedLink = $q(`.de-entry > .de-video-link[href="${ link.href }"]:not(title)`);
if(clonedLink) {
Videos.setLinkData(clonedLink, data, true);
}
}
link.textContent = title;
link.classList.add('de-video-title');
link.setAttribute('de-author', author);
link.title = (duration ? Lng.duration[lang] + duration : '') +
(publ ? `, ${ Lng.published[lang] + publ }\n` : '') +
Lng.author[lang] + author + (views ? ', ' + Lng.views[lang] + views : '');
}
get player() {
const { post } = this;
const value = $bBegin(post.msg, `
`);
Object.defineProperty(this, 'player', { value });
return value;
}
addLink(m, loader, link, isYtube) {
this.hasLinks = true;
this.linksCount++;
if(this.playerInfo === null) {
if(Cfg.embedYTube === 1) {
this._addThumb(m, isYtube);
}
} else if(!link && $q(`.de-video-link[href*="${ m[1] }"]`, this.post.msg)) {
return;
}
let dataObj;
if(loader && (dataObj = Videos._global.vData[+!isYtube][m[1]])) {
this.vData[+!isYtube].push(dataObj);
}
let time = '';
[time, m[2], m[3], m[4]] = Videos._fixTime(m[4], m[3], m[2]);
if(link) {
link.href = link.href.replace(/^http:/, 'https:');
if(time) {
link.setAttribute('de-time', time);
}
link.className = `de-video-link ${ isYtube ? 'de-ytube' : 'de-vimeo' }`;
} else {
const src = isYtube ?
`${ aib.protocol }//www.youtube.com/watch?v=${ m[1] }${ time ? '#t=' + time : '' }` :
`${ aib.protocol }//vimeo.com/${ m[1] }`;
link = $bEnd(this.post.msg, `${ dataObj ? '' : src }
`).firstChild;
}
if(dataObj) {
Videos.setLinkData(link, dataObj);
}
if(this.playerInfo === null || this.playerInfo === m) {
this.currentLink = link;
}
link.videoInfo = m;
let vidListEl;
if(Panel.isVidEnabled && (vidListEl = $id('de-video-list'))) {
updateVideoList(vidListEl, link, this.post.num);
}
if(loader && !dataObj) {
loader.runTask([link, isYtube, this, m[1]]);
}
}
clickLink(el, mode) {
const m = el.videoInfo;
if(this.playerInfo !== m) {
this.currentLink.classList.remove('de-current');
this.currentLink = el;
if(mode === 1) {
this._addThumb(m, el.classList.contains('de-ytube'));
} else {
el.classList.add('de-current');
this.setPlayer(m, el.classList.contains('de-ytube'));
}
return;
}
if(mode === 1) {
if($q('.de-video-thumb', this.player)) {
el.classList.add('de-current');
this.setPlayer(m, el.classList.contains('de-ytube'));
} else {
el.classList.remove('de-current');
this._addThumb(m, el.classList.contains('de-ytube'));
}
} else {
el.classList.remove('de-current');
$hide(this.player);
this.player.innerHTML = '';
this.playerInfo = null;
}
}
setPlayer(m, isYtube) {
Videos.addPlayer(this, m, isYtube);
}
toggleFloatedThumb(linkEl, isOutEvent) {
let el = $id('de-video-thumb-floated');
if(isOutEvent) {
el.remove();
return;
}
if(!el) {
el = $bEnd(doc.body, ` `);
}
const cr = linkEl.getBoundingClientRect();
const pvHeight = Cfg.YTubeHeigh;
const isTop = cr.top + cr.height + pvHeight < nav.viewportHeight();
el.style.cssText = `position: absolute; left: ${ deWindow.pageXOffset + cr.left }px; top: ${
deWindow.pageYOffset + (isTop ? cr.top + cr.height : cr.top - pvHeight) }px; width: ${
Cfg.YTubeWidth }px; height: ${ pvHeight }px; z-index: 9999;`;
}
updatePost(oldLinks, newLinks, cloned) {
const loader = !cloned && Videos._getTitlesLoader();
let j = 0;
for(let i = 0, len = newLinks.length; i < len; ++i) {
const el = newLinks[i];
const link = oldLinks[j];
if(link?.classList.contains('de-current')) {
this.currentLink = el;
}
if(cloned) {
el.videoInfo = link.videoInfo;
j++;
} else {
const m = el.href.match(Videos.ytReg);
if(m) {
this.addLink(m, loader, el, true);
j++;
}
}
}
this.currentLink = this.currentLink || newLinks[0];
if(loader) {
loader.completeTasks();
}
}
static _fixTime(seconds = 0, minutes = 0, hours = 0) {
if(seconds >= 60) {
minutes += Math.floor(seconds / 60);
seconds %= 60;
}
if(minutes >= 60) {
hours += Math.floor(seconds / 60);
minutes %= 60;
}
return [
(hours ? hours + 'h' : '') +
(minutes ? minutes + 'm' : '') +
(seconds ? seconds + 's' : ''),
hours, minutes, seconds
];
}
static _getTitlesLoader() {
return Cfg.YTubeTitles && new TasksPool(4, (num, info) => {
const [, isYtube,, id] = info;
if(isYtube) {
return Cfg.ytApiKey ? Videos._getYTInfoAPI(info, num, id) :
Videos._getYTInfoOembed(info, num, id);
}
return $ajax(`${ aib.protocol }//vimeo.com/api/v2/video/${ id }.json`, null, true).then(xhr => {
const entry = JSON.parse(xhr.responseText)[0];
return Videos._titlesLoaderHelper(
info, num,
entry.title,
entry.user_name,
entry.stats_number_of_plays,
/(.*)\s(.*)?/.exec(entry.upload_date)[1],
Videos._fixTime(entry.duration)[0]);
}).catch(() => Videos._titlesLoaderHelper(info, num));
}, () => (sesStorage['de-videos-data2'] = JSON.stringify(Videos._global.vData)));
}
static _getYTInfoAPI(info, num, id) {
return $ajax(
`https://www.googleapis.com/youtube/v3/videos?key=${ Cfg.ytApiKey }&id=${ id }` +
'&part=snippet,statistics,contentDetails&fields=items/snippet/title,items/snippet/publishedAt,' +
'items/snippet/channelTitle,items/statistics/viewCount,items/contentDetails/duration',
null, true
).then(xhr => {
const items = JSON.parse(xhr.responseText).items[0];
return Videos._titlesLoaderHelper(
info, num,
items.snippet.title,
items.snippet.channelTitle,
items.statistics.viewCount,
items.snippet.publishedAt.substr(0, 10),
items.contentDetails.duration.substr(2).toLowerCase());
}).catch(() => Videos._getYTInfoOembed(info, num, id));
}
static _getYTInfoOembed(info, num, id) {
const canSendCORS = nav.hasGMXHR || nav.canUseFetch;
return (canSendCORS ?
$ajax(`https://www.youtube.com/oembed?url=http%3A//youtube.com/watch%3Fv%3D${
id }&format=json`, null, true) :
$ajax(`https://noembed.com/embed?url=http%3A//youtube.com/watch%3Fv%3D${ id }&callback=?`)
).then(xhr => {
const res = xhr.responseText;
const json = JSON.parse(canSendCORS ? res : res.replace(/^[^{]+|\)$/g, ''));
return Videos._titlesLoaderHelper(info, num, json.title, json.author_name, null, null, null);
}).catch(() => Videos._titlesLoaderHelper(info, num));
}
static _titlesLoaderHelper([link, isYtube, videoObj, id], num, ...data) {
if(data.length) {
Videos.setLinkData(link, data);
Videos._global.vData[+!isYtube][id] = data;
videoObj.vData[+!isYtube].push(data);
if(videoObj.titleLoadFn) {
videoObj.titleLoadFn(data);
}
}
videoObj.loadedLinksCount++;
// Wait for 3 sec every 30 links
if(num % 30 === 0) {
return Promise.reject(new TasksPool.PauseError(3e3));
}
return new Promise(resolve => setTimeout(resolve, 250));
}
_addThumb(m, isYtube) {
const el = this.player;
this.playerInfo = m;
el.classList.remove('de-video-expanded');
$show(el);
const str = `` +
` `;
return;
}
el.innerHTML = `${ str }//vimeo.com/${ m[1] }" target="_blank">` +
' ';
$ajax(`${ aib.protocol }//vimeo.com/api/v2/video/${ m[1] }.json`, null, true).then(xhr => {
el.firstChild.firstChild.setAttribute('src', JSON.parse(xhr.responseText)[0].thumbnail_large);
}).catch(Function.prototype);
}
}
Videos.ytReg =
/^https?:\/\/(?:www\.|m\.)?youtu(?:be\.com\/(?:watch\?.*?v=|v\/|embed\/)|\.be\/)([a-zA-Z0-9-_]+).*?(?:t(?:ime)?=(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?)?$/;
Videos.vimReg =
/^https?:\/\/(?:www\.)?vimeo\.com\/(?:[^?]+\?clip_id=|.*?\/)?(\d+).*?(#t=\d+)?$/;
Videos._global = {
get vData() {
let value;
try {
value = Cfg.YTubeTitles ? JSON.parse(sesStorage['de-videos-data2'] || '[{}, {}]') : [{}, {}];
} catch(err) {
value = [{}, {}];
}
Object.defineProperty(this, 'vData', { value });
return value;
}
};
class VideosParser {
constructor() {
this._loader = Videos._getTitlesLoader();
}
endParser() {
if(this._loader) {
this._loader.completeTasks();
}
}
parse(data) {
const isPost = data instanceof AbstractPost;
const loader = this._loader;
VideosParser._parserHelper('a[href*="youtu"]', data, loader, isPost, true, Videos.ytReg);
if(Cfg.addVimeo) {
VideosParser._parserHelper('a[href*="vimeo.com"]', data, loader, isPost, false, Videos.vimReg);
}
const vids = aib.fixVideo(isPost, data);
for(let i = 0, len = vids.length; i < len; ++i) {
const [post, m, isYtube] = vids[i];
if(post) {
post.videos.addLink(m, loader, null, isYtube);
}
}
return this;
}
static _parserHelper(qPath, data, loader, isPost, isYtube, reg) {
const links = $Q(qPath, isPost ? data.el : data);
for(let i = 0, len = links.length; i < len; ++i) {
const link = links[i];
const m = link.href.match(reg);
if(m) {
const mPost = isPost ? data : aib.getPostOfEl(link);
if(mPost) {
mPost.videos.addLink(m, loader, link, isYtube);
}
}
}
}
}
// Embed .mp3 and Vocaroo links
function embedAudioLinks(data) {
const isPost = data instanceof AbstractPost;
if(Cfg.addMP3) {
const els = $Q('a[href*=".mp3"], a[href*=".opus"]', isPost ? data.el : data);
for(let i = 0, len = els.length; i < len; ++i) {
const link = els[i];
if((link.target !== '_blank' && link.rel !== 'nofollow') ||
!link.pathname.includes('.mp3') && !link.pathname.includes('.opus')
) {
continue;
}
const src = link.href;
const el = (isPost ? data : aib.getPostOfEl(link)).mp3Obj;
if(nav.canPlayMP3) {
if(!$q(`audio[src="${ src }"]`, el)) {
el.insertAdjacentHTML('beforeend',
`
`);
}
// Flash plugin for old browsers that not support HTML5 audio
} else if(!$q(`object[FlashVars*="${ src }"]`, el)) {
el.insertAdjacentHTML('beforeend', ' `);
}
}
}
if(Cfg.addVocaroo) {
$Q('a[href*="voca.ro"], a[href*="vocaroo.com"]', isPost ? data.el : data).forEach(link => {
if(!(link.previousSibling?.className === 'de-vocaroo')) {
link.insertAdjacentHTML('beforebegin',
``);
}
});
}
}
/* ==[ Ajax.js ]==============================================================================================
AJAX FUNCTIONS
=========================================================================================================== */
// Main AJAX util
function $ajax(url, params = null, isCORS = false) {
let resolve, reject, cancelFn;
const needTO = params ? params.useTimeout : false;
const WAITING_TIME = 5e3;
if(nav.canUseFetch &&
((isCORS ? !nav.hasGMXHR : !nav.canUseNativeXHR) || aib.hasRefererErr) &&
!(isCORS && nav.isTampermonkey)
) {
if(!params) {
params = {};
}
params.referrer =
doc.referrer.startsWith(aib.protocol + '//' + aib.host) ? doc.referrer : deWindow.location;
params.referrerPolicy = 'unsafe-url';
if(params.data) {
params.body = params.data;
delete params.data;
}
if(isCORS) {
params.mode = 'cors';
}
const controller = new AbortController();
params.signal = controller.signal;
const loadTO = needTO && setTimeout(() => {
reject(AjaxError.Timeout);
try {
controller.abort();
} catch(err) {}
}, WAITING_TIME);
cancelFn = () => {
if(needTO) {
clearTimeout(loadTO);
}
controller.abort();
};
fetch(aib.getAbsLink(url), params).then(async res => {
if(!aib.isAjaxStatusOK(res.status)) {
reject(new AjaxError(res.status, res.statusText));
return;
}
switch(params.responseType) {
case 'arraybuffer': res.response = await res.arrayBuffer(); break;
case 'blob': res.response = await res.blob(); break;
default: res.responseText = await res.text();
}
resolve(res);
}).catch(err => reject(getErrorMessage(err)));
} else if((isCORS || !nav.canUseNativeXHR) && nav.hasGMXHR) {
let gmxhr;
const timeoutFn = () => {
reject(AjaxError.Timeout);
try {
gmxhr.abort();
} catch(err) {}
};
let loadTO = needTO && setTimeout(timeoutFn, WAITING_TIME);
const newParams = {
method : params?.method || 'GET',
url : nav.isSafari ? aib.getAbsLink(url) : url,
onreadystatechange(e) {
if(needTO) {
clearTimeout(loadTO);
}
if(e.readyState === 4 && !(
// Violentmonkey gives extra stage with undefined responseText and 200 status
nav.isViolentmonkey && e.status === 200 &&
typeof e.responseText === 'undefined' && typeof e.response === 'undefined'
)) {
if(aib.isAjaxStatusOK(e.status)) {
resolve(e);
} else {
reject(new AjaxError(e.status, e.statusText));
}
} else if(needTO) {
loadTO = setTimeout(timeoutFn, WAITING_TIME);
}
}
};
if(params) {
if(params.onprogress) {
newParams.upload = { onprogress: params.onprogress };
delete params.onprogress;
}
delete params.method;
Object.assign(newParams, params);
}
if(nav.hasNewGM) {
GM.xmlHttpRequest(newParams);
cancelFn = Function.prototype; // GreaseMonkey 4 cannot cancel xhr's
} else {
gmxhr = GM_xmlhttpRequest(newParams);
cancelFn = () => {
if(needTO) {
clearTimeout(loadTO);
}
try {
gmxhr.abort();
} catch(err) {}
};
}
} else if(nav.canUseNativeXHR) {
const xhr = new XMLHttpRequest();
const timeoutFn = () => {
reject(AjaxError.Timeout);
xhr.abort();
};
let loadTO = needTO && setTimeout(timeoutFn, WAITING_TIME);
if(params?.onprogress) {
xhr.upload.onprogress = params.onprogress;
}
if(aib._4chan) {
xhr.withCredentials = true;
}
xhr.onreadystatechange = ({ target }) => {
if(needTO) {
clearTimeout(loadTO);
}
if(target.readyState === 4) {
if(aib.isAjaxStatusOK(target.status)) {
resolve(target);
} else {
reject(new AjaxError(target.status, target.statusText));
}
} else if(needTO) {
loadTO = setTimeout(timeoutFn, WAITING_TIME);
}
};
try {
xhr.open(params?.method || 'GET', aib.getAbsLink(url), true);
if(params) {
if(params.responseType) {
xhr.responseType = params.responseType;
}
const { headers } = params;
if(headers) {
for(const header in headers) {
if($hasProp(headers, header)) {
xhr.setRequestHeader(header, headers[header]);
}
}
}
}
xhr.send(params?.data || null);
cancelFn = () => {
if(needTO) {
clearTimeout(loadTO);
}
xhr.abort();
};
} catch(err) {
clearTimeout(loadTO);
nav.canUseNativeXHR = false;
return $ajax(url, params);
}
} else {
reject(new AjaxError(0, 'Ajax error: Canʼt send any type of request.'));
}
return new CancelablePromise((res, rej) => {
resolve = res;
reject = rej;
}, cancelFn);
}
class AjaxError {
constructor(code, message) {
this.code = code;
this.message = message;
}
toString() {
return this.code <= 0 ?
String(this.message || Lng.noConnect[lang]) :
`HTTP [${ this.code }] ${ this.message }`;
}
}
AjaxError.Success = new AjaxError(200, 'OK');
AjaxError.Locked = new AjaxError(-1, { toString: () => Lng.thrClosed[lang] });
AjaxError.Timeout = new AjaxError(0, { toString: () => Lng.noConnect[lang] + ' (timeout)' });
const AjaxCache = {
clearCache() {
this._data = new Map();
},
fixURL: url => `${ url }${ url.includes('?') ? '&' : '?' }nocache=${ Math.round(Math.random() * 1e12) }`,
runCachedAjax(url, useCache) {
const { hasCacheControl, params } = this._data.get(url) || {};
const ajaxURL = hasCacheControl === false ? this.fixURL(url) : url;
return $ajax(ajaxURL, useCache && params || { useTimeout: true }, aib._4chan).then(xhr =>
this.saveData(url, xhr) ? xhr : $ajax(this.fixURL(url), useCache && params, aib._4chan));
},
saveData(url, xhr) {
let ETag = null;
let LastModified = null;
let i = 0;
let hasCacheControl = false;
let headers = 'getAllResponseHeaders' in xhr ? xhr.getAllResponseHeaders() : xhr.responseHeaders;
headers = headers ? /* usual xhr */ headers.split('\r\n') : /* fetch */ xhr.headers;
for(const idx in headers) {
if(!$hasProp(headers, idx)) {
continue;
}
let header = headers[idx];
if(typeof header === 'string') { // usual xhr
const сIdx = header.indexOf(':');
if(сIdx === -1) {
continue;
}
const name = header.substring(0, сIdx);
const value = header.substring(сIdx + 2, header.length);
header = [name, value];
}
const hName = header[0].toLowerCase();
let matched = true;
switch(hName) {
case 'cache-control': hasCacheControl = true; break;
case 'last-modified': LastModified = header[1]; break;
case 'etag': ETag = header[1]; break;
default: matched = false;
}
if(matched && ++i === 3) {
break;
}
}
headers = null;
if(ETag || LastModified) {
headers = {};
if(ETag) {
headers['If-None-Match'] = ETag;
}
if(LastModified) {
headers['If-Modified-Since'] = LastModified;
}
}
const hasUrl = this._data.has(url);
this._data.set(url, {
hasCacheControl,
params: headers ? { headers, useTimeout: true } : { useTimeout: true }
});
return hasUrl || hasCacheControl;
},
_data: new Map()
};
function getAjaxResponseEl(text, needForm) {
return !text.includes('