NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name unlimited favs
// @namespace mail@zera.tax
// @author ZerataX
// @description Adds unlimited local favorite lists to sadpanda
// @homepage https://github.com/ZerataX/unlimted_favorites/
// @homepageURL https://github.com/ZerataX/unlimted_favorites/
// @supportURL https://github.com/ZerataX/unlimted_favorites/issues/
// @license MIT
// @include /^https://e(x|-)hentai\.org/.*$/
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @version 1.0.1
// ==/UserScript==
/* global GM_setValue GM_getValue GM_info GM_addStyle, selected, popUp, show_image_pane, hide_image_pane */
(async function () {
// CONSTANTS
const select = query => window.document.querySelector(query)
const selectAll = query => window.document.querySelectorAll(query)
const urlParams = new URLSearchParams(window.location.search)
// Magic Numbers
const HUEOFFSET = 75
// GLOBALS
let importString = ''
// CLASSES
class FavLists {
constructor (lists = []) {
this._lists = lists
}
get lists () { return this._lists }
newList (name = '', id = _ULF.newID(), galleries = []) {
console.debug(`new List with name: "${name}" and id: "${id}"`)
const index = _ULF.counter++
if (!name) {
name = `Favorites ${9 + index}`
}
const list = new FavList(name, id, galleries)
this._lists.push(list)
}
removeList (listID) {
this._lists.splice(this._lists.findIndex(list => list.id === listID), 1)
}
getListByGid (galleryID) {
return this._lists.find(list => list.getGallery(galleryID))
}
getListByLid (listID) {
return this._lists.find(list => list.id === listID)
}
toJSON () {
return this._lists.map(list => list.toJSON())
}
save () {
_ULF.json.lists = this.toJSON()
saveGM()
console.debug('ULF data saved')
}
}
class FavList {
constructor (name, id, galleries = []) {
this._name = name
this._id = id
this._galleries = galleries
}
get id () { return this._id }
get name () { return this._name }
set name (name) {
this._name = name
}
galleries (search = false, order = 'favorited', page = 0, count = 200) {
let galleries = this._galleries
const index = page * count
const tags = {
artist: [],
character: [],
female: [],
group: [],
language: [],
male: [],
misc: [],
parody: [],
reclass: []
}
if (search) {
const tagsRE = /-?(?:([a-zA-Z]+):)?(".+?\$?"|-?[\w*%$?]+)/g
let match
while (match = tagsRE.exec(search.text)) { // eslint-disable-line no-cond-assign
const [str, namespace, tag] = match
const include = (str[0] !== '-')
const regexString = tag
console.debug(tag)
const regex = new RegExp(regexString.replace(/"/g, '')
.replace(/\?/g, '.')
.replace(/_/g, '.')
.replace(/\*/g, '.*?')
.replace(/%/g, '.*?'), 'i')
switch (namespace) {
case 'artist':
tags.artist.push({ include, regex })
break
case 'f':
tags.female.push({ include, regex })
break
case 'female':
tags.female.push({ include, regex })
break
case 'c':
tags.character.push({ include, regex })
break
case 'character':
tags.character.push({ include, regex })
break
case 'g':
tags.group.push({ include, regex })
break
case 'group':
tags.group.push({ include, regex })
break
case 'circle':
tags.group.push({ include, regex })
break
case 'creator':
tags.group.push({ include, regex })
break
case 'l':
tags.language.push({ include, regex })
break
case 'language':
tags.language.push({ include, regex })
break
case 'm':
tags.male.push({ include, regex })
break
case 'male':
tags.male.push({ include, regex })
break
case 'p':
tags.parody.push({ include, regex })
break
case 'parody':
tags.parody.push({ include, regex })
break
case 'series':
tags.parody.push({ include, regex })
break
case 'r':
tags.reclass.push({ include, regex })
break
case 'reclass':
tags.reclass.push({ include, regex })
break
case 'misc':
tags.misc.push({ include, regex })
break
case undefined:
tags.misc.push({ include, regex })
break
default:
throw SyntaxError(`namespace '${namespace}' not supported`)
}
}
console.debug(tags)
const titleMatcher = (include, title, matchedTags = []) => {
let match = false
if (!(tags.misc.length)) { return false }
tags.misc.forEach(tag => {
if (include) {
if (tag.include && tag.regex.test(title)) {
matchedTags.push(tag)
match = true
}
} else {
if (!tag.include && tag.regex.test(title)) {
matchedTags.push(tag)
match = true
}
}
})
return match
}
const includeMatcher = (includeTag, includeNamespace, tags) => {
if (!includeTag.include) { return true }
return tags.some(tag => {
const [namespace, name] = (tag.includes(':')) ? tag.split(':') : ['misc', tag]
if (includeNamespace === 'misc' || includeNamespace === namespace) {
return includeTag.regex.test(name)
}
return false
})
}
// get galleries to include
console.debug(`galleries before include: ${galleries.length}`)
galleries = galleries.filter(gallery => {
let show = false
const matchedTags = [] // search tags used for notes / title should not be reused for gallery tags
if (search.name) {
show = (gallery.info.title && titleMatcher(true, gallery.info.title, matchedTags)) ||
(gallery.info.title_jpn && titleMatcher(true, gallery.info.title_jpn, matchedTags))
}
if (search.notes) {
show = show || titleMatcher(true, gallery.note, matchedTags)
}
if (search.tags) {
for (const namespace in tags) {
show = tags[namespace].every(tag => {
return matchedTags.some(mTag => tag.regex === mTag.regex) ||
includeMatcher(tag, namespace, gallery.info.tags)
})
if (!show) { break }
}
}
return show
})
console.debug(`galleries after include: ${galleries.length}`)
const excludeMatcher = (string) => {
const [namespace, name] = (string.includes(':')) ? string.split(':') : ['misc', string]
// check if string matches any tag in same namespace or in misc namespace
return (tags[namespace].some(tag => {
if (tag.include) { return false }
return tag.regex.test(name)
})) || (tags.misc.some(tag => {
if (tag.include) { return false }
return tag.regex.test(name)
}))
}
// now check for excludes
galleries = galleries.filter(gallery => {
if (search.name) {
if ((gallery.info.title && titleMatcher(false, gallery.info.title)) ||
(gallery.info.title_jpn && titleMatcher(false, gallery.info.title_jpn))) { return false }
}
if (search.notes) {
if (titleMatcher(false, gallery.note)) { return false }
}
if (search.tags) {
if (gallery.info.tags.some(tag => excludeMatcher(tag))) { return false }
}
return true
})
console.debug(`galleries after exclude: ${galleries.length}`)
console.debug(galleries)
}
switch (order) {
case 'favorited':
galleries.sort((a, b) => {
return new Date(b.timestamp) - new Date(a.timestamp)
})
break
case 'posted':
galleries.sort((a, b) => {
// unix to date: new Date(UNIX_timestamp * 1000);
return b.info.posted - a.info.posted
})
break
default:
throw SyntaxError('"order" has to be either "posted" or "favorited"')
}
return {
galleries: galleries.slice(index, index + count) || null,
number: galleries.length || 0,
tags: tags
}
}
getGallery (id) {
return this._galleries.find(gallery => gallery.id === parseInt(id))
}
removeGallery (id) {
this._galleries = this._galleries.filter(gallery => gallery.id !== parseInt(id))
}
addGallery (id, token, note = '') {
return new Promise((resolve, reject) => {
if (!this.getGallery(id)) {
try {
const gallery = new Gallery(id, token, note)
getGalleryInfo([gallery]).then(info => {
gallery.info = info[0]
this._galleries.push(gallery)
resolve('gallery added!')
})
} catch (error) {
// window.InternalError('could not get gallery info!')
reject(window.InternalError('could not get gallery info!'))
}
} else {
// window.Error('already added to this list!')
reject(window.Error('already added to this list!'))
}
})
}
toJSON () {
return {
name: this._name,
id: this._id,
galleries: this._galleries.map(gallery => gallery.toJSON())
}
}
}
class Gallery {
constructor (id, token, note = '', timestamp = new Date(), info = false) {
this._id = parseInt(id)
this._token = token
this._note = note
this._timestamp = timestamp
this._info = info
}
toJSON () {
return {
gid: this._id,
gt: this._token,
note: this._note,
timestamp: this._timestamp,
info: this._info
}
}
get id () { return this._id }
get token () { return this._token }
get note () { return this._note }
set note (note) { this._note = note }
get timestamp () { return this._timestamp }
get info () { return this._info }
set info (info) { this._info = info }
}
// FUNCTIONS
function parser (html) {
const template = document.createElement('template')
template.innerHTML = html
return template.content.firstElementChild
}
// SCRIPT INITIALIZATION
function createLists () {
const lists = _ULF.json.lists.map(list => {
_ULF.counter++
return new FavList(list.name,
list.id,
list.galleries.map(gallery => new Gallery(gallery.gid,
gallery.gt,
gallery.note,
gallery.timestamp,
gallery.info)))
})
return new FavLists(lists)
}
// LOAD SCRIPT
const _ULF = {
json: loadGM(),
counter: 0,
newID: () => { return '_' + Math.random().toString(36).substr(2, 9) }
}
_ULF.dict = createLists()
console.log(_ULF)
saveGM()
// USERSCRIPT SPECIFIC
function clearFavs () {
_ULF.json = {}
saveGM()
window.location.reload()
}
// save settings persistently
function saveGM () {
// save value to greasemonkey/tampermonkey etc.
GM_setValue('__unlimitedfavs__', JSON.stringify(_ULF.json))
}
// load persistently saved settings, or from a given JSON string
function loadGM (importString) {
const GMString = String(importString || GM_getValue('favsJson', '') || GM_getValue('__unlimitedfavs__', ''))
// set default if no import and no saved version
const defaultValue = {
lists: [{
name: 'Favorites 11',
id: '_' + Math.random().toString(36).substr(2, 9),
galleries: []
}],
version: GM_info.script.version
}
try {
const GMJSON = (GMString && GMString !== '{}') ? JSON.parse(GMString) : defaultValue
console.debug(GMJSON)
// VERSION ADJUSTMENTS
if (!GMJSON.version) {
GMJSON.version = '0.6.5'
}
// Allow to backup before doing any other changes to the user data
if (versionCompare(String(GMJSON.version), GM_info.script.version) === -1) {
// only backup on major version change
const oldMajor = parseInt(GMJSON.version.split('.')[1])
const newMajor = parseInt(GM_info.script.version.split('.')[1])
if (oldMajor !== newMajor) {
const confirmation = window.confirm(`Unlimited Favorites has been updated to version ${GM_info.script.version}, ` +
'do you want to create a backup before updating your data?\n' +
`see what's new here: https://github.com/ZerataX/unlimted_favorites/releases/tag/${GM_info.script.version}`)
if (confirmation) {
const fileName = 'unl_favs_' + new Date().toISOString() + '.json'
download(JSON.stringify(GMJSON), fileName, 'text/json')
}
}
}
if (versionCompare(String(GMJSON.version), '0.7.0') === -1) {
// fix saved JSONs from < 0.7.0 versions
// id from string to int
for (const list of GMJSON.lists) {
for (const gallery of list.galleries) {
gallery.gid = parseInt(gallery.gid)
}
}
}
if (versionCompare(String(GMJSON.version), '0.8.0') === -1) {
// fix saved JSONs from < 0.8.0 versions
// rename date to timestamp
for (const list of GMJSON.lists) {
list.id = '_' + Math.random().toString(36).substr(2, 9)
for (const gallery of list.galleries) {
gallery.timestamp = gallery.date
delete gallery.date
}
}
delete GMJSON.display
delete GMJSON.order
}
// update version
GMJSON.version = GM_info.script.version
// remove old save data
GM_setValue('favsJson', '')
return GMJSON
} catch {
window.alert('something went wrong trying to parse your settings, please download your settings and create an issue them attachedd here: https://github.com/ZerataX/unlimted_favorites/issues/new')
const fileName = 'unl_favs_' + new Date().toISOString() + '.json'
download(GMString, fileName, 'text/json')
return defaultValue
}
}
// SADPANDA API
function handleErrors (response) {
if (!response.ok || response.statux === 200) {
throw Error(response.statusText)
}
return response
}
async function getGalleryInfo (galleries) {
// [' + id +', "' + token + '" ]
const request = new window.Request('https://e-hentai.org/api.php',
{
method: 'POST',
body: JSON.stringify({
method: 'gdata',
gidlist: galleries.map(gallery => [parseInt(gallery.id), gallery.token]),
namespace: 1
})
})
return window.fetch(request)
.then(handleErrors)
.then(response => response.json())
.then(json => json.gmetadata)
.catch(error => {
console.error(error)
})
}
Promise.eachLimit = async (funcs, limit, ms) => {
const rest = funcs.slice(limit)
await Promise.all(funcs.slice(0, limit).map(async func => {
await func()
while (rest.length) {
try {
await sleep(ms).then(() => rest.shift()())
} catch (TypeError) {}
}
}))
}
// download file to local storage
function download (text, name, type) {
const a = document.createElement('a')
const file = new window.Blob([text], { type: type })
a.href = URL.createObjectURL(file)
a.download = name
a.click()
}
// based on: https://stackoverflow.com/a/6078873
function timeConverter (epoch) {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
const a = new Date(epoch * 1000)
const year = a.getFullYear()
const month = months[a.getMonth()]
const date = a.getDate()
const hour = a.getHours()
const min = a.getMinutes() < 10 ? '0' + a.getMinutes() : a.getMinutes()
// const sec = a.getSeconds() < 10 ? '0' + a.getSeconds() : a.getSeconds()
const time = year + '-' + month + '-' + date + ' ' + hour + ':' + min
return time
}
// based on: https://gist.github.com/alexey-bass/1115557
function versionCompare (left, right) {
if (typeof left + typeof right !== 'stringstring') { return false }
const a = left.split('.')
const b = right.split('.')
let i = 0; const len = Math.max(a.length, b.length)
for (; i < len; i++) {
if ((a[i] && !b[i] && parseInt(a[i]) > 0) || (parseInt(a[i]) > parseInt(b[i]))) {
return 1
} else if ((b[i] && !a[i] && parseInt(b[i]) > 0) || (parseInt(a[i]) < parseInt(b[i]))) {
return -1
}
}
return 0
}
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
// UI-MODIFICATIONS
// create list name input to rename/delete/add list
function getLargeThumbnail (url) {
// from: https://ehgt.org/ec/d6/ecd610aa9bc328660cdedfb7ba0200b80962e3b6-3994778-805-1240-png_l.jpg
// to: //ehgt.org/t/ec/d6/ecd610aa9bc328660cdedfb7ba0200b80962e3b6-3994778-805-1240-png_250.jpg
// from: https://exhentai.org/t/8b/d3/8bd3813abf795a744596201ddd7bb162ec95a86d-4438498-2400-3300-jpg_l.jpg
// to: //ehgt.org/t/8b/d3/8bd3813abf795a744596201ddd7bb162ec95a86d-4438498-2400-3300-jpg_250.jpg
return '//ehgt.org//t/' + url.split('/').slice(3).join('/').replace('_l', '_250')
}
function getRatingStyle (rating) {
// not entirely correct
const ratingOffset = [0, 0]
ratingOffset[1] = (80 - Math.round(rating) * 16) * -1
if ((Math.round(rating) - Math.floor(rating)) === 0) {
ratingOffset[0] = -20
ratingOffset[1] += 16
}
return `background-position:${ratingOffset[1]}px ${ratingOffset[0]}px;opacity:1`
}
function getCategoryClass (category) {
switch (category) {
case 'Misc':
return 'ct1'
case 'Doujinshi':
return 'ct2'
case 'Manga':
return 'ct3'
case 'Artist CG':
return 'ct4'
case 'Artist CG Sets':
return 'ct4'
case 'Game CG':
return 'ct5'
case 'Image Set':
return 'ct6'
case 'Cosplay':
return 'ct7'
case 'Asian Porn':
return 'ct8'
case 'Non-H':
return 'ct9'
case 'Western':
return 'cta'
default:
throw window.InternalError(`category type '${category}' not supported!`)
}
}
let inputcounter = 0
function newInput (name, id, template, counter, last = false) {
const selection = template.cloneNode(true)
const input = selection.lastElementChild.lastElementChild
input.name = `favorite_${10 + counter}`
input.placeholder = 'new list...'
input.setAttribute('lid', id)
input.value = name
input.classList.add('ulf', 'ulf_list_rename')
input.addEventListener('focusout', event => clickDeleteList(event.srcElement))
if (last) {
input.id = 'ulf_last_input'
input.addEventListener('focusout', event => clickAddList(event.srcElement))
}
selection.querySelector('.i').style.filter = `invert(100%) hue-rotate(${inputcounter * HUEOFFSET}deg)`
inputcounter++
return selection
}
function newItem (list, template, checked) {
const selection = template.cloneNode(true)
const counterDIV = selection.firstElementChild
const nameDIV = selection.lastElementChild
counterDIV.innerHTML = list._galleries.length
nameDIV.innerHTML = list.name
selection.onclick = () => { window.document.location = `/favorites.php?favcat=0&page=0&lid=${list.id}&ulfpage=0` }
selection.querySelector('.i').style.filter = `invert(100%) hue-rotate(${inputcounter * HUEOFFSET}deg)`
if (checked) {
selection.classList.add('fps')
} else {
selection.classList.remove('fps')
}
inputcounter++
return selection
}
function newExtended (gallery, template, tags = false) {
const selection = template.cloneNode(true)
const image = selection.querySelector('img')
const title = selection.querySelector('.glink')
const category = selection.querySelector('.gl3e')
const categoryTitle = category.children[0]
const dateUploaded = category.children[1]
const rating = category.children[2]
const uploader = category.children[3].firstElementChild
const pageCounter = category.children[4]
const torrent = category.children[5]
const dateFavorited = category.children[6].lastElementChild
const tagsSection = selection.querySelector('.gl3e').nextElementSibling
const note = selection.querySelector('.glfnote')
const checkbox = selection.querySelector('input[name="modifygids[]"]')
image.src = getLargeThumbnail(gallery.info.thumb)
image.alt = gallery.info.title || gallery.info.title_jpn
image.title = gallery.info.title || gallery.info.title_jpn
title.innerHTML = gallery.info.title || gallery.info.title_jpn
dateUploaded.innerHTML = timeConverter(gallery.info.posted)
dateUploaded.onclick = () => popUp(`/gallerypopups.php?gid=${gallery.id}&t=${gallery.token}&act=addfav`, 675, 415)
dateUploaded.id = `posted_${gallery.id}`
uploader.href = `uploader/${gallery.info.uploader}`
uploader.innerHTML = gallery.info.uploader
pageCounter.innerHTML = gallery.info.filecount
dateFavorited.innerHTML = timeConverter(new Date(gallery.timestamp).getTime() / 1000)
categoryTitle.innerHTML = gallery.info.category
categoryTitle.className = `cn ${getCategoryClass(gallery.info.category)}`
if ('torrents' in gallery.info && gallery.info.torrents.length) {
torrent.innerHTML = `<a href="/gallerytorrents.php?gid=${gallery.id}&t=${gallery.token}"` +
`onclick="return popUp('/gallerytorrents.php?gid=${gallery.id}&t=${gallery.token}', 610, 590)" rel="nofollow">` +
'<img src="https://exhentai.org/img/t.png" alt="T" title="Show torrents"></a>'
} else {
torrent.innerHTML = '<img src="https://exhentai.org/img/td.png" alt="T" title="No torrents available">'
}
note.innerHTML = (gallery.note) ? `Note: ${gallery.note}` : ''
note.id = `favnote_${gallery.id}`
note.style = ''
checkbox.value = gallery.id
rating.style = getRatingStyle(gallery.info.rating)
// add tags
const entryPoint = tagsSection.querySelector('tbody')
entryPoint.innerHTML = ''
const tagsCategorized = {}
gallery.info.tags.forEach(tag => {
const [namespace, name] = (tag.includes(':')) ? tag.split(':') : ['misc', tag]
if (!(namespace in tagsCategorized)) {
tagsCategorized[namespace] = []
}
const highlight = tags
? (tags[namespace].some(matchTag => matchTag.include && matchTag.regex.test(name)) ||
tags.misc.some(matchTag => matchTag.include && matchTag.regex.test(name)))
: false
tagsCategorized[namespace].push({ name, highlight })
})
for (const category in tagsCategorized) {
const categoryTR = parser('<tr></tr>')
const categoryTD = parser('<td></td>')
entryPoint.appendChild(categoryTR)
categoryTR.appendChild(parser(`<td class="tc">${category}:</td>`))
categoryTR.appendChild(categoryTD)
tagsCategorized[category].forEach(tag => {
const style = tag.highlight ? 'color:#090909;border-color:#ffbf36;background:radial-gradient(#ffbf36,#ffba00) !important' : ''
categoryTD.appendChild(parser(`<div class="gt" style="${style}" title="${category}:${tag.name}">${tag.name}</div>`))
})
}
// change links
const url = `/g/${gallery.id}/${gallery.token}/`
selection.querySelector('a').href = url
tagsSection.href = url
return selection
}
function newThumbnail (gallery, template, tags = false) {
const selection = template.cloneNode(true)
const image = selection.querySelector('img')
const title = selection.querySelector('.glink')
const category = selection.querySelector('.gl5t')
const categoryTitle = category.firstElementChild.firstElementChild
const dateUploaded = category.firstElementChild.children[1]
const rating = category.lastElementChild.firstElementChild
const pageCounter = category.lastElementChild.children[1]
const torrent = category.lastElementChild.children[2]
const tagsSection = selection.querySelector('.gl6t')
const note = selection.querySelector('.glfnote')
const checkbox = selection.querySelector('input[name="modifygids[]"]')
image.src = getLargeThumbnail(gallery.info.thumb)
image.alt = gallery.info.title || gallery.info.title_jpn
image.title = gallery.info.title || gallery.info.title_jpn
title.innerHTML = gallery.info.title || gallery.info.title_jpn
dateUploaded.innerHTML = timeConverter(gallery.info.posted)
dateUploaded.onclick = () => popUp(`/gallerypopups.php?gid=${gallery.id}&t=${gallery.token}&act=addfav`, 675, 415)
dateUploaded.id = `posted_${gallery.id}`
pageCounter.innerHTML = gallery.info.filecount
categoryTitle.innerHTML = gallery.info.category
categoryTitle.className = `cn ${getCategoryClass(gallery.info.category)}`
if ('torrents' in gallery.info && gallery.info.torrents.length) {
torrent.innerHTML = `<a href="/gallerytorrents.php?gid=${gallery.id}&t=${gallery.token}"` +
`onclick="return popUp('/gallerytorrents.php?gid=${gallery.id}&t=${gallery.token}', 610, 590)" rel="nofollow">` +
'<img src="https://exhentai.org/img/t.png" alt="T" title="Show torrents"></a>'
} else {
torrent.innerHTML = '<img src="https://exhentai.org/img/td.png" alt="T" title="No torrents available">'
}
note.innerHTML = (gallery.note) ? `Note: ${gallery.note}` : ''
note.id = `favnote_${gallery.id}`
note.style = ''
checkbox.value = gallery.id
rating.style = getRatingStyle(gallery.info.rating)
// add tags
tagsSection.innerHTML = ''
const tagsCategorized = {
female: [],
artist: [],
male: [],
character: [],
group: [],
language: [],
misc: [],
parody: [],
reclass: []
}
gallery.info.tags.forEach(tag => {
const [namespace, name] = (tag.includes(':')) ? tag.split(':') : ['misc', tag]
const highlight = tags
? (tags[namespace].some(matchTag => matchTag.include && matchTag.regex.test(name)) ||
tags.misc.some(matchTag => matchTag.include && matchTag.regex.test(name)))
: false
tagsCategorized[namespace].push({ name, highlight })
})
let index = 0
for (const category in tagsCategorized) {
tagsCategorized[category].forEach(tag => {
const style = 'color:#090909;border-color:#b58411c9;background:radial-gradient(#ffbf36,#ffba00);' +
`filter: hue-rotate(${index * HUEOFFSET}deg);`
if (tag.highlight) {
tagsSection.appendChild(parser(`<div class="gt" style="${style}" title="${category}:${tag.name}">${tag.name}</div>`))
}
})
index++
}
// change links
const url = `/g/${gallery.id}/${gallery.token}/`
selection.querySelector('a').href = url
image.parentElement.href = url
return selection
}
function newCompact (gallery, template, offset, tags = false) {
const selection = template.cloneNode(true)
const pane = selection.querySelector('.glthumb')
const paneImage = pane.querySelector('img')
const paneInfo = pane.lastElementChild
const paneCategoryTitle = paneInfo.firstElementChild.firstElementChild
const paneDate = paneInfo.firstElementChild.lastElementChild
const paneRating = paneInfo.lastElementChild.firstElementChild
const panePages = paneInfo.lastElementChild.lastElementChild
const categoryTitle = selection.querySelector('.glcat').firstElementChild
const glcut = selection.querySelector('.glcut')
const userInfo = selection.querySelector('.gl2c').lastElementChild
const dateUploaded = userInfo.children[0]
const rating = userInfo.children[1]
const torrent = userInfo.children[2]
const info = selection.querySelector('.glname')
const title = info.firstElementChild.children[0]
const tagsSection = info.firstElementChild.children[1]
const note = info.firstElementChild.children[2]
const dateFaved = selection.querySelector('.glfav')
const checkbox = selection.querySelector('input[name="modifygids[]"]')
info.onmouseover = () => show_image_pane(gallery.id)
info.onmouseout = () => hide_image_pane(gallery.id)
title.innerHTML = gallery.info.title || gallery.info.title_jpn
glcut.id = `ic${gallery.id}`
pane.id = `it${gallery.id}`
paneImage.src = getLargeThumbnail(gallery.info.thumb)
paneImage.alt = gallery.info.title || gallery.info.title_jpn
paneImage.title = gallery.info.title || gallery.info.title_jpn
paneCategoryTitle.innerHTML = gallery.info.category
paneCategoryTitle.className = `cn ${getCategoryClass(gallery.info.category)}`
paneCategoryTitle.id = `postedpop_${gallery.id}`
paneDate.innerHTML = timeConverter(gallery.info.posted)
paneDate.onclick = () => popUp(`/gallerypopups.php?gid=${gallery.id}&t=${gallery.token}&act=addfav`, 675, 415)
paneDate.id = `posted_${gallery.id}`
paneDate.style = 'border-color: rgb(238, 136, 238); background-color: rgba(224, 128, 224, 0.1);'
paneDate.style.filter = `invert(100%) hue-rotate(${offset * HUEOFFSET}deg)`
paneRating.style = getRatingStyle(gallery.info.rating)
panePages.innerHTML = gallery.info.filecount
dateUploaded.innerHTML = timeConverter(gallery.info.posted)
dateUploaded.onclick = () => popUp(`/gallerypopups.php?gid=${gallery.id}&t=${gallery.token}&act=addfav`, 675, 415)
dateUploaded.id = `posted_${gallery.id}`
dateFaved.innerHTML = timeConverter(new Date(gallery.timestamp).getTime() / 1000).replace(' ', '<br>')
categoryTitle.innerHTML = gallery.info.category
categoryTitle.className = `cn ${getCategoryClass(gallery.info.category)}`
if ('torrents' in gallery.info && gallery.info.torrents.length) {
torrent.innerHTML = `<a href="/gallerytorrents.php?gid=${gallery.id}&t=${gallery.token}"` +
`onclick="return popUp('/gallerytorrents.php?gid=${gallery.id}&t=${gallery.token}', 610, 590)" rel="nofollow">` +
'<img src="https://exhentai.org/img/t.png" alt="T" title="Show torrents"></a>'
} else {
torrent.innerHTML = '<img src="https://exhentai.org/img/td.png" alt="T" title="No torrents available">'
}
note.innerHTML = (gallery.note) ? `Note: ${gallery.note}` : ''
note.id = `favnote_${gallery.id}`
note.style = ''
checkbox.value = gallery.id
rating.style = getRatingStyle(gallery.info.rating)
// add tags
tagsSection.innerHTML = ''
const tagsCategorized = {
female: [],
artist: [],
male: [],
character: [],
group: [],
language: [],
misc: [],
parody: [],
reclass: []
}
gallery.info.tags.forEach(tag => {
const [namespace, name] = (tag.includes(':')) ? tag.split(':') : ['misc', tag]
const highlight = tags
? (tags[namespace].some(matchTag => matchTag.include && matchTag.regex.test(name)) ||
tags.misc.some(matchTag => matchTag.include && matchTag.regex.test(name)))
: false
if (highlight) {
tagsCategorized[namespace].unshift({ name, highlight })
} else {
tagsCategorized[namespace].push({ name, highlight })
}
})
let count = 0
for (const category in tagsCategorized) {
tagsCategorized[category].some(tag => {
const style = (tag.highlight) ? 'color:#090909;border-color:#b58411c9;background:radial-gradient(#ffbf36,#ffba00);' : ''
tagsSection.appendChild(parser(`<div class="gt" style="${style}" title="${category}:${tag.name}">${tag.name}</div>`))
count++
return count > 8
})
if (count > 8) { break }
}
// change links
const url = `/g/${gallery.id}/${gallery.token}/`
title.parentElement.href = url
// image.parentElement.href = url
return selection
}
function newMinimal (gallery, template, offset, tags = false) {
const selection = template.cloneNode(true)
const pane = selection.querySelector('.glthumb')
const paneImage = pane.querySelector('img')
const paneInfo = pane.lastElementChild
const paneCategoryTitle = paneInfo.firstElementChild.firstElementChild
const paneDate = paneInfo.firstElementChild.lastElementChild
const paneRating = paneInfo.lastElementChild.firstElementChild
const panePages = paneInfo.lastElementChild.lastElementChild
const categoryTitle = selection.querySelector('.glcat').firstElementChild
const glcut = selection.querySelector('.glcut')
const dateUploaded = selection.querySelector('.gl2m').children[2]
const rating = selection.querySelector('.gl4m').firstElementChild
const torrent = selection.querySelector('.gldown')
const info = selection.querySelector('.glname')
const title = info.firstElementChild.children[0]
const tagsSection = info.firstElementChild.children[1]
// if no tags the note section is at the position of the tagsection
const note = (tags) ? info.firstElementChild.children[2] : tagsSection
const dateFaved = selection.querySelector('.glfav')
const checkbox = selection.querySelector('input[name="modifygids[]"]')
info.onmouseover = () => show_image_pane(gallery.id)
info.onmouseout = () => hide_image_pane(gallery.id)
title.innerHTML = gallery.info.title || gallery.info.title_jpn
glcut.id = `ic${gallery.id}`
pane.id = `it${gallery.id}`
paneImage.src = getLargeThumbnail(gallery.info.thumb)
paneImage.alt = gallery.info.title || gallery.info.title_jpn
paneImage.title = gallery.info.title || gallery.info.title_jpn
paneCategoryTitle.innerHTML = gallery.info.category
paneCategoryTitle.className = `cn ${getCategoryClass(gallery.info.category)}`
paneCategoryTitle.id = `postedpop_${gallery.id}`
paneDate.innerHTML = timeConverter(gallery.info.posted)
paneDate.onclick = () => popUp(`/gallerypopups.php?gid=${gallery.id}&t=${gallery.token}&act=addfav`, 675, 415)
paneDate.id = `posted_${gallery.id}`
paneDate.style = 'border-color: rgb(238, 136, 238); background-color: rgba(224, 128, 224, 0.1);'
paneDate.style.filter = `invert(100%) hue-rotate(${offset * HUEOFFSET}deg)`
paneRating.style = getRatingStyle(gallery.info.rating)
panePages.innerHTML = gallery.info.filecount
dateUploaded.innerHTML = timeConverter(gallery.info.posted)
dateUploaded.onclick = () => popUp(`/gallerypopups.php?gid=${gallery.id}&t=${gallery.token}&act=addfav`, 675, 415)
dateUploaded.id = `posted_${gallery.id}`
dateFaved.innerHTML = timeConverter(new Date(gallery.timestamp).getTime() / 1000)
categoryTitle.innerHTML = gallery.info.category
categoryTitle.className = `cs ${getCategoryClass(gallery.info.category)}`
if ('torrents' in gallery.info && gallery.info.torrents.length) {
torrent.innerHTML = `<a href="/gallerytorrents.php?gid=${gallery.id}&t=${gallery.token}"` +
`onclick="return popUp('/gallerytorrents.php?gid=${gallery.id}&t=${gallery.token}', 610, 590)" rel="nofollow">` +
'<img src="https://exhentai.org/img/t.png" alt="T" title="Show torrents"></a>'
} else {
torrent.innerHTML = '<img src="https://exhentai.org/img/td.png" alt="T" title="No torrents available">'
}
note.innerHTML = (gallery.note) ? `Note: ${gallery.note}` : ''
note.id = `favnote_${gallery.id}`
note.style = ''
checkbox.value = gallery.id
rating.style = getRatingStyle(gallery.info.rating)
// add tags
tagsSection.innerHTML = ''
const tagsCategorized = {
female: [],
artist: [],
male: [],
character: [],
group: [],
language: [],
misc: [],
parody: [],
reclass: []
}
gallery.info.tags.forEach(tag => {
const [namespace, name] = (tag.includes(':')) ? tag.split(':') : ['misc', tag]
const highlight = tags
? (tags[namespace].some(matchTag => matchTag.include && matchTag.regex.test(name)) ||
tags.misc.some(matchTag => matchTag.include && matchTag.regex.test(name)))
: false
if (highlight) {
tagsCategorized[namespace].unshift({ name, highlight })
} else {
tagsCategorized[namespace].push({ name, highlight })
}
})
let count = 0
for (const category in tagsCategorized) {
tagsCategorized[category].some(tag => {
const style = (tag.highlight) ? 'color:#090909;border-color:#b58411c9;background:radial-gradient(#ffbf36,#ffba00);' : ''
tagsSection.appendChild(parser(`<div class="gt" style="${style}" title="${category}:${tag.name}">${tag.name}</div>`))
count++
return count > 5
})
if (count > 5) { break }
}
// change links
const url = `/g/${gallery.id}/${gallery.token}/`
title.parentElement.href = url
// image.parentElement.href = url
return selection
}
// BUTTON FUNCTIONS
const changeOrder = order => {
const request = new window.Request(`/favorites.php?inline_set=fs_${(order === 'favorited') ? 'p' : 'f'}`)
window.fetch(request).then(() => window.location.reload())
}
const changeMode = mode => {
const request = new window.Request(`/favorites.php?inline_set=dm_${mode}`)
window.fetch(request).then(() => window.location.reload())
}
const clickDeleteList = (input) => {
const value = input.value.trim()
const id = input.getAttribute('lid')
if (input.id === 'ulf_last_input') {
return
}
if (!value) {
// delete list
console.debug(`deleting list ${id}`)
try {
if (_ULF.dict.getListByLid(id).galleries().number) {
const response = window.confirm('This list contains still contains galleries, delete anyways?')
if (!response) { return }
}
_ULF.dict.removeList(id)
} catch (error) {
console.error(error)
window.alert('could not delete gallery')
return
}
input.parentElement.parentElement.remove()
inputcounter--
} else {
// rename list
const list = _ULF.dict.getListByLid(id)
if (!list) {
console.error(`list with ${id} not found`)
return
}
if (list.name !== value) {
console.debug(`changing ${list.name} to ${value}`)
try {
list.name = value
} catch (error) {
console.log(error)
}
}
}
_ULF.dict.save()
}
const clickAddList = (input) => {
const favsel = select('#favsel')
const template = input.parentElement.parentElement.cloneNode(true)
const id = input.getAttribute('lid')
if (input.id !== 'ulf_last_input') {
return
}
if (input.value.trim() !== '') {
console.debug('creating new list...')
_ULF.dict.newList(input.value, id)
input.removeAttribute('id')
input.classList.add('rename')
favsel.appendChild(newInput('', _ULF.newID(), template, _ULF.counter, true))
}
_ULF.dict.save()
}
const clickImport = (input) => {
if (!importString) {
window.alert('no file selected!')
return
}
try {
_ULF.json = loadGM(importString)
} catch (err) {
window.alert('no valid json supplied')
return
}
saveGM()
console.log('imported:')
console.log(_ULF.json)
window.location.reload()
}
const clickFileImport = (input) => {
const reader = new window.FileReader()
reader.onload = function () {
try {
importString = reader.result
console.log(JSON.parse(importString))
} catch (err) {
window.alert('no valid json supplied')
}
}
reader.readAsText(input.files[0])
}
// DIRECTORIES
// FAVORITES PAGE
if (window.location.pathname.includes('favorites.php')) {
const page = parseInt(urlParams.get('ulfpage'))
const lid = urlParams.get('lid')
const parent = select('h1 + .nosel')
const template = parent.children[9].cloneNode(true)
const sorter = select('.ido').children[3].firstElementChild
const order = sorter.innerText.split(' ')[1].trim().toLowerCase()
const mode = select('select')
const searchForm = select('form')
const searchBox = select('input[name=f_search]')
const searchButton = select('input[type=submit]')
const [nameCheck, tagsCheck, noteCheck] = selectAll('input[type=checkbox')
const pageSelections = [select('.ptt tr'), select('.ptb tr')]
const sum = select('.ip')
const count = 200
if (lid) {
const list = _ULF.dict.getListByLid(lid)
const offset = _ULF.dict._lists.indexOf(list)
// select current list item
const children = [...parent.children]
children.forEach(item => {
item.classList.remove('fps')
})
// modify search button
searchForm.onkeydown = (event) => {
const x = event.which
if (x === 13) {
event.preventDefault()
insertGalleries(searchBox.value)
}
}
searchButton.type = 'button'
searchButton.onclick = () => insertGalleries(searchBox.value)
// TODO: disable search enter
// change use posted/favorited order links
const orderLink = sorter.querySelector('a')
orderLink.href = '#'
orderLink.onclick = () => changeOrder(order)
// change mode links
mode.onchange = event => changeMode(event.srcElement.value)
// get gallery template
let galleryTemplate
let galleryLocation
switch (mode.value) {
case 'm':
galleryLocation = select('table.itg > tbody')
galleryTemplate = galleryLocation.children[1].cloneNode(true)
break
case 'p':
galleryLocation = select('table.itg > tbody')
galleryTemplate = galleryLocation.children[1].cloneNode(true)
break
case 'l':
galleryLocation = select('table.itg > tbody')
galleryTemplate = galleryLocation.children[1].cloneNode(true)
break
case 'e':
galleryLocation = select('table.itg > tbody')
galleryTemplate = galleryLocation.firstElementChild.cloneNode(true)
break
case 't':
galleryLocation = select('.itg.gld')
galleryTemplate = galleryLocation.firstElementChild.cloneNode(true)
break
default:
throw window.InternalError('current mode not supported, only supports ' +
'Minimal, Minimal+, Compact, Extended, Thumbnail')
}
const insertGalleries = (string = false) => {
if (string) {
if (window.location.hash) {
document.location = window.location.href.split('#')[0] + `#${encodeURIComponent(string)}`
} else {
document.location += `#${encodeURIComponent(string)}`
}
} else {
document.location = window.location.href.split('#')[0] + '#'
}
const search = (string)
? {
text: string,
name: nameCheck.checked,
notes: noteCheck.checked,
tags: tagsCheck.checked
}
: false
console.debug(search || 'no search')
const { galleries, number, tags } = list.galleries(search, order, page, count)
console.debug(`found ${number} galleries`)
sum.innerHTML = `Showing ${number.toLocaleString()} results`
orderLink.href = `#${string}`
// adjust page selection
pageSelections.forEach(pageSelection => {
const pageTemplate = pageSelection.children[1].cloneNode(true)
const pages = Math.ceil(number / count)
pageSelection.innerHTML = ''
// < element
if (page === 0) {
pageSelection.appendChild(parser('<td class="ptdd"><</td>'))
} else {
// if out of bounds
if ((page - 1) * count > number) {
console.error('out of bounds')
}
const pageElement = pageTemplate.cloneNode(true)
pageElement.querySelector('a').innerHTML = '<'
pageElement.querySelector('a').href = `/favorites.php?page=1&favcat=0&lid=${lid}&ulfpage=${page - 1}#${encodeURIComponent(string)}`
pageSelection.appendChild(pageElement)
}
// [0-9] elements
for (let index = 0; index < pages; index++) {
const pageElement = pageTemplate.cloneNode(true)
if (page !== index) {
pageElement.onclick = event => {
const href = event.srcElement.href || event.srcElement.firstElementChild.href
document.location = href
}
pageElement.classList.remove('ptds')
} else {
pageElement.classList.add('ptds')
}
pageElement.querySelector('a').innerHTML = index + 1
pageElement.querySelector('a').href = `/favorites.php?page=1&favcat=0&lid=${lid}&ulfpage=${index}#${encodeURIComponent(string)}`
pageSelection.appendChild(pageElement)
}
// > element
if (page === pages - 1) {
pageSelection.appendChild(parser('<td class="ptdd">></td>'))
} else {
pageTemplate.querySelector('a').innerHTML = '>'
pageTemplate.querySelector('a').href = `/favorites.php?page=1&favcat=0&lid=${lid}&ulfpage=${page + 1}#${encodeURIComponent(string)}`
pageTemplate.classList.remove('ptds')
pageSelection.appendChild(pageTemplate)
}
})
// add gallery items
galleryLocation.innerHTML = ''
let firstRow = ''
switch (mode.value) {
case 'm':
firstRow = parser('<tr><th></th><th>Published</th><th></th><th>Title</th><th></th><th colspan="2">Favorited</th></tr>')
galleryLocation.append(firstRow)
galleries.forEach(gallery => galleryLocation.append(newMinimal(gallery, galleryTemplate, offset)))
break
case 'p':
firstRow = parser('<tr><th></th><th>Published</th><th></th><th>Title</th><th></th><th colspan="2">Favorited</th></tr>')
galleryLocation.append(firstRow)
galleries.forEach(gallery => galleryLocation.append(newMinimal(gallery, galleryTemplate, offset, tags)))
break
case 'l':
firstRow = parser('<tr><th></th><th>Published</th><th>Title</th><th colspan="2">Favorited</th></tr>')
galleryLocation.append(firstRow)
galleries.forEach(gallery => galleryLocation.append(newCompact(gallery, galleryTemplate, offset, tags)))
break
case 'e':
galleries.forEach(gallery => galleryLocation.append(newExtended(gallery, galleryTemplate, tags)))
break
case 't':
galleries.forEach(gallery => galleryLocation.append(newThumbnail(gallery, galleryTemplate, tags)))
break
default:
throw window.InternalError('current mode not supported, only supports ' +
'Minimal, Minimal+, Compact, Extended, Thumbnail')
}
}
// save search in hash / reapply search from hash
if (window.location.hash) {
const hash = decodeURIComponent(window.location.hash.slice(1))
searchBox.value = (hash !== 'false') ? hash : ''
insertGalleries(searchBox.value)
} else {
insertGalleries()
}
// start a search when changing text input or categories
searchBox.onchange = () => insertGalleries(searchBox.value)
// searchBox.oninput = () => {
// let location = window.location.href.split('#')[0]
// searchForm.action = `${location}#${searchBox.value}`
// }
nameCheck.onclick = () => insertGalleries(searchBox.value)
tagsCheck.onclick = () => insertGalleries(searchBox.value)
noteCheck.onclick = () => insertGalleries(searchBox.value)
}
// insert favorite list items
const end = parent.children[10]
_ULF.dict.lists.forEach(list => {
parent.insertBefore(newItem(list, template, lid === list.id, mode.value), end)
})
}
// GALLERY PAGE
if (window.location.pathname.includes('/g/')) {
const [id, token] = window.location.pathname.split('/').slice(2)
const list = _ULF.dict.getListByGid(id)
// add favorite icon if gallery is in list
if (list) {
const offset = _ULF.dict._lists.indexOf(list)
const favBtn = select('#gdf')
// dumb gallery info
console.debug(list.getGallery(id))
favBtn.innerHTML = '<div style="float:left; cursor:pointer" id="fav">' +
`<div class="i" style="background-image:url(https://exhentai.org/img/fav.png); background-position:0px -173px; margin-left:16px" title="${list.name}">` +
`</div></div><div style="float:left"> <a id="favoritelink" href="#" onclick="return false">${list.name}</a></div><div class="c"></div>`
favBtn.querySelector('.i').style.filter = `invert(100%) hue-rotate(${offset * HUEOFFSET}deg)`
} else {
// dumb gallery info
const gallery = { id, token }
getGalleryInfo([gallery]).then(info => console.debug(info[0]))
}
}
// SETTINGS
if (window.location.pathname.includes('uconfig.php')) {
console.log('adding UI to settings...')
const favsel = select('#favsel')
// add list inputs
{
const template = favsel.lastElementChild.cloneNode(true)
template.querySelector('.i').title = 'unlimited favorites'
favsel.previousElementSibling.insertAdjacentHTML('beforeend',
`<br><br>
<b>Unlimited favorites:</b><br>
Write into the last input to create a <b>new list</b><br>
Click outside the text inputs to <b>save</b> your modifications!`)
_ULF.dict.lists.forEach((list, index) => {
favsel.appendChild(newInput(list.name, list.id, template, index))
})
favsel.appendChild(newInput('', _ULF.newID(), template, _ULF.counter, true))
}
// add buttons
{
const template = select('#apply').firstElementChild.cloneNode(true)
template.removeAttribute('id')
template.type = 'button'
template.classList.add('ulf')
const btnFileExport = template.cloneNode(true)
const btnFileImport = template.cloneNode(true)
const btnFakeFileImport = template.cloneNode(true)
const btnImport = template.cloneNode(true)
const btnClear = template.cloneNode(true)
const btnUpdate = template.cloneNode(true)
/*
btnFileImport.style.padding = '2px 33px 2px'
btnFileImport.style.margin = '0'
btnFileExport.style.padding = '2px 33px 2px'
btnFileExport.style.margin = '0'
*/
btnImport.value = 'import favs'
btnImport.setAttribute('for', 'ulf_import_json')
btnImport.onclick = event => clickImport(event.srcElement)
btnClear.value = 'delete all'
btnClear.onclick = () => {
if (window.confirm('delete all list? this action can\'t be undone!')) {
clearFavs()
}
}
btnFileImport.type = 'file'
btnFileImport.setAttribute('accept', '.json,application/json')
btnFileImport.id = 'ulf_import_json'
btnFileImport.style.display = 'none'
btnFileImport.onchange = event => clickFileImport(event.srcElement)
btnFakeFileImport.type = 'button'
btnFakeFileImport.value = 'select file'
btnFakeFileImport.id = 'ulf_import_button'
btnFakeFileImport.onclick = () => select('#ulf_import_json').click()
btnFileExport.name = 'ulf_export'
btnFileExport.value = 'export favs'
btnFileExport.onclick = () => {
const fileName = 'unl_favs_' + new Date().toISOString() + '.json'
download(JSON.stringify(_ULF.json), fileName, 'text/json')
}
btnUpdate.name = 'ulf_update'
btnUpdate.value = 'update all'
btnUpdate.alt = 'update information for all galleries (this may take a while)'
btnUpdate.onclick = async () => {
const response = window.confirm('Update all Galleries? This can take a while and possibly be destructive, consider creating a backup first!')
const counterMax = _ULF.dict.lists.reduce((acc, cur) => (acc._galleries) ? acc._galleries.length : acc + cur._galleries.length)
console.debug(`found ${counterMax} galleries`)
if (response === true) {
const limit = 25
const parallel = 4
const promises = []
await _ULF.dict.lists.forEach(async list => {
console.log(`queueing list ${list.name}`)
const max = list._galleries.length
for (let current = 0; current < max; current += (limit * parallel)) {
console.log(`queueing from ${current} to ${current + limit * parallel}`)
for (let x = 0; x < parallel; x++) {
promises.push(async () => {
const slice = list._galleries.slice(current + (limit * x), current + (limit * (x + 1)))
if (slice.length) {
getGalleryInfo(slice).then(entries => {
entries.forEach(info => {
list.getGallery(info.gid).info = info
})
})
}
})
}
}
})
await Promise.eachLimit(promises, parallel, 500).then(() => {
_ULF.dict.save()
window.alert(`updated ${counterMax} galleries!`)
})
}
}
// insert all buttons
const importBox = parser('<div id="ulf_import_box"></div>')
importBox.append(btnFakeFileImport)
importBox.append(btnFileImport)
importBox.append(btnImport)
importBox.append(btnFileExport)
importBox.append(btnClear)
importBox.append(btnUpdate)
favsel.parentElement.appendChild(importBox)
}
}
// ADD FAVORITES
if (window.location.pathname.includes('gallerypopups.php')) {
const gid = urlParams.get('gid')
const token = urlParams.get('t')
const list = _ULF.dict.getListByGid(gid) || false
let currentClick = list.id || selected
let lastClick = currentClick
const note = select('textarea[name=favnote]')
if (list) {
const gallery = list.getGallery(gid)
note.value = gallery.note
}
// disable apply button
const applyBtn = select('input[name=apply]')
const form = select('form')
applyBtn.type = 'button'
form.id = 'galpop_disabled'
const submitFavs = (src, apply = false) => {
const addULF = new Promise((resolve, reject) => {
currentClick = src.id
if (currentClick === lastClick || apply === true) {
console.debug('clicked already selected option, trying to perform action')
if (list) {
if (currentClick === 'favdel') {
console.debug(`removing a gallery from ULF list '${list.name}'`)
list.removeGallery(gid)
_ULF.dict.save()
// window.opener.location.reload(false)
resolve('gallery removed')
} else if (src.hasAttribute('lid')) {
const newList = _ULF.dict.getListByLid(src.getAttribute('lid'))
if (list === newList) {
console.debug('updating gallery info')
const gallery = list.getGallery(gid)
gallery.note = note.value
_ULF.dict.save()
select('#favdel').checked = true
resolve('gallery updated')
// don't understand why this needs to be here
window.opener.location.reload(false)
form.submit()
} else {
console.debug(`moving gallery from '${list.name}' to '${newList.name}'`)
list.removeGallery(gid)
newList.addGallery(gid, token, note.value).then(response => {
console.debug(response)
_ULF.dict.save()
select('#favdel').checked = true
resolve('gallery moved')
// don't understand why this needs to be here
window.opener.location.reload(false)
form.submit()
})
}
} else {
console.debug(`moving gallery from ULF list '${list.name}' to '${currentClick}'`)
list.removeGallery(gid)
_ULF.dict.save()
// window.opener.location.reload(false)
resolve('gallery moved')
}
} else {
if (src.hasAttribute('lid')) {
const newList = _ULF.dict.getListByLid(src.getAttribute('lid'))
console.debug(`adding a gallery to ULF list '${newList.name}'`)
newList.addGallery(gid, token, note.value).then(response => {
console.debug(response)
_ULF.dict.save()
select('#favdel').checked = true
resolve('gallery added')
// don't understand why this needs to be here
window.opener.location.reload(false)
form.submit()
})
} else if (currentClick === 'favdel') {
resolve('removing a gallery from normal list')
} else {
resolve('adding a gallery to normal list')
}
}
}
reject('do nothing') // eslint-disable-line prefer-promise-reject-errors
})
// after adding gallery to list submit form
addULF.then(response => {
console.debug(response)
form.submit()
}).catch(response => {
lastClick = currentClick
})
}
let inputcounter = 0
applyBtn.onclick = event => submitFavs(select("input[type='radio']:checked"), true)
const newButton = (name, id, template, counter) => {
const selection = template.cloneNode(true)
const input = selection.firstElementChild.firstElementChild
input.setAttribute('id', `fav${10 + counter}`)
input.setAttribute('lid', id)
input.value = name
input.classList.add('ulf', 'ulf_add_gallery')
input.onclick = event => submitFavs(event.srcElement)
if (list && id === list.id) {
input.checked = true
} else {
input.checked = false
}
selection.children[1].style.filter = `invert(100%) hue-rotate(${inputcounter * HUEOFFSET}deg)`
selection.children[1].onclick = () => input.click()
selection.children[2].innerHTML = name
selection.children[2].onclick = () => input.click()
inputcounter++
return selection
}
// add ULF button
const parent = select('.nosel')
const template = parent.children[9].cloneNode(true)
const children = [...parent.children]
children.forEach(item => {
const input = item.firstElementChild.firstElementChild
input.onclick = event => submitFavs(event.srcElement)
})
if (parent.children.length === 11) {
const deleteBtn = parent.lastElementChild
_ULF.dict.lists.forEach((list, index) => {
parent.insertBefore(newButton(list.name, list.id, template, index), deleteBtn)
})
} else {
const deleteBtn = parser('<div style="height:25px; cursor:pointer">' +
'<div style="float:left"><input type="radio" name="favcat" value="favdel" id="favdel" style="position:relative; top:-1px"></div>' +
'<div style="float:left; padding-left:5px" onclick="document.getElementById(\'favdel\').click()">Remove from Favorites</div>' +
'<div class="c"></div>' +
'</div>')
_ULF.dict.lists.forEach((list, index) => {
parent.append(newButton(list.name, list.id, template, index))
})
parent.append(deleteBtn)
const input = select('#favdel')
input.onclick = event => submitFavs(event.srcElement)
}
}
// MAIN PAGE
{
const mode = window.document.querySelector('select')
const items = window.document.querySelectorAll('.gldown')
// add fav highlight to gallery item
console.debug(`changing favorite highlighting for ${items.length} galleries`)
items.forEach(item => {
let favButton
switch (mode.value) {
case 'm':
favButton = item.parentElement.previousElementSibling.children[2]
break
case 'p':
favButton = item.parentElement.previousElementSibling.children[2]
break
case 'l':
favButton = item.parentElement.children[0]
break
case 'e':
favButton = item.parentElement.children[1]
break
case 't':
favButton = item.parentElement.previousElementSibling.children[1]
break
default:
throw window.InternalError('current mode not supported, only supports ' +
'Minimal, Minimal+, Compact, Extended, Thumbnail')
}
const gid = favButton.id.replace('posted_', '')
const list = _ULF.dict.getListByGid(gid) || _ULF.dict.getListByLid(urlParams.get('lid'))
if (list) {
const offset = _ULF.dict._lists.indexOf(list)
favButton.style = 'border-color: rgb(238, 136, 238); background-color: rgba(224, 128, 224, 0.1);'
favButton.style.filter = `invert(100%) hue-rotate(${offset * HUEOFFSET}deg)`
}
})
}
})()
GM_addStyle(`* {
.ulf_import_box {
width: 250px;
}
.ulf_import_box > input {
width: 100%
}
}`)