// ==UserScript== // @name New Userscript // @namespace http://tampermonkey.net/ // @version 2024-11-18 // @desc try to take over the world! // @author You // @match https://rst.runcity.org/*/cp_mgmt/?action=edit&cp_id=* // @icon https://www.google.com/s2/favicons?sz=64&domain=runcity.org // @grant none // @require https://code.jquery.com/jquery-3.7.1.min.js // @require https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js // @require https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/Leaflet.fullscreen.min.js // ==/UserScript== (function () { 'use strict'; class Property { id; name; content; desc; constructor(id, row) { this.id = id let columns = [...row.querySelectorAll('td, th')].map(el => el.cloneNode(true)) if (columns.length === 1) { this.content = columns[0].innerHTML.trim() } else if (columns.length >= 2) { this.name = columns[0].innerHTML.trim() let elementsToRemove = columns[1].querySelector("input[name=\"read_gps2\"]") if (elementsToRemove) { columns[1].removeChild(elementsToRemove) } this.content = columns[1].innerHTML .replaceAll("
", "") .replaceAll(/\s*Перейти к легенде.*$/gs, "") .replaceAll("для легенды (\"l\")", "") .replaceAll("общие (\"c\")", "") .replaceAll("для ист. справки (\"h\")", "") .replaceAll("Получить координаты КП из картинки", "") .trim() } if (columns.length === 3) { this.desc = columns[2].innerHTML.replaceAll("
", "").replaceAll(/(\n)\s*/g, "$1").trim() } } toDiv(altName = null, altDesc = null) { let div = document.createElement('div') let desc = altDesc ?? this.desc desc = desc ? "
" + "" + "
" : "" div.innerHTML = "" + "
" + this.content + "
" + desc return div } } function createFrom(rows, classList, data) { let container = document.createElement('div') container.classList.add(...classList.split(" ")) for (const options of data) { let prop = new Property(options.index, rows[options.index]) container.appendChild(prop.toDiv(options.name, options.desc)) } return container } function createFromMulti(rows, classList, data) { let rowData = [] let toIndex = data.to ?? rows.length - 1 for (let i = data.from; i <= toIndex; i++) { rowData.push({ index: i }) } return createFrom(rows, classList, rowData) } function addCss(link) { let css = document.createElement("link") css.href = link css.rel = "stylesheet" document.head.appendChild(css) } function makeDownloadLink(name, href = null) { let downloadLink = document.createElement("a") downloadLink.setAttribute("download", name) downloadLink.textContent = "Скачать" downloadLink.setAttribute("target", "_blank") if (href) downloadLink.href = href return downloadLink } function addDisplayedFile(input, type, file, preview, removeLink) { if (!input || !file || !removeLink) return let fileListContainer = input.parentElement.parentElement.querySelector(".file-list-container") let fileContainer = document.createElement("div") fileContainer.classList.add("file-container") const displayedFile = document.createElement(type ?? "a") displayedFile.classList.add("preview-small") if (type == "img") { displayedFile.src = preview displayedFile.addEventListener("click", () => { let dialog = document.querySelector("dialog") let dialogFile = displayedFile.cloneNode(true) dialogFile.src = file dialog.appendChild(dialogFile) dialog.showModal() }) } else if (type == "audio") { displayedFile.src = file displayedFile.setAttribute("controls", "") } else { displayedFile.href = file displayedFile.textContent = "Файл" } let fileButtonsContainer = document.createElement("div") fileButtonsContainer.classList.add("file-buttons-container") fileButtonsContainer.appendChild(makeDownloadLink(/[^/]*$/.exec(new URL(file).pathname)[0], file)) let deleteButton = document.createElement("button") deleteButton.type = "button" deleteButton.classList.add("button-delete") deleteButton.textContent = "x" deleteButton.addEventListener("click", e => { if (!confirm("Точно?")) return fetch(removeLink) .then(res => fileListContainer.removeChild(fileContainer)) }) fileButtonsContainer.appendChild(deleteButton) fileContainer.appendChild(displayedFile) fileContainer.appendChild(fileButtonsContainer) fileListContainer.appendChild(fileContainer) } function dataURLtoFile(dataurl, filename) { let arr = dataurl.split(',') let mime = arr[0].match(/:(.*?);/)[1] let bstr = atob(arr[arr.length - 1]) let n = bstr.length let u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new File([u8arr], filename, {type:mime}) } function getHtmlElementByFileType(file) { const htmlElementsForTypes = { "image/": "img", "audio/": "audio" } let res = "div" for (const [type, el] of Object.entries(htmlElementsForTypes)) { if (file.type.startsWith(type)) { res = el break } } let el = document.createElement(res) if (res == "audio") { el.setAttribute("controls", "") } return el } function handleFiles() { let dt = new DataTransfer() let storageLabel = `files-${this.dataset.index}` let storedFiles = localStorage.getItem(storageLabel) if (storedFiles) { let oldFiles = JSON.parse(localStorage.getItem(storageLabel)) for (const oldFile of oldFiles) { let oldFileObj = dataURLtoFile(oldFile.data, oldFile.name) dt.items.add(oldFileObj) } } for (let i = 0; i < this.files.length; i++) { const file = this.files[i] dt.items.add(file) let fileListContainer = this.parentElement.parentElement.querySelector(".file-list-container") let fileContainer = document.createElement("div") fileContainer.classList.add("file-container") const displayedFile = getHtmlElementByFileType(file) displayedFile.classList.add("preview") displayedFile.file = file displayedFile.addEventListener("click", () => { let dialog = document.querySelector("dialog") dialog.appendChild(displayedFile.cloneNode(true)) dialog.showModal() }) let fileButtonsContainer = document.createElement("div") fileButtonsContainer.classList.add("file-buttons-container") let downloadLink = makeDownloadLink(file.name) fileButtonsContainer.appendChild(downloadLink) let deleteButton = document.createElement("button") deleteButton.classList.add("button-delete") deleteButton.textContent = "x" deleteButton.type = "button" deleteButton.addEventListener("click", e => { let index = [...fileListContainer.children].indexOf(fileContainer) if (!confirm("Точно?")) return let storedFiles = JSON.parse(localStorage.getItem(storageLabel)) ?? [] storedFiles.splice(storedFiles.length - this.files.length + index, 1) localStorage.setItem(storageLabel, JSON.stringify(storedFiles)) let rdt = new DataTransfer() for (const file of this.files) { rdt.items.add(file) } rdt.items.remove(index) this.files = rdt.files fileListContainer.removeChild(fileContainer) }) fileButtonsContainer.appendChild(deleteButton) fileContainer.appendChild(displayedFile) fileContainer.appendChild(fileButtonsContainer) fileListContainer.appendChild(fileContainer) const reader = new FileReader() reader.onload = (e) => { displayedFile.src = e.target.result downloadLink.setAttribute("href", displayedFile.src) let storedFiles = JSON.parse(localStorage.getItem(storageLabel)) ?? [] storedFiles.push({data: displayedFile.src, name: file.name}) localStorage.setItem(storageLabel, JSON.stringify(storedFiles)) } reader.readAsDataURL(file) } this.files = dt.files } let prettifyFiles = (function() { var executed = false return function(insertedFileRows) { if (executed) return executed = true document.querySelector("#new").querySelectorAll("input[type=file]").forEach((element, index) => { element.id = `input-file-${index}` element.setAttribute("multiple", "multiple") element.dataset.index = index let pseudoInput = document.createElement("label") pseudoInput.classList.add("custom-file-upload") pseudoInput.setAttribute("for", element.id) pseudoInput.textContent = "+" element.parentElement.insertBefore(pseudoInput, element) let fileListContainer = document.createElement("div") fileListContainer.classList.add("file-list-container") element.parentElement.parentElement.appendChild(fileListContainer) element.addEventListener("change", handleFiles, false) }) moveFileInputValues(insertedFileRows) } })() function saveInputValues(from) { let fromInputs = [...from.querySelectorAll("input, textarea")] let result = new Map() for (const fromEl of fromInputs) { result.set(fromEl, fromEl.type == "radio" ? fromEl.checked : fromEl.value) } return result } function setInputValues(to, values) { let toInputs = [...to.querySelectorAll("input, textarea")] for (const [fromEl, value] of values) { let currentToInput = toInputs.find(toEl => toEl.type !== 'file' && toEl.type === fromEl.type && toEl.name === fromEl.name && toEl.id === fromEl.id && (toEl.type !== "radio" || toEl.value === fromEl.value) ) if (currentToInput) { currentToInput.checked = fromEl.checked currentToInput.value = fromEl.value } } } function moveFileInputValues(insertedFileRows) { for (const [inputRowIndex, rows] of insertedFileRows) { for (const row of rows) { let fileInput = null, type = null, fileUrl = null, deleteUrl = null, preview = null if (row.querySelector("img")) { type = "img" fileUrl = row.querySelector("td:first-child a").href preview = row.querySelector("img").src } else if (row.querySelector("audio")) { type = "audio" fileUrl = row.querySelector("audio").src } else { fileUrl = row.querySelector("td:first-child a").href } deleteUrl = row.querySelector("td:nth-child(2) a").href if (inputRowIndex == 23) { fileInput = "attachment1" } else if (inputRowIndex == 27) { fileInput = "attachment4" } else if (inputRowIndex == 31) { fileInput = "attachment2" } else if (inputRowIndex == 35) { fileInput = "attachment3" } else { console.log(inputRowIndex) return } addDisplayedFile(document.querySelector(`input[name="${fileInput}\[\]"]`), type, fileUrl, preview, deleteUrl) } } } function hide(elList) { elList.forEach(el => el.classList.add("hidden")) } function show(elList) { elList.forEach(el => el.classList.remove("hidden")) } let styles = ` #content-wrapper { display: flex; gap: 20px; } #props { width: unset !important; } form { width: 50%; } #map-wrapper { width: 50%; } #map { width: unset !important; } #new { display: flex; flex-direction: column; } #new > :not(:first-child) { padding-top: 5px; border-top: 1px solid #ddd; } #new > :not(:last-child) { padding-bottom: 5px; } #cps_main { width: 150px; } input[type=submit] { width: fit-content; } .desc-icon-container { display: flex; align-items: center; } .desc-icon { width: 15px; height: 15px; cursor: help; } button, input[type=submit] { cursor: pointer; } #new input[type=radio], input[type=checkbox] { margin: 0; } #new input[type="file"] { display: none; } #new div > input[type="checkbox"] { display: flex; align-items: center; } #new input#cp_number { width: 3em; } #new select#cps_main { width: 6em; } #new input:is(#lat, #lng) { width: 5.5em; } #new input[name="cp[name_int]"] { width: 34em; } .custom-file-upload { display: block; width: fit-content; border: 1px solid black; padding: 5px 14px; border-radius: 5px; box-shadow: 0 1px 3px #ddd; background: linear-gradient(white, #ddd); } .header > * { display: flex; gap: 5px; } :is(.header, .options) input { width: unset; } .header { display: flex; gap: 30px; align-items: start; } .options { display: flex; flex-wrap: wrap; gap: 10px; } .options > * { display: flex; gap: 5px; align-items: center; } .legend-container { display: flex; } .legend-container > :not(:last-child) { padding-right: 15px; border-right: 1px solid #ddd; } .legend-container > :not(:first-child) { padding-left: 15px; } .legend-container > * { display: flex; flex-direction: column; flex: 0 1 48%; gap: 5px; } .legend-container__desc-header { display: flex; gap: 10px; align-items: center; justify-content: end; } .legend-desc { display: flex; flex-direction: column; gap: 5px; } .collapsed, .hidden { display: none; } .collapse-button::after { content: "Свернуть" } .collapsed + * > .collapse-button::after { content: "Развернуть" } :is(.comment, .legend-desc) > * { display: flex; gap: 5px; } :is(.comment, .legend-desc) label { display: block; width: 10em; } :is(.comment, .legend-desc) > * > :nth-child(2) { flex: 1 1 auto; } textarea { width: 100%; } .files-container input[type=radio] { display: none; } .file-list-container { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 5px; } .file-container { display: flex; flex-direction: column; gap: 5px; } .file-container img.preview { width: 120px; } .file-container img.preview-small { width: 60px; } .file-container img.preview { cursor: zoom-in; } dialog .preview { max-width: 1000px; max-height: 600px; } button.button-delete { padding: 0; border: 0; margin: 0; background: none; font-size: 18px; } .admin-files-container { display: flex; } .admin-files-container > :not(:last-child) { padding-right: 10px; border-right: 1px solid #ddd; } .admin-files-container > :not(:first-child) { padding-left: 10px; } dialog img.preview { width: auto; } .file-buttons-container { display: flex; align-items: center; gap: 5px; } ` /* HEAD */ let styleSheet = document.createElement("style") styleSheet.textContent = styles document.head.appendChild(styleSheet) addCss("https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css") addCss('https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/leaflet.fullscreen.css') /* NEW DEFALUT VALUES */ document.querySelector(`#props input[name="new_file_type1"][value="l"]`).click() document.querySelector(`#props input[name="new_file_type4"][value="l"]`).click() /* CONTAINER */ let form = document.querySelector('form') let rows = [...document.querySelectorAll('#props > tbody > tr')] let insertedFileRows = new Map() let i = 0 while (i < rows.length) { if ([23, 27, 31, 35].includes(i) && rows[i].querySelector(`input[type="file"]`) == null) { if (!insertedFileRows.has(i)) { insertedFileRows.set(i, [rows[i]]) } else { let savedRows = insertedFileRows.get(i) savedRows.push(rows[i]) insertedFileRows.set(i, savedRows) } rows.splice(i, 1) } else { i++ } } let container = document.createElement('div') container.id = "new" let oldTable = document.querySelector("#props") /* HEADER */ let headerContainer = createFrom(rows, "header", [ { index: 2, name: "№", desc: "" }, { index: 8, name: "Название" } ]) let copyLink = document.createElement("div") copyLink.innerHTML = new Property(2, rows[2]).desc headerContainer.append(copyLink) /* TOP BUTTONS */ let topButtonsContainer = createFrom(rows, "buttons", [ { index: 0 } ]) let bottomButtonsContainer = topButtonsContainer.cloneNode(true) document.querySelectorAll("#props tr:is(:first-child, :last-child) th").forEach(el => { let prettifyButton = document.createElement("button") prettifyButton.type = "button" prettifyButton.textContent = "Сделать красиво" prettifyButton.addEventListener("click", () => { let inputValues = saveInputValues(oldTable) form.removeChild(oldTable) form.appendChild(container) setInputValues(container, inputValues) prettifyFiles(insertedFileRows) $("#cps_main").select2() }) el.appendChild(prettifyButton) }) ;[topButtonsContainer, bottomButtonsContainer].forEach(el => { let unglifyButton = document.createElement("button") unglifyButton.type = "button" unglifyButton.textContent = "Сделать некрасиво" unglifyButton.addEventListener("click", () => { let inputValues = saveInputValues(container) form.removeChild(container) form.appendChild(oldTable) setInputValues(oldTable, inputValues) }) el.querySelector("div > div > div").appendChild(unglifyButton) }) /* OPTIONS */ let firstContainer = createFrom(rows, "options", [ { index: 3 }, { index: 4, name: "КП-загадка" }, { index: 16, name: "Этапник" }, { index: 7, name: "Пиктограмма" }, { index: 12, name: "Нужна ИС" }, { index: 17, name: "Спрятать ИС" }, { index: 14, name: "Основной КП" }, { index: 10, name: "Широта" }, { index: 11, name: "Долгота" }, { index: 5, name: "Старт" }, { index: 6, name: "Финиш" }, { index: 15, name: "Знак" } ]) let commentContainer = createFrom(rows, "comment", [ { index: 9, desc: "" } ]) /* LEGEND */ let legendContainer = document.createElement("div") legendContainer.classList.add("legend-container") /* LEGEND DESC */ let legendDescContainer = document.createElement("div") legendDescContainer.classList.add("legend-container__desc") let legendDescHeader = document.createElement("div") legendDescHeader.classList.add("legend-container__desc-header") const LEGEND_RU_LABEL = "Русский" const LEGEND_EN_LABEL = "Английский" let legendLang = document.createElement("div") legendLang.classList.add("legend-desc__lang") legendLang.textContent = LEGEND_RU_LABEL legendDescHeader.appendChild(legendLang) let legendEnSwitchContainer = createFrom(rows, "legend-switch-container", [ { index: 48 } ]) legendEnSwitchContainer.addEventListener("click", event => { hide([legendRuDescContainer, legendRuHiddenDescContainer, legendEnSwitchContainer]) show([legendEnDescContainer, legendEnHiddenDescContainer, legendRuSwitchContainer]) legendLang.textContent = LEGEND_EN_LABEL }) legendDescHeader.appendChild(legendEnSwitchContainer) let copyDescButton = document.createElement("button") copyDescButton.type = "button" copyDescButton.textContent = "Копировать" copyDescButton.addEventListener("click", () => { let ruInputs = [...container.querySelectorAll(":is(input, textarea)[name^=\"cp_strings\[ru\]\"]")] let enInputs = [...container.querySelectorAll(":is(input, textarea)[name^=\"cp_strings\[en\]\"]")] for (const [i, enInput] of enInputs.entries()) { enInput.value = ruInputs[i].value } }) let legendRuSwitchContainer = createFrom(rows, "legend-switch-container hidden", [ { index: 39 } ]) legendRuSwitchContainer.addEventListener("click", event => { hide([legendEnDescContainer, legendEnHiddenDescContainer, legendRuSwitchContainer]) show([legendRuDescContainer, legendRuHiddenDescContainer, legendEnSwitchContainer]) legendLang.textContent = LEGEND_RU_LABEL }) legendDescHeader.appendChild(legendRuSwitchContainer) legendDescHeader.appendChild(copyDescButton) let legendRuDescContainer = createFrom(rows, "legend-desc", [ { index: 40, desc: "" }, { index: 41, desc: "" }, { index: 42, desc: "" }, { index: 43, desc: "" } ]) let legendRuHiddenDescContainer = createFrom(rows, "legend-desc collapsible collapsed", [ { index: 44, desc: "" }, { index: 45, desc: "" }, { index: 46, desc: "" } ]) let legendEnDescContainer = createFrom(rows, "legend-desc hidden", [ { index: 49, desc: "" }, { index: 50, desc: "" }, { index: 51, desc: "" }, { index: 52, desc: "" } ]) let legendEnHiddenDescContainer = createFrom(rows, "legend-desc collapsible collapsed hidden", [ { index: 53, desc: "" }, { index: 54, desc: "" }, { index: 55, desc: "" } ]) let hider = document.createElement("div") let hiderButton = document.createElement("button") hiderButton.classList.add("collapse-button") hiderButton.setAttribute("type", "button") hiderButton.addEventListener("click", event => { container.querySelectorAll(".legend-desc.collapsible").forEach(element => { element.classList.toggle("collapsed") }) }) hider.appendChild(hiderButton) legendDescContainer.appendChild(legendDescHeader) legendDescContainer.appendChild(legendRuDescContainer) legendDescContainer.appendChild(legendRuHiddenDescContainer) legendDescContainer.appendChild(legendEnDescContainer) legendDescContainer.appendChild(legendEnHiddenDescContainer) legendDescContainer.appendChild(hider) legendContainer.appendChild(legendDescContainer) /* LEGEND FILES */ let legendFilesContainer = document.createElement("div") legendFilesContainer.classList.add("legend-container__files") let imagesForLegendContainer = createFrom(rows, "files-container", [ { index: 23, name: "Фото в легенде" } ]) legendFilesContainer.appendChild(imagesForLegendContainer) let audioForLegendContainer = createFrom(rows, "files-container", [ { index: 27, name: "Файлы в легенде" } ]) legendFilesContainer.appendChild(audioForLegendContainer) legendContainer.appendChild(legendFilesContainer) let adminFilesContainer = document.createElement('div') adminFilesContainer.classList.add('admin-files-container') let imagesForAdminContainer = createFrom(rows, "files-container", [ { index: 31, name: "Фото в админке" } ]) adminFilesContainer.appendChild(imagesForAdminContainer) let audioForAdminContainer = createFrom(rows, "files-container", [ { index: 35, name: "Файлы в админке" } ]) adminFilesContainer.appendChild(audioForAdminContainer) /* BOTTOM OPTIONS */ let bottomOptionsContainer = createFromMulti(rows, "options bottom-options", { from: 59, to: rows.length - 3 }) /* APPEND ALL */ container.appendChild(topButtonsContainer) container.appendChild(headerContainer) container.appendChild(firstContainer) container.appendChild(commentContainer) container.appendChild(legendContainer) container.appendChild(adminFilesContainer) container.appendChild(bottomOptionsContainer) container.appendChild(bottomButtonsContainer) /* MAP */ let content = document.querySelector("#content") let contentWrapper = document.createElement("div") contentWrapper.id = "content-wrapper" contentWrapper.appendChild(document.querySelector("form")) contentWrapper.appendChild(document.querySelector("#map-wrapper")) content.appendChild(contentWrapper) $(() => { map.addControl(new L.Control.Fullscreen()) }) let panToCenter = document.createElement("button") panToCenter.type = "button" panToCenter.textContent = "В центр" panToCenter.addEventListener("click", () => { let lat = document.querySelector("input[name=\"cp\[lattitude\]\"").value let lon = document.querySelector("input[name=\"cp\[longitude\]\"").value map.setView(new L.LatLng(parseFloat(lat), parseFloat(lon)), 16) }) document.querySelector("#map_controls").appendChild(panToCenter) /* FILES */ localStorage.clear() let dialog = document.createElement("dialog") dialog.id = "dialog" document.body.appendChild(dialog) dialog.addEventListener("click", (e) => { if (e.target == dialog) { e.target.close() e.target.innerHTML = "" } }) })();