init
This commit is contained in:
commit
7d2c977a9f
19 changed files with 3165 additions and 0 deletions
442
css/options.css
Normal file
442
css/options.css
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
/* ==================== СТИЛИ СТРАНИЦЫ НАСТРОЕК ==================== */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #333;
|
||||
font-size: 32px;
|
||||
margin-bottom: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #f4f5f8;
|
||||
padding: 20px 24px 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.settings-col.secondary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 18px;
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e4ea;
|
||||
padding: 12px 14px 10px;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.settings-section h2 {
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
color: #555;
|
||||
font-size: 16px;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.setting-item label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.label-description {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="number"]:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* Поле пароля: text + маскировка точками (без type=password из-за особенностей рендеринга) */
|
||||
.password-field {
|
||||
-webkit-text-security: disc;
|
||||
text-security: disc;
|
||||
}
|
||||
|
||||
select {
|
||||
cursor: pointer;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Switch toggle */
|
||||
.switch-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switch-label .label-text,
|
||||
.switch-label .label-description {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: 0.4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* Компактные inline-элементы (label + input в одну линию) */
|
||||
.setting-item.inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.setting-item.inline label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Подсказки под полями (например, под паролем) */
|
||||
.hint {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Sub settings */
|
||||
.sub-settings {
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sub-settings.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Список модулей отладки: по умолчанию скрыт (нет .expanded при загрузке),
|
||||
показывается только при включённой отладке — без анимации при первой отрисовке */
|
||||
#debug-modules {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#debug-modules.expanded {
|
||||
max-height: 600px;
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e2e4ea;
|
||||
border-radius: 8px;
|
||||
transition: max-height 0.25s ease;
|
||||
}
|
||||
|
||||
.setting-item-compact {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.setting-item-compact label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.setting-item-compact label:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.setting-item-compact input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-item-compact span {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e0e0e0;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Status message */
|
||||
.status {
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.status.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Info section */
|
||||
.info-section {
|
||||
margin-top: 10px;
|
||||
padding: 14px 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.info-section h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.info-section p {
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-section p:last-child {
|
||||
margin-bottom: 0;
|
||||
margin-top: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hidden elements */
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
154
css/popup.css
Normal file
154
css/popup.css
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/* Popup — компактное выпадающее окно */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
min-width: 260px;
|
||||
}
|
||||
.popup {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.popup-header {
|
||||
padding: 12px 18px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
}
|
||||
.popup-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
.popup-body {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
.row:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.row-btn {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.switch-row {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.switch-wrap {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.switch-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.switch-track {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
border-radius: 11px;
|
||||
background: #cbd5e0;
|
||||
position: relative;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.switch-track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.switch-input:checked + .switch-track {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.switch-input:checked + .switch-track::after {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
.switch-row .label {
|
||||
flex: 1;
|
||||
line-height: 1.35;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.row-reload {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.row-reload.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.btn-reload {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
.btn-reload:hover {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.status-text.success { color: #2e7d32; }
|
||||
.status-text.error { color: #c62828; }
|
||||
.popup-footer {
|
||||
padding: 12px 18px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
background: linear-gradient(180deg, #f8f9fc 0%, #eef0f5 100%);
|
||||
}
|
||||
.popup-footer .footer-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #5a67d8;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.popup-footer .footer-link:hover {
|
||||
background: rgba(102, 126, 234, 0.12);
|
||||
color: #434190;
|
||||
}
|
||||
.popup-footer .footer-link:active {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
137
icons/caps.svg
Normal file
137
icons/caps.svg
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="_Слой_1"
|
||||
data-name="Слой_1"
|
||||
viewBox="0 0 364.15 160.94"
|
||||
version="1.1"
|
||||
sodipodi:docname="cleverpark.svg"
|
||||
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview11"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="7.3008574"
|
||||
inkscape:cx="182.03342"
|
||||
inkscape:cy="80.470001"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1377"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g11" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<linearGradient
|
||||
id="linearGradient11"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#667eea;stop-opacity:1;"
|
||||
offset="0.34104046"
|
||||
id="stop11" />
|
||||
<stop
|
||||
style="stop-color:#764ba2;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop12" />
|
||||
</linearGradient>
|
||||
<style
|
||||
id="style1">
|
||||
.cls-1 {
|
||||
fill: #47b972;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
clip-path: url(#clippath);
|
||||
}
|
||||
</style>
|
||||
<clipPath
|
||||
id="clippath">
|
||||
<rect
|
||||
class="cls-3"
|
||||
x="0"
|
||||
width="364.15"
|
||||
height="160.94"
|
||||
id="rect1" />
|
||||
</clipPath>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient11"
|
||||
id="linearGradient12"
|
||||
x1="9.0590372"
|
||||
y1="49.925152"
|
||||
x2="126.12096"
|
||||
y2="117.51089"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
class="cls-4"
|
||||
clip-path="url(#clippath)"
|
||||
id="g11">
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M171.9,83.72h-3.19v-17.92h3.19v17.92ZM180.58,65.8h3.82l-8.16,8.32,3.98,4.26c1.8,2.04,3.32,2.83,4.54,2.35l-.04,2.99c-1.22.37-2.47.23-3.74-.42-1.27-.65-2.42-1.65-3.43-3.01l-4.98-5.93,8.01-8.56Z"
|
||||
id="path1" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M203.83,83.72h-3.19v-14.74h-6.29l-.2,3.03c-.21,2.47-.52,4.52-.94,6.15-.41,1.63-.94,2.86-1.59,3.66-.65.81-1.33,1.37-2.05,1.67-.72.3-1.59.46-2.63.46l-.24-3.11c.29.03.6-.02.92-.14.32-.12.67-.38,1.06-.78.38-.4.73-.93,1.04-1.59.3-.66.58-1.6.84-2.81.25-1.21.42-2.6.5-4.16l.32-5.58h12.47v17.92Z"
|
||||
id="path2" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M222.67,79.26l1,2.51c-1.73,1.57-3.97,2.35-6.73,2.35-2.92,0-5.24-.85-6.97-2.55-1.75-1.7-2.63-3.97-2.63-6.81,0-2.68.82-4.91,2.47-6.69,1.62-1.78,3.88-2.67,6.77-2.67,2.39,0,4.37.77,5.93,2.31,1.59,1.52,2.39,3.48,2.39,5.9,0,.77-.07,1.49-.2,2.15h-13.78c.13,1.67.76,2.99,1.89,3.94,1.13.96,2.58,1.43,4.36,1.43,2.34,0,4.17-.62,5.5-1.87M216.42,68.39c-1.57,0-2.86.44-3.88,1.32-1.02.88-1.63,2.03-1.81,3.46h10.87c-.08-1.49-.58-2.65-1.49-3.5-.92-.85-2.14-1.28-3.68-1.28"
|
||||
id="path3" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M228.25,83.72v-17.92c.13,0,.32,0,.56-.02.24-.01.58-.02,1.04-.04.45-.01.86-.02,1.23-.02,1.96-.05,3.19-.08,3.66-.08,5.15,0,7.73,1.54,7.73,4.62,0,1.06-.33,1.97-1,2.73-.66.76-1.53,1.22-2.59,1.38v.08c2.79.5,4.18,1.91,4.18,4.22,0,3.48-2.75,5.22-8.24,5.22-.27,0-1.44-.03-3.51-.08-.4,0-.84,0-1.31-.02-.48-.01-.85-.03-1.12-.04-.27-.01-.48-.02-.64-.02M234.99,68.23c-.77,0-1.95.04-3.55.12v5.34h4.02c1.09,0,1.97-.25,2.63-.76.66-.51,1-1.18,1-2.03,0-1.78-1.37-2.67-4.1-2.67M235.27,75.68h-3.82v5.5c1.91.08,3.13.12,3.67.12,3.05,0,4.58-.96,4.58-2.87s-1.47-2.75-4.42-2.75"
|
||||
id="path4" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M260.84,79.26l1,2.51c-1.73,1.57-3.97,2.35-6.73,2.35-2.92,0-5.24-.85-6.97-2.55-1.75-1.7-2.63-3.97-2.63-6.81,0-2.68.82-4.91,2.47-6.69,1.62-1.78,3.88-2.67,6.77-2.67,2.39,0,4.37.77,5.93,2.31,1.59,1.52,2.39,3.48,2.39,5.9,0,.77-.07,1.49-.2,2.15h-13.78c.13,1.67.76,2.99,1.89,3.94,1.13.96,2.58,1.43,4.36,1.43,2.34,0,4.17-.62,5.5-1.87M254.59,68.39c-1.57,0-2.86.44-3.88,1.32-1.02.88-1.63,2.03-1.81,3.46h10.87c-.08-1.49-.58-2.65-1.49-3.5-.92-.85-2.14-1.28-3.68-1.28"
|
||||
id="path5" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M269.61,91.69h-3.19v-25.89h3.19v2.11c.64-.74,1.51-1.35,2.63-1.81,1.11-.46,2.27-.7,3.46-.7,2.55,0,4.55.85,6.02,2.55,1.51,1.67,2.27,3.85,2.27,6.53s-.86,5.03-2.59,6.89c-1.7,1.83-3.94,2.75-6.73,2.75-2.15,0-3.84-.41-5.06-1.24v8.8ZM274.79,68.39c-2.12,0-3.85.82-5.18,2.47v8.88c1.41.93,3,1.39,4.78,1.39,1.89,0,3.39-.6,4.52-1.81,1.13-1.21,1.69-2.78,1.69-4.72s-.52-3.39-1.55-4.52c-1.04-1.13-2.46-1.69-4.26-1.69"
|
||||
id="path6" />
|
||||
<polygon
|
||||
class="cls-2"
|
||||
points="304.17 83.72 299.59 83.72 299.59 70.1 291.95 70.1 291.95 83.72 287.36 83.72 287.36 65.8 304.17 65.8 304.17 83.72"
|
||||
id="polygon6" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M309.71,70.42l-1.15-3.34c1.94-1.14,4.05-1.71,6.33-1.71,2.5,0,4.32.61,5.46,1.83,1.14,1.22,1.71,3.09,1.71,5.62v10.91h-3.98v-2.07c-1.06,1.54-2.83,2.31-5.3,2.31-1.7,0-3.06-.5-4.08-1.5-1.02-.99-1.53-2.33-1.53-4,0-1.86.6-3.29,1.79-4.3,1.2-1.01,2.78-1.51,4.74-1.51,1.62,0,2.97.38,4.06,1.15.05-1.59-.18-2.76-.7-3.51-.52-.74-1.4-1.12-2.65-1.12-1.38,0-2.95.41-4.7,1.23M314.29,80.1c1.54,0,2.69-.54,3.46-1.63v-1.83c-.74-.58-1.79-.88-3.15-.88-.85,0-1.54.2-2.07.6-.53.4-.8.94-.8,1.63,0,.64.24,1.15.72,1.53.48.39,1.09.58,1.83.58"
|
||||
id="path7" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M330.45,91.69h-4.58v-25.89h4.58v1.71c.58-.64,1.38-1.16,2.39-1.55,1.01-.4,2.06-.6,3.15-.6,2.47,0,4.43.85,5.89,2.55,1.49,1.73,2.23,3.92,2.23,6.57,0,2.79-.88,5.1-2.63,6.93-1.75,1.83-4.04,2.75-6.85,2.75-1.81,0-3.2-.29-4.18-.88v8.41ZM334.55,69.66c-1.59,0-2.96.65-4.1,1.95v7.25c1.09.69,2.35,1.03,3.78,1.03,1.51,0,2.73-.49,3.64-1.47s1.37-2.26,1.37-3.82c0-1.49-.42-2.68-1.25-3.58-.84-.9-1.99-1.35-3.45-1.35"
|
||||
id="path8" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M351.68,83.72h-4.58v-17.92h4.58v17.92ZM358.65,65.8h5.46l-7.13,8.16,2.99,3.55c.61.77,1.29,1.35,2.03,1.73.74.38,1.46.46,2.15.22v4.26c-1.54.4-3.01.3-4.4-.3-1.39-.6-2.6-1.65-3.6-3.17l-4.38-5.9,6.89-8.56Z"
|
||||
id="path9" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M95.74,34.63h-55.37C18.99,34.63.79,51.25.03,72.61c-.8,22.43,17.15,40.86,39.4,40.86h25.34c1.56,0,2.82,1.26,2.82,2.82v14.27c0,2.01,2.43,3.01,3.85,1.59l17.86-17.86c.53-.53,1.24-.82,1.99-.82h3.52c21.38,0,39.58-16.62,40.34-37.99.8-22.43-17.15-40.86-39.4-40.86"
|
||||
id="path10"
|
||||
style="fill:url(#linearGradient12);fill-opacity:1" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M123.9,74.05c0,15.55-12.61,28.16-28.16,28.16s-28.16-12.61-28.16-28.16,12.61-28.16,28.16-28.16,28.16,12.61,28.16,28.16"
|
||||
id="path11" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
BIN
icons/icon128.png
Normal file
BIN
icons/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
icons/icon16.png
Normal file
BIN
icons/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
icons/icon48.png
Normal file
BIN
icons/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
170
js/content.js
Normal file
170
js/content.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
// ==================== ГЛАВНЫЙ ФАЙЛ ====================
|
||||
|
||||
(function() {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', waitForConfigAndInit);
|
||||
} else {
|
||||
waitForConfigAndInit();
|
||||
}
|
||||
|
||||
// Ожидание загрузки конфигурации перед инициализацией
|
||||
function waitForConfigAndInit() {
|
||||
const core = window.VideoIPRedirect;
|
||||
if (!core) {
|
||||
console.error('[CAPS_Enhancer] ❌ CORE не загружен!');
|
||||
return;
|
||||
}
|
||||
|
||||
const debugLog = (msg, ...args) => core.debugLog('content', msg, ...args);
|
||||
|
||||
// Если конфигурация уже загружена, запускаем инициализацию
|
||||
if (core.configReady) {
|
||||
debugLog('⚙️ Конфигурация готова, запуск инициализации...');
|
||||
init();
|
||||
} else {
|
||||
// Иначе ждём события о готовности конфигурации
|
||||
debugLog('⏳ Ожидание загрузки конфигурации...');
|
||||
document.addEventListener('VideoIPRedirect:ConfigReady', () => {
|
||||
debugLog('⚙️ Конфигурация загружена, запуск инициализации...');
|
||||
init();
|
||||
}, { once: true });
|
||||
|
||||
// Таймаут на случай, если событие не сработает
|
||||
setTimeout(() => {
|
||||
if (!core.configReady) {
|
||||
debugLog('⚠️ Таймаут ожидания конфигурации, запуск с настройками по умолчанию...');
|
||||
init();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
const core = window.VideoIPRedirect;
|
||||
const debugLog = (msg, ...args) => core.debugLog('content', msg, ...args);
|
||||
|
||||
const disabledSites = core.CONFIG.disabledSites || [];
|
||||
const hostname = window.location.hostname || '';
|
||||
if (disabledSites.length && disabledSites.includes(hostname)) {
|
||||
debugLog('⏹ Расширение отключено на этом сайте:', hostname);
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('========== ЗАПУСК ==========');
|
||||
debugLog('DOM загружен, инициализация...');
|
||||
debugLog('📋 Текущая конфигурация:', core.CONFIG);
|
||||
|
||||
const pageType = core.checkPageType();
|
||||
|
||||
// Замена логотипа CleverPark на caps.svg (если включено). Лого может появиться позже (SPA), поэтому повторяем попытки + наблюдатель.
|
||||
const LOGO_SELECTOR = 'img[src*="cleverpark.svg"], img[src*="cleverpark"], img[alt*="CLEVER PARK"], img[alt*="CLEVER"]';
|
||||
const replaceLogoEnabled = core.CONFIG.replaceLogo !== false;
|
||||
const hasGetURL = typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function';
|
||||
|
||||
function tryReplaceLogo(from) {
|
||||
if (!replaceLogoEnabled || !hasGetURL) return false;
|
||||
const logo = document.querySelector(LOGO_SELECTOR);
|
||||
if (logo && !logo.dataset.capsReplaced) {
|
||||
logo.dataset.capsReplaced = '1';
|
||||
logo.src = chrome.runtime.getURL('icons/caps.svg');
|
||||
debugLog('🖼️ [LOGO] Заменён на caps.svg', from ? '(' + from + ')' : '');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (replaceLogoEnabled && hasGetURL) {
|
||||
debugLog('🖼️ [LOGO] Включена замена, селектор:', LOGO_SELECTOR);
|
||||
tryReplaceLogo('init');
|
||||
[400, 1000, 2500].forEach((ms, i) => {
|
||||
setTimeout(() => { tryReplaceLogo('retry-' + (i + 1)); }, ms);
|
||||
});
|
||||
const logoObserver = new MutationObserver(() => {
|
||||
if (tryReplaceLogo('observer')) logoObserver.disconnect();
|
||||
});
|
||||
if (document.body) {
|
||||
logoObserver.observe(document.body, { childList: true, subtree: true });
|
||||
setTimeout(() => logoObserver.disconnect(), 10000);
|
||||
}
|
||||
}
|
||||
|
||||
// ПРИОРИТЕТ 1: Обработка страниц noVNC (переадресация или кнопка)
|
||||
if (pageType.isNoVNCPage && core.NoVNCRedirect) {
|
||||
const mode = core.CONFIG.novnc.mode;
|
||||
debugLog(`🖥️ Обнаружена страница noVNC, режим: ${mode}`);
|
||||
const handled = core.NoVNCRedirect.handleNoVNCPage();
|
||||
|
||||
// Если режим redirect, прерываем инициализацию (будет переадресация)
|
||||
// Если режим button, продолжаем работу (только добавили кнопку)
|
||||
if (mode === 'redirect' && handled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ПРИОРИТЕТ 2: Автологин на странице входа
|
||||
if (core.AutoLogin && core.AutoLogin.isLoginPage()) {
|
||||
debugLog('🔐 Обнаружена страница логина');
|
||||
core.AutoLogin.tryAutoLogin();
|
||||
// Продолжаем инициализацию (не прерываем)
|
||||
}
|
||||
|
||||
if (!pageType.hasAppMarker) {
|
||||
debugLog('⏹ Не CLEVER PARK, отключаемся');
|
||||
return;
|
||||
}
|
||||
|
||||
// Настраиваем Escape
|
||||
if (core.CloseHandler) {
|
||||
core.CloseHandler.setupEscapeHandler();
|
||||
}
|
||||
|
||||
// Видео страница
|
||||
if (pageType.isVideoPage && core.VideoWindow) {
|
||||
debugLog('🎬 ВИДЕО СТРАНИЦА');
|
||||
|
||||
// Используем утилиту для повторных попыток
|
||||
core.retryWithBackoff(() => core.VideoWindow.addButtonToVideoWindow());
|
||||
|
||||
const videoObserver = new MutationObserver(() => {
|
||||
core.VideoWindow.addButtonToVideoWindow();
|
||||
});
|
||||
|
||||
videoObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
// Главная страница
|
||||
if (core.Mnemoschema) {
|
||||
debugLog('🏠 ГЛАВНАЯ СТРАНИЦА');
|
||||
|
||||
const addMnemoButton = () => {
|
||||
if (!core.activeSchemeButton) {
|
||||
core.Mnemoschema.addSchemeButton();
|
||||
}
|
||||
};
|
||||
|
||||
// Используем утилиту для повторных попыток
|
||||
core.retryWithBackoff(addMnemoButton);
|
||||
}
|
||||
|
||||
debugLog('========== ГОТОВО ==========');
|
||||
}
|
||||
|
||||
// Обработка запроса на однократный вход из popup
|
||||
if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) {
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'PERFORM_LOGIN') {
|
||||
const core = window.VideoIPRedirect;
|
||||
if (!core || !core.AutoLogin) {
|
||||
sendResponse({ ok: false, error: 'not_ready' });
|
||||
return true;
|
||||
}
|
||||
const result = core.AutoLogin.performLoginOnce();
|
||||
sendResponse(result);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
396
js/options.js
Normal file
396
js/options.js
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
// ==================== СКРИПТ СТРАНИЦЫ НАСТРОЕК ====================
|
||||
|
||||
function getStr(path) {
|
||||
const o = window.CAPS_STRINGS || {};
|
||||
const val = path.split('.').reduce((acc, k) => acc && acc[k], o);
|
||||
return (val !== undefined && val !== null) ? val : path;
|
||||
}
|
||||
function applyOptionsStrings() {
|
||||
document.querySelectorAll('[data-i18n]').forEach((node) => {
|
||||
const key = node.getAttribute('data-i18n');
|
||||
const text = getStr(key);
|
||||
if (text === '') {
|
||||
node.style.display = 'none';
|
||||
} else if (text) {
|
||||
node.textContent = text;
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => {
|
||||
const key = node.getAttribute('data-i18n-placeholder');
|
||||
const text = getStr(key);
|
||||
if (text !== undefined && text !== null) node.placeholder = text;
|
||||
});
|
||||
const pageTitle = getStr('options.pageTitle');
|
||||
if (pageTitle) document.title = pageTitle;
|
||||
// Версия — из manifest.json (единый источник)
|
||||
const versionEl = document.getElementById('app-version');
|
||||
if (versionEl && typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getManifest) {
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
if (manifest && manifest.version) versionEl.textContent = manifest.version;
|
||||
}
|
||||
}
|
||||
|
||||
// Значения по умолчанию
|
||||
const DEFAULT_CONFIG = {
|
||||
debug: {
|
||||
enabled: true,
|
||||
modules: {
|
||||
core: true,
|
||||
video: true,
|
||||
mnemo: true,
|
||||
close: true,
|
||||
content: true,
|
||||
novnc: true,
|
||||
autologin: true
|
||||
}
|
||||
},
|
||||
ports: {
|
||||
video: '5000',
|
||||
scheme: '8080'
|
||||
},
|
||||
novnc: {
|
||||
mode: 'button',
|
||||
redirectDelay: 500
|
||||
},
|
||||
mnemo: {
|
||||
mode: 'host-port',
|
||||
skipOnDomain: false,
|
||||
addVideoButton: true,
|
||||
addSchemeButton: true
|
||||
},
|
||||
autologin: {
|
||||
enabled: false,
|
||||
username: '',
|
||||
password: '',
|
||||
autoSubmit: true,
|
||||
delay: 500
|
||||
},
|
||||
disabledSites: [],
|
||||
replaceLogo: true
|
||||
};
|
||||
|
||||
// Элементы формы
|
||||
const elements = {
|
||||
// Порты
|
||||
portVideo: document.getElementById('port-video'),
|
||||
portScheme: document.getElementById('port-scheme'),
|
||||
|
||||
// noVNC (страница экрана)
|
||||
novncRedirect: document.getElementById('novnc-redirect'),
|
||||
redirectDelay: document.getElementById('redirect-delay'),
|
||||
redirectDelayContainer: document.getElementById('redirect-delay-container'),
|
||||
|
||||
// Мнемосхема
|
||||
mnemoMode: document.getElementById('mnemo-mode'),
|
||||
mnemoSkipOnDomain: document.getElementById('mnemo-skip-on-domain'),
|
||||
mnemoAddSchemeButton: document.getElementById('mnemo-add-scheme-button'),
|
||||
mnemoAddVideoButton: document.getElementById('mnemo-add-video-button'),
|
||||
|
||||
// Замена логотипа
|
||||
replaceLogo: document.getElementById('replace-logo'),
|
||||
|
||||
// Автологин
|
||||
autologinEnabled: document.getElementById('autologin-enabled'),
|
||||
autologinSettings: document.getElementById('autologin-settings'),
|
||||
autologinUsername: document.getElementById('autologin-username'),
|
||||
autologinPassword: document.getElementById('autologin-password'),
|
||||
autologinAutoSubmit: document.getElementById('autologin-autosubmit'),
|
||||
autologinDelay: document.getElementById('autologin-delay'),
|
||||
|
||||
// Отладка
|
||||
debugEnabled: document.getElementById('debug-enabled'),
|
||||
debugModules: document.getElementById('debug-modules'),
|
||||
debugCore: document.getElementById('debug-core'),
|
||||
debugVideo: document.getElementById('debug-video'),
|
||||
debugMnemo: document.getElementById('debug-mnemo'),
|
||||
debugClose: document.getElementById('debug-close'),
|
||||
debugContent: document.getElementById('debug-content'),
|
||||
debugNovnc: document.getElementById('debug-novnc'),
|
||||
debugAutologin: document.getElementById('debug-autologin'),
|
||||
|
||||
// Кнопки
|
||||
saveBtn: document.getElementById('save-btn'),
|
||||
resetBtn: document.getElementById('reset-btn'),
|
||||
|
||||
// Статус
|
||||
status: document.getElementById('status')
|
||||
};
|
||||
|
||||
// Загрузка настроек при открытии страницы (сначала подставляем строки из strings.js)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
applyOptionsStrings();
|
||||
loadSettings();
|
||||
});
|
||||
|
||||
// Обработчики событий
|
||||
elements.saveBtn.addEventListener('click', saveSettings);
|
||||
elements.resetBtn.addEventListener('click', resetSettings);
|
||||
elements.debugEnabled.addEventListener('change', toggleDebugModules);
|
||||
elements.novncRedirect.addEventListener('change', toggleRedirectDelay);
|
||||
elements.autologinEnabled.addEventListener('change', toggleAutologinSettings);
|
||||
|
||||
// Загрузка сохранённых настроек
|
||||
function loadSettings() {
|
||||
console.log('Загрузка настроек...');
|
||||
|
||||
chrome.storage.sync.get(['config'], (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('Ошибка загрузки:', chrome.runtime.lastError);
|
||||
showStatus(getStr('options.statusLoadError') + ' ' + chrome.runtime.lastError.message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = result.config || DEFAULT_CONFIG;
|
||||
console.log('Настройки получены из storage:', result);
|
||||
console.log('Используемая конфигурация:', config);
|
||||
|
||||
// Порты
|
||||
elements.portVideo.value = config.ports?.video || DEFAULT_CONFIG.ports.video;
|
||||
elements.portScheme.value = config.ports?.scheme || DEFAULT_CONFIG.ports.scheme;
|
||||
|
||||
// noVNC
|
||||
elements.novncRedirect.checked = (config.novnc?.mode || DEFAULT_CONFIG.novnc.mode) === 'redirect';
|
||||
elements.redirectDelay.value = config.novnc?.redirectDelay || DEFAULT_CONFIG.novnc.redirectDelay;
|
||||
|
||||
// Мнемосхема
|
||||
const mnemoMode = config.mnemo?.mode;
|
||||
const mnemoSkip = config.mnemo?.skipOnDomain === true || mnemoMode === 'skip-on-domain';
|
||||
elements.mnemoMode.value = (mnemoMode === 'ip-scheme' ? 'host-scheme' : (mnemoMode === 'skip-on-domain' || mnemoMode === 'default' ? 'host-port' : (mnemoMode || 'host-port')));
|
||||
elements.mnemoSkipOnDomain.checked = mnemoSkip;
|
||||
elements.mnemoAddSchemeButton.checked = config.mnemo?.addSchemeButton !== false;
|
||||
elements.mnemoAddVideoButton.checked = config.mnemo?.addVideoButton !== false;
|
||||
|
||||
// Замена логотипа
|
||||
elements.replaceLogo.checked = config.replaceLogo !== false;
|
||||
|
||||
// Автологин
|
||||
elements.autologinEnabled.checked = config.autologin?.enabled || false;
|
||||
elements.autologinUsername.value = config.autologin?.username || '';
|
||||
elements.autologinPassword.value = config.autologin?.password || '';
|
||||
elements.autologinAutoSubmit.checked = config.autologin?.autoSubmit !== false;
|
||||
elements.autologinDelay.value = config.autologin?.delay || DEFAULT_CONFIG.autologin.delay;
|
||||
|
||||
// Отладка
|
||||
elements.debugEnabled.checked = config.debug?.enabled !== false;
|
||||
elements.debugCore.checked = config.debug?.modules?.core !== false;
|
||||
elements.debugVideo.checked = config.debug?.modules?.video !== false;
|
||||
elements.debugMnemo.checked = config.debug?.modules?.mnemo !== false;
|
||||
elements.debugClose.checked = config.debug?.modules?.close !== false;
|
||||
elements.debugContent.checked = config.debug?.modules?.content !== false;
|
||||
elements.debugNovnc.checked = config.debug?.modules?.novnc !== false;
|
||||
elements.debugAutologin.checked = config.debug?.modules?.autologin !== false;
|
||||
|
||||
// Обновляем UI
|
||||
toggleDebugModules();
|
||||
toggleRedirectDelay();
|
||||
toggleAutologinSettings();
|
||||
|
||||
console.log('✅ Настройки загружены успешно');
|
||||
});
|
||||
}
|
||||
|
||||
// Сохранение настроек
|
||||
function saveSettings() {
|
||||
// Валидация портов
|
||||
const portVideo = elements.portVideo.value.trim();
|
||||
const portScheme = elements.portScheme.value.trim();
|
||||
|
||||
if (!portVideo || isNaN(portVideo) || portVideo < 1 || portVideo > 65535) {
|
||||
showStatus(getStr('options.errorPortVideo'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!portScheme || isNaN(portScheme) || portScheme < 1 || portScheme > 65535) {
|
||||
showStatus(getStr('options.errorPortScheme'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const redirectDelay = parseInt(elements.redirectDelay.value) || 500;
|
||||
const autologinDelay = parseInt(elements.autologinDelay.value) || 500;
|
||||
|
||||
// Собираем конфигурацию
|
||||
const config = {
|
||||
debug: {
|
||||
enabled: elements.debugEnabled.checked,
|
||||
modules: {
|
||||
core: elements.debugCore.checked,
|
||||
video: elements.debugVideo.checked,
|
||||
mnemo: elements.debugMnemo.checked,
|
||||
close: elements.debugClose.checked,
|
||||
content: elements.debugContent.checked,
|
||||
novnc: elements.debugNovnc.checked,
|
||||
autologin: elements.debugAutologin.checked
|
||||
}
|
||||
},
|
||||
ports: {
|
||||
video: portVideo,
|
||||
scheme: portScheme
|
||||
},
|
||||
novnc: {
|
||||
mode: elements.novncRedirect.checked ? 'redirect' : 'button',
|
||||
redirectDelay: redirectDelay
|
||||
},
|
||||
mnemo: {
|
||||
mode: elements.mnemoMode.value,
|
||||
skipOnDomain: elements.mnemoSkipOnDomain.checked,
|
||||
addVideoButton: elements.mnemoAddVideoButton.checked,
|
||||
addSchemeButton: elements.mnemoAddSchemeButton.checked
|
||||
},
|
||||
replaceLogo: elements.replaceLogo.checked,
|
||||
autologin: {
|
||||
enabled: elements.autologinEnabled.checked,
|
||||
username: elements.autologinUsername.value.trim(),
|
||||
password: elements.autologinPassword.value,
|
||||
autoSubmit: elements.autologinAutoSubmit.checked,
|
||||
delay: autologinDelay
|
||||
}
|
||||
};
|
||||
// Сохраняем список отключённых сайтов из текущего storage
|
||||
chrome.storage.sync.get(['config'], (prev) => {
|
||||
if (prev.config && Array.isArray(prev.config.disabledSites)) {
|
||||
config.disabledSites = prev.config.disabledSites;
|
||||
} else {
|
||||
config.disabledSites = [];
|
||||
}
|
||||
|
||||
// Сохраняем в chrome.storage
|
||||
chrome.storage.sync.set({ config }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('Ошибка сохранения:', chrome.runtime.lastError);
|
||||
showStatus(getStr('options.statusSaveError') + ' ' + chrome.runtime.lastError.message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Настройки сохранены:', config);
|
||||
showStatus(getStr('options.statusSaveOk'), 'success');
|
||||
|
||||
// Отправляем сообщение всем вкладкам для обновления конфига
|
||||
chrome.tabs.query({}, (tabs) => {
|
||||
tabs.forEach(tab => {
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'CONFIG_UPDATED',
|
||||
config: config
|
||||
}, (response) => {
|
||||
// Игнорируем ошибки для вкладок, где нет content script
|
||||
if (chrome.runtime.lastError) {
|
||||
// Это нормально, не все вкладки имеют content script
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Сброс настроек к значениям по умолчанию
|
||||
function resetSettings() {
|
||||
if (!confirm(getStr('options.confirmReset'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Устанавливаем значения по умолчанию
|
||||
elements.portVideo.value = DEFAULT_CONFIG.ports.video;
|
||||
elements.portScheme.value = DEFAULT_CONFIG.ports.scheme;
|
||||
elements.novncRedirect.checked = DEFAULT_CONFIG.novnc.mode === 'redirect';
|
||||
elements.redirectDelay.value = DEFAULT_CONFIG.novnc.redirectDelay;
|
||||
elements.mnemoMode.value = DEFAULT_CONFIG.mnemo.mode;
|
||||
elements.mnemoSkipOnDomain.checked = DEFAULT_CONFIG.mnemo.skipOnDomain;
|
||||
elements.mnemoAddSchemeButton.checked = DEFAULT_CONFIG.mnemo.addSchemeButton;
|
||||
elements.mnemoAddVideoButton.checked = DEFAULT_CONFIG.mnemo.addVideoButton;
|
||||
elements.replaceLogo.checked = DEFAULT_CONFIG.replaceLogo !== false;
|
||||
elements.autologinEnabled.checked = DEFAULT_CONFIG.autologin.enabled;
|
||||
elements.autologinUsername.value = DEFAULT_CONFIG.autologin.username;
|
||||
elements.autologinPassword.value = DEFAULT_CONFIG.autologin.password;
|
||||
elements.autologinAutoSubmit.checked = DEFAULT_CONFIG.autologin.autoSubmit;
|
||||
elements.autologinDelay.value = DEFAULT_CONFIG.autologin.delay;
|
||||
elements.debugEnabled.checked = DEFAULT_CONFIG.debug.enabled;
|
||||
elements.debugCore.checked = DEFAULT_CONFIG.debug.modules.core;
|
||||
elements.debugVideo.checked = DEFAULT_CONFIG.debug.modules.video;
|
||||
elements.debugMnemo.checked = DEFAULT_CONFIG.debug.modules.mnemo;
|
||||
elements.debugClose.checked = DEFAULT_CONFIG.debug.modules.close;
|
||||
elements.debugContent.checked = DEFAULT_CONFIG.debug.modules.content;
|
||||
elements.debugNovnc.checked = DEFAULT_CONFIG.debug.modules.novnc;
|
||||
elements.debugAutologin.checked = DEFAULT_CONFIG.debug.modules.autologin;
|
||||
|
||||
// Сохраняем
|
||||
chrome.storage.sync.set({ config: DEFAULT_CONFIG }, () => {
|
||||
console.log('Настройки сброшены к значениям по умолчанию');
|
||||
showStatus(getStr('options.statusResetDone'), 'success');
|
||||
toggleDebugModules();
|
||||
toggleRedirectDelay();
|
||||
toggleAutologinSettings();
|
||||
});
|
||||
}
|
||||
|
||||
// Показать/скрыть модули отладки (при загрузке без .expanded блок изначально скрыт)
|
||||
function toggleDebugModules() {
|
||||
if (elements.debugEnabled.checked) {
|
||||
elements.debugModules.classList.add('expanded');
|
||||
} else {
|
||||
elements.debugModules.classList.remove('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
// Показать/скрыть поле задержки переадресации
|
||||
function toggleRedirectDelay() {
|
||||
if (elements.novncRedirect.checked) {
|
||||
elements.redirectDelayContainer.style.display = 'block';
|
||||
} else {
|
||||
elements.redirectDelayContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Показать/скрыть настройки автологина
|
||||
function toggleAutologinSettings() {
|
||||
if (elements.autologinEnabled.checked) {
|
||||
elements.autologinSettings.classList.remove('disabled');
|
||||
} else {
|
||||
elements.autologinSettings.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
// Показать статус сообщение
|
||||
function showStatus(message, type = 'success') {
|
||||
elements.status.textContent = message;
|
||||
elements.status.className = `status ${type}`;
|
||||
elements.status.classList.remove('hidden');
|
||||
|
||||
// Автоматически скрыть через 5 секунд
|
||||
setTimeout(() => {
|
||||
elements.status.classList.add('hidden');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Валидация при вводе (только цифры для портов)
|
||||
elements.portVideo.addEventListener('input', (e) => {
|
||||
e.target.value = e.target.value.replace(/[^0-9]/g, '');
|
||||
});
|
||||
|
||||
elements.portScheme.addEventListener('input', (e) => {
|
||||
e.target.value = e.target.value.replace(/[^0-9]/g, '');
|
||||
});
|
||||
|
||||
// Тестирование chrome.storage при загрузке страницы
|
||||
console.log('=== Проверка chrome.storage API ===');
|
||||
console.log('chrome.storage доступен:', typeof chrome.storage !== 'undefined');
|
||||
console.log('chrome.storage.sync доступен:', typeof chrome.storage?.sync !== 'undefined');
|
||||
|
||||
// Тестовое сохранение и чтение
|
||||
if (typeof chrome.storage !== 'undefined' && chrome.storage.sync) {
|
||||
chrome.storage.sync.set({ test: 'test_value' }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('❌ Тест сохранения FAILED:', chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log('✅ Тест сохранения OK');
|
||||
chrome.storage.sync.get(['test'], (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('❌ Тест чтения FAILED:', chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log('✅ Тест чтения OK:', result);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('❌ chrome.storage.sync недоступен!');
|
||||
}
|
||||
159
js/popup.js
Normal file
159
js/popup.js
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// ==================== POPUP РАСШИРЕНИЯ ====================
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
debug: { enabled: true },
|
||||
disabledSites: [],
|
||||
autologin: { username: '', password: '', delay: 500 }
|
||||
};
|
||||
|
||||
const el = {
|
||||
disableOnSite: document.getElementById('disable-on-site'),
|
||||
disableOnSiteLabel: document.getElementById('disable-on-site-label'),
|
||||
debug: document.getElementById('debug'),
|
||||
btnLogin: document.getElementById('btn-login'),
|
||||
loginStatus: document.getElementById('login-status'),
|
||||
reloadRow: document.getElementById('reload-row'),
|
||||
btnReload: document.getElementById('btn-reload')
|
||||
};
|
||||
|
||||
function getStr(path) {
|
||||
const o = window.CAPS_STRINGS || {};
|
||||
const val = path.split('.').reduce((acc, k) => acc && acc[k], o);
|
||||
return (val !== undefined && val !== null) ? val : path;
|
||||
}
|
||||
function applyPopupStrings() {
|
||||
document.querySelectorAll('[data-i18n]').forEach((node) => {
|
||||
const key = node.getAttribute('data-i18n');
|
||||
const text = getStr(key);
|
||||
if (text === '') {
|
||||
node.style.display = 'none';
|
||||
} else if (text && node.tagName !== 'OPTION') {
|
||||
node.textContent = text;
|
||||
}
|
||||
});
|
||||
}
|
||||
function updateDisableOnSiteLabel() {
|
||||
if (!el.disableOnSiteLabel) return;
|
||||
el.disableOnSiteLabel.textContent = el.disableOnSite.checked ? getStr('popup.siteWorking') : getStr('popup.siteDisabled');
|
||||
}
|
||||
|
||||
let currentHostname = '';
|
||||
|
||||
function showLoginStatus(text, type) {
|
||||
el.loginStatus.textContent = text;
|
||||
el.loginStatus.className = 'status-text ' + (type || '');
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.sync.get(['config'], (result) => {
|
||||
const config = result.config || {};
|
||||
resolve(config);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function saveConfig(updates) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.sync.get(['config'], (result) => {
|
||||
const config = { ...(result.config || {}), ...updates };
|
||||
if (!Array.isArray(config.disabledSites)) config.disabledSites = [];
|
||||
chrome.storage.sync.set({ config }, () => {
|
||||
// Уведомить вкладки об обновлении конфига
|
||||
chrome.tabs.query({}, (tabs) => {
|
||||
tabs.forEach((tab) => {
|
||||
chrome.tabs.sendMessage(tab.id, { type: 'CONFIG_UPDATED', config }).catch(() => {});
|
||||
});
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getActiveTab() {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
resolve(tabs[0] || null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
applyPopupStrings();
|
||||
const [config, tab] = await Promise.all([loadConfig(), getActiveTab()]);
|
||||
currentHostname = tab ? new URL(tab.url || 'http://a').hostname : '';
|
||||
|
||||
/* Слайд вкл = расширение работает (сайт НЕ в списке отключённых) */
|
||||
const disabledSites = config.disabledSites || [];
|
||||
el.disableOnSite.checked = currentHostname ? !disabledSites.includes(currentHostname) : false;
|
||||
el.disableOnSite.disabled = !currentHostname;
|
||||
updateDisableOnSiteLabel();
|
||||
|
||||
const debug = config.debug || DEFAULT_CONFIG.debug;
|
||||
el.debug.checked = debug.enabled !== false;
|
||||
|
||||
function showReloadButton() {
|
||||
if (el.reloadRow) el.reloadRow.classList.remove('hidden');
|
||||
}
|
||||
|
||||
el.disableOnSite.addEventListener('change', async () => {
|
||||
updateDisableOnSiteLabel();
|
||||
let list = (await loadConfig()).disabledSites || [];
|
||||
if (!Array.isArray(list)) list = [];
|
||||
if (el.disableOnSite.checked) {
|
||||
list = list.filter((h) => h !== currentHostname);
|
||||
} else {
|
||||
if (currentHostname && !list.includes(currentHostname)) list.push(currentHostname);
|
||||
}
|
||||
await saveConfig({ disabledSites: list });
|
||||
showReloadButton();
|
||||
});
|
||||
|
||||
el.debug.addEventListener('change', async () => {
|
||||
const config = await loadConfig();
|
||||
const debug = config.debug || { modules: {} };
|
||||
await saveConfig({ debug: { ...debug, enabled: el.debug.checked } });
|
||||
showReloadButton();
|
||||
});
|
||||
|
||||
el.btnReload.addEventListener('click', async () => {
|
||||
const tab = await getActiveTab();
|
||||
if (tab && tab.id) {
|
||||
chrome.tabs.reload(tab.id);
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
el.btnLogin.addEventListener('click', async () => {
|
||||
const tab = await getActiveTab();
|
||||
if (!tab || !tab.id) {
|
||||
showLoginStatus(getStr('popup.statusNoTab'), 'error');
|
||||
return;
|
||||
}
|
||||
const config = await loadConfig();
|
||||
const user = (config.autologin || {}).username;
|
||||
const pass = (config.autologin || {}).password;
|
||||
if (!user || !pass) {
|
||||
showLoginStatus(getStr('popup.statusNoCredentials'), 'error');
|
||||
return;
|
||||
}
|
||||
el.btnLogin.disabled = true;
|
||||
showLoginStatus('…', '');
|
||||
try {
|
||||
const response = await chrome.tabs.sendMessage(tab.id, { type: 'PERFORM_LOGIN' });
|
||||
if (response && response.ok) {
|
||||
showLoginStatus(getStr('popup.statusLoginOk'), 'success');
|
||||
} else {
|
||||
const err = (response && response.error) || 'error';
|
||||
const msg = err === 'not_login_page' ? getStr('popup.statusNotLoginPage') : err === 'no_credentials' ? getStr('popup.statusNoCredentialsShort') : getStr('popup.statusLoginError');
|
||||
showLoginStatus(msg, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showLoginStatus(getStr('popup.statusOpenLoginPage'), 'error');
|
||||
}
|
||||
el.btnLogin.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
108
js/strings.js
Normal file
108
js/strings.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* Все пользовательские тексты расширения.
|
||||
* Редактируйте этот файл, чтобы изменить подписи, кнопки и сообщения в popup и на странице настроек.
|
||||
*/
|
||||
window.CAPS_STRINGS = {
|
||||
// ========== Всплывающее окно (popup) ==========
|
||||
popup: {
|
||||
title: "CAPS Enhancer",
|
||||
siteWorking: "Расширение работает на этом сайте",
|
||||
siteDisabled: "Расширение отключено на этом сайте",
|
||||
debug: "Дебаг",
|
||||
login: "Автологин",
|
||||
reload: "🔄 Перезагрузить страницу",
|
||||
fullSettings: "⚙️ Полные настройки",
|
||||
// Сообщения при нажатии «Войти»
|
||||
statusNoTab: "Нет активной вкладки",
|
||||
statusNoCredentials: "Укажите логин и пароль в настройках",
|
||||
statusLoginOk: "Вход выполнен",
|
||||
statusNotLoginPage: "Не на странице логина",
|
||||
statusNoCredentialsShort: "Нет данных входа",
|
||||
statusLoginError: "Ошибка входа",
|
||||
statusOpenLoginPage: "Откройте страницу входа и нажмите снова"
|
||||
},
|
||||
|
||||
// ========== Страница настроек (options) ==========
|
||||
options: {
|
||||
pageTitle: "CAPS Enhancer - Настройки",
|
||||
headerTitle: "Настройки CAPS Enhancer",
|
||||
headerSubtitle: "Настройте расширение под свои нужды",
|
||||
|
||||
// noVNC (страница экрана)
|
||||
novncTitle: "🖥️ Страница экрана",
|
||||
novncRedirectLabel: "Включить переадресацию",
|
||||
novncRedirectDesc: "Автоматически перенаправлять со страницы экрана на настройки (лещ)",
|
||||
novncDelayLabel: "Задержка (мс)",
|
||||
novncDelayPlaceholder: "500",
|
||||
novncPortLabel: "Порт настроек",
|
||||
novncPortPlaceholder: "5000",
|
||||
|
||||
// Мнемосхема
|
||||
mnemoTitle: "📍 Мнемосхема",
|
||||
mnemoAddSchemeButtonLabel: "Добавлять кнопку «Мнемосхема»",
|
||||
mnemoAddSchemeButtonDesc: "Показывать кнопку перехода на мнемосхему на главной странице",
|
||||
mnemoModeLabel: "Режим перехода",
|
||||
mnemoModeDesc: "Куда ведёт кнопка «Мнемосхема»",
|
||||
mnemoModeHostPort: "host:порт (http://текущий_хост:8080)",
|
||||
mnemoModeHostScheme: "host/scheme (http://текущий_хост/scheme)",
|
||||
mnemoAddVideoButtonLabel: "Добавлять кнопку «Лещ»",
|
||||
mnemoAddVideoButtonDesc: "Кнопка перехода в настройки рядом с «Мнемосхема»",
|
||||
mnemoSkipOnDomainLabel: "Не добавлять на доменах",
|
||||
mnemoSkipOnDomainDesc: "При доменах по порту на мнему не попасть",
|
||||
|
||||
mnemoPortLabel: "Порт мнемосхемы",
|
||||
mnemoPortPlaceholder: "8080",
|
||||
|
||||
// Внешний вид
|
||||
appearanceTitle: "🖼️ Внешний вид",
|
||||
replaceLogoLabel: "Заменять логотип CleverPark на иконку CAPS Enhancer",
|
||||
replaceLogoDesc: "", // На главной странице показывать icons/caps.svg вместо логотипа CleverPark
|
||||
|
||||
// Автологин
|
||||
autologinTitle: "🔐 Автологин",
|
||||
autologinEnabledLabel: "Включить автозаполнение",
|
||||
autologinEnabledDesc: "Автоматически заполнять логин и пароль на :5000/login",
|
||||
autologinUsernameLabel: "Логин",
|
||||
autologinUsernameDesc: "",
|
||||
autologinUsernamePlaceholder: "admin",
|
||||
autologinPasswordLabel: "Пароль",
|
||||
autologinPasswordDesc: "",
|
||||
autologinPasswordPlaceholder: "••••••••",
|
||||
autologinPasswordHint: "Пароль хранится в настройках Chrome!⚠️",
|
||||
autologinAutoSubmitLabel: "Автоматически нажимать «Вход»",
|
||||
autologinDelayLabel: "Задержка (мс)",
|
||||
autologinDelayPlaceholder: "500",
|
||||
|
||||
// Отладка
|
||||
debugTitle: "🐛 Отладка",
|
||||
debugEnabledLabel: "Включить логи в консоль",
|
||||
debugEnabledDesc: "Показывать отладочные сообщения",
|
||||
debugModulesTitle: "Модули для отладки",
|
||||
debugCore: "🟢 Core (ядро)",
|
||||
debugVideo: "🔵 Video (видео окна)",
|
||||
debugMnemo: "🟠 Mnemo (мнемосхема)",
|
||||
debugClose: "🔴 Close (закрытие)",
|
||||
debugContent: "🟣 Content (главный)",
|
||||
debugNovnc: "💠 NoVNC (переадресация)",
|
||||
debugAutologin: "🔐 AutoLogin (автовход)",
|
||||
|
||||
// Информация
|
||||
infoTitle: "ℹ️ Информация",
|
||||
infoVersion: "Версия:",
|
||||
infoName: "Название:",
|
||||
infoReloadHint: "После изменения настроек перезагрузите страницы для применения изменений.",
|
||||
|
||||
// Кнопки
|
||||
saveBtn: "💾 Сохранить",
|
||||
resetBtn: "🔄 Сбросить",
|
||||
|
||||
// Сообщения статуса
|
||||
statusSaveOk: "Настройки успешно сохранены! Перезагрузите страницы для применения.",
|
||||
statusLoadError: "Ошибка загрузки настроек",
|
||||
statusSaveError: "Ошибка сохранения настроек",
|
||||
statusResetDone: "Настройки сброшены к значениям по умолчанию",
|
||||
confirmReset: "Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?",
|
||||
errorPortVideo: "Ошибка: Неверный порт для видео (1-65535)",
|
||||
errorPortScheme: "Ошибка: Неверный порт для мнемосхемы (1-65535)"
|
||||
}
|
||||
};
|
||||
39
manifest.json
Normal file
39
manifest.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "CAPS Enhancer",
|
||||
"version": "2.7",
|
||||
"description": "Добавляет всякие фичи для УТП в CAPS",
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"permissions": ["activeTab", "storage", "tabs"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "CAPS Enhancer"
|
||||
},
|
||||
"options_page": "options.html",
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["icons/caps.svg"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": [
|
||||
"modules/core.js",
|
||||
"modules/video-window.js",
|
||||
"modules/mnemoschema.js",
|
||||
"modules/close-handler.js",
|
||||
"modules/novnc-redirect.js",
|
||||
"modules/auto-login.js",
|
||||
"js/content.js"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
]
|
||||
}
|
||||
157
modules/auto-login.js
Normal file
157
modules/auto-login.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// ==================== МОДУЛЬ АВТОЛОГИНА ====================
|
||||
|
||||
(function() {
|
||||
const core = window.VideoIPRedirect;
|
||||
if (!core) {
|
||||
console.error('[VideoIPRedirect] CORE не загружен!');
|
||||
return;
|
||||
}
|
||||
|
||||
const debugLog = (msg, ...args) => core.debugLog('autologin', msg, ...args);
|
||||
|
||||
debugLog('🔐 Загрузка модуля...');
|
||||
|
||||
// Проверка, является ли страница страницей логина
|
||||
function isLoginPage() {
|
||||
// Проверка 1: Порт 5000
|
||||
const port = window.location.port;
|
||||
if (port !== '5000') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверка 2: URL содержит /login
|
||||
const path = window.location.pathname;
|
||||
if (!path.includes('/login')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверка 3: Наличие полей логина и пароля
|
||||
const usernameField = document.querySelector('input[name="username"]');
|
||||
const passwordField = document.querySelector('input[name="password"]');
|
||||
|
||||
if (!usernameField || !passwordField) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Главная функция автологина
|
||||
function performAutoLogin() {
|
||||
debugLog('========== ПРОВЕРКА АВТОЛОГИНА ==========');
|
||||
|
||||
// Проверяем, включен ли автологин
|
||||
if (!core.CONFIG.autologin.enabled) {
|
||||
debugLog('⏹ Автологин отключен в настройках');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем, что это страница логина
|
||||
if (!isLoginPage()) {
|
||||
debugLog('❌ Это не страница логина');
|
||||
return false;
|
||||
}
|
||||
|
||||
debugLog('✅ Обнаружена страница логина');
|
||||
|
||||
// Получаем данные для входа
|
||||
const username = core.CONFIG.autologin.username;
|
||||
const password = core.CONFIG.autologin.password;
|
||||
|
||||
if (!username || !password) {
|
||||
debugLog('⚠️ Логин или пароль не заданы в настройках');
|
||||
return false;
|
||||
}
|
||||
|
||||
debugLog('🔑 Данные для входа найдены');
|
||||
|
||||
// Находим поля
|
||||
const usernameField = document.querySelector('input[name="username"]');
|
||||
const passwordField = document.querySelector('input[name="password"]');
|
||||
const submitButton = document.querySelector('button[type="submit"]');
|
||||
|
||||
if (!usernameField || !passwordField) {
|
||||
debugLog('❌ Не удалось найти поля ввода');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Заполняем поля
|
||||
debugLog('✍️ Заполнение полей...');
|
||||
usernameField.value = username;
|
||||
passwordField.value = password;
|
||||
|
||||
// Генерируем события для совместимости с JS-фреймворками
|
||||
usernameField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
usernameField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
passwordField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
passwordField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
debugLog('✅ Поля заполнены');
|
||||
|
||||
// Автоматический вход, если включен
|
||||
if (core.CONFIG.autologin.autoSubmit && submitButton) {
|
||||
const delay = core.CONFIG.autologin.delay || 500;
|
||||
debugLog(`⏱️ Вход через ${delay}мс...`);
|
||||
|
||||
setTimeout(() => {
|
||||
debugLog('🚀 Нажатие кнопки "Вход"');
|
||||
submitButton.click();
|
||||
}, delay);
|
||||
} else {
|
||||
debugLog('ℹ️ Автоматический вход отключен');
|
||||
}
|
||||
|
||||
debugLog('========== АВТОЛОГИН ЗАВЕРШЕН ==========');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Функция с повторными попытками (на случай если DOM еще не загружен)
|
||||
function tryAutoLogin(attempts = 3) {
|
||||
debugLog(`🔄 Попытка автологина (осталось попыток: ${attempts})`);
|
||||
|
||||
const success = performAutoLogin();
|
||||
|
||||
if (!success && attempts > 0) {
|
||||
setTimeout(() => tryAutoLogin(attempts - 1), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Однократный вход по кнопке (без проверки enabled, всегда отправка формы)
|
||||
function performLoginOnce() {
|
||||
if (!isLoginPage()) {
|
||||
return { ok: false, error: 'not_login_page' };
|
||||
}
|
||||
const username = core.CONFIG.autologin.username;
|
||||
const password = core.CONFIG.autologin.password;
|
||||
if (!username || !password) {
|
||||
return { ok: false, error: 'no_credentials' };
|
||||
}
|
||||
const usernameField = document.querySelector('input[name="username"]');
|
||||
const passwordField = document.querySelector('input[name="password"]');
|
||||
const submitButton = document.querySelector('button[type="submit"]');
|
||||
if (!usernameField || !passwordField) {
|
||||
return { ok: false, error: 'no_fields' };
|
||||
}
|
||||
usernameField.value = username;
|
||||
passwordField.value = password;
|
||||
usernameField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
usernameField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
passwordField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
passwordField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
if (submitButton) {
|
||||
const delay = core.CONFIG.autologin.delay || 500;
|
||||
setTimeout(() => submitButton.click(), delay);
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// Публичный API
|
||||
window.VideoIPRedirect.AutoLogin = {
|
||||
performAutoLogin: performAutoLogin,
|
||||
tryAutoLogin: tryAutoLogin,
|
||||
isLoginPage: isLoginPage,
|
||||
performLoginOnce: performLoginOnce
|
||||
};
|
||||
|
||||
debugLog('✅ Модуль загружен');
|
||||
})();
|
||||
239
modules/close-handler.js
Normal file
239
modules/close-handler.js
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// ==================== МОДУЛЬ ЗАКРЫТИЯ ====================
|
||||
|
||||
(function() {
|
||||
const core = window.VideoIPRedirect;
|
||||
if (!core) {
|
||||
console.error('[CAPS_Enhancer] CORE не загружен!');
|
||||
return;
|
||||
}
|
||||
|
||||
const debugLog = (msg, ...args) => core.debugLog('close', msg, ...args);
|
||||
const isElementVisible = core.isElementVisible;
|
||||
|
||||
debugLog('❌ Загрузка модуля...');
|
||||
|
||||
// Функция для поиска кнопки закрытия ПО ТОЧНОМУ СЕЛЕКТОРУ
|
||||
function findVideoCloseButton() {
|
||||
debugLog('========== ПОИСК КНОПКИ ЗАКРЫТИЯ ВИДЕО ==========');
|
||||
|
||||
// 1. Ищем по точному классу sc-heWoWI
|
||||
debugLog('Поиск по селектору: button.sc-heWoWI');
|
||||
let closeBtn = document.querySelector('button.sc-heWoWI');
|
||||
|
||||
if (!closeBtn) {
|
||||
// 2. Ищем по части класса
|
||||
debugLog('Поиск по селектору: button[class*="sc-heWoWI"]');
|
||||
closeBtn = document.querySelector('button[class*="sc-heWoWI"]');
|
||||
}
|
||||
|
||||
if (!closeBtn) {
|
||||
// 3. Ищем по href="#"
|
||||
debugLog('Поиск по селектору: button[href="#"]');
|
||||
closeBtn = document.querySelector('button[href="#"]');
|
||||
}
|
||||
|
||||
if (!closeBtn) {
|
||||
// 4. Ищем по img с close_icon.png
|
||||
debugLog('Поиск по селектору: button img[src*="close_icon.png"]');
|
||||
const closeImg = document.querySelector('button img[src*="close_icon.png"]');
|
||||
if (closeImg) {
|
||||
closeBtn = closeImg.closest('button');
|
||||
}
|
||||
}
|
||||
|
||||
if (!closeBtn) {
|
||||
// 5. Ищем во всех окнах с видео
|
||||
debugLog('Поиск в окнах с видео');
|
||||
const videos = document.querySelectorAll('img[src*="/getVideo/"]');
|
||||
|
||||
for (let video of videos) {
|
||||
let parent = video.closest('div[style*="fixed"], div[style*="absolute"], div[class*="sc-"]');
|
||||
if (parent) {
|
||||
const btn = parent.querySelector('button.sc-heWoWI, button[href="#"], button img[src*="close_icon.png"]')?.closest('button');
|
||||
if (btn) {
|
||||
closeBtn = btn;
|
||||
debugLog('✅ Найдена кнопка в окне с видео');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closeBtn) {
|
||||
debugLog('✅ КНОПКА ЗАКРЫТИЯ НАЙДЕНА!');
|
||||
debugLog(' Текст:', closeBtn.textContent?.trim() || 'нет текста');
|
||||
debugLog(' Класс:', closeBtn.className);
|
||||
debugLog(' Href:', closeBtn.getAttribute('href'));
|
||||
debugLog(' HTML:', closeBtn.outerHTML.substring(0, 100));
|
||||
debugLog(' Видима:', isElementVisible(closeBtn));
|
||||
|
||||
// Проверяем наличие img внутри
|
||||
const img = closeBtn.querySelector('img');
|
||||
if (img) {
|
||||
debugLog(' Изображение:', {
|
||||
src: img.src,
|
||||
alt: img.alt
|
||||
});
|
||||
}
|
||||
} else {
|
||||
debugLog('❌ КНОПКА ЗАКРЫТИЯ НЕ НАЙДЕНА');
|
||||
|
||||
// Выводим все кнопки для отладки
|
||||
debugLog('Все кнопки на странице:');
|
||||
document.querySelectorAll('button').forEach((btn, i) => {
|
||||
debugLog(` Кнопка ${i}:`, {
|
||||
class: btn.className,
|
||||
href: btn.getAttribute('href'),
|
||||
hasImg: !!btn.querySelector('img'),
|
||||
text: btn.textContent?.trim() || 'нет текста'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
debugLog('========== ПОИСК ЗАВЕРШЕН ==========');
|
||||
return closeBtn;
|
||||
}
|
||||
|
||||
// Функция для поиска кнопки "Закрыть карточку"
|
||||
function findCloseCardButton() {
|
||||
debugLog('========== ПОИСК КНОПКИ "ЗАКРЫТЬ КАРТОЧКУ" ==========');
|
||||
|
||||
const allButtons = document.querySelectorAll('button');
|
||||
|
||||
for (let button of allButtons) {
|
||||
const text = button.textContent?.toLowerCase().trim() || '';
|
||||
if (text.includes('закрыть карточку')) {
|
||||
debugLog('✅ КНОПКА "ЗАКРЫТЬ КАРТОЧКУ" НАЙДЕНА!');
|
||||
debugLog(' Текст:', button.textContent?.trim());
|
||||
debugLog(' Класс:', button.className);
|
||||
debugLog(' Href:', button.getAttribute('href'));
|
||||
return button;
|
||||
}
|
||||
if (text.includes('отменить')) {
|
||||
debugLog('✅ КНОПКА "отменить" НАЙДЕНА!');
|
||||
debugLog(' Текст:', button.textContent?.trim());
|
||||
debugLog(' Класс:', button.className);
|
||||
debugLog(' Href:', button.getAttribute('href'));
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('❌ Кнопка "Закрыть карточку" не найдена');
|
||||
return null;
|
||||
}
|
||||
|
||||
function setupEscapeHandler() {
|
||||
debugLog('========== НАСТРОЙКА ОБРАБОТЧИКА ESCAPE ==========');
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
debugLog('🔴🔴🔴 ESCAPE НАЖАТ! 🔴🔴🔴');
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 1. Сначала ищем кнопку "Закрыть карточку"
|
||||
debugLog('--- ЭТАП 1: Поиск "Закрыть карточку" ---');
|
||||
const closeCardBtn = findCloseCardButton();
|
||||
|
||||
if (closeCardBtn && isElementVisible(closeCardBtn)) {
|
||||
debugLog('🟢 НАЖИМАЕМ "ЗАКРЫТЬ КАРТОЧКУ" 🟢');
|
||||
closeCardBtn.click();
|
||||
debugLog('✅ Клик выполнен');
|
||||
|
||||
setTimeout(() => {
|
||||
if (core.VideoWindow) {
|
||||
core.VideoWindow.removeButtonsFromWindow();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Если нет, ищем кнопку закрытия видео
|
||||
debugLog('--- ЭТАП 2: Поиск кнопки закрытия видео ---');
|
||||
const videoCloseBtn = findVideoCloseButton();
|
||||
|
||||
if (videoCloseBtn && isElementVisible(videoCloseBtn)) {
|
||||
debugLog('🟢 НАЖИМАЕМ КНОПКУ ЗАКРЫТИЯ ВИДЕО 🟢');
|
||||
videoCloseBtn.click();
|
||||
debugLog('✅ Клик выполнен');
|
||||
|
||||
setTimeout(() => {
|
||||
if (core.VideoWindow) {
|
||||
core.VideoWindow.removeButtonsFromWindow();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('❌ НЕ УДАЛОСЬ НАЙТИ КНОПКУ ДЛЯ ЗАКРЫТИЯ');
|
||||
}
|
||||
});
|
||||
|
||||
debugLog('✅ Обработчик Escape установлен');
|
||||
}
|
||||
|
||||
// Функция для принудительного добавления обработчика на кнопку
|
||||
function setupDirectCloseHandler() {
|
||||
debugLog('Настройка прямого обработчика на кнопку закрытия');
|
||||
|
||||
const closeBtn = findVideoCloseButton();
|
||||
if (closeBtn) {
|
||||
// Сохраняем оригинальный обработчик
|
||||
const originalClick = closeBtn.onclick;
|
||||
|
||||
// Добавляем свой обработчик
|
||||
closeBtn.addEventListener('click', function() {
|
||||
debugLog('Кнопка закрытия нажата!');
|
||||
setTimeout(() => {
|
||||
if (core.VideoWindow) {
|
||||
core.VideoWindow.removeButtonsFromWindow();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
debugLog('✅ Прямой обработчик добавлен');
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для отладки - подсветка всех кнопок закрытия на странице
|
||||
function colorAllCloseButtons() {
|
||||
debugLog('🎨 Подсветка всех кнопок закрытия');
|
||||
|
||||
const selectors = [
|
||||
'button.sc-heWoWI',
|
||||
'button[class*="sc-heWoWI"]',
|
||||
'button[href="#"]',
|
||||
'button img[src*="close_icon.png"]'
|
||||
];
|
||||
|
||||
let count = 0;
|
||||
selectors.forEach(selector => {
|
||||
const buttons = document.querySelectorAll(selector);
|
||||
buttons.forEach(btn => {
|
||||
const button = btn.tagName === 'BUTTON' ? btn : btn.closest('button');
|
||||
if (button && !button.dataset.colored) {
|
||||
button.style.border = '3px solid red';
|
||||
button.style.boxShadow = '0 0 10px red';
|
||||
button.dataset.colored = 'true';
|
||||
count++;
|
||||
debugLog(` Кнопка #${count}:`, button.className || 'без класса');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
debugLog(`✅ Подсвечено кнопок: ${count}`);
|
||||
return count;
|
||||
}
|
||||
|
||||
window.VideoIPRedirect.CloseHandler = {
|
||||
setupEscapeHandler: setupEscapeHandler,
|
||||
setupDirectCloseHandler: setupDirectCloseHandler,
|
||||
findVideoCloseButton: findVideoCloseButton,
|
||||
findCloseCardButton: findCloseCardButton,
|
||||
colorAllCloseButtons: colorAllCloseButtons,
|
||||
isElementVisible: isElementVisible
|
||||
};
|
||||
|
||||
debugLog('✅ Модуль загружен');
|
||||
})();
|
||||
355
modules/core.js
Normal file
355
modules/core.js
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
// ==================== ЯДРО РАСШИРЕНИЯ ====================
|
||||
|
||||
// ============ ГЛАВНАЯ КОНФИГУРАЦИЯ РАСШИРЕНИЯ ============
|
||||
// Значения по умолчанию (будут перезаписаны из chrome.storage)
|
||||
let CONFIG = {
|
||||
// Настройки отладки
|
||||
debug: {
|
||||
enabled: true, // Главный переключатель (если false, все логи отключены)
|
||||
modules: {
|
||||
core: true, // Логи ядра
|
||||
video: true, // Логи модуля видео
|
||||
mnemo: true, // Логи модуля мнемосхемы
|
||||
close: true, // Логи модуля закрытия
|
||||
content: true, // Логи главного файла
|
||||
novnc: true, // Логи модуля noVNC
|
||||
autologin: true // Логи модуля автологина
|
||||
}
|
||||
},
|
||||
|
||||
// Настройки портов
|
||||
ports: {
|
||||
video: '5000', // Порт для видео/настроек
|
||||
scheme: '8080' // Порт для мнемосхемы
|
||||
},
|
||||
|
||||
// Настройки модуля noVNC
|
||||
novnc: {
|
||||
mode: 'button', // 'redirect' - автоматическая переадресация, 'button' - добавить кнопку
|
||||
redirectDelay: 500 // Задержка перед переадресацией (мс), только для mode: 'redirect'
|
||||
},
|
||||
|
||||
// Настройки кнопки "Мнемосхема"
|
||||
mnemo: {
|
||||
mode: 'host-port', // 'host-port' — host:порт мнемосхемы, 'host-scheme' — http://хост/scheme
|
||||
skipOnDomain: false, // не добавлять кнопку при заходе по домену (не по IP)
|
||||
addVideoButton: true, // добавлять кнопку "Лещ" (переход на host:порт видео)
|
||||
addSchemeButton: true // добавлять кнопку "Мнемосхема"
|
||||
},
|
||||
|
||||
// Настройки автологина
|
||||
autologin: {
|
||||
enabled: false, // Включить/выключить автологин
|
||||
username: '', // Логин для автозаполнения
|
||||
password: '', // Пароль для автозаполнения
|
||||
autoSubmit: true, // Автоматически нажимать кнопку "Вход"
|
||||
delay: 500 // Задержка перед автовходом (мс)
|
||||
},
|
||||
|
||||
// Сайты, на которых расширение отключено (массив hostname, например ['example.com'])
|
||||
disabledSites: [],
|
||||
|
||||
// Замена логотипа CleverPark на иконку CAPS (icons/caps.svg)
|
||||
replaceLogo: true
|
||||
};
|
||||
|
||||
// Обновление конфигурации (изменяет существующий объект, а не создает новый)
|
||||
function updateConfig(newConfig) {
|
||||
// Обновляем debug
|
||||
if (newConfig.debug) {
|
||||
CONFIG.debug.enabled = newConfig.debug.enabled;
|
||||
if (newConfig.debug.modules) {
|
||||
Object.assign(CONFIG.debug.modules, newConfig.debug.modules);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем ports
|
||||
if (newConfig.ports) {
|
||||
Object.assign(CONFIG.ports, newConfig.ports);
|
||||
}
|
||||
|
||||
// Обновляем novnc
|
||||
if (newConfig.novnc) {
|
||||
Object.assign(CONFIG.novnc, newConfig.novnc);
|
||||
}
|
||||
|
||||
// Обновляем mnemo (совместимость со старыми значениями mode)
|
||||
if (newConfig.mnemo) {
|
||||
const m = newConfig.mnemo;
|
||||
const mode = m.mode === 'ip-scheme' ? 'host-scheme' : (m.mode === 'skip-on-domain' || m.mode === 'default' ? 'host-port' : (m.mode || 'host-port'));
|
||||
CONFIG.mnemo.mode = mode;
|
||||
CONFIG.mnemo.skipOnDomain = m.skipOnDomain === true || m.mode === 'skip-on-domain';
|
||||
if (typeof m.addVideoButton === 'boolean') CONFIG.mnemo.addVideoButton = m.addVideoButton;
|
||||
else if (CONFIG.mnemo.addVideoButton === undefined) CONFIG.mnemo.addVideoButton = true;
|
||||
if (typeof m.addSchemeButton === 'boolean') CONFIG.mnemo.addSchemeButton = m.addSchemeButton;
|
||||
else if (CONFIG.mnemo.addSchemeButton === undefined) CONFIG.mnemo.addSchemeButton = true;
|
||||
}
|
||||
|
||||
// Обновляем autologin
|
||||
if (newConfig.autologin) {
|
||||
Object.assign(CONFIG.autologin, newConfig.autologin);
|
||||
}
|
||||
|
||||
// Обновляем disabledSites
|
||||
if (Array.isArray(newConfig.disabledSites)) {
|
||||
CONFIG.disabledSites = newConfig.disabledSites;
|
||||
}
|
||||
|
||||
// Обновляем replaceLogo
|
||||
if (typeof newConfig.replaceLogo === 'boolean') {
|
||||
CONFIG.replaceLogo = newConfig.replaceLogo;
|
||||
}
|
||||
|
||||
// Обновляем константы портов
|
||||
window.VideoIPRedirect.TARGET_PORT_VIDEO = CONFIG.ports.video;
|
||||
window.VideoIPRedirect.TARGET_PORT_SCHEME = CONFIG.ports.scheme;
|
||||
}
|
||||
|
||||
// Загрузка конфигурации из chrome.storage (возвращает Promise)
|
||||
function loadConfig() {
|
||||
return new Promise((resolve) => {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage) {
|
||||
chrome.storage.sync.get(['config'], (result) => {
|
||||
if (result.config) {
|
||||
// Обновляем CONFIG из сохранённых настроек
|
||||
updateConfig(result.config);
|
||||
debugLog('core', '⚙️ Конфигурация загружена из настроек:', CONFIG);
|
||||
} else {
|
||||
debugLog('core', 'ℹ️ Используется конфигурация по умолчанию');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
debugLog('core', 'ℹ️ chrome.storage недоступен, используется конфигурация по умолчанию');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Слушаем обновления конфигурации
|
||||
if (typeof chrome !== 'undefined' && chrome.runtime) {
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'CONFIG_UPDATED') {
|
||||
updateConfig(message.config);
|
||||
debugLog('core', '🔄 Конфигурация обновлена:', CONFIG);
|
||||
|
||||
// Отправляем событие об обновлении конфигурации
|
||||
document.dispatchEvent(new CustomEvent('VideoIPRedirect:ConfigUpdated', {
|
||||
detail: { config: CONFIG }
|
||||
}));
|
||||
|
||||
debugLog('core', '💡 Для применения всех изменений рекомендуется перезагрузить страницу');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Константы портов (для обратной совместимости)
|
||||
const TARGET_PORT_VIDEO = CONFIG.ports.video;
|
||||
const TARGET_PORT_SCHEME = CONFIG.ports.scheme;
|
||||
|
||||
// Улучшенная функция отладочного логирования
|
||||
function debugLog(module, ...args) {
|
||||
// Проверяем глобальный флаг и флаг конкретного модуля
|
||||
if (!CONFIG.debug.enabled || !CONFIG.debug.modules[module]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Цвета для разных модулей
|
||||
const moduleColors = {
|
||||
core: '#4CAF50', // Зелёный
|
||||
video: '#2196F3', // Синий
|
||||
mnemo: '#FF9800', // Оранжевый
|
||||
close: '#F44336', // Красный
|
||||
content: '#9C27B0', // Фиолетовый
|
||||
novnc: '#00BCD4', // Голубой
|
||||
autologin: '#8BC34A' // Светло-зелёный
|
||||
};
|
||||
|
||||
const color = moduleColors[module] || '#607D8B';
|
||||
const prefix = `[CAPS_Enhancer::${module.toUpperCase()}]`;
|
||||
|
||||
console.log(
|
||||
`%c${prefix}`,
|
||||
`background: ${color}; color: white; padding: 2px 5px; border-radius: 3px; font-weight: bold;`,
|
||||
...args
|
||||
);
|
||||
}
|
||||
|
||||
// ПРОСТАЯ проверка типа страницы
|
||||
function checkPageType() {
|
||||
debugLog('core', '=== ПРОВЕРКА ТИПА СТРАНИЦЫ ===');
|
||||
|
||||
// 1. Проверяем маркер приложения
|
||||
const hasAppMarker = checkAppMarker();
|
||||
debugLog('core', 'Маркер приложения:', hasAppMarker ? '✅' : '❌');
|
||||
|
||||
// 2. Проверяем логотип (/images/cleverpark.svg или alt="CLEVER PARK")
|
||||
const logoSelector = 'img[src*="cleverpark.svg"], img[src*="cleverpark"], img[alt*="CLEVER PARK"], img[alt*="CLEVER"]';
|
||||
const logo = document.querySelector(logoSelector);
|
||||
const hasLogo = !!logo;
|
||||
debugLog('core', 'Логотип CLEVER PARK:', hasLogo ? '✅' : '❌');
|
||||
if (logo) debugLog('core', ' Найден лого:', logo.src);
|
||||
|
||||
// 3. Проверяем признаки страницы noVNC (экран на 80 порту)
|
||||
const isNoVNC = checkNoVNCPage();
|
||||
debugLog('core', 'Страница noVNC:', isNoVNC ? '✅' : '❌');
|
||||
|
||||
const pageType = {
|
||||
isMainPage: hasLogo && hasAppMarker && !isNoVNC,
|
||||
isVideoPage: !hasLogo && hasAppMarker && !isNoVNC,
|
||||
isNoVNCPage: isNoVNC,
|
||||
hasAppMarker: hasAppMarker,
|
||||
hasLogo: hasLogo
|
||||
};
|
||||
|
||||
debugLog('core', 'ИТОГОВЫЙ ТИП СТРАНИЦЫ:',
|
||||
pageType.isNoVNCPage ? 'NOVNC (экран)' :
|
||||
pageType.isMainPage ? 'ГЛАВНАЯ' :
|
||||
pageType.isVideoPage ? 'ВИДЕО' :
|
||||
'НЕ ЦЕЛЕВАЯ');
|
||||
|
||||
return pageType;
|
||||
}
|
||||
|
||||
// Проверка страницы noVNC по характерным признакам
|
||||
function checkNoVNCPage() {
|
||||
// Признак 1: noscript с текстом "You need to enable JavaScript to run this app."
|
||||
const noscripts = document.querySelectorAll('noscript');
|
||||
let hasNoVNCNoscript = false;
|
||||
for (let noscript of noscripts) {
|
||||
if (noscript.textContent.includes('You need to enable JavaScript to run this app')) {
|
||||
hasNoVNCNoscript = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Признак 2: div с классом начинающимся на "app_AppWindow__"
|
||||
const appWindow = document.querySelector('div[class*="app_AppWindow__"]');
|
||||
const hasAppWindow = !!appWindow;
|
||||
|
||||
// Страница noVNC определяется по наличию обоих признаков
|
||||
return hasNoVNCNoscript && hasAppWindow;
|
||||
}
|
||||
|
||||
// Проверка маркера приложения
|
||||
function checkAppMarker() {
|
||||
const noscripts = document.querySelectorAll('noscript');
|
||||
for (let noscript of noscripts) {
|
||||
if (noscript.textContent.includes('You need to enable JavaScript')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return document.body?.textContent?.includes('You need to enable JavaScript') || false;
|
||||
}
|
||||
|
||||
// ПРОСТОЕ извлечение IP
|
||||
function extractIpAndPort(videoUrl) {
|
||||
if (!videoUrl) return null;
|
||||
const match = videoUrl.match(/\/getVideo\/http:\/\/([^\/:?#]+)(?::(\d+))?/i);
|
||||
return match ? { ip: match[1], originalPort: match[2] || '80' } : null;
|
||||
}
|
||||
|
||||
// Проверка видимости элемента (общая функция для всех модулей)
|
||||
function isElementVisible(element) {
|
||||
if (!element) return false;
|
||||
|
||||
try {
|
||||
const style = window.getComputedStyle(element);
|
||||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Создание стилизованной кнопки (общая функция)
|
||||
function createStyledButton(text, onClick, className = 'redirect-btn') {
|
||||
const button = document.createElement('button');
|
||||
button.className = className;
|
||||
button.textContent = text;
|
||||
button.style.cssText = `
|
||||
margin-left: 12px;
|
||||
padding: 6px 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid rgba(255,255,255,0.5);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
font-family: Arial, sans-serif;
|
||||
`;
|
||||
|
||||
button.onmouseover = () => {
|
||||
button.style.transform = 'scale(1.05)';
|
||||
button.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
|
||||
};
|
||||
|
||||
button.onmouseout = () => {
|
||||
button.style.transform = 'scale(1)';
|
||||
button.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
|
||||
};
|
||||
|
||||
button.onclick = onClick;
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
// Утилита для повторных попыток
|
||||
function retryWithBackoff(fn, maxAttempts = 3, delays = [500, 1500, 3000]) {
|
||||
delays.forEach((delay, index) => {
|
||||
setTimeout(() => {
|
||||
if (index < maxAttempts) {
|
||||
fn();
|
||||
}
|
||||
}, delay);
|
||||
});
|
||||
}
|
||||
|
||||
// Глобальный объект
|
||||
window.VideoIPRedirect = {
|
||||
CONFIG: CONFIG,
|
||||
debugLog: debugLog,
|
||||
checkPageType: checkPageType,
|
||||
extractIpAndPort: extractIpAndPort,
|
||||
isElementVisible: isElementVisible,
|
||||
createStyledButton: createStyledButton,
|
||||
retryWithBackoff: retryWithBackoff,
|
||||
TARGET_PORT_VIDEO: TARGET_PORT_VIDEO,
|
||||
TARGET_PORT_SCHEME: TARGET_PORT_SCHEME,
|
||||
activeSchemeButton: false,
|
||||
|
||||
// Для обратной совместимости
|
||||
DEBUG_CONFIG: CONFIG.debug
|
||||
};
|
||||
|
||||
debugLog('core', '✅ CORE: Модуль инициализирован');
|
||||
debugLog('core', '📌 СТРАНИЦА:', window.location.href);
|
||||
debugLog('core', '📌 USER AGENT:', navigator.userAgent);
|
||||
|
||||
// Флаг готовности конфигурации
|
||||
window.VideoIPRedirect.configReady = false;
|
||||
|
||||
// Загружаем конфигурацию из настроек
|
||||
loadConfig().then(() => {
|
||||
window.VideoIPRedirect.configReady = true;
|
||||
debugLog('core', '🎯 Конфигурация готова к использованию');
|
||||
|
||||
// Отправляем событие о готовности конфигурации
|
||||
document.dispatchEvent(new CustomEvent('VideoIPRedirect:ConfigReady', {
|
||||
detail: { config: CONFIG }
|
||||
}));
|
||||
});
|
||||
179
modules/mnemoschema.js
Normal file
179
modules/mnemoschema.js
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// ==================== МОДУЛЬ МНЕМОСХЕМЫ ====================
|
||||
|
||||
(function() {
|
||||
const core = window.VideoIPRedirect;
|
||||
if (!core) {
|
||||
console.error('[CAPS_Enhancer] CORE не загружен!');
|
||||
return;
|
||||
}
|
||||
|
||||
const debugLog = (msg, ...args) => core.debugLog('mnemo', msg, ...args);
|
||||
|
||||
debugLog('🏠 Загрузка модуля...');
|
||||
|
||||
// Функция для получения хоста из текущего URL
|
||||
function getCurrentHostFromUrl() {
|
||||
const hostname = window.location.hostname;
|
||||
debugLog('Используем hostname:', hostname);
|
||||
return hostname;
|
||||
}
|
||||
|
||||
// Функция для поиска ссылки на мнемосхему
|
||||
function findMnemoLink() {
|
||||
debugLog('Поиск ссылки на мнемосхему');
|
||||
|
||||
const allLinks = document.querySelectorAll('a');
|
||||
|
||||
for (let link of allLinks) {
|
||||
const text = link.textContent.toLowerCase().trim();
|
||||
const href = link.getAttribute('href') || '';
|
||||
|
||||
if (text.includes('мнемосхема') || href.includes('/scheme')) {
|
||||
debugLog('✅ Найдена!');
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('❌ Ссылка не найдена');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Кнопка "Лещ" — переход на host:порт_видео (тот же стиль, что и Мнемосхема)
|
||||
function createVideoButton(host) {
|
||||
const oldVideoBtn = document.querySelector('.video-redirect-btn');
|
||||
if (oldVideoBtn) oldVideoBtn.remove();
|
||||
|
||||
const portVideo = core.TARGET_PORT_VIDEO || core.CONFIG?.ports?.video || '5000';
|
||||
const targetUrl = `http://${host}:${portVideo}`;
|
||||
|
||||
const button = core.createStyledButton(
|
||||
'Лещ',
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(targetUrl, '_blank');
|
||||
},
|
||||
'video-redirect-btn'
|
||||
);
|
||||
button.setAttribute('data-host', host);
|
||||
button.setAttribute('data-port', portVideo);
|
||||
return button;
|
||||
}
|
||||
|
||||
// Функция для создания кнопки "Мнемосхема"
|
||||
function createButton(originalLink, host) {
|
||||
const oldBtn = document.querySelector('.scheme-redirect-btn');
|
||||
if (oldBtn) oldBtn.remove();
|
||||
|
||||
const mode = core.CONFIG?.mnemo?.mode || 'host-port';
|
||||
const currentHostWithPort = window.location.host || host;
|
||||
|
||||
let targetUrl;
|
||||
if (mode === 'host-scheme') {
|
||||
targetUrl = `http://${currentHostWithPort}/scheme`;
|
||||
} else {
|
||||
targetUrl = `http://${host}:${core.TARGET_PORT_SCHEME}`;
|
||||
}
|
||||
|
||||
const button = core.createStyledButton(
|
||||
'Мнемосхема',
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(targetUrl, '_blank');
|
||||
},
|
||||
'scheme-redirect-btn'
|
||||
);
|
||||
|
||||
// Для кнопки мнемосхемы используем <a> вместо <button> для совместимости
|
||||
button.href = '#';
|
||||
button.setAttribute('data-host', host);
|
||||
button.setAttribute('data-port', core.TARGET_PORT_SCHEME);
|
||||
button.style.textDecoration = 'none';
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
// Главная функция
|
||||
function addButton() {
|
||||
debugLog('========== НАЧАЛО ==========');
|
||||
|
||||
const pageType = core.checkPageType();
|
||||
if (!pageType.isMainPage) {
|
||||
debugLog('Это НЕ главная страница');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Пропуск по домену: не добавлять кнопку, если зашли по домену (не по IP)
|
||||
const skipOnDomain = core.CONFIG?.mnemo?.skipOnDomain === true;
|
||||
if (skipOnDomain) {
|
||||
const hostname = window.location.hostname;
|
||||
const isIp = /^[0-9.]+$/.test(hostname);
|
||||
if (!isIp) {
|
||||
debugLog('skipOnDomain: заход по домену, кнопку не добавляем');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (core.activeSchemeButton) {
|
||||
debugLog('Кнопка уже добавлена');
|
||||
return false;
|
||||
}
|
||||
|
||||
const mnemoLink = findMnemoLink();
|
||||
if (!mnemoLink) {
|
||||
debugLog('Ссылка не найдена');
|
||||
return false;
|
||||
}
|
||||
|
||||
const host = getCurrentHostFromUrl();
|
||||
debugLog('Используем host:', host);
|
||||
|
||||
const addVideo = core.CONFIG?.mnemo?.addVideoButton !== false;
|
||||
const addScheme = core.CONFIG?.mnemo?.addSchemeButton !== false;
|
||||
if (!addVideo && !addScheme) {
|
||||
debugLog('Обе кнопки отключены в настройках');
|
||||
return false;
|
||||
}
|
||||
const wrapper = document.createElement('span');
|
||||
wrapper.className = 'scheme-buttons-wrapper';
|
||||
wrapper.style.display = 'inline-block';
|
||||
wrapper.style.verticalAlign = 'middle';
|
||||
if (addVideo) wrapper.appendChild(createVideoButton(host));
|
||||
if (addScheme) wrapper.appendChild(createButton(mnemoLink, host));
|
||||
mnemoLink.parentNode.replaceChild(wrapper, mnemoLink);
|
||||
|
||||
core.activeSchemeButton = true;
|
||||
debugLog('✅ Кнопка успешно добавлена!');
|
||||
debugLog('========== КОНЕЦ ==========');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
window.VideoIPRedirect.Mnemoschema = {
|
||||
addSchemeButton: addButton,
|
||||
removeSchemeButton: function() {
|
||||
const wrapper = document.querySelector('.scheme-buttons-wrapper');
|
||||
if (wrapper) {
|
||||
wrapper.remove();
|
||||
core.activeSchemeButton = false;
|
||||
return true;
|
||||
}
|
||||
const btn = document.querySelector('.scheme-redirect-btn');
|
||||
if (btn) {
|
||||
btn.remove();
|
||||
core.activeSchemeButton = false;
|
||||
return true;
|
||||
}
|
||||
const videoBtn = document.querySelector('.video-redirect-btn');
|
||||
if (videoBtn) {
|
||||
videoBtn.remove();
|
||||
core.activeSchemeButton = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
debugLog('✅ Модуль загружен');
|
||||
})();
|
||||
199
modules/novnc-redirect.js
Normal file
199
modules/novnc-redirect.js
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
// ==================== МОДУЛЬ ПЕРЕАДРЕСАЦИИ NOVNC ====================
|
||||
|
||||
(function() {
|
||||
const core = window.VideoIPRedirect;
|
||||
if (!core) {
|
||||
console.error('[VideoIPRedirect] CORE не загружен!');
|
||||
return;
|
||||
}
|
||||
|
||||
const debugLog = (msg, ...args) => core.debugLog('novnc', msg, ...args);
|
||||
|
||||
debugLog('🖥️ Загрузка модуля...');
|
||||
|
||||
// Главная функция - обработка страницы noVNC (переадресация или кнопка)
|
||||
function handleNoVNCPage() {
|
||||
debugLog('========== ОБРАБОТКА СТРАНИЦЫ NOVNC ==========');
|
||||
|
||||
const pageType = core.checkPageType();
|
||||
|
||||
if (!pageType.isNoVNCPage) {
|
||||
debugLog('Это не страница noVNC, обработка не требуется');
|
||||
return false;
|
||||
}
|
||||
|
||||
debugLog('⚠️ Обнаружена страница noVNC (экран на 80 порту)');
|
||||
|
||||
// Получаем режим работы из конфига
|
||||
const mode = core.CONFIG.novnc.mode;
|
||||
debugLog('Режим работы:', mode);
|
||||
|
||||
if (mode === 'redirect') {
|
||||
// Режим автоматической переадресации
|
||||
return autoRedirect();
|
||||
} else if (mode === 'button') {
|
||||
// Режим добавления кнопки
|
||||
return addSettingsButton();
|
||||
} else {
|
||||
debugLog('❌ Неизвестный режим:', mode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Автоматическая переадресация
|
||||
function autoRedirect() {
|
||||
debugLog('========== АВТОМАТИЧЕСКАЯ ПЕРЕАДРЕСАЦИЯ ==========');
|
||||
|
||||
const currentUrl = window.location.href;
|
||||
const currentHost = window.location.hostname;
|
||||
const currentPort = window.location.port || '80';
|
||||
|
||||
debugLog('Текущий URL:', currentUrl);
|
||||
debugLog('Текущий хост:', currentHost);
|
||||
debugLog('Текущий порт:', currentPort);
|
||||
|
||||
// Проверяем, что мы на 80 порту
|
||||
if (currentPort !== '80' && currentPort !== '') {
|
||||
debugLog('❌ Не на 80 порту, переадресация не нужна');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Формируем новый URL с портом 5000 (настройки)
|
||||
const newUrl = `http://${currentHost}:${core.TARGET_PORT_VIDEO}`;
|
||||
|
||||
debugLog('🔄 ВЫПОЛНЯЮ ПЕРЕАДРЕСАЦИЮ...');
|
||||
debugLog(' С:', currentUrl);
|
||||
debugLog(' На:', newUrl);
|
||||
|
||||
// Показываем уведомление пользователю
|
||||
showRedirectNotification(newUrl);
|
||||
|
||||
// Выполняем переадресацию через задержку из конфига
|
||||
const delay = core.CONFIG.novnc.redirectDelay || 500;
|
||||
setTimeout(() => {
|
||||
debugLog('✅ Переадресация выполнена');
|
||||
window.location.href = newUrl;
|
||||
}, delay);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Добавление кнопки для перехода на настройки
|
||||
function addSettingsButton() {
|
||||
debugLog('========== ДОБАВЛЕНИЕ КНОПКИ НАСТРОЕК ==========');
|
||||
|
||||
// Проверяем, не добавлена ли уже кнопка
|
||||
if (document.querySelector('.novnc-settings-btn')) {
|
||||
debugLog('Кнопка уже добавлена');
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentHost = window.location.hostname;
|
||||
const settingsUrl = `http://${currentHost}:${core.TARGET_PORT_VIDEO}`;
|
||||
|
||||
debugLog('URL настроек:', settingsUrl);
|
||||
|
||||
// Создаём кнопку используя общую функцию стилизации
|
||||
const button = core.createStyledButton(
|
||||
`⚙️ Настройки (${currentHost}:${core.TARGET_PORT_VIDEO})`,
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
debugLog('Переход на настройки:', settingsUrl);
|
||||
window.location.href = settingsUrl;
|
||||
},
|
||||
'novnc-settings-btn'
|
||||
);
|
||||
|
||||
// Дополнительные стили для позиционирования
|
||||
button.style.position = 'fixed';
|
||||
button.style.top = '20px';
|
||||
button.style.right = '20px';
|
||||
button.style.zIndex = '999999';
|
||||
button.style.margin = '0';
|
||||
button.style.fontSize = '14px';
|
||||
button.style.padding = '10px 20px';
|
||||
|
||||
// Добавляем кнопку на страницу
|
||||
document.body.appendChild(button);
|
||||
|
||||
debugLog('✅ Кнопка настроек добавлена');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Показать уведомление о переадресации
|
||||
function showRedirectNotification(targetUrl) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'novnc-redirect-notification';
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: linear-gradient(135deg, #00BCD4 0%, #0097A7 100%);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
z-index: 999999;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
animation: fadeInScale 0.3s ease-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
max-width: 400px;
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="spinner"></div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 16px; margin-bottom: 5px;">🔄 Переадресация...</div>
|
||||
<div style="font-size: 12px; opacity: 0.9; font-weight: normal;">${targetUrl}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Добавляем стили для анимации и крутилки
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
transform: translate(-50%, -50%) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.novnc-redirect-notification .spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(notification);
|
||||
|
||||
debugLog('📢 Уведомление показано');
|
||||
}
|
||||
|
||||
// Публичный API
|
||||
window.VideoIPRedirect.NoVNCRedirect = {
|
||||
handleNoVNCPage: handleNoVNCPage,
|
||||
autoRedirect: autoRedirect,
|
||||
addSettingsButton: addSettingsButton
|
||||
};
|
||||
|
||||
debugLog('✅ Модуль загружен');
|
||||
})();
|
||||
135
modules/video-window.js
Normal file
135
modules/video-window.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// ==================== МОДУЛЬ ВИДЕО ОКОН ====================
|
||||
|
||||
(function() {
|
||||
const core = window.VideoIPRedirect;
|
||||
if (!core) {
|
||||
console.error('[CAPS_Enhancer] CORE не загружен!');
|
||||
return;
|
||||
}
|
||||
|
||||
const debugLog = (msg, ...args) => core.debugLog('video', msg, ...args);
|
||||
const isElementVisible = core.isElementVisible;
|
||||
|
||||
debugLog('🎬 Загрузка модуля...');
|
||||
|
||||
// Функция для поиска АКТИВНОГО окна с видео
|
||||
function findActiveVideoWindow() {
|
||||
debugLog('Поиск активного окна с видео');
|
||||
const videos = document.querySelectorAll('img[src*=":8888/screen"][src*="/getVideo/"]');
|
||||
debugLog(`Найдено видео: ${videos.length}`);
|
||||
|
||||
if (videos.length === 0) {
|
||||
debugLog('Видео не найдены');
|
||||
return null;
|
||||
}
|
||||
|
||||
const video = videos[0];
|
||||
|
||||
let parent = video.parentElement;
|
||||
let depth = 0;
|
||||
let windowElement = null;
|
||||
let header = null;
|
||||
|
||||
while (parent && depth < 15) {
|
||||
const potentialHeader = parent.querySelector('div[class*="header"], div.header');
|
||||
if (potentialHeader && potentialHeader.querySelector('p')) {
|
||||
header = potentialHeader;
|
||||
windowElement = parent;
|
||||
debugLog(`Найден заголовок на глубине ${depth}`);
|
||||
break;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (!header) {
|
||||
const headers = document.querySelectorAll('div[class*="header"], div.header');
|
||||
|
||||
for (let h of headers) {
|
||||
const parentWindow = h.closest('div[style*="fixed"], div[style*="absolute"]');
|
||||
if (parentWindow && isElementVisible(parentWindow)) {
|
||||
header = h;
|
||||
windowElement = parentWindow;
|
||||
debugLog('Найден заголовок через родительское окно');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (header && windowElement) {
|
||||
const title = header.querySelector('p')?.textContent?.trim() || '';
|
||||
debugLog('✅ Активное окно найдено');
|
||||
return { window: windowElement, header, title, video };
|
||||
}
|
||||
|
||||
debugLog('❌ Активное окно не найдено');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Функция для добавления кнопки в заголовок
|
||||
function addButtonToHeader(header, ip) {
|
||||
if (header.querySelector('.video-redirect-btn')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const button = core.createStyledButton(
|
||||
`🔗 ${ip}:${core.TARGET_PORT_VIDEO}`,
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(`http://${ip}:${core.TARGET_PORT_VIDEO}`, '_blank');
|
||||
},
|
||||
'video-redirect-btn'
|
||||
);
|
||||
|
||||
const p = header.querySelector('p');
|
||||
if (p) {
|
||||
p.style.display = 'inline-block';
|
||||
p.style.marginRight = '8px';
|
||||
p.insertAdjacentElement('afterend', button);
|
||||
} else {
|
||||
header.appendChild(button);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
// Главная функция
|
||||
function addButtonToActiveWindow() {
|
||||
debugLog('========== НАЧАЛО ==========');
|
||||
|
||||
const pageType = core.checkPageType();
|
||||
if (!pageType.isVideoPage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeWindow = findActiveVideoWindow();
|
||||
if (!activeWindow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const src = activeWindow.video.src || activeWindow.video.getAttribute('src');
|
||||
const ipData = core.extractIpAndPort(src);
|
||||
|
||||
if (!ipData) {
|
||||
debugLog('Не удалось извлечь IP');
|
||||
return false;
|
||||
}
|
||||
|
||||
addButtonToHeader(activeWindow.header, ipData.ip);
|
||||
debugLog('✅ Кнопка добавлена');
|
||||
debugLog('========== КОНЕЦ ==========');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Публичный API
|
||||
window.VideoIPRedirect.VideoWindow = {
|
||||
addButtonToVideoWindow: addButtonToActiveWindow,
|
||||
removeButtonsFromWindow: function() {
|
||||
const btns = document.querySelectorAll('.video-redirect-btn');
|
||||
btns.forEach(btn => btn.remove());
|
||||
}
|
||||
};
|
||||
|
||||
debugLog('✅ Модуль загружен');
|
||||
})();
|
||||
252
options.html
Normal file
252
options.html
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CAPS Enhancer - Настройки</title>
|
||||
<link rel="stylesheet" href="css/options.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<img src="icons/icon128.png" alt="CAPS Enhancer" class="header-logo" onerror="this.style.display='none'">
|
||||
<div>
|
||||
<h1 data-i18n="options.headerTitle">⚙️ Настройки CAPS Enhancer</h1>
|
||||
<p class="subtitle" data-i18n="options.headerSubtitle">Настройте расширение под свои нужды</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="settings-grid">
|
||||
<div class="settings-col">
|
||||
<!-- Настройки noVNC (страница экрана) -->
|
||||
<section class="settings-section">
|
||||
<h2 data-i18n="options.novncTitle">🖥️ Страница экрана</h2>
|
||||
<div class="setting-item">
|
||||
<label class="switch-label">
|
||||
<span class="label-text" data-i18n="options.novncRedirectLabel">Включить переадресацию</span>
|
||||
<span class="label-description" data-i18n="options.novncRedirectDesc">Автоматически перенаправлять со страницы экрана на настройки (порт видео)</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="novnc-redirect">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item inline" id="redirect-delay-container">
|
||||
<label for="redirect-delay">
|
||||
<span class="label-text" data-i18n="options.novncDelayLabel">Задержка (мс)</span>
|
||||
</label>
|
||||
<input type="number" id="redirect-delay" data-i18n-placeholder="options.novncDelayPlaceholder" placeholder="500" min="0" max="5000" step="100">
|
||||
</div>
|
||||
<div class="setting-item inline">
|
||||
<label for="port-video">
|
||||
<span class="label-text" data-i18n="options.novncPortLabel">Порт для перехода (настройки)</span>
|
||||
</label>
|
||||
<input type="text" id="port-video" data-i18n-placeholder="options.novncPortPlaceholder" placeholder="5000" maxlength="5">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Настройки кнопки "Мнемосхема" -->
|
||||
<section class="settings-section">
|
||||
<h2 data-i18n="options.mnemoTitle">📍 Мнемосхема</h2>
|
||||
<div class="setting-item">
|
||||
<label class="switch-label">
|
||||
<span class="label-text" data-i18n="options.mnemoAddSchemeButtonLabel">Добавлять кнопку «Мнемосхема»</span>
|
||||
<span class="label-description" data-i18n="options.mnemoAddSchemeButtonDesc">Показывать кнопку перехода на мнемосхему на главной странице</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="mnemo-add-scheme-button">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="mnemo-mode">
|
||||
<span class="label-text" data-i18n="options.mnemoModeLabel">Режим перехода</span>
|
||||
<span class="label-description" data-i18n="options.mnemoModeDesc">Куда ведёт кнопка «Мнемосхема»</span>
|
||||
</label>
|
||||
<select id="mnemo-mode">
|
||||
<option value="host-port" data-i18n="options.mnemoModeHostPort">host:порт</option>
|
||||
<option value="host-scheme" data-i18n="options.mnemoModeHostScheme">host/scheme</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label class="switch-label">
|
||||
<span class="label-text" data-i18n="options.mnemoAddVideoButtonLabel">Добавлять кнопку «Лещ»</span>
|
||||
<span class="label-description" data-i18n="options.mnemoAddVideoButtonDesc">Кнопка перехода на host:порт видео (настройки) рядом с «Мнемосхема»</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="mnemo-add-video-button">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label class="switch-label">
|
||||
<span class="label-text" data-i18n="options.mnemoSkipOnDomainLabel">Не добавлять кнопку при заходе по домену</span>
|
||||
<span class="label-description" data-i18n="options.mnemoSkipOnDomainDesc">Если заходите по имени домена, а не по IP — кнопку не показывать</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="mnemo-skip-on-domain">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item inline">
|
||||
<label for="port-scheme">
|
||||
<span class="label-text" data-i18n="options.mnemoPortLabel">Порт мнемосхемы</span>
|
||||
</label>
|
||||
<input type="text" id="port-scheme" data-i18n-placeholder="options.mnemoPortPlaceholder" placeholder="8080" maxlength="5">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Замена логотипа -->
|
||||
<section class="settings-section">
|
||||
<h2 data-i18n="options.appearanceTitle">🖼️ Внешний вид</h2>
|
||||
<div class="setting-item">
|
||||
<label class="switch-label">
|
||||
<span class="label-text" data-i18n="options.replaceLogoLabel">Заменять логотип CleverPark на иконку CAPS</span>
|
||||
<span class="label-description" data-i18n="options.replaceLogoDesc">На главной странице показывать icons/caps.svg вместо логотипа CleverPark</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="replace-logo">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Настройки автологина -->
|
||||
<section class="settings-section">
|
||||
<h2 data-i18n="options.autologinTitle">🔐 Автологин</h2>
|
||||
<div class="setting-item">
|
||||
<label class="switch-label">
|
||||
<span class="label-text" data-i18n="options.autologinEnabledLabel">Включить автозаполнение</span>
|
||||
<span class="label-description" data-i18n="options.autologinEnabledDesc">Автоматически заполнять логин и пароль на /login (порт 5000)</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="autologin-enabled">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="autologin-settings" class="sub-settings">
|
||||
<div class="setting-item">
|
||||
<label for="autologin-username">
|
||||
<span class="label-text" data-i18n="options.autologinUsernameLabel">Логин</span>
|
||||
<span class="label-description" data-i18n="options.autologinUsernameDesc">Имя пользователя для автоматического входа</span>
|
||||
</label>
|
||||
<input type="text" id="autologin-username" data-i18n-placeholder="options.autologinUsernamePlaceholder" placeholder="admin" autocomplete="off">
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="autologin-password">
|
||||
<span class="label-text" data-i18n="options.autologinPasswordLabel">Пароль</span>
|
||||
<span class="label-description" data-i18n="options.autologinPasswordDesc">Пароль для автологина (только для тестовых/локальных систем)</span>
|
||||
</label>
|
||||
<input type="text" id="autologin-password" class="password-field" data-i18n-placeholder="options.autologinPasswordPlaceholder" placeholder="••••••••" autocomplete="off">
|
||||
<small class="hint" data-i18n="options.autologinPasswordHint">⚠️ Пароль хранится в настройках Chrome. Не используйте важные или личные пароли.</small>
|
||||
</div>
|
||||
<div class="setting-item inline">
|
||||
<label class="switch-label">
|
||||
<span class="label-text" data-i18n="options.autologinAutoSubmitLabel">Автоматически нажимать "Вход"</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="autologin-autosubmit">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item inline">
|
||||
<label for="autologin-delay">
|
||||
<span class="label-text" data-i18n="options.autologinDelayLabel">Задержка (мс)</span>
|
||||
</label>
|
||||
<input type="number" id="autologin-delay" data-i18n-placeholder="options.autologinDelayPlaceholder" placeholder="500" min="0" max="5000" step="100">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="settings-col secondary">
|
||||
<!-- Настройки отладки -->
|
||||
<section class="settings-section">
|
||||
<h2 data-i18n="options.debugTitle">🐛 Отладка</h2>
|
||||
<div class="setting-item">
|
||||
<label class="switch-label">
|
||||
<span class="label-text" data-i18n="options.debugEnabledLabel">Включить логи в консоль</span>
|
||||
<span class="label-description" data-i18n="options.debugEnabledDesc">Показывать отладочные сообщения</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="debug-enabled">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="debug-modules" class="sub-settings">
|
||||
<h3 data-i18n="options.debugModulesTitle">Модули для отладки</h3>
|
||||
<div class="setting-item-compact">
|
||||
<label>
|
||||
<input type="checkbox" id="debug-core" class="debug-module">
|
||||
<span data-i18n="options.debugCore">🟢 Core (ядро)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item-compact">
|
||||
<label>
|
||||
<input type="checkbox" id="debug-video" class="debug-module">
|
||||
<span data-i18n="options.debugVideo">🔵 Video (видео окна)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item-compact">
|
||||
<label>
|
||||
<input type="checkbox" id="debug-mnemo" class="debug-module">
|
||||
<span data-i18n="options.debugMnemo">🟠 Mnemo (мнемосхема)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item-compact">
|
||||
<label>
|
||||
<input type="checkbox" id="debug-close" class="debug-module">
|
||||
<span data-i18n="options.debugClose">🔴 Close (закрытие)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item-compact">
|
||||
<label>
|
||||
<input type="checkbox" id="debug-content" class="debug-module">
|
||||
<span data-i18n="options.debugContent">🟣 Content (главный)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item-compact">
|
||||
<label>
|
||||
<input type="checkbox" id="debug-novnc" class="debug-module">
|
||||
<span data-i18n="options.debugNovnc">💠 NoVNC (переадресация)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item-compact">
|
||||
<label>
|
||||
<input type="checkbox" id="debug-autologin" class="debug-module">
|
||||
<span data-i18n="options.debugAutologin">🔐 AutoLogin (автовход)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Информация -->
|
||||
<section class="info-section">
|
||||
<h3 data-i18n="options.infoTitle">ℹ️ Информация</h3>
|
||||
<p><strong data-i18n="options.infoVersion">Версия:</strong> <span id="app-version"></span></p>
|
||||
<p><strong data-i18n="options.infoName">Название:</strong> CAPS Enhancer</p>
|
||||
<p data-i18n="options.infoReloadHint">После изменения настроек перезагрузите страницы для применения изменений.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Статус -->
|
||||
<div id="status" class="status hidden"></div>
|
||||
|
||||
<!-- Кнопки действий -->
|
||||
<div class="actions">
|
||||
<button id="save-btn" class="btn btn-primary" data-i18n="options.saveBtn">💾 Сохранить</button>
|
||||
<button id="reset-btn" class="btn btn-secondary" data-i18n="options.resetBtn">🔄 Сбросить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/strings.js"></script>
|
||||
<script src="js/options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
44
popup.html
Normal file
44
popup.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CAPS Enhancer</title>
|
||||
<link rel="stylesheet" href="css/popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="popup">
|
||||
<header class="popup-header">
|
||||
<span class="popup-title" data-i18n="popup.title"></span>
|
||||
</header>
|
||||
<div class="popup-body">
|
||||
<label class="row switch-row">
|
||||
<span class="label" id="disable-on-site-label" data-i18n="popup.siteWorking"></span>
|
||||
<span class="switch-wrap">
|
||||
<input type="checkbox" id="disable-on-site" class="switch-input" />
|
||||
<span class="switch-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="row switch-row">
|
||||
<span class="label" data-i18n="popup.debug"></span>
|
||||
<span class="switch-wrap">
|
||||
<input type="checkbox" id="debug" class="switch-input" />
|
||||
<span class="switch-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="row row-btn">
|
||||
<button type="button" id="btn-login" class="btn btn-primary" data-i18n="popup.login"></button>
|
||||
<span id="login-status" class="status-text"></span>
|
||||
</div>
|
||||
<div class="row row-reload hidden" id="reload-row">
|
||||
<button type="button" id="btn-reload" class="btn btn-reload" data-i18n="popup.reload"></button>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="popup-footer">
|
||||
<a href="options.html" target="_blank" class="footer-link" data-i18n="popup.fullSettings"></a>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="js/strings.js"></script>
|
||||
<script src="js/popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue