From fb9168f700f0af42f4d46294acecfa19002763ed Mon Sep 17 00:00:00 2001 From: Zhora Shalyapin Date: Fri, 29 Nov 2024 12:36:39 +0000 Subject: [PATCH] first commit --- main.js | 867 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 867 insertions(+) create mode 100644 main.js diff --git a/main.js b/main.js new file mode 100644 index 0000000..705190e --- /dev/null +++ b/main.js @@ -0,0 +1,867 @@ +// ==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/msk2025/cp_mgmt/?action=edit&cp_id=88548 +// @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 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 = "Скачать" + 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("test.png", 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; + } + + 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; + } + + .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; + } + + .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; + } + + img.preview { + width: 120px; + } + + img.preview-small { + width: 60px; + } + + .files-container img.preview { + cursor: zoom-in; + } + + 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: "№" }, + { 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: 5, name: "Старт" }, + { index: 6, name: "Финиш" }, + { index: 16, name: "Этапник" }, + { index: 7, name: "Пиктограмма" }, + { index: 12, name: "Нужна ИС" }, + { index: 17, name: "Спрятать ИС" }, + { index: 14, name: "Основной КП" }, + { index: 10, name: "X" }, + { index: 11, name: "Y" }, + { 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") + + let legendEnSwitchContainer = createFrom(rows, "legend-switch-container", [ + { index: 48 } + ]) + legendEnSwitchContainer.addEventListener("click", event => { + hide([legendRuDescContainer, legendRuHiddenDescContainer, legendEnSwitchContainer]) + show([legendEnDescContainer, legendEnHiddenDescContainer, legendRuSwitchContainer]) + }) + 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]) + }) + 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 = createFrom(rows, "options bottom-options", [ + { index: 59 }, + { index: 60 }, + { index: 61 }, + { index: 62 }, + { index: 63 } + ]) + + /* 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)), 13) + }) + 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 = "" + } + }) + +})(); \ No newline at end of file