- // ==UserScript==
- // @name GGn Tag selector
- // @namespace ggntagselector
- // @version 1.2.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
- // ==/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",
- "mini.game",
- "music",
- "open.world",
- "parody",
- "party",
- "pinball",
- "platform",
- "point.and.click",
- "puzzle",
- "quiz",
- "rhythm",
- "roguelike",
- "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",
- "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
- })
- }
- }
-
-
- function addTagFromSearch(event) {
- let tag = event.target.value.trim()
- tag = tag.replaceAll(' ', '.')
- if (tag.length > 0) {
- add_tag(tag)
- }
- }
-
- 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;
- }
-
- .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 {
- visibility: hidden;;
- }
-
- #gts-selector a {
- font-size: inherit !important;
- }
-
- .gts-tag-link-wrapper {
- width: fit-content !important;
- max-width: 100px;
- scroll-snap-align: start;
- }
-
- .gts-tag-wrapper {
- width: fit-content !important;
- max-width: 100px;
- }
-
- .gts-category .gts-tag-wrapper {
- width: fit-content(120px);
- }
-
- .gts-category-genre .gts-tag-wrapper {
- max-width: 120px;
- width: max-content !important;
- }
-
- .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 {
- grid-column: span 2;
-
- .gts-tag-wrapper {
- max-width: unset;
- }
-
- .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*/
- `)
- // 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 += `<div class="gts-tag-wrapper"><button type="button" class="gts-tag" data-tag-idx="${idx}" data-tag="${tag}">${titlecase(tag)}</button>`
- if (idx < 9) {
- html += `<span data-tag-idx="${idx}" class="gts-tag-idx">${idx + 1}</span>`
- }
- html += `</div>`
- 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 = `<h1>Current Tags</h1> (<small>Click to remove</small>)
- <div class="gts-current-tags-inner">`
- 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 += `<div>${idx + 1}. <button type="button" class="gts-tag ${unlistedTags.includes(tag) ? 'gts-unlisted-tag' : ''}" data-tag="${tag}">${titlecase(tag)}</button></div>`
- }
- html += `</div>`
- 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 += `
- <div class="gts-left">
- <div class="gts-category gts-category-genre" tabindex="-1">
- <h1 class="gts-h1">Genre</h1>
- <div class="gts-category-inner" tabindex="-1">
- ${tagLinks}
- </div>
- </div>
- </div>`
- }
-
- }
- categoryAreaHTML += `<div class="gts-right" tabindex="-1">`
- 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 += `<div class="gts-category gts-category-${category}" tabindex="-1">`
- if (categoryKeys[currentUploadCategory].length > 1) {
- categoryAreaHTML += `<h1>${titlecase(category)}</h1>`
- }
- categoryAreaHTML += `
- <div class="gts-category-inner" tabindex="-1">
- ${tagLinks}
- </div>
- </div>`
- }
- 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 += `<div class="gts-preset">${idx + 1}.
- <button type="button" class="gts-preset-link gts-tag" data-preset="${preset}">
- ${preset.split(TAGSEPERATOR).map((tag) => titlecase(tag)).join(TAGSEPERATOR)}</button>
- </div>
- </div>`
- }
- 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 += `<div class="gts-favorite">${idx + 1}. <button type="button" class="gts-tag" data-tag="${tag}">${titlecase(tag)}</button></div></div>`
- }
- 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(addTagsToggle) {
- 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 = `
- <div class="gts-selectarea">
- <div class="gts-searchbar">
- <input id="gts-search" type="text" placeholder="Search (Enter to add as-is${addTagsToggle ? ', ctrl+enter to submit' : ''})">
- <div class="gts-settings-wrapper" tabindex="-1">
- <a href="/user.php?action=edit#ggn-tag-selector" target="_blank" tabindex="-1">[Settings]</a>
- </div>
- <div class="gts-checkbox-wrapper" style="text-align: right; min-width: 80px;">
- <input id="gts-favorite-checkbox" type="checkbox" tabindex="-1"><label class="gts-label" for="gts-favorite-checkbox">Favorite</label>
- </div>
- </div>
- <div id="gts-categoryarea" class="hide-idx gts-categoryarea gts-categoryarea-${currentUploadCategory}">
- </div>
- </div>
- <div class="gts-sidearea">
- <div class="gts-sidetopbar" tabindex="-1" style="text-align: right !important;">
- <div class="gts-checkbox-wrapper" style="text-align: right !important;">
- <input id="gts-remove-checkbox" type="checkbox" tabindex="-1"><label class="gts-label" for="gts-remove-checkbox">Remove</label>
- </div>
- </div>
- <h1>Presets</h1>
- <div id="gts-presetarea" tabindex="-1">
- </div>
- <h1>Favorites</h1>
- <div id="gts-favoritearea" tabindex="-1">
- </div>
- </div>
- <div class="gts-sidearea" style="display:flex;flex-direction:column;justify-content: start">
- <div id="gts-currenttagsarea"></div>
- <button type="button" style="display:none;font-size: smaller;" class="gts-remove-unlisted">Remove Unlisted Tags</button>
- </div>
- `
-
- 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') {
- addTagFromSearch(event)
- }
- 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'
- }
-
- 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) => {
- const 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(`button.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])
-
- tagBox = document.getElementById('tags') || document.querySelector('input[name=tags]')
- tagBox.setAttribute('onfocus', 'this.value = this.value')
- const addTagsToggle = document.getElementById('tags_add_toggle')
- insert_modal(addTagsToggle)
- insert_preset_button()
-
- if (addTagsToggle) { // on group page and has tagging priviledge (maybe)
- const groupTagEls = Array.from(document.querySelectorAll("a[href^='torrents.php?taglist=']"))
- const unlistedTagEls = groupTagEls.filter(tag => !allCurrentCategoryTags.includes(tag.textContent) && !specialTags.includes(tag.textContent))
-
- for (const groupTagEl of groupTagEls) {
- if (unlistedTagEls.includes(groupTagEl)) {
- groupTagEl.classList.add('gts-unlisted-tag')
- }
- }
- addTagsToggle.style.display = 'none'
- const addTagForm = document.getElementById('tag_add_form')
- addTagForm.style.display = 'block'
- // add/remove tags replaces the whole list
- new MutationObserver(() => {
- for (const el of document.querySelectorAll("a[href^='torrents.php?taglist=']")) {
- if (unlistedTagEls.some(t => t.href === el.href)) {
- el.classList.add('gts-unlisted-tag')
- }
- }
- }).observe(document.getElementById('tagslist'), {childList: true})
-
- tagBox.placeholder = 'Add tags'
- GM_addStyle(`#gts-search::placeholder {font-size: 0.9em;}`)
-
- searchBox.addEventListener('keydown', e => {
- if (e.ctrlKey && e.key === 'Enter') {
- addTagFromSearch(e)
- addTagForm.querySelector('input[type=submit]').click()
- hide_gts()
- }
- })
- }
-
- 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 = '<td colspan="2" id="ggn-tag-selector"><strong>GGn Tag Selector</strong></span>'
- const lastTr = document.querySelector('#userform > table > tbody > tr:last-child')
- lastTr.before(colhead)
- let hotkeyTr = document.createElement('tr')
- let html = `
- <td class="label"><strong>Hotkeys</strong></td>
- <td class="gts-hotkey-grid">
- `
- for (const [type, cHotkeys] of Object.entries(hotkeys)) {
- html += `<div class="gts-hotkey-col"><h1>${titlecase(type)}</h1>`
- for (const [idx, hotkey] of cHotkeys.entries()) {
- html += `<div>${idx + 1}. <input class="gts-settings" data-gts-settings="gts_hotkeys:${type}-${idx}" value="${hotkey}"></div>`
- }
- html += `</div>`
- }
- html += `<div class="gts-hotkey-col">
- <h1>Index peeker</h1>
- Hold <input type="text" style="width: 5em" class="gts-settings" data-gts-settings="gts_hotkey_prefixes:show_indices" value="${hotkeyPrefixes['show_indices']}"> 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.
- <h1>How to set combos/keys</h1>
- To set a combo, use the keys joined by the plus sign. For example, Ctrl + Shift + 1 is <span style="font-family: monospace">ctrl + shift + digit1</span>
- <ul>
- <li>Modifier keys: shift, alt, ctrl, cmd</li>
- <li>Numbers: digit1, digit2, digit3, digit4, digit5, digit6, digit7, digit8, digit9</li>
- <li>Alphabet: a, b, c, d, (etc.)</li>
- </ul>
- <div style="margin-top: 1em;">
- Other keys should also work. If not, use the <span style="font-family: monospace">event.code</span> value from <a target="_blank" href="https://www.toptal.com/developers/keycode">the keycode tool</a>.
- </div>
- </div>`
- html += `</td>`
- html += `<div style="margin-left: 1em;"><input type="button" id="gts-save-settings" value="Save GGn Tag Selector settings">
- <input type="button" id="gts-restore-settings" value="Restore Defaults"></div>`
- 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()
- }
- }