// ==UserScript== // @name GGn Tag selector // @namespace ggntagselector // @version 1.1.1 // @match *://gazellegames.net/upload.php* // @match *://gazellegames.net/torrents.php?*action=advanced* // @match *://gazellegames.net/torrents.php*id=* // @match *://gazellegames.net/requests.php* // @match *://gazellegames.net/user.php*action=edit* // @grant GM.setValue // @grant GM.getValue // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @license MIT // @author tweembp, ingts // @description Enhanced Tag selector for GGn // @downloadURL none // ==/UserScript== // noinspection CssUnresolvedCustomProperty,CssUnusedSymbol const locationhref = location.href const isUploadPage = locationhref.includes('upload.php'), isGroupPage = locationhref.includes('torrents.php?id='), isSearchPage = locationhref.includes('action=advanced'), isRequestPage = locationhref.includes('requests.php') && !locationhref.includes('action=new'), isCreateRequestPage = locationhref.includes('action=new'), isUserPage = locationhref.includes('user') const SEPERATOR = '|' const TAGSEPERATOR = ', ' const defaultHotkeys = { 'favorite': [ 'shift + digit1', 'shift + digit2', 'shift + digit3', 'shift + digit4', 'shift + digit5', 'shift + digit6', 'shift + digit7', 'shift + digit8', 'shift + digit9', ], 'preset': [ 'alt + digit1', 'alt + digit2', 'alt + digit3', 'alt + digit4', 'alt + digit5', 'alt + digit6', 'alt + digit7', 'alt + digit8', 'alt + digit9', ], } const defaulthotkeyPrefixes = { 'show_indices': 'shift' } const modifiers = ["shift", "alt", "ctrl", "cmd"] const categoryDict = { "genre": [ "4x", "action", "adventure", "aerial.combat", "agriculture", "arcade", "auto.battler", "beat.em.up", "board.game", "building", "bullet.hell", "card.game", "casual", "childrens", "city.building", "clicker", "d10.system", "d20.system", "driving", "dungeon.crawler", "educational", "exploration", "fighting", "fitness", "game.show", "grand.strategy", "hack.and.slash", "hidden.object", "horror", "hunting", "interactive.fiction", "jigsaw", "karaoke", "management", "match.3", "metroidvania", "mini.game", "music", "open.world", "parody", "party", "pinball", "platform", "point.and.click", "puzzle", "quiz", "rhythm", "roguelike", // "roguelite", "role.playing.game", "runner", "sandbox", "shoot.em.up", "shooter", "first.person.shooter", "third.person.shooter", "simulation", "solitaire", "space", "stealth", "strategy", "real.time.strategy", "turn.based.strategy", "stunts", "survival", "tabletop", "tactics", "text.adventure", "time.management", "tower.defense", "trivia", "typing", "vehicular.combat", "visual.novel", "wargame", "word.game", "word.construction", ], "theme": [ "adult", "romance", "comedy", "crime", "drama", "fantasy", "historical", "mystery", "thriller", "science.fiction", ], "sports": [ "american.football", "baseball", "basketball", "billiards", "blackjack", "bowling", "boxing", "chess", "cricket", "cycling", "extreme.sports", "fishing", "go", "golf", "hockey", "mahjong", "pachinko", "pinball", "poker", "racing", "rugby", "skateboarding", "slots", "snowboarding", "soccer", "sports", "tennis", "wrestling", ], "simulation": [ "business.simulation", "construction.simulation", "dating.simulation", "flight.simulation", "life.simulation", "space.simulation", "vehicle.simulation", "walking.simulation", ], "ost": [ "acappella", "acid.house", "acid.jazz", "acid.techno", "acoustic", "afrobeat", "alternative", "ambient", "arrangement", "ballad", "black.metal", "breakbeat", "breakcore", "chill.out", "chillwave", "chipbreak", "chiptune", "choral", "citypop", "classical", "country", "dance", "dark.ambient", "dark.electro", "dark.synth", "dark.wave", "downtempo", "dream.pop", "drum.and.bass", "dubstep", "electro", "electronic", "electronic.rock", "epic.metal", "euro.house", "experimental", "folk", "funk", "happy.hardcore", "hardcore", "heavy.metal", "hip.hop", "horrorcore", "house", "hymn", "indie.pop", "indie.rock", "industrial", "instrumental", "jazz", "lo.fi", "modern.classical", "new.age", "opera", "orchestral", "phonk", "piano", "pop", "rhythm.and.blues", "rock", "smooth.jazz", "sound.effects", "symphonic", "synth", "synth.pop", "synthwave", "traditional", "techno", "trance", "vaporwave", "violin", "vocal", ], "books": [ "art.book", "collection", "comic.book", "fiction", "game.design", "game.programming", "psychology", "social.science", "gamebook", "graphic.novel", "guide", "magazine", "non.fiction", "novelization", "programming", "business", "reference", "study" ], "applications": [ "apps.windows", "apps.linux", "apps.mac", "apps.android", "utility", "development", ], } // relevant keys for each upload category const categoryKeys = { 'Games': ["genre", "theme", "sports", "simulation"], 'E-Books': ['books'], 'Applications': ['applications'], 'OST': ['ost'] } const specialTags = ['pack', 'collection'] // common functions function titlecase(s) { let out = s.split('.').map((e) => { if (!["and", "em"].includes(e)) { return e[0].toUpperCase() + e.slice(1) } else { return e } }).join(' ') return out[0].toUpperCase() + out.slice(1) } function normalise_combo_string(s) { return s.trim().split('+').map((c) => c.trim().toLowerCase()).join(' + ') } function observe_element(element, property, callback, delay = 0) { let elementPrototype = Object.getPrototypeOf(element) if (elementPrototype.hasOwnProperty(property)) { let descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property) Object.defineProperty(element, property, { get: function () { return descriptor.get.apply(this, arguments) }, set: function () { let oldValue = this[property] descriptor.set.apply(this, arguments) let newValue = this[property] if (typeof callback == "function") { setTimeout(callback.bind(this, oldValue, newValue), delay) } return newValue }, configurable: true }) } } if (!isUserPage) { if (isSearchPage) { const taglist = document.getElementById('taglist') taglist.style.display = 'none' taglist.nextElementSibling.style.display = 'none' } if (isGroupPage) { document.getElementById('tags_add_note').remove() // "To add multiple tags separate by comma" text } // load settings let currentFavoritesDict = (GM_getValue('gts_favorites')) || {} let currentPresetsDict = (GM_getValue('gts_presets')) || {} let hotkeys = (GM_getValue('gts_hotkeys')) || defaultHotkeys let hotkeyPrefixes = (GM_getValue('gts_hotkey_prefixes')) || defaulthotkeyPrefixes let searchStringDict = {} for (const tags of Object.values(categoryDict)) { // map from tag => search title, string for (const tag of tags) { const title = titlecase(tag) searchStringDict[tag] = `${title.toLowerCase()}${SEPERATOR}${tag}` } } let foundTags = -1 let windowEvents = [] // language=CSS GM_addStyle(` .gts-selector *::-webkit-scrollbar { width: 3px; } .gts-selector *::-webkit-scrollbar-track { background: transparent; } .gts-selector *::-webkit-scrollbar-thumb { background-color: rgba(155, 155, 155, 0.5); border-radius: 20px; border: transparent; } .gts-unlisted-tag { color: coral !important; } .gts-remove-unlisted { margin-top: 15px; } #genre_tags { display: none !important } .gts-add-preset { display: none; } /*#torrents .gts-add-preset {*/ /* float: right;*/ /*}*/ .gts-selector { display: none; position: absolute; background-color: rgb(27, 48, 63); box-sizing: border-box; padding: .5em 1em 1em 1em; border: 3px solid var(--rowb); box-shadow: -3px 3px 5px var(--black); z-index: 99999; grid-template-columns: auto fit-content(180px) fit-content(180px); column-gap: 1em; min-width: min-content !important; max-width: 1000px !important; font-size: 13px; } .gts-selector h1 { margin: 0; font-weight: normal; padding-bottom: 0; } .gts-tag { height: fit-content; font-family: inherit; font-size: inherit; opacity: 1 !important; background: none!important; border: none; padding: 0!important; color: var(--lightBlue); text-decoration: none; cursor: pointer; text-align: start; } .gts-sidearea { min-width: 150px; box-sizing: border-box; border-left: 2px solid var(--grey); padding-left: 1em; } .gts-selector .gts-sidearea h1 { font-size: 1.2em; margin-top: 1em; margin-bottom: 0.25em; } .gts-sidearea h1:nth-child(2) { margin-top: 0; } .gts-current-tags-inner { font-size: 0.9em; margin-top: 1em; overflow-y: auto; max-height: 320px; } .gts-searchbar { display: grid; align-items: center; grid-template-columns: 3fr auto 1fr; column-gap: 1em; margin-bottom: 1em; } .gts-categoryarea { display: grid; grid-template-columns: 1fr 1fr; column-gap: 10px; .gts-right { display: grid; grid-template-columns: 1fr 1fr; height: 100%; column-gap: 1em; width: max-content; } .gts-left { height: 100%; } .gts-category-inner { display: grid; grid-template-columns: 1fr; column-gap: 1em; overflow-y: auto; max-height: 145px; } } .gts-category .gts-category-inner, #gts-favoritearea, #gts-presetarea { font-size: .9em; margin-top: 0.5em; width: max-content !important; } #gts-presetarea { max-height: 140px; overflow-y: auto; width: unset !important; } #gts-favoritearea { max-height: 140px; overflow-y: auto; grid-template-columns: 1fr 1fr; display: grid; column-gap: .5em; } .gts-category h1 { font-size: 1.1em; } .gts-category-genre .gts-category-inner { grid-template-columns: auto auto; max-height: 320px; } .gts-tag-idx { color: yellow; font-weight: bold; margin-left: 0.25em; } .hide-idx .gts-tag-idx { display: none; } #gts-selector a { font-size: inherit !important; } .gts-tag-link-wrapper { width: fit-content !important; max-width: 100px; scroll-snap-align: start; } .gts-category .gts-tag-link-wrapper { width: fit-content(120px); } .gts-category-genre .gts-tag-link-wrapper { max-width: 120px; width: max-content !important; } .gts-category-simulation { .gts-category-inner { width: 100% !important; } } /*region non-Games*/ .gts-categoryarea-E-Books, .gts-categoryarea-E-Books .gts-right, .gts-categoryarea-Applications, .gts-categoryarea-Applications .gts-right, .gts-categoryarea-OST, .gts-categoryarea-OST .gts-right { grid-template-columns: 1fr; } .gts-categoryarea-E-Books .gts-category .gts-category-inner, .gts-categoryarea-OST .gts-category .gts-category-inner, .gts-categoryarea-Applications .gts-category .gts-category-inner { max-height: 300px; grid-template-columns: repeat(6, fit-content(180px)); row-gap: 0.3em; } /*endregion*/wrapper { width: fit-content !important; max-width: 100px; scroll-snap-align: start; } .gts-category .gts-tag-wrapper { width: fit-content(120px); } .gts-category-genre .gts-tag-wrapper { max-width: 120px; width: max-content !important; } /* simulation category */ .gts-category:nth-of-type(3) { grid-column: span 2; .gts-tag-wrapper { max-width: unset; } } /*region non-Games*/ .gts-categoryarea-E-Books, .gts-categoryarea-E-Books .gts-right, .gts-categoryarea-Applications, .gts-categoryarea-Applications .gts-right, .gts-categoryarea-OST, .gts-categoryarea-OST .gts-right { grid-template-columns: 1fr; } .gts-categoryarea-E-Books .gts-category .gts-category-inner, .gts-categoryarea-OST .gts-category .gts-category-inner, .gts-categoryarea-Applications .gts-category .gts-category-inner { max-height: 300px; grid-template-columns: repeat(6, fit-content(180px)); row-gap: 0.3em; } /*endregion*/`) // renderer functions let tagBox, searchBox, modal, presetButton, currentUploadCategory, showIndicess, removeUnlistedButton let allCurrentCategoryTags = [] function render_tag_links(tags, idx) { let html = '' for (const tag of tags) { html += `
` if (idx < 9) { html += `${idx + 1}` } html += `
` idx += 1 } return [html, idx] } function filter_category_dict(query, categoryDict, currentUploadCategory = 'Games') { let filteredDict = {} foundTags = [] for (const [category, tags] of Object.entries(categoryDict)) { if (!categoryKeys[currentUploadCategory].includes(category)) { continue } filteredDict[category] = [] for (const tag of tags) { if (searchStringDict[tag].includes(query)) { filteredDict[category].push(tag) foundTags.push(tag) } } } return filteredDict } function draw_currenttagsarea() { removeUnlistedButton.style.display = 'none' let html = `

