avtopost.kultmost.ru/templates/base.html
2026-08-02 12:08:08 -04:00

900 lines
41 KiB
HTML
Raw Permalink 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.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://avtopost.kultmost.ru">
<link rel="dns-prefetch" href="https://avtopost.kultmost.ru">
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/ico" href="/static/favicon.ico">
<link rel="icon" type="image/ico" sizes="256x256" href="/static/favicon.ico">
<link rel="apple-touch-icon" type="image/ico" href="/static/favicon.ico">
<meta name="theme-color" content="#5F1798"/>
<title>{% block title %}{% endblock %}</title>
{% block head %}{% endblock %}
<style>
{% raw %}
h1,h2,h3,p {
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
hyphens: auto;
}
html{display: flex;justify-content: center;}
input, button {touch-action: manipulation;}
picture{cursor: zoom-in;}
*{margin:unset;box-sizing:border-box;position:relative;transition:300ms}button,input{all:unset}button,a{cursor:pointer}input{cursor:auto}button>span{text-wrap:nowrap}@font-face{font-family:"Inter";font-style:normal;font-weight:100;font-display:swap;src:url(/static/Inter-Thin.woff2) format("woff2")}@font-face{font-family:"Inter";font-style:normal;font-weight:200;font-display:swap;src:url(/static/Inter-ExtraLight.woff2) format("woff2")}@font-face{font-family:"Inter";font-style:normal;font-weight:300;font-display:swap;src:url(/static/Inter-Light.woff2) format("woff2")}@font-face{font-family:"Inter";font-style:normal;font-weight:400;font-display:swap;src:url(/static/Inter-Regular.woff2) format("woff2")}@font-face{font-family:"Inter";font-style:normal;font-weight:500;font-display:swap;src:url(/static/Inter-Medium.woff2) format("woff2")}@font-face{font-family:"Inter";font-style:normal;font-weight:600;font-display:swap;src:url(/static/Inter-SemiBold.woff2) format("woff2")}@font-face{font-family:"Inter";font-style:normal;font-weight:700;font-display:swap;src:url(/static/Inter-Bold.woff2) format("woff2")}@font-face{font-family:"Inter";font-style:normal;font-weight:800;font-display:swap;src:url(/static/Inter-ExtraBold.woff2) format("woff2")}@font-face{font-family:"Inter";font-style:normal;font-weight:900;font-display:swap;src:url(/static/Inter-Black.woff2) format("woff2")}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(/static/fonts/KFOmCnqEu92Fr1Mu72xKOzY.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(/static/fonts/KFOmCnqEu92Fr1Mu5mxKOzY.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(/static/fonts/KFOmCnqEu92Fr1Mu7GxKOzY.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(/static/fonts/KFOmCnqEu92Fr1Mu4mxK.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmEU9fBBc4.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmWUlfCRc4EsA.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmWUlfABc4EsA.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmWUlfChc4EsA.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmWUlfBBc4.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmYUtfCRc4EsA.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmYUtfABc4EsA.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmYUtfChc4EsA.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(/static/fonts/KFOlCnqEu92Fr1MmYUtfBBc4.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}::-webkit-scrollbar{height:5px}::-webkit-scrollbar-track{background:#0000}::-webkit-scrollbar-thumb{background:#BDBDBD}::-webkit-scrollbar-thumb:hover{background:#323d5b;border-radius:10px}body{scrollbar-width:none;max-width:1600px;display:grid;justify-content:center}body::-webkit-scrollbar{display:none}:root{font-family:Inter,sans-serif;font-feature-settings:"liga" 1,"calt" 1}a{text-decoration:none}*,::before,::after{box-sizing:border-box}.modal{transition:none;position:fixed;z-index:1000;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:#000c;display:flex;justify-content:center;align-items:center}.close{position:fixed;top:20px;right:30px;color:#fff;font-size:40px;cursor:pointer}#modalLink{transition:none;display:flex;position:fixed;bottom:13px;justify-content:center;width:335px;border-radius:1em;height:38px;gap:5px;align-items:center;background:#5F1798}
{% endraw %}
</style>
<style>
{% raw %}
.toast-container {position: fixed;top: 16px;right: 16px;display: flex;flex-direction: column;gap: 10px;z-index: 9999;pointer-events: none;}.toast {pointer-events: auto;width: 365px;max-width: calc(100vw - 40px);background: linear-gradient(90deg,#5f1798,#652895);color: #fff;border-radius: 10px;box-shadow: 0 8px 24px rgba(0,0,0,0.18);overflow: hidden;transform: translateX(110%);opacity: 0;transition: transform 360ms cubic-bezier(.2,.9,.3,1), opacity 260ms;display: flex;flex-direction: column;}.toast.show {transform: translateX(0);opacity: 1;}.toast .accent {height: 6px;width: 100%;}.toast.info .accent { background: linear-gradient(90deg,#0066ff,#3aa0ff); }.toast.error .accent { background: linear-gradient(90deg,#ff3b30,#ff7b7b); }.toast-header {padding: 10px 12px;font-size:18px;font-weight: 500;display:flex;align-items:center;justify-content:space-between;gap:8px;}.toast-body {padding: 10px 12px 14px;font-size:14px;}.toast-actions {display:flex;gap:8px;}.toast-close {background: transparent;border: none;font-weight:700;cursor:pointer;color: #666;padding:4px;border-radius:6px;}.toast-close:active { transform: scale(.98); }
{% endraw %}
</style>
<style>
{%raw%}
.offer-label{display:flex;align-items:center;gap:12px;cursor:pointer;user-select:none;font-size:15px;color:#333}.offer-checkbox{appearance:none;-webkit-appearance:none;width:22px;height:22px;border:2px solid #ccc;border-radius:6px;background-color:#fff;margin:0;flex-shrink:0;transition:all .25s ease;cursor:pointer;box-shadow:0 1px 3px #00000014}.offer-checkbox:hover{background-color:#5e17988f;box-shadow:0 0 0 3px #5F179826}.offer-checkbox:checked{background-color:#5F1798;box-shadow:0 0 0 3px #5F179826}.offer-checkbox:checked::after{content:'';display:block;width:100%;height:100%;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M20.285 6.293a1 1 0 00-1.414 0L9 16.172 5.129 12.3a1 1 0 00-1.414 1.414l4.95 4.95a1 1 0 001.414 0l10.586-10.586a1 1 0 000-1.414z'/%3E%3C/svg%3E");background-size:14px;background-position:center;background-repeat:no-repeat}.offer-label:hover .offer-checkbox{border-color:#5F1798}.offer-checkbox:disabled{background-color:#f5f5f5;border-color:#ddd;cursor:not-allowed}.dd-modal-backdrop{position:fixed;inset:0;background:#0006;display:flex;align-items:center;justify-content:center;z-index:1999}.dd-modal{background:#fff;padding:16px;border-radius:8px;min-width:320px;max-width:90%;box-shadow:0 6px 24px #0003}.dd-modal .dd-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.dd-modal .dd-body{max-height:50vh;overflow:auto;margin-bottom:12px}.dd-modal .dd-actions{text-align:right}.dd-actions > button{border-radius:5px;background:#5F1798;color:#fff;padding:8px 15px}.dd-modal button{cursor:pointer}.dd-close{background:transparent;border:0;font-size:18px}.dd-option{display:flex;align-items:center;gap:8px;padding:6px 4px;border-radius:4px}.dd-option:hover{background:#f4f4f4}
{% endraw %}
</style>
<style>
.b {
cursor: pointer;
transition: transform 0.1s linear, box‑shadow 0.1s linear;
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
}
.b:active {
transform: scale(0.9);
box-shadow: 5px 5px 5px rgba(95, 23, 152, 0.32);
}
.some-class{ display: flex;
height: 24px;
gap: 7px;
align-items: center;}
#modalToolTip {
display: none;z-index: 2000;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
align-items: center;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
border-radius: 10px;
overflow: auto;
max-height: 98%;
}
.close {
color: white;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: rgb(211, 211, 211);
text-decoration: none;
cursor: pointer;
}
.dd-option>span{display: flex;
justify-content: space-between;align-items: center;
width: 100%;}
</style>
</head>
<body {% if request.cookies.zoom%}style="zoom: {{ request.cookies.zoom }}%"{%endif%} onload="adjustBodyStyle()">
<script>
function setSecureCookie(name, value, days = 7) {const date = new Date();date.setTime(date.getTime() + (days * 86400000));document.cookie = `${name}=${encodeURIComponent(value)}; expires=${date.toUTCString()}; path=/; SameSite=Strict; Secure`;}
var userAgent = navigator.userAgent;var isFirefox = userAgent.indexOf("Firefox") !== -1;function isOldFirefox() {var match = userAgent.match(/Firefox\/(\d+)/);return match && parseInt(match[1], 10) < 126;};function iOS() {return ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes(navigator.platform) || (userAgent.includes("Mac") && "ontouchend" in document);};function adjustBodyStyle() {var body = document.body;var width;if (iOS()) {width = screen.width;} else {width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || 360;};const zoom = (width < 700) ? width * 100 / 400 : (width < 900) ? width * 100 / 800 : width * 100 / 1600;if (isFirefox && isOldFirefox()) {body.style.MozTransform = `scale(${zoom}%)`;body.style.MozTransformOrigin = '0% 0%';body.style.width = (width < 901) ? '360px' : '1600px';body.style.position = 'absolute';};setSecureCookie("zoom", zoom, 7);body.style.zoom = `${zoom}%`;};adjustBodyStyle();window.addEventListener('resize', adjustBodyStyle, { passive: true });
</script>
{% block content %}{% endblock %}
<div id="modal" class="modal" style="display: none;cursor: zoom-out;">
<span class="close" onclick="closeModalPic()">&times;</span>
<div style="width: 100%;height: 100%; position: absolute;" onclick="closeModalPic()"></div>
<img id="modalImg" alt="" src="/static/painter.svg" onclick="closeModalPic()" style="height: 100%;position: absolute;left: 0;transition: none;min-width: 100%;object-fit: scale-down;" />
<a id="modalLink" style="color: white;text-align: center;display: none;">
<span id="modalLinkText"></span>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#FFFFFF"><path d="m256-240-56-56 384-384H240v-80h480v480h-80v-344L256-240Z"/></svg>
</a>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('picture').forEach(picture => {
picture.addEventListener('click', function() {
const img = picture.querySelector('img');
const modalImg = document.getElementById('modalImg');
const modal = document.getElementById('modal');
const modalLink = document.getElementById('modalLink');
const modalLinkText = document.getElementById('modalLinkText');
let node = picture.parentElement;
while (node) {
if (node.tagName && node.tagName.toLowerCase() === 'a') {
return;
}
node = node.parentElement;
}
if (img.dataset.src){
modalImg.src = img.dataset.src;
} else {
modalImg.src = img.src;
}
if (picture.dataset.link && picture.dataset.label) {
modalLink.href = picture.dataset.link;
modalLinkText.textContent = picture.dataset.label;
modalLink.style.display = 'flex';
} else {
modalLink.style.display = 'none';
}
modal.style.display = 'flex';
});
});
});
function closeModalPic() {const modalImg = document.getElementById('modalImg');modalImg.src = '';document.getElementById('modal').style.display = 'none';}
</script>
<script>
{% raw %}
function showToast(e={}){let{message:t="",status:a="info",duration:l=4e3}=e,n=document.querySelector(".toast-container");n||((n=document.createElement("div")).className="toast-container",document.body.appendChild(n));let s=document.createElement("div");s.className=`toast ${a}`;let d=document.createElement("div");d.className="accent";let c=document.createElement("div");c.className="toast-header";let i=document.createElement("div");i.textContent=t;let o=document.createElement("button");o.className="toast-close",o.innerHTML="✕",c.appendChild(i),c.appendChild(o),s.appendChild(d),s.appendChild(c),n.appendChild(s),requestAnimationFrame(()=>s.classList.add("show"));let r=!1,p=()=>{r||(r=!0,s.classList.remove("show"),setTimeout(()=>{s.parentNode&&s.parentNode.removeChild(s)},380))};return o.addEventListener("click",p),s.addEventListener("click",e=>{e.target===s&&p()}),l>0&&setTimeout(p,l),{close:p}}
{% endraw %}
{% if notify %}
showToast({message: '{{notify.text}}', status: '{{notify.status}}', duration:{{notify.duration}}});
{% endif %}
</script>
<div id="shareModal"
style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);align-items:center;justify-content:center;z-index: 3000;">
<div style="background:#fff;padding:16px;border-radius:8px;min-width:260px;"><button id="closeModal"
style="float:right">×</button>
<h3>Поделиться</h3>
<div id="socialLinks" style="display:flex;gap:8px;margin-top:8px;flex-wrap: wrap;">
<!-- <a id="tw" href="#" target="_blank" rel="noopener"><img /></a> --><a id="fb" href="#" target="_blank"
rel="noopener"><img loading="lazy" alt="share link facebook" src="/static/icons/facebook.svg"></a><a id="vk" href="#" target="_blank"
rel="noopener"><img loading="lazy" alt="share link vk" src="/static/icons/vk.svg"></a><a id="tg" href="#" target="_blank"
rel="noopener"><img loading="lazy" alt="share link telegram" src="/static/icons/telegram.svg"></a><a id="ok" href="#" target="_blank"
rel="noopener"><img loading="lazy" alt="share link ok" src="/static/icons/ok.svg"></a><a id="wa" href="#" target="_blank"
rel="noopener"><img loading="lazy" alt="share link wapp" src="/static/icons/wapp.svg"></a><a id="inst" href="#" target="_blank"
rel="noopener"><img loading="lazy" alt="share link instagram" src="/static/icons/instagram.svg"></a><button id="copyBtn"
style="display: flex;align-items: center;gap: 5px;">Скопировать ссылку<img loading="lazy" alt="copy link for share"
src="/static/icons/copy.svg"> <span id="copyMsg"
style="margin-left:8px;color:green;display:none;">Скопировано!</span></button></div>
</div>
</div>
<script>
async function imageWithTextBlob(imageUrl, text, options = {}) {
const response = await fetch(imageUrl, {
mode: 'cors',
credentials: 'same-origin',
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const imageBlob = await response.blob();
const imageBitmap = await createImageBitmap(imageBlob);
// OffscreenCanvas (works in modern browsers; fallback to normal canvas if needed)
const canvas = typeof OffscreenCanvas !== 'undefined'
? new OffscreenCanvas(
options.width ?? imageBitmap.width,
options.height ?? imageBitmap.height
)
: document.createElement('canvas');
const width = canvas.width ?? (options.width ?? imageBitmap.width);
const height = canvas.height ?? (options.height ?? imageBitmap.height);
if (!(canvas instanceof OffscreenCanvas)) {
canvas.width = width;
canvas.height = height;
}
const ctx = canvas.getContext('2d', { alpha: true });
// Draw image
ctx.drawImage(imageBitmap, 0, 0, width, height);
// No text: return original bitmap as blob (rendered)
if (!text?.trim()) {
if (canvas.convertToBlob) {
return canvas.convertToBlob({
type: options.type ?? 'image/png',
quality: options.quality ?? 0.92,
});
}
return await new Promise((resolve) =>
canvas.toBlob((b) => resolve(b), options.type ?? 'image/png', options.quality ?? 0.92)
);
}
// === Text rendering ===
const basePadding = 6;
const fontSize = options.fontSize ?? Math.round(width * 0.05);
ctx.font = `${fontSize}px ${options.fontFamily ?? 'sans-serif'}`;
ctx.textBaseline = 'bottom';
ctx.textAlign = 'center';
const textColor = options.textColor ?? '#5F1798';
const bgColor = options.bgColor ?? '#fff';
const strokeColor = options.strokeColor ?? 'rgba(0,0,0,0.35)';
const shadowBlur = options.shadowBlur ?? 6;
const maxWidth = width - basePadding * 2 - (options.extraPadding ?? 0);
// Wrap text into lines
const words = text.split(' ');
let line = '';
const lines = [];
for (const word of words) {
const testLine = line ? line + ' ' + word : word;
if (ctx.measureText(testLine).width > maxWidth && line) {
lines.push(line);
line = word;
} else {
line = testLine;
}
}
if (line) lines.push(line);
const lineHeight = fontSize + (options.lineGap ?? 6);
const boxHeight = lines.length * lineHeight + basePadding * 2;
const maxLineWidth = Math.max(...lines.map((l) => ctx.measureText(l).width));
const boxWidth = maxLineWidth + basePadding * 2;
const bottomPadding = options.bottomPadding ?? Math.round(fontSize * 0.6);
const boxX = (width - boxWidth) / 2;
const boxY = height - bottomPadding - boxHeight;
const radius = options.radius ?? 8;
// Background rounded rect
ctx.save();
ctx.fillStyle = bgColor;
ctx.shadowColor = strokeColor;
ctx.shadowBlur = shadowBlur;
roundRect(ctx, boxX, boxY, boxWidth, boxHeight, radius, true, false);
ctx.restore();
// Text
ctx.fillStyle = textColor;
ctx.shadowColor = 'transparent';
for (let i = 0; i < lines.length; i++) {
const lineY = boxY + basePadding + (i + 1) * lineHeight - (lineHeight - fontSize) / 2;
const lineX = width / 2;
if (options.outline) {
ctx.lineWidth = options.outlineWidth ?? 2;
ctx.strokeStyle = options.outlineColor ?? 'rgba(255,255,255,0.8)';
ctx.strokeText(lines[i], lineX, lineY);
}
ctx.fillText(lines[i], lineX, lineY);
}
// Convert to blob
if (canvas.convertToBlob) {
return await canvas.convertToBlob({
type: options.type ?? 'image/png',
quality: options.quality ?? 0.92,
});
}
return await new Promise((resolve) =>
canvas.toBlob((b) => resolve(b), options.type ?? 'image/png', options.quality ?? 0.92)
);
}
function roundRect(ctx, x, y, w, h, r, fill, stroke) {
if (typeof r === 'number') r = { tl: r, tr: r, br: r, bl: r };
ctx.beginPath();
ctx.moveTo(x + r.tl, y);
ctx.lineTo(x + w - r.tr, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r.tr);
ctx.lineTo(x + w, y + h - r.br);
ctx.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h);
ctx.lineTo(x + r.bl, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r.bl);
ctx.lineTo(x, y + r.tl);
ctx.quadraticCurveTo(x, y, x + r.tl, y);
ctx.closePath();
if (fill) ctx.fill();
if (stroke) ctx.stroke();
}
document.addEventListener('DOMContentLoaded', () => {
const shareModal = document.getElementById('shareModal');
const closeshareModal = document.getElementById('closeModal');
const fb = document.getElementById('fb');
const vk = document.getElementById('vk');
const tg = document.getElementById('tg');
const ok = document.getElementById('ok');
const wa = document.getElementById('wa');
const inst = document.getElementById('inst');
const copyBtn = document.getElementById('copyBtn');
const copyMsg = document.getElementById('copyMsg');
function buildShareUrl(platform, url, text = '') {
const u = encodeURIComponent(url);
const t = encodeURIComponent(text);
switch (platform) {
case 'tw': return `https://twitter.com/intent/tweet?url=${u}&text=${t}`;
case 'vk': return `https://vk.com/share.php?url=${u}&title=${t}`;
case 'tg': return `https://telegram.me/share/url?url=${u}&text=${t}`;
case 'fb': return `https://www.facebook.com/sharer/sharer.php?u=${u}&quote=${t}`;
case 'ok': return `https://connect.ok.ru/offer?url=${u}&title=${t}`;
case 'wa': return `https://api.whatsapp.com/send?text=${t} ${u}`;
case 'inst': return `https://www.instagram.com/?url=${u}`;
default: return '#';
}
}
const shareBtns = document.querySelectorAll('#shareBtn');
shareBtns.forEach(btn => {
btn.addEventListener('click', async () => {
const link = window.location.origin + btn.dataset.link || window.location.href;
const text = btn.dataset.text || document.title;
const image = btn.dataset.image;
const imagetext = btn.dataset.imagetext || '';
let blob = null;
let file = null;
let objectUrl = null;
try {
if (image) {
const imageUrl = window.location.origin + image;
// В обработчике кнопки
const blob = await imageWithTextBlob(imageUrl, imagetext, {
textColor: '#5F1798',
});
console.log(`Generated blob for "${imagetext}" | size: ${blob.size} | url: ${imageUrl}`);
const filename = `shared-image-${Date.now()}.png`;
file = new File([blob], filename, { type: blob.type });
// Создаём object URL только если хотим (необязательно для navigator.share)
objectUrl = URL.createObjectURL(file);
if (navigator.canShare && navigator.canShare({ files: [file] })) {
await navigator.share({
title: text,
text: text,
url: link,
files: [file],
});
} else {
await navigator.share({
title: text,
text: '',
url: link,
});
}
} else {
await navigator.share({ title: text, text: '', url: link });
}
} catch (err) {
openshareModal(link, text);
} finally {
// Очистка
try {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
objectUrl = null;
}
} catch (e) { /* ignore */ }
// Удаляем ссылки, чтобы GC мог собрать объекты
blob = null;
file = null;
}
});
});
function openshareModal(link, text) {
vk.href = buildShareUrl('vk', link, text);
tg.href = buildShareUrl('tg', link, text);
fb.href = buildShareUrl('fb', link, text);
ok.href = buildShareUrl('ok', link, text);
wa.href = buildShareUrl('wa', link, text);
inst.href = buildShareUrl('inst', link, text);
shareModal.style.display = 'flex';
copyBtn.dataset.link = link;
shareModal.style.display = 'flex'
}
closeshareModal.addEventListener('click', () => {
shareModal.style.display = 'none';
});
shareModal.addEventListener('click', (e) => {
if (e.target === shareModal) shareModal.style.display = 'none';
});
async function copyToClipboard(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
} else {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
showCopyMessage();
} catch (e) {
showToast({message: 'Не удалось скопировать ссылку', status: 'error'});
}
}
function showCopyMessage() {
copyMsg.style.display = 'inline';
setTimeout(() => copyMsg.style.display = 'none', 2000);
}
copyBtn.addEventListener('click', () => {
const link = copyBtn.dataset.link || window.location.href;
copyToClipboard(link);
});
});
</script>
<script>
(async () => {
const parts_cookie = `; ${document.cookie}`.split(`; token=`);
const token_old = parts_cookie.pop().split(';')[0];
if (!token_old) return;
try {
let response = await fetch('/api/me');
if (!response.ok) {
response = await fetch('/api/old/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
}
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
if (!data.username) {
response = await fetch('/api/old/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Network response was still not ok');
}
}
} catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
})();
</script>
<script>
let deferredPrompt = null;
let hasUserInteracted = false;
let promptTimeout = null;
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
console.log('New Service Worker ready');
}
});
});
});
navigator.serviceWorker.addEventListener('message', event => {
if (event.data.type === 'SW_UPDATED') {
console.log('New Service Worker activated – reloading...');
window.location.reload();
}
});
}
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
console.log('beforeinstallprompt: готово к установке');
});
const markInteractionAndTryShow = () => {
if (hasUserInteracted) return;
hasUserInteracted = true;
document.removeEventListener('click', markInteractionAndTryShow);
document.removeEventListener('keydown', markInteractionAndTryShow);
document.removeEventListener('touchstart', safeTouchHandler);
tryShowInstallPrompt();
};
let touchMoved = false;
const safeTouchHandler = (e) => {
if (e.touches.length > 1) return;
document.addEventListener('touchmove', () => touchMoved = true, { once: true });
document.addEventListener('touchend', () => {
if (!touchMoved) markInteractionAndTryShow();
touchMoved = false;
}, { once: true });
};
document.addEventListener('click', markInteractionAndTryShow, { passive: true });
document.addEventListener('keydown', markInteractionAndTryShow, { passive: true });
document.addEventListener('touchstart', safeTouchHandler, { passive: true });
let scrollAllowed = false;
const scrollHandler = () => {
if (scrollAllowed && !hasUserInteracted) markInteractionAndTryShow();
};
document.addEventListener('scroll', scrollHandler, { passive: true });
document.addEventListener('click', () => scrollAllowed = true, { once: true });
document.addEventListener('touchstart', () => scrollAllowed = true, { once: true });
function tryShowInstallPrompt() {
if (!deferredPrompt) {
console.log('Промпт ещё не готов');
return;
}
if (promptTimeout) return;
promptTimeout = setTimeout(() => {
console.log('Показываем install-промпт');
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('PWA установлен!');
} else {
console.log('Установка отклонена');
}
deferredPrompt = null;
promptTimeout = null;
});
}, 1500);
}
</script>
<script>
(function() {
'use strict';
const root = document.getElementById('dd-modal-root') || document.body;
// Утилиты парсинга
function parseList(attr) {
if (!attr) return [];
try {
const fixed = attr.replace(/'/g, '"');
return JSON.parse(fixed);
} catch (e) {
return String(attr).split(',').map(s => s.trim()).filter(Boolean);
}
}
function parseSavedValue(el) {
const v = el.getAttribute('data-value');
if (!v) return [];
try {
return JSON.parse(v);
} catch (e) {
return v.includes(',')
? v.split(',').map(s => s.trim()).filter(Boolean)
: [v];
}
}
async function openModalForInput(triggerEl) {
const listRaw = triggerEl.getAttribute('data-list') || '[]';
const options = parseList(listRaw);
const isRadio = triggerEl.getAttribute('data-radio') === '1' ||
triggerEl.getAttribute('data-radio') === 'true';
const takeAll = triggerEl.getAttribute('data-takeall') === '1' ||
triggerEl.getAttribute('data-takeall') === 'true';
const allowCustom = triggerEl.getAttribute('data-custom') === '1' ||
triggerEl.getAttribute('data-custom') === 'true';
const title = triggerEl.getAttribute('data-title') || 'Выберите';
const saveLabel = triggerEl.getAttribute('data-label') || 'Сохранить';
const customPlaceholder = triggerEl.getAttribute('data-custom_placeholder') || 'Добавить...';
const initial = parseSavedValue(triggerEl);
return new Promise(resolve => {
const backdrop = document.createElement('div');
backdrop.className = 'dd-modal-backdrop';
const modal = document.createElement('div');
modal.className = 'dd-modal';
modal.innerHTML = `
<div class="dd-header">
<strong>${title}</strong>
<button class="dd-close" aria-label="Закрыть">&times;</button>
</div>
<div class="dd-custom-container" style="margin-top:8px;"></div>
<div class="dd-body"></div>
<div class="dd-actions" style="margin-top:12px;">
<button class="dd-save">${saveLabel}</button>
</div>
`;
backdrop.appendChild(modal);
root.appendChild(backdrop);
const body = modal.querySelector('.dd-body');
const customContainer = modal.querySelector('.dd-custom-container');
const saveBtn = modal.querySelector('.dd-save');
const closeBtn = modal.querySelector('.dd-close');
const groupName = 'dd-group-' + Date.now() + Math.random().toString(36).slice(2);
let currentOptions = options.slice();
let selected = initial.slice();
function renderOptions() {
body.innerHTML = '';
// "Выбрать все"
if (takeAll) {
const wrapper = createOption('Выбрать все', true, groupName, isRadio);
const takeAllInput = wrapper.querySelector('input');
takeAllInput.id = 'takeAll';
takeAllInput.checked = selected.length === 0 || selected.includes('Выбрать все');
body.appendChild(wrapper);
}
currentOptions.forEach(opt => {
const label = typeof opt === 'object' ? opt.name : String(opt);
const tooltip = typeof opt === 'object' ? opt.tooltip : null;
const count = typeof opt === 'object' ? opt.count : null;
const wrapper = createOption(label, false, groupName, isRadio, tooltip, count);
const input = wrapper.querySelector('input');
input.checked = selected.includes(label);
body.appendChild(wrapper);
});
}
function createOption(label, isTakeAll, name, isRadio, tooltip = null, count = null) {
const wrapper = document.createElement('label');
wrapper.className = 'dd-option';
const input = document.createElement('input');
input.type = isRadio ? 'radio' : 'checkbox';
input.name = name;
input.value = label;
input.className = 'offer-checkbox';
const span = document.createElement('span');
span.textContent = label;
const div = document.createElement('div');
div.className = 'some-class';
// count
if (count !== undefined && count !== null) {
const countNode = document.createElement('span');
countNode.textContent = count;
div.appendChild(countNode);
}
// tooltip
if (tooltip) {
const img = document.createElement('img');
img.className = 'tooltip-trigger';
img.dataset.tooltip = tooltip;
img.src = '/static/help.svg';
img.alt = 'Подсказка';
div.appendChild(img);
}
// куда-то добавляем div
span.appendChild(div);
wrapper.append(input, span);
return wrapper;
}
// Кастомные значения
if (allowCustom) {
customContainer.innerHTML = `
<div style="display:flex;gap:8px;align-items:center;flex-direction:column;">
<input type="text" class="dd-custom-input" placeholder="${customPlaceholder}"
style="flex:1;padding:6px;border:1px solid #ccc;width:100%;border-radius:4px;">
<button class="dd-custom-add" style="padding:6px 8px;width:100%;border-radius:5px;background:#5F1798;color:white;">
Добавить
</button>
</div>
`;
const customInput = customContainer.querySelector('.dd-custom-input');
const customAdd = customContainer.querySelector('.dd-custom-add');
const addCustom = (val) => {
val = String(val).trim();
if (!val) return;
if (!currentOptions.some(o => String(o) === val && typeof o !== 'object')) {
currentOptions.push(val);
triggerEl.setAttribute('data-list', JSON.stringify(currentOptions));
}
if (isRadio) {
selected = [val];
} else if (!selected.includes(val)) {
selected.push(val);
}
renderOptions();
updateCheckedState();
};
customAdd.addEventListener('click', () => {
addCustom(customInput.value);
customInput.value = '';
customInput.focus();
});
customInput.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
addCustom(customInput.value);
customInput.value = '';
}
});
}
function updateCheckedState() {
Array.from(body.querySelectorAll('input')).forEach(inp => {
inp.checked = selected.includes(inp.value);
});
}
function collectChecked() {
return Array.from(body.querySelectorAll('input:checked')).map(i => i.value);
}
function closeModal() {
const result = collectChecked();
triggerEl.setAttribute('data-value', isRadio ? (result[0] ?? '') : JSON.stringify(result));
if (triggerEl.type && triggerEl.type !== 'hidden') {
triggerEl.value = isRadio ? (result[0] ?? '') : result.join(', ');
}
backdrop.remove();
resolve(result);
}
// Обработчики закрытия
closeBtn.addEventListener('click', closeModal);
saveBtn.addEventListener('click', closeModal);
backdrop.addEventListener('click', e => {
if (e.target === backdrop) closeModal();
});
// Логика "Выбрать все"
function setupTakeAll() {
const takeAllInput = document.getElementById('takeAll');
if (!takeAllInput) return;
const allInputs = () => Array.from(body.querySelectorAll('input.offer-checkbox'));
takeAllInput.addEventListener('change', () => {
if (takeAllInput.checked) {
selected = [];
allInputs().forEach(inp => {
if (inp !== takeAllInput) inp.checked = false;
});
closeModal();
}
});
// Сброс "Выбрать все" при выборе других
body.addEventListener('change', e => {
const inp = e.target;
if (inp.type !== 'checkbox' || inp === takeAllInput) return;
if (inp.checked) {
takeAllInput.checked = false;
} else {
const anyChecked = allInputs().some(x => x !== takeAllInput && x.checked);
takeAllInput.checked = !anyChecked;
}
});
}
renderOptions();
setupTakeAll();
updateCheckedState();
});
}
// Главный обработчик
document.addEventListener('click', async function(e) {
const trigger = e.target.closest('[data-select="1"]');
if (!trigger) return;
const input = trigger.querySelector('input');
if (input) {
input.readOnly = true;
input.blur();
}
e.preventDefault();
const result = await openModalForInput(trigger);
if (result && result.length) {
trigger.dispatchEvent(new Event('change', { bubbles: true }));
}
}, true);
})();
</script>
<div id="dd-modal-root"></div>
<div id="modalToolTip">
<div class="modal-content">
<span id="closeToolTipBtn" class="close">&times;</span>
<p id="modalToolTipText" style="overflow: hidden;white-space: pre-line;"></p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('modalToolTip');
const modalText = document.getElementById('modalToolTipText');
const closeBtn = document.getElementById('closeToolTipBtn');
document.addEventListener('click', (event) => {
if (event.target.classList.contains('tooltip-trigger')) {
modalText.textContent = event.target.getAttribute('data-tooltip');
modal.style.display = 'flex';
}
});
closeBtn.onclick = () => {
modal.style.display = 'none';
};
window.onclick = (event) => {
if (event.target === modal) {
modal.style.display = 'none';
}
};
document.addEventListener('mouseenter', (event) => {
if (event.target.classList.contains('tooltip-trigger')) {
modalText.textContent = event.target.getAttribute('data-tooltip');
modal.style.display = 'block';
}
});
});
</script>
</body>
</html>