2025-07-18 00:58:03 +08:00

1619 lines
61 KiB
HTML

<style>
.oc .config-upload-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
}
.oc .config-upload-modal-overlay.show {
display: flex;
}
.oc .config-upload-modal {
background: var(--bg-white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
width: 90vw;
max-width: 550px;
min-width: 400px;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--border-light);
max-height: 85vh;
transition: all var(--transition-fast);
}
.oc .config-upload-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid var(--border-light);
background: var(--bg-gray);
flex-shrink: 0;
}
.oc .config-upload-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.oc .config-upload-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-top: 1px solid var(--border-light);
background: var(--bg-gray);
flex-shrink: 0;
}
.oc .config-upload-status {
font-size: 12px;
color: var(--text-secondary);
}
.oc .config-upload-buttons {
display: flex;
gap: 12px;
}
.oc .config-upload-content {
padding: 24px;
flex: 1;
overflow-y: auto;
}
.oc .upload-mode-selector {
margin-bottom: 24px;
border-radius: var(--radius-md);
}
.oc .mode-tabs {
display: flex;
background: var(--bg-gray);
border-radius: var(--radius-md);
padding: 4px;
gap: 4px;
}
.oc .mode-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
border: none;
border-radius: calc(var(--radius-md) - 2px);
background: transparent;
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
}
.oc .mode-tab:hover {
color: var(--text-primary);
background: rgba(59, 130, 246, 0.1);
}
.oc .mode-tab.active {
background: var(--primary-color);
color: white;
box-shadow: var(--shadow-sm);
}
.oc .mode-tab svg {
flex-shrink: 0;
}
.oc .upload-mode-content {
transition: all var(--transition-fast);
}
.oc .upload-zone {
border: 2px dashed var(--border-color);
border-radius: var(--radius-md);
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all var(--transition-fast);
background: var(--bg-light);
}
.oc .upload-zone:hover {
border-color: var(--primary-color);
background: rgba(59, 130, 246, 0.05);
}
.oc .upload-zone.dragover {
border-color: var(--primary-color);
background: rgba(59, 130, 246, 0.1);
}
.oc .upload-zone.has-file {
border-color: var(--success-color);
background: rgba(5, 150, 105, 0.05);
}
.oc .upload-icon {
margin-bottom: 16px;
color: var(--text-secondary);
}
.oc .upload-zone.has-file .upload-icon {
color: var(--success-color);
}
.oc .upload-primary {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
margin: 0 0 8px 0;
}
.oc .upload-secondary {
font-size: 12px;
color: var(--text-secondary);
margin: 0;
}
.oc .subscribe-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.oc .form-group {
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
}
.oc .form-group label {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
}
.oc .form-select-wrapper {
position: relative;
}
.oc .form-select-wrapper::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;
}
.oc .form-input,
.oc .form-select {
width: 100%;
height: 40px;
padding: 10px 12px;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white);
color: var(--text-primary);
font-size: 14px;
transition: all var(--transition-fast);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.oc .form-input:focus,
.oc .form-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.oc .form-input::placeholder {
color: var(--text-secondary);
}
.oc .form-select {
cursor: pointer;
}
.oc .form-textarea {
width: 100%;
min-height: 80px;
padding: 10px 12px;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white);
color: var(--text-primary);
font-size: 14px;
font-family: inherit;
resize: vertical;
transition: all var(--transition-fast);
}
.oc .form-textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.oc .form-row {
display: flex;
gap: 16px;
padding-top: 5px;
}
.oc .form-half {
flex: 1;
}
.oc .form-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
margin-bottom: 5px;
}
.oc .form-checkbox input[type="checkbox"] {
display: none;
}
.oc .checkmark {
width: 16px;
height: 16px;
border: 2px solid var(--border-light);
border-radius: 3px;
background: var(--bg-white);
position: relative;
transition: all var(--transition-fast);
flex-shrink: 0;
}
.oc .form-checkbox input[type="checkbox"]:checked + .checkmark {
background: var(--primary-color);
border-color: var(--primary-color);
}
.oc .form-checkbox input[type="checkbox"]:checked + .checkmark::after {
content: '';
position: absolute;
left: 3px;
top: 0px;
width: 4px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.oc .form-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.oc .form-help {
font-size: 12px;
color: var(--text-secondary);
margin-top: 0%;
margin-bottom: 10px;
line-height: 1.4;
display: flex;
align-items: flex-start;
gap: 6px;
}
.oc .form-help::before {
content: '';
display: inline-block;
width: 12px;
height: 12px;
flex-shrink: 0;
margin-top: 2px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'%3E%3C/path%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
background-size: contain;
opacity: 0.7;
}
.oc .filename-input-container {
margin-top: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.oc .filename-input-container label {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
white-space: nowrap;
}
.oc .filename-input-container input {
flex: 1;
height: 36px;
padding: 8px 12px;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white);
color: var(--text-primary);
font-size: 14px;
transition: all var(--transition-fast);
}
.oc .filename-input-container input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.oc .filename-extension {
font-size: 14px;
color: var(--text-secondary);
font-weight: 500;
}
.oc .upload-progress {
margin-top: 20px;
}
.oc .progress-bar {
height: 8px;
background: var(--bg-gray);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.oc .progress-fill {
height: 100%;
background: var(--primary-color);
border-radius: 4px;
transition: width 0.3s ease;
width: 0%;
}
.oc .progress-text {
font-size: 12px;
color: var(--text-secondary);
text-align: center;
}
.oc .config-upload-buttons .btn {
padding: 8px 16px;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
}
.oc .cancel-btn {
background: var(--bg-white);
color: var(--text-secondary);
}
.oc .cancel-btn:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.oc .upload-btn {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.oc .upload-btn:hover:not(:disabled) {
background: var(--primary-color);
opacity: 0.9;
}
.oc .upload-btn:disabled {
background: var(--bg-gray);
color: var(--text-secondary);
border-color: var(--border-light);
cursor: not-allowed;
}
.oc .advanced-options-container {
padding: 16px;
background: var(--bg-light);
border-radius: var(--radius-sm);
border: 1px solid var(--border-light);
transition: all var(--transition-fast);
}
.oc .advanced-options-container .form-group:last-child {
margin-bottom: 0;
}
.oc .advanced-options-container .sub-convert-options {
margin: 12px auto;
margin-left: 0;
background: rgba(59, 130, 246, 0.03);
border-color: rgba(59, 130, 246, 0.2);
max-width: 100%;
}
.oc .sub-convert-options {
margin: 16px auto;
padding: 16px;
background: var(--bg-light);
border-radius: var(--radius-sm);
border: 1px solid var(--border-light);
max-width: 95%;
width: 100%;
box-sizing: border-box;
}
.oc[data-darkmode="true"] .config-upload-modal {
background: var(--bg-white);
border-color: var(--border-light);
}
.oc[data-darkmode="true"] .config-upload-header,
.oc[data-darkmode="true"] .config-upload-footer {
background: var(--bg-gray);
border-color: var(--border-light);
}
.oc[data-darkmode="true"] .mode-tab {
color: var(--text-secondary);
}
.oc[data-darkmode="true"] .mode-tab:hover {
color: var(--text-primary);
background: rgba(96, 165, 250, 0.1);
}
.oc[data-darkmode="true"] .mode-tab.active {
background: var(--primary-color);
color: white;
}
.oc[data-darkmode="true"] .upload-zone {
border-color: var(--border-color);
background: var(--bg-light);
}
.oc[data-darkmode="true"] .upload-zone:hover {
border-color: var(--primary-color);
background: rgba(96, 165, 250, 0.05);
}
.oc[data-darkmode="true"] .upload-zone.dragover {
border-color: var(--primary-color);
background: rgba(96, 165, 250, 0.1);
}
.oc[data-darkmode="true"] .upload-zone.has-file {
border-color: var(--success-color);
background: rgba(52, 211, 153, 0.05);
}
.oc[data-darkmode="true"] .upload-zone.has-file .upload-icon {
color: var(--success-color);
}
.oc[data-darkmode="true"] .form-input,
.oc[data-darkmode="true"] .form-select,
.oc[data-darkmode="true"] .filename-input-container input {
background: var(--bg-white);
border-color: var(--border-light);
color: var(--text-primary);
background-color: var(--bg-gray) !important;
}
.oc[data-darkmode="true"] .form-input:focus,
.oc[data-darkmode="true"] .form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.1);
}
.oc[data-darkmode="true"] .form-input::placeholder {
color: var(--text-secondary);
}
.oc[data-darkmode="true"] .form-textarea {
background: var(--bg-gray);
border-color: var(--border-light);
color: var(--text-primary);
}
.oc[data-darkmode="true"] .form-textarea:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.1);
}
.oc[data-darkmode="true"] .checkmark {
border-color: var(--border-light);
background: var(--bg-gray);
}
.oc[data-darkmode="true"] .progress-bar {
background: var(--bg-gray);
}
.oc[data-darkmode="true"] .progress-fill {
background: var(--primary-color);
}
.oc[data-darkmode="true"] .cancel-btn {
background: var(--bg-white);
color: var(--text-secondary);
border-color: var(--border-light);
}
.oc[data-darkmode="true"] .cancel-btn:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.oc[data-darkmode="true"] .upload-btn {
background: var(--primary-color);
border-color: var(--primary-color);
}
.oc[data-darkmode="true"] .upload-btn:disabled {
background: var(--bg-gray);
color: var(--text-secondary);
border-color: var(--border-light);
}
.oc[data-darkmode="true"] .sub-convert-options {
background: rgba(96, 165, 250, 0.05);
border-color: var(--border-light);
}
.oc[data-darkmode="true"] .advanced-options-container {
background: rgba(96, 165, 250, 0.05);
border-color: var(--border-light);
}
.oc[data-darkmode="true"] .advanced-options-container .sub-convert-options {
background: rgba(96, 165, 250, 0.08);
border-color: var(--border-light);
}
.oc[data-darkmode="true"] .form-help::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23d0cfcf' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'%3E%3C/path%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E");
opacity: 0.6;
}
@media screen and (max-width: 768px) {
.oc .form-help {
font-size: 11px;
margin-bottom: 10px;
gap: 5px;
}
.oc .form-help::before {
width: 11px;
height: 11px;
margin-top: 1px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='11' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'%3E%3C/path%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E");
}
.oc[data-darkmode="true"] .form-help::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='11' viewBox='0 0 24 24' fill='none' stroke='%23d0cfcf' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'%3E%3C/path%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E");
}
.oc .sub-convert-options {
margin: 12px auto;
padding: 12px;
max-width: 98%;
}
}
@media screen and (max-width: 575px) {
.oc .form-help {
font-size: 10px;
margin-bottom: 8px;
gap: 4px;
}
.oc .form-help::before {
width: 10px;
height: 10px;
margin-top: 1px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'%3E%3C/path%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E");
}
.oc[data-darkmode="true"] .form-help::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%23d0cfcf' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'%3E%3C/path%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E");
}
.oc .sub-convert-options {
margin: 10px auto;
padding: 10px;
max-width: 100%;
}
}
@media screen and (max-width: 500px) {
.oc .config-upload-modal {
width: 95vw;
min-width: 320px;
}
.oc .config-upload-content {
padding: 20px;
}
.oc .upload-zone {
padding: 30px 15px;
}
.oc .mode-tab {
padding: 10px 12px;
font-size: 13px;
}
.oc .mode-tab svg {
width: 14px;
height: 14px;
}
}
</style>
<div class="oc">
<div class="config-upload-modal-overlay" id="config-upload-overlay">
<div class="config-upload-modal" id="config-upload-modal">
<div class="config-upload-header">
<div class="config-upload-title">
<span><%:Add Config File%></span>
</div>
<div class="config-upload-actions">
<button type="button" class="icon-btn" id="config-upload-close" title="<%:Close%>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<div class="config-upload-content">
<div class="upload-mode-selector">
<div class="mode-tabs">
<button type="button" class="mode-tab active" id="upload-mode-file" data-mode="file">
<svg width="16" height="16" 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,11 12,6 7,11"></polyline>
<line x1="12" y1="18" x2="12" y2="6"></line>
</svg>
<%:Upload File%>
</button>
<button type="button" class="mode-tab" id="upload-mode-subscribe" data-mode="subscribe">
<svg width="15" height="15" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.2401 16.373L17.1001 7.23303C14.4388 4.57168 10.0653 4.6303 7.33158 7.36397C4.59791 10.0976 4.53929 14.4712 7.20064 17.1325L15.1359 25.0678" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M32.9027 23.0031L40.838 30.9384C43.4994 33.5998 43.4407 37.9733 40.7071 40.707C37.9734 43.4407 33.5999 43.4993 30.9385 40.8379L21.7985 31.6979" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M26.1093 26.1416C28.843 23.4079 28.9016 19.0344 26.2403 16.373" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M21.7989 21.7984C19.0652 24.5321 19.0066 28.9056 21.6679 31.5669" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<%:Subscribe Link%>
</button>
</div>
</div>
<div class="upload-mode-content" id="mode-file-content">
<div class="upload-zone" id="upload-zone">
<div class="upload-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17,11 12,6 7,11"></polyline>
<line x1="12" y1="18" x2="12" y2="6"></line>
</svg>
</div>
<div class="upload-text">
<p class="upload-primary"><%:Click to select file or drag and drop%></p>
<p class="upload-secondary"><%:Support YAML files, max size 10MB%></p>
</div>
<input type="file" id="config-file-input" accept=".yaml,.yml" style="display: none;">
</div>
</div>
<div class="upload-mode-content" id="mode-subscribe-content" style="display: none;">
<div class="subscribe-form">
<div class="form-group">
<label for="subscribe-url-input"><%:Subscription URL%>:</label>
<textarea id="subscribe-url-input" placeholder="<%:Enter subscription URL or multiple links (one per line)%>" class="form-textarea" rows="4"></textarea>
<div class="form-help"><%:URI and subscription address is supported when online subscription conversion enabled, multiple links should be one per line or separated by%> |</div>
</div>
<div class="form-group">
<label for="subscribe-ua-input"><%:User-Agent%> (<%:Optional%>):</label>
<div class="form-select-wrapper">
<select id="subscribe-ua-input" class="form-select">
<option value="clash.meta">clash.meta</option>
<option value="clash-verge/v1.5.1">clash-verge/v1.5.1</option>
<option value="Clash">clash</option>
<option value="custom"><%:Custom%></option>
</select>
</div>
<input type="text" id="subscribe-ua-custom" placeholder="<%:Enter custom User-Agent%>" class="form-input" style="display: none; margin-top: 8px;" />
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="advanced-options-enable">
<span class="checkmark"></span>
<%:Advanced Options%>
</label>
<div class="form-help"><%:Show more subscription options%></div>
</div>
<div class="advanced-options-container" id="advanced-options-container" style="display: none;">
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="sub-convert-enable">
<span class="checkmark"></span>
<%:Subscribe Convert Online%>
</label>
<div class="form-help"><%:Convert Subscribe Online With Template%></div>
</div>
<div class="sub-convert-options" id="sub-convert-options" style="display: none;">
<div class="form-group">
<label for="convert-address-input"><%:Convert Address%>:</label>
<div class="form-select-wrapper">
<select id="convert-address-input" class="form-select">
<option value="https://api.dler.io/sub">api.dler.io (<%:Default%>)</option>
<option value="https://api.wcc.best/sub">api.wcc.best</option>
<option value="custom"><%:Custom%></option>
</select>
</div>
<input type="text" id="convert-address-custom" placeholder="<%:Enter custom convert address%>" class="form-input" style="display: none; margin-top: 8px;" />
<div class="form-help"><%:Note: There is A Risk of Privacy Leakage in Online Convert%></div>
</div>
<div class="form-group">
<label for="template-select"><%:Template Name%>:</label>
<div class="form-select-wrapper">
<select id="template-select" class="form-select">
<%
local file = io.open("/usr/share/openclash/res/sub_ini.list", "r")
if file then
for line in file:lines() do
if line ~= "" and line ~= nil then
local template_name = string.sub(luci.sys.exec(string.format("echo '%s' |awk -F ',' '{print $1}' 2>/dev/null", line)), 1, -2)
if template_name and template_name ~= "" then
%>
<option value="<%=template_name%>"><%=template_name%></option>
<%
end
end
end
file:close()
%>
<option value="0"><%:Custom Template%></option>
<%
end
%>
</select>
</div>
</div>
<div class="form-group" id="custom-template-group" style="display: none;">
<label for="custom-template-input"><%:Custom Template URL%>:</label>
<input type="text" id="custom-template-input" placeholder="<%:Enter custom template URL%>" class="form-input" />
</div>
<div class="form-row">
<div class="form-group form-half">
<label class="form-checkbox">
<input type="checkbox" id="emoji-enable">
<span class="checkmark"></span>
<%:Emoji%>
</label>
</div>
<div class="form-group form-half">
<label class="form-checkbox">
<input type="checkbox" id="udp-enable">
<span class="checkmark"></span>
<%:UDP Enable%>
</label>
</div>
</div>
<div class="form-row">
<div class="form-group form-half">
<label class="form-checkbox">
<input type="checkbox" id="skip-cert-verify">
<span class="checkmark"></span>
<%:skip-cert-verify%>
</label>
</div>
<div class="form-group form-half">
<label class="form-checkbox">
<input type="checkbox" id="sort-enable">
<span class="checkmark"></span>
<%:Sort%>
</label>
</div>
</div>
<div class="form-row">
<div class="form-group form-half">
<label class="form-checkbox">
<input type="checkbox" id="node-type-enable">
<span class="checkmark"></span>
<%:Append Node Type%>
</label>
</div>
<div class="form-group form-half">
<label class="form-checkbox">
<input type="checkbox" id="rule-provider-enable">
<span class="checkmark"></span>
<%:Use Rule Provider%>
</label>
</div>
</div>
<div class="form-group">
<label for="custom-params-input"><%:Custom Params%>:</label>
<textarea id="custom-params-input" placeholder="<%:eg: rename=match@replace, one param per line%>" class="form-textarea" rows="3"></textarea>
<div class="form-help"><%:eg: "rename=match@replace" , "rename=\\s+([2-9])[xX]@ (HIGH:$1)"%></div>
</div>
</div>
<div class="form-group">
<label for="keyword-input"><%:Keyword Match%> (<%:Optional%>):</label>
<textarea id="keyword-input" placeholder="<%:Enter keywords to include nodes (one per line)%>" class="form-textarea" rows="2"></textarea>
<div class="form-help"><%:eg: hk or tw&bgp%></div>
</div>
<div class="form-group">
<label for="exclude-keyword-input"><%:Exclude Keyword Match%> (<%:Optional%>):</label>
<textarea id="exclude-keyword-input" placeholder="<%:Enter keywords to exclude nodes (one per line)%>" class="form-textarea" rows="2"></textarea>
<div class="form-help"><%:eg: hk or tw&bgp%></div>
</div>
<div class="form-group">
<label for="exclude-default-select"><%:Exclude Keyword Match Default%>:</label>
<div class="form-checkbox-group">
<label class="form-checkbox">
<input type="checkbox" id="exclude-expire" value="过期时间">
<span class="checkmark"></span>
<%:Expire Time%>
</label>
<label class="form-checkbox">
<input type="checkbox" id="exclude-traffic" value="剩余流量">
<span class="checkmark"></span>
<%:Remaining Traffic%>
</label>
<label class="form-checkbox">
<input type="checkbox" id="exclude-tg" value="TG群">
<span class="checkmark"></span>
<%:TG Group%>
</label>
<label class="form-checkbox">
<input type="checkbox" id="exclude-website" value="官网">
<span class="checkmark"></span>
<%:Official Website%>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="filename-input-container">
<label for="config-filename-input"><%:Config Name%>:</label>
<input type="text" id="config-filename-input" placeholder="<%:Enter config name (without extension)%>" class="form-input"/>
<div class="filename-extension">.yaml</div>
</div>
<div class="upload-progress" id="upload-progress" style="display: none;">
<div class="progress-bar">
<div class="progress-fill" id="upload-progress-fill"></div>
</div>
<div class="progress-text" id="upload-progress-text"><%:Processing...%> 0%</div>
</div>
</div>
<div class="config-upload-footer">
<div class="config-upload-status">
<span id="config-upload-status-text"><%:Ready to add config%></span>
</div>
<div class="config-upload-buttons">
<button type="button" class="btn cancel-btn" id="config-upload-cancel"><%:Cancel%></button>
<button type="button" class="btn upload-btn" id="config-upload-submit" disabled><%:Add Config%></button>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
var ConfigUploader = {
overlay: null,
modal: null,
selectedFile: null,
isProcessing: false,
currentMode: 'file',
init: function() {
this.overlay = document.getElementById('config-upload-overlay');
this.modal = document.getElementById('config-upload-modal');
if (!this.overlay || !this.modal) {
return;
}
this.bindEvents();
},
bindEvents: function() {
var self = this;
var uploadZone = document.getElementById('upload-zone');
var fileInput = document.getElementById('config-file-input');
document.getElementById('upload-mode-file').addEventListener('click', function() {
self.switchMode('file');
});
document.getElementById('upload-mode-subscribe').addEventListener('click', function() {
self.switchMode('subscribe');
});
uploadZone.addEventListener('click', function() {
if (!self.isProcessing && self.currentMode === 'file') {
fileInput.click();
}
});
fileInput.addEventListener('change', function(e) {
if (e.target.files.length > 0) {
self.handleFileSelect(e.target.files[0]);
}
});
uploadZone.addEventListener('dragover', function(e) {
e.preventDefault();
if (!self.isProcessing && self.currentMode === 'file') {
uploadZone.classList.add('dragover');
}
});
uploadZone.addEventListener('dragleave', function(e) {
e.preventDefault();
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', function(e) {
e.preventDefault();
uploadZone.classList.remove('dragover');
if (!self.isProcessing && self.currentMode === 'file' && e.dataTransfer.files.length > 0) {
self.handleFileSelect(e.dataTransfer.files[0]);
}
});
var subscribeUrlInput = document.getElementById('subscribe-url-input');
var filenameInput = document.getElementById('config-filename-input');
var subscribeUaSelect = document.getElementById('subscribe-ua-input');
var subscribeUaCustom = document.getElementById('subscribe-ua-custom');
var subConvertEnable = document.getElementById('sub-convert-enable');
var subConvertOptions = document.getElementById('sub-convert-options');
var convertAddressSelect = document.getElementById('convert-address-input');
var convertAddressCustom = document.getElementById('convert-address-custom');
var templateSelect = document.getElementById('template-select');
var customTemplateGroup = document.getElementById('custom-template-group');
var advancedOptionsEnable = document.getElementById('advanced-options-enable');
var advancedOptionsContainer = document.getElementById('advanced-options-container');
subscribeUrlInput.addEventListener('input', function() {
self.updateSubmitButton();
self.autoFillConfigName();
});
filenameInput.addEventListener('input', this.updateSubmitButton.bind(this));
subscribeUaSelect.addEventListener('change', function() {
if (this.value === 'custom') {
subscribeUaCustom.style.display = 'block';
} else {
subscribeUaCustom.style.display = 'none';
}
});
advancedOptionsEnable.addEventListener('change', function() {
if (this.checked) {
advancedOptionsContainer.style.display = 'block';
} else {
advancedOptionsContainer.style.display = 'none';
self.resetAdvancedOptions();
}
self.updateSubmitButton();
});
subConvertEnable.addEventListener('change', function() {
if (this.checked) {
subConvertOptions.style.display = 'block';
} else {
subConvertOptions.style.display = 'none';
}
self.updateSubmitButton();
});
convertAddressSelect.addEventListener('change', function() {
if (this.value === 'custom') {
convertAddressCustom.style.display = 'block';
} else {
convertAddressCustom.style.display = 'none';
}
});
templateSelect.addEventListener('change', function() {
if (this.value === '0') {
customTemplateGroup.style.display = 'block';
} else {
customTemplateGroup.style.display = 'none';
}
});
document.getElementById('config-upload-submit').addEventListener('click', function() {
if (self.currentMode === 'file') {
self.uploadFile();
} else {
self.processSubscription();
}
});
document.getElementById('config-upload-cancel').addEventListener('click', function() {
if (!self.isProcessing) {
self.hide();
}
});
document.getElementById('config-upload-close').addEventListener('click', function() {
if (!self.isProcessing) {
self.hide();
}
});
this.overlay.addEventListener('click', function(e) {
if (e.target === self.overlay && !self.isProcessing) {
self.hide();
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && !self.isProcessing && self.overlay.classList.contains('show')) {
self.hide();
}
});
},
show: function() {
this.overlay.classList.add('show');
this.reset();
},
hide: function() {
this.overlay.classList.remove('show');
this.reset();
},
resetAdvancedOptions: function() {
document.getElementById('sub-convert-enable').checked = false;
document.getElementById('sub-convert-options').style.display = 'none';
document.getElementById('convert-address-input').value = 'https://api.dler.io/sub';
document.getElementById('convert-address-custom').style.display = 'none';
document.getElementById('convert-address-custom').value = '';
var templateSelect = document.getElementById('template-select');
if (templateSelect && templateSelect.options.length > 1) {
templateSelect.selectedIndex = 0;
}
document.getElementById('custom-template-group').style.display = 'none';
document.getElementById('custom-template-input').value = '';
document.getElementById('emoji-enable').checked = false;
document.getElementById('udp-enable').checked = false;
document.getElementById('skip-cert-verify').checked = false;
document.getElementById('sort-enable').checked = false;
document.getElementById('node-type-enable').checked = false;
document.getElementById('rule-provider-enable').checked = false;
document.getElementById('custom-params-input').value = '';
document.getElementById('keyword-input').value = '';
document.getElementById('exclude-keyword-input').value = '';
document.getElementById('exclude-expire').checked = false;
document.getElementById('exclude-traffic').checked = false;
document.getElementById('exclude-tg').checked = false;
document.getElementById('exclude-website').checked = false;
},
reset: function() {
this.selectedFile = null;
this.isProcessing = false;
this.currentMode = 'file';
this.switchMode('file');
document.getElementById('config-filename-input').value = '';
document.getElementById('subscribe-url-input').value = '';
document.getElementById('subscribe-ua-input').value = 'clash.meta';
document.getElementById('subscribe-ua-custom').style.display = 'none';
document.getElementById('advanced-options-enable').checked = false;
document.getElementById('advanced-options-container').style.display = 'none';
this.resetAdvancedOptions();
var templateSelect = document.getElementById('template-select');
if (templateSelect && templateSelect.options.length > 1) {
templateSelect.selectedIndex = 0;
}
document.getElementById('upload-progress').style.display = 'none';
document.getElementById('config-upload-status-text').textContent = '<%:Ready to add config%>';
this.updateSubmitButton();
},
switchMode: function(mode) {
this.currentMode = mode;
var modeFileTab = document.getElementById('upload-mode-file');
var modeSubscribeTab = document.getElementById('upload-mode-subscribe');
var modeFileContent = document.getElementById('mode-file-content');
var modeSubscribeContent = document.getElementById('mode-subscribe-content');
var statusText = document.getElementById('config-upload-status-text');
var uploadZone = document.getElementById('upload-zone');
if (mode === 'file') {
modeFileTab.classList.add('active');
modeSubscribeTab.classList.remove('active');
modeFileContent.style.display = 'block';
modeSubscribeContent.style.display = 'none';
statusText.textContent = '<%:Ready to upload file%>';
} else {
modeFileTab.classList.remove('active');
modeSubscribeTab.classList.add('active');
modeFileContent.style.display = 'none';
modeSubscribeContent.style.display = 'block';
statusText.textContent = '<%:Ready to add subscription%>';
}
this.selectedFile = null;
uploadZone.classList.remove('has-file');
uploadZone.querySelector('.upload-primary').textContent = '<%:Click to select file or drag and drop%>';
uploadZone.querySelector('.upload-secondary').textContent = '<%:Support YAML file, max size 10MB%>';
this.updateSubmitButton();
},
handleFileSelect: function(file) {
this.selectedFile = file;
var uploadZone = document.getElementById('upload-zone');
var filenameInput = document.getElementById('config-filename-input');
var statusText = document.getElementById('config-upload-status-text');
if (!file) {
uploadZone.classList.remove('has-file');
this.updateSubmitButton();
statusText.textContent = '<%:Ready to upload file%>';
return;
}
if (!file.name.match(/\.(yaml|yml)$/i)) {
alert('<%:Please select a YAML file%>');
return;
}
if (file.size > 10 * 1024 * 1024) {
alert('<%:File size exceeds 10MB limit%>');
return;
}
uploadZone.classList.add('has-file');
uploadZone.querySelector('.upload-primary').textContent = '<%:File selected:%> ' + file.name;
uploadZone.querySelector('.upload-secondary').textContent = '<%:Size:%> ' + this.formatFileSize(file.size);
var defaultName = file.name.replace(/\.(yaml|yml)$/i, '');
filenameInput.value = defaultName;
this.updateSubmitButton();
statusText.textContent = '<%:File ready to upload%>';
},
formatFileSize: function(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
},
updateSubmitButton: function() {
var filename = document.getElementById('config-filename-input').value.trim();
var submitBtn = document.getElementById('config-upload-submit');
var isValid = false;
if (this.currentMode === 'file') {
isValid = this.selectedFile && filename;
} else if (this.currentMode === 'subscribe') {
var url = document.getElementById('subscribe-url-input').value.trim();
var advancedEnabled = document.getElementById('advanced-options-enable').checked;
var subConvert = advancedEnabled && document.getElementById('sub-convert-enable').checked;
if (url && filename) {
if (subConvert) {
if (url.indexOf('\n') !== -1 || url.indexOf('|') !== -1) {
var links = url.indexOf('\n') !== -1 ? url.split('\n') : url.split('|');
for (var i = 0; i < links.length; i++) {
var link = links[i].trim();
if (link && (/^https?:\/\//.test(link) || /^[a-zA-Z]+:\/\//.test(link))) {
isValid = true;
break;
}
}
} else {
isValid = /^https?:\/\//.test(url) || /^[a-zA-Z]+:\/\//.test(url);
}
} else {
isValid = /^https?:\/\//.test(url) && url.indexOf('\n') === -1 && url.indexOf('|') === -1;
}
}
}
submitBtn.disabled = !isValid || this.isProcessing;
},
uploadFile: function() {
if (!this.selectedFile || this.isProcessing) return;
var filename = document.getElementById('config-filename-input').value.trim();
if (!filename) {
alert('<%:Please enter a filename%>');
return;
}
if (!/^[a-zA-Z0-9_\-\s\u4e00-\u9fa5\.]+$/.test(filename)) {
alert('<%:Filename contains invalid characters%>');
return;
}
var self = this;
this.isProcessing = true;
var submitBtn = document.getElementById('config-upload-submit');
var cancelBtn = document.getElementById('config-upload-cancel');
var statusText = document.getElementById('config-upload-status-text');
var progressContainer = document.getElementById('upload-progress');
var progressFill = document.getElementById('upload-progress-fill');
var progressText = document.getElementById('upload-progress-text');
submitBtn.disabled = true;
cancelBtn.disabled = true;
statusText.textContent = '<%:Uploading...%>';
progressContainer.style.display = 'block';
var progress = 0;
var progressInterval = setInterval(function() {
if (progress < 90) {
progress += Math.random() * 15;
progressFill.style.width = Math.min(progress, 90) + '%';
progressText.textContent = '<%:Uploading...%> ' + Math.floor(Math.min(progress, 90)) + '%';
}
}, 100);
var reader = new FileReader();
reader.onload = function(e) {
var fileContent = e.target.result;
var formData = new FormData();
formData.append('config_file', fileContent);
formData.append('filename', filename);
fetch('<%=luci.dispatcher.build_url("admin", "services", "openclash", "upload_config")%>', {
method: 'POST',
body: formData
})
.then(function(response) {
clearInterval(progressInterval);
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
return response.json();
})
.then(function(data) {
progressFill.style.width = '100%';
progressText.textContent = '<%:Upload completed%> 100%';
if (data.status === 'success') {
statusText.textContent = '<%:Upload successful%>';
setTimeout(function() {
self.hide();
if (typeof ConfigFileManager !== 'undefined' && ConfigFileManager.refreshConfigList) {
ConfigFileManager.refreshConfigList();
}
}, 2000);
} else {
throw new Error(data.message || '<%:Upload failed%>');
}
})
.catch(function(error) {
self.handleError('<%:Upload failed:%> ' + error.message);
});
};
reader.onerror = function() {
clearInterval(progressInterval);
self.handleError('<%:Failed to read file%>');
};
reader.readAsText(this.selectedFile, 'UTF-8');
},
processSubscription: function() {
var url = document.getElementById('subscribe-url-input').value.trim();
var filename = document.getElementById('config-filename-input').value.trim();
var userAgent = document.getElementById('subscribe-ua-input').value;
var subscribeUaCustom = document.getElementById('subscribe-ua-custom');
var advancedEnabled = document.getElementById('advanced-options-enable').checked;
var subConvert = advancedEnabled && document.getElementById('sub-convert-enable').checked;
var convertAddress = 'https://api.dler.io/sub';
var template = '';
var emoji = false;
var udp = false;
var skipCert = false;
var sort = false;
var nodeType = false;
var ruleProvider = false;
var customParams = '';
var keywords = '';
var excludeKeywords = '';
var excludeDefaults = [];
if (advancedEnabled) {
convertAddress = document.getElementById('convert-address-input').value;
var convertAddressCustom = document.getElementById('convert-address-custom').value;
template = document.getElementById('template-select').value;
var customTemplate = document.getElementById('custom-template-input').value;
emoji = document.getElementById('emoji-enable').checked;
udp = document.getElementById('udp-enable').checked;
skipCert = document.getElementById('skip-cert-verify').checked;
sort = document.getElementById('sort-enable').checked;
nodeType = document.getElementById('node-type-enable').checked;
ruleProvider = document.getElementById('rule-provider-enable').checked;
customParams = document.getElementById('custom-params-input').value;
keywords = document.getElementById('keyword-input').value;
excludeKeywords = document.getElementById('exclude-keyword-input').value;
if (document.getElementById('exclude-expire').checked) excludeDefaults.push('过期时间');
if (document.getElementById('exclude-traffic').checked) excludeDefaults.push('剩余流量');
if (document.getElementById('exclude-tg').checked) excludeDefaults.push('TG群');
if (document.getElementById('exclude-website').checked) excludeDefaults.push('官网');
if (convertAddress === 'custom') {
convertAddress = convertAddressCustom.trim() || 'https://api.dler.io/sub';
}
if (template === '0') {
template = customTemplate.trim();
}
}
if (userAgent === 'custom') {
userAgent = subscribeUaCustom.value.trim() || 'clash.meta';
}
if (!url || !filename) {
alert('<%:Please enter subscription URL and config name%>');
return;
}
var isValidFormat = false;
if (subConvert) {
if (url.indexOf('\n') !== -1 || url.indexOf('|') !== -1) {
var links = [];
if (url.indexOf('\n') !== -1) {
links = url.split('\n');
} else {
links = url.split('|');
}
for (var i = 0; i < links.length; i++) {
var link = links[i].trim();
if (link && (/^https?:\/\//.test(link) || /^[a-zA-Z]+:\/\//.test(link))) {
isValidFormat = true;
break;
}
}
} else {
if (/^https?:\/\//.test(url) || /^[a-zA-Z]+:\/\//.test(url)) {
isValidFormat = true;
}
}
} else {
if (/^https?:\/\//.test(url) && url.indexOf('\n') === -1 && url.indexOf('|') === -1) {
isValidFormat = true;
}
}
if (!isValidFormat) {
var errorMsg = subConvert ?
'<%:Invalid subscription URL format. Support HTTP/HTTPS subscription URLs or protocol links, can be separated by newlines or |%>' :
'<%:Invalid subscription URL format. Only single HTTP/HTTPS subscription URL is supported when subscription conversion is disabled%>';
alert(errorMsg);
return;
}
var requestData = {
name: filename,
address: url,
sub_ua: userAgent,
sub_convert: subConvert ? '1' : '0',
convert_address: convertAddress,
template: template,
emoji: emoji ? 'true' : 'false',
udp: udp ? 'true' : 'false',
skip_cert_verify: skipCert ? 'true' : 'false',
sort: sort ? 'true' : 'false',
node_type: nodeType ? 'true' : 'false',
rule_provider: ruleProvider ? 'true' : 'false',
custom_params: customParams,
keyword: keywords,
ex_keyword: excludeKeywords,
de_ex_keyword: excludeDefaults.join('\n')
};
var self = this;
this.isProcessing = true;
var submitBtn = document.getElementById('config-upload-submit');
var cancelBtn = document.getElementById('config-upload-cancel');
var statusText = document.getElementById('config-upload-status-text');
var progressContainer = document.getElementById('upload-progress');
var progressFill = document.getElementById('upload-progress-fill');
var progressText = document.getElementById('upload-progress-text');
submitBtn.disabled = true;
cancelBtn.disabled = true;
statusText.textContent = '<%:Adding subscription...%>';
progressContainer.style.display = 'block';
var progress = 0;
var progressInterval = setInterval(function() {
if (progress < 90) {
progress += Math.random() * 15;
progressFill.style.width = Math.min(progress, 90) + '%';
progressText.textContent = '<%:Processing...%> ' + Math.floor(Math.min(progress, 90)) + '%';
}
}, 100);
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "add_subscription")%>', {
name: filename,
address: url,
sub_ua: userAgent,
sub_convert: subConvert ? '1' : '0',
convert_address: convertAddress,
template: template,
emoji: emoji ? 'true' : 'false',
udp: udp ? 'true' : 'false',
skip_cert_verify: skipCert ? 'true' : 'false',
sort: sort ? 'true' : 'false',
node_type: nodeType ? 'true' : 'false',
rule_provider: ruleProvider ? 'true' : 'false',
custom_params: customParams,
keyword: keywords,
ex_keyword: excludeKeywords,
de_ex_keyword: excludeDefaults.join('\n')
}, function(x, data) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update_config")%>', {
filename: filename
}, function(x2, data2) {
clearInterval(progressInterval);
if (x2 && x2.status == 200) {
progressFill.style.width = '100%';
progressText.textContent = '<%:Subscription added successfully%> 100%';
statusText.textContent = '<%:Subscription added successfully%>';
setTimeout(function() {
self.hide();
if (typeof ConfigFileManager !== 'undefined' && ConfigFileManager.refreshConfigList) {
ConfigFileManager.refreshConfigList();
}
}, 2000);
} else {
self.handleError('<%:Failed to download subscription config%>');
}
});
} else {
clearInterval(progressInterval);
self.handleError('<%:Failed to add subscription%>');
}
});
},
autoFillConfigName: function() {
var url = document.getElementById('subscribe-url-input').value.trim();
var filenameInput = document.getElementById('config-filename-input');
if (!filenameInput.value.trim() && url) {
try {
var urlObj = new URL(url);
var hostname = urlObj.hostname;
var configName = hostname
.replace(/^(www\.|api\.|sub\.|subscribe\.)/, '')
.replace(/\.(com|net|org|cn|io|me|cc|xyz|top)$/, '')
.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_')
.replace(/_{2,}/g, '_')
.replace(/^_|_$/g, '');
if (!configName || configName.length < 2) {
configName = 'subscription_' + Date.now().toString().slice(-6);
}
if (configName.length > 30) {
configName = configName.substring(0, 30);
}
filenameInput.value = configName;
this.updateSubmitButton();
} catch (e) {
}
}
},
handleError: function(message) {
var statusText = document.getElementById('config-upload-status-text');
var progressText = document.getElementById('upload-progress-text');
var progressFill = document.getElementById('upload-progress-fill');
var submitBtn = document.getElementById('config-upload-submit');
var cancelBtn = document.getElementById('config-upload-cancel');
var progressContainer = document.getElementById('upload-progress');
statusText.textContent = '<%:Process failed%>';
progressText.textContent = '<%:Process failed%>';
progressFill.style.width = '0%';
alert(message);
this.isProcessing = false;
submitBtn.disabled = false;
cancelBtn.disabled = false;
progressContainer.style.display = 'none';
}
};
document.addEventListener('DOMContentLoaded', function() {
ConfigUploader.init();
});
</script>