Current Tags

(Click to remove)
` const tags = parse_text_to_tag_list(tagBox.value.trim()) const unlistedTags = tags.filter(tag => !allCurrentCategoryTags.includes(tag)) for (const [idx, tag] of tags.entries()) { html += `
${idx + 1}.
` } html += `
` const tagArea = document.querySelector('#gts-currenttagsarea') tagArea.innerHTML = html for (const tagLink of tagArea.querySelectorAll('.gts-tag')) { tagLink.onclick = event => { event.preventDefault() const currentTags = parse_text_to_tag_list(tagBox.value.trim()) const clickedTag = event.target.getAttribute('data-tag') tagBox.value = currentTags.filter(t => t !== clickedTag).join(TAGSEPERATOR) // draw_currenttagsarea() } } if (unlistedTags.length > 0) { removeUnlistedButton.style.display = 'block' removeUnlistedButton.onclick = () => { for (const unlistedTag of tagArea.querySelectorAll('.gts-unlisted-tag')) { unlistedTag.click() } } } } function draw_categoryarea(query = SEPERATOR) { let categoryAreaHTML = '' let idx = 0 let tagLinks const filteredDict = filter_category_dict(query, categoryDict, currentUploadCategory) if (currentUploadCategory === 'Games') { if (filteredDict['genre'].length > 0) { [tagLinks, idx] = render_tag_links(filteredDict['genre'], idx) categoryAreaHTML += `

