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

1528 lines
55 KiB
HTML

<style>
.oc[data-darkmode="true"] .config-editor-modal .CodeMirror {
background: var(--bg-white);
color: var(--text-primary);
}
.oc[data-darkmode="true"] .config-editor-modal .CodeMirror-gutters {
background: var(--bg-gray);
border-right: 1px solid var(--border-light);
}
.oc[data-darkmode="true"] .config-editor-modal .CodeMirror-linenumber {
color: var(--text-secondary);
}
.oc[data-darkmode="true"] .config-editor-modal .CodeMirror-scrollbar-filler,
.oc[data-darkmode="true"] .config-editor-modal .CodeMirror-gutter-filler {
background: var(--bg-gray);
}
.oc[data-darkmode="true"] #config-mode-tabs .mode-tab {
color: var(--text-secondary);
}
.oc[data-darkmode="true"] #config-mode-tabs .mode-tab:hover {
color: var(--text-primary);
background: rgba(96, 165, 250, 0.1);
}
.oc[data-darkmode="true"] #config-mode-tabs .mode-tab.active {
background: var(--primary-color);
color: white;
}
.oc[data-darkmode="true"] #config-mergeview-container .CodeMirror-merge-gap {
background: var(--text-secondary) !important;
}
.oc[data-darkmode="true"] .oc .config-editor-content {
border-bottom: 1px solid var(--border-light);
border-top: 1px solid var(--border-light);
}
.oc[data-darkmode="true"] .overwrite-banner {
background: rgba(255,80,80,0.18);
}
.oc[data-darkmode="true"] .overwrite-banner svg {
stroke: var(--error-color);
}
.oc[data-darkmode="true"] .overwrite-banner svg circle {
stroke: var(--error-color);
fill: rgba(255,80,80,0.18);
}
.oc .config-editor-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-editor-modal-overlay.show {
display: flex;
}
.oc .config-editor-modal {
background: var(--bg-white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
width: 90vw;
height: 85vh;
max-width: 1200px;
min-width: 600px;
min-height: 400px;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--border-light);
position: relative;
transition: all 0.3s ease;
}
.oc .config-editor-modal.maximized {
width: 98vw !important;
height: 95vh !important;
max-width: none !important;
}
.oc .config-editor-modal.minimized {
width: 70vw !important;
height: 70vh !important;
}
.oc .config-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border-light);
background: var(--bg-gray);
flex-shrink: 0;
cursor: move;
user-select: none;
min-width: 0;
}
.oc .config-editor-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
}
.oc .config-editor-title .config-file-name {
color: var(--primary-color);
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
vertical-align: bottom;
padding: 0;
}
.oc .config-editor-actions {
display: flex;
align-items: center;
gap: 12px;
}
.oc .size-btn {
width: 24px !important;
height: 24px !important;
min-width: 24px !important;
padding: 0 !important;
}
.oc .size-btn svg {
width: 12px !important;
height: 12px !important;
}
#config-mergeview-container {
width: 100%;
height: 100%;
display: block;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-white);
z-index: 2;
}
.oc .config-editor-content {
flex: 1;
position: relative;
overflow: hidden;
border-bottom: 1px solid var(--border-light);
border-top: 1px solid var(--border-light);
}
.oc .config-editor-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
background: var(--bg-white);
color: var(--text-secondary);
font-size: 14px;
}
.oc .loading-spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-light);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.oc .config-editor-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--bg-gray);
flex-shrink: 0;
position: relative;
}
.oc .config-editor-status {
font-size: 12px;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 50%;
}
.oc .config-editor-help {
font-size: 11px;
color: var(--text-secondary);
opacity: 0.8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 50%;
}
.oc .config-editor-resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
cursor: nw-resize;
background: linear-gradient(-45deg,
transparent 0%,
transparent 40%,
var(--border-color) 40%,
var(--border-color) 45%,
transparent 45%,
transparent 50%,
var(--border-color) 50%,
var(--border-color) 55%,
transparent 55%,
transparent 60%,
var(--border-color) 60%,
var(--border-color) 65%,
transparent 65%);
}
.oc #config-editor-textarea {
width: 100%;
height: 100%;
border: none;
outline: none;
resize: none;
font-size: 14px;
line-height: 1.5;
padding: 12px;
background: var(--bg-white);
color: var(--text-primary);
}
.oc .config-editor-modal .CodeMirror {
height: 100%;
font-size: 14px;
line-height: 1.5;
}
.oc #config-mode-tabs {
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
background: transparent;
border: none;
box-shadow: none;
border-radius: var(--radius-md);
}
.oc #config-mode-tabs .mode-tabs {
display: flex;
width: 100%;
background: var(--bg-gray);
border-radius: var(--radius-md);
padding: 4px;
gap: 4px;
margin: 0 auto;
}
.oc #config-mode-tabs .mode-tab {
flex: 1 1 0;
min-width: 0;
width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 0;
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);
box-sizing: border-box;
}
.oc #config-mode-tabs .mode-tab:hover {
color: var(--text-primary);
background: rgba(59, 130, 246, 0.1);
}
.oc #config-mode-tabs .mode-tab.active {
background: var(--primary-color);
color: white;
box-shadow: var(--shadow-sm);
}
.overwrite-banner {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
padding: 10px 20px;
background: rgba(255,80,80,0.12);
font-size: 14px;
text-align: center;
}
.overwrite-banner svg {
flex-shrink: 0;
display: block;
}
.overwrite-banner span {
flex: unset;
text-align: center;
display: inline-block;
color: var(--error-color);
line-height: 1.5;
vertical-align: middle;
}
.oc .config-editor-modal .CodeMirror.zoom-75 { font-size: 10.5px; }
.oc .config-editor-modal .CodeMirror.zoom-90 { font-size: 12.6px; }
.oc .config-editor-modal .CodeMirror.zoom-110 { font-size: 15.4px; }
.oc .config-editor-modal .CodeMirror.zoom-125 { font-size: 17.5px; }
.oc .config-editor-modal .CodeMirror.zoom-150 { font-size: 21px; }
.oc .config-editor-modal .CodeMirror.zoom-200 { font-size: 28px; }
#config-mergeview-container .CodeMirror-merge,
#config-mergeview-container .CodeMirror-merge-pane,
#config-mergeview-container .CodeMirror,
#config-mergeview-container .CodeMirror-scroll {
height: 100% !important;
min-height: 0 !important;
box-sizing: border-box;
}
#config-mergeview-container .CodeMirror-merge-gap {
height: 100% !important;
min-height: 0 !important;
}
#config-mergeview-container .CodeMirror-scroll {
overflow-y: auto !important;
overflow-x: hidden !important;
}
#config-mergeview-container .CodeMirror-merge-pane {
overflow: hidden;
}
#config-mergeview-container .CodeMirror-merge-r-chunk {
background: #0095ff2e !important;
}
#config-mergeview-container .CodeMirror-merge-r-connect {
fill: #0095ff2e !important;
stroke: #0095ff2e !important;
}
#config-mergeview-container .CodeMirror-merge {
border: none !important;
}
@media screen and (max-width: 768px) {
.oc .config-editor-modal {
width: 95vw;
height: 80vh;
min-width: 320px;
}
.oc .config-editor-actions {
gap: 5px;
}
}
</style>
<div class="oc">
<div class="config-editor-modal-overlay" id="config-editor-overlay">
<div class="config-editor-modal" id="config-editor-modal">
<div class="config-editor-header">
<div class="config-editor-title">
<span id="editTitle"><%:File Edit%>: </span>
<span class="config-file-name" id="config-file-name"><%:Loading...%></span>
</div>
<div class="config-editor-actions">
<button type="button" class="icon-btn" id="config-editor-layout" title="<%:Compare%>" style="display:none;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="8" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/>
<rect x="13" y="3" width="8" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
</button>
<button type="button" class="icon-btn" id="config-editor-download" title="<%:Download%>">
<svg width="14" height="14" 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="7,11 12,16 17,11"></polyline>
<line x1="12" y1="2" x2="12" y2="16"></line>
</svg>
</button>
<button type="button" class="icon-btn" id="config-editor-save" title="<%:Save%>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17,21 17,13 7,13 7,21"></polyline>
<polyline points="7,3 7,8 15,8"></polyline>
</svg>
</button>
<button type="button" class="icon-btn" id="config-editor-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 id="overwrite-banner" class="overwrite-banner" style="display:none;">
<svg style="flex-shrink:0;" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--error-color)" stroke-width="2">
<circle cx="12" cy="12" r="10" stroke="var(--error-color)" fill="rgba(255,80,80,0.12)"/>
<line x1="12" y1="5" x2="12" y2="13"/>
<circle cx="12" cy="16" r="0.5"/>
</svg>
<span>
<%:You are editing the overwrite script, please note that some settings may cause the abnormal, be careful with the modification!%>
</span>
</div>
<div id="config-mode-tabs" style="display:none;">
<div class="mode-tabs">
<button type="button" class="mode-tab active" id="tab-original-config" data-mode="original">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="vertical-align:middle;margin-right:4px;">
<rect x="4" y="4" width="16" height="16" rx="2" stroke="currentColor" stroke-width="2" fill="none"/>
<line x1="8" y1="8" x2="16" y2="8" stroke="currentColor" stroke-width="2"/>
<line x1="8" y1="12" x2="16" y2="12" stroke="currentColor" stroke-width="2"/>
<line x1="8" y1="16" x2="12" y2="16" stroke="currentColor" stroke-width="2"/>
</svg>
<%:Original Config%>
</button>
<button type="button" class="mode-tab" id="tab-runtime-config" data-mode="runtime">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="vertical-align:middle;margin-right:4px;">
<polygon points="13 2 3 14 12 14 11 22 21 10 13 10 13 2" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
<%:Runtime Config%>
</button>
</div>
</div>
<div class="config-editor-content">
<div class="config-editor-loading" id="config-editor-loading">
<div class="loading-spinner"></div>
<span><%:Loading config file...%></span>
</div>
<textarea id="config-editor-textarea" style="display: none;"></textarea>
<div id="config-mergeview-container" style="display:none;width:100%;height:100%;"></div>
</div>
<div class="config-editor-footer">
<div class="config-editor-status">
<span id="config-editor-status-text"><%:Ready%></span>
</div>
<div class="config-editor-help">
<span id="config-editor-help"><%:Press F11 for fullscreen, Esc to exit fullscreen, Ctrl+Mouse Wheel to zoom%></span>
</div>
<div class="config-editor-resize-handle" id="config-editor-resize-handle"></div>
</div>
</div>
</div>
</div>
<link rel="stylesheet" href="/luci-static/resources/openclash/lib/codemirror.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/material.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/material-log.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/idea.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/fold/foldgutter.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/lint/lint.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/display/fullscreen.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/dialog/dialog.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/search/matchesonscrollbar.css">
<script src="/luci-static/resources/openclash/lib/codemirror.js"></script>
<script src="/luci-static/resources/openclash/mode/yaml/yaml.js"></script>
<script src="/luci-static/resources/openclash/mode/lua/lua.js"></script>
<script src="/luci-static/resources/openclash/mode/shell/shell.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/foldcode.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/foldgutter.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/indent-fold.js"></script>
<script src="/luci-static/resources/openclash/addon/edit/matchbrackets.js"></script>
<script src="/luci-static/resources/openclash/addon/selection/active-line.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/lint.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/yaml-lint.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/js-yaml.min.js"></script>
<script src="/luci-static/resources/openclash/addon/display/fullscreen.js"></script>
<script src="/luci-static/resources/openclash/addon/display/autorefresh.js"></script>
<script src="/luci-static/resources/openclash/addon/dialog/dialog.js"></script>
<script src="/luci-static/resources/openclash/addon/search/searchcursor.js"></script>
<script src="/luci-static/resources/openclash/addon/search/search.js"></script>
<script src="/luci-static/resources/openclash/addon/scroll/annotatescrollbar.js"></script>
<script src="/luci-static/resources/openclash/addon/search/matchesonscrollbar.js"></script>
<script src="/luci-static/resources/openclash/addon/search/jump-to-line.js"></script>
<script src="/luci-static/resources/openclash/addon/merge/diff_match_patch.js"></script>
<script src="/luci-static/resources/openclash/addon/merge/merge.js"></script>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/merge/merge.css">
<script type="text/javascript">
var ConfigEditor = {
overlay: null,
modal: null,
editorInstance: null,
originalContent: '',
isModified: false,
currentZoom: 100,
currentConfigFile: '',
zoomLevels: [75, 90, 100, 110, 125, 150, 200],
isOverwrite: false,
currentViewMode: 'original',
runtimeContent: '',
mergeViewActive: false,
SVG_COMPARE: `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="8" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/>
<rect x="13" y="3" width="8" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
`,
SVG_RESTORE: `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
`,
init: function() {
this.overlay = document.getElementById('config-editor-overlay');
this.modal = document.getElementById('config-editor-modal');
this.mergeViewActive = false;
if (!this.overlay || !this.modal) {
return;
}
this.bindEvents();
},
bindEvents: function() {
var self = this;
document.getElementById('config-editor-save').addEventListener('click', function() {
self.saveConfigContent();
});
document.getElementById('config-editor-download').addEventListener('click', function() {
self.downloadConfigContent();
});
document.getElementById('config-editor-close').addEventListener('click', function() {
self.closeEditor();
});
document.addEventListener('keydown', function(e) {
if (!self.overlay.classList.contains('show')) return;
if ((e.ctrlKey || e.metaKey) && (e.key === '=' || e.key === '+')) {
e.preventDefault();
self.zoomIn();
} else if ((e.ctrlKey || e.metaKey) && e.key === '-') {
e.preventDefault();
self.zoomOut();
} else if ((e.ctrlKey || e.metaKey) && e.key === '0') {
e.preventDefault();
self.resetZoom();
} else if (e.key === 'Escape' && (!self.editorInstance || !self.editorInstance.getOption("fullScreen"))) {
self.closeEditor();
}
});
this.overlay.addEventListener('wheel', function(e) {
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
if (e.deltaY < 0) {
self.zoomIn();
} else {
self.zoomOut();
}
}
});
var tabOriginal = document.getElementById('tab-original-config');
var tabRuntime = document.getElementById('tab-runtime-config');
if (tabOriginal && tabRuntime) {
tabOriginal.addEventListener('click', function() {
if (self.currentViewMode !== 'original') {
self.currentViewMode = 'original';
self.loadConfigContent();
self.updateModeTabs();
}
});
tabRuntime.addEventListener('click', function() {
if (self.currentViewMode !== 'runtime') {
self.currentViewMode = 'runtime';
self.loadConfigContent();
self.updateModeTabs();
}
});
};
var layoutBtn = document.getElementById('config-editor-layout');
if (layoutBtn) {
layoutBtn.addEventListener('click', function() {
if (!self.mergeViewActive) {
self.showMergeView();
self.currentViewMode = 'original';
layoutBtn.title = "<%:Restore%>";
layoutBtn.innerHTML = self.SVG_RESTORE;
} else {
self.hideMergeView();
layoutBtn.title = "<%:Compare%>";
layoutBtn.innerHTML = self.SVG_COMPARE;
}
});
};
this.makeDraggable();
this.makeResizable();
},
show: function(configFile) {
this.isOverwrite = false;
var banner = document.getElementById('overwrite-banner');
if (banner) banner.style.display = 'none';
var tabs = document.getElementById('config-mode-tabs');
if (tabs) tabs.style.display = 'flex';
var layoutBtn = document.getElementById('config-editor-layout');
if (layoutBtn) layoutBtn.style.display = 'inline-block';
if (!configFile) {
alert('<%:Please select a config file first%>');
return;
}
this.currentViewMode = 'original';
this.runtimeContent = '';
this.currentConfigFile = configFile;
this.overlay.classList.add('show');
this.modal.classList.remove('maximized');
this.modal.classList.remove('minimized');
var editTitle = document.getElementById('editTitle');
if (editTitle) {
editTitle.textContent = '<%:File Edit%>: ';
}
var configNameElement = document.getElementById('config-file-name');
if (configNameElement) {
configNameElement.textContent = this.formatDisplayName(configFile);
}
this.isModified = false;
this.originalContent = '';
this.mergeViewActive = false;
this.hideMergeView();
this.updateModeTabs();
},
showOverwrite: function() {
this.isOverwrite = true;
var banner = document.getElementById('overwrite-banner');
if (banner) banner.style.display = 'flex';
var tabs = document.getElementById('config-mode-tabs');
if (tabs) tabs.style.display = 'none';
var layoutBtn = document.getElementById('config-editor-layout');
if (layoutBtn) layoutBtn.style.display = 'none';
this.currentConfigFile = '/etc/openclash/custom/openclash_custom_overwrite.sh';
this.overlay.classList.add('show');
this.modal.classList.remove('maximized');
this.modal.classList.remove('minimized');
var editTitle = document.getElementById('editTitle');
if (editTitle) {
editTitle.textContent = '<%:Overwrite Edit%>: ';
}
var configNameElement = document.getElementById('config-file-name');
if (configNameElement) {
configNameElement.textContent = this.formatDisplayName(this.currentConfigFile);
}
this.isModified = false;
this.originalContent = '';
this.mergeViewActive = false;
this.hideMergeView();
this.loadConfigContent();
},
hide: function() {
this.overlay.classList.remove('show');
if (this.editorInstance) {
if (this.editorInstance.toTextArea) this.editorInstance.toTextArea();
this.editorInstance = null;
}
this.isModified = false;
this.originalContent = '';
this.currentConfigFile = '';
var loadingDiv = document.getElementById('config-editor-loading');
var textarea = document.getElementById('config-editor-textarea');
var mergeview = document.getElementById('config-mergeview-container');
var editor_help = document.getElementById('config-editor-help');
if (loadingDiv) loadingDiv.style.display = 'flex';
if (textarea) textarea.style.display = 'none';
if (mergeview) mergeview.style.display = 'none';
if (layoutBtn) {
layoutBtn.classList.remove('active');
layoutBtn.title = "<%:Compare%>";
layoutBtn.innerHTML = this.SVG_COMPARE;
}
if (editor_help) editor_help.textContent = '<%:Press F11 for fullscreen, Esc to exit fullscreen, Ctrl+Mouse Wheel to zoom%>';
},
formatDisplayName: function(fileName) {
if (!fileName) return '<%:Unknown%>';
if (this.isOverwrite || fileName === '/etc/openclash/custom/openclash_custom_overwrite.sh') {
return 'openclash_custom_overwrite.sh';
}
var name = fileName.split('/').pop().split('\\').pop();
return name;
},
updateModeTabs: function() {
var tabOriginal = document.getElementById('tab-original-config');
var tabRuntime = document.getElementById('tab-runtime-config');
var saveBtn = document.getElementById('config-editor-save');
if (this.isOverwrite) {
if (tabOriginal && tabRuntime) {
tabOriginal.classList.remove('active');
tabRuntime.classList.remove('active');
}
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.style.opacity = '1';
saveBtn.style.cursor = 'pointer';
}
return;
}
if (tabOriginal && tabRuntime) {
if (this.currentViewMode === 'original') {
tabOriginal.classList.add('active');
tabRuntime.classList.remove('active');
if (saveBtn) {
saveBtn.disabled = !this.isModified;
saveBtn.style.opacity = this.isModified ? '1' : '0.5';
saveBtn.style.cursor = this.isModified ? 'pointer' : 'not-allowed';
}
} else {
tabOriginal.classList.remove('active');
tabRuntime.classList.add('active');
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.style.opacity = '0.5';
saveBtn.style.cursor = 'not-allowed';
}
}
}
},
loadConfigContent: function() {
var self = this;
var statusText = document.getElementById('config-editor-status-text');
var loadingDiv = document.getElementById('config-editor-loading');
var textarea = document.getElementById('config-editor-textarea');
var mergeview = document.getElementById('config-mergeview-container');
if (mergeview) mergeview.style.display = 'none';
if (textarea) textarea.style.display = 'block';
statusText.textContent = '<%:Loading...%>';
var url, mode;
if (this.isOverwrite) {
url = '/cgi-bin/luci/admin/services/openclash/config_file_read?config_file=' + encodeURIComponent('/etc/openclash/custom/openclash_custom_overwrite.sh');
mode = "text/x-sh";
} else if (this.currentViewMode === 'runtime') {
var runtimePath = '/etc/openclash/' + encodeURIComponent(this.formatDisplayName(this.currentConfigFile));
url = '/cgi-bin/luci/admin/services/openclash/config_file_read?config_file=' + runtimePath;
mode = "text/yaml";
} else {
url = '/cgi-bin/luci/admin/services/openclash/config_file_read?config_file=' + encodeURIComponent(this.currentConfigFile);
mode = "text/yaml";
}
function renderEditor(content, mode, readOnly, lint) {
loadingDiv.style.display = 'none';
textarea.value = content;
textarea.style.display = 'block';
if (self.editorInstance) {
if (self.editorInstance.toTextArea) self.editorInstance.toTextArea();
self.editorInstance = null;
}
self.editorInstance = CodeMirror.fromTextArea(textarea, {
mode: mode,
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "material",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
lint: lint,
readOnly: readOnly,
gutters: lint
? ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"]
: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) {
cm.setOption("fullScreen", false);
}
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add');
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ");
cm.replaceSelection(spaces);
}
},
"Ctrl-S": function(cm) {
if (!readOnly) self.saveConfigContent();
}
}
});
self.editorInstance.setSize('100%', '100%');
self.editorInstance.setValue(content);
self.editorInstance.refresh();
if (!readOnly) {
self.editorInstance.on("change", function() {
self.isModified = self.editorInstance.getValue() !== self.originalContent;
self.updateSaveButtonState();
});
}
}
if (!this.isOverwrite) {
if (this.currentViewMode === 'original' && this.originalContent) {
renderEditor(this.originalContent, "text/yaml", false, true);
statusText.textContent = '<%:Ready%>';
self.updateModeTabs();
return;
}
if (this.currentViewMode === 'runtime' && this.runtimeContent) {
renderEditor(this.runtimeContent, "text/yaml", true, false);
statusText.textContent = '<%:Runtime config (read only)%>';
self.updateModeTabs();
return;
}
}
fetch(url)
.then(function(response) {
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
return response.json();
})
.then(function(data) {
if (data.content !== undefined) {
if (self.currentViewMode === 'runtime' && !self.isOverwrite) {
self.runtimeContent = data.content;
renderEditor(self.runtimeContent, "text/yaml", true, false);
statusText.textContent = '<%:Runtime config (read only)%>';
} else {
self.originalContent = data.content;
renderEditor(self.originalContent, self.isOverwrite ? "text/x-sh" : "text/yaml", false, !self.isOverwrite);
statusText.textContent = '<%:Ready%>';
}
self.updateModeTabs();
} else {
throw new Error('Invalid response data');
}
})
.catch(function(error) {
loadingDiv.querySelector('span').textContent = '<%:Failed to load config file%>';
statusText.textContent = '<%:Load failed%>';
});
},
showMergeView: function() {
var self = this;
if (this.isOverwrite) return;
var container = document.getElementById('config-mergeview-container');
var textarea = document.getElementById('config-editor-textarea');
var loadingDiv = document.getElementById('config-editor-loading');
var tabs = document.getElementById('config-mode-tabs');
var editor_help = document.getElementById('config-editor-help');
var statusText = document.getElementById('config-editor-status-text');
if (tabs) tabs.style.display = 'none';
if (textarea) textarea.style.display = 'none';
if (loadingDiv) loadingDiv.style.display = 'none';
if (container) container.style.display = 'block';
if (statusText) statusText.textContent = '<%:Loading...%>';
if (editor_help) editor_help.textContent = '<%:Press F10 to toggle differences, F11 for fullscreen, Esc to exit fullscreen, Ctrl+Mouse Wheel to zoom%>';
var getOriginal = function() {
return new Promise(function(resolve, reject) {
if (self.originalContent) return resolve(self.originalContent);
var url = '/cgi-bin/luci/admin/services/openclash/config_file_read?config_file=' + encodeURIComponent(self.currentConfigFile);
fetch(url).then(function(r){return r.json()}).then(function(data){
resolve(data.content || '');
}).catch(function(){resolve('')});
});
};
var getRuntime = function() {
return new Promise(function(resolve, reject) {
if (self.runtimeContent) return resolve(self.runtimeContent);
var runtimePath = '/etc/openclash/' + encodeURIComponent(self.formatDisplayName(self.currentConfigFile));
var url = '/cgi-bin/luci/admin/services/openclash/config_file_read?config_file=' + runtimePath;
fetch(url).then(function(r){return r.json()}).then(function(data){
resolve(data.content || '');
}).catch(function(){resolve('')});
});
};
let showDifferences = true;
Promise.all([getOriginal(), getRuntime()]).then(function(contents){
var original = contents[0] || '';
var runtime = contents[1] || '';
container.innerHTML = '';
if (self.editorInstance && self.editorInstance.toTextArea) self.editorInstance.toTextArea();
self.editorInstance = CodeMirror.MergeView(container, {
value: original,
orig: runtime,
mode: "text/yaml",
theme: "material",
lineNumbers: true,
autoRefresh: true,
styleActiveLine: true,
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
lint: true,
highlightDifferences: showDifferences,
connect: null,
collapseIdentical: false,
readOnly: false,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
extraKeys: {
"F10": function() {
showDifferences = !showDifferences;
if (self.editorInstance && self.editorInstance.setShowDifferences) {
self.editorInstance.setShowDifferences(showDifferences);
}
},
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add');
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ");
cm.replaceSelection(spaces);
}
},
"Ctrl-S": function(cm) {
self.saveConfigContent();
}
}
});
var leftEditor = self.editorInstance.edit;
if (leftEditor) {
leftEditor.on("change", function() {
self.isModified = leftEditor.getValue() !== self.originalContent;
self.updateSaveButtonState();
});
}
if (self.editorInstance.editor)
self.editorInstance.editor().setSize('100%', '100%');
if (self.editorInstance.rightOriginal && self.editorInstance.rightOriginal())
self.editorInstance.rightOriginal().setSize('100%', '100%');
self.mergeViewActive = true;
if (statusText) statusText.textContent = '<%:Compare mode: left(Original Config), right(Runtime Config)%>';
var layoutBtn = document.getElementById('config-editor-layout');
if (layoutBtn) layoutBtn.classList.add('active');
});
},
hideMergeView: function() {
var container = document.getElementById('config-mergeview-container');
var textarea = document.getElementById('config-editor-textarea');
var tabs = document.getElementById('config-mode-tabs');
var editor_help = document.getElementById('config-editor-help');
var layoutBtn = document.getElementById('config-editor-layout');
if (container) {
container.innerHTML = '';
container.style.display = 'none';
}
if (textarea) textarea.style.display = 'block';
if (!this.isOverwrite && tabs) tabs.style.display = 'flex';
this.mergeViewActive = false;
this.loadConfigContent();
if (layoutBtn) {
layoutBtn.classList.remove('active');
layoutBtn.title = "<%:Compare%>";
layoutBtn.innerHTML = this.SVG_COMPARE;
}
if (editor_help) editor_help.textContent = '<%:Press F11 for fullscreen, Esc to exit fullscreen, Ctrl+Mouse Wheel to zoom%>';
},
saveConfigContent: function() {
if (!this.editorInstance || !this.isModified) {
return;
}
var self = this;
var statusText = document.getElementById('config-editor-status-text');
var saveBtn = document.getElementById('config-editor-save');
statusText.textContent = '<%:Saving...%>';
saveBtn.disabled = true;
var content;
if (this.mergeViewActive && this.editorInstance && this.editorInstance.edit) {
content = this.editorInstance.edit.getValue();
} else {
content = this.editorInstance.getValue();
}
if (!content) {
saveBtn.disabled = false;
statusText.textContent = '<%:Save failed%>';
alert('<%:Config file content is empty%>');
return;
}
var formData = new FormData();
if (this.isOverwrite) {
formData.append('config_file', '/etc/openclash/custom/openclash_custom_overwrite.sh');
} else {
formData.append('config_file', this.currentConfigFile);
}
formData.append('content', content);
fetch('/cgi-bin/luci/admin/services/openclash/config_file_save', {
method: 'POST',
body: formData
})
.then(function(response) {
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
return response.json();
})
.then(function(data) {
saveBtn.disabled = false;
if (data.status === 'success') {
self.originalContent = content;
self.isModified = false;
self.updateSaveButtonState();
statusText.textContent = '<%:Saved successfully%>';
setTimeout(function() {
if (statusText && statusText.textContent === '<%:Saved successfully%>') {
statusText.textContent = '<%:Ready%>';
}
}, 3000);
} else {
statusText.textContent = '<%:Save failed%>';
alert('<%:Failed to save config file:%> ' + (data.message || '<%:Unknown error%>'));
setTimeout(function() {
if (statusText && statusText.textContent === '<%:Save failed%>') {
statusText.textContent = '<%:Ready%>';
}
}, 3000);
}
})
.catch(function(error) {
saveBtn.disabled = false;
statusText.textContent = '<%:Save failed%>';
alert('<%:Save config failed:%> ' + error.message);
setTimeout(function() {
if (statusText && statusText.textContent === '<%:Save failed%>') {
statusText.textContent = '<%:Ready%>';
}
}, 3000);
});
},
downloadConfigContent: function() {
if (!this.editorInstance) {
alert('<%:Editor not ready%>');
return;
}
var content;
if (this.mergeViewActive && this.editorInstance && this.editorInstance.edit) {
content = this.editorInstance.edit.getValue();
} else {
content = this.editorInstance.getValue();
}
var filename;
if (this.isOverwrite) {
filename = 'openclash_custom_overwrite.sh';
} else {
filename = this.formatDisplayName(this.currentConfigFile);
if (!filename.toLowerCase().endsWith('.yaml') && !filename.toLowerCase().endsWith('.yml')) {
filename += '.yaml';
}
}
try {
var blob = new Blob([content], { type: 'text/yaml;charset=utf-8' });
var url = window.URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
var statusText = document.getElementById('config-editor-status-text');
if (statusText) {
var originalText = statusText.textContent;
statusText.textContent = '<%:Download started%>';
setTimeout(function() {
if (statusText && statusText.textContent === '<%:Download started%>') {
statusText.textContent = originalText;
}
}, 2000);
}
} catch (error) {
alert('<%:Download failed:%> ' + error.message);
}
},
updateSaveButtonState: function() {
this.updateModeTabs();
},
closeEditor: function() {
if (this.isModified) {
var r = confirm('<%:You have unsaved changes. Are you sure you want to close?%>');
if (!r) {
return;
}
}
this.hideMergeView();
this.hide();
},
updateZoom: function(newZoom) {
this.currentZoom = newZoom;
if (this.editorInstance) {
var cmWrapper = this.modal.querySelector('.CodeMirror');
if (cmWrapper) {
this.zoomLevels.forEach(function(level) {
cmWrapper.classList.remove('zoom-' + level);
});
if (this.currentZoom !== 100) {
cmWrapper.classList.add('zoom-' + this.currentZoom);
}
this.editorInstance.refresh();
}
}
},
zoomIn: function() {
var currentIndex = this.zoomLevels.indexOf(this.currentZoom);
if (currentIndex < this.zoomLevels.length - 1) {
this.updateZoom(this.zoomLevels[currentIndex + 1]);
}
},
zoomOut: function() {
var currentIndex = this.zoomLevels.indexOf(this.currentZoom);
if (currentIndex > 0) {
this.updateZoom(this.zoomLevels[currentIndex - 1]);
}
},
resetZoom: function() {
this.updateZoom(100);
},
makeDraggable: function() {
var self = this;
var header = this.modal.querySelector('.config-editor-header');
var startX, startY, startLeft, startTop;
var isDragging = false;
header.addEventListener('mousedown', function(e) {
if (e.target.closest('.config-editor-actions')) {
return;
}
isDragging = true;
startX = e.clientX;
startY = e.clientY;
var rect = self.modal.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
self.modal.style.position = 'fixed';
self.modal.style.left = startLeft + 'px';
self.modal.style.top = startTop + 'px';
self.modal.style.margin = '0';
self.modal.style.transform = 'none';
self.modal.style.transition = 'none';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
});
function onMouseMove(e) {
if (!isDragging) return;
var deltaX = e.clientX - startX;
var deltaY = e.clientY - startY;
var newLeft = startLeft + deltaX;
var newTop = startTop + deltaY;
var modalRect = self.modal.getBoundingClientRect();
var maxLeft = window.innerWidth - modalRect.width;
var maxTop = window.innerHeight - modalRect.height;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
newTop = Math.max(0, Math.min(newTop, maxTop));
self.modal.style.left = newLeft + 'px';
self.modal.style.top = newTop + 'px';
}
function onMouseUp() {
isDragging = false;
setTimeout(function() {
self.modal.style.transition = 'all 0.3s ease';
}, 50);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
header.addEventListener('touchstart', function(e) {
if (e.target.closest('.config-editor-actions')) {
return;
}
if (e.touches.length !== 1) return;
isDragging = true;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
var rect = self.modal.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
self.modal.style.position = 'fixed';
self.modal.style.left = startLeft + 'px';
self.modal.style.top = startTop + 'px';
self.modal.style.margin = '0';
self.modal.style.transform = 'none';
self.modal.style.transition = 'none';
document.addEventListener('touchmove', onTouchMove, {passive: false});
document.addEventListener('touchend', onTouchEnd);
e.preventDefault();
});
function onTouchMove(e) {
if (!isDragging || e.touches.length !== 1) return;
var deltaX = e.touches[0].clientX - startX;
var deltaY = e.touches[0].clientY - startY;
var newLeft = startLeft + deltaX;
var newTop = startTop + deltaY;
var modalRect = self.modal.getBoundingClientRect();
var maxLeft = window.innerWidth - modalRect.width;
var maxTop = window.innerHeight - modalRect.height;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
newTop = Math.max(0, Math.min(newTop, maxTop));
self.modal.style.left = newLeft + 'px';
self.modal.style.top = newTop + 'px';
e.preventDefault();
}
function onTouchEnd() {
isDragging = false;
setTimeout(function() {
self.modal.style.transition = 'all 0.3s ease';
}, 50);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
}
},
makeResizable: function() {
var self = this;
var resizeHandle = document.getElementById('config-editor-resize-handle');
var isResizing = false;
var startX, startY, startWidth, startHeight;
resizeHandle.addEventListener('mousedown', function(e) {
isResizing = true;
startX = e.clientX;
startY = e.clientY;
var rect = self.modal.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
self.modal.style.transition = 'none';
self.modal.style.width = startWidth + 'px';
self.modal.style.height = startHeight + 'px';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
});
function onMouseMove(e) {
if (!isResizing) return;
var deltaX = e.clientX - startX;
var deltaY = e.clientY - startY;
var newWidth = Math.max(400, startWidth + deltaX);
var newHeight = Math.max(300, startHeight + deltaY);
var maxWidth = window.innerWidth * 0.98;
var maxHeight = window.innerHeight * 0.95;
newWidth = Math.min(newWidth, maxWidth);
newHeight = Math.min(newHeight, maxHeight);
self.modal.style.width = newWidth + 'px';
self.modal.style.height = newHeight + 'px';
if (self.editorInstance) {
requestAnimationFrame(function() {
if (self.mergeViewActive && self.editorInstance.editor) {
self.editorInstance.editor().refresh();
if (self.editorInstance.rightOriginal)
self.editorInstance.rightOriginal().refresh();
} else {
self.editorInstance.refresh();
}
});
}
}
function onMouseUp() {
isResizing = false;
self.modal.style.transition = 'all 0.3s ease';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if (self.editorInstance) {
setTimeout(function() {
if (self.mergeViewActive && self.editorInstance.editor) {
self.editorInstance.editor().refresh();
if (self.editorInstance.rightOriginal)
self.editorInstance.rightOriginal().refresh();
} else {
self.editorInstance.refresh();
}
}, 50);
}
}
resizeHandle.addEventListener('touchstart', function(e) {
if (e.touches.length !== 1) return;
isResizing = true;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
var rect = self.modal.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
self.modal.style.transition = 'none';
self.modal.style.width = startWidth + 'px';
self.modal.style.height = startHeight + 'px';
document.addEventListener('touchmove', onTouchMove, {passive: false});
document.addEventListener('touchend', onTouchEnd);
e.preventDefault();
});
function onTouchMove(e) {
if (!isResizing || e.touches.length !== 1) return;
var deltaX = e.touches[0].clientX - startX;
var deltaY = e.touches[0].clientY - startY;
var newWidth = Math.max(320, startWidth + deltaX);
var newHeight = Math.max(200, startHeight + deltaY);
var maxWidth = window.innerWidth * 0.98;
var maxHeight = window.innerHeight * 0.95;
newWidth = Math.min(newWidth, maxWidth);
newHeight = Math.min(newHeight, maxHeight);
self.modal.style.width = newWidth + 'px';
self.modal.style.height = newHeight + 'px';
if (self.editorInstance) {
requestAnimationFrame(function() {
if (self.mergeViewActive && self.editorInstance.editor) {
self.editorInstance.editor().refresh();
if (self.editorInstance.rightOriginal)
self.editorInstance.rightOriginal().refresh();
} else {
self.editorInstance.refresh();
}
});
}
e.preventDefault();
}
function onTouchEnd() {
isResizing = false;
self.modal.style.transition = 'all 0.3s ease';
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
if (self.editorInstance) {
setTimeout(function() {
if (self.mergeViewActive && self.editorInstance.editor) {
self.editorInstance.editor().refresh();
if (self.editorInstance.rightOriginal)
self.editorInstance.rightOriginal().refresh();
} else {
self.editorInstance.refresh();
}
}, 50);
}
}
}
};
document.addEventListener('DOMContentLoaded', function() {
ConfigEditor.init();
});
</script>