2025-07-20 07:40:23 +08:00

5026 lines
195 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;
--warning-color: #f59e0b;
--error-color: #dc2626;
--warning-log: #ff00bb;
--watch-log: #b300ff;
--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: 8px 5px;
--gap-size: 10px;
--row-1-height: 160px;
--row-2-height: 140px;
--row-3-height: 140px;
--row-4-height: 250px;
}
.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;
--warning-color: #f9bb51;
--warning-log: #fa41c8;
--watch-log: #c147f5;
}
.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(--warning-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;
}
.oc .main-cards-container {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: minmax(60px, auto) 1fr;
gap: 10px;
margin: 16px auto;
align-items: start;
max-width: 100%;
width: 100%;
box-sizing: border-box;
overflow-x: auto;
overflow-y: visible;
}
.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: 8px;
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-secondary);
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;
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: 155px;
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);
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 5px;
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: 10px 12px;
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: 11px !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 1px !important;
min-width: 45px;
margin: 0 !important;
}
.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;
}
.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(8px, 2.2vw, 13px);
font-weight: 600;
color: var(--success-color);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
margin-top: 5px;
}
.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 {
display: inline-flex !important;
align-items: center !important;
justify-content: flex-start !important;
width: auto !important;
padding: 4px 8px !important;
gap: 6px !important;
font-size: 12px !important;
}
.oc #logo_btn .logo-text {
font-size: 11px;
color: var(--text-secondary);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.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%;
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;
}
.oc .subscription-progress-bar {
background: var(--bg-gray);
border-radius: 4px;
height: 10px;
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(--warning-color);
}
.oc .subscription-progress-fill.low {
background: var(--error-color);
}
.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;
}
.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-secondary);
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;
}
.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);
}
@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 .main-card {
grid-column: 1;
grid-template-rows: auto auto auto auto;
margin-bottom: 14px;
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: 8px 10px;
}
.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 .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: 80px;
font-size: 9px;
padding: 1px 4px;
}
.oc .dashboard-btn {
font-size: 8px !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: 5px 5px;
min-width: 50px;
height: 28px;
}
.oc .value-indicator b {
font-size: 10px;
}
.oc .stat-label {
font-size: 9px;
}
.oc .stat-value {
font-size: 8px;
}
.oc .subscription-info-text {
font-size: 10px;
}
.oc .config-file-name {
font-size: 15px;
}
.oc .sub-card {
padding: 8px;
gap: 4px;
}
.oc .sub-card.stat-item {
padding: 8px 4px;
gap: 6px;
}
.oc .card-content {
gap: 8px;
}
.oc .card-controls {
gap: 8px;
}
.oc .core-main-controls {
gap: 8px;
min-height: 28px;
}
.oc .core-control-buttons {
gap: 6px;
}
.oc .card-actions {
gap: 6px;
min-height: 24px;
}
.oc #logo_btn .logo-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 RELEASE_BRANCH = uci:get("openclash", "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 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 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%>" 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>
</button>
</div>
</div>
<div class="card-actions">
<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="" checked 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" checked 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="32" height="32" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<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>
</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>
<%:Upload 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="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_fakeip_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"><%:Connections%></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_fakeip_cache: document.getElementById('_flush_fakeip_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 prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var isDarkBg = this.isDarkBackground(document.body);
var shouldUseDark = prefersDark || isDarkBg;
ocContainers.forEach(function(ocContainer) {
if (shouldUseDark) {
ocContainer.setAttribute('data-darkmode', 'true');
} else {
ocContainer.removeAttribute('data-darkmode');
}
});
},
isDarkBackground: function(element) {
try {
var style = window.getComputedStyle(element);
var bgColor = style.backgroundColor;
if (!bgColor || bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)') {
var parent = element.parentElement;
if (parent && parent !== element) {
return this.isDarkBackground(parent);
}
return false;
}
var r, g, b;
if (/rgba?\(/.test(bgColor)) {
var rgb = bgColor.match(/[\d.]+/g);
r = parseFloat(rgb[0]);
g = parseFloat(rgb[1]);
b = parseFloat(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 if (bgColor.length === 7) {
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;
} catch (e) {
return false;
}
}
};
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) {
var fragment = document.createDocumentFragment();
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
},
retryAttempts: {},
maxRetries: 3,
reconnectDelay: 2000,
heartbeatInterval: 30000,
heartbeatTimers: {},
_ws_connect: false,
_ws_error: false,
_ws_retry: 0,
_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
};
this.retryAttempts[type] = 0;
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;
this._ws_error = false;
this._ws_retry = 0;
},
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.retryAttempts[type] = 0;
this.startHeartbeat(type);
if (type === 'traffic' || Object.keys(this.connections).length === 3) {
this._ws_connect = true;
this._ws_error = false;
this._ws_retry = 0;
}
},
handleError: function(type, error) {
var connection = this.connections[type];
if (connection) {
connection.state = this.connectionStates.ERROR;
}
this._ws_error = true;
this.stopHeartbeat(type);
if (type === 'traffic') {
this.enableFallbackMode();
}
},
handleClose: function(type, event) {
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._ws_error = true;
this.enableFallbackMode();
},
scheduleReconnect: function(type) {
var self = this;
var connection = this.connections[type];
if (!connection || this.retryAttempts[type] >= this.maxRetries) {
this.enableFallbackMode();
return;
}
this.retryAttempts[type] = (this.retryAttempts[type] || 0) + 1;
var delay = this.reconnectDelay * Math.pow(1.5, this.retryAttempts[type] - 1);
setTimeout(function() {
if (self.connections[type] && self.connections[type].state !== self.connectionStates.CONNECTED) {
self.createConnection(type, connection.url, connection.messageHandler, connection.options);
}
}, delay);
},
enableFallbackMode: function() {
this._ws_connect = false;
this.closeAll();
if (typeof NetworkStatsManager !== "undefined" && !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;
},
resetRetryCount: function(type) {
if (type) {
this.retryAttempts[type] = 0;
} else {
this.retryAttempts = {};
}
}
};
var ConfigFileManager = {
configList: [],
currentConfig: '',
currentConfigIndex: -1,
selectElement: null,
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;
} else if (data.files && Array.isArray(data.files)) {
configFiles = data.files;
} else {
configFiles = data.config_list || [];
}
if (data.current_config) {
currentConfigFile = data.current_config;
} else if (data.current) {
currentConfigFile = data.current;
}
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 (currentConfigFile && SubscriptionManager.currentConfigFile !== currentConfigFile) {
SubscriptionManager.currentConfigFile = currentConfigFile;
SubscriptionManager.getSubscriptionInfo();
}
} else {
this.hideSubscriptionDisplay();
SubscriptionManager.currentConfigFile = '';
}
this.updateNavigationArrows();
} catch (e) {
this.handleConfigListError();
}
},
handleConfigListError: function() {
this.updateSelectOptions([
{value: '', text: '<%:Failed to load config files%>', disabled: true}
]);
this.toggleEmptyState(true);
this.hideSubscriptionDisplay();
},
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);
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: 3,
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 && 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.sub_info && parsedData.sub_info !== "No Sub Info Found") {
if (this.currentConfigFile === requestConfigFile) {
this.displaySubscriptionInfo(parsedData);
}
} else if (parsedData.sub_info === "No Sub Info Found") {
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) {
StateManager.cachedXHRGetWithParams('<%=luci.dispatcher.build_url("admin", "services", "openclash", "sub_info_get")%>', {filename: filename}, function(x, status) {
if (SubscriptionManager.currentConfigFile !== requestConfigFile) {
return;
}
if (x && x.status == 200 && status.sub_info && status.sub_info !== "No Sub Info Found") {
SubscriptionManager.retryCount = 0;
localStorage.setItem('sub_info_' + filename, JSON.stringify(status));
SubscriptionManager.displaySubscriptionInfo(status);
} else if (x && x.status == 200 && status.sub_info === "No Sub Info Found") {
SubscriptionManager.retryCount = 0;
localStorage.setItem('sub_info_' + filename, JSON.stringify(status));
SubscriptionManager.showNoInfo();
} else {
SubscriptionManager.handleError();
}
}, 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');
var progressFill = document.getElementById('subscription-progress-fill');
var infoText = document.getElementById('subscription-info-text');
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.sub_info && data.sub_info !== "No Sub Info Found") {
if (progressSection) {
progressSection.style.display = 'flex';
}
if (!progressFill || !infoText) return;
var percent = data.percent || 0;
var used = data.surplus || data.used || '0 B';
var total = data.total || '0 B';
var expire = data.expire || '';
var daysLeft = data.day_left || 0;
progressFill.style.width = percent + '%';
progressFill.className = 'subscription-progress-fill ' +
(percent >= 50 ? 'high' : (percent >= 20 ? 'medium' : 'low'));
var infoString = used + ' / ' + total + ' (' + percent + '%)';
if (expire && daysLeft > 0) {
infoString += ' • ' + '<%:Remaining%> ' + daysLeft + ' <%:days%>';
infoString += ' • ' + '<%:Expire Date%>: ' + expire;
}
infoText.textContent = infoString;
if (infoText.scrollWidth > infoText.clientWidth) {
infoText.title = infoString;
} else {
infoText.title = '';
}
} else {
if (progressSection) {
progressSection.style.display = 'none';
}
}
if (detailsSection) {
detailsSection.style.display = 'flex';
}
},
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.style.display = 'none';
}
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';
}
},
showError: 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.style.display = 'none';
}
if (detailsSection) {
detailsSection.style.display = 'none';
}
},
handleError: function() {
if (this.retryCount >= this.maxRetries) {
this.showError();
this.retryCount = 0;
if (this.currentConfigFile) {
localStorage.removeItem('sub_info_' + this.extractFilename(this.currentConfigFile));
}
} else {
this.retryCount++;
setTimeout(function() {
SubscriptionManager.getSubscriptionInfo();
}, 2000);
}
},
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 LogManager = {
isPolling: false,
pollTimer: null,
maxPollTime: 15000,
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) {
DOMCache.oclog.style.display = 'inline-flex';
DOMCache.oclog.innerHTML = '<b style="color:var(--warning-color)">' + (initialMessage || '<%:Processing...%>') + '</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|level=error|FTL \[Config]/)) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "del_start_log")%>', null, function(x) {});
var errorMsg;
if (log.match(/level=(fatal|error)/)) {
var msgParts = log.split('msg=');
errorMsg = msgParts.length > 1 ? msgParts[1].replace(/"/g, '') : log;
} else { // FTL [Config]
var ftlParts = log.split('FTL [Config] ');
errorMsg = ftlParts.length > 1 ? ftlParts[1] : 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.innerHTML = '<b style="color:' + color + '">' + displayText + '</b>';
},
cleanLogContent: function(content) {
return content ? content.replace(/[\r\n]+/g, ' ').replace('##FINISH##', '').trim() : '';
},
getLogColor: function(log) {
if (log.includes("Tip:") || log.includes("提示:")) {
return 'var(--warning-color)';
} else if (log.includes("Error:") || log.includes("错误:") || log.includes("level=error")) {
return 'var(--error-color)';
} else if (log.includes("Warning:") || log.includes("警告:") || log.includes("level=warning")) {
return 'var(--warning-log)';
} else if (log.includes("Watchdog:") || log.includes("守护程序:")) {
return 'var(--watch-log)';
} else if (log.match(/level=info|Success|Started|Ready/)) {
return 'var(--success-color)';
} else {
return 'var(--text-secondary)';
}
},
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(--warning-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 <= 1.0 ? "var(--success-color)" : (loadValue <= 2.0 ? "var(--warning-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(--warning-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 <= 1.0 ? "var(--success-color)" : (loadValue <= 2.0 ? "var(--warning-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();
setTimeout(function() {
ConfigFileManager.init();
}, 200);
setTimeout(function() {
SubscriptionManager.init();
}, 200);
setTimeout(function() {
check_core();
}, 800);
setTimeout(function() {
loadAnnouncement();
}, 2000);
});
XHR.poll(3, '<%=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 +'&nbsp;<%: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 All Connections%>" onclick="return b_close_all_connection(this)"/>' : '<input type="button" class="dashboard-btn" value="<%:Close All Connections%>" 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 Fake-IP Cache%>" onclick="return b_flush_fakeip_cache(this)"/>' : '<input type="button" class="dashboard-btn" value="<%:Flush Fake-IP Cache%>" onclick="event.preventDefault(); event.stopPropagation(); return false;"/>';
updates.push({element: document.getElementById('_flush_fakeip_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);
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>";
}
});
DOMCache.copy_secret.style.display = "inline";
DOMCache.copy_address.style.display = "inline";
DOMCache.copy_mix_address.style.display = "inline";
DOMCache.copy_mix_secret.style.display = "inline";
DOMCache.copy_pac_config.style.display = "inline";
} 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()) {
if (!WSManager._ws_error || WSManager._ws_retry < 3) {
if (initializeWebSocketConnections(status)) {
WSManager._ws_retry++;
}
} else {
WSManager.enableFallbackMode();
}
} else {
if (NetworkStatsManager && NetworkStatsManager.isEnabled) {
NetworkStatsManager.stop();
}
if (SystemStatusManager && !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 = [
'<%:Tip: You can modify the profile on the profile page (for content that is not taken over)%>',
'<%:Tip: click the version icon above to jump to the client publishing page%>',
'<%:Tip: do not write configuration files? Try to create one click on the server page%>',
'<%:Tip: some website are abnormal? Try switching modes or using third-party rules%>',
'<%:Tip: using the fake IP mode can get a faster access experience%>',
'<%:Tip: query DNS by TLS & TCP & HTTPS can get better anti pollution effect%>',
'<%:Tip: openlash will check the configuration file parameters to ensure that it works properly%>',
'<%:Tip: the nameserver group must have at least one server set when using custom DNS%>',
'<%:Tip: the website access check shows the connection of the device currently logged in to the Luci page%>',
'<%:Tip: after started, please wait patiently until the connection is normal%>',
'<%:Tip: if you don not use IPv6, please turn off the DHCP service of IPv6, otherwise the connection will be abnormal%>',
'<%:Tip: you can update the version in the global 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 (status.core_type == "TUN" && DOMCache.radio.length != 4) {
script_radio = document.createElement("input");
script_radio.setAttribute("type", "radio");
script_radio.setAttribute("id", "script");
script_radio.setAttribute("name", "radios");
script_radio.setAttribute("value", "script");
script_radio.setAttribute("onclick", "javascript:return switch_rule_mode(this.value);");
script_radio_label = document.createElement("label");
script_radio_label.setAttribute("for", "script");
script_radio_label.setAttribute("class", "cbi-button-option");
script_radio_label.innerHTML = "Script";
DOMCache.radio_mode.appendChild(script_radio);
DOMCache.radio_mode.appendChild(script_radio_label);
} else if (status.core_type != "TUN" && DOMCache.radio.length == 4) {
DOMCache.radio_mode.removeChild(script_radio);
DOMCache.radio_mode.removeChild(script_radio_label);
}
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_terror() {
WSManager._ws_error = true;
NetworkStatsManager.start();
}
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_fakeip_cache = debounceButton(function(btn) {
btn.disabled = true;
btn.value = '<%:Flushing...%> ';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash","flush_fakeip_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 All Connections%>' : '<%:Close All Connections Failed%>';
});
return false;
});
function net_zashboard(btn) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "status")%>', null, function(x, status) {
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) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "status")%>', null, function(x, status) {
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) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "status")%>', null, function(x, status) {
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) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "status")%>', null, function(x, status) {
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() {
var status = StateManager.current_status;
var address;
if (status.daip && window.location.hostname == status.daip) {
address = 'http://' + status.daip + ':' + (status.cn_port || '9090') + '/ui/zashboard/#/setup?hostname=' + status.daip + '&port=' + (status.cn_port || '9090') + (status.dase ? '&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://';
address = ui_proto + status.db_foward_domain + ':' + status.db_foward_port + '/ui/zashboard/#/setup?hostname=' + status.db_foward_domain + '&port=' + status.db_foward_port + (status.dase ? '&secret=' + status.dase : '');
} else {
address = 'http://' + (status.daip || 'unknown') + ':' + (status.cn_port || '9090') + '/ui/zashboard/#/';
}
copyToClipboard(address, '<%: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;
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.indexOf("warning:") === 0) {
var warningMsg = data.error.replace("warning: ", "");
var warningTranslations = {
'No authentication configured, please be aware of the risk of information leakage!': '<%:No authentication configured, please be aware of the risk of information leakage!%>'
};
var translatedWarning = warningTranslations[warningMsg] || warningMsg;
alert('<%:Warning:%> ' + translatedWarning);
}
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);
}, 3000);
} 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 || 'OpenClash') +'&nbsp;<%: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);
}, 3000);
}
}
}
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);
}, 4000);
} 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);
}, 5000);
} 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;
}
var newUrl = prompt('<%:Paste the new url of subscribe infos sources here:%>', '');
if (newUrl === null) {
return false;
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "set_subinfo_url")%>', {
filename: 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%>'));
}
});
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 uploader not ready, please try again%>');
}
}, 500);
}
return false;
}
function editConfig() {
var currentConfig = ConfigFileManager.getSelectedConfig();
if (!currentConfig) {
alert('<%:Please select a config file first%>');
return false;
}
if (typeof ConfigEditor !== 'undefined' && ConfigEditor.show) {
ConfigEditor.show(currentConfig);
} else {
setTimeout(function() {
if (typeof ConfigEditor !== 'undefined' && ConfigEditor.show) {
ConfigEditor.show(currentConfig);
} else {
alert('<%:Config editor not ready, please try again%>');
}
}, 500);
}
return false;
}
function editOverwrite() {
if (typeof ConfigEditor !== 'undefined' && ConfigEditor.showOverwrite) {
ConfigEditor.showOverwrite();
} else {
setTimeout(function() {
if (typeof ConfigEditor !== 'undefined' && 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 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);
g = parseInt(rgb);
b = parseInt(rgb);
} else if (/#/.test(bgColor)) {
if (bgColor.length === 4) {
r = parseInt(bgColor + bgColor, 16);
g = parseInt(bgColor + bgColor, 16);
b = parseInt(bgColor + bgColor, 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;
};
//]]></script>
</html>