Genre

${tagLinks}
` } } categoryAreaHTML += `
` for (const [category, tags] of Object.entries(filteredDict)) { if ((currentUploadCategory === 'Games' && category === 'genre') || tags.length === 0) { continue } [tagLinks, idx] = render_tag_links(tags, idx) categoryAreaHTML += `
` if (categoryKeys[currentUploadCategory].length > 1) { categoryAreaHTML += `

${titlecase(category)}

` } categoryAreaHTML += `
${tagLinks}
` } document.querySelector('#gts-categoryarea').innerHTML = categoryAreaHTML document.querySelectorAll('#gts-categoryarea .gts-tag').forEach((el) => { el.addEventListener('click', (event) => { event.preventDefault() const tag = event.target.getAttribute('data-tag').trim() const favoriteChecked = check_favorite() if (favoriteChecked) { add_favorite(tag).then(() => { draw_favoritearea() register_hotkeys('favorite') }) } else { add_tag(tag) } }) }) } function draw_presetarea() { let html = '' const currentPresets = currentPresetsDict[currentUploadCategory] || [] for (const [idx, preset] of currentPresets.entries()) { html += `
${idx + 1}.
` } document.querySelector('#gts-presetarea').innerHTML = html document.querySelectorAll('#gts-presetarea .gts-preset-link').forEach((el) => { el.addEventListener('click', (event) => { event.preventDefault() const preset = event.target.getAttribute('data-preset').trim() if (check_remove()) { remove_preset(preset).then(() => { draw_presetarea() }) } else { tagBox.value = preset tagBox.focus() searchBox.value = '' searchBox.focus() } }) }) } function draw_favoritearea() { let html = '' const currentFavorites = currentFavoritesDict[currentUploadCategory] || [] for (const [idx, tag] of currentFavorites.entries()) { html += `
${idx + 1}.
` } document.querySelector('#gts-favoritearea').innerHTML = html document.querySelectorAll('#gts-favoritearea .gts-tag').forEach((el) => { el.addEventListener('click', (event) => { event.preventDefault() const tag = event.target.getAttribute('data-tag').trim() if (check_remove()) { remove_favorite(tag).then(() => { draw_favoritearea() register_hotkeys('favorite') }) } else { add_tag(tag) } }) }) } function insert_modal() { modal = document.createElement('div') const tagBoxStyle = tagBox.currentStyle || window.getComputedStyle(tagBox) const tdStyle = tagBox.parentElement.currentStyle || window.getComputedStyle(tagBox.parentElement) modal.style.top = (parseInt(tagBoxStyle.marginTop.replace('px', ''), 10) + parseInt(tagBoxStyle.marginBottom.replace('px', ''), 10) + tagBoxStyle.offsetHeight) + 'px' modal.style.left = (parseInt(tagBoxStyle.marginLeft.replace('px', ''), 10) + parseInt(tdStyle.paddingLeft.replace('px', ''), 10)) + 'px' modal.id = 'gts-selector' modal.classList.add('gts-selector') modal.setAttribute('tabindex', '-1') modal.innerHTML = `

