// ==UserScript==
// @name CFCodereviewer
// @namespace http://tampermonkey.net/
// @version 3.0.0
// @description Codereview codeforces
// @author kdzestelov
// @license MIT
// @match *://*codeforces.com/*
// @grant GM_xmlhttpRequest
// ==/UserScript==
const SHEET_URL = 'https://script.google.com/macros/s/AKfycbw7eHpt-69rYbxfpFrcv6n_JGU_jT-tx0p6RdlwKITLn9OojA9tNH60ciaYW3CAQqO46w/exec';
const SUB_RJ_BUTTON_CLASS = "submission-rj-button";
const SUB_AC_BUTTON_CLASS = "submission-ac-button";
const SUB_RG_BUTTON_CLASS = "submission-rejudge-form";
const SUB_COMMENT_SEND_BUTTON_CLASS = "submission-comment-send-form";
function getLast(href) {
return href.split("/").pop()
}
function getGroupUrl() {
const url = window.location.href
const contestIndex = url.indexOf("contest");
if (contestIndex === -1) return url;
return url.substring(0, contestIndex);
}
const getContestUrl = () => {
const url = window.location.href
const statusIndex = url.indexOf("status");
if (statusIndex === -1) return url;
return url.substring(0, statusIndex);
}
function getContestId() {
const url = window.location.href
const contestIndex = url.indexOf("contest");
if (contestIndex === -1) return "";
return url.substring(contestIndex + 8, contestIndex + 14);
}
function wrap(text) {
return '[' + text + ']';
}
function getSubRjButton(subId) {
return $("[submissionid=" + subId + "] ." + SUB_RJ_BUTTON_CLASS);
}
function getSubAcButton(subId) {
return $("[submissionid=" + subId + "] ." + SUB_AC_BUTTON_CLASS);
}
function getSubRgButton(subId) {
return $("[submissionid=" + subId + "] ." + SUB_RG_BUTTON_CLASS);
}
function getCommentSubAcButton(subId) {
return $("tr[data-submission-id=" + subId + "] ." + SUB_COMMENT_SEND_BUTTON_CLASS)
}
// Функция для блокировки всех кнопок выше кода (AC, RJ, RG)
function disableTopButtons(subId) {
console.log(`[disableTopButtons] Блокирую кнопки для посылки ${subId}`);
const acButton = getSubAcButton(subId);
const rjButton = getSubRjButton(subId);
const rgButton = getSubRgButton(subId);
const acCount = acButton.length;
const rjCount = rjButton.length;
const rgCount = rgButton.length;
acButton.each(function() {
this.disabled = true;
this.style.backgroundColor = "#999";
this.style.borderColor = "#999";
this.style.cursor = "not-allowed";
});
rjButton.each(function() {
this.disabled = true;
this.style.backgroundColor = "#999";
this.style.borderColor = "#999";
this.style.cursor = "not-allowed";
});
rgButton.each(function() {
this.disabled = true;
this.style.backgroundColor = "#999";
this.style.borderColor = "#999";
this.style.cursor = "not-allowed";
});
console.log(`[disableTopButtons] Заблокировано: AC=${acCount}, RJ=${rjCount}, RG=${rgCount}`);
}
// Функция для разблокировки всех кнопок выше кода
function enableTopButtons(subId) {
console.log(`[enableTopButtons] Разблокирую кнопки для посылки ${subId}`);
const acButton = getSubAcButton(subId);
const rjButton = getSubRjButton(subId);
const rgButton = getSubRgButton(subId);
const isAccepted = isSubAccepted(subId);
const acColor = isAccepted ? "#81D718" : "#13aa52";
const acCount = acButton.length;
const rjCount = rjButton.length;
const rgCount = rgButton.length;
acButton.each(function() {
this.disabled = false;
this.style.backgroundColor = acColor;
this.style.borderColor = acColor;
this.style.cursor = "pointer";
});
rjButton.each(function() {
this.disabled = false;
this.style.backgroundColor = "#EC431A";
this.style.borderColor = "#EC431A";
this.style.cursor = "pointer";
});
rgButton.each(function() {
this.disabled = false;
this.style.backgroundColor = "#176F95";
this.style.borderColor = "#176F95";
this.style.cursor = "pointer";
});
console.log(`[enableTopButtons] Разблокировано: AC=${acCount}, RJ=${rjCount}, RG=${rgCount}`);
}
function getProblemIndex(subId) {
return getLast($("tr[data-submission-id=" + subId + "] .status-small a").attr('href'));
}
function getSubRow(subId) {
return $('tr[data-submission-id="' + subId + '"]')
}
function getHandle(subId) {
return $("tr[data-submission-id=" + subId + "] .status-party-cell").text().trim();
}
function getAllSubmissionsRow() {
return $(".status-frame-datatable tbody tr")
}
function getSubmissionRow(subId) {
return $(".status-frame-datatable tbody tr[data-submission-id=" + subId + "]")
}
function getSubsId() {
return $(".information-box-link")
}
function getCorrectSubs() {
return $(".information-box-link .verdict-accepted").parent();
}
function getSubButtons() {
return $(".submission-action-form");
}
function getSideBar() {
return $("div[id=sidebar]")
}
function getFilterBox() {
return $(".status-filter-box");
}
const getSheetSubmissions = () => {
const fullUrl = SHEET_URL + "?type=get"
console.log(`[getSheetSubmissions] Начинаю загрузку посылок из Google Sheets`);
console.log(`[getSheetSubmissions] URL: ${fullUrl}`);
GM_xmlhttpRequest({
method: 'GET',
url: fullUrl,
onload: (response) => {
console.log(`[getSheetSubmissions] Получен ответ, status: ${response.status}`);
if (response.status === 200) {
try {
var submissions = JSON.parse(response.responseText)
submissions = JSON.parse(response.responseText)
localStorage.setItem("c_status", JSON.stringify(submissions));
console.log(`[getSheetSubmissions] Успешно загружено ${submissions.length} посылок:`, submissions);
} catch (e) {
console.error(`[getSheetSubmissions] Ошибка парсинга ответа:`, e);
console.error(`[getSheetSubmissions] Ответ:`, response.responseText);
}
} else {
console.error(`[getSheetSubmissions] Ошибка загрузки: status=${response.status}`);
console.error(`[getSheetSubmissions] Ответ:`, response.responseText);
}
},
onerror: (error) => {
console.error(`[getSheetSubmissions] Ошибка сети:`, error);
}
});
}
const acceptSheetSubmission = (subId, button, type, onSuccess, onError) => {
const full_url = SHEET_URL + "?type=" + type + "&value=" + subId;
console.log("acceptSheetSubmission вызвана для subId:", subId, "type:", type, "hasOnSuccess:", !!onSuccess, "hasOnError:", !!onError);
GM_xmlhttpRequest({
method: 'GET',
url: full_url,
timeout: 5000,
onload: (response) => {
console.log("acceptSheetSubmission onload, status:", response.status, "subId:", subId);
if (response.status === 200) {
const submissions = getSubmissions();
if (!submissions.includes(subId)) {
submissions.push(subId);
localStorage.setItem("c_status", JSON.stringify(submissions));
}
if (button) {
button.style.backgroundColor = "#81D718";
button.style.borderColor = "#81D718";
button.innerText = (type === "star" ? "🔥" : "Похвалить");
}
if (showed_codes[subId] != null && showed_codes[subId].showed) {
showed_codes[subId]["showButton"].click();
}
// Вызываем колбэк успеха после всех операций
console.log("acceptSheetSubmission вызываю onSuccess для subId:", subId);
if (onSuccess) {
try {
onSuccess();
} catch (e) {
console.error("Ошибка в колбэке onSuccess:", e);
}
} else {
console.warn("acceptSheetSubmission: onSuccess не предоставлен для subId:", subId);
}
} else {
console.log("acceptSheetSubmission ошибка статуса:", response.status);
if (button) {
button.innerText = "Ошибка!";
}
console.error(response);
// Вызываем колбэк ошибки
console.log("acceptSheetSubmission вызываю onError для subId:", subId);
if (onError) {
try {
onError();
} catch (e) {
console.error("Ошибка в колбэке onError:", e);
}
} else {
console.warn("acceptSheetSubmission: onError не предоставлен для subId:", subId);
}
}
},
onerror: (error) => {
console.error("acceptSheetSubmission onerror для subId:", subId, error);
if (button) {
button.innerText = "Ошибка!";
}
// Вызываем колбэк ошибки
console.log("acceptSheetSubmission вызываю onError из onerror для subId:", subId);
if (onError) {
try {
onError();
} catch (e) {
console.error("Ошибка в колбэке onError:", e);
}
} else {
console.warn("acceptSheetSubmission: onError не предоставлен для subId:", subId);
}
}
});
}
const getSubmissions = () => {
return JSON.parse(localStorage.getItem("c_status") ?? "[]");
};
const isSubAccepted = (subId) => {
return getSubmissions().includes(subId)
}
const acceptSubmission = (subId, button) => {
const isAlreadyAccepted = isSubAccepted(subId);
const type = isAlreadyAccepted ? "star" : "accept";
console.log(`[acceptSubmission] Принимаю посылку ${subId}, тип: ${type}, уже принята: ${isAlreadyAccepted}`);
acceptSheetSubmission(subId, button, type);
}
const showed_codes = {};
// Единые стили для кнопок в таблице посылок
const submissionButtonStyles = {
base: {
border: "1px solid",
borderRadius: "6px",
color: "#fff",
cursor: "pointer",
fontSize: "1.37rem",
fontWeight: "500",
transition: "all 0.2s ease",
boxShadow: "0 1px 3px rgba(0,0,0,0.12)"
},
showCode: {
backgroundColor: "#176F95",
borderColor: "#176F95",
padding: "0.6em 1em",
marginTop: "4px",
marginBottom: "4px",
width: "100%"
},
accept: {
padding: "0.55em 0.9em",
margin: "5px 5px 0 0",
width: "59%"
},
reject: {
backgroundColor: "#EC431A",
borderColor: "#EC431A",
padding: "0.55em 0.9em",
width: "37%"
},
rejudge: {
backgroundColor: "#176F95",
borderColor: "#176F95",
padding: "0.5em 0.8em",
margin: "5px 0",
width: "100%"
},
comment: {
margin: "4px",
width: "40%",
padding: "0.55em 0.9em"
}
};
// Вспомогательная функция для применения стилей кнопок
const applyButtonStyles = (element, ...styleObjs) => {
styleObjs.forEach(styleObj => Object.assign(element.style, styleObj));
};
function createSubShowButton(subId, lang) {
const button = document.createElement("button");
button.className = "submission-show";
button.innerText = "Показать " + lang;
applyButtonStyles(button, submissionButtonStyles.base, submissionButtonStyles.showCode);
button.onclick = (_) => showButtonClick(subId, button);
return button;
}
function patchCodeSection(subId) {
const patchLine = (i, line) => {
line.addEventListener('click', () => {
if(window.getSelection().toString() != "")
return;
// Проверяем, открыто ли плавающее окно комментариев для этой посылки
const panel = floatingCommentPanels[subId];
const floatingTextfield = panel && panel.style.display !== "none" ? panel.querySelector("#floating-comment-textfield-" + subId) : null;
if (floatingTextfield) {
// Используем плавающее окно, если оно открыто для этой посылки
const currentText = $(floatingTextfield).val();
const newLine = currentText.length === 0 ? "" : currentText + "\n";
$(floatingTextfield).val(newLine + "Строка " + (i + 1) + ": ");
// Фокусируемся без scrollIntoView, чтобы не смещать панель
const scrollY = window.scrollY;
floatingTextfield.focus();
// Восстанавливаем позицию скролла, если она изменилась
if (Math.abs(window.scrollY - scrollY) > 1) {
window.scrollTo(0, scrollY);
}
} else {
// Используем старое поведение для совместимости
const text = $("[data-submission-id=" + subId + "] textarea")
if (text.length > 0) {
text.val((text.val().length === 0 ? "" : text.val() + "\n") + "Строка " + (i + 1) + ":" + ' ')
var x = window.scrollX, y = window.scrollY;
text.focus();
window.scrollTo(x, y);
}
}
});
return line;
};
let pretty_code = $('[data-submission-id=' + subId + '] .program-source li');
const code_lines_count = pretty_code.length
// Добавляем подсветку при наведении на строки кода
pretty_code.each((i, line) => {
patchLine(i, line);
$(line).css("cursor", "pointer");
$(line).on("mouseenter", function() {
$(this).css("backgroundColor", "#e8f0fe");
});
$(line).on("mouseleave", function() {
$(this).css("backgroundColor", "");
});
});
pretty_code = pretty_code.parent().parent();
pretty_code.before((_) => {
const lines = document.createElement("pre");
lines.style.width = '4%';
lines.style.padding = "0.5em";
lines.style.display = 'inline-block';
const lineNs = [...Array(code_lines_count).keys()].map((i) => {
const line = document.createElement("span");
line.style.color = 'rgb(153, 153, 153)';
line.innerText = "[" + (i + 1) + "]";
line.style.display = "block";
line.style.textAlign = "right";
line.style.userSelect = "none";
line.style.cursor = "pointer";
return patchLine(i, line);
})
lines.append(...lineNs)
return lines
})
pretty_code.css({'display': 'inline-block', 'width': '90%'})
}
function showButtonClick(subId, button) {
console.log(`[showButtonClick] Клик по кнопке показа кода для посылки ${subId}`);
if (showed_codes[subId] != null) {
if (showed_codes[subId].showed == true) {
console.log(`[showButtonClick] Скрываю код для посылки ${subId}`);
$(showed_codes[subId].codeSection).hide();
button.innerText = "Показать код";
showed_codes[subId].showed = false;
// Закрываем плавающее окно при скрытии кода
hideFloatingPanel(subId);
} else if (showed_codes[subId].showed == false) {
console.log(`[showButtonClick] Показываю код для посылки ${subId}`);
$(showed_codes[subId].codeSection).show();
button.innerText = "Скрыть код";
showed_codes[subId].showed = true;
// Открываем плавающее окно при показе кода
showFloatingPanel(subId);
}
} else {
console.log(`[showButtonClick] Загружаю код для посылки ${subId}`);
button.innerText = showed_codes[subId] = "Загружаю код..."
const requestUrl = getContestUrl() + 'submission/' + subId;
console.log(`[showButtonClick] Запрос кода: ${requestUrl}`);
$.get(requestUrl, function (html) {
console.log(`[showButtonClick] Получен HTML для посылки ${subId}`);
const codeHtml = $(html).find(".SubmissionDetailsFrameRoundBox-" + subId).html()
if (codeHtml == undefined) {
console.error(`[showButtonClick] Не удалось найти код для посылки ${subId}`);
button.innerText = "Ошибка!";
//location.reload();
return;
}
console.log(`[showButtonClick] Создаю секцию кода для посылки ${subId}`);
const subCodeSection = createSubCodeSection(subId, codeHtml);
const subRow = getSubRow(subId);
subRow.after(subCodeSection)
prettyPrint(subId);
patchCodeSection(subId);
showed_codes[subId] = {
"showed": true,
"showButton": button,
"commentSection": null,
"codeSection": subCodeSection
}
button.innerText = "Скрыть код";
console.log(`[showButtonClick] Код успешно загружен для посылки ${subId}`);
// Открываем плавающее окно после загрузки кода
showFloatingPanel(subId);
}).fail(function(error) {
console.error(`[showButtonClick] Ошибка загрузки кода для посылки ${subId}:`, error);
button.innerText = "Ошибка!";
});
}
}
function createSubCodeSection(subId, codeHtml) {
const trSubCode = document.createElement("tr");
trSubCode.setAttribute('data-submission-id', subId);
const tdSubCode = document.createElement("td");
tdSubCode.setAttribute('colspan', '8');
tdSubCode.innerHTML = codeHtml;
tdSubCode.style.textAlign = "start"
trSubCode.append(tdSubCode);
return trSubCode;
}
const createCommentSection = (subId) => {
console.log(`[createCommentSection] Создаю секцию комментариев для посылки ${subId}`);
const subAcButton = getSubAcButton(subId)[0];
const isAccepted = isSubAccepted(subId);
const submissionType = isAccepted ? "star" : "accept";
const commentTextfield = createCommentTextfield()
const commentAcButton = commentSendButtonTemplate(subId, (isAccepted ? "Похвалить" : "Принять") + " с комментарием", (isAccepted ? "#81D718" : "#13aa52"), (subId, button) => {
const text = $(commentTextfield).val();
console.log(`[createCommentSection] Клик по кнопке "Принять с комментарием" для посылки ${subId}, текст комментария: "${text}"`);
// Блокируем кнопки выше кода при нажатии
disableTopButtons(subId);
if (text.length === 0) {
console.log(`[createCommentSection] Принимаю посылку ${subId} без комментария`);
button.innerText = "Принимаю...";
button.disabled = true;
acceptSheetSubmission(subId, subAcButton, submissionType,
() => {
// Успешное выполнение - разблокируем кнопки
console.log(`[createCommentSection] Посылка ${subId} успешно принята`);
enableTopButtons(subId);
},
() => {
// Ошибка - разблокируем кнопки
console.log(`[createCommentSection] Ошибка при принятии посылки ${subId}`);
enableTopButtons(subId);
button.innerText = "Ошибка!";
button.disabled = false;
button.style.backgroundColor = "#999";
}
);
} else {
console.log(`[createCommentSection] Отправляю комментарий для посылки ${subId}`);
button.innerText = "Отправляю...";
button.disabled = true;
const name = getHandle(subId)
const postUrl = getGroupUrl() + 'data/newAnnouncement';
const postData = {
contestId: getContestId(),
englishText: "",
russianText: text,
submittedProblemIndex: getProblemIndex(subId),
targetUserHandle: name,
announceInPairContest: true,
};
console.log(`[createCommentSection] POST запрос на отправку комментария: ${postUrl}`, postData);
$.post(postUrl, postData, () => {
console.log(`[createCommentSection] Комментарий успешно отправлен для посылки ${subId}`);
button.innerText = "Отправлено!";
acceptSubmission(subId, subAcButton);
// Разблокируем кнопки после успешной отправки
enableTopButtons(subId);
}).fail(function(error) {
console.error(`[createCommentSection] Ошибка при отправке комментария для посылки ${subId}:`, error);
button.innerText = "Ошибка!";
button.disabled = false;
button.style.backgroundColor = "#999";
// Разблокируем кнопки выше кода при ошибке
enableTopButtons(subId);
});
}
})
const commentRjButton = commentSendButtonTemplate(subId, "Отклонить с комментарием", "#EC431A", (subId, button) => {
const text = $(commentTextfield).val();
if (text.length > 0) {
console.log(`[createCommentSection] Клик по кнопке "Отклонить с комментарием" для посылки ${subId}, текст комментария: "${text}"`);
// Блокируем кнопки выше кода при нажатии
disableTopButtons(subId);
button.innerText = "Отклоняю...";
button.disabled = true;
const name = getHandle(subId)
const problem = getProblemIndex(subId)
const postUrl = getGroupUrl() + 'data/newAnnouncement';
const postData = {
contestId: getContestId(),
englishText: "",
russianText: text,
submittedProblemIndex: getProblemIndex(subId),
targetUserHandle: name,
announceInPairContest: true,
};
console.log(`[createCommentSection] POST запрос на отправку комментария при отклонении: ${postUrl}`, postData);
$.post(postUrl, postData, () => {
console.log(`[createCommentSection] Комментарий отправлен, начинаю отклонение посылки ${subId}`);
rejectSub(subId);
if (showed_codes[subId] != null) {
$(showed_codes[subId]["codeSection"]).hide();
}
const params = new URLSearchParams({
type: "rj",
name: name,
comment: "Пришел новый реджект! \n\n Комментарий к посылке по задаче " + problem + ":\n\n" + text,
});
const full_url = SHEET_URL + "?" + params;
console.log(`[createCommentSection] Отправляю данные в Google Sheets для реджекта: ${full_url}`);
GM_xmlhttpRequest({
method: 'GET',
url: full_url,
timeout: 5000,
onload: (response) => {
console.log(`[createCommentSection] Ответ от Google Sheets для реджекта, status: ${response.status}`);
if (response.status === 200) {
console.log(`[createCommentSection] Реджект успешно отправлен в Google Sheets для посылки ${subId}`);
button.innerText = "Отклонено";
// Разблокируем кнопки после успешной отправки (хотя они уже должны быть удалены rejectSub)
} else {
console.error(`[createCommentSection] Ошибка отправки реджекта в Google Sheets: status=${response.status}`);
button.innerText = "Ошибка!";
button.disabled = false;
button.style.backgroundColor = "#999";
// Разблокируем кнопки выше кода при ошибке
enableTopButtons(subId);
console.error(response);
}
},
onerror: (error) => {
console.error(`[createCommentSection] Ошибка сети при отправке реджекта в Google Sheets:`, error);
button.innerText = "Ошибка!";
button.disabled = false;
button.style.backgroundColor = "#999";
// Разблокируем кнопки выше кода при ошибке
enableTopButtons(subId);
}
});
console.log("Url to send comment: " + full_url);
}).fail(function(error) {
console.error(`[createCommentSection] Ошибка при отправке комментария при отклонении для посылки ${subId}:`, error);
button.innerText = "Ошибка!";
button.disabled = false;
button.style.backgroundColor = "#999";
// Разблокируем кнопки выше кода при ошибке
enableTopButtons(subId);
});
}
});
commentTextfield.addEventListener("keyup", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
commentRjButton.click();
}
});
const trSection = document.createElement("tr");
trSection.setAttribute('data-submission-id', subId);
const tdSection = document.createElement("td");
tdSection.setAttribute('colspan', '8');
const tdSectionTitle = document.createElement("div");
tdSectionTitle.className = "caption titled";
tdSectionTitle.innerText = "→ Комментарий";
// Стили заголовка секции
Object.assign(tdSectionTitle.style, {
textAlign: "left",
color: "#3B5998",
fontWeight: "600",
fontSize: "1.3rem",
marginBottom: "0.5em",
paddingLeft: "0.3em"
});
tdSection.append(tdSectionTitle, commentTextfield, commentAcButton, commentRjButton);
trSection.append(tdSection);
return trSection;
}
function createCommentTextfield() {
const textField = document.createElement("textarea");
textField.name = "russianText";
textField.className = "bottom-space-small monospaced";
textField.placeholder = "Комментарий для участника...";
// Стили текстового поля
Object.assign(textField.style, {
width: "80rem",
height: "5rem",
margin: "4px",
padding: "0.6em",
border: "1px solid #d0d0d0",
borderRadius: "6px",
fontSize: "1.24rem",
fontFamily: "monospace",
resize: "vertical",
transition: "border-color 0.2s ease"
});
// Эффект фокуса
textField.addEventListener("focus", () => {
textField.style.borderColor = "#3B5998";
textField.style.outline = "none";
});
textField.addEventListener("blur", () => {
textField.style.borderColor = "#d0d0d0";
});
return textField;
}
const commentSendButtonTemplate = (subId, text, color, action) => {
const button = document.createElement("button");
button.className = SUB_COMMENT_SEND_BUTTON_CLASS;
button.innerText = text;
applyButtonStyles(button, submissionButtonStyles.base);
button.style.margin = "4px";
button.style.width = "40%";
button.style.padding = "0.45em 0.7em";
button.style.backgroundColor = color;
button.style.borderColor = color;
button.onclick = () => action(subId, button);
return button;
}
const acButtonTemplate = (subId, action, text) => {
const acButton = document.createElement("button");
acButton.className = SUB_AC_BUTTON_CLASS;
acButton.innerText = text !== undefined ? text : (isSubAccepted(subId) ? "Похвалить" : "AC");
const color = (isSubAccepted(subId) ? "#81D718" : "#13aa52");
applyButtonStyles(acButton, submissionButtonStyles.base, submissionButtonStyles.accept);
acButton.style.backgroundColor = color;
acButton.style.borderColor = color;
acButton.onclick = (_) => action(subId, acButton);
return acButton;
}
const createAcButton = (template, subId, ...args) => {
return template(subId, (subId, button) => {
button.innerText = "Подтверждаю...";
button.style.borderColor = "gray";
button.style.backgroundColor = "gray";
acceptSubmission(subId, button);
}, ...args);
}
const createRjButton = (subId, text, action) => {
const rjButton = document.createElement("button");
rjButton.className = SUB_RJ_BUTTON_CLASS;
rjButton.innerText = text;
applyButtonStyles(rjButton, submissionButtonStyles.base, submissionButtonStyles.reject);
rjButton.onclick = (_) => action(subId, rjButton);
return rjButton;
}
const createRgButton = (subId) => {
const button = document.createElement("button");
button.className = SUB_RG_BUTTON_CLASS;
button.innerText = "Перетестировать";
applyButtonStyles(button, submissionButtonStyles.base, submissionButtonStyles.rejudge);
button.onclick = (_) => {
console.log(`[createRgButton] Клик по кнопке "Перетестировать" для посылки ${subId}`);
const requestUrl = getContestUrl() + 'submission/' + subId;
const data = {action: "rejudge", submissionId: subId};
console.log(`[createRgButton] Отправляю POST запрос на перетестирование: ${requestUrl}`, data);
$.post(requestUrl, data, (response) => {
console.log(`[createRgButton] Перетестирование успешно запущено для посылки ${subId}, перезагружаю страницу`);
location.reload();
}).fail(function(error) {
console.error(`[createRgButton] Ошибка при перетестировании посылки ${subId}:`, error);
button.innerText = "Ошибка!";
});
button.innerText = "Тестирую...";
};
return button;
}
const rejectSub = (subId) => {
console.log(`[rejectSub] Начинаю отклонение посылки ${subId}`);
const subRjButton = getSubRjButton(subId);
subRjButton.innerText = "Отклоняю...";
subRjButton.prop("disabled", true);
const subAcButton = getSubAcButton(subId);
const commentSubAcButton = getCommentSubAcButton(subId);
const requestUrl = getContestUrl() + 'submission/' + subId
const data = {action: "reject", submissionId: subId}
console.log(`[rejectSub] Отправляю POST запрос на отклонение: ${requestUrl}`, data);
$.post(requestUrl, data, function (response) {
console.log(`[rejectSub] Посылка ${subId} успешно отклонена`);
$("[submissionid=" + subId + "] .verdict-accepted").remove()
subAcButton.remove()
subRjButton.remove()
commentSubAcButton.remove();
console.log(`[rejectSub] Удалены кнопки для посылки ${subId}`);
}).fail(function(error) {
console.error(`[rejectSub] Ошибка при отклонении посылки ${subId}:`, error);
subRjButton.innerText = "Ошибка!";
subRjButton.css("background-color", "#999");
});
}
const patchSubmissions = () => {
const subsId = getSubsId();
const languages = subsId.parent().prev();
// Перемещаем кнопку "Показать" под задачу
languages.each((i, languageCell) => {
const subId = Number($(subsId[i])[0].getAttribute('submissionid'));
const language = languageCell.textContent.split('(')[0].trim();
// Находим строку таблицы
const $row = $(languageCell).closest("tr");
const taskCell = $row.find("td:nth-child(4)"); // Задача - 4-я колонка
if (taskCell.length && !taskCell.hasClass("patched")) {
taskCell.addClass("patched");
// Добавляем выравнивание по центру для ячейки
taskCell.css({
textAlign: "center",
verticalAlign: "middle"
});
// Создаем контейнер для задачи и кнопки
const container = $("<div>").css({
display: "flex",
flexDirection: "column",
gap: "0.4em",
alignItems: "center",
justifyContent: "center",
width: "100%"
});
// Сохраняем оригинальное содержимое задачи
const taskContent = $("<div>").html(taskCell.html());
// Создаем кнопку
const showButton = createSubShowButton(subId, language);
// Собираем контейнер
container.append(taskContent, showButton);
// Заменяем содержимое ячейки задачи
taskCell.empty().append(container);
// Очищаем ячейку с языком и оставляем только текст языка
$(languageCell).empty().text(language);
}
});
}
const patchCorrectSubmissions = () => {
const correctSubs = getCorrectSubs();
// Стилизуем строки таблицы
correctSubs.closest("tr").each((i, row) => {
const $row = $(row);
// Стили для строки
$row.css({
transition: "background-color 0.2s ease"
});
// Hover эффект
$row.hover(
function() {
$(this).css("background-color", "#f8f9fa");
},
function() {
$(this).css("background-color", "");
}
);
// Стилизуем ячейки
$row.find("td").css({
padding: "0.75em 0.6em",
verticalAlign: "middle"
});
});
correctSubs.parent().append((i) => {
const subId = Number($(correctSubs[i]).attr('submissionid'))
const acButton = createAcButton(acButtonTemplate, subId)
const rgButton = createRgButton(subId);
const rjButton = createRjButton(subId, "RJ", (subId, _) => {
rejectSub(subId);
});
return [acButton, rjButton, rgButton]
})
}
const patchContestSidebar = () => {
const contestsSidebar = $(".GroupContestsSidebarFrame ul a");
// Добавляем номера соревнований
contestsSidebar.before((i) => {
const contestHref = $(contestsSidebar[i]).attr('href');
return document.createTextNode(wrap(getLast(contestHref)));
});
// Извлекаем все уникальные группы из названий соревнований
const groups = new Set();
contestsSidebar.each((i, element) => {
const text = $(element).text().trim();
const match = text.match(/\(([^)]+)\)$/); // Ищем текст в скобках в конце
if (match) {
groups.add(match[1]);
}
});
if (groups.size > 0) {
// Создаем фильтр для групп
const filterContainer = document.createElement("div");
filterContainer.style.marginBottom = "0.5em";
filterContainer.style.padding = "0.5em";
const filterLabel = document.createElement("div");
filterLabel.style.color = "#3B5998";
filterLabel.style.fontWeight = "bold";
filterLabel.style.marginBottom = "0.3em";
filterLabel.style.fontSize = "1.1rem";
filterLabel.innerText = "Группа:";
const filterSelect = document.createElement("select");
filterSelect.style.width = "100%";
filterSelect.style.padding = "0.2em";
filterSelect.style.fontSize = "1.0rem";
// Добавляем опцию "Все группы"
const allOption = document.createElement("option");
allOption.value = "all";
allOption.innerText = "Все группы";
filterSelect.appendChild(allOption);
// Добавляем опции для каждой группы
Array.from(groups).sort().forEach(group => {
const option = document.createElement("option");
option.value = group;
option.innerText = group;
filterSelect.appendChild(option);
});
// Загружаем сохраненный фильтр
const savedGroup = localStorage.getItem("selectedContestGroup") || "all";
// Проверяем, существует ли сохраненная группа
const groupExists = savedGroup === "all" || groups.has(savedGroup);
filterSelect.value = groupExists ? savedGroup : "all";
// Функция фильтрации
const filterContests = (selectedGroup) => {
localStorage.setItem("selectedContestGroup", selectedGroup);
contestsSidebar.each((i, element) => {
const listItem = $(element).parent();
const text = $(element).text().trim();
const match = text.match(/\(([^)]+)\)$/);
if (selectedGroup === "all") {
listItem.show();
} else {
if (match && match[1] === selectedGroup) {
listItem.show();
} else {
listItem.hide();
}
}
});
};
// Обработчик изменения фильтра
filterSelect.onchange = () => filterContests(filterSelect.value);
filterContainer.appendChild(filterLabel);
filterContainer.appendChild(filterSelect);
// Вставляем фильтр в конец сайдбара
$(".GroupContestsSidebarFrame").append(filterContainer);
// Применяем сохраненный фильтр
filterContests(groupExists ? savedGroup : "all");
}
}
const patchSubmission = () => {
const buttons = getSubButtons()
if (buttons.length > 0) {
const subId = Number(getLast(location.pathname));
const acButton = createAcButton(acButtonTemplate, subId);
buttons[0].before(acButton);
}
}
const patchFilterBox = () => {
const filterBox = getFilterBox();
const sidebar = getSideBar();
filterBox.detach().prependTo(sidebar);
const filterBoxPart = filterBox.find(".status-filter-form-part")[0];
// Создаем тоггл "Только непроверенные"
const correctSubsId = getCorrectSubs().map((i, e) => Number($(e).attr("submissionid"))).toArray();
const filter = (checkbox) => {
localStorage.setItem("filterPendingSubs", checkbox.checked);
const filtered = correctSubsId.filter(subId => {
return !isSubAccepted(subId)
});
getAllSubmissionsRow().each((i, e) => {
const $row = $(e);
const submissionId = $row.attr('data-submission-id');
// Пропускаем строки без data-submission-id (например, хедер)
if (!submissionId) {
return;
}
if (checkbox.checked) {
if (!filtered.includes(Number(submissionId))) {
$row.hide();
}
} else {
$row.show()
}
});
};
const template = createFilterPendingCheckboxTemplate(filter);
const toggleContainer = template[0]
const checkbox = template[1]
const updateToggleAppearance = template[2]
checkbox.checked = ('true' === localStorage.getItem("filterPendingSubs") ?? false);
updateToggleAppearance();
filter(checkbox);
filterBoxPart.before(toggleContainer);
// Получаем все задачи с названиями из оригинального селектора
const originalSelect = $(".status-filter-box select[name='frameProblemIndex']");
const problems = [];
if (originalSelect.length > 0) {
originalSelect.find("option").each((i, option) => {
const value = $(option).val();
const text = $(option).text().trim();
// Пропускаем "Любая задача"
if (value && text && value !== "anyProblem") {
// Убираем букву из начала текста (например, "A - Название" -> "Название")
const nameOnly = text.replace(/^[A-Z]\s*-\s*/, '');
problems.push({ letter: value, name: nameOnly });
}
});
}
// Создаем селектор задач
if (problems.length > 0) {
const problemSelector = createProblemSelector(problems);
filterBoxPart.before(problemSelector);
// Скрываем оригинальный селектор задач
originalSelect.parent().parent().hide();
}
// Создаем селектор вердикта
const verdictSelector = createVerdictSelector();
filterBoxPart.before(verdictSelector);
// Скрываем оригинальный селектор вердикта
$(".status-filter-box select[name='verdictName']").parent().parent().hide();
}
function createVerdictSelector() {
// Стили
const styles = {
container: {
marginBottom: "0.3em",
padding: "0.2em 0"
},
label: {
color: "#3B5998",
fontWeight: "bold",
marginBottom: "0.4em",
fontSize: "1.1rem"
},
buttonsContainer: {
display: "flex",
flexWrap: "wrap",
gap: "0.6em"
},
button: {
padding: "0.35em 0.8em",
border: "1px solid #d0d0d0",
borderRadius: "6px",
fontSize: "1.1rem",
cursor: "pointer",
transition: "all 0.2s ease",
fontWeight: "500",
whiteSpace: "nowrap"
},
buttonActive: {
color: "#ffffff"
},
buttonInactive: {
backgroundColor: "#ffffff",
color: "#333",
borderColor: "#d0d0d0"
},
buttonHover: {
backgroundColor: "#e8f0fe"
}
};
// Данные вердиктов
const verdicts = [
{ value: "anyVerdict", label: "Все", color: "#3B5998" },
{ value: "OK", label: "AC", color: "#13aa52" },
{ value: "WRONG_ANSWER", label: "WA", color: "#e74c3c" },
{ value: "TIME_LIMIT_EXCEEDED", label: "TL/ML", color: "#f39c12" },
{ value: "REJECTED", label: "RJ", color: "#95a5a6" },
];
// Вспомогательная функция для применения стилей
const applyStyles = (element, ...styleObjs) => {
styleObjs.forEach(styleObj => Object.assign(element.style, styleObj));
};
// Создание основных элементов
const container = document.createElement("div");
applyStyles(container, styles.container);
const label = document.createElement("div");
applyStyles(label, styles.label);
label.innerText = "Вердикт:";
const buttonsContainer = document.createElement("div");
applyStyles(buttonsContainer, styles.buttonsContainer);
// Получаем ссылки на элементы формы
const originalSelect = $(".status-filter-box select[name='verdictName']");
const submitButton = $(".status-filter-box input[type='submit']");
// Определяем текущий выбор
let selectedVerdict = localStorage.getItem("selectedVerdict") || originalSelect.val() || "anyVerdict";
// Функция обновления стилей кнопок
const updateButtonStyles = (value) => {
buttonsContainer.querySelectorAll("button").forEach(btn => {
const btnValue = btn.getAttribute("data-value");
const verdict = verdicts.find(v => v.value === btnValue);
if (btnValue === value) {
btn.style.backgroundColor = verdict.color;
btn.style.color = "#ffffff";
btn.style.borderColor = verdict.color;
} else {
applyStyles(btn, styles.buttonInactive);
}
});
};
// Логика фильтрации
const filterByVerdict = (value) => {
selectedVerdict = value;
localStorage.setItem("selectedVerdict", value);
updateButtonStyles(value);
// Применяем фильтр
if (originalSelect.length > 0) {
originalSelect.val(value);
const form = originalSelect.closest("form");
if (form.length > 0) {
form.submit();
} else if (submitButton.length > 0) {
submitButton.click();
}
}
};
// Создание кнопки
const createButton = (verdict) => {
const button = document.createElement("button");
button.setAttribute("data-value", verdict.value);
button.innerText = verdict.label;
// Применяем базовые стили
applyStyles(button, styles.button);
// Применяем стиль в зависимости от выбора
if (selectedVerdict === verdict.value) {
button.style.backgroundColor = verdict.color;
button.style.color = "#ffffff";
button.style.borderColor = verdict.color;
} else {
applyStyles(button, styles.buttonInactive);
}
// Обработчики событий
button.addEventListener("mouseenter", () => {
if (selectedVerdict !== verdict.value) {
applyStyles(button, styles.buttonHover);
}
});
button.addEventListener("mouseleave", () => {
if (selectedVerdict !== verdict.value) {
applyStyles(button, styles.buttonInactive);
}
});
button.onclick = () => filterByVerdict(verdict.value);
return button;
};
// Сборка DOM
verdicts.forEach(verdict => {
buttonsContainer.appendChild(createButton(verdict));
});
container.appendChild(label);
container.appendChild(buttonsContainer);
return container;
}
function createProblemSelector(problems) {
// Стили
const styles = {
container: {
marginBottom: "0.3em",
padding: "0.2em 0"
},
label: {
color: "#3B5998",
fontWeight: "bold",
marginBottom: "0.4em",
fontSize: "1.1rem"
},
buttonsContainer: {
display: "flex",
flexDirection: "column",
gap: "0.5em"
},
button: {
padding: "0.35em 0.9em",
border: "1px solid #d0d0d0",
borderRadius: "6px",
fontSize: "1rem",
cursor: "pointer",
transition: "all 0.2s ease",
fontWeight: "400",
textAlign: "left",
width: "100%"
},
buttonActive: {
backgroundColor: "#3B5998",
color: "#ffffff",
borderColor: "#3B5998"
},
buttonInactive: {
backgroundColor: "#ffffff",
color: "#333",
borderColor: "#d0d0d0"
},
buttonHover: {
backgroundColor: "#e8f0fe"
},
letterSpan: {
fontWeight: "bold"
}
};
// Вспомогательная функция для применения стилей
const applyStyles = (element, ...styleObjs) => {
styleObjs.forEach(styleObj => Object.assign(element.style, styleObj));
};
// Создание основных элементов
const container = document.createElement("div");
applyStyles(container, styles.container);
const label = document.createElement("div");
applyStyles(label, styles.label);
label.innerText = "Задача:";
const buttonsContainer = document.createElement("div");
applyStyles(buttonsContainer, styles.buttonsContainer);
// Получаем ссылки на элементы формы
const originalSelect = $(".status-filter-box select[name='frameProblemIndex']");
const submitButton = $(".status-filter-box input[type='submit']");
// Определяем текущий выбор
let selectedLetter = localStorage.getItem("selectedProblemLetter") || originalSelect.val() || "anyProblem";
// Функция обновления стилей кнопок
const updateButtonStyles = (letter) => {
buttonsContainer.querySelectorAll("button").forEach(btn => {
const btnLetter = btn.getAttribute("data-letter");
const letterSpan = btn.querySelector("span:first-child");
if (btnLetter === letter) {
applyStyles(btn, styles.buttonActive);
if (letterSpan) letterSpan.style.color = "#ffffff";
} else {
applyStyles(btn, styles.buttonInactive);
if (letterSpan) letterSpan.style.color = "#3B5998";
}
});
};
// Логика фильтрации
const filterByProblem = (letter) => {
selectedLetter = letter;
localStorage.setItem("selectedProblemLetter", letter);
updateButtonStyles(letter);
// Применяем фильтр
if (originalSelect.length > 0) {
originalSelect.val(letter);
const form = originalSelect.closest("form");
if (form.length > 0) {
form.submit();
} else if (submitButton.length > 0) {
submitButton.click();
}
}
};
// Создание кнопки
const createButton = (problem, isAll = false) => {
const button = document.createElement("button");
const letter = isAll ? "anyProblem" : problem.letter;
button.setAttribute("data-letter", letter);
if (isAll) {
button.innerText = "Все задачи";
} else {
const letterSpan = document.createElement("span");
applyStyles(letterSpan, styles.letterSpan);
letterSpan.style.color = selectedLetter === letter ? "#ffffff" : "#3B5998";
letterSpan.innerText = problem.letter;
const textSpan = document.createElement("span");
textSpan.innerText = " - " + problem.name;
button.appendChild(letterSpan);
button.appendChild(textSpan);
}
// Применяем базовые стили
applyStyles(button, styles.button);
// Применяем стиль в зависимости от выбора
if (selectedLetter === letter) {
applyStyles(button, styles.buttonActive);
} else {
applyStyles(button, styles.buttonInactive);
}
// Обработчики событий
button.addEventListener("mouseenter", () => {
if (selectedLetter !== letter) {
applyStyles(button, styles.buttonHover);
}
});
button.addEventListener("mouseleave", () => {
if (selectedLetter !== letter) {
applyStyles(button, styles.buttonInactive);
}
});
button.onclick = () => filterByProblem(letter);
return button;
};
// Сборка DOM
buttonsContainer.appendChild(createButton(null, true));
problems.forEach(problem => {
buttonsContainer.appendChild(createButton(problem));
});
container.appendChild(label);
container.appendChild(buttonsContainer);
return container;
}
function createFilterPendingCheckboxTemplate(action) {
// Стили
const styles = {
container: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0.3em 0",
marginBottom: "0.3em"
},
title: {
color: "#333",
fontSize: "0.95rem",
fontWeight: "400"
},
checkbox: {
display: "none"
},
toggleLabel: {
position: "relative",
display: "inline-block",
width: "36px",
height: "20px",
cursor: "pointer"
},
toggleBackground: {
position: "absolute",
top: "0",
left: "0",
right: "0",
bottom: "0",
borderRadius: "20px",
transition: "background-color 0.3s ease"
},
toggleSlider: {
position: "absolute",
top: "2px",
left: "2px",
width: "16px",
height: "16px",
backgroundColor: "#fff",
borderRadius: "50%",
transition: "transform 0.3s ease",
boxShadow: "0 1px 3px rgba(0,0,0,0.2)"
}
};
// Вспомогательная функция для применения стилей
const applyStyles = (element, styleObj) => {
Object.assign(element.style, styleObj);
};
// Создание элементов
const container = document.createElement("div");
applyStyles(container, styles.container);
const title = document.createElement("span");
applyStyles(title, styles.title);
title.innerText = "Только непроверенные";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = "pending-filter-toggle";
applyStyles(checkbox, styles.checkbox);
const toggleLabel = document.createElement("label");
toggleLabel.htmlFor = "pending-filter-toggle";
applyStyles(toggleLabel, styles.toggleLabel);
const toggleBackground = document.createElement("span");
applyStyles(toggleBackground, styles.toggleBackground);
toggleBackground.style.backgroundColor = "#ccc";
const toggleSlider = document.createElement("span");
applyStyles(toggleSlider, styles.toggleSlider);
// Сборка DOM
toggleBackground.appendChild(toggleSlider);
toggleLabel.appendChild(toggleBackground);
container.appendChild(title);
container.appendChild(checkbox);
container.appendChild(toggleLabel);
// Логика обновления состояния
const updateToggleAppearance = () => {
if (checkbox.checked) {
toggleBackground.style.backgroundColor = "#3B5998";
toggleSlider.style.transform = "translateX(16px)";
} else {
toggleBackground.style.backgroundColor = "#ccc";
toggleSlider.style.transform = "translateX(0)";
}
};
// Обработчик события
checkbox.onclick = () => {
updateToggleAppearance();
action(checkbox);
};
return [container, checkbox, updateToggleAppearance];
}
// Создание плавающих окон для комментариев (по одному на каждую посылку)
const floatingCommentPanels = {}; // { subId: panel }
let positionUpdateAnimationFrame = null;
const panelTargetPositions = {}; // { subId: { top: number } } - целевые позиции для плавного движения
const createFloatingCommentPanel = (subId) => {
// Если окно уже существует, убеждаемся что оно в DOM и возвращаем его
if (floatingCommentPanels[subId]) {
const existingPanel = floatingCommentPanels[subId];
// Проверяем, что панель все еще в DOM
if (document.body.contains(existingPanel)) {
return existingPanel;
} else {
// Если панель была удалена из DOM, удаляем из словаря и создаем новую
delete floatingCommentPanels[subId];
}
}
const panel = document.createElement("div");
panel.id = "floating-comment-panel-" + subId;
panel.setAttribute("data-submission-id", subId);
// Стили панели (изначальные, будут обновлены при показе)
Object.assign(panel.style, {
position: "fixed",
left: "20px",
top: "50%",
transform: "translateY(-50%)",
width: "320px",
maxHeight: "80vh",
backgroundColor: "#ffffff",
border: "2px solid #3B5998",
borderRadius: "8px",
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
zIndex: "10000",
display: "none",
flexDirection: "column",
overflow: "hidden"
});
// Заголовок панели
const header = document.createElement("div");
Object.assign(header.style, {
backgroundColor: "#3B5998",
color: "#ffffff",
padding: "0.8em 1em",
fontWeight: "600",
fontSize: "1.1rem",
display: "flex",
justifyContent: "space-between",
alignItems: "center"
});
const title = document.createElement("span");
title.id = "floating-panel-title-" + subId;
title.textContent = "Комментарий к посылке";
const closeBtn = document.createElement("button");
closeBtn.textContent = "×";
closeBtn.style.cssText = "background: none; border: none; color: #fff; font-size: 1.5rem; cursor: pointer; padding: 0; width: 24px; height: 24px; line-height: 1;";
closeBtn.onclick = () => hideFloatingPanel(subId);
header.appendChild(title);
header.appendChild(closeBtn);
// Контейнер для комментариев
const commentContainer = document.createElement("div");
commentContainer.id = "floating-comment-container-" + subId;
Object.assign(commentContainer.style, {
padding: "1em",
display: "flex",
flexDirection: "column",
gap: "0.8em",
flex: "1",
minHeight: "0"
});
// Заголовок секции комментариев
const commentTitle = document.createElement("div");
commentTitle.textContent = "→ Комментарий";
Object.assign(commentTitle.style, {
color: "#3B5998",
fontWeight: "600",
fontSize: "1.1rem",
marginBottom: "0.3em",
flexShrink: "0"
});
// Контейнер для textarea и кнопок, выровненный по нижнему краю
const inputButtonsWrapper = document.createElement("div");
Object.assign(inputButtonsWrapper.style, {
display: "flex",
flexDirection: "column",
gap: "0.8em",
marginTop: "auto",
flexShrink: "0",
width: "100%", // Обеспечиваем полную ширину контейнера
boxSizing: "border-box"
});
// Текстовое поле для комментария
const commentTextfield = createCommentTextfield();
commentTextfield.id = "floating-comment-textfield-" + subId;
// Переопределяем стили для правильного выравнивания с кнопками
Object.assign(commentTextfield.style, {
width: "100%",
height: "80px",
fontSize: "0.95rem",
margin: "0", // Убираем margin для правильного выравнивания
padding: "0.6em",
boxSizing: "border-box", // Учитываем padding в ширине
outline: "none"
});
commentTextfield.setAttribute("tabindex", "0");
// Контейнер для кнопок
const buttonsContainer = document.createElement("div");
buttonsContainer.id = "floating-comment-buttons-container-" + subId;
Object.assign(buttonsContainer.style, {
display: "flex",
gap: "0.5em",
justifyContent: "space-between",
width: "100%", // Обеспечиваем одинаковую ширину с textarea
boxSizing: "border-box"
});
// Добавляем textarea и кнопки в обёртку
inputButtonsWrapper.appendChild(commentTextfield);
inputButtonsWrapper.appendChild(buttonsContainer);
commentContainer.appendChild(commentTitle);
commentContainer.appendChild(inputButtonsWrapper);
panel.appendChild(header);
panel.appendChild(commentContainer);
document.body.appendChild(panel);
floatingCommentPanels[subId] = panel;
// Запускаем обновление позиций через requestAnimationFrame для максимальной плавности
if (!positionUpdateAnimationFrame) {
const updateLoop = () => {
updateAllPanelPositions();
positionUpdateAnimationFrame = requestAnimationFrame(updateLoop);
};
positionUpdateAnimationFrame = requestAnimationFrame(updateLoop);
}
return panel;
};
// Обновление позиций всех открытых панелей
const updateAllPanelPositions = () => {
Object.keys(floatingCommentPanels).forEach(subId => {
const panel = floatingCommentPanels[subId];
if (panel && panel.style.display !== "none") {
updatePanelPosition(Number(subId));
}
});
};
// Обновление позиции конкретной панели относительно её codeSection
const updatePanelPosition = (subId) => {
const panel = floatingCommentPanels[subId];
if (!panel) return;
const codeData = showed_codes[subId];
if (!codeData || !codeData.codeSection) {
// Если кода нет, скрываем панель
if (panel.style.display !== "none") {
panel.style.display = "none";
}
return;
}
const codeSection = $(codeData.codeSection);
if (!codeSection.length || !codeSection.is(":visible")) {
panel.style.display = "none";
return;
}
// Получаем позицию блока кода относительно viewport
const codeRect = codeSection[0].getBoundingClientRect();
const panelWidth = 320;
const margin = 20;
// Позиционируем панель слева от блока кода (с учетом скролла)
const left = codeRect.left - panelWidth - margin;
// Если панель не помещается слева, размещаем справа
if (left < 20) {
panel.style.left = (codeRect.left + codeRect.width + margin) + "px";
} else {
panel.style.left = left + "px";
}
// Получаем текущую позицию панели
const currentTop = parseFloat(panel.style.top) || (codeRect.top + codeRect.height / 2 - 150);
// Позиционируем по вертикали с логикой следования за границами
const codeTop = codeRect.top;
const codeHeight = codeRect.height;
const codeBottom = codeTop + codeHeight;
const panelHeight = panel.offsetHeight || 300;
const viewportCenter = window.innerHeight / 2;
// Целевая позиция - центр экрана
let targetTop = viewportCenter - (panelHeight / 2);
// Определяем, касается ли окно границ блока кода (с учетом движения)
const panelTop = currentTop;
const panelBottom = currentTop + panelHeight;
// Проверяем касание с учетом направления движения
const tolerance = 3; // Допуск в 3px
const touchesTop = Math.abs(panelTop - codeTop) < tolerance;
const touchesBottom = Math.abs(panelBottom - codeBottom) < tolerance;
// Определяем, стремится ли окно к границе (движется ли в сторону границы)
const movingTowardTop = (targetTop - currentTop) < 0 && targetTop <= codeTop;
const movingTowardBottom = (targetTop - currentTop) > 0 && targetTop + panelHeight >= codeBottom;
// Если окно касается верхней границы или движется к ней - привязываемся к верху
if (touchesTop || (movingTowardTop && panelTop <= codeTop + tolerance)) {
targetTop = codeTop;
}
// Если окно касается нижней границы или движется к ней - привязываемся к низу
else if (touchesBottom || (movingTowardBottom && panelBottom >= codeBottom - tolerance)) {
targetTop = codeBottom - panelHeight;
}
// Иначе стремимся к центру экрана, но с ограничениями
else {
// Ограничиваем: верх окна не может быть выше верха блока кода
if (targetTop < codeTop) {
targetTop = codeTop;
}
// Ограничиваем: низ окна не может быть ниже низа блока кода
if (targetTop + panelHeight > codeBottom) {
targetTop = codeBottom - panelHeight;
}
}
// Плавная интерполяция к целевой позиции (коэффициент 0.15 для плавности)
const smoothFactor = 0.15;
const newTop = currentTop + (targetTop - currentTop) * smoothFactor;
// Сохраняем целевую позицию для следующего кадра
panelTargetPositions[subId] = { top: targetTop };
// Используем fixed позиционирование с координатами относительно viewport
panel.style.top = newTop + "px";
panel.style.transform = "none";
};
const showFloatingPanel = (subId) => {
console.log(`[showFloatingPanel] Показываю плавающее окно для посылки ${subId}`);
// Создаем или получаем панель для этой посылки
const panel = createFloatingCommentPanel(subId);
const title = panel.querySelector("#floating-panel-title-" + subId);
const commentContainer = panel.querySelector("#floating-comment-container-" + subId);
const commentTextfield = panel.querySelector("#floating-comment-textfield-" + subId);
const buttonsContainer = panel.querySelector("#floating-comment-buttons-container-" + subId);
// Очищаем поле комментария
if (commentTextfield) {
commentTextfield.value = "";
}
// Обновляем заголовок
const handle = getHandle(subId);
const problem = getProblemIndex(subId);
if (title) {
title.textContent = `#${subId} - ${handle} (${problem})`;
}
// Очищаем старые кнопки и проверяем наличие контейнера
if (!buttonsContainer) {
console.error(`[showFloatingPanel] Контейнер кнопок не найден для посылки ${subId}!`);
return;
}
buttonsContainer.innerHTML = "";
// Создаем кнопки для этой посылки
const subAcButton = getSubAcButton(subId)[0];
const isAccepted = isSubAccepted(subId);
const submissionType = isAccepted ? "star" : "accept";
console.log(`[showFloatingPanel] Посылка ${subId}: handle=${handle}, problem=${problem}, isAccepted=${isAccepted}, type=${submissionType}`);
const commentAcButton = commentSendButtonTemplate(subId,
(isAccepted ? "Похвалить" : "Принять") + " с комментарием",
(isAccepted ? "#81D718" : "#13aa52"),
(subId, button) => {
const text = $(commentTextfield).val();
console.log(`[showFloatingPanel] Клик по кнопке "Принять с комментарием" для посылки ${subId}, текст комментария: "${text}"`);
// Блокируем кнопки выше кода при нажатии
disableTopButtons(subId);
if (text.length === 0) {
console.log(`[showFloatingPanel] Принимаю посылку ${subId} без комментария`);
button.innerText = "Принимаю...";
button.disabled = true;
// Не закрываем форму сразу - ждем завершения acceptSubmission
// Вызываем acceptSheetSubmission с колбэками для закрытия панели
acceptSheetSubmission(subId, subAcButton, submissionType,
() => {
// Успешное выполнение - закрываем панель
console.log(`[showFloatingPanel] acceptSheetSubmission успешно завершен для посылки ${subId}, закрываем панель`);
button.innerText = "Принято!";
hideFloatingPanel(subId);
},
() => {
// Ошибка - разблокируем кнопки
console.log(`[showFloatingPanel] acceptSheetSubmission завершился с ошибкой для посылки ${subId}`);
enableTopButtons(subId);
button.innerText = "Ошибка!";
button.disabled = false;
button.style.backgroundColor = "#999";
}
);
} else {
console.log(`[showFloatingPanel] Отправляю комментарий для посылки ${subId}`);
button.innerText = "Отправляю...";
button.disabled = true;
const name = getHandle(subId);
const problemIndex = getProblemIndex(subId);
const postUrl = getGroupUrl() + 'data/newAnnouncement';
const postData = {
contestId: getContestId(),
englishText: "",
russianText: text,
submittedProblemIndex: problemIndex,
targetUserHandle: name,
announceInPairContest: true,
};
console.log(`[showFloatingPanel] POST запрос на отправку комментария: ${postUrl}`, postData);
$.post(postUrl, postData, () => {
console.log(`[showFloatingPanel] Комментарий успешно отправлен для посылки ${subId}`);
button.innerText = "Отправлено!";
acceptSubmission(subId, subAcButton);
// Закрываем форму только после успешной отправки
hideFloatingPanel(subId);
}).fail(function(error) {
console.error(`[showFloatingPanel] Ошибка при отправке комментария для посылки ${subId}:`, error);
button.innerText = "Ошибка!";
button.disabled = false;
button.style.backgroundColor = "#999";
// Разблокируем кнопки выше кода при ошибке
enableTopButtons(subId);
});
}
}
);
const commentRjButton = commentSendButtonTemplate(subId, "Отклонить с комментарием", "#EC431A", (subId, button) => {
const text = $(commentTextfield).val();
if (text.length > 0) {
console.log(`[showFloatingPanel] Клик по кнопке "Отклонить с комментарием" для посылки ${subId}, текст комментария: "${text}"`);
// Блокируем кнопки выше кода при нажатии
disableTopButtons(subId);
button.innerText = "Отклоняю...";
button.disabled = true;
const name = getHandle(subId);
const problem = getProblemIndex(subId);
const postUrl = getGroupUrl() + 'data/newAnnouncement';
const postData = {
contestId: getContestId(),
englishText: "",
russianText: text,
submittedProblemIndex: getProblemIndex(subId),
targetUserHandle: name,
announceInPairContest: true,
};
console.log(`[showFloatingPanel] POST запрос на отправку комментария при отклонении: ${postUrl}`, postData);
$.post(postUrl, postData, () => {
console.log(`[showFloatingPanel] Комментарий отправлен, начинаю отклонение посылки ${subId}`);
rejectSub(subId);
// Не закрываем панель сразу - ждем завершения отправки в Sheet
const params = new URLSearchParams({
type: "rj",
name: name,
comment: "Пришел новый реджект! \n\n Комментарий к посылке по задаче " + problem + ":\n\n" + text,
});
const full_url = SHEET_URL + "?" + params;
console.log(`[showFloatingPanel] Отправляю данные в Google Sheets для реджекта: ${full_url}`);
GM_xmlhttpRequest({
method: 'GET',
url: full_url,
timeout: 5000,
onload: (response) => {
console.log(`[showFloatingPanel] Ответ от Google Sheets для реджекта, status: ${response.status}`);
if (response.status === 200) {
// Закрываем панель только после успешной отправки
console.log(`[showFloatingPanel] Реджект успешно отправлен в Google Sheets для посылки ${subId}`);
hideFloatingPanel(subId);
button.innerText = "Отклонено";
} else {
console.error(`[showFloatingPanel] Ошибка отправки реджекта в Google Sheets: status=${response.status}`);
button.innerText = "Ошибка!";
button.disabled = false;
button.style.backgroundColor = "#999";
// Разблокируем кнопки выше кода при ошибке
enableTopButtons(subId);
console.error(response);
}
},
onerror: (error) => {
console.error(`[showFloatingPanel] Ошибка сети при отправке реджекта в Google Sheets:`, error);
button.innerText = "Ошибка!";
button.disabled = false;
button.style.backgroundColor = "#999";
// Разблокируем кнопки выше кода при ошибке
enableTopButtons(subId);
}
});
}).fail(function(error) {
console.error(`[showFloatingPanel] Ошибка при отправке комментария при отклонении для посылки ${subId}:`, error);
button.innerText = "Ошибка!";
button.disabled = false;
button.style.backgroundColor = "#999";
// Разблокируем кнопки выше кода при ошибке
enableTopButtons(subId);
});
}
});
// Настраиваем стили кнопок для панели
commentAcButton.style.width = "48%";
commentRjButton.style.width = "48%";
// Добавляем обработчик Enter для комментария
if (commentTextfield) {
commentTextfield.onkeyup = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
commentRjButton.click();
}
};
}
buttonsContainer.appendChild(commentAcButton);
buttonsContainer.appendChild(commentRjButton);
// Показываем панель
panel.style.display = "flex";
// Обновляем позицию относительно блока кода сразу и через небольшой таймаут
updatePanelPosition(subId);
requestAnimationFrame(() => {
updatePanelPosition(subId);
});
// Также обновляем позицию при изменении содержимого панели
setTimeout(() => {
updatePanelPosition(subId);
}, 100);
};
const hideFloatingPanel = (subId) => {
console.log(`[hideFloatingPanel] Скрываю плавающее окно для посылки ${subId}`);
const panel = floatingCommentPanels[subId];
if (panel) {
panel.style.display = "none";
// Отключаем ResizeObserver, если он был создан
if (panel._resizeObserver) {
panel._resizeObserver.disconnect();
panel._resizeObserver = null;
}
// Удаляем панель из словаря
delete floatingCommentPanels[subId];
// Удаляем панель из DOM
panel.remove();
// Останавливаем анимацию обновления, если больше нет открытых панелей
const remainingPanels = Object.keys(floatingCommentPanels).length;
if (remainingPanels === 0 && positionUpdateAnimationFrame) {
console.log(`[hideFloatingPanel] Останавливаю анимацию обновления позиций (нет открытых панелей)`);
cancelAnimationFrame(positionUpdateAnimationFrame);
positionUpdateAnimationFrame = null;
} else {
console.log(`[hideFloatingPanel] Осталось открытых панелей: ${remainingPanels}`);
}
// Удаляем целевую позицию
delete panelTargetPositions[subId];
console.log(`[hideFloatingPanel] Плавающее окно успешно скрыто для посылки ${subId}`);
} else {
console.warn(`[hideFloatingPanel] Панель для посылки ${subId} не найдена`);
}
};
// Стилизация таблицы посылок
const styleSubmissionsTable = () => {
const table = $(".status-frame-datatable");
// Стили для таблицы
table.css({
borderCollapse: "separate",
borderSpacing: "0",
width: "100%"
});
// Находим и скрываем заголовки (они в tbody как tr.first-row)
table.find("tr.first-row th").each(function() {
const headerText = $(this).text().trim();
if (headerText === "Когда") {
$(this).hide();
}
if (headerText === "Язык") {
$(this).hide();
}
if (headerText === "Время") {
$(this).attr("colspan", "2");
$(this).text("Ресурсы");
}
if (headerText === "Память") {
$(this).hide();
}
});
// Стилизуем заголовки таблицы
table.find("tr.first-row th:visible").css({
backgroundColor: "#f5f5f5",
color: "#333",
fontWeight: "600",
fontSize: "1.24rem",
padding: "0.85em 0.6em",
borderBottom: "2px solid #d0d0d0",
textAlign: "center"
});
// Стилизуем все строки (пропускаем строку с заголовками)
table.find("tbody tr").not(".first-row").each(function() {
const $row = $(this);
// Добавляем тонкую границу между строками
$row.find("td").css({
borderBottom: "1px solid #f0f0f0",
fontSize: "1.24rem"
});
// Стилизуем вердикт "Полное решение"
$row.find(".verdict-accepted").css({
fontWeight: "600",
color: "#13aa52",
fontSize: "1.3rem"
});
// Обрабатываем все ячейки строки
const allCells = $row.find("td");
// Размещаем время посылки под номером (первая ячейка)
const submissionCell = allCells.eq(0); // № - первая ячейка
const whenCell = allCells.eq(1); // Когда - вторая ячейка
const langCell = allCells.eq(4); // Язык - пятая ячейка
// Обрабатываем номер и время
if (submissionCell.length && whenCell.length && !submissionCell.hasClass("number-patched")) {
submissionCell.addClass("number-patched");
// Добавляем выравнивание по центру для ячейки
submissionCell.css({
textAlign: "center",
verticalAlign: "middle"
});
const submissionLink = submissionCell.find("a"); // Ссылка с номером посылки
const timeText = whenCell.text().trim();
if (submissionLink.length && timeText) {
// Создаем контейнер для номера и времени
const container = $("<div>").css({
display: "flex",
flexDirection: "column",
gap: "0.3em",
alignItems: "center",
justifyContent: "center"
});
// Стилизуем номер посылки
const linkWrapper = $("<div>").css({
fontWeight: "500",
color: "#3B5998",
fontSize: "1.24rem"
});
linkWrapper.append(submissionLink.clone());
// Создаем чипс для времени
const timeChip = $("<div>").css({
backgroundColor: "#f5f5f5",
color: "#666",
padding: "0.2em 0.5em",
borderRadius: "10px",
fontSize: "0.85rem",
fontWeight: "500",
whiteSpace: "nowrap"
}).text(timeText);
container.append(linkWrapper, timeChip);
// Заменяем содержимое ячейки с номером
submissionCell.empty().append(container);
}
}
// Скрываем ячейку "Когда"
if (whenCell.length) {
whenCell.hide();
}
// Скрываем ячейку "Язык"
if (langCell.length) {
langCell.hide();
}
// Объединяем время и память в чипсы "Ресурсы"
// Находим последние две ячейки (время и память)
const timeCell = allCells.eq(-2); // Предпоследняя ячейка - время
const memoryCell = allCells.eq(-1); // Последняя ячейка - память
if (timeCell.length && memoryCell.length &&
!timeCell.hasClass("merged") && !memoryCell.hasClass("merged")) {
const timeText = timeCell.text().trim();
const memoryText = memoryCell.text().trim();
// Создаем контейнер для чипсов ресурсов
const chipsContainer = $("<div>").css({
display: "flex",
flexDirection: "column",
gap: "0.4em",
justifyContent: "center",
alignItems: "center"
});
// Создаем чипс для времени
const timeChip = $("<span>").css({
backgroundColor: "#e3f2fd",
color: "#1976d2",
padding: "0.3em 0.7em",
borderRadius: "12px",
fontSize: "0.95rem",
fontWeight: "500",
fontFamily: "monospace",
whiteSpace: "nowrap"
}).text(timeText);
// Создаем чипс для памяти
const memoryChip = $("<span>").css({
backgroundColor: "#f3e5f5",
color: "#7b1fa2",
padding: "0.3em 0.7em",
borderRadius: "12px",
fontSize: "0.95rem",
fontWeight: "500",
fontFamily: "monospace",
whiteSpace: "nowrap"
}).text(memoryText);
chipsContainer.append(timeChip, memoryChip);
// Объединяем ячейки
timeCell.attr("colspan", "2");
timeCell.empty().append(chipsContainer);
timeCell.addClass("merged");
memoryCell.hide();
memoryCell.addClass("merged");
}
});
// Добавляем визуальный эффект при наведении на строки
table.find("tbody tr").not(".first-row").each(function() {
const $row = $(this);
if (!$row.hasClass("hover-enabled")) {
$row.addClass("hover-enabled");
$row.css("cursor", "pointer");
$row.on("mouseenter", function() {
$(this).css("backgroundColor", "#f8f9fa");
});
$row.on("mouseleave", function() {
$(this).css("backgroundColor", "");
});
}
});
};
(function () {
getSheetSubmissions();
try {
patchContestSidebar();
} catch (e) {
console.error(e);
}
try {
patchFilterBox();
} catch (e) {
console.error(e);
}
try {
patchCorrectSubmissions();
} catch (e) {
console.error(e);
}
try {
patchSubmissions();
} catch (e) {
console.error(e);
}
try {
patchSubmission();
} catch (e) {
console.error(e);
}
try {
styleSubmissionsTable();
} catch (e) {
console.error(e);
}
// Добавляем обработчик Escape для закрытия всех панелей
try {
$(document).on("keydown", function(e) {
if (e.key === "Escape") {
// Закрываем все открытые панели
Object.keys(floatingCommentPanels).forEach(subId => {
const panel = floatingCommentPanels[subId];
if (panel && panel.style.display !== "none") {
hideFloatingPanel(Number(subId));
}
});
}
});
// Добавляем обработчик скролла для обновления позиций панелей
let scrollTimeout = null;
$(window).on("scroll resize", function() {
// Используем throttling для оптимизации
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
scrollTimeout = setTimeout(() => {
updateAllPanelPositions();
}, 10);
});
// Также обновляем при изменении размера документа (например, при изменении размера таблицы)
const mutationObserver = new MutationObserver(() => {
updateAllPanelPositions();
});
// Наблюдаем за изменениями в таблице
const table = $(".status-frame-datatable");
if (table.length) {
mutationObserver.observe(table[0], {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class']
});
}
} catch (e) {
console.error("Ошибка при инициализации плавающих окон:", e);
}
})();