您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add tags more easily
// ==UserScript== // @name GGn Tag Helper // @description Add tags more easily // @version 2.2.1 // @match *://gazellegames.net/upload.php* // @match *://gazellegames.net/torrents.php?*action=advanced* // @match *://gazellegames.net/torrents.php*id=* // @exclude *://gazellegames.net/torrents.php*action=editgroup* // @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 // @namespace ggntagselector // ==/UserScript== // noinspection CssUnresolvedCustomProperty,CssUnusedSymbol,DuplicatedCode 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.php') const TAGSEPERATOR = ', ' let hotkeys = GM_getValue('hotkeys') if (!hotkeys) { hotkeys = { "index": { "modifier": "Shift", "keys": ["1", "2", "3", "4", "5", "Q", "W", "E", "R", "T"] }, "favorites": { "modifier": "Shift", "keys": ["1", "2", "3", "4", "5", "Q", "W", "E", "R", "T"] }, "presets": { "modifier": "Alt", "keys": ["1", "2", "3", "4", "5", "Q", "W", "E", "R", "T"] } } GM_setValue('hotkeys', hotkeys) } const trailingCommaRegex = /(?:, *)+$/ 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) } if (!isUserPage) { let modal, tagInput, currentUploadCategory = 'Games', favsList, presetsList, currentTagsList, removalCheckbox // language=CSS GM_addStyle(` #tag-helper { display: none; grid-template-columns: 200px 300px 200px; grid-template-rows: repeat(2, auto); gap: 15px; 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; min-width: min-content; max-width: 800px; font-size: 13px; label input[type=checkbox] { margin: 0 5px 0 0; } section { div.spaced { display: flex; justify-content: space-between; align-items: center; } div.list { display: flex; flex-direction: column; max-height: 450px; overflow: auto; } button { margin-right: 10px; word-break: break-word; } h1 { font-weight: normal; padding-bottom: 0; font-size: 1.2em; margin: 0.5em 0 0.5em 0; } div.tag-wrapper { display: flex; align-items: center; gap: 0.25em; } } .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; } } .th-tag-idx { color: yellow; float: right; display: none; font-family: monospace; } `) if (isGroupPage) document.getElementById('add_tags_link').click() modal = document.createElement('div') document.body.appendChild(modal) modal.id = 'tag-helper' modal.innerHTML = //language=HTML ` <section style="grid-column: 1"> <div class="spaced"> <h1>Favorites</h1> <button type="button" id="th-add-fav">Add</button> </div> <div class="list" id="th-favs"></div> </section> <section style="grid-column: 2;"> <div class="spaced"> <h1>Presets</h1> <button type="button" id="th-add-preset">Add</button> </div> <div class="list" id="th-presets"></div> </section> <section style="grid-column: 3;"> <h1>Current Tags</h1> <div class="list" id="th-currenttags"></div> </section> <label style="display:flex;align-items:center;font-size: 0.9em;grid-row: 2;grid-column: 1/3"> <input type="checkbox" id="th-remove"> Remove (click favorite or preset when checked) </label> ` favsList = document.getElementById('th-favs') presetsList = document.getElementById('th-presets') currentTagsList = document.getElementById('th-currenttags') removalCheckbox = document.getElementById('th-remove') let currentFavoritesDict = (GM_getValue('favorites')) || {} let currentPresetsDict = (GM_getValue('presets')) || {} function init() { if (isUploadPage || isCreateRequestPage) { currentUploadCategory = document.querySelector('#categories').value } else if (isGroupPage) { const categoryHeaderText = document.querySelector('#group_nofo_bigdiv > div.head > strong').textContent if (categoryHeaderText.includes('Application')) { currentUploadCategory = 'Applications' } else if (categoryHeaderText.includes('OST')) { currentUploadCategory = 'OST' } else if (categoryHeaderText.includes('Book')) { currentUploadCategory = 'E-Books' } else if (categoryHeaderText.includes('Game')) { currentUploadCategory = 'Games' } } else if (isSearchPage || isRequestPage) { const checkedBoxes = document.querySelectorAll('input[type=checkbox][name^=filter_cat]:checked') if (checkedBoxes.length > 0) { const lastChecked = checkedBoxes[checkedBoxes.length - 1] currentUploadCategory = { 1: "Games", 2: "Applications", 3: "E-Books", 4: "OST", }[/\d/.exec(lastChecked.id)[0]] } } tagInput = isCreateRequestPage ? document.getElementById(`tags_${currentUploadCategory}`).firstElementChild : (document.getElementById('tags') || document.querySelector('[name=add_tags_input]') || document.querySelector('[name=tags]')) const tagInputRect = tagInput.getBoundingClientRect() modal.style.top = `${tagInputRect.top + window.scrollY + tagInputRect.height + 5}px` modal.style.left = `${tagInputRect.left + window.scrollX + 250}px` drawFavorites() drawPresets() drawCurrentTags() tagInput.addEventListener('keyup', () => { for (const span of suggestionIdxSpans) { span.style.display = 'none' } for (const span of favIdxSpans) { span.style.display = 'none' } for (const span of presetIdxSpans) { span.style.display = 'none' } }) tagInput.addEventListener('blur', () => { tagInput.value = tagInput.value.replace(trailingCommaRegex, '') }) tagInput.addEventListener('focus', () => { modal.style.display = 'grid' addCommaToEndAndDrawCurrent() }) tagInput.addEventListener('change', () => { drawCurrentTags() }) window.addEventListener('click', e => { if (!(e.target === tagInput || modal.contains(e.target) || e.target.className === 'tag')) modal.style.display = 'none' }) window.addEventListener('keydown', ev => { if (ev.code === 'Escape') { modal.style.display = 'none' } }) } init() if (isUploadPage) { tagInput.style.width = '100%' tagInput.size = 80 document.getElementById('categories').addEventListener('change', () => { new MutationObserver(() => init()) .observe(document.getElementById('dynamic_form'), {childList: true, subtree: true}) }) } else if (isSearchPage || isRequestPage) { document.querySelector('.cat_list').addEventListener('change', e => { if (!e.target.checked) return init() }) } else if (isCreateRequestPage) { // it doesn't use dynamic form document.getElementById('categories').addEventListener('change', () => { init() }) } /** @type {HTMLInputElement} */ const autocompleteDiv = tagInput.nextElementSibling const addTagsButton = document.getElementById('add_tags_button') const groupTagDivs = document.getElementsByClassName('group_tag') const suggestionIdxSpans = autocompleteDiv.getElementsByClassName('th-tag-idx') const favIdxSpans = favsList.getElementsByClassName('th-tag-idx') const presetIdxSpans = presetsList.getElementsByClassName('th-tag-idx') const autocompleteItems = autocompleteDiv.getElementsByClassName('tag_autocomplete_items') // this is to prevent submission when accepting a suggestion using tab let acceptedSuggestion = false new MutationObserver(() => { // not using addedNodes because it's empty if a suggestion remains at the same position for (let idx = 0; idx < autocompleteItems.length; idx++) { const autocompleteItem = autocompleteItems[idx] autocompleteItem.addEventListener('click', () => { acceptedSuggestion = true addCommaToEndAndDrawCurrent() }) if (idx > 0) { const span = document.createElement('span') autocompleteItem.append(span) span.textContent = hotkeys.index.keys?.[idx - 1] ?? '' span.className = 'th-tag-idx' } } }).observe(autocompleteDiv, {childList: true}) /** * @param {KeyboardEvent} event * @param {string} hotkeyType */ function isCorrectKeyModifier(event, hotkeyType) { const modifier = hotkeys[hotkeyType].modifier return (event.shiftKey && !isSearchPage && modifier === 'Shift') || (event.altKey && modifier === 'Alt') || (event.ctrlKey && modifier === 'Control') || (event.metaKey && modifier === 'Meta') } /** * @param {KeyboardEvent} ev * @param {string} hotkeyType * @param {HTMLCollectionOf<HTMLSpanElement>} spanList * @param {string} code * @param {HTMLElement} tagList * @param {boolean} [skip1] */ function handleListIndexPress(ev, hotkeyType, spanList, code, tagList, skip1) { if (isCorrectKeyModifier(ev, hotkeyType)) { ev.preventDefault() for (const span of spanList) { span.style.display = 'inline' } const keyIndex = hotkeys.index.keys.indexOf(code) if (keyIndex !== -1) { const child = tagList.children[keyIndex + (skip1 ? 1 : 0)]; (child.querySelector('button') || child).click() } } } tagInput.addEventListener('keydown', /** @param {KeyboardEvent} ev */ev => { const code = ev.code.replace('Digit', '').replace('Key', '') if (code === 'Space') { addCommaToEndAndDrawCurrent() tagInput.value = tagInput.value.replace(/ $/, '') return } const hasSuggestions = autocompleteDiv.children.length > 0 if (hasSuggestions) { // add suggestion by index handleListIndexPress(ev, 'index', suggestionIdxSpans, code, autocompleteDiv, true) } else { handleListIndexPress(ev, 'favorites', favIdxSpans, code, favsList) } handleListIndexPress(ev, 'presets', presetIdxSpans, code, presetsList) // tab submit shortcut if (isGroupPage && tagInput.value && !hasSuggestions && !acceptedSuggestion && code === 'Tab') { ev.preventDefault() addTagsButton.click() } acceptedSuggestion = false }) let originalTagColor function addTag(tag) { const groupTags = [...groupTagDivs].map(div => div.children[0]) const existingTag = groupTags.find(t => t.textContent === tag) if (existingTag) { originalTagColor ??= window.getComputedStyle(existingTag).getPropertyValue('color') existingTag.style.color = '#69c364' setTimeout(() => existingTag.style.color = originalTagColor, 1000) return } if (!tagInput.value) { tagInput.value = tag } else { const tags = tagInput.value.replace(trailingCommaRegex, '').split(TAGSEPERATOR) if (!tags.includes(tag)) { tags.push(tag) } tagInput.value = tags.join(TAGSEPERATOR) } tagInput.focus() tagInput.setSelectionRange(-1, -1) addCommaToEndAndDrawCurrent() drawCurrentTags() } // region Favorites // // // // function drawFavorites() { let html = '' for (const [idx, tag] of getFavorites().entries()) { html += `<div class="spaced"> <div class="tag-wrapper">${idx + 1}. <button type="button" class="tag" data-tag="${tag}">${titlecase(tag)}</button></div> <span class="th-tag-idx">${hotkeys.favorites.keys?.[idx] ?? ''}</span> </div>` } favsList.innerHTML = html favsList.querySelectorAll('.tag').forEach(el => { el.addEventListener('click', event => { event.preventDefault() const tag = event.target.dataset.tag if (removalCheckbox.checked) { removeFavorite(tag).then(() => { drawFavorites() }) } else { addTag(tag) } }) }) } async function removeFavorite(tag) { let _temp = [] for (const fav of getFavorites()) { if (fav !== tag) { _temp.push(fav) } } currentFavoritesDict[currentUploadCategory] = _temp return GM.setValue('favorites', currentFavoritesDict) } document.getElementById('th-add-fav').onclick = async () => { const currentFavorites = getFavorites() const tags = parse_text_to_tag_list() .filter((value, index, array) => !array.some(value => currentFavorites.includes(value))) currentFavoritesDict[currentUploadCategory] = currentFavorites.concat(...tags) await GM.setValue('favorites', currentFavoritesDict) drawFavorites() } function getFavorites() { return currentFavoritesDict[currentUploadCategory] || [] } // // // // // endregion //region Presets // // // // function drawPresets() { let html = '' for (const [idx, preset] of getPresets().entries()) { html += `<div class="spaced"> <div class="tag-wrapper">${idx + 1}. <button type="button" class="tag" data-preset="${preset}"> ${preset.split(TAGSEPERATOR).map((tag) => titlecase(tag)).join(TAGSEPERATOR)}</button></div> <span class="th-tag-idx">${hotkeys.presets.keys?.[idx] ?? ''}</span> </div>` } presetsList.innerHTML = html presetsList.querySelectorAll('.tag').forEach((el) => { el.addEventListener('click', event => { event.preventDefault() const preset = event.target.dataset.preset if (removalCheckbox.checked) { removePreset(preset).then(() => { drawPresets() }) } else { for (const tag of parse_text_to_tag_list(preset)) { addTag(tag) } } }) }) } function getPresets() { return currentPresetsDict[currentUploadCategory] || [] } async function removePreset(preset) { let _temp = [] for (const pres of getPresets()) { if (pres !== preset) { _temp.push(pres) } } currentPresetsDict[currentUploadCategory] = _temp return GM.setValue('presets', currentPresetsDict) } document.getElementById('th-add-preset').onclick = async () => { const str = parse_text_to_tag_list().join(TAGSEPERATOR) const currentPresets = getPresets() if (!currentPresets.includes(str)) { currentPresetsDict[currentUploadCategory] = currentPresets.concat(str) await GM.setValue('presets', currentPresetsDict) drawPresets() } } // // // // //endregion /** @returns {string[]} */ function parse_text_to_tag_list(text = tagInput.value) { let tagList = [] for (let tag of text.replaceAll(' ', '').split(TAGSEPERATOR.trim())) { tag.trim() && tagList.push(tag) } return tagList } function addCommaToEndAndDrawCurrent() { if (tagInput.value && !trailingCommaRegex.test(tagInput.value)) { tagInput.value += TAGSEPERATOR } drawCurrentTags() } function drawCurrentTags() { let html = '' const tags = parse_text_to_tag_list() for (const [idx, tag] of tags.entries()) { html += `<div class="tag-wrapper">${idx + 1}. <button type="button" class="tag" data-tag="${tag}">${titlecase(tag)}</button></div>` } currentTagsList.innerHTML = html for (const tagLink of currentTagsList.querySelectorAll('.tag')) { tagLink.onclick = event => { const currentTags = parse_text_to_tag_list() const clickedTag = event.target.getAttribute('data-tag') tagInput.value = currentTags.filter(t => t !== clickedTag).join(TAGSEPERATOR) tagInput.focus() } } } } else { // language=CSS GM_addStyle(` #tag-helper { display: grid; flex-direction: column; gap: 10px; h1 { font-size: 1.1em; margin: 0; } input[type=text] { width: 50%; } } `) const colhead = document.createElement('tr') colhead.classList.add('colhead_dark') colhead.innerHTML = '<td colspan="2" ><strong>Tag Helper</strong></span>' const lastTr = document.querySelector('#userform > table > tbody > tr:last-child') lastTr.before(colhead) const hotkeyTr = document.createElement('tr') hotkeyTr.innerHTML = `<td class="label"><strong>Hotkeys</strong></td>` const td = document.createElement('td') td.id = 'tag-helper' hotkeyTr.append(td) for (const [name, obj] of Object.entries(hotkeys)) { // language=HTML td.innerHTML += ` <h1>${titlecase(name)}</h1> <div> <select name="${name}-modifier"> <option value="Shift">shift</option> <option value="Alt">alt</option> <option value="Control">ctrl</option> <option value="Meta">meta</option> </select> <input type="text" name="${name}-keys" value="${obj.keys.join(', ')}"> </div> ` const selects = td.querySelectorAll('select') selects[selects.length - 1].value = obj.modifier } const saveButton = document.createElement('button') td.append(saveButton) saveButton.textContent = 'Save' saveButton.style.width = 'max-content' saveButton.style.fontSize = 'larger' saveButton.type = 'button' saveButton.onclick = () => { for (const name of Object.keys(hotkeys)) { const modifierInput = td.querySelector(`select[name="${name}-modifier"]`) const keysInput = td.querySelector(`input[name="${name}-keys"]`) hotkeys[name].modifier = modifierInput.value hotkeys[name].keys = keysInput.value.split(', ') } GM_setValue('hotkeys', hotkeys) saveButton.textContent = 'Saved' } colhead.after(hotkeyTr) }