Presets

Favorites

` tagBox.parentElement.style.position = 'relative' tagBox.parentElement.appendChild(modal) draw_categoryarea() removeUnlistedButton = modal.querySelector('.gts-remove-unlisted') searchBox = document.querySelector('#gts-search') searchBox.addEventListener('keydown', (event) => { if (event.key === 'Enter' || (event.key === 'Tab' && foundTags.length === 1)) { event.preventDefault() event.stopPropagation() } }) searchBox.addEventListener('keyup', (event) => { if (event.key === 'Tab' && foundTags.length === 1) { add_tag(foundTags[0]) } else if (event.key === 'Enter') { let tag = event.target.value.trim() tag = tag.replaceAll(' ', '.') if (tag.length > 0) { add_tag(tag) } } let query = event.target.value.trim() if (query === '') { query = SEPERATOR } query = query.toLowerCase() draw_categoryarea(query) if (event.code === 'Escape') { hide_gts() } }) draw_presetarea() draw_favoritearea() draw_currenttagsarea() } function insert_preset_button() { presetButton = document.createElement('button') presetButton.id = 'gts-add-preset' presetButton.classList.add('gts-add-preset') presetButton.type = 'button' presetButton.setAttribute('tabindex', '-1') presetButton.textContent = 'Add Preset' if (!isGroupPage) { tagBox.after(presetButton) if (!isUploadPage) presetButton.style.marginLeft = '5px' } else { const div = document.createElement('div') const submitButton = tagBox.nextElementSibling div.style.cssText = ` display: flex; justify-content: end; align-items: center; ` tagBox.after(div) div.append(presetButton, submitButton) } presetButton.addEventListener('click', () => { const preset = tagBox.value.trim() add_preset(preset).then(() => { draw_presetarea() }) }) } // actions function add_tag(tag) { const currentValue = tagBox.value.trim() tag = tag.trim().toLowerCase() if (currentValue === "") { tagBox.value = tag } else { let tags = currentValue.split(TAGSEPERATOR) if (!tags.includes(tag)) { tags.push(tag) } tagBox.value = tags.join(TAGSEPERATOR) } tagBox.focus() tagBox.setSelectionRange(-1, -1) searchBox.focus() searchBox.value = '' draw_categoryarea() } async function add_favorite(tag) { const currentFavorites = currentFavoritesDict[currentUploadCategory] || [] if (currentFavorites.length < 9 && !currentFavorites.includes(tag)) { currentFavoritesDict[currentUploadCategory] = currentFavorites.concat(tag) return GM.setValue('gts_favorites', currentFavoritesDict) } } async function remove_favorite(tag) { const currentFavorites = currentFavoritesDict[currentUploadCategory] || [] let _temp = [] for (const fav of currentFavorites) { if (fav !== tag) { _temp.push(fav) } } currentFavoritesDict[currentUploadCategory] = _temp return GM.setValue('gts_favorites', currentFavoritesDict) } function parse_text_to_tag_list(text) { let tagList = [] for (let tag of text.split(TAGSEPERATOR.trim())) { tag = tag.trim() if (tag !== '') { tagList.push(tag) } } return tagList } async function add_preset(rawPreset) { let preset = parse_text_to_tag_list(rawPreset) const currentPresets = currentPresetsDict[currentUploadCategory] || [] preset = preset.join(TAGSEPERATOR) if (!currentPresets.includes(preset)) { currentPresetsDict[currentUploadCategory] = currentPresets.concat(preset) return GM.setValue('gts_presets', currentPresetsDict) } } async function remove_preset(preset) { let _temp = [] const currentPresets = currentPresetsDict[currentUploadCategory] || [] for (const pres of currentPresets) { if (pres !== preset) { _temp.push(pres) } } currentPresetsDict[currentUploadCategory] = _temp return GM.setValue('gts_presets', currentPresetsDict) } function check_favorite() { return document.querySelector('#gts-favorite-checkbox').checked } function check_remove() { return document.querySelector('#gts-remove-checkbox').checked } function check_gts_element(element) { if (typeof element === 'undefined' || !(element instanceof HTMLElement)) { return false } const _id = element.id || '' const _class = element.getAttribute('class') || '' return (_id === 'tags' || _id.includes('gts-') || _class.includes('gts-')) } function hide_gts() { modal.style.display = 'none' presetButton.style.display = 'none' } function show_gts() { if (!check_gts_active()) { modal.style.display = 'grid' presetButton.style.display = 'inline' searchBox.focus() draw_currenttagsarea() } } function hide_indices() { document.querySelector('#gts-categoryarea').classList.add('hide-idx') showIndicess = false } function show_indices() { document.querySelector('#gts-categoryarea').classList.remove('hide-idx') showIndicess = true } function check_gts_active() { return (modal.style.display === 'grid') && (presetButton.style.display === 'block') } function check_query_exists() { // returns true if there is query return searchBox.value.trim() !== '' } function get_index_from_code(code) { if (code.indexOf('Digit') === 0) { return parseInt(code.replaceAll('Digit', ''), 10) - 1 } return null } function get_current_upload_category(defaultCategory = 'Games') { if (isSearchPage || isRequestPage) { const list = document.querySelectorAll('input[type=checkbox][name^=filter_cat]:checked') if (list.length < 1) return defaultCategory const lastChecked = list[list.length - 1] return { 1: "Games", 2: "Applications", 3: "E-Books", 4: "OST", }[/\d/.exec(lastChecked.id)[0]] } let categoryElement = document.querySelector('#categories') if (categoryElement) { return categoryElement.value } categoryElement = document.querySelector('#group_nofo_bigdiv .head:first-child') const s = categoryElement.innerText.trim() if (s.indexOf('Application') !== -1) { return 'Applications' } else if (s.indexOf('OST') !== -1) { return 'OST' } else if (s.indexOf('Book') !== -1) { return 'E-Books' } else if (s.indexOf('Game') !== -1) { return 'Games' } return defaultCategory } function check_hotkey_prefix(event, type) { let eventModifiers = [event.shiftKey, event.altKey, event.ctrlKey, event.metaKey] const targetKeys = hotkeyPrefixes[type].split(' + ').map((key) => key.trim().toLowerCase()) for (let i = 0; i < modifiers.length; i++) { if (targetKeys.includes(modifiers[i]) !== eventModifiers[i]) { return false } } return true } function get_hotkey_target(event, type) { for (const [idx, hotkey] of Object.entries(hotkeys[type])) { let normalKeys = [] const targetKeys = hotkey.split('+').map((s) => { key = s.toLowerCase().trim() if (!modifiers.includes(key)) { normalKeys.push(key) } return key }) let modifierMismatch = false let eventModifiers = [event.shiftKey, event.altKey, event.ctrlKey, event.metaKey] for (let i = 0; i < modifiers.length; i++) { if (targetKeys.includes(modifiers[i]) !== eventModifiers[i]) { modifierMismatch = true break } } if (modifierMismatch) { continue } if (normalKeys.length > 0 && ( !(normalKeys.includes(event.key.toLowerCase()) || normalKeys.includes(event.code.toLowerCase())) )) { continue } return idx } return null } function register_hotkeys(type) { if (['favorite', 'preset'].includes(type) && !windowEvents.includes(`hotkey-${type}`)) { window.addEventListener('keydown', (event) => { if (!check_gts_active()) { return } const target = get_hotkey_target(event, type) let currentList if (type === 'favorite') { if (check_query_exists()) { return // return early if query is active } currentList = currentFavoritesDict[currentUploadCategory] || [] } else if (type === 'preset') { // if we're working with presets, // we proceed anyway currentList = currentPresetsDict[currentUploadCategory] || [] } if (target !== null) { if (target < currentList.length) { event.preventDefault() if (type === 'favorite') { add_tag(currentList[target]) } else if (type === 'preset') { tagBox.value = currentList[target] tagBox.focus() } searchBox.focus() } } }, true) } else if (type === 'show_indices' && !windowEvents.includes(!`hotkey-${type}`)) { window.addEventListener('keydown', (event) => { if (!check_gts_active() || !check_query_exists()) { return } if (check_hotkey_prefix(event, type)) { show_indices() const idx = get_index_from_code(event.code) if (idx !== null) { document.querySelector(`a.gts-tag[data-tag-idx="${idx}"]`).click() event.preventDefault() } } }, true) window.addEventListener('keyup', () => { if (showIndicess) { hide_indices() } }, true) } windowEvents.push(`hotkey-${type}`) } // initialiser function init() { const modal = document.querySelector('#gts-selector') if (modal) { modal.remove() } currentUploadCategory = get_current_upload_category() allCurrentCategoryTags = categoryKeys[currentUploadCategory].flatMap(c => categoryDict[c]) if (isGroupPage) { const groupTagEls = Array.from(document.querySelectorAll("a[href^='torrents.php?taglist=']")) const unlistedTagEls = groupTagEls .filter(tag => !allCurrentCategoryTags.includes(tag.textContent) && !specialTags.some(t => t === tag.textContent)) for (const groupTagEl of groupTagEls) { if (unlistedTagEls.includes(groupTagEl)) { groupTagEl.classList.add('gts-unlisted-tag') } } } tagBox = document.getElementById('tags') || document.querySelector('input[name=tags]') tagBox.setAttribute('onfocus', 'this.value = this.value') insert_modal() insert_preset_button() if (!windowEvents.includes('click')) { window.addEventListener('click', (event) => { if (!check_gts_element(event.target)) { setTimeout(() => { if (!check_gts_element(document.activeElement)) { hide_gts() } }, 50) } }, true) windowEvents.push('click') } if (!windowEvents.includes('esc')) { window.addEventListener('keyup', (event) => { if (event.code === 'Escape') { if (check_gts_active()) { hide_gts() } } }, true) windowEvents.push('esc') } tagBox.addEventListener('focus', show_gts) tagBox.addEventListener('click', show_gts) tagBox.addEventListener('keyup', (event) => { if (event.code !== 'Escape') { draw_currenttagsarea() } }) register_hotkeys('favorite') register_hotkeys('preset') register_hotkeys('show_indices') draw_currenttagsarea() // watch for value change in the tagBox observe_element(tagBox, 'value', (_) => { draw_currenttagsarea() }) } if (isUploadPage) { const observerTarget = document.querySelector('#dynamic_form') let observer = new MutationObserver(init) const observerConfig = {childList: true, attributes: false, subtree: false} if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init) observer.observe(observerTarget, observerConfig) } else { init() observer.observe(observerTarget, observerConfig) } } else { init() if (isSearchPage || isRequestPage) { document.querySelector('.cat_list').addEventListener('change', e => { if (e.target.checked) { init() } }) } else if (isCreateRequestPage) { // it doesn't use dynamic form init() document.getElementById('categories').addEventListener('change', () => { init() }) } } } else { let hotkeys = (GM_getValue('gts_hotkeys')) || defaultHotkeys let hotkeyPrefixes = (GM_getValue('gts_hotkey_prefixes')) || defaulthotkeyPrefixes GM_addStyle(` #gts-save-settings { min-width: 200px; } .gts-hotkey-grid { display: grid; column-gap: 1em; grid-template-columns: repeat(2, fit-content(400px)) 1fr; } .gts-hotkey-grid h1 { font-size: 1.1em; } .gts-hotkey-col div { margin-bottom: 0.25em; } `) async function init() { let colhead = document.createElement('tr') colhead.classList.add('colhead_dark') colhead.innerHTML = 'GGn Tag Selector' const lastTr = document.querySelector('#userform > table > tbody > tr:last-child') lastTr.before(colhead) let hotkeyTr = document.createElement('tr') let html = ` Hotkeys ` for (const [type, cHotkeys] of Object.entries(hotkeys)) { html += `

