mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 18:27:52 +08:00
5509 lines
209 KiB
HTML
5509 lines
209 KiB
HTML
<html>
|
|
<head>
|
|
<style>
|
|
.oc {
|
|
--bg-white: #ffffff;
|
|
--bg-light: #f8fafc;
|
|
--bg-gray: #f1f5f9;
|
|
--text-primary: #374151;
|
|
--text-secondary: #64748b;
|
|
--text-title: #4d4d4d;
|
|
--border-color: #b1b1b1;
|
|
--border-light: #e2e8f0;
|
|
--hover-bg: #f8fafc;
|
|
--primary-color: #3b82f6;
|
|
--success-color: #059669;
|
|
--success-dark: #047857;
|
|
--tip-color: #f59e0b;
|
|
--error-color: #dc2626;
|
|
--warning-color: #ff00bb;
|
|
--watchdog-color: #b300ff;
|
|
--info-color: #3b82f6;
|
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
--radius-sm: 6px;
|
|
--radius-md: 6px;
|
|
--radius-lg: 8px;
|
|
--transition-fast: 0.15s ease;
|
|
--transition-normal: 0.3s ease;
|
|
|
|
--control-height: 32px;
|
|
--card-padding: 6px 8px;
|
|
--gap-size: 10px;
|
|
|
|
--row-1-height: 160px;
|
|
--row-2-height: 140px;
|
|
--row-3-height: 140px;
|
|
--row-4-height: 280px;
|
|
}
|
|
|
|
.oc[data-darkmode="true"] {
|
|
--bg-white: #1f2937;
|
|
--bg-light: #374151;
|
|
--bg-gray: #4b5563;
|
|
--text-primary: #ebebeb;
|
|
--text-secondary: #d0cfcf;
|
|
--text-title: #e5e7eb;
|
|
--border-color: #939393;
|
|
--border-light: #6b7280;
|
|
--hover-bg: #374151;
|
|
--primary-color: #3b82f6;
|
|
--success-color: #34d399;
|
|
--success-dark: #10b981;
|
|
--error-color: #ff7070;
|
|
--tip-color: #f9bb51;
|
|
--warning-color: #fa41c8;
|
|
--watchdog-color: #c147f5;
|
|
--info-color: #3b82f6;
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .plugin-toggle-slider {
|
|
background-color: #6b7280;
|
|
border-color: #6b7280;
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .plugin-toggle-switch input:checked + .plugin-toggle-slider {
|
|
background-color: var(--primary-color);
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .plugin-toggle-slider:before {
|
|
background-color: #f9fafb;
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .version-display .update-dot {
|
|
background: var(--error-color);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .announcement-banner {
|
|
background: linear-gradient(135deg, #3555bf, #6c83c5);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .announcement-banner::before {
|
|
background: linear-gradient(to right, #3555bf 80%, #3555bf 100%);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] #megaphone {
|
|
filter: brightness(0) invert(1);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .subscription-progress-bar {
|
|
background: #374151;
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .subscription-progress-fill.high {
|
|
background: var(--success-color);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .subscription-progress-fill.medium {
|
|
background: var(--tip-color);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .subscription-progress-fill.low {
|
|
background: var(--error-color);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .subscription-loading,
|
|
.oc[data-darkmode="true"] .subscription-error,
|
|
.oc[data-darkmode="true"] .subscription-no-info {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .subscription-error {
|
|
color: var(--error-color);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .subscription-nav-arrow.left {
|
|
border-right-color: var(--text-secondary);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .subscription-nav-arrow.right {
|
|
border-left-color: var(--text-secondary);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .subscription-nav-arrow.left:hover {
|
|
border-right-color: var(--primary-color);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .subscription-nav-arrow.right:hover {
|
|
border-left-color: var(--primary-color);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .dashboard-btn:disabled {
|
|
background: #4b5563 !important;
|
|
color: #9ca3af !important;
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .config-select {
|
|
background: var(--bg-white) !important;
|
|
border-color: var(--border-light) !important;
|
|
color: var(--text-primary) !important;
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .config-select option {
|
|
background: var(--bg-white) !important;
|
|
color: var(--text-primary) !important;
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .config-select:hover {
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .config-select:focus {
|
|
border-color: var(--primary-color);
|
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] .config-selector::after {
|
|
border-top-color: var(--text-secondary);
|
|
}
|
|
|
|
.oc[data-darkmode="true"] svg {
|
|
filter: none;
|
|
}
|
|
|
|
.oc * {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.oc {
|
|
font-family: var(--font-family-base);
|
|
line-height: 1.6;
|
|
color: var(--text-primary);
|
|
background-color: var(--bg-light);
|
|
width: 100%;
|
|
max-width: 100%;
|
|
box-sizing: border-box;
|
|
overflow-x: hidden;
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
|
|
.oc .main-cards-container {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
grid-template-rows: minmax(60px, auto) 1fr;
|
|
gap: 10px;
|
|
margin: 0 auto;
|
|
align-items: start;
|
|
max-width: 100%;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
overflow-x: auto;
|
|
overflow-y: visible;
|
|
}
|
|
|
|
.oc .announcement-card {
|
|
grid-column: 1 / 3;
|
|
grid-row: 1;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
box-sizing: border-box;
|
|
border: 1px solid var(--border-light);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
background: var(--bg-white);
|
|
display: flex;
|
|
align-items: stretch;
|
|
margin: 0;
|
|
padding: 12px;
|
|
}
|
|
|
|
.oc .main-card {
|
|
background: var(--bg-white);
|
|
border: 1px solid var(--border-light);
|
|
border-radius: var(--radius-lg);
|
|
padding: 12px;
|
|
box-shadow: var(--shadow-sm);
|
|
transition: all var(--transition-fast);
|
|
display: grid;
|
|
grid-template-rows: var(--row-1-height) var(--row-2-height) var(--row-3-height) var(--row-4-height);
|
|
gap: 8px;
|
|
overflow: visible;
|
|
grid-row: 2;
|
|
max-width: 100%;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.oc .main-card:first-of-type {
|
|
grid-column: 1;
|
|
}
|
|
|
|
.oc .main-card:last-of-type {
|
|
grid-column: 2;
|
|
}
|
|
|
|
.oc .card-row {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: stretch;
|
|
height: 100%;
|
|
max-width: 100%;
|
|
box-sizing: border-box;
|
|
min-width: 250px;
|
|
}
|
|
|
|
.oc .card-row:nth-child(1) {
|
|
min-height: var(--row-1-height);
|
|
}
|
|
|
|
.oc .card-row:nth-child(2) {
|
|
min-height: var(--row-2-height);
|
|
}
|
|
|
|
.oc .card-row:nth-child(3) {
|
|
min-height: var(--row-3-height);
|
|
}
|
|
|
|
.oc .card-row:nth-child(4) {
|
|
min-height: var(--row-4-height);
|
|
}
|
|
|
|
.oc .card-row.full-width .sub-card {
|
|
flex: 1;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.oc .card-row.dual-cards {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.oc .card-row.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 2fr);
|
|
gap: 8px;
|
|
max-width: 100%;
|
|
min-width: auto;
|
|
}
|
|
|
|
.oc .sub-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg-gray);
|
|
border: 1px solid var(--border-light);
|
|
border-radius: var(--radius-md);
|
|
padding: var(--card-padding);
|
|
position: relative;
|
|
height: 100%;
|
|
min-height: 0;
|
|
gap: 5px;
|
|
max-width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.oc .sub-card.stat-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
padding: 10px 6px;
|
|
min-height: 0;
|
|
}
|
|
|
|
.oc .sub-card.dashboard-container,
|
|
.oc .sub-card.quick-actions-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
min-height: 0;
|
|
}
|
|
|
|
.oc .card-title {
|
|
font-size: clamp(10px, 2.5vw, 14px);
|
|
font-weight: 600;
|
|
color: var(--text-title);
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
height: 20px;
|
|
flex-shrink: 0;
|
|
padding-left: 5px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
min-width: 0;
|
|
}
|
|
|
|
.oc .version-display-container {
|
|
position: absolute;
|
|
right: 10px;
|
|
display: flex;
|
|
gap: 8px;
|
|
z-index: 2;
|
|
}
|
|
|
|
.oc .card-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
.oc .config-file-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
.oc .config-file-top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
width: 100%;
|
|
}
|
|
|
|
.oc .card-value {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
line-height: 1.4;
|
|
word-break: break-word;
|
|
min-height: var(--control-height);
|
|
display: flex;
|
|
align-items: center;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.oc .card-controls {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.oc .card-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 6px;
|
|
margin-top: auto;
|
|
flex-wrap: nowrap;
|
|
flex-shrink: 0;
|
|
min-height: 28px;
|
|
padding-top: 3px;
|
|
}
|
|
|
|
.oc .core-status-row .card-actions {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.oc .core-status-row .sub-card {
|
|
gap: 20px;
|
|
}
|
|
|
|
.oc .core-main-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between !important;
|
|
gap: 10px;
|
|
flex: 0 0 auto;
|
|
min-height: var(--control-height);
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
}
|
|
|
|
.oc .core-status-toggle,
|
|
.oc .core-control-buttons {
|
|
min-width: 0;
|
|
}
|
|
|
|
.oc .core-status-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex: 1;
|
|
}
|
|
|
|
.oc .core-control-buttons {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
flex-shrink: 0;
|
|
flex-wrap: nowrap;
|
|
}
|
|
|
|
.oc .mode-row .sub-card {
|
|
justify-content: flex-start;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.oc .mode-row .sub-card .card-title {
|
|
height: 20px;
|
|
min-height: 20px;
|
|
margin-bottom: 12px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.oc .mode-row .sub-card:first-child .card-content {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: flex-start;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex: 1;
|
|
}
|
|
|
|
.oc .icon-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px;
|
|
height: 28px;
|
|
min-width: 28px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-white);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
padding: 0;
|
|
font-size: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.oc .icon-btn:hover {
|
|
background: var(--hover-bg) !important;
|
|
border-color: var(--primary-color) !important;
|
|
color: var(--primary-color) !important;
|
|
transform: translateY(-1px) !important;
|
|
box-shadow: var(--shadow-sm) !important;
|
|
}
|
|
|
|
.oc .icon-btn svg {
|
|
width: 14px !important;
|
|
height: 14px !important;
|
|
flex-shrink: 0 !important;
|
|
}
|
|
|
|
.oc .version-display {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
background: var(--bg-white);
|
|
border: 1px solid var(--border-light);
|
|
border-radius: var(--radius-sm);
|
|
padding: 2px 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
max-width: 160px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
height: 20px;
|
|
}
|
|
|
|
.oc .version-display .version-text {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.oc .version-display .update-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--error-color);
|
|
flex-shrink: 0;
|
|
animation: gentlePulse 4s cubic-bezier(.17,.67,.83,.67) infinite;
|
|
}
|
|
|
|
@keyframes gentlePulse {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.4;
|
|
transform: scale(0.9);
|
|
}
|
|
}
|
|
|
|
.oc .version-display:hover {
|
|
background: var(--hover-bg);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.oc .config-selector {
|
|
position: relative;
|
|
flex: 1;
|
|
}
|
|
|
|
.oc .config-select {
|
|
width: 100%;
|
|
height: var(--control-height);
|
|
padding: 5px 12px;
|
|
border: 1px solid var(--border-light) !important;
|
|
background-color: var(--bg-white) !important;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-white);
|
|
color: var(--text-primary);
|
|
font-size: 15px;
|
|
appearance: none;
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
box-sizing: border-box;
|
|
min-width: 0;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.oc .config-select:hover {
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.oc .config-select:focus {
|
|
outline: none;
|
|
border-color: var(--primary-color);
|
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.oc .config-select option {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
font-size: 15px;
|
|
line-height: 1.4;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
background: var(--bg-white);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.oc .config-selector::after {
|
|
content: '';
|
|
position: absolute;
|
|
right: 12px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 0;
|
|
height: 0;
|
|
border-left: 4px solid transparent;
|
|
border-right: 4px solid transparent;
|
|
border-top: 4px solid var(--text-secondary);
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
.oc .config-file-bottom .card-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
margin: 0;
|
|
flex-direction: row;
|
|
}
|
|
|
|
.oc .cbi-button-group {
|
|
display: flex;
|
|
background: var(--bg-white);
|
|
border-radius: var(--radius-sm);
|
|
padding: 2px;
|
|
gap: 2px;
|
|
position: relative;
|
|
border: 1px solid var(--border-light);
|
|
box-shadow: var(--shadow-sm);
|
|
height: var(--control-height);
|
|
align-items: center;
|
|
white-space: nowrap;
|
|
width: 100%;
|
|
}
|
|
|
|
.oc .cbi-button-group input[type="radio"] {
|
|
position: absolute;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
height: var(--control-height)-4px;
|
|
}
|
|
|
|
.oc .cbi-button-option {
|
|
flex: 1 1 0;
|
|
padding: 5px 10px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
border-radius: calc(var(--radius-sm) - 3px);
|
|
transition: all var(--transition-fast);
|
|
user-select: none;
|
|
text-align: center;
|
|
min-width: 0;
|
|
height: calc(var(--control-height) - 6px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
line-height: 1;
|
|
}
|
|
|
|
.oc .cbi-button-option:hover {
|
|
color: var(--text-primary);
|
|
background: rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.oc input[type="radio"]:checked + .cbi-button-option {
|
|
background: var(--primary-color);
|
|
color: white;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.oc .value-indicator {
|
|
font-weight: 500;
|
|
padding: 6px 6px;
|
|
border-radius: var(--radius-sm);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--bg-white);
|
|
color: var(--text-secondary);
|
|
border: 1px solid var(--border-light);
|
|
text-align: center;
|
|
height: var(--control-height);
|
|
min-width: 90px;
|
|
white-space: nowrap;
|
|
line-height: 1.2;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.oc .value-indicator b {
|
|
font-size: 12px;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
max-width: 100%;
|
|
display: inline-block;
|
|
}
|
|
|
|
.oc .help-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
color: var(--primary-color);
|
|
text-decoration: none;
|
|
opacity: 0.7;
|
|
transition: opacity var(--transition-fast);
|
|
margin-bottom: 2px;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
.oc .help-link:hover {
|
|
opacity: 1;
|
|
background: rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.oc .help-link svg {
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
|
|
.oc .dashboard-buttons,
|
|
.oc .quick-actions-buttons {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex: 1;
|
|
}
|
|
|
|
.oc .dashboard-btn {
|
|
font-size: 12px !important;
|
|
font-weight: 500 !important;
|
|
color: white !important;
|
|
cursor: pointer !important;
|
|
border-radius: var(--radius-md) !important;
|
|
border: none !important;
|
|
background: #1473e6;
|
|
background-color: var(--primary-color) !important;
|
|
transition: all var(--transition-fast) !important;
|
|
white-space: normal !important;
|
|
text-align: center !important;
|
|
text-decoration: none !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
line-height: 1.2 !important;
|
|
flex: 1 !important;
|
|
height: 42px !important;
|
|
outline: none !important;
|
|
padding: 2px 2px !important;
|
|
min-width: 45px;
|
|
margin: 0 !important;
|
|
word-break: normal;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.oc .dashboard-btn:hover {
|
|
background: #2563eb !important;
|
|
}
|
|
|
|
.oc .stat-label {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-align: center;
|
|
position: relative;
|
|
white-space: nowrap;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.oc .stat-label::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -5px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 20px;
|
|
height: 1px;
|
|
background-color: var(--border-color);
|
|
}
|
|
|
|
.oc .stat-value {
|
|
font-family: var(--font-family-mono);
|
|
font-size: clamp(9px, 2.2vw, 12px);
|
|
font-weight: 600;
|
|
color: var(--success-color);
|
|
text-align: center;
|
|
white-space: normal;
|
|
word-break: normal;
|
|
overflow-wrap: break-word;
|
|
margin-top: 5px;
|
|
min-height: 38px;
|
|
}
|
|
|
|
.oc .plugin-toggle-container {
|
|
margin-left: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.oc .plugin-toggle-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 44px;
|
|
height: 22px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.oc .plugin-toggle-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.oc .plugin-toggle-slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: #e5e7eb;
|
|
transition: 0.2s;
|
|
border-radius: 22px;
|
|
border: 1px solid var(--border-light);
|
|
}
|
|
|
|
.oc .plugin-toggle-slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 16px;
|
|
width: 16px;
|
|
left: 2px;
|
|
top: 2px;
|
|
background-color: white;
|
|
transition: 0.2s;
|
|
border-radius: 50%;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.oc .plugin-toggle-switch input:checked + .plugin-toggle-slider {
|
|
background-color: var(--primary-color);
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.oc .plugin-toggle-switch input:checked + .plugin-toggle-slider:before {
|
|
transform: translateX(22px);
|
|
}
|
|
|
|
.oc #logo_btn,
|
|
.oc #edit_overwrite {
|
|
display: inline-flex !important;
|
|
align-items: center !important;
|
|
justify-content: flex-start !important;
|
|
width: auto !important;
|
|
padding: 2px 6px !important;
|
|
gap: 6px !important;
|
|
font-size: 12px !important;
|
|
}
|
|
|
|
.oc #logo_btn .logo-text,
|
|
.oc #edit_overwrite .btn-text {
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
line-height: 12px !important;
|
|
}
|
|
|
|
.oc #logo_btn .mihomo-link {
|
|
color: var(--primary-color);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.oc .config-subscription-info {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin-left: 16px;
|
|
}
|
|
|
|
.oc .subscription-info-container {
|
|
background: var(--bg-white);
|
|
border: 1px solid var(--border-light);
|
|
border-radius: var(--radius-sm);
|
|
padding: var(--card-padding);
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
height: 180px;
|
|
box-shadow: var(--shadow-sm);
|
|
transition: all var(--transition-fast);
|
|
position: relative;
|
|
}
|
|
|
|
.oc .subscription-info-container:hover {
|
|
border-color: var(--primary-color);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.oc .subscription-info-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
flex: 1;
|
|
}
|
|
|
|
.oc .subscription-info-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
min-height: 24px;
|
|
min-width: 0;
|
|
gap: 8px;
|
|
}
|
|
|
|
.oc .subscription-info-details {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
flex: 1;
|
|
min-height: 80px;
|
|
padding-left: 20px;
|
|
padding-right: 20px;
|
|
}
|
|
|
|
.oc .subscription-progress {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin: auto 0;
|
|
}
|
|
|
|
.oc .subscription-progress-bar {
|
|
background: var(--bg-gray);
|
|
border-radius: 4px;
|
|
height: 12px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.oc .subscription-progress-fill {
|
|
height: 100%;
|
|
border-radius: 4px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.oc .subscription-progress-fill.high {
|
|
background: var(--success-color);
|
|
}
|
|
|
|
.oc .subscription-progress-fill.medium {
|
|
background: var(--tip-color);
|
|
}
|
|
|
|
.oc .subscription-progress-fill.low {
|
|
background: var(--error-color);
|
|
}
|
|
|
|
.oc .subscription-info-title {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
line-height: 1.4;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
display: block;
|
|
}
|
|
|
|
.oc .subscription-info-text {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
line-height: 1.4;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
display: block;
|
|
}
|
|
|
|
/* Multiple Providers Grid Layout */
|
|
.oc .subscription-providers-grid {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 8px 0;
|
|
max-width: 100%;
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
cursor: grab;
|
|
user-select: none;
|
|
}
|
|
|
|
.oc .subscription-providers-grid::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.oc .subscription-providers-grid.dragging {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.oc .subscription-providers-grid[data-count="1"] {
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.oc .subscription-providers-grid[data-count="2"],
|
|
.oc .subscription-providers-grid[data-count="3"] {
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.oc .provider-card {
|
|
background: transparent;
|
|
padding: 0;
|
|
transition: all 0.2s ease;
|
|
min-width: calc(33.333% - 8px);
|
|
flex: 0 0 calc(33.333% - 8px);
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
/* For 1-2 providers, allow them to fill available space */
|
|
.oc .subscription-providers-grid[data-count="1"] .provider-card {
|
|
min-width: 100%;
|
|
flex: 1 1 100%;
|
|
}
|
|
|
|
.oc .subscription-providers-grid[data-count="2"] .provider-card {
|
|
min-width: calc(50% - 6px);
|
|
flex: 1 1 calc(50% - 6px);
|
|
}
|
|
|
|
/* For 3+ providers, fixed 33.333% width */
|
|
/* data-count="3" uses the base rule: flex: 0 0 calc(33.333% - 8px) */
|
|
|
|
/* Dark mode support for provider cards */
|
|
.oc[data-darkmode="true"] .provider-card {
|
|
background: transparent;
|
|
}
|
|
|
|
.oc .subscription-loading,
|
|
.oc .subscription-error,
|
|
.oc .subscription-no-info {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-secondary);
|
|
font-size: 12px;
|
|
text-align: center;
|
|
min-height: 40px;
|
|
flex: 1;
|
|
}
|
|
|
|
.oc .subscription-error {
|
|
color: var(--error-color);
|
|
}
|
|
|
|
.oc .subscription-nav-arrow {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 0;
|
|
height: 0;
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
z-index: 3;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.oc .subscription-nav-arrow:hover {
|
|
opacity: 1;
|
|
transform: translateY(-50%) scale(1.2);
|
|
}
|
|
|
|
.oc .subscription-nav-arrow.disabled {
|
|
opacity: 0.2;
|
|
cursor: not-allowed;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.oc .subscription-nav-arrow.left {
|
|
left: 8px;
|
|
border-top: 6px solid transparent;
|
|
border-bottom: 6px solid transparent;
|
|
border-right: 8px solid var(--text-secondary);
|
|
}
|
|
|
|
.oc .subscription-nav-arrow.right {
|
|
right: 8px;
|
|
border-top: 6px solid transparent;
|
|
border-bottom: 6px solid transparent;
|
|
border-left: 8px solid var(--text-secondary);
|
|
}
|
|
|
|
.oc .subscription-nav-arrow.left:hover {
|
|
border-right-color: var(--primary-color);
|
|
}
|
|
|
|
.oc .subscription-nav-arrow.right:hover {
|
|
border-left-color: var(--primary-color);
|
|
}
|
|
|
|
.oc .config-file-name {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
flex: 1 1 0%;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
min-width: 0;
|
|
max-width: 100%;
|
|
padding-left: 20px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.oc .config-file-bottom {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
min-height: var(--control-height);
|
|
}
|
|
|
|
.oc .config-file-empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 20px;
|
|
padding: 40px 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.oc .config-file-empty-message {
|
|
font-size: 16px;
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.oc .config-upload-btn-large {
|
|
min-width: 200px !important;
|
|
height: 60px !important;
|
|
font-size: 16px !important;
|
|
font-weight: 600 !important;
|
|
padding: 0 30px !important;
|
|
border-radius: var(--radius-lg) !important;
|
|
background: var(--primary-color) !important;
|
|
color: white !important;
|
|
border: none !important;
|
|
cursor: pointer !important;
|
|
transition: all var(--transition-fast) !important;
|
|
box-shadow: var(--shadow-md) !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
gap: 12px !important;
|
|
}
|
|
|
|
.oc .config-upload-btn-large:hover {
|
|
background: #2563eb !important;
|
|
transform: translateY(-2px) !important;
|
|
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.3) !important;
|
|
}
|
|
|
|
.oc .config-upload-btn-large svg {
|
|
width: 20px !important;
|
|
height: 20px !important;
|
|
}
|
|
|
|
.oc .config-file-content.empty-state .config-file-bottom {
|
|
display: none;
|
|
}
|
|
|
|
.oc .file-info {
|
|
display: flex;
|
|
flex-wrap: nowrap;
|
|
flex-direction: row;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
min-width: 0;
|
|
max-width: 30vw;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.oc .file-info-item {
|
|
display: block;
|
|
align-items: center;
|
|
white-space: nowrap;
|
|
padding-right: 20px;
|
|
font-size: 13px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 240px;
|
|
}
|
|
|
|
.oc .announcement-banner {
|
|
background: linear-gradient(275deg, var(--primary-color), #1d4ed8);
|
|
color: white;
|
|
padding: 12px;
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
position: relative;
|
|
height: 38px;
|
|
box-shadow: var(--shadow-md);
|
|
grid-column: 1 / 3;
|
|
grid-row: 1;
|
|
width: 80%;
|
|
max-width: 95%;
|
|
justify-self: center;
|
|
align-self: center;
|
|
box-sizing: border-box;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.oc .announcement-content {
|
|
position: absolute;
|
|
white-space: nowrap;
|
|
padding-right: 50px;
|
|
left: 100%;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
z-index: 1;
|
|
font-weight: 500;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
animation-fill-mode: forwards;
|
|
animation-timing-function: linear;
|
|
}
|
|
|
|
.oc .announcement-content.scrolling {
|
|
animation-name: announceScroll;
|
|
opacity: 1;
|
|
}
|
|
|
|
.oc .announcement-content.paused {
|
|
animation-play-state: paused;
|
|
}
|
|
|
|
@keyframes announceScroll {
|
|
0% {
|
|
transform: translateY(-50%) translateX(0);
|
|
}
|
|
100% {
|
|
transform: translateY(-50%) translateX(var(--scroll-distance));
|
|
}
|
|
}
|
|
|
|
.oc .megaphone-container {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 40px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
z-index: 2;
|
|
border-top-left-radius: var(--radius-lg);
|
|
border-bottom-left-radius: var(--radius-lg);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.oc .announcement-banner::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 45px;
|
|
border-top-left-radius: var(--radius-lg);
|
|
border-bottom-left-radius: var(--radius-lg);
|
|
background: linear-gradient(to right, #1d4ed8 80%, #1d4ed8 100%);
|
|
z-index: 3;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.oc #megaphone {
|
|
position: relative;
|
|
z-index: 4;
|
|
filter: brightness(0) invert(1);
|
|
transform: scaleX(-1);
|
|
}
|
|
|
|
.oc .hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
@media screen and (max-width: 1200px) {
|
|
.oc .main-cards-container {
|
|
grid-template-columns: 1fr;
|
|
gap: 11px;
|
|
max-width: 95%;
|
|
width: 95%;
|
|
}
|
|
|
|
.oc .announcement-banner {
|
|
grid-column: 1;
|
|
width: 80%;
|
|
max-width: 95%;
|
|
}
|
|
|
|
.oc .announcement-card {
|
|
grid-column: 1;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.oc .main-card {
|
|
grid-column: 1;
|
|
grid-template-rows: auto auto auto auto;
|
|
max-width: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.oc .main-card:first-of-type {
|
|
grid-column: 1;
|
|
grid-row: 2;
|
|
}
|
|
|
|
.oc .main-card:last-of-type {
|
|
grid-column: 1;
|
|
grid-row: 3;
|
|
}
|
|
|
|
.oc .card-row.dual-cards {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
|
|
.oc .card-row.stats-grid {
|
|
grid-template-columns: repeat(4, 2fr);
|
|
}
|
|
|
|
.oc .config-selector {
|
|
min-width: 200px;
|
|
flex: 1;
|
|
}
|
|
|
|
.oc .config-select {
|
|
min-width: 200px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.oc .config-select option {
|
|
font-size: 14px;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.oc .card-row:nth-child(1),
|
|
.oc .card-row:nth-child(2),
|
|
.oc .card-row:nth-child(3),
|
|
.oc .card-row:nth-child(4) {
|
|
min-height: auto;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 768px) {
|
|
.oc .main-cards-container {
|
|
max-width: 98%;
|
|
width: 98%;
|
|
gap: 10px;
|
|
}
|
|
|
|
.oc .main-card {
|
|
grid-template-rows: auto auto auto auto;
|
|
}
|
|
|
|
.oc .sub-card {
|
|
padding: 12px;
|
|
min-height: auto;
|
|
}
|
|
|
|
.oc .card-row {
|
|
gap: 12px;
|
|
}
|
|
|
|
.oc .card-row.stats-grid {
|
|
grid-template-columns: repeat(2, 2fr);
|
|
}
|
|
|
|
.oc .announcement-banner {
|
|
width: 85%;
|
|
max-width: 98%;
|
|
}
|
|
|
|
.oc .dashboard-btn {
|
|
font-size: 10px !important;
|
|
height: 40px !important;
|
|
}
|
|
|
|
.oc .card-value {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.oc .value-indicator {
|
|
font-size: 11px;
|
|
padding: 5px 5px;
|
|
}
|
|
|
|
.oc .cbi-button-option {
|
|
padding: 6px 6px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.oc .card-row:nth-child(1),
|
|
.oc .card-row:nth-child(2),
|
|
.oc .card-row:nth-child(3),
|
|
.oc .card-row:nth-child(4) {
|
|
min-height: auto;
|
|
}
|
|
|
|
.oc .config-file-content {
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.oc .config-selector {
|
|
min-width: 150px;
|
|
flex: 1;
|
|
}
|
|
|
|
.oc .config-select {
|
|
min-width: 150px;
|
|
font-size: 13px;
|
|
padding: 4px 10px;
|
|
}
|
|
|
|
.oc .config-select option {
|
|
font-size: 13px;
|
|
padding: 6px 10px;
|
|
}
|
|
|
|
.oc .config-selector::after {
|
|
right: 10px;
|
|
border-left: 3px solid transparent;
|
|
border-right: 3px solid transparent;
|
|
border-top: 3px solid var(--text-secondary);
|
|
}
|
|
|
|
.oc .config-subscription-info {
|
|
margin-left: 12px;
|
|
}
|
|
|
|
.oc .subscription-info-container {
|
|
padding: 8px;
|
|
}
|
|
|
|
.oc .subscription-info-header {
|
|
gap: 8px;
|
|
}
|
|
|
|
.oc .config-file-name {
|
|
max-width: 100%;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.oc .config-file-bottom {
|
|
flex-direction: row;
|
|
align-items: stretch;
|
|
gap: 8px;
|
|
}
|
|
|
|
.oc .config-file-bottom .card-actions {
|
|
justify-content: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.oc .subscription-info-text {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.oc .subscription-info-title {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.oc .subscription-progress-bar {
|
|
height: 10px;
|
|
}
|
|
|
|
.oc .file-info {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.oc .file-info-item {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.oc .config-upload-btn-large {
|
|
min-width: 180px !important;
|
|
height: 50px !important;
|
|
font-size: 14px !important;
|
|
padding: 0 24px !important;
|
|
}
|
|
|
|
.oc .config-file-empty-message {
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 575px) {
|
|
.oc .main-cards-container {
|
|
gap: 8px;
|
|
max-width: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.oc .main-card {
|
|
padding: 8px;
|
|
}
|
|
|
|
.oc .card-row {
|
|
gap: 10px;
|
|
}
|
|
|
|
.oc .card-row.dual-cards {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.oc .card-row.stats-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.oc .announcement-banner {
|
|
width: 90%;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.oc .icon-btn {
|
|
width: 24px;
|
|
height: 24px;
|
|
min-width: 24px;
|
|
}
|
|
|
|
.oc .icon-btn svg {
|
|
width: 12px !important;
|
|
height: 12px !important;
|
|
}
|
|
|
|
.oc .version-display {
|
|
max-width: 100px;
|
|
font-size: 9px;
|
|
padding: 1px 4px;
|
|
}
|
|
|
|
.oc .dashboard-btn {
|
|
font-size: 9px !important;
|
|
height: 25px !important;
|
|
}
|
|
|
|
.oc .cbi-button-group {
|
|
padding: 2px;
|
|
gap: 1px;
|
|
height: 28px;
|
|
}
|
|
|
|
.oc input[type="radio"]:checked + .cbi-button-option {
|
|
height: 22px;
|
|
}
|
|
|
|
.oc .cbi-button-option {
|
|
padding: 4px 4px;
|
|
font-size: 10px;
|
|
min-width: 35px;
|
|
}
|
|
|
|
.oc .config-file-top {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 12px;
|
|
}
|
|
|
|
.oc .config-subscription-info {
|
|
margin-left: 0;
|
|
justify-content: center;
|
|
}
|
|
|
|
.oc .subscription-info-header {
|
|
gap: 6px;
|
|
}
|
|
|
|
.oc .file-info {
|
|
gap: 4px;
|
|
font-size: 8px;
|
|
}
|
|
|
|
.oc .mode-row .sub-card:first-child .card-content {
|
|
flex-direction: row;
|
|
gap: 8px;
|
|
}
|
|
|
|
.oc .card-row:nth-child(1),
|
|
.oc .card-row:nth-child(2),
|
|
.oc .card-row:nth-child(3),
|
|
.oc .card-row:nth-child(4) {
|
|
min-height: auto;
|
|
}
|
|
|
|
.oc .config-selector {
|
|
min-width: 110px;
|
|
flex: 1;
|
|
}
|
|
|
|
.oc .config-select {
|
|
min-width: 110px;
|
|
font-size: 12px;
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
.oc .config-select option {
|
|
font-size: 12px;
|
|
padding: 5px 8px;
|
|
}
|
|
|
|
.oc .config-selector::after {
|
|
right: 8px;
|
|
}
|
|
|
|
.oc .card-value {
|
|
font-size: 11px;
|
|
min-height: 28px;
|
|
}
|
|
|
|
.oc .value-indicator {
|
|
padding: 4px 4px;
|
|
min-width: 50px;
|
|
height: 28px;
|
|
}
|
|
|
|
.oc .value-indicator b {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.oc .stat-label {
|
|
font-size: 9px;
|
|
}
|
|
|
|
.oc .stat-value {
|
|
min-height: 30px;
|
|
}
|
|
|
|
.oc .subscription-info-text {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.oc .subscription-info-title {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.oc .config-file-name {
|
|
font-size: 15px;
|
|
}
|
|
|
|
.oc .sub-card {
|
|
padding: 8px;
|
|
gap: 4px;
|
|
}
|
|
|
|
.oc .sub-card.stat-item {
|
|
padding: 8px 4px;
|
|
}
|
|
|
|
.oc .card-content {
|
|
gap: 8px;
|
|
}
|
|
|
|
.oc .card-controls {
|
|
gap: 8px;
|
|
}
|
|
|
|
.oc .core-main-controls {
|
|
gap: 8px;
|
|
min-height: 28px;
|
|
}
|
|
|
|
.oc .core-control-buttons {
|
|
max-width: 120px;
|
|
gap: 6px;
|
|
}
|
|
|
|
.oc .card-actions {
|
|
gap: 6px;
|
|
min-height: 24px;
|
|
}
|
|
|
|
.oc #logo_btn .logo-text,
|
|
.oc #edit_overwrite .btn-text {
|
|
font-size: 8px;
|
|
}
|
|
|
|
.oc #logo_btn .mihomo-link {
|
|
font-size: 8px;
|
|
}
|
|
|
|
.oc .file-info-item {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.oc .config-upload-btn-large {
|
|
min-width: 160px !important;
|
|
height: 45px !important;
|
|
font-size: 13px !important;
|
|
padding: 0 20px !important;
|
|
}
|
|
|
|
.oc .config-file-empty-state {
|
|
padding: 30px 15px;
|
|
gap: 15px;
|
|
}
|
|
|
|
.oc .config-file-empty-message {
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<%
|
|
local uci = require("luci.model.uci").cursor()
|
|
local fs = require "luci.openclash"
|
|
local RELEASE_BRANCH = fs.uci_get_config("config", "release_branch")
|
|
local random = tostring(os.time()):reverse():sub(1, 9)
|
|
%>
|
|
<fieldset class="cbi-section">
|
|
<table width="100%">
|
|
<tr>
|
|
<td colspan="4">
|
|
<div class="oc">
|
|
<div class="main-cards-container">
|
|
<div class="announcement-card">
|
|
<div id="announcement-banner" class="announcement-banner">
|
|
<div class="megaphone-container"></div>
|
|
<svg id="megaphone" xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" viewBox="0 0 256 256">
|
|
<path d="M228.54,86.66l-176.06-54A16,16,0,0,0,32,48V192a16,16,0,0,0,16,16,16,16,0,0,0,4.52-.65L136,181.73V192a16,16,0,0,0,16,16h32a16,16,0,0,0,16-16v-29.9l28.54-8.75A16.09,16.09,0,0,0,240,138V102A16.09,16.09,0,0,0,228.54,86.66ZM136,165,48,192V48l88,27Zm48,27H152V176.82L184,167Zm40-54-.11,0L152,160.08V79.92l71.89,22,.11,0v36Z"></path>
|
|
</svg>
|
|
<div id="announcement-content" class="announcement-content"></div>
|
|
</div>
|
|
</div>
|
|
<div class="main-card">
|
|
<!-- Row 1: Running Status -->
|
|
<div class="card-row full-width core-status-row">
|
|
<div class="sub-card">
|
|
<div class="card-title"><%:Running Status%></div>
|
|
<div class="version-display-container">
|
|
<div class="version-display" id="plugin-version-display" style="display: none;">
|
|
<span class="version-text" id="plugin-version-text">-</span>
|
|
</div>
|
|
<div class="version-display" id="core-version-display" style="display: none;">
|
|
<span class="version-text" id="core-version-text">-</span>
|
|
</div>
|
|
</div>
|
|
<div class="core-main-controls">
|
|
<div class="core-status-toggle">
|
|
<span id="_clash" class="value-indicator"><%:Collecting data...%></span>
|
|
<span id="_oclog" class="value-indicator" style="display: none;"><%:Collecting data...%></span>
|
|
<div class="plugin-toggle-container">
|
|
<label class="plugin-toggle-switch">
|
|
<input type="checkbox" id="plugin_toggle" onchange="togglePlugin(this)">
|
|
<span class="plugin-toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="core-control-buttons">
|
|
<button type="button" class="icon-btn action-btn" id="restart_core" title="<%:Restart%>" onclick="return restartCore()">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6"></path><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
|
|
</button>
|
|
<button type="button" class="icon-btn action-btn" id="edit_overwrite" title="<%:Overwrite Module%>" onclick="return editOverwrite()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256">
|
|
<path d="M12,111l112,64a8,8,0,0,0,7.94,0l112-64a8,8,0,0,0,0-13.9l-112-64a8,8,0,0,0-7.94,0l-112,64A8,8,0,0,0,12,111ZM128,49.21,223.87,104,128,158.79,32.13,104ZM246.94,140A8,8,0,0,1,244,151L132,215a8,8,0,0,1-7.94,0L12,151A8,8,0,0,1,20,137.05l108,61.74,108-61.74A8,8,0,0,1,246.94,140Z"></path>
|
|
</svg>
|
|
<span class="btn-text"><%:Overwrite Module%></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-actions">
|
|
<button type="button" class="icon-btn theme-toggle-btn" id="theme-toggle" onclick="toggleThemeMode()">
|
|
<svg id="sun-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<title><%:Light Theme%></title>
|
|
<circle cx="12" cy="12" r="5"></circle>
|
|
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
</svg>
|
|
<svg id="moon-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none">
|
|
<title><%:Dark Theme%></title>
|
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
</svg>
|
|
<svg id="auto-icon" width="14" height="14" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="4" style="display: none">
|
|
<title><%:Auto Theme%></title>
|
|
<path d="M24 44C29.9601 44 26.3359 35.136 30 31C33.1264 27.4709 44 29.0856 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z"/>
|
|
<path d="M28 17C29.6569 17 31 15.6569 31 14C31 12.3431 29.6569 11 28 11C26.3431 11 25 12.3431 25 14C25 15.6569 26.3431 17 28 17Z"/>
|
|
<path d="M16 21C17.6569 21 19 19.6569 19 18C19 16.3431 17.6569 15 16 15C14.3431 15 13 16.3431 13 18C13 19.6569 14.3431 21 16 21Z"/>
|
|
<path d="M17 34C18.6569 34 20 32.6569 20 31C20 29.3431 18.6569 28 17 28C15.3431 28 14 29.3431 14 31C14 32.6569 15.3431 34 17 34Z"/>
|
|
</svg>
|
|
</button>
|
|
<button type="button" class="icon-btn action-btn" id="go_wiki" title="<%:Wiki%>" onclick="return wikipage()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 10v6M2 10l10-5 10 5-10 5z"></path><path d="M6 12v5c3 3 9 3 12 0v-5"></path></svg></button>
|
|
<button type="button" class="icon-btn action-btn" id="go_tutorials" title="<%:Tutorials%>" onclick="return gitbookpage()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></button>
|
|
<button type="button" class="icon-btn action-btn" id="go_star" title="<%:Star%>" onclick="return homepage()"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg></button>
|
|
<button type="button" class="icon-btn action-btn" id="go_telegram" title="<%:Telegram%>" onclick="return telegrampage()"><svg width="14" height="14" viewBox="0 0 256 256" fill="none" stroke="currentColor" stroke-width="20"><path d="M80,134.87,170.26,214a8,8,0,0,0,13.09-4.21L224,33.22a1,1,0,0,0-1.34-1.15L20,111.38A6.23,6.23,0,0,0,21,123.3Z" stroke-linecap="round" stroke-linejoin="round"/><line x1="80" y1="134.87" x2="223.41" y2="32.09" stroke-linecap="round" stroke-linejoin="round"/><path d="M124.37,173.78,93.76,205.54A8,8,0,0,1,80,200V134.87" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
|
|
<button type="button" class="icon-btn action-btn" id="go_sponsor" title="<%:Sponsor%>" onclick="return sponsorpage()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg></button>
|
|
<button type="button" class="icon-btn action-btn" id="logo_btn" title="Mihomo" onclick="return go_mihomo();">
|
|
<img id="_logo" src="/luci-static/resources/openclash/img/logo.png?<%=random%>" loading="lazy" width="14px" height="14px" alt="Mihomo"/>
|
|
<span class="logo-text">Powered by <span class="mihomo-link">Mihomo</span></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 2: Running Mode & Proxy Mode -->
|
|
<div class="card-row dual-cards mode-row">
|
|
<div class="sub-card">
|
|
<div class="card-title"><%:Running Mode%></div>
|
|
<div class="card-content">
|
|
<div class="card-value"><span id="_mode" class="value-indicator"><%:Collecting data...%></span></div>
|
|
<div class="card-controls">
|
|
<div id="radio-ru-mode" class="cbi-button-group" style="display: inline-flex;">
|
|
<input type="radio" id="normal" name="radios-ru" value="" onclick="return switch_run_mode(this.value)"/><label for="normal" id="run_normal" class="cbi-button-option"><%:Compat%></label>
|
|
<input type="radio" id="tun" name="radios-ru" value="-tun" onclick="return switch_run_mode(this.value)"/><label for="tun" class="cbi-button-option"><%:TUN%></label>
|
|
<input type="radio" id="mix" name="radios-ru" value="-mix" onclick="return switch_run_mode(this.value)"/><label for="mix" class="cbi-button-option"><%:Mix%></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sub-card">
|
|
<div class="card-title"><%:Proxy Mode%></div>
|
|
<div class="card-content">
|
|
<div class="card-controls">
|
|
<div id="radio-mode" class="cbi-button-group" style="display: inline-flex;">
|
|
<input type="radio" id="rule" name="radios" value="rule" onclick="return switch_rule_mode(this.value)"/><label for="rule" class="cbi-button-option"><%:Rule%></label>
|
|
<input type="radio" id="global" name="radios" value="global" onclick="return switch_rule_mode(this.value)"/><label for="global" class="cbi-button-option"><%:Global%></label>
|
|
<input type="radio" id="direct" name="radios" value="direct" onclick="return switch_rule_mode(this.value)"/><label for="direct" class="cbi-button-option"><%:Direct%></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 3: Settings -->
|
|
<div class="card-row stats-grid">
|
|
<div class="sub-card">
|
|
<div class="card-title"><%:Area Bypass%><div class="help-link" title="<%:Bypass Specified Regions Network Flows, Improve Performance, If Inaccessibility on Bypass Gateway, Try to Enable Bypass Gateway Compatible Option%>"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></div></div>
|
|
<div class="card-content">
|
|
<div class="card-controls">
|
|
<div id="oc-setting-oversea" class="cbi-button-group" style="display: inline-flex;">
|
|
<input type="radio" id="oc_setting_oversea_1" name="oc-setting-oversea" value="1" onclick="return switch_oc_setting_oversea('1')"/><label for="oc_setting_oversea_1" class="cbi-button-option"><%:Mainland%></label>
|
|
<input type="radio" id="oc_setting_oversea_2" name="oc-setting-oversea" value="2" onclick="return switch_oc_setting_oversea('2')"/><label for="oc_setting_oversea_2" class="cbi-button-option"><%:Oversea%></label>
|
|
<input type="radio" id="oc_setting_oversea_0" name="oc-setting-oversea" value="0" onclick="return switch_oc_setting_oversea('0')"/><label for="oc_setting_oversea_0" class="cbi-button-option"><%:Off%></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sub-card">
|
|
<div class="card-title"><%:Sniffer%><a href="javascript:void(0);" onclick="window.open('https://wiki.metacubex.one/config/sniff/?h=sniff#_1', '_blank');" class="help-link" title="<%:Sniff the domain name of the traffics to avoid rule-based proxy failure%>"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></a></div>
|
|
<div class="card-content">
|
|
<div class="card-controls">
|
|
<div id="dns-setting-sniffer" class="cbi-button-group" style="display: inline-flex;">
|
|
<input type="radio" id="meta_sniffer_on" name="meta-sniffer-radios" value="1" onclick="return switch_meta_sniffer('1')"/><label for="meta_sniffer_on" class="cbi-button-option"><%:On%></label>
|
|
<input type="radio" id="meta_sniffer_off" name="meta-sniffer-radios" value="0" onclick="return switch_meta_sniffer('0')"/><label for="meta_sniffer_off" class="cbi-button-option"><%:Off%></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sub-card">
|
|
<div class="card-title"><%:DNS Proxy%><a href="javascript:void(0);" onclick="window.open('https://wiki.metacubex.one/config/dns/?h=res#respect-rules', '_blank');" class="help-link" title="<%:DNS querys respect-rules to preventing access failure%>"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></a></div>
|
|
<div class="card-content">
|
|
<div class="card-controls">
|
|
<div id="dns-setting-respect" class="cbi-button-group" style="display: inline-flex;">
|
|
<input type="radio" id="respect_rules_on" name="respect-rules-radios" value="1" onclick="return switch_respect_rules('1')"/><label for="respect_rules_on" class="cbi-button-option"><%:On%></label>
|
|
<input type="radio" id="respect_rules_off" name="respect-rules-radios" value="0" onclick="return switch_respect_rules('0')"/><label for="respect_rules_off" class="cbi-button-option"><%:Off%></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sub-card">
|
|
<div class="card-title"><%:Stream Unlock%><div class="help-link" title="<%:Auto Select Proxy For Streaming Unlock, Support Netflix, Disney Plus, HBO And YouTube Premium, etc%>"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></div></div>
|
|
<div class="card-content">
|
|
<div class="card-controls">
|
|
<div id="stream-unlock-setting" class="cbi-button-group" style="display: inline-flex;">
|
|
<input type="radio" id="stream_unlock_on" name="stream-unlock-radios" value="1" onclick="return switch_stream_unlock('1')"/><label for="stream_unlock_on" class="cbi-button-option"><%:On%></label>
|
|
<input type="radio" id="stream_unlock_off" name="stream-unlock-radios" value="0" onclick="return switch_stream_unlock('0')"/><label for="stream_unlock_off" class="cbi-button-option"><%:Off%></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 4: Config File -->
|
|
<div class="card-row full-width">
|
|
<div class="sub-card">
|
|
<div class="card-title"><%:Config File%></div>
|
|
<div class="config-file-content">
|
|
<div id="subscription-info-display" class="subscription-info-container" style="display: none;">
|
|
<div class="subscription-nav-arrow left" id="subscription-prev-arrow" onclick="switchToPreviousConfig()" title="<%:Previous%>"></div>
|
|
<div class="subscription-nav-arrow right" id="subscription-next-arrow" onclick="switchToNextConfig()" title="<%:Next%>"></div>
|
|
<div class="subscription-info-main">
|
|
<div class="subscription-info-header">
|
|
<div class="config-file-name" id="current-config-name"><%:No Config Selected%></div>
|
|
<div class="file-info" id="file-info-section">
|
|
<span id="file-modify-time" class="file-info-item"><%:Modified: %> --</span>
|
|
</div>
|
|
</div>
|
|
<div class="subscription-info-details">
|
|
<div class="subscription-progress" id="subscription-progress-section" style="display: none;">
|
|
<div class="subscription-progress-bar">
|
|
<div id="subscription-progress-fill" class="subscription-progress-fill high" style="width: 0%"></div>
|
|
</div>
|
|
<div id="subscription-info-text" class="subscription-info-text"><%:Collecting data...%></div>
|
|
</div>
|
|
<div class="card-actions">
|
|
<button type="button" class="icon-btn action-btn" id="refresh-subscription" title="<%:Refresh%>" onclick="return refreshSubscriptionInfo()">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M23 4v6h-6"></path>
|
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
|
</svg>
|
|
</button>
|
|
<button type="button" class="icon-btn action-btn" id="config-subscription-url" title="<%:Specify URL%>" onclick="return setSubscriptionUrl()">
|
|
<svg width="14" height="14" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="4">
|
|
<path d="M4 6H44V36H29L24 41L19 36H4V6Z"/>
|
|
<path d="M23 21H25.0025" />
|
|
<path d="M33.001 21H34.9999"/>
|
|
<path d="M13.001 21H14.9999"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="config-file-empty-state" class="config-file-empty-state" style="display: none;">
|
|
<div class="config-file-empty-message"><%:No config files found, Please upload a config file to get started%></div>
|
|
<button type="button" class="config-upload-btn-large" id="upload_config_large" onclick="return uploadConfig()">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
<polyline points="17,8 12,3 7,8"></polyline>
|
|
<line x1="12" y1="3" x2="12" y2="15"></line>
|
|
</svg>
|
|
<%:Add Config File%>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="config-file-bottom">
|
|
<div class="config-selector">
|
|
<select id="config_file_select" class="config-select"><option value=""><%:Collecting data...%></option></select>
|
|
</div>
|
|
<div class="card-actions">
|
|
<button type="button" class="icon-btn action-btn" id="switch_config" title="<%:SwiTch%>" onclick="return switchConfig()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg></button>
|
|
<button type="button" class="icon-btn action-btn" id="update_config" title="<%:Update%>" onclick="return updateConfig()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M21 21v-5h-5"></path></svg></button>
|
|
<button type="button" class="icon-btn action-btn" id="edit_config" title="<%:Edit%>" onclick="return editConfig()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button>
|
|
<button type="button" class="icon-btn action-btn" id="edit_subscribe" title="<%:Edit Subscription%>" onclick="return editSubscribe()"><svg width="14" height="14" viewBox="0 0 48 48" fill="none"><path d="M18.2838 43.1713C14.9327 42.1736 11.9498 40.3213 9.58787 37.867C10.469 36.8227 11 35.4734 11 34.0001C11 30.6864 8.31371 28.0001 5 28.0001C4.79955 28.0001 4.60139 28.01 4.40599 28.0292C4.13979 26.7277 4 25.3803 4 24.0001C4 21.9095 4.32077 19.8938 4.91579 17.9995C4.94381 17.9999 4.97188 18.0001 5 18.0001C8.31371 18.0001 11 15.3138 11 12.0001C11 11.0488 10.7786 10.1493 10.3846 9.35011C12.6975 7.1995 15.5205 5.59002 18.6521 4.72314C19.6444 6.66819 21.6667 8.00013 24 8.00013C26.3333 8.00013 28.3556 6.66819 29.3479 4.72314C32.4795 5.59002 35.3025 7.1995 37.6154 9.35011C37.2214 10.1493 37 11.0488 37 12.0001C37 15.3138 39.6863 18.0001 43 18.0001C43.0281 18.0001 43.0562 17.9999 43.0842 17.9995C43.6792 19.8938 44 21.9095 44 24.0001C44 25.3803 43.8602 26.7277 43.594 28.0292C43.3986 28.01 43.2005 28.0001 43 28.0001C39.6863 28.0001 37 30.6864 37 34.0001C37 35.4734 37.531 36.8227 38.4121 37.867C36.0502 40.3213 33.0673 42.1736 29.7162 43.1713C28.9428 40.752 26.676 39.0001 24 39.0001C21.324 39.0001 19.0572 40.752 18.2838 43.1713Z" fill="none" stroke="currentColor" stroke-width="4" stroke-linejoin="round"/><path d="M24 31C27.866 31 31 27.866 31 24C31 20.134 27.866 17 24 17C20.134 17 17 20.134 17 24C17 27.866 20.134 31 24 31Z" fill="none" stroke="currentColor" stroke-width="4" stroke-linejoin="round"/></svg></button>
|
|
<button type="button" class="icon-btn action-btn" id="upload_config" title="<%:Add%>" onclick="return uploadConfig()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-card">
|
|
<!-- Row 1: Control Panel & Mix Proxy -->
|
|
<div class="card-row dual-cards">
|
|
<div class="sub-card">
|
|
<div class="card-title"><%:Control Panel%></div>
|
|
<div class="card-content">
|
|
<div class="card-value">
|
|
<span id="_daip" class="value-indicator"><%:Collecting data...%></span>
|
|
</div>
|
|
</div>
|
|
<div class="card-actions">
|
|
<button type="button" class="icon-btn copy-btn" id="copy_address" title="<%:Copy Address%>" onclick="return copyAddress()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button>
|
|
<button type="button" class="icon-btn copy-btn" id="copy_secret" title="<%:Copy Secret%>" onclick="return copySecret()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><circle cx="12" cy="16" r="1"></circle><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></button>
|
|
</div>
|
|
</div>
|
|
<div class="sub-card">
|
|
<div class="card-title"><%:Mix Proxy%></div>
|
|
<div class="card-content">
|
|
<div class="card-value">
|
|
<span id="_mix_proxy" class="value-indicator"><%:Collecting data...%></span>
|
|
</div>
|
|
</div>
|
|
<div class="card-actions">
|
|
<button type="button" class="icon-btn copy-btn" id="copy_mix_address" title="<%:Copy Address%>" onclick="return copyMixAddress()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button>
|
|
<button type="button" class="icon-btn action-btn" id="copy_pac_config" title="<%:Get PAC Config%>" onclick="return generatePacConfig()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path><path d="M2 12h20"></path></svg></button>
|
|
<button type="button" class="icon-btn copy-btn" id="copy_mix_secret" title="<%:Copy Auth Info%>" onclick="return copyMixAuth()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><circle cx="12" cy="16" r="1"></circle><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 2: Dashboards -->
|
|
<div class="card-row full-width">
|
|
<div class="sub-card dashboard-container">
|
|
<div class="card-title"><%:Control Panel%></div>
|
|
<div class="dashboard-buttons">
|
|
<button class="dashboard-btn" id="_web"><%:Collecting data...%></button>
|
|
<button class="dashboard-btn" id="_webm"><%:Collecting data...%></button>
|
|
<button class="dashboard-btn" id="_webz"><%:Collecting data...%></button>
|
|
<button class="dashboard-btn" id="_webo"><%:Collecting data...%></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 3: Quick Actions -->
|
|
<div class="card-row full-width">
|
|
<div class="sub-card quick-actions-container">
|
|
<div class="card-title"><%:Quick Action%></div>
|
|
<div class="quick-actions-buttons">
|
|
<button class="dashboard-btn" id="_close_all_connection_btn"><%:Collecting data...%></button>
|
|
<button class="dashboard-btn" id="_reload_firewall_btn"><%:Collecting data...%></button>
|
|
<button class="dashboard-btn" id="_flush_dns_cache_btn"><%:Collecting data...%></button>
|
|
<button class="dashboard-btn" id="_one_key_update_btn"><%:Collecting data...%></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 4: Statistics -->
|
|
<div class="card-row stats-grid">
|
|
<div class="sub-card stat-item"><span class="stat-label"><%:Up%></span><span id="upload_" class="stat-value">0 KB/S</span></div>
|
|
<div class="sub-card stat-item"><span class="stat-label"><%:Down%></span><span id="download_" class="stat-value">0 KB/S</span></div>
|
|
<div class="sub-card stat-item"><span class="stat-label"><%:Up Total%></span><span id="uploadtotal_" class="stat-value">0 KB</span></div>
|
|
<div class="sub-card stat-item"><span class="stat-label"><%:Down Total%></span><span id="downloadtotal_" class="stat-value">0 KB</span></div>
|
|
<div class="sub-card stat-item"><span class="stat-label"><%:Connect%></span><span id="connect_t" class="stat-value">0</span></div>
|
|
<div class="sub-card stat-item"><span class="stat-label"><%:Ram%></span><span id="mem_t" class="stat-value">0 KB</span></div>
|
|
<div class="sub-card stat-item"><span class="stat-label"><%:CPU%></span><span id="cpu_t" class="stat-value">0 %</span></div>
|
|
<div class="sub-card stat-item"><span class="stat-label"><%:Load Avg%></span><span id="load_a" class="stat-value">0 %</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</fieldset>
|
|
|
|
<script type="text/javascript">//<![CDATA[
|
|
var DOMCache = {
|
|
clash: document.getElementById('_clash'),
|
|
mode: document.getElementById('_mode'),
|
|
web: document.getElementById('_web'),
|
|
webo: document.getElementById('_webo'),
|
|
webm: document.getElementById('_webm'),
|
|
webz: document.getElementById('_webz'),
|
|
daip: document.getElementById('_daip'),
|
|
dase: document.getElementById('_dase'),
|
|
oclog: document.getElementById('_oclog'),
|
|
close_all_connection: document.getElementById('_close_all_connection'),
|
|
reload_firewall: document.getElementById('_reload_firewall'),
|
|
one_key_update: document.getElementById('_one_key_update'),
|
|
flush_dns_cache: document.getElementById('_flush_dns_cache'),
|
|
radio_mode: document.getElementById('radio-mode'),
|
|
radio: document.getElementsByName("radios"),
|
|
radio_ru: document.getElementsByName("radios-ru"),
|
|
radio_run_normal: document.getElementById("run_normal"),
|
|
copy_secret: document.getElementById('copy_secret'),
|
|
copy_address: document.getElementById('copy_address'),
|
|
copy_mix_address: document.getElementById('copy_mix_address'),
|
|
copy_mix_secret: document.getElementById('copy_mix_secret'),
|
|
copy_pac_config: document.getElementById('copy_pac_config'),
|
|
mix_proxy: document.getElementById('_mix_proxy'),
|
|
dns_setting_sniffer: document.getElementById('dns-setting-sniffer'),
|
|
dns_setting_respect: document.getElementById('dns-setting-respect'),
|
|
meta_sniffer_on: document.getElementById('meta_sniffer_on'),
|
|
meta_sniffer_off: document.getElementById('meta_sniffer_off'),
|
|
respect_rules_on: document.getElementById('respect_rules_on'),
|
|
respect_rules_off: document.getElementById('respect_rules_off'),
|
|
oc_setting_oversea: document.getElementById('oc-setting-oversea'),
|
|
oc_setting_oversea_0: document.getElementById('oc_setting_oversea_0'),
|
|
oc_setting_oversea_1: document.getElementById('oc_setting_oversea_1'),
|
|
oc_setting_oversea_2: document.getElementById('oc_setting_oversea_2'),
|
|
stream_unlock_setting: document.getElementById('stream-unlock-setting'),
|
|
stream_unlock_on: document.getElementById('stream_unlock_on'),
|
|
stream_unlock_off: document.getElementById('stream_unlock_off'),
|
|
core_version_display: document.getElementById('core-version-display'),
|
|
core_version_text: document.getElementById('core-version-text'),
|
|
plugin_version_display: document.getElementById('plugin-version-display'),
|
|
plugin_version_text: document.getElementById('plugin-version-text')
|
|
};
|
|
|
|
var DarkModeDetector = {
|
|
init: function() {
|
|
this.applyDarkMode();
|
|
},
|
|
|
|
applyDarkMode: function() {
|
|
var ocContainers = document.querySelectorAll('.oc');
|
|
if (!ocContainers.length) return;
|
|
|
|
var theme = localStorage.getItem('oc-theme') || 'auto';
|
|
var shouldUseDark;
|
|
|
|
if (theme === 'light') {
|
|
shouldUseDark = false;
|
|
} else if (theme === 'dark') {
|
|
shouldUseDark = true;
|
|
} else {
|
|
shouldUseDark = isDarkBackground(document.body);
|
|
}
|
|
|
|
ocContainers.forEach(function(ocContainer) {
|
|
if (shouldUseDark) {
|
|
ocContainer.setAttribute('data-darkmode', 'true');
|
|
} else {
|
|
ocContainer.removeAttribute('data-darkmode');
|
|
}
|
|
});
|
|
|
|
var sunIcon = document.getElementById('sun-icon');
|
|
var moonIcon = document.getElementById('moon-icon');
|
|
var autoIcon = document.getElementById('auto-icon');
|
|
if (sunIcon && moonIcon && autoIcon) {
|
|
sunIcon.style.display = 'none';
|
|
moonIcon.style.display = 'none';
|
|
autoIcon.style.display = 'none';
|
|
|
|
if (theme === 'light') {
|
|
sunIcon.style.display = 'inline';
|
|
} else if (theme === 'dark') {
|
|
moonIcon.style.display = 'inline';
|
|
} else {
|
|
autoIcon.style.display = 'inline';
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var StateManager = {
|
|
current_status: {},
|
|
cached_proxy_info: null,
|
|
last_request_time: {},
|
|
request_cache: {},
|
|
cache_ttl: 5000,
|
|
pending_requests: new Map(),
|
|
|
|
_cachedXHR: function(url, params, callback, force) {
|
|
var cacheKey = params ? url + JSON.stringify(params) : url;
|
|
var now = Date.now();
|
|
|
|
if (this.pending_requests.has(cacheKey)) {
|
|
this.pending_requests.get(cacheKey).push(callback);
|
|
return;
|
|
}
|
|
|
|
var cacheExists = this.request_cache.hasOwnProperty(cacheKey);
|
|
var isCacheStale = cacheExists && (now - this.last_request_time[cacheKey] >= this.cache_ttl);
|
|
|
|
if (!force && cacheExists && !isCacheStale) {
|
|
setTimeout(function() {
|
|
callback({ status: 200, fromCache: true }, StateManager.request_cache[cacheKey]);
|
|
}, 0);
|
|
return;
|
|
}
|
|
|
|
if (!force && cacheExists && isCacheStale) {
|
|
setTimeout(function() {
|
|
callback({ status: 200, fromCache: true, stale: true }, StateManager.request_cache[cacheKey]);
|
|
}, 0);
|
|
}
|
|
|
|
this.pending_requests.set(cacheKey, [callback]);
|
|
var self = this;
|
|
|
|
XHR.get(url, params, function(x, data) {
|
|
var callbacks = self.pending_requests.get(cacheKey) || [];
|
|
self.pending_requests.delete(cacheKey);
|
|
|
|
if (x && x.status == 200 && data) {
|
|
self.request_cache[cacheKey] = data;
|
|
self.last_request_time[cacheKey] = Date.now();
|
|
}
|
|
|
|
callbacks.forEach(function(cb) {
|
|
try {
|
|
cb(x, data);
|
|
} catch (e) {
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
cachedXHRGet: function(url, callback, force) {
|
|
this._cachedXHR(url, null, callback, force);
|
|
},
|
|
|
|
cachedXHRGetWithParams: function(url, params, callback, force) {
|
|
this._cachedXHR(url, params, callback, force);
|
|
},
|
|
|
|
clearCache: function(url, params) {
|
|
var cacheKey = params ? url + JSON.stringify(params) : url;
|
|
delete this.request_cache[cacheKey];
|
|
delete this.last_request_time[cacheKey];
|
|
},
|
|
|
|
clearAllCache: function() {
|
|
this.request_cache = {};
|
|
this.last_request_time = {};
|
|
},
|
|
|
|
hasCache: function(url, params) {
|
|
var cacheKey = params ? url + JSON.stringify(params) : url;
|
|
return !!this.request_cache[cacheKey];
|
|
},
|
|
|
|
getCacheAge: function(url, params) {
|
|
var cacheKey = params ? url + JSON.stringify(params) : url;
|
|
if (!this.last_request_time[cacheKey]) {
|
|
return null;
|
|
}
|
|
return Date.now() - this.last_request_time[cacheKey];
|
|
},
|
|
|
|
setCacheTTL: function(ttl) {
|
|
this.cache_ttl = ttl;
|
|
},
|
|
|
|
batchUpdateDOM: function(updates) {
|
|
for (var i = 0; i < updates.length; i++) {
|
|
var update = updates[i];
|
|
if (update.element && update.content !== undefined) {
|
|
update.element.innerHTML = update.content;
|
|
}
|
|
}
|
|
},
|
|
|
|
cachedXHRGetWithRetry: function(url, params, callback, force, maxRetries) {
|
|
maxRetries = maxRetries || 3;
|
|
var retryCount = 0;
|
|
var self = this;
|
|
|
|
function attemptRequest() {
|
|
self.cachedXHRGetWithParams(url, params, function(x, data) {
|
|
if (x && x.status == 200) {
|
|
callback(x, data);
|
|
} else if (retryCount < maxRetries) {
|
|
retryCount++;
|
|
setTimeout(attemptRequest, 1000 * retryCount);
|
|
} else {
|
|
callback(x, data);
|
|
}
|
|
}, force && retryCount === 0);
|
|
}
|
|
|
|
attemptRequest();
|
|
}
|
|
};
|
|
|
|
var WSManager = {
|
|
connections: {},
|
|
connectionStates: {
|
|
CONNECTING: 0,
|
|
CONNECTED: 1,
|
|
DISCONNECTED: 2,
|
|
ERROR: 3
|
|
},
|
|
reconnectDelay: 5000,
|
|
heartbeatInterval: 30000,
|
|
heartbeatTimers: {},
|
|
_ws_connect: false,
|
|
_allowedCloseCodes: [1000, 1001, 1005, 1006],
|
|
|
|
isSupported: function() {
|
|
return typeof window.WebSocket !== "undefined";
|
|
},
|
|
|
|
createConnection: function(type, url, messageHandler, options) {
|
|
options = options || {};
|
|
|
|
this.closeConnection(type);
|
|
|
|
if (!this.isSupported()) {
|
|
this.enableFallbackMode();
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
var ws = new window.WebSocket(url);
|
|
|
|
this.connections[type] = {
|
|
socket: ws,
|
|
url: url,
|
|
messageHandler: messageHandler,
|
|
state: this.connectionStates.CONNECTING,
|
|
lastActivity: Date.now(),
|
|
options: options
|
|
};
|
|
|
|
if (ws.addEventListener) {
|
|
ws.addEventListener('open', this.handleOpen.bind(this, type));
|
|
ws.addEventListener('message', this.handleMessage.bind(this, type));
|
|
ws.addEventListener('error', this.handleError.bind(this, type));
|
|
ws.addEventListener('close', this.handleClose.bind(this, type));
|
|
} else {
|
|
ws.onopen = this.handleOpen.bind(this, type);
|
|
ws.onmessage = this.handleMessage.bind(this, type);
|
|
ws.onerror = this.handleError.bind(this, type);
|
|
ws.onclose = this.handleClose.bind(this, type);
|
|
}
|
|
|
|
return ws;
|
|
} catch (error) {
|
|
this.handleConnectionError(type, error);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
startHeartbeat: function(type) {
|
|
var self = this;
|
|
this.stopHeartbeat(type);
|
|
|
|
this.heartbeatTimers[type] = setInterval(function() {
|
|
var connection = self.connections[type];
|
|
if (connection && connection.socket && connection.state === self.connectionStates.CONNECTED) {
|
|
var timeSinceLastActivity = Date.now() - connection.lastActivity;
|
|
if (timeSinceLastActivity > self.heartbeatInterval * 3) {
|
|
self.reconnectConnection(type);
|
|
}
|
|
}
|
|
}, this.heartbeatInterval);
|
|
|
|
if (!this._visibilityHandlerAdded) {
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (document.hidden) {
|
|
self.stopHeartbeat(type);
|
|
} else {
|
|
self.startHeartbeat(type);
|
|
}
|
|
});
|
|
this._visibilityHandlerAdded = true;
|
|
}
|
|
},
|
|
|
|
stopHeartbeat: function(type) {
|
|
if (this.heartbeatTimers[type]) {
|
|
clearInterval(this.heartbeatTimers[type]);
|
|
delete this.heartbeatTimers[type];
|
|
}
|
|
},
|
|
|
|
reconnectConnection: function(type) {
|
|
var connection = this.connections[type];
|
|
if (connection) {
|
|
this.createConnection(type, connection.url, connection.messageHandler, connection.options);
|
|
}
|
|
},
|
|
|
|
closeConnection: function(type) {
|
|
var connection = this.connections[type];
|
|
if (connection && connection.socket) {
|
|
this.stopHeartbeat(type);
|
|
try {
|
|
connection.socket.close(1000, 'Normal closure');
|
|
} catch (error) {}
|
|
delete this.connections[type];
|
|
}
|
|
},
|
|
|
|
closeAll: function() {
|
|
var types = Object.keys(this.connections);
|
|
for (var i = 0; i < types.length; i++) {
|
|
this.closeConnection(types[i]);
|
|
}
|
|
this._ws_connect = false;
|
|
},
|
|
|
|
handleMessage: function(type, event) {
|
|
var connection = this.connections[type];
|
|
if (connection && connection.messageHandler) {
|
|
connection.lastActivity = Date.now();
|
|
try {
|
|
var data;
|
|
try {
|
|
data = JSON.parse(event.data);
|
|
} catch (e) {
|
|
data = event.data;
|
|
}
|
|
connection.messageHandler({ data: data, raw: event.data, event: event });
|
|
} catch (error) {
|
|
}
|
|
}
|
|
},
|
|
|
|
handleOpen: function(type, event) {
|
|
var connection = this.connections[type];
|
|
if (connection) {
|
|
connection.state = this.connectionStates.CONNECTED;
|
|
connection.lastActivity = Date.now();
|
|
}
|
|
this.startHeartbeat(type);
|
|
|
|
if (Object.keys(this.connections).length === 3) {
|
|
this._ws_connect = true;
|
|
if (NetworkStatsManager.isEnabled) {
|
|
NetworkStatsManager.stop();
|
|
}
|
|
}
|
|
},
|
|
|
|
handleError: function(type, error) {
|
|
var connection = this.connections[type];
|
|
if (connection) {
|
|
connection.state = this.connectionStates.ERROR;
|
|
}
|
|
this.stopHeartbeat(type);
|
|
this.enableFallbackMode();
|
|
},
|
|
|
|
handleClose: function(type, event) {
|
|
// if not connected, start log display because core maybe restart
|
|
LogManager.startLogDisplay();
|
|
|
|
var connection = this.connections[type];
|
|
if (connection) {
|
|
connection.state = this.connectionStates.DISCONNECTED;
|
|
}
|
|
this.stopHeartbeat(type);
|
|
|
|
if (this._allowedCloseCodes.indexOf(event.code) === -1) {
|
|
this.scheduleReconnect(type);
|
|
}
|
|
},
|
|
|
|
handleConnectionError: function(type, error) {
|
|
var connection = this.connections[type];
|
|
if (connection) {
|
|
connection.state = this.connectionStates.ERROR;
|
|
}
|
|
this.enableFallbackMode();
|
|
},
|
|
|
|
scheduleReconnect: function(type) {
|
|
var self = this;
|
|
var connection = this.connections[type];
|
|
|
|
if (!connection) {
|
|
this.enableFallbackMode();
|
|
return;
|
|
}
|
|
|
|
setTimeout(function() {
|
|
if (self.connections[type] && self.connections[type].state !== self.connectionStates.CONNECTED) {
|
|
self.createConnection(type, connection.url, connection.messageHandler, connection.options);
|
|
}
|
|
}, this.reconnectDelay);
|
|
},
|
|
|
|
enableFallbackMode: function() {
|
|
this._ws_connect = false;
|
|
this.closeAll();
|
|
if (!NetworkStatsManager.isEnabled) {
|
|
NetworkStatsManager.start();
|
|
}
|
|
},
|
|
|
|
getConnectionState: function(type) {
|
|
var connection = this.connections[type];
|
|
return connection ? connection.state : this.connectionStates.DISCONNECTED;
|
|
},
|
|
|
|
getAllConnectionStates: function() {
|
|
var states = {};
|
|
for (var type in this.connections) {
|
|
states[type] = this.getConnectionState(type);
|
|
}
|
|
return states;
|
|
},
|
|
|
|
hasActiveConnections: function() {
|
|
for (var type in this.connections) {
|
|
if (this.getConnectionState(type) === this.connectionStates.CONNECTED) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
var ConfigFileManager = {
|
|
configList: [],
|
|
currentConfig: '',
|
|
currentConfigIndex: -1,
|
|
selectElement: null,
|
|
rawCurrentConfig: '',
|
|
|
|
init: function() {
|
|
this.selectElement = document.getElementById('config_file_select');
|
|
if (this.selectElement) {
|
|
this.loadConfigFileList();
|
|
this.setupEventListeners();
|
|
}
|
|
},
|
|
|
|
setupEventListeners: function() {
|
|
if (this.selectElement) {
|
|
this.selectElement.addEventListener('change', this.onConfigChange.bind(this));
|
|
}
|
|
},
|
|
|
|
loadConfigFileList: function() {
|
|
this.updateSelectOptions([{value: '', text: '<%:Collecting data...%>', disabled: true}]);
|
|
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "config_file_list")%>', function(x, data) {
|
|
if (x && x.status == 200) {
|
|
ConfigFileManager.handleConfigListResponse(data);
|
|
} else {
|
|
ConfigFileManager.handleConfigListError();
|
|
}
|
|
}, true);
|
|
},
|
|
|
|
handleConfigListResponse: function(data) {
|
|
try {
|
|
var configFiles = [];
|
|
var currentConfigFile = '';
|
|
var currentFileInfo = null;
|
|
|
|
if (data.config_files && Array.isArray(data.config_files)) {
|
|
configFiles = data.config_files;
|
|
configFiles.sort(function(a, b) {
|
|
var nameA = typeof a === 'string' ? a : (a.name || a.filename || a.path || a);
|
|
var nameB = typeof b === 'string' ? b : (b.name || b.filename || b.path || b);
|
|
return nameA.localeCompare(nameB);
|
|
});
|
|
} else {
|
|
configFiles = [];
|
|
}
|
|
|
|
if (data.current_config) {
|
|
currentConfigFile = data.current_config;
|
|
}
|
|
|
|
this.rawCurrentConfig = currentConfigFile;
|
|
this.configList = configFiles;
|
|
this.currentConfig = currentConfigFile;
|
|
this.currentConfigIndex = -1;
|
|
|
|
this.toggleEmptyState(configFiles.length === 0);
|
|
|
|
this.updateConfigSelect(configFiles, currentConfigFile);
|
|
|
|
if (configFiles.length > 0) {
|
|
if (currentConfigFile) {
|
|
for (var i = 0; i < configFiles.length; i++) {
|
|
var file = configFiles[i];
|
|
var filePath = typeof file === 'string' ? file : (file.path || file.filepath || file);
|
|
|
|
if (filePath === currentConfigFile) {
|
|
this.currentConfigIndex = i;
|
|
if (typeof file === 'object' && file.mtime && file.size) {
|
|
currentFileInfo = {
|
|
mtime: file.mtime,
|
|
size: file.size
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.updateSubscriptionDisplay(currentConfigFile, currentFileInfo);
|
|
|
|
if (SubscriptionManager.currentConfigFile !== currentConfigFile) {
|
|
SubscriptionManager.currentConfigFile = currentConfigFile;
|
|
SubscriptionManager.getSubscriptionInfo();
|
|
}
|
|
} else {
|
|
this.hideSubscriptionDisplay();
|
|
SubscriptionManager.currentConfigFile = '';
|
|
}
|
|
|
|
this.updateNavigationArrows();
|
|
|
|
if (this._retryGetConfigList) {
|
|
clearInterval(this._retryGetConfigList);
|
|
this._retryGetConfigList = null;
|
|
}
|
|
|
|
} catch (e) {
|
|
this.handleConfigListError();
|
|
}
|
|
},
|
|
|
|
handleConfigListError: function() {
|
|
this.updateSelectOptions([
|
|
{value: '', text: '<%:Failed to load config files%>', disabled: true}
|
|
]);
|
|
|
|
this.toggleEmptyState(true);
|
|
this.hideSubscriptionDisplay();
|
|
|
|
if (!this._retryGetConfigList) {
|
|
var self = this;
|
|
this._retryGetConfigList = setInterval(function() {
|
|
self.loadConfigFileList();
|
|
}, 5000);
|
|
}
|
|
},
|
|
|
|
toggleEmptyState: function(isEmpty) {
|
|
var emptyStateElement = document.getElementById('config-file-empty-state');
|
|
var configFileBottom = document.querySelector('.config-file-bottom');
|
|
var configFileContent = document.querySelector('.config-file-content');
|
|
|
|
if (isEmpty) {
|
|
if (emptyStateElement) {
|
|
emptyStateElement.style.display = 'flex';
|
|
}
|
|
if (configFileBottom) {
|
|
configFileBottom.style.display = 'none';
|
|
}
|
|
if (configFileContent) {
|
|
configFileContent.classList.add('empty-state');
|
|
}
|
|
this.hideSubscriptionDisplay();
|
|
} else {
|
|
if (emptyStateElement) {
|
|
emptyStateElement.style.display = 'none';
|
|
}
|
|
if (configFileBottom) {
|
|
configFileBottom.style.display = 'flex';
|
|
}
|
|
if (configFileContent) {
|
|
configFileContent.classList.remove('empty-state');
|
|
}
|
|
}
|
|
},
|
|
|
|
hideSubscriptionDisplay: function() {
|
|
var subscriptionDisplay = document.getElementById('subscription-info-display');
|
|
if (subscriptionDisplay) {
|
|
subscriptionDisplay.style.display = 'none';
|
|
}
|
|
},
|
|
|
|
updateConfigSelect: function(configFiles, currentConfig) {
|
|
var options = [];
|
|
|
|
if (!configFiles || configFiles.length === 0) {
|
|
options.push({
|
|
value: '',
|
|
text: '<%:No config files found%>',
|
|
disabled: true
|
|
});
|
|
} else {
|
|
options.push({
|
|
value: '',
|
|
text: '<%:Please select a config file%>',
|
|
disabled: false
|
|
});
|
|
|
|
configFiles.forEach(function(file) {
|
|
var fileName, filePath;
|
|
|
|
if (typeof file === 'string') {
|
|
fileName = file;
|
|
filePath = file;
|
|
} else {
|
|
fileName = file.name || file.filename || file.path || file;
|
|
filePath = file.path || file.filepath || file.name || file;
|
|
}
|
|
|
|
var displayName = fileName;
|
|
|
|
options.push({
|
|
value: filePath,
|
|
text: displayName,
|
|
disabled: false,
|
|
selected: filePath === currentConfig || fileName === currentConfig
|
|
});
|
|
});
|
|
}
|
|
|
|
this.updateSelectOptions(options);
|
|
},
|
|
|
|
updateSelectOptions: function(options) {
|
|
if (!this.selectElement) return;
|
|
|
|
this.selectElement.innerHTML = '';
|
|
|
|
options.forEach(function(option) {
|
|
var optionElement = document.createElement('option');
|
|
optionElement.value = option.value;
|
|
optionElement.textContent = option.text;
|
|
optionElement.disabled = option.disabled || false;
|
|
optionElement.selected = option.selected || false;
|
|
|
|
this.selectElement.appendChild(optionElement);
|
|
}, this);
|
|
},
|
|
|
|
updateSubscriptionDisplay: function(configFile, fileInfo) {
|
|
var container = document.getElementById('subscription-info-display');
|
|
var configNameElement = document.getElementById('current-config-name');
|
|
var fileModifyTimeElement = document.getElementById('file-modify-time');
|
|
var detailsSection = document.getElementById('subscription-info-details');
|
|
|
|
if (!container) return;
|
|
|
|
if (this.configList.length === 0) {
|
|
container.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
if (!configFile) {
|
|
container.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
var configFileContent = document.querySelector('.config-file-content');
|
|
if (configFileContent && configFileContent.classList.contains('empty-state')) {
|
|
container.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
container.style.display = 'flex';
|
|
|
|
if (configNameElement) {
|
|
var displayName = this.formatDisplayName(configFile);
|
|
if (configFile === this.rawCurrentConfig) {
|
|
configNameElement.innerHTML =
|
|
'<svg width="0.6em" height="0.6em" viewBox="0 0 24 24" fill="var(--primary-color)" style="vertical-align:middle;margin-right:0.2em;margin-bottom:0.2em;">' +
|
|
'<circle cx="12" cy="12" r="8" fill="var(--primary-color)"/>' +
|
|
'</svg>' + displayName;
|
|
} else {
|
|
configNameElement.textContent = displayName;
|
|
}
|
|
if (configNameElement.scrollWidth > configNameElement.clientWidth) {
|
|
configNameElement.title = displayName;
|
|
} else {
|
|
configNameElement.title = '';
|
|
}
|
|
}
|
|
|
|
if (fileInfo) {
|
|
if (fileModifyTimeElement) {
|
|
var modifyTime = this.formatUnixTime(fileInfo.mtime);
|
|
fileModifyTimeElement.textContent = '<%:Update Time%>: ' + modifyTime;
|
|
if (fileModifyTimeElement.scrollWidth > fileModifyTimeElement.clientWidth) {
|
|
fileModifyTimeElement.title = fileModifyTimeElement.textContent;
|
|
} else {
|
|
fileModifyTimeElement.title = '';
|
|
}
|
|
}
|
|
|
|
if (detailsSection) {
|
|
detailsSection.style.display = 'flex';
|
|
}
|
|
} else {
|
|
if (fileModifyTimeElement) {
|
|
fileModifyTimeElement.textContent = '<%:Update Time%> --';
|
|
}
|
|
|
|
if (detailsSection) {
|
|
detailsSection.style.display = 'none';
|
|
}
|
|
}
|
|
},
|
|
|
|
formatUnixTime: function(unixTimestamp) {
|
|
if (!unixTimestamp || unixTimestamp === 0) {
|
|
return '--';
|
|
}
|
|
|
|
try {
|
|
var date = new Date(unixTimestamp * 1000);
|
|
var year = date.getFullYear();
|
|
var month = String(date.getMonth() + 1).padStart(2, '0');
|
|
var day = String(date.getDate()).padStart(2, '0');
|
|
var hour = String(date.getHours()).padStart(2, '0');
|
|
var minute = String(date.getMinutes()).padStart(2, '0');
|
|
var second = String(date.getSeconds()).padStart(2, '0');
|
|
|
|
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
|
|
} catch (e) {
|
|
return '--';
|
|
}
|
|
},
|
|
|
|
formatFileSize: function(bytes) {
|
|
if (!bytes || bytes === 0) return '--';
|
|
|
|
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
|
|
if (i === 0) {
|
|
return bytes + ' ' + sizes[i];
|
|
}
|
|
|
|
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
|
|
},
|
|
|
|
formatDisplayName: function(fileName) {
|
|
if (!fileName) return '<%:Unknown%>';
|
|
|
|
var name = fileName.split('/').pop().split('\\').pop();
|
|
|
|
//name = name.replace(/\.(yaml|yml)$/i, '');
|
|
|
|
if (name.length > 30) {
|
|
name = name.substring(0, 27) + '...';
|
|
}
|
|
|
|
return name;
|
|
},
|
|
|
|
onConfigChange: function(event) {
|
|
var selectedValue = event.target.value;
|
|
|
|
if (selectedValue) {
|
|
this.currentConfig = selectedValue;
|
|
|
|
for (var i = 0; i < this.configList.length; i++) {
|
|
var file = this.configList[i];
|
|
var filePath = typeof file === 'string' ? file : (file.path || file.filepath || file);
|
|
|
|
if (filePath === selectedValue) {
|
|
this.currentConfigIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var selectedFileInfo = this.getConfigFileInfo(selectedValue);
|
|
this.updateSubscriptionDisplay(selectedValue, selectedFileInfo);
|
|
this.updateNavigationArrows();
|
|
|
|
if (SubscriptionManager.currentConfigFile !== selectedValue) {
|
|
SubscriptionManager.currentConfigFile = selectedValue;
|
|
var detailsSection = document.getElementById('subscription-info-details');
|
|
if (detailsSection) {
|
|
detailsSection.style.display = 'none';
|
|
}
|
|
SubscriptionManager.getSubscriptionInfo();
|
|
}
|
|
} else {
|
|
this.currentConfig = '';
|
|
this.currentConfigIndex = -1;
|
|
SubscriptionManager.currentConfigFile = '';
|
|
this.hideSubscriptionDisplay();
|
|
this.updateNavigationArrows();
|
|
}
|
|
},
|
|
|
|
getConfigFileInfo: function(selectedValue) {
|
|
var selectedFileInfo = null;
|
|
for (var i = 0; i < this.configList.length; i++) {
|
|
var file = this.configList[i];
|
|
var filePath = typeof file === 'string' ? file : (file.path || file.filepath || file);
|
|
|
|
if (filePath === selectedValue) {
|
|
if (typeof file === 'object' && file.mtime && file.size) {
|
|
selectedFileInfo = {
|
|
mtime: file.mtime,
|
|
size: file.size
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return selectedFileInfo;
|
|
},
|
|
|
|
updateNavigationArrows: function() {
|
|
var prevArrow = document.getElementById('subscription-prev-arrow');
|
|
var nextArrow = document.getElementById('subscription-next-arrow');
|
|
|
|
if (!prevArrow || !nextArrow) return;
|
|
|
|
var hasMultipleConfigs = this.configList.length > 1;
|
|
var currentIndex = this.currentConfigIndex;
|
|
|
|
if (!hasMultipleConfigs || currentIndex === -1) {
|
|
prevArrow.style.display = 'none';
|
|
nextArrow.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
prevArrow.style.display = 'block';
|
|
nextArrow.style.display = 'block';
|
|
|
|
prevArrow.classList.remove('disabled');
|
|
nextArrow.classList.remove('disabled');
|
|
},
|
|
|
|
switchToConfigByIndex: function(index) {
|
|
if (this.configList.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
if (index < 0) {
|
|
index = this.configList.length - 1;
|
|
} else if (index >= this.configList.length) {
|
|
index = 0;
|
|
}
|
|
|
|
var file = this.configList[index];
|
|
var filePath = typeof file === 'string' ? file : (file.path || file.filepath || file);
|
|
|
|
if (this.selectElement) {
|
|
this.selectElement.value = filePath;
|
|
}
|
|
|
|
this.currentConfig = filePath;
|
|
this.currentConfigIndex = index;
|
|
|
|
var fileInfo = this.getConfigFileInfo(filePath);
|
|
this.updateSubscriptionDisplay(filePath, fileInfo);
|
|
this.updateNavigationArrows();
|
|
|
|
if (SubscriptionManager.currentConfigFile !== filePath) {
|
|
SubscriptionManager.currentConfigFile = filePath;
|
|
var detailsSection = document.getElementById('subscription-info-details');
|
|
if (detailsSection) {
|
|
detailsSection.style.display = 'none';
|
|
}
|
|
SubscriptionManager.getSubscriptionInfo();
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
refreshConfigList: function() {
|
|
this.loadConfigFileList();
|
|
},
|
|
|
|
getCurrentConfig: function() {
|
|
return this.currentConfig;
|
|
},
|
|
|
|
getSelectedConfig: function() {
|
|
return this.selectElement ? this.selectElement.value : '';
|
|
}
|
|
};
|
|
|
|
var SubscriptionManager = {
|
|
currentConfigFile: '',
|
|
retryCount: 0,
|
|
maxRetries: 5,
|
|
updateTimer: null,
|
|
isInitialized: false,
|
|
|
|
init: function() {
|
|
if (this.isInitialized) return;
|
|
this.isInitialized = true;
|
|
|
|
setTimeout(function() {
|
|
SubscriptionManager.loadSubscriptionInfo();
|
|
SubscriptionManager.startAutoUpdate();
|
|
}, 500);
|
|
},
|
|
|
|
loadSubscriptionInfo: function() {
|
|
var currentConfig = ConfigFileManager.getCurrentConfig() || ConfigFileManager.getSelectedConfig();
|
|
if (currentConfig !== this.currentConfigFile) {
|
|
this.currentConfigFile = currentConfig;
|
|
this.getSubscriptionInfo();
|
|
}
|
|
},
|
|
|
|
getSubscriptionInfo: function() {
|
|
if (ConfigFileManager.configList.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (!this.currentConfigFile) return;
|
|
|
|
var requestConfigFile = this.currentConfigFile;
|
|
var filename = this.extractFilename(this.currentConfigFile);
|
|
if (!filename) return;
|
|
|
|
var cachedData = localStorage.getItem('sub_info_' + filename);
|
|
var shouldFetchNew = true;
|
|
|
|
if (cachedData) {
|
|
try {
|
|
var parsedData = JSON.parse(cachedData);
|
|
|
|
if (parsedData.providers) {
|
|
if (parsedData.providers.length > 0) {
|
|
if (this.currentConfigFile === requestConfigFile) {
|
|
this.displaySubscriptionInfo(parsedData);
|
|
}
|
|
} else {
|
|
if (this.currentConfigFile === requestConfigFile) {
|
|
this.showNoInfo();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (parsedData.get_time) {
|
|
var currentTime = Math.floor(Date.now() / 1000);
|
|
var cacheTime = parseInt(parsedData.get_time);
|
|
var timeDiff = currentTime - cacheTime;
|
|
var halfHourInSeconds = 30 * 60;
|
|
|
|
if (timeDiff <= halfHourInSeconds) {
|
|
shouldFetchNew = false;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
shouldFetchNew = true;
|
|
}
|
|
}
|
|
|
|
if (shouldFetchNew) {
|
|
if (!cachedData) {
|
|
this.showLoading();
|
|
}
|
|
|
|
StateManager.cachedXHRGetWithParams('<%=luci.dispatcher.build_url("admin", "services", "openclash", "sub_info_get")%>', {filename: filename}, function(x, status) {
|
|
if (SubscriptionManager.currentConfigFile !== requestConfigFile) {
|
|
return;
|
|
}
|
|
|
|
var needsErrorHandling = false;
|
|
if (x && x.status == 200 && status.providers) {
|
|
var newProvidersLength = (status.providers) ? status.providers.length : 0;
|
|
var cachedProvidersLength = (parsedData && parsedData.providers) ? parsedData.providers.length : 0;
|
|
|
|
if (newProvidersLength !== cachedProvidersLength && cachedProvidersLength > newProvidersLength) {
|
|
needsErrorHandling = true;
|
|
}
|
|
|
|
if (!needsErrorHandling) {
|
|
SubscriptionManager.retryCount = 0;
|
|
localStorage.setItem('sub_info_' + filename, JSON.stringify(status));
|
|
if (status.providers && status.providers.length === 0) {
|
|
SubscriptionManager.showNoInfo();
|
|
return;
|
|
}
|
|
SubscriptionManager.displaySubscriptionInfo(status);
|
|
return;
|
|
}
|
|
} else {
|
|
needsErrorHandling = true;
|
|
}
|
|
|
|
if (needsErrorHandling) {
|
|
SubscriptionManager.handleError(cachedData);
|
|
}
|
|
}, true);
|
|
}
|
|
},
|
|
|
|
displaySubscriptionInfo: function(data) {
|
|
if (ConfigFileManager.configList.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var container = document.getElementById('subscription-info-display');
|
|
var progressSection = document.getElementById('subscription-progress-section');
|
|
var detailsSection = document.getElementById('subscription-info-details');
|
|
|
|
if (!container) return;
|
|
|
|
var configFileContent = document.querySelector('.config-file-content');
|
|
if (configFileContent && configFileContent.classList.contains('empty-state')) {
|
|
return;
|
|
}
|
|
|
|
container.style.display = 'flex';
|
|
|
|
if (data && data.providers && data.providers.length > 0) {
|
|
if (data.providers.length > 1 || data.providers[0].provider_name) {
|
|
this.displayMultipleProviders(data.providers, progressSection);
|
|
} else {
|
|
this.displaySingleProvider(data.providers[0], progressSection);
|
|
}
|
|
} else {
|
|
progressSection.style.display = 'none';
|
|
}
|
|
|
|
if (detailsSection) {
|
|
detailsSection.style.display = 'flex';
|
|
}
|
|
},
|
|
|
|
displaySingleProvider: function(provider, progressSection) {
|
|
if (!progressSection) return;
|
|
|
|
progressSection.innerHTML = '';
|
|
progressSection.style.display = 'flex';
|
|
progressSection.className = 'subscription-progress';
|
|
|
|
var progressBar = document.createElement('div');
|
|
progressBar.className = 'subscription-progress-bar';
|
|
|
|
progressFill = document.createElement('div');
|
|
progressFill.className = 'subscription-progress-fill';
|
|
progressBar.appendChild(progressFill);
|
|
|
|
infoText = document.createElement('div');
|
|
infoText.className = 'subscription-info-text';
|
|
|
|
progressSection.appendChild(progressBar);
|
|
progressSection.appendChild(infoText);
|
|
|
|
var percent = parseFloat(provider.percent) || 0;
|
|
var used = String(provider.surplus || provider.used || '0 B');
|
|
var total = String(provider.total || '0 B');
|
|
var expire = String(provider.expire || '');
|
|
var daysLeft = parseInt(provider.day_left, 10);
|
|
if (isNaN(daysLeft)) daysLeft = 0;
|
|
|
|
progressFill.style.width = percent + '%';
|
|
progressFill.className = 'subscription-progress-fill ' +
|
|
(percent >= 50 ? 'high' : (percent >= 20 ? 'medium' : 'low'));
|
|
|
|
var infoString = used + ' / ' + total + ' (' + percent + '%)';
|
|
var tooltipString = infoString;
|
|
if (expire && expire !== 'null' && daysLeft > 0) {
|
|
infoString += ' • ' + expire + ' (<%:Remaining%> ' + daysLeft + ' <%:days%>)';
|
|
tooltipString += '\n' + expire + ' (<%:Remaining%> ' + daysLeft + ' <%:days%>)';
|
|
}
|
|
|
|
infoText.textContent = infoString;
|
|
infoText.title = tooltipString;
|
|
},
|
|
|
|
displayMultipleProviders: function(providers, progressSection) {
|
|
if (!progressSection) return;
|
|
|
|
var displayProviders = providers;
|
|
var providerCount = displayProviders.length;
|
|
|
|
progressSection.innerHTML = '';
|
|
progressSection.style.display = 'flex';
|
|
progressSection.className = 'subscription-providers-grid';
|
|
progressSection.setAttribute('data-count', providerCount);
|
|
|
|
displayProviders.forEach(function(provider) {
|
|
var card = document.createElement('div');
|
|
card.className = 'provider-card';
|
|
|
|
var cardName = document.createElement('div');
|
|
var providerName = String(provider.provider_name || 'Unknown');
|
|
cardName.className = 'subscription-info-title';
|
|
cardName.textContent = providerName;
|
|
cardName.title = providerName;
|
|
card.appendChild(cardName);
|
|
|
|
var progressBar = document.createElement('div');
|
|
progressBar.className = 'subscription-progress-bar';
|
|
|
|
var progressFill = document.createElement('div');
|
|
var percent = parseFloat(provider.percent) || 0;
|
|
progressFill.style.width = percent + '%';
|
|
progressFill.className = 'subscription-progress-fill ' +
|
|
(percent >= 50 ? 'high' : (percent >= 20 ? 'medium' : 'low'));
|
|
|
|
progressBar.appendChild(progressFill);
|
|
card.appendChild(progressBar);
|
|
|
|
var infoText = document.createElement('div');
|
|
infoText.className = 'subscription-info-text';
|
|
|
|
var used = String(provider.used || '0 B');
|
|
var total = String(provider.total || 'N/A');
|
|
var expire = String(provider.expire || '');
|
|
var daysLeft = parseInt(provider.day_left, 10);
|
|
if (isNaN(daysLeft)) daysLeft = 0;
|
|
|
|
var infoString = used + ' / ' + total + ' (' + percent + '%)';
|
|
if (expire && expire !== 'null' && daysLeft > 0) {
|
|
infoString += ' • ' + expire + ' (<%:Remaining%> ' + daysLeft + ' <%:days%>)';
|
|
}
|
|
|
|
var tooltipString = used + ' / ' + total + ' (' + percent + '%)';
|
|
if (expire && expire !== 'null' && daysLeft > 0) {
|
|
tooltipString += '\n' + expire + ' (<%:Remaining%> ' + daysLeft + ' <%:days%>)';
|
|
}
|
|
|
|
infoText.textContent = infoString;
|
|
infoText.title = tooltipString;
|
|
|
|
card.appendChild(infoText);
|
|
progressSection.appendChild(card);
|
|
});
|
|
|
|
if (providerCount > 3) {
|
|
this.initDragScroll(progressSection);
|
|
}
|
|
},
|
|
|
|
initDragScroll: function(element) {
|
|
var isDragging = false;
|
|
var startX;
|
|
var scrollLeft;
|
|
var hasMoved = false;
|
|
|
|
var onMouseDown = function(e) {
|
|
if (e.target.tagName === 'A' || e.target.tagName === 'BUTTON') {
|
|
return;
|
|
}
|
|
|
|
isDragging = true;
|
|
hasMoved = false;
|
|
startX = e.pageX - element.offsetLeft;
|
|
scrollLeft = element.scrollLeft;
|
|
element.classList.add('dragging');
|
|
};
|
|
|
|
var onMouseMove = function(e) {
|
|
if (!isDragging) return;
|
|
|
|
var x = e.pageX - element.offsetLeft;
|
|
var walk = (x - startX) * 1.5;
|
|
|
|
if (Math.abs(walk) > 5) {
|
|
hasMoved = true;
|
|
e.preventDefault();
|
|
element.scrollLeft = scrollLeft - walk;
|
|
}
|
|
};
|
|
|
|
var onMouseUp = function() {
|
|
isDragging = false;
|
|
element.classList.remove('dragging');
|
|
};
|
|
|
|
var onMouseLeave = function() {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
element.classList.remove('dragging');
|
|
}
|
|
};
|
|
|
|
element.addEventListener('mousedown', onMouseDown);
|
|
element.addEventListener('mousemove', onMouseMove);
|
|
element.addEventListener('mouseup', onMouseUp);
|
|
element.addEventListener('mouseleave', onMouseLeave);
|
|
|
|
element.addEventListener('dragstart', function(e) {
|
|
e.preventDefault();
|
|
});
|
|
},
|
|
|
|
showNoInfo: function() {
|
|
if (ConfigFileManager.configList.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var container = document.getElementById('subscription-info-display');
|
|
var progressSection = document.getElementById('subscription-progress-section');
|
|
var detailsSection = document.getElementById('subscription-info-details');
|
|
|
|
if (!container) return;
|
|
|
|
var configFileContent = document.querySelector('.config-file-content');
|
|
if (configFileContent && configFileContent.classList.contains('empty-state')) {
|
|
return;
|
|
}
|
|
|
|
container.style.display = 'flex';
|
|
|
|
if (progressSection) {
|
|
progressSection.innerHTML = '<div class="subscription-no-info"><%:No Sub Info Found%></div>';
|
|
progressSection.style.display = 'flex';
|
|
progressSection.className = 'subscription-progress';
|
|
}
|
|
|
|
if (detailsSection) {
|
|
var fileModifyTimeElement = document.getElementById('file-modify-time');
|
|
|
|
var hasFileInfo = false;
|
|
if (fileModifyTimeElement) {
|
|
var modifyTimeText = fileModifyTimeElement.textContent || '';
|
|
hasFileInfo = !modifyTimeText.includes('--');
|
|
}
|
|
|
|
detailsSection.style.display = hasFileInfo ? 'flex' : 'none';
|
|
}
|
|
},
|
|
|
|
showLoading: function() {
|
|
if (ConfigFileManager.configList.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var container = document.getElementById('subscription-info-display');
|
|
var progressSection = document.getElementById('subscription-progress-section');
|
|
var detailsSection = document.getElementById('subscription-info-details');
|
|
|
|
if (!container) return;
|
|
|
|
var configFileContent = document.querySelector('.config-file-content');
|
|
if (configFileContent && configFileContent.classList.contains('empty-state')) {
|
|
return;
|
|
}
|
|
|
|
container.style.display = 'flex';
|
|
|
|
if (progressSection) {
|
|
progressSection.innerHTML = '<div class="subscription-loading"><%:Collecting data...%></div>';
|
|
progressSection.style.display = 'flex';
|
|
progressSection.className = 'subscription-progress';
|
|
}
|
|
|
|
if (detailsSection) {
|
|
detailsSection.style.display = 'none';
|
|
}
|
|
},
|
|
|
|
handleError: function(cachedData) {
|
|
if (!cachedData) {
|
|
this.showNoInfo();
|
|
}
|
|
|
|
if (this.retryCount >= this.maxRetries) {
|
|
this.retryCount = 0;
|
|
if (this.currentConfigFile && cachedData) {
|
|
localStorage.removeItem('sub_info_' + this.extractFilename(this.currentConfigFile));
|
|
}
|
|
} else {
|
|
this.retryCount++;
|
|
setTimeout(function() {
|
|
SubscriptionManager.getSubscriptionInfo();
|
|
}, 15000);
|
|
}
|
|
},
|
|
|
|
extractFilename: function(path) {
|
|
if (!path) return '';
|
|
var parts = path.split('/');
|
|
var filename = parts[parts.length - 1];
|
|
|
|
if (filename.endsWith('.yaml')) {
|
|
filename = filename.slice(0, -5);
|
|
} else if (filename.endsWith('.yml')) {
|
|
filename = filename.slice(0, -4);
|
|
}
|
|
|
|
return filename;
|
|
},
|
|
|
|
startAutoUpdate: function() {
|
|
if (this.updateTimer) {
|
|
clearTimeout(this.updateTimer);
|
|
}
|
|
|
|
this.updateTimer = setTimeout(function() {
|
|
SubscriptionManager.getSubscriptionInfo();
|
|
SubscriptionManager.startAutoUpdate();
|
|
}, 60000 * 15);
|
|
},
|
|
|
|
stopAutoUpdate: function() {
|
|
if (this.updateTimer) {
|
|
clearTimeout(this.updateTimer);
|
|
this.updateTimer = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
var SubscriptionUrlSetter = {
|
|
filename: '',
|
|
|
|
show: function(filename) {
|
|
this.filename = filename;
|
|
var overlay = document.getElementById('subscription-url-overlay');
|
|
var textarea = document.getElementById('subscription-url-textarea');
|
|
if (overlay && textarea) {
|
|
textarea.value = '';
|
|
overlay.style.display = 'flex';
|
|
textarea.focus();
|
|
this.bindEvents();
|
|
this.loadUrls();
|
|
}
|
|
},
|
|
|
|
hide: function() {
|
|
var overlay = document.getElementById('subscription-url-overlay');
|
|
if (overlay) {
|
|
overlay.style.display = 'none';
|
|
}
|
|
this.unbindEvents();
|
|
},
|
|
|
|
bindEvents: function() {
|
|
var textarea = document.getElementById('subscription-url-textarea');
|
|
var overlay = document.getElementById('subscription-url-overlay');
|
|
|
|
if (textarea) {
|
|
textarea.addEventListener('keydown', this.handleKeyDown.bind(this));
|
|
}
|
|
if (overlay) {
|
|
overlay.addEventListener('click', this.handleOverlayClick.bind(this));
|
|
}
|
|
},
|
|
|
|
unbindEvents: function() {
|
|
var textarea = document.getElementById('subscription-url-textarea');
|
|
var overlay = document.getElementById('subscription-url-overlay');
|
|
|
|
if (textarea) {
|
|
textarea.removeEventListener('keydown', this.handleKeyDown.bind(this));
|
|
}
|
|
if (overlay) {
|
|
overlay.removeEventListener('click', this.handleOverlayClick.bind(this));
|
|
}
|
|
},
|
|
|
|
handleKeyDown: function(event) {
|
|
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
this.submit();
|
|
} else if (event.key === 'Escape') {
|
|
this.hide();
|
|
}
|
|
},
|
|
|
|
handleOverlayClick: function(event) {
|
|
if (event.target.id === 'subscription-url-overlay') {
|
|
this.hide();
|
|
}
|
|
},
|
|
|
|
submit: function() {
|
|
var textarea = document.getElementById('subscription-url-textarea');
|
|
if (!textarea) return;
|
|
|
|
var urls = textarea.value.trim();
|
|
|
|
var urlList = urls.split('\n').map(function(url) {
|
|
return url.trim();
|
|
}).filter(function(url) {
|
|
return url !== '';
|
|
});
|
|
|
|
var newUrl = urlList.join('\n');
|
|
|
|
this.hide();
|
|
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "set_subinfo_url")%>', {
|
|
filename: this.filename,
|
|
url: newUrl
|
|
}, function(x, status) {
|
|
if (x && x.status == 200 && status.info === "Success") {
|
|
refreshSubscriptionInfo();
|
|
} else if (x && x.status == 200 && status.info === "Delete success") {
|
|
refreshSubscriptionInfo();
|
|
} else {
|
|
alert('<%:Specify subscribe infos sources url failed:%>\n' + (status.info || '<%:Unknown error%>'));
|
|
}
|
|
});
|
|
},
|
|
|
|
handleSubURL: function(urlresult) {
|
|
var textarea = document.getElementById('subscription-url-textarea');
|
|
if (!textarea) return;
|
|
|
|
var urls = '';
|
|
if (urlresult.type && urlresult.type === "multiple") {
|
|
for (var data in urlresult.providers) {
|
|
urls += urlresult.providers[data].url + '#name=' + urlresult.providers[data].name + '\n';
|
|
}
|
|
}
|
|
if (urlresult.type && urlresult.type === "single") {
|
|
urls = urlresult.url;
|
|
}
|
|
textarea.value = urls;
|
|
},
|
|
|
|
loadUrls: function() {
|
|
var self = this;
|
|
var existingData = localStorage.getItem('sub_info_' + this.filename);
|
|
if (existingData) {
|
|
existingData = JSON.parse(existingData);
|
|
if (existingData.url_result) {
|
|
this.handleSubURL(existingData.url_result);
|
|
return;
|
|
}
|
|
};
|
|
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "get_subscribe_info_data")%>', {
|
|
filename: this.filename
|
|
}, function(x, status) {
|
|
if (x && x.status == 200 && status) {
|
|
self.handleSubURL(status);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
var LogManager = {
|
|
isPolling: false,
|
|
pollTimer: null,
|
|
maxPollTime: 60000,
|
|
pollInterval: 1000,
|
|
startTime: 0,
|
|
lastLogContent: '',
|
|
retryCount: 0,
|
|
maxRetries: 3,
|
|
retryDelay: 1000,
|
|
|
|
startLogDisplay: function(initialMessage) {
|
|
if (this.isPolling) {
|
|
this.stopLogDisplay();
|
|
}
|
|
if (this.pollTimer) {
|
|
clearTimeout(this.pollTimer);
|
|
this.pollTimer = null;
|
|
}
|
|
|
|
StateManager.clearCache('<%=luci.dispatcher.build_url("admin", "services", "openclash", "startlog")%>');
|
|
|
|
if (DOMCache.oclog && initialMessage) {
|
|
DOMCache.oclog.style.display = 'inline-flex';
|
|
DOMCache.oclog.innerHTML = '<b style="color:var(--tip-color)">' + initialMessage + '</b>';
|
|
}
|
|
|
|
this.isPolling = true;
|
|
this.startTime = Date.now();
|
|
this.lastLogContent = '';
|
|
this.retryCount = 0;
|
|
|
|
var self = this;
|
|
setTimeout(function() {
|
|
if (self.isPolling) {
|
|
self.pollLog();
|
|
}
|
|
}, 2000);
|
|
},
|
|
|
|
stopLogDisplay: function() {
|
|
this.isPolling = false;
|
|
if (this.pollTimer) {
|
|
clearTimeout(this.pollTimer);
|
|
this.pollTimer = null;
|
|
}
|
|
this.lastLogContent = '';
|
|
this.retryCount = 0;
|
|
if (DOMCache.oclog) {
|
|
setTimeout(function() {
|
|
if (!LogManager.isPolling) {
|
|
DOMCache.oclog.style.display = 'none';
|
|
}
|
|
}, 1000);
|
|
}
|
|
},
|
|
|
|
pollLog: function() {
|
|
if (!this.isPolling) return;
|
|
|
|
if (Date.now() - this.startTime > this.maxPollTime) {
|
|
this.stopLogDisplay();
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "startlog")%>', function(x, status) {
|
|
if (!self.isPolling) return;
|
|
|
|
if (x && x.status == 200 && status && typeof status.startlog !== 'undefined') {
|
|
var logContent = (status.startlog || '').trim();
|
|
|
|
if (self.checkForErrors(logContent)) {
|
|
self.stopLogDisplay();
|
|
return;
|
|
}
|
|
|
|
var contentToDisplay = logContent.replace('##FINISH##', '').trim();
|
|
if (contentToDisplay && contentToDisplay !== self.lastLogContent) {
|
|
self.displayLog(contentToDisplay);
|
|
}
|
|
|
|
if (logContent.includes('##FINISH##')) {
|
|
setTimeout(function() {
|
|
self.stopLogDisplay();
|
|
}, 3000);
|
|
return;
|
|
}
|
|
|
|
if (logContent !== '') {
|
|
self.retryCount = 0;
|
|
self.scheduleNextPoll(self.pollInterval);
|
|
} else {
|
|
self.handleEmptyResponse();
|
|
}
|
|
} else {
|
|
self.handleFailure();
|
|
}
|
|
}, true);
|
|
},
|
|
|
|
handleEmptyResponse: function() {
|
|
this.scheduleNextPoll(this.pollInterval);
|
|
},
|
|
|
|
handleFailure: function() {
|
|
if (this.retryCount < this.maxRetries) {
|
|
this.retryCount++;
|
|
var delay = Math.min(this.retryDelay * Math.pow(2, this.retryCount - 1), 5000);
|
|
this.scheduleNextPoll(delay);
|
|
} else {
|
|
this.stopLogDisplay();
|
|
}
|
|
},
|
|
|
|
scheduleNextPoll: function(delay) {
|
|
var self = this;
|
|
this.pollTimer = setTimeout(function() {
|
|
self.pollLog();
|
|
}, delay);
|
|
},
|
|
|
|
checkForErrors: function(log) {
|
|
if (!log) return false;
|
|
if (log.match(/level=(fatal|error)/)) {
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "del_start_log")%>', null, function(x) {});
|
|
|
|
var msgParts = log.split('msg=');
|
|
var errorMsg = msgParts.length > 1 ? msgParts[1].replace(/"/g, '') : log;
|
|
|
|
setTimeout(function() {
|
|
alert('<%:OpenClash Start Failed%>:\n\n' + errorMsg);
|
|
}, 500);
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
displayLog: function(logContent) {
|
|
if (!this.isPolling || !DOMCache.oclog) return;
|
|
|
|
var cleanLog = this.cleanLogContent(logContent);
|
|
this.lastLogContent = cleanLog;
|
|
|
|
var color = this.getLogColor(cleanLog);
|
|
var displayText = this.formatLogText(cleanLog);
|
|
|
|
DOMCache.oclog.style.display = 'inline-flex';
|
|
DOMCache.oclog.innerHTML = '<b style="color:' + color + '">' + displayText + '</b>';
|
|
},
|
|
|
|
cleanLogContent: function(content) {
|
|
return content ? content.replace(/[\r\n]+/g, ' ').replace('##FINISH##', '').trim() : '';
|
|
},
|
|
|
|
getLogColor: function(log) {
|
|
for (var levelKey in window.levelTranslations) {
|
|
var translatedText = '[' + window.levelTranslations[levelKey] + ']';
|
|
if (log.includes(translatedText)) {
|
|
return "var(--" + levelKey + "-color)";
|
|
}
|
|
}
|
|
return "var(--info-color)";
|
|
},
|
|
|
|
formatLogText: function(log) {
|
|
if (!log) return '<%:Processing...%>';
|
|
|
|
var cleanText = log.replace(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.\d]*Z?\s*/, '')
|
|
.replace(/^time="[^"]*"\s*/, '')
|
|
.replace(/^level=\w+\s*/, '')
|
|
.replace(/^msg="?([^"]*)"?\s*/, '$1');
|
|
|
|
if (cleanText.length > 60) {
|
|
cleanText = cleanText.substring(0, 57) + '...';
|
|
}
|
|
|
|
return this.escapeHtml(cleanText) || '<%:Processing...%>';
|
|
},
|
|
|
|
escapeHtml: function(text) {
|
|
var div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
};
|
|
|
|
var SystemStatusManager = {
|
|
pollTimer: null,
|
|
pollInterval: 5000,
|
|
retryCount: 0,
|
|
maxRetries: 3,
|
|
isEnabled: false,
|
|
|
|
start: function() {
|
|
if (this.isEnabled) return;
|
|
this.isEnabled = true;
|
|
this.retryCount = 0;
|
|
this.poll();
|
|
},
|
|
|
|
stop: function() {
|
|
this.isEnabled = false;
|
|
if (this.pollTimer) {
|
|
clearTimeout(this.pollTimer);
|
|
this.pollTimer = null;
|
|
}
|
|
},
|
|
|
|
poll: function() {
|
|
if (!this.isEnabled) return;
|
|
|
|
var self = this;
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "toolbar_show_sys")%>', function(x, status) {
|
|
if (x && x.status == 200 && x.responseText != "") {
|
|
self.retryCount = 0;
|
|
var cpuColor = status.cpu <= 50 ? "var(--success-color)" : (status.cpu <= 80 ? "var(--tip-color)" : "var(--error-color)");
|
|
var cpuValue = status.cpu <= 100 ? status.cpu + " %" : "0 %";
|
|
document.getElementById("cpu_t").innerHTML = "<font style=\"color:"+cpuColor+"\">"+cpuValue+"</font>";
|
|
|
|
var loadValue = parseFloat(status.load_avg) || 0;
|
|
var loadColor = loadValue <= 50 ? "var(--success-color)" : (loadValue <= 80 ? "var(--tip-color)" : "var(--error-color)");
|
|
document.getElementById("load_a").innerHTML = "<font style=\"color:"+loadColor+"\">"+status.load_avg+" %</font>";
|
|
} else {
|
|
self.handleError();
|
|
}
|
|
|
|
if (self.isEnabled) {
|
|
self.pollTimer = setTimeout(function() {
|
|
self.poll();
|
|
}, self.pollInterval);
|
|
}
|
|
}, true);
|
|
},
|
|
|
|
handleError: function() {
|
|
this.retryCount++;
|
|
|
|
if (this.retryCount >= this.maxRetries) {
|
|
document.getElementById("cpu_t").innerHTML = "<font style=\"color:var(--success-color)\">0 %</font>";
|
|
document.getElementById("load_a").innerHTML = "<font style=\"color:var(--success-color)\">0 %</font>";
|
|
this.retryCount = 0;
|
|
}
|
|
},
|
|
|
|
setPollInterval: function(interval) {
|
|
this.pollInterval = interval;
|
|
}
|
|
};
|
|
|
|
var NetworkStatsManager = {
|
|
pollTimer: null,
|
|
pollInterval: 3000,
|
|
retryCount: 0,
|
|
maxRetries: 3,
|
|
isEnabled: false,
|
|
|
|
start: function() {
|
|
if (this.isEnabled) return;
|
|
this.isEnabled = true;
|
|
this.retryCount = 0;
|
|
this.poll();
|
|
},
|
|
|
|
stop: function() {
|
|
this.isEnabled = false;
|
|
if (this.pollTimer) {
|
|
clearTimeout(this.pollTimer);
|
|
this.pollTimer = null;
|
|
}
|
|
},
|
|
|
|
poll: function() {
|
|
if (!this.isEnabled) return;
|
|
|
|
var self = this;
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "toolbar_show")%>', function(x, status) {
|
|
if (x && x.status == 200 && x.responseText != "") {
|
|
self.retryCount = 0;
|
|
self.updateNetworkStats(status);
|
|
} else {
|
|
self.handleError();
|
|
}
|
|
|
|
if (self.isEnabled) {
|
|
self.pollTimer = setTimeout(function() {
|
|
self.poll();
|
|
}, self.pollInterval);
|
|
}
|
|
}, true);
|
|
},
|
|
|
|
updateNetworkStats: function(status) {
|
|
var updates = [
|
|
{element: document.getElementById("upload_"), content: "<font style=\"color:var(--success-color)\">"+status.up+"</font>"},
|
|
{element: document.getElementById("download_"), content: "<font style=\"color:var(--success-color)\">"+status.down+"</font>"},
|
|
{element: document.getElementById("uploadtotal_"), content: "<font style=\"color:var(--success-color)\">"+status.up_total+"</font>"},
|
|
{element: document.getElementById("downloadtotal_"), content: "<font style=\"color:var(--success-color)\">"+status.down_total+"</font>"},
|
|
{element: document.getElementById("mem_t"), content: "<font style=\"color:var(--success-color)\">"+status.mem+"</font>"},
|
|
{element: document.getElementById("connect_t"), content: "<font style=\"color:var(--success-color)\">"+status.connections+"</font>"}
|
|
];
|
|
|
|
if (!SystemStatusManager.isEnabled) {
|
|
var cpuColor = status.cpu <= 50 ? "var(--success-color)" : (status.cpu <= 80 ? "var(--tip-color)" : "var(--error-color)");
|
|
var cpuValue = status.cpu <= 100 ? status.cpu + " %" : "0 %";
|
|
updates.push({element: document.getElementById("cpu_t"), content: "<font style=\"color:"+cpuColor+"\">"+cpuValue+"</font>"});
|
|
|
|
var loadValue = parseFloat(status.load_avg) || 0;
|
|
var loadColor = loadValue <= 50 ? "var(--success-color)" : (loadValue <= 80 ? "var(--tip-color)" : "var(--error-color)");
|
|
updates.push({element: document.getElementById("load_a"), content: "<font style=\"color:"+loadColor+"\">"+status.load_avg+" %</font>"});
|
|
}
|
|
|
|
StateManager.batchUpdateDOM(updates);
|
|
},
|
|
|
|
handleError: function() {
|
|
this.retryCount++;
|
|
|
|
if (this.retryCount >= this.maxRetries) {
|
|
var fallbackUpdates = [
|
|
{element: document.getElementById("upload_"), content: "<font style=\"color:var(--success-color)\">0 B/S</font>"},
|
|
{element: document.getElementById("download_"), content: "<font style=\"color:var(--success-color)\">0 B/S</font>"},
|
|
{element: document.getElementById("uploadtotal_"), content: "<font style=\"color:var(--success-color)\">0 KB</font>"},
|
|
{element: document.getElementById("downloadtotal_"), content: "<font style=\"color:var(--success-color)\">0 KB</font>"},
|
|
{element: document.getElementById("mem_t"), content: "<font style=\"color:var(--success-color)\">0 KB</font>"},
|
|
{element: document.getElementById("connect_t"), content: "<font style=\"color:var(--success-color)\">0</font>"}
|
|
];
|
|
|
|
if (!SystemStatusManager.isEnabled) {
|
|
fallbackUpdates.push({element: document.getElementById("cpu_t"), content: "<font style=\"color:var(--success-color)\">0 %</font>"});
|
|
fallbackUpdates.push({element: document.getElementById("load_a"), content: "<font style=\"color:var(--success-color)\">0 %</font>"});
|
|
}
|
|
|
|
StateManager.batchUpdateDOM(fallbackUpdates);
|
|
this.retryCount = 0;
|
|
}
|
|
},
|
|
|
|
setPollInterval: function(interval) {
|
|
this.pollInterval = interval;
|
|
}
|
|
};
|
|
|
|
var SettingsManager = {
|
|
pendingOperations: new Set(),
|
|
pausedPolls: new Set(),
|
|
|
|
pausePoll: function(pollName, duration) {
|
|
this.pausedPolls.add(pollName);
|
|
setTimeout(() => {
|
|
this.pausedPolls.delete(pollName);
|
|
}, duration || 3000);
|
|
},
|
|
|
|
isPollPaused: function(pollName) {
|
|
return this.pausedPolls.has(pollName);
|
|
},
|
|
|
|
updateUIState: function(setting, value) {
|
|
setTimeout(() => {
|
|
function setCheckedAndDisabled(elements, checkedIndex) {
|
|
for (let i = 0; i < elements.length; i++) {
|
|
elements[i].checked = (i === checkedIndex);
|
|
elements[i].disabled = true;
|
|
}
|
|
}
|
|
switch(setting) {
|
|
case 'meta_sniffer':
|
|
var metaSnifferOn = document.getElementById('meta_sniffer_on');
|
|
var metaSnifferOff = document.getElementById('meta_sniffer_off');
|
|
if (metaSnifferOn && metaSnifferOff) {
|
|
setCheckedAndDisabled([metaSnifferOn, metaSnifferOff], value === '1' ? 0 : 1);
|
|
var changeEvent = new Event('change', { bubbles: true });
|
|
(value === '1' ? metaSnifferOn : metaSnifferOff).dispatchEvent(changeEvent);
|
|
}
|
|
break;
|
|
case 'respect_rules':
|
|
var respectRulesOn = document.getElementById('respect_rules_on');
|
|
var respectRulesOff = document.getElementById('respect_rules_off');
|
|
if (respectRulesOn && respectRulesOff) {
|
|
setCheckedAndDisabled([respectRulesOn, respectRulesOff], value === '1' ? 0 : 1);
|
|
var changeEvent = new Event('change', { bubbles: true });
|
|
(value === '1' ? respectRulesOn : respectRulesOff).dispatchEvent(changeEvent);
|
|
}
|
|
break;
|
|
case 'oversea':
|
|
var oversea0 = document.getElementById('oc_setting_oversea_0');
|
|
var oversea1 = document.getElementById('oc_setting_oversea_1');
|
|
var oversea2 = document.getElementById('oc_setting_oversea_2');
|
|
if (oversea0 && oversea1 && oversea2) {
|
|
setCheckedAndDisabled([oversea0, oversea1, oversea2], value === '0' ? 0 : (value === '1' ? 1 : 2));
|
|
var changeEvent = new Event('change', { bubbles: true });
|
|
[oversea0, oversea1, oversea2][value === '0' ? 0 : (value === '1' ? 1 : 2)].dispatchEvent(changeEvent);
|
|
}
|
|
break;
|
|
case 'stream_unlock':
|
|
var streamUnlockOn = document.getElementById('stream_unlock_on');
|
|
var streamUnlockOff = document.getElementById('stream_unlock_off');
|
|
if (streamUnlockOn && streamUnlockOff) {
|
|
setCheckedAndDisabled([streamUnlockOn, streamUnlockOff], value === '1' ? 0 : 1);
|
|
var changeEvent = new Event('change', { bubbles: true });
|
|
(value === '1' ? streamUnlockOn : streamUnlockOff).dispatchEvent(changeEvent);
|
|
}
|
|
break;
|
|
case 'rule_mode':
|
|
var radioElements = document.getElementsByName("radios");
|
|
if (radioElements && radioElements.length > 0) {
|
|
for (var i = 0; i < radioElements.length; i++) {
|
|
radioElements[i].checked = radioElements[i].value === value;
|
|
radioElements[i].disabled = true;
|
|
if (radioElements[i].checked) {
|
|
var changeEvent = new Event('change', { bubbles: true });
|
|
radioElements[i].dispatchEvent(changeEvent);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 'run_mode':
|
|
var radioRuElements = document.getElementsByName("radios-ru");
|
|
if (radioRuElements && radioRuElements.length > 0) {
|
|
for (var i = 0; i < radioRuElements.length; i++) {
|
|
radioRuElements[i].checked = radioRuElements[i].value === value;
|
|
radioRuElements[i].disabled = true;
|
|
if (radioRuElements[i].checked) {
|
|
var changeEvent = new Event('change', { bubbles: true });
|
|
radioRuElements[i].dispatchEvent(changeEvent);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}, 10);
|
|
},
|
|
|
|
switchSetting: function(setting, value, endpoint, additionalParams) {
|
|
var operationKey = setting + '_' + value;
|
|
|
|
if (this.pendingOperations.has(operationKey)) {
|
|
return false;
|
|
}
|
|
|
|
this.pendingOperations.add(operationKey);
|
|
|
|
try {
|
|
this.updateUIState(setting, value);
|
|
} catch (e) {}
|
|
|
|
let pollName;
|
|
if (setting === 'rule_mode') {
|
|
pollName = 'rule_mode';
|
|
} else if (setting === 'run_mode') {
|
|
pollName = 'run_mode';
|
|
} else {
|
|
pollName = 'oc_settings';
|
|
}
|
|
this.pausedPolls.add(pollName);
|
|
|
|
var params = Object.assign({}, additionalParams || {});
|
|
if (setting !== 'rule_mode' && setting !== 'run_mode') {
|
|
params.setting = setting;
|
|
params.value = value;
|
|
} else if (setting === 'rule_mode') {
|
|
params.rule_mode = value;
|
|
} else if (setting === 'run_mode') {
|
|
params.run_mode = value;
|
|
}
|
|
|
|
var self = this;
|
|
|
|
XHR.get(endpoint, params, function(x, status) {
|
|
setTimeout(function() {
|
|
self.pendingOperations.delete(operationKey);
|
|
|
|
switch(setting) {
|
|
case 'meta_sniffer':
|
|
var metaSnifferOn = document.getElementById('meta_sniffer_on');
|
|
var metaSnifferOff = document.getElementById('meta_sniffer_off');
|
|
if (metaSnifferOn) metaSnifferOn.disabled = false;
|
|
if (metaSnifferOff) metaSnifferOff.disabled = false;
|
|
break;
|
|
case 'respect_rules':
|
|
var respectRulesOn = document.getElementById('respect_rules_on');
|
|
var respectRulesOff = document.getElementById('respect_rules_off');
|
|
if (respectRulesOn) respectRulesOn.disabled = false;
|
|
if (respectRulesOff) respectRulesOff.disabled = false;
|
|
break;
|
|
case 'oversea':
|
|
var oversea0 = document.getElementById('oc_setting_oversea_0');
|
|
var oversea1 = document.getElementById('oc_setting_oversea_1');
|
|
var oversea2 = document.getElementById('oc_setting_oversea_2');
|
|
if (oversea0) oversea0.disabled = false;
|
|
if (oversea1) oversea1.disabled = false;
|
|
if (oversea2) oversea2.disabled = false;
|
|
break;
|
|
case 'stream_unlock':
|
|
var streamUnlockOn = document.getElementById('stream_unlock_on');
|
|
var streamUnlockOff = document.getElementById('stream_unlock_off');
|
|
if (streamUnlockOn) streamUnlockOn.disabled = false;
|
|
if (streamUnlockOff) streamUnlockOff.disabled = false;
|
|
break;
|
|
case 'rule_mode':
|
|
var radioElements = document.getElementsByName("radios");
|
|
if (radioElements && radioElements.length > 0) {
|
|
for (var i = 0; i < radioElements.length; i++) {
|
|
radioElements[i].disabled = false;
|
|
}
|
|
}
|
|
break;
|
|
case 'run_mode':
|
|
var radioRuElements = document.getElementsByName("radios-ru");
|
|
if (radioRuElements && radioRuElements.length > 0) {
|
|
for (var i = 0; i < radioRuElements.length; i++) {
|
|
radioRuElements[i].disabled = false;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
self.pausedPolls.delete(pollName);
|
|
|
|
if (!x || x.status !== 200) {
|
|
alert(self.getErrorMessage(setting));
|
|
}
|
|
}, 1500);
|
|
});
|
|
|
|
return false;
|
|
},
|
|
|
|
getErrorMessage: function(setting) {
|
|
var messages = {
|
|
'meta_sniffer': '<%:Sniffer setting failed%>',
|
|
'respect_rules': '<%:Respect Rules setting failed%>',
|
|
'oversea': '<%:Area bypass setting failed%>',
|
|
'stream_unlock': '<%:Stream Unlock setting failed%>',
|
|
'rule_mode': '<%:Proxy Mode switching failed!%>',
|
|
'run_mode': '<%:Running Mode switching failed!%>'
|
|
};
|
|
return messages[setting] || '<%:Operation failed%>';
|
|
}
|
|
};
|
|
|
|
var script_radio, script_radio_label;
|
|
var s, gr;
|
|
var pluginToggleUserAction = false;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
DarkModeDetector.init();
|
|
ConfigFileManager.init();
|
|
|
|
setTimeout(function() {
|
|
SubscriptionManager.init();
|
|
}, 300);
|
|
|
|
setTimeout(function() {
|
|
check_core();
|
|
}, 1000);
|
|
|
|
setTimeout(function() {
|
|
loadAnnouncement();
|
|
}, 2000);
|
|
});
|
|
|
|
XHR.poll(5, '<%=luci.dispatcher.build_url("admin", "services", "openclash", "status")%>', null, function(x, status) {
|
|
if (x && x.status == 200) {
|
|
var updates = [];
|
|
|
|
if (!pluginToggleUserAction) {
|
|
updates.push({
|
|
element: DOMCache.clash,
|
|
content: status.clash ? '<b style=color:var(--success-color)>' + status.core_type +' <%:Running%></b>' : '<b style=color:var(--error-color)><%:Not Running%></b>'
|
|
});
|
|
|
|
updatePluginToggleState(status.clash);
|
|
}
|
|
|
|
get_run_mode();
|
|
get_rule_mode();
|
|
get_oc_settings();
|
|
|
|
var webContent = status.clash ? '<input type="button" class="dashboard-btn" value="<%:Yacd%>" onclick="return ycad_dashboard(this)"/>' : '<input type="button" class="dashboard-btn" value="<%:Yacd%>" onclick="event.preventDefault(); event.stopPropagation(); return false;"/>';
|
|
updates.push({element: DOMCache.web, content: webContent});
|
|
|
|
var weboContent = status.clash ? '<input type="button" class="dashboard-btn" value="<%:Dashboard%>" onclick="return net_dashboard(this)"/>' : '<input type="button" class="dashboard-btn" value="<%:Dashboard%>" onclick="event.preventDefault(); event.stopPropagation(); return false;"/>';
|
|
updates.push({element: DOMCache.webo, content: weboContent});
|
|
|
|
var webmContent = status.clash ? '<input type="button" class="dashboard-btn" value="<%:Metacubexd%>" onclick="return meta_dashboard(this)"/>' : '<input type="button" class="dashboard-btn" value="<%:Metacubexd%>" onclick="event.preventDefault(); event.stopPropagation(); return false;"/>';
|
|
updates.push({element: DOMCache.webm, content: webmContent});
|
|
|
|
var webzContent = status.clash ? '<input type="button" class="dashboard-btn" value="<%:Zashboard%>" onclick="return net_zashboard(this)"/>' : '<input type="button" class="dashboard-btn" value="<%:Zashboard%>" onclick="event.preventDefault(); event.stopPropagation(); return false;"/>';
|
|
updates.push({element: DOMCache.webz, content: webzContent});
|
|
|
|
var closeConnContent = status.clash ? '<input type="button" class="dashboard-btn" value="<%:Close Connect%>" onclick="return b_close_all_connection(this)"/>' : '<input type="button" class="dashboard-btn" value="<%:Close Connect%>" onclick="event.preventDefault(); event.stopPropagation(); return false;"/>';
|
|
updates.push({element: document.getElementById('_close_all_connection_btn'), content: closeConnContent});
|
|
|
|
var reloadFwContent = status.clash ? '<input type="button" class="dashboard-btn" value="<%:Reload Firewall%>" onclick="return b_reload_firewall(this)"/>' : '<input type="button" class="dashboard-btn" value="<%:Reload Firewall%>" onclick="event.preventDefault(); event.stopPropagation(); return false;"/>';
|
|
updates.push({element: document.getElementById('_reload_firewall_btn'), content: reloadFwContent});
|
|
|
|
var flushCacheContent = status.clash ? '<input type="button" class="dashboard-btn" value="<%:Flush DNS%>" onclick="return b_flush_dns_cache(this)"/>' : '<input type="button" class="dashboard-btn" value="<%:Flush DNS Cache%>" onclick="event.preventDefault(); event.stopPropagation(); return false;"/>';
|
|
updates.push({element: document.getElementById('_flush_dns_cache_btn'), content: flushCacheContent});
|
|
|
|
var oneKeyUpdateContent = '<input type="button" class="dashboard-btn" value="<%:Check Update%>" onclick="return all_one_key_update(this)"/>';
|
|
updates.push({element: document.getElementById('_one_key_update_btn'), content: oneKeyUpdateContent});
|
|
|
|
StateManager.batchUpdateDOM(updates);
|
|
|
|
if (!status.yacd) {
|
|
DOMCache.web.classList.add('hidden');
|
|
}
|
|
|
|
if (!status.dashboard) {
|
|
DOMCache.webo.classList.add('hidden');
|
|
}
|
|
|
|
if (!status.metacubexd) {
|
|
DOMCache.webm.classList.add('hidden');
|
|
}
|
|
|
|
if (!status.zashboard) {
|
|
DOMCache.webz.classList.add('hidden');
|
|
}
|
|
|
|
StateManager.current_status = status;
|
|
|
|
if (status.daip) {
|
|
var daipContent, dapoContent;
|
|
|
|
if (status.daip && window.location.hostname == status.daip) {
|
|
dapoContent = status.cn_port ? ":"+status.cn_port : "";
|
|
daipContent = status.daip ? "<b style=color:var(--success-color)>"+status.daip+dapoContent+"</b>" : "<b style=color:var(--error-color)>"+"<%:Not Set%>"+"</b>";
|
|
} else if (status.daip && window.location.hostname != status.daip && status.db_foward_domain && status.db_foward_port) {
|
|
dapoContent = status.db_foward_port ? ":"+status.db_foward_port : "";
|
|
daipContent = status.db_foward_domain ? "<b style=color:var(--success-color)>"+status.db_foward_domain+dapoContent+"</b>" : "<b style=color:var(--error-color)>"+"<%:Not Set%>"+"</b>";
|
|
} else {
|
|
dapoContent = status.cn_port ? ":"+status.cn_port : "";
|
|
daipContent = status.daip ? "<b style=color:var(--success-color)>"+status.daip+dapoContent+"</b>" : "<b style=color:var(--error-color)>"+"<%:Not Set%>"+"</b>";
|
|
}
|
|
|
|
DOMCache.daip.innerHTML = daipContent;
|
|
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "proxy_info")%>', function(x, proxy_info) {
|
|
if (x && x.status == 200) {
|
|
StateManager.cached_proxy_info = proxy_info;
|
|
var proxy_ip = status.daip;
|
|
var mix_addr = proxy_ip + ':' + (proxy_info.mixed_port || '7893');
|
|
DOMCache.mix_proxy.innerHTML = "<b style=color:var(--success-color)>" + mix_addr + "</b>";
|
|
} else {
|
|
StateManager.cached_proxy_info = null;
|
|
DOMCache.mix_proxy.innerHTML = "<b style=color:var(--error-color)><%:Not Available%></b>";
|
|
}
|
|
});
|
|
} else {
|
|
DOMCache.daip.innerHTML = "<b style=color:var(--error-color)><%:Not Available%></b>";
|
|
StateManager.cached_proxy_info = null;
|
|
DOMCache.mix_proxy.innerHTML = "<b style=color:var(--error-color)><%:Not Available%></b>";
|
|
DOMCache.copy_secret.style.display = "none";
|
|
DOMCache.copy_address.style.display = "none";
|
|
DOMCache.copy_mix_address.style.display = "none";
|
|
DOMCache.copy_mix_secret.style.display = "none";
|
|
DOMCache.copy_pac_config.style.display = "none";
|
|
}
|
|
|
|
if (status.clash && status.daip) {
|
|
if (!WSManager.hasActiveConnections()) {
|
|
initializeWebSocketConnections(status);
|
|
} else {
|
|
if (NetworkStatsManager.isEnabled) {
|
|
NetworkStatsManager.stop();
|
|
}
|
|
|
|
if (!SystemStatusManager.isEnabled) {
|
|
SystemStatusManager.start();
|
|
}
|
|
}
|
|
} else {
|
|
WSManager.closeAll();
|
|
SystemStatusManager.stop();
|
|
NetworkStatsManager.stop();
|
|
}
|
|
}
|
|
clashversion_check();
|
|
});
|
|
|
|
window.addEventListener('beforeunload', function() {
|
|
WSManager.closeAll();
|
|
SystemStatusManager.stop();
|
|
NetworkStatsManager.stop();
|
|
});
|
|
|
|
function initializeWebSocketConnections(status) {
|
|
if (!status || !status.clash || !status.daip) {
|
|
return false;
|
|
}
|
|
|
|
var protocol = getWebSocketProtocol(status);
|
|
var token = status.dase;
|
|
|
|
var connections = [
|
|
{
|
|
type: 'traffic',
|
|
endpoint: '/traffic',
|
|
handler: ws_tmessage
|
|
},
|
|
{
|
|
type: 'connections',
|
|
endpoint: '/connections',
|
|
handler: ws_cmessage
|
|
},
|
|
{
|
|
type: 'memory',
|
|
endpoint: '/memory',
|
|
handler: ws_mmessage
|
|
}
|
|
];
|
|
|
|
connections.forEach(function(conn) {
|
|
var url = protocol + conn.endpoint + (token ? '?token=' + token : '');
|
|
WSManager.createConnection(conn.type, url, conn.handler);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
function getWebSocketProtocol(status) {
|
|
var protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
|
var host, port;
|
|
|
|
if (status.daip && window.location.hostname === status.daip) {
|
|
host = status.daip;
|
|
port = status.cn_port;
|
|
} else if (status.daip && window.location.hostname !== status.daip &&
|
|
status.db_foward_domain && status.db_foward_port) {
|
|
host = status.db_foward_domain;
|
|
port = status.db_foward_port;
|
|
} else {
|
|
host = status.daip;
|
|
port = status.cn_port;
|
|
}
|
|
|
|
return protocol + host + ":" + port;
|
|
}
|
|
|
|
function loadAnnouncement() {
|
|
var userLang = navigator.language || navigator.userLanguage;
|
|
var isChineseUser = userLang.indexOf('zh') === 0;
|
|
|
|
var tips = [
|
|
'<%:You can modify the profile on the profile page (for content that is not taken over)%>',
|
|
'<%:do not write configuration files? Try to create one click on the server page%>',
|
|
'<%:some website are abnormal? Try switching modes or using third-party rules%>',
|
|
'<%:using the fake IP mode can get a faster access experience%>',
|
|
'<%:the nameserver group must have at least one server set when using custom DNS%>',
|
|
'<%:after started, please wait patiently until the connection is normal%>',
|
|
'<%:if you don not use IPv6, please turn off the DHCP service of IPv6, otherwise the connection will be abnormal%>',
|
|
'<%:you can update the version in the settings page%>',
|
|
'<%:Note: It is not recommended to enable IPv6 and related services for routing. Most of the network connection problems reported so far are related to it%>',
|
|
'<%:Note: Turning on secure DNS in the browser will cause abnormal shunting, please be careful to turn it off%>',
|
|
'<%:Note: Some software will modify the device HOSTS, which will cause abnormal shunt, please pay attention to check%>',
|
|
'<%:Note: The default proxy routes local traffic, BT, PT download, etc., please use Redir-Host mode as much as possible and pay attention to traffic avoidance%>'
|
|
];
|
|
|
|
function calculateAnimationDuration(bannerWidth, contentWidth) {
|
|
var screenWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
|
var refreshRate = window.screen && window.screen.refreshRate ? window.screen.refreshRate : 60;
|
|
if (refreshRate <= 0) refreshRate = 60;
|
|
|
|
if (refreshRate > 480) refreshRate = 480;
|
|
|
|
var baseDuration;
|
|
if (screenWidth <= 575) {
|
|
baseDuration = 12000;
|
|
} else if (screenWidth <= 768) {
|
|
baseDuration = 11000;
|
|
} else if (screenWidth <= 1200) {
|
|
baseDuration = 10000;
|
|
} else {
|
|
baseDuration = 9000;
|
|
}
|
|
|
|
var refreshRateMultiplier = 1;
|
|
if (refreshRate > 60) {
|
|
refreshRateMultiplier = 1 + (refreshRate - 60) / 240;
|
|
}
|
|
|
|
var duration = baseDuration * refreshRateMultiplier;
|
|
|
|
return Math.max(4000, Math.min(duration, 18000));
|
|
}
|
|
|
|
function updateScrollAnimation(banner, content, duration) {
|
|
var bannerWidth = banner.offsetWidth;
|
|
var contentWidth = content.offsetWidth;
|
|
var scrollDistance = -(contentWidth + bannerWidth);
|
|
|
|
banner.style.setProperty('--scroll-distance', scrollDistance + 'px');
|
|
|
|
content.style.animationDuration = duration + 'ms';
|
|
}
|
|
|
|
function getRandomTips(count) {
|
|
var shuffled = tips.slice();
|
|
for (var i = shuffled.length - 1; i > 0; i--) {
|
|
var j = Math.floor(Math.random() * (i + 1));
|
|
var temp = shuffled[i];
|
|
shuffled[i] = shuffled[j];
|
|
shuffled[j] = temp;
|
|
}
|
|
return shuffled.slice(0, count);
|
|
}
|
|
|
|
function updateIcon(isRandomTips) {
|
|
var megaphoneElement = document.getElementById('megaphone');
|
|
if (!megaphoneElement) return;
|
|
|
|
if (isRandomTips) {
|
|
megaphoneElement.innerHTML = '<path d="M176,232a8,8,0,0,1-8,8H88a8,8,0,0,1,0-16h80A8,8,0,0,1,176,232Zm40-128a87.55,87.55,0,0,1-33.64,69.21A16.24,16.24,0,0,0,176,186v6a16,16,0,0,1-16,16H96a16,16,0,0,1-16-16v-6a16,16,0,0,0-6.23-12.66A87.59,87.59,0,0,1,40,104.49C39.74,56.83,78.26,17.14,125.88,16A88,88,0,0,1,216,104Zm-16,0a72,72,0,0,0-73.74-72c-39,.92-70.47,33.39-70.26,72.39a71.65,71.65,0,0,0,27.64,56.3A32,32,0,0,1,96,186v6h64v-6a32.15,32.15,0,0,1,12.47-25.35A71.65,71.65,0,0,0,200,104Zm-16.11-9.34a57.6,57.6,0,0,0-46.56-46.55,8,8,0,0,0-2.66,15.78c16.57,2.79,30.63,16.85,33.44,33.45A8,8,0,0,0,176,104a9,9,0,0,0,1.35-.11A8,8,0,0,0,183.89,94.66Z" stroke-width="2"></path>';
|
|
megaphoneElement.setAttribute('viewBox', '0 0 256 256');
|
|
megaphoneElement.setAttribute('stroke-width', '3');
|
|
} else {
|
|
megaphoneElement.innerHTML = '<path d="M228.54,86.66l-176.06-54A16,16,0,0,0,32,48V192a16,16,0,0,0,16,16,16,16,0,0,0,4.52-.65L136,181.73V192a16,16,0,0,0,16,16h32a16,16,0,0,0,16-16v-29.9l28.54-8.75A16.09,16.09,0,0,0,240,138V102A16.09,16.09,0,0,0,228.54,86.66ZM136,165,48,192V48l88,27Zm48,27H152V176.82L184,167Zm40-54-.11,0L152,160.08V79.92l71.89,22,.11,0v36Z"></path>';
|
|
megaphoneElement.setAttribute('viewBox', '0 0 256 256');
|
|
}
|
|
}
|
|
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "announcement")%>', function(x, status) {
|
|
var banner = document.getElementById('announcement-banner');
|
|
var content = document.getElementById('announcement-content');
|
|
var announcements = [];
|
|
var isRandomTips = false;
|
|
|
|
if (!banner || !content) return;
|
|
|
|
if (x && x.status == 200 && status.content) {
|
|
try {
|
|
var contentData = status.content;
|
|
if (typeof contentData === 'string') {
|
|
contentData = JSON.parse(contentData);
|
|
}
|
|
|
|
if (Array.isArray(contentData)) {
|
|
if (contentData.length > 0 && (contentData[0].zh || contentData[0].en)) {
|
|
contentData.forEach(function(item) {
|
|
if (isChineseUser && item.zh) {
|
|
announcements.push(item.zh);
|
|
} else if (item.en) {
|
|
announcements.push(item.en);
|
|
} else if (item.zh) {
|
|
announcements.push(item.zh);
|
|
}
|
|
});
|
|
} else {
|
|
announcements = contentData.filter(function(item) {
|
|
return typeof item === 'string' && item.trim() !== '';
|
|
});
|
|
}
|
|
} else if (typeof contentData === 'string' && contentData.trim() !== '') {
|
|
announcements = [contentData];
|
|
}
|
|
} catch (e) {
|
|
if (typeof status.content === 'string' && status.content.trim() !== '') {
|
|
announcements = [status.content];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (announcements.length === 0) {
|
|
announcements = getRandomTips(3);
|
|
isRandomTips = true;
|
|
}
|
|
|
|
updateIcon(isRandomTips);
|
|
banner.style.display = 'block';
|
|
|
|
var currentIndex = 0;
|
|
var isHovered = false;
|
|
var pauseTimeout = null;
|
|
var nextAnimationTimeout = null;
|
|
|
|
banner.addEventListener('mouseenter', function() {
|
|
isHovered = true;
|
|
content.classList.add('paused');
|
|
});
|
|
banner.addEventListener('mouseleave', function() {
|
|
isHovered = false;
|
|
content.classList.remove('paused');
|
|
});
|
|
|
|
var resizeTimeout;
|
|
window.addEventListener('resize', function() {
|
|
clearTimeout(resizeTimeout);
|
|
resizeTimeout = setTimeout(function() {
|
|
if (content.classList.contains('scrolling')) {
|
|
var duration = calculateAnimationDuration(banner.offsetWidth, content.offsetWidth);
|
|
updateScrollAnimation(banner, content, duration);
|
|
}
|
|
}, 250);
|
|
});
|
|
|
|
function startScrollAnimation() {
|
|
if (nextAnimationTimeout) {
|
|
clearTimeout(nextAnimationTimeout);
|
|
nextAnimationTimeout = null;
|
|
}
|
|
|
|
content.textContent = announcements[currentIndex];
|
|
|
|
setTimeout(function() {
|
|
var duration = calculateAnimationDuration(banner.offsetWidth, content.offsetWidth);
|
|
updateScrollAnimation(banner, content, duration);
|
|
|
|
content.classList.remove('scrolling');
|
|
content.offsetHeight;
|
|
content.classList.add('scrolling');
|
|
|
|
var onAnimationEnd = function(e) {
|
|
if (e.target === content && e.animationName === 'announceScroll') {
|
|
content.removeEventListener('animationend', onAnimationEnd);
|
|
|
|
pauseTimeout = setTimeout(function() {
|
|
if (!isHovered) {
|
|
currentIndex = (currentIndex + 1) % announcements.length;
|
|
|
|
if (isRandomTips && currentIndex === 0) {
|
|
announcements = getRandomTips(3);
|
|
}
|
|
|
|
startScrollAnimation();
|
|
}
|
|
}, 2000);
|
|
}
|
|
};
|
|
|
|
content.addEventListener('animationend', onAnimationEnd);
|
|
|
|
}, 50);
|
|
}
|
|
|
|
setTimeout(function() {
|
|
startScrollAnimation();
|
|
}, 300);
|
|
|
|
window.addEventListener('beforeunload', function() {
|
|
if (pauseTimeout) clearTimeout(pauseTimeout);
|
|
if (nextAnimationTimeout) clearTimeout(nextAnimationTimeout);
|
|
});
|
|
});
|
|
}
|
|
|
|
function get_rule_mode() {
|
|
if (SettingsManager.isPollPaused('rule_mode')) {
|
|
return;
|
|
}
|
|
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "rule_mode")%>', function(x, status) {
|
|
if (x && x.status == 200 && status.mode != "") {
|
|
if (!SettingsManager.pendingOperations.has('rule_mode_' + status.mode)) {
|
|
for (var i = 0; i < DOMCache.radio.length; i++) {
|
|
if (DOMCache.radio[i].value == status.mode && !DOMCache.radio[i].checked) {
|
|
DOMCache.radio[i].checked = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
function switch_rule_mode(value) {
|
|
return SettingsManager.switchSetting(
|
|
'rule_mode',
|
|
value,
|
|
'<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_rule_mode")%>'
|
|
);
|
|
}
|
|
|
|
function get_run_mode() {
|
|
if (SettingsManager.isPollPaused('run_mode')) {
|
|
return;
|
|
}
|
|
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "get_run_mode")%>', function(x, status) {
|
|
if (x && x.status == 200 && status.mode) {
|
|
if (status.mode == "fake-ip" || status.mode == "fake-ip-tun" || status.mode == "fake-ip-mix") {
|
|
DOMCache.mode.innerHTML = "<b style=color:var(--success-color)><%:Fake-IP%></b>";
|
|
DOMCache.radio_run_normal.innerHTML = "<%:Enhance%>";
|
|
} else if (status.mode == "redir-host" || status.mode == "redir-host-tun" || status.mode == "redir-host-mix") {
|
|
DOMCache.mode.innerHTML = "<b style=color:var(--success-color)><%:Redir-Host%></b>";
|
|
DOMCache.radio_run_normal.innerHTML = "<%:Compat%>";
|
|
}
|
|
|
|
var expectedValue = status["mode"].split("-")[2] == undefined ? "" : ("-" + status["mode"].split("-")[2]);
|
|
var operationKey = 'run_mode_' + status.mode;
|
|
|
|
if (!SettingsManager.pendingOperations.has(operationKey)) {
|
|
for (var i = 0; i < DOMCache.radio_ru.length; i++) {
|
|
if (DOMCache.radio_ru[i].value == expectedValue && !DOMCache.radio_ru[i].checked) {
|
|
DOMCache.radio_ru[i].checked = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
function switch_run_mode(value) {
|
|
LogManager.startLogDisplay('<%:Saving...%>');
|
|
return SettingsManager.switchSetting(
|
|
'run_mode',
|
|
value,
|
|
'<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_run_mode")%>'
|
|
);
|
|
}
|
|
|
|
function winOpen(url) {
|
|
var winOpen = window.open(url);
|
|
if (winOpen == null || typeof(winOpen) == 'undefined') {
|
|
window.location.href = url;
|
|
}
|
|
}
|
|
|
|
function ws_tmessage(event) {
|
|
var dataObj = event && event.data !== undefined ? event.data : event;
|
|
var data;
|
|
if (typeof dataObj === 'string') {
|
|
try {
|
|
data = JSON.parse(dataObj);
|
|
} catch (e) {
|
|
data = {};
|
|
}
|
|
} else {
|
|
data = dataObj;
|
|
}
|
|
var uploadElement = document.getElementById("upload_");
|
|
var downloadElement = document.getElementById("download_");
|
|
uploadElement.innerHTML = data.up ? "<font style=\"color:var(--success-color)\">"+bytesToSize(data.up)+"/S</font>" : "<font style=\"color:var(--success-color)\">0 B/S</font>";
|
|
downloadElement.innerHTML = data.down ? "<font style=\"color:var(--success-color)\">"+bytesToSize(data.down)+"/S</font>" : "<font style=\"color:var(--success-color)\">0 B/S</font>";
|
|
}
|
|
|
|
function ws_cmessage(event) {
|
|
var dataObj = event && event.data !== undefined ? event.data : event;
|
|
var data;
|
|
if (typeof dataObj === 'string') {
|
|
try {
|
|
data = JSON.parse(dataObj);
|
|
} catch (e) {
|
|
data = {};
|
|
}
|
|
} else {
|
|
data = dataObj;
|
|
}
|
|
var updates = [
|
|
{
|
|
element: document.getElementById("uploadtotal_"),
|
|
content: data.uploadTotal ? "<font style=\"color:var(--success-color)\">"+bytesToSize(data.uploadTotal)+"</font>" : "<font style=\"color:var(--success-color)\">0 KB</font>"
|
|
},
|
|
{
|
|
element: document.getElementById("downloadtotal_"),
|
|
content: data.downloadTotal ? "<font style=\"color:var(--success-color)\">"+bytesToSize(data.downloadTotal)+"</font>" : "<font style=\"color:var(--success-color)\">0 KB</font>"
|
|
},
|
|
{
|
|
element: document.getElementById("connect_t"),
|
|
content: data.connections ? "<font style=\"color:var(--success-color)\">"+Object.keys(data.connections).length+"</font>" : "<font style=\"color:var(--success-color)\">0</font>"
|
|
}
|
|
];
|
|
StateManager.batchUpdateDOM(updates);
|
|
}
|
|
|
|
function ws_mmessage(event) {
|
|
var dataObj = event && event.data !== undefined ? event.data : event;
|
|
var data;
|
|
if (typeof dataObj === 'string') {
|
|
try {
|
|
data = JSON.parse(dataObj);
|
|
} catch (e) {
|
|
data = {};
|
|
}
|
|
} else {
|
|
data = dataObj;
|
|
}
|
|
var memElement = document.getElementById("mem_t");
|
|
memElement.innerHTML = data.inuse ? "<font style=\"color:var(--success-color)\">"+bytesToSize(data.inuse)+"</font>" : "<font style=\"color:var(--success-color)\">0 KB</font>";
|
|
if (!SystemStatusManager.isEnabled) {
|
|
SystemStatusManager.start();
|
|
}
|
|
}
|
|
|
|
function bytesToSize(bytes) {
|
|
var sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
if (bytes == 0) return '0 B';
|
|
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return i == 0 ? (bytes / Math.pow(1024, i)) + ' ' + sizes[i] : (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
|
|
}
|
|
|
|
var buttonDebounce = {};
|
|
function debounceButton(fn, delay) {
|
|
return function(btn) {
|
|
var key = btn.id || btn.value;
|
|
if (buttonDebounce[key]) {
|
|
clearTimeout(buttonDebounce[key]);
|
|
}
|
|
buttonDebounce[key] = setTimeout(function() {
|
|
fn(btn);
|
|
delete buttonDebounce[key];
|
|
}, delay || 300);
|
|
return false;
|
|
};
|
|
}
|
|
|
|
var all_one_key_update = debounceButton(function(btn) {
|
|
btn.value = '<%:Check Update%>';
|
|
btn.disabled = false;
|
|
select_git_cdn();
|
|
return false;
|
|
});
|
|
|
|
var b_flush_dns_cache = debounceButton(function(btn) {
|
|
btn.disabled = true;
|
|
btn.value = '<%:Flushing...%> ';
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash","flush_dns_cache")%>', null, function(x, status) {
|
|
if (x && x.status == 200) {
|
|
btn.value = (status.flush_status == "0" || status.flush_status != "") ? '<%:Flush Failed%>' : '<%:Flush Successful%>';
|
|
} else {
|
|
btn.value = '<%:Flush Timeout%>';
|
|
}
|
|
});
|
|
btn.disabled = false;
|
|
return false;
|
|
});
|
|
|
|
var b_reload_firewall = debounceButton(function(btn) {
|
|
btn.disabled = true;
|
|
btn.value = '<%:Reloading...%>';
|
|
LogManager.startLogDisplay('<%:Reloading...%>');
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "reload_firewall")%>', null, function(x, status) {
|
|
btn.disabled = false;
|
|
btn.value = (x && x.status == 200) ? '<%:Reload Firewall%>' : '<%:Firewall Rules Reset Failed%>';
|
|
});
|
|
return false;
|
|
});
|
|
|
|
var b_close_all_connection = debounceButton(function(btn) {
|
|
btn.disabled = true;
|
|
btn.value = '<%:Reloading...%>';
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "close_all_connection")%>', null, function(x, status) {
|
|
btn.disabled = false;
|
|
btn.value = (x && x.status == 200) ? '<%:Close Connect%>' : '<%:Close Connect Failed%>';
|
|
});
|
|
return false;
|
|
});
|
|
|
|
function net_zashboard(btn) {
|
|
if (StateManager.current_status) {
|
|
var status = StateManager.current_status;
|
|
} else {
|
|
setTimeout(function() {
|
|
net_zashboard(btn)
|
|
}, 1000);
|
|
return false;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.value = '<%:Zashboard%>';
|
|
var url9;
|
|
if (status.daip && window.location.hostname == status.daip) {
|
|
url9 = 'http://' + window.location.hostname + ':' + status.cn_port + '/ui/zashboard/#/setup?hostname=' + window.location.hostname + '&port=' + status.cn_port + '&secret=' + status.dase;
|
|
} else if (status.daip && window.location.hostname != status.daip && status.db_foward_domain && status.db_foward_port) {
|
|
var ui_proto = status.db_forward_ssl == 0 ? 'http://' : 'https://';
|
|
url9 = ui_proto + status.db_foward_domain + ':' + status.db_foward_port + '/ui/zashboard/#/setup?hostname=' + status.db_foward_domain + '&port=' + status.db_foward_port + '&secret=' + status.dase;
|
|
} else {
|
|
url9 = 'http://' + window.location.hostname + ':' + status.cn_port + '/ui/zashboard/#/';
|
|
}
|
|
winOpen(url9);
|
|
return false;
|
|
}
|
|
|
|
function meta_dashboard(btn) {
|
|
if (StateManager.current_status) {
|
|
var status = StateManager.current_status;
|
|
} else {
|
|
setTimeout(function() {
|
|
meta_dashboard(btn)
|
|
}, 1000);
|
|
return false;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.value = '<%:Metacubexd%>';
|
|
var url9;
|
|
if (status.daip && window.location.hostname == status.daip) {
|
|
url9 = 'http://' + window.location.hostname + ':' + status.cn_port + '/ui/metacubexd/#/setup?hostname=' + window.location.hostname + '&port=' + status.cn_port + '&secret=' + status.dase;
|
|
} else if (status.daip && window.location.hostname != status.daip && status.db_foward_domain && status.db_foward_port) {
|
|
var ui_proto = status.db_forward_ssl == 0 ? 'http://' : 'https://';
|
|
url9 = ui_proto + status.db_foward_domain + ':' + status.db_foward_port + '/ui/metacubexd/#/setup?hostname=' + status.db_foward_domain + '&port=' + status.db_foward_port + '&secret=' + status.dase;
|
|
} else {
|
|
url9 = 'http://' + window.location.hostname + ':' + status.cn_port + '/ui/metacubexd/#/';
|
|
}
|
|
winOpen(url9);
|
|
return false;
|
|
}
|
|
|
|
function ycad_dashboard(btn) {
|
|
if (StateManager.current_status) {
|
|
var status = StateManager.current_status;
|
|
} else {
|
|
setTimeout(function() {
|
|
ycad_dashboard(btn)
|
|
}, 1000);
|
|
return false;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.value = '<%:Yacd%>';
|
|
var url1;
|
|
if (status.daip && window.location.hostname == status.daip) {
|
|
url1 = 'http://' + window.location.hostname + ':' + status.cn_port + '/ui/yacd/?hostname=' + window.location.hostname + '&port=' + status.cn_port + '&secret=' + status.dase;
|
|
} else if (status.daip && window.location.hostname != status.daip && status.db_foward_domain && status.db_foward_port) {
|
|
var ui_proto = status.db_forward_ssl == 0 ? 'http://' : 'https://';
|
|
url1 = ui_proto + status.db_foward_domain + ':' + status.db_foward_port + '/ui/yacd/?hostname=' + status.db_foward_domain + '&port=' + status.db_foward_port + '&secret=' + status.dase;
|
|
} else {
|
|
url1 = 'http://' + window.location.hostname + ':' + status.cn_port + '/ui/yacd/';
|
|
}
|
|
winOpen(url1);
|
|
return false;
|
|
}
|
|
|
|
function net_dashboard(btn) {
|
|
if (StateManager.current_status) {
|
|
var status = StateManager.current_status;
|
|
} else {
|
|
setTimeout(function() {
|
|
net_dashboard(btn)
|
|
}, 1000);
|
|
return false;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.value = '<%:Dashboard%>';
|
|
var url2;
|
|
if (status.daip && window.location.hostname == status.daip) {
|
|
url2 = 'http://' + window.location.hostname + ':' + status.cn_port + '/ui/dashboard/#/?host=' + window.location.hostname + '&port=' + status.cn_port + '&secret=' + status.dase;
|
|
} else if (status.daip && window.location.hostname != status.daip && status.db_foward_domain && status.db_foward_port) {
|
|
var ui_proto = status.db_forward_ssl == 0 ? 'http://' : 'https://';
|
|
url2 = ui_proto + status.db_foward_domain + ':' + status.db_foward_port + '/ui/dashboard/#/?host=' + status.db_foward_domain + '&port=' + status.db_foward_port + '&secret=' + status.dase;
|
|
} else {
|
|
url2 = 'http://' + window.location.hostname + ':' + status.cn_port + '/ui/dashboard/';
|
|
}
|
|
winOpen(url2);
|
|
return false;
|
|
}
|
|
|
|
function homepage() {
|
|
url3 = 'https://github.com/vernesong/OpenClash';
|
|
winOpen(url3);
|
|
}
|
|
|
|
function gitbookpage() {
|
|
url8 = 'https://wiki.metacubex.one';
|
|
winOpen(url8);
|
|
}
|
|
|
|
function wikipage() {
|
|
url5 = 'https://github.com/vernesong/OpenClash/wiki';
|
|
winOpen(url5);
|
|
}
|
|
|
|
function telegrampage() {
|
|
url6 = 'https://t.me/ctcgfw_openwrt_discuss';
|
|
winOpen(url6);
|
|
}
|
|
|
|
function sponsorpage() {
|
|
url7 = 'https://ko-fi.com/vernesong';
|
|
winOpen(url7);
|
|
}
|
|
|
|
function go_mihomo() {
|
|
var url4 = 'https://www.github.com/metacubex/mihomo';
|
|
winOpen(url4);
|
|
}
|
|
|
|
function clashversion_check() {
|
|
function compareVersions(v1, v2) {
|
|
if (!v1 || !v2) return 0;
|
|
var ver1 = v1.replace(/^v/, '').split('.');
|
|
var ver2 = v2.replace(/^v/, '').split('.');
|
|
|
|
var maxLen = Math.max(ver1.length, ver2.length);
|
|
while (ver1.length < maxLen) ver1.push('0');
|
|
while (ver2.length < maxLen) ver2.push('0');
|
|
|
|
for (var i = 0; i < maxLen; i++) {
|
|
var num1 = parseInt(ver1[i], 10) || 0;
|
|
var num2 = parseInt(ver2[i], 10) || 0;
|
|
if (num1 > num2) return 1;
|
|
if (num1 < num2) return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update")%>', function(x, status) {
|
|
if (x && x.status == 200) {
|
|
if (status.coremetacv && status.coremetacv !== "0") {
|
|
var coreVersionText = status.coremetacv;
|
|
var hasUpdate = false;
|
|
|
|
if (status.corelv && status.corelv !== "" && status.corelv !== "loading..." && status.corelv !== status.coremetacv) {
|
|
hasUpdate = true;
|
|
}
|
|
|
|
DOMCache.core_version_text.textContent = coreVersionText;
|
|
if (DOMCache.core_version_text.scrollWidth > DOMCache.core_version_text.clientWidth) {
|
|
DOMCache.core_version_text.title = coreVersionText;
|
|
} else {
|
|
DOMCache.core_version_text.title = '';
|
|
}
|
|
DOMCache.core_version_display.style.display = 'flex';
|
|
|
|
var existingDot = DOMCache.core_version_display.querySelector('.update-dot');
|
|
if (existingDot) {
|
|
existingDot.remove();
|
|
}
|
|
|
|
if (hasUpdate) {
|
|
var updateDot = document.createElement('span');
|
|
updateDot.className = 'update-dot';
|
|
DOMCache.core_version_display.appendChild(updateDot);
|
|
DOMCache.core_version_display.title = '<%:New version available%>: ' + status.corelv;
|
|
DOMCache.core_version_text.removeAttribute('title');
|
|
} else {
|
|
DOMCache.core_version_display.title = '';
|
|
}
|
|
} else {
|
|
DOMCache.core_version_display.style.display = 'none';
|
|
DOMCache.core_version_display.title = '';
|
|
}
|
|
|
|
if (status.opcv && status.opcv !== "0") {
|
|
var pluginVersionText = status.opcv;
|
|
var hasUpdate = false;
|
|
|
|
if (status.oplv && status.oplv !== "" && status.oplv !== "loading...") {
|
|
if (compareVersions(status.oplv, status.opcv) > 0) {
|
|
hasUpdate = true;
|
|
}
|
|
}
|
|
|
|
DOMCache.plugin_version_text.textContent = pluginVersionText;
|
|
if (DOMCache.plugin_version_text.scrollWidth > DOMCache.plugin_version_text.clientWidth) {
|
|
DOMCache.plugin_version_text.title = pluginVersionText;
|
|
} else {
|
|
DOMCache.plugin_version_text.title = '';
|
|
}
|
|
DOMCache.plugin_version_display.style.display = 'flex';
|
|
|
|
var existingDot = DOMCache.plugin_version_display.querySelector('.update-dot');
|
|
if (existingDot) {
|
|
existingDot.remove();
|
|
}
|
|
|
|
if (hasUpdate) {
|
|
var updateDot = document.createElement('span');
|
|
updateDot.className = 'update-dot';
|
|
DOMCache.plugin_version_display.appendChild(updateDot);
|
|
DOMCache.plugin_version_display.title = '<%:New version available%>: ' + status.oplv;
|
|
DOMCache.plugin_version_text.removeAttribute('title');
|
|
} else {
|
|
DOMCache.plugin_version_display.title = '';
|
|
}
|
|
} else {
|
|
DOMCache.plugin_version_display.style.display = 'none';
|
|
DOMCache.plugin_version_display.title = '';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function logo_error(imgobj, imgSrc) {
|
|
imgobj.src = imgSrc;
|
|
}
|
|
|
|
function imgerrorfuns(imgobj, imgSrc) {
|
|
setTimeout(function() {
|
|
imgobj.src = imgSrc;
|
|
imgobj.loading = "lazy";
|
|
}, 10000);
|
|
}
|
|
|
|
function check_core() {
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "check_core")%>', function(x, status) {
|
|
if (x && x.status == 200) {
|
|
if (status.core_status != "1") {
|
|
var r = confirm("<%:You have not installed the core yet, do you want to download and install it now?%>");
|
|
if (r == true) {
|
|
return select_git_cdn("core_download");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function copyToClipboard(text, successMessage) {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard.writeText(text).then(function() {
|
|
prompt(successMessage, text);
|
|
}).catch(function(err) {
|
|
fallbackCopyTextToClipboard(text, successMessage);
|
|
});
|
|
} else {
|
|
fallbackCopyTextToClipboard(text, successMessage);
|
|
}
|
|
}
|
|
|
|
function copyAddress() {
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "dashboard_type")%>', null, function(x, status) {
|
|
if (x && x.status == 200) {
|
|
var panel = status.default_dashboard;
|
|
if (panel && StateManager.current_status[panel]) {
|
|
var url;
|
|
var currentStatus = StateManager.current_status;
|
|
if (currentStatus.daip && window.location.hostname == currentStatus.daip) {
|
|
url = 'http://' + window.location.hostname + ':' + currentStatus.cn_port + '/ui/' + panel;
|
|
} else if (currentStatus.daip && window.location.hostname != currentStatus.daip && currentStatus.db_foward_domain && currentStatus.db_foward_port) {
|
|
var ui_proto = currentStatus.db_forward_ssl == 0 ? 'http://' : 'https://';
|
|
url = ui_proto + currentStatus.db_foward_domain + ':' + currentStatus.db_foward_port + '/ui/' + panel;
|
|
} else {
|
|
url = 'http://' + window.location.hostname + ':' + currentStatus.cn_port + '/ui/' + panel;
|
|
}
|
|
|
|
if (panel === 'zashboard' || panel === 'metacubexd') {
|
|
url += '/#/setup?hostname=' + (currentStatus.daip && window.location.hostname == currentStatus.daip ? window.location.hostname : (currentStatus.db_foward_domain || window.location.hostname)) + '&port=' + (currentStatus.daip && window.location.hostname == currentStatus.daip ? currentStatus.cn_port : (currentStatus.db_foward_port || currentStatus.cn_port)) + (currentStatus.dase ? '&secret=' + currentStatus.dase : '');
|
|
} else if (panel === 'yacd') {
|
|
url += '/?hostname=' + (currentStatus.daip && window.location.hostname == currentStatus.daip ? window.location.hostname : (currentStatus.db_foward_domain || window.location.hostname)) + '&port=' + (currentStatus.daip && window.location.hostname == currentStatus.daip ? currentStatus.cn_port : (currentStatus.db_foward_port || currentStatus.cn_port)) + (currentStatus.dase ? '&secret=' + currentStatus.dase : '');
|
|
} else if (panel === 'dashboard') {
|
|
url += '/#/?host=' + (currentStatus.daip && window.location.hostname == currentStatus.daip ? window.location.hostname : (currentStatus.db_foward_domain || window.location.hostname)) + '&port=' + (currentStatus.daip && window.location.hostname == currentStatus.daip ? currentStatus.cn_port : (currentStatus.db_foward_port || currentStatus.cn_port)) + (currentStatus.dase ? '&secret=' + currentStatus.dase : '');
|
|
}
|
|
|
|
copyToClipboard(url, '<%:Control panel address copied:%> ');
|
|
} else {
|
|
var fallbackUrl = 'http://' + (StateManager.current_status.daip || 'unknown') + ':' + (StateManager.current_status.cn_port || '9090') + '/ui/zashboard/#/';
|
|
copyToClipboard(fallbackUrl, '<%:Control panel address copied:%> ');
|
|
}
|
|
} else {
|
|
var fallbackUrl = 'http://' + (StateManager.current_status.daip || 'unknown') + ':' + (StateManager.current_status.cn_port || '9090') + '/ui/zashboard/#/';
|
|
copyToClipboard(fallbackUrl, '<%:Control panel address copied:%> ');
|
|
}
|
|
});
|
|
return false;
|
|
}
|
|
|
|
function copySecret() {
|
|
var secret = StateManager.current_status.dase || '';
|
|
if (secret === '') {
|
|
alert('<%:No control panel secret set%>');
|
|
return false;
|
|
}
|
|
copyToClipboard(secret, '<%:Control panel secret copied:%> ');
|
|
return false;
|
|
}
|
|
|
|
function copyMixAuth() {
|
|
if (StateManager.cached_proxy_info) {
|
|
if (StateManager.cached_proxy_info.auth_user && StateManager.cached_proxy_info.auth_pass) {
|
|
var authText = StateManager.cached_proxy_info.auth_user + ':' + StateManager.cached_proxy_info.auth_pass;
|
|
copyToClipboard(authText, '<%:Proxy auth info copied:%> ');
|
|
} else {
|
|
alert('<%:No proxy auth info set%>');
|
|
}
|
|
} else {
|
|
alert('<%:Proxy info not available, please try again later%>');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function copyMixAddress() {
|
|
if (StateManager.cached_proxy_info && StateManager.current_status.daip) {
|
|
var mixPort = StateManager.cached_proxy_info.mixed_port || '7893';
|
|
var proxyIp = StateManager.current_status.daip;
|
|
var proxyText = proxyIp + ':' + mixPort;
|
|
if (StateManager.cached_proxy_info.auth_user && StateManager.cached_proxy_info.auth_pass) {
|
|
proxyText = StateManager.cached_proxy_info.auth_user + ':' + StateManager.cached_proxy_info.auth_pass + '@' + proxyText;
|
|
}
|
|
proxyText = 'http://' + proxyText;
|
|
copyToClipboard(proxyText, '<%:Mix proxy address copied:%> ');
|
|
} else {
|
|
alert('<%:Proxy info not available, please try again later%>');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function get_oc_settings() {
|
|
if (SettingsManager.isPollPaused('oc_settings')) {
|
|
return;
|
|
}
|
|
|
|
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "oc_settings")%>', function(x, info) {
|
|
if (x && x.status == 200) {
|
|
if (!SettingsManager.pendingOperations.has('meta_sniffer_' + info.meta_sniffer)) {
|
|
DOMCache.meta_sniffer_on.checked = info.meta_sniffer == "1";
|
|
DOMCache.meta_sniffer_off.checked = info.meta_sniffer != "1";
|
|
}
|
|
|
|
if (!SettingsManager.pendingOperations.has('respect_rules_' + info.respect_rules)) {
|
|
DOMCache.respect_rules_on.checked = info.respect_rules == "1";
|
|
DOMCache.respect_rules_off.checked = info.respect_rules != "1";
|
|
}
|
|
|
|
if (!SettingsManager.pendingOperations.has('oversea_' + info.oversea)) {
|
|
if (info.oversea == "0") {
|
|
DOMCache.oc_setting_oversea_0.checked = true;
|
|
} else if (info.oversea == "1") {
|
|
DOMCache.oc_setting_oversea_1.checked = true;
|
|
} else if (info.oversea == "2") {
|
|
DOMCache.oc_setting_oversea_2.checked = true;
|
|
}
|
|
}
|
|
|
|
if (!SettingsManager.pendingOperations.has('stream_unlock_' + info.stream_unlock)) {
|
|
DOMCache.stream_unlock_on.checked = info.stream_unlock === '1';
|
|
DOMCache.stream_unlock_off.checked = info.stream_unlock !== '1';
|
|
}
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
function switch_oc_setting_oversea(value) {
|
|
LogManager.startLogDisplay('<%:Saving...%>');
|
|
return SettingsManager.switchSetting(
|
|
'oversea',
|
|
value,
|
|
'<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_oc_setting")%>'
|
|
);
|
|
}
|
|
|
|
function switch_meta_sniffer(value) {
|
|
return SettingsManager.switchSetting(
|
|
'meta_sniffer',
|
|
value,
|
|
'<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_oc_setting")%>'
|
|
);
|
|
}
|
|
|
|
function switch_respect_rules(value) {
|
|
return SettingsManager.switchSetting(
|
|
'respect_rules',
|
|
value,
|
|
'<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_oc_setting")%>'
|
|
);
|
|
}
|
|
|
|
function switch_stream_unlock(value) {
|
|
return SettingsManager.switchSetting(
|
|
'stream_unlock',
|
|
value,
|
|
'<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_oc_setting")%>'
|
|
);
|
|
}
|
|
|
|
function generatePacConfig() {
|
|
if (StateManager.current_status.daip) {
|
|
var currentUrl = {
|
|
protocol: window.location.protocol,
|
|
hostname: window.location.hostname,
|
|
host: window.location.host,
|
|
port: window.location.port,
|
|
href: window.location.href
|
|
};
|
|
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "generate_pac")%>', {
|
|
client_protocol: currentUrl.protocol.replace(':', ''),
|
|
client_hostname: currentUrl.hostname,
|
|
client_host: currentUrl.host,
|
|
client_port: currentUrl.port || '',
|
|
client_href: currentUrl.href
|
|
}, function(x, data) {
|
|
if (x && x.status == 200 && data.pac_url) {
|
|
if (data.error && data.error !== "") {
|
|
alert(data.error);
|
|
}
|
|
copyToClipboard(data.pac_url, '<%:PAC file URL copied:%> ');
|
|
} else if (data.error) {
|
|
errorinfos = {
|
|
'Proxy service not running': '<%:Proxy service not running%>',
|
|
'Unable to get proxy IP': '<%:Unable to get proxy IP%>',
|
|
'Failed to write PAC file': '<%:Failed to write PAC file%>'
|
|
};
|
|
var errorMsg = errorinfos[data.error] || data.error;
|
|
alert('<%:PAC file generation failed%>: ' + errorMsg);
|
|
} else {
|
|
alert('<%:PAC file generation failed%>');
|
|
}
|
|
});
|
|
} else {
|
|
alert('<%:Proxy service not available, please try again later%>');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function fallbackCopyTextToClipboard(text, successMessage) {
|
|
var textArea = document.createElement("textarea");
|
|
textArea.value = text;
|
|
textArea.style.top = "0";
|
|
textArea.style.left = "0";
|
|
textArea.style.position = "fixed";
|
|
textArea.style.opacity = "0";
|
|
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
try {
|
|
var successful = document.execCommand('copy');
|
|
document.body.removeChild(textArea);
|
|
|
|
if (successful) {
|
|
prompt(successMessage, text);
|
|
} else {
|
|
prompt('<%:Copy failed, please copy manually:%>', text);
|
|
}
|
|
} catch (err) {
|
|
document.body.removeChild(textArea);
|
|
prompt('<%:Copy failed, please copy manually:%>', text);
|
|
}
|
|
}
|
|
|
|
function togglePlugin(toggleElement) {
|
|
var isEnabled = toggleElement.checked;
|
|
|
|
if (isEnabled) {
|
|
var currentConfig = ConfigFileManager.getCurrentConfig() || ConfigFileManager.getSelectedConfig();
|
|
if (!currentConfig) {
|
|
toggleElement.checked = false;
|
|
alert('<%:Please select a config file first%>');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
toggleElement.disabled = true;
|
|
pluginToggleUserAction = true;
|
|
var action = isEnabled ? 'start' : 'stop';
|
|
var initialMessage = isEnabled ? '<%:Starting...%>' : '<%:Stopping...%>';
|
|
|
|
LogManager.startLogDisplay(initialMessage);
|
|
|
|
var requestParams = { action: action };
|
|
|
|
if (isEnabled) {
|
|
var currentConfig = ConfigFileManager.getCurrentConfig();
|
|
var selectedConfig = ConfigFileManager.getSelectedConfig();
|
|
|
|
if (!currentConfig && selectedConfig) {
|
|
requestParams.config_file = configFileName;
|
|
}
|
|
}
|
|
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "action")%>', requestParams, function(x, status) {
|
|
if (x && x.status == 200) {
|
|
setTimeout(function() {
|
|
pluginToggleUserAction = false;
|
|
updatePluginToggleState(StateManager.current_status.clash || false);
|
|
}, 2000);
|
|
} else {
|
|
toggleElement.checked = !isEnabled;
|
|
|
|
var errorMessage = isEnabled ?
|
|
'<%:Failed to start OpenClash%>' :
|
|
'<%:Failed to stop OpenClash%>';
|
|
alert(errorMessage);
|
|
|
|
if (DOMCache.clash) {
|
|
DOMCache.clash.innerHTML = '<b style="color:var(--error-color)"><%:Operation Failed%></b>';
|
|
}
|
|
|
|
pluginToggleUserAction = false;
|
|
}
|
|
toggleElement.disabled = false;
|
|
});
|
|
}
|
|
|
|
function updatePluginToggleState(isRunning) {
|
|
if (pluginToggleUserAction) {
|
|
return;
|
|
}
|
|
|
|
var toggleElement = document.getElementById('plugin_toggle');
|
|
if (toggleElement) {
|
|
toggleElement.checked = isRunning;
|
|
toggleElement.disabled = false;
|
|
|
|
if (DOMCache.clash && StateManager.current_status) {
|
|
DOMCache.clash.innerHTML = isRunning ?
|
|
'<b style=color:var(--success-color)>' + StateManager.current_status.core_type +' <%:Running%></b>' :
|
|
'<b style=color:var(--error-color)><%:Not Running%></b>';
|
|
}
|
|
|
|
if (!isRunning && StateManager.current_status && !StateManager.current_status.clash) {
|
|
setTimeout(function() {
|
|
updatePluginToggleState(StateManager.current_status.clash || false);
|
|
}, 2000);
|
|
}
|
|
}
|
|
}
|
|
|
|
function switchConfig() {
|
|
var currentConfig = ConfigFileManager.getSelectedConfig();
|
|
if (!currentConfig) {
|
|
alert('<%:Please select a config file first%>');
|
|
return false;
|
|
}
|
|
|
|
pluginToggleUserAction = true;
|
|
|
|
LogManager.startLogDisplay('<%:Switching Config...%>');
|
|
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_config")%>', {
|
|
config_file: currentConfig
|
|
}, function(x, status) {
|
|
if (x && x.status == 200 && status.status === 'success') {
|
|
ConfigFileManager.refreshConfigList();
|
|
setTimeout(function() {
|
|
pluginToggleUserAction = false;
|
|
updatePluginToggleState(StateManager.current_status.clash || false);
|
|
}, 2000);
|
|
} else {
|
|
alert('<%:Failed to switch config file:%> ' + (status.message || '<%:Unknown error%>'));
|
|
|
|
if (DOMCache.oclog) {
|
|
DOMCache.oclog.innerHTML = '<b style="color:var(--error-color)"><%:Switch Failed%></b>';
|
|
}
|
|
|
|
pluginToggleUserAction = false;
|
|
}
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
function updateConfig() {
|
|
var currentConfig = ConfigFileManager.getSelectedConfig();
|
|
if (!currentConfig) {
|
|
alert('<%:Please select a config file first%>');
|
|
return false;
|
|
}
|
|
|
|
var filename = SubscriptionManager.extractFilename(currentConfig);
|
|
if (!filename) {
|
|
alert('<%:Invalid config file selected%>');
|
|
return false;
|
|
}
|
|
|
|
pluginToggleUserAction = true;
|
|
|
|
LogManager.startLogDisplay('<%:Updating Config...%>');
|
|
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update_config")%>', {
|
|
filename: filename
|
|
}, function(x, status) {
|
|
if (x && x.status == 200) {
|
|
if (status.status === 'success') {
|
|
setTimeout(function() {
|
|
|
|
refreshSubscriptionInfo();
|
|
|
|
ConfigFileManager.refreshConfigList();
|
|
|
|
pluginToggleUserAction = false;
|
|
|
|
}, 2000);
|
|
} else {
|
|
pluginToggleUserAction = false;
|
|
|
|
if (DOMCache.oclog) {
|
|
DOMCache.oclog.innerHTML = '<b style="color:var(--error-color)"><%:Update Failed%></b>';
|
|
}
|
|
|
|
alert('<%:Failed to update config file:%> ' + (status.message || status.error || '<%:Unknown error%>'));
|
|
}
|
|
} else {
|
|
pluginToggleUserAction = false;
|
|
|
|
if (DOMCache.oclog) {
|
|
DOMCache.oclog.innerHTML = '<b style="color:var(--error-color)"><%:Update Failed%></b>';
|
|
}
|
|
|
|
alert('<%:Failed to update config file, please try again later%>');
|
|
}
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
function restartCore() {
|
|
var currentConfig = ConfigFileManager.getCurrentConfig() || ConfigFileManager.getSelectedConfig();
|
|
if (!currentConfig) {
|
|
alert('<%:Please select a config file first%>');
|
|
return false;
|
|
}
|
|
|
|
pluginToggleUserAction = true;
|
|
|
|
var toggleElement = document.getElementById('plugin_toggle');
|
|
if (toggleElement) {
|
|
toggleElement.disabled = true;
|
|
}
|
|
|
|
LogManager.startLogDisplay('<%:Restarting...%>');
|
|
|
|
var requestParams = { action: 'restart' };
|
|
|
|
var currentConfigValue = ConfigFileManager.getCurrentConfig();
|
|
var selectedConfig = ConfigFileManager.getSelectedConfig();
|
|
|
|
if (!currentConfigValue && selectedConfig) {
|
|
requestParams.config_file = configFileName;
|
|
}
|
|
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "action")%>', requestParams, function(x, status) {
|
|
if (x && x.status == 200) {
|
|
setTimeout(function() {
|
|
pluginToggleUserAction = false;
|
|
updatePluginToggleState(StateManager.current_status.clash || false);
|
|
}, 2000);
|
|
ConfigFileManager.refreshConfigList();
|
|
} else {
|
|
if (toggleElement) {
|
|
toggleElement.disabled = false;
|
|
}
|
|
alert('<%:Failed to restart core%>');
|
|
|
|
pluginToggleUserAction = false;
|
|
}
|
|
});
|
|
return false;
|
|
}
|
|
|
|
function refreshSubscriptionInfo() {
|
|
var currentConfig = ConfigFileManager.getSelectedConfig();
|
|
if (!currentConfig) {
|
|
alert('<%:Please select a config file first%>');
|
|
return false;
|
|
}
|
|
|
|
SubscriptionManager.currentConfigFile = currentConfig;
|
|
SubscriptionManager.retryCount = 0;
|
|
|
|
var filename = SubscriptionManager.extractFilename(currentConfig);
|
|
localStorage.removeItem('sub_info_' + filename);
|
|
SubscriptionManager.getSubscriptionInfo();
|
|
return false;
|
|
}
|
|
|
|
function setSubscriptionUrl() {
|
|
var currentConfig = ConfigFileManager.getSelectedConfig();
|
|
if (!currentConfig) {
|
|
alert('<%:Please select a config file first%>');
|
|
return false;
|
|
}
|
|
|
|
var filename = SubscriptionManager.extractFilename(currentConfig);
|
|
if (!filename) {
|
|
alert('<%:Invalid config file selected%>');
|
|
return false;
|
|
}
|
|
|
|
SubscriptionUrlSetter.show(filename);
|
|
return false;
|
|
}
|
|
|
|
function uploadConfig() {
|
|
if (typeof ConfigUploader !== 'undefined' && ConfigUploader.show) {
|
|
ConfigUploader.show();
|
|
} else {
|
|
setTimeout(function() {
|
|
if (typeof ConfigUploader !== 'undefined' && ConfigUploader.show) {
|
|
ConfigUploader.show();
|
|
} else {
|
|
alert('<%:Config editor not ready, please try again%>');
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function editSubscribe() {
|
|
var currentConfig = ConfigFileManager.getSelectedConfig();
|
|
if (!currentConfig) {
|
|
alert('<%:Please select a config file first%>');
|
|
return false;
|
|
}
|
|
|
|
var filename = SubscriptionManager.extractFilename(currentConfig);
|
|
if (!filename) {
|
|
alert('<%:Invalid config file selected%>');
|
|
return false;
|
|
}
|
|
|
|
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "get_subscribe_data")%>', {
|
|
filename: filename
|
|
}, function(x, status) {
|
|
if (x.status == 200) {
|
|
if (ConfigUploader && ConfigUploader.showEditSubscribe) {
|
|
ConfigUploader.showEditSubscribe(status, filename);
|
|
} else {
|
|
setTimeout(function() {
|
|
if (ConfigUploader && ConfigUploader.showEditSubscribe) {
|
|
ConfigUploader.showEditSubscribe(status, filename);
|
|
} else {
|
|
alert('<%:Config editor not ready, please try again%>');
|
|
}
|
|
}, 500);
|
|
}
|
|
} else {
|
|
alert('<%:Failed to get subscribe data%>');
|
|
}
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
function editConfig() {
|
|
var currentConfig = ConfigFileManager.getSelectedConfig();
|
|
if (!currentConfig) {
|
|
alert('<%:Please select a config file first%>');
|
|
return false;
|
|
}
|
|
|
|
if (ConfigEditor.show) {
|
|
ConfigEditor.show(currentConfig);
|
|
} else {
|
|
setTimeout(function() {
|
|
if (ConfigEditor.show) {
|
|
ConfigEditor.show(currentConfig);
|
|
} else {
|
|
alert('<%:Config editor not ready, please try again%>');
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function editOverwrite() {
|
|
if (ConfigEditor.showOverwrite) {
|
|
ConfigEditor.showOverwrite();
|
|
} else {
|
|
setTimeout(function() {
|
|
if (ConfigEditor.showOverwrite) {
|
|
ConfigEditor.showOverwrite();
|
|
} else {
|
|
alert('<%:Config editor not ready, please try again%>');
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function switchToPreviousConfig() {
|
|
if (ConfigFileManager.configList.length > 1) {
|
|
var newIndex = ConfigFileManager.currentConfigIndex - 1;
|
|
if (newIndex < 0) {
|
|
newIndex = ConfigFileManager.configList.length - 1;
|
|
}
|
|
ConfigFileManager.switchToConfigByIndex(newIndex);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function switchToNextConfig() {
|
|
if (ConfigFileManager.configList.length > 1) {
|
|
var newIndex = ConfigFileManager.currentConfigIndex + 1;
|
|
if (newIndex >= ConfigFileManager.configList.length) {
|
|
newIndex = 0;
|
|
}
|
|
ConfigFileManager.switchToConfigByIndex(newIndex);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isDarkBackground(element) {
|
|
var cachedTheme = localStorage.getItem('oc-theme');
|
|
if (cachedTheme === 'dark') {
|
|
return true;
|
|
} else if (cachedTheme === 'light') {
|
|
return false;
|
|
}
|
|
|
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
return true;
|
|
}
|
|
|
|
var style = window.getComputedStyle(element);
|
|
var bgColor = style.backgroundColor;
|
|
let r, g, b;
|
|
if (/rgb\(/.test(bgColor)) {
|
|
var rgb = bgColor.match(/\d+/g);
|
|
r = parseInt(rgb[0]);
|
|
g = parseInt(rgb[1]);
|
|
b = parseInt(rgb[2]);
|
|
} else if (/#/.test(bgColor)) {
|
|
if (bgColor.length === 4) {
|
|
r = parseInt(bgColor[1] + bgColor[1], 16);
|
|
g = parseInt(bgColor[2] + bgColor[2], 16);
|
|
b = parseInt(bgColor[3] + bgColor[3], 16);
|
|
} else {
|
|
r = parseInt(bgColor.slice(1, 3), 16);
|
|
g = parseInt(bgColor.slice(3, 5), 16);
|
|
b = parseInt(bgColor.slice(5, 7), 16);
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
var luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
return luminance < 128;
|
|
};
|
|
|
|
function toggleThemeMode() {
|
|
var themes = ['light', 'dark', 'auto'];
|
|
var currentTheme = localStorage.getItem('oc-theme') || 'auto';
|
|
var currentIndex = themes.indexOf(currentTheme);
|
|
if (currentIndex === -1) currentIndex = 2;
|
|
var newIndex = (currentIndex + 1) % themes.length;
|
|
var newTheme = themes[newIndex];
|
|
localStorage.setItem('oc-theme', newTheme);
|
|
DarkModeDetector.init();
|
|
}
|
|
//]]></script>
|
|
</html> |