· 6 years ago · Apr 03, 2020, 11:10 PM
1// ==UserScript==
2// @name Dollchan Extension Tools
3// @version 20.3.17.0
4// @namespace http://www.freedollchan.org/scripts/*
5// @author Sthephan Shinkufag @ FreeDollChan
6// @copyright © Dollchan Extension Team. See the LICENSE file for license rights and limitations (MIT).
7// @description Doing some profit for imageboards
8// @icon https://raw.github.com/SthephanShinkufag/Dollchan-Extension-Tools/master/Icon.png
9// @updateURL https://raw.github.com/SthephanShinkufag/Dollchan-Extension-Tools/master/Dollchan_Extension_Tools.meta.js
10// @nocompat Chrome
11// @run-at document-start
12// @grant GM_getValue
13// @grant GM_setValue
14// @grant GM_deleteValue
15// @grant GM_openInTab
16// @grant GM_xmlhttpRequest
17// @grant GM.getValue
18// @grant GM.setValue
19// @grant GM.deleteValue
20// @grant GM.xmlHttpRequest
21// @grant unsafeWindow
22// @include *
23// ==/UserScript==
24
25/* eslint indent: ["error", "tab", {
26 "flatTernaryExpressions": true,
27 "outerIIFEBody": 0 }] */
28
29(function deMainFuncInner(deWindow, prestoStorage, FormData, scrollTo, localData) {
30'use strict';
31
32const version = '20.3.17.0';
33const commit = '772b6eb';
34
35/* ==[ DefaultCfg.js ]========================================================================================
36 DEFAULT CONFIG
37=========================================================================================================== */
38
39const defaultCfg = {
40 disabled : 0, // Dollchan enabled by default
41 language : 0, // Dollchan language [0=ru, 1=en]
42 hideBySpell : 1, // hide posts by spells
43 spells : null, // user defined spells
44 sortSpells : 0, // sort spells and remove duplicates
45 hideRefPsts : 0, // hide replies to hidden posts
46 nextPageThr : 0, // load threads from next pages instead of hidden
47 delHiddPost : 0, // remove placeholders [0=off, 1=all, 2=posts only, 3=threads only]
48 ajaxUpdThr : 1, // threads updater
49 updThrDelay : 20, // update interval (sec)
50 updCount : 1, // show countdown to thread update
51 favIcoBlink : 0, // blink the favicon on new posts
52 desktNotif : 0, // desktop notifications for new posts
53 noErrInTitle : 0, // don't show error code in title (except 404)
54 markNewPosts : 1, // highlight new posts with color
55 useDobrAPI : 1, // dobrochan: use json API
56 markMyPosts : 1, // highlight my own posts
57 expandTrunc : 0, // auto-expand truncated posts
58 showHideBtn : 1, // show "Hide" buttons [0=off, 1=with menu, 2=no menu]
59 showRepBtn : 1, // show "Quick reply" buttons [0=off, 1=with menu, 2=no menu]
60 postBtnsCSS : 1, // post buttons style [0=simple, 1=gradient grey, 2=custom]
61 postBtnsBack : '#8c8c8c', // custom background color
62 thrBtns : 1, /* additional buttons under threads
63 [0=off, 1=all, 2=all (on board), 3='New posts' on board] */
64 noSpoilers : 1, // text spoilers expansion [0=off, 1=grey, 2=native]
65 limitPostMsg : 2000, // limit text width in posts nessages
66 widePosts : 0, // stretch posts to screen width
67 noPostNames : 0, // hide poster names
68 correctTime : 0, // time correction in posts
69 timeOffset : '+0', // time offset (h)
70 timePattern : '', // search pattern
71 timeRPattern : '', // replace pattern
72 expandImgs : 2, // expand images on click [0=off, 1=in post, 2=by center]
73 imgNavBtns : 1, // add buttons to navigate images
74 imgInfoLink : 1, // show name under expanded image
75 resizeDPI : 0, // don't upscale images on high DPI displays
76 resizeImgs : 1, // resize large images to fit screen [0=off', '1=by width', '2=width+height]
77 minImgSize : 100, // minimal size for expanded images (px)
78 zoomFactor : 25, // images zoom sensibility [1-100%]
79 webmControl : 1, // show control bar for WebM
80 webmTitles : 0, // load titles from WebM metadata
81 webmVolume : 100, // default volume for WebM [0-100%]
82 minWebmWidth : 320, // minimal width for WebM (px)
83 preLoadImgs : 0, // preload images [0=off, 1=all, 2=non-WebM]
84 findImgFile : 0, // detect embedded files in images
85 openImgs : 0, // replace thumbs with original images [0=off, 1=all, 2=GIFs only, 3=non-GIFs]
86 imgSrcBtns : 1, // add "Search" buttons for images
87 imgNames : 0, // image names in links [0=off, 1=original, 2=hide]
88 maskImgs : 0, // NSFW mode
89 maskVisib : 7, // image opacity in NSFW mode [0-100%]
90 linksNavig : 1, // posts navigation by >>links
91 linksOver : 100, // delay appearance (ms)
92 linksOut : 1500, // delay disappearance (ms)
93 markViewed : 0, // mark viewed posts
94 strikeHidd : 0, // strike >>links to hidden posts
95 removeHidd : 0, // also remove from reply maps
96 noNavigHidd : 0, // don't show previews for hidden posts
97 markMyLinks : 1, // mark links to my posts with (You)
98 crossLinks : 0, // replace http:// with >>/b/links*
99 decodeLinks : 0, // decode %D0%A5%D1 in links
100 insertNum : 1, // insert >>link on №postnumber click*
101 addOPLink : 0, // insert >>link when replying to OP on board
102 addImgs : 0, // load images to jpg/png/gif links*
103 addMP3 : 1, // embed mp3 links
104 addVocaroo : 1, // embed Vocaroo links
105 embedYTube : 1, // embed YouTube links [0=off, 1=preview+player, 2=onclick]
106 YTubeWidth : 360, // player width (px)
107 YTubeHeigh : 270, // player height (px)
108 YTubeTitles : 0, // load titles for YouTube links
109 ytApiKey : '', // YouTube API key
110 addVimeo : 1, // embed Vimeo links
111 ajaxPosting : 1, // posting without refresh
112 postSameImg : 1, // ability to post duplicate images
113 removeEXIF : 1, // remove EXIF from JPEG
114 removeFName : 0, // clear file names [0=off, 1=empty, 2=unixtime, 3=unixtime-random]
115 sendErrNotif : 1, // inform in title about post send error
116 scrAfterRep : 0, // scroll to bottom after reply
117 fileInputs : 2, // enhanced file attachment field [0=off, 1=simple, 2=preview]
118 addPostForm : 2, // reply form display in thread [0=at top, 1=at bottom, 2=hidden]
119 spacedQuote : 1, // insert a space when quoting "> "
120 favOnReply : 1, // add thread to favorites after reply
121 warnSubjTrip : 0, // warn about a tripcode in "Subject" field
122 addSageBtn : 1, // replace "Email" with Sage button
123 saveSage : 1, // remember sage
124 sageReply : 0, // reply with sage
125 altCaptcha : 0, // use alternative captcha (if available)
126 capUpdTime : 300, // captcha update interval (sec)
127 captchaLang : 1, // forced captcha input language [0=off, 1=en, 2=ru]
128 addTextBtns : 1, // text markup buttons [0=off, 1=graphics, 2=text, 3=usual]
129 txtBtnsLoc : 1, // located at [0=top, 1=bottom]
130 userPassw : 1, // user password
131 passwValue : '', // value
132 userName : 0, // user name
133 nameValue : '', // value
134 noBoardRule : 0, // hide board rules
135 noPassword : 1, // hide form "Password" field
136 noName : 0, // hide form "Name" field
137 noSubj : 0, // hide form "Subject" field
138 scriptStyle : 0, /* Dollchan style
139 [0=Gradient darkblue, 1=gradient blue, 2=solid grey, 3=transparent blue, 4=square dark] */
140 userCSS : 0, // user CSS
141 userCSSTxt : '', // css text
142 expandPanel : 0, // show full main panel
143 animation : 1, // CSS3 animation
144 hotKeys : 1, // hotkeys
145 loadPages : 1, // number of pages that are loaded on F5
146 panelCounter : 1, // panel counter for posts/images [0=off, 1=all posts, 2=except hidden]
147 hideReplies : 0, // show only op-posts in threads list
148 rePageTitle : 1, // show thread title in the page tab
149 inftyScroll : 1, // infinite scrolling for pages
150 scrollToTop : 0, // always scroll to top in the threads list
151 saveScroll : 1, // remember the scroll position in threads
152 favThrOrder : 0, /* threads sorting order in the Favorites window
153 [0=by opnum, 1=by opnum (desc), 2=by adding, 3=by adding (desc)] */
154 favWinOn : 0, // Always open the Favorites window
155 closePopups : 0, // close popups automatically
156 updDollchan : 2, // Check for Dollchan updates [0=off, 1=per day, 2=2days, 3=week, 4=2weeks, 5=month]
157 textaWidth : 300, // textarea width (px)
158 textaHeight : 115, // textarea height (px)
159 replyWinDrag : 0, // draggable "Quick Reply" form
160 replyWinX : 'right: 0', // "Quick Reply" form X position
161 replyWinY : 'top: 0', // "Quick Reply" form Y position
162 cfgTab : 'filters', // remembered tab in "Settings" window
163 cfgWinDrag : 0, // draggable "Settings" window
164 cfgWinX : 'right: 0', // "Settings" window X position
165 cfgWinY : 'top: 0', // "Settings" window Y position
166 hidWinDrag : 0, // draggable "Hidden" window
167 hidWinX : 'right: 0', // "Hidden" window X position
168 hidWinY : 'top: 0', // "Hidden" window Y position
169 favWinDrag : 0, // draggable "Favorites" window
170 favWinX : 'right: 0', // "Favorites" window X position
171 favWinY : 'top: 0', // "Favorites" window Y position
172 favWinWidth : 500, // "Favorites" window width (px)
173 vidWinDrag : 0, // draggable "Video" window
174 vidWinX : 'right: 0', // "Video" window X position
175 vidWinY : 'top: 0' // "Video" window Y position
176};
177
178/* ==[ Localization.js ]======================================================================================
179 LOCALIZATION
180=========================================================================================================== */
181
182const Lng = {
183 // "Settings" window: tab names
184 cfgTab: {
185 filters : ['Фильтры', 'Filters', 'Фільтри'],
186 posts : ['Посты', 'Posts', 'Пости'],
187 images : ['Картинки', 'Images', 'Зображ.'],
188 links : ['Ссылки', 'Links', 'Посил.'],
189 form : ['Форма', 'Form', 'Форма'],
190 common : ['Общее', 'Common', 'Спільне'],
191 info : ['Инфо', 'Info', 'Інфо']
192 },
193 // "Settings" window: options
194 cfg: {
195 language: {
196 sel : [['Ru', 'En', 'Ua'], ['Ru', 'En', 'Ua'], ['Ru', 'En', 'Ua']],
197 txt : ['', '', '']
198 },
199
200 // "Filters" tab
201 hideBySpell: [
202 'Спеллы: ',
203 'Magic spells: ',
204 'Спелли: '],
205 sortSpells: [
206 'Сортировать спеллы и удалять дубликаты',
207 'Sort spells and remove duplicates',
208 'Сортувати спелли та видаляти дублікати'],
209 hideRefPsts: [
210 'Скрывать ответы на скрытые посты',
211 'Hide replies to hidden posts',
212 'Ховати відповіді на сховані пости'],
213 nextPageThr: [
214 'Скрытые треды - загружать со следующих страниц',
215 'Load threads from next pages instead of hidden',
216 'Сховані треди - брати з наступних сторінок'],
217 delHiddPost: {
218 sel: [
219 ['Откл.', 'Всё', 'Только посты', 'Только треды'],
220 ['Disable', 'All', 'Posts only', 'Threads only'],
221 ['Вимк.', 'Все', 'Лише пости', 'Лише треди']],
222 txt: [
223 'Удалять скрытое',
224 'Remove placeholders',
225 'Видаляти сховане']
226 },
227
228 // "Posts" tab
229 ajaxUpdThr: [
230 'Апдейтер тредов ',
231 'Threads updater ',
232 'Оновлювач тредів '],
233 updThrDelay: [
234 '(сек)',
235 '(sec)',
236 '(сек)'],
237 updCount: [
238 'Обратный счетчик обновления треда',
239 'Show countdown to thread update',
240 'Зворотній відлік оновлення треду'],
241 favIcoBlink: [
242 'Мигать фавиконом при появлении новых постов',
243 'Blink the favicon on new posts',
244 'Блимати фавіконом в разі появи нових постів'],
245 desktNotif: [
246 'Уведомлять о новых постах на рабочем столе',
247 'Desktop notifications for new posts',
248 'Повідомляти про нові пости на стільниці'],
249 noErrInTitle: [
250 'Не показывать номер ошибки в заголовке',
251 'Donʼt show error code in pageʼs title',
252 'Не показувати номер помилки в заголовку'],
253 markNewPosts: [
254 'Выделять цветом новые посты',
255 'Highlight new posts with color',
256 'Виділяти кольором нові пости'],
257 useDobrAPI: [
258 'dobrochan: использовать JSON API',
259 'dobrochan: use JSON API',
260 'dobrochan: використовувати JSON API'],
261 markMyPosts: [
262 'Выделять цветом мои посты',
263 'Highlight my own posts',
264 'Виділяти кольором мої пости'],
265 expandTrunc: [
266 'Авторазворот сокращенных постов*',
267 'Autoexpand truncated posts*',
268 'Авторозгортання скорочених постів*'],
269 thrBtns: {
270 sel: [
271 ['Откл.', 'Все', 'Все (на доске)', '"Новые посты" на доске'],
272 ['Disable', 'All', 'All (on board)', '"New posts" on board'],
273 ['Вимк.', 'Всі', 'Всі (на дошці)', '"Нові пости" на дошці']],
274 txt: [
275 'Кнопки под тредами',
276 'Buttons under threads',
277 'Кнопки під тредами']
278 },
279 showHideBtn: {
280 sel: [
281 ['Откл.', 'С меню', 'Без меню'],
282 ['Disable', 'With menu', 'No menu'],
283 ['Вимк.', 'Із меню', 'Без меню']],
284 txt: [
285 'Кнопки "Скрыть пост/тред"',
286 '"Hide post/thread" buttons',
287 'Кнопки "Сховати пост/тред"']
288 },
289 showRepBtn: {
290 sel: [
291 ['Откл.', 'С меню', 'Без меню'],
292 ['Disable', 'With menu', 'No menu'],
293 ['Вимк.', 'Із меню', 'Без меню']],
294 txt: [
295 'Кнопки "Ответить на пост/тред"',
296 '"Reply to post/thread" buttons',
297 'Кнопки "Відповісти на пост/тред"']
298 },
299 postBtnsCSS: {
300 sel: [
301 ['Упрощенные', 'Серый градиент', 'Настраиваемые'],
302 ['Simple', 'Gradient grey', 'Custom'],
303 ['Спрощені', 'Сірий градієнт', 'Користувацькі']],
304 txt: [
305 'Кнопки постов ',
306 'Post buttons ',
307 'Кнопки постів ']
308 },
309 noSpoilers: {
310 sel: [
311 ['Откл.', 'Серое', 'Родное'],
312 ['Disable', 'Grey', 'Native'],
313 ['Вимк.', 'Сіре', 'Рідне']],
314 txt: [
315 'Раскрытие текстовых спойлеров',
316 'Text spoilers expansion',
317 'Розкриття текстових спойлерів']
318 },
319 limitPostMsg: [
320 'Ограничение ширины текста в постах (px)',
321 'Limit text width in posts messages (px)',
322 'Обмеження ширини тексту в постах (px)'
323 ],
324 widePosts: [
325 'Растягивать посты по ширине экрана',
326 'Stretch posts to page width',
327 'Розтягувати пости на ширину екрану'],
328 noPostNames: [
329 'Скрывать имена в постах',
330 'Hide poster names',
331 'Ховати імена в постах'],
332 correctTime: [
333 'Коррекция времени в постах* ',
334 'Time correction in posts* ',
335 'Корекція часу в постах* '],
336 timeOffset: [
337 'разница (ч) ',
338 'time offset (h) ',
339 'різниця (год) '],
340 timePattern: [
341 'Шаблон поиска',
342 'Search pattern',
343 'Шаблон пошуку'],
344 timeRPattern: [
345 'Шаблон замены',
346 'Replace pattern',
347 'Шаблон заміни'],
348
349 // "Images" tab
350 expandImgs: {
351 sel: [
352 ['Откл.', 'В посте', 'По центру'],
353 ['Disable', 'In post', 'By center'],
354 ['Вимк.', 'В пості', 'По центру']],
355 txt: [
356 'Раскрывать картинки по клику',
357 'Expand images on click',
358 'Розгортати зображення по кліку']
359 },
360 imgNavBtns: [
361 'Добавлять кнопки навигации по картинкам',
362 'Add buttons to navigate images',
363 'Додавати кнопки навігації по зображеннях'],
364 imgInfoLink: [
365 'Имя файла под раскрытой картинкой',
366 'Show file name under expanded image',
367 'Імʼя файлу під розкритим зображенням'],
368 resizeDPI: [
369 'Не растягивать на дисплеях с высоким DPI',
370 'Donʼt upscale images on high DPI displays',
371 'Не розтягувати на дисплеях з високим DPI'],
372 resizeImgs: {
373 sel: [
374 ['Откл.', 'По ширине', 'Шир.+выс.'],
375 ['Disable', 'By width', 'Width+Height'],
376 ['Вимк.', 'По ширині', 'Шир.+выс.']],
377 txt: [
378 'Уменьшать при раскрытии в посте',
379 'Fit to screen for expanding in post',
380 'Зменшувати при розкритті в пості']
381 },
382 minImgSize: [
383 'Миним. размер раскрытых картинок (px)',
384 'Minimal size for expanded images (px)',
385 'Мінім. розмір розгорнутих зображень (px)'],
386 zoomFactor: [
387 'Чувствительность зума картинок [1-100%]',
388 'Images zoom sensibility [1-100%]',
389 'Чутливість зуму зображень [1-100%]'],
390 webmControl: [
391 'Показывать контрол-бар для WebM',
392 'Show control bar for WebM',
393 'Показувати смугу керування для WebM'],
394 webmTitles: [
395 'Получать названия WebM из метаданных',
396 'Load titles from WebM metadata',
397 'Отримувати назви WebM з метаданих'],
398 webmVolume: [
399 'Громкость WebM по умолчанию [0-100%]',
400 'Default volume for WebM [0-100%]',
401 'Гучність WebM по замовчуванню [0-100%]'],
402 minWebmWidth: [
403 'Минимальная ширина WebM (px)',
404 'Minimal width for WebM (px)',
405 'Мінімальна ширина WebM (px)'],
406 preLoadImgs: {
407 sel: [
408 ['Откл.', 'Все', 'Без WebM'],
409 ['Disable', 'All', 'Non-WebM'],
410 ['Вимк.', 'Всі', 'Крім WebM']],
411 txt: [
412 'Предварительно загружать картинки*',
413 'Preload images*',
414 'Наперед завантажувати зображення*']
415 },
416 findImgFile: [
417 'Распознавать файлы, встроенные в картинках*',
418 'Detect embedded files in images*',
419 'Розпізнавати файли, що вбудовані в зображення*'],
420 openImgs: {
421 sel: [
422 ['Откл.', 'Все подряд', 'Только GIF', 'Кроме GIF'],
423 ['Disable', 'All types', 'Only GIF', 'Non-GIF'],
424 ['Вимк.', 'Всі', 'Лише GIF', 'Крім GIF']],
425 txt: [
426 'Заменять картинки на оригиналы*',
427 'Replace thumbnails with original images*',
428 'Замінювати зображення на оригінали*']
429 },
430 imgSrcBtns: [
431 'Добавлять кнопки "Поиск" для картинок',
432 'Add "Search" buttons for images',
433 'Додавати кнопки "Пошук" для зображень'],
434 imgNames: {
435 sel: [
436 ['Не изменять', 'Настоящие (сокр.)', 'Скрывать', 'Настоящие (полные)'],
437 ['Don`t change', 'Original (trunc.)', 'Hide', 'Original (full)'],
438 ['Не змінювати', 'Справжні (скороч.)', 'Ховати', 'Справжні (повні)']],
439 txt: [
440 'имена картинок',
441 'filenames',
442 'імена зображень']
443 },
444 maskVisib: [
445 'Видимость для NSFW-картинок [0-100%]',
446 'Visibility for NSFW images [0-100%]',
447 'Видимість для NSFW-зображень [0-100%]'],
448
449 // "Links" tab
450 linksNavig: [
451 'Навигация постов по >>ссылкам* ',
452 'Posts navigation by >>links* ',
453 'Навігація постів по >>посиланнях* '],
454 linksOver: [
455 'Появление ',
456 'Appearance ',
457 'Поява '],
458 linksOut: [
459 'Пропадание (мс)',
460 'Disappearance (ms)',
461 'Зникнення (мс)'],
462 markViewed: [
463 'Помечать просмотренные посты',
464 'Mark viewed posts',
465 'Позначати переглянуті пости'],
466 strikeHidd: [
467 'Зачеркивать >>ссылки на скрытые посты',
468 'Strike >>links to hidden posts',
469 'Закреслювати >>посилання на сховані пости'],
470 removeHidd: [
471 'Также удалять из обратных >>ссылок',
472 'Also remove from >>backlinks',
473 'Також видаляти із зворотніх >>посилань'],
474 noNavigHidd: [
475 'Не отображать превью для скрытых постов',
476 'Donʼt show previews for hidden posts',
477 'Не показувати превʼю до cхованих постів'],
478 markMyLinks: [
479 'Помечать ссылки на мои посты как (You)',
480 'Mark links to my posts with (You)',
481 'Позначати посилання на мої пости як (You)'],
482 crossLinks: [
483 'Заменять http:// на >>/b/ссылки*',
484 'Replace http:// with >>/b/links*',
485 'Замінювати https:// на >>/b/посилання*'],
486 decodeLinks: [
487 'Декодировать %D0%A5%D1 в ссылках*',
488 'Decode %D0%A5%D1 in links*',
489 'Декодувати %D0%A5%D1 в посиланнях*'],
490 insertNum: [
491 'Вставлять >>ссылку по клику на №поста*',
492 'Insert >>link on №postnumber click*',
493 'Вставляти >>посилання на клік по №посту*'],
494 addOPLink: [
495 '>>ссылка при ответе на OP в списке тредов',
496 'Insert >>link when replying to OP on threads list',
497 '>>посилання при відповіді на OP у списці тредів'],
498 addImgs: [
499 'Загружать картинки к jpg/png/gif ссылкам*',
500 'Load images for jpg/png/gif links*',
501 'Додавати зображення до jpg/png/gif посилань*'],
502 addMP3: [
503 'Плеер к mp3 ссылкам* ',
504 'Player for mp3 links* ',
505 'Плеєр до mp3 посилань* '],
506 addVocaroo: [
507 'к Vocaroo ссылкам*',
508 'for Vocaroo links*',
509 'до Vocaroo посилань*'],
510 addVimeo: [
511 'Добавлять плеер к Vimeo ссылкам*',
512 'Add player for Vimeo links*',
513 'Додавати плеєр до Vimeo посилань*'],
514 embedYTube: {
515 sel: [
516 ['Ничего', 'Превью+плеер', 'Плеер по клику'],
517 ['Nothing', 'Preview+player', 'On click player'],
518 ['Нічого', 'Превʼю+плеєр', 'Плеєр по кліку']],
519 txt: [
520 'к YouTube ссылкам* ',
521 'for YouTube links* ',
522 'до YouTube посилань* ']
523 },
524 YTubeTitles: [
525 'Загружать названия к YouTube ссылкам*',
526 'Load titles for YouTube links*',
527 'Отримувати назви до YouTube посилань*'],
528 ytApiKey: [
529 'Ключ YT API*',
530 'YT API Key*',
531 'Ключ YT API*'],
532
533 // "Form" tab
534 ajaxPosting: [
535 'Отправка постов без перезагрузки*',
536 'Posting without page refresh*',
537 'Постування без оновлення сторінки*'],
538 postSameImg: [
539 'Возможность отправки одинаковых картинок',
540 'Ability to post duplicate images',
541 'Можливість надсилання однакових зображень'],
542 removeEXIF: [
543 'Удалять EXIF из JPEG ',
544 'Remove EXIF from JPEG ',
545 'Видаляти EXIF з JPEG '],
546 removeFName: {
547 sel: [
548 ['Не изменять', 'Удалять', 'Unixtime', 'Unixtime-random'],
549 ['Don`t change', 'Clear', 'Unixtime', 'Unixtime-random'],
550 ['Не змінювати', 'Видаляти', 'Unixtime', 'Unixtime-random']],
551 txt: [
552 'имена файлов',
553 'file names',
554 'імена файлів']
555 },
556 sendErrNotif: [
557 'Оповещать в заголовке об ошибке отправки',
558 'Inform in title about post send error',
559 'Сповіщати в заголовку про помилку надсилання'],
560 scrAfterRep: [
561 'Перемещаться в конец треда после отправки',
562 'Scroll to bottom after reply',
563 'Гортати в кінець треду після надсилання'],
564 fileInputs: {
565 sel: [
566 ['Откл.', 'Упрощ.', 'Превью'],
567 ['Disable', 'Simple', 'Preview'],
568 ['Вимкн.', 'Спрощене', 'Превʼю']],
569 txt: [
570 'Улучшенное поле добавления файлов',
571 'Enhanced file attachment field',
572 'Покращене поле додавання файлів']
573 },
574 addPostForm: {
575 sel: [
576 ['Сверху', 'Внизу', 'Скрытая'],
577 ['At top', 'At bottom', 'Hidden'],
578 ['Вгорі', 'Знизу', 'Прихована']],
579 txt: [
580 'Форма ответа в треде',
581 'Reply form display in thread',
582 'Форма відповіді в треді']
583 },
584 spacedQuote: [
585 'Вставлять пробел при цитировании "> "',
586 'Insert a space when quoting "> "',
587 'Вставляти пробіл при цитуванні "> "'],
588 favOnReply: [
589 'Добавлять тред в "Избранное" после ответа',
590 'Add thread to "Favorites" after reply',
591 'Додавати тред в "Вибране" після відповіді'],
592 warnSubjTrip: [
593 'Оповещать о трипкоде в поле "Тема"',
594 'Warn about a tripcode in "Subject" field',
595 'Сповіщувати про трипкод в полі "Тема"'],
596 addSageBtn: [
597 'Кнопка Sage вместо поля "Email" ',
598 'Replace "Email" with Sage button ',
599 'Кнопка Sage замість "E-mail" '],
600 saveSage: [
601 'Помнить сажу',
602 'Remember sage',
603 'Памʼятати сажу'],
604 altCaptcha: [
605 'Использовать альтернативную капчу',
606 'Use alternative captcha',
607 'Використовувати альтернативну капчу'],
608 capUpdTime: [
609 'Интервал обновления капчи (сек)',
610 'Captcha update interval (sec)',
611 'Інтервал оновлення капчі (сек)'],
612 captchaLang: {
613 sel: [
614 ['Откл.', 'Eng', 'Rus'],
615 ['Disable', 'Eng', 'Rus'],
616 ['Вимк.', 'Eng', 'Ukr']],
617 txt: [
618 'Принудительный язык ввода капчи',
619 'Forced captcha input language',
620 'Примусова мова вводу капчі']
621 },
622 addTextBtns: {
623 sel: [
624 ['Откл.', 'Графические', 'Упрощённые', 'Стандартные'],
625 ['Disable', 'As images', 'As text', 'Standard'],
626 ['Вимк.', 'Графічні', 'Спрощені', 'Стандартні']],
627 txt: [
628 'Кнопки разметки текста ',
629 'Text markup buttons ',
630 'Кнопки розмітки тексту ']
631 },
632 txtBtnsLoc: [
633 'Внизу',
634 'At bottom',
635 'Знизу'],
636 userPassw: [
637 'Постоянный пароль',
638 'Fixed password',
639 'Постійний пароль'],
640 userName: [
641 'Постоянное имя',
642 'Fixed name',
643 'Постійне імʼя'],
644 noBoardRule: [
645 'Правила ',
646 'Rules ',
647 'Правила '],
648 noPassword: [
649 'Пароль ',
650 'Password ',
651 'Пароль '],
652 noName: [
653 'Имя ',
654 'Name ',
655 'Імʼя '],
656 noSubj: [
657 'Тему',
658 'Subject',
659 'Тему'],
660
661 // "Common" tab
662 scriptStyle: {
663 sel: [
664 ['Gradient darkblue', 'Gradient blue', 'Solid grey', 'Transparent blue', 'Square dark'],
665 ['Gradient darkblue', 'Gradient blue', 'Solid grey', 'Transparent blue', 'Square dark'],
666 ['Gradient darkblue', 'Gradient blue', 'Solid grey', 'Transparent blue', 'Square dark']],
667 txt: [
668 'Стиль Dollchan',
669 'Dollchan style',
670 'Стиль Dollchan']
671 },
672 userCSS: [
673 'Пользовательский CSS',
674 'User CSS',
675 'Користувацький CSS'],
676 animation: [
677 'CSS3 анимация',
678 'CSS3 animation',
679 'CSS3 анімація'],
680 hotKeys: [
681 'Горячие клавиши',
682 'Hotkeys',
683 'Гарячі клавіші'],
684 loadPages: [
685 'Количество страниц, загружаемых по F5',
686 'Number of pages that are loaded on F5 ',
687 'Кількість сторінок, що завантажуються по F5'],
688 panelCounter: {
689 sel: [
690 ['Откл.', 'Все посты', 'Без скрытых'],
691 ['Disabled', 'All posts', 'Except hidden'],
692 ['Вимкн.', 'Усі пости', 'Крім схованих']],
693 txt: [
694 'Счетчик постов/картинок в треде',
695 'Сounter for posts/images in thread',
696 'Лічильник постів/зображень в треді']
697 },
698 rePageTitle: [
699 'Название треда в заголовке вкладки*',
700 'Show thread title in the page tab*',
701 'Назва треду в заголовку вкладки*'],
702 inftyScroll: [
703 'Бесконечная прокрутка страниц',
704 'Infinite scrolling for pages',
705 'Нескінченна прокрутка сторінок'],
706 hideReplies: [
707 'Показывать только OP в списке тредов*',
708 'Show only OP in threads list*',
709 'Показувати лише OP в списку тредів*'],
710 scrollToTop: [
711 'Всегда перемещаться вверх в списке тредов',
712 'Always scroll to top in the threads list',
713 'Завжди гортати догори в списку тредів'],
714 saveScroll: [
715 'Запоминать позицию скролла в тредах',
716 'Remember the scroll position in threads',
717 'Пам`ятати позицію скролла в тредах'],
718 favThrOrder: {
719 sel: [
720 ['По номеру', 'По номеру (убыв)', 'По добавлению', 'По добавлению (убыв)'],
721 ['By number', 'By number (desc)', 'By adding', 'By adding (desc)'],
722 ['За номером', 'За номером (зменш)', 'По додаванню', 'По додаванню (зменш)']],
723 txt: [
724 'Сортировка в Избранном',
725 'Sorting in Favorites',
726 'Сортування в Вибраному']
727 },
728 favWinOn: [
729 'Всегда открывать окно Избранное',
730 'Always open the Favorites window',
731 'Завжди відкривати вікно Вибране'],
732 closePopups: [
733 'Автоматически закрывать уведомления',
734 'Close popups automatically',
735 'Автоматично закривати сповіщення'],
736 updDollchan: {
737 sel: [
738 ['Откл.', 'Каждый день', 'Каждые 2 дня', 'Каждую неделю', 'Каждые 2 недели', 'Каждый месяц'],
739 ['Disable', 'Every day', 'Every 2 days', 'Every week', 'Every 2 weeks', 'Every month'],
740 ['Вимкн.', 'Щодня', 'Кожні 2 дні', 'Щотижня', 'Кожні 2 тижні', 'Щомісяця']],
741 txt: [
742 'Проверять обновления Dollchan',
743 'Check for Dollchan updates',
744 'Перевіряти оновлення Dollchan']
745 }
746 },
747
748 // Main panel buttons: tooltips
749 panelBtn: {
750 attach: [
751 'Прикрепить/Открепить панель',
752 'Attach/Detach panel',
753 'Закріпити/відкріпити панель'],
754 cfg: [
755 'Настройки',
756 'Settings',
757 'Налаштування'],
758 hid: [
759 'Скрытое',
760 'Hidden',
761 'Сховане'],
762 fav: [
763 'Избранное',
764 'Favorites',
765 'Вибране'],
766 vid: [
767 'Ссылки на видео',
768 'Video links',
769 'Посилання на відео'],
770 refresh: [
771 'Обновить',
772 'Refresh',
773 'Оновити'],
774 goback: [
775 'Назад на доску',
776 'Return to board',
777 'Назад до дошки'],
778 gonext: [
779 'На %s страницу',
780 'Go to page %s',
781 'До %s сторінки'],
782 goup: [
783 'В начало страницы',
784 'Scroll to top',
785 'Прогорнути догори'],
786 godown: [
787 'В конец страницы',
788 'Scroll to bottom',
789 'Прогорнути донизу'],
790 expimg: [
791 'Раскрыть все картинки',
792 'Expand all images',
793 'Розгорнути всі зображення'],
794 maskimg: [
795 'Режим NSFW',
796 'NSFW mode',
797 'Режим NSFW'],
798 preimg: [
799 'Предзагрузить картинки\r\n([Ctrl+Click] только для новых постов)',
800 'Preload images\r\n([Ctrl+Click] for new posts only)',
801 'Наперед завантажити зображення\r\n([Ctrl+Click] лише для нових постів)'],
802 savethr: [
803 'Сохранить на диск',
804 'Save to disk',
805 'Зберегти на диск'],
806 'upd-on': [
807 'Выключить автообновление треда',
808 'Disable thread updater',
809 'Вимкнути оновлювач треду'],
810 'upd-off': [
811 'Включить автообновление треда',
812 'Enable thread updater',
813 'Увімкнути оновлювач треду'],
814 'audio-off': [
815 'Звуковое оповещение о новых постах',
816 'Sound notification about new posts',
817 'Звукове сповіщення про нові пости'],
818 catalog: [
819 'Перейти в каталог',
820 'Go to catalog',
821 'Перейти до каталогу'],
822 enable: [
823 'Включить/выключить Dollchan',
824 'Turn on/off the Dollchan',
825 'Увімкнути/вимкнути Dollchan'],
826 pcount: [
827 'Постов в треде',
828 'Posts in thread',
829 'Постів у треді'],
830 pcountNotHid: [
831 'Постов в треде (без скрытых)',
832 'Posts in thread (without hidden)',
833 'Постів у треді (крім схованих)'],
834 imglen: [
835 'Картинок в треде',
836 'Images in thread',
837 'Зображень у треді'],
838 posters: [
839 'Постящих в треде',
840 'Posters in thread',
841 'Постувачів у треді']
842 },
843
844 // Post buttons: tooltips
845 togglePost: [
846 'Скрыть/Раскрыть пост',
847 'Hide/Unhide post',
848 'Сховати/показати пост'],
849 toggleThr: [
850 'Скрыть/Раскрыть тред',
851 'Hide/Unhide thread',
852 'Сховати/показати тред'],
853 replyToPost: [
854 'Ответить на пост',
855 'Reply to post',
856 'Відповісти на пост'],
857 replyToThr: [
858 'Ответить в тред',
859 'Reply to thread',
860 'Відповісти в тред'],
861 expandThr: [
862 'Развернуть тред',
863 'Expand thread',
864 'Розгорнути тред'],
865 addFav: [
866 'Добавить тред в Избранное',
867 'Add thread to Favorites',
868 'Додати тред в Вибране'],
869 delFav: [
870 'Убрать тред из Избранного',
871 'Remove thread from Favorites',
872 'Прибрати тред з Вибраного'],
873 attachPview: [
874 'Закрепить превью',
875 'Attach preview',
876 'Закріпити превʼю'],
877
878 // Windows buttons: tooltips
879 closeWindow: [
880 'Закрыть окно',
881 'Close window',
882 'Закрити вікно'],
883 closeReply: [
884 'Закрыть форму',
885 'Close form',
886 'Закрити форму'],
887 toPanel: [
888 'Закрепить на панели',
889 'Attach to panel',
890 'Закріпити на панелі'],
891 makeDrag: [
892 'Сделать перетаскиваемым окном',
893 'Make draggable window',
894 'Зробити перетягуваним вікном'],
895 underPost: [
896 'Разместить форму после поста',
897 'Move form under post',
898 'Розмістити форму після посту'],
899 clearForm: [
900 'Очистить форму',
901 'Clear form',
902 'Очистити форму'],
903
904 // Markup buttons: tooltips
905 txtBtn: [
906 ['Жирный', 'Bold', 'Жирний'],
907 ['Курсив', 'Italic', 'Курсив'],
908 ['Подчеркнутый', 'Underlined', 'Підкреслений'],
909 ['Зачеркнутый', 'Strike', 'Закреслений'],
910 ['Спойлер', 'Spoiler', 'Спойлер'],
911 ['Код', 'Code', 'Код'],
912 ['Верхний индекс', 'Superscript', 'Верхній індекс'],
913 ['Нижний индекс', 'Subscript', 'Нижній індекс'],
914 ['Цитировать выделенное', 'Quote selected', 'Цитувати виділене']],
915
916 // Drop-down menus: options
917 selHiderMenu: { // "Hide" post button
918 sel: [
919 'Скрывать выделенное',
920 'Hide selected text',
921 'Ховати виділене'],
922 name: [
923 'Скрывать по имени',
924 'Hide by name',
925 'Ховати по імені'],
926 trip: [
927 'Скрывать по трипкоду',
928 'Hide by tripcode',
929 'Ховати по тріпкоду'],
930 img: [
931 'Скрывать по размеру картинки',
932 'Hide by image size',
933 'Ховати по розміру зображення'],
934 imgn: [
935 'Скрывать по имени картинки',
936 'Hide by image name',
937 'Ховати по імені зображення'],
938 ihash: [
939 'Скрывать схожие картинки',
940 'Hide by similar images',
941 'Ховати подібні зображення'],
942 noimg: [
943 'Скрывать без картинок',
944 'Hide without images',
945 'Ховати без зображень'],
946 notext: [
947 'Скрывать без текста',
948 'Hide without text',
949 'Ховати без тексту'],
950 text: [
951 'Скрыть схожий текст',
952 'Hide similar text',
953 'Сховати схожий текст'],
954 refs: [
955 'Скрыть с ответами',
956 'Hide with replies',
957 'Сховати з відповідями'],
958 refsonly: [
959 'Скрывать ответы',
960 'Hide replies',
961 'Ховати відповіді']
962 },
963 selExpandThr: [ // "Expand thread" post button
964 ['+10 постов', 'Последние 30', 'Последние 50', 'Последние 100', 'Весь тред'],
965 ['+10 posts', 'Last 30 posts', 'Last 50 posts', 'Last 100 posts', 'Entire thread'],
966 ['+10 постів', 'Останні 30', 'Останні 50', 'Останні 100', 'Весь тред']],
967 selAjaxPages: [ // "Refresh" panel button
968 ['1 страница', '2 страницы', '3 страницы', '4 страницы', '5 страниц'],
969 ['1 page', '2 pages', '3 pages', '4 pages', '5 pages'],
970 ['1 сторінка', '2 сторінки', '3 сторінки', '4 сторінки', '5 сторінок']],
971 selSaveThr: [ // "Save to disk" panel button
972 ['Скачать весь тред', 'Скачать картинки'],
973 ['Download thread', 'Download images'],
974 ['Завантажити весь тред', 'Завантажити зображення']],
975 selAudioNotif: [ // "Sound notification" panel button
976 ['Каждые 30 сек.', 'Каждую минуту', 'Каждые 2 мин.', 'Каждые 5 мин.'],
977 ['Every 30 sec.', 'Every minute', 'Every 2 min.', 'Every 5 min.'],
978 ['Кожні 30 сек.', 'Щохвилини', 'Кожні 2 хв.', 'Кожні 5 хв.']],
979 reportPost: [
980 'Жалоба на пост',
981 'Report post',
982 'Скарга на пост'],
983 reportThr: [
984 'Жалоба на тред',
985 'Report thread',
986 'Скарга на тред'],
987 markMyPost: [
988 'Пометить пост как мой',
989 'Mark post as mine',
990 'Відмітити пост як мій'
991 ],
992 deleteMyPost: [
993 'Убрать из моих постов',
994 'Delete from my posts',
995 'Прибрати з моїх постів'
996 ],
997
998 // Sauce search for images and video frames
999 searchIn: [
1000 'Искать в ',
1001 'Search in ',
1002 'Шукати в '],
1003 frameSearch: [
1004 'Поиск кадра в ',
1005 'Frame search in ',
1006 'Пошук кадру в '],
1007 gotoResults: [
1008 'Перейти к результатам поиска',
1009 'Go to search results',
1010 'Перейти до результатів пошуку'],
1011 getFrameLinks: [
1012 'Получить ссылки для поиска этого кадра',
1013 'Get links to search this frame',
1014 'Отримати посилання для пошуку цього кадру'],
1015 saveFrame: [
1016 'Сохранить полученный кадр',
1017 'Save the received frame',
1018 'Зберегти отриманий кадр'],
1019 errSaucenao: [
1020 'Ошибка: не могу загрузить на saucenao.com',
1021 'Error: can`t load to saucenao.com',
1022 'Помилка: не можу завантажити на saucenao.com'],
1023
1024 // Hotkeys editor
1025 hotKeyEdit: [[
1026 // Ru
1027 '%l%i24 – предыдущая страница/картинка%/l',
1028 '%l%i217 – следующая страница/картинка%/l',
1029 '%l%i21 – тред (на доске)/пост (в треде) ниже%/l',
1030 '%l%i20 – тред (на доске)/пост (в треде) выше%/l',
1031 '%l%i31 – пост (на доске) ниже%/l',
1032 '%l%i30 – пост (на доске) выше%/l',
1033 '%l%i23 – скрыть пост/тред%/l',
1034 '%l%i32 – перейти в тред%/l',
1035 '%l%i33 – развернуть тред%/l',
1036 '%l%i211 – раскрыть картинку в посте%/l',
1037 '%l%i22 – быстрый ответ%/l',
1038 '%l%i25t – отправить пост%/l',
1039 '%l%i210 – открыть/закрыть "Настройки"%/l',
1040 '%l%i26 – открыть/закрыть "Избранное"%/l',
1041 '%l%i27 – открыть/закрыть "Скрытое"%/l',
1042 '%l%i218 – открыть/закрыть "Видео"%/l',
1043 '%l%i28 – открыть/закрыть панель%/l',
1044 '%l%i29 – вкл./выкл. режим NSFW%/l',
1045 '%l%i40 – обновить тред (в треде)%/l',
1046 '%l%i212t – жирный%/l',
1047 '%l%i213t – курсив%/l',
1048 '%l%i214t – зачеркнутый%/l',
1049 '%l%i215t – спойлер%/l',
1050 '%l%i216t – код%/l'], [
1051 // En
1052 '%l%i24 – previous page/image%/l',
1053 '%l%i217 – next page/image%/l',
1054 '%l%i21 – thread (on board)/post (in thread) below%/l',
1055 '%l%i20 – thread (on board)/post (in thread) above%/l',
1056 '%l%i31 – on board post below%/l',
1057 '%l%i30 – on board post above%/l',
1058 '%l%i23 – hide post/thread%/l',
1059 '%l%i32 – go to thread%/l',
1060 '%l%i33 – expand thread%/l',
1061 '%l%i211 – expand postʼs images%/l',
1062 '%l%i22 – quick reply%/l',
1063 '%l%i25t – send post%/l',
1064 '%l%i210 – open/close "Settings"%/l',
1065 '%l%i26 – open/close "Favorites"%/l',
1066 '%l%i27 – open/close "Hidden"%/l',
1067 '%l%i218 – open/close "Videos"%/l',
1068 '%l%i28 – open/close main panel%/l',
1069 '%l%i29 – toggle NSFW mode%/l',
1070 '%l%i40 – update thread%/l',
1071 '%l%i212t – bold%/l',
1072 '%l%i213t – italic%/l',
1073 '%l%i214t – strike%/l',
1074 '%l%i215t – spoiler%/l',
1075 '%l%i216t – code%/l'], [
1076 // Ua
1077 '%l%i24 – попередня сторінка/зображення%/l',
1078 '%l%i217 – наступна сторінка/зображення%/l',
1079 '%l%i21 – тред (на дошці)/пост (в треді) нижче%/l',
1080 '%l%i20 – тред (на дошці)/пост (в треді) вище%/l',
1081 '%l%i31 – пост (на дошці) нижче%/l',
1082 '%l%i30 – пост (на дошці) вище%/l',
1083 '%l%i23 – приховати пост/тред%/l',
1084 '%l%i32 – перейти в тред%/l',
1085 '%l%i33 – розгорнути тред%/l',
1086 '%l%i211 – розгорнути зображення в пості%/l',
1087 '%l%i22 – швидка відповідь%/l',
1088 '%l%i25t – відправити пост%/l',
1089 '%l%i210 – відкрити/закрити "Налаштування"%/l',
1090 '%l%i26 – відкрити/закрити "Вибране"%/l',
1091 '%l%i27 – відкрити/закрити "Сховане"%/l',
1092 '%l%i218 – відкрити/закрити "Посилання на відео"%/l',
1093 '%l%i28 – відкрити/закрити панель%/l',
1094 '%l%i29 – увімкнути/вимкнути режим NSFW%/l',
1095 '%l%i40 – оновити тред (в треді)%/l',
1096 '%l%i212t – жирний%/l',
1097 '%l%i213t – курсив%/l',
1098 '%l%i214t – закреслений%/l',
1099 '%l%i215t – спойлер%/l',
1100 '%l%i216t – код%/l']],
1101
1102 // Time correction in posts
1103 cTimeError: [
1104 'Неправильные настройки времени',
1105 'Invalid time settings',
1106 'Неправильні налаштування часу'],
1107 month: [
1108 ['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'],
1109 ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
1110 ['січ', 'лют', 'бер', 'кві', 'тра', 'чер', 'лип', 'сер', 'вер', 'жов', 'лис', 'гру']],
1111 fullMonth: [
1112 ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
1113 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'],
1114 ['January', 'February', 'March', 'April', 'May', 'June',
1115 'July', 'August', 'September', 'October', 'November', 'December'],
1116 ['січня', 'лютого', 'березня', 'квітня', 'травня', 'червня',
1117 'липня', 'серпня', 'вересня', 'жовтня', 'листопада', 'грудня']],
1118 week: [
1119 ['Вск', 'Пнд', 'Втр', 'Срд', 'Чтв', 'Птн', 'Сбт'],
1120 ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
1121 ['Нед', 'Пон', 'Вів', 'Сер', 'Чет', 'Птн', 'Сбт']],
1122 monthDict: {
1123 /* eslint-disable */
1124 янв: 0, фев: 1, мар: 2, апр: 3, май: 4, мая: 4, июн: 5, июл: 6, авг: 7, сен: 8, окт: 9, ноя: 10, дек: 11,
1125 jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
1126 січ: 0, лют: 1, бер: 2, кві: 3, тра: 4, чер: 5, лип: 6, сер: 7, вер: 8, жов: 9, лис: 10, гру: 11
1127 /* eslint-enable */
1128 },
1129
1130 // Spells: popups
1131 seSyntaxErr: [
1132 'синтаксическая ошибка в аргументе спелла: %s',
1133 'syntax error in argument of spell: %s',
1134 'синтаксична помилка в аргументі спеллу: %s'],
1135 seUnknown: [
1136 'неизвестный спелл: %s',
1137 'unknown spell: %s',
1138 'невідомий спелл: %s'],
1139 seMissOp: [
1140 'пропущен оператор',
1141 'missing operator',
1142 'пропущено оператор'],
1143 seMissArg: [
1144 'пропущен аргумент спелла: %s',
1145 'missing argument of spell: %s',
1146 'пропущено аргумент спеллу: %s'],
1147 seMissSpell: [
1148 'пропущен спелл',
1149 'missing spell',
1150 'пропущено спелл'],
1151 seErrRegex: [
1152 'синтаксическая ошибка в регулярном выражении: %s',
1153 'syntax error in regular expression: %s',
1154 'синтаксична помилка в регулярному виразі: %s'],
1155 seUnexpChar: [
1156 'неожиданный символ: %s',
1157 'unexpected character: %s',
1158 'неочікуваний символ: %s'],
1159 seMissClBkt: [
1160 'пропущена закрывающая скобка',
1161 "missing ')' in expression",
1162 'пропущено закривну дужку'],
1163 seRepsInParens: [
1164 'спелл %s не должен располагаться в скобках',
1165 'spell %s shouldnʼt be inside parentheses',
1166 'спелл %s не може бути в дужках'],
1167 seOpInReps: [
1168 'недопустимо использовать оператор %s со спеллами #rep и #outrep',
1169 'donʼt use operator %s with spells #rep & #outrep',
1170 'неприпустимо використовувати оператор %s зі спеллами #rep и #outrep'],
1171 seRow: [
1172 ' (строка ',
1173 ' (row ',
1174 ' (рядок '],
1175 seCol: [
1176 ', столбец ',
1177 ', column ',
1178 ', стовпчик '],
1179
1180 // Data editor
1181 editInTxt: [
1182 'Правка в текстовом формате',
1183 'Edit in text format',
1184 'Правка в текстовому форматі'],
1185 editor: {
1186 cfg: [
1187 'Редактирование настроек',
1188 'Edit settings',
1189 'Редагування налаштувань'],
1190 hidden: [
1191 'Редактирование скрытых тредов',
1192 'Edit hidden threads',
1193 'Редагування схованих тредів'],
1194 favor: [
1195 'Редактирование избранного',
1196 'Edit favorites',
1197 'Редагування вибраного'],
1198 css: [
1199 'Редактирование CSS',
1200 'Edit CSS',
1201 'Редагування CSS']
1202 },
1203
1204 // Settings import/export/clearing
1205 fileImpExp: [
1206 'Импорт/экспорт настроек в файл',
1207 'Import/export config to file',
1208 'Імпорт/експорт налаштувань до файлу'],
1209 fileToData: [
1210 'Загрузить данные из файла',
1211 'Load data from a file',
1212 'Завантажити дані з файла'],
1213 dataToFile: [
1214 'Получить файл</a> с данными',
1215 'Get the file</a> with data',
1216 'Отримати файл</a> з даними'],
1217 globalCfg: [
1218 'Глобальные настройки',
1219 'Global config',
1220 'Глобальні налаштування'],
1221 loadGlobal: [
1222 'и применить к этому домену',
1223 'and apply to this domain',
1224 'і застосувати до цього домену'],
1225 saveGlobal: [
1226 'текущие настройки как глобальные',
1227 'current config as global',
1228 'поточні налаштування як глобальні'],
1229 descrGlobal: [
1230 'Глобальные настройки применяются по умолчанию<br>при первом посещении других доменов',
1231 'Global config is applied by default<br>on the first visit of other domains',
1232 'Глобальні налаштування застосовуються по замовчуванню<br>під час першого відвідання інших доменів'],
1233 resetCfg: [
1234 'Сбросить в настройки по умолчанию',
1235 'Reset config to defaults',
1236 'Скинути в налаштування по замовчуванню'],
1237 resetData: [
1238 'Очистить выбранные данные',
1239 'Reset selected data',
1240 'Очистити обрані дані'],
1241 allDomains: [
1242 'для всех доменов',
1243 'for all domains',
1244 'для всіх доменів'],
1245 delEntries: [
1246 'Удалить выбранные записи',
1247 'Delete selected entries',
1248 'Видалити обрані записи'],
1249 saveChanges: [
1250 'Сохранить внесенные изменения',
1251 'Save your changes',
1252 'Зберегти внесені зміни'],
1253 hidPostThr: [
1254 'Скрытые посты и треды',
1255 'Hidden posts and threads',
1256 'Сховані пости та треди'],
1257 myPosts: [
1258 'Мои посты',
1259 'My posts',
1260 'Мої пости'],
1261
1262 // Settings window: Common/Info tab
1263 checkNow: [
1264 'Проверить сейчас',
1265 'Check now',
1266 'Перевірити зараз'],
1267 updAvail: [
1268 'Доступно обновление Dollchan: %s',
1269 'Dollchan update available: %s!',
1270 'Доступне оновлення Dollchan: %s'],
1271 newCommitsAvail: [
1272 'Обнаружены новые исправления: %s',
1273 'New fixes detected: %s',
1274 'Виявлено нові виправлення: %s'],
1275 changeLog: [
1276 'Список изменений',
1277 'List of changes',
1278 'Список змін'],
1279 haveLatestStable: [
1280 'Ваша версия %s является последней из стабильных.',
1281 'Your %s version is the latest from stable versions.',
1282 'Ваша версія %s є останньою зі стабільних.'],
1283 haveLatestCommit: [
1284 'Ваша версия %s содержит последние исправления.',
1285 'Your %s version contains all the latest fixes.',
1286 'Ваша версія %s містить всі останні виправлення.'],
1287 thrViewed: [
1288 'Тредов посещено',
1289 'Threads visited',
1290 'Тредів відвідано'],
1291 thrCreated: [
1292 'Тредов создано',
1293 'Threads created',
1294 'Тредів створено'],
1295 thrHidden: [
1296 'Тредов скрыто',
1297 'Threads hidden',
1298 'Тредів сховано'],
1299 postsSent: [
1300 'Постов отправлено',
1301 'Posts sent',
1302 'Постів надіслано'],
1303 total: [
1304 'Всего',
1305 'Total',
1306 'Всього'],
1307 debug: [
1308 'Отладка',
1309 'Debug',
1310 'Відлагодження'],
1311 infoDebug: [
1312 'Информация для отладки',
1313 'Information for debugging',
1314 'Інформація для відлагодження'],
1315
1316 // Favorites window: tooltips
1317 infoCount: [
1318 'Обновить счетчики постов',
1319 'Refresh posts counters',
1320 'Оновити лічильники постів'],
1321 infoPage: [
1322 'Проверить положение тредов (до 10-й страницы)',
1323 'Check for threads position (up to 10th page)',
1324 'Перевірити актуальність тредів (до 10 сторінки)'],
1325 clrDeleted: [
1326 'Очистить недоступные (404) треды',
1327 'Clear inaccessible (404) threads',
1328 'Очистити недоступні (404) треди'],
1329 oldPosts: [
1330 'Постов при последнем посещении',
1331 'Posts at the last visit',
1332 'Постів під час останнього відвідування'],
1333 newPosts: [
1334 'Количество новых постов',
1335 'Number of new posts',
1336 'Кількість нових постів'],
1337 myPostsRep: [
1338 'Ответов на ваши посты',
1339 'Replies to your posts',
1340 'Відповідей на ваші пости'],
1341 thrPage: [
1342 'Тред на @странице',
1343 'Thread on @page',
1344 'Тред на @сторінці'],
1345 goToThread: [
1346 'Перейти к треду',
1347 'Go to the thread',
1348 'Перейти до треду'],
1349 goToBoard: [
1350 'Перейти к доске',
1351 'Go to the board',
1352 'Перейти до дошки'],
1353 toggleEntries: [
1354 'Скрыть/раскрыть записи',
1355 'Hide/expand entries',
1356 'Сховати/розкрити записи'],
1357
1358 // Video links: tooltips
1359 hideLnkList: [
1360 'Скрыть/Показать список ссылок',
1361 'Hide/Unhide list of links',
1362 'Сховати/показати перелік посилань'],
1363 expandVideo: [
1364 'Развернуть/Свернуть видео',
1365 'Expand/Collapse video',
1366 'Розгорнути/згорнути відео'],
1367 prevVideo: [
1368 'Предыдущее видео',
1369 'Previous video',
1370 'Попереднє відео'],
1371 nextVideo: [
1372 'Следующее видео',
1373 'Next video',
1374 'Наступне відео'],
1375 duration: [
1376 'Продолжительность: ',
1377 'Duration: ',
1378 'Тривалість: '],
1379 published: [
1380 'опубликовано: ',
1381 'published: ',
1382 'опубліковано: '],
1383 author: [
1384 'Автор: ',
1385 'Author: ',
1386 'Автор: '],
1387 views: [
1388 'просмотров: ',
1389 'views: ',
1390 'переглядів: '],
1391
1392 // Postform file inputs: tooltips
1393 pasteImage: [
1394 'Ctrl+V - вставить картинку из буфера',
1395 'Ctrl+V - paste an image from clipboard',
1396 'Ctrl+V - додати зображення з буферу'],
1397 dropFileHere: [
1398 'Бросьте сюда файл(ы) или ссылку',
1399 'Drop file(s) or link here',
1400 'Киньте сюди файл(и) чи посилання'],
1401 youCanDrag: [
1402 'Можно перетаскивать картинки и ссылки на файлы\r\nпрямо со страницы или других сайтов',
1403 'You can drag images and file links\r\ndirectly from the page or other sites',
1404 'Можна перетягувати зображення чи посилання на файли\r\nбезпосередньо зі сторінки чи інших сайтів'],
1405 removeFile: [
1406 'Удалить файл',
1407 'Remove file',
1408 'Видалити файл'],
1409 renameFile: [
1410 'Переименовать файл',
1411 'Rename file',
1412 'Перейменувати файл'],
1413 spoilFile: [
1414 'Спойлер',
1415 'Spoiler',
1416 'Спойлер'],
1417 addManually: [
1418 'Ввести ссылку на файл вручную',
1419 'Enter a link to the file manually',
1420 'Ввести посилання на файл вручну'],
1421 enterTheLink: [
1422 "Введите ссылку и нажмите '+'",
1423 "Enter the link and click '+'",
1424 "Введіть посилання та натисніть '+'"],
1425 helpAddFile: [
1426 'Встроить ogg/rar/zip/7z в картинку',
1427 'Embed ogg/rar/zip/7z into the image',
1428 'Вбудувати ogg/rar/zip/7z в зображення'],
1429
1430 // Post images: tooltips
1431 expImgInline: [
1432 '[Click] открыть в посте, [Ctrl+Click] по центру',
1433 '[Click] expand in post, [Ctrl+Click] by center',
1434 '[Click] розгорнути в пості, [Ctrl+Click] в центрі'],
1435 expImgFull: [
1436 '[Click] открыть по центру, [Ctrl+Click] в посте',
1437 '[Click] expand by center, [Ctrl+Click] in post',
1438 '[Click] розгорнути в центрі, [Ctrl+Click] в пості'],
1439 nextImg: [
1440 'Следующая картинка',
1441 'Next image',
1442 'Наступне зображення'],
1443 prevImg: [
1444 'Предыдущая картинка',
1445 'Previous image',
1446 'Попереднє зображення'],
1447 rotateImg: [
1448 'Повернуть вправо',
1449 'Rotate right',
1450 'Повернути вправо'],
1451 autoPlayOn: [
1452 'Автоматически воспроизводить следующее видео',
1453 'Automatically play the next video',
1454 'Автоматично відтворювати наступне відео'],
1455 autoPlayOff: [
1456 'Отключить автовоспроизведение',
1457 'Disable autoplay',
1458 'Відключити автовідтворення'],
1459 downloadFile: [
1460 'Скачать содержащийся в картинке файл',
1461 'Download embedded file from the image',
1462 'Завантажити файл, що міститься в зображенні'],
1463 openOriginal: [
1464 'Открыть оригинал в новой вкладке',
1465 'Open the original image in new tab',
1466 'Відкрити оригінал в новій вкладці'],
1467
1468 // Threads/images download: popups
1469 loadImage: [
1470 'Загружаются картинки',
1471 'Loading images',
1472 'Завантажуються зображення'],
1473 loadFile: [
1474 'Загружаются файлы',
1475 'Loading files',
1476 'Завантажуються файли'],
1477 cantLoad: [
1478 'Не могу загрузить',
1479 'Canʼt load',
1480 'Не можу завантажити'],
1481 willSavePview: [
1482 'Будет сохранено превью',
1483 'Thumbnail will be saved',
1484 'Буде збережено превʼю'],
1485 loadErrors: [
1486 'Во время загрузки произошли ошибки:',
1487 'An error occurred during the loading:',
1488 'Під час завантаження сталися помилки:'],
1489
1490 // Ajax: popups
1491 succDeleted: [
1492 'Успешно удалено!',
1493 'Succesfully deleted!',
1494 'Успішно видалено!'],
1495 succReported: [
1496 'Жалоба успешно отправлена',
1497 'Succesfully reported',
1498 'Скарга успішно відправлена'],
1499 errDelete: [
1500 'Не могу удалить',
1501 'Canʼt delete',
1502 'Не можу видалити'],
1503 fileCorrupt: [
1504 'Файл повреждён',
1505 'File is corrupt',
1506 'Файл пошкоджено'],
1507 errCorruptData: [
1508 'Ошибка: сервер отправил повреждённые данные',
1509 'Error: server sent corrupted data',
1510 'Помилка: сервер надіслав пошкоджені дані'],
1511 noConnect: [
1512 'Ошибка подключения',
1513 'Connection failed',
1514 'Помилка зʼєднання'],
1515 thrNotFound: [
1516 'Тред недоступен',
1517 'Thread is unavailable',
1518 'Тред недоступний'],
1519 thrClosed: [
1520 'Тред закрыт',
1521 'Thread is closed',
1522 'Тред закрито'],
1523 thrArchived: [
1524 'Тред в архиве',
1525 'Thread is archived',
1526 'Тред заархівовано'],
1527
1528 // Other warnings
1529 internalError: [
1530 'Внутренняя ошибка:\n',
1531 'Internal error:\n',
1532 'Внутрішня помилка:\n'],
1533 postNotFound: [
1534 'Пост не найден',
1535 'Post not found',
1536 'Пост не знайдено'],
1537 noHidThr: [
1538 'Нет скрытых тредов…',
1539 'No hidden threads…',
1540 'Немає схованих постів…'],
1541 noFavThr: [
1542 'Нет избранных тредов…',
1543 'Favorites is empty…',
1544 'Немає вибраних тредів…'],
1545 noVideoLinks: [
1546 'Нет ссылок на видео…',
1547 'No video links…',
1548 'Немає посилань на відео…'],
1549 invalidData: [
1550 'Некорректный формат данных',
1551 'Incorrect data format',
1552 'Некоректний формат даних'],
1553 noGlobalCfg: [
1554 'Глобальные настройки не найдены',
1555 'Global config not found',
1556 'Глобальні налаштування не знайдено'],
1557 subjHasTrip: [
1558 'Поле "Тема" содержит трипкод!',
1559 '"Subject" field contains a tripcode!',
1560 'Поле "Тема" містить трипкод!'],
1561 errMsEdgeWebm: [
1562 'Загрузите скрипт для воспроизведения WebM (VP9/Opus)',
1563 'Please load a script to play WebM (VP9/Opus)',
1564 'Завантажте скрипт для відтворення WebM (VP9/Opus)'],
1565 errFormLoad: [
1566 'Не удаётся загрузить форму ответа',
1567 'Can`t load the reply form',
1568 'Не вдалося завантажити форму відповіді'
1569 ],
1570
1571 // Single words
1572 second : ['с', 's', 'с'],
1573 sizeByte : [' Байт', ' Byte', ' Байт'],
1574 sizeKByte : [' КБ', ' KB', ' КБ'],
1575 sizeMByte : [' МБ', ' MB', ' МБ'],
1576 sizeGByte : [' ГБ', ' GB', ' ГБ'],
1577 name : ['Имя', 'Name', 'Імʼя'],
1578 subj : ['Тема', 'Subject', 'Тема'],
1579 mail : ['Почта', 'Email', 'Пошта'],
1580 video : ['Видео', 'Video', 'Відео'],
1581 cap : ['Капча', 'Captcha', 'Капча'],
1582 add : ['Добавить', 'Add', 'Додати'],
1583 apply : ['Применить', 'Apply', 'Застосувати'],
1584 cancel : ['Отмена', 'Cancel', 'Скасувати'],
1585 clear : ['Очистить', 'Clear', 'Очистити'],
1586 refresh : ['Обновить', 'Refresh', 'Оновити'],
1587 save : ['Сохранить', 'Save', 'Зберегти'],
1588 load : ['Загрузить', 'Load', 'Завантажити'],
1589 edit : ['Правка', 'Edit', 'Правка'],
1590 file : ['Файл', 'File', 'Файл'],
1591 global : ['Глобальные', 'Global', 'Глобальні'],
1592 reset : ['Сброс', 'Reset', 'Скинути'],
1593 remove : ['Удалить', 'Remove', 'Видалити'],
1594 change : ['Сменить', 'Change', 'Змінити'],
1595 page : ['Страница', 'Page', 'Сторінка'],
1596 reply : ['Ответ', 'Reply', 'Відповідь'],
1597 replies : ['Ответы:', 'Replies:', 'Відповіді:'],
1598 makeReply : ['Ответить', 'Reply', 'Відповісти'],
1599 error : ['Ошибка', 'Error', 'Помилка'],
1600 loading : ['Загрузка…', 'Loading…', 'Завантаження…'],
1601 sending : ['Отправка…', 'Sending…', 'Надсилання…'],
1602 checking : ['Проверка…', 'Checking…', 'Перевірка…'],
1603 updating : ['Обновление…', 'Updating…', 'Оновлення…'],
1604 deleting : ['Удаление…', 'Deleting…', 'Видалення…'],
1605 deleted : ['удалён', 'deleted', 'видалено'],
1606 hide : ['Скрыть: ', 'Hide: ', 'Сховати: '],
1607
1608 // Miscellaneous
1609 hidePosts: [
1610 'Скрыть посты',
1611 'Hide posts',
1612 'Сховати пости'],
1613 showPosts: [
1614 'Показать посты',
1615 'Show posts',
1616 'Показати пости'],
1617 getNewPosts: [
1618 'Получить новые посты',
1619 'Get new posts',
1620 'Отримати нові пости'],
1621 makeThr: [
1622 'Создать тред',
1623 'Create thread',
1624 'Створити тред'],
1625 collapseThr: [
1626 'Свернуть тред',
1627 'Collapse thread',
1628 'Згорнути тред'],
1629 hiddenThr: [
1630 'Скрытый тред',
1631 'Hidden thread',
1632 'Схований тред'],
1633 hideForm: [
1634 'Скрыть форму',
1635 'Hide form',
1636 'Сховати форму'],
1637 noSage: [
1638 'Без сажи',
1639 'No sage',
1640 'Без сажі'],
1641 postsOmitted: [
1642 'Пропущено ответов: ',
1643 'Posts omitted: ',
1644 'Пропущено відповідей: '],
1645 newPost: [
1646 ['новый пост', 'новых поста', 'новых постов'],
1647 ['new post', 'new posts', 'new posts'],
1648 ['новий пост', 'нових пости', 'нових постів']],
1649 youReplies: [
1650 ['ответ Вам', 'ответа Вам', 'ответов Вам'],
1651 ['reply to You', 'replies to You', 'replies to You'],
1652 ['відповідь Вам', 'відповіді Вам', 'відповідей Вам']],
1653 latestPost: [
1654 'Последний пост',
1655 'Latest post',
1656 'Останній пост'],
1657 donateMsg: [
1658 '<b>Спасибо за использование Dollchan Extension!</b><br>Вы можете поддержать проект пожертвованием',
1659 '<b>Thank You for using Dollchan Extension!</b><br>You can support the project by donating',
1660 '<b>Дякуємо за використання Dollchan Extension!</b><br>Ви можете підтримати проект пожертвою'],
1661 firefoxAddon: [
1662 'Firefox аддон</a> доступен!',
1663 'Firefox add-on</a> is available!',
1664 'Firefox аддон</a> доступний!']
1665};
1666
1667/* ==[ GlobalVars.js ]== */
1668
1669const doc = deWindow.document;
1670const emptyFn = Function.prototype;
1671const aProto = Array.prototype;
1672const gitWiki = 'https://github.com/SthephanShinkufag/Dollchan-Extension-Tools/wiki/';
1673const gitRaw = 'https://raw.githubusercontent.com/SthephanShinkufag/Dollchan-Extension-Tools/master/';
1674
1675let $each, aib, Cfg, docBody, dTime, dummy, isExpImg, isPreImg, lang, locStorage, nav, needScroll, pByEl,
1676 pByNum, pr, sesStorage, updater;
1677let quotetxt = '';
1678let visPosts = 2;
1679let topWinZ = 10;
1680
1681/* ==[ Utils.js ]=============================================================================================
1682 UTILS
1683=========================================================================================================== */
1684
1685// DOM SEARCH
1686
1687const $Q = (path, root = docBody) => root.querySelectorAll(path);
1688
1689const $q = (path, root = docBody) => root.querySelector(path);
1690
1691const $id = id => doc.getElementById(id);
1692
1693function $parent(el, tagName) {
1694 do {
1695 el = el.parentElement;
1696 } while(el && el.tagName !== tagName);
1697 return el;
1698}
1699
1700function $qParent(el, path) {
1701 do {
1702 el = el.parentElement;
1703 } while(el && !nav.matchesSelector(el, path));
1704 return el;
1705}
1706
1707// DOM MODIFIERS
1708
1709function $before(el, node) {
1710 el.parentNode.insertBefore(node, el);
1711}
1712
1713function $after(el, node) {
1714 const nextEl = el.nextSibling;
1715 if(nextEl) {
1716 el.parentNode.insertBefore(node, nextEl);
1717 } else {
1718 el.parentNode.appendChild(node);
1719 }
1720}
1721
1722function $bBegin(sibling, html) {
1723 sibling.insertAdjacentHTML('beforebegin', html);
1724 return sibling.previousSibling;
1725}
1726
1727function $aBegin(parent, html) {
1728 parent.insertAdjacentHTML('afterbegin', html);
1729 return parent.firstChild;
1730}
1731
1732function $bEnd(parent, html) {
1733 parent.insertAdjacentHTML('beforeend', html);
1734 return parent.lastChild;
1735}
1736
1737function $aEnd(sibling, html) {
1738 sibling.insertAdjacentHTML('afterend', html);
1739 return sibling.nextSibling;
1740}
1741
1742function $replace(origEl, newEl) {
1743 if(typeof newEl === 'string') {
1744 origEl.insertAdjacentHTML('afterend', newEl);
1745 origEl.remove();
1746 } else {
1747 origEl.parentNode.replaceChild(newEl, origEl);
1748 }
1749}
1750
1751function $del(el) {
1752 if(el) {
1753 el.remove();
1754 }
1755}
1756
1757function $delAll(path, root = docBody) {
1758 $each(root.querySelectorAll(path, root), el => el.remove());
1759}
1760
1761function $add(html) {
1762 dummy.innerHTML = html;
1763 return dummy.firstElementChild;
1764}
1765
1766const $txt = el => doc.createTextNode(el);
1767
1768// TODO: Get rid of this function and paste buttons in html
1769function $btn(val, ttl, fn, className = 'de-button') {
1770 const el = doc.createElement('input');
1771 el.type = 'button';
1772 el.className = className;
1773 el.value = val;
1774 el.title = ttl;
1775 el.addEventListener('click', fn);
1776 return el;
1777}
1778
1779function $script(text) {
1780 // We can't insert scripts directly as html
1781 const el = doc.createElement('script');
1782 el.type = 'text/javascript';
1783 el.textContent = text;
1784 doc.head.appendChild(el).remove();
1785}
1786
1787function $css(text) {
1788 if(nav.isSafari && !('flex' in docBody.style)) {
1789 text = text.replace(/(transform|transition|flex|align-items)/g, ' -webkit-$1');
1790 }
1791 return $bEnd(doc.head, `<style type="text/css">${ text }</style>`);
1792}
1793
1794function $DOM(html) {
1795 const myDoc = doc.implementation.createHTMLDocument('');
1796 myDoc.documentElement.innerHTML = html;
1797 return myDoc;
1798}
1799
1800// CSS UTILS
1801
1802function $toggle(el, needToShow = el.style.display) {
1803 if(needToShow) {
1804 el.style.removeProperty('display');
1805 } else {
1806 el.style.display = 'none';
1807 }
1808}
1809
1810function $show(el) {
1811 el.style.removeProperty('display');
1812}
1813
1814function $hide(el) {
1815 el.style.display = 'none';
1816}
1817
1818function $animate(el, cName, isRemove = false) {
1819 el.addEventListener('animationend', function aEvent() {
1820 el.removeEventListener('animationend', aEvent);
1821 if(isRemove) {
1822 el.remove();
1823 } else {
1824 el.classList.remove(cName);
1825 }
1826 });
1827 el.classList.add(cName);
1828}
1829
1830// Checks the validity of the user inputted color
1831function checkCSSColor(color) {
1832 if(!color || color === 'inherit' || color === 'currentColor') {
1833 return false;
1834 }
1835 if(color === 'transparent') {
1836 return true;
1837 }
1838 const image = doc.createElement('img');
1839 image.style.color = 'rgb(0, 0, 0)';
1840 image.style.color = color;
1841 if(image.style.color !== 'rgb(0, 0, 0)') {
1842 return true;
1843 }
1844 image.style.color = 'rgb(255, 255, 255)';
1845 image.style.color = color;
1846 return image.style.color !== 'rgb(255, 255, 255)';
1847}
1848
1849// OTHER UTILS
1850
1851const pad2 = i => (i < 10 ? '0' : '') + i;
1852
1853const arrTags = (arr, start, end) => start + arr.join(end + start) + end;
1854
1855const fixBrd = b => `/${ b }${ b ? '/' : '' }`;
1856
1857const getAbsLink = url => (
1858 url[1] === '/' ? aib.prot :
1859 url[0] === '/' ? aib.prot + '//' + aib.host : '') + url;
1860
1861// Prepares a string to be used as a new RegExp argument
1862const quoteReg = str => (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
1863
1864// Converts a string to a regular expression
1865function toRegExp(str, noG) {
1866 const l = str.lastIndexOf('/');
1867 const flags = str.substr(l + 1);
1868 return new RegExp(str.substr(1, l - 1), noG ? flags.replace('g', '') : flags);
1869}
1870
1871function toggleAttr(el, name, value, isAdd) {
1872 if(isAdd) {
1873 el.setAttribute(name, value);
1874 } else {
1875 el.removeAttribute(name);
1876 }
1877}
1878
1879function $pd(e) {
1880 e.preventDefault();
1881}
1882
1883function $isEmpty(obj) {
1884 for(const i in obj) {
1885 if(obj.hasOwnProperty(i)) {
1886 return false;
1887 }
1888 }
1889 return true;
1890}
1891
1892function insertText(el, txt) {
1893 const scrtop = el.scrollTop;
1894 const start = el.selectionStart;
1895 el.value = el.value.substr(0, start) + txt + el.value.substr(el.selectionEnd);
1896 el.setSelectionRange(start + txt.length, start + txt.length);
1897 el.focus();
1898 el.scrollTop = scrtop;
1899}
1900
1901// XXX: SVG events hack for Opera Presto
1902function fixEventEl(el) {
1903 if(el && nav.isPresto) {
1904 const svg = el.correspondingUseElement;
1905 if(svg) {
1906 el = svg.ownerSVGElement;
1907 }
1908 }
1909 return el;
1910}
1911
1912// Allows to record the duration of code execution
1913const Logger = {
1914 finish() {
1915 this._finished = true;
1916 this._marks.push(['LoggerFinish', Date.now()]);
1917 },
1918 getLogData(isFull) {
1919 const marks = this._marks;
1920 const timeLog = [];
1921 let duration, i = 1;
1922 let lastExtra = 0;
1923 for(let len = marks.length - 1; i < len; ++i) {
1924 duration = marks[i][1] - marks[i - 1][1] + lastExtra;
1925 if(isFull || duration > 1) {
1926 lastExtra = 0;
1927 timeLog.push([marks[i][0], duration]);
1928 } else { // Ignore logs equal to 0ms
1929 lastExtra = duration;
1930 }
1931 }
1932 timeLog.push([Lng.total[lang], marks[i][1] - marks[0][1]]);
1933 return timeLog;
1934 },
1935 initLogger() {
1936 this._marks.push(['LoggerInit', Date.now()]);
1937 },
1938 log(text) {
1939 if(!this._finished) {
1940 this._marks.push([text, Date.now()]);
1941 }
1942 },
1943
1944 _finished : false,
1945 _marks : []
1946};
1947
1948// Some async operations should be cancelable, to ignore all the chaining callbacks of promises.
1949// Cancellation is supposed to flow through a graph of promise dependencies. When a promise is cancelled, it
1950// will propagate to the farthest pending promises and reject them with the cancel reason CancelError.
1951function CancelError() {}
1952class CancelablePromise {
1953 constructor(resolver, cancelFn) {
1954 this._promise = new Promise((resolve, reject) => {
1955 this._reject = reject;
1956 resolver(value => {
1957 resolve(value);
1958 this._isResolved = true;
1959 }, reason => {
1960 reject(reason);
1961 this._isResolved = true;
1962 });
1963 });
1964 this._cancelFn = cancelFn;
1965 this._isResolved = false;
1966 }
1967 static reject(val) {
1968 return new CancelablePromise((res, rej) => rej(val));
1969 }
1970 static resolve(val) {
1971 return new CancelablePromise(res => res(val));
1972 }
1973 cancelPromise() {
1974 this._reject(new CancelError());
1975 if(!this._isResolved && this._cancelFn) {
1976 this._cancelFn();
1977 }
1978 }
1979 catch(eb) {
1980 return this.then(void 0, eb);
1981 }
1982 then(cb, eb) {
1983 const children = [];
1984 const wrap = fn => (...args) => {
1985 const child = fn(...args);
1986 if(child instanceof CancelablePromise) {
1987 children.push(child);
1988 }
1989 return child;
1990 };
1991 return new CancelablePromise(
1992 resolve => resolve(this._promise.then(cb && wrap(cb), eb && wrap(eb))), () => {
1993 for(const child of children) {
1994 child.cancelPromise();
1995 }
1996 this.cancelPromise();
1997 });
1998 }
1999}
2000
2001class Maybe {
2002 constructor(Ctor/* , ...args */) {
2003 this._ctor = Ctor;
2004 // this._args = args;
2005 this.hasValue = false;
2006 }
2007 get value() {
2008 const Ctor = this._ctor;
2009 this.hasValue = !!Ctor;
2010 const value = Ctor ? new Ctor(/* ...this._args */) : null;
2011 Object.defineProperty(this, 'value', { value });
2012 return value;
2013 }
2014}
2015
2016class TemporaryContent {
2017 constructor(key) {
2018 const oClass = /* new.target */this.constructor; // https://github.com/babel/babel/issues/1088
2019 if(oClass.purgeTO) {
2020 clearTimeout(oClass.purgeTO);
2021 }
2022 oClass.purgeTO = setTimeout(() => oClass.purge(), oClass.purgeSecs);
2023 if(oClass.data) {
2024 const rv = oClass.data.get(key);
2025 if(rv) {
2026 return rv;
2027 }
2028 } else {
2029 oClass.data = new Map();
2030 }
2031 oClass.data.set(key, this);
2032 }
2033 static get(key) {
2034 return this.data ? this.data.get(key) : null;
2035 }
2036 static has(key) {
2037 return this.data ? this.data.has(key) : false;
2038 }
2039 static purge() {
2040 if(this.purgeTO) {
2041 clearTimeout(this.purgeTO);
2042 this.purgeTO = null;
2043 }
2044 this.data = null;
2045 }
2046 static removeTempData(key) {
2047 if(this.data) {
2048 this.data.delete(key);
2049 }
2050 }
2051}
2052TemporaryContent.purgeSecs = 6e4;
2053
2054class TasksPool {
2055 constructor(tasksCount, taskFunc, endFn) {
2056 this.array = [];
2057 this.running = 0;
2058 this.num = 1;
2059 this.func = taskFunc;
2060 this.endFn = endFn;
2061 this.max = tasksCount;
2062 this.completed = this.paused = this.stopped = false;
2063 }
2064 completeTasks() {
2065 if(!this.stopped) {
2066 if(this.array.length === 0 && this.running === 0) {
2067 this.endFn();
2068 } else {
2069 this.completed = true;
2070 }
2071 }
2072 }
2073 pauseTasks() {
2074 this.paused = true;
2075 }
2076 runTask(data) {
2077 if(!this.stopped) {
2078 if(this.paused || this.running === this.max) {
2079 this.array.push(data);
2080 } else {
2081 this._runTask(data);
2082 this.running++;
2083 }
2084 }
2085 }
2086 stopTasks() {
2087 this.stopped = true;
2088 this.endFn();
2089 }
2090
2091 _continueTasks() {
2092 if(!this.stopped) {
2093 this.paused = false;
2094 if(this.array.length === 0) {
2095 if(this.completed) {
2096 this.endFn();
2097 }
2098 return;
2099 }
2100 while(this.array.length !== 0 && this.running !== this.max) {
2101 this._runTask(this.array.shift());
2102 this.running++;
2103 }
2104 }
2105 }
2106 _endTask() {
2107 if(!this.stopped) {
2108 if(!this.paused && this.array.length !== 0) {
2109 this._runTask(this.array.shift());
2110 return;
2111 }
2112 this.running--;
2113 if(!this.paused && this.completed && this.running === 0) {
2114 this.endFn();
2115 }
2116 }
2117 }
2118 _runTask(data) {
2119 this.func(this.num++, data).then(() => this._endTask(), err => {
2120 if(err instanceof TasksPool.PauseError) {
2121 this.pauseTasks();
2122 if(err.duration !== -1) {
2123 setTimeout(() => this._continueTasks(), err.duration);
2124 }
2125 } else {
2126 this._endTask();
2127 throw err;
2128 }
2129 });
2130 }
2131}
2132TasksPool.PauseError = function(duration) {
2133 this.name = 'TasksPool.PauseError';
2134 this.duration = duration;
2135};
2136
2137class WorkerPool {
2138 constructor(mReqs, wrkFn, errFn) {
2139 if(!nav.hasWorker) {
2140 this.runWorker = (data, transferObjs, fn) => fn(wrkFn(data));
2141 return;
2142 }
2143 const url = deWindow.URL.createObjectURL(new Blob([`self.onmessage = function(e) {
2144 var info = (${ String(wrkFn) })(e.data);
2145 if(info.data) {
2146 self.postMessage(info, [info.data]);
2147 } else {
2148 self.postMessage(info);
2149 }
2150 }`], { type: 'text/javascript' }));
2151 this._pool = new TasksPool(mReqs, (num, data) => this._createWorker(num, data), null);
2152 this._freeWorkers = [];
2153 this._url = url;
2154 this._errFn = errFn;
2155 while(mReqs--) {
2156 this._freeWorkers.push(new Worker(url));
2157 }
2158 }
2159 clearWorkers() {
2160 deWindow.URL.revokeObjectURL(this._url);
2161 this._freeWorkers.forEach(w => w.terminate());
2162 this._freeWorkers = [];
2163 }
2164 runWorker(data, transferObjs, fn) {
2165 this._pool.runTask([data, transferObjs, fn]);
2166 }
2167
2168 _createWorker(num, data) {
2169 return new Promise(resolve => {
2170 const worker = this._freeWorkers.pop();
2171 const [sendData, transferObjs, fn] = data;
2172 worker.onmessage = e => {
2173 fn(e.data);
2174 this._freeWorkers.push(worker);
2175 resolve();
2176 };
2177 worker.onerror = err => {
2178 resolve();
2179 this._freeWorkers.push(worker);
2180 this._errFn(err);
2181 };
2182 worker.postMessage(sendData, transferObjs);
2183 });
2184 }
2185}
2186
2187class TarBuilder {
2188 constructor() {
2189 this._data = [];
2190 }
2191 addFile(filepath, input) {
2192 let i, checksum = 0;
2193 const fileSize = input.length;
2194 const header = new Uint8Array(512);
2195 const nameLen = Math.min(filepath.length, 100);
2196 for(i = 0; i < nameLen; ++i) {
2197 header[i] = filepath.charCodeAt(i) & 0xFF;
2198 }
2199 TarBuilder._padSet(header, 100, '100777', 8); // fileMode
2200 TarBuilder._padSet(header, 108, '0', 8); // uid
2201 TarBuilder._padSet(header, 116, '0', 8); // gid
2202 TarBuilder._padSet(header, 124, fileSize.toString(8), 13); // fileSize
2203 TarBuilder._padSet(header, 136, Math.floor(Date.now() / 1e3).toString(8), 12); // mtime
2204 TarBuilder._padSet(header, 148, ' ', 8); // checksum
2205 // type ('0')
2206 header[156] = 0x30;
2207 for(i = 0; i < 157; ++i) {
2208 checksum += header[i];
2209 }
2210 // checksum
2211 TarBuilder._padSet(header, 148, checksum.toString(8), 8);
2212 this._data.push(header, input);
2213 if((i = Math.ceil(fileSize / 512) * 512 - fileSize) !== 0) {
2214 this._data.push(new Uint8Array(i));
2215 }
2216 }
2217 addString(filepath, str) {
2218 const sDat = unescape(encodeURIComponent(str));
2219 this.addFile(filepath, new Uint8Array(sDat.length).map((val, i) => sDat.charCodeAt(i) & 0xFF));
2220 }
2221 get() {
2222 this._data.push(new Uint8Array(1024));
2223 return new Blob(this._data, { type: 'application/x-tar' });
2224 }
2225
2226 static _padSet(data, offset, num, len) {
2227 let i = 0;
2228 const nLen = num.length;
2229 len -= 2;
2230 while(nLen < len) {
2231 data[offset++] = 0x20; // ' '
2232 len--;
2233 }
2234 while(i < nLen) {
2235 data[offset++] = num.charCodeAt(i++);
2236 }
2237 data[offset] = 0x20; // ' '
2238 }
2239}
2240
2241class WebmParser {
2242 constructor(data) {
2243 let offset = 0;
2244 const dv = nav.getUnsafeDataView(data);
2245 const len = dv.byteLength;
2246 const el = new WebmParser.Element(dv, len, 0);
2247 const voids = [];
2248 const EBMLId = 0x1A45DFA3;
2249 const segmentId = 0x18538067;
2250 const voidId = 0xEC;
2251 this.voidId = voidId;
2252 error: do {
2253 if(el.error || el.id !== EBMLId) {
2254 break;
2255 }
2256 this.EBML = el;
2257 offset += el.headSize + el.size;
2258 while(true) {
2259 const el = new WebmParser.Element(dv, len, offset);
2260 if(el.error) {
2261 break error;
2262 }
2263 if(el.id === segmentId) {
2264 this.segment = el;
2265 break; // Ignore everything after first segment
2266 } else if(el.id === voidId) {
2267 voids.push(el);
2268 } else {
2269 break error;
2270 }
2271 offset += el.headSize + el.size;
2272 }
2273 this.voids = voids;
2274 this.data = data;
2275 this.length = len;
2276 this.rv = [null];
2277 this.error = false;
2278 return;
2279 } while(false);
2280 this.error = true;
2281 }
2282 addWebmData(data) {
2283 if(this.error || !data) {
2284 return this;
2285 }
2286 const size = typeof data === 'string' ? data.length : data.byteLength;
2287 if(size > 127) {
2288 this.error = true;
2289 return;
2290 }
2291 this.rv.push(new Uint8Array([this.voidId, 0x80 | size]), data);
2292 return this;
2293 }
2294 getWebmData() {
2295 if(this.error) {
2296 return null;
2297 }
2298 this.rv[0] = nav.getUnsafeUint8Array(this.data, 0, this.segment.endOffset);
2299 return this.rv;
2300 }
2301}
2302WebmParser.Element = function(elData, dataLength, offset) {
2303 this.error = false;
2304 this.id = 0;
2305 if(offset + 4 >= dataLength) {
2306 return;
2307 }
2308 let num = elData.getUint32(offset);
2309 let leadZeroes = Math.clz32(num);
2310 if(leadZeroes > 3) {
2311 this.error = true;
2312 return;
2313 }
2314 offset += leadZeroes + 1;
2315 if(offset >= dataLength) {
2316 this.error = true;
2317 return;
2318 }
2319 this.id = num >>> (8 * (3 - leadZeroes));
2320 this.headSize = leadZeroes + 1;
2321 num = elData.getUint32(offset);
2322 leadZeroes = Math.clz32(num);
2323 let size = num & (0xFFFFFFFF >>> (leadZeroes + 1));
2324 if(leadZeroes > 3) {
2325 const shift = 8 * (7 - leadZeroes);
2326 if(size >>> shift !== 0 || offset + 4 > dataLength) {
2327 this.error = true;
2328 return; // We cannot handle webm-files with size greater than 4Gb :(
2329 }
2330 size = (size << (32 - shift)) | (elData.getUint32(offset + 4) >>> shift);
2331 } else {
2332 size >>>= 8 * (3 - leadZeroes);
2333 }
2334 this.headSize += leadZeroes + 1;
2335 offset += leadZeroes + 1;
2336 if(offset + size > dataLength) {
2337 this.error = true;
2338 return;
2339 }
2340 this.data = elData;
2341 this.offset = offset;
2342 this.endOffset = offset + size;
2343 this.size = size;
2344};
2345
2346function getErrorMessage(err) {
2347 if(err instanceof AjaxError) {
2348 return err.toString();
2349 }
2350 if(typeof err === 'string') {
2351 return err;
2352 }
2353 const { stack, name, message } = err;
2354 return Lng.internalError[lang] + (
2355 !stack ? `${ name }: ${ message }` :
2356 nav.isWebkit ? stack : `${ name }: ${ message }\n${ !nav.isFirefox ? stack : stack.replace(
2357 /^([^@]*).*\/(.+)$/gm,
2358 (str, fName, line) => ` at ${ fName ? `${ fName } (${ line })` : line }`
2359 ) }`
2360 );
2361}
2362
2363async function readFile(file, asText = false) {
2364 return new Promise(resolve => {
2365 const fr = new FileReader();
2366 // XXX: firefox hack to prevent 'XrayWrapper denied access to property "then"' errors
2367 fr.onload = e => resolve({ data: e.target.result });
2368 if(asText) {
2369 fr.readAsText(file);
2370 } else {
2371 fr.readAsArrayBuffer(file);
2372 }
2373 });
2374}
2375
2376const prettifySize = val =>
2377 val > 512 * 1024 * 1024 ? (val / (1024 ** 3)).toFixed(2) + Lng.sizeGByte[lang] :
2378 val > 512 * 1024 ? (val / (1024 ** 2)).toFixed(2) + Lng.sizeMByte[lang] :
2379 val > 512 ? (val / 1024).toFixed(2) + Lng.sizeKByte[lang] :
2380 val.toFixed(2) + Lng.sizeByte[lang];
2381
2382function getFileType(url) {
2383 const dotIdx = url.lastIndexOf('.') + 1;
2384 switch(dotIdx && url.substr(dotIdx).toLowerCase()) {
2385 case 'gif': return 'image/gif';
2386 case 'jpeg':
2387 case 'jpg': return 'image/jpeg';
2388 case 'mp4': return 'video/mp4';
2389 case 'ogv': return 'video/ogv';
2390 case 'png': return 'image/png';
2391 case 'webm': return 'video/webm';
2392 case 'webp': return 'image/webp';
2393 default: return '';
2394 }
2395}
2396
2397function downloadBlob(blob, name) {
2398 const url = nav.isMsEdge ? navigator.msSaveOrOpenBlob(blob, name) : deWindow.URL.createObjectURL(blob);
2399 const link = $bEnd(docBody, `<a href="${ url }" download="${ name }"></a>`);
2400 link.click();
2401 setTimeout(() => {
2402 deWindow.URL.revokeObjectURL(url);
2403 link.remove();
2404 }, 2e5);
2405}
2406
2407/* ==[ Storage.js ]===========================================================================================
2408 STORAGE
2409=========================================================================================================== */
2410
2411// Gets data from the global storage
2412async function getStored(id) {
2413 if(nav.hasNewGM) {
2414 const value = await GM.getValue(id);
2415 return value;
2416 } else if(nav.hasOldGM) {
2417 return GM_getValue(id);
2418 } else if(nav.hasWebStorage) {
2419 // Read storage.local first. If it not existed then read storage.sync
2420 const value = await new Promise(resolve => chrome.storage.local.get(id, obj => {
2421 if(Object.keys(obj).length) {
2422 resolve(obj[id]);
2423 } else {
2424 chrome.storage.sync.get(id, obj => resolve(obj[id]));
2425 }
2426 }));
2427 return value;
2428 } else if(nav.hasPrestoStorage) {
2429 return prestoStorage.getItem(id);
2430 }
2431 return locStorage[id];
2432}
2433
2434// Saves data into the global storage
2435// FIXME: make async?
2436function setStored(id, value) {
2437 if(nav.hasNewGM) {
2438 return GM.setValue(id, value);
2439 } else if(nav.hasOldGM) {
2440 GM_setValue(id, value);
2441 } else if(nav.hasWebStorage) {
2442 const obj = {};
2443 obj[id] = value;
2444 chrome.storage.sync.set(obj, () => {
2445 if(chrome.runtime.lastError) {
2446 // Store into storage.local if the storage.sync limit is exceeded
2447 chrome.storage.local.set(obj, emptyFn);
2448 chrome.storage.sync.remove(id, emptyFn);
2449 } else {
2450 chrome.storage.local.remove(id, emptyFn);
2451 }
2452 });
2453 } else if(nav.hasPrestoStorage) {
2454 prestoStorage.setItem(id, value);
2455 } else {
2456 locStorage[id] = value;
2457 }
2458}
2459
2460// Removes data from the global storage
2461// FIXME: make async?
2462function delStored(id) {
2463 if(nav.hasNewGM) {
2464 return GM.deleteValue(id);
2465 } else if(nav.hasOldGM) {
2466 GM_deleteValue(id);
2467 } else if(nav.hasWebStorage) {
2468 chrome.storage.sync.remove(id, emptyFn);
2469 } else if(nav.hasPrestoStorage) {
2470 prestoStorage.removeItem(id);
2471 } else {
2472 locStorage.removeItem(id);
2473 }
2474}
2475
2476// Receives and parses JSON data into an object
2477async function getStoredObj(id) {
2478 return JSON.parse(await getStored(id) || '{}') || {};
2479}
2480
2481// Replaces the domain config with an object. Removes the domain config, if there is no object.
2482function saveCfgObj(dm, obj) {
2483 getStoredObj('DESU_Config').then(val => {
2484 if(obj) {
2485 val[dm] = obj;
2486 } else {
2487 delete val[dm];
2488 }
2489 setStored('DESU_Config', JSON.stringify(val));
2490 });
2491}
2492
2493// Saves the value for a particular config option
2494function saveCfg(id, val) {
2495 if(Cfg[id] !== val) {
2496 Cfg[id] = val;
2497 saveCfgObj(aib.dm, Cfg);
2498 }
2499}
2500
2501// Toggles a particular config option (1|0)
2502function toggleCfg(id) {
2503 saveCfg(id, +!Cfg[id]);
2504}
2505
2506function readData() {
2507 return Promise.all([readFavorites(), readCfg()]);
2508}
2509
2510// Config initialization, checking for Dollchan update.
2511async function readCfg() {
2512 let obj;
2513 const val = await getStoredObj('DESU_Config');
2514 if(!(aib.dm in val) || $isEmpty(obj = val[aib.dm])) {
2515 const isGlobal = nav.hasGlobalStorage && !!val.global;
2516 obj = isGlobal ? val.global : {};
2517 if(isGlobal) {
2518 delete obj.correctTime;
2519 delete obj.captchaLang;
2520 }
2521 }
2522 defaultCfg.captchaLang = aib.capLang;
2523 defaultCfg.language = +!String(navigator.language).toLowerCase().startsWith('ru');
2524 Cfg = Object.assign(Object.create(defaultCfg), obj);
2525 if(!Cfg.timeOffset) {
2526 Cfg.timeOffset = '+0';
2527 }
2528 if(!Cfg.timePattern) {
2529 Cfg.timePattern = aib.timePattern;
2530 }
2531 if(aib.prot !== 'http:') { // Vocaroo doesn't support https
2532 Cfg.addVocaroo = 0;
2533 }
2534 if(aib.dobrochan && !Cfg.useDobrAPI) {
2535 aib.JsonBuilder = null;
2536 }
2537 if(!('FormData' in deWindow)) {
2538 Cfg.ajaxPosting = 0;
2539 }
2540 if(!Cfg.ajaxPosting) {
2541 Cfg.fileInputs = 0;
2542 }
2543 if(!('Notification' in deWindow)) {
2544 Cfg.desktNotif = 0;
2545 }
2546 if(nav.isPresto) {
2547 Cfg.preLoadImgs = 0;
2548 Cfg.findImgFile = 0;
2549 if(!nav.hasOldGM) {
2550 Cfg.updDollchan = 0;
2551 }
2552 Cfg.fileInputs = 0;
2553 }
2554 if(nav.scriptHandler === 'WebExtension') {
2555 Cfg.updDollchan = 0;
2556 }
2557 if(Cfg.updThrDelay < 10) {
2558 Cfg.updThrDelay = 10;
2559 }
2560 if(!Cfg.addSageBtn || !Cfg.saveSage) {
2561 Cfg.sageReply = 0;
2562 }
2563 if(!Cfg.passwValue) {
2564 Cfg.passwValue = Math.round(Math.random() * 1e12).toString(32);
2565 }
2566 if(!Cfg.stats) {
2567 Cfg.stats = { view: 0, op: 0, reply: 0 };
2568 }
2569 if(Cfg.addYouTube !== undefined) {
2570 Cfg.embedYTube = Cfg.addYouTube === 0 ? 0 : Cfg.addYouTube === 1 ? 2 : 1;
2571 delete Cfg.addYouTube;
2572 }
2573 lang = Cfg.language;
2574 if(val.commit !== commit && !localData) {
2575 const font = ' style="font: 13px monospace; color: green;"';
2576 const donateMsg = Lng.donateMsg[lang] + ':<br style="margin-bottom: 8px;">' +
2577 '<div class="de-logo"><svg><use xlink:href="#de-symbol-panel-logo"/></svg></div>' +
2578 '<div style="display: inline-block;"><b><i>Yandex.Money</i></b><br>' +
2579 `<span class="de-list de-depend"><i${
2580 font }>410012122418236</i></span><br><b><i>WebMoney</i></b><br>` +
2581 `<span class="de-list de-depend">WMZ – <i${ font }>Z100197626370</i></span><br>` +
2582 `<span class="de-list de-depend">WMR – <i${ font }>R266614957054</i></span><br>` +
2583 `<span class="de-list de-depend">WMU – <i${ font }>U142375546253</i></span><br>` +
2584 `<b><i>Bitcoin</i></b><br><span class="de-list de-depend">P2PKH – <i${
2585 font }>15xEo7BVQ3zjztJqKSRVhTq3tt3rNSHFpC</i></span><br>` +
2586 `<span class="de-list de-depend">P2SH – <i${
2587 font }>3AhNPPpvtxQoFCLXk5e9Hzh6Ex9h7EoNzq</i></span></div>` +
2588 (nav.firefoxVer >= 56 && nav.scriptHandler !== 'WebExtension' ?
2589 `<br><br>New: <a href="https://addons.mozilla.org/${ lang === 1 ? 'en-US' : 'ru' }` +
2590 '/firefox/addon/dollchan-extension/" target="_blank">' + Lng.firefoxAddon[lang] : '');
2591 const popupFn = () => $popup('donate', donateMsg);
2592 if(doc.readyState === 'loading') {
2593 doc.addEventListener('DOMContentLoaded', () => setTimeout(popupFn, 1e3));
2594 } else {
2595 setTimeout(popupFn, 1e3);
2596 }
2597 val.commit = commit;
2598 }
2599 setStored('DESU_Config', JSON.stringify(val));
2600 if(Cfg.updDollchan && !localData) {
2601 checkForUpdates(false, val.lastUpd).then(html => {
2602 if(doc.readyState === 'loading') {
2603 doc.addEventListener('DOMContentLoaded', () => $popup('updavail', html));
2604 } else {
2605 $popup('updavail', html);
2606 }
2607 }, emptyFn);
2608 }
2609}
2610
2611// Initialize of hidden and favorites. Run spells.
2612function readPostsData(firstPost, favObj) {
2613 let sVis = null;
2614 try {
2615 // Get hidden posts and threads that cached in current session
2616 const str = aib.t ? sesStorage['de-hidden-' + aib.b + aib.t] : null;
2617 if(str) {
2618 const json = JSON.parse(str);
2619 if(json.hash === (Cfg.hideBySpell ? Spells.hash : 0) &&
2620 pByNum.has(json.lastNum) && pByNum.get(json.lastNum).count === json.lastCount
2621 ) {
2622 sVis = json.data && json.data[0] instanceof Array ? json.data : null;
2623 }
2624 }
2625 } catch(err) {
2626 sesStorage['de-hidden-' + aib.b + aib.t] = null;
2627 }
2628 if(!firstPost) {
2629 return;
2630 }
2631 let updateFav = null;
2632 const favBrd = (aib.host in favObj) && (aib.b in favObj[aib.host]) ? favObj[aib.host][aib.b] : {};
2633 const spellsHide = Cfg.hideBySpell;
2634 const maybeSpells = new Maybe(SpellsRunner);
2635
2636 // Search existed posts in stored data
2637 for(let post = firstPost; post; post = post.next) {
2638 const { num } = post;
2639 // Mark favorite threads, update favorites data
2640 if(post.isOp && (num in favBrd)) {
2641 const f = favBrd[num];
2642 const { thr } = post;
2643 post.toggleFavBtn(true);
2644 post.thr.isFav = true;
2645 if(aib.t) {
2646 f.cnt = thr.pcount;
2647 f.new = f.you = 0;
2648 if(Cfg.markNewPosts && f.last) {
2649 let lastPost = pByNum.get(+f.last.match(/\d+/));
2650 if(lastPost) {
2651 // Mark all new posts after last viewed post
2652 while((lastPost = lastPost.next)) {
2653 Post.addMark(lastPost.el, true);
2654 }
2655 }
2656 }
2657 f.last = aib.anchor + thr.last.num;
2658 } else {
2659 f.new = thr.pcount - f.cnt;
2660 }
2661 updateFav = [aib.host, aib.b, aib.t, [thr.pcount, thr.last.num], 'update'];
2662 }
2663 if(HiddenPosts.has(num)) {
2664 HiddenPosts.hideHidden(post, num);
2665 continue;
2666 }
2667 let hideData;
2668 if(post.isOp) {
2669 if(HiddenThreads.has(num)) {
2670 hideData = [true, null];
2671 } else if(spellsHide) {
2672 hideData = sVis && sVis[post.count];
2673 }
2674 } else if(spellsHide) {
2675 hideData = sVis && sVis[post.count];
2676 } else {
2677 continue;
2678 }
2679 if(!hideData) {
2680 maybeSpells.value.runSpells(post); // Apply spells if posts not hidden
2681 } else if(hideData[0]) {
2682 if(post.isHidden) {
2683 post.spellHidden = true;
2684 } else {
2685 post.spellHide(hideData[1]);
2686 }
2687 }
2688 }
2689 if(maybeSpells.hasValue) {
2690 maybeSpells.value.endSpells();
2691 }
2692 if(aib.t && Cfg.panelCounter === 2) {
2693 $id('de-panel-info-pcount').textContent = Thread.first.pcount - Thread.first.hidCounter;
2694 }
2695 if(updateFav) {
2696 saveFavorites(favObj);
2697 sendStorageEvent('__de-favorites', updateFav);
2698 }
2699 // After following a link from Favorites, we need to open Favorites again.
2700 const hasFavWinKey = sesStorage['de-fav-win'] === '1';
2701 if(hasFavWinKey || Cfg.favWinOn) {
2702 toggleWindow('fav', !!$q('#de-win-fav.de-win-active'), null, true);
2703 if(hasFavWinKey) {
2704 sesStorage.removeItem('de-fav-win');
2705 }
2706 }
2707 let data = sesStorage['de-fav-newthr'];
2708 if(data) { // Detecting the created new thread and adding it to Favorites.
2709 data = JSON.parse(data);
2710 const isTimeOut = !data.num && (Date.now() - data.date > 2e4);
2711 if(data.num === firstPost.num || !firstPost.next && !isTimeOut) {
2712 firstPost.thr.toggleFavState(true);
2713 sesStorage.removeItem('de-fav-newthr');
2714 } else if(isTimeOut) {
2715 sesStorage.removeItem('de-fav-newthr');
2716 }
2717 }
2718 if(Cfg.nextPageThr && DelForm.first === DelForm.last) {
2719 const hidThrEls = $Q('.de-thr-hid', firstPost.thr.form.el);
2720 const hidThrLen = hidThrEls.length;
2721 if(hidThrLen) {
2722 Pages.addPage(hidThrLen);
2723 }
2724 }
2725}
2726
2727function readFavorites() {
2728 return getStoredObj('DESU_Favorites');
2729}
2730
2731function saveFavorites(data) {
2732 setStored('DESU_Favorites', JSON.stringify(data));
2733}
2734
2735// Get posts that were read by posts previews
2736function readViewedPosts() {
2737 if(!Cfg.markViewed) {
2738 return;
2739 }
2740 const data = sesStorage['de-viewed'];
2741 if(data) {
2742 data.split(',').forEach(pNum => {
2743 const post = pByNum.get(+pNum);
2744 if(post) {
2745 post.el.classList.add('de-viewed');
2746 post.isViewed = true;
2747 }
2748 });
2749 }
2750}
2751
2752// HIDDEN AND MY POSTS STORAGE
2753
2754class PostsStorage {
2755 constructor() {
2756 this.storageName = '';
2757 this.__cachedTime = null;
2758 this._cachedStorage = null;
2759 this._cacheTO = null;
2760 }
2761 get(num) {
2762 const storage = this._readStorage()[aib.b];
2763 if(storage) {
2764 const val = storage[num];
2765 return val ? val[2] : null;
2766 }
2767 return null;
2768 }
2769 has(num) {
2770 const storage = this._readStorage()[aib.b];
2771 return storage ? storage.hasOwnProperty(num) : false;
2772 }
2773 purge() {
2774 this._cacheTO = this.__cachedTime = this._cachedStorage = null;
2775 }
2776 removeStorage(num, board = aib.b) {
2777 const storage = this._readStorage();
2778 const bStorage = storage[board];
2779 if(bStorage && bStorage.hasOwnProperty(num)) {
2780 delete bStorage[num];
2781 if($isEmpty(bStorage)) {
2782 delete storage[board];
2783 }
2784 this._saveStorage();
2785 }
2786 }
2787 set(num, thrNum, data = true) {
2788 const storage = this._readStorage();
2789 if(storage && storage.$count > 5e3) {
2790 const minDate = Date.now() - 5 * 24 * 3600 * 1e3;
2791 for(const b in storage) {
2792 if(storage.hasOwnProperty(b)) {
2793 const data = storage[b];
2794 for(const key in data) {
2795 if(data.hasOwnProperty(key) && data[key][0] < minDate) {
2796 delete data[key];
2797 }
2798 }
2799 }
2800 }
2801 }
2802 (storage[aib.b] || (storage[aib.b] = {}))[num] = [this._cachedTime, thrNum, data];
2803 this._saveStorage();
2804 }
2805
2806 static _migrateOld(newName, oldName) {
2807 if(locStorage.hasOwnProperty(oldName)) {
2808 locStorage[newName] = locStorage[oldName];
2809 locStorage.removeItem(oldName);
2810 }
2811 }
2812 get _cachedTime() {
2813 return this.__cachedTime || (this.__cachedTime = Date.now());
2814 }
2815 _readStorage() {
2816 if(this._cachedStorage) {
2817 return this._cachedStorage;
2818 }
2819 const data = locStorage[this.storageName];
2820 if(data) {
2821 try {
2822 return (this._cachedStorage = JSON.parse(data));
2823 } catch(err) {}
2824 }
2825 return (this._cachedStorage = {});
2826 }
2827 _saveStorage() {
2828 if(this._cacheTO === null) {
2829 this._cacheTO = setTimeout(() => {
2830 if(this._cachedStorage) {
2831 locStorage[this.storageName] = JSON.stringify(this._cachedStorage);
2832 }
2833 this.purge();
2834 }, 0);
2835 }
2836 }
2837}
2838
2839const HiddenPosts = new class HiddenPostsClass extends PostsStorage {
2840 constructor() {
2841 super();
2842 this.storageName = 'de-posts';
2843 }
2844 hideHidden(post, num) {
2845 const uHideData = HiddenPosts.get(num);
2846 if(!uHideData && post.isOp && HiddenThreads.has(num)) {
2847 post.setUserVisib(true);
2848 } else {
2849 post.setUserVisib(!!uHideData, false);
2850 }
2851 }
2852
2853 _readStorage() {
2854 PostsStorage._migrateOld(this.storageName, 'de-threads-new'); // Old storage has wrong name
2855 return super._readStorage();
2856 }
2857}();
2858
2859const HiddenThreads = new class HiddenThreadsClass extends PostsStorage {
2860 constructor() {
2861 super();
2862 this.storageName = 'de-threads';
2863 }
2864 getCount() {
2865 const storage = this._readStorage();
2866 let rv = 0;
2867 for(const b in storage) {
2868 rv += Object.keys(storage[b]).length;
2869 }
2870 return rv;
2871 }
2872 getRawData() {
2873 return this._readStorage();
2874 }
2875 saveRawData(data) {
2876 locStorage[this.storageName] = JSON.stringify(data);
2877 this.purge();
2878 }
2879
2880 _readStorage() {
2881 PostsStorage._migrateOld(this.storageName, ''); // Old storage has wrong name
2882 return super._readStorage();
2883 }
2884}();
2885
2886const MyPosts = new class MyPostsClass extends PostsStorage {
2887 constructor() {
2888 super();
2889 this.storageName = 'de-myposts';
2890 this._cachedData = null;
2891 }
2892 has(num) {
2893 return this._cachedData.has(num);
2894 }
2895 purge() {
2896 super.purge();
2897 this._cachedData = null;
2898 this._readStorage();
2899 }
2900 readStorage() {
2901 this._readStorage();
2902 }
2903 set(num, thrNum) {
2904 super.set(num, thrNum);
2905 this._cachedData.add(+num);
2906 sendStorageEvent('__de-mypost', 1);
2907 }
2908
2909 _readStorage() {
2910 if(this._cachedData && this._cachedStorage) {
2911 return this._cachedStorage;
2912 }
2913 PostsStorage._migrateOld(this.storageName, 'de-myposts-new');
2914 const rv = super._readStorage();
2915 this._cachedData = rv[aib.b] ? new Set(Object.keys(rv[aib.b]).map(val => +val)) : new Set();
2916 return rv;
2917 }
2918}();
2919
2920function sendStorageEvent(name, value) {
2921 locStorage[name] = typeof value === 'string' ? value : JSON.stringify(value);
2922 locStorage.removeItem(name);
2923}
2924
2925function initStorageEvent() {
2926 doc.defaultView.addEventListener('storage', e => {
2927 let data, temp, val = e.newValue;
2928 if(!val) {
2929 return;
2930 }
2931 switch(e.key) {
2932 case '__de-favorites': {
2933 try {
2934 data = JSON.parse(val);
2935 } catch(err) {
2936 return;
2937 }
2938 updateFavWindow(...data);
2939 return;
2940 }
2941 case '__de-mypost': MyPosts.purge(); return;
2942 case '__de-webmvolume':
2943 val = +val || 0;
2944 Cfg.webmVolume = val;
2945 temp = $q('input[info="webmVolume"]');
2946 if(temp) {
2947 temp.value = val;
2948 }
2949 return;
2950 case '__de-post':
2951 (() => {
2952 try {
2953 data = JSON.parse(val);
2954 } catch(err) {
2955 return;
2956 }
2957 HiddenThreads.purge();
2958 HiddenPosts.purge();
2959 if(data.brd === aib.b) {
2960 let post = pByNum.get(data.num);
2961 if(post && (post.isHidden ^ data.hide)) {
2962 post.setUserVisib(data.hide, false);
2963 } else if((post = pByNum.get(data.thrNum))) {
2964 post.thr.userTouched.set(data.num, data.hide);
2965 }
2966 }
2967 toggleWindow('hid', true);
2968 })();
2969 return;
2970 case 'de-threads':
2971 HiddenThreads.purge();
2972 Thread.first.updateHidden(HiddenThreads.getRawData()[aib.b]);
2973 toggleWindow('hid', true);
2974 return;
2975 case '__de-spells': (() => {
2976 try {
2977 data = JSON.parse(val);
2978 } catch(err) {
2979 return;
2980 }
2981 Cfg.hideBySpell = +data.hide;
2982 temp = $q('input[info="hideBySpell"]');
2983 if(temp) {
2984 temp.checked = data.hide;
2985 }
2986 $hide(docBody);
2987 if(data.data) {
2988 Spells.setSpells(data.data, false);
2989 Cfg.spells = JSON.stringify(data.data);
2990 temp = $id('de-spell-txt');
2991 if(temp) {
2992 temp.value = Spells.list;
2993 }
2994 } else {
2995 SpellsRunner.unhideAll();
2996 Spells.disableSpells();
2997 temp = $id('de-spell-txt');
2998 if(temp) {
2999 temp.value = '';
3000 }
3001 }
3002 $show(docBody);
3003 })();
3004 }
3005 });
3006}
3007
3008/* ==[ Panel.js ]=============================================================================================
3009 MAIN PANEL
3010=========================================================================================================== */
3011
3012const Panel = Object.create({
3013 isVidEnabled: false,
3014 initPanel(formEl) {
3015 const imgLen = $Q(aib.qPostImg, formEl).length;
3016 const isThr = aib.t;
3017 (pr && pr.pArea[0] || formEl).insertAdjacentHTML('beforebegin', `<div id="de-main">
3018 <div id="de-panel">
3019 <div id="de-panel-logo" title="${ Lng.panelBtn.attach[lang] }">
3020 <svg class="de-panel-logo-svg">
3021 <use xlink:href="#de-symbol-panel-logo"/>
3022 </svg>
3023 </div>
3024 <span id="de-panel-buttons"${ !Cfg.expandPanel ? ' style="display: none;"' : '' }>
3025 ${ Cfg.disabled ? this._getButton('enable') : this._getButton('cfg') +
3026 this._getButton('hid') +
3027 this._getButton('fav') +
3028 (Cfg.embedYTube ? this._getButton('vid') : '') +
3029 (!localData ?
3030 this._getButton('refresh') +
3031 (isThr || aib.page !== aib.firstPage ? this._getButton('goback') : '') +
3032 (!isThr && aib.page !== aib.lastPage ? this._getButton('gonext') : '') : '') +
3033 this._getButton('goup') +
3034 this._getButton('godown') +
3035 (imgLen ? this._getButton('expimg') + this._getButton('maskimg') : '') +
3036 (!localData && !nav.isPresto ?
3037 (imgLen && !Cfg.preLoadImgs ? this._getButton('preimg') : '') +
3038 (isThr ? this._getButton('savethr') : '') : '') +
3039 (!localData && isThr ?
3040 this._getButton(Cfg.ajaxUpdThr && !aib.isArchived ? 'upd-on' : 'upd-off') +
3041 (!nav.isSafari ? this._getButton('audio-off') : '') : '') +
3042 (aib.hasCatalog ? this._getButton('catalog') : '') +
3043 this._getButton('enable') +
3044 (isThr && Thread.first ? `<span id="de-panel-info">
3045 <span id="de-panel-info-pcount" title="` +
3046 `${ Lng.panelBtn[Cfg.panelCounter !== 2 ? 'pcount' : 'pcountNotHid'][lang] }">` +
3047 `${ Thread.first.pcount }</span>
3048 <span id="de-panel-info-icount" title="${ Lng.panelBtn.imglen[lang] }">${
3049 imgLen }</span>
3050 <span id="de-panel-info-acount" title="${ Lng.panelBtn.posters[lang] }"></span>
3051 </span>` : '') }
3052 </span>
3053 </div>
3054 ${ Cfg.disabled ? '' : '<div id="de-wrapper-popup"></div><hr style="clear: both;">' }
3055 </div>`);
3056 this._el = $id('de-panel');
3057 this._el.addEventListener('click', this, true);
3058 this._el.addEventListener('mouseover', this);
3059 this._el.addEventListener('mouseout', this);
3060 this._buttons = $id('de-panel-buttons');
3061 this.isNew = true;
3062 },
3063 removeMain() {
3064 this._el.removeEventListener('click', this, true);
3065 this._el.removeEventListener('mouseover', this);
3066 this._el.removeEventListener('mouseout', this);
3067 delete this._pcountEl;
3068 delete this._icountEl;
3069 delete this._acountEl;
3070 $id('de-main').remove();
3071 },
3072 handleEvent(e) {
3073 if('isTrusted' in e && !e.isTrusted) {
3074 return;
3075 }
3076 let el = fixEventEl(e.target);
3077 el = el.tagName.toLowerCase() === 'svg' ? el.parentNode : el;
3078 switch(e.type) {
3079 case 'click':
3080 switch(el.id) {
3081 case 'de-panel-logo':
3082 if(Cfg.expandPanel && !$q('.de-win-active')) {
3083 $hide(this._buttons);
3084 }
3085 toggleCfg('expandPanel');
3086 return;
3087 case 'de-panel-cfg': toggleWindow('cfg', false); break;
3088 case 'de-panel-hid': toggleWindow('hid', false); break;
3089 case 'de-panel-fav': toggleWindow('fav', false); break;
3090 case 'de-panel-vid':
3091 this.isVidEnabled = !this.isVidEnabled;
3092 toggleWindow('vid', false);
3093 break;
3094 case 'de-panel-refresh': deWindow.location.reload(); break;
3095 case 'de-panel-goup': scrollTo(0, 0); break;
3096 case 'de-panel-godown': scrollTo(0, docBody.scrollHeight || docBody.offsetHeight); break;
3097 case 'de-panel-expimg':
3098 el.classList.toggle('de-panel-button-active');
3099 isExpImg = !isExpImg;
3100 $del($q('.de-fullimg-center'));
3101 for(let post = Thread.first.op; post; post = post.next) {
3102 post.toggleImages(isExpImg, false);
3103 }
3104 break;
3105 case 'de-panel-preimg':
3106 el.classList.toggle('de-panel-button-active');
3107 isPreImg = !isPreImg;
3108 if(!e.ctrlKey) {
3109 for(const { el } of DelForm) {
3110 ContentLoader.preloadImages(el);
3111 }
3112 }
3113 break;
3114 case 'de-panel-maskimg':
3115 el.classList.toggle('de-panel-button-active');
3116 toggleCfg('maskImgs');
3117 updateCSS();
3118 break;
3119 case 'de-panel-upd-on':
3120 case 'de-panel-upd-warn':
3121 case 'de-panel-upd-off':
3122 updater.toggle();
3123 break;
3124 case 'de-panel-audio-on':
3125 case 'de-panel-audio-off':
3126 if(updater.toggleAudio(0)) {
3127 updater.enableUpdater();
3128 el.id = 'de-panel-audio-on';
3129 } else {
3130 el.id = 'de-panel-audio-off';
3131 }
3132 $del($q('.de-menu'));
3133 break;
3134 case 'de-panel-savethr': break;
3135 case 'de-panel-enable':
3136 toggleCfg('disabled');
3137 deWindow.location.reload();
3138 break;
3139 default: return;
3140 }
3141 $pd(e);
3142 return;
3143 case 'mouseover':
3144 if(!Cfg.expandPanel) {
3145 clearTimeout(this._hideTO);
3146 $show(this._buttons);
3147 }
3148 switch(el.id) {
3149 case 'de-panel-cfg': KeyEditListener.setTitle(el, 10); break;
3150 case 'de-panel-hid': KeyEditListener.setTitle(el, 7); break;
3151 case 'de-panel-fav': KeyEditListener.setTitle(el, 6); break;
3152 case 'de-panel-vid': KeyEditListener.setTitle(el, 18); break;
3153 case 'de-panel-goback': KeyEditListener.setTitle(el, 4); break;
3154 case 'de-panel-gonext': KeyEditListener.setTitle(el, 17); break;
3155 case 'de-panel-maskimg': KeyEditListener.setTitle(el, 9); break;
3156 case 'de-panel-refresh':
3157 if(aib.t) {
3158 return;
3159 }
3160 /* falls through */
3161 case 'de-panel-savethr':
3162 case 'de-panel-audio-off':
3163 if(this._menu && this._menu.parentEl === el) {
3164 return;
3165 }
3166 this._menuTO = setTimeout(() => {
3167 this._menu = addMenu(el);
3168 this._menu.onover = () => clearTimeout(this._hideTO);
3169 this._menu.onout = () => this._prepareToHide(null);
3170 this._menu.onremove = () => (this._menu = null);
3171 }, Cfg.linksOver);
3172 }
3173 return;
3174 default: // mouseout
3175 this._prepareToHide(fixEventEl(e.relatedTarget));
3176 switch(el.id) {
3177 case 'de-panel-refresh':
3178 case 'de-panel-savethr':
3179 case 'de-panel-audio-off':
3180 clearTimeout(this._menuTO);
3181 this._menuTO = 0;
3182 }
3183 }
3184 },
3185 updateCounter(postCount, imgsCount, postersCount) {
3186 this._pcountEl.textContent = postCount;
3187 this._icountEl.textContent = imgsCount;
3188 this._acountEl.textContent = postersCount;
3189 this.isNew = false;
3190 },
3191
3192 _el : null,
3193 _hideTO : 0,
3194 _menu : null,
3195 _menuTO : 0,
3196 get _acountEl() {
3197 const value = $id('de-panel-info-acount');
3198 Object.defineProperty(this, '_acountEl', { value, configurable: true });
3199 return value;
3200 },
3201 get _icountEl() {
3202 const value = $id('de-panel-info-icount');
3203 Object.defineProperty(this, '_icountEl', { value, configurable: true });
3204 return value;
3205 },
3206 get _pcountEl() {
3207 const value = $id('de-panel-info-pcount');
3208 Object.defineProperty(this, '_pcountEl', { value, configurable: true });
3209 return value;
3210 },
3211 _getButton(id) {
3212 let page, href, title, useId;
3213 switch(id) {
3214 case 'goback':
3215 page = Math.max(aib.page - 1, 0);
3216 href = aib.getPageUrl(aib.b, page);
3217 if(!aib.t) {
3218 title = Lng.panelBtn.gonext[lang].replace('%s', page);
3219 }
3220 useId = 'arrow';
3221 break;
3222 case 'gonext':
3223 page = aib.page + 1;
3224 href = aib.getPageUrl(aib.b, page);
3225 title = Lng.panelBtn.gonext[lang].replace('%s', page);
3226 /* falls through */
3227 case 'goup':
3228 case 'godown':
3229 useId = 'arrow';
3230 break;
3231 case 'upd-on':
3232 case 'upd-off':
3233 useId = 'upd';
3234 break;
3235 case 'catalog':
3236 href = aib.catalogUrl;
3237 }
3238 // XXX Opera Presto: keep in sync with updMachine._setUpdateStatus
3239 return `<a id="de-panel-${ id }" class="de-abtn de-panel-button" title="${
3240 title || Lng.panelBtn[id][lang] }" href="${ href || '#' }">
3241 <svg class="de-panel-svg">
3242 ${ id !== 'audio-off' ? `
3243 <use xlink:href="#de-symbol-panel-${ useId || id }"/>` : `
3244 <use class="de-use-audio-off" xlink:href="#de-symbol-panel-audio-off"/>
3245 <use class="de-use-audio-on" xlink:href="#de-symbol-panel-audio-on"/>` }
3246 </svg>
3247 </a>`;
3248 },
3249 _prepareToHide(rt) {
3250 if(!Cfg.expandPanel && !$q('.de-win-active') &&
3251 (!rt || !this._el.contains(rt.farthestViewportElement || rt))
3252 ) {
3253 this._hideTO = setTimeout(() => $hide(this._buttons), 500);
3254 }
3255 }
3256});
3257
3258/* ==[ WindowUtils.js ]=======================================================================================
3259 WINDOW: UTILS
3260=========================================================================================================== */
3261
3262function updateWinZ(style) {
3263 if(style.zIndex < topWinZ) {
3264 style.zIndex = ++topWinZ;
3265 }
3266}
3267
3268function makeDraggable(name, win, head) {
3269 head.addEventListener('mousedown', {
3270 _oldX : 0,
3271 _oldY : 0,
3272 _win : win,
3273 _wStyle : win.style,
3274 _X : 0,
3275 _Y : 0,
3276 _Z : 0,
3277 handleEvent(e) {
3278 if(!Cfg[name + 'WinDrag']) {
3279 return;
3280 }
3281 const { clientX: curX, clientY: curY } = e;
3282 switch(e.type) {
3283 case 'mousedown':
3284 this._oldX = curX;
3285 this._oldY = curY;
3286 this._X = Cfg[name + 'WinX'];
3287 this._Y = Cfg[name + 'WinY'];
3288 if(this._Z < topWinZ) {
3289 this._Z = this._wStyle.zIndex = ++topWinZ;
3290 }
3291 docBody.addEventListener('mouseleave', this);
3292 docBody.addEventListener('mousemove', this);
3293 docBody.addEventListener('mouseup', this);
3294 $pd(e);
3295 return;
3296 case 'mousemove': {
3297 const maxX = Post.sizing.wWidth - this._win.offsetWidth;
3298 const maxY = Post.sizing.wHeight - this._win.offsetHeight - 25;
3299 const cr = this._win.getBoundingClientRect();
3300 const x = cr.left + curX - this._oldX;
3301 const y = cr.top + curY - this._oldY;
3302 this._X = x >= maxX || curX > this._oldX && x > maxX - 20 ? 'right: 0' :
3303 x < 0 || curX < this._oldX && x < 20 ? 'left: 0' :
3304 `left: ${ x }px`;
3305 this._Y = y >= maxY || curY > this._oldY && y > maxY - 20 ? 'bottom: 25px' :
3306 y < 0 || curY < this._oldY && y < 20 ? 'top: 0' :
3307 `top: ${ y }px`;
3308 const { width } = this._wStyle;
3309 this._win.setAttribute('style', `${ this._X }; ${ this._Y }; z-index: ${ this._Z }${
3310 width ? '; width: ' + width : '' }`);
3311 this._oldX = curX;
3312 this._oldY = curY;
3313 return;
3314 }
3315 case 'mouseleave':
3316 case 'mouseup':
3317 docBody.removeEventListener('mouseleave', this);
3318 docBody.removeEventListener('mousemove', this);
3319 docBody.removeEventListener('mouseup', this);
3320 saveCfg(name + 'WinX', this._X);
3321 saveCfg(name + 'WinY', this._Y);
3322 }
3323 }
3324 });
3325}
3326
3327class WinResizer {
3328 constructor(name, dir, cfgName, win, target) {
3329 this.name = name;
3330 this.dir = dir;
3331 this.cfgName = cfgName;
3332 this.vertical = dir === 'top' || dir === 'bottom';
3333 this.win = win;
3334 this.wStyle = this.win.style;
3335 this.tStyle = target.style;
3336 $q('.de-resizer-' + dir, win).addEventListener('mousedown', this);
3337 }
3338 handleEvent(e) {
3339 let val, x, y;
3340 const { wWidth: maxX, wHeight: maxY } = Post.sizing;
3341 const { width } = this.wStyle;
3342 const cr = this.win.getBoundingClientRect();
3343 const z = `; z-index: ${ this.wStyle.zIndex }${ width ? '; width:' + width : '' }`;
3344 switch(e.type) {
3345 case 'mousedown':
3346 if(this.win.classList.contains('de-win-fixed')) {
3347 x = 'right: 0';
3348 y = 'bottom: 25px';
3349 } else {
3350 x = Cfg[this.name + 'WinX'];
3351 y = Cfg[this.name + 'WinY'];
3352 }
3353 switch(this.dir) {
3354 case 'top': val = `${ x }; bottom: ${ maxY - cr.bottom }px${ z }`; break;
3355 case 'bottom': val = `${ x }; top: ${ cr.top }px${ z }`; break;
3356 case 'left': val = `right: ${ maxX - cr.right }px; ${ y + z }`; break;
3357 case 'right': val = `left: ${ cr.left }px; ${ y + z }`;
3358 }
3359 this.win.setAttribute('style', val);
3360 docBody.addEventListener('mousemove', this);
3361 docBody.addEventListener('mouseup', this);
3362 $pd(e);
3363 return;
3364 case 'mousemove':
3365 if(this.vertical) {
3366 val = e.clientY;
3367 this.tStyle.setProperty('height', Math.max(parseInt(this.tStyle.height, 10) + (
3368 this.dir === 'top' ? cr.top - (val < 20 ? 0 : val) :
3369 (val > maxY - 45 ? maxY - 25 : val) - cr.bottom
3370 ), 90) + 'px', 'important');
3371 } else {
3372 val = e.clientX;
3373 this.tStyle.setProperty('width', Math.max(parseInt(this.tStyle.width, 10) + (
3374 this.dir === 'left' ? cr.left - (val < 20 ? 0 : val) :
3375 (val > maxX - 20 ? maxX : val) - cr.right
3376 ), this.name === 'reply' ? 275 : 400) + 'px', 'important');
3377 }
3378 return;
3379 default: // mouseup
3380 docBody.removeEventListener('mousemove', this);
3381 docBody.removeEventListener('mouseup', this);
3382 saveCfg(this.cfgName, parseInt(this.vertical ? this.tStyle.height : this.tStyle.width, 10));
3383 if(this.win.classList.contains('de-win-fixed')) {
3384 this.win.setAttribute('style', 'right: 0; bottom: 25px' + z);
3385 return;
3386 }
3387 if(this.vertical) {
3388 saveCfg(this.name + 'WinY', cr.top < 1 ? 'top: 0' :
3389 cr.bottom > maxY - 26 ? 'bottom: 25px' : `top: ${ cr.top }px`);
3390 } else {
3391 saveCfg(this.name + 'WinX', cr.left < 1 ? 'left: 0' :
3392 cr.right > maxX - 1 ? 'right: 0' : `left: ${ cr.left }px`);
3393 }
3394 this.win.setAttribute('style', Cfg[this.name + 'WinX'] + '; ' + Cfg[this.name + 'WinY'] + z);
3395 }
3396 }
3397}
3398
3399function toggleWindow(name, isUpdate, data, noAnim) {
3400 let el, win = $id('de-win-' + name);
3401 const isActive = win && win.classList.contains('de-win-active');
3402 if(isUpdate && !isActive) {
3403 return;
3404 }
3405 if(!win) {
3406 const winAttr = (Cfg[name + 'WinDrag'] ?
3407 `de-win" style="${ Cfg[name + 'WinX'] }; ${ Cfg[name + 'WinY'] }` :
3408 'de-win-fixed" style="right: 0; bottom: 25px'
3409 ) + (name !== 'fav' ? '' : `; width: ${ Cfg.favWinWidth }px; `);
3410 win = $aBegin($id('de-main'), `<div id="de-win-${ name }" class="${ winAttr }; display: none;">
3411 <div class="de-win-head">
3412 <span class="de-win-title">
3413 ${ name === 'cfg' ? 'Dollchan Extension Tools' : Lng.panelBtn[name][lang] }
3414 </span>
3415 <span class="de-win-buttons">
3416 <svg class="de-win-btn-toggle"><use xlink:href="#de-symbol-win-arrow"/></svg>
3417 <svg class="de-win-btn-close"><use xlink:href="#de-symbol-win-close"/></svg>
3418 </span>
3419 </div>
3420 <div class="de-win-body"></div>
3421 ${ name !== 'fav' ? '' : `
3422 <div class="de-resizer de-resizer-left"></div>
3423 <div class="de-resizer de-resizer-right"></div>` }
3424 </div>`);
3425 const winBody = $q('.de-win-body', win);
3426 if(name === 'cfg') {
3427 winBody.className = 'de-win-body ' + aib.cReply;
3428 } else {
3429 setTimeout(() => {
3430 const backColor = getComputedStyle(docBody).getPropertyValue('background-color');
3431 winBody.style.backgroundColor = backColor !== 'transparent' ? backColor : '#EEE';
3432 }, 100);
3433 }
3434 if(name === 'fav') {
3435 new WinResizer('fav', 'left', 'favWinWidth', win, win);
3436 new WinResizer('fav', 'right', 'favWinWidth', win, win);
3437 }
3438 el = $q('.de-win-buttons', win);
3439 el.onmouseover = e => {
3440 const el = fixEventEl(e.target);
3441 const parent = el.parentNode;
3442 switch(el.classList[0]) {
3443 case 'de-win-btn-close': parent.title = Lng.closeWindow[lang]; break;
3444 case 'de-win-btn-toggle':
3445 parent.title = Cfg[name + 'WinDrag'] ? Lng.toPanel[lang] : Lng.makeDrag[lang];
3446 }
3447 };
3448 el.lastElementChild.onclick = () => toggleWindow(name, false);
3449 $q('.de-win-btn-toggle', el).onclick = () => {
3450 toggleCfg(name + 'WinDrag');
3451 const isDrag = Cfg[name + 'WinDrag'];
3452 if(!isDrag) {
3453 const temp = $q('.de-win-active.de-win-fixed', win.parentNode);
3454 if(temp) {
3455 toggleWindow(temp.id.substr(7), false);
3456 }
3457 }
3458 win.classList.toggle('de-win', isDrag);
3459 win.classList.toggle('de-win-fixed', !isDrag);
3460 const { width } = win.style;
3461 win.style.cssText = `${ isDrag ? `${ Cfg[name + 'WinX'] }; ${ Cfg[name + 'WinY'] }` :
3462 'right: 0; bottom: 25px' }${ width ? '; width: ' + width : '' }`;
3463 updateWinZ(win.style);
3464 };
3465 makeDraggable(name, win, $q('.de-win-head', win));
3466 }
3467 updateWinZ(win.style);
3468 let isRemove = !isUpdate && isActive;
3469 if(!isRemove && !win.classList.contains('de-win') &&
3470 (el = $q(`.de-win-active.de-win-fixed:not(#de-win-${ name })`, win.parentNode))
3471 ) {
3472 toggleWindow(el.id.substr(7), false);
3473 }
3474 const isAnim = !noAnim && !isUpdate && Cfg.animation;
3475 let body = $q('.de-win-body', win);
3476 if(isAnim && body.hasChildNodes()) {
3477 win.addEventListener('animationend', function aEvent(e) {
3478 e.target.removeEventListener('animationend', aEvent);
3479 showWindow(win, body, name, isRemove, data, Cfg.animation);
3480 win = body = name = isRemove = data = null;
3481 });
3482 win.classList.remove('de-win-open');
3483 win.classList.add('de-win-close');
3484 } else {
3485 showWindow(win, body, name, isRemove, data, isAnim);
3486 }
3487}
3488
3489function showWindow(win, body, name, isRemove, data, isAnim) {
3490 body.innerHTML = '';
3491 win.classList.toggle('de-win-active', !isRemove);
3492 if(isRemove) {
3493 win.classList.remove('de-win-close');
3494 $hide(win);
3495 if(!Cfg.expandPanel && !$q('.de-win-active')) {
3496 $hide($id('de-panel-buttons'));
3497 }
3498 return;
3499 }
3500 if(!Cfg.expandPanel) {
3501 $show($id('de-panel-buttons'));
3502 }
3503 switch(name) {
3504 case 'fav':
3505 if(data) {
3506 showFavoritesWindow(body, data);
3507 break;
3508 }
3509 readFavorites().then(favObj => {
3510 showFavoritesWindow(body, favObj);
3511 $show(win);
3512 if(isAnim) {
3513 win.classList.add('de-win-open');
3514 }
3515 });
3516 return;
3517 case 'cfg': CfgWindow.initCfgWindow(body); break;
3518 case 'hid': showHiddenWindow(body); break;
3519 case 'vid': showVideosWindow(body);
3520 }
3521 $show(win);
3522 if(isAnim) {
3523 win.classList.add('de-win-open');
3524 }
3525}
3526
3527/* ==[ WindowVidHid.js ]======================================================================================
3528 WINDOW: VIDEOS, HIDDEN THREADS
3529=========================================================================================================== */
3530
3531function showVideosWindow(body) {
3532 const els = $Q('.de-video-link');
3533 if(!els.length) {
3534 body.innerHTML = `<b>${ Lng.noVideoLinks[lang] }</b>`;
3535 return;
3536 }
3537 // EXCLUDED FROM FIREFOX EXTENSION - START
3538 if(!$id('de-ytube-api')) {
3539 // YouTube APT script. We can't insert scripts directly as html.
3540 const script = doc.createElement('script');
3541 script.type = 'text/javascript';
3542 script.src = aib.prot + '//www.youtube.com/player_api';
3543 doc.head.appendChild(script).id = 'de-ytube-api';
3544 }
3545 // EXCLUDED FROM FIREFOX EXTENSION - END
3546 body.innerHTML = `<div de-disableautoplay class="de-video-obj"></div>
3547 <div id="de-video-buttons">
3548 <a class="de-abtn" id="de-video-btn-prev" href="#" title="${ Lng.prevVideo[lang] }">◀</a>
3549 <a class="de-abtn" id="de-video-btn-resize" href="#" title="${ Lng.expandVideo[lang] }"></a>
3550 <a class="de-abtn" id="de-video-btn-next" href="#" title="${ Lng.nextVideo[lang] }">▶</a>
3551 <a class="de-abtn" id="de-video-btn-hide" href="#" title="${ Lng.hideLnkList[lang] }">▲</a>
3552 </div>`;
3553 const linkList = $add(`<div id="de-video-list" style="max-width: ${
3554 +Cfg.YTubeWidth + 40 }px; max-height: ${
3555 nav.viewportHeight() * 0.92 - +Cfg.YTubeHeigh - 82 }px;"></div>`);
3556
3557 // EXCLUDED FROM FIREFOX EXTENSION - START
3558 // A script to detect the end of current video playback, and auto play next. Uses YouTube API.
3559 // The first video should not start automatically!
3560 const script = doc.createElement('script');
3561 script.type = 'text/javascript';
3562 script.textContent = `(function() {
3563 if('YT' in window && 'Player' in window.YT) {
3564 onYouTubePlayerAPIReady();
3565 } else {
3566 window.onYouTubePlayerAPIReady = onYouTubePlayerAPIReady;
3567 }
3568 function onYouTubePlayerAPIReady() {
3569 window.de_addVideoEvents =
3570 addEvents.bind(document.querySelector('#de-win-vid > .de-win-body > .de-video-obj'));
3571 window.de_addVideoEvents();
3572 }
3573 function addEvents() {
3574 var autoplay = true;
3575 if(this.hasAttribute('de-disableautoplay')) {
3576 autoplay = false;
3577 this.removeAttribute('de-disableautoplay');
3578 }
3579 new YT.Player(this.firstChild, { events: {
3580 'onError': gotoNextVideo,
3581 'onReady': autoplay ? function(e) {
3582 e.target.playVideo();
3583 } : Function.prototype,
3584 'onStateChange': function(e) {
3585 if(e.data === 0) {
3586 gotoNextVideo();
3587 }
3588 }
3589 }});
3590 }
3591 function gotoNextVideo() {
3592 document.getElementById("de-video-btn-next").click();
3593 }
3594 })();`;
3595 body.appendChild(script);
3596 // EXCLUDED FROM FIREFOX EXTENSION - END
3597
3598 // Events for control buttons
3599 body.addEventListener('click', {
3600 linkList,
3601 currentLink : null,
3602 listHidden : false,
3603 player : body.firstElementChild,
3604 playerInfo : null,
3605 handleEvent(e) {
3606 const el = e.target;
3607 if(el.classList.contains('de-abtn')) {
3608 let node;
3609 switch(el.id) {
3610 case 'de-video-btn-hide': { // Fold/unfold list of links
3611 const isHide = this.listHidden = !this.listHidden;
3612 $toggle(this.linkList, !isHide);
3613 el.textContent = isHide ? '\u25BC' : '\u25B2';
3614 break;
3615 }
3616 case 'de-video-btn-prev': // Play previous video
3617 node = this.currentLink.parentNode;
3618 node = node.previousElementSibling || node.parentNode.lastElementChild;
3619 node.lastElementChild.click();
3620 break;
3621 case 'de-video-btn-next': // Play next video
3622 node = this.currentLink.parentNode;
3623 node = node.nextElementSibling || node.parentNode.firstElementChild;
3624 node.lastElementChild.click();
3625 break;
3626 case 'de-video-btn-resize': { // Expand/collapse video player
3627 const exp = this.player.className === 'de-video-obj';
3628 this.player.className = exp ? 'de-video-obj de-video-expanded' : 'de-video-obj';
3629 this.linkList.style.maxWidth = `${ exp ? 894 : +Cfg.YTubeWidth + 40 }px`;
3630 this.linkList.style.maxHeight = `${ nav.viewportHeight() * 0.92 -
3631 (exp ? 562 : +Cfg.YTubeHeigh + 82) }px`;
3632 }
3633 }
3634 $pd(e);
3635 return;
3636 } else if(!el.classList.contains('de-video-link')) { // Clicking on ">" before link
3637 // Go to post that contains this link
3638 pByNum.get(+el.getAttribute('de-num')).selectAndScrollTo();
3639 return;
3640 }
3641 const info = el.videoInfo;
3642 if(this.playerInfo !== info) { // Prevents same link clicking
3643 // Mark new link as a current and add player for it
3644 if(this.currentLink) {
3645 this.currentLink.classList.remove('de-current');
3646 }
3647 this.currentLink = el;
3648 el.classList.add('de-current');
3649 Videos.addPlayer(this, info, el.classList.contains('de-ytube'), true);
3650 }
3651 $pd(e);
3652 }
3653 }, true);
3654
3655 // Copy all video links into videos list
3656 for(let i = 0, len = els.length; i < len; ++i) {
3657 updateVideoList(linkList, els[i], aib.getPostOfEl(els[i]).num);
3658 }
3659 body.appendChild(linkList);
3660 $q('.de-video-link', linkList).click();
3661}
3662
3663function updateVideoList(parent, link, num) {
3664 const el = link.cloneNode(true);
3665 el.videoInfo = link.videoInfo;
3666 $bEnd(parent, `<div class="de-entry ${ aib.cReply }">
3667 <a class="de-video-refpost" title=">>${ num }" de-num="${ num }">>></a>
3668 </div>`).appendChild(el).classList.remove('de-current');
3669 el.setAttribute('onclick', 'window.de_addVideoEvents && window.de_addVideoEvents();');
3670}
3671
3672// HIDDEN THREADS WINDOW
3673function showHiddenWindow(body) {
3674 const hThr = HiddenThreads.getRawData();
3675 const hasThreads = !$isEmpty(hThr);
3676 if(hasThreads) {
3677 // Generate DOM for the list of hidden threads
3678 for(const b in hThr) {
3679 if($isEmpty(hThr[b])) {
3680 continue;
3681 }
3682 const block = $bEnd(body,
3683 `<div class="de-fold-block"><input type="checkbox"><b>/${ b }</b></div>`);
3684 block.firstChild.onclick =
3685 e => $each($Q('.de-entry > input', block), el => (el.checked = e.target.checked));
3686 for(const tNum in hThr[b]) {
3687 $bEnd(block, `<div class="de-entry ${ aib.cReply }" info="${ b };${ tNum }">
3688 <input type="checkbox">
3689 <a href="${ aib.getThrUrl(b, tNum) }" target="_blank">${ tNum }</a>
3690 <div class="de-entry-title">- ${ hThr[b][tNum][2] }</div>
3691 </div>`);
3692 }
3693 }
3694 }
3695 const btns = $bEnd(body, (!hasThreads ? `<center><b>${ Lng.noHidThr[lang] }</b></center>` : '') +
3696 '<div id="de-hid-buttons"></div>');
3697
3698 // "Edit" button. Calls a popup with editor to edit Hidden in JSON.
3699 btns.appendChild(getEditButton('hidden', fn => fn(HiddenThreads.getRawData(), true, data => {
3700 HiddenThreads.saveRawData(data);
3701 Thread.first.updateHidden(data[aib.b]);
3702 toggleWindow('hid', true);
3703 })));
3704
3705 // "Clear" button. Allows to clear 404'd threads.
3706 btns.appendChild($btn(Lng.clear[lang], Lng.clrDeleted[lang], async e => {
3707 // Sequentially load threads, and remove inaccessible
3708 const els = $Q('.de-entry[info]', e.target.parentNode.parentNode);
3709 for(let i = 0, len = els.length; i < len; ++i) {
3710 const [b, tNum] = els[i].getAttribute('info').split(';');
3711 await $ajax(aib.getThrUrl(b, tNum)).catch(err => {
3712 if(err.code === 404) {
3713 HiddenThreads.removeStorage(tNum, b);
3714 HiddenPosts.removeStorage(tNum, b);
3715 }
3716 });
3717 }
3718 toggleWindow('hid', true);
3719 }));
3720
3721 // "Delete" button. Allows to delete selected threads
3722 btns.appendChild($btn(Lng.remove[lang], Lng.delEntries[lang], () => {
3723 $each($Q('.de-entry[info]', body), el => {
3724 if(!$q('input', el).checked) {
3725 return;
3726 }
3727 const [brd, tNum] = el.getAttribute('info').split(';');
3728 const num = +tNum;
3729 if(pByNum.has(num)) {
3730 pByNum.get(num).setUserVisib(false);
3731 } else {
3732 sendStorageEvent('__de-post', { brd, num, hide: false, thrNum: num });
3733 }
3734 HiddenThreads.removeStorage(num, brd);
3735 HiddenPosts.set(num, num, false); // Actually unhide thread by its oppost
3736 });
3737 toggleWindow('hid', true);
3738 }));
3739}
3740
3741/* ==[ WindowFavorites.js ]===================================================================================
3742 WINDOW: FAVORITES
3743=========================================================================================================== */
3744
3745function saveRenewFavorites(favObj) {
3746 saveFavorites(favObj);
3747 toggleWindow('fav', true, favObj);
3748}
3749
3750function removeFavEntry(favObj, h, b, num) {
3751 let f;
3752 if((h in favObj) && (b in favObj[h]) && (num in (f = favObj[h][b]))) {
3753 delete f[num];
3754 if(!(Object.keys(f).length - +f.hasOwnProperty('url') - +f.hasOwnProperty('hide'))) {
3755 delete favObj[h][b];
3756 if($isEmpty(favObj[h])) {
3757 delete favObj[h];
3758 }
3759 }
3760 }
3761}
3762
3763function toggleThrFavBtn(h, b, num, isEnable) {
3764 if(h === aib.host && b === aib.b && pByNum.has(num)) {
3765 const post = pByNum.get(num);
3766 post.toggleFavBtn(isEnable);
3767 post.thr.isFav = isEnable;
3768 }
3769}
3770
3771function updateFavorites(num, value, mode) {
3772 readFavorites().then(favObj => {
3773 let isUpdate = false;
3774 let f = favObj[aib.host];
3775 if(!f || !f[aib.b] || !(f = f[aib.b][num])) {
3776 return;
3777 }
3778 switch(mode) {
3779 case 'error':
3780 if(f.err !== value) {
3781 isUpdate = true;
3782 }
3783 f.err = value;
3784 break;
3785 case 'update':
3786 if(f.cnt !== value[0]) {
3787 isUpdate = true;
3788 }
3789 f.cnt = value[0];
3790 f.new = f.you = 0;
3791 f.last = aib.anchor + value[1];
3792 }
3793 const data = [aib.host, aib.b, num, value, mode];
3794 if(isUpdate) {
3795 updateFavWindow(...data);
3796 saveFavorites(favObj);
3797 sendStorageEvent('__de-favorites', data);
3798 }
3799 });
3800}
3801
3802function updateFavWindow(h, b, num, value, mode) {
3803 if(mode === 'add' || mode === 'delete') {
3804 toggleThrFavBtn(h, b, num, mode === 'add');
3805 toggleWindow('fav', true, value);
3806 return;
3807 }
3808 const winEl = $q('#de-win-fav > .de-win-body');
3809 if(!winEl || !winEl.hasChildNodes()) {
3810 return;
3811 }
3812 const el = $q(`.de-entry[de-host="${ h }"][de-board="${ b }"][de-num="${ num }"] > .de-fav-inf`, winEl);
3813 if(!el) {
3814 return;
3815 }
3816 const [iconEl, youEl, newEl, oldEl] = [...el.children];
3817 $hide(youEl);
3818 $hide(newEl);
3819 if(mode === 'error') {
3820 iconEl.firstElementChild.setAttribute('class', 'de-fav-inf-icon de-fav-unavail');
3821 iconEl.title = value;
3822 return;
3823 }
3824 youEl.textContent = 0;
3825 newEl.textContent = 0;
3826 oldEl.textContent = value[0];
3827}
3828
3829// Delete previously marked entries from Favorites
3830function cleanFavorites() {
3831 const els = $Q('.de-entry[de-removed]');
3832 const len = els.length;
3833 if(!len) {
3834 return;
3835 }
3836 readFavorites().then(favObj => {
3837 for(let i = 0; i < len; ++i) {
3838 const el = els[i];
3839 const h = el.getAttribute('de-host');
3840 const b = el.getAttribute('de-board');
3841 const num = +el.getAttribute('de-num');
3842 removeFavEntry(favObj, h, b, num);
3843 toggleThrFavBtn(h, b, num, false);
3844 }
3845 saveRenewFavorites(favObj);
3846 });
3847}
3848
3849function showFavoritesWindow(body, favObj) {
3850 let html = '';
3851 // Create the list of favorite threads
3852 for(const h in favObj) {
3853 for(const b in favObj[h]) {
3854 const f = favObj[h][b];
3855 const hb = `de-host="${ h }" de-board="${ b }"`;
3856 const delBtn = `<span class="de-fav-del-btn">
3857 <svg><use xlink:href="#de-symbol-win-close"></use></svg>
3858 </span>`;
3859 let fArr, innerHtml = '';
3860 switch(Cfg.favThrOrder) {
3861 case 0: fArr = Object.entries(f); break;
3862 case 1: fArr = Object.entries(f).reverse(); break;
3863 case 2: fArr = Object.entries(f).sort((a, b) => (a[1].time || 0) - (b[1].time || 0)); break;
3864 case 3: fArr = Object.entries(f).sort((a, b) => (b[1].time || 0) - (a[1].time || 0));
3865 }
3866 for(let i = 0, len = fArr.length; i < len; ++i) {
3867 const tNum = fArr[i][0];
3868 if(tNum === 'url' || tNum === 'hide') {
3869 continue;
3870 }
3871 const t = f[tNum];
3872 if(!t.url.startsWith('http')) { // XXX: compatibility with older versions
3873 t.url = (h === aib.host ? aib.prot + '//' : 'http://') + h + t.url;
3874 }
3875 // Generate DOM for separate entry
3876 const favLinkHref = t.url + (
3877 !t.last ? '' :
3878 t.last.startsWith('#') ? t.last :
3879 h === aib.host ? aib.anchor + t.last : '');
3880 const favInfIwrapTitle = !t.err ? '' :
3881 t.err === 'Closed' ? `title="${ Lng.thrClosed[lang] }"` : `title="${ t.err }"`;
3882 const favInfIconClass = !t.err ? '' :
3883 t.err === 'Closed' || t.err === 'Archived' ? 'de-fav-closed' : 'de-fav-unavail';
3884 const favInfYouDisp = t.you ? '' : ' style="display: none;"';
3885 const favInfNewDisp = t.new ? '' : ' style="display: none;"';
3886 innerHtml += `<div class="de-entry ${ aib.cReply }" ${
3887 hb } de-num="${ tNum }" de-url="${ t.url }">
3888 ${ delBtn }
3889 <a class="de-fav-link" title="${ Lng.goToThread[lang] }"` +
3890 ` href="${ favLinkHref }" rel="noreferrer">${ tNum }</a>
3891 <div class="de-entry-title">- ${ t.txt }</div>
3892 <div class="de-fav-inf">
3893 <span class="de-fav-inf-iwrap" ${ favInfIwrapTitle }>
3894 <svg class="de-fav-inf-icon ${ favInfIconClass }">
3895 <use class="de-fav-closed-use" xlink:href="#de-symbol-closed"/>
3896 <use class="de-fav-unavail-use" xlink:href="#de-symbol-unavail"/>
3897 <use class="de-fav-wait-use" xlink:href="#de-symbol-wait"/>
3898 </svg>
3899 </span>
3900 <span class="de-fav-inf-you" title="${ Lng.myPostsRep[lang] }"${ favInfYouDisp }>
3901 ${ t.you || 0 }</span>
3902 <span class="de-fav-inf-new" title="${ Lng.newPosts[lang] }"${ favInfNewDisp }>
3903 ${ t.new || 0 }</span>
3904 <span class="de-fav-inf-old" title="${ Lng.oldPosts[lang] }">${ t.cnt }</span>
3905 <span class="de-fav-inf-page" title="${ Lng.thrPage[lang] }"></span>
3906 </div>
3907 </div>`;
3908 }
3909 if(!innerHtml) {
3910 continue;
3911 }
3912 const isHide = f.hide === undefined ? h !== aib.host : f.hide;
3913 // Building a foldable block for specific board
3914 html += `<div class="de-fold-block${ h === aib.host && b === aib.b ? ' de-fav-current' : '' }">
3915 <div class="de-fav-header">
3916 ${ delBtn }
3917 <a class="de-fav-header-link" title="${ Lng.goToBoard[lang] }"` +
3918 ` href="${ f.url }" rel="noreferrer">${ h }/${ b }</a>
3919 <a class="de-abtn de-fav-header-btn" title="${ Lng.toggleEntries[lang] }"` +
3920 ` href="#">${ isHide ? '▼' : '▲' }</a>
3921 </div>
3922 <div class="de-fav-entries${ isHide ? ' de-fav-entries-hide' : '' }" ${ hb }>
3923 ${ innerHtml }
3924 </div>
3925 </div>`;
3926 }
3927 }
3928
3929 // Appending DOM and events
3930 if(html) {
3931 $bEnd(body, `<div class="de-fav-table">${ html }</div>`).addEventListener('click', e => {
3932 let el = fixEventEl(e.target);
3933 let parentEl = el.parentNode;
3934 if(el.tagName.toLowerCase() === 'svg') {
3935 el = parentEl;
3936 parentEl = parentEl.parentNode;
3937 }
3938 switch(el.className) {
3939 case 'de-fav-link':
3940 sesStorage['de-fav-win'] = '1'; // Favorites will open again after following a link
3941 // We need to scroll to last seen post after following a link,
3942 // remembering of scroll position is no longer needed
3943 sesStorage.removeItem('de-scroll-' +
3944 parentEl.getAttribute('de-board') + (parentEl.getAttribute('de-num') || ''));
3945 break;
3946 case 'de-fav-del-btn': {
3947 const wasChecked = el.getAttribute('de-checked') === '';
3948 const toggleFn = btnEl => toggleAttr(btnEl, 'de-checked', '', !wasChecked);
3949 toggleFn(el);
3950 if(parentEl.className === 'de-fav-header') {
3951 // Select/unselect all checkboxes in board block
3952 const entriesEl = parentEl.nextElementSibling;
3953 $each($Q('.de-fav-del-btn', entriesEl), toggleFn);
3954 if(!wasChecked && entriesEl.classList.contains('de-fav-entries-hide')) {
3955 entriesEl.classList.remove('de-fav-entries-hide');
3956 }
3957 }
3958 const isShowDelBtns = !!$q('.de-entry > .de-fav-del-btn[de-checked]', body);
3959 $toggle($id('de-fav-buttons'), !isShowDelBtns);
3960 $toggle($id('de-fav-del-confirm'), isShowDelBtns);
3961 break;
3962 }
3963 case 'de-abtn de-fav-header-btn': {
3964 const entriesEl = parentEl.nextElementSibling;
3965 const isHide = !entriesEl.classList.contains('de-fav-entries-hide');
3966 el.innerHTML = isHide ? '▼' : '▲';
3967 favObj[entriesEl.getAttribute('de-host')][entriesEl.getAttribute('de-board')].hide = isHide;
3968 saveFavorites(favObj);
3969 $pd(e);
3970 entriesEl.classList.toggle('de-fav-entries-hide');
3971 }
3972 }
3973 });
3974 } else {
3975 $bEnd(body, `<center><b>${ Lng.noFavThr[lang] }</b></center>`);
3976 }
3977 const btns = $bEnd(body, '<div id="de-fav-buttons"></div>');
3978
3979 // "Edit" button. Calls a popup with editor to edit Favorites in JSON.
3980 btns.appendChild(getEditButton('favor',
3981 fn => readFavorites().then(favObj => fn(favObj, true, saveRenewFavorites))));
3982
3983 // "Refresh" button. Updates counters of new posts for each thread entry.
3984 btns.appendChild($btn(Lng.refresh[lang], Lng.infoCount[lang], async () => {
3985 const favObj = await readFavorites();
3986 if(!favObj[aib.host]) {
3987 return;
3988 }
3989 let isUpdate = false;
3990 let last404 = false;
3991 const myposts = JSON.parse(locStorage['de-myposts'] || '{}');
3992 const els = $Q('.de-entry');
3993 for(let i = 0, len = els.length; i < len; ++i) {
3994 const el = els[i];
3995 const host = el.getAttribute('de-host');
3996 const b = el.getAttribute('de-board');
3997 const num = el.getAttribute('de-num');
3998 const f = favObj[host][b][num];
3999 // Updating doesn't works for other domains because of different posts structure
4000 // Updating is not needed in closed threads
4001 if(host !== aib.host || f.err === 'Closed' || f.err === 'Archived') {
4002 continue;
4003 }
4004 const [titleEl, youEl, countEl] = [...el.lastElementChild.children];
4005 const iconEl = titleEl.firstElementChild;
4006 // setAttribute for class is used because of SVG (for correct work in some browsers)
4007 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-wait');
4008 titleEl.title = Lng.updating[lang];
4009 let form, isArchived;
4010 try {
4011 if(!aib.iichan) {
4012 form = await ajaxLoad(aib.getThrUrl(b, num));
4013 } else {
4014 [form, isArchived] = await ajaxLoad(aib.getThrUrl(b, num), true, false, true);
4015 }
4016 last404 = false;
4017 } catch(err) {
4018 if((err instanceof AjaxError) && err.code === 404) { // Check for 404 error twice
4019 if(last404) {
4020 Thread.removeSavedData(b, num); // Doesn't work. Not done now.
4021 } else {
4022 last404 = true;
4023 --i; // Repeat this cycle again
4024 continue;
4025 }
4026 }
4027 last404 = false;
4028 $hide(countEl);
4029 $hide(youEl);
4030 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-unavail');
4031 f.err = titleEl.title = getErrorMessage(err);
4032 isUpdate = true;
4033 continue;
4034 }
4035 if(aib.qClosed && $q(aib.qClosed, form)) { // Check for closed thread
4036 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-closed');
4037 titleEl.title = Lng.thrClosed[lang];
4038 f.err = 'Closed';
4039 isUpdate = true;
4040 } else if(isArchived) { // Moves archived threads into b/arch (iichan only)
4041 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-closed');
4042 titleEl.title = Lng.thrArchived[lang];
4043 f.err = 'Archived';
4044 const arch = b + '/arch';
4045 const fo = favObj[host];
4046 (fo[arch] || (fo[arch] = { url: favObj[host][b].url + 'arch/' }))[num] = Object.assign({}, f);
4047 removeFavEntry(favObj, host, b, num);
4048 isUpdate = true;
4049 } else {
4050 // Thread is available and not closed
4051 iconEl.setAttribute('class', 'de-fav-inf-icon');
4052 titleEl.removeAttribute('title');
4053 if(f.err) { // Cancel error status if existed
4054 delete f.err;
4055 isUpdate = true;
4056 }
4057 }
4058 // Updating a counter of new posts
4059 const posts = $Q(aib.qRPost, form);
4060 const cnt = posts.length + 1 - f.cnt;
4061 countEl.textContent = cnt;
4062 if(cnt === 0) {
4063 $hide(countEl); // Hide counter if no new posts
4064 $hide(youEl);
4065 } else {
4066 $show(countEl);
4067 f.new = cnt;
4068 isUpdate = true;
4069 // Check for replies to my posts
4070 if(myposts && myposts[b]) {
4071 f.you = 0;
4072 for(let j = 0; j < cnt; ++j) {
4073 const links = $Q(aib.qPostMsg.split(', ').join(' a, ') + ' a',
4074 posts[posts.length - 1 - j]);
4075 for(let a = 0, len = links.length; a < len; ++a) {
4076 const tc = links[a].textContent;
4077 if(tc[0] === '>' && tc[1] === '>' && myposts[b][tc.substr(2)]) {
4078 f.you++;
4079 }
4080 }
4081 }
4082 if(f.you) {
4083 youEl.textContent = f.you;
4084 $show(youEl);
4085 }
4086 }
4087 }
4088 }
4089 AjaxCache.clearCache();
4090 if(isUpdate) {
4091 saveFavorites(favObj);
4092 }
4093 }));
4094
4095 // "Page" button. Shows on which page every thread is existed.
4096 btns.appendChild($btn(Lng.page[lang], Lng.infoPage[lang], async () => {
4097 const els = $Q('.de-fav-current > .de-fav-entries > .de-entry');
4098 const len = els.length;
4099 if(!len) { // Cancel if no existed entries
4100 return;
4101 }
4102 $popup('load-pages', Lng.loading[lang], true);
4103 // Create indexed array of entries and "waiting" SVG icon for each entry
4104 const thrInfo = [];
4105 for(let i = 0; i < len; ++i) {
4106 const el = els[i];
4107 const iconEl = $q('.de-fav-inf-icon', el);
4108 const titleEl = iconEl.parentNode;
4109 thrInfo.push({
4110 found : false,
4111 num : +el.getAttribute('de-num'),
4112 pageEl : $q('.de-fav-inf-page', el),
4113 iconClass : iconEl.getAttribute('class'),
4114 iconEl,
4115 iconTitle : titleEl.getAttribute('title'),
4116 titleEl
4117 });
4118 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-wait');
4119 titleEl.title = Lng.updating[lang];
4120 }
4121 // Sequentially load pages and search for favorites threads
4122 // We cannot know a count of pages while in the thread
4123 const endPage = (aib.lastPage || 10) + 1; // Check up to 10 page, if we don't know
4124 let infoLoaded = 0;
4125 const updateInf = (inf, page) => {
4126 inf.iconEl.setAttribute('class', inf.iconClass);
4127 toggleAttr(inf.titleEl, 'title', inf.iconTitle, inf.iconTitle);
4128 inf.pageEl.textContent = '@' + page;
4129 };
4130 for(let page = 0; page < endPage; ++page) {
4131 const tNums = new Set();
4132 try {
4133 const form = await ajaxLoad(aib.getPageUrl(aib.b, page));
4134 const els = DelForm.getThreads(form);
4135 for(let i = 0, len = els.length; i < len; ++i) {
4136 tNums.add(aib.getTNum(els[i]));
4137 }
4138 } catch(err) {
4139 continue;
4140 }
4141 // Search for threads on current page
4142 for(let i = 0; i < len; ++i) {
4143 const inf = thrInfo[i];
4144 if(tNums.has(inf.num)) {
4145 updateInf(inf, page);
4146 inf.found = true;
4147 infoLoaded++;
4148 }
4149 }
4150 if(infoLoaded === len) { // Stop pages loading when all favorite threads checked
4151 break;
4152 }
4153 }
4154 // Process missed threads that not found
4155 for(let i = 0; i < len; ++i) {
4156 const inf = thrInfo[i];
4157 if(!inf.found) {
4158 updateInf(inf, '?');
4159 }
4160 }
4161 closePopup('load-pages');
4162 }));
4163
4164 // "Clear" button. Allows to clear 404'd threads.
4165 btns.appendChild($btn(Lng.clear[lang], Lng.clrDeleted[lang], async () => {
4166 // Sequentially load threads, and remove inaccessible
4167 let last404 = false;
4168 const els = $Q('.de-entry');
4169 const parent = $q('.de-fav-table');
4170 parent.classList.add('de-fav-table-unfold');
4171 for(let i = 0, len = els.length; i < len; ++i) {
4172 const el = els[i];
4173 const iconEl = $q('.de-fav-inf-icon', el);
4174 const titleEl = iconEl.parentNode;
4175 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-wait');
4176 titleEl.title = Lng.updating[lang];
4177 await $ajax(el.getAttribute('de-url'), null, true).then(() => {
4178 iconEl.setAttribute('class', 'de-fav-inf-icon');
4179 titleEl.removeAttribute('title');
4180 last404 = false;
4181 }).catch(err => {
4182 if(err.code === 404) { // Check for 404 error twice
4183 if(!last404) {
4184 last404 = true;
4185 --i; // Repeat this cycle again
4186 return;
4187 }
4188 Thread.removeSavedData(el.getAttribute('de-board'), // Doesn't work. Not done now.
4189 +el.getAttribute('de-num'));
4190 el.setAttribute('de-removed', ''); // Mark an entry as deleted
4191 }
4192 iconEl.setAttribute('class', 'de-fav-inf-icon de-fav-unavail');
4193 titleEl.title = getErrorMessage(err);
4194 last404 = false;
4195 });
4196 }
4197 cleanFavorites(); // Delete marked entries
4198 parent.classList.remove('de-fav-table-unfold');
4199 }));
4200
4201 // Deletion confirm/cancel buttons
4202 const delBtns = $bEnd(body, '<div id="de-fav-del-confirm" style="display: none;"></div>');
4203 delBtns.appendChild($btn(Lng.remove[lang], Lng.delEntries[lang], () => {
4204 $each($Q('.de-entry > .de-fav-del-btn[de-checked]', body),
4205 el => el.parentNode.setAttribute('de-removed', ''));
4206 cleanFavorites(); // Delete marked entries
4207 $show(btns);
4208 $hide(delBtns);
4209 }));
4210 delBtns.appendChild($btn(Lng.cancel[lang], '', () => {
4211 $each($Q('.de-fav-del-btn', body), el => el.removeAttribute('de-checked'));
4212 $show(btns);
4213 $hide(delBtns);
4214 }));
4215}
4216
4217/* ==[ WindowSettings.js ]====================================================================================
4218 WINDOW: SETTINGS
4219=========================================================================================================== */
4220
4221const CfgWindow = {
4222 initCfgWindow(body) {
4223 body.addEventListener('click', this);
4224 body.addEventListener('mouseover', this);
4225 body.addEventListener('mouseout', this);
4226 body.addEventListener('change', this);
4227 body.addEventListener('keyup', this);
4228 body.addEventListener('keydown', this);
4229 body.addEventListener('scroll', this);
4230
4231 // Create tab bar and bottom buttons
4232 let div = $bEnd(body, `<div id="de-cfg-bar">${
4233 this._getTab('filters') +
4234 this._getTab('posts') +
4235 this._getTab('images') +
4236 this._getTab('links') +
4237 (pr.form || pr.oeForm ? this._getTab('form') : '') +
4238 this._getTab('common') +
4239 this._getTab('info')
4240 }</div><div id="de-cfg-buttons">${ this._getSel('language') }</div>`);
4241
4242 // Open default or current tab
4243 this._clickTab(Cfg.cfgTab);
4244
4245 // "Edit" button. Calls a popup with editor to edit Settings in JSON.
4246 div.appendChild(getEditButton('cfg', fn => fn(Cfg, true, data => {
4247 saveCfgObj(aib.dm, data);
4248 deWindow.location.reload();
4249 })));
4250
4251 // "Global" button. Allows to save/load global settings.
4252 nav.hasGlobalStorage && div.appendChild($btn(Lng.global[lang], Lng.globalCfg[lang], () => {
4253 const el = $popup('cfg-global', `<b>${ Lng.globalCfg[lang] }:</b>`);
4254 // "Load" button. Applies global settings for current domain.
4255 $bEnd(el, `<div id="de-list"><input type="button" value="${
4256 Lng.load[lang] }"> ${ Lng.loadGlobal[lang] }</div>`
4257 ).firstElementChild.onclick = () => getStoredObj('DESU_Config').then(data => {
4258 if(data && ('global' in data) && !$isEmpty(data.global)) {
4259 saveCfgObj(aib.dm, data.global);
4260 deWindow.location.reload();
4261 } else {
4262 $popup('err-noglobalcfg', Lng.noGlobalCfg[lang]);
4263 }
4264 });
4265 // "Save" button. Copies the domain settings into global.
4266 div = $bEnd(el, `<div id="de-list"><input type="button" value="${
4267 Lng.save[lang] }"> ${ Lng.saveGlobal[lang] }</div>`
4268 ).firstElementChild.onclick = () => getStoredObj('DESU_Config').then(data => {
4269 const obj = {};
4270 const com = data[aib.dm];
4271 for(const i in com) {
4272 if(i !== 'correctTime' && i !== 'timePattern' && i !== 'userCSS' &&
4273 i !== 'userCSSTxt' && i !== 'stats' && com[i] !== defaultCfg[i]
4274 ) {
4275 obj[i] = com[i];
4276 }
4277 }
4278 data.global = obj;
4279 saveCfgObj('global', data.global);
4280 toggleWindow('cfg', true);
4281 });
4282 el.insertAdjacentHTML('beforeend', `<hr><small>${ Lng.descrGlobal[lang] }</small>`);
4283 }));
4284
4285 // "File" button. Allows to save and load settings/favorites/hidden/etc from file.
4286 !nav.isPresto && div.appendChild($btn(Lng.file[lang], Lng.fileImpExp[lang], () => {
4287 const list = this._getList([
4288 Lng.panelBtn.cfg[lang] + ' ' + Lng.allDomains[lang],
4289 Lng.panelBtn.fav[lang],
4290 Lng.hidPostThr[lang] + ` (${ aib.dm })`,
4291 Lng.myPosts[lang] + ` (${ aib.dm })`
4292 ]);
4293 // Create popup with controls
4294 $popup('cfg-file', `<b>${ Lng.fileImpExp[lang] }:</b><hr><!--
4295 --><div class="de-list">${ Lng.fileToData[lang] }:<div class="de-depend"><!--
4296 --><input type="file" accept=".json" id="de-import-file"></div></div><hr><!--
4297 --><div class="de-list"><a id="de-export-file" href="#">${ Lng.dataToFile[lang] }:<!--
4298 --><div class="de-depend">${ list }</div></div>`);
4299 // Import data from a file to the storage
4300 $id('de-import-file').onchange = e => {
4301 const file = e.target.files[0];
4302 if(!file) {
4303 return;
4304 }
4305 readFile(file, true).then(({ data }) => {
4306 let obj;
4307 try {
4308 obj = JSON.parse(data);
4309 } catch(err) {
4310 $popup('err-invaliddata', Lng.invalidData[lang]);
4311 return;
4312 }
4313 const cfgObj = obj.settings;
4314 const favObj = obj.favorites;
4315 const dmObj = obj[aib.dm];
4316 const isOldCfg = !cfgObj && !favObj && !dmObj;
4317 if(isOldCfg) {
4318 setStored('DESU_Config', data);
4319 }
4320 if(cfgObj) {
4321 try {
4322 setStored('DESU_Config', JSON.stringify(cfgObj));
4323 setStored('DESU_keys', JSON.stringify(obj.hotkeys));
4324 } catch(err) {}
4325 }
4326 if(favObj) {
4327 saveRenewFavorites(favObj);
4328 }
4329 if(dmObj) {
4330 if(dmObj.posts) {
4331 locStorage['de-posts'] = JSON.stringify(dmObj.posts);
4332 }
4333 if(dmObj.threads) {
4334 locStorage['de-threads'] = JSON.stringify(dmObj.threads);
4335 }
4336 if(dmObj.myposts) {
4337 locStorage['de-myposts'] = JSON.stringify(dmObj.myposts);
4338 }
4339 }
4340 if(cfgObj || dmObj || isOldCfg) {
4341 $popup('cfg-file', Lng.updating[lang], true);
4342 deWindow.location.reload();
4343 return;
4344 }
4345 closePopup('cfg-file');
4346 });
4347 };
4348
4349 // Export data from a storage to the file. The file will be named by date and type of storage.
4350 // For example, like "DE_20160727_1540_Cfg+Fav+domain.com(Hid+You).json".
4351 const expFile = $id('de-export-file');
4352 const els = $Q('input', expFile.nextElementSibling);
4353 els[0].checked = true;
4354 expFile.addEventListener('click', async e => {
4355 const name = [], nameDm = [], d = new Date();
4356 let val = [], valDm = [];
4357 for(let i = 0, len = els.length; i < len; ++i) {
4358 if(!els[i].checked) {
4359 continue;
4360 }
4361 switch(i) {
4362 case 0: name.push('Cfg'); {
4363 const cfgData = await Promise.all([getStored('DESU_Config'), getStored('DESU_keys')]);
4364 val.push(`"settings":${ cfgData[0] }`, `"hotkeys":${ cfgData[1] || '""' }`);
4365 break;
4366 }
4367 case 1: name.push('Fav');
4368 val.push(`"favorites":${ await getStored('DESU_Favorites') || '{}' }`);
4369 break;
4370 case 2: nameDm.push('Hid');
4371 valDm.push(`"posts":${ locStorage['de-posts'] || '{}' }`,
4372 `"threads":${ locStorage['de-threads'] || '{}' }`);
4373 break;
4374 case 3: nameDm.push('You');
4375 valDm.push(`"myposts":${ locStorage['de-myposts'] || '{}' }`);
4376 }
4377 }
4378 if((valDm = valDm.join(','))) {
4379 val.push(`"${ aib.dm }":{${ valDm }}`);
4380 name.push(`${ aib.dm } (${ nameDm.join('+') })`);
4381 }
4382 if((val = val.join(','))) {
4383 downloadBlob(new Blob([`{${ val }}`], { type: 'application/json' }),
4384 `DE_${ d.getFullYear() }${ pad2(d.getMonth() + 1) }${ pad2(d.getDate()) }_${
4385 pad2(d.getHours()) }${ pad2(d.getMinutes()) }_${ name.join('+') }.json`);
4386 }
4387 $pd(e);
4388 }, true);
4389 }));
4390
4391 // "Clear" button. Allows to clear settings/favorites/hidden/etc optionally.
4392 div.appendChild($btn(Lng.reset[lang] + '…', Lng.resetCfg[lang], () => $popup(
4393 'cfg-reset',
4394 `<b>${ Lng.resetData[lang] }:</b><hr>` +
4395 `<div class="de-list"><b>${ aib.dm }:</b>${
4396 this._getList([Lng.panelBtn.cfg[lang], Lng.hidPostThr[lang], Lng.myPosts[lang]])
4397 }</div><hr>` +
4398 `<div class="de-list"><b>${ Lng.allDomains[lang] }:</b>${
4399 this._getList([Lng.panelBtn.cfg[lang], Lng.panelBtn.fav[lang]])
4400 }</div><hr>`
4401 ).appendChild($btn(Lng.clear[lang], '', e => {
4402 const els = $Q('input[type="checkbox"]', e.target.parentNode);
4403 for(let i = 1, len = els.length; i < len; ++i) {
4404 if(!els[i].checked) {
4405 continue;
4406 }
4407 switch(i) {
4408 case 1:
4409 locStorage.removeItem('de-posts');
4410 locStorage.removeItem('de-threads');
4411 break;
4412 case 2: locStorage.removeItem('de-myposts'); break;
4413 case 4: delStored('DESU_Favorites');
4414 }
4415 }
4416 if(els[3].checked) {
4417 delStored('DESU_Config');
4418 delStored('DESU_keys');
4419 } else if(els[0].checked) {
4420 getStoredObj('DESU_Config').then(data => {
4421 delete data[aib.dm];
4422 setStored('DESU_Config', JSON.stringify(data));
4423 $popup('cfg-reset', Lng.updating[lang], true);
4424 deWindow.location.reload();
4425 });
4426 return;
4427 }
4428 $popup('cfg-reset', Lng.updating[lang], true);
4429 deWindow.location.reload();
4430 }))));
4431 },
4432
4433 // Event handler for Setting window and its controls.
4434 handleEvent(e) {
4435 const { type, target: el } = e;
4436 const tag = el.tagName;
4437 if(type === 'click' && tag === 'DIV' && el.classList.contains('de-cfg-tab')) {
4438 const info = el.getAttribute('info');
4439 this._clickTab(info);
4440 saveCfg('cfgTab', info);
4441 }
4442 if(type === 'change' && tag === 'SELECT') {
4443 const info = el.getAttribute('info');
4444 saveCfg(info, el.selectedIndex);
4445 this._updateDependant();
4446 switch(info) {
4447 case 'language':
4448 lang = el.selectedIndex;
4449 Panel.removeMain();
4450 if(pr.form) {
4451 pr.addMarkupPanel();
4452 pr.setPlaceholders();
4453 pr.updateLanguage();
4454 aib.updateSubmitBtn(pr.subm);
4455 if(pr.files) {
4456 $each($Q('.de-file-img, .de-file-txt-input', pr.form),
4457 el => (el.title = Lng.youCanDrag[lang]));
4458 }
4459 }
4460 this._updateCSS();
4461 Panel.initPanel(DelForm.first.el);
4462 toggleWindow('cfg', false);
4463 break;
4464 case 'delHiddPost': {
4465 const isHide = Cfg.delHiddPost === 1 || Cfg.delHiddPost === 2;
4466 for(let post = Thread.first.op; post; post = post.next) {
4467 if(post.isHidden && !post.isOp) {
4468 post.wrap.classList.toggle('de-hidden', isHide);
4469 }
4470 }
4471 updateCSS();
4472 break;
4473 }
4474 case 'postBtnsCSS':
4475 updateCSS();
4476 if(nav.isPresto) {
4477 $q('.de-svg-icons').remove();
4478 addSVGIcons();
4479 }
4480 break;
4481 case 'thrBtns':
4482 case 'noSpoilers':
4483 case 'resizeImgs': updateCSS(); break;
4484 case 'expandImgs':
4485 updateCSS();
4486 AttachedImage.closeImg();
4487 break;
4488 case 'imgNames':
4489 if(Cfg.imgNames) {
4490 for(const { el } of DelForm) {
4491 processImgInfoLinks(el, 0, Cfg.imgNames);
4492 }
4493 } else {
4494 $each($Q('.de-img-name'), el => (el.textContent = el.getAttribute('de-img-name-old')));
4495 }
4496 updateCSS();
4497 break;
4498 case 'fileInputs':
4499 pr.files.changeMode();
4500 pr.setPlaceholders();
4501 updateCSS();
4502 break;
4503 case 'addPostForm':
4504 pr.isBottom = Cfg.addPostForm === 1;
4505 pr.setReply(false, !aib.t || Cfg.addPostForm > 1);
4506 break;
4507 case 'addTextBtns': pr.addMarkupPanel();
4508 /* falls through */
4509 case 'scriptStyle':
4510 case 'panelCounter': this._updateCSS(); break;
4511 case 'favThrOrder':
4512 readFavorites().then(favObj => {
4513 const body = $q('#de-win-fav > .de-win-body');
4514 body.innerHTML = '';
4515 showFavoritesWindow(body, favObj);
4516 });
4517 }
4518 return;
4519 }
4520 if(type === 'click' && tag === 'INPUT' && el.type === 'checkbox') {
4521 const info = el.getAttribute('info');
4522 toggleCfg(info);
4523 this._updateDependant();
4524 switch(info) {
4525 case 'expandTrunc':
4526 case 'showHideBtn':
4527 case 'showRepBtn':
4528 case 'widePosts':
4529 case 'noPostNames':
4530 case 'imgNavBtns':
4531 case 'strikeHidd':
4532 case 'removeHidd':
4533 case 'noBoardRule':
4534 case 'userCSS': updateCSS(); break;
4535 case 'hideBySpell': Spells.toggle(); break;
4536 case 'sortSpells':
4537 if(Cfg.sortSpells) {
4538 Spells.toggle();
4539 }
4540 break;
4541 case 'hideRefPsts':
4542 for(let post = Thread.first.op; post; post = post.next) {
4543 if(!Cfg.hideRefPsts) {
4544 post.ref.unhideRef();
4545 } else if(post.isHidden) {
4546 post.ref.hideRef();
4547 }
4548 }
4549 break;
4550 case 'ajaxUpdThr':
4551 if(aib.t) {
4552 if(Cfg.ajaxUpdThr) {
4553 updater.enableUpdater();
4554 } else {
4555 updater.disableUpdater();
4556 }
4557 }
4558 break;
4559 case 'updCount': updater.toggleCounter(Cfg.updCount); break;
4560 case 'desktNotif':
4561 if(Cfg.desktNotif) {
4562 Notification.requestPermission();
4563 }
4564 break;
4565 case 'markNewPosts': Post.clearMarks(); break;
4566 case 'useDobrAPI': aib.JsonBuilder = Cfg.useDobrAPI ? DobrochanPostsBuilder : null; break;
4567 case 'markMyPosts':
4568 case 'markMyLinks':
4569 if(!Cfg.markMyPosts && !Cfg.markMyLinks) {
4570 locStorage.removeItem('de-myposts');
4571 MyPosts.purge();
4572 }
4573 updateCSS();
4574 break;
4575 case 'correctTime': DateTime.toggleSettings(el); break;
4576 case 'imgInfoLink': {
4577 const img = $q('.de-fullimg-wrap');
4578 if(img) {
4579 img.click();
4580 }
4581 updateCSS();
4582 break;
4583 }
4584 case 'imgSrcBtns':
4585 if(Cfg.imgSrcBtns) {
4586 for(const { el } of DelForm) {
4587 processImgInfoLinks(el, 1, 0);
4588 $each($Q('.de-img-embed'),
4589 el => addImgSrcButtons(el.parentNode.nextSibling.nextSibling));
4590 }
4591 } else {
4592 $delAll('.de-btn-src');
4593 }
4594 break;
4595 case 'addSageBtn':
4596 PostForm.hideField($parent(pr.mail, 'LABEL') || pr.mail);
4597 setTimeout(() => pr.toggleSage(), 0);
4598 updateCSS();
4599 break;
4600 case 'altCaptcha': pr.cap.initCapPromise(); break;
4601 case 'txtBtnsLoc':
4602 pr.addMarkupPanel();
4603 updateCSS();
4604 break;
4605 case 'userPassw': PostForm.setUserPassw(); break;
4606 case 'userName': PostForm.setUserName(); break;
4607 case 'noPassword': $toggle($qParent(pr.passw, aib.qFormTr)); break;
4608 case 'noName': PostForm.hideField(pr.name); break;
4609 case 'noSubj': PostForm.hideField(pr.subj); break;
4610 case 'inftyScroll': toggleInfinityScroll(); break;
4611 case 'hotKeys':
4612 if(Cfg.hotKeys) {
4613 HotKeys.enableHotKeys();
4614 } else {
4615 HotKeys.disableHotKeys();
4616 }
4617 }
4618 return;
4619 }
4620 if(type === 'click' && tag === 'INPUT' && el.type === 'button') {
4621 switch(el.id) {
4622 case 'de-cfg-button-pass':
4623 $q('input[info="passwValue"]').value = Math.round(Math.random() * 1e12).toString(32);
4624 PostForm.setUserPassw();
4625 break;
4626 case 'de-cfg-button-keys':
4627 $pd(e);
4628 if($id('de-popup-edit-hotkeys')) {
4629 return;
4630 }
4631 Promise.resolve(HotKeys.readKeys()).then(keys => {
4632 const temp = KeyEditListener.getEditMarkup(keys);
4633 const el = $popup('edit-hotkeys', temp[1]);
4634 const fn = new KeyEditListener(el, keys, temp[0]);
4635 el.addEventListener('focus', fn, true);
4636 el.addEventListener('blur', fn, true);
4637 el.addEventListener('click', fn, true);
4638 el.addEventListener('keydown', fn, true);
4639 el.addEventListener('keyup', fn, true);
4640 });
4641 break;
4642 case 'de-cfg-button-updnow':
4643 $popup('updavail', Lng.loading[lang], true);
4644 getStoredObj('DESU_Config')
4645 .then(data => checkForUpdates(true, data.lastUpd))
4646 .then(html => $popup('updavail', html), emptyFn);
4647 break;
4648 case 'de-cfg-button-debug': {
4649 const perf = {};
4650 const arr = Logger.getLogData(true);
4651 for(let i = 0, len = arr.length; i < len; ++i) {
4652 perf[arr[i][0]] = arr[i][1];
4653 }
4654 $popup('cfg-debug', Lng.infoDebug[lang] + ':<textarea readonly class="de-editor"></textarea>'
4655 ).firstElementChild.value = JSON.stringify({
4656 version : version + '.' + commit,
4657 location : String(deWindow.location),
4658 nav,
4659 Cfg,
4660 sSpells : Spells.list.split('\n'),
4661 oSpells : sesStorage[`de-spells-${ aib.b }${ aib.t || '' }`],
4662 perf
4663 }, (key, value) => {
4664 switch(key) {
4665 case 'stats':
4666 case 'nameValue':
4667 case 'passwValue':
4668 case 'ytApiKey': return void 0;
4669 }
4670 return key in defaultCfg && value === defaultCfg[key] ? void 0 : value;
4671 }, '\t');
4672 }
4673 }
4674 }
4675 if(type === 'keyup' && tag === 'INPUT' && el.type === 'text') {
4676 const info = el.getAttribute('info');
4677 switch(info) {
4678 case 'postBtnsBack': {
4679 const isCheck = checkCSSColor(el.value);
4680 el.classList.toggle('de-input-error', !isCheck);
4681 if(isCheck) {
4682 saveCfg('postBtnsBack', el.value);
4683 updateCSS();
4684 }
4685 break;
4686 }
4687 case 'limitPostMsg':
4688 saveCfg('limitPostMsg', Math.max(+el.value || 0, 50));
4689 updateCSS();
4690 break;
4691 case 'minImgSize': saveCfg('minImgSize', Math.max(+el.value, 1)); break;
4692 case 'zoomFactor': saveCfg('zoomFactor', Math.min(Math.max(+el.value, 1), 100)); break;
4693 case 'webmVolume': {
4694 const val = Math.min(+el.value || 0, 100);
4695 saveCfg('webmVolume', val);
4696 sendStorageEvent('__de-webmvolume', val);
4697 break;
4698 }
4699 case 'minWebmWidth': saveCfg('minWebmWidth', Math.max(+el.value, Cfg.minImgSize)); break;
4700 case 'maskVisib':
4701 saveCfg('maskVisib', Math.min(+el.value || 0, 100));
4702 updateCSS();
4703 break;
4704 case 'linksOver': saveCfg('linksOver', +el.value | 0); break;
4705 case 'linksOut': saveCfg('linksOut', +el.value | 0); break;
4706 case 'ytApiKey': saveCfg('ytApiKey', el.value.trim()); break;
4707 case 'passwValue': PostForm.setUserPassw(); break;
4708 case 'nameValue': PostForm.setUserName(); break;
4709 default: saveCfg(info, el.value);
4710 }
4711 return;
4712 }
4713 if(tag === 'A') {
4714 if(el.id === 'de-btn-spell-add') {
4715 switch(e.type) {
4716 case 'click': $pd(e); break;
4717 case 'mouseover': el.odelay = setTimeout(() => addMenu(el), Cfg.linksOver); break;
4718 case 'mouseout': clearTimeout(el.odelay);
4719 }
4720 return;
4721 }
4722 if(type === 'click') {
4723 switch(el.id) {
4724 case 'de-btn-spell-apply':
4725 $pd(e);
4726 saveCfg('hideBySpell', 1);
4727 $q('input[info="hideBySpell"]').checked = true;
4728 Spells.toggle();
4729 break;
4730 case 'de-btn-spell-clear':
4731 $pd(e);
4732 if(!confirm(Lng.clear[lang] + '?')) {
4733 return;
4734 }
4735 $id('de-spell-txt').value = '';
4736 Spells.toggle();
4737 }
4738 }
4739 return;
4740 }
4741 if(tag === 'TEXTAREA' && el.id === 'de-spell-txt' && (type === 'keydown' || type === 'scroll')) {
4742 this._updateRowMeter(el);
4743 }
4744 },
4745
4746 // Switch content in Settings by clicking on tab
4747 _clickTab(info) {
4748 const el = $q(`.de-cfg-tab[info="${ info }"]`);
4749 if(el.hasAttribute('selected')) {
4750 return;
4751 }
4752 const prefTab = $q('.de-cfg-body');
4753 if(prefTab) {
4754 prefTab.className = 'de-cfg-unvis';
4755 $q('.de-cfg-tab[selected]').removeAttribute('selected');
4756 }
4757 el.setAttribute('selected', '');
4758 const id = el.getAttribute('info');
4759 let newTab = $id('de-cfg-' + id);
4760 if(!newTab) {
4761 newTab = $aEnd($id('de-cfg-bar'),
4762 id === 'filters' ? this._getCfgFilters() :
4763 id === 'posts' ? this._getCfgPosts() :
4764 id === 'images' ? this._getCfgImages() :
4765 id === 'links' ? this._getCfgLinks() :
4766 id === 'form' ? this._getCfgForm() :
4767 id === 'common' ? this._getCfgCommon() :
4768 this._getCfgInfo());
4769 if(id === 'filters') {
4770 this._updateRowMeter($id('de-spell-txt'));
4771 }
4772 if(id === 'common') {
4773 // XXX: remove and make insertion in this._getCfgCommon()
4774 $after($q('input[info="userCSS"]').parentNode, getEditButton(
4775 'css',
4776 fn => fn(Cfg.userCSSTxt, false, function() {
4777 saveCfg('userCSSTxt', this.value);
4778 updateCSS();
4779 toggleWindow('cfg', true);
4780 }),
4781 'de-cfg-button'
4782 ));
4783 }
4784 }
4785 newTab.className = 'de-cfg-body';
4786 if(id === 'filters') {
4787 $id('de-spell-txt').value = Spells.list;
4788 }
4789 this._updateDependant();
4790
4791 // Updates all inputs according to config
4792 const els = $Q('.de-cfg-chkbox, .de-cfg-inptxt, .de-cfg-select', newTab.parentNode);
4793 for(let i = 0, len = els.length; i < len; ++i) {
4794 const el = els[i];
4795 const info = el.getAttribute('info');
4796 if(el.tagName === 'INPUT') {
4797 if(el.type === 'checkbox') {
4798 el.checked = !!Cfg[info];
4799 } else {
4800 el.value = Cfg[info];
4801 }
4802 } else {
4803 el.selectedIndex = Cfg[info];
4804 }
4805 }
4806 },
4807
4808 // "Filters" tab
4809 _getCfgFilters() {
4810 return `<div id="de-cfg-filters" class="de-cfg-unvis">
4811 <div id="de-spell-panel">
4812 ${ this._getBox('hideBySpell') }
4813 <a id="de-btn-spell-add" class="de-abtn de-spell-btn" href="#">${ Lng.add[lang] }</a>
4814 <a id="de-btn-spell-apply" class="de-abtn de-spell-btn" href="#">${ Lng.apply[lang] }</a>
4815 <a id="de-btn-spell-clear" class="de-abtn de-spell-btn" href="#">${ Lng.clear[lang] }</a>
4816 <a class="de-abtn de-spell-btn" href="${ gitWiki }Spells-` +
4817 `${ lang ? 'en' : 'ru' }" target="_blank">[?]</a>
4818 </div>
4819 <div id="de-spell-editor">
4820 <div id="de-spell-rowmeter"></div>
4821 <textarea id="de-spell-txt" wrap="off"></textarea>
4822 </div>
4823 ${ this._getBox('sortSpells') }<br>
4824 ${ this._getBox('hideRefPsts') }<br>
4825 ${ this._getBox('nextPageThr') }<br>
4826 ${ this._getSel('delHiddPost') }
4827 </div>`;
4828 },
4829
4830 // "Posts" tab
4831 _getCfgPosts() {
4832 return `<div id="de-cfg-posts" class="de-cfg-unvis">
4833 ${ localData ? '' : `${ this._getBox('ajaxUpdThr') }
4834 ${ this._getInp('updThrDelay') }
4835 <div class="de-depend">
4836 ${ this._getBox('updCount') }<br>
4837 ${ this._getBox('favIcoBlink') }<br>
4838 ${ 'Notification' in deWindow ? this._getBox('desktNotif') + '<br>' : '' }
4839 ${ this._getBox('noErrInTitle') }<br>
4840 ${ this._getBox('markNewPosts') }<br>
4841 ${ aib.dobrochan ? this._getBox('useDobrAPI') : '' }
4842 </div>` }
4843 ${ this._getBox('markMyPosts') }<br>
4844 ${ !localData ? `${ this._getBox('expandTrunc') }<br>` : '' }
4845 ${ this._getSel('showHideBtn') }<br>
4846 ${ !localData ? this._getSel('showRepBtn') : '' }<br>
4847 ${ this._getSel('postBtnsCSS') }
4848 ${ this._getInp('postBtnsBack', false, 8) }<br>
4849 ${ !localData ? this._getSel('thrBtns') : '' }<br>
4850 ${ this._getSel('noSpoilers') }<br>
4851 ${ this._getInp('limitPostMsg', true, 5) }<br>
4852 ${ this._getBox('widePosts') }<br>
4853 ${ this._getBox('noPostNames') }<br>
4854 ${ this._getBox('correctTime') }
4855 ${ this._getInp('timeOffset', true, 1) }
4856 <a class="de-abtn" target="_blank" href="${ gitWiki }Settings-time-` +
4857 `${ lang ? 'en' : 'ru' }">[?]</a>
4858 <div class="de-depend">
4859 ${ this._getInp('timePattern', true, 24) }<br>
4860 ${ this._getInp('timeRPattern', true, 24) }
4861 </div>
4862 </div>`;
4863 },
4864
4865 // "Images" tab
4866 _getCfgImages() {
4867 return `<div id="de-cfg-images" class="de-cfg-unvis">
4868 ${ this._getSel('expandImgs') }<br>
4869 <div class="de-depend">
4870 ${ this._getBox('imgNavBtns') }<br>
4871 ${ this._getBox('imgInfoLink') }<br>
4872 ${ this._getSel('resizeImgs') }<br>
4873 ${ Post.sizing.dPxRatio > 1 ? this._getBox('resizeDPI') + '<br>' : '' }
4874 ${ this._getInp('minImgSize') }<br>
4875 ${ this._getInp('zoomFactor') }<br>
4876 ${ this._getBox('webmControl') }<br>
4877 ${ this._getBox('webmTitles') }<br>
4878 ${ this._getInp('webmVolume') }<br>
4879 ${ this._getInp('minWebmWidth') }
4880 </div>
4881 ${ nav.isPresto ? '' : this._getSel('preLoadImgs') + '<br>' }
4882 ${ nav.isPresto || aib._4chan ? '' : `<div class="de-depend">
4883 ${ this._getBox('findImgFile') }
4884 </div>` }
4885 ${ this._getSel('openImgs') }<br>
4886 ${ this._getBox('imgSrcBtns') }<br>
4887 ${ this._getSel('imgNames') }<br>
4888 ${ this._getInp('maskVisib') }
4889 </div>`;
4890 },
4891
4892 // "Links" tab
4893 _getCfgLinks() {
4894 return `<div id="de-cfg-links" class="de-cfg-unvis">
4895 ${ this._getBox('linksNavig') }
4896 <div class="de-depend">
4897 ${ this._getInp('linksOver') }
4898 ${ this._getInp('linksOut') }<br>
4899 ${ this._getBox('markViewed') }<br>
4900 ${ this._getBox('strikeHidd') }
4901 <div class="de-depend">${ this._getBox('removeHidd') }</div>
4902 ${ this._getBox('noNavigHidd') }
4903 </div>
4904 ${ this._getBox('markMyLinks') }<br>
4905 ${ this._getBox('crossLinks') }<br>
4906 ${ this._getBox('decodeLinks') }<br>
4907 ${ this._getBox('insertNum') }<br>
4908 ${ !localData ? `${ this._getBox('addOPLink') }<br>
4909 ${ this._getBox('addImgs') }<br>` : '' }
4910 <div>
4911 ${ this._getBox('addMP3') }
4912 ${ aib.prot === 'http:' ? this._getBox('addVocaroo') : '' }
4913 </div>
4914 ${ this._getSel('embedYTube') }
4915 <div class="de-depend">
4916 ${ this._getInp('YTubeWidth', false) }\u00D7
4917 ${ this._getInp('YTubeHeigh', false) }(px)<br>
4918 ${ this._getBox('YTubeTitles') }<br>
4919 ${ this._getInp('ytApiKey', true, 25) }<br>
4920 ${ this._getBox('addVimeo') }
4921 </div>
4922 </div>`;
4923 },
4924
4925 // "Form" tab
4926 _getCfgForm() {
4927 return `<div id="de-cfg-form" class="de-cfg-unvis">
4928 ${ this._getBox('ajaxPosting') }<br>
4929 ${ pr.form ? `<div class="de-depend">
4930 ${ this._getBox('postSameImg') }<br>
4931 ${ this._getBox('removeEXIF') }<br>
4932 ${ this._getSel('removeFName') }<br>
4933 ${ this._getBox('sendErrNotif') }<br>
4934 ${ this._getBox('scrAfterRep') }<br>
4935 ${ pr.files && !nav.isPresto ? this._getSel('fileInputs') : '' }
4936 </div>` : '' }
4937 ${ pr.form ? this._getSel('addPostForm') + '<br>' : '' }
4938 ${ pr.txta ? this._getBox('spacedQuote') + '<br>' : '' }
4939 ${ this._getBox('favOnReply') }<br>
4940 ${ pr.subj ? this._getBox('warnSubjTrip') + '<br>' : '' }
4941 ${ pr.mail ? `${ this._getBox('addSageBtn') }
4942 ${ this._getBox('saveSage') }<br>` : '' }
4943 ${ pr.cap ? `${ aib.hasAltCaptcha ? `${ this._getBox('altCaptcha') }<br>` : '' }
4944 ${ this._getInp('capUpdTime') }<br>
4945 ${ this._getSel('captchaLang') }<br>` : '' }
4946 ${ pr.txta ? `${ this._getSel('addTextBtns') }
4947 ${ !aib._4chan ? this._getBox('txtBtnsLoc') : '' }<br>` : '' }
4948 ${ pr.passw ? `${ this._getInp('passwValue', false, 9) }
4949 ${ this._getBox('userPassw') }<input type="button"` +
4950 ` id="de-cfg-button-pass" class="de-cfg-button" value="${ Lng.change[lang] }"><br>` : '' }
4951 ${ pr.name ? `${ this._getInp('nameValue', false, 9) }
4952 ${ this._getBox('userName') }<br>` : '' }
4953 ${ pr.rules || pr.passw || pr.name ? Lng.hide[lang] +
4954 (pr.rules ? this._getBox('noBoardRule') : '') +
4955 (pr.passw ? this._getBox('noPassword') : '') +
4956 (pr.name ? this._getBox('noName') : '') +
4957 (pr.subj ? this._getBox('noSubj') : '') : '' }
4958 </div>`;
4959 },
4960
4961 // "Common" tab
4962 _getCfgCommon() {
4963 return `<div id="de-cfg-common" class="de-cfg-unvis">
4964 ${ this._getSel('scriptStyle') }<br>
4965 ${ this._getBox('userCSS') }
4966 <a href="${ gitWiki }css-tricks" class="de-abtn" target="_blank">[?]</a><br>
4967 ${ 'animation' in docBody.style ? this._getBox('animation') + '<br>' : '' }
4968 ${ this._getBox('hotKeys') }
4969 <input type="button" id="de-cfg-button-keys" class="de-cfg-button" value="${ Lng.edit[lang] }">
4970 <div class="de-depend">${ this._getInp('loadPages') }</div>
4971 ${ this._getSel('panelCounter') }<br>
4972 ${ this._getBox('rePageTitle') }<br>
4973 ${ !localData ? `${ this._getBox('inftyScroll') }<br>
4974 ${ this._getBox('hideReplies') }<br>
4975 ${ this._getBox('scrollToTop') }<br>` : '' }
4976 ${ this._getBox('saveScroll') }<br>
4977 ${ this._getSel('favThrOrder') }<br>
4978 ${ this._getBox('favWinOn') }<br>
4979 ${ this._getBox('closePopups') }
4980 </div>`;
4981 },
4982
4983 // "Info" tab
4984 _getCfgInfo() {
4985 const statsTable = this._getInfoTable([
4986 [Lng.thrViewed[lang], Cfg.stats.view],
4987 [Lng.thrCreated[lang], Cfg.stats.op],
4988 [Lng.thrHidden[lang], HiddenThreads.getCount()],
4989 [Lng.postsSent[lang], Cfg.stats.reply]
4990 ], false);
4991 return `<div id="de-cfg-info" class="de-cfg-unvis">
4992 <div style="padding-bottom: 10px;">
4993 <a href="${ gitWiki }versions" target="_blank">v${ version }.${ commit }` +
4994 `${ nav.isESNext ? '.es6' : '' }</a> |
4995 <a href="https://dscript.me/" target="_blank">Homepage</a> |
4996 <a href="${ gitWiki }${ lang === 1 ? 'home-en/' : '' }" target="_blank">Github</a> |
4997 <input type="button" id="de-cfg-button-debug" value="` +
4998 `${ Lng.debug[lang] }" title="${ Lng.infoDebug[lang] }">
4999 </div>
5000 <div id="de-info-table">
5001 <div id="de-info-stats">${ statsTable }</div>
5002 <div id="de-info-log">${ this._getInfoTable(Logger.getLogData(false), true) }</div>
5003 </div>
5004 ${ !nav.hasWebStorage && !nav.isPresto && !localData || nav.hasGMXHR ? `
5005 <div style="margin-top: 3px; text-align: center;">>>
5006 <input type="button" id="de-cfg-button-updnow" value="${ Lng.checkNow[lang] }">
5007 <<</div>
5008 ${ this._getSel('updDollchan') }` : '' }
5009 </div>`;
5010 },
5011
5012 // Creates a label with checkbox for option switching
5013 _getBox: id => `<label class="de-cfg-label">
5014 <input class="de-cfg-chkbox" info="${ id }" type="checkbox"> ${ Lng.cfg[id][lang] }</label>`,
5015 // Creates a table for Info tab
5016 _getInfoTable: (data, needMs) => data.map(val => `<div class="de-info-row">
5017 <span class="de-info-name">${ val[0] }</span>
5018 <span>${ val[1] + (needMs ? 'ms' : '') }</span></div>`).join(''),
5019 // Creates a text input for text option values
5020 _getInp(id, addText = true, size = 2) {
5021 const el = doc.createElement('div');
5022 el.appendChild($txt(Cfg[id])); // Escape HTML
5023 return `<label class="de-cfg-label">
5024 <input class="de-cfg-inptxt" info="${ id }" type="text" size="${ size }" value="${
5025 el.innerHTML }">${ addText && Lng.cfg[id] ? Lng.cfg[id][lang] : '' }</label>`;
5026 },
5027 // Creates a menu with a list of checkboxes. Uses for popup window.
5028 _getList : arr => arrTags(arr, '<label class="de-block"><input type="checkbox"> ', '</label>'),
5029 // Creates a select for multiple option values
5030 _getSel : id => `<label class="de-cfg-label"><select class="de-cfg-select" info="${ id }">${
5031 Lng.cfg[id].sel[lang].map((val, i) => `<option value="${ i }">${ val }</option>`).join('')
5032 }</select> ${ Lng.cfg[id].txt[lang] } </label>`,
5033 // Creates a tab for tab bar
5034 _getTab: id => `<div class="${ aib.cReply } de-cfg-tab" info="${ id }">${ Lng.cfgTab[id][lang] }</div>`,
5035 // Switching the dependent inputs according to their parents
5036 _toggleDependant(state, arr) {
5037 let i = arr.length;
5038 const nState = !state;
5039 while(i--) {
5040 const el = $q(arr[i]);
5041 if(el) {
5042 el.disabled = nState;
5043 }
5044 }
5045 },
5046 _updateCSS() {
5047 $delAll('#de-css, #de-css-dynamic, #de-css-user', doc.head);
5048 scriptCSS();
5049 },
5050 _updateDependant() {
5051 const fn = this._toggleDependant;
5052 fn(Cfg.ajaxUpdThr, [
5053 'input[info="updThrDelay"]', 'input[info="updCount"]', 'input[info="favIcoBlink"]',
5054 'input[info="markNewPosts"]', 'input[info="desktNotif"]', 'input[info="noErrInTitle"]'
5055 ]);
5056 fn(Cfg.postBtnsCSS === 2, ['input[info="postBtnsBack"]']);
5057 fn(Cfg.expandImgs, [
5058 'input[info="imgNavBtns"]', 'input[info="imgInfoLink"]', 'input[info="resizeDPI"]',
5059 'select[info="resizeImgs"]', 'input[info="minImgSize"]', 'input[info="zoomFactor"]',
5060 'input[info="webmControl"]', 'input[info="webmTitles"]', 'input[info="webmVolume"]',
5061 'input[info="minWebmWidth"]'
5062 ]);
5063 fn(Cfg.preLoadImgs, ['input[info="findImgFile"]']);
5064 fn(Cfg.linksNavig, [
5065 'input[info="linksOver"]', 'input[info="linksOut"]', 'input[info="markViewed"]',
5066 'input[info="strikeHidd"]', 'input[info="noNavigHidd"]'
5067 ]);
5068 fn(Cfg.strikeHidd && Cfg.linksNavig, ['input[info="removeHidd"]']);
5069 fn(Cfg.embedYTube, [
5070 'input[info="YTubeWidth"]', 'input[info="YTubeHeigh"]', 'input[info="YTubeTitles"]',
5071 'input[info="ytApiKey"]', 'input[info="addVimeo"]'
5072 ]);
5073 fn(Cfg.YTubeTitles, ['input[info="ytApiKey"]']);
5074 fn(Cfg.ajaxPosting, [
5075 'input[info="postSameImg"]', 'input[info="removeEXIF"]', 'select[info="removeFName"]',
5076 'input[info="sendErrNotif"]', 'input[info="scrAfterRep"]', 'select[info="fileInputs"]'
5077 ]);
5078 fn(Cfg.addSageBtn, ['input[info="saveSage"]']);
5079 fn(Cfg.addTextBtns, ['input[info="txtBtnsLoc"]']);
5080 fn(Cfg.hotKeys, ['input[info="loadPages"]']);
5081 },
5082 // Updates row counter in spells editor
5083 _updateRowMeter(node) {
5084 const top = node.scrollTop;
5085 const el = node.previousElementSibling;
5086 let num = el.numLines || 1;
5087 let i = 19;
5088 if(num - i < ((top / 12) | 0 + 1)) {
5089 let str = '';
5090 while(i--) {
5091 str += `${ num++ }<br>`;
5092 }
5093 el.insertAdjacentHTML('beforeend', str);
5094 el.numLines = num;
5095 }
5096 el.scrollTop = top;
5097 }
5098};
5099
5100/* ==[ MenuPopups.js ]========================================================================================
5101 POPUPS & MENU
5102=========================================================================================================== */
5103
5104function closePopup(data) {
5105 const el = typeof data === 'string' ? $id('de-popup-' + data) : data;
5106 if(el) {
5107 el.closeTimeout = null;
5108 if(Cfg.animation) {
5109 $animate(el, 'de-close', true);
5110 } else {
5111 el.remove();
5112 }
5113 }
5114}
5115
5116function $popup(id, txt, isWait = false) {
5117 let el = $id('de-popup-' + id);
5118 const buttonHTML = isWait ? '<svg class="de-wait"><use xlink:href="#de-symbol-wait"/></svg>' : '\u2716 ';
5119 if(el) {
5120 $q('div', el).innerHTML = txt.trim();
5121 $q('span', el).innerHTML = buttonHTML;
5122 if(!isWait && Cfg.animation) {
5123 $animate(el, 'de-blink');
5124 }
5125 } else {
5126 el = $bEnd($id('de-wrapper-popup'), `<div class="${ aib.cReply } de-popup" id="de-popup-${ id }">
5127 <span class="de-popup-btn">${ buttonHTML }</span>
5128 <div class="de-popup-msg">${ txt.trim() }</div>
5129 </div>`);
5130 el.onclick = e => {
5131 let el = fixEventEl(e.target);
5132 el = el.tagName.toLowerCase() === 'svg' ? el.parentNode : el;
5133 if(el.className === 'de-popup-btn') {
5134 closePopup(el.parentNode);
5135 }
5136 };
5137 if(Cfg.animation) {
5138 $animate(el, 'de-open');
5139 }
5140 }
5141 if(Cfg.closePopups && !isWait && !id.includes('edit') && !id.includes('cfg')) {
5142 el.closeTimeout = setTimeout(closePopup, 6e3, el);
5143 }
5144 return el.lastElementChild;
5145}
5146
5147// Adds button that calls a popup with the text editor. Useful to edit settings.
5148function getEditButton(name, getDataFn, className = 'de-button') {
5149 return $btn(Lng.edit[lang], Lng.editInTxt[lang], () => getDataFn((val, isJSON, saveFn) => {
5150 // Create popup window with textarea.
5151 const el = $popup('edit-' + name,
5152 `<b>${ Lng.editor[name][lang] }</b><textarea class="de-editor"></textarea>`);
5153 const ta = el.lastChild;
5154 ta.value = isJSON ? JSON.stringify(val, null, '\t') : val;
5155 // "Save" button. If there a JSON data, parses and saves on success.
5156 el.appendChild($btn(Lng.save[lang], Lng.saveChanges[lang], !isJSON ? saveFn.bind(ta) : () => {
5157 let data;
5158 try {
5159 data = JSON.parse(ta.value.trim().replace(/[\n\r\t]/g, '') || '{}');
5160 } finally {
5161 if(!data) {
5162 $popup('err-invaliddata', Lng.invalidData[lang]);
5163 return;
5164 }
5165 saveFn(data);
5166 closePopup('edit-' + name);
5167 closePopup('err-invaliddata');
5168 }
5169 }));
5170 }), className);
5171}
5172
5173class Menu {
5174 constructor(parentEl, html, clickFn, isFixed = true) {
5175 this.onout = null;
5176 this.onover = null;
5177 this.onremove = null;
5178 this._closeTO = 0;
5179 const el = $bEnd(docBody, `<div class="${ aib.cReply } de-menu" style="position: ${
5180 isFixed ? 'fixed' : 'absolute' }; left: 0px; top: 0px; visibility: hidden;">${ html }</div>`);
5181 const cr = parentEl.getBoundingClientRect();
5182 const { style, offsetWidth: w, offsetHeight: h } = el;
5183 style.left = (isFixed ? 0 : deWindow.pageXOffset) +
5184 (cr.left + w < Post.sizing.wWidth ? cr.left : cr.right - w) + 'px';
5185 style.top = (isFixed ? 0 : deWindow.pageYOffset) +
5186 (cr.bottom + h < Post.sizing.wHeight ? cr.bottom - 0.5 : cr.top - h + 0.5) + 'px';
5187 style.removeProperty('visibility');
5188 this._clickFn = clickFn;
5189 this._el = el;
5190 this.parentEl = parentEl;
5191 el.addEventListener('mouseover', this, true);
5192 el.addEventListener('mouseout', this, true);
5193 el.addEventListener('click', this);
5194 parentEl.addEventListener('mouseout', this);
5195 }
5196 static getMenuImgSrc(data) {
5197 let p;
5198 if(typeof data === 'string') {
5199 p = encodeURIComponent(data) + '" target="_blank">' + Lng.frameSearch[lang];
5200 } else {
5201 const link = data.nextSibling;
5202 p = encodeURIComponent(data.getAttribute('de-href') || link.getAttribute('de-href') ||
5203 link.href) + '" target="_blank">' + Lng.searchIn[lang];
5204 }
5205 return arrTags([
5206 `de-src-google" href="https://www.google.com/searchbyimage?image_url=${ p }Google`,
5207 `de-src-yandex" href="https://yandex.com/images/search?rpt=imageview&img_url=${ p }Yandex`,
5208 `de-src-tineye" href="https://tineye.com/search/?url=${ p }TinEye`,
5209 `de-src-saucenao" href="https://saucenao.com/search.php?url=${ p }SauceNAO`,
5210 `de-src-iqdb" href="https://iqdb.org/?url=${ p }IQDB`,
5211 `de-src-tracemoe" href="https://trace.moe/?auto&url=${ p }TraceMoe`
5212 ], '<a class="de-menu-item ', '</a>');
5213 }
5214 handleEvent(e) {
5215 let isOverEvent = false;
5216 switch(e.type) {
5217 case 'click':
5218 if(e.target.classList.contains('de-menu-item')) {
5219 this.removeMenu();
5220 this._clickFn(e.target);
5221 if(!Cfg.expandPanel && !$q('.de-win-active')) {
5222 $hide($id('de-panel-buttons'));
5223 }
5224 }
5225 break;
5226 case 'mouseover': isOverEvent = true;
5227 /* falls through */
5228 case 'mouseout': {
5229 clearTimeout(this._closeTO);
5230 let rt = fixEventEl(e.relatedTarget);
5231 rt = rt && rt.farthestViewportElement || rt;
5232 if(!rt || (rt !== this._el && !this._el.contains(rt))) {
5233 if(isOverEvent) {
5234 if(this.onover) {
5235 this.onover();
5236 }
5237 } else if(!rt || (rt !== this.parentEl && !this.parentEl.contains(rt))) {
5238 this._closeTO = setTimeout(() => this.removeMenu(), 75);
5239 if(this.onout) {
5240 this.onout();
5241 }
5242 }
5243 }
5244 }
5245 }
5246 }
5247 removeMenu() {
5248 if(!this._el) {
5249 return;
5250 }
5251 if(this.onremove) {
5252 this.onremove();
5253 }
5254 this._el.removeEventListener('mouseover', this, true);
5255 this._el.removeEventListener('mouseout', this, true);
5256 this.parentEl.removeEventListener('mouseout', this);
5257 this._el.removeEventListener('click', this);
5258 this._el.remove();
5259 this._el = null;
5260 }
5261}
5262
5263function addMenu(el) {
5264 const fn = a => arrTags(a, '<span class="de-menu-item">', '</span>');
5265 switch(el.id) {
5266 case 'de-btn-spell-add':
5267 return new Menu(el, `<div style="display: inline-block; border-right: 1px solid grey;">${
5268 fn('#words,#exp,#exph,#imgn,#ihash,#subj,#name,#trip,#img,#sage'.split(','))
5269 }</div><div style="display: inline-block;">${
5270 fn('#op,#tlen,#all,#video,#vauthor,#num,#wipe,#rep,#outrep,<br>'.split(',')) }</div>`,
5271 ({ textContent: s }) => insertText($id('de-spell-txt'), s +
5272 (!aib.t || s === '#op' || s === '#rep' || s === '#outrep' ? '' : `[${ aib.b },${ aib.t }]`) +
5273 (Spells.needArg[Spells.names.indexOf(s.substr(1))] ? '(' : '')));
5274 case 'de-panel-refresh':
5275 return new Menu(el, fn(Lng.selAjaxPages[lang]),
5276 el => Pages.loadPages(aProto.indexOf.call(el.parentNode.children, el) + 1));
5277 case 'de-panel-savethr':
5278 return new Menu(el, fn($q(aib.qPostImg, DelForm.first.el) ?
5279 Lng.selSaveThr[lang] : [Lng.selSaveThr[lang][0]]),
5280 el => {
5281 if($id('de-popup-savethr')) {
5282 return;
5283 }
5284 const imgOnly = !!aProto.indexOf.call(el.parentNode.children, el);
5285 if(ContentLoader.isLoading) {
5286 $popup('savethr', Lng.loading[lang], true);
5287 ContentLoader.afterFn = () => ContentLoader.downloadThread(imgOnly);
5288 ContentLoader.popupId = 'savethr';
5289 } else {
5290 ContentLoader.downloadThread(imgOnly);
5291 }
5292 });
5293 case 'de-panel-audio-off':
5294 return new Menu(el, fn(Lng.selAudioNotif[lang]), el => {
5295 updater.enableUpdater();
5296 updater.toggleAudio([3e4, 6e4, 12e4, 3e5][aProto.indexOf.call(el.parentNode.children, el)]);
5297 $id('de-panel-audio-off').id = 'de-panel-audio-on';
5298 });
5299 }
5300}
5301
5302/* ==[ Hotkeys.js ]===========================================================================================
5303 HOTKEYS
5304=========================================================================================================== */
5305
5306const HotKeys = {
5307 cPost : null,
5308 enabled : false,
5309 gKeys : null,
5310 lastPageOffset : 0,
5311 ntKeys : null,
5312 tKeys : null,
5313 version : 7,
5314 clearCPost() {
5315 this.cPost = null;
5316 this.lastPageOffset = 0;
5317 },
5318 disableHotKeys() {
5319 if(this.enabled) {
5320 this.enabled = false;
5321 if(this.cPost) {
5322 this.cPost.unselect();
5323 }
5324 this.clearCPost();
5325 this.gKeys = this.ntKeys = this.tKeys = null;
5326 doc.removeEventListener('keydown', this, true);
5327 }
5328 },
5329 enableHotKeys() {
5330 if(!this.enabled) {
5331 this.enabled = true;
5332 this._paused = false;
5333 Promise.resolve(this.readKeys()).then(keys => {
5334 if(this.enabled) {
5335 [,, this.gKeys, this.ntKeys, this.tKeys] = keys;
5336 doc.addEventListener('keydown', this, true);
5337 }
5338 });
5339 }
5340 },
5341 getDefaultKeys: () => [HotKeys.version, nav.isFirefox, [
5342 // GLOBAL KEYS
5343 /* One post/thread above */ 0x004B /* = K */,
5344 /* One post/thread below */ 0x004A /* = J */,
5345 /* Reply or create thread */ 0x0052 /* = R */,
5346 /* Hide selected thread/post */ 0x0048 /* = H */,
5347 /* Open previous page/image */ 0x1025 /* = Ctrl+Left */,
5348 /* Send post (txt) */ 0x900D /* = Ctrl+Enter */,
5349 /* Open/close "Favorites" */ 0x4046 /* = Alt+F */,
5350 /* Open/close "Hidden" */ 0x4048 /* = Alt+H */,
5351 /* Open/close panel */ 0x0050 /* = P */,
5352 /* Mask/unmask images */ 0x0042 /* = B */,
5353 /* Open/close "Settings" */ 0x4053 /* = Alt+S */,
5354 /* Expand current image */ 0x0049 /* = I */,
5355 /* Bold text */ 0xC042 /* = Alt+B */,
5356 /* Italic text */ 0xC049 /* = Alt+I */,
5357 /* Strike text */ 0xC054 /* = Alt+T */,
5358 /* Spoiler text */ 0xC050 /* = Alt+P */,
5359 /* Code text */ 0xC043 /* = Alt+C */,
5360 /* Open next page/image */ 0x1027 /* = Ctrl+Right */,
5361 /* Open/close "Video" */ 0x4056 /* = Alt+V */
5362 ], [// NON-THREAD KEYS
5363 /* One post above */ 0x004D /* = M */,
5364 /* One post below */ 0x004E /* = N */,
5365 /* Open thread */ 0x0056 /* = V */,
5366 /* Expand thread */ 0x0045 /* = E */
5367 ], [// THREAD KEYS
5368 /* Update thread */ 0x0055 /* = U */
5369 ]],
5370 handleEvent(e) {
5371 if(this._paused || e.metaKey) {
5372 return;
5373 }
5374 let idx;
5375 const isThr = aib.t;
5376 const el = e.target;
5377 const tag = el.tagName;
5378 const kc = e.keyCode |
5379 (e.ctrlKey ? 0x1000 : 0) |
5380 (e.shiftKey ? 0x2000 : 0) |
5381 (e.altKey ? 0x4000 : 0) |
5382 (tag === 'TEXTAREA' ||
5383 tag === 'INPUT' && (el.type === 'text' || el.type === 'password') ? 0x8000 : 0);
5384 if(kc === 0x74 || kc === 0x8074) { // F5
5385 if(isThr || $id('de-popup-load-pages')) {
5386 return;
5387 }
5388 AttachedImage.closeImg();
5389 Pages.loadPages(+Cfg.loadPages);
5390 } else if(kc === 0x1B) { // ESC
5391 if(AttachedImage.viewer) {
5392 AttachedImage.closeImg();
5393 return;
5394 }
5395 if(this.cPost) {
5396 this.cPost.unselect();
5397 this.cPost = null;
5398 }
5399 if(isThr) {
5400 Post.clearMarks();
5401 }
5402 this.lastPageOffset = 0;
5403 } else if(kc === 0x801B) { // ESC (txt)
5404 el.blur();
5405 } else {
5406 let post;
5407 const globIdx = this.gKeys.indexOf(kc);
5408 switch(globIdx) {
5409 case 2: // Quick reply
5410 if(pr.form) {
5411 post = this.cPost || this._getFirstVisPost(false, true) || Thread.first.op;
5412 this.cPost = post;
5413 pr.showQuickReply(post, post.num, true, false);
5414 post.select();
5415 }
5416 break;
5417 case 3: // Hide selected thread/post
5418 post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
5419 if(post) {
5420 post.setUserVisib(!post.isHidden);
5421 this._scroll(post, false, post.isOp);
5422 }
5423 break;
5424 case 4: // Open previous page/image
5425 if(AttachedImage.viewer) {
5426 AttachedImage.viewer.navigate(false);
5427 } else if(isThr || aib.page !== aib.firstPage) {
5428 deWindow.location.pathname = aib.getPageUrl(aib.b, isThr ? 0 : aib.page - 1);
5429 }
5430 break;
5431 case 5: // Send post (txt)
5432 if(el !== pr.txta && el !== pr.cap.textEl) {
5433 return;
5434 }
5435 pr.subm.click();
5436 break;
5437 case 6: // Open/close "Favorites"
5438 toggleWindow('fav', false);
5439 break;
5440 case 7: // Open/close "Hidden"
5441 toggleWindow('hid', false);
5442 break;
5443 case 8: // Open/close panel
5444 $toggle($id('de-panel-buttons'));
5445 break;
5446 case 9: // Mask/unmask images
5447 toggleCfg('maskImgs');
5448 updateCSS();
5449 break;
5450 case 10: // Open/close "Settings"
5451 toggleWindow('cfg', false);
5452 break;
5453 case 11: // Expand current image
5454 post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
5455 if(post) {
5456 post.toggleImages();
5457 }
5458 break;
5459 case 12: // Bold text (txt)
5460 if(el !== pr.txta) {
5461 return;
5462 }
5463 $id('de-btn-bold').click();
5464 break;
5465 case 13: // Italic text (txt)
5466 if(el !== pr.txta) {
5467 return;
5468 }
5469 $id('de-btn-italic').click();
5470 break;
5471 case 14: // Strike text (txt)
5472 if(el !== pr.txta) {
5473 return;
5474 }
5475 $id('de-btn-strike').click();
5476 break;
5477 case 15: // Spoiler text (txt)
5478 if(el !== pr.txta) {
5479 return;
5480 }
5481 $id('de-btn-spoil').click();
5482 break;
5483 case 16: // Code text (txt)
5484 if(el !== pr.txta) {
5485 return;
5486 }
5487 $id('de-btn-code').click();
5488 break;
5489 case 17: // Open next page/image
5490 if(AttachedImage.viewer) {
5491 AttachedImage.viewer.navigate(true);
5492 } else if(!isThr) {
5493 const pageNum = DelForm.last.pageNum + 1;
5494 if(pageNum <= aib.lastPage) {
5495 deWindow.location.pathname = aib.getPageUrl(aib.b, pageNum);
5496 }
5497 }
5498 break;
5499 case 18: // Open/close "Videos"
5500 toggleWindow('vid', false);
5501 break;
5502 case -1:
5503 if(isThr) {
5504 idx = this.tKeys.indexOf(kc);
5505 if(idx === 0) { // Update thread
5506 updater.forceLoad(null);
5507 break;
5508 }
5509 return;
5510 }
5511 idx = this.ntKeys.indexOf(kc);
5512 if(idx === -1) {
5513 return;
5514 } else if(idx === 2) { // Open thread
5515 post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
5516 if(post) {
5517 if(typeof GM_openInTab === 'function') {
5518 GM_openInTab(aib.getThrUrl(aib.b, post.tNum), false, true);
5519 } else {
5520 deWindow.open(aib.getThrUrl(aib.b, post.tNum), '_blank');
5521 }
5522 }
5523 break;
5524 } else if(idx === 3) { // Expand/collapse thread
5525 post = this._getFirstVisPost(false, true) || this._getNextVisPost(null, true, false);
5526 if(post) {
5527 if(post.thr.loadCount !== 0 && post.thr.op.next.count === 1) {
5528 const nextThr = post.thr.nextNotHidden;
5529 post.thr.loadPosts(visPosts, !!nextThr);
5530 post = (nextThr || post.thr).op;
5531 } else {
5532 post.thr.loadPosts('all');
5533 post = post.thr.op;
5534 }
5535 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + post.top);
5536 if(this.cPost && this.cPost !== post) {
5537 this.cPost.unselect();
5538 this.cPost = post;
5539 }
5540 }
5541 break;
5542 }
5543 /* falls through */
5544 default: {
5545 const scrollToThr = !isThr && (globIdx === 0 || globIdx === 1);
5546 this._scroll(this._getFirstVisPost(scrollToThr, false),
5547 globIdx === 0 || idx === 0, scrollToThr);
5548 }
5549 }
5550 }
5551 $pd(e);
5552 e.stopPropagation();
5553 },
5554 pauseHotKeys() {
5555 this._paused = true;
5556 },
5557 async readKeys() {
5558 const str = await getStored('DESU_keys');
5559 if(!str) {
5560 return this.getDefaultKeys();
5561 }
5562 let keys;
5563 try {
5564 keys = JSON.parse(str);
5565 } finally {
5566 if(!keys) {
5567 return this.getDefaultKeys();
5568 }
5569 if(keys[0] !== this.version) {
5570 const tKeys = this.getDefaultKeys();
5571 switch(keys[0]) {
5572 case 1:
5573 keys[2][11] = tKeys[2][11];
5574 keys[4] = tKeys[4];
5575 /* falls through */
5576 case 2:
5577 keys[2][12] = tKeys[2][12];
5578 keys[2][13] = tKeys[2][13];
5579 keys[2][14] = tKeys[2][14];
5580 keys[2][15] = tKeys[2][15];
5581 keys[2][16] = tKeys[2][16];
5582 /* falls through */
5583 case 3:
5584 keys[2][17] = keys[3][3];
5585 keys[3][3] = keys[3].splice(4, 1)[0];
5586 /* falls through */
5587 case 4:
5588 case 5:
5589 case 6:
5590 keys[2][18] = tKeys[2][18];
5591 }
5592 keys[0] = this.version;
5593 setStored('DESU_keys', JSON.stringify(keys));
5594 }
5595 if(keys[1] ^ nav.isFirefox) {
5596 const mapFunc = nav.isFirefox ?
5597 key => key === 189 ? 173 : key === 187 ? 61 : key === 186 ? 59 : key :
5598 key => key === 173 ? 189 : key === 61 ? 187 : key === 59 ? 186 : key;
5599 keys[1] = nav.isFirefox;
5600 keys[2] = keys[2].map(mapFunc);
5601 keys[3] = keys[3].map(mapFunc);
5602 setStored('DESU_keys', JSON.stringify(keys));
5603 }
5604 return keys;
5605 }
5606 },
5607 resume(keys) {
5608 [,, this.gKeys, this.ntKeys, this.tKeys] = keys;
5609 this._paused = false;
5610 },
5611
5612 _paused: false,
5613 _getNextVisPost(cPost, isOp, toUp) {
5614 if(isOp) {
5615 const thr = cPost ? toUp ? cPost.thr.prevNotHidden : cPost.thr.nextNotHidden :
5616 Thread.first.isHidden ? Thread.first.nextNotHidden : Thread.first;
5617 return thr ? thr.op : null;
5618 }
5619 return cPost ? cPost.getAdjacentVisPost(toUp) : Thread.first.isHidden ||
5620 Thread.first.op.isHidden ? Thread.first.op.getAdjacentVisPost(toUp) : Thread.first.op;
5621 },
5622 _getFirstVisPost(getThread, getFull) {
5623 if(this.lastPageOffset !== deWindow.pageYOffset) {
5624 let post = getThread ? Thread.first : Thread.first.op;
5625 while(post.top < 1) {
5626 const tPost = post.next;
5627 if(!tPost) {
5628 break;
5629 }
5630 post = tPost;
5631 }
5632 if(this.cPost) {
5633 this.cPost.unselect();
5634 }
5635 this.cPost = getThread ? getFull ? post.op : post.op.prev : getFull ? post : post.prev;
5636 this.lastPageOffset = deWindow.pageYOffset;
5637 }
5638 return this.cPost;
5639 },
5640 _scroll(post, toUp, toThread) {
5641 const next = this._getNextVisPost(post, toThread, toUp);
5642 if(!next) {
5643 if(!aib.t) {
5644 const pageNum = toUp ? DelForm.first.pageNum - 1 : DelForm.last.pageNum + 1;
5645 if(toUp ? pageNum >= aib.firstPage : pageNum <= aib.lastPage) {
5646 deWindow.location.pathname = aib.getPageUrl(aib.b, pageNum);
5647 }
5648 }
5649 return;
5650 }
5651 if(post) {
5652 post.unselect();
5653 }
5654 if(toThread) {
5655 next.el.scrollIntoView();
5656 } else {
5657 scrollTo(0, deWindow.pageYOffset + next.el.getBoundingClientRect().top -
5658 Post.sizing.wHeight / 2 + next.el.clientHeight / 2);
5659 }
5660 this.lastPageOffset = deWindow.pageYOffset;
5661 next.select();
5662 this.cPost = next;
5663 }
5664};
5665
5666class KeyEditListener {
5667 constructor(popupEl, keys, allKeys) {
5668 this.cEl = null;
5669 this.cKey = -1;
5670 this.errorInput = false;
5671 const aInputs = [...$Q('.de-input-key', popupEl)];
5672 for(let i = 0, len = allKeys.length; i < len; ++i) {
5673 const k = allKeys[i];
5674 if(k !== 0) {
5675 for(let j = i + 1; j < len; ++j) {
5676 if(k === allKeys[j]) {
5677 aInputs[i].classList.add('de-input-error');
5678 aInputs[j].classList.add('de-input-error');
5679 break;
5680 }
5681 }
5682 }
5683 }
5684 this.popupEl = popupEl;
5685 this.keys = keys;
5686 this.initKeys = JSON.parse(JSON.stringify(keys));
5687 this.allKeys = allKeys;
5688 this.allInputs = aInputs;
5689 this.errCount = $Q('.de-input-error', popupEl).length;
5690 if(this.errCount !== 0) {
5691 this.saveButton.disabled = true;
5692 }
5693 }
5694 static getEditMarkup(keys) {
5695 const allKeys = [];
5696 return [allKeys, `${ Lng.hotKeyEdit[lang].join('')
5697 .replace(/%l/g, '<label class="de-block">')
5698 .replace(/%\/l/g, '</label>')
5699 .replace(/%i([2-4])([0-9]+)(t)?/g, (all, id1, id2, isText) => {
5700 const key = keys[+id1][+id2];
5701 allKeys.push(key);
5702 return `<input class="de-input-key" type="text" de-id1="${ id1 }" de-id2="${ id2 }` +
5703 `" size="16" value="${ KeyEditListener.getStrKey(key) }${
5704 isText ? '" de-text' : '"' } readonly>`;
5705 }) }<input type="button" id="de-keys-save" class="de-button" value="${ Lng.save[lang] }">` +
5706 `<input type="button" id="de-keys-reset" class="de-button" value="${ Lng.reset[lang] }">`];
5707 }
5708 static getStrKey(key) {
5709 return (key & 0x1000 ? 'Ctrl+' : '') +
5710 (key & 0x2000 ? 'Shift+' : '') +
5711 (key & 0x4000 ? 'Alt+' : '') +
5712 KeyEditListener.keyCodes[key & 0xFFF];
5713 }
5714 static setTitle(el, idx) {
5715 let title = el.getAttribute('de-title');
5716 if(!title) {
5717 title = el.getAttribute('title');
5718 el.setAttribute('de-title', title);
5719 }
5720 if(HotKeys.enabled && idx !== -1) {
5721 title += ` [${ KeyEditListener.getStrKey(HotKeys.gKeys[idx]) }]`;
5722 }
5723 el.title = title;
5724 }
5725 get saveButton() {
5726 const value = $id('de-keys-save');
5727 Object.defineProperty(this, 'saveButton', { value, configurable: true });
5728 return value;
5729 }
5730 handleEvent(e) {
5731 let key, el = e.target;
5732 switch(e.type) {
5733 case 'blur':
5734 if(HotKeys.enabled && this.errCount === 0) {
5735 HotKeys.resume(this.keys);
5736 }
5737 el.classList.remove('de-input-selected');
5738 this.cEl = null;
5739 return;
5740 case 'focus':
5741 if(HotKeys.enabled) {
5742 HotKeys.pauseHotKeys();
5743 }
5744 el.classList.add('de-input-selected');
5745 this.cEl = el;
5746 return;
5747 case 'click': {
5748 let keys;
5749 if(el.id === 'de-keys-reset') {
5750 this.keys = HotKeys.getDefaultKeys();
5751 this.initKeys = HotKeys.getDefaultKeys();
5752 if(HotKeys.enabled) {
5753 HotKeys.resume(this.keys);
5754 }
5755 [this.allKeys, this.popupEl.innerHTML] = KeyEditListener.getEditMarkup(this.keys);
5756 this.allInputs = [...$Q('.de-input-key', this.popupEl)];
5757 this.errCount = 0;
5758 delete this.saveButton;
5759 break;
5760 } else if(el.id === 'de-keys-save') {
5761 ({ keys } = this);
5762 setStored('DESU_keys', JSON.stringify(keys));
5763 } else if(el.className === 'de-popup-btn') {
5764 keys = this.initKeys;
5765 } else {
5766 return;
5767 }
5768 if(HotKeys.enabled) {
5769 HotKeys.resume(keys);
5770 }
5771 closePopup('edit-hotkeys');
5772 break;
5773 }
5774 case 'keydown': {
5775 if(!this.cEl) {
5776 return;
5777 }
5778 key = e.keyCode;
5779 if(key === 0x1B || key === 0x2E) { // ESC, DEL
5780 this.cEl.value = '';
5781 this.cKey = 0;
5782 this.errorInput = false;
5783 break;
5784 }
5785 const keyStr = KeyEditListener.keyCodes[key];
5786 if(keyStr === undefined) {
5787 this.cKey = -1;
5788 return;
5789 }
5790 let str = '';
5791 if(e.ctrlKey) {
5792 str += 'Ctrl+';
5793 }
5794 if(e.shiftKey) {
5795 str += 'Shift+';
5796 }
5797 if(e.altKey) {
5798 str += 'Alt+';
5799 }
5800 if(key === 16 || key === 17 || key === 18) {
5801 this.errorInput = true;
5802 this.cKey = 0;
5803 } else {
5804 this.cKey = key | (e.ctrlKey ? 0x1000 : 0) | (e.shiftKey ? 0x2000 : 0) |
5805 (e.altKey ? 0x4000 : 0) | (this.cEl.hasAttribute('de-text') ? 0x8000 : 0);
5806 this.errorInput = false;
5807 str += keyStr;
5808 }
5809 this.cEl.value = str;
5810 break;
5811 }
5812 case 'keyup': {
5813 el = this.cEl;
5814 key = this.cKey;
5815 if(!el || key === -1) {
5816 return;
5817 }
5818 let rEl;
5819 const isError = el.classList.contains('de-input-error');
5820 if(!this.errorInput && key !== -1) {
5821 let idx = this.allInputs.indexOf(el);
5822 const oKey = this.allKeys[idx];
5823 if(oKey === key) {
5824 this.errorInput = false;
5825 break;
5826 }
5827 const rIdx = key === 0 ? -1 : this.allKeys.indexOf(key);
5828 this.allKeys[idx] = key;
5829 if(isError) {
5830 idx = this.allKeys.indexOf(oKey);
5831 if(idx !== -1 && this.allKeys.indexOf(oKey, idx + 1) === -1) {
5832 rEl = this.allInputs[idx];
5833 if(rEl.classList.contains('de-input-error')) {
5834 this.errCount--;
5835 rEl.classList.remove('de-input-error');
5836 }
5837 }
5838 if(rIdx === -1) {
5839 this.errCount--;
5840 el.classList.remove('de-input-error');
5841 }
5842 }
5843 if(rIdx === -1) {
5844 this.keys[+el.getAttribute('de-id1')][+el.getAttribute('de-id2')] = key;
5845 if(this.errCount === 0) {
5846 this.saveButton.disabled = false;
5847 }
5848 this.errorInput = false;
5849 break;
5850 }
5851 rEl = this.allInputs[rIdx];
5852 if(!rEl.classList.contains('de-input-error')) {
5853 this.errCount++;
5854 rEl.classList.add('de-input-error');
5855 }
5856 }
5857 if(!isError) {
5858 this.errCount++;
5859 el.classList.add('de-input-error');
5860 }
5861 if(this.errCount !== 0) {
5862 this.saveButton.disabled = true;
5863 }
5864 }
5865 }
5866 $pd(e);
5867 }
5868}
5869// Browsers have different codes for these keys (see HotKeys.readKeys):
5870// Firefox - '-' - 173, '=' - 61, ';' - 59
5871// Chrome/Opera: '-' - 189, '=' - 187, ';' - 186
5872/* eslint-disable comma-spacing, comma-style, no-sparse-arrays */
5873KeyEditListener.keyCodes = [
5874 '',,,,,,,,'Backspace','Tab',,,,'Enter',,,'Shift','Ctrl','Alt',/* Pause/Break */,/* Caps Lock */,,,,,,,
5875 /* Esc */,,,,,'Space',/* PgUp */,/* PgDn */,/* End */,/* Home */,'←','↑','→','↓',,,,,/* Insert */,
5876 /* Del */,,'0','1','2','3','4','5','6','7','8','9',,';',,'=',,,,'A','B','C','D','E','F','G','H','I','J',
5877 'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',/* Left WIN */,/* Right WIN */,
5878 /* Select */,,,'Num 0','Num 1','Num 2','Num 3','Num 4','Num 5','Num 6','Num 7','Num 8','Num 9','Num *',
5879 'Num +',,'Num -','Num .','Num /',/* F1 */,/* F2 */,/* F3 */,/* F4 */,/* F5 */,/* F6 */,/* F7 */,/* F8 */,
5880 /* F9 */,/* F10 */,/* F11 */,/* F12 */,,,,,,,,,,,,,,,,,,,,,/* Num Lock */,/* Scroll Lock */,,,,,,,,,,,,,,,
5881 ,,,,,,,,,,,,,'-',,,,,,,,,,,,,';','=',',','-','.','/','`',,,,,,,,,,,,,,,,,,,,,,,,,,,'[','\\',']',"'"
5882];
5883/* eslint-enable comma-spacing, comma-style, no-sparse-arrays */
5884
5885/* ==[ ContentLoad.js ]=======================================================================================
5886 CONTENT DOWNLOADING
5887 images/video preloading, rarjpeg detecting, thread/images downloading
5888=========================================================================================================== */
5889
5890const ContentLoader = {
5891 afterFn : null,
5892 isLoading : false,
5893 popupId : null,
5894 downloadThread(imgOnly) {
5895 let progress, counter, current = 1,
5896 warnings = '',
5897 tar = new TarBuilder();
5898 const dc = imgOnly ? doc : doc.documentElement.cloneNode(true);
5899 let els = [...$Q(aib.qPostImg, $q('[de-form]', dc))];
5900 let count = els.length;
5901 this._thrPool = new TasksPool(4, (num, data) => this.loadImgData(data[0]).then(imgData => {
5902 const [url, fName, el, imgLink] = data;
5903 let safeName = fName.replace(/[\\/:*?"<>|]/g, '_');
5904 progress.value = counter.innerHTML = current++;
5905 if(imgLink) {
5906 let thumbName = safeName.replace(/\.[a-z]+$/, '.png');
5907 if(imgOnly) {
5908 thumbName = 'thumb-' + thumbName;
5909 } else {
5910 thumbName = 'thumbs/' + thumbName;
5911 safeName = imgData ? 'images/' + safeName : thumbName;
5912 imgLink.href = $q('a[de-href], ' + aib.qImgNameLink, aib.getImgWrap(el)).href = safeName;
5913 }
5914 if(imgData) {
5915 tar.addFile(safeName, imgData);
5916 } else {
5917 warnings += `<br>${ Lng.cantLoad[lang] } <a href="${ url }">${ url }</a>` +
5918 `<br>${ Lng.willSavePview[lang] }`;
5919 $popup('err-files', Lng.loadErrors[lang] + warnings);
5920 if(imgOnly) {
5921 return this.getDataFromImg(el).then(data => tar.addFile(thumbName, data), emptyFn);
5922 }
5923 }
5924 return imgOnly ? null : this.getDataFromImg(el).then(data => {
5925 el.src = thumbName;
5926 tar.addFile(thumbName, data);
5927 }, () => (el.src = safeName));
5928 } else if(imgData && imgData.length > 0) {
5929 tar.addFile(el.href = el.src = 'data/' + safeName, imgData);
5930 } else {
5931 $del(el);
5932 }
5933 }), () => {
5934 const docName = `${ aib.dm }-${ aib.b.replace(/[\\/:*?"<>|]/g, '') }-${ aib.t }`;
5935 if(!imgOnly) {
5936 $q('head', dc).insertAdjacentHTML('beforeend',
5937 '<script type="text/javascript" src="data/dollscript.js" charset="utf-8"></script>');
5938 const dcBody = $q('body', dc);
5939 dcBody.classList.remove('de-runned');
5940 dcBody.classList.add('de-mode-local');
5941 $delAll('#de-css, #de-css-dynamic, #de-css-user', dc);
5942 tar.addString('data/dollscript.js', `${ nav.isESNext ?
5943 `(${ String(deMainFuncInner) })(window, null, null, (x, y) => window.scrollTo(x, y), ` :
5944 `(${ String(/* global deMainFuncOuter */ deMainFuncOuter) })(`
5945 }${ JSON.stringify({ dm: aib.dm, b: aib.b, t: aib.t }) });`);
5946 const dt = doc.doctype;
5947 tar.addString(docName + '.html', '<!DOCTYPE ' + dt.name +
5948 (dt.publicId ? ` PUBLIC "${ dt.publicId }"` : dt.systemId ? ' SYSTEM' : '') +
5949 (dt.systemId ? ` "${ dt.systemId }"` : '') + '>' + dc.outerHTML);
5950 }
5951 downloadBlob(tar.get(), docName + (imgOnly ? '-images.tar' : '.tar'));
5952 closePopup('load-files');
5953 this._thrPool = tar = warnings = count = current = imgOnly = progress = counter = null;
5954 });
5955 els.forEach(el => {
5956 const imgLink = $parent(el, 'A');
5957 if(imgLink) {
5958 const url = imgLink.href;
5959 this._thrPool.runTask([url, imgLink.getAttribute('download') ||
5960 url.substring(url.lastIndexOf('/') + 1), el, imgLink]);
5961 }
5962 });
5963 if(!imgOnly) {
5964 $delAll('#de-main, .de-parea, .de-post-btns, .de-btn-src, .de-refmap, .de-thr-buttons, ' +
5965 '.de-video-obj, #de-win-reply, link[rel="alternate stylesheet"], script, ' + aib.qForm, dc);
5966 $each($Q('a', dc), el => {
5967 let num;
5968 const tc = el.textContent;
5969 if(tc[0] === '>' && tc[1] === '>' && (num = +tc.substr(2)) && pByNum.has(num)) {
5970 el.href = aib.anchor + num;
5971 if(!el.classList.contains('de-link-postref')) {
5972 el.className = 'de-link-postref ' + el.className;
5973 }
5974 } else {
5975 el.href = getAbsLink(el.href);
5976 }
5977 });
5978 $each($Q(aib.qRPost, dc), (el, i) => el.setAttribute('de-num', i ? aib.getPNum(el) : aib.t));
5979 const files = [];
5980 const urlRegex = new RegExp(`^\\/\\/?|^https?:\\/\\/([^\\/]*\\.)?${
5981 quoteReg(aib._4chan ? '4cdn.org' : aib.dm) }\\/`, 'i');
5982 $each($Q('link, *[src]', dc), el => {
5983 if(els.indexOf(el) !== -1) {
5984 return;
5985 }
5986 let url = el.tagName === 'LINK' ? el.href : el.src;
5987 if(!urlRegex.test(url)) {
5988 el.remove();
5989 return;
5990 }
5991 let fName = url.substring(url.lastIndexOf('/') + 1)
5992 .replace(/[\\/:*?"<>|]/g, '_').toLowerCase();
5993 if(files.indexOf(fName) !== -1) {
5994 let temp = url.lastIndexOf('.');
5995 const ext = url.substring(temp);
5996 url = url.substring(0, temp);
5997 fName = fName.substring(0, fName.lastIndexOf('.'));
5998 for(let i = 0; ; ++i) {
5999 temp = `${ fName }(${ i })${ ext }`;
6000 if(files.indexOf(temp) === -1) {
6001 break;
6002 }
6003 }
6004 fName = temp;
6005 }
6006 files.push(fName);
6007 this._thrPool.runTask([url, fName, el, null]);
6008 count++;
6009 });
6010 }
6011 $popup('load-files', `${ imgOnly ? Lng.loadImage[lang] : Lng.loadFile[lang] }:<br><progress ` +
6012 `id="de-loadprogress" value="0" max="${ count }"></progress> <span>1</span>/${ count }`, true);
6013 progress = $id('de-loadprogress');
6014 counter = progress.nextElementSibling;
6015 this._thrPool.completeTasks();
6016 els = null;
6017 },
6018 getDataFromImg(el) {
6019 try {
6020 const cnv = this._canvas || (this._canvas = doc.createElement('canvas'));
6021 cnv.width = el.width || el.videoWidth;
6022 cnv.height = el.height || el.videoHeight;
6023 cnv.getContext('2d').drawImage(el, 0, 0);
6024 return Promise.resolve(new Uint8Array(atob(cnv.toDataURL('image/png').split(',')[1])
6025 .split('').map(a => a.charCodeAt())));
6026 } catch(err) {
6027 return this.loadImgData(el.src);
6028 }
6029 },
6030 loadImgData: (url, repeatOnError = true) => $ajax(
6031 url, { responseType: 'arraybuffer' }, !url.startsWith('blob')
6032 ).then(xhr => {
6033 if('response' in xhr) {
6034 try {
6035 return nav.getUnsafeUint8Array(xhr.response);
6036 } catch(err) {}
6037 }
6038 const txt = xhr.responseText;
6039 return new Uint8Array(txt.length).map((val, i) => txt.charCodeAt(i) & 0xFF);
6040 }, err => err.code !== 404 && repeatOnError ? ContentLoader.loadImgData(url, false) : null),
6041 preloadImages(data) {
6042 if(!Cfg.preLoadImgs && !Cfg.openImgs && !isPreImg) {
6043 return;
6044 }
6045 let preloadPool;
6046 const isPost = data instanceof AbstractPost;
6047 const els = $Q(aib.qPostImg, isPost ? data.el : data);
6048 const len = els.length;
6049 if(isPreImg || Cfg.preLoadImgs) {
6050 let cImg = 1;
6051 const mReqs = isPost ? 1 : 4;
6052 const rarJpgFinder = (isPreImg || Cfg.findImgFile) && new WorkerPool(mReqs, this._detectImgFile,
6053 err => console.error('File detector error:', `line: ${ err.lineno } - ${ err.message }`));
6054 preloadPool = new TasksPool(mReqs, (num, data) => this.loadImgData(data[0]).then(imageData => {
6055 const [url, imgLink, iType, isRepToOrig, el, isVideo] = data;
6056 if(imageData) {
6057 const fName = url.substring(url.lastIndexOf('/') + 1);
6058 const nameLink = $q(aib.qImgNameLink, aib.getImgWrap(el));
6059 imgLink.setAttribute('download', fName);
6060 if(!Cfg.imgNames) {
6061 nameLink.setAttribute('download', fName);
6062 nameLink.setAttribute('de-href', nameLink.href);
6063 }
6064 imgLink.href = nameLink.href =
6065 deWindow.URL.createObjectURL(new Blob([imageData], { type: iType }));
6066 if(isVideo) {
6067 el.setAttribute('de-video', '');
6068 }
6069 if(isRepToOrig) {
6070 el.src = imgLink.href;
6071 }
6072 if(rarJpgFinder) {
6073 rarJpgFinder.runWorker(imageData.buffer, [imageData.buffer],
6074 info => this._addImgFileIcon(nameLink, fName, info));
6075 }
6076 }
6077 if(this.popupId) {
6078 $popup(this.popupId, `${ Lng.loadImage[lang] }: ${ cImg }/${ len }`, true);
6079 }
6080 cImg++;
6081 }), () => {
6082 this.isLoading = false;
6083 if(this.afterFn) {
6084 this.afterFn();
6085 this.afterFn = this.popupId = null;
6086 }
6087 if(rarJpgFinder) {
6088 rarJpgFinder.clearWorkers();
6089 }
6090 });
6091 this.isLoading = true;
6092 }
6093 for(let i = 0; i < len; ++i) {
6094 const el = els[i];
6095 const imgLink = $parent(el, 'A');
6096 if(!imgLink) {
6097 continue;
6098 }
6099 let isRepToOrig = !!Cfg.openImgs;
6100 const url = imgLink.href;
6101 const type = getFileType(url);
6102 const isVideo = type && (type === 'video/webm' || type === 'video/mp4' || type === 'video/ogv');
6103 if(!type || isVideo && Cfg.preLoadImgs === 2) {
6104 continue;
6105 } else if(type === 'image/gif') {
6106 isRepToOrig &= Cfg.openImgs !== 3;
6107 } else {
6108 if(isVideo) {
6109 isRepToOrig = false;
6110 }
6111 isRepToOrig &= Cfg.openImgs !== 2;
6112 }
6113 if(preloadPool) {
6114 preloadPool.runTask([url, imgLink, type, isRepToOrig, el, isVideo]);
6115 } else if(isRepToOrig) {
6116 el.src = url;
6117 }
6118 }
6119 if(preloadPool) {
6120 preloadPool.completeTasks();
6121 }
6122 },
6123
6124 _canvas : null,
6125 _thrPool : null,
6126 _addImgFileIcon(nameLink, fName, info) {
6127 const { type } = info;
6128 if(typeof type === 'undefined') {
6129 return;
6130 }
6131 const ext = ['7z', 'zip', 'rar', 'ogg', 'mp3'][type];
6132 nameLink.insertAdjacentHTML('afterend', `<a href="${ deWindow.URL.createObjectURL(
6133 new Blob([nav.getUnsafeUint8Array(info.data, info.idx)], {
6134 type: [
6135 'application/x-7z-compressed',
6136 'application/zip',
6137 'application/x-rar-compressed',
6138 'audio/ogg',
6139 'audio/mpeg'][type]
6140 })
6141 ) }" class="de-img-${ type > 2 ? 'audio' : 'arch' }" title="${ Lng.downloadFile[lang] }" download="${
6142 fName.substring(0, fName.lastIndexOf('.')) }.${ ext }">.${ ext }</a>`);
6143 },
6144 // Finds built-in files in jpg and png
6145 _detectImgFile: arrBuf => {
6146 let i, j;
6147 const dat = new Uint8Array(arrBuf);
6148 let len = dat.length;
6149 /* JPG [ff d8 ff e0] = [яШяа] */
6150 if(dat[0] === 0xFF && dat[1] === 0xD8) {
6151 for(i = 0, j = 0; i < len - 1; ++i) {
6152 if(dat[i] === 0xFF) {
6153 /* Built-in JPG */
6154 if(dat[i + 1] === 0xD8) {
6155 j++;
6156 /* JPG end [ff d9] */
6157 } else if(dat[i + 1] === 0xD9 && --j === 0) {
6158 i += 2;
6159 break;
6160 }
6161 }
6162 }
6163 /* PNG [89 50 4e 47] = [‰PNG] */
6164 } else if(dat[0] === 0x89 && dat[1] === 0x50) {
6165 for(i = 0; i < len - 7; ++i) {
6166 /* PNG end [49 45 4e 44 ae 42 60 82] */
6167 if(dat[i] === 0x49 && dat[i + 1] === 0x45 && dat[i + 2] === 0x4E && dat[i + 3] === 0x44) {
6168 i += 8;
6169 break;
6170 }
6171 }
6172 } else {
6173 return {};
6174 }
6175 if(i === len || len - i <= 60) { // Ignore small files (<60 bytes)
6176 return {};
6177 }
6178 for(len = i + 90; i < len; ++i) {
6179 /* 7Z [37 7a bc af] = [7zјЇ] */
6180 if(dat[i] === 0x37 && dat[i + 1] === 0x7A && dat[i + 2] === 0xBC) {
6181 return { type: 0, idx: i, data: arrBuf };
6182 /* ZIP [50 4b 03 04] = [PK..] */
6183 } else if(dat[i] === 0x50 && dat[i + 1] === 0x4B && dat[i + 2] === 0x03) {
6184 return { type: 1, idx: i, data: arrBuf };
6185 /* RAR [52 61 72 21] = [Rar!] */
6186 } else if(dat[i] === 0x52 && dat[i + 1] === 0x61 && dat[i + 2] === 0x72) {
6187 return { type: 2, idx: i, data: arrBuf };
6188 /* OGG [4f 67 67 53] = [OggS] */
6189 } else if(dat[i] === 0x4F && dat[i + 1] === 0x67 && dat[i + 2] === 0x67) {
6190 return { type: 3, idx: i, data: arrBuf };
6191 /* MP3 [0x49 0x44 0x33] = [ID3] */
6192 } else if(dat[i] === 0x49 && dat[i + 1] === 0x44 && dat[i + 2] === 0x33) {
6193 return { type: 4, idx: i, data: arrBuf };
6194 }
6195 }
6196 return {};
6197 }
6198};
6199
6200/* ==[ TimeCorrection.js ]====================================================================================
6201 TIME CORRECTION
6202=========================================================================================================== */
6203
6204class DateTime {
6205 constructor(pattern, rPattern, diff, dtLang, onRPat) {
6206 this.pad2 = pad2;
6207 this.genDateTime = null;
6208 this.onRPat = null;
6209 if(DateTime.checkPattern(pattern)) {
6210 this.disabled = true;
6211 return;
6212 }
6213 this.regex = pattern
6214 .replace(/(?:[sihdny]\?){2,}/g, str => `(?:${ str.replace(/\?/g, '') })?`)
6215 .replace(/-/g, '[^<]')
6216 .replace(/\+/g, '[^0-9<]')
6217 .replace(/([sihdny]+)/g, '($1)')
6218 .replace(/[sihdny]/g, '\\d')
6219 .replace(/m|w/g, '([a-zA-Zа-яА-Я]+)');
6220 this.pattern = pattern.replace(/[?\-+]+/g, '').replace(/([a-z])\1+/g, '$1');
6221 this.diff = parseInt(diff, 10);
6222 this.arrW = Lng.week[dtLang];
6223 this.arrM = Lng.month[dtLang];
6224 this.arrFM = Lng.fullMonth[dtLang];
6225 if(rPattern) {
6226 this.genDateTime = this.genRFunc(rPattern);
6227 } else {
6228 this.onRPat = onRPat;
6229 }
6230 }
6231 static checkPattern(val) {
6232 return !val.includes('i') || !val.includes('h') || !val.includes('d') ||
6233 !val.includes('y') || !(val.includes('n') || val.includes('m')) ||
6234 /[^?\-+sihdmwny]|mm|ww|\?\?|([ihdny]\?)\1+/.test(val);
6235 }
6236 static toggleSettings(el) {
6237 if(el.checked && (!/^[+-]\d{1,2}$/.test(Cfg.timeOffset) || DateTime.checkPattern(Cfg.timePattern))) {
6238 $popup('err-correcttime', Lng.cTimeError[lang]);
6239 saveCfg('correctTime', 0);
6240 el.checked = false;
6241 }
6242 }
6243 genRFunc(rPattern) {
6244 return dtime => rPattern.replace('_o', (this.diff < 0 ? '' : '+') + this.diff)
6245 .replace('_s', () => this.pad2(dtime.getSeconds()))
6246 .replace('_i', () => this.pad2(dtime.getMinutes()))
6247 .replace('_h', () => this.pad2(dtime.getHours()))
6248 .replace('_d', () => this.pad2(dtime.getDate()))
6249 .replace('_w', () => this.arrW[dtime.getDay()])
6250 .replace('_n', () => this.pad2(dtime.getMonth() + 1))
6251 .replace('_m', () => this.arrM[dtime.getMonth()])
6252 .replace('_M', () => this.arrFM[dtime.getMonth()])
6253 .replace('_y', () => ('' + dtime.getFullYear()).substring(2))
6254 .replace('_Y', () => dtime.getFullYear());
6255 }
6256 getRPattern(txt) {
6257 const m = txt.match(new RegExp(this.regex));
6258 if(!m) {
6259 this.disabled = true;
6260 return false;
6261 }
6262 let rPattern = '';
6263 for(let i = 1, len = m.length, j = 0, str = m[0]; i < len;) {
6264 const a = m[i++];
6265 if(!a) {
6266 continue;
6267 }
6268 let p = this.pattern[i - 2];
6269 if((p === 'm' || p === 'y') && a.length > 3) {
6270 p = p.toUpperCase();
6271 }
6272 const k = str.indexOf(a, j);
6273 rPattern += str.substring(j, k) + '_' + p;
6274 j = k + a.length;
6275 }
6276 if(this.onRPat) {
6277 this.onRPat(rPattern);
6278 }
6279 this.genDateTime = this.genRFunc(rPattern);
6280 return true;
6281 }
6282 fix(txt) {
6283 if(this.disabled || (!this.genDateTime && !this.getRPattern(txt))) {
6284 return txt;
6285 }
6286 return txt.replace(new RegExp(this.regex, 'g'), (str, ...args) => {
6287 let second, minute, hour, day, month, year;
6288 for(let i = 0; i < 7; ++i) {
6289 const a = args[i];
6290 switch(this.pattern[i]) {
6291 case 's': second = a; break;
6292 case 'i': minute = a; break;
6293 case 'h': hour = a; break;
6294 case 'd': day = a; break;
6295 case 'n': month = a - 1; break;
6296 case 'y': year = a; break;
6297 case 'm': month = Lng.monthDict[a.slice(0, 3).toLowerCase()] || 0; break;
6298 }
6299 }
6300 const dtime = new Date(year.length === 2 ? '20' + year :
6301 year, month, day, hour, minute, second || 0);
6302 dtime.setHours(dtime.getHours() + this.diff);
6303 return this.genDateTime(dtime);
6304 });
6305 }
6306}
6307
6308/* ==[ Players.js ]===========================================================================================
6309 PLAYERS / LINKS EMBEDDERS
6310 youtube, vimeo, mp3, vocaroo embedding players
6311=========================================================================================================== */
6312
6313class Videos {
6314 constructor(post, player = null, playerInfo = null) {
6315 this.currentLink = null;
6316 this.hasLinks = false;
6317 this.linksCount = 0;
6318 this.loadedLinksCount = 0;
6319 this.playerInfo = null;
6320 this.post = post;
6321 this.titleLoadFn = null;
6322 this.vData = [[], []];
6323 if(player && playerInfo) {
6324 Object.defineProperty(this, 'player', { value: player });
6325 this.playerInfo = playerInfo;
6326 }
6327 }
6328 static addPlayer(obj, m, isYtube, enableJsapi = false) {
6329 const el = obj.player;
6330 obj.playerInfo = m;
6331 let txt;
6332 if(isYtube) {
6333 const list = m[0].match(/list=[^&#]+/);
6334 txt = `<iframe class="de-video-player" src="https://www.youtube.com/embed/${ m[1] }?start=` +
6335 ((m[2] ? m[2] * 3600 : 0) + (m[3] ? m[3] * 60 : 0) + (m[4] ? +m[4] : 0)) +
6336 (enableJsapi ? '&enablejsapi=1' : Cfg.embedYTube === 1 ? '&autoplay=1' : '') +
6337 (list ? '&' + list[0] : '') + '" frameborder="0" allowfullscreen></iframe>';
6338 } else {
6339 const id = m[1] + (m[2] ? m[2] : '');
6340 txt = `<iframe class="de-video-player" src="${ aib.prot }//player.vimeo.com/video/${ id }${
6341 Cfg.embedYTube === 1 ? '?autoplay=1' : ''
6342 }" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>`;
6343 }
6344 el.innerHTML = txt + (enableJsapi ? '' :
6345 `<span class="de-video-resizer" title="${ Lng.expandVideo[lang] }"></span>`);
6346 $show(el);
6347 if(!enableJsapi) {
6348 el.lastChild.onclick = e => e.target.parentNode.classList.toggle('de-video-expanded');
6349 }
6350 }
6351 static setLinkData(link, data, isCloned = false) {
6352 const [title, author, views, publ, duration] = data;
6353 if(Panel.isVidEnabled && !isCloned) {
6354 const clonedLink = $q(`.de-entry > .de-video-link[href="${ link.href }"]:not(title)`);
6355 if(clonedLink) {
6356 Videos.setLinkData(clonedLink, data, true);
6357 }
6358 }
6359 link.textContent = title;
6360 link.classList.add('de-video-title');
6361 link.setAttribute('de-author', author);
6362 link.title = (duration ? Lng.duration[lang] + duration : '') +
6363 (publ ? `, ${ Lng.published[lang] + publ }\n` : '') +
6364 Lng.author[lang] + author + (views ? ', ' + Lng.views[lang] + views : '');
6365 }
6366 get player() {
6367 const { post } = this;
6368 const value = aib.insertYtPlayer(post.msg, `<div class="de-video-obj${
6369 post.images.hasAttachments && !post.isOp ? ' de-video-obj-inline' : '' }"></div>`);
6370 Object.defineProperty(this, 'player', { value });
6371 return value;
6372 }
6373 addLink(m, loader, link, isYtube) {
6374 this.hasLinks = true;
6375 this.linksCount++;
6376 if(this.playerInfo === null) {
6377 if(Cfg.embedYTube === 1) {
6378 this._addThumb(m, isYtube);
6379 }
6380 } else if(!link && $q(`.de-video-link[href*="${ m[1] }"]`, this.post.msg)) {
6381 return;
6382 }
6383 let dataObj;
6384 if(loader && (dataObj = Videos._global.vData[+!isYtube][m[1]])) {
6385 this.vData[+!isYtube].push(dataObj);
6386 }
6387 let time = '';
6388 [time, m[2], m[3], m[4]] = Videos._fixTime(m[4], m[3], m[2]);
6389 if(link) {
6390 link.href = link.href.replace(/^http:/, 'https:');
6391 if(time) {
6392 link.setAttribute('de-time', time);
6393 }
6394 link.className = `de-video-link ${ isYtube ? 'de-ytube' : 'de-vimeo' }`;
6395 } else {
6396 const src = isYtube ?
6397 `${ aib.prot }//www.youtube.com/watch?v=${ m[1] }${ time ? '#t=' + time : '' }` :
6398 `${ aib.prot }//vimeo.com/${ m[1] }`;
6399 link = $bEnd(this.post.msg, `<p class="de-video-ext"><a class="de-video-link ${
6400 isYtube ? 'de-ytube' : 'de-vimeo' }${ time ? '" de-time="' + time : ''
6401 }" href="${ src }">${ dataObj ? '' : src }</a></p>`).firstChild;
6402 }
6403 if(dataObj) {
6404 Videos.setLinkData(link, dataObj);
6405 }
6406 if(this.playerInfo === null || this.playerInfo === m) {
6407 this.currentLink = link;
6408 }
6409 link.videoInfo = m;
6410 let vidListEl;
6411 if(Panel.isVidEnabled && (vidListEl = $id('de-video-list'))) {
6412 updateVideoList(vidListEl, link, this.post.num);
6413 }
6414 if(loader && !dataObj) {
6415 loader.runTask([link, isYtube, this, m[1]]);
6416 }
6417 }
6418 clickLink(el, mode) {
6419 const m = el.videoInfo;
6420 if(this.playerInfo !== m) {
6421 this.currentLink.classList.remove('de-current');
6422 this.currentLink = el;
6423 if(mode === 1) {
6424 this._addThumb(m, el.classList.contains('de-ytube'));
6425 } else {
6426 el.classList.add('de-current');
6427 this.setPlayer(m, el.classList.contains('de-ytube'));
6428 }
6429 return;
6430 }
6431 if(mode === 1) {
6432 if($q('.de-video-thumb', this.player)) {
6433 el.classList.add('de-current');
6434 this.setPlayer(m, el.classList.contains('de-ytube'));
6435 } else {
6436 el.classList.remove('de-current');
6437 this._addThumb(m, el.classList.contains('de-ytube'));
6438 }
6439 } else {
6440 el.classList.remove('de-current');
6441 $hide(this.player);
6442 this.player.innerHTML = '';
6443 this.playerInfo = null;
6444 }
6445 }
6446 setPlayer(m, isYtube) {
6447 Videos.addPlayer(this, m, isYtube);
6448 }
6449 toggleFloatedThumb(linkEl, isOutEvent) {
6450 let el = $id('de-video-thumb-floated');
6451 if(isOutEvent) {
6452 $del(el);
6453 return;
6454 }
6455 if(!el) {
6456 el = $bEnd(docBody, `<img id="de-video-thumb-floated" src="https://i.ytimg.com/vi/${
6457 linkEl.videoInfo[1] }/0.jpg">`);
6458 }
6459 const cr = linkEl.getBoundingClientRect();
6460 const pvHeight = Cfg.YTubeHeigh;
6461 const isTop = cr.top + cr.height + pvHeight < nav.viewportHeight();
6462 el.style.cssText = `position: absolute; left: ${ deWindow.pageXOffset + cr.left }px; top: ${
6463 deWindow.pageYOffset + (isTop ? cr.top + cr.height : cr.top - pvHeight) }px; width: ${
6464 Cfg.YTubeWidth }px; height: ${ pvHeight }px; z-index: 9999;`;
6465 }
6466 updatePost(oldLinks, newLinks, cloned) {
6467 const loader = !cloned && Videos._getTitlesLoader();
6468 let j = 0;
6469 for(let i = 0, len = newLinks.length; i < len; ++i) {
6470 const el = newLinks[i];
6471 const link = oldLinks[j];
6472 if(link && link.classList.contains('de-current')) {
6473 this.currentLink = el;
6474 }
6475 if(cloned) {
6476 el.videoInfo = link.videoInfo;
6477 j++;
6478 } else {
6479 const m = el.href.match(Videos.ytReg);
6480 if(m) {
6481 this.addLink(m, loader, el, true);
6482 j++;
6483 }
6484 }
6485 }
6486 this.currentLink = this.currentLink || newLinks[0];
6487 if(loader) {
6488 loader.completeTasks();
6489 }
6490 }
6491
6492 static _fixTime(seconds = 0, minutes = 0, hours = 0) {
6493 if(seconds >= 60) {
6494 minutes += Math.floor(seconds / 60);
6495 seconds %= 60;
6496 }
6497 if(minutes >= 60) {
6498 hours += Math.floor(seconds / 60);
6499 minutes %= 60;
6500 }
6501 return [
6502 (hours ? hours + 'h' : '') +
6503 (minutes ? minutes + 'm' : '') +
6504 (seconds ? seconds + 's' : ''),
6505 hours, minutes, seconds
6506 ];
6507 }
6508 static _getTitlesLoader() {
6509 return Cfg.YTubeTitles && new TasksPool(4, (num, info) => {
6510 const [, isYtube,, id] = info;
6511 if(isYtube) {
6512 return Cfg.ytApiKey ? Videos._getYTInfoAPI(info, num, id) :
6513 Videos._getYTInfoOembed(info, num, id);
6514 }
6515 return $ajax(`${ aib.prot }//vimeo.com/api/v2/video/${ id }.json`, null, true).then(xhr => {
6516 const entry = JSON.parse(xhr.responseText)[0];
6517 return Videos._titlesLoaderHelper(
6518 info, num,
6519 entry.title,
6520 entry.user_name,
6521 entry.stats_number_of_plays,
6522 /(.*)\s(.*)?/.exec(entry.upload_date)[1],
6523 Videos._fixTime(entry.duration)[0]);
6524 }).catch(() => Videos._titlesLoaderHelper(info, num));
6525 }, () => (sesStorage['de-videos-data2'] = JSON.stringify(Videos._global.vData)));
6526 }
6527 static _getYTInfoAPI(info, num, id) {
6528 return $ajax(
6529 `https://www.googleapis.com/youtube/v3/videos?key=${ Cfg.ytApiKey }&id=${ id }` +
6530 '&part=snippet,statistics,contentDetails&fields=items/snippet/title,items/snippet/publishedAt,' +
6531 'items/snippet/channelTitle,items/statistics/viewCount,items/contentDetails/duration',
6532 null, true
6533 ).then(xhr => {
6534 const items = JSON.parse(xhr.responseText).items[0];
6535 return Videos._titlesLoaderHelper(
6536 info, num,
6537 items.snippet.title,
6538 items.snippet.channelTitle,
6539 items.statistics.viewCount,
6540 items.snippet.publishedAt.substr(0, 10),
6541 items.contentDetails.duration.substr(2).toLowerCase());
6542 }).catch(() => Videos._getYTInfoOembed(info, num, id));
6543 }
6544 static _getYTInfoOembed(info, num, id) {
6545 const canSendCORS = nav.hasGMXHR || nav.canUseFetch;
6546 return (canSendCORS ?
6547 $ajax(`https://www.youtube.com/oembed?url=http%3A//youtube.com/watch%3Fv%3D${
6548 id }&format=json`, null, true) :
6549 $ajax(`https://noembed.com/embed?url=http%3A//youtube.com/watch%3Fv%3D${ id }&callback=?`)
6550 ).then(xhr => {
6551 const res = xhr.responseText;
6552 const json = JSON.parse(canSendCORS ? res : res.replace(/^[^{]+|\)$/g, ''));
6553 return Videos._titlesLoaderHelper(info, num, json.title, json.author_name, null, null, null);
6554 }).catch(() => Videos._titlesLoaderHelper(info, num));
6555 }
6556 static _titlesLoaderHelper([link, isYtube, videoObj, id], num, ...data) {
6557 if(data.length !== 0) {
6558 Videos.setLinkData(link, data);
6559 Videos._global.vData[+!isYtube][id] = data;
6560 videoObj.vData[+!isYtube].push(data);
6561 if(videoObj.titleLoadFn) {
6562 videoObj.titleLoadFn(data);
6563 }
6564 }
6565 videoObj.loadedLinksCount++;
6566 // Wait for 3 sec every 30 links
6567 if(num % 30 === 0) {
6568 return Promise.reject(new TasksPool.PauseError(3e3));
6569 }
6570 return new Promise(resolve => setTimeout(resolve, 250));
6571 }
6572 _addThumb(m, isYtube) {
6573 const el = this.player;
6574 this.playerInfo = m;
6575 el.classList.remove('de-video-expanded');
6576 $show(el);
6577 const str = `<a class="de-video-player" href="${ aib.prot }`;
6578 if(isYtube) {
6579 el.innerHTML = `${ str }//www.youtube.com/watch?v=${ m[1] }" target="_blank">` +
6580 `<img class="de-video-thumb de-ytube" src="https://i.ytimg.com/vi/${ m[1] }/0.jpg"></a>`;
6581 return;
6582 }
6583 el.innerHTML = `${ str }//vimeo.com/${ m[1] }" target="_blank">` +
6584 '<img class="de-video-thumb de-vimeo" src=""></a>';
6585 $ajax(`${ aib.prot }//vimeo.com/api/v2/video/${ m[1] }.json`, null, true).then(xhr => {
6586 el.firstChild.firstChild.setAttribute('src', JSON.parse(xhr.responseText)[0].thumbnail_large);
6587 }).catch(emptyFn);
6588 }
6589}
6590Videos.ytReg =
6591 /^https?:\/\/(?:www\.|m\.)?youtu(?:be\.com\/(?:watch\?.*?v=|v\/|embed\/)|\.be\/)([a-zA-Z0-9-_]+).*?(?:t(?:ime)?=(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?)?$/;
6592Videos.vimReg =
6593 /^https?:\/\/(?:www\.)?vimeo\.com\/(?:[^?]+\?clip_id=|.*?\/)?(\d+).*?(#t=\d+)?$/;
6594Videos._global = {
6595 get vData() {
6596 let value;
6597 try {
6598 value = Cfg.YTubeTitles ? JSON.parse(sesStorage['de-videos-data2'] || '[{}, {}]') : [{}, {}];
6599 } catch(err) {
6600 value = [{}, {}];
6601 }
6602 Object.defineProperty(this, 'vData', { value });
6603 return value;
6604 }
6605};
6606
6607class VideosParser {
6608 constructor() {
6609 this._loader = Videos._getTitlesLoader();
6610 }
6611 endParser() {
6612 if(this._loader) {
6613 this._loader.completeTasks();
6614 }
6615 }
6616 parse(data) {
6617 const isPost = data instanceof AbstractPost;
6618 const loader = this._loader;
6619 VideosParser._parserHelper('a[href*="youtu"]', data, loader, isPost, true, Videos.ytReg);
6620 if(Cfg.addVimeo) {
6621 VideosParser._parserHelper('a[href*="vimeo.com"]', data, loader, isPost, false, Videos.vimReg);
6622 }
6623 const vids = aib.fixVideo(isPost, data);
6624 for(let i = 0, len = vids.length; i < len; ++i) {
6625 const [post, m, isYtube] = vids[i];
6626 if(post) {
6627 post.videos.addLink(m, loader, null, isYtube);
6628 }
6629 }
6630 return this;
6631 }
6632
6633 static _parserHelper(qPath, data, loader, isPost, isYtube, reg) {
6634 const links = $Q(qPath, isPost ? data.el : data);
6635 for(let i = 0, len = links.length; i < len; ++i) {
6636 const link = links[i];
6637 const m = link.href.match(reg);
6638 if(m) {
6639 const mPost = isPost ? data : aib.getPostOfEl(link);
6640 if(mPost) {
6641 mPost.videos.addLink(m, loader, link, isYtube);
6642 }
6643 }
6644 }
6645 }
6646}
6647
6648// Embed .mp3 and Vocaroo links
6649function embedAudioLinks(data) {
6650 const isPost = data instanceof AbstractPost;
6651 if(Cfg.addMP3) {
6652 const els = $Q('a[href*=".mp3"]', isPost ? data.el : data);
6653 for(let i = 0, len = els.length; i < len; ++i) {
6654 const link = els[i];
6655 if((link.target !== '_blank' && link.rel !== 'nofollow') || !link.pathname.includes('.mp3')) {
6656 continue;
6657 }
6658 const src = link.href;
6659 const el = (isPost ? data : aib.getPostOfEl(link)).mp3Obj;
6660 if(nav.canPlayMP3) {
6661 if(!$q(`audio[src="${ src }"]`, el)) {
6662 el.insertAdjacentHTML('beforeend',
6663 `<p><audio src="${ src }" preload="none" controls></audio></p>`);
6664 }
6665 // Flash plugin for old browsers that not support HTML5 audio
6666 } else if(!$q(`object[FlashVars*="${ src }"]`, el)) {
6667 el.insertAdjacentHTML('beforeend', '<object data="' +
6668 'http://junglebook2007.narod.ru/audio/player.swf" type="application/x-shockwave-flash" ' +
6669 'wmode="transparent" width="220" height="16" FlashVars="playerID=1&' +
6670 'bg=0x808080&leftbg=0xB3B3B3&lefticon=0x000000&rightbg=0x808080&' +
6671 'rightbghover=0x999999&rightcon=0x000000&righticonhover=0xffffff&' +
6672 'text=0xffffff&slider=0x222222&track=0xf5f5dc&border=0x666666&' +
6673 `loader=0x7fc7ff&loop=yes&autostart=no&soundFile=${ src }"><br>`);
6674 }
6675 }
6676 }
6677 if(Cfg.addVocaroo) {
6678 const els = $Q('a[href*="vocaroo.com"]', isPost ? data.el : data);
6679 for(let i = 0, len = els.length; i < len; ++i) {
6680 const link = els[i];
6681 const el = link.previousSibling;
6682 if(!el || el.className !== 'de-vocaroo') { // Don't embed already embedded links
6683 link.insertAdjacentHTML('beforebegin', `<div class="de-vocaroo">
6684 <embed src="http://vocaroo.com/player.swf?playMediaID=${ link.href.split('/').pop() }` +
6685 `" width="148" height="44" wmode="transparent" type="application/x-shockwave-flash">
6686 </div>`);
6687 }
6688 }
6689 }
6690}
6691
6692/* ==[ Ajax.js ]==============================================================================================
6693 AJAX FUNCTIONS
6694=========================================================================================================== */
6695
6696// Main AJAX util
6697function $ajax(url, params = null, isCORS = false) {
6698 let resolve, reject, cancelFn;
6699 const needTO = params ? params.useTimeout : false;
6700 const WAITING_TIME = 5e3;
6701 if(((isCORS ? !nav.hasGMXHR : !nav.canUseNativeXHR) || aib.hasRefererErr && nav.canUseFetch) &&
6702 (nav.canUseFetchBlob || !url.startsWith('blob'))
6703 ) {
6704 if(!params) {
6705 params = {};
6706 }
6707 params.referrer =
6708 doc.referrer.startsWith(aib.prot + '//' + aib.host) ? doc.referrer : deWindow.location;
6709 if(params.data) {
6710 params.body = params.data;
6711 delete params.data;
6712 }
6713 if(isCORS) {
6714 params.mode = 'cors';
6715 }
6716 url = getAbsLink(url);
6717 // Chrome-extension: avoid CORS in content script. Sending data to background.js
6718 if(isCORS && nav.isChrome && nav.scriptHandler === 'WebExtension') {
6719 if(params.body) {
6720 // Converting image as Uint8Array to text data for sending in POST request from background.js
6721 let textData = '';
6722 const arrData = params.body.arr;
6723 for(let i = 0, len = arrData.length; i < len; ++i) {
6724 textData += String.fromCharCode(arrData[i]);
6725 }
6726 params.body.arr = textData;
6727 }
6728 chrome.runtime.sendMessage({ 'de-messsage': 'corsRequest', url, params }, res => {
6729 const { answer } = res;
6730 if(res.isError || !aib.isAjaxStatusOK(res.status)) {
6731 reject(res.statusText ?
6732 new AjaxError(res.status, res.statusText) : getErrorMessage(answer));
6733 return;
6734 }
6735 const obj = {};
6736 switch(params.responseType) {
6737 case 'arraybuffer':
6738 case 'blob': { // Converting text data from the background.js response to arraybuffer/blob
6739 const buf = new ArrayBuffer(answer.length);
6740 const bufView = new Uint8Array(buf);
6741 for(let i = 0, len = answer.length; i < len; ++i) {
6742 bufView[i] = answer.charCodeAt(i);
6743 }
6744 obj.response = params.responseType === 'blob' ? new Blob([buf]) : buf;
6745 break;
6746 }
6747 default: obj.responseText = answer;
6748 }
6749 resolve(obj);
6750 });
6751 } else {
6752 const controller = new AbortController();
6753 params.signal = controller.signal;
6754 const loadTO = needTO && setTimeout(() => {
6755 reject(AjaxError.Timeout);
6756 try {
6757 controller.abort();
6758 } catch(err) {}
6759 }, WAITING_TIME);
6760 cancelFn = () => {
6761 if(needTO) {
6762 clearTimeout(loadTO);
6763 }
6764 controller.abort();
6765 };
6766 fetch(url, params).then(async res => {
6767 if(!aib.isAjaxStatusOK(res.status)) {
6768 reject(new AjaxError(res.status, res.statusText));
6769 return;
6770 }
6771 switch(params.responseType) {
6772 case 'arraybuffer': res.response = await res.arrayBuffer(); break;
6773 case 'blob': res.response = await res.blob(); break;
6774 default: res.responseText = await res.text();
6775 }
6776 resolve(res);
6777 }).catch(err => reject(getErrorMessage(err)));
6778 }
6779 } else if((isCORS || !nav.canUseNativeXHR) && nav.hasGMXHR) {
6780 let gmxhr;
6781 const timeoutFn = () => {
6782 reject(AjaxError.Timeout);
6783 try {
6784 gmxhr.abort();
6785 } catch(err) {}
6786 };
6787 let loadTO = needTO && setTimeout(timeoutFn, WAITING_TIME);
6788 const obj = {
6789 method : (params && params.method) || 'GET',
6790 url : nav.fixLink(url),
6791 onreadystatechange(e) {
6792 if(needTO) {
6793 clearTimeout(loadTO);
6794 }
6795 if(e.readyState === 4) {
6796 if(aib.isAjaxStatusOK(e.status)) {
6797 resolve(e);
6798 } else {
6799 reject(new AjaxError(e.status, e.statusText));
6800 }
6801 } else if(needTO) {
6802 loadTO = setTimeout(timeoutFn, WAITING_TIME);
6803 }
6804 }
6805 };
6806 if(params) {
6807 if(params.onprogress) {
6808 obj.upload = { onprogress: params.onprogress };
6809 delete params.onprogress;
6810 }
6811 delete params.method;
6812 Object.assign(obj, params);
6813 }
6814 if(nav.hasNewGM) {
6815 GM.xmlHttpRequest(obj);
6816 cancelFn = emptyFn; // GreaseMonkey 4 cannot cancel xhr's
6817 } else {
6818 gmxhr = GM_xmlhttpRequest(obj);
6819 cancelFn = () => {
6820 if(needTO) {
6821 clearTimeout(loadTO);
6822 }
6823 try {
6824 gmxhr.abort();
6825 } catch(err) {}
6826 };
6827 }
6828 } else if(nav.canUseNativeXHR) {
6829 const xhr = new XMLHttpRequest();
6830 const timeoutFn = () => {
6831 reject(AjaxError.Timeout);
6832 xhr.abort();
6833 };
6834 let loadTO = needTO && setTimeout(timeoutFn, WAITING_TIME);
6835 if(params && params.onprogress) {
6836 xhr.upload.onprogress = params.onprogress;
6837 }
6838 xhr.onreadystatechange = ({ target }) => {
6839 if(needTO) {
6840 clearTimeout(loadTO);
6841 }
6842 if(target.readyState === 4) {
6843 if(aib.isAjaxStatusOK(target.status)) {
6844 resolve(target);
6845 } else {
6846 reject(new AjaxError(target.status, target.statusText));
6847 }
6848 } else if(needTO) {
6849 loadTO = setTimeout(timeoutFn, WAITING_TIME);
6850 }
6851 };
6852 try {
6853 xhr.open((params && params.method) || 'GET', getAbsLink(url), true);
6854 if(params) {
6855 if(params.responseType) {
6856 xhr.responseType = params.responseType;
6857 }
6858 const { headers } = params;
6859 if(headers) {
6860 for(const h in headers) {
6861 if(headers.hasOwnProperty(h)) {
6862 xhr.setRequestHeader(h, headers[h]);
6863 }
6864 }
6865 }
6866 }
6867 xhr.send(params && params.data || null);
6868 cancelFn = () => {
6869 if(needTO) {
6870 clearTimeout(loadTO);
6871 }
6872 xhr.abort();
6873 };
6874 } catch(err) {
6875 clearTimeout(loadTO);
6876 nav.canUseNativeXHR = false;
6877 return $ajax(url, params);
6878 }
6879 } else {
6880 reject(new AjaxError(0, 'Ajax error: Can`t send any type of request.'));
6881 }
6882 return new CancelablePromise((res, rej) => {
6883 resolve = res;
6884 reject = rej;
6885 }, cancelFn);
6886}
6887
6888class AjaxError {
6889 constructor(code, message) {
6890 this.code = code;
6891 this.message = message;
6892 }
6893 toString() {
6894 return this.code <= 0 ?
6895 String(this.message || Lng.noConnect[lang]) :
6896 `HTTP [${ this.code }] ${ this.message }`;
6897 }
6898}
6899AjaxError.Success = new AjaxError(200, 'OK');
6900AjaxError.Locked = new AjaxError(-1, { toString: () => Lng.thrClosed[lang] });
6901AjaxError.Timeout = new AjaxError(0, { toString: () => Lng.noConnect[lang] + ' (timeout)' });
6902
6903const AjaxCache = {
6904 clearCache() {
6905 this._data = new Map();
6906 },
6907 fixURL: url => `${ url }${ url.includes('?') ? '&' : '?' }nocache=${ Math.random() }`,
6908 runCachedAjax(url, useCache) {
6909 const { hasCacheControl, params } = this._data.get(url) || {};
6910 const ajaxURL = hasCacheControl === false ? this.fixURL(url) : url;
6911 return $ajax(ajaxURL, useCache && params || { useTimeout: true }, aib._4chan).then(xhr =>
6912 this.saveData(url, xhr) ? xhr : $ajax(this.fixURL(url), useCache && params, aib._4chan));
6913 },
6914 saveData(url, xhr) {
6915 let ETag = null;
6916 let LastModified = null;
6917 let i = 0;
6918 let hasCacheControl = false;
6919 let headers = 'getAllResponseHeaders' in xhr ? xhr.getAllResponseHeaders() : xhr.responseHeaders;
6920 headers = headers ? /* usual xhr */ headers.split('\r\n') : /* fetch */ xhr.headers;
6921 for(const idx in headers) {
6922 if(!headers.hasOwnProperty(idx)) {
6923 continue;
6924 }
6925 let header = headers[idx];
6926 if(typeof header === 'string') { // usual xhr
6927 const сIdx = header.indexOf(':');
6928 if(сIdx === -1) {
6929 continue;
6930 }
6931 const name = header.substring(0, сIdx);
6932 const value = header.substring(сIdx + 2, header.length);
6933 header = [name, value];
6934 }
6935 const hName = header[0].toLowerCase();
6936 let matched = true;
6937 switch(hName) {
6938 case 'cache-control': hasCacheControl = true; break;
6939 case 'last-modified': LastModified = header[1]; break;
6940 case 'etag': ETag = header[1]; break;
6941 default: matched = false;
6942 }
6943 if(matched && ++i === 3) {
6944 break;
6945 }
6946 }
6947 headers = null;
6948 if(ETag || LastModified) {
6949 headers = {};
6950 if(ETag) {
6951 headers['If-None-Match'] = ETag;
6952 }
6953 if(LastModified) {
6954 headers['If-Modified-Since'] = LastModified;
6955 }
6956 }
6957 const hasUrl = this._data.has(url);
6958 this._data.set(url, {
6959 hasCacheControl,
6960 params: headers ? { headers, useTimeout: true } : { useTimeout: true }
6961 });
6962 return hasUrl || hasCacheControl;
6963 },
6964
6965 _data: new Map()
6966};
6967
6968function ajaxLoad(url, returnForm = true, useCache = false, checkArch = false) {
6969 return AjaxCache.runCachedAjax(url, useCache).then(xhr => {
6970 let el;
6971 const text = xhr.responseText;
6972 if(text.includes('</html>')) {
6973 el = returnForm ? $q(aib.qDForm, $DOM(text)) : $DOM(text);
6974 }
6975 return !el ? CancelablePromise.reject(new AjaxError(0, Lng.errCorruptData[lang])) :
6976 checkArch ? [el, (xhr.responseURL || '').includes('/arch/')] : el;
6977 }, err => err.code === 304 ? null : CancelablePromise.reject(err));
6978}
6979
6980function ajaxPostsLoad(brd, tNum, useCache, useJson = true) {
6981 if(useJson && aib.JsonBuilder) {
6982 return AjaxCache.runCachedAjax(aib.getJsonApiUrl(brd, tNum), useCache).then(xhr => {
6983 try {
6984 return new aib.JsonBuilder(JSON.parse(xhr.responseText), brd);
6985 } catch(err) {
6986 if(err instanceof AjaxError) {
6987 return CancelablePromise.reject(err);
6988 }
6989 console.warn(`API error: ${ err }. Switching to DOM parsing!`);
6990 aib.JsonBuilder = null;
6991 return ajaxPostsLoad(brd, tNum, useCache);
6992 }
6993 }, err => err.code === 304 ? null : CancelablePromise.reject(err));
6994 }
6995 return aib.iichan ?
6996 ajaxLoad(aib.getThrUrl(brd, tNum), true, useCache, true)
6997 .then(data => data && data[0] ? new DOMPostsBuilder(data[0], data[1]) : null) :
6998 ajaxLoad(aib.getThrUrl(brd, tNum), true, useCache)
6999 .then(form => form ? new DOMPostsBuilder(form) : null);
7000}
7001
7002function infoLoadErrors(err, showError = true) {
7003 const isAjax = err instanceof AjaxError;
7004 const eCode = isAjax ? err.code : 0;
7005 if(eCode === 200) {
7006 closePopup('newposts');
7007 } else if(isAjax && eCode === 0) {
7008 $popup('newposts', err.message ? String(err.message) :
7009 `${ Lng.noConnect[lang] }: \n${ getErrorMessage(err) }`);
7010 } else {
7011 $popup('newposts', `${ Lng.thrNotFound[lang] } (№${ aib.t }): \n${ getErrorMessage(err) }`);
7012 if(showError) {
7013 doc.title = `{${ eCode }} ${ doc.title }`;
7014 }
7015 }
7016}
7017
7018/* ==[ Pages.js ]=============================================================================================
7019 PAGES LOADER
7020=========================================================================================================== */
7021
7022const Pages = {
7023 addPage(needThreads = 0, pageNum = DelForm.last.pageNum + 1) {
7024 if(this._isAdding || pageNum > aib.lastPage || needThreads && pageNum > 4) {
7025 return;
7026 }
7027 this._isAdding = true;
7028 DelForm.last.el.insertAdjacentHTML('beforeend',
7029 `<div class="de-addpage-wait"><hr><center style="font-size: 1.5em"><svg class="de-wait">
7030 <use xlink:href="#de-symbol-wait"/></svg>${ Lng.loading[lang] }</center></div>`);
7031 MyPosts.purge();
7032 this._addingPromise = ajaxLoad(aib.getPageUrl(aib.b, pageNum)).then(async formEl => {
7033 const newForm = this._addForm(formEl, pageNum);
7034 if(newForm.firstThr) {
7035 if(!needThreads) {
7036 return this._updateForms(DelForm.last);
7037 }
7038 $hide(newForm.el);
7039 await this._updateForms(DelForm.last);
7040 const firstForm = DelForm.first;
7041 let thr = newForm.firstThr;
7042 do {
7043 if(thr.isHidden) {
7044 DelForm.tNums.delete(thr.num);
7045 } else {
7046 const oldLastThr = firstForm.lastThr;
7047 $after(oldLastThr.el, thr.el);
7048 newForm.firstThr = thr.next;
7049 thr.prev = oldLastThr;
7050 thr.form = firstForm;
7051 firstForm.lastThr = oldLastThr.next = thr;
7052 needThreads--;
7053 }
7054 thr = thr.next;
7055 } while(needThreads && thr);
7056 DelForm.last = firstForm;
7057 firstForm.next = firstForm.lastThr.next = null;
7058 newForm.el.remove();
7059 this._endAdding();
7060 if(needThreads) {
7061 this.addPage(needThreads, pageNum + 1);
7062 }
7063 return CancelablePromise.reject(new CancelError());
7064 }
7065 this._endAdding();
7066 this.addPage();
7067 return CancelablePromise.reject(new CancelError());
7068 }).then(() => this._endAdding()).catch(err => {
7069 if(!(err instanceof CancelError)) {
7070 $popup('add-page', getErrorMessage(err));
7071 this._endAdding();
7072 }
7073 });
7074 },
7075 async loadPages(count) {
7076 $popup('load-pages', Lng.loading[lang], true);
7077 if(this._addingPromise) {
7078 this._addingPromise.cancelPromise();
7079 this._endAdding();
7080 }
7081 PviewsCache.purge();
7082 isExpImg = false;
7083 pByEl = new Map();
7084 pByNum = new Map();
7085 Post.hiddenNums = new Set();
7086 AttachedImage.closeImg();
7087 if(pr.isQuick) {
7088 pr.clearForm();
7089 }
7090 DelForm.tNums = new Set();
7091 for(const form of DelForm) {
7092 $each($Q('a[href^="blob:"]', form.el), el => URL.revokeObjectURL(el.href));
7093 $hide(form.el);
7094 if(form === DelForm.last) {
7095 break;
7096 }
7097 form.el.remove();
7098 }
7099 DelForm.first = DelForm.last;
7100 for(let i = aib.page, len = Math.min(aib.lastPage + 1, aib.page + count); i < len; ++i) {
7101 try {
7102 this._addForm(await ajaxLoad(aib.getPageUrl(aib.b, i)), i);
7103 } catch(err) {
7104 $popup('load-pages', getErrorMessage(err));
7105 }
7106 }
7107 const { first } = DelForm;
7108 if(first !== DelForm.last) {
7109 DelForm.first = first.next;
7110 first.el.remove();
7111 await this._updateForms(DelForm.first);
7112 closePopup('load-pages');
7113 }
7114 },
7115
7116 _isAdding : false,
7117 _addingPromise : null,
7118 _addForm(formEl, pageNum) {
7119 formEl = doc.adoptNode(formEl);
7120 $hide(formEl = aib.fixHTML(formEl));
7121 $after(DelForm.last.el, formEl);
7122 const form = new DelForm(formEl, +pageNum, DelForm.last);
7123 DelForm.last = form;
7124 form.addStuff();
7125 if(pageNum !== aib.page && form.firstThr) {
7126 formEl.insertAdjacentHTML('afterbegin', `<div class="de-page-num">
7127 <center style="font-size: 2em">${ Lng.page[lang] } ${ pageNum }</center><hr></div>`);
7128 }
7129 $show(formEl);
7130 return form;
7131 },
7132 _endAdding() {
7133 $q('.de-addpage-wait').remove();
7134 this._isAdding = false;
7135 this._addingPromise = null;
7136 },
7137 async _updateForms(newForm) {
7138 readPostsData(newForm.firstThr.op, await readFavorites());
7139 if(pr.passw) {
7140 PostForm.setUserPassw();
7141 }
7142 embedPostMsgImages(newForm.el);
7143 if(HotKeys.enabled) {
7144 HotKeys.clearCPost();
7145 }
7146 }
7147};
7148
7149function toggleInfinityScroll() {
7150 if(!aib.t) {
7151 const evtName = 'onwheel' in doc.defaultView ? 'wheel' : 'mousewheel';
7152 if(Cfg.inftyScroll) {
7153 doc.defaultView.addEventListener(evtName, toggleInfinityScroll.onwheel);
7154 } else {
7155 doc.defaultView.removeEventListener(evtName, toggleInfinityScroll.onwheel);
7156 }
7157 }
7158}
7159toggleInfinityScroll.onwheel = e => {
7160 if((e.type === 'wheel' ? e.deltaY : -('wheelDeltaY' in e ? e.wheelDeltaY : e.wheelDelta)) > 0) {
7161 deWindow.requestAnimationFrame(() => {
7162 if(Thread.last.bottom - 150 < Post.sizing.wHeight) {
7163 Pages.addPage();
7164 }
7165 });
7166 }
7167};
7168
7169/* ==[ Spells.js ]============================================================================================
7170 SPELLS
7171=========================================================================================================== */
7172
7173const Spells = Object.create({
7174 hash: null,
7175 get hiders() {
7176 this._initSpells();
7177 return this.hiders;
7178 },
7179 get list() {
7180 if(Cfg.spells === null) {
7181 return '#wipe(samelines,samewords,longwords,symbols,numbers,whitespace)';
7182 }
7183 let data;
7184 try {
7185 data = JSON.parse(Cfg.spells);
7186 } catch(err) {
7187 return '';
7188 }
7189 const [, s, reps, oreps] = data;
7190 let str = s ? this._decompileScope(s, '')[0].join('\n') : '';
7191 if(reps || oreps) {
7192 if(str) {
7193 str += '\n\n';
7194 }
7195 if(reps) {
7196 for(const rep of reps) {
7197 str += this._decompileRep(rep, false) + '\n';
7198 }
7199 }
7200 if(oreps) {
7201 for(const orep of oreps) {
7202 str += this._decompileRep(orep, true) + '\n';
7203 }
7204 }
7205 str = str.substr(0, str.length - 1);
7206 }
7207 return str;
7208 },
7209 get names() {
7210 return [
7211 'words', 'exp', 'exph', 'imgn', 'ihash', 'subj', 'name', 'trip', 'img', 'sage', 'op', 'tlen',
7212 'all', 'video', 'wipe', 'num', 'vauthor'
7213 ];
7214 },
7215 get needArg() {
7216 return [
7217 /* words */ true, /* exp */ true, /* exph */ true, /* imgn */ true, /* ihash */ true,
7218 /* subj */ false, /* name */ true, /* trip */ false, /* img */ false, /* sage */ false,
7219 /* op */ false, /* tlen */ false, /* all */ false, /* video */ false, /* wipe */ false,
7220 /* num */ true, /* vauthor */ true
7221 ];
7222 },
7223 get outreps() {
7224 this._initSpells();
7225 return this.outreps;
7226 },
7227 get reps() {
7228 this._initSpells();
7229 return this.reps;
7230 },
7231 addSpell(type, arg, isNeg) {
7232 const fld = $id('de-spell-txt');
7233 const val = fld && fld.value;
7234 const chk = $q('input[info="hideBySpell"]');
7235 let spells = val && this.parseText(val);
7236 if(!val || spells) {
7237 if(!spells) {
7238 try {
7239 spells = JSON.parse(Cfg.spells);
7240 } catch(err) {}
7241 spells = spells || [Date.now(), [], null, null];
7242 }
7243 let idx, isAdded = true;
7244 const scope = aib.t ? [aib.b, aib.t] : null;
7245 if(spells[1]) {
7246 const sScope = String(scope);
7247 const sArg = String(arg);
7248 spells[1].some(scope && isNeg ? (spell, i) => {
7249 let data;
7250 if(spell[0] === 0xFF &&
7251 ((data = spell[1]) instanceof Array) &&
7252 data.length === 2 &&
7253 data[0][0] === 0x20C &&
7254 data[1][0] === type &&
7255 data[1][2] == null &&
7256 String(data[1][1]) === sArg &&
7257 String(data[0][2]) === sScope
7258 ) {
7259 idx = i;
7260 return true;
7261 }
7262 return (spell[0] & 0x200) !== 0;
7263 } : (spell, i) => {
7264 if(spell[0] === type && String(spell[1]) === sArg && String(spell[2]) === sScope) {
7265 idx = i;
7266 return true;
7267 }
7268 return (spell[0] & 0x200) !== 0;
7269 });
7270 } else {
7271 spells[1] = [];
7272 }
7273 if(typeof idx === 'undefined') {
7274 if(scope && isNeg) {
7275 spells[1].unshift([0xFF, [[0x20C, '', scope], [type, arg, void 0]], void 0]);
7276 } else {
7277 spells[1].unshift([type, arg, scope]);
7278 }
7279 } else if(Cfg.hideBySpell) {
7280 if(spells[1].length === 1) {
7281 spells[1] = null;
7282 } else {
7283 spells[1].splice(idx, 1);
7284 }
7285 isAdded = false;
7286 }
7287 if(isAdded) {
7288 saveCfg('hideBySpell', 1);
7289 if(chk) {
7290 chk.checked = true;
7291 }
7292 } else if(!spells[1] && !spells[2] && !spells[3]) {
7293 saveCfg('hideBySpell', 0);
7294 if(chk) {
7295 chk.checked = false;
7296 }
7297 }
7298 saveCfg('spells', JSON.stringify(spells));
7299 this.setSpells(spells, true);
7300 if(fld) {
7301 fld.value = this.list;
7302 }
7303 Pview.updatePosition(true);
7304 return;
7305 }
7306 if(chk) {
7307 chk.checked = false;
7308 }
7309 },
7310 decompileSpell(type, neg, val, scope, wipeMsg = null) {
7311 let spell = (neg ? '!#' : '#') + this.names[type] +
7312 (scope ? `[${ scope[0] }${ scope[1] ? `,${ scope[1] === -1 ? '' : scope[1] }` : '' }]` : '');
7313 if(!val) {
7314 return spell;
7315 }
7316 // #img
7317 if(type === 8) {
7318 return spell + '(' + (val[0] === 2 ? '>' : val[0] === 1 ? '<' : '=') +
7319 (val[1] ? val[1][0] + (val[1][1] === val[1][0] ? '' : '-' + val[1][1]) : '') +
7320 (val[2] ? '@' + val[2][0] + (val[2][0] === val[2][1] ? '' : '-' + val[2][1]) + 'x' +
7321 val[2][2] + (val[2][2] === val[2][3] ? '' : '-' + val[2][3]) : '') + ')';
7322 // #wipe
7323 } else if(type === 14) {
7324 if(val === 0x3F && !wipeMsg) {
7325 return spell;
7326 }
7327 const [msgBit, msgData] = wipeMsg || [];
7328 const names = [];
7329 const bits = {
7330 1 : 'samelines',
7331 2 : 'samewords',
7332 4 : 'longwords',
7333 8 : 'symbols',
7334 16 : 'capslock',
7335 32 : 'numbers',
7336 64 : 'whitespace'
7337 };
7338 for(const bit in bits) {
7339 if(+bit !== msgBit && (val & +bit)) {
7340 names.push(bits[bit]);
7341 }
7342 }
7343 if(msgBit) {
7344 names.push(bits[msgBit].toUpperCase() + (msgData ? ': ' + msgData : ''));
7345 }
7346 return `${ spell }(${ names.join(',') })`;
7347 // #num, #tlen
7348 } else if(type === 15 || type === 11) {
7349 let temp_, temp = val[1].length - 1;
7350 if(temp !== -1) {
7351 for(temp_ = []; temp >= 0; --temp) {
7352 temp_.push(val[1][temp][0] + '-' + val[1][temp][1]);
7353 }
7354 temp_.reverse();
7355 }
7356 spell += '(';
7357 if(val[0].length) {
7358 spell += val[0].join(',') + (temp_ ? ',' : '');
7359 }
7360 if(temp_) {
7361 spell += temp_.join(',');
7362 }
7363 return spell + ')';
7364 // #words, #name, #trip, #vauthor
7365 } else if(type === 0 || type === 6 || type === 7 || type === 16) {
7366 return `${ spell }(${ val.replace(/([)\\])/g, '\\$1').replace(/\n/g, '\\n') })`;
7367 } else {
7368 return `${ spell }(${ String(val) })`;
7369 }
7370 },
7371 disableSpells() {
7372 const value = null;
7373 const configurable = true;
7374 Object.defineProperties(this, {
7375 hiders : { configurable, value },
7376 outreps : { configurable, value },
7377 reps : { configurable, value }
7378 });
7379 saveCfg('hideBySpell', 0);
7380 },
7381 outReplace(txt) {
7382 for(const orep of this.outreps) {
7383 txt = txt.replace(orep[0], orep[1]);
7384 }
7385 return txt;
7386 },
7387 parseText(text) {
7388 const codeGen = new SpellsCodegen(text);
7389 const data = codeGen.generate();
7390 if(codeGen.hasError) {
7391 $popup('err-spell', Lng.error[lang] + ': ' + codeGen.errorSpell);
7392 } else if(data) {
7393 if(data[0] && Cfg.sortSpells) {
7394 this._sort(data[0]);
7395 }
7396 return [Date.now(), ...data];
7397 }
7398 return null;
7399 },
7400 replace(txt) {
7401 for(const rep of this.reps) {
7402 txt = txt.replace(rep[0], rep[1]);
7403 }
7404 return txt;
7405 },
7406 setSpells(spells, sync) {
7407 if(sync) {
7408 this._sync(spells);
7409 }
7410 if(!Cfg.hideBySpell) {
7411 SpellsRunner.unhideAll();
7412 this.disableSpells();
7413 return;
7414 }
7415 this._optimize(spells);
7416 if(this.hiders) {
7417 const sRunner = new SpellsRunner();
7418 for(let post = Thread.first.op; post; post = post.next) {
7419 sRunner.runSpells(post);
7420 }
7421 sRunner.endSpells();
7422 } else {
7423 SpellsRunner.unhideAll();
7424 }
7425 },
7426 toggle() {
7427 let spells;
7428 const fld = $id('de-spell-txt');
7429 const val = fld.value;
7430 if(val && (spells = this.parseText(val))) {
7431 closePopup('err-spell');
7432 this.setSpells(spells, true);
7433 saveCfg('spells', JSON.stringify(spells));
7434 fld.value = this.list;
7435 } else {
7436 if(!val) {
7437 closePopup('err-spell');
7438 SpellsRunner.unhideAll();
7439 this.disableSpells();
7440 saveCfg('spells', JSON.stringify([Date.now(), null, null, null]));
7441 sendStorageEvent('__de-spells', '{ hide: false, data: null }');
7442 }
7443 $q('input[info="hideBySpell"]').checked = false;
7444 }
7445 },
7446
7447 _decompileRep(rep, isOrep) {
7448 return (isOrep ? '#outrep' : '#rep') +
7449 (rep[0] ? `[${ rep[0] }${ rep[1] ? `,${ rep[1] === -1 ? '' : rep[1] }` : '' }]` : '') +
7450 `(${ rep[2] },${ rep[3].replace(/([)\\])/g, '\\$1').replace(/\n/g, '\\n') })`;
7451 },
7452 _decompileScope(scope, indent) {
7453 const dScope = [];
7454 let hScope = false;
7455 for(let i = 0, j = 0, len = scope.length; i < len; ++i, ++j) {
7456 const spell = scope[i];
7457 const type = spell[0] & 0xFF;
7458 if(type === 0xFF) {
7459 hScope = true;
7460 const temp = this._decompileScope(spell[1], indent + ' ');
7461 if(temp[1]) {
7462 const str = `${ spell[0] & 0x100 ? '!(\n' : '(\n' }${ indent } ` +
7463 `${ temp[0].join(`\n${ indent } `) }\n${ indent })`;
7464 if(j === 0) {
7465 dScope[0] = str;
7466 } else {
7467 dScope[--j] += ' ' + str;
7468 }
7469 } else {
7470 dScope[j] = `${ spell[0] & 0x100 ? '!(' : '(' }${ temp[0].join(' ') })`;
7471 }
7472 } else {
7473 dScope[j] = this.decompileSpell(type, spell[0] & 0x100, spell[1], spell[2]);
7474 }
7475 if(i !== len - 1) {
7476 dScope[j] += spell[0] & 0x200 ? ' &' : ' |';
7477 }
7478 }
7479 return [dScope, dScope.length > 2 || hScope];
7480 },
7481 _initSpells() {
7482 if(!Cfg.hideBySpell) {
7483 const value = null;
7484 const configurable = true;
7485 Object.defineProperties(this, {
7486 hiders : { configurable, value },
7487 outreps : { configurable, value },
7488 reps : { configurable, value }
7489 });
7490 return;
7491 }
7492 let spells, data;
7493 try {
7494 spells = JSON.parse(Cfg.spells);
7495 data = JSON.parse(sesStorage[`de-spells-${ aib.b }${ aib.t || '' }`]);
7496 } catch(err) {}
7497 if(data && spells && data[0] === spells[0]) {
7498 this.hash = data[0];
7499 this._setData(data[1], data[2], data[3]);
7500 return;
7501 }
7502 if(spells) {
7503 this._optimize(spells);
7504 } else {
7505 this.disableSpells();
7506 }
7507 },
7508 _initHiders(data) {
7509 if(data) {
7510 for(const item of data) {
7511 const val = item[1];
7512 if(val) {
7513 switch(item[0] & 0xFF) {
7514 case 1:
7515 case 2:
7516 case 3:
7517 case 5:
7518 case 13: item[1] = toRegExp(val, true); break;
7519 case 0xFF: this._initHiders(val);
7520 }
7521 }
7522 }
7523 }
7524 return data;
7525 },
7526 _initReps(data) {
7527 if(data) {
7528 for(const item of data) {
7529 item[0] = toRegExp(item[0], false);
7530 }
7531 }
7532 return data;
7533 },
7534 _optimize(data) {
7535 const arr = [
7536 data[1] ? this._optimizeSpells(data[1]) : null,
7537 data[2] ? this._optimizeReps(data[2]) : null,
7538 data[3] ? this._optimizeReps(data[3]) : null
7539 ];
7540 sesStorage[`de-spells-${ aib.b }${ aib.t || '' }`] = JSON.stringify([data[0], ...arr]);
7541 this.hash = data[0];
7542 this._setData(...arr);
7543 },
7544 _optimizeReps(data) {
7545 const rv = [];
7546 for(const [r0, r1, r2, r3] of data) {
7547 if(!r0 || (r0 === aib.b && (r1 === -1 ? !aib.t : !r1 || +r1 === aib.t))) {
7548 rv.push([r2, r3]);
7549 }
7550 }
7551 return !rv.length ? null : rv;
7552 },
7553 _optimizeSpells(spells) {
7554 let neg, lastSpell = -1;
7555 let newSpells = [];
7556 for(let i = 0, len = spells.length; i < len; ++i) {
7557 let j;
7558 const spell = spells[i];
7559 let flags = spell[0];
7560 const type = flags & 0xFF;
7561 neg = (flags & 0x100) !== 0;
7562 if(type === 0xFF) {
7563 const parensSpells = this._optimizeSpells(spell[1]);
7564 if(parensSpells) {
7565 if(parensSpells.length !== 1) {
7566 newSpells.push([flags, parensSpells]);
7567 lastSpell++;
7568 continue;
7569 } else if((parensSpells[0][0] & 0xFF) !== 12) {
7570 newSpells.push([(parensSpells[0][0] | (flags & 0x200)) ^ (flags & 0x100),
7571 parensSpells[0][1]]);
7572 lastSpell++;
7573 continue;
7574 }
7575 flags = parensSpells[0][0];
7576 neg = !(neg ^ ((flags & 0x100) !== 0));
7577 }
7578 } else {
7579 const scope = spell[2];
7580 if(!scope || (
7581 scope[0] === aib.b &&
7582 (scope[1] === -1 ? !aib.t : !scope[1] || +scope[1] === aib.t)
7583 )) {
7584 if(type === 12) {
7585 neg = !neg;
7586 } else {
7587 newSpells.push([flags, spell[1]]);
7588 lastSpell++;
7589 continue;
7590 }
7591 }
7592 }
7593 for(j = lastSpell; j >= 0 && (((newSpells[j][0] & 0x200) !== 0) ^ neg); --j) /* empty */;
7594 if(j !== lastSpell) {
7595 newSpells = newSpells.slice(0, j + 1);
7596 lastSpell = j;
7597 }
7598 if(neg && j !== -1) {
7599 newSpells[j][0] &= 0x1FF;
7600 }
7601 if(((flags & 0x200) !== 0) ^ neg) {
7602 break;
7603 }
7604 }
7605 return lastSpell === -1 ? neg ? [[12, '']] : null : newSpells;
7606 },
7607 _setData(hiders, reps, outreps) {
7608 const configurable = true;
7609 Object.defineProperties(this, {
7610 hiders : { configurable, value: this._initHiders(hiders) },
7611 outreps : { configurable, value: this._initReps(outreps) },
7612 reps : { configurable, value: this._initReps(reps) }
7613 });
7614 },
7615 _sort(sp) {
7616 // Wraps AND-spells with brackets for proper sorting
7617 for(let i = 0, len = sp.length - 1; i < len; ++i) {
7618 if(sp[i][0] > 0x200) {
7619 const temp = [0xFF, []];
7620 do {
7621 temp[1].push(sp.splice(i, 1)[0]);
7622 len--;
7623 } while(sp[i][0] > 0x200);
7624 temp[1].push(sp.splice(i, 1)[0]);
7625 sp.splice(i, 0, temp);
7626 }
7627 }
7628 sp = sp.sort();
7629 for(let i = 0, len = sp.length - 1; i < len; ++i) {
7630 // Removes duplicates and weaker spells
7631 const j = i + 1;
7632 if(sp[i][0] === sp[j][0] &&
7633 sp[i][1] <= sp[j][1] &&
7634 sp[i][1] >= sp[j][1] &&
7635 (sp[i][2] === null || // Stronger spell with 3 parameters
7636 sp[i][2] === undefined || // Equal spells with 2 parameters
7637 (sp[i][2] <= sp[j][2] && sp[i][2] >= sp[j][2]))
7638 ) { // Equal spells with 3 parameters
7639 sp.splice(j, 1);
7640 i--;
7641 len--;
7642 // Moves brackets to the end of the list
7643 } else if(sp[i][0] === 0xFF) {
7644 sp.push(sp.splice(i, 1)[0]);
7645 i--;
7646 len--;
7647 }
7648 }
7649 },
7650 _sync(data) {
7651 sendStorageEvent('__de-spells', { hide: !!Cfg.hideBySpell, data });
7652 }
7653});
7654
7655class SpellsCodegen {
7656 constructor(sList) {
7657 this.TYPE_UNKNOWN = 0;
7658 this.TYPE_ANDOR = 1;
7659 this.TYPE_NOT = 2;
7660 this.TYPE_SPELL = 3;
7661 this.TYPE_PARENTHESES = 4;
7662 this.TYPE_REPLACER = 5;
7663 this.hasError = false;
7664 this._col = 1;
7665 this._errMsg = '';
7666 this._errMsgArg = null;
7667 this._line = 1;
7668 this._sList = sList;
7669 }
7670 get errorSpell() {
7671 return !this.hasError ? '' :
7672 (this._errMsgArg ? this._errMsg.replace('%s', this._errMsgArg) : this._errMsg) +
7673 Lng.seRow[lang] + this._line + Lng.seCol[lang] + this._col + ')';
7674 }
7675 generate() {
7676 return this._sList ? this._generate(this._sList, false) : null;
7677 }
7678
7679 static _getScope(str) {
7680 const m = str.match(/^\[([a-z0-9/]+)(?:(,)|,(\s*[0-9]+))?\]/);
7681 return m ? [m[0].length, [m[1], m[3] ? +m[3] : m[2] ? -1 : false]] : null;
7682 }
7683 static _getText(str, haveBracket) {
7684 if(haveBracket && (str[0] !== '(')) {
7685 return [0, ''];
7686 }
7687 let rv = '';
7688 for(let i = haveBracket ? 1 : 0, len = str.length; i < len; ++i) {
7689 const ch = str[i];
7690 if(ch === '\\') {
7691 if(i === len - 1) {
7692 return null;
7693 }
7694 switch(str[i + 1]) {
7695 case 'n': rv += '\n'; break;
7696 case '\\': rv += '\\'; break;
7697 case ')': rv += ')'; break;
7698 default: return null;
7699 }
7700 ++i;
7701 } else if(ch === ')') {
7702 return [i + 1, rv];
7703 } else {
7704 rv += ch;
7705 }
7706 }
7707 return null;
7708 }
7709 _generate(sList, inParens) {
7710 const spellsArr = [];
7711 let reps = [];
7712 let outreps = [];
7713 let lastType = this.TYPE_UNKNOWN;
7714 let hasReps = false;
7715 for(let i = 0, len = sList.length; i < len; i++, this._col++) {
7716 let res;
7717 switch(sList[i]) {
7718 case '\n':
7719 this._line++;
7720 this._col = 0;
7721 /* falls through */
7722 case '\r':
7723 case ' ': continue;
7724 case '#': {
7725 let name = '';
7726 i++;
7727 this._col++;
7728 while((sList[i] >= 'a' && sList[i] <= 'z') || (sList[i] >= 'A' && sList[i] <= 'Z')) {
7729 name += sList[i].toLowerCase();
7730 i++;
7731 this._col++;
7732 }
7733 if(name === 'rep' || name === 'outrep') {
7734 if(!hasReps) {
7735 if(inParens) {
7736 this._col -= 1 + name.length;
7737 this._setError(Lng.seRepsInParens[lang], '#' + name);
7738 return null;
7739 }
7740 if(lastType === this.TYPE_ANDOR || lastType === this.TYPE_NOT) {
7741 i -= 1 + name.length;
7742 this._col -= 1 + name.length;
7743 lookBack:
7744 while(i >= 0) {
7745 switch(sList[i]) {
7746 case '\n':
7747 case '\r':
7748 case ' ':
7749 i--;
7750 this._col--;
7751 break;
7752 default:
7753 break lookBack;
7754 }
7755 }
7756 this._setError(Lng.seOpInReps[lang], sList[i]);
7757 return null;
7758 }
7759 hasReps = true;
7760 }
7761 res = this._doRep(name, sList.substr(i));
7762 if(!res) {
7763 return null;
7764 }
7765 (name === 'rep' ? reps : outreps).push(res[1]);
7766 i += res[0] - 1;
7767 this._col += res[0] - 1;
7768 lastType = this.TYPE_REPLACER;
7769 } else {
7770 if(lastType === this.TYPE_SPELL || lastType === this.TYPE_PARENTHESES) {
7771 this._setError(Lng.seMissOp[lang], null);
7772 return null;
7773 }
7774 res = this._doSpell(name, sList.substr(i), lastType === this.TYPE_NOT);
7775 if(!res) {
7776 return null;
7777 }
7778 i += res[0] - 1;
7779 this._col += res[0] - 1;
7780 spellsArr.push(res[1]);
7781 lastType = this.TYPE_SPELL;
7782 }
7783 break;
7784 }
7785 case '(':
7786 if(hasReps) {
7787 this._setError(Lng.seUnexpChar[lang], '(');
7788 return null;
7789 }
7790 if(lastType === this.TYPE_SPELL || lastType === this.TYPE_PARENTHESES) {
7791 this._setError(Lng.seMissOp[lang], null);
7792 return null;
7793 }
7794 res = this._generate(sList.substr(i + 1), true);
7795 if(!res) {
7796 return null;
7797 }
7798 i += res[0] + 1;
7799 spellsArr.push([lastType === this.TYPE_NOT ? 0x1FF : 0xFF, res[1]]);
7800 lastType = this.TYPE_PARENTHESES;
7801 break;
7802 case '|':
7803 case '&':
7804 if(hasReps) {
7805 this._setError(Lng.seUnexpChar[lang], sList[i]);
7806 return null;
7807 }
7808 if(lastType !== this.TYPE_SPELL && lastType !== this.TYPE_PARENTHESES) {
7809 this._setError(Lng.seMissSpell[lang], null);
7810 return null;
7811 }
7812 if(sList[i] === '&') {
7813 spellsArr[spellsArr.length - 1][0] |= 0x200;
7814 }
7815 lastType = this.TYPE_ANDOR;
7816 break;
7817 case '!':
7818 if(hasReps) {
7819 this._setError(Lng.seUnexpChar[lang], '!');
7820 return null;
7821 }
7822 if(lastType !== this.TYPE_ANDOR && lastType !== this.TYPE_UNKNOWN) {
7823 this._setError(Lng.seMissOp[lang], null);
7824 return null;
7825 }
7826 lastType = this.TYPE_NOT;
7827 break;
7828 case ')':
7829 if(hasReps) {
7830 this._setError(Lng.seUnexpChar[lang], ')');
7831 return null;
7832 }
7833 if(lastType === this.TYPE_ANDOR || lastType === this.TYPE_NOT) {
7834 this._setError(Lng.seMissSpell[lang], null);
7835 return null;
7836 }
7837 if(inParens) {
7838 return [i, spellsArr];
7839 }
7840 /* falls through */
7841 default:
7842 this._setError(Lng.seUnexpChar[lang], sList[i]);
7843 return null;
7844 }
7845 }
7846 if(inParens) {
7847 this._setError(Lng.seMissClBkt[lang], null);
7848 return null;
7849 }
7850 if(lastType !== this.TYPE_SPELL &&
7851 lastType !== this.TYPE_PARENTHESES &&
7852 lastType !== this.TYPE_REPLACER
7853 ) {
7854 this._setError(Lng.seMissSpell[lang], null);
7855 return null;
7856 }
7857 if(!reps.length) {
7858 reps = false;
7859 }
7860 if(!outreps.length) {
7861 outreps = false;
7862 }
7863 return [spellsArr, reps, outreps];
7864 }
7865 _getRegex(str, haveComma) {
7866 const m = str.match(/^\((\/.*?[^\\]\/[igm]*)(?:\)|\s*(,))/);
7867 if(!m || haveComma !== Boolean(m[2])) {
7868 return null;
7869 }
7870 const val = m[1];
7871 try {
7872 toRegExp(val, true);
7873 } catch(err) {
7874 this._setError(Lng.seErrRegex[lang], val);
7875 return null;
7876 }
7877 return [m[0].length, val];
7878 }
7879 _doRep(name, str) {
7880 let scope = SpellsCodegen._getScope(str);
7881 if(scope) {
7882 str = str.substring(scope[0]);
7883 } else {
7884 scope = [0, ['', '']];
7885 }
7886 const regex = this._getRegex(str, true);
7887 if(regex) {
7888 str = str.substring(regex[0]);
7889 if(str[0] === ')') {
7890 return [regex[0] + scope[0] + 1, [scope[1][0], scope[1][1], regex[1], '']];
7891 }
7892 const val = SpellsCodegen._getText(str, false);
7893 if(val) {
7894 return [val[0] + regex[0] + scope[0], [scope[1][0], scope[1][1], regex[1], val[1]]];
7895 }
7896 }
7897 this._setError(Lng.seSyntaxErr[lang], name);
7898 return null;
7899 }
7900 _doSpell(name, str, isNeg) {
7901 let m, val, scope = null, i = 0;
7902 const spellIdx = Spells.names.indexOf(name);
7903 if(spellIdx === -1) {
7904 this._setError(Lng.seUnknown[lang], name);
7905 return null;
7906 }
7907 let temp = SpellsCodegen._getScope(str);
7908 if(temp) {
7909 i += temp[0];
7910 str = str.substring(temp[0]);
7911 scope = temp[1];
7912 }
7913 const spellType = isNeg ? spellIdx | 0x100 : spellIdx;
7914 if(str[0] !== '(' || str[1] === ')') {
7915 if(Spells.needArg[spellIdx]) {
7916 this._setError(Lng.seMissArg[lang], name);
7917 return null;
7918 }
7919 return [str[0] === '(' ? i + 2 : i, [spellType, spellIdx === 14 ? 0x3F : '', scope]];
7920 }
7921 switch(spellIdx) {
7922 // #ihash
7923 case 4:
7924 m = str.match(/^\((\d+)\)/);
7925 if(!isNaN(+m[1])) {
7926 return [i + m[0].length, [spellType, +m[1], scope]];
7927 }
7928 break;
7929 // #img
7930 case 8:
7931 m = str.match(/^\(([><=])(?:(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?)?(?:@(\d+)(?:-(\d+))?x(\d+)(?:-(\d+))?)?\)/);
7932 if(m && (m[2] || m[4])) {
7933 return [i + m[0].length, [spellType, [
7934 m[1] === '=' ? 0 : m[1] === '<' ? 1 : 2,
7935 m[2] && [+m[2], m[3] ? +m[3] : +m[2]],
7936 m[4] && [+m[4], m[5] ? +m[5] : +m[4], +m[6], m[7] ? +m[7] : +m[6]]
7937 ], scope]];
7938 }
7939 break;
7940 // #wipe
7941 case 14:
7942 m = str.match(/^\(([a-z, ]+)\)/);
7943 if(m) {
7944 let val = 0;
7945 const arr = m[1].split(/, */);
7946 for(let i = 0, len = arr.length; i < len; ++i) {
7947 switch(arr[i]) {
7948 case 'samelines': val |= 1; break;
7949 case 'samewords': val |= 2; break;
7950 case 'longwords': val |= 4; break;
7951 case 'symbols': val |= 8; break;
7952 case 'capslock': val |= 16; break;
7953 case 'numbers': val |= 32; break;
7954 case 'whitespace': val |= 64; break;
7955 default: val = -1;
7956 }
7957 }
7958 if(val !== -1) {
7959 return [i + m[0].length, [spellType, val, scope]];
7960 }
7961 }
7962 break;
7963 // #tlen, #num
7964 case 11:
7965 case 15:
7966 m = str.match(/^\(([\d-, ]+)\)/);
7967 if(m) {
7968 m[1].split(/, */).forEach(function(v) {
7969 if(v.includes('-')) {
7970 const nums = v.split('-');
7971 nums[0] = +nums[0];
7972 nums[1] = +nums[1];
7973 this[1].push(nums);
7974 } else {
7975 this[0].push(+v);
7976 }
7977 }, val = [[], []]);
7978 return [i + m[0].length, [spellType, val, scope]];
7979 }
7980 break;
7981 // #exp, #exph, #imgn, #subj, #video
7982 case 1:
7983 case 2:
7984 case 3:
7985 case 5:
7986 case 13:
7987 temp = this._getRegex(str, false);
7988 if(temp) {
7989 return [i + temp[0], [spellType, temp[1], scope]];
7990 }
7991 break;
7992 // #sage, #op, #all, #trip, #name, #words, #vauthor
7993 default:
7994 temp = SpellsCodegen._getText(str, true);
7995 if(temp) {
7996 return [i + temp[0], [spellType, spellIdx === 0 ? temp[1].toLowerCase() : temp[1], scope]];
7997 }
7998 }
7999 this._setError(Lng.seSyntaxErr[lang], name);
8000 return null;
8001 }
8002 _setError(msg, arg) {
8003 this.hasError = true;
8004 this._errMsg = msg;
8005 this._errMsgArg = arg;
8006 }
8007}
8008
8009class SpellsRunner {
8010 constructor() {
8011 this.hasNumSpell = false;
8012 this._endPromise = null;
8013 this._spells = Spells.hiders;
8014 if(!this._spells) {
8015 this.runSpells = SpellsRunner._unhidePost;
8016 SpellsRunner.cachedData = null;
8017 }
8018 }
8019 static unhideAll() {
8020 if(aib.t) {
8021 sesStorage['de-hidden-' + aib.b + aib.t] = null;
8022 }
8023 for(let post = Thread.first.op; post; post = post.next) {
8024 if(post.spellHidden) {
8025 post.spellUnhide();
8026 }
8027 }
8028 }
8029 endSpells() {
8030 if(this._endPromise) {
8031 this._endPromise.then(() => this._savePostsHelper());
8032 } else {
8033 this._savePostsHelper();
8034 }
8035 }
8036 runSpells(post) {
8037 let res = (new SpellsInterpreter(post, this._spells)).runInterpreter();
8038 if(res instanceof Promise) {
8039 res = res.then(val => this._checkRes(post, val));
8040 this._endPromise = this._endPromise ? this._endPromise.then(() => res) : res;
8041 return 0;
8042 }
8043 return this._checkRes(post, res);
8044 }
8045
8046 static _unhidePost(post) {
8047 if(post.spellHidden) {
8048 post.spellUnhide();
8049 if(SpellsRunner.cachedData && !post.isDeleted) {
8050 SpellsRunner.cachedData[post.count] = [false, null];
8051 }
8052 }
8053 return 0;
8054 }
8055 _checkRes(post, [hasNumSpell, val, msg]) {
8056 this.hasNumSpell |= hasNumSpell;
8057 if(val) {
8058 post.spellHide(msg);
8059 if(SpellsRunner.cachedData && !post.isDeleted) {
8060 SpellsRunner.cachedData[post.count] = [true, msg];
8061 }
8062 return 1;
8063 }
8064 return SpellsRunner._unhidePost(post);
8065 }
8066 _savePostsHelper() {
8067 if(this._spells) {
8068 if(aib.t) {
8069 const lPost = Thread.first.lastNotDeleted;
8070 let data = null;
8071 if(Spells.hiders) {
8072 if(SpellsRunner.cachedData) {
8073 data = SpellsRunner.cachedData;
8074 } else {
8075 data = [];
8076 for(let post = Thread.first.op; post; post = post.nextNotDeleted) {
8077 data.push(post.spellHidden ? [true, Post.Note.text] : [false, null]);
8078 }
8079 SpellsRunner.cachedData = data;
8080 }
8081 }
8082 sesStorage['de-hidden-' + aib.b + aib.t] = !data ? null : JSON.stringify({
8083 hash : Cfg.hideBySpell ? Spells.hash : 0,
8084 lastCount : lPost.count,
8085 lastNum : lPost.num,
8086 data
8087 });
8088 }
8089 toggleWindow('hid', true);
8090 }
8091 ImagesHashStorage.endFn();
8092 }
8093}
8094SpellsRunner.cachedData = null;
8095
8096class SpellsInterpreter {
8097 constructor(post, spells) {
8098 this.hasNumSpell = false;
8099 this._ctx = [spells.length, spells, 0, false];
8100 this._deep = 0;
8101 this._lastTSpells = [];
8102 this._post = post;
8103 this._triggeredSpellsStack = [this._lastTSpells];
8104 this._wipeMsg = null;
8105 }
8106 runInterpreter() {
8107 let rv, stopCheck;
8108 let isNegScope = this._ctx.pop();
8109 let i = this._ctx.pop();
8110 let scope = this._ctx.pop();
8111 let len = this._ctx.pop();
8112 while(true) {
8113 if(i < len) {
8114 const type = scope[i][0] & 0xFF;
8115 if(type === 0xFF) {
8116 this._deep++;
8117 this._ctx.push(len, scope, i, isNegScope);
8118 isNegScope = !!(((scope[i][0] & 0x100) !== 0) ^ isNegScope);
8119 scope = scope[i][1];
8120 len = scope.length;
8121 i = 0;
8122 this._lastTSpells = [];
8123 this._triggeredSpellsStack.push(this._lastTSpells);
8124 continue;
8125 }
8126 const val = this._runSpell(type, scope[i][1]);
8127 if(val instanceof Promise) {
8128 this._ctx.push(len, scope, ++i, isNegScope);
8129 return val.then(v => this._asyncContinue(v));
8130 }
8131 [rv, stopCheck] = this._checkRes(scope[i], val, isNegScope);
8132 if(!stopCheck) {
8133 i++;
8134 continue;
8135 }
8136 }
8137 if(this._deep !== 0) {
8138 this._deep--;
8139 isNegScope = this._ctx.pop();
8140 i = this._ctx.pop();
8141 scope = this._ctx.pop();
8142 len = this._ctx.pop();
8143 if(((scope[i][0] & 0x200) === 0) ^ rv) {
8144 i++;
8145 this._triggeredSpellsStack.pop();
8146 this._lastTSpells = this._triggeredSpellsStack[this._triggeredSpellsStack.length - 1];
8147 continue;
8148 }
8149 }
8150 return [this.hasNumSpell, rv, rv ? this._getMsg() : null];
8151 }
8152 }
8153
8154 static _tlenNum_helper(val, num) {
8155 for(let arr = val[0], i = arr.length - 1; i >= 0; --i) {
8156 if(arr[i] === num) {
8157 return true;
8158 }
8159 }
8160 for(let arr = val[1], i = arr.length - 1; i >= 0; --i) {
8161 if(num >= arr[i][0] && num <= arr[i][1]) {
8162 return true;
8163 }
8164 }
8165 return false;
8166 }
8167 _asyncContinue(val) {
8168 const cl = this._ctx.length;
8169 const spell = this._ctx[cl - 3][this._ctx[cl - 2] - 1];
8170 const [rv, stopCheck] = this._checkRes(spell, val, this._ctx[cl - 1]);
8171 return stopCheck ? [this.hasNumSpell, rv, rv ? this._getMsg() : null] : this.runInterpreter();
8172 }
8173 _checkRes(spell, val, isNegScope) {
8174 const flags = spell[0];
8175 const isAndSpell = ((flags & 0x200) !== 0) ^ isNegScope;
8176 const isNegSpell = ((flags & 0x100) !== 0) ^ isNegScope;
8177 if(isNegSpell ^ val) {
8178 this._lastTSpells.push([isNegSpell, spell, (spell[0] & 0xFF) === 14 ? this._wipeMsg : null]);
8179 return [true, !isAndSpell];
8180 }
8181 this._lastTSpells.length = 0;
8182 return [false, isAndSpell];
8183 }
8184 _getMsg() {
8185 const rv = [];
8186 for(const spellEls of this._triggeredSpellsStack) {
8187 for(const [isNeg, spell, wipeMsg] of spellEls) {
8188 rv.push(Spells.decompileSpell(spell[0] & 0xFF, isNeg, spell[1], spell[2], wipeMsg));
8189 }
8190 }
8191 return rv.join(' & ');
8192 }
8193 _runSpell(spellId, val) {
8194 switch(spellId) {
8195 case 0: return this._words(val);
8196 case 1: return this._exp(val);
8197 case 2: return this._exph(val);
8198 case 3: return this._imgn(val);
8199 case 4: return this._ihash(val);
8200 case 5: return this._subj(val);
8201 case 6: return this._name(val);
8202 case 7: return this._trip(val);
8203 case 8: return this._img(val);
8204 case 9: return this._sage(val);
8205 case 10: return this._op(val);
8206 case 11: return this._tlen(val);
8207 case 12: return this._all(val);
8208 case 13: return this._video(val);
8209 case 14: return this._wipe(val);
8210 case 15:
8211 this.hasNumSpell = true;
8212 return this._num(val);
8213 case 16: return this._vauthor(val);
8214 }
8215 }
8216
8217 _all() {
8218 return true;
8219 }
8220 _exp(val) {
8221 return val.test(this._post.text);
8222 }
8223 _exph(val) {
8224 return val.test(this._post.html);
8225 }
8226 async _ihash(val) {
8227 for(const image of this._post.images) {
8228 if((image instanceof AttachedImage) && await ImagesHashStorage.getHash(image) === val) {
8229 return true;
8230 }
8231 }
8232 return false;
8233 }
8234 _img(val) {
8235 const { images } = this._post;
8236 const [compareRule, weightVals, sizeVals] = val;
8237 if(!val) {
8238 return images.hasAttachments;
8239 }
8240 for(const image of images) {
8241 if(!(image instanceof AttachedImage)) {
8242 continue;
8243 }
8244 if(weightVals) {
8245 const w = image.weight;
8246 let isHide;
8247 switch(compareRule) {
8248 case 0: isHide = w >= weightVals[0] && w <= weightVals[1]; break;
8249 case 1: isHide = w < weightVals[0]; break;
8250 case 2: isHide = w > weightVals[0]; break;
8251 }
8252 if(!isHide) {
8253 continue;
8254 } else if(!sizeVals) {
8255 return true;
8256 }
8257 }
8258 if(sizeVals) {
8259 const { height: h, width: w } = image;
8260 switch(compareRule) {
8261 case 0:
8262 if(w >= sizeVals[0] && w <= sizeVals[1] && h >= sizeVals[2] && h <= sizeVals[3]) {
8263 return true;
8264 }
8265 break;
8266 case 1:
8267 if(w < sizeVals[0] && h < sizeVals[3]) {
8268 return true;
8269 }
8270 break;
8271 case 2:
8272 if(w > sizeVals[0] && h > sizeVals[3]) {
8273 return true;
8274 }
8275 }
8276 }
8277 }
8278 return false;
8279 }
8280 _imgn(val) {
8281 for(const image of this._post.images) {
8282 if((image instanceof AttachedImage) && val.test(image.name)) {
8283 return true;
8284 }
8285 }
8286 return false;
8287 }
8288 _name(val) {
8289 const pName = this._post.posterName;
8290 return pName ? !val || pName.includes(val) : false;
8291 }
8292 _num(val) {
8293 return SpellsInterpreter._tlenNum_helper(val, this._post.count + 1);
8294 }
8295 _op() {
8296 return this._post.isOp;
8297 }
8298 _sage() {
8299 return this._post.sage;
8300 }
8301 _subj(val) {
8302 const pSubj = this._post.subj;
8303 return pSubj ? !val || val.test(pSubj) : false;
8304 }
8305 _tlen(val) {
8306 const text = this._post.text.replace(/\s+(?=\s)|\n/g, '');
8307 return !val ? !!text : SpellsInterpreter._tlenNum_helper(val, text.length);
8308 }
8309 _trip(val) {
8310 const pTrip = this._post.posterTrip;
8311 return pTrip ? !val || pTrip.includes(val) : false;
8312 }
8313 _vauthor(val) {
8314 return this._videoVauthor(val, true);
8315 }
8316 _video(val) {
8317 return this._videoVauthor(val, false);
8318 }
8319 _videoVauthor(val, isAuthorSpell) {
8320 const { videos } = this._post;
8321 if(!val) {
8322 return !!videos.hasLinks;
8323 }
8324 if(!videos.hasLinks || !Cfg.YTubeTitles) {
8325 return false;
8326 }
8327 for(const siteData of videos.vData) {
8328 for(const data of siteData) {
8329 if(isAuthorSpell ? val === data[1] : val.test(data[0])) {
8330 return true;
8331 }
8332 }
8333 }
8334 if(videos.linksCount === videos.loadedLinksCount) {
8335 return false;
8336 }
8337 return new Promise(resolve => (videos.titleLoadFn = data => {
8338 if(isAuthorSpell ? val === data[1] : val.test(data[0])) {
8339 resolve(true);
8340 } else if(videos.linksCount === videos.loadedLinksCount) {
8341 resolve(false);
8342 } else {
8343 return;
8344 }
8345 videos.titleLoadFn = null;
8346 }));
8347 }
8348 _wipe(val) {
8349 let arr, len, x;
8350 const txt = this._post.text;
8351 // (1 << 0): samelines
8352 if(val & 1) {
8353 arr = txt.replace(/>/g, '').split(/\s*\n\s*/);
8354 if((len = arr.length) > 5) {
8355 arr.sort();
8356 for(let i = 0, n = len / 4; i < len;) {
8357 x = arr[i];
8358 let j = 0;
8359 while(arr[i++] === x) {
8360 j++;
8361 }
8362 if(j > 4 && j > n && x) {
8363 this._wipeMsg = [1, `"${ x.substr(0, 20) }" x${ j + 1 }`];
8364 return true;
8365 }
8366 }
8367 }
8368 }
8369 // (1 << 1): samewords
8370 if(val & 2) {
8371 arr = txt.replace(/[\s.?!,>]+/g, ' ').toUpperCase().split(' ');
8372 if((len = arr.length) > 3) {
8373 arr.sort();
8374 let keys = 0;
8375 let pop = 0;
8376 for(let i = 0, n = len / 4; i < len; keys++) {
8377 x = arr[i];
8378 let j = 0;
8379 while(arr[i++] === x) {
8380 j++;
8381 }
8382 if(len > 25) {
8383 if(j > pop && x.length > 2) {
8384 pop = j;
8385 }
8386 if(pop >= n) {
8387 this._wipeMsg = [2, `same "${ x.substr(0, 20) }" x${ pop + 1 }`];
8388 return true;
8389 }
8390 }
8391 }
8392 x = keys / len;
8393 if(x < 0.25) {
8394 this._wipeMsg = [2, `uniq ${ (x * 100).toFixed(0) }%`];
8395 return true;
8396 }
8397 }
8398 }
8399 // (1 << 2): longwords
8400 if(val & 4) {
8401 arr = txt.replace(/https*:\/\/.*?(\s|$)/g, '').replace(/[\s.?!,>:;-]+/g, ' ').split(' ');
8402 if(arr[0].length > 50 || ((len = arr.length) > 1 && arr.join('').length / len > 10)) {
8403 this._wipeMsg = [4, null];
8404 return true;
8405 }
8406 }
8407 // (1 << 3): symbols
8408 if(val & 8) {
8409 const _txt = txt.replace(/\s+/g, '');
8410 if((len = _txt.length) > 30 && (x = _txt.replace(/[0-9a-zа-я.?!,]/ig, '').length / len) > 0.4) {
8411 this._wipeMsg = [8, `${ (x * 100).toFixed(0) }%`];
8412 return true;
8413 }
8414 }
8415 // (1 << 4): capslock
8416 if(val & 16) {
8417 arr = txt.replace(/[\s.?!;,-]+/g, ' ').trim().split(' ');
8418 if((len = arr.length) > 4) {
8419 let n = 0;
8420 let capsw = 0;
8421 let casew = 0;
8422 for(let i = 0; i < len; ++i) {
8423 x = arr[i];
8424 if((x.match(/[a-zа-я]/ig) || []).length < 5) {
8425 continue;
8426 }
8427 if((x.match(/[A-ZА-Я]/g) || []).length > 2) {
8428 casew++;
8429 }
8430 if(x === x.toUpperCase()) {
8431 capsw++;
8432 }
8433 n++;
8434 }
8435 if(capsw / n >= 0.3 && n > 4) {
8436 this._wipeMsg = [16, `CAPS ${ capsw / arr.length * 100 }%`];
8437 return true;
8438 } else if(casew / n >= 0.3 && n > 8) {
8439 this._wipeMsg = [16, `cAsE ${ casew / arr.length * 100 }%`];
8440 return true;
8441 }
8442 }
8443 }
8444 // (1 << 5): numbers
8445 if(val & 32) {
8446 const _txt = txt.replace(/\s+/g, ' ').replace(/>>\d+|https*:\/\/.*?(?: |$)/g, '');
8447 if((len = _txt.length) > 30 && (x = (len - _txt.replace(/\d/g, '').length) / len) > 0.4) {
8448 this._wipeMsg = [32, `${ Math.round(x * 100) }%`];
8449 return true;
8450 }
8451 }
8452 // (1 << 5): whitespace
8453 if(val & 64) {
8454 if(/(?:\n\s*){10}/i.test(txt)) {
8455 this._wipeMsg = [64, null];
8456 return true;
8457 }
8458 }
8459 return false;
8460 }
8461 _words(val) {
8462 return this._post.text.toLowerCase().includes(val) || this._post.subj.toLowerCase().includes(val);
8463 }
8464}
8465
8466/* ==[ Form.js ]==============================================================================================
8467 POSTFORM
8468 postform improving, quick reply window, markup text panel, sage button, etc
8469=========================================================================================================== */
8470
8471class PostForm {
8472 constructor(form, oeForm = null, ignoreForm = false) {
8473 this.isBottom = false;
8474 this.isHidden = false;
8475 this.isQuick = false;
8476 this.lastQuickPNum = -1;
8477 this.pArea = [];
8478 this.pForm = null;
8479 this.qArea = null;
8480 this._pBtn = [];
8481 const qOeForm = 'form[name="oeform"], form[action*="paint"]';
8482 this.oeForm = oeForm || $q(qOeForm);
8483 if(!ignoreForm && !form) {
8484 if(this.oeForm) {
8485 ajaxLoad(aib.getThrUrl(aib.b, Thread.first.num), false).then(loadedDoc => {
8486 const form = $q(aib.qForm, loadedDoc);
8487 const oeForm = $q(qOeForm, loadedDoc);
8488 pr = new PostForm(form && doc.adoptNode(form), oeForm && doc.adoptNode(oeForm), true);
8489 }, () => (pr = new PostForm(null, null, true)));
8490 } else {
8491 this.form = null;
8492 }
8493 return;
8494 }
8495 this.tNum = aib.t;
8496 this.form = form;
8497 this.files = null;
8498 this.txta = $q(aib.qFormTxta, form);
8499 this.subm = $q(aib.qFormSubm, form);
8500 this.name = $q(aib.qFormName, form);
8501 this.mail = $q(aib.qFormMail, form);
8502 this.subj = $q(aib.qFormSubj, form);
8503 this.passw = $q(aib.qFormPassw, form);
8504 this.rules = $q(aib.qFormRules, form);
8505 this.video = $q('tr input[name="video"], tr input[name="embed"]', form);
8506 this._initFileInputs();
8507 this._makeHideableContainer();
8508 this._makeWindow();
8509 if(!form || !this.txta) {
8510 return;
8511 }
8512 form.style.display = 'inline-block';
8513 form.style.textAlign = 'left';
8514 const { qArea, txta } = this;
8515 new WinResizer('reply', 'top', 'textaHeight', qArea, txta);
8516 new WinResizer('reply', 'left', 'textaWidth', qArea, txta);
8517 new WinResizer('reply', 'right', 'textaWidth', qArea, txta);
8518 new WinResizer('reply', 'bottom', 'textaHeight', qArea, txta);
8519 this._initTextarea();
8520 this.addMarkupPanel();
8521 this.setPlaceholders();
8522 this.updateLanguage();
8523 this._initCaptcha();
8524 this._initSubmit();
8525 if(Cfg.ajaxPosting) {
8526 this._initAjaxPosting();
8527 }
8528 if(Cfg.addSageBtn && this.mail) {
8529 PostForm.hideField($parent(this.mail, 'LABEL') || this.mail);
8530 setTimeout(() => this.toggleSage(), 0);
8531 }
8532 if(Cfg.noPassword && this.passw) {
8533 $hide($qParent(this.passw, aib.qFormTr));
8534 }
8535 if(Cfg.noName && this.name) {
8536 PostForm.hideField(this.name);
8537 }
8538 if(Cfg.noSubj && this.subj) {
8539 PostForm.hideField(this.subj);
8540 }
8541 if(Cfg.userName && this.name) {
8542 setTimeout(PostForm.setUserName, 0);
8543 }
8544 if(this.passw) {
8545 setTimeout(PostForm.setUserPassw, 0);
8546 }
8547 }
8548 static hideField(el) {
8549 const next = el.nextElementSibling;
8550 $toggle(next && (next.style.display !== 'none') ||
8551 el.previousElementSibling ? el : $qParent(el, aib.qFormTr));
8552 }
8553 static setUserName() {
8554 const el = $q('input[info="nameValue"]');
8555 if(el) {
8556 saveCfg('nameValue', el.value);
8557 }
8558 pr.name.value = Cfg.userName ? Cfg.nameValue : '';
8559 }
8560 static setUserPassw() {
8561 if(!Cfg.userPassw) {
8562 return;
8563 }
8564 const el = $q('input[info="passwValue"]');
8565 if(el) {
8566 saveCfg('passwValue', el.value);
8567 }
8568 const value = pr.passw.value = Cfg.passwValue;
8569 for(const { passEl } of DelForm) {
8570 if(passEl) {
8571 passEl.value = value;
8572 }
8573 }
8574 }
8575 get isVisible() {
8576 if(!this.isHidden && this.isBottom && $q(':focus', this.pForm)) {
8577 const cr = this.pForm.getBoundingClientRect();
8578 return cr.bottom > 0 && cr.top < nav.viewportHeight();
8579 }
8580 return false;
8581 }
8582 get sageBtn() {
8583 const value = $aEnd(this.subm, '<span id="de-sagebtn"><svg class="de-btn-sage">' +
8584 '<use xlink:href="#de-symbol-post-sage"/></svg></span>');
8585 value.onclick = () => {
8586 toggleCfg('sageReply');
8587 this.toggleSage();
8588 };
8589 Object.defineProperty(this, 'sageBtn', { value });
8590 return value;
8591 }
8592 get top() {
8593 return this.pForm.getBoundingClientRect().top;
8594 }
8595 addMarkupPanel() {
8596 let el = $id('de-txt-panel');
8597 if(!Cfg.addTextBtns) {
8598 $del(el);
8599 return;
8600 }
8601 if(!el) {
8602 el = $add('<span id="de-txt-panel"></span>');
8603 el.addEventListener('click', this);
8604 el.addEventListener('mouseover', this);
8605 }
8606 el.style.cssFloat = Cfg.txtBtnsLoc ? 'none' : 'right';
8607 $after(Cfg.txtBtnsLoc ? $id('de-resizer-text') || this.txta : this.subm, el);
8608 const id = ['bold', 'italic', 'under', 'strike', 'spoil', 'code', 'sup', 'sub'];
8609 const val = ['B', 'i', 'U', 'S', '%', 'C', 'x\u00b2', 'x\u2082'];
8610 const mode = Cfg.addTextBtns;
8611 let html = '';
8612 for(let i = 0, len = aib.markupTags.length; i < len; ++i) {
8613 const tag = aib.markupTags[i];
8614 if(tag) {
8615 html += `<div id="de-btn-${ id[i] }" de-title="${ Lng.txtBtn[i][lang] }" de-tag="${ tag }">${
8616 mode === 2 ? `${ !html ? '[' : '' } <a class="de-abtn" href="#">${ val[i] }</a> /` :
8617 mode === 3 ? `<button type="button" style="font-weight: bold;">${ val[i] }</button>` :
8618 `<svg><use xlink:href="#de-symbol-markup-${ id[i] }"/></svg>`
8619 }</div>`;
8620 }
8621 }
8622 el.innerHTML = `${ html }<div id="de-btn-quote" de-title="${ Lng.txtBtn[8][lang] }" de-tag="q">${
8623 mode === 2 ? ' <a class="de-abtn" href="#">></a> ]' :
8624 mode === 3 ? '<button type="button" style="font-weight: bold;">></button>' :
8625 '<svg><use xlink:href="#de-symbol-markup-quote"/></svg>'
8626 }</span>`;
8627 }
8628 clearForm() {
8629 if(this.txta) {
8630 this.txta.value = '';
8631 }
8632 if(this.files) {
8633 this.files.clearInputs();
8634 }
8635 if(this.video) {
8636 this.video.value = '';
8637 }
8638 }
8639 closeReply() {
8640 if(this.isQuick) {
8641 this.isQuick = false;
8642 this.lastQuickPNum = -1;
8643 if(!aib.t) {
8644 this._toggleQuickReply(false);
8645 }
8646 this.setReply(false, !aib.t || Cfg.addPostForm > 1);
8647 }
8648 }
8649 handleEvent(e) {
8650 let el = e.target;
8651 if(el.tagName !== 'DIV') {
8652 el = el.parentNode;
8653 }
8654 const { id } = el;
8655 if(!id.startsWith('de-btn')) {
8656 return;
8657 }
8658 if(e.type === 'mouseover') {
8659 if(id === 'de-btn-quote') {
8660 quotetxt = deWindow.getSelection().toString();
8661 }
8662 let key = -1;
8663 if(HotKeys.enabled) {
8664 switch(id.substr(7)) {
8665 case 'bold': key = 12; break;
8666 case 'italic': key = 13; break;
8667 case 'strike': key = 14; break;
8668 case 'spoil': key = 15; break;
8669 case 'code': key = 16;
8670 }
8671 }
8672 KeyEditListener.setTitle(el, key);
8673 return;
8674 }
8675 const txtaEl = pr.txta;
8676 const { selectionStart: start, selectionEnd: end } = txtaEl;
8677 const quote = Cfg.spacedQuote ? '> ' : '>';
8678 if(id === 'de-btn-quote') {
8679 insertText(txtaEl, quote + (start === end ? quotetxt : txtaEl.value.substring(start, end))
8680 .replace(/\n/gm, '\n' + quote));
8681 quotetxt = '';
8682 } else {
8683 const { scrtop } = txtaEl;
8684 const val = PostForm._wrapText(el.getAttribute('de-tag'), txtaEl.value.substring(start, end));
8685 const len = start + val[0];
8686 txtaEl.value = txtaEl.value.substr(0, start) + val[1] + txtaEl.value.substr(end);
8687 txtaEl.setSelectionRange(len, len);
8688 txtaEl.focus();
8689 txtaEl.scrollTop = scrtop;
8690 }
8691 $pd(e);
8692 e.stopPropagation();
8693 }
8694 refreshCap(isErr = false) {
8695 if(this.cap) {
8696 this.cap.refreshCaptcha(isErr, isErr, this.tNum);
8697 }
8698 }
8699 setPlaceholders() {
8700 if(aib.kusaba || !aib.multiFile && Cfg.fileInputs === 2) {
8701 return;
8702 }
8703 this._setPlaceholder('name');
8704 this._setPlaceholder('subj');
8705 this._setPlaceholder('mail');
8706 this._setPlaceholder('video');
8707 if(this.cap) {
8708 this._setPlaceholder('cap');
8709 }
8710 }
8711 setReply(isQuick, needToHide) {
8712 if(isQuick) {
8713 $after(this.qArea.firstChild, this.pForm);
8714 } else {
8715 $after(this.pArea[+this.isBottom], this.qArea);
8716 $after(this._pBtn[+this.isBottom], this.pForm);
8717 }
8718 this.isHidden = needToHide;
8719 $toggle(this.qArea, isQuick);
8720 $toggle(this.pForm, !needToHide);
8721 this.updatePAreaBtns();
8722 }
8723 showMainReply(isBottom, e) {
8724 this.closeReply();
8725 if(!aib.t) {
8726 this.tNum = false;
8727 this.refreshCap();
8728 }
8729 if(this.isBottom === isBottom) {
8730 $toggle(this.pForm, this.isHidden);
8731 this.isHidden = !this.isHidden;
8732 this.updatePAreaBtns();
8733 } else {
8734 this.isBottom = isBottom;
8735 this.setReply(false, false);
8736 }
8737 if(e) {
8738 $pd(e);
8739 }
8740 }
8741 showQuickReply(post, pNum, isCloseReply, isNumClick, isNoLink = false) {
8742 if(!this.isQuick) {
8743 this.isQuick = true;
8744 this.setReply(true, false);
8745 $q('a', this._pBtn[+this.isBottom]).className =
8746 `de-abtn de-parea-btn-${ aib.t ? 'reply' : 'thr' }`;
8747 } else if(isCloseReply && !quotetxt && post.wrap.nextElementSibling === this.qArea) {
8748 this.closeReply();
8749 return;
8750 }
8751 $after(post.wrap, this.qArea);
8752 if(this.qArea.classList.contains('de-win')) {
8753 updateWinZ(this.qArea.style);
8754 }
8755 const qNum = post.thr.num;
8756 if(!aib.t) {
8757 this._toggleQuickReply(qNum);
8758 }
8759 if(!this.form) {
8760 return;
8761 }
8762 if(!aib.t && this.tNum !== qNum) {
8763 this.tNum = qNum;
8764 this.refreshCap();
8765 }
8766 this.tNum = qNum;
8767 const txt = this.txta.value;
8768 const isOnNewLine = txt === '' || txt.slice(-1) === '\n';
8769 const link = isNoLink || post.isOp && !Cfg.addOPLink && !aib.t && !isNumClick ? '' :
8770 isNumClick ? `>>${ pNum }${ isOnNewLine ? '\n' : '' }` :
8771 (isOnNewLine ? '' : '\n') +
8772 (this.lastQuickPNum === pNum && txt.includes('>>' + pNum) ? '' : `>>${ pNum }\n`);
8773 const quote = !quotetxt ? '' : `${ quotetxt.replace(/^\n|\n$/g, '')
8774 .replace(/(^|\n)(.)/gm, `$1>${ Cfg.spacedQuote ? ' ' : '' }$2`) }\n`;
8775 insertText(this.txta, link + quote);
8776 const winTitle = post.thr.op.title.trim();
8777 $q('.de-win-title', this.qArea).textContent =
8778 (winTitle.length < 28 ? winTitle : `${ winTitle.substr(0, 30) }\u2026`) || `#${ pNum }`;
8779 this.lastQuickPNum = pNum;
8780 }
8781 toggleSage() {
8782 if(!Cfg.addSageBtn || !this.mail) {
8783 return;
8784 }
8785 const isSage = Cfg.sageReply;
8786 this.sageBtn.style.opacity = isSage ? '1' : '.3';
8787 this.sageBtn.title = isSage ? 'SAGE!' : Lng.noSage[lang];
8788 if(this.mail.type === 'text') {
8789 this.mail.value = isSage ? 'sage' : aib._4chan ? 'noko' : '';
8790 } else {
8791 this.mail.checked = isSage;
8792 }
8793 }
8794 updateLanguage() {
8795 this.txta.title = Lng.pasteImage[lang];
8796 aib.updateSubmitBtn(this.subm);
8797 }
8798 updatePAreaBtns() {
8799 const txt = 'de-abtn de-parea-btn-';
8800 const rep = aib.t ? 'reply' : 'thr';
8801 $q('a', this._pBtn[+this.isBottom]).className = txt + (!this.pForm.style.display ? 'close' : rep);
8802 $q('a', this._pBtn[+!this.isBottom]).className = txt + rep;
8803 }
8804
8805 static _wrapText(tag, text) {
8806 let isBB = aib.markupBB;
8807 if(tag.startsWith('[')) {
8808 tag = tag.substr(1);
8809 isBB = true;
8810 }
8811 if(isBB) {
8812 if(text.includes('\n')) {
8813 const str = `[${ tag }]${ text }[/${ tag }]`;
8814 return [str.length, str];
8815 }
8816 const m = text.match(/^(\s*)(.*?)(\s*)$/);
8817 const str = `${ m[1] }[${ tag }]${ m[2] }[/${ tag }]${ m[3] }`;
8818 return [!m[2].length ? m[1].length + tag.length + 2 : str.length, str];
8819 }
8820 let m, rv = '', i = 0;
8821 const arr = text.split('\n');
8822 for(let len = arr.length; i < len; ++i) {
8823 m = arr[i].match(/^(\s*)(.*?)(\s*)$/);
8824 rv += '\n' + m[1] + (tag === '^H' ? m[2] + '^H'.repeat(m[2].length) : tag + m[2] + tag) + m[3];
8825 }
8826 return [i === 1 && !m[2].length && tag !== '^H' ?
8827 m[1].length + tag.length :
8828 rv.length - 1, rv.slice(1)];
8829 }
8830 _initAjaxPosting() {
8831 let el;
8832 if(aib.qFormRedir && (el = $q(aib.qFormRedir, this.form))) {
8833 aib.disableRedirection(el);
8834 }
8835 this.form.onsubmit = e => {
8836 $pd(e);
8837 $popup('upload', Lng.sending[lang], true);
8838 html5Submit(this.form, this.subm, true).then(checkUpload)
8839 .catch(err => $popup('upload', getErrorMessage(err)));
8840 };
8841 }
8842 _initCaptcha() {
8843 const capEl =
8844 $q('input[type="text"][name*="aptcha"], *[id*="captcha"], *[class*="captcha"]', this.form);
8845 if(!capEl) {
8846 this.cap = null;
8847 return;
8848 }
8849 this.cap = new Captcha(capEl, this.tNum);
8850 const updCapFn = () => {
8851 this.cap.addCaptcha();
8852 this.cap.updateOutdated();
8853 };
8854 this.txta.addEventListener('focus', updCapFn);
8855 if(this.files) {
8856 this.files.onchange = updCapFn;
8857 }
8858 this.form.addEventListener('click', () => this.cap.addCaptcha(), true);
8859 }
8860 _initFileInputs() {
8861 const fileEl = $q(aib.qFormFile, this.form);
8862 if(!fileEl) {
8863 return;
8864 }
8865 if(aib.fixFileInputs) {
8866 aib.fixFileInputs($qParent(fileEl, aib.qFormTd));
8867 }
8868 this.files = new Files(this, $q(aib.qFormFile, this.form));
8869 // We need to clear file inputs in case if session was restored.
8870 deWindow.addEventListener('load',
8871 () => setTimeout(() => !this.files.filesCount && this.files.clearInputs(), 0));
8872 }
8873 _initSubmit() {
8874 this.subm.addEventListener('click', e => {
8875 if(aib.makaba && !aib._2channel && !Cfg.altCaptcha) {
8876 if(!this.cap.isSubmitWait) {
8877 $pd(e);
8878 $popup('upload', 'reCaptcha...', true);
8879 this.cap.isSubmitWait = true;
8880 this.refreshCap();
8881 return;
8882 }
8883 this.cap.isSubmitWait = false;
8884 }
8885 if(Cfg.warnSubjTrip && this.subj && /#.|##./.test(this.subj.value)) {
8886 $pd(e);
8887 $popup('upload', Lng.subjHasTrip[lang]);
8888 return;
8889 }
8890 let val = this.txta.value;
8891 if(Spells.outreps) {
8892 val = Spells.outReplace(val);
8893 }
8894 if(this.tNum && pByNum.get(this.tNum).subj === 'Dollchan Extension Tools') {
8895 const temp = `\n\n${ PostForm._wrapText(aib.markupTags[5],
8896 `${ '-'.repeat(50) }\n${ nav.ua }\nv${ version }.${ commit }${
8897 nav.isESNext ? '.es6' : '' } [${ nav.scriptHandler }]`
8898 )[1] }`;
8899 if(!val.includes(temp)) {
8900 val += temp;
8901 }
8902 }
8903 this.txta.value = val;
8904 this.toggleSage();
8905 if(Cfg.ajaxPosting) {
8906 $popup('upload', Lng.checking[lang], true);
8907 }
8908 if(this.video && (val = this.video.value) && (val = val.match(Videos.ytReg))) {
8909 this.video.value = 'http://www.youtube.com/watch?v=' + val[1];
8910 }
8911 if(this.isQuick) {
8912 $hide(this.pForm);
8913 $hide(this.qArea);
8914 $after(this._pBtn[+this.isBottom], this.pForm);
8915 }
8916 updater.pauseUpdater();
8917 });
8918 }
8919 _initTextarea() {
8920 const el = this.txta;
8921 if(aib.dobrochan) {
8922 el.removeAttribute('id');
8923 }
8924 el.classList.add('de-textarea');
8925 const { style } = el;
8926 style.setProperty('width', Cfg.textaWidth + 'px', 'important');
8927 style.setProperty('height', Cfg.textaHeight + 'px', 'important');
8928 // Allow to scroll page on PgUp/PgDn
8929 el.addEventListener('keypress', e => {
8930 const code = e.charCode || e.keyCode;
8931 if((code === 33 /* PgUp */ || code === 34 /* PgDn */) && e.which === 0) {
8932 e.target.blur();
8933 deWindow.focus();
8934 }
8935 });
8936 // Add image from clipboard to file inputs on Ctrl+V
8937 el.addEventListener('paste', e => {
8938 if('clipboardData' in e) {
8939 for(const item of e.clipboardData.items) {
8940 if(item.kind === 'file') {
8941 const inputs = this.files._inputs;
8942 for(let i = 0, len = inputs.length; i < len; ++i) {
8943 const input = inputs[i];
8944 if(!input.hasFile) {
8945 const file = item.getAsFile();
8946 input._addUrlFile(URL.createObjectURL(file), file);
8947 break;
8948 }
8949 }
8950 }
8951 }
8952 }
8953 });
8954 // Make textarea resizer
8955 if(nav.isFirefox || nav.isWebkit) {
8956 el.addEventListener('mouseup', ({ target }) => {
8957 const s = target.style;
8958 const { width, height } = s;
8959 s.setProperty('width', width + 'px', 'important');
8960 s.setProperty('height', height + 'px', 'important');
8961 saveCfg('textaWidth', parseInt(width, 10));
8962 saveCfg('textaHeight', parseInt(height, 10));
8963 });
8964 return;
8965 }
8966 $aEnd(el, '<div id="de-resizer-text"></div>').addEventListener('mousedown', {
8967 _el : el,
8968 _elStyle : style,
8969 handleEvent(e) {
8970 switch(e.type) {
8971 case 'mousedown':
8972 docBody.addEventListener('mousemove', this);
8973 docBody.addEventListener('mouseup', this);
8974 $pd(e);
8975 return;
8976 case 'mousemove': {
8977 const cr = this._el.getBoundingClientRect();
8978 this._elStyle.setProperty('width', (e.clientX - cr.left) + 'px', 'important');
8979 this._elStyle.setProperty('height', (e.clientY - cr.top) + 'px', 'important');
8980 return;
8981 }
8982 default: // mouseup
8983 docBody.removeEventListener('mousemove', this);
8984 docBody.removeEventListener('mouseup', this);
8985 saveCfg('textaWidth', parseInt(this._elStyle.width, 10));
8986 saveCfg('textaHeight', parseInt(this._elStyle.height, 10));
8987 }
8988 }
8989 });
8990 }
8991 _makeHideableContainer() {
8992 this.pForm = $add('<div id="de-pform" class="de-win-body"></div>');
8993 if(this.form) {
8994 this.pForm.appendChild(this.form);
8995 }
8996 if(this.oeForm) {
8997 this.pForm.appendChild(this.oeForm);
8998 }
8999 const html = '<div class="de-parea"><div>[<a href="#"></a>]</div><hr></div>';
9000 this.pArea = [
9001 $bBegin(DelForm.first.el, html),
9002 $aEnd(aib._4chan ? $q('.board', DelForm.first.el) : DelForm.first.el, html)
9003 ];
9004 this._pBtn = [this.pArea[0].firstChild, this.pArea[1].firstChild];
9005 this._pBtn[0].firstElementChild.onclick = e => this.showMainReply(false, e);
9006 this._pBtn[1].firstElementChild.onclick = e => this.showMainReply(true, e);
9007 this.qArea = $add(`<div style="display: none; ${ Cfg.replyWinX }; ${
9008 Cfg.replyWinY }; z-index: ${ ++topWinZ };" id="de-win-reply" class="${
9009 aib.cReply + (Cfg.replyWinDrag ? ' de-win' : ' de-win-inpost') }"></div>`);
9010 this.isBottom = Cfg.addPostForm === 1;
9011 this.setReply(false, !aib.t || Cfg.addPostForm > 1);
9012 }
9013 _makeWindow() {
9014 makeDraggable('reply', this.qArea, $aBegin(this.qArea, `<div class="de-win-head">
9015 <span class="de-win-title"></span>
9016 <span class="de-win-buttons">
9017 <svg class="de-win-btn-clear"><use xlink:href="#de-symbol-unavail"/></svg>
9018 <svg class="de-win-btn-toggle"><use xlink:href="#de-symbol-win-arrow"/></svg>
9019 <svg class="de-win-btn-close"><use xlink:href="#de-symbol-win-close"/></svg>
9020 </span>
9021 </div>
9022 <div class="de-resizer de-resizer-top"></div>
9023 <div class="de-resizer de-resizer-left"></div>
9024 <div class="de-resizer de-resizer-right"></div>
9025 <div class="de-resizer de-resizer-bottom"></div>`));
9026 const buttons = $q('.de-win-buttons', this.qArea);
9027 buttons.onmouseover = ({ target }) => {
9028 const el = target.parentNode;
9029 switch(fixEventEl(target).classList[0]) {
9030 case 'de-win-btn-clear': el.title = Lng.clearForm[lang]; break;
9031 case 'de-win-btn-close': el.title = Lng.closeReply[lang]; break;
9032 case 'de-win-btn-toggle': el.title = Cfg.replyWinDrag ? Lng.underPost[lang] : Lng.makeDrag[lang];
9033 }
9034 };
9035 const [clearBtn, toggleBtn, closeBtn] = [...buttons.children];
9036 clearBtn.onclick = () => {
9037 saveCfg('sageReply', 0);
9038 this.toggleSage();
9039 this.files.clearInputs();
9040 [this.txta, this.name, this.mail, this.subj, this.video, this.cap && this.cap.textEl].forEach(
9041 el => el && (el.value = ''));
9042 };
9043 toggleBtn.onclick = () => {
9044 toggleCfg('replyWinDrag');
9045 if(Cfg.replyWinDrag) {
9046 this.qArea.className = aib.cReply + ' de-win';
9047 updateWinZ(this.qArea.style);
9048 } else {
9049 this.qArea.className = aib.cReply + ' de-win-inpost';
9050 this.txta.focus();
9051 }
9052 };
9053 closeBtn.onclick = () => this.closeReply();
9054 }
9055 _setPlaceholder(val) {
9056 const el = val === 'cap' ? this.cap.textEl : this[val];
9057 if(el) {
9058 toggleAttr(el, 'placeholder', Lng[val][lang], aib.multiFile || Cfg.fileInputs !== 2);
9059 }
9060 }
9061 _toggleQuickReply(tNum) {
9062 if(this.oeForm) {
9063 $del($q('input[name="oek_parent"]', this.oeForm));
9064 if(tNum) {
9065 this.oeForm.insertAdjacentHTML('afterbegin',
9066 `<input type="hidden" value="${ tNum }" name="oek_parent">`);
9067 }
9068 }
9069 if(this.form) {
9070 if(aib.changeReplyMode && tNum !== this.tNum) {
9071 aib.changeReplyMode(this.form, tNum);
9072 }
9073 $del($q(`input[name="${ aib.formParent }"]`, this.form));
9074 if(tNum) {
9075 this.form.insertAdjacentHTML('afterbegin',
9076 `<input type="hidden" name="${ aib.formParent }" value="${ tNum }">`);
9077 }
9078 }
9079 }
9080}
9081
9082/* ==[ FormSubmit.js ]========================================================================================
9083 SUBMIT
9084 postform/delform html5/iframe submit, images and webms parsing, duplicate files posting, EXIF clearing
9085=========================================================================================================== */
9086
9087function getSubmitError(dc) {
9088 if(!dc.body.hasChildNodes() || $q(aib.qDForm, dc)) {
9089 return null;
9090 }
9091 const err = [...$Q(aib.qError, dc)].map(str => str.innerHTML + '\n').join('')
9092 .replace(/<a [^>]+>Назад.+|<br.+/, '') || dc.body.innerHTML;
9093 return aib.isIgnoreError(err) ? null : err;
9094}
9095
9096function checkUpload(data) {
9097 let error = null;
9098 let postNum = null;
9099 const isDocument = data instanceof HTMLDocument;
9100 if(aib.getSubmitData) {
9101 if(aib.jsonSubmit) {
9102 const _data = (isDocument ? data.body.textContent : data).trim();
9103 try {
9104 data = JSON.parse(_data);
9105 } catch(err) {
9106 error = getSubmitError(_data);
9107 }
9108 }
9109 if(!error) {
9110 ({ error, postNum } = aib.getSubmitData(data));
9111 }
9112 } else {
9113 error = getSubmitError(data);
9114 }
9115 if(error) {
9116 if(pr.isQuick) {
9117 pr.setReply(true, false);
9118 }
9119 if(/[cf]aptch|капч|подтвер|verifi/i.test(error)) {
9120 pr.refreshCap(true);
9121 }
9122 $popup('upload', error.toString());
9123 updater.sendErrNotif();
9124 updater.continueUpdater();
9125 DollchanAPI.notify('submitform', { success: false, error });
9126 return;
9127 }
9128 const { tNum } = pr;
9129 if((Cfg.markMyPosts || Cfg.markMyLinks) && postNum) {
9130 MyPosts.set(postNum, tNum || postNum);
9131 }
9132 if(Cfg.favOnReply && !Cfg.sageReply) {
9133 if(tNum) {
9134 const { thr } = pByNum.get(tNum);
9135 if(!thr.isFav) {
9136 thr.toggleFavState(true);
9137 }
9138 } else {
9139 sesStorage['de-fav-newthr'] = JSON.stringify({ num: postNum, date: Date.now() });
9140 }
9141 }
9142 pr.clearForm();
9143 DollchanAPI.notify('submitform', { success: true, num: postNum });
9144 Cfg.stats[tNum ? 'reply' : 'op']++;
9145 saveCfgObj(aib.dm, Cfg);
9146 if(!tNum) {
9147 if(postNum) {
9148 deWindow.location.assign(aib.getThrUrl(aib.b, postNum));
9149 } else if(isDocument) {
9150 const dForm = $q(aib.qDForm, data);
9151 if(dForm) {
9152 deWindow.location.assign(aib.getThrUrl(aib.b, aib.getTNum(dForm)));
9153 }
9154 }
9155 return;
9156 }
9157 if(aib.t) {
9158 Post.clearMarks();
9159 Thread.first.loadNewPosts().then(() => AjaxError.Success, err => err).then(err => {
9160 infoLoadErrors(err);
9161 if(Cfg.scrAfterRep) {
9162 scrollTo(0, deWindow.pageYOffset + Thread.first.last.el.getBoundingClientRect().top);
9163 }
9164 updater.continueUpdater(true);
9165 closePopup('upload');
9166 });
9167 } else {
9168 pByNum.get(tNum).thr.loadPosts('new', false, false).then(() => closePopup('upload'));
9169 }
9170 pr.closeReply();
9171 pr.refreshCap();
9172}
9173
9174async function checkDelete(data) {
9175 const err = getSubmitError(data instanceof HTMLDocument ? data : $DOM(data));
9176 if(err) {
9177 $popup('delete', Lng.errDelete[lang] + ':\n' + err);
9178 updater.sendErrNotif();
9179 return;
9180 }
9181 const els = $Q(`[de-form] ${ aib.qRPost } input:checked`);
9182 const threads = new Set();
9183 const isThr = aib.t;
9184 for(let i = 0, len = els.length; i < len; ++i) {
9185 const el = els[i];
9186 el.checked = false;
9187 if(!isThr) {
9188 threads.add(aib.getPostOfEl(el).thr);
9189 }
9190 }
9191 if(isThr) {
9192 Post.clearMarks();
9193 await Thread.first.loadNewPosts().catch(err => infoLoadErrors(err));
9194 } else {
9195 await Promise.all([...threads].map(thr => thr.loadPosts('new', false, false)));
9196 }
9197 $popup('delete', Lng.succDeleted[lang]);
9198}
9199
9200// https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
9201function isFormElDisabled(el) {
9202 switch(el.tagName.toLowerCase()) {
9203 case 'button':
9204 case 'input':
9205 case 'select':
9206 case 'textarea':
9207 if(el.hasAttribute('disabled')) {
9208 return true;
9209 }
9210 /* falls through */
9211 default:
9212 if(nav.matchesSelector(el, 'fieldset[disabled] > :not(legend):not(:first-of-type) *')) {
9213 return true;
9214 }
9215 }
9216 return false;
9217}
9218// https://html.spec.whatwg.org/multipage/forms.html#constructing-form-data-set
9219function * getFormElements(form, submitter) {
9220 const controls = $Q('button, input, keygen, object, select, textarea', form);
9221 const fixName = name => name ? name.replace(/([^\r])\n|\r([^\n])/g, '$1\r\n$2') : '';
9222
9223 constructSet:
9224 for(let i = 0, len = controls.length; i < len; ++i) {
9225 const field = controls[i];
9226 const tagName = field.tagName.toLowerCase();
9227 const type = field.getAttribute('type');
9228 const name = field.getAttribute('name');
9229 if($parent(field, 'DATALIST', form) || isFormElDisabled(field) ||
9230 field !== submitter && (
9231 tagName === 'button' ||
9232 tagName === 'input' && (type === 'submit' || type === 'reset' || type === 'button')
9233 ) ||
9234 tagName === 'input' && (
9235 type === 'checkbox' && !field.checked ||
9236 type === 'radio' && !field.checked ||
9237 type === 'image' && !name
9238 ) ||
9239 tagName === 'object'
9240 ) {
9241 continue;
9242 }
9243 if(tagName === 'select') {
9244 const options = $Q('select > option, select > optgrout > option', field);
9245 for(let j = 0, jlen = options.length; j < jlen; ++j) {
9246 const option = options[j];
9247 if(option.selected && !isFormElDisabled(option)) {
9248 yield { type, el: field, name: fixName(name), value: option.value };
9249 }
9250 }
9251 } else if(tagName === 'input') {
9252 switch(type) {
9253 case 'image': throw new Error('input[type="image"] is not supported');
9254 case 'checkbox':
9255 case 'radio':
9256 yield { type, el: field, name: fixName(name), value: field.value || 'on' };
9257 continue constructSet;
9258 case 'file': {
9259 let img;
9260 if(field.files.length > 0) {
9261 const { files } = field;
9262 for(let j = 0, jlen = files.length; j < jlen; ++j) {
9263 yield { name, type, el: field, value: files[j] };
9264 }
9265 } else if(field.obj && (img = field.obj.imgFile)) {
9266 yield {
9267 name,
9268 type,
9269 el : field,
9270 value : new File([img.data], img.name, { type: img.type })
9271 };
9272 } else {
9273 yield {
9274 el : field,
9275 name : fixName(name),
9276 type : 'application/octet-stream',
9277 value : new File([''], '')
9278 };
9279 }
9280 continue constructSet;
9281 }
9282 }
9283 }
9284 if(type === 'textarea') {
9285 yield { type, el: field, name: name || '', value: field.value };
9286 } else {
9287 yield { type, el: field, name: fixName(name), value: field.value };
9288 }
9289 const dirname = field.getAttribute('dirname');
9290 if(dirname) {
9291 yield {
9292 el : field,
9293 name : fixName(dirname),
9294 type : 'direction',
9295 value : nav.matchesSelector(field, ':dir(rtl)') ? 'rtl' : 'ltr'
9296 };
9297 }
9298 }
9299}
9300
9301function getUploadFunc() {
9302 $popup('upload', Lng.sending[lang] +
9303 '<br><progress id="de-uploadprogress" value="0" max="1" style="display: none; width: 200px;">' +
9304 '</progress><div style="display: none; font: bold 12px arial;">' +
9305 '<span></span> / <span></span> (<span></span>)</div>', true);
9306 let isInited = false;
9307 const beginTime = Date.now();
9308 const progress = $id('de-uploadprogress');
9309 const counterWrap = progress.nextElementSibling;
9310 const [counterEl, totalEl, speedEl] = [...counterWrap.children];
9311 return ({ total, loaded: i }) => {
9312 if(!isInited) {
9313 progress.setAttribute('max', total);
9314 $show(progress);
9315 totalEl.textContent = prettifySize(total);
9316 $show(counterWrap);
9317 isInited = true;
9318 }
9319 progress.value = i;
9320 counterEl.textContent = prettifySize(i);
9321 speedEl.textContent = `${ prettifySize(1e3 * i / (Date.now() - beginTime)) }/${ Lng.second[lang] }`;
9322 };
9323}
9324
9325async function html5Submit(form, submitter, needProgress = false) {
9326 const data = new FormData();
9327 let hasFiles = false;
9328 for(const { name, value, type, el } of getFormElements(form, submitter)) {
9329 let val = value;
9330 if(name === 'de-file-txt') {
9331 continue;
9332 }
9333 if(type === 'file') {
9334 hasFiles = true;
9335 const fileName = value.name;
9336 const fileExt = fileName.substring(fileName.lastIndexOf('.'));
9337 const newFileName =
9338 !Cfg.removeFName || el.obj && el.obj.imgFile && el.obj.imgFile.isConstName ? fileName : (
9339 Cfg.removeFName === 1 ? '' :
9340 // 5 years = 5*365*24*60*60*1e3 = 15768e7
9341 Date.now() - (Cfg.removeFName === 2 ? 0 : Math.round(Math.random() * 15768e7))
9342 ) + fileExt;
9343 const mime = value.type;
9344 if((Cfg.postSameImg || Cfg.removeEXIF) && (
9345 mime === 'image/jpeg' ||
9346 mime === 'image/png' ||
9347 mime === 'image/gif' ||
9348 mime === 'video/webm' && !aib.makaba)
9349 ) {
9350 const cleanData = cleanFile((await readFile(value)).data, el.obj ? el.obj.extraFile : null);
9351 if(!cleanData) {
9352 return Promise.reject(new Error(Lng.fileCorrupt[lang] + ': ' + fileName));
9353 }
9354 val = new File(cleanData, newFileName, { type: mime });
9355 } else if(Cfg.removeFName) {
9356 val = new File([value], newFileName, { type: mime });
9357 }
9358 }
9359 data.append(name, val);
9360 }
9361 if(aib.sendHTML5Post) {
9362 return aib.sendHTML5Post(form, data, needProgress, hasFiles);
9363 }
9364 const ajaxParams = { data, method: 'POST' };
9365 if(needProgress && hasFiles) {
9366 ajaxParams.onprogress = getUploadFunc();
9367 }
9368 return $ajax(form.action, ajaxParams)
9369 .then(xhr => aib.jsonSubmit ? xhr.responseText : $DOM(xhr.responseText))
9370 .catch(err => Promise.reject(err));
9371}
9372
9373function cleanFile(data, extraData) {
9374 const img = nav.getUnsafeUint8Array(data);
9375 const rand = Cfg.postSameImg && String(Math.round(Math.random() * 1e6));
9376 const rv = extraData ?
9377 rand ? [img, extraData, rand] : [img, extraData] :
9378 rand ? [img, rand] : [img];
9379 const rExif = !!Cfg.removeEXIF;
9380 if(!rand && !rExif && !extraData) {
9381 return rv;
9382 }
9383 let i, len, val, lIdx, jpgDat;
9384 const subarray = (begin, end) => nav.getUnsafeUint8Array(data, begin, end - begin);
9385 // JPG
9386 if(img[0] === 0xFF && img[1] === 0xD8) {
9387 let deep = 1;
9388 for(i = 2, len = img.length - 1, val = [null, null], lIdx = 2, jpgDat = null; i < len;) {
9389 if(img[i] === 0xFF) {
9390 if(rExif) {
9391 // Remove exif data
9392 if(!jpgDat && deep === 1) {
9393 if(img[i + 1] === 0xE1 && img[i + 4] === 0x45) {
9394 jpgDat = readExif(data, i + 10, (img[i + 2] << 8) + img[i + 3]);
9395 } else if(img[i + 1] === 0xE0 && img[i + 7] === 0x46 &&
9396 (img[i + 2] !== 0 || img[i + 3] >= 0x0E || img[i + 15] !== 0xFF)
9397 ) {
9398 jpgDat = subarray(i + 11, i + 16);
9399 }
9400 }
9401 if(((img[i + 1] >> 4) === 0xE && img[i + 1] !== 0xEE) || img[i + 1] === 0xFE) {
9402 if(lIdx !== i) {
9403 val.push(subarray(lIdx, i));
9404 }
9405 i += 2 + (img[i + 2] << 8) + img[i + 3];
9406 lIdx = i;
9407 continue;
9408 }
9409 } else if(img[i + 1] === 0xD8) { // Jpg start marker [0xFFD8]
9410 deep++;
9411 i++;
9412 continue;
9413 }
9414 if(img[i + 1] === 0xD9 && --deep === 0) { // Jpg end marker [0xFFD9]
9415 break;
9416 }
9417 }
9418 i++;
9419 }
9420 i += 2;
9421 if(!extraData && len - i > 75) {
9422 i = len;
9423 }
9424 if(lIdx === 2) {
9425 // Remove data after the end marker
9426 if(i !== len) {
9427 rv[0] = nav.getUnsafeUint8Array(data, 0, i);
9428 }
9429 return rv;
9430 }
9431 val[0] = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0, 0, 0x0E, 0x4A, 0x46, 0x49, 0x46, 0, 1, 1]);
9432 val[1] = jpgDat || new Uint8Array([0, 0, 1, 0, 1]);
9433 val.push(subarray(lIdx, i));
9434 if(extraData) {
9435 val.push(extraData);
9436 }
9437 if(rand) {
9438 val.push(rand);
9439 }
9440 return val;
9441 }
9442 // PNG
9443 if(img[0] === 0x89 && img[1] === 0x50) {
9444 // Search for end marker [0x49454e44]
9445 for(i = 0, len = img.length - 7; i < len && (
9446 img[i] !== 0x49 ||
9447 img[i + 1] !== 0x45 ||
9448 img[i + 2] !== 0x4E ||
9449 img[i + 3] !== 0x44
9450 ); ++i) /* empty */;
9451 i += 8;
9452 // Remove data after the end marker
9453 if(i !== len && (extraData || len - i <= 75)) {
9454 rv[0] = nav.getUnsafeUint8Array(data, 0, i);
9455 }
9456 return rv;
9457 }
9458 // GIF
9459 if(img[0] === 0x47 && img[1] === 0x49 && img[2] === 0x46) {
9460 // Search for last frame end marker [0x003B]
9461 i = len = img.length;
9462 while(i && img[--i - 1] !== 0x00 && img[i] !== 0x3B) /* empty */;
9463 // Remove data after the end marker
9464 if(++i !== len) {
9465 rv[0] = nav.getUnsafeUint8Array(data, 0, i);
9466 }
9467 return rv;
9468 }
9469 // WEBM
9470 if(img[0] === 0x1a && img[1] === 0x45 && img[2] === 0xDF && img[3] === 0xA3) {
9471 return new WebmParser(data).addWebmData(rand).getWebmData();
9472 }
9473 return null;
9474}
9475
9476function readExif(data, off, len) {
9477 let xRes = 0;
9478 let yRes = 0;
9479 let resT = 0;
9480 const dv = nav.getUnsafeDataView(data, off);
9481 const le = String.fromCharCode(dv.getUint8(0), dv.getUint8(1)) !== 'MM';
9482 if(dv.getUint16(2, le) !== 0x2A) {
9483 return null;
9484 }
9485 const i = dv.getUint32(4, le);
9486 if(i > len) {
9487 return null;
9488 }
9489 for(let j = 0, tgLen = dv.getUint16(i, le); j < tgLen; ++j) {
9490 let dE = i + 2 + 12 * j;
9491 const tag = dv.getUint16(dE, le);
9492 if(tag === 0x0128) {
9493 resT = dv.getUint16(dE + 8, le) - 1;
9494 } else if(tag === 0x011A || tag === 0x011B) {
9495 dE = dv.getUint32(dE + 8, le);
9496 if(dE > len) {
9497 return null;
9498 }
9499 if(tag === 0x11A) {
9500 xRes = Math.round(dv.getUint32(dE, le) / dv.getUint32(dE + 4, le));
9501 } else {
9502 yRes = Math.round(dv.getUint32(dE, le) / dv.getUint32(dE + 4, le));
9503 }
9504 }
9505 }
9506 xRes = xRes || yRes;
9507 yRes = yRes || xRes;
9508 return new Uint8Array([resT & 0xFF, xRes >> 8, xRes & 0xFF, yRes >> 8, yRes & 0xFF]);
9509}
9510
9511/* ==[ FormFile.js ]==========================================================================================
9512 FILE INPUTS
9513 image/video files in postform: preview, adding by url, drag-n-drop, deleting
9514=========================================================================================================== */
9515
9516class Files {
9517 constructor(form, fileEl) {
9518 this.filesCount = 0;
9519 this.fileTr = $qParent(fileEl, aib.qFormTr);
9520 this.onchange = null;
9521 this._form = form;
9522 this._inputs = [];
9523 const els = $Q('input[type="file"]', this.fileTr);
9524 for(let i = 0, len = els.length; i < len; ++i) {
9525 this._inputs.push(new FileInput(this, els[i]));
9526 }
9527 this._files = [];
9528 this.hideEmpty();
9529 }
9530 get rarInput() {
9531 const value = $bEnd(docBody, '<input type="file" style="display: none;">');
9532 Object.defineProperty(this, 'rarInput', { value });
9533 return value;
9534 }
9535 get thumbsEl() {
9536 let value;
9537 if(aib.multiFile) {
9538 value = $aEnd(this.fileTr, '<div id="de-file-area"></div>');
9539 } else {
9540 value = $qParent(this._form.txta, aib.qFormTd).previousElementSibling;
9541 value.innerHTML = `<div style="display: none;">${ value.innerHTML }</div><div></div>`;
9542 value = value.lastChild;
9543 }
9544 Object.defineProperty(this, 'thumbsEl', { value });
9545 return value;
9546 }
9547 changeMode() {
9548 const isThumbMode = Cfg.fileInputs === 2;
9549 for(const inp of this._inputs) {
9550 inp.changeMode(isThumbMode);
9551 }
9552 this.hideEmpty();
9553 }
9554 clearInputs() {
9555 for(const inp of this._inputs) {
9556 inp.clearInp();
9557 }
9558 this.hideEmpty();
9559 }
9560 hideEmpty() {
9561 for(let els = this._inputs, i = els.length - 1; i > 0; --i) {
9562 const inp = els[i];
9563 if(inp.hasFile) {
9564 break;
9565 } else if(els[i - 1].hasFile) {
9566 inp.showInp();
9567 break;
9568 }
9569 inp.hideInp();
9570 }
9571 }
9572}
9573
9574class FileInput {
9575 constructor(parent, el) {
9576 this.extraFile = null;
9577 this.hasFile = false;
9578 this.imgFile = null;
9579 this._input = el;
9580 this._isTxtEditable = false;
9581 this._isTxtEditName = false;
9582 this._mediaEl = null;
9583 this._parent = parent;
9584 this._rarMsg = null;
9585 this._spoilEl = $q(aib.qFormSpoiler, el.parentNode);
9586 this._thumb = null;
9587 this._utils = $add(`<div class="de-file-utils">
9588 <span class="de-file-btn-rar" title="${ Lng.helpAddFile[lang] }" style="display: none;">
9589 <svg><use xlink:href="#de-symbol-file-rar"/></svg></span>
9590 <input class="de-file-spoil" type="checkbox" title="` +
9591 `${ Lng.spoilFile[lang] }" style="display: none;">
9592 <span class="de-file-btn-txt" title="${ Lng.addManually[lang] }">
9593 <svg><use xlink:href="#de-symbol-file-txt"/></svg></span>
9594 <span class="de-file-btn-ren" title="${ Lng.renameFile[lang] }" style="display: none;">
9595 <svg><use xlink:href="#de-symbol-file-ren"/></svg></span>
9596 <span class="de-file-btn-del" title="${ Lng.removeFile[lang] }" style="display: none;">
9597 <svg><use xlink:href="#de-symbol-file-del"/></svg></span>
9598 </div>`);
9599 [this._btnRar, this._btnSpoil, this._btnTxt, this._btnRen, this._btnDel] = [...this._utils.children];
9600 this._utils.addEventListener('click', this);
9601 this._txtWrap = $add(`<span class="de-file-txt-wrap">
9602 <input type="text" name="de-file-txt" class="de-file-txt-input de-file-txt-noedit" title="` +
9603 `${ Lng.youCanDrag[lang] }" placeholder="${ Lng.dropFileHere[lang] }">
9604 <input type="button" class="de-file-txt-add" value="+" title="` +
9605 `${ Lng.add[lang] }" style="display: none;"></span>`);
9606 [this._txtInput, this._txtAddBtn] = [...this._txtWrap.children];
9607 this._txtWrap.addEventListener('click', this);
9608 this._toggleDragEvents(this._txtWrap, true);
9609 el.obj = this;
9610 el.classList.add('de-file-input');
9611 el.addEventListener('change', this);
9612 if(el.files && el.files[0]) {
9613 this._removeFile();
9614 }
9615 if(Cfg.fileInputs) {
9616 $hide(el);
9617 if(aib.multiFile) {
9618 this._input.setAttribute('multiple', true);
9619 }
9620 }
9621 if(FileInput._isThumbMode) {
9622 this._initThumbs();
9623 } else {
9624 $before(this._input, this._txtWrap);
9625 $after(this._input, this._utils);
9626 }
9627 }
9628 changeMode(showThumbs) {
9629 $toggle(this._input, !Cfg.fileInputs);
9630 toggleAttr(this._input, 'multiple', true, aib.multiFile && Cfg.fileInputs);
9631 $toggle(this._btnRen, Cfg.fileInputs && this.hasFile);
9632 if(!(showThumbs ^ !!this._thumb)) {
9633 return;
9634 }
9635 if(showThumbs) {
9636 this._initThumbs();
9637 return;
9638 }
9639 $before(this._input, this._txtWrap);
9640 $after(this._input, this._utils);
9641 $del($q('de-file-txt-area'));
9642 $show(this._parent.fileTr);
9643 $show(this._txtWrap);
9644 if(this._mediaEl) {
9645 deWindow.URL.revokeObjectURL(this._mediaEl.src);
9646 }
9647 this._toggleDragEvents(this._thumb, false);
9648 $del(this._thumb);
9649 this._thumb = this._mediaEl = null;
9650 }
9651 clearInp() {
9652 if(FileInput._isThumbMode) {
9653 this._thumb.classList.add('de-file-off');
9654 if(this._mediaEl) {
9655 deWindow.URL.revokeObjectURL(this._mediaEl.src);
9656 this._mediaEl.parentNode.title = Lng.youCanDrag[lang];
9657 this._mediaEl.remove();
9658 this._mediaEl = null;
9659 }
9660 }
9661 if(this._btnDel) {
9662 this._toggleDelBtn(false);
9663 $hide(this._btnSpoil);
9664 if(this._spoilEl) {
9665 this._spoilEl.checked = this._btnSpoil.checked = false;
9666 }
9667 $hide(this._btnRar);
9668 $hide(this._txtAddBtn);
9669 $del(this._rarMsg);
9670 if(FileInput._isThumbMode) {
9671 $hide(this._txtWrap);
9672 }
9673 this._txtInput.value = '';
9674 this._txtInput.classList.add('de-file-txt-noedit');
9675 this._txtInput.placeholder = Lng.dropFileHere[lang];
9676 }
9677 this.extraFile = this.imgFile = null;
9678 this._isTxtEditable = this._isTxtEditName = false;
9679 this._changeFilesCount(-1);
9680 this._removeFile();
9681 }
9682 handleEvent(e) {
9683 const el = e.target;
9684 const thumb = this._thumb;
9685 const isThumb = el === thumb || el.className === 'de-file-img';
9686 switch(e.type) {
9687 case 'change': {
9688 const inpArray = this._parent._inputs;
9689 const curInpIdx = inpArray.indexOf(this);
9690 const filesLen = el.files.length;
9691 if(filesLen > 1) {
9692 const allowedLen = Math.min(filesLen, inpArray.length - curInpIdx);
9693 let j = allowedLen;
9694 for(let i = 0; i < allowedLen; ++i) {
9695 FileInput._readDroppedFile(inpArray[curInpIdx + i], el.files[i]).then(() => {
9696 if(!--j) { // Clear original file input after all allowed files will be read.
9697 this._removeFileHelper();
9698 }
9699 });
9700 this._parent._files[curInpIdx + i] = el.files[i];
9701 }
9702 } else {
9703 if(filesLen > 0) {
9704 setTimeout(() => this._onFileChange(false), 20);
9705 this._parent._files[curInpIdx] = el.files[0];
9706 } else {
9707 this.clearInp();
9708 delete this._parent._files[curInpIdx];
9709 }
9710 }
9711 DollchanAPI.notify('filechange', this._parent._files);
9712 return;
9713 }
9714 case 'click': {
9715 const parent = el.parentNode;
9716 if(isThumb) {
9717 this._input.click();
9718 } else if(parent === this._btnDel) {
9719 this.clearInp();
9720 this._parent.hideEmpty();
9721 delete this._parent._files[this._parent._inputs.indexOf(this)];
9722 DollchanAPI.notify('filechange', this._parent._files);
9723 } else if(parent === this._btnRar) {
9724 this._addRarJpeg();
9725 } else if(parent === this._btnRen) {
9726 const isShow = this._isTxtEditName = !this._isTxtEditName;
9727 this._isTxtEditable = !this._isTxtEditable;
9728 if(FileInput._isThumbMode) {
9729 $toggle(this._txtWrap, isShow);
9730 }
9731 $toggle(this._txtAddBtn, isShow);
9732 this._txtInput.classList.toggle('de-file-txt-noedit', !isShow);
9733 if(isShow) {
9734 this._txtInput.focus();
9735 }
9736 } else if(parent === this._btnTxt) {
9737 this._toggleDelBtn(this._isTxtEditable = true);
9738 $show(this._txtAddBtn);
9739 if(FileInput._isThumbMode) {
9740 $toggle(this._txtWrap);
9741 }
9742 this._txtInput.classList.remove('de-file-txt-noedit');
9743 this._txtInput.placeholder = Lng.enterTheLink[lang];
9744 this._txtInput.focus();
9745 } else if(el === this._btnSpoil) {
9746 this._spoilEl.checked = this._btnSpoil.checked;
9747 return;
9748 } else if(el === this._txtAddBtn) {
9749 if(this._isTxtEditName) {
9750 if(FileInput._isThumbMode) {
9751 $hide(this._txtWrap);
9752 }
9753 $hide(this._txtAddBtn);
9754 this._txtInput.classList.add('de-file-txt-noedit');
9755 this._isTxtEditable = this._isTxtEditName = false;
9756 const newName = this._txtInput.value;
9757 if(!newName) {
9758 this._txtInput.value = this.imgFile ? this.imgFile.name : this._input.files[0].name;
9759 return;
9760 }
9761 if(this.imgFile) {
9762 this.imgFile.isConstName = true;
9763 this.imgFile.name = newName;
9764 if(FileInput._isThumbMode) {
9765 this._addThumbTitle(newName, this.imgFile.data.byteLength);
9766 }
9767 return;
9768 }
9769 const file = this._input.files[0];
9770 readFile(file).then(({ data }) => {
9771 this.imgFile = { data, name: newName, type: file.type, isConstName: true };
9772 this._removeFileHelper(); // Clear the original file
9773 if(FileInput._isThumbMode) {
9774 this._addThumbTitle(newName, data.byteLength);
9775 }
9776 });
9777 return;
9778 } else {
9779 this._addUrlFile(this._txtInput.value);
9780 }
9781 } else if(el === this._txtInput && !this._isTxtEditable) {
9782 this._input.click();
9783 this._txtInput.blur();
9784 }
9785 $pd(e);
9786 e.stopPropagation();
9787 return;
9788 }
9789 case 'dragenter':
9790 if(isThumb) {
9791 thumb.classList.add('de-file-drag');
9792 }
9793 return;
9794 case 'dragleave':
9795 if(isThumb && el.classList.contains('de-file-img')) {
9796 thumb.classList.remove('de-file-drag');
9797 }
9798 return;
9799 case 'drop': {
9800 const dt = e.dataTransfer;
9801 if(!isThumb && el !== this._txtInput) {
9802 return;
9803 }
9804 const filesLen = dt.files.length;
9805 if(filesLen) {
9806 const inpArray = this._parent._inputs;
9807 const inpLen = inpArray.length;
9808 for(let i = inpArray.indexOf(this), j = 0; i < inpLen && j < filesLen; ++i, ++j) {
9809 FileInput._readDroppedFile(inpArray[i], dt.files[j]);
9810 this._parent._files[i] = dt.files[j];
9811 }
9812 DollchanAPI.notify('filechange', this._parent._files);
9813 } else {
9814 this._addUrlFile(dt.getData('text/plain'));
9815 }
9816 if(FileInput._isThumbMode) {
9817 setTimeout(() => thumb.classList.remove('de-file-drag'), 10);
9818 }
9819 $pd(e);
9820 e.stopPropagation();
9821 }
9822 }
9823 }
9824 hideInp() {
9825 if(FileInput._isThumbMode) {
9826 this._toggleDelBtn(false);
9827 $hide(this._thumb);
9828 $hide(this._txtWrap);
9829 }
9830 $hide(this._wrap);
9831 }
9832 showInp() {
9833 if(FileInput._isThumbMode) {
9834 $show(this._thumb);
9835 }
9836 $show(this._wrap);
9837 }
9838
9839 static get _isThumbMode() {
9840 return Cfg.fileInputs === 2;
9841 }
9842 static _readDroppedFile(inputObj, file) {
9843 return readFile(file).then(({ data }) => {
9844 inputObj.imgFile = { data, name: file.name, type: file.type };
9845 inputObj.showInp();
9846 inputObj._onFileChange(true);
9847 });
9848 }
9849 get _wrap() {
9850 return aib.multiFile ? this._input.parentNode : this._input;
9851 }
9852 _addNewThumb(fileData, fileName, fileType, fileSize) {
9853 let el = this._thumb;
9854 el.classList.remove('de-file-off');
9855 el = el.firstChild.firstChild;
9856 el.title = `${ fileName }, ${ (fileSize / 1024).toFixed(2) }KB`;
9857 this._mediaEl = el = $aBegin(el, fileType.startsWith('video/') ?
9858 '<video class="de-file-img" loop autoplay muted src=""></video>' :
9859 '<img class="de-file-img" src="">');
9860 el.src = deWindow.URL.createObjectURL(new Blob([fileData]));
9861 if((el = el.nextSibling)) {
9862 deWindow.URL.revokeObjectURL(el.src);
9863 el.remove();
9864 }
9865 }
9866 _addRarJpeg() {
9867 const el = this._parent.rarInput;
9868 el.onchange = e => {
9869 $hide(this._btnRar);
9870 const myBtn = this._rarMsg = $aBegin(this._utils,
9871 '<span><svg class="de-wait"><use xlink:href="#de-symbol-wait"/></svg></span>');
9872 const file = e.target.files[0];
9873 readFile(file).then(({ data }) => {
9874 if(this._rarMsg === myBtn) {
9875 myBtn.className = 'de-file-rarmsg';
9876 const origFileName = this.imgFile ? this.imgFile.name : this._input.files[0].name;
9877 myBtn.title = origFileName + ' + ' + file.name;
9878 myBtn.textContent = origFileName.split('.').pop() + ' + ' + file.name.split('.').pop();
9879 this.extraFile = data;
9880 }
9881 });
9882 };
9883 el.click();
9884 }
9885 _addThumbTitle(name, size) {
9886 this._thumb.firstChild.firstChild.title = `${ name }, ${ (size / 1024).toFixed(2) }KB`;
9887 }
9888 _addUrlFile(url, file = null) {
9889 if(!url) {
9890 return Promise.reject(new Error('URL is null'));
9891 }
9892 $popup('file-loading', Lng.loading[lang], true);
9893 return ContentLoader.loadImgData(url, false).then(data => {
9894 if(file) {
9895 deWindow.URL.revokeObjectURL(url);
9896 }
9897 if(!data) {
9898 $popup('file-loading', Lng.cantLoad[lang] + ' URL: ' + url);
9899 return;
9900 }
9901 closePopup('file-loading');
9902 this._isTxtEditable = this._isTxtEditName = false;
9903 let name = file ? file.name : url.split('/').pop();
9904 const type = file && file.type || getFileType(name);
9905 if(!type || name.includes('?')) {
9906 let ext;
9907 switch((data[0] << 8) | data[1]) {
9908 case 0xFFD8: ext = 'jpg'; break;
9909 case 0x8950: ext = 'png'; break;
9910 case 0x4749: ext = 'gif'; break;
9911 case 0x1A45: ext = 'webm'; break;
9912 default: ext = '';
9913 }
9914 if(ext) {
9915 name = name.split('?').shift() + '.' + ext;
9916 }
9917 }
9918 this.imgFile = { data: data.buffer, name, type: type || getFileType(name) };
9919 if(!file) {
9920 file = new Blob([data], { type: this.imgFile.type });
9921 file.name = name;
9922 }
9923 this._parent._files[this._parent._inputs.indexOf(this)] = file;
9924 DollchanAPI.notify('filechange', this._parent._files);
9925 if(FileInput._isThumbMode) {
9926 $hide(this._txtWrap);
9927 }
9928 this._onFileChange(true);
9929 });
9930 }
9931 _changeFilesCount(val) {
9932 this._parent.filesCount = Math.max(this._parent.filesCount + val, 0);
9933 if(aib.dobrochan) {
9934 $id('post_files_count').value = this._parent.filesCount + 1;
9935 }
9936 }
9937 _initThumbs() {
9938 const { fileTr } = this._parent;
9939 $hide(fileTr);
9940 $hide(this._txtWrap);
9941 const isTr = fileTr.tagName === 'TR';
9942 const txtArea = $q('.de-file-txt-area') || $bBegin(fileTr, isTr ?
9943 '<tr class="de-file-txt-area"><td class="postblock"></td><td></td></tr>' :
9944 '<div class="de-file-txt-area"></div>');
9945 (isTr ? txtArea.lastChild : txtArea).appendChild(this._txtWrap);
9946 this._thumb = $bEnd(this._parent.thumbsEl,
9947 `<div class="de-file de-file-off"><div class="de-file-img"><div class="de-file-img" title="${
9948 Lng.youCanDrag[lang] }"></div></div></div>`);
9949 this._thumb.addEventListener('click', this);
9950 this._thumb.addEventListener('dragenter', this);
9951 this._thumb.appendChild(this._utils);
9952 this._toggleDragEvents(this._thumb, true);
9953 if(this.hasFile) {
9954 this._showFileThumb();
9955 }
9956 }
9957 _onFileChange(hasImgFile) {
9958 this._txtInput.value = hasImgFile ? this.imgFile.name : this._input.files[0].name;
9959 if(!hasImgFile) {
9960 this.imgFile = null;
9961 }
9962 if(this._parent.onchange) {
9963 this._parent.onchange();
9964 }
9965 if(FileInput._isThumbMode) {
9966 this._showFileThumb();
9967 }
9968 if(this.hasFile) {
9969 this.extraFile = null;
9970 } else {
9971 this.hasFile = true;
9972 this._changeFilesCount(+1);
9973 this._toggleDelBtn(true);
9974 $hide(this._txtAddBtn);
9975 if(FileInput._isThumbMode) {
9976 $hide(this._txtWrap);
9977 }
9978 if(this._spoilEl) {
9979 this._btnSpoil.checked = this._spoilEl.checked;
9980 $show(this._btnSpoil);
9981 }
9982 this._txtInput.classList.add('de-file-txt-noedit');
9983 this._txtInput.placeholder = Lng.dropFileHere[lang];
9984 }
9985 this._parent.hideEmpty();
9986 if(!nav.isPresto && !aib._4chan &&
9987 /^image\/(?:png|jpeg)$/.test(hasImgFile ? this.imgFile.type : this._input.files[0].type)
9988 ) {
9989 $del(this._rarMsg);
9990 $show(this._btnRar);
9991 }
9992 }
9993 _removeFile() {
9994 this._removeFileHelper();
9995 this.hasFile = false;
9996 if(this._parent._files) {
9997 delete this._parent._files[this._parent._inputs.indexOf(this)];
9998 }
9999 }
10000 _removeFileHelper() {
10001 const oldEl = this._input;
10002 const newEl = $aEnd(oldEl, oldEl.outerHTML);
10003 oldEl.removeEventListener('change', this);
10004 newEl.addEventListener('change', this);
10005 newEl.obj = this;
10006 this._input = newEl;
10007 oldEl.remove();
10008 }
10009 _showFileThumb() {
10010 const { imgFile } = this;
10011 if(imgFile) {
10012 this._addNewThumb(imgFile.data, imgFile.name, imgFile.type, imgFile.data.byteLength);
10013 return;
10014 }
10015 const file = this._input.files[0];
10016 if(file) {
10017 readFile(file).then(({ data }) => {
10018 if(this._input.files[0] === file) {
10019 this._addNewThumb(data, file.name, file.type, file.size);
10020 }
10021 });
10022 }
10023 }
10024 _toggleDelBtn(isShow) {
10025 $toggle(this._btnDel, isShow);
10026 $toggle(this._btnRen, Cfg.fileInputs && isShow && this.hasFile);
10027 $toggle(this._btnTxt, !isShow);
10028 }
10029 _toggleDragEvents(el, isAdd) {
10030 const name = isAdd ? 'addEventListener' : 'removeEventListener';
10031 el[name]('dragover', $pd);
10032 el[name]('dragenter', this);
10033 el[name]('dragleave', this);
10034 el[name]('drop', this);
10035 }
10036}
10037
10038/* ==[ FormCaptcha.js ]=======================================================================================
10039 CAPTCHA
10040=========================================================================================================== */
10041
10042class Captcha {
10043 constructor(el, initNum) {
10044 this.hasCaptcha = true;
10045 this.textEl = null;
10046 this.tNum = initNum;
10047 this.parentEl = nav.matchesSelector(el, aib.qFormTr) ? el : aib.getCapParent(el);
10048 this.isAdded = false;
10049 this.isSubmitWait = false;
10050 this._isRecap = !aib._02ch && !!$q('[id*="recaptcha"], [class*="recaptcha"]', this.parentEl);
10051 this._lastUpdate = null;
10052 this.originHTML = this.parentEl.innerHTML;
10053 $hide(this.parentEl);
10054 if(!this._isRecap) {
10055 this.parentEl.innerHTML = '';
10056 }
10057 }
10058 addCaptcha() {
10059 if(this.isAdded) { // Run this function only once
10060 return;
10061 }
10062 this.isAdded = true;
10063 if(!this._isRecap) {
10064 this.parentEl.innerHTML = this.originHTML;
10065 this.textEl = $q('input[type="text"][name*="aptcha"]', this.parentEl);
10066 } else {
10067 const el = $q('#g-recaptcha, .g-recaptcha');
10068 $replace(el, `<div id="g-recaptcha" class="g-recaptcha" data-sitekey="${
10069 el.getAttribute('data-sitekey') }"></div>`);
10070 }
10071 this.initCapPromise();
10072 }
10073 handleEvent(e) {
10074 switch(e.type) {
10075 case 'keypress': {
10076 if(!Cfg.captchaLang || e.which === 0) {
10077 return;
10078 }
10079 const ruUa = 'йцукенгшщзхъїфыівапролджэєячсмитьбюёґ';
10080 const en = "qwertyuiop[]]assdfghjkl;''zxcvbnm,.`\\";
10081 const code = e.charCode || e.keyCode;
10082 let i, chr = String.fromCharCode(code).toLowerCase();
10083 if(Cfg.captchaLang === 1) {
10084 if(code < 0x0410 || code > 0x04FF || (i = ruUa.indexOf(chr)) === -1) {
10085 return;
10086 }
10087 chr = en[i];
10088 } else {
10089 if(code < 0x0021 || code > 0x007A || (i = en.indexOf(chr)) === -1) {
10090 return;
10091 }
10092 chr = ruUa[i];
10093 }
10094 insertText(e.target, chr);
10095 break;
10096 }
10097 case 'focus': this.updateOutdated();
10098 }
10099 $pd(e);
10100 e.stopPropagation();
10101 }
10102 initCapPromise() {
10103 const initPromise = aib.initCaptcha ? aib.initCaptcha(this) : null;
10104 if(initPromise) {
10105 initPromise.then(() => this.showCaptcha(), err => {
10106 if(err instanceof AjaxError) {
10107 this._setUpdateError(err);
10108 } else {
10109 this.hasCaptcha = false;
10110 }
10111 });
10112 } else if(this.hasCaptcha) {
10113 this.showCaptcha(true);
10114 }
10115 }
10116 initImage(img) {
10117 img.title = Lng.refresh[lang];
10118 img.alt = Lng.loading[lang];
10119 img.style.cssText = 'vertical-align: text-bottom; border: none; cursor: pointer;';
10120 img.onclick = () => this.refreshCaptcha(true);
10121 }
10122 initTextEl() {
10123 this.textEl.autocomplete = 'off';
10124 if(!aib.kusaba && (aib.multiFile || Cfg.fileInputs !== 2)) {
10125 this.textEl.placeholder = Lng.cap[lang];
10126 }
10127 this.textEl.addEventListener('keypress', this);
10128 this.textEl.onkeypress = null;
10129 this.textEl.addEventListener('focus', this);
10130 this.textEl.onfocus = null;
10131 }
10132 showCaptcha(isUpdateImage = false) {
10133 if(!this.textEl) {
10134 $show(this.parentEl);
10135 if(aib.updateCaptcha) {
10136 aib.updateCaptcha(this, false);
10137 } else if(this._isRecap) {
10138 this._updateRecap();
10139 }
10140 return;
10141 }
10142 this.initTextEl();
10143 let img;
10144 if(this._isRecap || !(img = $q('img', this.parentEl))) {
10145 $show(this.parentEl);
10146 return;
10147 }
10148 this.initImage(img);
10149 const a = img.parentNode;
10150 if(a.tagName === 'A') {
10151 $replace(a, img);
10152 }
10153 if(isUpdateImage) {
10154 this.refreshCaptcha(false);
10155 } else {
10156 this._lastUpdate = Date.now();
10157 }
10158 $show(this.parentEl);
10159 }
10160 refreshCaptcha(isFocus, isErr = false, tNum = this.tNum) {
10161 if(!this.isAdded || tNum !== this.tNum) {
10162 this.tNum = tNum;
10163 this.isAdded = false;
10164 this.hasCaptcha = true;
10165 this.textEl = null;
10166 $hide(this.parentEl);
10167 this.addCaptcha();
10168 return;
10169 } else if(!this.hasCaptcha && !isErr) {
10170 return;
10171 }
10172 this._lastUpdate = Date.now();
10173 if(aib.updateCaptcha) {
10174 const updatePromise = aib.updateCaptcha(this, isErr);
10175 if(updatePromise) {
10176 updatePromise.then(() => this._updateTextEl(isFocus), err => this._setUpdateError(err));
10177 }
10178 } else if(this._isRecap) {
10179 this._updateRecap();
10180 } else if(this.textEl) {
10181 this._updateTextEl(isFocus);
10182 const img = $q('img', this.parentEl);
10183 if(!img) {
10184 return;
10185 }
10186 if(aib.getCaptchaSrc) {
10187 const src = img.getAttribute('src');
10188 if(src) {
10189 img.src = '';
10190 img.src = aib.getCaptchaSrc(src, tNum);
10191 }
10192 } else {
10193 img.click();
10194 }
10195 }
10196 }
10197 updateHelper(url, fn) {
10198 if(aib._capUpdPromise) {
10199 aib._capUpdPromise.cancelPromise();
10200 }
10201 return (aib._capUpdPromise = $ajax(url).then(xhr => {
10202 aib._capUpdPromise = null;
10203 fn(xhr);
10204 }, err => {
10205 if(!(err instanceof CancelError)) {
10206 aib._capUpdPromise = null;
10207 return CancelablePromise.reject(err);
10208 }
10209 }));
10210 }
10211 updateOutdated() {
10212 if(this._lastUpdate && (Date.now() - this._lastUpdate > Cfg.capUpdTime * 1e3)) {
10213 this.refreshCaptcha(false);
10214 }
10215 }
10216
10217 _setUpdateError(e) {
10218 if(e) {
10219 this.parentEl = e.toString();
10220 this.isAdded = false;
10221 this.parentEl.onclick = () => {
10222 this.parentEl.onclick = null;
10223 this.addCaptcha();
10224 };
10225 $show(this.parentEl);
10226 }
10227 }
10228 _updateRecap() {
10229 // EXCLUDED FROM FIREFOX EXTENSION - START
10230 const script = doc.createElement('script');
10231 script.type = 'text/javascript';
10232 script.src = aib.prot + '//www.google.com/recaptcha/api.js';
10233 doc.head.appendChild(script);
10234 setTimeout(() => script.remove(), 1e5);
10235 // EXCLUDED FROM FIREFOX EXTENSION - END
10236 }
10237 _updateTextEl(isFocus) {
10238 if(this.textEl) {
10239 this.textEl.value = '';
10240 if(isFocus) {
10241 this.textEl.focus();
10242 }
10243 }
10244 }
10245}
10246
10247/* ==[ Posts.js ]=============================================================================================
10248 POSTS
10249=========================================================================================================== */
10250
10251class AbstractPost {
10252 constructor(thr, num, isOp) {
10253 this.isOp = isOp;
10254 this.kid = null;
10255 this.num = num;
10256 this.ref = new RefMap(this);
10257 this.thr = thr;
10258 this._hasEvents = false;
10259 this._linkDelay = 0;
10260 this._menu = null;
10261 this._menuDelay = 0;
10262 }
10263 get btnFav() {
10264 const value = $q('.de-btn-fav, .de-btn-fav-sel', this.btns);
10265 Object.defineProperty(this, 'btnFav', { value });
10266 return value;
10267 }
10268 get btnHide() {
10269 const value = this.btns.firstChild;
10270 Object.defineProperty(this, 'btnHide', { value });
10271 return value;
10272 }
10273 get images() {
10274 const value = new PostImages(this);
10275 Object.defineProperty(this, 'images', { value });
10276 return value;
10277 }
10278 get mp3Obj() {
10279 const value = $bBegin(this.msg, '<div class="de-mp3"></div>');
10280 Object.defineProperty(this, 'mp3Obj', { value });
10281 return value;
10282 }
10283 * refLinks() {
10284 const links = $Q('a', this.msg);
10285 for(let lNum, i = 0, len = links.length; i < len; ++i) {
10286 const link = links[i];
10287 const tc = link.textContent;
10288 if(tc[0] !== '>' || tc[1] !== '>' || !(lNum = parseInt(tc.substr(2), 10))) {
10289 continue;
10290 }
10291 yield [link, lNum];
10292 }
10293 }
10294 get msg() {
10295 const value = $q(aib.qPostMsg, this.el);
10296 Object.defineProperty(this, 'msg', { value, configurable: true });
10297 return value;
10298 }
10299 get trunc() {
10300 let value = null;
10301 const el = aib.qTrunc && $q(aib.qTrunc, this.el);
10302 if(el && /long|full comment|gekürzt|слишком|длинн|мног|полн/i.test(el.textContent)) {
10303 value = el;
10304 }
10305 Object.defineProperty(this, 'trunc', { value, configurable: true });
10306 return value;
10307 }
10308 get videos() {
10309 const value = Cfg.embedYTube ? new Videos(this) : null;
10310 Object.defineProperty(this, 'videos', { value });
10311 return value;
10312 }
10313 addFuncs() {
10314 RefMap.updateRefMap(this, true);
10315 embedAudioLinks(this);
10316 }
10317 handleEvent(e) {
10318 let temp, el = fixEventEl(e.target);
10319 const { type } = e;
10320 const isOutEvent = type === 'mouseout';
10321 const isPview = this instanceof Pview;
10322 if(type === 'click') {
10323 switch(e.button) {
10324 case 0: break;
10325 case 1: e.stopPropagation(); // Skip the click on wheel button
10326 /* falls through */
10327 default: return;
10328 }
10329 if(this._menu) { // Hide the dropdown menu after the click on its option
10330 this._menu.removeMenu();
10331 this._menu = null;
10332 }
10333 switch(el.tagName) {
10334 case 'A':
10335 // Click on YouTube link - show/hide player or thumbnail
10336 if(el.classList.contains('de-video-link')) {
10337 this.videos.clickLink(el, Cfg.embedYTube);
10338 $pd(e);
10339 return;
10340 }
10341 // Check if the link is not an image container
10342 if(!(temp = el.firstElementChild) || temp.tagName !== 'IMG') {
10343 temp = el.parentNode;
10344 if(temp === this.trunc) { // Click on "truncated message" link
10345 this._getFullMsg(temp, false);
10346 $pd(e);
10347 e.stopPropagation();
10348 } else if(Cfg.insertNum && pr.form && (this._pref === temp || this._pref === el) &&
10349 !/Reply|Ответ/.test(el.textContent)
10350 ) { // Click on post number link - show quick reply or redirect with an #anchor
10351 $pd(e);
10352 e.stopPropagation();
10353 if(!Cfg.showRepBtn) {
10354 quotetxt = deWindow.getSelection().toString();
10355 pr.showQuickReply(isPview ? Pview.topParent : this, this.num, !isPview, false);
10356 quotetxt = '';
10357 } else if(pr.isQuick || (aib.t && pr.isHidden)) {
10358 pr.showQuickReply(isPview ? Pview.topParent : this, this.num, false, true);
10359 } else if(aib.t) {
10360 const formText = pr.txta.value;
10361 const isOnNewLine = formText === '' || formText.slice(-1) === '\n';
10362 insertText(pr.txta, `>>${ this.num }${ isOnNewLine ? '\n' : '' }`);
10363 } else {
10364 deWindow.location.assign(el.href.replace(/#i/, '#'));
10365 }
10366 } else if((temp = el.textContent)[0] === '>' &&
10367 temp[1] === '>' && !temp[2].includes('/')
10368 ) { // Click on >>link - scroll to the referenced post
10369 const post = pByNum.get(+temp.match(/\d+/));
10370 if(post) {
10371 post.selectAndScrollTo();
10372 }
10373 }
10374 return;
10375 }
10376 el = temp; // The link is an image container
10377 /* falls through */
10378 case 'IMG': // Click on attached image - expand/collapse
10379 if(el.classList.contains('de-video-thumb')) {
10380 if(Cfg.embedYTube === 1) {
10381 const { videos } = this;
10382 videos.currentLink.classList.add('de-current');
10383 videos.setPlayer(videos.playerInfo, el.classList.contains('de-ytube'));
10384 $pd(e);
10385 }
10386 } else if(Cfg.expandImgs !== 0) {
10387 this._clickImage(el, e);
10388 }
10389 return;
10390 case 'OBJECT':
10391 case 'VIDEO': // Click on attached video - expand/collapse
10392 if(Cfg.expandImgs !== 0 && !ExpandableImage.isControlClick(e)) {
10393 this._clickImage(el, e);
10394 }
10395 return;
10396 }
10397 if(aib.makaba) {
10398 // Makaba: Click on like/dislike elements
10399 let c = el.classList;
10400 if(c.contains('post__rate') || c[0] === 'like-div' || c[0] === 'dislike-div' ||
10401 (temp = el.parentNode) && (
10402 (c = temp.classList).contains('post__rate') ||
10403 c[0] === 'like-div' ||
10404 c[0] === 'dislike-div') ||
10405 (temp = temp.parentNode) && (
10406 (c = temp.className) === 'like-div' ||
10407 c === 'dislike-div')
10408 ) {
10409 const task = temp.id.split('-')[0];
10410 const num = +temp.id.match(/\d+/);
10411 $ajax(`/makaba/likes.fcgi?task=${ task }&board=${ aib.b }&num=${ num }`).then(xhr => {
10412 const data = JSON.parse(xhr.responseText);
10413 if(data.Status !== 'OK') {
10414 $popup('err-2chlike', data.Reason);
10415 return;
10416 }
10417 temp.classList.add(`${ task }-div-checked`, `post__rate_${ task }d`);
10418 const countEl = $q(`.${ task }-count, #${ task }-count${ num }`, temp);
10419 countEl.textContent = +countEl.textContent + 1;
10420 }, () => $popup('err-2chlike', Lng.noConnect[lang]));
10421 }
10422 // Makaba: Click on "truncated message" link
10423 if(el.classList.contains('expand-large-comment')) {
10424 this._getFullMsg(el, false);
10425 $pd(e);
10426 e.stopPropagation();
10427 }
10428 }
10429 // Click on post buttons
10430 switch(el.classList[0]) {
10431 case 'de-btn-expthr': this.thr.loadPosts('all'); return;
10432 case 'de-btn-fav': this.thr.toggleFavState(true, isPview ? this : null); return;
10433 case 'de-btn-fav-sel': this.thr.toggleFavState(false, isPview ? this : null); return;
10434 case 'de-btn-hide':
10435 case 'de-btn-hide-user':
10436 case 'de-btn-unhide':
10437 case 'de-btn-unhide-user': this.setUserVisib(!this.isHidden); return;
10438 case 'de-btn-reply':
10439 pr.showQuickReply(isPview ? Pview.topParent : this, this.num, !isPview, false);
10440 quotetxt = '';
10441 return;
10442 case 'de-btn-sage': Spells.addSpell(9, '', false); return;
10443 case 'de-btn-stick': this.toggleSticky(true); return;
10444 case 'de-btn-stick-on': this.toggleSticky(false); return;
10445 }
10446 return;
10447 }
10448 if(!this._hasEvents) {
10449 this._hasEvents = true;
10450 this.el.addEventListener('click', this, true);
10451 this.el.addEventListener('mouseout', this, true);
10452 }
10453 // Mouseover/mouseout on YouTube links
10454 if(el.classList.contains('de-video-link')) {
10455 if(aib.makaba && !el.videoInfo) {
10456 const origMsg = this.msg.firstChild;
10457 this.videos.updatePost($Q('.de-video-link', origMsg),
10458 $Q('.de-video-link', origMsg.nextSibling), true);
10459 }
10460 if(Cfg.embedYTube === 2) {
10461 this.videos.toggleFloatedThumb(el, isOutEvent);
10462 }
10463 }
10464 // Mouseover/mouseout on attached images/videos - update title
10465 if(!isOutEvent && Cfg.expandImgs && el.tagName === 'IMG' && !el.classList.contains('de-fullimg') &&
10466 (temp = this.images.getImageByEl(el)) && (temp.isImage || temp.isVideo)
10467 ) {
10468 el.title = Cfg.expandImgs === 1 ? Lng.expImgInline[lang] : Lng.expImgFull[lang];
10469 }
10470 // Mouseover/mouseout on post buttons - update title, add/delete dropdown menu
10471 switch(el.classList[0]) {
10472 case 'de-post-btns': el.removeAttribute('title'); return;
10473 case 'de-btn-reply': {
10474 const title = this.btns.title = this.isOp ? Lng.replyToThr[lang] : Lng.replyToPost[lang];
10475 if(Cfg.showRepBtn === 1) {
10476 if(!isOutEvent) {
10477 quotetxt = deWindow.getSelection().toString();
10478 }
10479 this._addMenu(el, isOutEvent,
10480 `<span class="de-menu-item" info="post-reply">${ title }</span>` +
10481 (aib.reportForm ? `<span class="de-menu-item" info="post-report">${
10482 this.num === this.thr.num ? Lng.reportThr[lang] : Lng.reportPost[lang] }</span>` : ''
10483 ) +
10484 (Cfg.markMyPosts || Cfg.markMyLinks ? `<span class="de-menu-item" info="post-markmy">${
10485 MyPosts.has(this.num) ? Lng.deleteMyPost[lang] : Lng.markMyPost[lang] }</span>` : ''
10486 ));
10487 }
10488 return;
10489 }
10490 case 'de-btn-hide':
10491 case 'de-btn-hide-user':
10492 case 'de-btn-unhide':
10493 case 'de-btn-unhide-user':
10494 this.btns.title = this.isOp ? Lng.toggleThr[lang] : Lng.togglePost[lang];
10495 if(Cfg.showHideBtn === 1) {
10496 this._addMenu(el, isOutEvent,
10497 (this instanceof Pview ? pByNum.get(this.num) : this)._getMenuHide());
10498 }
10499 return;
10500 case 'de-btn-expthr':
10501 this.btns.title = Lng.expandThr[lang];
10502 this._addMenu(el, isOutEvent, arrTags(Lng.selExpandThr[lang],
10503 '<span class="de-menu-item" info="thr-exp">', '</span>'));
10504 return;
10505 case 'de-btn-fav': this.btns.title = Lng.addFav[lang]; return;
10506 case 'de-btn-fav-sel': this.btns.title = Lng.delFav[lang]; return;
10507 case 'de-btn-sage': this.btns.title = 'SAGE'; return;
10508 case 'de-btn-stick': this.btns.title = Lng.attachPview[lang]; return;
10509 case 'de-btn-src':
10510 if(el.parentNode.className !== 'de-fullimg-info') {
10511 this._addMenu(el, isOutEvent, Menu.getMenuImgSrc(el));
10512 }
10513 return;
10514 // Mouseover/mouseout on >>links - show/delete post previews
10515 default:
10516 if(!Cfg.linksNavig || el.tagName !== 'A' || el.isNotRefLink) {
10517 return;
10518 }
10519 if(!el.textContent.startsWith('>>')) {
10520 el.isNotRefLink = true;
10521 return;
10522 }
10523 // Don't use classList here, 'de-link-postref ' should be first
10524 el.className = 'de-link-postref ' + el.className;
10525 /* falls through */
10526 case 'de-link-backref':
10527 case 'de-link-postref':
10528 if(!Cfg.linksNavig) {
10529 return;
10530 }
10531 if(isOutEvent) { // Mouseout - We need to delete previews
10532 clearTimeout(this._linkDelay);
10533 if(!(aib.getPostOfEl(fixEventEl(e.relatedTarget)) instanceof Pview) && Pview.top) {
10534 Pview.top.markToDel(); // If cursor is not over one of previews - delete all previews
10535 } else if(this.kid) {
10536 this.kid.markToDel(); // If cursor is over any preview - delete its kids
10537 }
10538 } else { // Mouseover - we need to show a preview for this link
10539 this._linkDelay = setTimeout(() => (this.kid = Pview.showPview(this, el)), Cfg.linksOver);
10540 }
10541 $pd(e);
10542 e.stopPropagation();
10543 }
10544 }
10545 toggleFavBtn(isEnable) {
10546 const elClass = isEnable ? 'de-btn-fav-sel' : 'de-btn-fav';
10547 if(this.btnFav) {
10548 this.btnFav.setAttribute('class', elClass);
10549 }
10550 if(this.thr.btnFav) {
10551 this.thr.btnFav.setAttribute('class', elClass);
10552 }
10553 }
10554 updateMsg(newMsg, sRunner) {
10555 let videoExt, videoLinks;
10556 const origMsg = aib.dobrochan ? this.msg.firstElementChild : this.msg;
10557 if(Cfg.embedYTube) {
10558 videoExt = $q('.de-video-ext', origMsg);
10559 videoLinks = $Q(':not(.de-video-ext) > .de-video-link', origMsg);
10560 }
10561 $replace(origMsg, newMsg);
10562 Object.defineProperties(this, {
10563 msg : { configurable: true, value: newMsg },
10564 trunc : { configurable: true, value: null }
10565 });
10566 Post.Сontent.removeTempData(this);
10567 if(Cfg.embedYTube) {
10568 this.videos.updatePost(videoLinks, $Q('a[href*="youtu"], a[href*="vimeo.com"]', newMsg), false);
10569 if(videoExt) {
10570 newMsg.appendChild(videoExt);
10571 }
10572 }
10573 this.addFuncs();
10574 sRunner.runSpells(this);
10575 embedPostMsgImages(this.el);
10576 if(this.isHidden) {
10577 this.hideContent(this.isHidden);
10578 }
10579 closePopup('load-fullmsg');
10580 }
10581
10582 _addMenu(el, isOutEvent, html) {
10583 if(!this.menu || this.menu.parentEl !== el) {
10584 if(isOutEvent) {
10585 clearTimeout(this._menuDelay);
10586 } else {
10587 this._menuDelay = setTimeout(() => this._showMenu(el, html), Cfg.linksOver);
10588 }
10589 }
10590 }
10591 _clickImage(el, e) {
10592 const image = this.images.getImageByEl(el);
10593 if(!image || (!image.isImage && !image.isVideo)) {
10594 return;
10595 }
10596 image.expandImg((Cfg.expandImgs === 1) ^ e.ctrlKey, e);
10597 $pd(e);
10598 e.stopPropagation();
10599 }
10600 _getFullMsg(truncEl, isInit) {
10601 if(aib.deleteTruncMsg) {
10602 aib.deleteTruncMsg(this, truncEl, isInit);
10603 return;
10604 }
10605 if(!isInit) {
10606 $popup('load-fullmsg', Lng.loading[lang], true);
10607 }
10608 ajaxLoad(aib.getThrUrl(aib.b, this.tNum)).then(form => {
10609 let sourceEl;
10610 const maybeSpells = new Maybe(SpellsRunner);
10611 if(this.isOp) {
10612 sourceEl = form;
10613 } else {
10614 const posts = $Q(aib.qRPost, form);
10615 for(let i = 0, len = posts.length; i < len; ++i) {
10616 const post = posts[i];
10617 if(this.num === aib.getPNum(post)) {
10618 sourceEl = post;
10619 break;
10620 }
10621 }
10622 }
10623 if(sourceEl) {
10624 this.updateMsg(aib.fixHTML(doc.adoptNode($q(aib.qPostMsg, sourceEl))), maybeSpells.value);
10625 truncEl.remove();
10626 }
10627 if(maybeSpells.hasValue) {
10628 maybeSpells.value.endSpells();
10629 }
10630 }, emptyFn);
10631 }
10632 _showMenu(el, html) {
10633 if(this._menu) {
10634 this._menu.removeMenu();
10635 }
10636 this._menu = new Menu(el, html,
10637 el => (this instanceof Pview ? pByNum.get(this.num) : this)._clickMenu(el), false);
10638 this._menu.onremove = () => (this._menu = null);
10639 }
10640}
10641
10642class Post extends AbstractPost {
10643 constructor(el, thr, num, count, isOp, prev) {
10644 super(thr, num, isOp);
10645 this.count = count;
10646 this.el = el;
10647 this.isDeleted = false;
10648 this.isHidden = false;
10649 this.isOmitted = false;
10650 this.isViewed = false;
10651 this.next = null;
10652 this.prev = prev;
10653 this.spellHidden = false;
10654 this.userToggled = false;
10655 this._selRange = null;
10656 this._selText = '';
10657 if(prev) {
10658 prev.next = this;
10659 }
10660 pByEl.set(el, this);
10661 pByNum.set(num, this);
10662 let isMyPost = MyPosts.has(num);
10663 if(isMyPost) {
10664 this.el.classList.add('de-mypost');
10665 } else if(localData && this.el.classList.contains('de-mypost')) {
10666 MyPosts.set(num, thr.num);
10667 isMyPost = true;
10668 }
10669 el.classList.add(isOp ? 'de-oppost' : 'de-reply');
10670 this.sage = aib.getSage(el);
10671 this.btns = $aEnd(this._pref = $q(aib.qPostRef, el),
10672 '<span class="de-post-btns">' + Post.getPostBtns(isOp, aib.t) +
10673 (this.sage ? '<svg class="de-btn-sage"><use xlink:href="#de-symbol-post-sage"/></svg>' : '') +
10674 (isOp ? '' : `<span class="de-post-counter">${ count + 1 }</span>`) +
10675 (isMyPost ? '<span class="de-post-counter-you">(You)</span>' : '') + '</span>');
10676 this.counterEl = isOp ? null : $q('.de-post-counter', this.btns);
10677 if(Cfg.expandTrunc && this.trunc) {
10678 this._getFullMsg(this.trunc, true);
10679 }
10680 el.addEventListener('mouseover', this, true);
10681 }
10682 static addMark(postEl, forced) {
10683 if(!doc.hidden && !forced) {
10684 Post.clearMarks();
10685 } else {
10686 if(!Post.hasNew) {
10687 Post.hasNew = true;
10688 doc.addEventListener('click', Post.clearMarks, true);
10689 }
10690 postEl.classList.add('de-new-post');
10691 }
10692 }
10693 static clearMarks() {
10694 if(Post.hasNew) {
10695 Post.hasNew = false;
10696 $each($Q('.de-new-post'), el => el.classList.remove('de-new-post'));
10697 doc.removeEventListener('click', Post.clearMarks, true);
10698 }
10699 }
10700 static getPostBtns(isOp, noExpThr) {
10701 return '<svg class="de-btn-hide"><use class="de-btn-hide-use" xlink:href="#de-symbol-post-hide"/>' +
10702 '<use class="de-btn-unhide-use" xlink:href="#de-symbol-post-unhide"/></svg>' +
10703 '<svg class="de-btn-reply"><use xlink:href="#de-symbol-post-reply"/></svg>' + (isOp ?
10704 (noExpThr ? '' : '<svg class="de-btn-expthr"><use xlink:href="#de-symbol-post-expthr"/></svg>') +
10705 '<svg class="de-btn-fav"><use xlink:href="#de-symbol-post-fav"/></svg>' : '');
10706 }
10707 static findSameText(pNum, isHidden, words, curPost) {
10708 const curWords = Post.getWrds(curPost.text);
10709 const len = curWords.length;
10710 let i = words.length;
10711 const olen = i;
10712 let _olen = i;
10713 let n = 0;
10714 if(len < olen * 0.4 || len > olen * 3) {
10715 return;
10716 }
10717 while(i--) {
10718 if(olen > 6 && words[i].length < 3) {
10719 _olen--;
10720 continue;
10721 }
10722 let j = len;
10723 while(j--) {
10724 if(curWords[j] === words[i] || words[i].match(/>>\d+/) && curWords[j].match(/>>\d+/)) {
10725 n++;
10726 }
10727 }
10728 }
10729 if(n < _olen * 0.4 || len > _olen * 3) {
10730 return;
10731 }
10732 if(isHidden) {
10733 if(curPost.spellHidden) {
10734 Post.Note.reset();
10735 } else {
10736 curPost.setVisib(false);
10737 }
10738 if(curPost.userToggled) {
10739 HiddenPosts.removeStorage(curPost.num);
10740 curPost.userToggled = false;
10741 }
10742 } else {
10743 curPost.setUserVisib(true, true, 'similar to >>' + pNum);
10744 }
10745 return false;
10746 }
10747 static getWrds(text) {
10748 return text.replace(/\s+/g, ' ').replace(/[^a-zа-яё ]/ig, '').trim().substring(0, 800).split(' ');
10749 }
10750 static hideContent(headerEl, btnHide, isUser, isHide) {
10751 if(!isHide) {
10752 btnHide.setAttribute('class', isUser ? 'de-btn-hide-user' : 'de-btn-hide');
10753 $each($Q('.de-post-hiddencontent', headerEl.parentNode),
10754 el => el.classList.remove('de-post-hiddencontent'));
10755 return;
10756 }
10757 if(aib.t) {
10758 Thread.first.hidCounter++;
10759 }
10760 btnHide.setAttribute('class', isUser ? 'de-btn-unhide-user' : 'de-btn-unhide');
10761 if(headerEl) {
10762 for(let el = headerEl.nextElementSibling; el; el = el.nextElementSibling) {
10763 el.classList.add('de-post-hiddencontent');
10764 }
10765 }
10766 }
10767 get banned() {
10768 const value = aib.getBanId(this.el);
10769 Object.defineProperty(this, 'banned', { value, writable: true });
10770 return value;
10771 }
10772 get bottom() {
10773 return (this.isOp && this.isHidden ? this.thr.el.previousElementSibling : this.el)
10774 .getBoundingClientRect().bottom;
10775 }
10776 get headerEl() {
10777 return new Post.Сontent(this).headerEl;
10778 }
10779 get html() {
10780 return new Post.Сontent(this).html;
10781 }
10782 get nextInThread() {
10783 const post = this.next;
10784 return !post || post.count === 0 ? null : post;
10785 }
10786 get nextNotDeleted() {
10787 let post = this.nextInThread;
10788 while(post && post.isDeleted) {
10789 post = post.nextInThread;
10790 }
10791 return post;
10792 }
10793 get note() {
10794 const value = new Post.Note(this);
10795 Object.defineProperty(this, 'note', { value });
10796 return value;
10797 }
10798 get posterName() {
10799 return new Post.Сontent(this).posterName;
10800 }
10801 get posterTrip() {
10802 return new Post.Сontent(this).posterTrip;
10803 }
10804 get subj() {
10805 return new Post.Сontent(this).subj;
10806 }
10807 get text() {
10808 return new Post.Сontent(this).text;
10809 }
10810 get title() {
10811 return new Post.Сontent(this).title;
10812 }
10813 get tNum() {
10814 return this.thr.num;
10815 }
10816 get top() {
10817 return (this.isOp && this.isHidden ? this.thr.el.previousElementSibling : this.el)
10818 .getBoundingClientRect().top;
10819 }
10820 get wrap() {
10821 return new Post.Сontent(this).wrap;
10822 }
10823 addFuncs() {
10824 super.addFuncs();
10825 if(isExpImg) {
10826 this.toggleImages(true, false);
10827 }
10828 }
10829 deleteCounter() {
10830 this.isDeleted = true;
10831 this.counterEl.textContent = Lng.deleted[lang];
10832 this.counterEl.classList.add('de-post-counter-deleted');
10833 this.el.classList.add('de-post-removed');
10834 this.wrap.classList.add('de-wrap-removed');
10835 }
10836 deletePost(isRemovePost) {
10837 if(isRemovePost) {
10838 this.wrap.remove();
10839 pByEl.delete(this.el);
10840 pByNum.delete(this.num);
10841 if(this.isHidden) {
10842 this.ref.unhideRef();
10843 }
10844 RefMap.updateRefMap(this, false);
10845 if((this.prev.next = this.next)) {
10846 this.next.prev = this.prev;
10847 }
10848 return;
10849 }
10850 this.deleteCounter();
10851 ($q('input[type="checkbox"]', this.el) || {}).disabled = true;
10852 }
10853 getAdjacentVisPost(toUp) {
10854 let post = toUp ? this.prev : this.next;
10855 while(post) {
10856 if(post.thr.isHidden) {
10857 post = toUp ? post.thr.op.prev : post.thr.last.next;
10858 } else if(post.isHidden || post.isOmitted) {
10859 post = toUp ? post.prev : post.next;
10860 } else {
10861 return post;
10862 }
10863 }
10864 return null;
10865 }
10866 hideContent(needToHide) {
10867 if(this.isOp) {
10868 if(!aib.t) {
10869 $toggle(this.thr.el, !needToHide);
10870 $toggle(this.thr.btns, !needToHide);
10871 }
10872 } else {
10873 Post.hideContent(this.headerEl, this.btnHide, this.userToggled, needToHide);
10874 }
10875 }
10876 select() {
10877 if(this.isOp) {
10878 if(this.isHidden) {
10879 this.thr.el.previousElementSibling.classList.add('de-selected');
10880 }
10881 this.thr.el.classList.add('de-selected');
10882 } else {
10883 this.el.classList.add('de-selected');
10884 }
10885 }
10886 selectAndScrollTo(scrollNode = this.el) {
10887 scrollTo(0, deWindow.pageYOffset + scrollNode.getBoundingClientRect().top -
10888 Post.sizing.wHeight / 2 + scrollNode.clientHeight / 2);
10889 if(HotKeys.enabled) {
10890 if(HotKeys.cPost) {
10891 HotKeys.cPost.unselect();
10892 }
10893 HotKeys.cPost = this;
10894 HotKeys.lastPageOffset = deWindow.pageYOffset;
10895 } else {
10896 const el = $q('.de-selected');
10897 if(el) {
10898 el.unselect();
10899 }
10900 }
10901 this.select();
10902 }
10903 setUserVisib(isHide, isSave = true, note = null) {
10904 this.userToggled = true;
10905 this.setVisib(isHide, note);
10906 if(this.isOp || this.isHidden === isHide) {
10907 const hideClass = isHide ? 'de-btn-unhide-user' : 'de-btn-hide-user';
10908 this.btnHide.setAttribute('class', hideClass);
10909 if(this.isOp) {
10910 this.thr.btnHide.setAttribute('class', hideClass);
10911 }
10912 }
10913 if(isSave) {
10914 const { num } = this;
10915 HiddenPosts.set(num, this.thr.num, isHide);
10916 if(this.isOp) {
10917 if(isHide) {
10918 HiddenThreads.set(num, num, this.title);
10919 } else {
10920 HiddenThreads.removeStorage(num);
10921 }
10922 }
10923 sendStorageEvent('__de-post', {
10924 hide : isHide,
10925 brd : aib.b,
10926 num,
10927 thrNum : this.thr.num,
10928 title : this.isOp ? this.title : ''
10929 });
10930 }
10931 this.ref.toggleRef(isHide, false);
10932 }
10933 setVisib(isHide, note = null) {
10934 if(this.isHidden === isHide) {
10935 if(isHide && note) {
10936 this.note.set(note);
10937 }
10938 return;
10939 }
10940 if(this.isOp) {
10941 this.thr.isHidden = isHide;
10942 } else {
10943 if(Cfg.delHiddPost === 1 || Cfg.delHiddPost === 2) {
10944 this.wrap.classList.toggle('de-hidden', isHide);
10945 } else {
10946 this._pref.onmouseover = this._pref.onmouseout = !isHide ? null : e => {
10947 const yOffset = deWindow.pageYOffset;
10948 this.hideContent(e.type === 'mouseout');
10949 scrollTo(deWindow.pageXOffset, yOffset);
10950 };
10951 }
10952 }
10953 if(Cfg.strikeHidd) {
10954 setTimeout(() => this._strikePostNum(isHide), 50);
10955 }
10956 if(isHide) {
10957 this.note.set(note);
10958 } else {
10959 this.note.hideNote();
10960 }
10961 this.hideContent(this.isHidden = isHide);
10962 }
10963 spellHide(note) {
10964 this.spellHidden = true;
10965 if(!this.userToggled) {
10966 this.setVisib(true, note);
10967 this.ref.hideRef();
10968 }
10969 }
10970 spellUnhide() {
10971 this.spellHidden = false;
10972 if(!this.userToggled) {
10973 this.setVisib(false);
10974 this.ref.unhideRef();
10975 }
10976 }
10977 toggleImages(isExpand = !this.images.expanded, isExpandVideos = true) {
10978 for(const image of this.images) {
10979 if((image.isImage || isExpandVideos && image.isVideo) && (image.expanded ^ isExpand)) {
10980 if(isExpand) {
10981 image.expandImg(true, null);
10982 } else {
10983 image.collapseImg(null);
10984 }
10985 }
10986 }
10987 }
10988 unselect() {
10989 if(this.isOp) {
10990 const el = $id('de-thr-hid-' + this.num);
10991 if(el) {
10992 el.classList.remove('de-selected');
10993 }
10994 this.thr.el.classList.remove('de-selected');
10995 } else {
10996 this.el.classList.remove('de-selected');
10997 }
10998 }
10999
11000 _clickMenu(el) {
11001 const isHide = !this.isHidden;
11002 const isPview = this instanceof Pview;
11003 const { num } = this;
11004 switch(el.getAttribute('info')) {
11005 case 'hide-sel': {
11006 let { startContainer: start, endContainer: end } = this._selRange;
11007 if(start.nodeType === 3) {
11008 start = start.parentNode;
11009 }
11010 if(end.nodeType === 3) {
11011 end = end.parentNode;
11012 }
11013 const inMsgSel = `${ aib.qPostMsg }, ${ aib.qPostMsg } *`;
11014 if((nav.matchesSelector(start, inMsgSel) && nav.matchesSelector(end, inMsgSel)) || (
11015 nav.matchesSelector(start, aib.qPostSubj) &&
11016 nav.matchesSelector(end, aib.qPostSubj)
11017 )) {
11018 if(this._selText.includes('\n')) {
11019 Spells.addSpell(1 /* #exp */,
11020 `/${ quoteReg(this._selText).replace(/\r?\n/g, '\\n') }/`, false);
11021 } else {
11022 Spells.addSpell(0 /* #words */, this._selText.toLowerCase(), false);
11023 }
11024 } else {
11025 dummy.innerHTML = '';
11026 dummy.appendChild(this._selRange.cloneContents());
11027 Spells.addSpell(2 /* #exph */,
11028 `/${ quoteReg(dummy.innerHTML.replace(/^<[^>]+>|<[^>]+>$/g, '')) }/`, false);
11029 }
11030 return;
11031 }
11032 case 'hide-name': Spells.addSpell(6 /* #name */, this.posterName, false); return;
11033 case 'hide-trip': Spells.addSpell(7 /* #trip */, this.posterTrip, false); return;
11034 case 'hide-img': {
11035 const { weight: w, width: wi, height: h } = this.images.firstAttach;
11036 Spells.addSpell(8 /* #img */, [0, [w, w], [wi, wi, h, h]], false);
11037 return;
11038 }
11039 case 'hide-imgn':
11040 Spells.addSpell(3 /* #imgn */, `/${ quoteReg(this.images.firstAttach.name) }/`, false);
11041 return;
11042 case 'hide-ihash':
11043 ImagesHashStorage.getHash(this.images.firstAttach).then(hash => {
11044 if(hash !== -1) {
11045 Spells.addSpell(4 /* #ihash */, hash, false);
11046 }
11047 });
11048 return;
11049 case 'hide-noimg': Spells.addSpell(0x108 /* (#all & !#img) */, '', true); return;
11050 case 'hide-text': {
11051 const words = Post.getWrds(this.text);
11052 for(let post = Thread.first.op; post; post = post.next) {
11053 Post.findSameText(num, !isHide, words, post);
11054 }
11055 return;
11056 }
11057 case 'hide-notext': Spells.addSpell(0x10B /* (#all & !#tlen) */, '', true); return;
11058 case 'hide-refs':
11059 this.ref.toggleRef(isHide, true);
11060 this.setUserVisib(isHide);
11061 return;
11062 case 'hide-refsonly': Spells.addSpell(0 /* #words */, '>>' + num, false); return;
11063 case 'post-markmy': {
11064 const isAdd = !MyPosts.has(num);
11065 if(isAdd) {
11066 MyPosts.set(num, this.thr.num);
11067 } else {
11068 MyPosts.removeStorage(num);
11069 }
11070 this.el.classList.toggle('de-mypost', isAdd);
11071 $each($Q(`[de-form] ${ aib.qPostMsg } a[href$="${ aib.anchor + num }"]`), el => {
11072 const post = aib.getPostOfEl(el);
11073 if(post.el !== this.el) {
11074 el.classList.toggle('de-ref-you', isAdd);
11075 post.el.classList.toggle('de-mypost-reply', isAdd);
11076 }
11077 });
11078 return;
11079 }
11080 case 'post-reply':
11081 pr.showQuickReply(isPview ? Pview.topParent : this, num, !isPview, false);
11082 quotetxt = '';
11083 return;
11084 case 'post-report': aib.reportForm(num, this.thr.num); return;
11085 case 'thr-exp': {
11086 const task = +el.textContent.match(/\d+/);
11087 this.thr.loadPosts(!task ? 'all' : task === 10 ? 'more' : task);
11088 }
11089 }
11090 }
11091 _getMenuHide() {
11092 const item = name => `<span info="hide-${ name }" class="de-menu-item">${
11093 Lng.selHiderMenu[name][lang] }</span>`;
11094 const sel = deWindow.getSelection();
11095 const ssel = sel.toString().trim();
11096 if(ssel) {
11097 this._selText = ssel;
11098 this._selRange = sel.getRangeAt(0);
11099 }
11100 return `${ ssel ? item('sel') : '' }${
11101 this.posterName ? item('name') : '' }${
11102 this.posterTrip ? item('trip') : '' }${
11103 this.images.hasAttachments ? item('img') + item('imgn') + item('ihash') : item('noimg') }${
11104 this.text ? item('text') : item('notext') }${
11105 !Cfg.hideRefPsts && this.ref.hasMap ? item('refs') : '' }${
11106 item('refsonly') }`;
11107 }
11108 _strikePostNum(isHide) {
11109 const { num } = this;
11110 if(isHide) {
11111 Post.hiddenNums.add(+num);
11112 } else {
11113 Post.hiddenNums.delete(+num);
11114 }
11115 $each($Q(`[de-form] a[href$="${ aib.anchor + num }"]`), el => {
11116 el.classList.toggle('de-link-hid', isHide);
11117 if(Cfg.removeHidd && el.classList.contains('de-link-backref')) {
11118 const refMapEl = el.parentNode;
11119 if(isHide === !$q('.de-link-backref:not(.de-link-hid)', refMapEl)) {
11120 $toggle(refMapEl, !isHide);
11121 }
11122 }
11123 });
11124 }
11125}
11126Post.hasNew = false;
11127Post.hiddenNums = new Set();
11128Post.Сontent = class PostContent extends TemporaryContent {
11129 constructor(post) {
11130 super(post);
11131 if(this._isInited) {
11132 return;
11133 }
11134 this._isInited = true;
11135 this.el = post.el;
11136 this.post = post;
11137 }
11138 get headerEl() {
11139 const value = $q(aib.qPostHeader, this.el);
11140 Object.defineProperty(this, 'headerEl', { value });
11141 return value;
11142 }
11143 get html() {
11144 const value = this.el.outerHTML;
11145 Object.defineProperty(this, 'html', { value });
11146 return value;
11147 }
11148 get posterName() {
11149 const pName = $q(aib.qPostName, this.el);
11150 const value = pName ? pName.textContent.trim().replace(/\s/g, ' ') : '';
11151 Object.defineProperty(this, 'posterName', { value });
11152 return value;
11153 }
11154 get posterTrip() {
11155 const pTrip = $q(aib.qPostTrip, this.el);
11156 const value = pTrip ? pTrip.textContent : '';
11157 Object.defineProperty(this, 'posterTrip', { value });
11158 return value;
11159 }
11160 get subj() {
11161 const subj = $q(aib.qPostSubj, this.el);
11162 const value = subj ? subj.textContent : '';
11163 Object.defineProperty(this, 'subj', { value });
11164 return value;
11165 }
11166 get text() {
11167 const value = this.post.msg.innerHTML
11168 .replace(/<\/?(?:br|p|li)[^>]*?>/gi, '\n')
11169 .replace(/<[^>]+?>/g, '')
11170 .replace(/>/g, '>')
11171 .replace(/</g, '<')
11172 .replace(/ /g, '\u00A0').trim();
11173 Object.defineProperty(this, 'text', { value });
11174 return value;
11175 }
11176 get title() {
11177 const value = this.subj || this.text.substring(0, 70).replace(/\s+/g, ' ');
11178 Object.defineProperty(this, 'title', { value });
11179 return value;
11180 }
11181 get wrap() {
11182 const value = aib.getPostWrap(this.el, this.post.isOp);
11183 Object.defineProperty(this, 'wrap', { value });
11184 return value;
11185 }
11186};
11187Post.Note = class PostNote {
11188 constructor(post) {
11189 this.text = null;
11190 this._post = post;
11191 this.isHideThr = this._post.isOp && !aib.t; // Hide threads only on board
11192 if(!this.isHideThr) {
11193 // Create usual post note
11194 this._noteEl = this.textEl = $bEnd(post.btns, '<span class="de-post-note"></span>');
11195 return;
11196 }
11197 // Create a stub before the thread, that also hides thread by CSS
11198 this._noteEl = $bBegin(post.thr.el, `<div class="${ aib.cReply } de-thr-hid" id="de-thr-hid-${
11199 post.num }">${ Lng.hiddenThr[lang] }: <a href="#">№${ post.num }</a>
11200 <span class="de-thread-note"></span>
11201 </div>`);
11202 this._aEl = $q('a', this._noteEl);
11203 this.textEl = this._aEl.nextElementSibling;
11204 }
11205 hideNote() {
11206 if(this.isHideThr) {
11207 this._aEl.onmouseover = this._aEl.onmouseout = this._aEl.onclick = null;
11208 }
11209 $hide(this._noteEl);
11210 }
11211 reset() {
11212 this.text = null;
11213 if(this.isHideThr) {
11214 this.set(null);
11215 } else {
11216 this.hideNote();
11217 }
11218 }
11219 set(note) {
11220 this.text = note;
11221 let text;
11222 if(this.isHideThr) {
11223 this._aEl.onmouseover = this._aEl.onmouseout = e => this._post.hideContent(e.type === 'mouseout');
11224 this._aEl.onclick = e => {
11225 $pd(e);
11226 this._post.setUserVisib(!this._post.isHidden);
11227 };
11228 text = (this._post.title ? `(${ this._post.title }) ` : '') +
11229 (note ? `[autohide: ${ note }]` : '');
11230 } else {
11231 text = note ? `autohide: ${ note }` : '';
11232 }
11233 this.textEl.textContent = text;
11234 $show(this._noteEl);
11235 }
11236};
11237Post.sizing = {
11238 get dPxRatio() {
11239 const value = deWindow.devicePixelRatio || 1;
11240 Object.defineProperty(this, 'dPxRatio', { value });
11241 return value;
11242 },
11243 get wHeight() {
11244 const value = nav.viewportHeight();
11245 if(!this._enabled) {
11246 doc.defaultView.addEventListener('resize', this);
11247 this._enabled = true;
11248 }
11249 Object.defineProperties(this, {
11250 wHeight : { writable: true, configurable: true, value },
11251 wWidth : { writable: true, configurable: true, value: nav.viewportWidth() }
11252 });
11253 return value;
11254 },
11255 get wWidth() {
11256 const value = nav.viewportWidth();
11257 if(!this._enabled) {
11258 doc.defaultView.addEventListener('resize', this);
11259 this._enabled = true;
11260 }
11261 Object.defineProperties(this, {
11262 wHeight : { writable: true, configurable: true, value: nav.viewportHeight() },
11263 wWidth : { writable: true, configurable: true, value }
11264 });
11265 return value;
11266 },
11267 handleEvent() {
11268 this.wHeight = nav.viewportHeight();
11269 this.wWidth = nav.viewportWidth();
11270 },
11271
11272 _enabled: false
11273};
11274
11275/* ==[ PostPreviews.js ]======================================================================================
11276 POST PREVIEWS
11277=========================================================================================================== */
11278
11279class Pview extends AbstractPost {
11280 constructor(parent, link, pNum, tNum) {
11281 super(parent.thr, pNum, pNum === tNum);
11282 this.isSticky = false;
11283 this.parent = parent;
11284 this.remoteThr = null;
11285 this.tNum = tNum;
11286 this._isCached = false;
11287 this._isLeft = false;
11288 this._isTop = false;
11289 this._link = link;
11290 this._newPos = null;
11291 this._offsetTop = 0;
11292 this._readDelay = 0;
11293 let post = pByNum.get(pNum);
11294 if(post && (!post.isOp || !(parent instanceof Pview) || !parent._isCached)) {
11295 this._buildPview(post);
11296 return;
11297 }
11298 this._isCached = true;
11299 this.brd = link.pathname.match(/^\/?(.+\/)/)[1].replace(aib.res, '').replace(/\/$/, '');
11300 if(PviewsCache.has(this.brd + tNum)) {
11301 post = PviewsCache.get(this.brd + tNum).getPost(pNum);
11302 if(post) {
11303 this._buildPview(post);
11304 } else {
11305 this._showPview(this.el = $add(`<div class="${ aib.cReply } de-pview-info de-pview">
11306 ${ Lng.postNotFound[lang] }</div>`));
11307 }
11308 return;
11309 }
11310 this._showPview(this.el = $add(`<div class="${ aib.cReply } de-pview-info de-pview">
11311 <svg class="de-wait"><use xlink:href="#de-symbol-wait"/></svg>${ Lng.loading[lang] }</div>`));
11312
11313 // Get post preview via ajax. Always use DOM parsing.
11314 this._loadPromise = ajaxPostsLoad(this.brd, tNum, false, false)
11315 .then(pBuilder => this._onload(pBuilder), err => this._onerror(err));
11316 }
11317 static get topParent() {
11318 return Pview.top ? Pview.top.parent : null;
11319 }
11320 static showPview(parent, link) {
11321 const tNum = +(link.pathname.match(/.+?\/[^\d]*(\d+)/) || [0, aib.getPostOfEl(link).tNum])[1];
11322 let pNum = link.textContent.match(/\d+/g);
11323 pNum = pNum ? +pNum.pop() : tNum;
11324 const isTop = !(parent instanceof Pview);
11325 let pv = isTop ? Pview.top : parent.kid;
11326 clearTimeout(Pview._delTO);
11327 if(pv && pv.num === pNum) {
11328 if(pv.kid) {
11329 pv.kid.deletePview();
11330 }
11331 if(pv._link !== link) {
11332 // If cursor hovers new link with the same number - move old preview here
11333 pv._setPosition(link, Cfg.animation);
11334 pv._link.classList.remove('de-link-parent');
11335 link.classList.add('de-link-parent');
11336 pv._link = link;
11337 if(pv.parent.num !== parent.num) {
11338 $each($Q('.de-link-pview', pv.el), el => el.classList.remove('de-link-pview'));
11339 Pview._markLink(pv.el, parent.num);
11340 }
11341 }
11342 pv.parent = parent;
11343 } else if(!Cfg.noNavigHidd || !pByNum.has(pNum) || !pByNum.get(pNum).hidden) {
11344 // Show new preview under new link
11345 if(pv) {
11346 pv.deletePview();
11347 }
11348 pv = new Pview(parent, link, pNum, tNum);
11349 if(isTop) {
11350 Pview.top = pv;
11351 }
11352 } else {
11353 return null;
11354 }
11355 return pv;
11356 }
11357 static updatePosition(scroll) {
11358 let pv = Pview.top;
11359 if(!pv) {
11360 return;
11361 }
11362 const { parent } = pv;
11363 if(parent.isOmitted) {
11364 pv.deletePview();
11365 return;
11366 }
11367 if(parent.thr.loadCount === 1 && !parent.el.contains(pv._link)) {
11368 const el = parent.ref.getElByNum(pv.num);
11369 if(!el) {
11370 pv.deletePview();
11371 return;
11372 }
11373 pv._link = el;
11374 }
11375 const cr = parent.isHidden ? parent : pv._link.getBoundingClientRect();
11376 const diff = pv._isTop ?
11377 pv._offsetTop - deWindow.pageYOffset - cr.bottom :
11378 pv._offsetTop + pv.el.offsetHeight - deWindow.pageYOffset - cr.top;
11379 if(Math.abs(diff) > 1) {
11380 if(scroll) {
11381 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset - diff);
11382 }
11383 do {
11384 pv._offsetTop -= diff;
11385 pv.el.style.top = Math.max(pv._offsetTop, 0) + 'px';
11386 } while((pv = pv.kid));
11387 }
11388 }
11389 get stickBtn() {
11390 const value = $q('.de-btn-stick', this.el);
11391 Object.defineProperty(this, 'stickBtn', { value });
11392 return value;
11393 }
11394 deletePview() {
11395 this.parent.kid = null;
11396 this._link.classList.remove('de-link-parent');
11397 if(Pview.top === this) {
11398 Pview.top = null;
11399 }
11400 if(this._loadPromise) {
11401 this._loadPromise.cancelPromise();
11402 this._loadPromise = null;
11403 }
11404 let vPost = AttachedImage.viewer && AttachedImage.viewer.data.post;
11405 let pv = this;
11406 do {
11407 clearTimeout(pv._readDelay);
11408 if(vPost === pv) {
11409 AttachedImage.closeImg();
11410 vPost = null;
11411 }
11412 const { el } = pv;
11413 pByEl.delete(el);
11414 if(Cfg.animation) {
11415 $animate(el, 'de-pview-anim', true);
11416 el.style.animationName =
11417 `de-post-close-${ this._isTop ? 't' : 'b' }${ this._isLeft ? 'l' : 'r' }`;
11418 } else {
11419 el.remove();
11420 }
11421 } while((pv = pv.kid));
11422 }
11423 deleteNonSticky() {
11424 let lastSticky = null, pv = this;
11425 do {
11426 if(pv.isSticky) {
11427 lastSticky = pv;
11428 }
11429 } while((pv = pv.kid));
11430 if(!lastSticky) {
11431 this.deletePview();
11432 } else if(lastSticky.kid) {
11433 lastSticky.kid.deletePview();
11434 }
11435 }
11436 handleEvent(e) {
11437 const pv = e.target;
11438 if(e.type === 'animationend' && pv.style.animationName) {
11439 pv.classList.remove('de-pview-anim');
11440 pv.style.cssText = this._newPos;
11441 this._newPos = null;
11442 $delAll('.de-css-move', doc.head);
11443 pv.removeEventListener('animationend', this);
11444 return;
11445 }
11446 let isOverEvent = false;
11447 checkMouse: do {
11448 switch(e.type) {
11449 case 'mouseover': isOverEvent = true; break;
11450 case 'mouseout': break;
11451 default: break checkMouse;
11452 }
11453 const el = fixEventEl(e.relatedTarget);
11454 if(!el ||
11455 isOverEvent && (el.tagName !== 'A' || el.isNotRefLink) ||
11456 el !== this.el && !this.el.contains(el)
11457 ) {
11458 if(isOverEvent) {
11459 this.mouseEnter();
11460 } else if(Pview.top) {
11461 Pview.top.markToDel();
11462 }
11463 }
11464 } while(false);
11465 if(!this.loading) {
11466 super.handleEvent(e);
11467 }
11468 }
11469 markToDel() {
11470 clearTimeout(Pview._delTO);
11471 Pview._delTO = setTimeout(() => this.deleteNonSticky(), Cfg.linksOut);
11472 }
11473 mouseEnter() {
11474 if(this.kid) {
11475 this.kid.markToDel();
11476 } else {
11477 clearTimeout(Pview._delTO);
11478 }
11479 }
11480 setUserVisib() {
11481 const post = pByNum.get(this.num);
11482 const isHide = post.isHidden;
11483 post.setUserVisib(!isHide);
11484 Pview.updatePosition(true);
11485 $each($Q(`.de-btn-pview-hide[de-num="${ this.num }"]`), el => {
11486 el.setAttribute('class',
11487 `${ isHide ? 'de-btn-hide-user' : 'de-btn-unhide-user' } de-btn-pview-hide`);
11488 el.parentNode.classList.toggle('de-post-hide', !isHide);
11489 });
11490 }
11491 toggleSticky(isEnabled) {
11492 this.stickBtn.setAttribute('class', isEnabled ? 'de-btn-stick-on' : 'de-btn-stick');
11493 this.isSticky = isEnabled;
11494 }
11495
11496 static _markLink(el, num) {
11497 $each($Q(`a[href*="${ num }"]`, el),
11498 el => el.textContent.startsWith('>>' + num) && el.classList.add('de-link-pview'));
11499 }
11500 async _buildPview(post) {
11501 $del(this.el);
11502 const { num } = this;
11503 const pv = this.el = post.el.cloneNode(true);
11504 pByEl.set(pv, this);
11505 const isMyPost = MyPosts.has(num);
11506 pv.className = `${ aib.cReply } de-pview${
11507 post.isViewed ? ' de-viewed' : '' }${ isMyPost ? ' de-mypost' : '' }` +
11508 `${ post.el.classList.contains('de-mypost-reply') ? ' de-mypost-reply' : '' }`;
11509 $show(pv);
11510 $each($Q('.de-post-hiddencontent', pv), el => el.classList.remove('de-post-hiddencontent'));
11511 if(Cfg.linksNavig) {
11512 Pview._markLink(pv, this.parent.num);
11513 }
11514 this._pref = $q(aib.qPostRef, pv);
11515 this._link.classList.add('de-link-parent');
11516 const { isOp } = this;
11517 let f;
11518 const isFav = isOp && (post.thr.isFav ||
11519 ((f = (await readFavorites())[aib.host]) && (f = f[this.brd]) && (num in f)));
11520 const isCached = post instanceof CacheItem;
11521 const pCountHtml = (post.isDeleted ? ` de-post-counter-deleted">${ Lng.deleted[lang] }</span>` :
11522 `">${ isOp ? '(OP)' : post.count + +!(aib.JsonBuilder && isCached) }</span>`) +
11523 (isMyPost ? '<span class="de-post-counter-you">(You)</span>' : '');
11524 const pText = '<svg class="de-btn-reply"><use xlink:href="#de-symbol-post-reply"/></svg>' +
11525 (isOp ? `<svg class="${ isFav ? 'de-btn-fav-sel' : 'de-btn-fav' }">` +
11526 '<use xlink:href="#de-symbol-post-fav"></use></svg>' : '') +
11527 (post.sage ? '<svg class="de-btn-sage"><use xlink:href="#de-symbol-post-sage"/></svg>' : '') +
11528 '<svg class="de-btn-stick"><use xlink:href="#de-symbol-post-stick"/></svg>' +
11529 '<span class="de-post-counter' + pCountHtml;
11530 if(isCached) {
11531 if(isOp) {
11532 this.remoteThr = post.thr;
11533 }
11534 this.btns = $aEnd(this._pref, `<span class="de-post-btns">${ pText }</span>`);
11535 embedAudioLinks(this);
11536 if(Cfg.embedYTube) {
11537 new VideosParser().parse(this).endParser();
11538 }
11539 embedPostMsgImages(pv);
11540 processImgInfoLinks(this);
11541 } else {
11542 const btnsEl = this.btns = $q('.de-post-btns', pv);
11543 $del($q('.de-post-counter', btnsEl));
11544 if(post.isHidden) {
11545 btnsEl.classList.add('de-post-hide');
11546 }
11547 btnsEl.innerHTML = `<svg class="de-btn-${ post.isHidden ? 'unhide' : 'hide' }${
11548 post.userToggled ? '-user' : '' } de-btn-pview-hide" de-num="${ num }"><!--
11549 --><use class="de-btn-hide-use" xlink:href="#de-symbol-post-hide"/><!--
11550 --><use class="de-btn-unhide-use" xlink:href="#de-symbol-post-unhide"/></svg>${ pText }`;
11551 $delAll(`${ !aib.t && isOp ? aib.qOmitted + ', ' : '' }.de-fullimg-wrap, .de-fullimg-after`, pv);
11552 $each($Q(aib.qPostImg, pv), el => $show(el.parentNode));
11553 const link = $q('.de-link-parent', pv);
11554 if(link) {
11555 link.classList.remove('de-link-parent');
11556 }
11557 if(Cfg.embedYTube && post.videos.hasLinks) {
11558 if(post.videos.playerInfo !== null) {
11559 Object.defineProperty(this, 'videos',
11560 { value: new Videos(this, $q('.de-video-obj', pv), post.videos.playerInfo) });
11561 }
11562 this.videos.updatePost($Q('.de-video-link', post.el), $Q('.de-video-link', pv), true);
11563 }
11564 if(Cfg.addImgs) {
11565 $each($Q('.de-img-embed', pv), $show);
11566 }
11567 if(Cfg.markViewed) {
11568 this._readDelay = setTimeout(post => {
11569 if(!post.isViewed) {
11570 post.el.classList.add('de-viewed');
11571 post.isViewed = true;
11572 }
11573 const arr = (sesStorage['de-viewed'] || '').split(',');
11574 arr.push(post.num);
11575 sesStorage['de-viewed'] = arr;
11576 }, post.text.length > 100 ? 2e3 : 500, post);
11577 }
11578 }
11579 pv.addEventListener('click', this, true);
11580 this._showPview(pv);
11581 }
11582 _onerror(err) {
11583 if(!(err instanceof CancelError)) {
11584 this.el.innerHTML = (err instanceof AjaxError) && err.code === 404 ?
11585 Lng.postNotFound[lang] : getErrorMessage(err);
11586 }
11587 }
11588 _onload(pBuilder) {
11589 const b = this.brd;
11590 const { num } = this.parent;
11591 const post = new PviewsCache(pBuilder, b, this.tNum).getPost(this.num);
11592 if(post && (aib.b !== b || !post.ref.hasMap || !post.ref.has(num))) {
11593 (post.ref.hasMap ? $q('.de-refmap', post.el) : $aEnd(post.msg, '<div class="de-refmap"></div>'))
11594 .insertAdjacentHTML('afterbegin', `<a class="de-link-backref" href="${
11595 aib.getThrUrl(b, this.parent.tNum) + aib.anchor + num }">>>${
11596 aib.b === b ? '' : `/${ aib.b }/` }${ num }</a><span class="de-refcomma">, </span>`);
11597 }
11598 if(post) {
11599 this._buildPview(post);
11600 } else {
11601 this.el.innerHTML = Lng.postNotFound[lang];
11602 }
11603 }
11604 _setPosition(link, isAnim) {
11605 let oldCSS;
11606 const cr = link.getBoundingClientRect();
11607 const offX = cr.left + deWindow.pageXOffset + cr.width / 2;
11608 const offY = cr.top;
11609 const bWidth = nav.viewportWidth();
11610 const isLeft = offX < bWidth / 2;
11611 const pv = this.el;
11612 const temp = isLeft ? offX : offX - Math.min(parseInt(pv.offsetWidth, 10), offX - 10);
11613 const lmw = `max-width:${ bWidth - temp - 10 }px; left:${ temp }px;`;
11614 const { style } = pv;
11615 if(isAnim) {
11616 oldCSS = style.cssText;
11617 }
11618 style.cssText = (isAnim ? 'opacity: 0; ' : '') + lmw;
11619 let top = pv.offsetHeight;
11620 const isTop = offY + top + cr.height < nav.viewportHeight() || offY - top < 5;
11621 top = deWindow.pageYOffset + (isTop ? offY + cr.height : offY - top);
11622 this._offsetTop = top;
11623 this._isLeft = isLeft;
11624 this._isTop = isTop;
11625 if(!isAnim) {
11626 style.top = top + 'px';
11627 return;
11628 }
11629 const uId = 'de-movecss-' + Math.round(Math.random() * 1e3);
11630 $css(`@keyframes ${ uId } { to { ${ lmw } top:${ top }px; } }`).className = 'de-css-move';
11631 if(this._newPos) {
11632 style.cssText = this._newPos;
11633 pv.removeEventListener('animationend', this);
11634 } else {
11635 style.cssText = oldCSS;
11636 }
11637 this._newPos = `${ lmw } top:${ top }px;`;
11638 pv.addEventListener('animationend', this);
11639 pv.classList.add('de-pview-anim');
11640 style.animationName = uId;
11641 }
11642 _showMenu(el, html) {
11643 super._showMenu(el, html);
11644 this._menu.onover = () => this.mouseEnter();
11645 this._menu.onout = () => Pview.top.markToDel();
11646 }
11647 _showPview(el) {
11648 el.addEventListener('mouseover', this, true);
11649 el.addEventListener('mouseout', this, true);
11650 this.thr.form.el.appendChild(el);
11651 this._setPosition(this._link, false);
11652 if(Cfg.animation) {
11653 el.addEventListener('animationend', function aEvent() {
11654 el.removeEventListener('animationend', aEvent);
11655 el.classList.remove('de-pview-anim');
11656 el.style.animationName = '';
11657 });
11658 el.classList.add('de-pview-anim');
11659 el.style.animationName = `de-post-open-${ this._isTop ? 't' : 'b' }${ this._isLeft ? 'l' : 'r' }`;
11660 }
11661 }
11662}
11663Pview.top = null;
11664Pview._delTO = null;
11665
11666class CacheItem {
11667 constructor(pBuilder, thrUrl, count) {
11668 this._pBuilder = pBuilder;
11669 this._thrUrl = thrUrl;
11670 this.count = count;
11671 this.isDeleted = false;
11672 this.isInited = false;
11673 this.isOp = count === 0;
11674 this.isViewed = false;
11675 }
11676 * refLinks() {
11677 yield * this._pBuilder.getRefLinks(this.count, this._thrUrl);
11678 }
11679 get msg() {
11680 const value = $q(aib.qPostMsg, this.el);
11681 Object.defineProperty(this, 'msg', { value });
11682 return value;
11683 }
11684 get ref() {
11685 const value = new RefMap(this);
11686 Object.defineProperty(this, 'ref', { value });
11687 return value;
11688 }
11689 get sage() {
11690 const value = aib.getSage(this.el);
11691 Object.defineProperty(this, 'sage', { value });
11692 return value;
11693 }
11694 get title() {
11695 return new Post.Сontent(this).title;
11696 }
11697 get el() {
11698 const value = this.isOp ? this._pBuilder.getOpEl() : this._pBuilder.getPostEl(this.count - 1);
11699 Object.defineProperty(this, 'el', { value: doc.adoptNode(value) });
11700 return value;
11701 }
11702 get thr() {
11703 let value = null;
11704 if(this.isOp) {
11705 const pcount = this._pBuilder.length;
11706 value = { lastNum: this._pBuilder.getPNum(pcount - 1), pcount };
11707 Object.defineProperty(value, 'title', { get: () => this.title });
11708 }
11709 Object.defineProperty(this, 'thr', { value });
11710 return value;
11711 }
11712}
11713
11714class PviewsCache extends TemporaryContent {
11715 constructor(pBuilder, b, tNum) {
11716 super(b + tNum);
11717 if(this._isInited) {
11718 return;
11719 }
11720 this._isInited = true;
11721 const lPByNum = new Map();
11722 const thrUrl = aib.getThrUrl(b, tNum);
11723 lPByNum.set(tNum, new CacheItem(pBuilder, thrUrl, 0));
11724 for(let i = 0; i < pBuilder.length; ++i) {
11725 lPByNum.set(pBuilder.getPNum(i), new CacheItem(pBuilder, thrUrl, i + 1));
11726 }
11727 DelForm.tNums.add(tNum);
11728 this._b = b;
11729 this._posts = lPByNum;
11730 if(Cfg.linksNavig) {
11731 RefMap.gen(lPByNum);
11732 }
11733 }
11734 getPost(num) {
11735 const post = this._posts.get(num);
11736 if(post && !post.isInited) {
11737 if(this._b === aib.b && pByNum.has(num)) {
11738 post.ref.makeUnion(pByNum.get(num).ref);
11739 }
11740 if(post.ref.hasMap) {
11741 post.ref.initPostRef(post._thrUrl,
11742 Cfg.strikeHidd && Post.hiddenNums.size ? Post.hiddenNums : null);
11743 }
11744 post.isInited = true;
11745 }
11746 return post;
11747 }
11748}
11749PviewsCache.purgeSecs = 3e5;
11750
11751/* ==[ PostImages.js ]========================================================================================
11752 IMAGES
11753 images expanding (in post / by center), navigate buttons, image-links embedding
11754=========================================================================================================== */
11755
11756// Navigation buttons for expanding of images/videos by center
11757class ImagesNavigBtns {
11758 constructor(viewerObj) {
11759 const btns = $bEnd(docBody, `<div style="display: none;">
11760 <div id="de-img-btn-prev" class="de-img-btn" de-title="${ Lng.prevImg[lang] }">
11761 <svg><use xlink:href="#de-symbol-img-btn-arrow"/></svg></div>
11762 <div id="de-img-btn-next" class="de-img-btn" de-title="${ Lng.nextImg[lang] }">
11763 <svg><use xlink:href="#de-symbol-img-btn-arrow"/></svg></div>
11764 <div id="de-img-btn-auto" class="de-img-btn de-img-btn-none" title="${ Lng.autoPlayOn[lang] }">
11765 <svg><use xlink:href="#de-symbol-img-btn-auto"/></svg></div>
11766 <div id="de-img-btn-rotate" class="de-img-btn" title="${ Lng.rotateImg[lang] }">
11767 <svg><use xlink:href="#de-symbol-img-btn-rotate"/></svg></div></div>`);
11768 [this.prevBtn, this.nextBtn, this.autoBtn] = [...btns.children];
11769 this._btns = btns;
11770 this._btnsStyle = btns.style;
11771 this._hideTmt = 0;
11772 this._isHidden = true;
11773 this._oldX = -1;
11774 this._oldY = -1;
11775 this._viewer = viewerObj;
11776 doc.defaultView.addEventListener('mousemove', this);
11777 btns.addEventListener('mouseover', this);
11778 }
11779 handleEvent(e) {
11780 switch(e.type) {
11781 case 'mousemove': {
11782 const { clientX: curX, clientY: curY } = e;
11783 if(this._oldX !== curX || this._oldY !== curY) {
11784 this._oldX = curX;
11785 this._oldY = curY;
11786 this.showBtns();
11787 }
11788 return;
11789 }
11790 case 'mouseover':
11791 if(!this.hasEvents) {
11792 this.hasEvents = true;
11793 this._btns.addEventListener('mouseout', this);
11794 this._btns.addEventListener('click', this);
11795 }
11796 if(!this._isHidden) {
11797 clearTimeout(this._hideTmt);
11798 KeyEditListener.setTitle(this.prevBtn, 4);
11799 KeyEditListener.setTitle(this.nextBtn, 17);
11800 }
11801 return;
11802 case 'mouseout': this._setHideTmt(); return;
11803 case 'click': {
11804 const parent = e.target.parentNode;
11805 const viewer = this._viewer;
11806 switch(parent.id) {
11807 case 'de-img-btn-next': viewer.navigate(true); return;
11808 case 'de-img-btn-prev': viewer.navigate(false); return;
11809 case 'de-img-btn-rotate': viewer.rotateView(true); return;
11810 case 'de-img-btn-auto':
11811 this.autoBtn.title = (viewer.isAutoPlay = !viewer.isAutoPlay) ?
11812 Lng.autoPlayOff[lang] : Lng.autoPlayOn[lang];
11813 viewer.toggleVideoLoop();
11814 parent.classList.toggle('de-img-btn-auto-on');
11815 }
11816 }
11817 }
11818 }
11819 hideBtns() {
11820 this._btnsStyle.display = 'none';
11821 this._isHidden = true;
11822 this._oldX = this._oldY = -1;
11823 }
11824 removeBtns() {
11825 this._btns.remove();
11826 doc.defaultView.removeEventListener('mousemove', this);
11827 clearTimeout(this._hideTmt);
11828 }
11829 showBtns() {
11830 if(this._isHidden) {
11831 this._btnsStyle.removeProperty('display');
11832 this._isHidden = false;
11833 this._setHideTmt();
11834 }
11835 }
11836
11837 _setHideTmt() {
11838 clearTimeout(this._hideTmt);
11839 this._hideTmt = setTimeout(() => this.hideBtns(), 2e3);
11840 }
11841}
11842
11843// Expanding of images/videos BY CENTER: resizing, moving, opening, closing
11844class ImagesViewer {
11845 constructor(data) {
11846 this.data = null;
11847 this.isAutoPlay = false;
11848 this._data = null;
11849 this._elStyle = null;
11850 this._fullEl = null;
11851 this._height = 0;
11852 this._minSize = 0;
11853 this._moved = false;
11854 this._oldL = 0;
11855 this._oldT = 0;
11856 this._oldX = 0;
11857 this._oldY = 0;
11858 this._parentEl = null;
11859 this._width = 0;
11860 this._showFullImg(data);
11861 }
11862 closeImgViewer(e) {
11863 if(this.hasOwnProperty('_btns')) {
11864 this._btns.removeBtns();
11865 }
11866 this._removeFullImg(e);
11867 }
11868 handleEvent(e) {
11869 switch(e.type) {
11870 case 'mousedown':
11871 if(this.data.isVideo && ExpandableImage.isControlClick(e)) {
11872 return;
11873 }
11874 this._oldX = e.clientX;
11875 this._oldY = e.clientY;
11876 docBody.addEventListener('mousemove', this, true);
11877 docBody.addEventListener('mouseup', this, true);
11878 break;
11879 case 'mousemove': {
11880 const { clientX: curX, clientY: curY } = e;
11881 if(curX !== this._oldX || curY !== this._oldY) {
11882 this._oldL = parseInt(this._elStyle.left, 10) + curX - this._oldX;
11883 this._elStyle.left = this._oldL + 'px';
11884 this._oldT = parseInt(this._elStyle.top, 10) + curY - this._oldY;
11885 this._elStyle.top = this._oldT + 'px';
11886 this._oldX = curX;
11887 this._oldY = curY;
11888 this._moved = true;
11889 }
11890 return;
11891 }
11892 case 'mouseup':
11893 docBody.removeEventListener('mousemove', this, true);
11894 docBody.removeEventListener('mouseup', this, true);
11895 return;
11896 case 'click': {
11897 const el = e.target;
11898 if(this.data.isVideo && ExpandableImage.isControlClick(e) ||
11899 el.tagName !== 'IMG' &&
11900 el.tagName !== 'VIDEO' &&
11901 !el.classList.contains('de-fullimg-wrap') &&
11902 !el.classList.contains('de-fullimg-wrap-link') &&
11903 !el.classList.contains('de-fullimg-video-hack') &&
11904 el.className !== 'de-fullimg-load'
11905 ) {
11906 return;
11907 }
11908 if(e.button === 0) {
11909 if(this._moved) {
11910 this._moved = false;
11911 } else {
11912 this.closeImgViewer(e);
11913 AttachedImage.viewer = null;
11914 }
11915 e.stopPropagation();
11916 break;
11917 }
11918 return;
11919 }
11920 case 'mousewheel':
11921 this._handleWheelEvent(e.clientX, e.clientY,
11922 -1 / 40 * ('wheelDeltaY' in e ? e.wheelDeltaY : e.wheelDelta));
11923 break;
11924 default: // 'wheel' event
11925 this._handleWheelEvent(e.clientX, e.clientY, e.deltaY);
11926 }
11927 $pd(e);
11928 }
11929 navigate(isForward, isVideoOnly = false) {
11930 let { data } = this;
11931 data.cancelWebmLoad(this._fullEl);
11932 do {
11933 data = data.getFollowImg(isForward);
11934 } while(data && !data.isVideo && !data.isImage || isVideoOnly && data.isImage);
11935 if(data) {
11936 this.updateImgViewer(data, true, null);
11937 data.post.selectAndScrollTo(data.post.images.first.el);
11938 }
11939 }
11940 rotateView(isNextAngle) {
11941 if(isNextAngle) {
11942 this.data.rotate += this.data.rotate === 270 ? -270 : 90;
11943 }
11944 const angle = this.data.rotate;
11945 const isVert = angle === 90 || angle === 270;
11946 const img = $q('img, video', this._fullEl);
11947 img.style.transform = `rotate(${ angle }deg)${
11948 angle === 90 ? ' translateY(-100%)' : angle === 270 ? ' translateX(-100%)' : '' }`;
11949 img.classList.toggle('de-fullimg-rotated', isVert);
11950 img.style.height = `${ (isVert ? this._height / this._width : 1) * 100 }%`;
11951 if(this.data.isVideo && nav.firefoxVer >= 59) {
11952 img.previousElementSibling.style =
11953 (isVert ? 'width: calc(100% - 40px); height: 100%; ' : '') +
11954 (angle === 90 ? 'right: 0; ' : '') +
11955 (angle === 180 ? 'bottom: 0;' : '');
11956 }
11957 if(isNextAngle || angle !== 180) {
11958 this._rotateFullImg(this._fullEl);
11959 }
11960 }
11961 toggleVideoLoop() {
11962 if(this.data.isVideo) {
11963 toggleAttr($q('video', this._fullEl), 'loop', '', !this.isAutoPlay);
11964 }
11965 }
11966 updateImgViewer(data, showButtons, e) {
11967 this._removeFullImg(e);
11968 this._showFullImg(data, showButtons);
11969 }
11970
11971 get _btns() {
11972 const value = new ImagesNavigBtns(this);
11973 Object.defineProperty(this, '_btns', { value });
11974 return value;
11975 }
11976 get _zoomFactor() {
11977 const value = 1 + (Cfg.zoomFactor / 100);
11978 Object.defineProperty(this, '_zoomFactor', { value });
11979 return value;
11980 }
11981 _handleWheelEvent(clientX, clientY, delta) {
11982 if(delta === 0) {
11983 return;
11984 }
11985 let width, height;
11986 const { _width: oldW, _height: oldH } = this;
11987 if(delta > 0) {
11988 width = oldW / this._zoomFactor;
11989 height = oldH / this._zoomFactor;
11990 if(width <= this._minSize && height <= this._minSize) {
11991 return;
11992 }
11993 } else {
11994 width = oldW * this._zoomFactor;
11995 height = oldH * this._zoomFactor;
11996 }
11997 this._width = width;
11998 this._height = height;
11999 this._elStyle.width = width + 'px';
12000 this._elStyle.height = height + 'px';
12001 this._oldL = parseInt(clientX - (width / oldW) * (clientX - this._oldL), 10);
12002 this._elStyle.left = this._oldL + 'px';
12003 this._oldT = parseInt(clientY - (height / oldH) * (clientY - this._oldT), 10);
12004 this._elStyle.top = this._oldT + 'px';
12005 }
12006 _removeFullImg(e) {
12007 const { data } = this;
12008 data.cancelWebmLoad(this._fullEl);
12009 if(data.inPview && data.post.isSticky) {
12010 data.post.toggleSticky(false);
12011 }
12012 this._parentEl.remove();
12013 if(e && data.inPview) {
12014 data.sendCloseEvent(e, false);
12015 }
12016 }
12017 _resizeFullImg(el) {
12018 if(el !== this._fullEl) {
12019 return;
12020 }
12021 let [width, height, minSize] = this.data.computeFullSize();
12022 this._minSize = minSize ? minSize / this._zoomFactor : Cfg.minImgSize;
12023 if(Post.sizing.wWidth - this._oldL - this._width < 5 ||
12024 Post.sizing.wHeight - this._oldT - this._height < 5
12025 ) {
12026 return;
12027 }
12028 const cPointX = this._oldL + this._width / 2;
12029 const cPointY = this._oldT + this._height / 2;
12030 const maxWidth = (Post.sizing.wWidth - cPointX - 2) * 2;
12031 const maxHeight = (Post.sizing.wHeight - cPointY - 2) * 2;
12032 if(width > maxWidth || height > maxHeight) {
12033 const ar = width / height;
12034 if(ar > maxWidth / maxHeight) {
12035 width = maxWidth;
12036 height = width / ar;
12037 } else {
12038 height = maxHeight;
12039 width = height * ar;
12040 }
12041 if(minSize && width < minSize || height < minSize) {
12042 this._minSize = Math.max(width, height);
12043 }
12044 }
12045 this._width = width;
12046 this._height = height;
12047 this._elStyle.width = width + 'px';
12048 this._elStyle.height = height + 'px';
12049 this._elStyle.left = `${ this._oldL = parseInt(cPointX - width / 2, 10) }px`;
12050 this._elStyle.top = `${ this._oldT = parseInt(cPointY - height / 2, 10) }px`;
12051 }
12052 _rotateFullImg(el) {
12053 if(el !== this._fullEl) {
12054 return;
12055 }
12056 const { _width, _height } = this;
12057 this._width = _height;
12058 this._height = _width;
12059 this._elStyle.width = _height + 'px';
12060 this._elStyle.height = _width + 'px';
12061 const halfWidth = _width / 2;
12062 const halfHeight = _height / 2;
12063 this._elStyle.left = `${ this._oldL = parseInt(this._oldL + halfWidth - halfHeight, 10) }px`;
12064 this._elStyle.top = `${ this._oldT = parseInt(this._oldT + halfHeight - halfWidth, 10) }px`;
12065 }
12066 _showFullImg(data) {
12067 const [width, height, minSize] = data.computeFullSize();
12068 this._fullEl = data.getFullImg(false, el => this._resizeFullImg(el), el => this._rotateFullImg(el));
12069 this._width = width;
12070 this._height = height;
12071 this._minSize = minSize ? minSize / this._zoomFactor : Cfg.minImgSize;
12072 this._oldL = (Post.sizing.wWidth - width) / 2 - 1;
12073 this._oldT = (Post.sizing.wHeight - height) / 2 - 1;
12074 const el = $add(`<div class="de-fullimg-center${
12075 data.isVideo ? ' de-fullimg-center-video' : '' }" style="top:${ this._oldT -
12076 (Cfg.imgInfoLink ? 11 : 0) - (nav.firefoxVer >= 59 && data.isVideo ? 10 : 0) }px; left:${
12077 this._oldL }px; width:${ width }px; height:${ height }px; display: block"></div>`);
12078 el.appendChild(this._fullEl);
12079 if(data.isImage) {
12080 $aBegin(this._fullEl, `<a class="de-fullimg-wrap-link" href="${ data.src }"></a>`)
12081 .appendChild($q('img', this._fullEl));
12082 }
12083 this._elStyle = el.style;
12084 this.data = data;
12085 this._parentEl = el;
12086 el.addEventListener('onwheel' in el ? 'wheel' : 'mousewheel', this, true);
12087 el.addEventListener('mousedown', this, true);
12088 el.addEventListener('click', this, true);
12089 data.srcBtnEvents(this);
12090 if(data.inPview && !data.post.isSticky) {
12091 data.post.toggleSticky(true);
12092 }
12093 const btns = this._btns;
12094 if(!data.inPview) {
12095 btns.showBtns();
12096 btns.autoBtn.classList.toggle('de-img-btn-none', !data.isVideo);
12097 } else if(this.hasOwnProperty('_btns')) {
12098 btns.hideBtns();
12099 }
12100 data.post.thr.form.el.appendChild(el);
12101 this.toggleVideoLoop();
12102 if(this.data.rotate) {
12103 this.rotateView(false);
12104 }
12105 data.checkForRedirect(this._fullEl);
12106 }
12107}
12108
12109// Post image/video main initialization
12110class ExpandableImage {
12111 constructor(post, el, prev) {
12112 this.el = el;
12113 this.expanded = false;
12114 this.next = null;
12115 this.post = post;
12116 this.prev = prev;
12117 this.redirected = false;
12118 this.rotate = 0;
12119 this._fullEl = null;
12120 this._webmTitleLoad = null;
12121 if(prev) {
12122 prev.next = this;
12123 }
12124 }
12125 static isControlClick(e) {
12126 return Cfg.webmControl && e.clientY > (e.target.getBoundingClientRect().bottom - 40);
12127 }
12128 get height() {
12129 return (this._size || [-1, -1])[1];
12130 }
12131 get inPview() {
12132 const value = this.post instanceof Pview;
12133 Object.defineProperty(this, 'inPview', { value });
12134 return value;
12135 }
12136 get isImage() {
12137 const value = /(jpe?g|png|gif|webp)$/i.test(this.src) ||
12138 (this.src.startsWith('blob:') && !this.el.hasAttribute('de-video'));
12139 Object.defineProperty(this, 'isImage', { value });
12140 return value;
12141 }
12142 get isVideo() {
12143 const value = /(webm|mp4|ogv)(&|$)/i.test(this.src) ||
12144 (this.src.startsWith('blob:') && this.el.hasAttribute('de-video'));
12145 Object.defineProperty(this, 'isVideo', { value });
12146 return value;
12147 }
12148 get src() {
12149 const value = this._getImageSrc();
12150 Object.defineProperty(this, 'src', { value, configurable: true });
12151 return value;
12152 }
12153 get width() {
12154 return (this._size || [-1, -1])[0];
12155 }
12156 cancelWebmLoad(fullEl) {
12157 if(this.isVideo) {
12158 const videoEl = $q('video', fullEl);
12159 videoEl.pause();
12160 videoEl.removeAttribute('src');
12161 videoEl.load();
12162 }
12163 if(this._webmTitleLoad) {
12164 this._webmTitleLoad.cancelPromise();
12165 this._webmTitleLoad = null;
12166 }
12167 }
12168 checkForRedirect(fullEl) {
12169 if(!aib.getImgRedirectSrc || this.redirected) {
12170 return;
12171 }
12172 aib.getImgRedirectSrc(this.src).then(newSrc => {
12173 this.redirected = true;
12174 Object.defineProperty(this, 'src', { value: newSrc });
12175 $q('img, video', fullEl).src = this.el.src =
12176 this.el.parentNode.href = $q(aib.qImgNameLink, aib.getImgWrap(this.el)).href = newSrc;
12177 if(!this.isVideo) {
12178 $q('a', fullEl).href = newSrc;
12179 }
12180 });
12181 }
12182 collapseImg(e) { // Collapse an image that expanded in post
12183 if(e && this.isVideo && ExpandableImage.isControlClick(e)) {
12184 return;
12185 }
12186 let fullImgTop;
12187 if(e) {
12188 fullImgTop = e.target.getBoundingClientRect().top;
12189 }
12190 this.cancelWebmLoad(this._fullEl);
12191 this.expanded = false;
12192 this._fullEl.remove();
12193 this._fullEl = null;
12194 $show(this.el.parentNode);
12195 (aib.hasPicWrap ? this._getImageParent : this.el.parentNode).nextSibling.remove();
12196 if(e) {
12197 $pd(e);
12198 if(this.inPview) {
12199 this.sendCloseEvent(e, true);
12200 }
12201 const origImgTop = this.el.getBoundingClientRect().top;
12202 if(fullImgTop < 0 || origImgTop < 0) {
12203 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + origImgTop);
12204 }
12205 }
12206 }
12207 computeFullSize() {
12208 if(!this._size) {
12209 if(this.isVideo) {
12210 return [0, 0, null];
12211 }
12212 const el = new Image();
12213 el.src = this.el.src;
12214 return [el.width, el.height, null];
12215 }
12216 let [width, height] = this._size;
12217 if(Cfg.resizeDPI) {
12218 width /= Post.sizing.dPxRatio;
12219 height /= Post.sizing.dPxRatio;
12220 }
12221 const minSize = this.isVideo ? Math.max(Cfg.minImgSize, Cfg.minWebmWidth) : Cfg.minImgSize;
12222 if(width < minSize && height < minSize) {
12223 const ar = width / height;
12224 if(width > height) {
12225 width = minSize;
12226 height = width / ar;
12227 } else {
12228 height = minSize;
12229 width = this.isVideo ? minSize : height * ar;
12230 }
12231 }
12232 const maxWidth = Post.sizing.wWidth - 2;
12233 const maxHeight = Post.sizing.wHeight -
12234 (Cfg.imgInfoLink ? 24 : 2) - (nav.firefoxVer >= 59 && this.isVideo ? 19 : 0);
12235 if(width > maxWidth || height > maxHeight) {
12236 const ar = width / height;
12237 if(ar > maxWidth / maxHeight) {
12238 width = maxWidth;
12239 height = width / ar;
12240 } else {
12241 height = maxHeight;
12242 width = height * ar;
12243 }
12244 if(width < minSize || height < minSize) {
12245 return [width, height, Math.max(width, height)];
12246 }
12247 }
12248 return [width, height, null];
12249 }
12250 expandImg(inPost, e) {
12251 if(e && !e.bubbles) {
12252 return;
12253 }
12254 if(!inPost) {
12255 const { viewer } = AttachedImage;
12256 if(!viewer) {
12257 AttachedImage.viewer = new ImagesViewer(this);
12258 return;
12259 }
12260 if(viewer.data === this) {
12261 viewer.closeImgViewer(e);
12262 AttachedImage.viewer = null;
12263 return;
12264 }
12265 viewer.updateImgViewer(this, e);
12266 return;
12267 }
12268 let origImgTop;
12269 if(e) {
12270 origImgTop = e.target.getBoundingClientRect().top;
12271 }
12272 this.expanded = true;
12273 const { el } = this;
12274 (aib.hasPicWrap ? this._getImageParent : el.parentNode).insertAdjacentHTML('afterend',
12275 '<div class="de-fullimg-after"></div>');
12276 this._fullEl = this.getFullImg(true, null, null);
12277 this._fullEl.addEventListener('click', e => this.collapseImg(e), true);
12278 this.srcBtnEvents(this);
12279 $hide(el.parentNode);
12280 $after(el.parentNode, this._fullEl);
12281 this.checkForRedirect(this._fullEl);
12282 if(e) {
12283 const fullImgTop = this._fullEl.getBoundingClientRect().top;
12284 if(fullImgTop < 0 || origImgTop < 0) {
12285 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + fullImgTop);
12286 }
12287 }
12288 }
12289 getFollowImg(isForward) {
12290 const nImage = isForward ? this.next : this.prev;
12291 if(nImage) {
12292 return nImage;
12293 }
12294 let imgs, { post } = this;
12295 do {
12296 post = post.getAdjacentVisPost(!isForward);
12297 if(!post) {
12298 post = isForward ? Thread.first.op : Thread.last.last;
12299 if(post.isHidden || post.thr.isHidden) {
12300 post = post.getAdjacentVisPost(!isForward);
12301 if(!post) {
12302 return null;
12303 }
12304 }
12305 }
12306 imgs = post.images;
12307 } while(imgs.first === null);
12308 return isForward ? imgs.first : imgs.last;
12309 }
12310 getFullImg(inPost, onsizechange, onrotate) {
12311 let wrapEl, name, origSrc;
12312 const src = this._getImageSrc();
12313 const parent = this._getImageParent;
12314 if(this.el.className !== 'de-img-embed') {
12315 const nameEl = $q(aib.qImgNameLink, parent) || $q('a', parent);
12316 origSrc = nameEl.getAttribute('de-href') || nameEl.href;
12317 ({ name } = this);
12318 } else {
12319 origSrc = parent.href;
12320 name = origSrc.split('/').pop();
12321 }
12322 const imgNameEl = (Cfg.imgSrcBtns ?
12323 '<svg class="de-btn-src"><use xlink:href="#de-symbol-post-src"></use></svg>' : '') +
12324 `<a class="de-fullimg-link" target="_blank" title="${
12325 Lng.openOriginal[lang] }" href="${ origSrc }">${ name }`;
12326 const wrapClass = `${ inPost ? ' de-fullimg-wrap-inpost' : ` de-fullimg-wrap-center${
12327 this._size ? '' : ' de-fullimg-wrap-nosize' }` }${
12328 this.isVideo ? ' de-fullimg-video' : '' }`;
12329 // Expand images: JPG, PNG, GIF, WEBP
12330 if(!this.isVideo) {
12331 const waitEl = !aib.getImgRedirectSrc && this._size ? '' :
12332 '<svg class="de-fullimg-load"><use xlink:href="#de-symbol-wait"/></svg>';
12333 wrapEl = $add(`<div class="de-fullimg-wrap${ wrapClass }">
12334 ${ waitEl }
12335 <img class="de-fullimg" src="${ src }" alt="${ src }">
12336 <div class="de-fullimg-info">${ imgNameEl }</a></div>
12337 </div>`);
12338 const imgEl = $q('.de-fullimg', wrapEl);
12339 imgEl.onload = imgEl.onerror = ({ target: img }) => {
12340 if(!(img.naturalHeight + img.naturalWidth)) {
12341 if(!img.onceLoaded) {
12342 img.src = img.src;
12343 img.onceLoaded = true;
12344 }
12345 return;
12346 }
12347 const { naturalWidth: newW, naturalHeight: newH } = img;
12348 const ar = this._size ? this._size[1] / this._size[0] : newH / newW;
12349 const isRotated = !img.scrollWidth ? false :
12350 img.scrollHeight / img.scrollWidth > 1 ? ar < 1 : ar > 1;
12351 if(!this._size || isRotated) {
12352 this._size = isRotated ? [newH, newW] : [newW, newH];
12353 }
12354 const parentEl = img.parentNode.parentNode;
12355 const waitEl = $q('.de-fullimg-load', parentEl);
12356 if(waitEl) {
12357 $hide(waitEl);
12358 parentEl.classList.remove('de-fullimg-wrap-nosize');
12359 if(onsizechange) {
12360 onsizechange(parentEl);
12361 }
12362 } else if(isRotated && onrotate) {
12363 onrotate(parentEl);
12364 }
12365 };
12366 DollchanAPI.notify('expandmedia', src);
12367 return wrapEl;
12368 }
12369
12370 // Expand videos: WEBM, MP4
12371 // FIXME: handle null size videos
12372 const isWebm = origSrc.split('.').pop() === 'webm';
12373 const needTitle = isWebm && Cfg.webmTitles;
12374 let inPostSize = '';
12375 if(inPost) {
12376 const [width, height] = this.computeFullSize();
12377 inPostSize = ` style="width: ${ width }px; height: ${ height }px;"`;
12378 }
12379 const hasTitle = needTitle && this.el.hasAttribute('de-metatitle');
12380 const title = hasTitle ? this.el.getAttribute('de-metatitle') : '';
12381 wrapEl = $add(`<div class="de-fullimg-wrap${ wrapClass }"${ inPostSize }>${
12382 nav.firefoxVer < 59 ? '' : '<div class="de-fullimg-video-hack"></div>' }
12383 <video src="${ src }" ` +
12384 `${ hasTitle && title ? `title="${ title }" ` : '' }loop autoplay ` +
12385 `${ Cfg.webmControl ? 'controls ' : '' }` +
12386 `${ Cfg.webmVolume === 0 ? 'muted ' : '' }></video>
12387 <div class="de-fullimg-info">
12388 ${ imgNameEl }${ hasTitle && title ? ` - ${ title }` : '' }</a>
12389 ${ needTitle && !hasTitle ? `<svg class="de-wait">
12390 <use xlink:href="#de-symbol-wait"/></svg>` : '' }
12391 </div>
12392 </div>`);
12393 const videoEl = $q('video', wrapEl);
12394 videoEl.volume = Cfg.webmVolume / 100;
12395 videoEl.addEventListener('ended', () => AttachedImage.viewer.navigate(true, true));
12396 videoEl.addEventListener('error', ({ target: el }) => {
12397 if(!el.onceLoaded) {
12398 el.load();
12399 el.onceLoaded = true;
12400 }
12401 });
12402 if(!this._size) {
12403 videoEl.addEventListener('loadedmetadata', ({ target: el }) => {
12404 this._size = [el.videoWidth, el.videoHeight];
12405 onsizechange(wrapEl);
12406 });
12407 }
12408 // Sync webm volume on all browser tabs
12409 setTimeout(() => videoEl.dispatchEvent(new CustomEvent('volumechange')), 150);
12410 videoEl.addEventListener('volumechange', ({ target: el, isTrusted }) => {
12411 const val = el.muted ? 0 : Math.round(el.volume * 100);
12412 if(isTrusted && val !== Cfg.webmVolume) {
12413 saveCfg('webmVolume', val);
12414 sendStorageEvent('__de-webmvolume', val);
12415 }
12416 });
12417 // MS Edge needs an external app with DollchanAPI to play webms
12418 if(nav.isMsEdge && isWebm && !DollchanAPI.hasListener('expandmedia')) {
12419 const href = 'https://github.com/Kagami/webmify/';
12420 $popup('err-expandmedia', `${ Lng.errMsEdgeWebm[lang] }:\n<a href="${
12421 href }" target="_blank">${ href }</a>`, false);
12422 }
12423 // Get webm title: load file and parse its metadata
12424 if(needTitle && !hasTitle) {
12425 this._webmTitleLoad = ContentLoader.loadImgData(videoEl.src, false).then(data => {
12426 $hide($q('.de-wait', wrapEl));
12427 if(!data) {
12428 return;
12429 }
12430 let str = '', d = (new WebmParser(data.buffer)).getWebmData();
12431 if(!d) {
12432 return;
12433 }
12434 d = d[0];
12435 for(let i = 0, len = d.length; i < len; ++i) {
12436 // Segment Info = 0x1549A966, segment title = 0x7BA9[length | 0x80]
12437 if(d[i] === 0x49 && d[i + 1] === 0xA9 && d[i + 2] === 0x66 &&
12438 d[i + 18] === 0x7B && d[i + 19] === 0xA9
12439 ) {
12440 i += 20;
12441 for(let end = (d[i++] & 0x7F) + i; i < end; ++i) {
12442 str += String.fromCharCode(d[i]);
12443 }
12444 break;
12445 }
12446 }
12447 const loadedTitle = decodeURIComponent(escape(str));
12448 this.el.setAttribute('de-metatitle', loadedTitle);
12449 if(str) {
12450 $q('.de-fullimg-link', wrapEl).textContent +=
12451 ` - ${ videoEl.title = loadedTitle.replace(/\./g, ' ') }`;
12452 }
12453 });
12454 }
12455 DollchanAPI.notify('expandmedia', src);
12456 return wrapEl;
12457 }
12458 sendCloseEvent(e, inPost) {
12459 let { post } = this;
12460 let cr = post.el.getBoundingClientRect();
12461 const x = e.pageX - deWindow.pageXOffset;
12462 const y = e.pageY - deWindow.pageYOffset;
12463 if(!inPost) {
12464 while(x > cr.right || x < cr.left || y > cr.bottom || y < cr.top) {
12465 post = post.parent;
12466 if(post && (post instanceof Pview)) {
12467 cr = post.el.getBoundingClientRect();
12468 } else {
12469 if(Pview.top) {
12470 Pview.top.markToDel();
12471 }
12472 return;
12473 }
12474 }
12475 post.mouseEnter();
12476 } else if(x > cr.right || y > cr.bottom && Pview.top) {
12477 Pview.top.markToDel();
12478 }
12479 }
12480 srcBtnEvents({ _fullEl }) {
12481 if(!Cfg.imgSrcBtns) {
12482 return;
12483 }
12484 const srcBtnEl = $q('.de-btn-src', _fullEl);
12485 srcBtnEl.addEventListener('mouseover', () => (srcBtnEl.odelay = setTimeout(() => {
12486 const menuHtml = !this.isVideo ? Menu.getMenuImgSrc(srcBtnEl) :
12487 `<span class="de-menu-item">${ Lng.getFrameLinks[lang] }</span>`;
12488 new Menu(srcBtnEl, menuHtml, !this.isVideo ? emptyFn : optiontEl => {
12489 ContentLoader.getDataFromImg($q('video', _fullEl)).then(arr => {
12490 $popup('upload', Lng.sending[lang], true);
12491 const name = this.name.substring(0, this.name.lastIndexOf('.')) + '.png';
12492 const blob = new Blob([arr], { type: 'image/png' });
12493 let formData;
12494 if(!nav.isChrome || nav.scriptHandler !== 'WebExtension') {
12495 formData = new FormData();
12496 formData.append('file', blob, name);
12497 }
12498 const ajaxParams = { data: formData || { arr, name }, method: 'POST' };
12499 const frameLinkHtml = `<a class="de-menu-item de-list" href="${
12500 deWindow.URL.createObjectURL(blob) }" download="${ name }" target="_blank">${
12501 Lng.saveFrame[lang] }</a>`;
12502 $ajax('https://tmp.saucenao.com/', ajaxParams, true).then(xhr => {
12503 let hostUrl, errMsg = Lng.errSaucenao[lang];
12504 try {
12505 const res = JSON.parse(xhr.responseText);
12506 if(res.status === 'success') {
12507 hostUrl = res.url ? Menu.getMenuImgSrc(res.url) : '';
12508 } else {
12509 errMsg += ':<br>' + res.error_message;
12510 }
12511 } catch(e) {}
12512 $popup('upload', (hostUrl || errMsg) + frameLinkHtml);
12513 }, () => $popup('upload', Lng.errSaucenao[lang] + frameLinkHtml));
12514 }, emptyFn);
12515 });
12516 }, Cfg.linksOver)));
12517 srcBtnEl.addEventListener('mouseout', e => clearTimeout(e.target.odelay));
12518 }
12519
12520 get _size() {
12521 const value = this._getImageSize();
12522 Object.defineProperty(this, '_size', { value, writable: true });
12523 return value;
12524 }
12525}
12526
12527// Initialization of embedded image that added to the link in post message
12528class EmbeddedImage extends ExpandableImage {
12529 get _getImageParent() {
12530 const value = this.el.parentNode;
12531 Object.defineProperty(this, '_getImageParent', { value });
12532 return value;
12533 }
12534 _getImageSize() {
12535 return [this.el.naturalWidth, this.el.naturalHeight];
12536 }
12537 _getImageSrc() {
12538 return this.el.src;
12539 }
12540}
12541
12542// Initialization of image/video that attached to the post
12543class AttachedImage extends ExpandableImage {
12544 static closeImg() {
12545 const { viewer } = AttachedImage;
12546 if(viewer) {
12547 viewer.closeImgViewer(null);
12548 AttachedImage.viewer = null;
12549 }
12550 }
12551 get info() {
12552 const value = aib.getImgInfo(this._getImageParent);
12553 Object.defineProperty(this, 'info', { value });
12554 return value;
12555 }
12556 get name() {
12557 const value = aib.getImgRealName(this._getImageParent).trim();
12558 Object.defineProperty(this, 'name', { value });
12559 return value;
12560 }
12561 get nameLink() {
12562 const value = $q(aib.qImgNameLink, this._getImageParent);
12563 Object.defineProperty(this, 'nameLink', { value });
12564 return value;
12565 }
12566 get weight() {
12567 let value = 0;
12568 if(this.info) {
12569 const w = this.info.match(/(\d+(?:[.,]\d+)?)\s*([mмkк])?i?[bб]/i);
12570 const w1 = w[1].replace(',', '.');
12571 value = w[2] === 'M' ? (w1 * 1e3) | 0 : !w[2] ? Math.round(w1 / 1e3) : w1;
12572 }
12573 Object.defineProperty(this, 'weight', { value });
12574 return value;
12575 }
12576
12577 get _getImageParent() {
12578 const value = aib.getImgWrap(this.el);
12579 Object.defineProperty(this, '_getImageParent', { value });
12580 return value;
12581 }
12582 _getImageSize() {
12583 if(this.info) {
12584 const size = this.info.match(/(?:[\s(]|^)(\d+)\s?[x\u00D7]\s?(\d+)(?:[)\s,]|$)/);
12585 return size ? [size[1], size[2]] : null;
12586 }
12587 return null;
12588 }
12589 _getImageSrc() {
12590 // XXX: DON'T USE aib.getImgSrcLink(this.el).href
12591 // If #ihash spells enabled, Chrome reads href in ajaxed posts as empty -> image can't be expanded!
12592 return aib.getImgSrcLink(this.el).getAttribute('href');
12593 }
12594}
12595AttachedImage.viewer = null;
12596
12597// A class that finds a set of images in a post
12598class PostImages {
12599 constructor(post) {
12600 let first = null, last = null, els = $Q(aib.qPostImg, post.el);
12601 let hasAttachments = false;
12602 const filesMap = new Map();
12603 for(let i = 0, len = els.length; i < len; ++i) {
12604 const el = els[i];
12605 last = new AttachedImage(post, el, last);
12606 filesMap.set(el, last);
12607 hasAttachments = true;
12608 if(!first) {
12609 first = last;
12610 }
12611 }
12612 if(Cfg.addImgs || localData) {
12613 els = $Q('.de-img-embed', post.el);
12614 for(let i = 0, len = els.length; i < len; ++i) {
12615 const el = els[i];
12616 last = new EmbeddedImage(post, el, last);
12617 filesMap.set(el, last);
12618 if(!first) {
12619 first = last;
12620 }
12621 }
12622 }
12623 this.first = first;
12624 this.last = last;
12625 this.hasAttachments = hasAttachments;
12626 this._map = filesMap;
12627 }
12628 get expanded() {
12629 for(let img = this.first; img; img = img.next) {
12630 if(img.expanded) {
12631 return true;
12632 }
12633 }
12634 return false;
12635 }
12636 get firstAttach() {
12637 return this.hasAttachments ? this.first : null;
12638 }
12639 getImageByEl(el) {
12640 return this._map.get(el);
12641 }
12642 [Symbol.iterator]() {
12643 return {
12644 _img: this.first,
12645 next() {
12646 const value = this._img;
12647 if(value) {
12648 this._img = value.next;
12649 return { value, done: false };
12650 }
12651 return { done: true };
12652 }
12653 };
12654 }
12655}
12656
12657const ImagesHashStorage = Object.create({
12658 get getHash() {
12659 const value = this._getHashHelper.bind(this);
12660 Object.defineProperty(this, 'getHash', { value });
12661 return value;
12662 },
12663 endFn() {
12664 if(this.hasOwnProperty('_storage')) {
12665 sesStorage['de-imageshash'] = JSON.stringify(this._storage);
12666 }
12667 if(this.hasOwnProperty('_workers')) {
12668 this._workers.clearWorkers();
12669 delete this._workers;
12670 }
12671 },
12672
12673 get _canvas() {
12674 const value = doc.createElement('canvas');
12675 Object.defineProperty(this, '_canvas', { value });
12676 return value;
12677 },
12678 get _storage() {
12679 let value = null;
12680 try {
12681 value = JSON.parse(sesStorage['de-imageshash']);
12682 } finally {
12683 if(!value) {
12684 value = {};
12685 }
12686 Object.defineProperty(this, '_storage', { value });
12687 return value;
12688 }
12689 },
12690 get _workers() {
12691 const value = new WorkerPool(4, this._genImgHash, emptyFn);
12692 Object.defineProperty(this, '_workers', { value, configurable: true });
12693 return value;
12694 },
12695 _genImgHash: ([arrBuf, oldw, oldh]) => {
12696 const buf = new Uint8Array(arrBuf);
12697 const size = oldw * oldh;
12698 for(let i = 0, j = 0; i < size; i++, j += 4) {
12699 buf[i] = buf[j] * 0.3 + buf[j + 1] * 0.59 + buf[j + 2] * 0.11;
12700 }
12701 const newh = 8;
12702 const neww = 8;
12703 const levels = 3;
12704 const areas = 256 / levels;
12705 const values = 256 / (levels - 1);
12706 let hash = 0;
12707 for(let i = 0; i < newh; ++i) {
12708 for(let j = 0; j < neww; ++j) {
12709 let temp = i / (newh - 1) * (oldh - 1);
12710 const l = Math.min(temp | 0, oldh - 2);
12711 const u = temp - l;
12712 temp = j / (neww - 1) * (oldw - 1);
12713 const c = Math.min(temp | 0, oldw - 2);
12714 const t = temp - c;
12715 hash = (hash << 4) + Math.min(values * (((buf[l * oldw + c] * ((1 - t) * (1 - u)) +
12716 buf[l * oldw + c + 1] * (t * (1 - u)) +
12717 buf[(l + 1) * oldw + c + 1] * (t * u) +
12718 buf[(l + 1) * oldw + c] * ((1 - t) * u)) / areas) | 0), 255);
12719 const g = hash & 0xF0000000;
12720 if(g) {
12721 hash ^= g >>> 24;
12722 }
12723 hash &= ~g;
12724 }
12725 }
12726 return { hash };
12727 },
12728 async _getHashHelper({ el, src }) {
12729 if(src in this._storage) {
12730 return this._storage[src];
12731 }
12732 if(!el.complete) {
12733 await new Promise(resolve => el.addEventListener('load', () => resolve()));
12734 }
12735 if(el.naturalWidth + el.naturalHeight === 0) {
12736 return -1;
12737 }
12738 let data, buffer, val = -1;
12739 const { naturalWidth: w, naturalHeight: h } = el;
12740 if(aib._4chan) {
12741 const imgData = await ContentLoader.loadImgData(el.src);
12742 if(imgData) {
12743 ({ buffer } = imgData);
12744 }
12745 } else {
12746 const cnv = this._canvas;
12747 cnv.width = w;
12748 cnv.height = h;
12749 const ctx = cnv.getContext('2d');
12750 ctx.drawImage(el, 0, 0);
12751 ({ buffer } = ctx.getImageData(0, 0, w, h).data);
12752 }
12753 if(buffer) {
12754 data = await new Promise(resolve =>
12755 this._workers.runWorker([buffer, w, h], [buffer], val => resolve(val)));
12756 if(data && ('hash' in data)) {
12757 val = data.hash;
12758 }
12759 }
12760 this._storage[src] = val;
12761 return val;
12762 }
12763});
12764
12765function addImgSrcButtons(link, src) {
12766 link.insertAdjacentHTML('beforebegin', `<svg class="de-btn-src"${
12767 src ? ` de-href="${ src }"` : '' }><use xlink:href="#de-symbol-post-src"/></svg>`);
12768}
12769
12770// Adding features for info links of images
12771function processImgInfoLinks(parent, addSrc = Cfg.imgSrcBtns, imgNames = Cfg.imgNames) {
12772 if(addSrc || imgNames) {
12773 if(parent instanceof AbstractPost) {
12774 processPostImgInfoLinks(parent, addSrc, imgNames);
12775 } else {
12776 const posts = $Q(aib.qRPost + ', ' + aib.qOPost + ', .de-oppost', parent);
12777 for(let i = 0, len = posts.length; i < len; ++i) {
12778 processPostImgInfoLinks(pByEl.get(posts[i]), addSrc, imgNames);
12779 }
12780 }
12781 }
12782}
12783
12784function processPostImgInfoLinks(post, addSrc, imgNames) {
12785 if(!post) {
12786 return;
12787 }
12788 for(const image of post.images) {
12789 const link = image.nameLink;
12790 if(!link) {
12791 return;
12792 }
12793 if(addSrc) {
12794 addImgSrcButtons(link, image.isVideo ? image.el.src : null);
12795 }
12796 const { name } = image;
12797 if(!link.classList.contains('de-img-name')) {
12798 link.classList.add('de-img-name');
12799 link.title = name;
12800 link.setAttribute('download', name);
12801 link.setAttribute('de-href', link.href);
12802 }
12803 if(imgNames) {
12804 let ext;
12805 if(!(ext = link.getAttribute('de-img-ext'))) {
12806 ext = name.split('.').pop() || link.href.split('/').pop().split('.').pop();
12807 link.setAttribute('de-img-ext', ext);
12808 link.setAttribute('de-img-name-old', link.textContent);
12809 }
12810 link.textContent = imgNames === 2 ? ext : name;
12811 }
12812 }
12813}
12814
12815// Adding image previews before links in post message
12816function embedPostMsgImages(el) {
12817 if(!Cfg.addImgs || localData) {
12818 return;
12819 }
12820 const els = $Q(aib.qMsgImgLink, el);
12821 for(let i = 0, len = els.length; i < len; ++i) {
12822 const link = els[i];
12823 const url = link.href;
12824 if(url.includes('?') || aib.getPostOfEl(link).hidden) {
12825 continue;
12826 }
12827 $bBegin(link, `<a href="${
12828 link.href }" target="_blank"><img class="de-img-embed" src="${ url }"></a><br>`);
12829 if(Cfg.imgSrcBtns) {
12830 addImgSrcButtons(link);
12831 }
12832 }
12833}
12834
12835/* ==[ PostBuilders.js ]======================================================================================
12836 BUILDERS FOR LOADED POSTS
12837=========================================================================================================== */
12838
12839class DOMPostsBuilder {
12840 constructor(form, isArchived) {
12841 this._form = form;
12842 this._posts = $Q(aib.qRPost, form);
12843 this.length = this._posts.length;
12844 this.postersCount = '';
12845 this._isArchived = isArchived;
12846 }
12847 get isClosed() {
12848 return aib.qClosed && !!$q(aib.qClosed, this._form) || this._isArchived;
12849 }
12850 getOpMessage() {
12851 return aib.fixHTML(doc.adoptNode($q(aib.qPostMsg, this._form)));
12852 }
12853 getPNum(i) {
12854 return aib.getPNum(this._posts[i]);
12855 }
12856 getOpEl() {
12857 return aib.fixHTML(aib.getOp($q(aib.qThread, this._form) || this._form));
12858 }
12859 getPostEl(i) {
12860 return aib.fixHTML(this._posts[i]);
12861 }
12862 * getRefLinks(i, thrUrl) { // i === 0 - OP-post
12863 const msg = i === 0 ? $q(aib.qPostMsg, this._form) : $q(aib.qPostMsg, this._posts[i - 1]);
12864 const links = $Q('a', msg);
12865 for(let i = 0, len = links.length; i < len; ++i) {
12866 const link = links[i];
12867 const tc = link.textContent;
12868 if(tc[0] === '>' && tc[1] === '>') {
12869 const lNum = parseInt(tc.substr(2), 10);
12870 if(lNum) {
12871 yield [link, lNum];
12872 const url = link.getAttribute('href');
12873 if(url[0] === '#') {
12874 link.setAttribute('href', thrUrl + url);
12875 }
12876 }
12877 }
12878 }
12879 }
12880 * bannedPostsData() {
12881 const banEls = $Q(aib.qBan, this._form);
12882 for(let i = 0, len = banEls.length; i < len; ++i) {
12883 const banEl = banEls[i];
12884 const postEl = aib.getPostElOfEl(banEl);
12885 yield [1, postEl ? aib.getPNum(postEl) : null, doc.adoptNode(banEl)];
12886 }
12887 }
12888}
12889
12890class _4chanPostsBuilder {
12891 constructor(json, brd) {
12892 this._posts = json.posts;
12893 this._brd = brd;
12894 this.length = json.posts.length - 1;
12895 this.postersCount = this._posts[0].unique_ips;
12896 }
12897 static fixFileName(name, maxLength) {
12898 const decodedName = name.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'")
12899 .replace(/</g, '<').replace(/>/g, '>');
12900 return decodedName.length <= maxLength ? { isFixed: false, name } : {
12901 isFixed : true,
12902 name : decodedName.slice(0, 25).replace(/&/g, '&').replace(/"/g, '"')
12903 .replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>')
12904 };
12905 }
12906 get isClosed() {
12907 return !!(this._posts[0].closed || this._posts[0].archived);
12908 }
12909 getOpMessage() {
12910 const { no, com } = this._posts[0];
12911 return $add(aib.fixHTML(`<blockquote class="postMessage" id="m${ no }"> ${ com }</blockquote>`));
12912 }
12913 getPNum(i) {
12914 return this._posts[i + 1].no;
12915 }
12916 getOpEl() {
12917 return this.getPostEl(-1);
12918 }
12919 getPostEl(i) {
12920 return $add(aib.fixHTML(this.getPostHTML(i))).lastElementChild;
12921 }
12922 getPostHTML(i) {
12923 const data = this._posts[i + 1];
12924 const num = data.no;
12925 const brd = this._brd;
12926 const _icon = id => `//s.4cdn.org/image/${ id }${
12927 deWindow.devicePixelRatio < 2 ? '.gif' : '@2x.gif' }`;
12928
12929 // --- FILE ---
12930 let fileHTML = '';
12931 if(data.filedeleted) {
12932 fileHTML = `<div id="f${ num }" class="file"><span class="fileThumb">
12933 <img src="${ _icon('filedeleted-res') }" class="fileDeletedRes" alt="File deleted.">
12934 </span></div>`;
12935 } else if(typeof data.filename === 'string') {
12936 let { name, isFixed: needTitle } = _4chanPostsBuilder.fixFileName(data.filename, 30);
12937 name += data.ext;
12938 if(!data.tn_w && !data.tn_h && data.ext === '.gif') {
12939 data.tn_w = data.w;
12940 data.tn_h = data.h;
12941 }
12942 const isSpoiler = data.spoiler;
12943 if(isSpoiler) {
12944 name = 'Spoiler Image';
12945 data.tn_w = data.tn_h = 100;
12946 needTitle = false;
12947 }
12948 const size = prettifySize(data.fsize);
12949 const fileTextTitle = isSpoiler ? ` title="${ data.filename + data.ext }"` : '';
12950 const aHref = needTitle ? `title="${ data.filename + data.ext }"` : '';
12951 const imgSrc = isSpoiler ? '//s.4cdn.org/image/spoiler.png' :
12952 `//i.4cdn.org/${ brd }/${ data.tim }s.jpg`;
12953 fileHTML = `<div class="file" id="f${ num }">
12954 <div class="fileText" id="fT${ num }"${ fileTextTitle }>File:
12955 <a href="//i.4cdn.org/${ brd }/${ data.tim +
12956 data.ext }" ${ aHref } target="_blank">${ name }</a>
12957 (${ size }, ${ data.ext === '.pdf' ? 'PDF' : data.w + 'x' + data.h })
12958 </div>
12959 <a class="fileThumb ${ isSpoiler ? 'imgspoiler' : '' }" href="//i.4cdn.org/${ brd }/` +
12960 `${ data.tim + data.ext }" target="_blank">
12961 <img src="${ imgSrc }" alt="${ size }" data-md5="` +
12962 `${ data.md5 }" style="height: ${ data.tn_h }px; width: ${ data.tn_w }px;">
12963 <div data-tip="" data-tip-cb="mShowFull" class="mFileInfo mobile">
12964 ${ size } ${ data.ext.substr(1).toUpperCase() }
12965 </div>
12966 </a>
12967 </div>`;
12968 }
12969
12970 // --- CAPCODE ---
12971 let highlight = '', ccBy = '';
12972 let cc = data.capcode;
12973 switch(cc) {
12974 case 'admin_highlight':
12975 highlight = ' highlightPost';
12976 cc = 'admin';
12977 /* falls through */
12978 case 'admin': ccBy = 'Administrators'; break;
12979 case 'mod': ccBy = 'Moderators'; break;
12980 case 'developer': ccBy = 'Developers'; break;
12981 case 'manager': ccBy = 'Managers'; break;
12982 case 'founder': ccBy = 'Founder';
12983 }
12984 let ccName = '', ccText = '', ccImg = '', ccClass = '';
12985 if(cc) {
12986 ccName = cc[0].toUpperCase() + cc.slice(1);
12987 ccText = `<strong class="capcode hand id_${ cc === 'founder' ? 'admin' : cc }` +
12988 `" title="Highlight posts by ${ ccBy }">## ${ ccName }</strong>`;
12989 ccImg = `<img src="${ _icon(cc + 'icon') }" alt="${
12990 ccName } Icon." title="This user is 4chan ${ ccName }." class="identityIcon">`;
12991 ccClass = 'capcode' + (cc === 'founder' ? 'Admin' : ccName);
12992 }
12993
12994 // --- POST ---
12995 const { name = '' } = data;
12996 const nameEl = `<span class="name">${ name }</span>`;
12997 const mobNameEl = name.length <= 30 ? nameEl :
12998 `<span class="name" data-tip data-tip-cb="mShowFull">${ name.substring(30) }(…)</span>`;
12999 const tripEl = `${ data.trip ? `<span class="postertrip">${ data.trip }</span>` : '' }`;
13000 const posteruidEl = data.id && !data.capcode ? `<span class="posteruid id_${ data.id }` +
13001 `">(ID: <span class="hand" title="Highlight posts by this ID">${ data.id }</span>)</span>` : '';
13002 const flagEl = data.country ? `<span title="${ data.country_name }" class="flag flag-${
13003 data.country.toLowerCase() }"></span>` : '';
13004 const emailEl = data.email ? `<a href="mailto:${
13005 data.email.replace(/ /g, '%20') }" class="useremail">` : '';
13006 const replyEl = `<a href="#p${ num }" title="Link to this post">No.</a><a href="javascript:quote('${
13007 num }');" title="Reply to this post">${ num }</a>`;
13008 const subjEl = `<span class="subject">${ data.sub || '' }</span>`;
13009 return `<div class="postContainer replyContainer" id="pc${ num }">
13010 <div class="sideArrows" id="sa${ num }">>></div>
13011 <div id="p${ num }" class="post ${ i === -1 ? 'op' : 'reply' } ${ highlight }">
13012 <div class="postInfoM mobile" id="pim${ num }">
13013 <span class="nameBlock ${ ccClass }">
13014 ${ mobNameEl }
13015 ${ tripEl }
13016 ${ ccText }
13017 ${ ccImg }
13018 ${ posteruidEl }
13019 ${ flagEl }<br>
13020 ${ subjEl }
13021 </span>
13022 <span class="dateTime postNum" data-utc="${ data.time }">${ data.now } ${ replyEl }</span>
13023 </div>
13024 <div class="postInfo desktop" id="pi${ num }">
13025 <input name="${ num }" value="delete" type="checkbox">
13026 ${ subjEl }
13027 <span class="nameBlock ${ ccClass }">
13028 ${ emailEl }
13029 ${ nameEl }
13030 ${ tripEl }
13031 ${ ccText }
13032 ${ data.email ? '</a>' : '' }
13033 ${ ccImg }
13034 ${ posteruidEl }
13035 ${ flagEl }
13036 </span>
13037 <span class="dateTime" data-utc="${ data.time }">${ data.now }</span>
13038 <span class="postNum desktop">${ replyEl }</span>
13039 </div>
13040 ${ fileHTML }
13041 <blockquote class="postMessage" id="m${ num }"> ${ data.com || '' }</blockquote>
13042 </div>
13043 </div>`;
13044 }
13045 * bannedPostsData() {}
13046}
13047_4chanPostsBuilder._customSpoiler = new Map();
13048
13049class DobrochanPostsBuilder {
13050 constructor(json, brd) {
13051 if(json.error) {
13052 throw new AjaxError(0, `API error: ${ json.error.message }`);
13053 }
13054 this._json = json.result;
13055 this._brd = brd;
13056 this._posts = json.result.threads[0].posts;
13057 this.length = this._posts.length - 1;
13058 this.postersCount = '';
13059 }
13060 get isClosed() {
13061 return !!this._json.threads[0].archived;
13062 }
13063 getOpMessage() {
13064 return $add(aib.fixHTML(`<div class="postbody"> ${ this._posts[0].message_html }</div>`));
13065 }
13066 getPNum(i) {
13067 return this._posts[i + 1].display_id;
13068 }
13069 getOpEl() {
13070 return this.getPostEl(-1);
13071 }
13072 getPostEl(i) {
13073 const el = $add(aib.fixHTML(this.getPostHTML(i)));
13074 if(i === -1) {
13075 return el;
13076 }
13077 return el.firstElementChild.firstElementChild.lastElementChild;
13078 }
13079 getPostHTML(i) {
13080 const data = this._posts[i + 1];
13081 const num = data.display_id;
13082 const brd = this._brd;
13083 const multiFile = data.files.length > 1;
13084
13085 // --- FILE ---
13086 let filesHTML = '';
13087 for(const { file_id, metadata, rating, size, src, thumb, thumb_height, thumb_width } of data.files) {
13088 let fileName, fullFileName, th = thumb;
13089 let thumbW = 200;
13090 let thumbH = 200;
13091 const ext = src.split('.').pop();
13092 if(brd === 'b' || brd === 'rf') {
13093 fileName = fullFileName = th.split('/').pop();
13094 } else {
13095 fileName = fullFileName = src.split('/').pop();
13096 if(multiFile && fileName.length > 20) {
13097 fileName = fileName.substr(0, 20 - ext.length) + '(…)' + ext;
13098 }
13099 }
13100 const maxRating = 'r15'; // FIXME: read from settings
13101 if(rating === 'r-18g' && maxRating !== 'r-18g') {
13102 th = 'images/r-18g.png';
13103 } else if(rating === 'r-18' && (maxRating !== 'r-18g' || maxRating !== 'r-18')) {
13104 th = 'images/r-18.png';
13105 } else if(rating === 'r-15' && maxRating === 'sfw') {
13106 th = 'images/r-15.png';
13107 } else if(rating === 'illegal') {
13108 th = 'images/illegal.png';
13109 } else {
13110 thumbW = thumb_width;
13111 thumbH = thumb_height;
13112 }
13113 const fileInfo = `<div class="fileinfo${ multiFile ? ' limited' : '' }">Файл:
13114 <a href="/${ src }" title="${ fullFileName }" target="_blank">${ fileName }</a><br>
13115 <em>${ ext }, ${ prettifySize(size) }, ${ metadata.width }x${ metadata.height }
13116 </em>${ multiFile ? '' : ' - Нажмите на картинку для увеличения' }<br>
13117 <a class="edit_ icon" href="/utils/image/edit/${ file_id }/${ num }">
13118 <img title="edit" alt="edit" src="/images/blank.png">
13119 </a>
13120 </div>`;
13121 filesHTML += `${ multiFile ? '' : fileInfo }
13122 <div id="file_${ num }_${ file_id }" class="file">${ multiFile ? fileInfo : '' }
13123 <a href="/${ src }" target="_blank">
13124 <img class="thumb" src="/${ th }" width="${ thumbW }" height="${ thumbH }">
13125 </a>
13126 </div>`;
13127 }
13128
13129 // --- POST ---
13130 const date = data.date.replace(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/,
13131 (all, y, mo, d, h, m, s) => {
13132 const dt = new Date(y, +mo - 1, d, h, m, s);
13133 return `${ pad2(dt.getDate()) } ${ Lng.fullMonth[1][dt.getMonth()] } ${ dt.getFullYear()
13134 } (${ Lng.week[1][dt.getDay()] }) ${ pad2(dt.getHours()) }:${ pad2(dt.getMinutes()) }`;
13135 });
13136 const isOp = i === -1;
13137 return `${ isOp ? `<div id="post_${ num }" class="oppost post">` :
13138 `<table id="post_${ num }" class="replypost post"><tbody><tr>
13139 <td class="doubledash">>></td>
13140 <td class="reply" id="reply${ num }">` }
13141 <a name="i${ num }"></a>
13142 <label>
13143 <input name="${ num }" value="${ data.thread_id }" ` +
13144 `class="delete_checkbox" id="delbox_${ num }" type="checkbox">
13145 ${ data.subject ? `<span class="replytitle">${ data.subject }</span>` : '' }
13146 <span class="postername">${ data.name || 'Анонимус' }</span> ${ date }
13147 </label>
13148 <span class="reflink">
13149 <a href="/${ brd }/res/${ data.thread_id }.xhtml#i${ num }"> No.${ num }</a>
13150 </span><br>
13151 ${ filesHTML }
13152 ${ multiFile ? '<div style="clear: both;"></div>' : '' }
13153 <div class="postbody"> ${ data.message_html }</div>
13154 ${ isOp ? '</div>' : '</td></tr></tbody></table>' }`;
13155 }
13156 * bannedPostsData() {}
13157}
13158
13159class MakabaPostsBuilder {
13160 constructor(json, brd) {
13161 if(json.Error) {
13162 throw new AjaxError(0, `API error: ${ json.Error } (${ json.Code })`);
13163 }
13164 this._json = json;
13165 this._brd = brd;
13166 this._posts = json.threads[0].posts;
13167 this.length = aib._2channel ? json.counter_posts - 1 : json.posts_count;
13168 this.postersCount = json.unique_posters;
13169 }
13170 get isClosed() {
13171 return this._json.is_closed;
13172 }
13173 getOpMessage() {
13174 return $add(aib.fixHTML(this._getPostMsg(this._posts[0])));
13175 }
13176 getPNum(i) {
13177 return this._posts[i + 1].num;
13178 }
13179 getOpEl() {
13180 return this.getPostEl(-1);
13181 }
13182 getPostEl(i) {
13183 return $add(aib.fixHTML(this.getPostHTML(i))).firstElementChild;
13184 }
13185 getPostHTML(i) {
13186 const data = this._posts[i + 1];
13187 const { num } = data;
13188 const brd = this._brd;
13189 const isNew = this._isNew;
13190 const p = isNew ? 'post__' : '';
13191 const _switch = (val, obj) => val in obj ? obj[val] : obj['@@default'];
13192
13193 // --- FILE ---
13194 let filesHTML = '';
13195 if(data.files && data.files.length !== 0) {
13196 filesHTML = `<div class="${ isNew ? 'post__images post__images_type_' : 'images images-' }${
13197 data.files.length === 1 ? 'single' : 'multi' }">`;
13198 for(const file of data.files) {
13199 const imgId = num + '-' + file.md5;
13200 const { fullname = file.name, displayname: dispName = file.name } = file;
13201 const isVideo = file.type === 6 || file.type === 10;
13202 const imgClass = isNew ?
13203 `post__file-preview${ isVideo ? ' post__file-webm' : '' }${
13204 data.nsfw ? ' post__file-nsfw' : '' }` :
13205 `img preview${ isVideo ? ' webm-file' : '' }`;
13206 filesHTML += `<figure class="${ p }image">
13207 <figcaption class="${ p }file-attr">
13208 <a id="title-${ imgId }" class="desktop" target="_blank" href="` +
13209 `${ file.type === 100 /* is sticker */ ? file.install : file.path }"` +
13210 `${ dispName === fullname ? '' : ` title="${ fullname }"` }>${ dispName }</a>
13211 <span class="${ isNew ? 'post__filezise' : 'filesize' }">(${ file.size }Кб, ` +
13212 `${ file.width }x${ file.height }${ isVideo ? ', ' + file.duration : '' })</span>
13213 </figcaption>
13214 <div id="exlink-${ imgId }"${ isNew ? '' : 'class="image-link"' }>
13215 <a ${ isNew ? 'class="post__image-link" ' : '' }href="${ file.path }">
13216 <img class="${ imgClass }" src="${ file.thumbnail }" alt="${ file.width }x` +
13217 `${ file.height }" width="${ file.tn_width }" height="${ file.tn_height }">
13218 </a>
13219 </div>
13220 </figure>`;
13221 }
13222 filesHTML += '</div>';
13223 }
13224
13225 // --- POST ---
13226 const emailEl = data.email ?
13227 `<a href="${ data.email }" class="${ isNew ? 'post__' : 'post-' }email">${ data.name }</a>` :
13228 `<span class="${ isNew ? 'post__anon' : 'ananimas' }">${ data.name }</span>`;
13229 const tripEl = !data.trip ? '' : `<span class="${ _switch(data.trip, {
13230 '!!%adm%!!' : `${ p }adm">## ${ aib._2channel ? 'Admin' : 'Abu' } ##`,
13231 '!!%mod%!!' : `${ p }mod">## Mod ##`,
13232 '!!%Inquisitor%!!' : `${ p }inquisitor">## Applejack ##`,
13233 '!!%coder%!!' : `${ p }mod">## Кодер ##`,
13234 '!!%curunir%!!' : `${ p }mod">## Curunir ##`,
13235 '@@default' :
13236 `${ data.trip_style ? data.trip_style : isNew ? 'post__trip' : 'postertrip' }">` + data.trip
13237 }) }</span>`;
13238 const refHref = `/${ brd }/res/${ parseInt(data.parent) || num }.html#${ num }`;
13239 let rate = '';
13240 if(this._hasLikes) {
13241 const likes = `<div id="like-div${ num }" class="${ isNew ?
13242 `post__detailpart post__rate post__rate_type_like" title="Мне это нравится">
13243 <svg xmlns="http://www.w3.org/2000/svg" class="post__rate-icon icon">
13244 <use xlink:href="#icon__thunder"></use></svg>` :
13245 'like-div"> <span class="like-icon"> <i class="fa fa-bolt"></i></span>'
13246 } <span id="like-count${ num }"${ isNew ? '' : 'class="like-count"' }>`;
13247 const dislikes = likes.replace(/like/g, 'dislike').replace('icon__thunder', 'icon__thumbdown');
13248 rate = `${ likes }${ data.likes || 0 }</span></div>
13249 ${ dislikes }${ data.dislikes || 0 }</span></div>`;
13250 }
13251 const isOp = i === -1;
13252 const wrapClass = !isNew ? 'post-wrapper' : isOp ? 'thread__oppost' : 'thread__post';
13253 const timeReflink = `<span class="${ isNew ? 'post__time' : 'posttime' }">${ data.date }</span>
13254 <span class="${ isNew ? 'post__detailpart' : 'reflink' }">` +
13255 `<a id="${ num }" ${ isNew ? 'class="post__reflink" ' : '' }href="${ refHref }">` +
13256 `${ aib._2channel ? 'No.' : '№' }</a>` +
13257 `<a class="${ isNew ? 'post__reflink ' : '' }postbtn-reply-href" href="${ refHref }"` +
13258 ` name="${ num }">${ num }</a>
13259 </span>`;
13260 return `<div id="post-${ num }" class="${ wrapClass }">
13261 <div class="post ${ isNew ? 'post_type_' : '' }${ isOp ? 'oppost' : 'reply' }` +
13262 `${ filesHTML ? ' post_withimg' : '' }" id="post-body-${ num }" data-num="${ num }">
13263 <div id="post-details-${ num }" class="${ isNew ? 'post__details' : 'post-details' }">
13264 <input type="checkbox" name="delete" value="${ num }">
13265 ${ !data.subject ? '' : `<span class="${ isNew ? 'post__' : 'post-' }title">` +
13266 `${ data.subject + (data.tags ? ` /${ data.tags }/` : '') }</span>` }
13267 ${ emailEl }
13268 ${ data.icon ? `<span class="${ isNew ? 'post__' : 'post-' }icon">` +
13269 `${ data.icon }</span>` : '' }
13270 ${ tripEl }
13271 ${ data.op === 1 ? `<span class="${ p }ophui"># OP</span> ` : '' }
13272 ${ isNew ? timeReflink : `<span class="posttime-reflink">
13273 ${ timeReflink }
13274 </span>` }
13275 ${ rate }
13276 </div>
13277 ${ filesHTML }
13278 ${ this._getPostMsg(data) }
13279 </div>
13280 </div>`;
13281 }
13282 * bannedPostsData() {
13283 const p = this._isNew ? 'post__' : '';
13284 for(const { banned, num } of this._posts) {
13285 switch(banned) {
13286 case 1:
13287 yield [1, num, $add(`<span class="${ p }pomyanem">(Автор этого поста был забанен.)</span>`)];
13288 break;
13289 case 2:
13290 yield [2, num, $add(`<span class="${ p }pomyanem">` +
13291 '(Автор этого поста был предупрежден.)</span>')];
13292 break;
13293 }
13294 }
13295 }
13296
13297 get _hasLikes() {
13298 const value = !!$q('.like-div, .post__rate');
13299 Object.defineProperty(this, '_hasLikes', { value });
13300 return value;
13301 }
13302 get _isNew() {
13303 const value = !!$q('.post_type_oppost');
13304 Object.defineProperty(this, '_isNew', { value });
13305 return value;
13306 }
13307 _getPostMsg(data) {
13308 const _switch = (val, obj) => val in obj ? obj[val] : obj['@@default'];
13309 const comment = data.comment.replace(/<script /ig, '<!--<textarea ')
13310 .replace(/<\/script>/ig, '</textarea>-->');
13311 const p = this._isNew ? 'post__' : '';
13312 return `<blockquote id="m${ data.num }" class="${ this._isNew ? 'post__' : 'post-' }message">` +
13313 `${ comment }${ _switch(data.banned, {
13314 1 : `<br><span class="${ p }pomyanem">(Автор этого поста был забанен.)</span>`,
13315 2 : `<br><span class="${ p }pomyanem">(Автор этого поста был предупрежден.)</span>`,
13316 '@@default' : ''
13317 }) }</blockquote>`;
13318 }
13319}
13320
13321/* ==[ RefMap.js ]============================================================================================
13322 REFERENCE LINKS MAP
13323=========================================================================================================== */
13324
13325class RefMap {
13326 constructor(post) {
13327 this.hasMap = false;
13328 this._isHidden = false;
13329 this._isInited = false;
13330 this._post = post;
13331 this._set = new Set();
13332 }
13333 static gen(posts) {
13334 const { tNums } = DelForm;
13335 for(const [pNum, post] of posts) {
13336 for(const [link, lNum] of post.refLinks()) { // link might be from another document
13337 if(MyPosts.has(lNum)) {
13338 link.classList.add('de-ref-you');
13339 if(!MyPosts.has(pNum) && (post instanceof AbstractPost)) {
13340 post.el.classList.add('de-mypost-reply');
13341 }
13342 }
13343 if(!aib.hasOPNum && tNums.has(lNum)) {
13344 link.classList.add('de-ref-op');
13345 }
13346 if(!posts.has(lNum)) {
13347 continue;
13348 }
13349 const { ref } = posts.get(lNum);
13350 if(ref._isInited) {
13351 ref.addRefNum(post, pNum);
13352 } else {
13353 ref._set.add(pNum);
13354 ref.hasMap = true;
13355 }
13356 }
13357 }
13358 }
13359 static initRefMap(form) {
13360 let post = form.firstThr && form.firstThr.op;
13361 if(post && Cfg.linksNavig) {
13362 this.gen(pByNum);
13363 const strNums = Cfg.strikeHidd && Post.hiddenNums.size ? Post.hiddenNums : null;
13364 for(; post; post = post.next) {
13365 if(post.ref.hasMap) {
13366 post.ref.initPostRef('', strNums);
13367 }
13368 }
13369 }
13370 }
13371 static updateRefMap(post, isAdd) {
13372 const pNum = post.num;
13373 const strNums = isAdd && Cfg.strikeHidd && Post.hiddenNums.size ? Post.hiddenNums : null;
13374 const links = $Q('a', post.msg);
13375 for(let lNum, i = 0, len = links.length; i < len; ++i) {
13376 const link = links[i];
13377 const tc = link.textContent;
13378 if(tc[0] !== '>' || tc[1] !== '>' || !(lNum = parseInt(tc.substr(2), 10))) {
13379 continue;
13380 }
13381 if(isAdd && MyPosts.has(lNum)) {
13382 link.classList.add('de-ref-you');
13383 if(!MyPosts.has(pNum)) {
13384 const postClass = post.el.classList;
13385 if(!postClass.contains('de-mypost-reply')) {
13386 postClass.add('de-mypost-reply');
13387 updater.refToYou(pNum);
13388 }
13389 }
13390 }
13391 if(!pByNum.has(lNum)) {
13392 continue;
13393 }
13394 const lPost = pByNum.get(lNum);
13395 if(!aib.t) {
13396 link.href = `#${ aib._4chan ? 'p' : '' }${ lNum }`;
13397 }
13398 if(!isAdd) {
13399 lPost.ref.removeLink(pNum);
13400 return;
13401 }
13402 if(strNums && strNums.has(lNum)) {
13403 link.classList.add('de-link-hid');
13404 }
13405 if(!aib.hasOPNum && DelForm.tNums.has(lNum)) {
13406 link.classList.add('de-ref-op');
13407 }
13408 lPost.ref.hasMap = true;
13409 lPost.ref.addRefNum(post, pNum, strNums && strNums.has(pNum));
13410 }
13411 }
13412 addRefNum(post, num, isHidden = null) {
13413 if(isHidden === null) {
13414 const strNums = Cfg.strikeHidd && Post.hiddenNums.size ? Post.hiddenNums : null;
13415 isHidden = strNums ? strNums.has(+num) : false;
13416 }
13417 if(!this._set.has(num)) {
13418 this._set.add(num);
13419 this._el.insertAdjacentHTML('beforeend', this._getHTML(num, '', isHidden));
13420 if(Cfg.hideRefPsts && this._post.isHidden && (post instanceof Post)) {
13421 post.setVisib(true, 'reference to >>' + num);
13422 post.ref.hideRef();
13423 }
13424 }
13425 }
13426 getElByNum(num) {
13427 return $q(`a[href$="${ num }"]`, this._el);
13428 }
13429 has(num) {
13430 return this._set.has(num);
13431 }
13432 hideRef(isForced = false) {
13433 if(!isForced && !Cfg.hideRefPsts || !this.hasMap || this._isHidden) {
13434 return;
13435 }
13436 this._isHidden = true;
13437 for(const num of this._set) {
13438 const post = pByNum.get(num);
13439 if(post && !post.isHidden) {
13440 if(isForced) {
13441 post.setUserVisib(true, true, 'reference to >>' + this._post.num);
13442 post.ref.hideRef(true);
13443 } else if(!post.userToggled) {
13444 post.setVisib(true, 'reference to >>' + this._post.num);
13445 post.ref.hideRef();
13446 }
13447 }
13448 }
13449 }
13450 initPostRef(tUrl, strNums) {
13451 let html = '';
13452 for(const num of this._set) {
13453 html += this._getHTML(num, tUrl, strNums && strNums.has(num));
13454 }
13455 this._createEl(html, false);
13456 this._isInited = true;
13457 }
13458 makeUnion(oRef) {
13459 this._set = new Set([...this._set, ...oRef._set].sort((a, b) => a - b));
13460 }
13461 removeLink(num) {
13462 this._set.delete(num);
13463 if(!this._set.size) {
13464 this.removeMap();
13465 } else {
13466 const el = this.getElByNum(num);
13467 if(el) {
13468 $del(el.nextSibling);
13469 el.remove();
13470 }
13471 }
13472 }
13473 removeMap() {
13474 this._set = new Set();
13475 this._el.remove();
13476 delete this._el;
13477 this.hasMap = false;
13478 }
13479 toggleRef(isHide, isForced) {
13480 if(isHide) {
13481 this.hideRef(isForced);
13482 } else {
13483 this.unhideRef(isForced);
13484 }
13485 }
13486 unhideRef(isForced = false) {
13487 if(this._isHidden && !this.hasMap) {
13488 return;
13489 }
13490 this._isHidden = false;
13491 for(const num of this._set) {
13492 const post = pByNum.get(num);
13493 if(post && post.isHidden && !post.spellHidden) {
13494 if(isForced) {
13495 post.setUserVisib(false);
13496 post.ref.unhideRef(true);
13497 } else if(!post.userToggled) {
13498 post.setVisib(false);
13499 post.ref.unhideRef();
13500 }
13501 }
13502 }
13503 }
13504
13505 get _el() {
13506 let value = $q('.de-refmap', this._post.el);
13507 if(!value) {
13508 this._createEl('', this._post.isHidden);
13509 value = $q('.de-refmap', this._post.el);
13510 }
13511 Object.defineProperty(this, '_el', { value, configurable: true });
13512 return value;
13513 }
13514 _createEl(innerHTML, isHidden) {
13515 let el;
13516 const { msg } = this._post;
13517 const html = `<div class="de-refmap${
13518 isHidden ? ' de-post-hiddencontent' : '' }">${ innerHTML }</div>`;
13519 if(aib.dobrochan && (el = msg.nextElementSibling)) {
13520 el.insertAdjacentHTML('beforeend', html);
13521 } else {
13522 msg.insertAdjacentHTML('afterend', html);
13523 }
13524 }
13525 _getHTML(num, tUrl, isHidden) {
13526 return `<a href="${ tUrl }${ aib.anchor }${ num }" class="de-link-backref${
13527 isHidden ? ' de-link-hid' : '' }${ MyPosts.has(num) ? ' de-ref-you' : ''
13528 }">>>${ num }</a><span class="de-refcomma">, </span>`;
13529 }
13530}
13531
13532/* ==[ Threads.js ]===========================================================================================
13533 THREADS
13534=========================================================================================================== */
13535
13536class Thread {
13537 constructor(el, num, prev, form) {
13538 this.hasNew = false;
13539 this.hidCounter = 0;
13540 this.isFav = false;
13541 this.isHidden = false;
13542 this.loadCount = 0;
13543 this.next = null;
13544 this.num = num;
13545 const els = $Q(aib.qRPost, el);
13546 const len = els.length;
13547 const omt = aib.t ? 1 : aib.getOmitted($q(aib.qOmitted, el), len);
13548 this.pcount = omt + len;
13549 this.el = el;
13550 this.prev = prev;
13551 this.form = form;
13552 this._lastModified = '';
13553 if(prev) {
13554 prev.next = this;
13555 }
13556 let lastPost = this.op = new Post(aib.getOp(el), this, num, 0, true, prev ? prev.last : null);
13557 pByEl.set(el, lastPost);
13558 for(let i = 0; i < len; ++i) {
13559 const pEl = els[i];
13560 lastPost = new Post(pEl, this, aib.getPNum(pEl), omt + i, false, lastPost);
13561 }
13562 this.last = lastPost;
13563 el.setAttribute('de-thread', null);
13564 visPosts = Math.max(visPosts, len);
13565 if(localData) {
13566 return;
13567 }
13568 this.btns = $bEnd(el, `<div class="de-thr-buttons">${ Post.getPostBtns(true, true) }
13569 <span class="de-thr-updater">[<a class="de-thr-updater-link de-abtn" href="#"></a>` +
13570 (!aib.t ? ']</span>' : '<span id="de-updater-count" style="display: none;"></span>]</span>') +
13571 '</div>');
13572 this.btns.addEventListener('click', this);
13573 this.btns.addEventListener('mouseover', this);
13574 [this.btnHide,, this.btnFav, this.btnUpd] = [...this.btns.children];
13575 if(!aib.t && Cfg.hideReplies) {
13576 this.btnReplies = $bEnd(this.btns,
13577 ' <span class="de-btn-replies">[<a class="de-abtn" href="#"></a>]</span>');
13578 this._toggleReplies();
13579 }
13580 }
13581 static get first() {
13582 return DelForm.first.firstThr;
13583 }
13584 static get last() {
13585 return DelForm.last.lastThr;
13586 }
13587 static removeSavedData() {
13588 // TODO: remove relevant spells, hidden posts and user posts
13589 }
13590 get bottom() {
13591 return this.isHidden || Cfg.hideReplies ? this.op.bottom : this.last.bottom;
13592 }
13593 get lastNotDeleted() {
13594 let post = this.last;
13595 while(post.isDeleted) {
13596 post = post.prev;
13597 }
13598 return post;
13599 }
13600 get nextNotHidden() {
13601 let thr;
13602 for(thr = this.next; thr && thr.isHidden; thr = thr.next) /* empty */;
13603 return thr;
13604 }
13605 get prevNotHidden() {
13606 let thr;
13607 for(thr = this.prev; thr && thr.isHidden; thr = thr.prev) /* empty */;
13608 return thr;
13609 }
13610 get top() {
13611 return this.op.top;
13612 }
13613 get userTouched() {
13614 const value = new Map();
13615 Object.defineProperty(this, 'userTouched', { value });
13616 return value;
13617 }
13618 deletePosts(post, delAll, isRemovePost) {
13619 SpellsRunner.cachedData = null;
13620 let count = 0;
13621 do {
13622 if(isRemovePost && this.last === post) {
13623 this.last = post.prev;
13624 }
13625 post.deletePost(isRemovePost);
13626 post = post.nextNotDeleted;
13627 count++;
13628 } while(delAll && post);
13629 for(let tPost = post; tPost; tPost = tPost.nextInThread) {
13630 if(!tPost.isDeleted) {
13631 tPost.count -= count;
13632 tPost.counterEl.textContent = tPost.count + 1;
13633 }
13634 }
13635 this.pcount -= count;
13636 return post;
13637 }
13638 handleEvent(e) {
13639 $pd(e);
13640 const el = fixEventEl(e.target);
13641 const elClass = el.classList[0];
13642 const nextThr = this.next;
13643 let oldCoord = false;
13644 if(e.type === 'click') {
13645 switch(elClass) {
13646 case 'de-btn-fav': this.toggleFavState(true); break;
13647 case 'de-btn-fav-sel': this.toggleFavState(false); break;
13648 case 'de-btn-hide':
13649 case 'de-btn-hide-user':
13650 case 'de-btn-unhide-user':
13651 oldCoord = nextThr && nextThr.top;
13652 this.op.setUserVisib(!this.isHidden);
13653 break;
13654 case 'de-btn-reply': pr.showQuickReply(this.last, this.num, false, false, true); break;
13655 case 'de-btn-replies':
13656 case 'de-replies-show':
13657 case 'de-replies-hide':
13658 oldCoord = !nextThr || this.last.isOmitted ? null : nextThr.top;
13659 this._toggleReplies();
13660 break;
13661 case 'de-thr-collapse':
13662 case 'de-thr-collapse-link': this.loadPosts(visPosts, true); break;
13663 case 'de-thr-updater':
13664 case 'de-thr-updater-link':
13665 if(aib.t) {
13666 updater.forceLoad();
13667 } else {
13668 this.loadPosts('new');
13669 }
13670 }
13671 if(oldCoord) {
13672 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + nextThr.top - oldCoord);
13673 }
13674 } else if(e.type === 'mouseover') {
13675 switch(el.classList[0]) {
13676 case 'de-btn-reply':
13677 this.btns.title = Lng.replyToThr[lang];
13678 quotetxt = deWindow.getSelection().toString();
13679 return;
13680 case 'de-btn-hide':
13681 case 'de-btn-hide-user':
13682 case 'de-btn-unhide':
13683 case 'de-btn-unhide-user': this.btns.title = Lng.toggleThr[lang]; return;
13684 case 'de-btn-fav': this.btns.title = Lng.addFav[lang]; return;
13685 case 'de-btn-fav-sel': this.btns.title = Lng.delFav[lang]; return;
13686 default: this.btns.removeAttribute('title');
13687 }
13688 }
13689 }
13690 /*
13691 * Thread loading via ajax.
13692 * Calls from the list of threads, not in a thread.
13693 * Adds posts to current thread accoring to task:
13694 * @param {String|Number} task
13695 * 'new' - get new posts,
13696 * 'all' - get all posts,
13697 * 'more' - show 10 omitted posts + get new posts
13698 * {number} - get last N posts
13699 * @param {Boolean} isSmartScroll - keeps the scroll position relative to the next thread.
13700 * @param {Boolean} isInformUser - shows a popup with waiting animation
13701 * @returns {Promise} - resolves with null, to chain with function when loading ends
13702 */
13703 loadPosts(task, isSmartScroll = false, isInformUser = true) {
13704 if(isInformUser) {
13705 $popup('load-thr', Lng.loading[lang], true);
13706 }
13707 return ajaxPostsLoad(aib.b, this.num, false).then(
13708 pBuilder => this._loadFromBuilder(task, isSmartScroll, pBuilder),
13709 err => $popup('load-thr', getErrorMessage(err)));
13710 }
13711 /*
13712 * New posts loading via ajax.
13713 * Calls by thread updater, by clicking on >>[Get new posts] button, and after sending a reply.
13714 * Adds new posts to the end of current thread.
13715 * @returns {Promise} - resolves with Object, { newCount: Number, locked: Boolean }
13716 */
13717 loadNewPosts() {
13718 return ajaxPostsLoad(aib.b, this.num, true)
13719 .then(pBuilder => pBuilder ? this._loadNewFromBuilder(pBuilder) : { newCount: 0, locked: false });
13720 }
13721 toggleFavState(isEnable, preview = null) {
13722 let h, b, num, cnt, txt, last;
13723 if(preview) {
13724 preview.toggleFavBtn(isEnable);
13725 }
13726 if(!preview || preview.num === this.num) { // Oppost or usual preview
13727 this.op.toggleFavBtn(isEnable);
13728 this.isFav = isEnable;
13729 ({ host: h, b } = aib);
13730 ({ num } = this);
13731 cnt = this.pcount;
13732 txt = this.op.title;
13733 last = aib.anchor + this.last.num;
13734 } else { // Loaded preview for oppost in remote thread
13735 h = aib.host;
13736 ({ brd: b, num } = preview);
13737 cnt = preview.remoteThr.pcount;
13738 txt = preview.remoteThr.title;
13739 last = aib.anchor + preview.remoteThr.lastNum;
13740 }
13741 readFavorites().then(favObj => {
13742 if(isEnable) {
13743 let f = favObj[h] || (favObj[h] = {});
13744 f = f[b] || (f[b] = {});
13745 f.url = aib.prot + '//' + aib.host + aib.getPageUrl(b, 0);
13746 f[num] = { cnt, new: 0, you: 0, txt, url: aib.getThrUrl(b, num), last, time: Date.now() };
13747 } else {
13748 removeFavEntry(favObj, h, b, num);
13749 }
13750 sendStorageEvent('__de-favorites', [h, b, num, favObj, isEnable ? 'add' : 'delete']);
13751 saveRenewFavorites(favObj);
13752 });
13753 }
13754 updateHidden(data) {
13755 let thr = this;
13756 do {
13757 const realHid = data ? data.hasOwnProperty(thr.num) : false;
13758 if(thr.isHidden ^ realHid) {
13759 if(realHid) {
13760 thr.op.setUserVisib(true, false);
13761 data[thr.num] = thr.op.title;
13762 } else if(thr.isHidden) {
13763 thr.op.setUserVisib(false, false);
13764 }
13765 }
13766 } while((thr = thr.next));
13767 }
13768
13769 _addPost(parent, el, i, prev, maybeVParser) {
13770 const num = aib.getPNum(el);
13771 const wrap = doc.adoptNode(aib.getPostWrap(el, false));
13772 const post = new Post(el, this, num, i, false, prev);
13773 parent.appendChild(wrap);
13774 if(aib.t && !doc.hidden && Cfg.animation) {
13775 $animate(el, 'de-post-new');
13776 }
13777 if(this.userTouched.has(num)) {
13778 post.setUserVisib(this.userTouched.get(num), false);
13779 this.userTouched.delete(num);
13780 } else if(HiddenPosts.has(num)) {
13781 HiddenPosts.hideHidden(post, num);
13782 }
13783 if(maybeVParser.value) {
13784 maybeVParser.value.parse(post);
13785 }
13786 processImgInfoLinks(post);
13787 post.addFuncs();
13788 ContentLoader.preloadImages(post);
13789 if(aib.t && Cfg.markNewPosts) {
13790 Post.addMark(el, false);
13791 }
13792 return post;
13793 }
13794 _checkBans(pBuilder) {
13795 if(!aib.qBan) {
13796 return;
13797 }
13798 for(const [banId, bNum, bEl] of pBuilder.bannedPostsData()) {
13799 const post = bNum ? pByNum.get(bNum) : this.op;
13800 if(post && post.banned !== banId) {
13801 $del($q(aib.qBan, post.el));
13802 post.msg.appendChild(bEl);
13803 post.banned = banId;
13804 }
13805 }
13806 }
13807 _importPosts(last, pBuilder, begin, end, maybeVParser, maybeSpells) {
13808 const nums = [];
13809 const newCount = end - begin;
13810 let newVisCount = newCount;
13811 let fragm;
13812 if(aib.JsonBuilder && nav.hasTemplate) {
13813 const html = [];
13814 for(let i = begin; i < end; ++i) {
13815 html.push(pBuilder.getPostHTML(i));
13816 nums.push(pBuilder.getPNum(i));
13817 }
13818 const temp = doc.createElement('template');
13819 temp.innerHTML = aib.fixHTML(html.join(''));
13820 fragm = temp.content;
13821 const posts = $Q(aib.qRPost, fragm);
13822 for(let i = 0, len = posts.length; i < len; ++i) {
13823 last = this._addPost(fragm, posts[i], begin + i + 1, last, maybeVParser);
13824 newVisCount -= maybeSpells.value.runSpells(last);
13825 embedPostMsgImages(last.el);
13826 }
13827 } else {
13828 fragm = doc.createDocumentFragment();
13829 for(; begin < end; ++begin) {
13830 last = this._addPost(fragm, pBuilder.getPostEl(begin), begin + 1, last, maybeVParser);
13831 nums.push(last.num);
13832 newVisCount -= maybeSpells.value.runSpells(last);
13833 embedPostMsgImages(last.el);
13834 }
13835 }
13836 return [newCount, newVisCount, fragm, last, nums];
13837 }
13838 _loadFromBuilder(last, smartScroll, pBuilder) {
13839 let nextCoord;
13840 const maybeSpells = new Maybe(SpellsRunner);
13841 if(smartScroll) {
13842 if(this.next) {
13843 nextCoord = this.next.top;
13844 } else {
13845 smartScroll = false;
13846 }
13847 }
13848 pr.closeReply();
13849 const { op, el: thrEl } = this;
13850 $del($q(aib.qOmitted + ', .de-omitted', thrEl));
13851 if(this.loadCount === 0) {
13852 if(op.trunc) {
13853 op.updateMsg(pBuilder.getOpMessage(), maybeSpells.value);
13854 }
13855 op.ref.removeMap();
13856 }
13857 this.loadCount++;
13858 this._parsePosts(pBuilder);
13859 let needToHide, needToOmit, needToShow;
13860 let post = op.next;
13861 let needRMUpdate = false;
13862 let existed = this.pcount === 1 ? 0 : this.pcount - post.count;
13863 switch(last) {
13864 case 'new': // get new posts
13865 needToHide = $Q('.de-hidden', thrEl).length;
13866 needToOmit = needToHide + post.count - 1;
13867 needToShow = pBuilder.length - needToOmit;
13868 break;
13869 case 'all': // get all posts
13870 needToHide = needToOmit = 0;
13871 needToShow = pBuilder.length;
13872 break;
13873 case 'more': // show 10 omitted posts + get new posts
13874 needToHide = $Q('.de-hidden', thrEl).length - 10;
13875 needToOmit = Math.max(needToHide + post.count - 1, 0);
13876 needToHide = Math.max(needToHide, 0);
13877 needToShow = pBuilder.length - needToOmit;
13878 break;
13879 default: // get last posts
13880 needToHide = Math.max(existed - last, 0);
13881 needToOmit = Math.max(pBuilder.length - last, 0);
13882 needToShow = last;
13883 }
13884 if(needToHide) {
13885 while(existed-- !== needToShow) {
13886 post.wrap.classList.add('de-hidden');
13887 post.isOmitted = true;
13888 post = post.next;
13889 }
13890 } else {
13891 const nonExisted = pBuilder.length - existed;
13892 const maybeVParser = new Maybe(Cfg.embedYTube ? VideosParser : null);
13893 const [,, fragm, last, nums] = this._importPosts(
13894 op, pBuilder,
13895 Math.max(0, nonExisted + existed - needToShow),
13896 nonExisted,
13897 maybeVParser,
13898 maybeSpells);
13899 if(maybeVParser.hasValue) {
13900 maybeVParser.value.endParser();
13901 }
13902 $after(op.wrap, fragm);
13903 DollchanAPI.notify('newpost', nums);
13904 last.next = post;
13905 if(post) {
13906 post.prev = last;
13907 }
13908 needRMUpdate = true;
13909 needToShow = Math.min(nonExisted + existed, needToShow);
13910 }
13911 while(existed-- !== 0) {
13912 if(post.trunc) {
13913 const newMsg = doc.adoptNode($q(aib.qPostMsg, pBuilder.getPostEl(post.count - 1)));
13914 post.updateMsg(aib.fixHTML(newMsg), maybeSpells.value);
13915 }
13916 if(post.isOmitted) {
13917 post.wrap.classList.remove('de-hidden');
13918 post.isOmitted = false;
13919 }
13920 if(needRMUpdate) {
13921 RefMap.updateRefMap(post, true);
13922 }
13923 post = post.next;
13924 }
13925 if(maybeSpells.hasValue) {
13926 maybeSpells.value.endSpells();
13927 }
13928 const btns = this._moveBtnsToEnd();
13929 if(!$q('.de-thr-collapse', btns)) {
13930 $bEnd(btns, `<span class="de-thr-collapse"> [<a class="de-thr-collapse-link de-abtn" href="${
13931 aib.getThrUrl(aib.b, this.num) }"></a>]</span>`);
13932 }
13933 if(needToShow > visPosts) {
13934 thrNavPanel.addThr(this);
13935 btns.lastChild.style.display = 'initial';
13936 } else {
13937 thrNavPanel.removeThr(this);
13938 $hide(btns.lastChild);
13939 }
13940 if(needToOmit > 0) {
13941 op.el.insertAdjacentHTML('afterend', `<div class="de-omitted">${ needToOmit }</div>`);
13942 }
13943 if(smartScroll) {
13944 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + this.next.top - nextCoord);
13945 }
13946 Pview.updatePosition(false);
13947 if(Cfg.hideReplies) {
13948 this.btnReplies.firstElementChild.className = 'de-replies-hide de-abtn';
13949 if(Cfg.updThrBtns) {
13950 $show(this.btnUpd);
13951 }
13952 }
13953 closePopup('load-thr');
13954 }
13955 _loadNewFromBuilder(pBuilder) {
13956 const lastOffset = pr.isVisible ? pr.top : null;
13957 const [newPosts, newVisPosts] = this._parsePosts(pBuilder);
13958 this._moveBtnsToEnd();
13959 if(lastOffset !== null) {
13960 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset + pr.top - lastOffset);
13961 }
13962 if(newPosts !== 0 || Panel.isNew) {
13963 Panel.updateCounter(
13964 pBuilder.length + 1 - (Cfg.panelCounter === 2 ? this.hidCounter : 0),
13965 $Q(`.de-reply:not(.de-post-removed) ${
13966 aib.qPostImg }, .de-oppost ${ aib.qPostImg }`, this.el).length,
13967 pBuilder.postersCount);
13968 Pview.updatePosition(true);
13969 }
13970 if(pBuilder.isClosed) {
13971 AjaxCache.clearCache();
13972 }
13973 return { newCount: newVisPosts, locked: pBuilder.isClosed };
13974 }
13975 _moveBtnsToEnd() {
13976 const { btns, el } = this;
13977 if(btns !== el.lastChild) {
13978 el.appendChild(btns);
13979 }
13980 return btns;
13981 }
13982 _parsePosts(pBuilder) {
13983 this._checkBans(pBuilder);
13984 let newPosts = 0;
13985 let newVisPosts = 0;
13986 let post = this.lastNotDeleted;
13987 const len = pBuilder.length;
13988 const maybeSpells = new Maybe(SpellsRunner);
13989 const maybeVParser = new Maybe(Cfg.embedYTube ? VideosParser : null);
13990 const { count } = post;
13991 if(count !== 0 && (aib.dobrochan || count > len || pBuilder.getPNum(count - 1) !== post.num)) {
13992 post = this.op.nextNotDeleted;
13993 let i = post.count - 1;
13994 let firstChangedPost = null;
13995 for(; i < len && post;) {
13996 const { num, prev } = post;
13997 const iNum = pBuilder.getPNum(i);
13998 if(num === iNum) {
13999 i++;
14000 post = post.nextNotDeleted;
14001 continue;
14002 }
14003 if(num <= iNum) {
14004 if(!firstChangedPost) {
14005 firstChangedPost = post;
14006 }
14007 post = this.deletePosts(post, false, !aib.t);
14008 continue;
14009 }
14010 if(!firstChangedPost) {
14011 firstChangedPost = prev;
14012 }
14013 let cnt = 0;
14014 do {
14015 cnt++;
14016 i++;
14017 } while(pBuilder.getPNum(i) < num);
14018 const res = this._importPosts(prev, pBuilder, i - cnt, i, maybeVParser, maybeSpells);
14019 newPosts += res[0];
14020 this.pcount += res[0];
14021 newVisPosts += res[1];
14022 $after(prev.wrap, res[2]);
14023 res[3].next = post;
14024 post.prev = res[3];
14025 DollchanAPI.notify('newpost', res[4]);
14026 for(let temp = post; temp; temp = temp.nextInThread) {
14027 temp.count += cnt;
14028 }
14029 }
14030 if(i === len && post) {
14031 this.deletePosts(post, true, !aib.t);
14032 }
14033 if(firstChangedPost && maybeSpells.hasValue && maybeSpells.value.hasNumSpell) {
14034 for(post = firstChangedPost.nextInThread; post; post = post.nextInThread) {
14035 maybeSpells.value.runSpells(post);
14036 }
14037 }
14038 if(newPosts !== 0) {
14039 for(post = firstChangedPost; post; post = post.nextInThread) {
14040 RefMap.updateRefMap(post, true);
14041 }
14042 }
14043 }
14044 if(len + 1 > this.pcount) {
14045 const res = this._importPosts(this.last, pBuilder, this.lastNotDeleted.count,
14046 len, maybeVParser, maybeSpells);
14047 newPosts += res[0];
14048 newVisPosts += res[1];
14049 (aib.qPostsParent ? $q(aib.qPostsParent, this.el) : this.el).appendChild(res[2]);
14050 this.last = res[3];
14051 DollchanAPI.notify('newpost', res[4]);
14052 this.pcount = len + 1;
14053 }
14054 updateFavorites(this.op.num, [this.pcount, this.last.num], 'update');
14055 if(maybeVParser.hasValue) {
14056 maybeVParser.value.endParser();
14057 }
14058 if(maybeSpells.hasValue) {
14059 maybeSpells.value.endSpells();
14060 }
14061 return [newPosts, newVisPosts];
14062 }
14063 _toggleReplies() {
14064 const isHide = !this.last.isOmitted;
14065 let post = this.op;
14066 let i = 0;
14067 for(; post !== this.last; ++i) {
14068 (post = post.next).isOmitted = isHide;
14069 post.wrap.classList.toggle('de-hidden', isHide);
14070 }
14071 this.btnReplies.firstElementChild.className =
14072 `${ isHide ? 'de-replies-show' : 'de-replies-hide' } de-abtn`;
14073 $each(this.btns.children, el => el !== this.btnReplies && $toggle(el, !isHide));
14074 $del($q(aib.qOmitted + ', .de-omitted', this.el));
14075 i = this.pcount - 1 - (isHide ? 0 : i);
14076 if(i) {
14077 this.op.el.insertAdjacentHTML('afterend', `<span class="de-omitted">${ i }</span> `);
14078 }
14079 }
14080}
14081
14082const thrNavPanel = {
14083 addThr(thr) {
14084 this._thrs.add(thr.el);
14085 if(this._thrs.size === 1) {
14086 doc.defaultView.addEventListener('scroll', this);
14087 }
14088 if(!this._visible) {
14089 this._checkThreads();
14090 }
14091 },
14092 handleEvent(e) {
14093 switch(e.type) {
14094 case 'scroll': deWindow.requestAnimationFrame(() => this._checkThreads()); break;
14095 case 'mouseover': this._expandCollapse(true, fixEventEl(e.relatedTarget)); break;
14096 case 'mouseout': this._expandCollapse(false, fixEventEl(e.relatedTarget)); break;
14097 case 'click': this._handleClick(e); break;
14098 }
14099 },
14100 initThrNav() {
14101 const el = $bEnd(docBody, `
14102 <div id="de-thr-navpanel" class="de-thr-navpanel-hidden" style="display: none;">
14103 <svg id="de-thr-navarrow"><use xlink:href="#de-symbol-thr-nav-arrow"/></svg>
14104 <div id="de-thr-navup">
14105 <svg viewBox="0 0 24 24"><use xlink:href="#de-symbol-thr-nav-up"/></svg>
14106 </div>
14107 <div id="de-thr-navdown">
14108 <svg viewBox="0 0 24 24"><use xlink:href="#de-symbol-thr-nav-down"/></svg>
14109 </div>
14110 </div>`);
14111 el.addEventListener('mouseover', this, true);
14112 el.addEventListener('mouseout', this, true);
14113 el.addEventListener('click', this, true);
14114 this._el = el;
14115 this._thrs = new Set();
14116 },
14117 removeThr(thr) {
14118 this._thrs.delete(thr.el);
14119 if(!this._thrs.size) {
14120 $hide(this._el);
14121 this._currentThr = null;
14122 this._visible = false;
14123 doc.defaultView.removeEventListener('scroll', this);
14124 }
14125 },
14126
14127 _currentThr : null,
14128 _el : null,
14129 _toggleTO : 0,
14130 _thrs : null,
14131 _visible : false,
14132 _checkThreads() {
14133 const el = this._findCurrentThread();
14134 if(el) {
14135 if(!this._visible) {
14136 this._toggleNavPanel(false);
14137 }
14138 this._currentThr = el;
14139 } else if(this._visible) {
14140 this._toggleNavPanel(true);
14141 }
14142 },
14143 _expandCollapse(isExpand, rt) {
14144 if(!rt || !this._el.contains(rt.farthestViewportElement || rt)) {
14145 clearTimeout(this._toggleTO);
14146 this._toggleTO = setTimeout(() => this._el.classList.toggle('de-thr-navpanel-hidden', !isExpand),
14147 Cfg.linksOver);
14148 }
14149 },
14150 _findCurrentThread() {
14151 Object.defineProperty(this, '_findCurrentThread', {
14152 value: 'elementsFromPoint' in doc ?
14153 () => doc.elementsFromPoint(Post.sizing.wWidth / 2, Post.sizing.wHeight / 2)
14154 .find(el => this._thrs.has(el)) :
14155 () => {
14156 let el = doc.elementFromPoint(Post.sizing.wWidth / 2, Post.sizing.wHeight / 2);
14157 while(el) {
14158 if(this._thrs.has(el)) {
14159 return el;
14160 }
14161 el = el.parentElement;
14162 }
14163 return undefined;
14164 }
14165 });
14166 return this._findCurrentThread();
14167 },
14168 _handleClick(e) {
14169 const el = fixEventEl(e.target);
14170 switch((el.tagName.toLowerCase() === 'svg' ? el.parentNode : el).id) {
14171 case 'de-thr-navup':
14172 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset +
14173 this._currentThr.getBoundingClientRect().top - 50);
14174 break;
14175 case 'de-thr-navdown':
14176 scrollTo(deWindow.pageXOffset, deWindow.pageYOffset +
14177 this._currentThr.getBoundingClientRect().bottom - Post.sizing.wHeight + 50);
14178 break;
14179 }
14180 },
14181 _toggleNavPanel(isHide) {
14182 this._el.style.display = isHide ? 'none' : 'initial';
14183 this._visible = !isHide;
14184 }
14185};
14186
14187/* ==[ ThreadUpdater.js ]=====================================================================================
14188 THREAD UPDATER
14189=========================================================================================================== */
14190
14191function initThreadUpdater(title, enableUpdate) {
14192 let focusLoadTime, disabledByUser = true;
14193 let enabled = false;
14194 let repliesToYou = new Set();
14195 let lastECode = 200;
14196 let newPosts = 0;
14197 let paused = false;
14198 let sendError = false;
14199 const storageName = `de-lastpcount-${ aib.b }-${ aib.t }`;
14200
14201 const audio = {
14202 enabled : false,
14203 repeatMS : 0,
14204 disableAudio() {
14205 this.stopAudio();
14206 this.enabled = false;
14207 const btn = $id('de-panel-audio-on');
14208 if(btn) {
14209 btn.id = 'de-panel-audio-off';
14210 }
14211 },
14212 playAudio() {
14213 this.stopAudio();
14214 if(this.repeatMS === 0) {
14215 this._el.play();
14216 return;
14217 }
14218 this._playInterval = setInterval(() => this._el.play(), this.repeatMS);
14219 },
14220 stopAudio() {
14221 if(this._playInterval) {
14222 clearInterval(this._playInterval);
14223 this._playInterval = null;
14224 }
14225 },
14226
14227 get _el() {
14228 const value = doc.createElement('audio');
14229 value.setAttribute('preload', 'auto');
14230 value.src = gitRaw + 'signal.ogg';
14231 Object.defineProperty(this, '_el', { value });
14232 return value;
14233 }
14234 };
14235
14236 const counter = {
14237 count(delayMS, useCounter, callback) {
14238 if(!this._enabled || !useCounter) {
14239 this._countingTO = setTimeout(() => {
14240 this._countingTO = null;
14241 callback();
14242 }, delayMS);
14243 return;
14244 }
14245 let seconds = delayMS / 1e3;
14246 this._set(seconds);
14247 this._countingIV = setInterval(() => {
14248 seconds--;
14249 if(seconds === 0) {
14250 this._stopCounter();
14251 callback();
14252 } else {
14253 this._set(seconds);
14254 }
14255 }, 1e3);
14256 },
14257 disableCounter() {
14258 this._enabled = false;
14259 this._stopCounter();
14260 $hide(this._el);
14261 },
14262 enableCounter() {
14263 this._enabled = true;
14264 $show(this._el);
14265 },
14266 setWait() {
14267 this._stopCounter();
14268 if(this._enabled) {
14269 this._el.innerHTML = '<svg class="de-wait"><use xlink:href="#de-symbol-wait"/></svg>';
14270 }
14271 },
14272
14273 _countingTO : null,
14274 _countingIV : null,
14275 _enabled : false,
14276 get _el() {
14277 const value = $id('de-updater-count');
14278 Object.defineProperty(this, '_el', { value });
14279 return value;
14280 },
14281 _set(seconds) {
14282 this._el.innerHTML = seconds;
14283 },
14284 _stopCounter() {
14285 if(this._countingIV) {
14286 clearInterval(this._countingIV);
14287 this._countingIV = null;
14288 }
14289 if(this._countingTO) {
14290 clearTimeout(this._countingTO);
14291 this._countingTO = null;
14292 }
14293 }
14294 };
14295
14296 const favicon = {
14297 get canBlink() {
14298 return Cfg.favIcoBlink && !!this.originalIcon;
14299 },
14300 get originalIcon() {
14301 return this._iconEl ? this._iconEl.href : null;
14302 },
14303 initIcons() {
14304 if(this._isInited) {
14305 return;
14306 }
14307 this._isInited = true;
14308 const icon = new Image();
14309 icon.onload = e => {
14310 try {
14311 this._initIconsHelper(e.target);
14312 } catch(err) {
14313 console.warn('Icon error:', err);
14314 }
14315 };
14316 if(aib._4chan) {
14317 // Due to CORS we cannot apply href to icon.src directly
14318 $ajax(this._iconEl.href, { responseType: 'blob' }, true).then(xhr => {
14319 icon.src = 'response' in xhr ?
14320 deWindow.URL.createObjectURL(xhr.response) : '/favicon.ico';
14321 });
14322 return;
14323 }
14324 icon.src = this._iconEl.href;
14325 },
14326 startBlink(isError) {
14327 const iconUrl = !this._hasIcons ? this._emptyIcon :
14328 isError ? this._iconError :
14329 repliesToYou.size ? this._getIconYou(newPosts) : this._getIconNew(newPosts);
14330 if(this._blinkInterv) {
14331 if(this._currentIcon === iconUrl) {
14332 return;
14333 }
14334 clearInterval(this._blinkInterv);
14335 }
14336 this._currentIcon = iconUrl;
14337 this._blinkInterv = setInterval(() => this._setIcon((this._isOrigIcon = !this._isOrigIcon) ?
14338 this.originalIcon : this._currentIcon), this._blinkMS);
14339 },
14340 stopBlink() {
14341 if(this._blinkInterv) {
14342 clearInterval(this._blinkInterv);
14343 this._blinkInterv = null;
14344 }
14345 if(!this._isOrigIcon) {
14346 this._setIcon(this.originalIcon);
14347 this._isOrigIcon = true;
14348 }
14349 },
14350 updateIcon(isError) {
14351 if(!isError && !newPosts) {
14352 this._setIcon(this.originalIcon);
14353 } else if(this._hasIcons) {
14354 this._setIcon(isError ? this._iconError :
14355 repliesToYou.size ? this._getIconYou(newPosts) : this._getIconNew(newPosts));
14356 }
14357 },
14358
14359 _blinkInterv : null,
14360 _blinkMS : 800,
14361 _currentIcon : null,
14362 _emptyIcon : 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==',
14363 _getIconNew : newPosts => null,
14364 _getIconYou : newPosts => null,
14365 _hasIcons : false,
14366 _iconError : null,
14367 _iconsNew : [],
14368 _iconsYou : [],
14369 _isInited : false,
14370 _isOrigIcon : true,
14371 get _iconEl() {
14372 const el = $q('link[rel="shortcut icon"]', doc.head) ||
14373 $bEnd(doc.head, '<link href="/favicon.ico" rel="shortcut icon"/>');
14374 Object.defineProperties(this, {
14375 _iconEl : { value: el, writable: true },
14376 originalIcon : { value: el.href }
14377 });
14378 return el;
14379 },
14380 _drawCanvCircle(ctx, strokeColor, fillColor, scale) {
14381 ctx.beginPath();
14382 ctx.arc(10.5 * scale, 10.5 * scale, 5 * scale, 0, 2 * Math.PI);
14383 ctx.fillStyle = fillColor;
14384 ctx.fill();
14385 ctx.lineWidth = 1;
14386 ctx.strokeStyle = strokeColor;
14387 ctx.stroke();
14388 },
14389 _drawCanvLines(ctx, line1, line2, color, width, scale) {
14390 ctx.beginPath();
14391 ctx.strokeStyle = color;
14392 ctx.lineWidth = width * scale;
14393 ctx.moveTo(line1[0] * scale, line1[1] * scale);
14394 ctx.lineTo(line1[2] * scale, line1[3] * scale);
14395 ctx.moveTo(line2[0] * scale, line2[1] * scale);
14396 ctx.lineTo(line2[2] * scale, line2[3] * scale);
14397 ctx.stroke();
14398 },
14399 _drawIconsNewYou(ctx, canvas, id, iconCircle, scale) {
14400 ctx.putImageData(iconCircle, 0, 0);
14401 ctx.fillStyle = '#fff';
14402 if(id) {
14403 ctx.font = `bold ${ 12 * scale }px Arial`;
14404 ctx.fillText(id, 7 * scale, 15 * scale);
14405 } else {
14406 ctx.fillRect(6 * scale, 9 * scale, 2 * scale, 3 * scale);
14407 ctx.fillRect(9.5 * scale, 9 * scale, 2 * scale, 3 * scale);
14408 ctx.fillRect(13 * scale, 9 * scale, 2 * scale, 3 * scale);
14409 }
14410 return canvas.toDataURL('image/png');
14411 },
14412 _initIconsHelper(icon) {
14413 const canvas = doc.createElement('canvas');
14414 const ctx = canvas.getContext('2d');
14415 const wh = Math.max(icon.naturalHeight, 16 * (deWindow.devicePixelRatio || 1));
14416 const scale = wh / 16;
14417 canvas.width = canvas.height = wh;
14418 ctx.drawImage(icon, 0, 0, wh, wh);
14419 const original = ctx.getImageData(0, 0, wh, wh);
14420 // Error (red cross)
14421 this._drawCanvLines(ctx, [15, 15, 7, 7], [7, 15, 15, 7], '#780000', 3, scale);
14422 this._drawCanvLines(ctx, [14.5, 14.5, 7.5, 7.5], [7.5, 14.5, 14.5, 7.5], '#fa2020', 1.5, scale);
14423 this._iconError = canvas.toDataURL('image/png');
14424 // New posts (green circle)
14425 ctx.putImageData(original, 0, 0);
14426 this._drawCanvCircle(ctx, '#174f1d', '#00a000', scale);
14427 const iconNewCircle = ctx.getImageData(0, 0, wh, wh);
14428 // Replies to you (blue circle)
14429 ctx.putImageData(original, 0, 0);
14430 this._drawCanvCircle(ctx, '#122091', '#1b6df5', scale);
14431 const iconYouCircle = ctx.getImageData(0, 0, wh, wh);
14432 this._getIconNew = newPosts => {
14433 const id = newPosts < 10 ? newPosts : 0;
14434 return this._iconsNew[id] || (this._iconsNew[id] =
14435 this._drawIconsNewYou(ctx, canvas, id, iconNewCircle, scale));
14436 };
14437 this._getIconYou = newPosts => {
14438 const id = newPosts < 10 ? newPosts : 0;
14439 return this._iconsYou[id] || (this._iconsYou[id] =
14440 this._drawIconsNewYou(ctx, canvas, id, iconYouCircle, scale));
14441 };
14442 this._hasIcons = true;
14443 },
14444 _setIcon(iconUrl) {
14445 this._iconEl.remove();
14446 this._iconEl = $aBegin(doc.head, `<link rel="shortcut icon" href="${ iconUrl }">`);
14447 }
14448 };
14449
14450 const notification = {
14451 get canShow() {
14452 return Cfg.desktNotif && this._granted;
14453 },
14454 checkPermission() {
14455 if(Cfg.desktNotif && ('permission' in Notification)) {
14456 switch(Notification.permission.toLowerCase()) {
14457 case 'default': this._requestPermission(); break;
14458 case 'denied': saveCfg('desktNotif', 0);
14459 }
14460 }
14461 },
14462 closeNotif() {
14463 if(this._notifEl) {
14464 this._notifEl.close();
14465 this._notifEl = null;
14466 }
14467 },
14468 showNotif() {
14469 const lngQuantity = num => {
14470 const new10 = num % 10;
14471 return lang === 1 ? +(num !== 1) :
14472 new10 > 4 || new10 === 0 || (((num % 100) / 10) | 0) === 1 ? 2 :
14473 new10 === 1 ? 0 : 1;
14474 };
14475 const post = Thread.first.last;
14476 const toYou = repliesToYou.size;
14477 const notif = new Notification(`${ aib.dm }/${ aib.b }/${ aib.t }: ${ newPosts } ${
14478 Lng.newPost[lang][lngQuantity(newPosts)] }. ${
14479 toYou ? `${ toYou } ${ Lng.youReplies[lang][lngQuantity(toYou)] }.` : '' }`,
14480 {
14481 body : Lng.latestPost[lang] + ':\n' + post.text.substring(0, 250).replace(/\s+/g, ' '),
14482 icon : post.images.firstAttach ? post.images.firstAttach.src : favicon.originalIcon,
14483 tag : aib.dm + aib.b + aib.t
14484 });
14485 notif.onshow = () => setTimeout(() => notif === this._notifEl && this.closeNotif(), 12e3);
14486 notif.onclick = () => deWindow.focus();
14487 notif.onerror = () => {
14488 deWindow.focus();
14489 this._requestPermission();
14490 };
14491 this._notifEl = notif;
14492 },
14493
14494 _closeTO : null,
14495 _granted : true,
14496 _notifEl : null,
14497 _requestPermission() {
14498 this._granted = false;
14499 Notification.requestPermission(state => {
14500 if(state.toLowerCase() === 'denied') {
14501 saveCfg('desktNotif', 0);
14502 } else {
14503 this._granted = true;
14504 }
14505 });
14506 }
14507 };
14508
14509 const updMachine = {
14510 start(needSleep = false, loadOnce = false) {
14511 if(this._state !== -1) {
14512 this.stopUpdater(false);
14513 }
14514 this._state = 0;
14515 this._loadOnce = loadOnce;
14516 this._delay = this._initDelay = Cfg.updThrDelay * 1e3;
14517 if(!loadOnce) {
14518 this._setUpdateStatus('on');
14519 }
14520 this._makeStep(needSleep);
14521 },
14522 stopUpdater(updateStatus = true) {
14523 if(this._state !== -1) {
14524 this._state = -1;
14525 if(this._loadPromise) {
14526 this._loadPromise.cancelPromise();
14527 this._loadPromise = null;
14528 }
14529 counter.setWait();
14530 if(updateStatus) {
14531 this._setUpdateStatus('off');
14532 }
14533 }
14534 },
14535
14536 _delay : 0,
14537 _initDelay : 0,
14538 _loadOnce : false,
14539 _loadPromise : null,
14540 _seconds : 0,
14541 _state : -1,
14542 get _panelButton() {
14543 const value = $q('a[id^="de-panel-upd"]');
14544 if(value) {
14545 Object.defineProperty(this, '_panelButton', { value });
14546 }
14547 return value;
14548 },
14549 _handleNewPosts(lPosts, err) {
14550 if(err instanceof CancelError) {
14551 return;
14552 }
14553 infoLoadErrors(err, false);
14554 const eCode = err instanceof AjaxError ? err.code : 0;
14555 if(eCode !== 200 && eCode !== 304) {
14556 if(doc.hidden && favicon.canBlink) {
14557 favicon.startBlink(true);
14558 }
14559 if(eCode === -1 || (eCode === 404 && lastECode === 404)) {
14560 Thread.removeSavedData(aib.b, aib.t);
14561 updateTitle(eCode);
14562 disableUpdater();
14563 } else {
14564 this._setUpdateStatus('warn');
14565 if(!Cfg.noErrInTitle) {
14566 updateTitle(eCode);
14567 }
14568 this._makeStep();
14569 }
14570 lastECode = eCode;
14571 updateFavorites(aib.t, getErrorMessage(err), 'error');
14572 return;
14573 }
14574 if(lastECode !== 200) {
14575 favicon.stopBlink();
14576 this._setUpdateStatus('on');
14577 if(!Cfg.noErrInTitle) {
14578 updateTitle(eCode);
14579 }
14580 }
14581 lastECode = eCode;
14582 if(doc.hidden) {
14583 if(lPosts !== 0) {
14584 newPosts += lPosts;
14585 updateTitle();
14586 if(favicon.canBlink) {
14587 favicon.startBlink(false);
14588 }
14589 if(notification.canShow) {
14590 notification.showNotif();
14591 }
14592 if(audio.enabled) {
14593 audio.playAudio();
14594 }
14595 sesStorage[storageName] = Thread.first.pcount;
14596 this._delay = this._initDelay;
14597 } else if(this._delay !== 12e4) {
14598 this._delay = Math.min(this._delay + this._initDelay, 12e4);
14599 }
14600 }
14601 this._makeStep();
14602 },
14603 _makeStep(needSleep = true) {
14604 while(true) {
14605 switch(this._state) {
14606 case 0:
14607 if(needSleep) {
14608 this._state = 1;
14609 counter.count(this._delay, !doc.hidden, () => this._makeStep());
14610 return;
14611 }
14612 /* falls through */
14613 case 1:
14614 counter.setWait();
14615 this._state = 2;
14616 this._loadPromise = Thread.first.loadNewPosts().then(
14617 ({ newCount, locked }) =>
14618 this._handleNewPosts(newCount, locked ? AjaxError.Locked : AjaxError.Success),
14619 err => this._handleNewPosts(0, err));
14620 return;
14621 case 2:
14622 this._loadPromise = null;
14623 if(this._loadOnce) {
14624 this._state = -1;
14625 return;
14626 }
14627 this._state = 0;
14628 break;
14629 default:
14630 console.error('Invalid thread updater state:', this._state, new Error().stack);
14631 return;
14632 }
14633 }
14634 },
14635 _setUpdateStatus(status) {
14636 if(this._panelButton) {
14637 this._panelButton.id = 'de-panel-upd-' + status;
14638 this._panelButton.title = Lng.panelBtn[`upd-${ status === 'off' ? 'off' : 'on' }`][lang];
14639 if(nav.isPresto) {
14640 this._panelButton.innerHTML =
14641 '<svg class="de-panel-svg"><use xlink:href="#de-symbol-panel-upd"/></svg>';
14642 }
14643 }
14644 }
14645 };
14646
14647 function enableUpdater() {
14648 enabled = true;
14649 disabledByUser = paused = false;
14650 repliesToYou = new Set();
14651 newPosts = 0;
14652 focusLoadTime = -1e4;
14653 notification.checkPermission();
14654 if(Cfg.updCount) {
14655 counter.enableCounter();
14656 }
14657 favicon.initIcons();
14658 }
14659
14660 function disableUpdater() {
14661 if(enabled) {
14662 audio.disableAudio();
14663 counter.disableCounter();
14664 updMachine.stopUpdater();
14665 enabled = false;
14666 }
14667 }
14668
14669 function forceLoadPosts() {
14670 if(enabled && paused) {
14671 return;
14672 }
14673 if(!enabled && !disabledByUser) {
14674 enableUpdater();
14675 }
14676 updMachine.start(false, !enabled);
14677 }
14678
14679 function updateTitle(eCode = lastECode) {
14680 doc.title = (sendError === true ? `{${ Lng.error[lang] }} ` : '') +
14681 (eCode <= 0 || eCode === 200 ? '' : `{${ eCode }} `) +
14682 (newPosts ? `[${ newPosts }] ` : '') + title;
14683 favicon.updateIcon(eCode !== 200 && eCode !== 304);
14684 }
14685
14686 doc.addEventListener('visibilitychange', e => {
14687 if(!doc.hidden) {
14688 const focusTime = e.timeStamp;
14689 favicon.stopBlink();
14690 audio.stopAudio();
14691 notification.closeNotif();
14692 newPosts = 0;
14693 repliesToYou = new Set();
14694 sendError = false;
14695 setTimeout(() => {
14696 updateTitle();
14697 if(enabled && focusTime - focusLoadTime > 1e4) {
14698 focusLoadTime = focusTime;
14699 forceLoadPosts();
14700 }
14701 }, 200);
14702 } else if(Thread.first) {
14703 Post.clearMarks();
14704 }
14705 });
14706 if(enableUpdate) {
14707 enableUpdater();
14708 updMachine.start(true);
14709 }
14710
14711 return {
14712 continueUpdater(needSleep = false) {
14713 if(enabled && paused) {
14714 updMachine.start(needSleep);
14715 paused = false;
14716 }
14717 },
14718 disableUpdater() {
14719 disabledByUser = true;
14720 disableUpdater();
14721 },
14722 enableUpdater() {
14723 if(!enabled) {
14724 enableUpdater();
14725 updMachine.start();
14726 }
14727 },
14728 forceLoad(e) {
14729 if(e) {
14730 $pd(e);
14731 }
14732 Post.clearMarks();
14733 if(enabled && paused) {
14734 return;
14735 }
14736 $popup('newposts', Lng.loading[lang], true);
14737 forceLoadPosts();
14738 },
14739 pauseUpdater() {
14740 if(enabled && !paused) {
14741 updMachine.stopUpdater();
14742 paused = true;
14743 }
14744 },
14745 refToYou(pNum) {
14746 if(doc.hidden) {
14747 repliesToYou.add(pNum);
14748 }
14749 },
14750 toggle() {
14751 if(enabled) {
14752 this.disableUpdater();
14753 } else {
14754 this.enableUpdater();
14755 }
14756 },
14757 toggleAudio(repeatMS) {
14758 if(audio.enabled) {
14759 audio.stopAudio();
14760 return (audio.enabled = false);
14761 }
14762 audio.repeatMS = repeatMS;
14763 return (audio.enabled = true);
14764 },
14765 toggleCounter(enableCnt) {
14766 if(enableCnt) {
14767 counter.enableCounter();
14768 counter.setWait();
14769 } else {
14770 counter.disableCounter();
14771 }
14772 forceLoadPosts();
14773 },
14774 sendErrNotif() {
14775 if(Cfg.sendErrNotif && doc.hidden) {
14776 sendError = true;
14777 updateTitle();
14778 }
14779 }
14780 };
14781}
14782
14783/* ==[ DelForm.js ]===========================================================================================
14784 DELFORM
14785=========================================================================================================== */
14786
14787class DelForm {
14788 constructor(formEl, pageNum, prev) {
14789 let thr = null;
14790 this.el = formEl;
14791 this.firstThr = null;
14792 this.lastThr = null;
14793 this.next = null;
14794 this.pageNum = pageNum;
14795 this.prev = prev;
14796 if(prev) {
14797 prev.next = this;
14798 thr = prev.lastThr;
14799 }
14800 formEl.setAttribute('de-form', '');
14801 formEl.removeAttribute('id');
14802 $delAll('script', this.el);
14803 const threads = DelForm.getThreads(this.el);
14804 for(let i = 0, len = threads.length; i < len; ++i) {
14805 const num = aib.getTNum(threads[i]);
14806 if(!DelForm.tNums.has(num)) {
14807 DelForm.tNums.add(num);
14808 thr = new Thread(threads[i], num, thr, this);
14809 if(this.firstThr === null) {
14810 this.firstThr = thr;
14811 }
14812 continue;
14813 }
14814 const el = threads[i];
14815 const thrNext = threads[i + 1];
14816 let elNext = el.nextSibling;
14817 while(elNext && elNext !== thrNext) {
14818 elNext.remove();
14819 elNext = el.nextSibling;
14820 }
14821 el.remove();
14822 console.log('Repeated thread: ' + num);
14823 }
14824 if(this.firstThr === null) {
14825 if(prev) {
14826 this.lastThr = prev.lastThr;
14827 }
14828 return;
14829 }
14830 this.lastThr = thr;
14831 }
14832 static getThreads(formEl) {
14833 let threads = $Q(aib.qThread, formEl);
14834 let len = threads.length;
14835 if(len === 0) {
14836 if(localData) {
14837 threads = $Q('div[de-thread]');
14838 len = threads.length;
14839 }
14840 if(len === 0) {
14841 threads = DelForm._parseClasslessThreads(formEl);
14842 }
14843 }
14844 return threads;
14845 }
14846 static [Symbol.iterator]() {
14847 return {
14848 _data: this.first,
14849 next() {
14850 const value = this._data;
14851 if(value) {
14852 this._data = value.next;
14853 return { value, done: false };
14854 }
14855 return { done: true };
14856 }
14857 };
14858 }
14859
14860 static _parseClasslessThreads(formEl) {
14861 let i, len, cThr = doc.createElement('div');
14862 const threads = [];
14863 const fNodes = [...formEl.childNodes];
14864 for(i = 0, len = fNodes.length - 1; i < len; ++i) {
14865 const el = fNodes[i];
14866 if(el.tagName === 'HR') {
14867 formEl.insertBefore(cThr, el);
14868 const lastEl = cThr.lastElementChild;
14869 if(lastEl.tagName === 'BR') {
14870 formEl.insertBefore(lastEl, el);
14871 }
14872 try {
14873 aib.getTNum(cThr);
14874 threads.push(cThr);
14875 } catch(err) {}
14876 cThr = doc.createElement('div');
14877 } else {
14878 cThr.appendChild(el);
14879 }
14880 }
14881 cThr.appendChild(fNodes[i]);
14882 formEl.appendChild(cThr);
14883 return threads;
14884 }
14885 get passEl() {
14886 const value = aib.qDelPassw ? $q(aib.qDelPassw, this.el) : null;
14887 Object.defineProperty(this, 'passEl', { value });
14888 return value;
14889 }
14890 addStuff() {
14891 const { el } = this;
14892 if(Cfg.ajaxPosting && !localData) {
14893 const delBtn = aib.qDelBut ? $q(aib.qDelBut, el) : null;
14894 if(delBtn) {
14895 el.onsubmit = $pd;
14896 delBtn.onclick = e => {
14897 $pd(e);
14898 pr.closeReply();
14899 $popup('delete', Lng.deleting[lang], true);
14900 html5Submit(el, e.target).then(checkDelete)
14901 .catch(err => $popup('delete', getErrorMessage(err)));
14902 };
14903 }
14904 Logger.log('Init AJAX');
14905 }
14906 ContentLoader.preloadImages(el);
14907 Logger.log('Preload images');
14908 embedAudioLinks(el);
14909 Logger.log('Audio links');
14910 if(Cfg.embedYTube) {
14911 new VideosParser().parse(el).endParser();
14912 Logger.log('Video links');
14913 }
14914 processImgInfoLinks(el);
14915 Logger.log('Image names');
14916 RefMap.initRefMap(this);
14917 Logger.log('Reflinks map');
14918 }
14919}
14920DelForm.tNums = new Set();
14921
14922/* ==[ Browser.js ]===========================================================================================
14923 BROWSER DETECTORS AND DEPENDENCIES
14924=========================================================================================================== */
14925
14926function checkStorage() {
14927 try {
14928 locStorage = deWindow.localStorage;
14929 sesStorage = deWindow.sessionStorage;
14930 sesStorage['de-test'] = 1;
14931 } catch(err) {
14932 if(typeof unsafeWindow !== 'undefined') {
14933 locStorage = unsafeWindow.localStorage;
14934 sesStorage = unsafeWindow.sessionStorage;
14935 }
14936 }
14937 if(!(locStorage && (typeof locStorage === 'object') && sesStorage)) {
14938 console.error('Webstorage error: please, enable webstorage!');
14939 return false;
14940 }
14941 return true;
14942}
14943
14944// Browser identification and browser-specific hacks
14945function initNavFuncs() {
14946 const ua = navigator.userAgent;
14947 const isFirefox = ua.includes('Gecko/');
14948 const isWebkit = ua.includes('WebKit/');
14949 const isChrome = isWebkit && ua.includes('Chrome/');
14950 const isSafari = isWebkit && !isChrome;
14951 const hasPrestoStorage = !!prestoStorage && !ua.includes('Opera Mobi');
14952 const hasNewGM = /* global GM */ typeof GM !== 'undefined' && typeof GM.xmlHttpRequest === 'function';
14953 const canUseFetch = 'AbortController' in deWindow; // Firefox 57+, Chrome 66+, Safari 11.1+
14954 let scriptHandler, hasWebStorage = false;
14955 let hasOldGM = false;
14956 if(hasNewGM) {
14957 scriptHandler = GM.info ? `${ GM.info.scriptHandler } ${ GM.info.version }` : 'Greasemonkey';
14958 } else {
14959 try {
14960 hasOldGM = (typeof GM_setValue === 'function') &&
14961 (!isChrome || !GM_setValue.toString().includes('not supported'));
14962 } catch(err) {
14963 hasOldGM = err.message === 'Permission denied to access property "toString"'; // Chrome
14964 }
14965 hasWebStorage = !hasOldGM && (isFirefox || ('chrome' in deWindow)) &&
14966 (typeof chrome === 'object') && !!chrome && !!chrome.storage;
14967 scriptHandler = hasWebStorage ? 'WebExtension' :
14968 typeof GM_info === 'undefined' ? isFirefox ? 'Scriptish' : 'Unknown' :
14969 GM_info.scriptHandler ? `${ GM_info.scriptHandler } ${ GM_info.version }` :
14970 isFirefox ? 'Greasemonkey' : 'Unknown';
14971 }
14972 if(!('requestAnimationFrame' in deWindow)) { // XXX: Opera Presto
14973 deWindow.requestAnimationFrame = fn => setTimeout(fn, 0);
14974 }
14975 if(!('remove' in Element.prototype)) { // XXX: Opera Presto
14976 Element.prototype.remove = function() {
14977 const el = this.parentNode;
14978 if(el) {
14979 el.removeChild(this);
14980 }
14981 };
14982 }
14983 const nlProto = NodeList.prototype;
14984 $each = 'forEach' in nlProto ?
14985 (els, cb) => nlProto.forEach.call(els, cb) :
14986 (els, cb) => aProto.forEach.call(els, cb);
14987 let needFileHack = false;
14988 try {
14989 new File([''], '');
14990 if(isFirefox || isSafari) {
14991 needFileHack = !FormData.prototype.get;
14992 }
14993 } catch(err) {
14994 needFileHack = true;
14995 }
14996 if(needFileHack && FormData) { // XXX: Firefox < 39, Chrome < 50, Safari < 11
14997 const OrigFormData = FormData;
14998 const origAppend = FormData.prototype.append;
14999 FormData = function FormData(form) {
15000 const rv = form ? new OrigFormData(form) : new OrigFormData();
15001 rv.append = function append(name, value, fileName = null) {
15002 if(value instanceof Blob && 'name' in value && fileName === null) {
15003 return origAppend.call(this, name, value, value.name);
15004 }
15005 return origAppend.apply(this, arguments);
15006 };
15007 return rv;
15008 };
15009 deWindow.File = function File(arr, name) {
15010 const rv = new Blob(arr);
15011 rv.name = name;
15012 return rv;
15013 };
15014 }
15015 nav = {
15016 cssMatches: (leftSel, ...rules) => leftSel.split(', ').map(
15017 val => val + rules.join(', ' + val)
15018 ).join(', '),
15019 canUseFetch,
15020 canUseFetchBlob : canUseFetch && !(isChrome && scriptHandler === 'WebExtension'),
15021 canUseNativeXHR : true,
15022 firefoxVer : isFirefox ? +(ua.match(/Firefox\/(\d+)/) || [0, 0])[1] : 0,
15023 fixLink : isSafari ? getAbsLink : url => url,
15024 hasGlobalStorage : hasOldGM || hasNewGM || hasWebStorage || hasPrestoStorage,
15025 hasGMXHR : (typeof GM_xmlhttpRequest === 'function') ||
15026 hasNewGM && (typeof GM.xmlHttpRequest === 'function'),
15027 hasNewGM,
15028 hasOldGM,
15029 hasPrestoStorage,
15030 hasWebStorage,
15031 isChrome,
15032 isESNext : typeof deMainFuncOuter === 'undefined',
15033 isFirefox,
15034 isMsEdge : ua.includes('Edge/'),
15035 isPresto : !!deWindow.opera,
15036 isSafari,
15037 isWebkit,
15038 scriptHandler,
15039 ua : navigator.userAgent + (isFirefox ? ` [${ navigator.buildID }]` : ''),
15040
15041 get canPlayMP3() {
15042 const value = !!new Audio().canPlayType('audio/mpeg;');
15043 Object.defineProperty(this, 'canPlayMP3', { value });
15044 return value;
15045 },
15046 get hasTemplate() {
15047 const value = 'content' in doc.createElement('template');
15048 Object.defineProperty(this, 'hasTemplate', { value });
15049 return value;
15050 },
15051 get hasWorker() {
15052 let value = false;
15053 try {
15054 value = 'Worker' in deWindow && 'URL' in deWindow;
15055 } catch(err) {}
15056 if(value && this.isFirefox) {
15057 value = this.firefoxVer >= 40;
15058 }
15059 Object.defineProperty(this, 'hasWorker', { value });
15060 return value;
15061 },
15062 get matchesSelector() {
15063 const dE = doc.documentElement;
15064 const func = dE.matches || dE.mozMatchesSelector ||
15065 dE.webkitMatchesSelector || dE.oMatchesSelector;
15066 const value = (el, sel) => func.call(el, sel);
15067 Object.defineProperty(this, 'matchesSelector', { value });
15068 return value;
15069 },
15070 get viewportHeight() {
15071 const value = doc.compatMode && doc.compatMode === 'CSS1Compat' ?
15072 () => doc.documentElement.clientHeight : () => docBody.clientHeight;
15073 Object.defineProperty(this, 'viewportHeight', { value });
15074 return value;
15075 },
15076 get viewportWidth() {
15077 const value = doc.compatMode && doc.compatMode === 'CSS1Compat' ?
15078 () => doc.documentElement.clientWidth : () => docBody.clientWidth;
15079 Object.defineProperty(this, 'viewportWidth', { value });
15080 return value;
15081 },
15082 getUnsafeUint8Array(data, i, len) { // XXX: Old Greasemonkeys
15083 let Ctor = Uint8Array;
15084 if(nav.isFirefox && nav.hasOldGM) {
15085 try {
15086 if(!(new Uint8Array(data) instanceof Uint8Array)) {
15087 Ctor = unsafeWindow.Uint8Array;
15088 }
15089 } catch(err) {
15090 Ctor = unsafeWindow.Uint8Array;
15091 }
15092 }
15093 switch(arguments.length) {
15094 case 1: return new Ctor(data);
15095 case 2: return new Ctor(data, i);
15096 case 3: return new Ctor(data, i, len);
15097 }
15098 throw new Error();
15099 },
15100 getUnsafeDataView(data, offset) { // XXX: Old Greasemonkeys
15101 const value = new DataView(data, offset || 0);
15102 return !nav.isFirefox || !nav.hasOldGM || (value instanceof DataView) ? value :
15103 new unsafeWindow.DataView(data, offset || 0);
15104 }
15105 };
15106}
15107
15108/* ==[ BoardDefaults.js ]=====================================================================================
15109 IMAGEBOARD DEFAULTS
15110=========================================================================================================== */
15111
15112class BaseBoard {
15113 constructor(prot, dm) {
15114 // Query paths
15115 this.cReply = 'reply';
15116 this.qBan = null;
15117 this.qClosed = null;
15118 this.qDelBut = 'input[type="submit"]';
15119 this.qDelPassw = 'input[type="password"], input[name="password"]';
15120 this.qDForm = '#delform, form[name="delform"]';
15121 this.qError = 'h1, h2, font[size="5"]';
15122 this.qForm = '#postform';
15123 this.qFormFile = 'tr input[type="file"]';
15124 this.qFormPassw = 'tr input[type="password"]';
15125 this.qFormRedir = 'input[name="postredir"][value="1"]';
15126 this.qFormRules = '.rules, #rules';
15127 this.qFormSpoiler = 'input[type="checkbox"][name="spoiler"]'; // Ernstchan
15128 this.qFormSubm = 'tr input[type="submit"]';
15129 this.qFormTd = 'td';
15130 this.qFormTr = 'tr';
15131 this.qFormTxta = 'tr:not([style*="none"]) textarea:not([style*="display:none"])'; // Makaba
15132 this.qImgInfo = '.filesize';
15133 this.qOmitted = '.omittedposts';
15134 this.qOPost = '.oppost';
15135 this.qPages = 'table[border="1"] > tbody > tr > td:nth-child(2) > a:last-of-type';
15136 this.qPostHeader = '.de-post-btns';
15137 this.qPostImg = '.thumb, .ca_thumb, img[src*="thumb"], img[src*="/spoiler"], img[src^="blob:"]';
15138 this.qPostMsg = 'blockquote';
15139 this.qPostName = '.postername, .commentpostername';
15140 this.qPostSubj = '.filetitle';
15141 this.qPostTrip = '.postertrip';
15142 this.qPostRef = '.reflink';
15143 this.qPostsParent = null;
15144 this.qRPost = '.reply';
15145 this.qTrunc = '.abbrev, .abbr, .shortened';
15146 this._qOPostEnd = 'form > table, div > table, div[id^="repl"]';
15147
15148 // Other propertioes
15149 this.anchor = '#';
15150 this.b = '';
15151 this.dm = dm;
15152 this.docExt = null;
15153 this.firstPage = 0;
15154 this.formParent = 'parent';
15155 this.hasAltCaptcha = false;
15156 this.hasCatalog = false;
15157 this.hasOPNum = false;
15158 this.hasPicWrap = false;
15159 this.hasRefererErr = false;
15160 this.hasTextLinks = false;
15161 this.host = deWindow.location.hostname;
15162 this.JsonBuilder = null;
15163 this.jsonSubmit = false;
15164 this.markupBB = false;
15165 this.multiFile = false;
15166 this.page = 0;
15167 this.prot = prot;
15168 this.res = 'res/';
15169 this.ru = false;
15170 this.t = false;
15171 this.timePattern = 'w+dd+m+yyyy+hh+ii+ss';
15172
15173 // Imageboard-specific booleans
15174 this._02ch = false;
15175 this._2channel = false;
15176 this._4chan = false;
15177 this.dobrochan = false;
15178 this.iichan = false;
15179 this.makaba = false;
15180 }
15181 get qFormMail() {
15182 return nav.cssMatches('tr:not([style*="none"]) input:not([type="hidden"]):not([style*="none"])',
15183 '[name="email"]', '[name="em"]', '[name="field2"]', '[name="sage"]');
15184 }
15185 get qFormName() {
15186 return nav.cssMatches('tr:not([style*="none"]) input:not([type="hidden"]):not([style*="none"])',
15187 '[name="name"]', '[name="field1"]');
15188 }
15189 get qFormSubj() {
15190 return nav.cssMatches('tr:not([style*="none"]) input:not([type="hidden"]):not([style*="none"])',
15191 '[name="subject"]', '[name="field3"]');
15192 }
15193 get qImgNameLink() {
15194 const value = nav.cssMatches(this.qImgInfo.split(', ').join(' a, ') + ' a',
15195 '[href$=".jpg"]', '[href$=".jpeg"]', '[href$=".png"]', '[href$=".gif"]', '[href$=".webm"]',
15196 '[href$=".webp"]', '[href$=".mp4"]', '[href$=".ogv"]', '[href$=".apng"]', ', [href^="blob:"]');
15197 Object.defineProperty(this, 'qImgNameLink', { value });
15198 return value;
15199 }
15200 get qMsgImgLink() { // Sets here only
15201 const value = nav.cssMatches(this.qPostMsg.split(', ').join(' a, ') + ' a',
15202 '[href$=".jpg"]', '[href$=".jpeg"]', '[href$=".png"]', '[href$=".gif"]');
15203 Object.defineProperty(this, 'qMsgImgLink', { value });
15204 return value;
15205 }
15206 get qThread() {
15207 const value = $q('.thread') ? '.thread' : '[id^="thread"]';
15208 Object.defineProperty(this, 'qThread', { value });
15209 return value;
15210 }
15211 get capLang() { // _410chan
15212 return this.ru ? 2 : 1;
15213 }
15214 get catalogUrl() { // Iichan
15215 return `${ this.prot }//${ this.host }/${ this.b }/catalog.html`;
15216 }
15217 get changeReplyMode() {
15218 return null;
15219 }
15220 get css() {
15221 return '';
15222 }
15223 get deleteTruncMsg() {
15224 return null;
15225 }
15226 get fixDeadLinks() { // _4chan
15227 return null;
15228 }
15229 get fixHTMLHelper() {
15230 return null;
15231 }
15232 get fixFileInputs() {
15233 return null;
15234 }
15235 get getImgRedirectSrc() { // Archived
15236 return null;
15237 }
15238 get getSubmitData() {
15239 return null;
15240 }
15241 get initCaptcha() {
15242 return null;
15243 }
15244 get isArchived() {
15245 return false;
15246 }
15247 get lastPage() { // Makaba
15248 const el = $q(this.qPages);
15249 let value = el && +aProto.pop.call(el.textContent.match(/\d+/g) || []) || 0;
15250 if(this.page === value + 1) {
15251 value++;
15252 }
15253 Object.defineProperty(this, 'lastPage', { value });
15254 return value;
15255 }
15256 get markupTags() {
15257 return this.markupBB ? ['b', 'i', 'u', 's', 'spoiler', 'code'] : ['**', '*', '', '^H', '%%', '`'];
15258 }
15259 get observeContent() { // Makaba
15260 return null;
15261 }
15262 get reCrossLinks() { // Sets here only
15263 const value = new RegExp(`>https?:\\/\\/[^\\/]*${ this.dm }\\/([a-z0-9]+)\\/${
15264 quoteReg(this.res) }(\\d+)(?:[^#<]+)?(?:#i?(\\d+))?<`, 'g');
15265 Object.defineProperty(this, 'reCrossLinks', { value });
15266 return value;
15267 }
15268 get reportForm() {
15269 return null;
15270 }
15271 get sendHTML5Post() { // Lynxchan
15272 return null;
15273 }
15274 get updateCaptcha() {
15275 return null;
15276 }
15277 disableRedirection(el) { // Dobrochan
15278 $hide($qParent(el, aib.qFormTr));
15279 el.checked = true;
15280 }
15281 fixHTML(data, isForm = false) {
15282 if(!(dTime || Spells.reps || Cfg.crossLinks || Cfg.decodeLinks ||
15283 this.fixHTMLHelper || this.fixDeadLinks || this.hasTextLinks)
15284 ) {
15285 return data;
15286 }
15287 let str;
15288 if(typeof data === 'string') {
15289 str = data;
15290 } else if(isForm) {
15291 data.id = 'de-dform-old';
15292 str = data.outerHTML;
15293 } else {
15294 str = data.innerHTML;
15295 }
15296 if(dTime) {
15297 str = dTime.fix(str);
15298 }
15299 if(this.fixHTMLHelper) {
15300 str = this.fixHTMLHelper(str);
15301 }
15302 if(this.fixDeadLinks) {
15303 str = this.fixDeadLinks(str);
15304 }
15305 if(this.hasTextLinks) {
15306 str = str.replace(/(^|>|\s|>)(https*:\/\/[^"<>]*?)(<\/a>)?(?=$|<|\s)/ig,
15307 (x, a, b, c) => c ? x : `${ a }<a rel="noreferrer" href="${ b }">${ b }</a>`);
15308 }
15309 if(Spells.reps) {
15310 str = Spells.replace(str);
15311 }
15312 if(Cfg.crossLinks) {
15313 str = str.replace(aib.reCrossLinks, (_, b, tNum, pNum) => `>>>/${ b }/${ pNum || tNum }<`);
15314 }
15315 if(Cfg.decodeLinks) {
15316 str = str.replace(/>https?:\/\/[^<]+</ig, match => {
15317 try {
15318 return decodeURI(match);
15319 } catch(err) {}
15320 return match;
15321 });
15322 }
15323 if(typeof data === 'string') {
15324 return str;
15325 }
15326 if(isForm) {
15327 const newForm = $bBegin(data, str);
15328 $hide(data);
15329 deWindow.addEventListener('load', () => $del($id('de-dform-old')));
15330 return newForm;
15331 }
15332 data.innerHTML = str;
15333 return data;
15334 }
15335 fixVideo(isPost, data) {
15336 const videos = [];
15337 const els = $Q('embed, object, iframe', isPost ? data.el : data);
15338 for(let i = 0, len = els.length; i < len; ++i) {
15339 const el = els[i];
15340 const src = el.src || el.data;
15341 if(!src) {
15342 continue;
15343 }
15344 let m = src.match(Videos.ytReg);
15345 if(m) {
15346 videos.push([isPost ? data : this.getPostOfEl(el), m, true]);
15347 $del(el);
15348 }
15349 if(Cfg.addVimeo && (m = src.match(Videos.vimReg))) {
15350 videos.push([isPost ? data : this.getPostOfEl(el), m, false]);
15351 $del(el);
15352 }
15353 }
15354 return videos;
15355 }
15356 getBanId(postEl) { // Makaba
15357 return this.qBan && $q(this.qBan, postEl) ? 1 : 0;
15358 }
15359 getCapParent(el) {
15360 return $qParent(el, this.qFormTr);
15361 }
15362 getCaptchaSrc(src, tNum) {
15363 const temp = src.replace(/pl$/, 'pl?key=mainpage&dummy=')
15364 .replace(/dummy=[\d.]*/, 'dummy=' + Math.random());
15365 return tNum ? temp.replace(/mainpage|res\d+/, 'res' + tNum) : temp.replace(/res\d+/, 'mainpage');
15366 }
15367 getImgInfo(wrap) {
15368 const el = $q(this.qImgInfo, wrap);
15369 return el ? el.textContent : '';
15370 }
15371 getImgRealName(wrap) {
15372 const el = $q(this.qImgNameLink, wrap);
15373 return el ? el.title || el.textContent : '';
15374 }
15375 getImgSrcLink(img) {
15376 return $parent(img, 'A');
15377 }
15378 getImgWrap(img) {
15379 return ($parent(img, 'A') || img).parentNode;
15380 }
15381 getJsonApiUrl() {}
15382 getOmitted(el) {
15383 return +(el && (el.textContent || '').match(/\d+/)) + 1;
15384 }
15385 getOp(thr) { // Arhivach
15386 let op = localData ? $q('.de-oppost', thr) : $q(this.qOPost, thr);
15387 if(op) {
15388 return op;
15389 }
15390 op = thr.ownerDocument.createElement('div');
15391 op.classList.add('de-oppost');
15392 let el;
15393 const opEnd = $q(this._qOPostEnd, thr);
15394 while((el = thr.firstChild) && (el !== opEnd)) {
15395 op.appendChild(el);
15396 }
15397 if(thr.hasChildNodes()) {
15398 thr.insertBefore(op, thr.firstChild);
15399 } else {
15400 thr.appendChild(op);
15401 }
15402 return op;
15403 }
15404 getPageUrl(b, p) {
15405 return fixBrd(b) + (p > 0 ? p + this.docExt : '');
15406 }
15407 getPNum(post) {
15408 return +post.id.match(/\d+/);
15409 }
15410 getPostElOfEl(el) {
15411 const sel = this.qRPost + ', [de-thread], .de-pview';
15412 while(el && !nav.matchesSelector(el, sel)) {
15413 el = el.parentElement;
15414 }
15415 return el;
15416 }
15417 getPostOfEl(el) { // Sets here only
15418 return pByEl.get(this.getPostElOfEl(el));
15419 }
15420 getPostWrap(el, isOp) {
15421 if(isOp) {
15422 return el;
15423 }
15424 Object.defineProperty(this, 'getPostWrap',
15425 { value: el.tagName === 'TD' ? (el, isOp) => isOp ? el : $parent(el, 'TABLE') : el => el });
15426 return this.getPostWrap(el, isOp);
15427 }
15428 getSage(post) {
15429 if($q('.sage', post)) {
15430 return true;
15431 }
15432 const el = $q('a[href^="mailto:"], a[href="sage"]', post);
15433 return !!el && /sage/i.test(el.href);
15434 }
15435 getThrUrl(b, tNum) { // Arhivach
15436 return this.prot + '//' + this.host + fixBrd(b) + this.res + tNum + this.docExt;
15437 }
15438 getTNum(thr) {
15439 return +$q('input[type="checkbox"]', thr).value;
15440 }
15441 insertYtPlayer(msg, playerHtml) { // Dobrochan
15442 return $bBegin(msg, playerHtml);
15443 }
15444 isAjaxStatusOK(status) {
15445 return status === 200 || status === 206;
15446 }
15447 isIgnoreError(txt) { // Lynxchan
15448 return /successful|uploaded|updating|post deleted|post created|обновл|удален[о.]/i.test(txt);
15449 }
15450 parseURL() {
15451 const url = (deWindow.location.pathname || '').replace(/^[/]+/, '').replace(/[/]+/g, '/');
15452 if(url.match(this.res)) { // We are in thread
15453 const temp = url.split(this.res);
15454 this.b = temp[0].replace(/\/$/, '');
15455 this.t = +temp[1].match(/^[^\d]?\d+/)[0];
15456 this.page = this.firstPage;
15457 } else { // We are on board
15458 const temp = url.match(/\/?(\d+)[^/]*?$/);
15459 this.page = temp && +temp[1] || this.firstPage;
15460 this.b = url.replace(temp && this.page ? temp[0] : /\/(?:[^/]+\.[a-z]+)?$/, '');
15461 }
15462 if(this.docExt === null) {
15463 this.docExt = (url.match(/\.[a-z]+$/) || ['.html'])[0];
15464 }
15465 }
15466 updateSubmitBtn(el) {
15467 el.value = Lng.reply[lang];
15468 }
15469}
15470
15471/* ==[ BoardDetector.js ]=====================================================================================
15472 IMAGEBOARD DETECTOR
15473=========================================================================================================== */
15474
15475function getImageBoard(checkDomains, checkEngines) {
15476 const ibDomains = {};
15477 const ibEngines = [];
15478
15479 // ENGINES
15480 ibEngines.push(['form[action$="wakaba.pl"]', BaseBoard]);
15481
15482 class Kusaba extends BaseBoard {
15483 constructor(prot, dm) {
15484 super(prot, dm);
15485 this.kusaba = true;
15486
15487 this.qError = 'h1, h2, div[style*="1.25em"]';
15488 this.qFormRedir = 'input[name="redirecttothread"][value="1"]';
15489
15490 this.formParent = 'replythread';
15491 this.markupBB = true;
15492 }
15493 get css() {
15494 return `.extrabtns > a, .extrabtns > span, #newposts_get, .replymode,
15495 .ui-resizable-handle, blockquote + a { display: none !important; }
15496 .ui-wrapper { display: inline-block; width: auto !important;
15497 height: auto !important; padding: 0 !important; }`;
15498 }
15499 getCaptchaSrc(src) {
15500 return src.replace(/\?[^?]+$|$/, '?' + Math.random());
15501 }
15502 getImgRealName(wrap) {
15503 const el = $q('.filesize', wrap);
15504 if(el) {
15505 const info = el.textContent.split(',');
15506 if(info.length > 2) {
15507 return info.pop().replace(')', '');
15508 }
15509 }
15510 return super.getImgRealName(wrap);
15511 }
15512 init() {
15513 const el = $id('posttypeindicator');
15514 if(el) {
15515 [el.previousSibling, el.nextSibling, el].forEach($del);
15516 }
15517 return false;
15518 }
15519 }
15520 ibEngines.push(['script[src*="kusaba"]', Kusaba], ['form#delform[action$="/board.php"]', Kusaba]);
15521
15522 class Tinyboard extends BaseBoard {
15523 constructor(prot, dm) {
15524 super(prot, dm);
15525
15526 this.cReply = 'post reply';
15527 this.qClosed = '.fa-lock';
15528 this.qDForm = 'form[name*="postcontrols"]';
15529 this.qForm = 'form[name="post"]';
15530 this.qFormPassw = 'input[name="password"]:not([type="hidden"])';
15531 this.qFormRedir = null;
15532 this.qImgInfo = '.fileinfo';
15533 this.qOmitted = '.omitted';
15534 this.qPages = '.pages';
15535 this.qPostHeader = '.intro';
15536 this.qPostMsg = '.body';
15537 this.qPostName = '.name';
15538 this.qPostRef = '.post_no + a';
15539 this.qPostSubj = '.subject';
15540 this.qPostTrip = '.trip';
15541 this.qTrunc = '.toolong';
15542 this._origInputs = null;
15543 this._qOPostEnd = '.post.reply';
15544
15545 this.firstPage = 1;
15546 this.formParent = 'thread';
15547 this.hasCatalog = true;
15548 this.hasRefererErr = true;
15549 this.jsonSubmit = true;
15550 this.timePattern = 'nn+dd+yy++w++hh+ii+ss';
15551 }
15552 get qImgNameLink() {
15553 return 'p.fileinfo > a:first-of-type';
15554 }
15555 get css() {
15556 return `.banner, .hide-thread-link, .mentioned,
15557 .post-hover { display: none !important; }
15558 div.post.reply:not(.de-entry):not(.de-cfg-tab):not(.de-win-body) {
15559 float: left !important; clear: left; display: block; }
15560 ${ Cfg.imgNames ? `.postfilename, .unimportant > a[download] { display: none }
15561 .fileinfo > .unimportant { white-space: nowrap; }` : '' }`;
15562 }
15563 get markupTags() {
15564 return ["'''", "''", '__', '~~', '**', '[code'];
15565 }
15566 async changeReplyMode(form, tNum) {
15567 if(!this._origInputs && !$q('input[name="hash"]', form)) {
15568 // Board without antibot protection
15569 pr.subm.value = Lng.reply[lang];
15570 const pageInp = $q('input[name="page"]', form);
15571 if(tNum) {
15572 $del(pageInp);
15573 } else if(!pageInp) {
15574 form.insertAdjacentHTML('beforeend', '<input name="page" value="1" type="hidden">');
15575 }
15576 return;
15577 }
15578 const query = 'div[style="display:none"], input[style="display:none"], ' +
15579 'span[style="display:none"], textarea[style="display:none"], ' +
15580 'input[type="hidden"]:not(.de-input-hidden)';
15581 if(!$q('input[name="thread"]', form)) {
15582 // Switching from the thread creation to post reply mode occurs. Saving the original fields.
15583 this._origInputs = [doc.createElement('div'), pr.subm.value];
15584 $each($Q(query, form), el => this._origInputs[0].appendChild(el));
15585 } else if(!tNum) {
15586 // Switching from the post reply to thread creation occurs. Restoring the original fields.
15587 pr.subm.value = this._origInputs[1];
15588 $delAll(query, form);
15589 form.insertAdjacentHTML('beforeend', this._origInputs[0].innerHTML);
15590 this._origInputs = null;
15591 return;
15592 }
15593 // Post reply mode. Loading a thread with a form that contains the correct hidden fields.
15594 const errFn = () => {
15595 $popup('load-form', Lng.errFormLoad[lang]);
15596 pr.closeReply();
15597 };
15598 $popup('load-form', Lng.loading[lang], true);
15599 await ajaxLoad(aib.getThrUrl(this.b, tNum), false).then(loadedDoc => {
15600 const loadedForm = $q(this.qForm, loadedDoc);
15601 if(!loadedForm) {
15602 errFn();
15603 return;
15604 }
15605 pr.subm.value = $q(this.qFormSubm, loadedDoc).value;
15606 $delAll(query, form);
15607 $each($Q(query, loadedForm), el => form.appendChild(doc.adoptNode(el)));
15608 closePopup('load-form');
15609 }, errFn);
15610 }
15611 fixHTML(data, isForm) {
15612 const formEl = super.fixHTML(data, isForm);
15613 $each($Q('br.clear', formEl), brEl => {
15614 const hr = brEl.nextElementSibling;
15615 if(hr && hr.tagName === 'HR') {
15616 $after(brEl.parentNode, hr);
15617 }
15618 brEl.remove();
15619 });
15620 return formEl;
15621 }
15622 fixVideo(isPost, data) {
15623 return Array.from($Q('.video-container, #ytplayer', isPost ? data.el : data), el => {
15624 const value = [isPost ? data : this.getPostOfEl(el), el.id === 'ytplayer' ?
15625 el.src.match(Videos.ytReg) : ['', el.getAttribute('data-video')], true];
15626 el.remove();
15627 return value;
15628 });
15629 }
15630 getImgRealName(wrap) {
15631 const el = $q('.postfilename', wrap) ||
15632 $q('.unimportant > a[download]', wrap) || $q(this.qImgNameLink, wrap);
15633 return el.title || el.textContent;
15634 }
15635 getPageUrl(b, p) {
15636 return p > 1 ? fixBrd(b) + p + this.docExt : fixBrd(b);
15637 }
15638 getSubmitData({ error, id }) {
15639 return { error, postNum: id && +id };
15640 }
15641 getTNum(thr) {
15642 return +$q('input[type="checkbox"]', thr).name.match(/\d+/);
15643 }
15644 init() {
15645 $script('window.FormData = void 0');
15646 const formEl = $q('form[name="post"]');
15647 if(formEl) {
15648 formEl.insertAdjacentHTML('beforeend',
15649 '<input class="de-input-hidden" name="json_response" value="1" type="hidden">');
15650 }
15651 return false;
15652 }
15653 isAjaxStatusOK(status) {
15654 return status === 200 || status === 206 || status === 400;
15655 }
15656 updateSubmitBtn() {}
15657 }
15658 ibEngines.push(['form[name*="postcontrols"]', Tinyboard]);
15659
15660 class Vichan extends Tinyboard {
15661 constructor(prot, dm) {
15662 super(prot, dm);
15663
15664 this.qDelPassw = '#password';
15665 this.qPostImg = '.post-image[alt]:not(.deleted)';
15666
15667 this.multiFile = true;
15668 }
15669 get css() {
15670 return `${ super.css }
15671 #expand-all-images, #expand-all-images + .unimportant, .fileinfo > span[style*="nowrap;"],
15672 .post-btn, small, .watchThread { display: none !important; }
15673 body { padding: 0 5px !important; }
15674 .boardlist { z-index: 1 !important; }
15675 .fileinfo { width: 240px; }
15676 .multifile { width: auto !important; }`;
15677 }
15678 fixFileInputs(el) {
15679 el.innerHTML = Array.from({ length: 5 }, (val, i) =>
15680 `<div${ i ? ' style="display: none;"' : '' }>` +
15681 `<input type="file" name="file${ i ? i + 1 : '' }"></div>`
15682 ).join('');
15683 }
15684 fixHTMLHelper(str) {
15685 return str.replace(/"\/player\.php\?v=([^&]+)&[^"]+"/g, '"$1"');
15686 }
15687 init() {
15688 super.init();
15689 if(locStorage.file_dragdrop !== 'false') {
15690 locStorage.file_dragdrop = false;
15691 deWindow.location.reload();
15692 return true;
15693 }
15694 $script('highlightReply = Function.prototype');
15695 setTimeout(() => $del($id('updater')), 0);
15696 const textarea = $id('body');
15697 if(textarea) {
15698 textarea.removeAttribute('id');
15699 }
15700 return false;
15701 }
15702 }
15703 ibEngines.push(['tr#upload', Vichan]);
15704
15705 class TinyIB extends BaseBoard {
15706 constructor(prot, dm) {
15707 super(prot, dm);
15708
15709 this.qError = 'body[align=center] div, div[style="margin-top: 50px;"]';
15710 this.qPostImg = 'img.thumb, video.thumb';
15711 this.qPostMsg = '.message';
15712 }
15713 get css() {
15714 return '.replymode { display: none; }';
15715 }
15716 fixHTMLHelper(str) {
15717 return str.replace(/="\.\.\//g, `="/${ this.b }/`);
15718 }
15719 getCaptchaSrc(src) {
15720 return src.replace(/\?[^?]+$|$/, '?' + Math.random());
15721 }
15722 getImgWrap(img) {
15723 return img.parentNode.parentNode.parentNode;
15724 }
15725 getImgRealName(wrap) {
15726 const el = $q('.filesize', wrap);
15727 if(el) {
15728 const info = el.textContent.split(',');
15729 if(info.length > 2) {
15730 return info.pop().replace(')', '');
15731 }
15732 }
15733 return super.getImgRealName(wrap);
15734 }
15735 init() {
15736 defaultCfg.addTextBtns = 0;
15737 $each($Q('.message > .omittedposts'),
15738 el => $replace(el, '<span class="abbrev">Post too long. <a href="#">Click to view.</a>'));
15739 return false;
15740 }
15741 }
15742 ibEngines.push(['form[action$="imgboard.php?delete"]', TinyIB]);
15743
15744 class newTinyIB extends TinyIB {
15745 constructor(prot, dm) {
15746 super(prot, dm);
15747
15748 this.hasCatalog = true;
15749 this.markupBB = true;
15750 this.multiFile = true;
15751 this.timePattern = 'yy+nn+dd+w+hh+ii+ss';
15752 }
15753 get fixHTMLHelper() {
15754 return null;
15755 }
15756 fixFileInputs(el) {
15757 const str = '><input type="file" name="file[]"></div>';
15758 el.innerHTML = '<div' + str + ('<div style="display: none;"' + str).repeat(3);
15759 }
15760 getImgRealName(wrap) {
15761 return $q('.filesize > a', wrap).textContent;
15762 }
15763 init() {
15764 return false;
15765 }
15766 }
15767 ibEngines.push(['body.tinyib', newTinyIB]);
15768
15769 class Lynxchan extends BaseBoard {
15770 constructor(prot, dm) {
15771 super(prot, dm);
15772
15773 this.cReply = 'innerPost';
15774 this.qDForm = 'form[action$="contentActions.js"]';
15775 this.qDelBut = '#deleteFormButton';
15776 this.qError = '#errorLabel, #labelMessage';
15777 this.qForm = '.form-post, form[action$="newThread.js"], form[action$="replyThread.js"]';
15778 this.qFormPassw = 'input[name="password"]';
15779 this.qFormRules = '.form-post > .small';
15780 this.qFormSubm = '#formButton, #de-postform-submit';
15781 this.qImgInfo = '.uploadDetails';
15782 this.qOmitted = '.labelOmission';
15783 this.qOPost = '.innerOP';
15784 this.qPages = '#divPages';
15785 this.qPostHeader = '.postInfo, .de-post-btns';
15786 this.qPostImg = '.imgLink > img, img[src*="/.media/"]';
15787 this.qPostMsg = '.divMessage';
15788 this.qPostRef = '.linkQuote';
15789 this.qPostSubj = '.labelSubject';
15790 this.qPostsParent = '.divPosts';
15791 this.qRPost = '.innerPost';
15792 this.qTrunc = '.contentOmissionIndicator';
15793 this._qOPostEnd = '.divPosts';
15794
15795 this.firstPage = 1;
15796 this.formParent = 'threadId';
15797 this.hasCatalog = true;
15798 this.jsonSubmit = true;
15799 this.multiFile = true;
15800
15801 this._hasNewAPI = false;
15802 }
15803 get qImgNameLink() {
15804 return '.originalNameLink';
15805 }
15806 get qThread() {
15807 return '.opCell';
15808 }
15809 get css() {
15810 return `.de-video-link + div[style="display: inline;"] > .embedButton, .de-parea > hr,
15811 .divRefresh, #jsButton, .hideButton, .nameLink, #newPostFieldset, .panelBacklinks,
15812 .quoteTooltip, body > div[style^="display: inline;"] { display: none !important; }
15813 .divPosts { margin: 0 0; }
15814 #formButton { display: initial !important; }
15815 .form-post button, .form-post input, .form-post img { width: initial; }`;
15816 }
15817 get markupTags() {
15818 return ["'''", "''", '__', '~~', '**', '[code'];
15819 }
15820 changeReplyMode(form, tNum) {
15821 const action = form.getAttribute('action');
15822 form.setAttribute('action', tNum ? action.replace('newThread', 'replyThread') :
15823 action.replace('replyThread', 'newThread'));
15824 }
15825 fixFileInputs(el) {
15826 const str = '><input name="files" type="file"></div>';
15827 const maxEl = $id('labelMaxFiles');
15828 el.innerHTML = '<div' + str +
15829 ('<div style="display: none;"' + str).repeat((maxEl ? +maxEl.textContent : 3) - 1);
15830 }
15831 getCapParent(el) {
15832 return $id('captchaDiv');
15833 }
15834 getImgRealName(wrap) {
15835 return $q('.originalNameLink', wrap).textContent;
15836 }
15837 getImgSrcLink(img) {
15838 const el = img.parentNode;
15839 return el.tagName === 'A' ? el : $q('.originalNameLink', el.parentNode);
15840 }
15841 getImgWrap(img) {
15842 return $parent(img, 'FIGURE');
15843 }
15844 getPageUrl(b, p) {
15845 return fixBrd(b) + (p > 1 ? p + this.docExt : 'index.html');
15846 }
15847 getPNum(post) {
15848 return +$q('.deletionCheckBox', post).name.split('-')[2];
15849 }
15850 getPostWrap(el, isOp) {
15851 return isOp ? el : el.parentNode;
15852 }
15853 getSubmitData({ status, data }) {
15854 return {
15855 error : status === 'error' ? data : null,
15856 postNum : status === 'ok' ? +data : null
15857 };
15858 }
15859 getTNum(thr) {
15860 return +$q('.deletionCheckBox', thr).name.split('-')[1];
15861 }
15862 init() {
15863 const submEl = $id('formButton');
15864 if(submEl && submEl.type === 'button') {
15865 this._hasNewAPI = true;
15866 $replace(submEl, `<button id="de-postform-submit" type="submit">${
15867 submEl.innerHTML }</button>`);
15868 }
15869 $script(`if("autoRefresh" in window) {
15870 clearInterval(refreshTimer);
15871 }
15872 if("thread" in window && thread.refreshTimer) {
15873 clearInterval(thread.refreshTimer);
15874 Object.defineProperty(thread, "startTimer",
15875 { value: Function.prototype, writable: false, configurable: false });
15876 }`);
15877 const el = $q(this.qForm);
15878 if(el && !$q('td', el)) {
15879 const table = $aBegin($q(this.qForm), '<table><tbody></tbody></table>').firstChild;
15880 const els = $Q('#captchaDiv, #divUpload, #fieldEmail, #fieldMessage, #fieldName,' +
15881 ' #fieldPostingPassword, #fieldSubject');
15882 for(let i = 0, len = els.length; i < len; ++i) {
15883 $bEnd(table, '<tr><th></th><td></td></tr>').lastChild.appendChild(els[i]);
15884 }
15885 }
15886 return false;
15887 }
15888 isAjaxStatusOK(status) {
15889 return status === 200 || status === 206 || status === 400 || status === 500;
15890 }
15891 isIgnoreError(txt) {
15892 try {
15893 const obj = JSON.parse(txt);
15894 if(obj.status === 'ok' && obj.data && (obj.data.removedThreads || obj.data.removedPosts)) {
15895 return true;
15896 }
15897 } catch(err) {}
15898 return false;
15899 }
15900 async sendHTML5Post(form, data, needProgress, hasFiles) {
15901 let ajaxParams;
15902 if(this._hasNewAPI) {
15903 ajaxParams = { data, method: 'POST' };
15904 } else {
15905 const getBase64 = async file => new Promise((resolve, reject) => {
15906 const reader = new FileReader();
15907 reader.readAsDataURL(file);
15908 reader.onload = () => resolve(reader.result);
15909 reader.onerror = err => reject(err);
15910 });
15911 const getCookies = () => {
15912 const obj = {};
15913 const cookies = doc.cookie.split(';');
15914 for(let i = 0, len = cookies.length; i < len; ++i) {
15915 const parts = cookies[i].split('=');
15916 obj[parts.shift().trim()] = decodeURI(parts.join('='));
15917 }
15918 return obj;
15919 };
15920 const dataObj = { files: [] };
15921 const files = [];
15922 data.forEach(async (value, key) => {
15923 if(key !== 'files') {
15924 dataObj[key] = value;
15925 } else {
15926 files.push(value);
15927 }
15928 });
15929 for(let i = 0, len = files.length; i < len; ++i) {
15930 const file = files[i];
15931 if(file.type) {
15932 dataObj.files.push({
15933 content: `data:${ file.type };base64,${
15934 await getBase64(file).then(data => data.split(',')[1]) }`,
15935 name : file.name,
15936 spoiler : false
15937 });
15938 }
15939 }
15940 const cookieObj = getCookies();
15941 ajaxParams = {
15942 data: JSON.stringify({
15943 captchaId : cookieObj.captchaid,
15944 bypassId : cookieObj.bypass,
15945 parameters : dataObj,
15946 auth : { login: cookieObj.login, hash: cookieObj.hash }
15947 }),
15948 headers : { 'Content-Type': 'application/x-www-form-urlencoded' },
15949 method : 'POST'
15950 };
15951 }
15952 if(needProgress && hasFiles) {
15953 ajaxParams.onprogress = getUploadFunc();
15954 }
15955 const task = form.action.split('/').pop();
15956 const url = this._hasNewAPI ? `/${ task }?json=1` : '/.api/' + task.replace('.js', '');
15957 return $ajax(url, ajaxParams).then(xhr => xhr.responseText).catch(err => Promise.reject(err));
15958 }
15959 updateCaptcha() {
15960 $script('reloadCaptcha();');
15961 return null;
15962 }
15963 updateSubmitBtn(el) {
15964 el.textContent = Lng.reply[lang];
15965 }
15966 }
15967 ibEngines.push(['form[action$="contentActions.js"]', Lynxchan]);
15968
15969 class FoolFuuka extends BaseBoard {
15970 constructor(prot, dm) {
15971 super(prot, dm);
15972
15973 this.cReply = 'post_wrapper';
15974 this.qDForm = '#main';
15975 this.qImgInfo = '.post_file_metadata, .thread_image_box > .post_file';
15976 this.qOmitted = '.omitted_text';
15977 this.qPages = '.paginate > ul > li:nth-last-child(3)';
15978 this.qPostHeader = 'header';
15979 this.qPostImg = '.post_image, .thread_image';
15980 this.qPostMsg = '.text';
15981 this.qPostRef = '.post_data > a[data-function="quote"]';
15982 this.qPostSubj = '.post_title';
15983 this.qPostsParent = '.posts';
15984 this.qRPost = '.post[id]';
15985 this._qOPostEnd = '.posts';
15986
15987 this.docExt = '';
15988 this.firstPage = 1;
15989 this.res = 'thread/';
15990 }
15991 get qImgNameLink() {
15992 return '.post_file_filename';
15993 }
15994 get qThread() {
15995 return '.thread[id]';
15996 }
15997 get css() {
15998 return `.backlink_list { display: none !important; }
15999 .de-oppost > .thread_image_box { float: left; margin: 0 20px 10px 15px; text-align: center;
16000 color: #bfbfbf; font-size: .8em; line-height: 150%; }`;
16001 }
16002 get isArchived() {
16003 return true;
16004 }
16005 fixHTMLHelper(str) {
16006 return str.replace(/\/#(\d+)"/g, '#$1"').replace(/\/post\/(\d+)\/"/g, '/#$1"');
16007 }
16008 getImgWrap(img) {
16009 return img.parentNode.parentNode.parentNode;
16010 }
16011 getPageUrl(b, p) {
16012 return fixBrd(b) + (p > 1 ? `page/${ p }/` : '');
16013 }
16014 getTNum(thr) {
16015 return +thr.getAttribute('data-thread-num');
16016 }
16017 init() {
16018 defaultCfg.ajaxUpdThr = 0;
16019 const el = $q('.search_box');
16020 if(el) {
16021 docBody.appendChild(el);
16022 }
16023 return false;
16024 }
16025 parseURL() {
16026 super.parseURL();
16027 this.page = +(this.b.match(/\/page\/(\d+)/) || [1, 1])[1];
16028 this.b = this.b.replace(/\/page\/\d+/, '');
16029 }
16030 }
16031 ibEngines.push(['meta[name="generator"][content^="FoolFuuka"]', FoolFuuka]);
16032
16033 // DOMAINS
16034 class _02ch extends Kusaba {
16035 constructor(prot, dm) {
16036 super(prot, dm);
16037 this._02ch = true;
16038
16039 this.hasCatalog = true;
16040 this._capUpdPromise = null;
16041 }
16042 updateCaptcha(cap) {
16043 return cap.updateHelper('/captcha_update.php', xhr => {
16044 cap.parentEl.innerHTML = xhr.responseText;
16045 cap.textEl = $id('recaptcha_response_field');
16046 cap.initImage($q('img', cap.parentEl));
16047 cap.initTextEl();
16048 });
16049 }
16050 }
16051 ibDomains['02ch.su'] = _02ch;
16052
16053 class _2__ch extends BaseBoard {
16054 constructor(prot, dm) {
16055 super(prot, dm);
16056
16057 this.qPages = 'table[border="1"] td > a:last-of-type';
16058 this.qPostImg = 'img.thumb';
16059 this._qOPostEnd = 'table:not(.postfiles)';
16060
16061 this.docExt = '.html';
16062 this.hasPicWrap = true;
16063 this.jsonSubmit = true;
16064 this.markupBB = true;
16065 this.multiFile = true;
16066 this.ru = true;
16067 }
16068 get qThread() {
16069 return '.threadz';
16070 }
16071 get css() {
16072 return 'span[id$="_display"], #fastload { display: none; }';
16073 }
16074 get initCaptcha() {
16075 $id('captchadiv').innerHTML =
16076 `<img src="${ this.getCaptchaSrc() }" style="vertical-align: bottom;" id="imgcaptcha">`;
16077 return null;
16078 }
16079 fixFileInputs(el) {
16080 const str = '><input type="file" name="file"></div>';
16081 el.innerHTML = '<div' + str + ('<div style="display: none;"' + str).repeat(3);
16082 }
16083 fixHTMLHelper(str) {
16084 return str.replace(/data-original="\//g, 'src="/');
16085 }
16086 getCaptchaSrc() {
16087 return `/${ this.b }/captcha.fpl?${ Math.random() }`;
16088 }
16089 getImgWrap(img) {
16090 return img.parentNode.parentNode.parentNode;
16091 }
16092 getOmitted(el, len) {
16093 let txt;
16094 return el && (txt = el.textContent) ? +txt.match(/\d+/) - len : 1;
16095 }
16096 getPageUrl(b, p) {
16097 return `${ fixBrd(b) }${ p > 0 ? p : 0 }.memhtml`;
16098 }
16099 getSubmitData(json) {
16100 let error = null;
16101 let postNum = null;
16102 if(json.post) {
16103 postNum = +json.post;
16104 } else {
16105 error = Lng.error[lang];
16106 if(json.error) {
16107 error += ': ' + json.error.text;
16108 }
16109 }
16110 return { error, postNum };
16111 }
16112 init() {
16113 const btnEl = $q('#postform input[type="button"]');
16114 if(btnEl) {
16115 $replace(btnEl, '<input type="submit" value="Отправить">');
16116 }
16117 const dFormEl = $q(this.qDForm);
16118 $delAll('input[type="hidden"]', dFormEl);
16119 dFormEl.appendChild($q('.userdelete'));
16120 return false;
16121 }
16122 }
16123 ibDomains['2--ch.ru'] = _2__ch;
16124 ibDomains['2-ch.su'] = _2__ch;
16125
16126 class /* _2ch */ Makaba extends BaseBoard {
16127 constructor(prot, dm) {
16128 super(prot, dm);
16129 this.makaba = true;
16130
16131 this.cReply = 'de-reply-class';
16132 this.qBan = '.post__pomyanem';
16133 this.qClosed = '.sticky-img[src$="locked.png"]';
16134 this.qDForm = '#posts-form';
16135 this.qFormFile = '.postform__raw.filer input[type="file"]';
16136 this.qFormRedir = null;
16137 this.qFormRules = '.rules';
16138 this.qFormSubm = '#submit';
16139 this.qFormTd = '.postform__raw';
16140 this.qFormTr = '.postform__raw';
16141 this.qFormTxta = '#shampoo';
16142 this.qImgInfo = '.post__file-attr';
16143 this.qOmitted = '.thread__missed';
16144 this.qOPost = '.post_type_oppost';
16145 this.qPostHeader = '.post__details';
16146 this.qPostImg = '.post__file-preview';
16147 this.qPostMsg = '.post__message';
16148 this.qPostName = '.post__anon, .post__email';
16149 this.qPostRef = '.post__reflink:nth-child(2)';
16150 this.qPostSubj = '.post__title';
16151 this.qRPost = '.post_type_reply[data-num]';
16152 this.qTrunc = null;
16153
16154 this.formParent = 'thread';
16155 this.hasAltCaptcha = true;
16156 this.hasCatalog = true;
16157 this.hasOPNum = true;
16158 this.hasPicWrap = true;
16159 this.JsonBuilder = MakabaPostsBuilder;
16160 this.jsonSubmit = true;
16161 this.markupBB = true;
16162 this.multiFile = true;
16163 this.timePattern = 'dd+nn+yy+w+hh+ii+ss';
16164 this._capUpdPromise = null;
16165 }
16166 get qFormMail() {
16167 return 'input[name="email"]';
16168 }
16169 get qFormName() {
16170 return 'input[name="name"]';
16171 }
16172 get qFormSubj() {
16173 return 'input[name="subject"]';
16174 }
16175 get qImgNameLink() {
16176 return '.file-attr > .desktop, .post__file-attr > .desktop';
16177 }
16178 get css() {
16179 return `#alert-undefined, .cntnt__header > hr, .cntnt__right > hr, #CommentToolbar,
16180 .de-btn-src + a + a, #de-win-reply #tags, #down-nav-arrow, .media-expand-button,
16181 .media-thumbnail, .newpost, .post__btn:not(.icon_type_active), .post__message .icon,
16182 .post__number, .post__panel, .post__refmap, .postform__len, .postform-hr,
16183 .thread-nav > :not(.search), #up-nav-arrow { display: none !important; }
16184 .captcha { overflow: hidden; max-width: 300px; }
16185 .captcha > img { display: block; width: 364px; margin: -45px 0 -22px 0; }
16186 .de-btn-src + a { display: inline-flex; }
16187 .de-pview > .post__details { margin-left: 4px; }
16188 .de-reply-class { background: var(--theme_default_postbg);
16189 border: 1px solid var(--theme_default_border); border-radius: 3px; }
16190 .oekaki-height, .oekaki-width { width: 36px !important; }
16191 .post_type_reply { max-width: 100%; }
16192 .postform { width: auto; }
16193 .postform__sticker-btn, .postform__sticker-prev { bottom: ` +
16194 `${ !Cfg.txtBtnsLoc || !Cfg.addTextBtns ? 3 :
16195 Cfg.addTextBtns === 1 ? 28 : Cfg.addTextBtns === 2 ? 19 : 25 }px !important; }
16196 ${ Cfg.addSageBtn ? `.options__box[onclick="ToggleSage()"]
16197 { display: none !important; }` : '' }
16198 ${ Cfg.expandTrunc ? `.expand-large-comment,
16199 div[id^="shrinked-post"] { display: none !important; }
16200 div[id^="original-post"] { display: block !important; }` : '' }
16201 ${ Cfg.imgNames === 2 ? `.post__filezise { display: inline !important; }
16202 .post__file-attr { margin-bottom: 1px; }` : '' }
16203 ${ Cfg.noSpoilers ? '.spoiler::after { width: 0; }' : '' }`;
16204 }
16205 get isArchived() {
16206 return this.b.includes('/arch');
16207 }
16208 get lastPage() {
16209 const els = $Q('.pager > a:not([class])');
16210 const value = els ? els.length : 1;
16211 Object.defineProperty(this, 'lastPage', { value });
16212 return value;
16213 }
16214 get markupTags() {
16215 return ['B', 'I', 'U', 'S', 'SPOILER', '', 'SUP', 'SUB'];
16216 }
16217 get reportForm() {
16218 const value = (pNum, tNum) => ($q('input[type="button"]', $popup(
16219 'edit-report',
16220 `<input name="comment" value="" placeholder="${
16221 pNum === tNum ? Lng.reportThr[lang] : Lng.reportPost[lang]
16222 }" type="text"> <input value="OK" type="button">`)
16223 ).onclick = e => {
16224 const inpEl = e.target.previousElementSibling;
16225 if(!inpEl.value) {
16226 inpEl.classList.add('de-input-error');
16227 return;
16228 }
16229 const formData = new FormData();
16230 formData.append('task', 'report');
16231 formData.append('board', this.b);
16232 formData.append('thread', tNum);
16233 formData.append('posts', pNum);
16234 formData.append('comment', inpEl.value);
16235 closePopup('edit-report');
16236 $popup('report', Lng.sending[lang], true);
16237 $ajax('/makaba/makaba.fcgi?json=1', { method: 'POST', data: formData }).then(xhr => {
16238 let obj;
16239 try {
16240 obj = JSON.parse(xhr.responseText);
16241 } catch(e) {}
16242 $popup('report', !obj ? Lng.error[lang] + ': ' + xhr.responseText :
16243 (obj.message || Lng.succReported[lang]) + ': ' + obj.message_title);
16244 });
16245 });
16246 Object.defineProperty(this, 'reportForm', { value });
16247 return value;
16248 }
16249 deleteTruncMsg(post, el) {
16250 el.previousSibling.remove();
16251 $show(el.previousSibling);
16252 el.remove();
16253 }
16254 fixFileInputs(el) {
16255 el.innerHTML = Array.from({ length: 8 }, (val, i) =>
16256 `<div${ i ? ' style="display: none;"' : '' }><input type="file" name="formimages[]"></div>`
16257 ).join('');
16258 }
16259 getBanId(postEl) {
16260 const el = $q(this.qBan, postEl);
16261 return !el ? 0 : el.textContent.includes('предупрежден') ? 2 : 1;
16262 }
16263 getImgWrap(img) {
16264 return $parent(img, 'FIGURE');
16265 }
16266 getJsonApiUrl(brd, tNum) {
16267 return `/${ brd }/res/${ tNum }.json`;
16268 }
16269 getPNum(post) {
16270 return +post.getAttribute('data-num');
16271 }
16272 getPostWrap(el) {
16273 return el.parentNode;
16274 }
16275 getSage(post) {
16276 this.getSage = !$q('span[id^="id_tag_"]') ? super.getSage : post => {
16277 return !$q('span[id^="id_tag_"], .post__ophui', post);
16278 };
16279 return this.getSage(post);
16280 }
16281 getSubmitData(json) {
16282 let error = null;
16283 let postNum = null;
16284 if(json.Status === 'OK') {
16285 postNum = +json.Num;
16286 } else if(json.Status === 'Redirect') {
16287 postNum = +json.Target;
16288 } else {
16289 error = Lng.error[lang] + ': ' + json.Reason;
16290 }
16291 return { error, postNum };
16292 }
16293 init() {
16294 if($q('section.posts')) { // Old Makaba engine
16295 this.cReply = 'post reply';
16296 this.qBan = '.pomyanem';
16297 this.qFormFile = 'tr input[type="file"]';
16298 this.qFormRules = '.rules-area';
16299 this.qFormTd = 'td';
16300 this.qFormTr = 'tr';
16301 this.qImgInfo = '.file-attr';
16302 this.qOmitted = '.mess-post';
16303 this.qOPost = '.oppost';
16304 this.qPostHeader = '.post-details';
16305 this.qPostImg = '.preview';
16306 this.qPostMsg = '.post-message';
16307 this.qPostName = '.ananimas, .post-email';
16308 this.qPostRef = '.reflink';
16309 this.qPostSubj = '.post-title';
16310 this.qRPost = '.post.reply[data-num]';
16311 const { css } = this;
16312 Object.defineProperty(this, 'css', {
16313 configurable : true,
16314 get : () => `${ css }
16315 #ABU-alert-wait, .ABU-refmap, .fa-media-icon, .kupi-passcode-suka, .logo + hr,
16316 .media-expand-button, #media-thumbnail, .message-byte-len, .nav-arrows, .norm-reply,
16317 .postform-hr, .postpanel > :not(img), .posts > hr, .reflink::before,
16318 .thread-nav > :not(.search), .toolbar-area { display: none !important; }
16319 ${ Cfg.addSageBtn ? `.box[onclick="ToggleSage()"] {
16320 display: none !important; }` : '' }
16321 ${ Cfg.imgNames === 2 ? `.filesize { display: inline !important; }
16322 .file-attr { margin-bottom: 1px; }` : '' }`
16323 });
16324 }
16325 $script(`(function() {
16326 function fixGlobalFunc(name) {
16327 Object.defineProperty(window, name,
16328 { value: Function.prototype, writable: false, configurable: false });
16329 }
16330 fixGlobalFunc("$alert");
16331 fixGlobalFunc("autorefresh_start"); // Old makaba only
16332 fixGlobalFunc("linkremover");
16333 fixGlobalFunc("Media");
16334 fixGlobalFunc("MExpandMedia");
16335 window.FormData = void 0;
16336 $(function() { $(window).off(); });
16337 })();`);
16338 $each($Q('.autorefresh'), el => {
16339 const inpEl = $q('input', el);
16340 if(inpEl.checked) {
16341 inpEl.click();
16342 }
16343 el.remove();
16344 });
16345 const el = $id('shampoo');
16346 if(el) {
16347 el.tabIndex = 1;
16348 }
16349 return false;
16350 }
16351 initCaptcha(cap) {
16352 const box = $q('.captcha-box, .captcha');
16353 if(!Cfg.altCaptcha) {
16354 box.innerHTML = `<div id="captcha-widget-main"></div>
16355 <input name="captcha_type" value="invisible_recaptcha" type="hidden">`;
16356 return null;
16357 }
16358 const img = box.firstChild;
16359 if(!img || img.tagName !== 'IMG') {
16360 box.innerHTML = `<img>
16361 <input name="2chaptcha_value" maxlength="6" type="text">
16362 <input name="captcha_type" value="2chaptcha" type="hidden">
16363 <input name="2chaptcha_id" type="hidden">`;
16364 const [img, inp] = [...box.children];
16365 img.onclick = () => this.updateCaptcha(cap);
16366 inp.tabIndex = 999;
16367 cap.textEl = inp;
16368 }
16369 return null;
16370 }
16371 observeContent(checkDomains, dataPromise) {
16372 if($q('#posts-form > .thread, form[de-form] > .thread')) {
16373 return true;
16374 }
16375 const initObserver = new MutationObserver(mutations => {
16376 const el = mutations[0].addedNodes[0];
16377 if(el && el.className === 'thread') {
16378 initObserver.disconnect();
16379 runMain(checkDomains, dataPromise);
16380 }
16381 });
16382 const el = $q('#posts-form, form[de-form]');
16383 if(el) {
16384 initObserver.observe(el, { childList: true });
16385 }
16386 return false;
16387 }
16388 updateCaptcha(cap) {
16389 const url = Cfg.altCaptcha ? `/api/captcha/2chaptcha/id?board=${ this.b }&thread=` + pr.tNum :
16390 '/api/captcha/invisible_recaptcha/id';
16391 return cap.updateHelper(url, xhr => {
16392 const box = $q('.captcha-box, .captcha');
16393 let data = xhr.responseText;
16394 try {
16395 data = JSON.parse(data);
16396 } catch(err) {}
16397 if(cap.isSubmitWait && data.result !== 1) {
16398 pr.subm.click();
16399 }
16400 switch(data.result) {
16401 case 0: box.textContent = 'Пасскод недействителен. Перелогиньтесь.'; break;
16402 case 2: box.textContent = 'Вы - пасскодобоярин.'; break;
16403 case 3: return CancelablePromise.reject(new CancelError()); // Captcha is disabled
16404 case 1: // Captcha is enabled
16405 if(data.type === 'invisible_recaptcha') {
16406 if(!cap.isSubmitWait) {
16407 break;
16408 }
16409 $q('.captcha__key').value = data.id;
16410 $script($id('captcha-widget').hasChildNodes() ?
16411 `grecaptcha.reset(deCapWidget);
16412 grecaptcha.execute(deCapWidget);` :
16413 `deCapWidget = grecaptcha.render('captcha-widget', {
16414 sitekey : '${ data.id }',
16415 theme : 'light',
16416 size : 'invisible',
16417 callback: function() {
16418 var el = document.getElementById('captcha-widget-main');
16419 el.innerHTML = '<input type="hidden" name="g-recaptcha-response">';
16420 el.firstChild.value = grecaptcha.getResponse();
16421 document.getElementById('submit').click();
16422 }
16423 });
16424 grecaptcha.execute(deCapWidget);`);
16425 break;
16426 } else if(data.type === '2chaptcha') {
16427 const img = box.firstChild;
16428 img.src = '';
16429 img.src = `/api/captcha/2chaptcha/image/${ data.id }`;
16430 box.lastChild.value = data.id;
16431 break;
16432 }
16433 /* falls through */
16434 default: box.innerHTML = data;
16435 }
16436 });
16437 }
16438 }
16439 ibDomains['2ch.hk'] = Makaba;
16440 ibDomains['2ch.pm'] = Makaba;
16441 ibDomains['2ch.re'] = Makaba;
16442 ibDomains['2ch.wf'] = Makaba;
16443
16444 class _2chan extends BaseBoard {
16445 constructor(prot, dm) {
16446 super(prot, dm);
16447
16448 this.qDForm = 'form:not([enctype])';
16449 this.qForm = '#fm';
16450 this.qFormRedir = null;
16451 this.qFormRules = '.chui';
16452 this.qOmitted = 'font[color="#707070"]';
16453 this.qPostImg = 'a[href$=".jpg"] > img, a[href$=".png"] > img, a[href$=".gif"] > img';
16454 this.qPostRef = '.del';
16455 this.qRPost = 'td:nth-child(2)';
16456
16457 this.docExt = '.htm';
16458 this.formParent = 'resto';
16459 }
16460 get qImgNameLink() {
16461 return 'a[href$=".jpg"], a[href$=".png"], a[href$=".gif"]';
16462 }
16463 get qThread() {
16464 return '.thre';
16465 }
16466 get css() {
16467 return `.ftbl { width: auto; margin: 0; }
16468 .reply { background: #f0e0d6; }
16469 span { font-size: inherit; }`;
16470 }
16471 getPageUrl(b, p) {
16472 return fixBrd(b) + (p > 0 ? p + this.docExt : 'futaba.htm');
16473 }
16474 getPNum(post) {
16475 return +$q('input', post).name;
16476 }
16477 getPostElOfEl(el) {
16478 while(el && el.tagName !== 'TD' && !el.hasAttribute('de-thread')) {
16479 el = el.parentElement;
16480 }
16481 return el;
16482 }
16483 getTNum(thr) {
16484 return +$q('input[type="checkbox"]', thr).name.match(/\d+/);
16485 }
16486 init() {
16487 $del($q('base', doc.head)); // <base> is not compartible with SVG
16488 return false;
16489 }
16490 }
16491 ibDomains['2chan.net'] = _2chan;
16492
16493 class _2channel extends Makaba {
16494 constructor(prot, dm) {
16495 super(prot, dm);
16496 this._2channel = true;
16497
16498 this.hasAltCaptcha = false;
16499 }
16500 get reportForm() {
16501 return null;
16502 }
16503 fixFileInputs(el) {
16504 el.innerHTML = Array.from({ length: 4 }, (val, i) =>
16505 `<div${ i ? ' style="display: none;"' : '' }><input type="file" name="formimages[]"></div>`
16506 ).join('');
16507 }
16508 fixHTMLHelper(str) {
16509 return str.replace(/src="[^>]+" data-src="/g, 'src="');
16510 }
16511 getCapParent(el) {
16512 return $q('.captcha');
16513 }
16514 init() {
16515 super.init();
16516 this.qFormFile = 'input[name="formimages[]"]';
16517 this.qFormTd = 'div[class^="freply__"]';
16518 this.qFormTr = 'div[class^="freply__"]';
16519 const { css } = this;
16520 Object.defineProperty(this, 'css', {
16521 configurable : true,
16522 get : () => `${ css }
16523 #AlertBox, .postform__checkbox.first, .postform__header, .refmap, #youtube-thumb-float
16524 { display: none !important; }
16525 .de-win-open:not(#de-win-cfg) > .de-win-body { background-color: #eee !important; }
16526 .preview.lazy { opacity: 1; }`
16527 });
16528 let el = $q('.captcha');
16529 if(el) {
16530 $before($q('.freply__files-and-captcha'), el);
16531 }
16532 el = $id('postform');
16533 if(el) {
16534 el.setAttribute('action', el.getAttribute('action') + '?json=1');
16535 }
16536 return false;
16537 }
16538 initCaptcha(cap) {
16539 return this.updateCaptcha(cap);
16540 }
16541 updateCaptcha(cap) {
16542 const url = `/api/captcha/service_id?board=${ this.b }&thread=` + pr.tNum;
16543 return cap.updateHelper(url, xhr => {
16544 const box = $q('.captcha');
16545 let data = xhr.responseText;
16546 try {
16547 data = JSON.parse(data);
16548 } catch(err) {}
16549 switch(data.result) {
16550 case 1: { // Captcha is enabled
16551 const el = $q('.captcha__image');
16552 const img = $q('img', el) || $aBegin(el, '<img>');
16553 img.src = '';
16554 img.src = `/api/captcha/image/${ data.id }`;
16555 $q('input[name="captcha_id"]').value = data.id;
16556 break;
16557 }
16558 case 2: return CancelablePromise.reject(new CancelError()); // Captcha is disabled
16559 case 3: box.innerHTML = 'Вам больше не нужно вводить капчу.'; break;
16560 default: box.innerHTML = data;
16561 }
16562 $show(box);
16563 box.removeAttribute('hidden');
16564 cap.textEl.tabIndex = 999;
16565 });
16566 }
16567 }
16568 ibDomains['2channel.moe'] = _2channel;
16569 ibDomains['2channel5xx5xchx.onion'] = _2channel;
16570
16571 class _2chRip extends BaseBoard {
16572 constructor(prot, dm) {
16573 super(prot, dm);
16574
16575 this.jsonSubmit = true;
16576 this.ru = true;
16577
16578 this._capUpdPromise = null;
16579 }
16580 get css() {
16581 return `small[id^="rfmap_"], #submit_button, .qreply_btn { display: none; }
16582 #subject + div { display: inline !important; }
16583 .replypage .reply .reflink::before { content: "" }`;
16584 }
16585 getSubmitData(json) {
16586 return {
16587 error : json.message ? json.message_title + ': ' + json.message : null,
16588 postNum : json.num ? +json.num : null
16589 };
16590 }
16591 init() {
16592 $script('postFormSubmit = Function.prototype;');
16593 $bEnd($id('postform'), '<input type="hidden" name="json" value="1">');
16594 return false;
16595 }
16596 updateCaptcha(cap) {
16597 return cap.updateHelper('/cgi/captcha?task=get_id', ({ responseText: id }) => {
16598 $id('imgcaptcha').src = '/cgi/captcha?task=get_image&id=' + id;
16599 $id('captchaid').value = id;
16600 });
16601 }
16602 }
16603 ibDomains['2ch.rip'] = _2chRip;
16604 ibDomains['dva-ch.net'] = _2chRip;
16605
16606 class _410chan extends Kusaba {
16607 constructor(prot, dm) {
16608 super(prot, dm);
16609
16610 this.qFormRedir = 'input#noko';
16611 this.qPages = '.pgstbl > table > tbody > tr > td:nth-child(2)';
16612
16613 this.ru = true;
16614 this.hasCatalog = true;
16615 this.markupBB = false;
16616 this.timePattern = 'dd+nn+yyyy++w++hh+ii+ss';
16617 this._capUpdPromise = null;
16618 }
16619 get capLang() {
16620 return 0;
16621 }
16622 get css() {
16623 return `${ super.css }
16624 #resizer { display: none; }
16625 form > span { margin-top: 5px; }
16626 .de-thr-hid { display: inherit; }
16627 .reflink::after { content: none !important; }
16628 .spoiler-image:hover::after { content: none !important; }
16629 .topmenu { z-index: 1; }`;
16630 }
16631 get markupTags() {
16632 return ['**', '*', '__', '^^', '%%', '`'];
16633 }
16634 getCaptchaSrc(src) {
16635 return src.replace(/\?[^?]+$|$/, `?board=${ aib.b }&${ Math.random() }`);
16636 }
16637 getSage(post) {
16638 const el = $q('.filetitle', post);
16639 return !!el && el.textContent.includes('\u21E9');
16640 }
16641 updateCaptcha(cap) {
16642 return cap.updateHelper(`/api_adaptive.php?board=${ this.b }`, xhr => {
16643 if(xhr.responseText === '1') {
16644 cap.textEl.disabled = true;
16645 setTimeout(() => (cap.textEl.value = 'проезд оплачен'), 0);
16646 return;
16647 }
16648 cap.textEl.disabled = false;
16649 cap.textEl.value = '';
16650 const img = $q('img', cap.parentEl);
16651 const src = img.getAttribute('src');
16652 img.src = '';
16653 img.src = this.getCaptchaSrc(src);
16654 });
16655 }
16656 }
16657 ibDomains['410chan.org'] = _410chan;
16658
16659 class _4chan extends BaseBoard {
16660 constructor(prot, dm) {
16661 super(prot, dm);
16662 this._4chan = true;
16663
16664 this.cReply = 'post reply';
16665 this.qBan = 'strong[style="color: red;"]';
16666 this.qClosed = '.archivedIcon';
16667 this.qDelBut = '.deleteform > input[type="submit"]';
16668 this.qError = '#errmsg';
16669 this.qForm = 'form[name="post"]';
16670 this.qFormRedir = null;
16671 this.qImgInfo = '.fileText';
16672 this.qOmitted = '.summary.desktop';
16673 this.qOPost = '.op';
16674 this.qPages = '.pagelist > .pages:not(.cataloglink) > a:last-of-type';
16675 this.qPostHeader = '.postInfo';
16676 this.qPostImg = '.fileThumb > img:not(.fileDeletedRes)';
16677 this.qPostName = '.name';
16678 this.qPostRef = '.postInfo > .postNum';
16679 this.qPostSubj = '.subject';
16680 this._qOPostEnd = '.replyContainer';
16681
16682 this.anchor = '#p';
16683 this.docExt = '';
16684 this.firstPage = 1;
16685 this.formParent = 'resto';
16686 this.hasAltCaptcha = true;
16687 this.hasCatalog = true;
16688 this.hasTextLinks = true;
16689 this.JsonBuilder = _4chanPostsBuilder;
16690 this.res = 'thread/';
16691 this.timePattern = 'nn+dd+yy+w+hh+ii-?s?s?';
16692 }
16693 get qFormSubj() {
16694 return 'input[name="sub"]';
16695 }
16696 get qImgNameLink() {
16697 return '.fileText > a';
16698 }
16699 get css() {
16700 return `.backlink, #blotter, .de-file-utils + .desktop, .extButton, hr.desktop, .navLinks,
16701 .postMenuBtn, #togglePostFormLink { display: none !important; }
16702 #bottomReportBtn { display: initial !important; }
16703 #g-recaptcha { height: initial; }
16704 .postForm { display: table !important; width: auto !important; }
16705 textarea { margin-right: 0 !important; }
16706 ${ Cfg.widePosts ? '.sideArrows { display: none; }' : '' }`;
16707 }
16708 get markupTags() {
16709 return ['', '', '', '', $q('input[type="checkbox"][name="spoiler"]') ? '[spoiler' : '',
16710 this.b === 'g' ? '[code' : ''];
16711 }
16712 get updateCaptcha() {
16713 let value = null;
16714 const tr = $id('captchaFormPart');
16715 if(tr) {
16716 value = () => {
16717 if(Cfg.altCaptcha) {
16718 $id('g-recaptcha').innerHTML = $q('noscript', tr).innerHTML;
16719 } else {
16720 $replace($id('g-recaptcha'), '<div id="g-recaptcha"></div>');
16721 $script('initRecaptcha();');
16722 }
16723 tr.removeAttribute('onclick');
16724 return null;
16725 };
16726 }
16727 Object.defineProperty(this, 'updateCaptcha', { value });
16728 return value;
16729 }
16730 fixDeadLinks(str) {
16731 return str.replace(/<span class="deadlink">>>(\d+)<\/span>/g,
16732 '<a class="de-ref-del deadlink" href="#p$1">>>$1</a>');
16733 }
16734 fixHTMLHelper(str) {
16735 return str.replace(/<span>([^<]+)(?:<\/?wbr>)?([^<]+)<\/span> \[<a [^>]+>Embed<\/a>\]/g, '$1$2')
16736 .replace(/<\/?wbr>/g, '').replace(/( \(OP\)| →)<\/a/g, '</a');
16737 }
16738 fixVideo(isPost, data) {
16739 return [];
16740 }
16741 getImgInfo(wrap) {
16742 const el = $q(this.qImgInfo, wrap);
16743 return el ? el.lastChild.textContent : '';
16744 }
16745 getImgRealName(wrap) {
16746 const el = $q(this.qImgNameLink, wrap);
16747 return el ? el.title || el.parentNode.title || el.textContent : '';
16748 }
16749 getJsonApiUrl(brd, tNum) {
16750 return `//a.4cdn.org/${ brd }/thread/${ tNum }.json`;
16751 }
16752 getImgWrap(img) {
16753 return img.parentNode.parentNode;
16754 }
16755 getPageUrl(b, p) {
16756 return fixBrd(b) + (p > 1 ? p : '');
16757 }
16758 getPostWrap(el) {
16759 return el.parentNode;
16760 }
16761 getSage(post) {
16762 return !!$q('.id_Heaven, .useremail[href^="mailto:sage"]', post);
16763 }
16764 getSubmitData(dc) {
16765 let error = null;
16766 let postNum = null;
16767 const errEl = $q('#errmsg', dc);
16768 if(errEl) {
16769 error = errEl.innerHTML;
16770 } else {
16771 try {
16772 postNum = +$q('h1', dc).nextSibling.textContent.match(/no:(\d+)/)[1];
16773 } catch(err) {}
16774 }
16775 return { error, postNum };
16776 }
16777 getTNum(thr) {
16778 return +$q('input[type="checkbox"]', thr).name.match(/\d+/);
16779 }
16780 init() {
16781 Cfg.findImgFile = 0;
16782 Cfg.txtBtnsLoc = 0;
16783 const el = $id('styleSelector');
16784 if(el) {
16785 el.setAttribute('onchange', 'setActiveStyleSheet(this.value);');
16786 }
16787 return false;
16788 }
16789 }
16790 ibDomains['4chan.org'] = _4chan;
16791 ibDomains['4channel.org'] = _4chan;
16792
16793 class _55chan extends Vichan {
16794 constructor(prot, dm) {
16795 super(prot, dm);
16796
16797 this.qFormRules = '.regras';
16798
16799 this._capUpdPromise = null;
16800 }
16801 get qImgNameLink() {
16802 return '.fileinfo > a:last-of-type';
16803 }
16804 get qThread() {
16805 return 'div[data-board]';
16806 }
16807 get css() {
16808 return `${ super.css }
16809 input.delete, #post-moderation-fields { display: initial !important; }`;
16810 }
16811 initCaptcha(cap) {
16812 $q('td', cap.parentEl).innerHTML = `<input placeholder="${ Lng.cap[lang] }" class="captcha_text` +
16813 `" type="text" name="captcha_text" size="25" maxlength="8" autocomplete="off">
16814 <input class="captcha_cookie de-input-hidden" name="captcha_cookie" type="hidden">
16815 <div class="captcha_html"></div>`;
16816 cap.textEl = $q('.captcha_text', cap.parentEl);
16817 return this.updateCaptcha(cap, true);
16818 }
16819 updateCaptcha(cap) {
16820 return cap.updateHelper(
16821 '/8chan-captcha/entrypoint.php?mode=get&extra=abcdefghijklmnopqrstuvwxyz',
16822 xhr => {
16823 const obj = JSON.parse(xhr.responseText);
16824 $q('.captcha_cookie', cap.parentEl).value = obj.cookie;
16825 $q('.captcha_html', cap.parentEl).innerHTML = obj.captchahtml;
16826 const img = $q('img', cap.parentEl);
16827 if(img) {
16828 cap.initImage(img);
16829 }
16830 });
16831 }
16832 }
16833 ibDomains['55chan.org'] = _55chan;
16834
16835 class Archived extends FoolFuuka {
16836 getImgRedirectSrc(url) {
16837 return $ajax(url).then(xhr => xhr.responseText.match(/<meta[^>]+url=([^"]+)">/)[1]);
16838 }
16839 }
16840 ibDomains['archived.moe'] = Archived;
16841
16842 class Arhivach extends BaseBoard {
16843 constructor(prot, dm) {
16844 super(prot, dm);
16845
16846 this.cReply = 'post';
16847 this.qDelBut = null;
16848 this.qDelPassw = null;
16849 this.qDForm = 'body > .container-fluid';
16850 this.qPostHeader = '.post_head';
16851 this.qPostImg = '.post_image > img';
16852 this.qPostMsg = '.post_comment_body';
16853 this.qPostRef = '.post_id, .post_head > b';
16854 this.qPostSubj = '.post_subject';
16855 this.qRPost = '.post[postid]:not([postid=""])';
16856
16857 this.docExt = '';
16858 this.hasOPNum = true;
16859 this.res = 'thread/';
16860 }
16861 get qImgNameLink() {
16862 return '.img_filename';
16863 }
16864 get qThread() {
16865 return '.thread_inner';
16866 }
16867 get css() {
16868 return `.media-expand-button, .post_replies, .post_num, .poster_sage { display: none !important; }
16869 .navbar-fixed-top, .thread_header_fixed { z-index: 5 !important; }
16870 .post { overflow-x: auto !important; }
16871 .thread_inner img.de-fullimg { max-width: 100% !important; max-height: 100% !important; }`;
16872 }
16873 get isArchived() {
16874 return true;
16875 }
16876 fixHTML(data, isForm) {
16877 const formEl = super.fixHTML(data, isForm);
16878 const links = $Q('.expand_image', formEl);
16879 for(let i = 0, len = links.length; i < len; ++i) {
16880 const link = links[i];
16881 link.href = link.getAttribute('onclick').match(/https?:\/[^']+/)[0];
16882 link.removeAttribute('onclick');
16883 }
16884 return formEl;
16885 }
16886 getImgInfo(wrap) {
16887 return wrap.title;
16888 }
16889 getImgWrap(img) {
16890 return $parent(img, 'A').parentNode;
16891 }
16892 getOp(el) {
16893 return $q('.post:first-child', el);
16894 }
16895 getPNum(post) {
16896 return +post.getAttribute('postid');
16897 }
16898 getSage(post) {
16899 return !!$q('.poster_sage', post);
16900 }
16901 getThrUrl() {
16902 return $q('link[rel="canonical"]', doc.head).href;
16903 }
16904 getTNum(thr) {
16905 return this.getPNum(this.getOp(thr));
16906 }
16907 init() {
16908 defaultCfg.ajaxUpdThr = 0;
16909 setTimeout(() => {
16910 const delPosts = $Q('.post_deleted');
16911 for(let i = 0, len = delPosts.length; i < len; ++i) {
16912 const post = pByNum.get(this.getPNum(delPosts[i]));
16913 if(post) {
16914 post.thr.deletePosts(post, false, false);
16915 }
16916 }
16917 $css(`.post { background-color: ${
16918 getComputedStyle($q('.post')).getPropertyValue('background-color') } !important; }`);
16919 }, 500);
16920 return false;
16921 }
16922 }
16923 ibDomains['arhivach.ng'] = Arhivach;
16924 ibDomains['arhivachovtj2jrp.onion'] = Arhivach;
16925
16926 class Animach extends Vichan {
16927 constructor(prot, dm) {
16928 super(prot, dm);
16929
16930 this.hasRefererErr = false;
16931 this.markupBB = true;
16932 }
16933 get css() {
16934 return `${ super.css }\r\n\t${
16935 Cfg.noSpoilers ? `span.spoiler, span.spoiler:hover { ${
16936 Cfg.noSpoilers === 1 ? 'color: #F5F5F5 !important; background-color: #888 !important' :
16937 'color: inherit !important' }; transition: none !important; }` : '' }
16938 #thread-interactions { display: none; }
16939 .reflink::after { content: "" !important; }`;
16940 }
16941 get markupTags() {
16942 return ['b', 'i', 'u', 's', 'spoiler', ''];
16943 }
16944 getImgWrap(img) {
16945 return img.parentNode.parentNode;
16946 }
16947 }
16948 ibDomains['animach.pw'] = Animach;
16949
16950 ibDomains['desuchan.moe'] = BaseBoard;
16951 ibDomains['desuchan.net'] = BaseBoard;
16952
16953 class CrystalCafe extends Tinyboard {
16954 constructor(prot, dm) {
16955 super(prot, dm);
16956
16957 this.qRPost = '.post.reply';
16958 }
16959 get qImgNameLink() {
16960 return '.fileinfo > a[title]';
16961 }
16962 getImgInfo(wrap) {
16963 return $q(this.qImgNameLink, wrap).title;
16964 }
16965 getTNum(thr) {
16966 return +thr.id.match(/\d+/);
16967 }
16968 }
16969 ibDomains['crystal.cafe'] = CrystalCafe;
16970
16971 class Diochan extends Kusaba {
16972 constructor(prot, dm) {
16973 super(prot, dm);
16974
16975 this.qImgInfo = '.filesize, .fileinfo';
16976
16977 this.multiFile = true;
16978 }
16979 get qImgNameLink() {
16980 return '.filesize > a, .file_reply > a';
16981 }
16982 get css() {
16983 return `${ super.css }
16984 .resize, .backlink, .postblock, .sage { display: none; }`;
16985 }
16986 fixFileInputs(el) {
16987 const str = `><input type="file" name="imagefile[]">${ $q('#spoiler') ?
16988 '<input type="checkbox" name="spoiler" style="display: none;">' : '' }</div>`;
16989 el.innerHTML = '<div' + str + ('<div style="display: none;"' + str).repeat(2);
16990 $delAll('.file2, .file3, .fileurl1, .fileurl2, .fileurl3');
16991 }
16992 }
16993 ibDomains['diochan.com'] = Diochan;
16994
16995 class Dobrochan extends BaseBoard {
16996 constructor(prot, dm) {
16997 super(prot, dm);
16998 this.dobrochan = true;
16999
17000 this.qClosed = 'img[src="/images/locked.png"]';
17001 this.qDForm = 'form[action*="delete"]';
17002 this.qError = '.post-error, h2';
17003 this.qFormRedir = 'select[name="goto"]';
17004 this.qImgInfo = '.fileinfo';
17005 this.qOmitted = '.abbrev > span:last-of-type';
17006 this.qPages = '.pages > tbody > tr > td';
17007 this.qPostMsg = '.postbody';
17008 this.qPostSubj = '.replytitle';
17009 this.qTrunc = '.abbrev > span:first-of-type';
17010
17011 this.anchor = '#i';
17012 this.formParent = 'thread_id';
17013 this.hasPicWrap = true;
17014 this.JsonBuilder = DobrochanPostsBuilder;
17015 this.multiFile = true;
17016 this.ru = true;
17017 this.timePattern = 'dd+m+?+?+?+?+?+yyyy++w++hh+ii-?s?s?';
17018 }
17019 get css() {
17020 return `.de-video-obj-inline { margin-left: 5px; }
17021 .delete > img, .popup, .reply_, .search_google, .search_iqdb { display: none; }
17022 .delete { background: none; }
17023 .delete_checkbox { position: static !important; }`;
17024 }
17025 deleteTruncMsg(post, el, isInit) {
17026 [el.previousSibling, el.nextSibling, el].forEach($del);
17027 if(isInit) {
17028 $replace(post.msg.firstElementChild, $q('.alternate > div', post.el));
17029 } else {
17030 const sRunner = new SpellsRunner();
17031 post.updateMsg($q('.alternate > div', post.el), sRunner);
17032 sRunner.endSpells();
17033 }
17034 }
17035 disableRedirection(el) {
17036 $hide($parent(el, 'TR'));
17037 el.selectedIndex = 1;
17038 }
17039 fixFileInputs(el) {
17040 $each($Q('input[type="file"]', el), el => el.removeAttribute('onchange'));
17041 el.firstElementChild.value = 1;
17042 }
17043 getImgSrcLink(img) {
17044 // There can be a censored <img> without <a> parent
17045 const el = img.parentNode;
17046 return el.tagName === 'A' ? el :
17047 $q('.fileinfo > a', img.previousElementSibling ? el : el.parentNode);
17048 }
17049 getImgWrap(img) {
17050 const el = img.parentNode;
17051 return el.tagName === 'A' ? (el.previousElementSibling ? el : el.parentNode).parentNode :
17052 img.previousElementSibling ? el : el.parentNode;
17053 }
17054 getJsonApiUrl(brd, tNum) {
17055 return `/api/thread/${ brd }/${ tNum }/all.json?new_format&message_html&board`;
17056 }
17057 getOmitted(el) {
17058 while(el) {
17059 const m = el.textContent.match(/(\d+) posts are omitted/);
17060 if(m) {
17061 return +m[1] + 1;
17062 }
17063 el = el.previousElementSibling;
17064 }
17065 return 1;
17066 }
17067 getPageUrl(b, p) {
17068 return fixBrd(b) + (p > 0 ? p + this.docExt : 'index.xhtml');
17069 }
17070 getTNum(thr) {
17071 return +$q('a[name]', thr).name.match(/\d+/);
17072 }
17073 init() {
17074 if(deWindow.location.pathname === '/settings') {
17075 $q('input[type="button"]').addEventListener('click',
17076 () => readCfg().then(() => saveCfg('__hanarating', $id('rating').value)));
17077 return true;
17078 }
17079 $script('UploadProgress = Function.prototype;');
17080 const el = $id('postform');
17081 if(el) {
17082 el.appendChild($q('.rules'));
17083 }
17084 return false;
17085 }
17086 initCaptcha(cap) {
17087 if(!cap.textEl) {
17088 $hide($q('img', cap.parentEl));
17089 $show(cap.parentEl);
17090 }
17091 return null;
17092 }
17093 insertYtPlayer(msg, playerHtml) {
17094 const prev = msg.previousElementSibling;
17095 return $bBegin(prev.tagName === 'BR' ? prev : msg, playerHtml);
17096 }
17097 updateCaptcha(cap, isErr) {
17098 const img = $q('img', cap.parentEl);
17099 if(!img) {
17100 return null;
17101 }
17102 if(cap.textEl) {
17103 const src = img.getAttribute('src').split('/').slice(0, -1).join('/') +
17104 `/${ Date.now() }.png`;
17105 img.src = '';
17106 img.src = src;
17107 cap.textEl.value = '';
17108 } else if(isErr) {
17109 const el = img.parentNode;
17110 el.innerHTML = '';
17111 el.appendChild(img);
17112 img.insertAdjacentHTML('afterend', '<br><input placeholder="Капча" autocomplete="off"' +
17113 ' id="captcha" name="captcha" size="35" type="text">');
17114 $show(img);
17115 cap.isAdded = false;
17116 cap.originHTML = cap.parentEl.innerHTML;
17117 cap.addCaptcha();
17118 }
17119 return null;
17120 }
17121 }
17122 ibDomains['dobrochan.com'] = Dobrochan;
17123 ibDomains['dobrochan.net'] = Dobrochan;
17124 ibDomains['dobrochan.org'] = Dobrochan;
17125 ibDomains['dobrochan.ru'] = Dobrochan;
17126
17127 class Endchan extends Lynxchan {
17128 constructor(prot, dm) {
17129 super(prot, dm);
17130
17131 this.qTrunc = '.contentOmissionIndicator > p';
17132 }
17133 get css() {
17134 return `${ super.css }
17135 .bottomNav, .delLink, #expandAll, .hidePost, .hideThread, .linkLast50,
17136 .linkPreview, #modeBanner, .watchButton { display: none !important; }
17137 #de-main, .de-pview { font-size: 75%; }`;
17138 }
17139 init() {
17140 super.init();
17141 $each($Q('.imgLink > img[src^="/.youtube/"]'), el => $del($parent(el, 'FIGURE')));
17142 $each($Q('.youtube_wrapper'), el => {
17143 const src = $q('a', el).href;
17144 $del($bBegin(el, `<a href="${ src }">${ src }</a>`).nextSibling);
17145 });
17146 return false;
17147 }
17148 }
17149 ibDomains['endchan.net'] = Endchan;
17150
17151 class Ernstchan extends BaseBoard {
17152 constructor(prot, dm) {
17153 super(prot, dm);
17154
17155 this.cReply = 'post';
17156 this.qError = '.error > .info';
17157 this.qFormRedir = 'input[name="gb2"][value="thread"]';
17158 this.qFormSpoiler = 'input[type="checkbox"][name="spoilered"]';
17159 this.qOPost = '.thread_OP';
17160 this.qPages = '.pagelist > li:nth-last-child(2)';
17161 this.qPostHeader = '.post_head';
17162 this.qPostMsg = '.text';
17163 this.qPostSubj = '.subject';
17164 this.qPostTrip = '.tripcode';
17165 this.qRPost = '.thread_reply';
17166 this.qTrunc = '.tldr';
17167 this.docExt = '';
17168 this.firstPage = 1;
17169 this.markupBB = true;
17170 this.multiFile = true;
17171 this.res = 'thread/';
17172 }
17173 get qImgNameLink() {
17174 return '.filename > a';
17175 }
17176 get css() {
17177 return `.content > hr, .de-parea > hr, .de-pview > .doubledash, .sage { display: none !important }
17178 .de-pview > .post { margin-left: 0; border: none; }
17179 #de-win-reply { float:left; margin-left:2em }`;
17180 }
17181 fixFileInputs(el) {
17182 const str = `><input name="file" type="file">
17183 <input type="hidden" name="spoilered" value="0">
17184 <input type="checkbox" name="spoilered" value="1"></div>`;
17185 el.innerHTML = '<div' + str + ('<div style="display: none;"' + str).repeat(3);
17186 }
17187 getImgWrap(img) {
17188 return img.parentNode.parentNode.parentNode;
17189 }
17190 getPageUrl(b, p) {
17191 return p > 1 ? fixBrd(b) + 'page/' + p : fixBrd(b);
17192 }
17193 getPostElOfEl(el) {
17194 while(el && !nav.matchesSelector(el, '.post')) {
17195 el = el.parentElement;
17196 }
17197 return el.parentNode;
17198 }
17199 }
17200 ibDomains['ernstchan.xyz'] = Ernstchan;
17201
17202 class Gensokyo extends Kusaba {
17203 constructor(prot, dm) {
17204 super(prot, dm);
17205
17206 this.hasRefererErr = true;
17207 }
17208 }
17209 ibDomains['gensokyo.4otaku.org'] = Gensokyo;
17210
17211 class Iichan extends BaseBoard {
17212 constructor(prot, dm) {
17213 super(prot, dm);
17214 this.iichan = true;
17215
17216 this.hasCatalog = true;
17217 }
17218 get qFormMail() {
17219 return 'input[name="nya2"]';
17220 }
17221 get qFormName() {
17222 return 'td > input[name="nya1"]';
17223 }
17224 get qFormSubj() {
17225 return 'input[name="nya3"]';
17226 }
17227 get catalogUrl() {
17228 return `${ this.prot }//${ this.host }/${ this.b }/catalogue.html`;
17229 }
17230 get css() {
17231 return `${ !this.t ? '' : 'hr + #de-main { margin-top: -32px; } .logo { margin-bottom: 14px; }' }
17232 .iichan-hide-thread-btn, .iichan-quick-reply-btn, .postnum { display: none; }
17233 .replypage div[id^="thread"] span.reflink::after { content: none; }`;
17234 }
17235 get isArchived() {
17236 return this.b.includes('/arch');
17237 }
17238 getImgRealName(wrap) {
17239 return $q('.filesize > em', wrap).textContent.split(',')[2] || super.getImgRealName(wrap);
17240 }
17241 init() {
17242 defaultCfg.addSageBtn = 0;
17243 $script('highlight = Function.prototype;');
17244 let el = $q(this.qFormSpoiler);
17245 if(el) {
17246 $hide(el = el.parentNode);
17247 $del(el.previousSibling);
17248 }
17249 return false;
17250 }
17251 }
17252 ibDomains['iichan.hk'] = Iichan;
17253
17254 class Kohlchan extends Lynxchan {
17255 constructor(prot, dm) {
17256 super(prot, dm);
17257
17258 this.qFormRules = '#rules_row';
17259
17260 this.hasTextLinks = true;
17261 this.timePattern = 'yyyy+nn+dd+hh+ii+ss';
17262 }
17263 get css() {
17264 return `${ super.css }
17265 #postingForm, .sage { display: none; }`;
17266 }
17267 getImgRealName(wrap) {
17268 return $q('.originalNameLink', wrap).title;
17269 }
17270 getSage(post) {
17271 return !!$q('.sage', post).hasChildNodes();
17272 }
17273 init() {
17274 if(!this.host.includes('nocsp.')) {
17275 deWindow.location.assign(deWindow.location.href
17276 .replace(/(www\.)?kohlchan\.net/, 'nocsp.kohlchan.net'));
17277 return true;
17278 }
17279 return super.init();
17280 }
17281 }
17282 ibDomains['kohlchan.net'] = Kohlchan;
17283
17284 class Kropyvach extends Vichan {
17285 constructor(prot, dm) {
17286 super(prot, dm);
17287
17288 this.markupBB = true;
17289 }
17290 get css() {
17291 return super.css + (this.t ? '' : '\r\n.de-btn-reply { display: none !important; }');
17292 }
17293 get markupTags() {
17294 return ['b', 'i', 'u', 's', 'spoiler', 'code'];
17295 }
17296 }
17297 ibDomains['kropyva.ch'] = Kropyvach;
17298
17299 class Lainchan extends Vichan {
17300 constructor(prot, dm) {
17301 super(prot, dm);
17302
17303 this.qOPost = '.op';
17304 }
17305 get css() {
17306 return `${ super.css }
17307 .sidearrows { display: none !important; }
17308 .bar { z-index: 1; }
17309 ${ Cfg.imgNames ? '.details > a { display: none; }' : '' }`;
17310 }
17311 getImgRealName(wrap) {
17312 return $q('.details > a, .postfilename', wrap).textContent;
17313 }
17314 init() {
17315 super.init();
17316 $each($Q('.files + .post.op'), el => el.insertBefore(el.previousElementSibling, el.firstChild));
17317 return false;
17318 }
17319 }
17320 ibDomains['lainchan.org'] = Lainchan;
17321
17322 class Niuchan extends Kusaba {
17323 get css() {
17324 return `${ super.css }
17325 .replybacklinks, .resize { display: none; }`;
17326 }
17327 }
17328 ibDomains['niuchan.org'] = Niuchan;
17329
17330 class Nowere extends BaseBoard {
17331 get markupTags() {
17332 return ['**', '***', '', '^H', '', ''];
17333 }
17334 init() {
17335 $script('highlight = Function.prototype;');
17336 return false;
17337 }
17338 }
17339 ibDomains['nowere.net'] = Nowere;
17340
17341 class Ponyach extends BaseBoard {
17342 constructor(prot, dm) {
17343 super(prot, dm);
17344
17345 this.qBan = 'font[color="#FF0000"]';
17346 this.qImgInfo = '.filesize[style="display: inline;"]';
17347
17348 this.formParent = 'replythread';
17349 this.jsonSubmit = true;
17350 this.multiFile = true;
17351 }
17352 get qImgNameLink() {
17353 return 'a:first-of-type';
17354 }
17355 getImgInfo(wrap) {
17356 return wrap.textContent;
17357 }
17358 getImgRealName(wrap) {
17359 return $q('.mobile_filename_hide', wrap).textContent;
17360 }
17361 getImgWrap(img) {
17362 return $id('fs_' + img.alt);
17363 }
17364 getPNum(post) {
17365 return +post.getAttribute('data-num');
17366 }
17367 getSubmitData({ error, id }) {
17368 return { error, postNum: id && +id };
17369 }
17370 init() {
17371 const el = $id('postform');
17372 if(el) {
17373 el.setAttribute('action', el.getAttribute('action') + '?json=1');
17374 }
17375 defaultCfg.postSameImg = 0;
17376 defaultCfg.removeEXIF = 0;
17377 return false;
17378 }
17379 }
17380 ibDomains['ponyach.ru'] = Ponyach;
17381
17382 class Ponychan extends Tinyboard {
17383 constructor(prot, dm) {
17384 super(prot, dm);
17385
17386 this.qOPost = '.opContainer';
17387
17388 this.jsonSubmit = false;
17389 }
17390 get css() {
17391 return `${ super.css }
17392 .mature_thread { display: block !important; }
17393 .mature_warning { display: none; }
17394 ${ Cfg.imgNames ? '.post-filename { display: none; }' : '' }`;
17395 }
17396 getImgRealName(wrap) {
17397 return $q('.post-filename', wrap).textContent;
17398 }
17399 init() {
17400 super.init();
17401 $each($Q('img[data-mature-src]'), el => (el.src = el.getAttribute('data-mature-src')));
17402 return false;
17403 }
17404 }
17405 ibDomains['ponychan.net'] = Ponychan;
17406
17407 class Synch extends Tinyboard {
17408 constructor(prot, dm) {
17409 super(prot, dm);
17410
17411 this.qImgInfo = '.unimportant';
17412 this.qPages = '.pagination';
17413
17414 this.markupBB = true;
17415 }
17416 get qImgNameLink() {
17417 return '.file-info > a';
17418 }
17419 get css() {
17420 return `${ super.css }
17421 .fa-sort { display: none; }
17422 time::after { content: none; }`;
17423 }
17424 get markupTags() {
17425 return ['b', 'i', 'u', 's', 'spoiler', 'code', 'sup', 'sub'];
17426 }
17427 init() {
17428 const val = '{ "simpleNavbar": true }';
17429 if(locStorage.settings !== val) {
17430 locStorage.settings = val;
17431 deWindow.location.reload();
17432 return true;
17433 }
17434 super.init();
17435 defaultCfg.timePattern = 'w+dd+m+yyyy+hh+ii+ss';
17436 defaultCfg.timeOffset = 4;
17437 defaultCfg.correctTime = 1;
17438 return false;
17439 }
17440 fixHTML(data, isForm) {
17441 const formEl = super.fixHTML(data, isForm);
17442 const els = $Q('.btn-group', formEl);
17443 for(let i = 0, len = els.length; i < len; ++i) {
17444 $replace(els[i], $q('a', els[i]));
17445 }
17446 return formEl;
17447 }
17448 }
17449 ibDomains['syn-ch.ru'] = Synch;
17450 ibDomains['syn-ch.com'] = Synch;
17451 ibDomains['syn-ch.org'] = Synch;
17452
17453 class Warosu extends BaseBoard {
17454 constructor(prot, dm) {
17455 super(prot, dm);
17456
17457 this.qDForm = '.content';
17458 this.qForm = '.subreply';
17459 this.qPostRef = '.js';
17460 this.qImgInfo = 'span';
17461 this.qOPost = 'div[itemscope]';
17462
17463 this.res = 'thread/';
17464 }
17465 get css() {
17466 return '.quoted-by { display: none !important; }';
17467 }
17468 getTNum(thr) {
17469 return +$q('div[itemscope]', thr).id.match(/\d+/);
17470 }
17471 fixHTMLHelper(str) {
17472 return str.replace(/\/post\/(\d+)"/g, '/#$1"');
17473 }
17474 }
17475 ibDomains['warosu.org'] = Warosu;
17476
17477 const wLoc = deWindow.location;
17478 const prot = wLoc.protocol;
17479 let dm = localData && localData.dm;
17480 if(checkDomains) {
17481 if(!dm) {
17482 const ibKeys = Object.keys(ibDomains);
17483 let i = ibKeys.length;
17484 const host = wLoc.hostname.toLowerCase();
17485 while(i--) {
17486 dm = ibKeys[i];
17487 if(host === dm || host.endsWith('.' + dm)) {
17488 return new ibDomains[dm](prot, dm);
17489 }
17490 }
17491 } else if(dm in ibDomains) {
17492 return new ibDomains[dm](prot, dm);
17493 }
17494 }
17495 if(!dm) {
17496 dm = wLoc.hostname;
17497 }
17498 if(!dm || !checkEngines) {
17499 return null;
17500 }
17501 dm = dm.match(/(?:(?:[^.]+\.)(?=org\.|net\.|com\.))?[^.]+\.[^.]+$|^\d+\.\d+\.\d+\.\d+$|localhost/)[0];
17502 for(let i = ibEngines.length - 1; i >= 0; --i) {
17503 const [path, Ctor] = ibEngines[i];
17504 if($q(path, doc)) {
17505 return new Ctor(prot, dm);
17506 }
17507 }
17508 return null;
17509}
17510
17511/* ==[ Misc.js ]==============================================================================================
17512 MISCELLANEOUS
17513=========================================================================================================== */
17514
17515// You can use Dollchan API listeners in Your external scripts and apps
17516// More info: https://github.com/SthephanShinkufag/Dollchan-Extension-Tools/wiki/dollchan-api
17517const DollchanAPI = {
17518 initAPI() {
17519 this.hasListeners = false;
17520 if(!('MessageChannel' in deWindow)) {
17521 return;
17522 }
17523 const channel = new MessageChannel();
17524 this.port = channel.port1;
17525 this.port.onmessage = this._handleMessage;
17526 this.activeListeners = new Set();
17527 const port = channel.port2;
17528 doc.defaultView.addEventListener('message', e => {
17529 if(e.data === 'de-request-api-message') {
17530 this.hasListeners = true;
17531 doc.defaultView.postMessage('de-answer-api-message', '*', [port]);
17532 }
17533 });
17534 },
17535 hasListener: name => DollchanAPI.hasListeners && DollchanAPI.activeListeners.has(name),
17536 notify(name, data) {
17537 if(this.hasListener(name)) {
17538 this.port.postMessage({ name, data });
17539 }
17540 },
17541
17542 _handleMessage({ data: arg }) {
17543 if(!arg || !arg.name) {
17544 return;
17545 }
17546 let rv = null;
17547 const { name, data } = arg;
17548 switch(name.toLowerCase()) {
17549 case 'registerapi':
17550 if(data) {
17551 rv = {};
17552 for(const aName of data) {
17553 rv[aName] = DollchanAPI._register(aName.toLowerCase());
17554 }
17555 }
17556 break;
17557 }
17558 DollchanAPI.port.postMessage({ name, data: rv });
17559 },
17560 _register(name) {
17561 switch(name) {
17562 case 'expandmedia':
17563 case 'filechange':
17564 case 'newpost':
17565 case 'submitform': break;
17566 default: return false;
17567 }
17568 this.activeListeners.add(name);
17569 return true;
17570 }
17571};
17572
17573// Checking for Dollchan updates from github
17574function checkForUpdates(isManual, lastUpdateTime) {
17575 if(!isManual) {
17576 if(Date.now() - +lastUpdateTime < [0, 1, 2, 7, 14, 30][Cfg.updDollchan] * 1e3 * 60 * 60 * 24) {
17577 return Promise.reject();
17578 }
17579 }
17580 return $ajax(
17581 gitRaw + 'src/modules/Wrap.js', { 'Content-Type': 'text/plain' }, true
17582 ).then(({ responseText }) => {
17583 const v = responseText.match(/const version = '([0-9.]+)';/);
17584 const remoteVer = v && v[1] ? v[1].split('.') : null;
17585 if(!remoteVer) {
17586 return Promise.reject();
17587 }
17588 const currentVer = version.split('.');
17589 const src = `${ gitRaw }${ nav.isESNext ? 'src/' : '' }Dollchan_Extension_Tools.${
17590 nav.isESNext ? 'es6.' : '' }user.js`;
17591 saveCfgObj('lastUpd', Date.now());
17592 const link = `<a style="color: blue; font-weight: bold;" href="${ src }">`;
17593 const chLogLink = `<a target="_blank" href="${ gitWiki }${
17594 lang === 1 ? 'versions-en' : 'versions' }">\r\n${ Lng.changeLog[lang] }<a>`;
17595 for(let i = 0, len = Math.max(currentVer.length, remoteVer.length); i < len; ++i) {
17596 if((+remoteVer[i] || 0) > (+currentVer[i] || 0)) {
17597 return `${ link }${ Lng.updAvail[lang].replace('%s', v[1]) }</a>${ chLogLink }`;
17598 } else if((+remoteVer[i] || 0) < (+currentVer[i] || 0)) {
17599 break;
17600 }
17601 }
17602 if(isManual) {
17603 const c = responseText.match(/const commit = '([0-9abcdef]+)';/)[1];
17604 const vc = version + '.' + c;
17605 return c === commit ? Lng.haveLatestCommit[lang].replace('%s', vc) :
17606 `${ Lng.haveLatestStable[lang].replace('%s', version) }\r\n${
17607 Lng.newCommitsAvail[lang].replace('%s', `${ link }${ vc }</a>${ chLogLink }`) }`;
17608 }
17609 return Promise.reject();
17610 }, () => !isManual ?
17611 Promise.reject() : `<div style="color: red; font-weigth: bold;">${ Lng.noConnect[lang] }</div>`
17612 );
17613}
17614
17615function initPage() {
17616 if(aib.t) {
17617 if(Cfg.rePageTitle && Thread.first) {
17618 doc.title = `/${ aib.b } - ${ Thread.first.op.title }`;
17619 }
17620 if(!localData) {
17621 Cfg.stats.view++;
17622 saveCfgObj(aib.dm, Cfg);
17623 }
17624 } else {
17625 thrNavPanel.initThrNav();
17626 }
17627 if(!localData) {
17628 updater = initThreadUpdater(doc.title, aib.t && Cfg.ajaxUpdThr && !aib.isArchived);
17629 }
17630}
17631
17632function scrollPage() {
17633 if(!aib.t && Cfg.scrollToTop) {
17634 scrollTo(0, 1);
17635 return;
17636 }
17637 setTimeout(() => {
17638 let post, num;
17639 const { hash } = deWindow.location;
17640 if(hash && (num = hash.match(/#[ip]?(\d+)$/)) &&
17641 (num = +num[1]) && (post = pByNum.get(num)) && !post.isOp
17642 ) {
17643 post.selectAndScrollTo();
17644 return;
17645 }
17646 const id = 'de-scroll-' + aib.b + (aib.t || '');
17647 const val = +sesStorage[id];
17648 if(val && needScroll && Cfg.saveScroll) {
17649 scrollTo(0, val);
17650 sesStorage.removeItem(id);
17651 }
17652 }, 0);
17653}
17654
17655/* ==[ SvgIcons.js ]==========================================================================================
17656 SVG ICONS
17657=========================================================================================================== */
17658
17659/* eslint-disable max-len */
17660
17661function addSVGIcons() {
17662 docBody.insertAdjacentHTML('beforeend', `
17663 <div id="de-svg-icons">
17664 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
17665 <defs>
17666 <linearGradient id="de-btn-back-gradient" x1="50%" y1="0%" y2="100%" x2="50%">
17667 <stop offset="0%" stop-color="#A0A0A0"/>
17668 <stop offset="50%" stop-color="#505050"/>
17669 <stop offset="100%" stop-color="#A0A0A0"/>
17670 </linearGradient>
17671 <linearGradient id="de-file-del-gradient" x1="50%" y1="10%" x2="50%" y2="90%">
17672 <stop offset="0" stop-color="#fbd"/>
17673 <stop offset="50%" stop-color="#f30"/>
17674 </linearGradient>
17675 </defs>
17676
17677 <!-- POST ICONS -->
17678 <symbol viewBox="0 0 16 16" id="de-symbol-post-back">
17679 <path class="de-post-btns-back" d="M4 1Q1 1 1 4v8q0 3 3 3h8q3 0 3-3V4q0-3-3-3z"/>
17680 </symbol>
17681 <symbol viewBox="0 0 16 16" id="de-symbol-post-hide">
17682 <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
17683 <path class="de-svg-stroke" stroke-width="2.5" d="M4.5 11.5l7-7M11.5 11.5l-7-7"/>
17684 </symbol>
17685 <symbol viewBox="0 0 16 16" id="de-symbol-post-unhide">
17686 <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
17687 <path class="de-svg-stroke" stroke-width="2" d="M8 4v8M4 8h8"/>
17688 </symbol>
17689 <symbol viewBox="0 0 16 16" id="de-symbol-post-reply">
17690 <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
17691 <path class="de-svg-fill" d="M5 11c0 .8.6 1.2 1.3.7l5-3c.6-.4.6-1 0-1.5l-5-3C5.6 4 5 4.3 5 5v6z"/>
17692 </symbol>
17693 <symbol viewBox="0 0 16 16" id="de-symbol-post-expthr">
17694 <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
17695 <path class="de-svg-fill" d="M4.5 6L8 3l3.5 3H9.25v4h2.25L8 13l-3.5-3h2.25V6z"/>
17696 </symbol>
17697 <symbol viewBox="0 0 16 16" id="de-symbol-post-fav">
17698 <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
17699 <path class="de-svg-fill" d="M8 3l1.5 3 3.5.5-2.5 2.2 1 3.8-3.5-2-3.5 2 1-3.8L3 6.5 6.5 6 8 3z"/>
17700 </symbol>
17701 <symbol viewBox="0 0 16 16" id="de-symbol-post-stick">
17702 <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
17703 <path class="de-svg-fill" d="M5 5h6v6H5z"/>
17704 </symbol>
17705 <symbol viewBox="0 0 16 16" id="de-symbol-post-sage">
17706 <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
17707 <path class="de-svg-fill" d="M4 9h8l-4 4.5zm2-6h4v1H6zm0 2h4v1H6zm0 2h4v1H6z"/>
17708 </symbol>
17709 <symbol viewBox="0 0 16 16" id="de-symbol-post-src">
17710 <use class="de-post-btns-back" xlink:href="#de-symbol-post-back"/>
17711 <circle class="de-svg-stroke" stroke-width="2" cx="7" cy="7" r="2.5"/>
17712 <path class="de-svg-stroke" stroke-width="2" d="M9 9l3 3"/>
17713 </symbol>
17714
17715 <!-- FILE ICONS -->
17716 <symbol viewBox="0 0 16 16" id="de-symbol-file-del">
17717 <path fill="url(#de-file-del-gradient)" stroke="#ca2900" d="M4 1.3l4 4 4-4L14.8 4l-4 4 4 4-2.8 2.8-4-4-4 4L1.3 12l4-4-4-4L4 1.3z"/>
17718 </symbol>
17719 <symbol viewBox="0 0 16 16" width="16" height="16" id="de-symbol-file-rar">
17720 <path stroke="#07ac07" stroke-width="2" d="M3 13h13"/>
17721 <path stroke="#03043f" stroke-width="4" d="M3 10h13"/>
17722 <path stroke="#cc5dc1" stroke-width="2" d="M3 7h13"/>
17723 <path fill="#ccd0db" d="M3 14l-3-3V3l3 3v8z"/>
17724 <path fill="#666" d="M3 5L0 2v1l3 3V5zm0 3L0 5v1l3 3V8zm0 3L0 8v1l3 3v-1zm0 3l-3-3v1l3 3v-1z"/>
17725 <path stroke="#103cef" stroke-width="2" d="M3 10h13"/>
17726 <path stroke="#294f1d" d="M3 14.5h13"/>
17727 <path fill="#994a95" d="M13 2H0l3 3h13l-3-3z"/>
17728 <path stroke="#7C467a" d="M3 5.5h13"/>
17729 <path stroke="#513400" stroke-width="2" d="M9.5 15V5"/>
17730 <path fill="#513400" d="M10.5 5l-3-3h-2l3 3h2z"/>
17731 <path stroke="#ceab00" stroke-width="4" d="M7 10h5"/>
17732 <path fill="none" stroke="#222" d="M8.5 9v1.5h2V9"/>
17733 </symbol>
17734 <symbol viewBox="0 0 16 16" id="de-symbol-file-ren">
17735 <circle fill="#ffe888" stroke="#333" stroke-width=".75" cx="6" cy="14" r="1.5"/>
17736 <circle fill="#ffe888" stroke="#333" stroke-width=".75" cx="10" cy="14" r="1.5"/>
17737 <circle fill="#ffe888" stroke="#333" stroke-width=".75" cx="14" cy="14" r="1.5"/>
17738 <path fill="#fcb45e" stroke="#3a2200" stroke-width=".75" d="M2 8L9.5.5l1.8 1.8-7.5 7.5L2 8z"/>
17739 <path fill="#ff8a33" stroke="#3a2200" stroke-width=".75" d="M3.8 9.8l7.5-7.5L13 4l-7.5 7.5-1.7-1.7z"/>
17740 <path fill="#ffe888" stroke="#333" stroke-width=".75" d="M2 8l-.5.5L1 9v3.5h3.5l1-1-1.7-1.7L2 8z"/>
17741 <path stroke="#333" d="M1 12.5L2.5 11"/>
17742 </symbol>
17743 <symbol viewBox="0 0 16 16" id="de-symbol-file-txt">
17744 <circle fill="#2cabe1" cx="8" cy="8" r="7.5"/>
17745 <line stroke="#fff" stroke-width="2" x1="8" y1="3" x2="8" y2="13"/>
17746 <line stroke="#fff" stroke-width="2" x1="3" y1="8" x2="13" y2="8"/>
17747 </symbol>
17748
17749 <!-- WINDOW ICONS -->
17750 <symbol viewBox="0 0 16 16" id="de-symbol-win-arrow">
17751 <path class="de-svg-stroke" stroke-width="3.5" d="M8 13V6"/>
17752 <path class="de-svg-fill" d="M3.5 7h9L8 2.5 3.5 7z"/>
17753 </symbol>
17754 <symbol viewBox="0 0 16 16" id="de-symbol-win-close">
17755 <path class="de-svg-stroke" stroke-width="2.5" d="M3.5 3.5l9 9m-9 0l9-9"/>
17756 </symbol>
17757
17758 <!-- THREAD NAVIGATION ICONS -->
17759 <symbol viewBox="0 0 7 7" id="de-symbol-thr-nav-arrow">
17760 <path class="de-svg-fill" d="M6 3.5L2 0v7z"/>
17761 </symbol>
17762 <symbol viewBox="0 0 24 24" id="de-symbol-thr-nav-up">
17763 <path class="de-svg-stroke" stroke-width="3" d="M3 22.5l9-9 9 9M3 13.5l9-9 9 9"/>
17764 </symbol>
17765 <symbol viewBox="0 0 24 24" id="de-symbol-thr-nav-down">
17766 <path class="de-svg-stroke" stroke-width="3" d="M3 11.5l9 9 9-9M3 2.5l9 9 9-9"/>
17767 </symbol>
17768
17769 <!-- IMAGE BUTTON ICONS -->
17770 <symbol viewBox="0 0 32 32" id="de-symbol-img-btn-arrow">
17771 <path class="de-svg-stroke" stroke-width="8" d="M0 16h20"/>
17772 <path class="de-svg-stroke" stroke-width="9" d="M13 3l16 16M13 29l16-16"/>
17773 </symbol>
17774 <symbol viewBox="0 0 32 32" id="de-symbol-img-btn-auto">
17775 <path class="de-svg-fill" d="M13.2 26.6c-3.1 2.4-5.9.5-5.9-3.3V8.7c0-3.8 2.8-5.6 6.1-3.3l12.5 7.1c3.1 1.9 3.1 5.2 0 7.1 0-.1-12.7 7-12.7 7z"/>
17776 </symbol>
17777 <symbol viewBox="0 0 32 32" id="de-symbol-img-btn-rotate">
17778 <path class="de-svg-stroke" stroke-width="7" d="M16 4c6.6 0 12 5.4 12 12s-5.4 12-12 12S4 22.6 4 16"/>
17779 <path class="de-svg-fill" d="M13.5 19.2L0 27V11.4z"/>
17780 </symbol>
17781
17782 <!-- MAIN PANEL -->
17783 <symbol viewBox="0 0 25 25" id="de-symbol-panel-logo">
17784 <path class="de-svg-fill" d="M22 5h-10v16h4v-14h6z"/>
17785 <path class="de-svg-stroke" stroke-width="3" d="M22 20.5H12c-2.8 0-5.7 0-5.7-4s2.8-4 5.7-4H21"/>
17786 </symbol>
17787 <symbol viewBox="0 0 25 25" id="de-symbol-panel-cfg">
17788 <circle class="de-svg-stroke" stroke-width="3" cx="12.5" cy="12.5" r="6"/>
17789 <path class="de-svg-stroke" stroke-width="3" d="M12.5 6.5v-3M18.5 12.5h3M12.5 18.5v3M6.5 12.5h-3M16.7 8.3L19 6M16.7 16.7L19 19M8.3 16.7L6 19M8.3 8.3L6 6"/>
17790 </symbol>
17791 <symbol viewBox="0 0 25 25" id="de-symbol-panel-hid">
17792 <path class="de-svg-stroke" stroke-width="4" d="M6 19L19 6M6 6l13 13"/>
17793 </symbol>
17794 <symbol viewBox="0 0 25 25" id="de-symbol-panel-fav">
17795 <path class="de-svg-fill" d="M12.5 3.5l2.5 6 6.5.5-5 4.2 2 6.8-6-4-6 4 2-6.8-5-4.2 6.5-.5 2.5-6z"/>
17796 </symbol>
17797 <symbol viewBox="0 0 25 25" id="de-symbol-panel-vid">
17798 <path class="de-svg-fill" d="M12.5 4a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zm-1 13c-1.3 1-2.5.2-2.5-1.4V9.4C9 7.8 10.2 7 11.6 8l5.3 3c1.3.8 1.3 2.2 0 3l-5.4 3z"/>
17799 </symbol>
17800 <symbol viewBox="0 0 25 25" id="de-symbol-panel-refresh">
17801 <path class="de-svg-fill" d="M14 4v4.3a4.5 4.5 0 1 1-3 0V4a8.5 8.5 0 1 0 3 0z"/>
17802 <path class="de-svg-fill" d="M13 11V4h7"/>
17803 </symbol>
17804 <symbol viewBox="0 0 25 25" id="de-symbol-panel-arrow">
17805 <path class="de-svg-stroke" stroke-width="5" d="M4 12.5h12"/>
17806 <path class="de-svg-fill" d="M14 19V6l7 6.5"/>
17807 </symbol>
17808 <symbol viewBox="0 0 25 25" id="de-symbol-panel-expimg">
17809 <path class="de-svg-stroke" stroke-width="2" d="M20 18c0 1-1 2-2 2H7c-1 0-2-1-2-2V7c0-1 1-2 2-2h11c1 0 2 1 2 2v11z"/>
17810 <path stroke-width="3" d="M8 12.5h9"/>
17811 <path d="M10 8v9l-5-4.5M15 17V8l5 4.5"/>
17812 </symbol>
17813 <symbol viewBox="0 0 25 25" id="de-symbol-panel-maskimg">
17814 <path class="de-svg-stroke" stroke-width="2" d="M20 18c0 1-1 2-2 2H7c-1 0-2-1-2-2V7c0-1 1-2 2-2h11c1 0 2 1 2 2v11z"/>
17815 <path d="M5 20L20 5M5 15.5L15.5 5M5 11l6-6M20 9.5L9.5 20M20 14l-6 6"/>
17816 </symbol>
17817 <symbol viewBox="0 0 25 25" id="de-symbol-panel-preimg">
17818 <path class="de-svg-stroke" stroke-width="2" d="M20 18c0 1-1 2-2 2H7c-1 0-2-1-2-2V7c0-1 1-2 2-2h11c1 0 2 1 2 2v11z"/>
17819 <path stroke-width="3" d="M12.5 17V9"/>
17820 <path d="M8 15h9l-4.5 5"/>
17821 </symbol>
17822 <symbol viewBox="0 0 25 25" id="de-symbol-panel-savethr">
17823 <path class="de-svg-fill" d="M18 4h-1v6H8V4H6C5 4 4 5 4 6v13c0 1 1 2 2 2h13c1 0 2-1 2-2V7l-3-3zM6 20v-8h13v8H6z"/>
17824 <path class="de-svg-stroke" stroke-width="3" d="M13.5 9V4"/>
17825 </symbol>
17826 <symbol viewBox="0 0 25 25" id="de-symbol-panel-upd">
17827 <circle cx="12.5" cy="10.8" r="4"/>
17828 <path class="de-svg-stroke" stroke-width="2" stroke-linejoin="round" d="M4.5 12q8-10,16 0q-8 10,-16 0z"/>
17829 <path class="de-svg-stroke" d="M11 7L9.8 5M14 7l1.2-2M11 17l-1.2 2m4.2-2l1.2 2M7 8.5L5.3 6.8M7 15.5l-1.7 1.7M18 8.5l1.7-1.7M18 15.5l1.7 1.7"/>
17830 </symbol>
17831 <symbol viewBox="0 0 25 25" id="de-symbol-panel-audio-off">
17832 <path class="de-svg-fill" d="M13 21V4L8 9H4v7h4l5 5z"/>
17833 <path class="de-svg-stroke" stroke-width="2" d="M15 9.5l6 6m0-6l-6 6"/>
17834 </symbol>
17835 <symbol viewBox="0 0 25 25" id="de-symbol-panel-audio-on">
17836 <path class="de-svg-fill" d="M13 21V4L8 9H4v7h4z"/>
17837 <path class="de-svg-stroke" stroke-width="2" d="M15.5 7.5c1.7 3.3 1.7 6.7 0 10m3-12.5c3 5 3 10 0 15"/>
17838 </symbol>
17839 <symbol viewBox="0 0 25 25" id="de-symbol-panel-catalog">
17840 <path class="de-svg-fill" d="M5 5h3v3H5zm12 0h3v3h-3zm-4 0h3v3h-3zM9 5h3v3H9zM5 9h3v3H5zm12 0h3v3h-3zm-4 0h3v3h-3zM9 9h3v3H9zm-4 4h3v3H5zm12 0h3v3h-3zm-4 0h3v3h-3zm-4 0h3v3H9zm-4 4h3v3H5zm12 0h3v3h-3zm-4 0h3v3h-3zm-4 0h3v3H9z"/>
17841 </symbol>
17842 <symbol viewBox="0 0 25 25" id="de-symbol-panel-enable">
17843 <path class="de-svg-stroke" stroke-width="3" d="M12.5 4v8"/>
17844 <path class="de-svg-fill" d="M16 4.8v4a5 5 0 0 1-3.5 8.7A5 5 0 0 1 9 9V4.7a8.5 8.5 0 1 0 7 0z"/>
17845 </symbol>
17846
17847 <!-- MARKUP BUTTONS -->
17848 <symbol viewBox="0 0 23 22" id="de-symbol-markup-back">
17849 <path class="de-markup-back" stroke-width="2" d="M6 1q-5 0,-5 5v10q0 5,5 5h11q5 0,5 -5v-10q0 -5,-5-5z"/>
17850 </symbol>
17851 <symbol viewBox="0 0 23 22" id="de-symbol-markup-bold">
17852 <use xlink:href="#de-symbol-markup-back"/>
17853 <text x="5.5" y="17" style="font-family: sans-serif; font-size: 17px; font-weight: 800;">B</text>
17854 </symbol>
17855 <symbol viewBox="0 0 23 22" id="de-symbol-markup-italic">
17856 <use xlink:href="#de-symbol-markup-back"/>
17857 <text x="8" y="17" style="font-family: sans-serif; font-size: 17px; font-weight: 600; font-style: italic;">i</text>
17858 </symbol>
17859 <symbol viewBox="0 0 23 22" id="de-symbol-markup-under">
17860 <use xlink:href="#de-symbol-markup-back"/>
17861 <text x="6" y="15" width="20" style="font-family: sans-serif; font-size: 17px; font-weight: 600;">u</text>
17862 <path stroke="#444" stroke-width="1.5" d="M6 17H17.5"/>
17863 </symbol>
17864 <symbol viewBox="0 0 23 22" id="de-symbol-markup-strike">
17865 <use xlink:href="#de-symbol-markup-back"/>
17866 <text x="4" y="17" style="font-family: sans-serif; font-size: 22px; font-weight: 600; font-style: italic;">s</text>
17867 <path stroke="#444" d="M4 11H19"/>
17868 </symbol>
17869 <symbol viewBox="0 0 23 22" id="de-symbol-markup-spoil">
17870 <use xlink:href="#de-symbol-markup-back"/>
17871 <path stroke="#666" stroke-width="10" d="M4 11H19"/>
17872 </symbol>
17873 <symbol viewBox="0 0 23 22" id="de-symbol-markup-code">
17874 <use xlink:href="#de-symbol-markup-back"/>
17875 <text x="5" y="17" style="font-family: 'Lucida Console', monospace; font-size: 18px; font-weight: 600;">C</text>
17876 </symbol>
17877 <symbol viewBox="0 0 23 22" id="de-symbol-markup-sup">
17878 <use xlink:href="#de-symbol-markup-back"/>
17879 <text x="4" y="15" style="font-family: sans-serif; font-size: 16px; font-weight: 600;">x</text>
17880 <text x="14" y="10" style="font-family: sans-serif; font-size: 8px; font-weight: 600;">2</text>
17881 </symbol>
17882 <symbol viewBox="0 0 23 22" id="de-symbol-markup-sub">
17883 <use xlink:href="#de-symbol-markup-back"/>
17884 <text x="4" y="15" style="font-family: sans-serif; font-size: 16px; font-weight: 600;">x</text>
17885 <text x="14" y="17" style="font-family: sans-serif; font-size: 8px; font-weight: 600;">2</text>
17886 </symbol>
17887 <symbol viewBox="0 0 23 22" id="de-symbol-markup-quote">
17888 <use xlink:href="#de-symbol-markup-back"/>
17889 <text x="6" y="18" style="font-family: sans-serif; font-size: 20px; font-weight: 600;">></text>
17890 </symbol>
17891
17892 <!-- OTHER -->
17893 <symbol viewBox="0 0 16 16" id="de-symbol-wait">
17894 <circle fill="#929087" cx="8" cy="2" r="2"/>
17895 <circle fill="#C5C2BA" cx="8" cy="14" r="2"/>
17896 <circle fill="#ACAAA0" cx="2" cy="8" r="2"/>
17897 <circle fill="#79766C" cx="14" cy="8" r="2"/>
17898 <circle fill="#D2CFC6" cx="12.25" cy="12.25" r="2"/>
17899 <circle fill="#9F9C93" cx="3.75" cy="3.75" r="2"/>
17900 <circle fill="#B9B6AE" cx="3.75" cy="12.25" r="2"/>
17901 <circle fill="#868379" cx="12.25" cy="3.75" r="2"/>
17902 </symbol>
17903 <symbol viewBox="0 0 16 16" id="de-symbol-closed">
17904 <image display="inline" width="16" height="16" xlink:href="data:image/gif;base64,R0lGODlhEAAQAKIAAP3rqPPOd+y6V+WmN+Dg4M7OzmZmZv///yH5BAEAAAcALAAAAAAQABAAAANCeLrWvZARUqqJkjiLj9FMcWHf6IldGZqM4zqRAcw0zXpAoO/6LfeNnS8XcAhjAIHSoFwim0wockCtUodWq+/1UiQAADs="/>
17905 </symbol>
17906 <symbol viewBox="0 0 16 16" id="de-symbol-unavail">
17907 <circle class="de-svg-stroke" fill="none" stroke-width="2" cx="8" cy="8" r="5"/>
17908 <path class="de-svg-stroke" stroke-width="2" d="M4 4l8 8"/>
17909 </symbol>
17910 </svg>
17911 </div>`);
17912}
17913
17914/* eslint-enable max-len */
17915
17916/* ==[ Css.js ]===============================================================================================
17917 CSS
17918=========================================================================================================== */
17919
17920/* eslint-disable max-len */
17921
17922function scriptCSS() {
17923 const cont = (id, src) => `${ id }::before { content: ""; display: inline-block; vertical-align: -3px; padding: 16px 16px 0 0; margin-right: 4px; background: url(${ src }) no-repeat center; background-size: contain; }`;
17924
17925 let x = `
17926 /* Main panel */
17927 #de-panel { position: fixed; right: 0; bottom: 0; z-index: 9999; border-radius: 15px 0 0 0; cursor: default; display: flex; min-height: 25px; color: #F5F5F5; }
17928 #de-panel-logo { flex: none; margin: auto 3px auto 0; cursor: pointer; }
17929 #de-panel-buttons { flex: 0 1 auto; display: flex; flex-flow: row wrap; align-items: center; padding: 0 0 0 2px; margin: 0; border-left: 1px solid #616b86; }
17930 .de-panel-button { display: block; flex: none; margin: 0 1px; padding: 0; transition: all .3s ease; }
17931 .de-panel-button-active { stroke: #32ff32 !important; fill: #32ff32 !important; }
17932 a.de-panel-button, a.de-panel-button:hover { color: inherit !important; }
17933 .de-panel-svg, #de-panel-logo, .de-panel-logo-svg, .de-panel-button { width: 25px; height: 25px; }
17934 #de-panel-expimg, #de-panel-maskimg, #de-panel-preimg { stroke: currentColor; fill: currentColor; }
17935 #de-panel-goback { transform: rotate(180deg); will-change: transform; }
17936 #de-panel-godown { transform: rotate(90deg); will-change: transform; }
17937 #de-panel-goup { transform: rotate(-90deg); will-change: transform; }
17938 #de-panel-upd-on { fill: #32ff32; }
17939 #de-panel-upd-warn { fill: #fff441; }
17940 #de-panel-upd-off { fill: #ff3232; }
17941 #de-panel-audio-on > .de-panel-svg > .de-use-audio-off, #de-panel-audio-off > .de-panel-svg > .de-use-audio-on { display: none; }
17942 #de-panel-info { display: flex; flex: none; padding: 0 6px; margin-left: 2px; border-left: 1px solid #616b86; font: 18px serif; }
17943 #de-panel-info-icount::before, #de-panel-info-acount:not(:empty)::before { content: "/"; }
17944 #de-svg-icons, #de-svg-icons > svg { height: 0; width: 0; position: fixed; }
17945 .de-svg-fill { stroke: none; fill: currentColor; }
17946 .de-svg-stroke { stroke: currentColor; fill: none; }
17947 use { fill: inherit; pointer-events: none; }
17948
17949 /* Panel theme */
17950 .de-img-btn, #de-panel, .de-win-head ${ [
17951 /* Gradient darkblue */
17952 '{ background: linear-gradient(to bottom, #7b849b, #616b86 8%, #3a414f 52%, rgba(0,0,0,0) 52%), linear-gradient(to bottom, rgba(0,0,0,0) 48%, #121212 52%, #1f2740 100%); }',
17953 /* Gradient blue */
17954 `{ background: linear-gradient(to bottom, #4b90df, #3d77be 20%, #376cb0 28%, #295591 52%, rgba(0,0,0,0) 52%), linear-gradient(to bottom, rgba(0,0,0,0) 48%, #183d77 52%, #1f4485 72%, #264c90 80%, #325f9e 100%); }
17955 #de-panel-buttons, #de-panel-info { border-color: #8fbbed; }`,
17956 /* Solid grey */
17957 `{ background-color: #777; }
17958 #de-panel-buttons, #de-panel-info { border-color: #ccc; }
17959 .de-panel-svg:hover { border: 2px solid #444; border-radius: 5px; box-sizing: border-box; transition: none; }`,
17960 /* Transparent blue */
17961 '{ background-color: rgba(0,20,80,.72); }',
17962 /* Square dark */
17963 `{ background: none; background-color: #333; border-radius: 0 !important; }
17964 #de-win-reply.de-win { border-radius: 0 !important; }
17965 #de-panel-buttons, #de-panel-info { border-color: #666; }`
17966 ][Cfg.scriptStyle] }
17967 .de-logo { background: linear-gradient(to bottom, #7b849b, #616b86 8%, #3a414f 52%, rgba(0,0,0,0) 52%), linear-gradient(to bottom, rgba(0,0,0,0) 48%, #121212 52%, #1f2740 100%); }
17968 ${ Cfg.scriptStyle === 2 ?
17969 '.de-panel-svg:hover { border: 2px solid #444; border-radius: 5px; box-sizing: border-box; transition: none; }' :
17970 '.de-panel-button:hover { background-color: rgba(255,255,255,.15); box-shadow: 0 0 3px rgba(143,187,237,.5); }' }\r\n`;
17971
17972 if(Cfg.disabled) {
17973 $css(x).id = 'de-css';
17974 return;
17975 }
17976
17977 x += `
17978 /* Windows */
17979 .de-win .de-win-btn-toggle { transform: rotate(180deg); }
17980 .de-resizer { position: absolute; }
17981 .de-resizer-bottom { height: 6px; bottom: -3px; left: 0; right: 0; cursor: ns-resize; }
17982 .de-resizer-left { width: 6px; top: 0px; bottom: 0px; left: -3px; cursor: ew-resize; }
17983 .de-resizer-right { width: 6px; top: 0px; bottom: 0px; right: -3px; cursor: ew-resize; }
17984 .de-resizer-top { height: 6px; top: -3px; left: 0; right: 0; cursor: ns-resize; }
17985 .de-win > .de-win-head { cursor: move; }
17986 .de-win-buttons { position: absolute; right: 0; margin: 0 2px 0 0; font-size: 0; cursor: pointer; }
17987 .de-win-buttons > svg { transition: background .3s ease, box-shadow .3s ease; }
17988 .de-win-buttons > svg:hover { background-color: rgba(255,255,255,.2); box-shadow: 0 0 2px rgba(255,255,255,.4); }
17989 .de-win-inpost > .de-win-head > .de-win-buttons > svg:hover { background-color: rgba(64,64,64,.15); box-shadow: 0 0 2px rgba(64,64,64,.3); }
17990 #de-win-cfg { width: 355px; }
17991 #de-win-cfg, #de-win-fav, #de-win-hid, #de-win-vid { position: fixed; max-height: 92%; overflow-x: hidden; overflow-y: auto; }
17992 #de-win-cfg > .de-win-body { float: none; display: block; width: auto; min-width: 0; max-width: 100% !important; padding: 0 !important; margin: 0 !important; border: none; }
17993 #de-win-fav > .de-win-body, #de-win-hid > .de-win-body, #de-win-vid > .de-win-body { padding: 6px; border: 1px solid gray; }
17994 #de-win-hid { max-width: 60%; }
17995 #de-win-vid > .de-win-body { display: flex; flex-direction: column; align-items: center; }
17996 #de-win-vid .de-entry { white-space: normal; }
17997 .de-win-head { position: relative; padding: 2px; border-radius: 10px 10px 0 0; color: #F5F5F5; font: bold 14px/16px arial; text-align: center; cursor: default; }
17998
17999 /* Settings window */
18000 .de-block { display: block; }
18001 #de-btn-spell-add { margin-left: auto; }
18002 #de-cfg-bar { display: flex; margin: 0; padding: 0; }
18003 .de-cfg-body { min-height: 353px; padding: 9px 7px 7px; margin-top: -1px; font: 13px/15px arial !important; -moz-box-sizing: content-box; box-sizing: content-box; }
18004 .de-cfg-body, #de-cfg-buttons { border: 1px solid #183d77; border-top: none; }
18005 .de-cfg-button { padding: 0 ${ nav.isFirefox ? '2' : '4' }px !important; margin: 0 4px; height: 21px; font: 12px arial !important; }
18006 #de-cfg-button-debug { padding: 0 2px; font: 13px/15px arial; }
18007 #de-cfg-buttons { display: flex; align-items: center; padding: 3px; }
18008 #de-cfg-buttons > label { flex: 1 0 auto; }
18009 .de-cfg-chkbox { ${ nav.isPresto ? '' : 'vertical-align: -1px !important; ' }margin: 2px 1px !important; }
18010 #de-cfg-info { display: flex; flex-direction: column; }
18011 input[type="text"].de-cfg-inptxt { width: auto; height: auto; min-height: 0; padding: 0 2px !important; margin: 1px 4px 1px 0 !important; font: 13px arial !important; border-width: 1px; }
18012 .de-cfg-inptxt, .de-cfg-label, .de-cfg-select { display: inline; width: auto; height: auto !important; font: 13px/15px arial !important; }
18013 .de-cfg-label { padding: 0; margin: 0; }
18014 .de-cfg-select { padding: 0 2px; margin: 1px 0; font: 13px arial !important; float: none; }
18015 .de-cfg-tab { flex: 1 0 auto; display: block !important; margin: 0 !important; float: none !important; width: auto !important; min-width: 0 !important; padding: 4px 0 !important; box-shadow: none !important; border: 1px solid #444 !important; border-radius: 4px 4px 0 0 !important; opacity: 1; font: bold 12px arial; text-align: center; cursor: default; background-image: linear-gradient(to bottom, rgba(132,132,132,.35) 0%, rgba(79,79,79,.35) 50%, rgba(40,40,40,.35) 50%, rgba(80,80,80,.35) 100%) !important; }
18016 .de-cfg-tab:hover { background-image: linear-gradient(to top, rgba(132,132,132,.35) 0%, rgba(79,79,79,.35) 50%, rgba(40,40,40,.35) 50%, rgba(80,80,80,.35) 100%) !important; }
18017 .de-cfg-tab[selected], .de-cfg-tab[selected]:hover { background-image: none !important; border-bottom: none !important; }
18018 .de-cfg-tab::${ nav.isFirefox ? '-moz-' : '' }selection { background: transparent; }
18019 .de-cfg-unvis { display: none !important; }
18020 .de-depend { padding-left: 17px; }
18021 #de-info-log, #de-info-stats { width: 100%; padding: 0px 7px; }
18022 #de-info-log { overflow-y: auto; border-left: 1px solid grey; }
18023 .de-info-name { flex: 1 0 auto; }
18024 .de-info-row { display: flex; }
18025 #de-info-table { display: flex; flex: 1 0 auto; }
18026 .de-spell-btn { padding: 0 4px; }
18027 #de-spell-editor { display: flex; align-items: stretch; height: 258px; padding: 2px 0; }
18028 #de-spell-panel { display: flex; }
18029 #de-spell-txt { padding: 2px !important; margin: 0; width: 100%; min-width: 0; border: none !important; outline: none !important; font: 12px courier new; ${ nav.isPresto ? '' : 'resize: none !important; ' }}
18030 #de-spell-rowmeter { padding: 2px 3px 0 0; overflow: hidden; min-width: 2em; background-color: #616b86; text-align: right; color: #fff; font: 12px courier new; }
18031 #de-win-cfg.de-win-fixed { z-index: 10001 !important; }
18032
18033 /* Settings window theme */
18034 ${ [/* Gradient darkblue */
18035 `#de-cfg-bar { background-color: #1f2740; }
18036 .de-cfg-tab { border-color: #121421 !important; }`,
18037 /* Gradient blue */
18038 `#de-cfg-bar { background-color: #325f9e; }
18039 .de-cfg-tab { border-color: #183d77 !important; }`,
18040 /* Solid grey */
18041 `#de-cfg-bar, #de-spell-rowmeter { background-color: #777; }
18042 .de-cfg-body, #de-cfg-buttons { border-color: #444; }`,
18043 /* Transparent blue */
18044 `#de-cfg-bar { background-color: rgba(0,20,80,.72); }
18045 .de-cfg-tab { border-color: #001450 !important; }`,
18046 /* Square dark */
18047 `#de-cfg-bar { background-color: #222; }
18048 .de-cfg-body, #de-cfg-buttons { border-color: #666; }`
18049 ][Cfg.scriptStyle] }
18050
18051 /* Favorites window */
18052 .de-entry { display: flex !important; align-items: center; float: none !important; padding: 0 !important; margin: 1px 0 !important; min-width: 0 !important; border: none !important; font-size: 13px; overflow: hidden !important; white-space: nowrap; }
18053 .de-entry-title { flex: auto; padding-left: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
18054 #de-fav-buttons, #de-hid-buttons { padding-top:6px; }
18055 .de-fav-entries { border-top: 1px solid rgba(80,80,80,.3); }
18056 .de-fav-entries-hide, .de-fav-inf-icon:not(.de-fav-closed):not(.de-fav-unavail):not(.de-fav-wait), .de-fav-closed > .de-fav-unavail-use, .de-fav-closed > .de-fav-wait-use, .de-fav-unavail > .de-fav-closed-use, .de-fav-unavail > .de-fav-wait-use, .de-fav-wait > .de-fav-closed-use, .de-fav-wait > .de-fav-unavail-use { display: none; }
18057 .de-fav-del-btn { margin-left: 2px; cursor: pointer; }
18058 .de-fav-del-btn > svg { width: 12px; height: 12px; opacity: 0.65; vertical-align: -2px; }
18059 .de-fav-del-btn[de-checked] > svg { color: red; background-color: rgba(255,0,0,.2); border-radius: 7px; opacity: 1; }
18060 .de-fav-header { display: flex; cursor: pointer; font-size: 13px; }
18061 .de-fav-header-btn { flex: 1 0 auto; margin-right: 2px; font-size: 11px; color: inherit; text-align: right; opacity: 0.65; }
18062 .de-fav-header-link { margin-left: 2px; color: inherit; font-weight: bold; text-decoration: none; outline: none; }
18063 .de-fav-inf { flex: none; padding: 0 4px 0 10px; font: bold 14px serif; cursor: default; }
18064 .de-fav-inf-icon, .de-fav-inf-iwrap { width: 16px; height: 16px; }
18065 .de-fav-inf-icon { margin-bottom: -3px; }
18066 .de-fav-inf-new { color: #424f79; }
18067 .de-fav-inf-new::after { content: " +"; }
18068 .de-fav-inf-old { color: #4f7942; }
18069 .de-fav-inf-you { padding: 0 4px; margin-right: 4px; border-radius: 3px; color: #fff; background-color: #424f79; opacity: 0.65; }
18070 .de-fav-link { flex: none; margin-left: 2px; text-decoration: none; border: none; }
18071 .de-fav-table-unfold > .de-fold-block > .de-fav-entries { display: initial !important; }
18072 .de-fav-unavail { color: #cf4436; }
18073 .de-fold-block { border: 1px solid rgba(120,120,120,.8); border-radius: 2px; }
18074 .de-fold-block:not(:first-child) { border-top: none; }
18075
18076 /* Post panel */
18077 .de-btn-hide > .de-btn-unhide-use, .de-btn-hide-user > .de-btn-unhide-use, .de-btn-unhide > .de-btn-hide-use, .de-btn-unhide-user > .de-btn-hide-use { display: none; }
18078 .de-btn-expthr, .de-btn-fav, .de-btn-fav-sel, .de-btn-hide, .de-btn-hide-user, .de-btn-reply, .de-btn-sage, .de-btn-src, .de-btn-stick, .de-btn-stick-on, .de-btn-unhide, .de-btn-unhide-user, .de-win-btn-clear, .de-win-btn-close, .de-win-btn-toggle { margin: 0 2px -3px 0 !important; cursor: pointer; width: 16px; height: 16px; }${
18079 !pr.form && !pr.oeForm ? '.de-btn-reply { display: none; }' : '' }
18080 .de-post-btns { margin-left: 4px; }
18081 .de-post-btns-back { fill: inherit; stroke: none; }
18082 .de-post-note:not(:empty) { color: inherit; margin: 0 4px; vertical-align: 1px; font: italic bold 12px serif; }
18083 .de-thread-note { font-style: italic; }
18084
18085 /* Sauce buttons */
18086 ${ cont('.de-src-google', 'data:image/gif;base64,R0lGODlhEAAQAMQAAIy0+tHh/gJc8Qlh8UyM9H2r9/3///7//x+OfACSJy+mTZHQos3Te////f///v3HAP+uAPzWjvWTWeUTAPSdl/79/f////39/f///wAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABgALAAAAAAQABAAAAVuICaKh2Eax6hih+W+bqoaLjXdE+UaY2vhwInrInLhdBYbDEOL3GBQS4X2gEiiUBoEAhMIBl6CpaHlvrxocaO1XUQBgsLYxUgkot7AGONS2N0WCwgCYhZFfXaJCQguDiMvC34JCoCOKlgvK49QKyEAOw==') }
18087 ${ cont('.de-src-yandex', 'data:image/gif;base64,R0lGODlhEAAQAMQAAP////v7+/r5+fb29vHx8eLi4tnZ2dTU1NDQ0MvLy8fHx8TExPzJv/immvlXRvq0re4UEdeGhtbFxcnJyby8vKampm5ubv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABcALAAAAAAQABAAAAVx4CWOZCkOQKqq5uoCQhnMQuPcc2CQuc3YuQBJQHQ8BA8HkUgYDZ6Qx+ABeVoVIoL2RmhAtODmpUD23iDkdEFkaBsiEAfE3a6IDngJJALH4ycjCIJyCXCCgiQJhXuLigl2IwqSk5QUJRQLmZqaFiaeniEAOw==') }
18088 ${ cont('.de-src-tineye', 'data:image/gif;base64,R0lGODlhEAAQAMQAAP///wAAAAwOEhg4UDZ7skGNxkuf3Vycxx4nLerw9DlSX2FnanO21Epxg4LO62KOnpXj+ZGcmb7CvbZ6RfxmAIxBCzsGAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABcALAAAAAAQABAAAAWlYGIUZGGc6HgkxzSRozIgdHMUx0NR5hH8v8VjeFsoDAyFpcKcQByMnIGgYDAikmxkOLQWqNAGQhAQLBC/BgSXC0QivwgAgFbjHLQ5XDKXBBRrBA80WAABRgkJPwxfDw2HCYYBfHABDSQMDgoIEgs/ZgoPDmuYEFEHmQ6jT5ckBKirEE8HCgEWJQe5ZwJjQBYRBwQMsk8RFgJkP04sVrHGNAEVEAAhADs=') }
18089 ${ cont('.de-src-saucenao', 'data:image/gif;base64,R0lGODlhEAAQAIAAAP///wAAACwAAAAAEAAQAAACJ4yPacDtvpQCkU1KT0P75i49mbSAZACd6HN2pmbBI7pe9K1+4q5KBQA7') }
18090 ${ cont('.de-src-iqdb', 'data:image/gif;base64,R0lGODlhEAAQAMQAAP//////AP8A//8AAAD//wD/AAAA/wAAANx/hV1ISW9YWvLOd/u0T+WlTNaqcKdtMv/r1mxML7OCVoxtUbmmlfPRuKKGeHpcTvK3nEEvKGpRTCcbGU48OYVua3tkYv///yH5BAEAAB8ALAAAAAAQABAAAAW4INV5ZDcmibM0iVJRnukpmnJJDdNwnDWSk4vCNMnpGJXfwqHReDQJTuTxmEhmqoTFEtRsNhltRTFZTBKUl2IzAmeE2csFA8kcKADLl5NgSDgaGRYAFVYQABkRgAxnGhcdEJEVhxYSDhwNEQkaEhSRGBgIGBUOGBwXERkcExyeFaGjr4E8qgcHDpKgkQpRGhwZYA4Vw6CvPBOpwD0ODlMSoxcJOQ8ZyhccBxkSkRRFDw4SD1/jF5MQIQA7') }
18091 ${ cont('.de-src-tracemoe', 'data:image/gif;base64,R0lGODlhEAAPALMAAAAAAP///9fY18HBwTg4ODw1No6PjoFnZhoBAXNGRf/V1KmRkf///wAAAAAAAAAAACH5BAEAAAwALAAAAAAQAA8AAAQ5EMhJq7046w0I9hMhBAIoiSQ4BiS1tgQrg7EceIM8UDm7S4aBwRIcYgoFjmSxQHASCkWCc6gelJkIADs=') }
18092
18093 /* Posts counter */
18094 .de-post-counter { margin: 0 4px 0 2px; vertical-align: 1px; font: bold 11px tahoma; color: #4f7942; cursor: default; }
18095 .de-post-counter-deleted { color: #727579; }
18096 .de-post-counter-you { vertical-align: 1px; font: bold 11px tahoma; color: #505a7a; cursor: default; }
18097
18098 /* Text markup buttons */
18099 .de-markup-back { fill: #f0f0f0; stroke: #808080; }
18100 #de-txt-panel { display: block; font-weight: bold; cursor: pointer; }
18101 #de-txt-panel > div { display: inline-block; }
18102 #de-txt-panel > div > svg { width: 23px; height: 22px; margin: 0 2px; }\r\n`;
18103
18104 if('animation' in docBody.style) {
18105 x += `
18106 /* Show/hide animation */
18107 @keyframes de-open { 0% { transform: translateY(-100%); } 100% { transform: translateY(0); } }
18108 @keyframes de-close { 0% { transform: translateY(0); } 100% { transform: translateY(-100%); } }
18109 @keyframes de-blink {
18110 0%, 100% { transform: translateX(0); }
18111 10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
18112 20%, 40%, 60%, 80% { transform: translateX(10px); }
18113 }
18114 @keyframes de-post-open-tl { from { transform: translate(-50%,-50%) scale(0); opacity: 0; } }
18115 @keyframes de-post-open-bl { from { transform: translate(-50%,50%) scale(0); opacity: 0; } }
18116 @keyframes de-post-open-tr { from { transform: translate(50%,-50%) scale(0); opacity: 0; } }
18117 @keyframes de-post-open-br { from { transform: translate(50%,50%) scale(0); opacity: 0; } }
18118 @keyframes de-post-close-tl { to { transform: translate(-50%,-50%) scale(0); opacity: 0; } }
18119 @keyframes de-post-close-bl { to { transform: translate(-50%,50%) scale(0); opacity: 0; } }
18120 @keyframes de-post-close-tr { to { transform: translate(50%,-50%) scale(0); opacity: 0; } }
18121 @keyframes de-post-close-br { to { transform: translate(50%,50%) scale(0); opacity: 0; } }
18122 @keyframes de-post-new { from { transform: translate(0,-50%) scaleY(0); opacity: 0; } }
18123 @keyframes de-win-open { from { transform: translate(0,50%) scaleY(0); opacity: 0; } }
18124 @keyframes de-win-close { to { transform: translate(0,50%) scaleY(0); opacity: 0; } }
18125 .de-pview-anim { animation-duration: .2s; animation-timing-function: ease-in-out; animation-fill-mode: both; }
18126 .de-open { animation: de-open .15s ease-out both; }
18127 .de-close { animation: de-close .15s ease-in both; }
18128 .de-blink { animation: de-blink .7s ease-in-out both; }
18129 .de-post-new { animation: de-post-new .2s ease-out both; }
18130 .de-win-open { animation: de-win-open .2s ease-out backwards; }
18131 .de-win-close { animation: de-win-close .2s ease-in both; }\r\n`;
18132 } else {
18133 Cfg.animation = 0;
18134 }
18135
18136 let p = Math.max(Cfg.minImgSize || 0, 50);
18137 x += `
18138 /* Full images */
18139 .de-img-embed, .de-fullimg { border: none; outline: none; cursor: pointer; image-orientation: from-image; }
18140 .de-img-embed { max-width: 200px; max-height: 200px; }
18141 .de-fullimg { display: block; }
18142 .de-fullimg, .de-fullimg-wrap-link { flex: 0 0 auto; transition: none !important; max-width: none; max-height: none; }
18143 .de-fullimg-after { clear: left; }
18144 .de-fullimg-center { position: fixed; margin: 0 !important; z-index: 9999; background-color: #ccc; border: 1px solid black !important; -moz-box-sizing: content-box; box-sizing: content-box; }
18145 .de-fullimg-info { position: absolute; bottom: -22px; left: 50%; padding: 1px 4px; transform: translateX(-50%); background-color: rgba(64,64,64,.8); white-space: nowrap; line-height: 17px; }
18146 .de-fullimg-info > .de-btn-src { color: #fff; }
18147 .de-fullimg-link { float: none !important; display: inline-block; font: bold 12px tahoma; color: #fff !important; text-decoration: none; outline: none; }
18148 .de-fullimg-link:hover { color: #fff !important; background: rgba(64,64,64,.6); }
18149 .de-fullimg-load { position: absolute; z-index: 2; width: 50px; height: 50px; top: 50%; left: 50%; margin: -25px; }
18150 .de-fullimg-rotated { transform-origin: top left; width: auto !important; max-width: none !important; }
18151 .de-fullimg-video-hack { width: 100%; height: calc(100% - 40px); position: absolute; z-index: 1; cursor: pointer; }
18152 .de-fullimg-wrap { position: relative; margin-bottom: 24px; }
18153 .de-fullimg-wrap-center, .de-fullimg-wrap-link, .de-fullimg-video > video { width: 100%; height: 100%; max-height: 100%; }
18154 .de-fullimg-wrap-center > .de-fullimg-wrap-link > .de-fullimg { height: 100%; }
18155 .de-fullimg-wrap-inpost { min-width: ${ p }px; min-height: ${ p }px; float: left; ${ aib.multiFile ? '' : 'margin: 2px 5px; -moz-box-sizing: border-box; box-sizing: border-box; ' } }
18156 .de-fullimg-wrap-nosize > .de-fullimg-wrap-link > .de-fullimg { opacity: 0.3; }
18157 .de-img-btn { position: fixed; top: 50%; z-index: 10000; height: 36px; width: 36px; border-radius: 10px 0 0 10px; color: #f0f0f0; cursor: pointer; }
18158 .de-img-btn > svg { height: 32px; width: 32px; margin: 2px; }
18159 #de-img-btn-auto { right: 0; margin-top: 58px; }
18160 .de-img-btn-auto-on { color: #ffe100; }
18161 #de-img-btn-next { right: 0; margin-top: -18px; }
18162 .de-img-btn-none { display: none; }
18163 #de-img-btn-prev { left: 0; margin-top: -18px; transform: scaleX(-1); }
18164 #de-img-btn-rotate { right: 0; margin-top: 20px; }
18165
18166 /* Embedders */
18167 ${ cont('.de-video-link.de-ytube', 'https://youtube.com/favicon.ico') }
18168 ${ cont('.de-video-link.de-vimeo', 'https://vimeo.com/favicon.ico') }
18169 ${ cont('.de-img-arch', 'data:image/gif;base64,R0lGODlhEAAQALMAAF82SsxdwQMEP6+zzRA872NmZQesBylPHYBBHP///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAAAQABAAQARTMMlJaxqjiL2L51sGjCOCkGiBGWyLtC0KmPIoqUOg78i+ZwOCUOgpDIW3g3KJWC4t0ElBRqtdMr6AKRsA1qYy3JGgMR4xGpAAoRYkVDDWKx6NRgAAOw==') }
18170 ${ cont('.de-img-audio', 'data:image/gif;base64,R0lGODlhEAAQAKIAAGya4wFLukKG4oq3802i7Bqy9P///wAAACH5BAEAAAYALAAAAAAQABAAQANBaLrcHsMN4QQYhE01OoCcQIyOYQGooKpV1GwNuAwAa9RkqTPpWqGj0YTSELg0RIYM+TjOkgba0sOaAEbGBW7HTQAAOw==') }
18171 .de-current::after { content: " \u25CF"; }
18172 .de-img-arch, .de-img-audio { margin-left: 4px; color: inherit; text-decoration: none; font-weight: bold; }
18173 .de-mp3 { margin: 5px 20px; }
18174 .de-video-obj { margin: 5px 20px; white-space: nowrap; }
18175 .de-video-obj-inline { display: inline-block; }
18176 #de-video-btn-resize { padding: 0 14px 8px 0; margin: 0 8px; border: 2px solid; border-radius: 2px; }
18177 #de-video-btn-hide, #de-video-btn-prev { margin-left: auto; }
18178 #de-video-buttons { display: flex; margin-bottom: 2px; align-items: center; width: 100%; line-height: 16px; }
18179 #de-video-buttons > a:not(:hover) { color: inherit; }
18180 .de-video-expanded { width: 854px !important; height: 480px !important; }
18181 #de-video-list { padding: 0 0 4px; overflow-y: auto; width: 100%; }
18182 .de-video-refpost { margin: 0 3px; color: inherit; text-decoration: none; cursor: pointer; }
18183 .de-video-resizer::after { content: "\u2795"; margin: 0 -15px 0 3px; vertical-align: 6px; color: #000; font-size: 12px; cursor: pointer; }
18184 .de-video-player, .de-video-thumb { width: 100%; height: 100%; }
18185 a.de-video-player { display: inline-block; position: relative; border-spacing: 0; border: none; }
18186 a.de-video-player::after { content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAWCAQAAACMYb/JAAAArklEQVR4AYXSr05CYRjA4cPGxjRosTijdvNJzmD1CrwAvQWugASNwGg0MoErOIVCPCMx0hmBMaAA4mPX8/2rT/i+9/1lPu0M3MtCN1OAvS+NEFkDmHqoJwcAbHzUkb9n7C5FqLynCAzdpAhLrynCRc9VnEDpKUWYpUmZIlt5nBQeY889amvGPj33HBvdt45WbAELeWyNP/qu/8dwBrDyVp9UBRi5DYXZdTLxEs77F5bCVAHlDJ1UAAAAAElFTkSuQmCC"); position: absolute;top: 50%; left: 50%; padding: 12px 24px; margin: -22px 0 0 -32px; background-color: rgba(255,0,0,.4); border-radius: 8px; line-height: 0; }
18187 a.de-video-player:hover::after { background-color: rgba(255,0,0,.7); }
18188 .de-video-title[de-time]::after { content: " [" attr(de-time) "]"; color: red; }
18189 .de-video-title[de-time].de-current::after { content: " [" attr(de-time) "] \u25CF"; color: red; }
18190 .de-vocaroo > embed { display: inline-block; }
18191 video { background: black; }
18192
18193 /* File inputs */
18194 .de-file { display: inline-block; vertical-align: top; margin: 1px; height: ${ p = aib.multiFile ? 90 : 130 }px; width: ${ p }px; text-align: center; background-color: rgba(96,96,96,.15); border: 1px dashed grey; }
18195 .de-file > .de-file-img > div { display: flex; justify-content: center; align-items: center; height: ${ p }px; cursor: pointer; }
18196 .de-file > .de-file-utils { display: none; height: 18px; margin-top: -20px; padding: 1px 0; background: rgba(64,64,64,.6); position: relative; -moz-box-sizing: initial; box-sizing: initial; }
18197 .de-file > .de-file-utils > .de-file-rarmsg { display: block; position: absolute; bottom: 20px; width: 100%; margin: 0; background: rgba(64,64,64,.6); color: #fff; }
18198 #de-file-area { margin-top: 1px; width: 275px; min-width: 100%; max-width: 100%; overflow-x: auto; overflow-y: hidden; white-space: nowrap; }
18199 .de-file-drag { background: rgba(96,96,96,.8); border: 1px solid grey; opacity: .7; }
18200 .de-file:hover:not(.de-file-drag) > .de-file-utils { display: block !important; }
18201 img.de-file-img, video.de-file-img { max-width: ${ p }px; max-height: ${ p }px; }
18202 .de-file-input { max-width: 300px; }
18203 .de-file-input + .de-file-utils { margin-left: 4px; }
18204 .de-file-off > .de-file-img > div::after { content: "${ Lng.dropFileHere[lang] }"; display: block; width: 80px; margin: 0 auto; font: 11px arial; opacity: .8; white-space: initial; }
18205 .de-file-rarmsg { margin: 0 2px; vertical-align: 4px; font: bold 11px tahoma; cursor: default; }
18206 .de-file-btn-del, .de-file-btn-rar, .de-file-btn-ren, .de-file-btn-txt { margin: 0 1px; cursor: pointer; }
18207 .de-file-btn-del > svg, .de-file-btn-rar > svg, .de-file-btn-ren > svg, .de-file-btn-txt > svg { width: 16px; height: 16px; }
18208 .de-file-spoil { margin: 0 3px; vertical-align: 1px; }
18209 .de-file-txt-add { font-weight: bold; width: 21px; padding: 0 !important; }
18210 .de-file-txt-input { border: 1px solid #9c9c9c; padding: 2px; font: 12px/16px sans-serif; }
18211 .de-file-txt-noedit { background: rgba(255,255,255,.5); cursor: pointer; }
18212 .de-file-utils { display: inline-block; float: none; vertical-align: -3px; }
18213
18214 /* Reply form */
18215 .de-parea { text-align: center; }
18216 .de-parea-btn-close::after { content: "${ Lng.hideForm[lang] }"; }
18217 .de-parea-btn-thr::after { content: "${ Lng.makeThr[lang] }"; }
18218 .de-parea-btn-reply::after { content: "${ Lng.makeReply[lang] }"; }
18219 #de-pform > form { padding: 0; margin: 0; border: none; }
18220 #de-pform input[type="text"], #de-pform input[type="file"] { width: 200px; }
18221 #de-resizer-text { display: inline-block !important; float: none !important; padding: 5px; margin: ${ nav.isPresto ? '-2px -10px' : '0 0 -2px -10px' }; border-bottom: 2px solid #666; border-right: 2px solid #666; cursor: se-resize; }
18222 .de-win-inpost { float: none; clear: left; display: inline-block; width: auto; padding: 3px; margin: 2px 0; }
18223 .de-win-inpost > .de-resizer { display: none; }
18224 .de-win-inpost > .de-win-head { background: none; color: inherit; }
18225 #de-win-reply { width: auto !important; min-width: 0; padding: 0 !important; border: none !important; }
18226 #de-win-reply.de-win { position: fixed !important; padding: 0 !important; margin: 0 !important; border-radius: 10px 10px 0 0; }
18227 #de-win-reply.de-win > .de-win-body { padding: 2px 2px 0 1px; border: 1px solid gray; }
18228 #de-win-reply.de-win .de-textarea { min-width: 98% !important; resize: none !important; }
18229 #de-win-reply.de-win #de-resizer-text { display: none !important; }
18230 #de-sagebtn { display: inline-block; margin: 3px 4px 0 4px !important; cursor: pointer; }
18231 .de-textarea { display: inline-block; padding: 3px !important; min-width: 275px !important; min-height: 90px !important; resize: both; transition: none !important; }
18232
18233 /* Thread navigation */
18234 #de-thr-navarrow { display: none; position: absolute; top: 50%; left: 34px; transform: translateY(-50%); width: 7px; height: 7px; }
18235 #de-thr-navpanel { color: #F5F5F5; height: 98px; width: 41px; position: fixed; top: 50%; left: 0px; padding: 0; margin: -49px 0 0; background: #777; border: 1px solid #525252; border-left: none; border-radius: 0 5px 5px 0; cursor: pointer; z-index: 1000; }
18236 .de-thr-navpanel-hidden { opacity: .7; margin-left: -34px !important; }
18237 .de-thr-navpanel-hidden > #de-thr-navarrow { display: initial; }
18238 #de-thr-navup { padding: 12px 9px 13px 8px; border-radius: 0 5px 0 0; }
18239 #de-thr-navdown { padding: 13px 9px 12px 8px; border-radius: 0 0 5px 0; }
18240 #de-thr-navup, #de-thr-navdown { width: 41px; height: 49px; -moz-box-sizing: border-box; box-sizing: border-box; }
18241 :not(.de-thr-navpanel-hidden) > #de-thr-navup:hover, :not(.de-thr-navpanel-hidden) > #de-thr-navdown:hover { background: #555; }
18242
18243 /* Other */
18244 .de-abtn { text-decoration: none !important; outline: none; }
18245 .de-button { flex: none; padding: 0 ${ nav.isFirefox ? 2 : 4 }px !important; margin: 1px 2px; height: 24px; font: 13px arial; }
18246 .de-editor { display: block; font: 12px courier new; width: 619px; height: 337px; tab-size: 4; -moz-tab-size: 4; -o-tab-size: 4; }
18247 .de-hidden { float: left; overflow: hidden !important; margin: 0 !important; padding: 0 !important; border: none !important; width: 0 !important; height: 0 !important; display: inline !important; }
18248 .de-input-key { padding: 0 2px !important; margin: 0 !important; font: 13px/15px arial !important; }
18249 input[type="text"].de-input-selected { background: rgba(255,255,150,0.4) !important }
18250 .de-link-backref { text-decoration: none; }
18251 .de-link-parent { outline: 1px dotted !important; }
18252 .de-link-pview { font-weight: bold; }
18253 .de-list { padding-top: 4px; }
18254 .de-list::before { content: "\u25CF"; margin-right: 4px; }
18255 .de-logo { display: inline-block; margin-right: 10px; fill: inherit; color: #F5F5F5; border-radius: 80px 0 0 0; }
18256 .de-logo > svg { width: 144px; height: 144px; }
18257 .de-menu { padding: 0 !important; margin: 0 !important; width: auto !important; min-width: 0 !important; z-index: 10002; border: 1px solid grey !important; text-align: left; }
18258 .de-menu-item { display: block; padding: 3px 10px; color: inherit; text-decoration: none; font: 13px arial; white-space: nowrap; cursor: pointer; }
18259 .de-menu-item:hover { background-color: #222; color: #fff; }
18260 .de-omitted { color: grey; }
18261 .de-omitted::before { content: "${ Lng.postsOmitted[lang] }"; }
18262 .de-popup { overflow: visible !important; clear: both !important; width: auto !important; min-width: 0pt !important; padding: 8px !important; margin: 1px !important; border: 1px solid grey !important; display: block !important; float: right !important; max-width: initial !important; }
18263 .de-popup-btn { display: inline-block; vertical-align: top; color: green; cursor: pointer; line-height: 1.15; }
18264 .de-popup-msg { display: inline-block; white-space: pre-wrap; }
18265 .de-popup-msg > hr { margin: 0 !important; }
18266 .de-post-hiddencontent { display: none !important; }
18267 .de-pview { position: absolute; width: auto; min-width: 0; z-index: 9999; border: 1px solid grey !important; margin: 0 !important; display: block !important; }
18268 .de-pview-info { padding: 3px 6px !important; }
18269 .de-ref-del::after { content: " (Del)"; }
18270 .de-ref-op::after { content: " (OP)"; }
18271 .de-refcomma:last-child { display: none; }
18272 .de-refmap { margin: 10px 4px 4px 4px; font-size: 75%; font-style: italic; }
18273 .de-refmap::before { content: "${ Lng.replies[lang] } "; }
18274 .de-replies-hide::after { content: "${ Lng.hidePosts[lang] }"; }
18275 .de-replies-show::after { content: "${ Lng.showPosts[lang] }"; }
18276 .de-thr-buttons { clear: left; margin-top: 5px; }
18277 ${ aib.t ? '.de-thr-buttons > .de-btn-reply { display: none; }' : '' }
18278 .de-thr-collapse-link::after { content: "${ Lng.collapseThr[lang] }"; }
18279 .de-thr-hid { display: block; padding: 2px; }
18280 .de-thr-updater-link::after { content: "${ Lng.getNewPosts[lang] }"; }
18281 #de-updater-count::before { content: ": "; }
18282 .de-viewed { color: #747488 !important; }
18283 .de-wait, .de-fav-wait , .de-fullimg-load { animation: de-wait-anim 1s linear infinite; }
18284 .de-wait { margin: 0 2px -3px 0 !important; width: 16px; height: 16px; }
18285 #de-wrapper-popup { overflow-x: hidden !important; overflow-y: auto !important; -moz-box-sizing: border-box; box-sizing: border-box; max-height: 100vh; position: fixed; right: 0; top: 0; z-index: 9999; font: 14px arial; cursor: default; }
18286 @keyframes de-wait-anim { to { transform: rotate(360deg); } }
18287 form > hr { clear: both }`;
18288
18289 $css(x).id = 'de-css';
18290 $css('').id = 'de-css-dynamic';
18291 $css('').id = 'de-css-user';
18292 updateCSS();
18293}
18294
18295function updateCSS() {
18296 const x = `
18297 .de-video-obj { width: ${ Cfg.YTubeWidth }px; height: ${ Cfg.YTubeHeigh }px; }
18298 .de-new-post { ${ nav.isPresto ?
18299 'border-left: 4px solid rgba(107,134,97,.7); border-right: 4px solid rgba(107,134,97,.7)' :
18300 'box-shadow: 6px 0 2px -2px rgba(107,134,97,.8), -6px 0 2px -2px rgba(107,134,97,.8)' } !important; }
18301 .de-selected, .de-input-error { ${ nav.isPresto ?
18302 'border-left: 4px solid rgba(220,0,0,.7); border-right: 4px solid rgba(220,0,0,.7)' :
18303 'box-shadow: 6px 0 2px -2px rgba(220,0,0,.8), -6px 0 2px -2px rgba(220,0,0,.8)' } !important; }
18304 ${ Cfg.markMyPosts ?
18305 `.de-mypost { ${ nav.isPresto ?
18306 'border-left: 4px solid rgba(97,107,134,.7); border-right: 4px solid rgba(97,107,134,.7)' :
18307 'box-shadow: 6px 0 2px -2px rgba(97,107,134,.8), -6px 0 2px -2px rgba(97,107,134,.8)'
18308 } !important; }
18309 .de-mypost-reply:not(.de-pview) { position: relative; }
18310 .de-mypost-reply::before { content: ""; position: absolute; top: -0; bottom: 0; left: -1px; border-left: 5px dotted rgba(97,107,134,.8) !important; }` : '' }
18311 ${ Cfg.markMyLinks ?
18312 `.de-ref-del.de-ref-you::after { content: " (Del)(You)"; }
18313 .de-ref-op.de-ref-you::after { content: " (OP)(You)"; }
18314 .de-ref-you::after { content: " (You)"; }` :
18315 '.de-post-counter-you { display: none; }' }
18316 ${ Cfg.postBtnsCSS === 0 ?
18317 `.de-btn-expthr, .de-btn-fav, .de-btn-hide, .de-btn-reply, .de-btn-src, .de-btn-stick, .de-btn-unhide { fill: rgba(0,0,0,0); color: currentColor; }
18318 .de-btn-fav-sel, .de-btn-hide-user, .de-btn-sage, .de-btn-stick-on, .de-btn-unhide-user { fill: rgba(0,0,0,0); color: #F00; }` :
18319 `.de-btn-expthr, .de-btn-fav, .de-btn-hide, .de-btn-reply, .de-btn-sage, .de-btn-src, .de-btn-stick, .de-btn-unhide { color: #F5F5F5; }
18320 .de-btn-expthr, .de-btn-fav, .de-btn-fav-sel, .de-btn-hide, .de-btn-hide-user, .de-btn-reply, .de-btn-src, .de-btn-stick, .de-btn-stick-on, .de-btn-unhide, .de-btn-unhide-user { fill: ${ Cfg.postBtnsCSS === 1 && !nav.isPresto ? 'url(#de-btn-back-gradient)' : Cfg.postBtnsBack }; }
18321 .de-btn-fav-sel { color: #FFE100; }
18322 .de-btn-hide-user { color: #BFFFBF; }
18323 .de-btn-sage { fill: #4B4B4B; }
18324 .de-btn-stick-on { color: #BFFFBF; }
18325 .de-btn-unhide-user { color: #FFBFBF; }` }
18326 .de-fullimg-wrap-inpost > .de-fullimg { ${ Cfg.resizeImgs ?
18327 `max-width: 100%;${ Cfg.resizeImgs === 2 ? ' max-height: 96vh' : '' }` :
18328 'width: auto' }; }
18329 ${ Cfg.maskImgs ?
18330 `${ aib.qPostImg }, .de-img-embed, .de-video-obj { opacity: ${ Cfg.maskVisib / 100 } !important; }
18331 ${ aib.qPostImg.split(', ').join(':hover, ') }:hover, .de-img-embed:hover, .de-video-obj:hover { opacity: 1 !important; }
18332 .de-video-obj:not(.de-video-obj-inline) { clear: both; }` : '' }
18333 ${ Cfg.imgNames === 1 ? '.de-img-name { max-width: 165px; overflow: hidden; }' : '' }
18334 ${ Cfg.imgNames === 1 || Cfg.imgNames === 3 ?
18335 '.de-img-name { display: inline-block; white-space: nowrap; vertical-align: bottom; text-overflow: ellipsis; }' :
18336 Cfg.imgNames === 2 ? '.de-img-name { text-decoration: none !important; text-transform: capitalize; }' : '' }
18337 ${ Cfg.widePosts ? '.de-reply { float: none; width: 99.9%; margin-left: 0; }' : '' }
18338 ${ aib.qPostMsg } { max-width: ${ Cfg.limitPostMsg }px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; }
18339 ${ Cfg.strikeHidd ? '.de-link-hid { text-decoration: line-through !important; }' : '' }
18340 ${ Cfg.noSpoilers === 1 ?
18341 `.spoiler, s { color: #F5F5F5 !important; background-color: #888 !important; }
18342 .spoiler > a, s > a:not(:hover) { color: #F5F5F5 !important; background-color: #888 !important; }` : '' }
18343 ${ Cfg.noSpoilers === 2 ?
18344 `.spoiler, s { color: inherit !important; }
18345 .spoiler > a, s > a:not(:hover) { color: inherit !important; }` : '' }
18346 ${ Cfg.addSageBtn ? '' : '#de-sagebtn, ' }
18347 ${ Cfg.delHiddPost === 1 || Cfg.delHiddPost === 3 ?
18348 '.de-thr-hid, .de-thr-hid + div + br, .de-thr-hid + div + hr, .de-thr-hid + div + br + hr, .de-thr-hid + div + div + hr, ' :
18349 '.de-thr-hid:not([style="display: none;"]) + div + br, ' }
18350 ${ Cfg.imgNavBtns ? '' : '.de-img-btn, ' }
18351 ${ Cfg.imgInfoLink ? '' : '.de-fullimg-info, ' }
18352 ${ Cfg.noPostNames ? `${ aib.qPostName }, ${ aib.qPostTrip }, ` : '' }
18353 ${ Cfg.noBoardRule ? `${ aib.qFormRules }, ` : '' }
18354 ${ Cfg.panelCounter ? '' : '#de-panel-info, ' }
18355 ${ Cfg.removeHidd ? '.de-link-backref.de-link-hid, .de-link-backref.de-link-hid + .de-refcomma, ' : '' }
18356 ${ Cfg.showHideBtn ? '' : '.de-btn-hide, ' }
18357 ${ Cfg.showRepBtn ? '' : '.de-btn-reply, ' }
18358 ${ Cfg.thrBtns || aib.t ? '' : '.de-thr-updater, ' }
18359 ${ Cfg.thrBtns === 1 || Cfg.thrBtns === 2 && !aib.t ? '' : '.de-thr-buttons > svg, ' }
18360 ${ Cfg.ajaxPosting ? '' : '.de-file-btn-rar, .de-file-btn-txt, ' }
18361 ${ Cfg.fileInputs ? '' : '.de-file-txt-wrap, .de-file-btn-txt, ' }
18362 ${ !aib.kusaba && (aib.multiFile || Cfg.fileInputs !== 2) ?
18363 '#de-pform form > table > tbody > tr > td:not([colspan]):first-child, #de-pform form > table > tbody > tr > th:first-child, ' : '' }body > hr, .postarea, .theader { display: none !important; }\r\n`;
18364 $id('de-css-dynamic').textContent = (x + aib.css).replace(/[\r\n\t]+/g, '\r\n\t');
18365 $id('de-css-user').textContent = Cfg.userCSS ? Cfg.userCSSTxt : '';
18366}
18367
18368/* eslint-enable max-len */
18369
18370/* ==[ Main.js ]==============================================================================================
18371 MAIN
18372=========================================================================================================== */
18373
18374function runFrames() {
18375 let inf;
18376 if(typeof GM !== 'undefined') {
18377 inf = GM.info;
18378 } else {
18379 if(typeof GM_info === 'undefined') {
18380 return;
18381 }
18382 inf = GM_info;
18383 }
18384 if(!inf || inf.scriptHandler !== 'Greasemonkey' || !deWindow.frames[0]) {
18385 return;
18386 }
18387 const deMainFuncFrame = frameEl => {
18388 const fDoc = frameEl.contentDocument;
18389 if(fDoc) {
18390 const deWindow = fDoc.defaultView;
18391 deMainFuncInner(
18392 deWindow,
18393 deWindow.opera && deWindow.opera.scriptStorage,
18394 deWindow.FormData,
18395 (x, y) => deWindow.scrollTo(x, y),
18396 typeof localData === 'object' ? localData : null
18397 );
18398 }
18399 };
18400 for(let i = 0, len = deWindow.length; i < len; ++i) {
18401 const frameEl = deWindow.frames[i].frameElement;
18402 const fDoc = frameEl.contentDocument;
18403 if(fDoc) {
18404 if(String(fDoc.defaultView.location) === 'about:blank') {
18405 frameEl.onload = () => deMainFuncFrame(frameEl);
18406 } else if(fDoc.readyState === 'loading') {
18407 fDoc.addEventListener('DOMContentLoaded', () => deMainFuncFrame(frameEl));
18408 } else {
18409 deMainFuncFrame(frameEl);
18410 }
18411 }
18412 }
18413}
18414
18415async function runMain(checkDomains, dataPromise) {
18416 Logger.initLogger();
18417 if(!(docBody = doc.body) || !aib && !(aib = getImageBoard(checkDomains, true))) {
18418 return;
18419 }
18420 let formEl = $q(aib.qDForm + ', form[de-form]');
18421 if(!formEl) {
18422 runFrames();
18423 return;
18424 }
18425 if(docBody.classList.contains('de-runned') ||
18426 aib.observeContent && !aib.observeContent(checkDomains, dataPromise)
18427 ) {
18428 return;
18429 }
18430 Logger.log('Imageboard check');
18431 if(!locStorage) {
18432 if(!checkStorage()) {
18433 return;
18434 }
18435 initNavFuncs();
18436 }
18437 const [favObj] = await (dataPromise || readData());
18438 if(!Cfg.disabled && aib.init && aib.init() || !localData && docBody.classList.contains('de-mode-local')) {
18439 return;
18440 }
18441 docBody.classList.add('de-runned');
18442 Logger.log('Storage loading');
18443 addSVGIcons();
18444 if(Cfg.disabled) {
18445 Panel.initPanel(formEl);
18446 scriptCSS();
18447 return;
18448 }
18449 if('toJSON' in aProto) {
18450 delete aProto.toJSON;
18451 }
18452 initStorageEvent();
18453 DollchanAPI.initAPI();
18454 if(localData) {
18455 aib.prot = 'http:';
18456 aib.host = aib.dm;
18457 aib.b = localData.b;
18458 aib.t = localData.t;
18459 aib.docExt = '.html';
18460 } else {
18461 aib.parseURL();
18462 }
18463 if(aib.t || !Cfg.scrollToTop) {
18464 doc.defaultView.addEventListener('beforeunload', () => {
18465 sesStorage['de-scroll-' + aib.b + (aib.t || '')] = deWindow.pageYOffset;
18466 });
18467 }
18468 Logger.log('Init');
18469 if(Cfg.correctTime) {
18470 dTime = new DateTime(Cfg.timePattern, Cfg.timeRPattern, Cfg.timeOffset, lang,
18471 rp => saveCfg('timeRPattern', rp));
18472 Logger.log('Time correction');
18473 }
18474 MyPosts.readStorage();
18475 Logger.log('Read my posts');
18476 $hide(docBody);
18477 dummy = doc.createElement('div');
18478 formEl = aib.fixHTML(formEl, true);
18479 Logger.log('Replace delform');
18480 pByEl = new Map();
18481 pByNum = new Map();
18482 try {
18483 DelForm.last = DelForm.first = new DelForm(formEl, aib.page, null);
18484 if(!Thread.first) {
18485 console.error('No threads detected!');
18486 }
18487 } catch(err) {
18488 console.error('Delform parsing error:', getErrorMessage(err));
18489 $show(docBody);
18490 return;
18491 }
18492 Logger.log('Parse delform');
18493 const storageName = `de-lastpcount-${ aib.b }-${ aib.t }`;
18494 if(aib.t && !!sesStorage[storageName] && (sesStorage[storageName] > Thread.first.pcount)) {
18495 sesStorage.removeItem(storageName);
18496 deWindow.location.reload();
18497 }
18498 pr = new PostForm($q(aib.qForm));
18499 Logger.log('Parse postform');
18500 if(Cfg.hotKeys) {
18501 HotKeys.enableHotKeys();
18502 Logger.log('Init keybinds');
18503 }
18504 initPage();
18505 Logger.log('Init page');
18506 Panel.initPanel(formEl);
18507 Logger.log('Add panel');
18508 embedPostMsgImages(DelForm.first.el);
18509 Logger.log('Image-links');
18510 DelForm.first.addStuff();
18511 readViewedPosts();
18512 scriptCSS();
18513 Logger.log('Apply CSS');
18514 $show(docBody);
18515 Logger.log('Display page');
18516 toggleInfinityScroll();
18517 Logger.log('Infinity scroll');
18518 const { firstThr } = DelForm.first;
18519 if(firstThr) {
18520 readPostsData(firstThr.op, favObj);
18521 }
18522 Logger.log('Hide posts');
18523 scrollPage();
18524 Logger.log('Scroll page');
18525 if(localData) {
18526 $each($Q('.de-post-removed'), el => {
18527 const post = pByEl.get(el);
18528 if(post) {
18529 post.deletePost(false);
18530 }
18531 });
18532 Logger.log('Local changings');
18533 }
18534 Logger.finish();
18535}
18536
18537function initMain() {
18538 if(doc.readyState !== 'loading') {
18539 needScroll = false;
18540 runMain(true, null);
18541 return;
18542 }
18543 let dataPromise = null;
18544 if((aib = getImageBoard(true, false))) {
18545 if(!checkStorage()) {
18546 return;
18547 }
18548 initNavFuncs();
18549 dataPromise = readData();
18550 }
18551 needScroll = true;
18552 doc.addEventListener('onwheel' in doc.defaultView ? 'wheel' : 'mousewheel', function wFunc(e) {
18553 needScroll = false;
18554 doc.removeEventListener(e.type, wFunc);
18555 });
18556 doc.addEventListener('DOMContentLoaded', () => runMain(false, dataPromise));
18557}
18558
18559initMain();
18560
18561/* ==[ Tail ]== */
18562}(
18563 window,
18564 window.opera && window.opera.scriptStorage,
18565 window.FormData,
18566 (x, y) => window.scrollTo(x, y),
18567 /* global localData */ typeof localData === 'object' ? localData : null
18568));