${titlecase(type)}

` for (const [idx, hotkey] of cHotkeys.entries()) { html += `
${idx + 1}.
` } html += `
` } html += `

Index peeker

Hold to display indices of the filtered results (modifier keys/their combinations only). Use the key along with a digit (1-9) to add the tag according to the index. Note that peeking/adding by index will not work if the filter query is empty.

How to set combos/keys

To set a combo, use the keys joined by the plus sign. For example, Ctrl + Shift + 1 is ctrl + shift + digit1
Other keys should also work. If not, use the event.code value from the keycode tool.
` html += `` html += `
` hotkeyTr.innerHTML = html colhead.after(hotkeyTr) document.querySelector('#gts-save-settings').addEventListener('click', (event) => { const originalText = event.target.value let newData = { 'gts_hotkeys': hotkeys, 'gts_hotkey_prefixes': hotkeyPrefixes } event.target.value = 'Saving ...' document.querySelectorAll('.gts-settings').forEach((el) => { const meta = el.getAttribute('data-gts-settings') const rawValue = el.value const [settingKey, settingSubKey] = meta.split(':') if (settingKey === 'gts_hotkey_prefixes') { newData[settingKey][settingSubKey] = normalise_combo_string(rawValue) } else if (settingKey === 'gts_hotkeys') { const [type, idx] = settingSubKey.split('-') // normalise the value newData[settingKey][type][idx] = normalise_combo_string(rawValue) } }) let promises = [] for (const [key, value] of Object.entries(newData)) { promises.push(GM.setValue(key, value)) } Promise.all(promises).then(() => { event.target.value = 'Saved!' setTimeout(() => { event.target.value = originalText }, 500) }) }) document.querySelector('#gts-restore-settings').addEventListener('click', () => { let defaults = { 'gts_hotkeys': defaultHotkeys, 'gts_hotkey_prefixes': hotkeyPrefixes } document.querySelectorAll('.gts-settings').forEach((el) => { const meta = el.getAttribute('data-gts-settings') const [settingKey, settingSubKey] = meta.split(':') if (settingKey === 'gts_hotkey_prefixes') { el.value = defaults[settingKey][settingSubKey] } else if (settingKey === 'gts_hotkeys') { const [type, idx] = settingSubKey.split('-') el.value = defaults[settingKey][type][idx] } }) }) if (window.location.hash.substring(1) === 'ggn-tag-selector') { document.querySelector('#ggn-tag-selector').scrollIntoView() } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init) } else { init() } }