tampermonkey/main.js
2024-11-29 12:45:56 +00:00

868 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==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("<br>", "")
.replaceAll(/\s*Перейти к легенде.*$/gs, "")
.replaceAll("для легенды (\"l\")", "")
.replaceAll("общие (\"c\")", "")
.replaceAll("для ист. справки (\"h\")", "")
.replaceAll("Получить координаты КП из картинки", "")
.trim()
}
if (columns.length === 3) {
this.desc = columns[2].innerHTML.replaceAll("<br>", "").replaceAll(/(\n)\s*/g, "$1").trim()
}
}
toDiv(altName = null, altDesc = null) {
let div = document.createElement('div')
let desc = altDesc ?? this.desc
desc = desc ? "<span title='" + desc + "'>?</span>" : ""
div.innerHTML = "<label for=''>" + (altName ?? this.name ?? "") + "</label>" + "<div>" + this.content + "</div>" +
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 = "Скачать"
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;
}
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;
}
.file-container img.preview {
width: 120px;
}
.file-container 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 = ""
}
})
})();