mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-23 00:17:16 +08:00
3512 lines
144 KiB
HTML
3512 lines
144 KiB
HTML
<style>
|
|
@font-face {
|
|
font-family: 'Twemoji Mozilla';
|
|
src: url('/luci-static/resources/openclash/fonts/TwemojiMozilla-flags-B12sb_Bp.woff2') format('woff2');
|
|
}
|
|
.oc[data-darkmode="true"] .config-editor-model .CodeMirror {
|
|
background: var(--bg-white);
|
|
color: var(--text-primary);
|
|
}
|
|
.oc[data-darkmode="true"] .config-editor-model .CodeMirror-gutters {
|
|
background: var(--bg-gray);
|
|
border-right: 1px solid var(--border-light);
|
|
}
|
|
.oc[data-darkmode="true"] .config-editor-model .CodeMirror-linenumber {
|
|
color: var(--text-secondary);
|
|
}
|
|
.oc[data-darkmode="true"] .config-editor-model .CodeMirror-scrollbar-filler,
|
|
.oc[data-darkmode="true"] .config-editor-model .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-model-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-model-overlay.show {
|
|
display: flex;
|
|
}
|
|
.oc .config-editor-model {
|
|
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-model.maximized {
|
|
width: 98vw !important;
|
|
height: 95vh !important;
|
|
max-width: none !important;
|
|
}
|
|
.oc .config-editor-model.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 #editTitle,
|
|
.oc .config-editor-title #config-file-name {
|
|
user-select: text;
|
|
-webkit-user-select: text;
|
|
cursor: text;
|
|
}
|
|
.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-model .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-model .CodeMirror.zoom-75 { font-size: 10.5px; }
|
|
.oc .config-editor-model .CodeMirror.zoom-90 { font-size: 12.6px; }
|
|
.oc .config-editor-model .CodeMirror.zoom-110 { font-size: 15.4px; }
|
|
.oc .config-editor-model .CodeMirror.zoom-125 { font-size: 17.5px; }
|
|
.oc .config-editor-model .CodeMirror.zoom-150 { font-size: 21px; }
|
|
.oc .config-editor-model .CodeMirror.zoom-200 { font-size: 28px; }
|
|
.oc .config-editor-model .CodeMirror, .oc .config-editor-model .CodeMirror-line {
|
|
font-family: 'Twemoji Mozilla', "Microsoft Yahei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB" !important;
|
|
}
|
|
.oc .config-editor-model .CodeMirror-hints.log {
|
|
font-family: 'Twemoji Mozilla', "Open Sans", "PingFangSC-Regular", "Microsoft Yahei", "WenQuanYi Micro Hei", "Helvetica Neue", "Helvetica", "Hiragino Sans GB", "sans-serif" !important;
|
|
}
|
|
#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;
|
|
}
|
|
.oc .update-row {
|
|
display: flex;
|
|
gap: 12px;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
}
|
|
.oc .update-row > .form-select-wrapper {
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
}
|
|
.oc .update-row .form-select {
|
|
width: 100%;
|
|
min-width: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
.oc .overwrite-config-dropdown {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
.oc .overwrite-config-dropdown-btn {
|
|
width: 100%;
|
|
min-height: 34px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 6px 10px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
}
|
|
.oc .overwrite-config-dropdown.form-select-wrapper {
|
|
width: 100%;
|
|
}
|
|
.oc .overwrite-config-dropdown-btn.form-select {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
-moz-appearance: none;
|
|
}
|
|
.oc .overwrite-config-dropdown-text {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
max-width: calc(100% - 20px);
|
|
text-align: left;
|
|
}
|
|
.oc .overwrite-config-dropdown-arrow {
|
|
margin-left: 8px;
|
|
width: 0;
|
|
height: 0;
|
|
border-left: 4px solid transparent;
|
|
border-right: 4px solid transparent;
|
|
border-top: 4px solid var(--text-secondary);
|
|
flex-shrink: 0;
|
|
}
|
|
.oc .overwrite-config-dropdown-panel {
|
|
display: none;
|
|
position: absolute;
|
|
top: calc(100% + 4px);
|
|
left: 0;
|
|
right: 0;
|
|
max-height: 220px;
|
|
overflow-y: auto;
|
|
border: 1px solid var(--border-light);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-white);
|
|
box-shadow: var(--shadow-md);
|
|
z-index: 10003;
|
|
}
|
|
.oc .overwrite-config-dropdown.open .overwrite-config-dropdown-panel {
|
|
display: block;
|
|
}
|
|
.oc .overwrite-config-option {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
padding: 8px 10px;
|
|
border-bottom: 1px solid var(--border-light);
|
|
background: var(--bg-white);
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
}
|
|
.oc .overwrite-config-option:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.oc .overwrite-config-option:hover {
|
|
background: var(--primary-color);
|
|
color: var(--select-hover);
|
|
}
|
|
.oc[data-darkmode="true"] .overwrite-config-option {
|
|
background: var(--bg-gray);
|
|
border-bottom-color: var(--border-light);
|
|
}
|
|
.oc[data-darkmode="true"] .overwrite-config-option:hover {
|
|
background: var(--primary-color);
|
|
color: var(--select-hover);
|
|
}
|
|
.oc .overwrite-config-option:hover .overwrite-config-option-state {
|
|
border-color: var(--select-hover);
|
|
}
|
|
.oc[data-darkmode="true"] .overwrite-config-option-state {
|
|
background: var(--bg-white);
|
|
border-color: var(--border-color);
|
|
}
|
|
.oc .overwrite-config-option.disabled-by-all {
|
|
cursor: not-allowed;
|
|
opacity: 0.55;
|
|
background: var(--bg-gray);
|
|
color: var(--text-secondary);
|
|
}
|
|
.oc .overwrite-config-option.disabled-by-all:hover {
|
|
background: var(--bg-gray);
|
|
color: var(--text-secondary);
|
|
}
|
|
.oc .overwrite-config-option.disabled-by-all .overwrite-config-option-state {
|
|
border-color: var(--border-light);
|
|
color: transparent;
|
|
}
|
|
.oc .overwrite-config-option-left {
|
|
display: flex;
|
|
align-items: center;
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.oc .overwrite-config-option-left input[type="checkbox"] {
|
|
position: absolute;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
.oc .overwrite-config-option-name {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-size: 13px;
|
|
}
|
|
.oc .overwrite-config-option-state {
|
|
flex-shrink: 0;
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 3px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: transparent;
|
|
background: var(--bg-white);
|
|
font-size: 12px;
|
|
line-height: 1;
|
|
}
|
|
.oc .overwrite-config-option.selected .overwrite-config-option-state {
|
|
border-color: var(--primary-color);
|
|
background: var(--primary-color);
|
|
color: var(--select-hover);
|
|
}
|
|
.oc .overwrite-config-dropdown.disabled .overwrite-config-dropdown-btn {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
.oc .overwrite-card-row {
|
|
display: flex;
|
|
flex-wrap: nowrap;
|
|
overflow-x: auto;
|
|
gap: 12px;
|
|
padding: 8px;
|
|
max-width: 100%;
|
|
min-width: 0;
|
|
align-items: stretch;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--primary-color) var(--bg-gray);
|
|
}
|
|
.oc .overwrite-card-row::-webkit-scrollbar {
|
|
height: 8px;
|
|
}
|
|
.oc .overwrite-card-row::-webkit-scrollbar-thumb {
|
|
background: var(--primary-color);
|
|
border-radius: 4px;
|
|
}
|
|
.oc .overwrite-card-row::-webkit-scrollbar-track {
|
|
background: var(--bg-gray);
|
|
border-radius: 4px;
|
|
}
|
|
.oc .sub-card.overwrite-item {
|
|
width: 190px;
|
|
max-width: 220px;
|
|
flex: 0 0 auto;
|
|
box-sizing: border-box;
|
|
gap: 12px;
|
|
height: 75px;
|
|
}
|
|
.oc .sub-card.overwrite-item,
|
|
.oc .sub-card.overwrite-item:active,
|
|
.oc .sub-card.overwrite-item:focus {
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
.oc .overwrite-drag-line {
|
|
display: block;
|
|
min-width: 4px;
|
|
width: 4px;
|
|
background: var(--primary-color);
|
|
border-radius: 2px;
|
|
margin: 0 2px;
|
|
align-self: stretch;
|
|
height: auto;
|
|
}
|
|
.oc .oc-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 36px;
|
|
height: 20px;
|
|
vertical-align: middle;
|
|
}
|
|
.oc .oc-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.oc .oc-switch-slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background-color: #ccc;
|
|
border-radius: 20px;
|
|
transition: .2s;
|
|
}
|
|
.oc .oc-switch-slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 16px; width: 16px;
|
|
left: 2px; bottom: 2px;
|
|
background-color: white;
|
|
border-radius: 50%;
|
|
transition: .2s;
|
|
}
|
|
.oc .oc-switch input:checked + .oc-switch-slider {
|
|
background-color: var(--primary-color, #2196F3);
|
|
}
|
|
.oc .oc-switch input:checked + .oc-switch-slider:before {
|
|
transform: translateX(16px);
|
|
}
|
|
.overwrite-add-icon {
|
|
font-size: 32px;
|
|
text-align: center;
|
|
color: var(--primary-color);
|
|
}
|
|
.overwrite-title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
}
|
|
.overwrite-title {
|
|
font-weight: bold;
|
|
font-size: 15px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
max-width: 70%;
|
|
}
|
|
.overwrite-info {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
.overwrite-refresh-btn {
|
|
position: absolute;
|
|
right: 72px;
|
|
bottom: 8px;
|
|
}
|
|
.overwrite-gear-btn {
|
|
position: absolute;
|
|
right: 40px;
|
|
bottom: 8px;
|
|
}
|
|
.overwrite-del-btn {
|
|
position: absolute;
|
|
right: 8px;
|
|
bottom: 8px;
|
|
}
|
|
.sub-card.overwrite-item.active {
|
|
border-color: var(--primary-color);
|
|
box-shadow: 0 0 0 2px var(--primary-color);
|
|
}
|
|
.sub-card.overwrite-item.dragging {
|
|
opacity: 0.5;
|
|
z-index: 1000;
|
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
|
transition: none !important;
|
|
}
|
|
@media screen and (max-width: 768px) {
|
|
.oc .config-editor-model {
|
|
width: 95vw;
|
|
height: 80vh;
|
|
min-width: 320px;
|
|
}
|
|
.oc .config-editor-actions {
|
|
gap: 5px;
|
|
}
|
|
.oc .overwrite-card-row {
|
|
gap: 6px;
|
|
}
|
|
.oc .sub-card.overwrite-item {
|
|
width: 185px;
|
|
max-width: 200px;
|
|
gap: 10px;
|
|
}
|
|
.oc .overwrite-drag-line {
|
|
margin: -2px;
|
|
width: 2px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="oc">
|
|
<div class="config-editor-model-overlay" id="config-editor-overlay">
|
|
<div class="config-editor-model" id="config-editor-model">
|
|
<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 id="overwrite-card-bar" style="display:none;position:relative;margin:12px;">
|
|
<div class="overwrite-card-row" id="overwrite-card-list"></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/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/shell/shell.js"></script>
|
|
<script src="/luci-static/resources/openclash/mode/properties/properties.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 levelTranslations = {
|
|
'info': '<%:Info%>',
|
|
'warning': '<%:Warning%>',
|
|
'error': '<%:Error%>',
|
|
'debug': '<%:Debug%>',
|
|
'tip': '<%:Tip%>',
|
|
'watchdog': '<%:Watchdog%>',
|
|
'fatal': '<%:Fatal%>'
|
|
};
|
|
|
|
var ConfigEditor = {
|
|
overlay: null,
|
|
model: 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>
|
|
`,
|
|
overwriteFiles: [],
|
|
overwriteSubInfo: {},
|
|
overwriteCardBar: null,
|
|
overwriteDrag: {
|
|
dragging: null,
|
|
startIndex: null,
|
|
touchTimer: null,
|
|
isTouchDragging: false,
|
|
touchDraggingMoved: false,
|
|
touchDraggingCard: null,
|
|
startTouch: null
|
|
},
|
|
|
|
init: function() {
|
|
this.overlay = document.getElementById('config-editor-overlay');
|
|
this.model = document.getElementById('config-editor-model');
|
|
this.overwriteCardBar = document.getElementById('overwrite-card-bar');
|
|
this.mergeViewActive = false;
|
|
|
|
if (!this.overlay || !this.model) {
|
|
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;
|
|
this.currentViewMode = 'original';
|
|
this.currentConfigFile = '';
|
|
this.originalContent = '';
|
|
this.runtimeContent = '';
|
|
this.isModified = false;
|
|
this.mergeViewActive = false;
|
|
|
|
if (this.editorInstance) {
|
|
if (this.editorInstance.toTextArea) this.editorInstance.toTextArea();
|
|
this.editorInstance = null;
|
|
}
|
|
var textarea = document.getElementById('config-editor-textarea');
|
|
if (textarea) {
|
|
textarea.value = '';
|
|
textarea.style.display = 'none';
|
|
}
|
|
var loadingDiv = document.getElementById('config-editor-loading');
|
|
if (loadingDiv) loadingDiv.style.display = 'flex';
|
|
var mergeview = document.getElementById('config-mergeview-container');
|
|
if (mergeview) mergeview.style.display = 'none';
|
|
|
|
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-flex';
|
|
|
|
if (!configFile) {
|
|
alert('<%:Please select a config file first%>');
|
|
return;
|
|
}
|
|
|
|
this.currentConfigFile = configFile;
|
|
this.overlay.classList.add('show');
|
|
|
|
this.model.classList.remove('maximized');
|
|
this.model.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.hideMergeView();
|
|
this.updateModeTabs();
|
|
this.loadConfigContent();
|
|
},
|
|
|
|
showOverwrite: function() {
|
|
this.isOverwrite = true;
|
|
this.currentViewMode = 'original';
|
|
this.originalContent = '';
|
|
this.runtimeContent = '';
|
|
this.isModified = false;
|
|
this.mergeViewActive = false;
|
|
|
|
if (this.editorInstance) {
|
|
if (this.editorInstance.toTextArea) this.editorInstance.toTextArea();
|
|
this.editorInstance = null;
|
|
}
|
|
var textarea = document.getElementById('config-editor-textarea');
|
|
if (textarea) {
|
|
textarea.value = '';
|
|
textarea.style.display = 'none';
|
|
}
|
|
var loadingDiv = document.getElementById('config-editor-loading');
|
|
if (loadingDiv) loadingDiv.style.display = 'flex';
|
|
var mergeview = document.getElementById('config-mergeview-container');
|
|
if (mergeview) mergeview.style.display = 'none';
|
|
|
|
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';
|
|
|
|
if (!this.currentConfigFile) {
|
|
this.currentConfigFile = '/etc/openclash/custom/openclash_custom_overwrite.sh';
|
|
}
|
|
this.overlay.classList.add('show');
|
|
|
|
this.model.classList.remove('maximized');
|
|
this.model.classList.remove('minimized');
|
|
|
|
var editTitle = document.getElementById('editTitle');
|
|
if (editTitle) {
|
|
editTitle.textContent = '<%:Overwrite Edit%>: ';
|
|
}
|
|
|
|
if (this.overwriteCardBar) this.overwriteCardBar.style.display = 'block';
|
|
this.loadOverwriteFiles();
|
|
|
|
var configNameElement = document.getElementById('config-file-name');
|
|
if (configNameElement) {
|
|
configNameElement.textContent = this.formatDisplayName(this.currentConfigFile);
|
|
}
|
|
|
|
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.overwriteCardBar.style.display = 'none';
|
|
if (!this.isOverwrite) {
|
|
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');
|
|
var layoutBtn = document.getElementById('config-editor-layout');
|
|
|
|
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 (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 = !this.isModified;
|
|
saveBtn.style.opacity = this.isModified ? '1' : '0.5';
|
|
saveBtn.style.cursor = this.isModified ? 'pointer' : 'not-allowed';
|
|
}
|
|
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) {
|
|
var file = this.currentConfigFile || '/etc/openclash/custom/openclash_custom_overwrite.sh';
|
|
url = '/cgi-bin/luci/admin/services/openclash/config_file_read?config_file=' + encodeURIComponent(file);
|
|
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
mode = "text/yaml";
|
|
} else if (file.endsWith('.sh')) {
|
|
mode = "text/x-sh";
|
|
} else {
|
|
mode = "text/x-properties";
|
|
}
|
|
} 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, mode, self.isOverwrite ? false : 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', this.currentConfigFile || '/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 = this.formatDisplayName(this.currentConfigFile);
|
|
if (!filename) 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();
|
|
},
|
|
|
|
loadOverwriteFiles: function() {
|
|
var self = this;
|
|
fetch('/cgi-bin/luci/admin/services/openclash/overwrite_file_list')
|
|
.then(r => r.json())
|
|
.then(function(data) {
|
|
var files = [];
|
|
if (data.overwrite_files) {
|
|
files = data.overwrite_files.filter(function(f) {
|
|
return (f.path && (f.path.indexOf('/etc/openclash/overwrite/') === 0 || f.path === '/etc/openclash/custom/openclash_custom_overwrite.sh'));
|
|
});
|
|
}
|
|
files = files.filter(f => f.path !== '/etc/openclash/custom/openclash_custom_overwrite.sh');
|
|
files.unshift({
|
|
path: '/etc/openclash/custom/openclash_custom_overwrite.sh',
|
|
name: 'openclash_custom_overwrite.sh'
|
|
});
|
|
self.overwriteFiles = files;
|
|
self.loadOverwriteSubInfo();
|
|
});
|
|
},
|
|
|
|
loadOverwriteSubInfo: function() {
|
|
var self = this;
|
|
fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info')
|
|
.then(r => r.json())
|
|
.then(function(data) {
|
|
if (data.status === 'success') {
|
|
self.overwriteSubInfo = (data && data.data) ? data.data : {};
|
|
Object.keys(self.overwriteSubInfo).forEach(function(key) {
|
|
var subInfo = self.overwriteSubInfo[key] || {};
|
|
subInfo.config = self.parseOverwriteConfigValue(subInfo.config);
|
|
self.overwriteSubInfo[key] = subInfo;
|
|
});
|
|
self.overwriteFiles.sort(function(a, b) {
|
|
var an = a.name || (a.path ? a.path.split('/').pop() : '');
|
|
var bn = b.name || (b.path ? b.path.split('/').pop() : '');
|
|
var ao = self.overwriteSubInfo[an] && self.overwriteSubInfo[an].order ? parseInt(self.overwriteSubInfo[an].order) : 0;
|
|
var bo = self.overwriteSubInfo[bn] && self.overwriteSubInfo[bn].order ? parseInt(self.overwriteSubInfo[bn].order) : 0;
|
|
return ao - bo;
|
|
});
|
|
self.renderOverwriteCards();
|
|
} else {
|
|
var statusText = document.getElementById('overwrite-edit-status-text');
|
|
statusText.textContent = '<%:Failed to save subscription info:%> ' + (data.message || '');
|
|
}
|
|
});
|
|
},
|
|
|
|
renderOverwriteCards: function() {
|
|
var self = this;
|
|
var bar = this.overwriteCardBar;
|
|
if (!bar) return;
|
|
var list = document.getElementById('overwrite-card-list');
|
|
if (!list) return;
|
|
list.innerHTML = '';
|
|
|
|
var statusText = document.getElementById('config-editor-status-text');
|
|
|
|
var addCard = document.createElement('div');
|
|
addCard.className = 'sub-card overwrite-item';
|
|
addCard.style.cursor = 'pointer';
|
|
addCard.innerHTML = '<div class="overwrite-add-icon">+</div>';
|
|
addCard.title = '<%:Add New File%>';
|
|
addCard.onclick = function() {
|
|
ConfigEditor.showAddOverwritemodel();
|
|
};
|
|
list.appendChild(addCard);
|
|
|
|
var customIdx = self.overwriteFiles.findIndex(f => (f.name === 'openclash_custom_overwrite.sh' || (f.path && f.path.endsWith('/openclash_custom_overwrite.sh'))));
|
|
var customFile = customIdx !== -1 ? self.overwriteFiles[customIdx] : null;
|
|
|
|
if (customFile) {
|
|
var name = customFile.name || (customFile.path ? customFile.path.split('/').pop() : '');
|
|
var card = document.createElement('div');
|
|
card.className = 'sub-card overwrite-item';
|
|
card.dataset.file = customFile.path;
|
|
card.dataset.index = 'custom';
|
|
|
|
if (self.currentConfigFile === customFile.path) {
|
|
card.classList.add('active');
|
|
}
|
|
|
|
var titleRow = document.createElement('div');
|
|
titleRow.className = 'overwrite-title-row';
|
|
|
|
var title = document.createElement('div');
|
|
title.textContent = name;
|
|
title.className = 'overwrite-title';
|
|
title.title = name;
|
|
|
|
titleRow.appendChild(title);
|
|
card.appendChild(titleRow);
|
|
|
|
var sub = self.overwriteSubInfo[name] || {};
|
|
var info = document.createElement('div');
|
|
info.className = 'overwrite-info';
|
|
info.textContent = sub.url ? '<%:Subscription%>' : '<%:Local File%>';
|
|
card.appendChild(info);
|
|
|
|
card.onclick = function(e) {
|
|
if (self.currentConfigFile !== customFile.path) {
|
|
self.currentConfigFile = customFile.path;
|
|
self.loadConfigContent();
|
|
self.renderOverwriteCards();
|
|
var configNameElement = document.getElementById('config-file-name');
|
|
if (configNameElement) {
|
|
configNameElement.textContent = self.formatDisplayName(customFile.path);
|
|
}
|
|
}
|
|
};
|
|
|
|
card.draggable = false;
|
|
|
|
list.appendChild(card);
|
|
}
|
|
|
|
var files = self.overwriteFiles.filter((f, i) => i !== customIdx);
|
|
self.overwriteFileCardCount = files.length;
|
|
files.forEach(function(file, idx) {
|
|
var name = file.name || (file.path ? file.path.split('/').pop() : '');
|
|
var card = document.createElement('div');
|
|
card.className = 'sub-card overwrite-item';
|
|
card.dataset.file = file.path;
|
|
card.dataset.index = idx;
|
|
|
|
if (self.currentConfigFile === file.path) {
|
|
card.classList.add('active');
|
|
}
|
|
|
|
var titleRow = document.createElement('div');
|
|
titleRow.className = 'overwrite-title-row';
|
|
|
|
var title = document.createElement('div');
|
|
title.textContent = name;
|
|
title.className = 'overwrite-title';
|
|
card.appendChild(title);
|
|
|
|
var sub = self.overwriteSubInfo[name] || {};
|
|
var enable = typeof sub.enable !== 'undefined' ? sub.enable : 0;
|
|
var switchLabel = document.createElement('label');
|
|
switchLabel.className = 'oc-switch';
|
|
|
|
var switchInput = document.createElement('input');
|
|
switchInput.type = 'checkbox';
|
|
switchInput.checked = enable == 1 ? true : false;
|
|
switchInput.onchange = function(e) {
|
|
e.stopPropagation();
|
|
var newEnable = switchInput.checked ? 1 : 0;
|
|
var formData = new FormData();
|
|
formData.append('filename', name);
|
|
formData.append('type', sub.type || 'file');
|
|
formData.append('url', sub.url || '');
|
|
formData.append('update_days', sub.update_days || '');
|
|
formData.append('update_hour', sub.update_hour || '');
|
|
formData.append('order', (typeof sub.order !== 'undefined' && sub.order !== null && sub.order !== '') ? sub.order : idx);
|
|
formData.append('param', sub.param || '');
|
|
formData.append('config', self.parseOverwriteConfigValue(sub.config).join('\n'));
|
|
formData.append('enable', newEnable);
|
|
fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
|
|
method: 'POST',
|
|
body: formData
|
|
}).then(r=>r.json()).then(function(data){
|
|
if (data.status === 'success') {
|
|
self.overwriteSubInfo[name] = self.overwriteSubInfo[name] || {};
|
|
self.overwriteSubInfo[name].enable = newEnable;
|
|
} else {
|
|
statusText.textContent = '<%:Failed to update enable status%>';
|
|
switchInput.checked = !switchInput.checked;
|
|
}
|
|
}).catch(function(){
|
|
statusText.textContent = '<%:Failed to update enable status%>';
|
|
switchInput.checked = !switchInput.checked;
|
|
});
|
|
};
|
|
switchInput.onclick = function(e) { e.stopPropagation(); };
|
|
switchInput.onmousedown = function(e) { e.stopPropagation(); };
|
|
switchInput.ontouchstart = function(e) { e.stopPropagation(); };
|
|
switchLabel.onclick = function(e) { e.stopPropagation(); };
|
|
switchLabel.onmousedown = function(e) { e.stopPropagation(); };
|
|
switchLabel.ontouchstart = function(e) { e.stopPropagation(); };
|
|
|
|
var switchSpan = document.createElement('span');
|
|
switchSpan.className = 'oc-switch-slider';
|
|
|
|
switchLabel.appendChild(switchInput);
|
|
switchLabel.appendChild(switchSpan);
|
|
|
|
titleRow.appendChild(title);
|
|
titleRow.appendChild(switchLabel);
|
|
card.appendChild(titleRow);
|
|
|
|
var info = document.createElement('div');
|
|
info.className = 'overwrite-info';
|
|
info.textContent = sub.url ? '<%:Subscription%>' : '<%:Local File%>';
|
|
card.appendChild(info);
|
|
|
|
if (sub.type === 'http') {
|
|
var refresh = document.createElement('button');
|
|
refresh.className = 'icon-btn overwrite-refresh-btn';
|
|
refresh.type = 'button';
|
|
refresh.title = '<%:Refresh Subscription%>';
|
|
refresh.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.13-3.36L23 10"/><path d="M1 14l4.35 4.35A9 9 0 0 0 20.49 15"/></svg>';
|
|
refresh.onclick = function(e) {
|
|
e.stopPropagation();
|
|
var statusText = document.getElementById('config-editor-status-text');
|
|
if (statusText) {
|
|
statusText.textContent = '<%:Refreshing subscription...%>';
|
|
}
|
|
refresh.disabled = true;
|
|
var formData = new FormData();
|
|
formData.append('filename', name);
|
|
formData.append('type', sub.type || 'http');
|
|
formData.append('url', sub.url || '');
|
|
formData.append('update_days', sub.update_days || '');
|
|
formData.append('update_hour', sub.update_hour || '');
|
|
formData.append('order', (typeof sub.order !== 'undefined' && sub.order !== null && sub.order !== '') ? sub.order : idx);
|
|
formData.append('param', sub.param || '');
|
|
formData.append('config', self.parseOverwriteConfigValue(sub.config).join('\n'));
|
|
formData.append('enable', typeof sub.enable !== 'undefined' ? sub.enable : 1);
|
|
fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
|
|
method: 'POST',
|
|
body: formData
|
|
}).then(r=>r.json()).then(function(data){
|
|
refresh.disabled = false;
|
|
if (statusText) {
|
|
if (data.status === 'success') {
|
|
statusText.textContent = '<%:Subscription refreshed successfully%>';
|
|
} else {
|
|
statusText.textContent = '<%:Subscription refresh failed%>: ' + (data.message || '');
|
|
}
|
|
}
|
|
setTimeout(function(){
|
|
if (statusText && (statusText.textContent.startsWith('<%:Subscription refreshed successfully%>') || statusText.textContent.startsWith('<%:Subscription refresh failed%>'))) {
|
|
statusText.textContent = '<%:Ready%>';
|
|
}
|
|
}, 2000);
|
|
}).catch(function(){
|
|
refresh.disabled = false;
|
|
if (statusText) statusText.textContent = '<%:Subscription refresh failed%>';
|
|
setTimeout(function(){
|
|
if (statusText && statusText.textContent.startsWith('<%:Subscription refresh failed%>')) {
|
|
statusText.textContent = '<%:Ready%>';
|
|
}
|
|
}, 2000);
|
|
});
|
|
setTimeout(function(){
|
|
self.loadConfigContent();
|
|
}, 1000);
|
|
};
|
|
card.appendChild(refresh);
|
|
}
|
|
|
|
var gear = document.createElement('button');
|
|
gear.className = 'icon-btn overwrite-gear-btn';
|
|
gear.type = 'button';
|
|
gear.title = '<%:Edit Module Info%>';
|
|
gear.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.09a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.09a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.09a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
|
|
gear.onclick = function(e) {
|
|
e.stopPropagation();
|
|
ConfigEditor.showOverwriteSubmodel(name, sub, file.path);
|
|
};
|
|
card.appendChild(gear);
|
|
|
|
var del = document.createElement('button');
|
|
del.className = 'icon-btn overwrite-del-btn';
|
|
del.type = 'button';
|
|
del.title = '<%:Delete%>';
|
|
del.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></svg>';
|
|
del.onclick = function(e) {
|
|
e.stopPropagation();
|
|
if (confirm('<%:Are you sure you want to delete this file and its subscription info?%>')) {
|
|
fetch('/cgi-bin/luci/admin/services/openclash/delete_overwrite_file', {
|
|
method: 'POST',
|
|
body: new URLSearchParams({ filename: name })
|
|
})
|
|
.then(r => r.json())
|
|
.then(function(data) {
|
|
if (data.status === 'success') {
|
|
self.loadOverwriteFiles();
|
|
setTimeout(function() {
|
|
var files = self.overwriteFiles.filter(f => f.name !== name && f.name !== 'openclash_custom_overwrite.sh');
|
|
if (files.length > 0) {
|
|
self.currentConfigFile = files[0].path;
|
|
self.loadConfigContent();
|
|
self.renderOverwriteCards();
|
|
var configNameElement = document.getElementById('config-file-name');
|
|
if (configNameElement) {
|
|
configNameElement.textContent = self.formatDisplayName(files[0].path);
|
|
}
|
|
} else {
|
|
self.currentConfigFile = '/etc/openclash/custom/openclash_custom_overwrite.sh';
|
|
self.loadConfigContent();
|
|
self.renderOverwriteCards();
|
|
var configNameElement = document.getElementById('config-file-name');
|
|
if (configNameElement) {
|
|
configNameElement.textContent = self.formatDisplayName(self.currentConfigFile);
|
|
}
|
|
}
|
|
}, 300);
|
|
} else {
|
|
statusText.textContent = '<%:Delete failed%>: ' + (data.message || '');
|
|
}
|
|
});
|
|
}
|
|
};
|
|
card.appendChild(del);
|
|
|
|
card.onclick = function(e) {
|
|
if (self.currentConfigFile !== file.path) {
|
|
self.currentConfigFile = file.path;
|
|
self.loadConfigContent();
|
|
self.renderOverwriteCards();
|
|
var configNameElement = document.getElementById('config-file-name');
|
|
if (configNameElement) {
|
|
configNameElement.textContent = self.formatDisplayName(file.path);
|
|
}
|
|
}
|
|
};
|
|
|
|
card.draggable = true;
|
|
card.addEventListener('dragstart', function(e) {
|
|
self.overwriteDrag.dragging = card;
|
|
self.overwriteDrag.startIndex = idx;
|
|
card.classList.add('dragging');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
});
|
|
card.addEventListener('dragend', function(e) {
|
|
card.classList.remove('dragging');
|
|
self.overwriteDrag.dragging = null;
|
|
self.overwriteDrag.startIndex = null;
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(line => line.remove());
|
|
});
|
|
card.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
|
|
var rect = card.getBoundingClientRect();
|
|
var listRect = list.getBoundingClientRect();
|
|
var cardIndex = parseInt(card.dataset.index);
|
|
|
|
var mid = rect.left + rect.width * 0.5;
|
|
|
|
let insertPos = null;
|
|
if (e.clientX < mid) {
|
|
insertPos = 'before';
|
|
} else if (e.clientX >= mid) {
|
|
insertPos = 'after';
|
|
}
|
|
|
|
let currentLine = list.querySelector('.overwrite-drag-line');
|
|
|
|
if (!currentLine && insertPos) {
|
|
var line = document.createElement('div');
|
|
line.className = 'overwrite-drag-line';
|
|
if (insertPos === 'before') {
|
|
list.insertBefore(line, card);
|
|
} else if (insertPos === 'after') {
|
|
if (card.nextSibling) {
|
|
list.insertBefore(line, card.nextSibling);
|
|
} else {
|
|
list.appendChild(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
var scrollZone = 40;
|
|
if (e.clientX - listRect.left < scrollZone) {
|
|
list.scrollLeft -= 5;
|
|
} else if (listRect.right - e.clientX < scrollZone) {
|
|
list.scrollLeft += 5;
|
|
}
|
|
});
|
|
|
|
card.addEventListener('drop', function(e) {
|
|
e.preventDefault();
|
|
var dragging = self.overwriteDrag.dragging;
|
|
if (!dragging || dragging === card) return;
|
|
var from = parseInt(dragging.dataset.index);
|
|
var to = parseInt(card.dataset.index);
|
|
var rect = card.getBoundingClientRect();
|
|
var midX = rect.left + rect.width / 2;
|
|
if (e.clientX < midX) {
|
|
} else {
|
|
to++;
|
|
}
|
|
if (from === to || from + 1 === to) {
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(line => line.remove());
|
|
self.saveOverwriteSort();
|
|
return;
|
|
}
|
|
var arr = files;
|
|
var moved = arr.splice(from, 1)[0];
|
|
arr.splice(to > from ? to - 1 : to, 0, moved);
|
|
var newArr = [];
|
|
if (customFile) newArr.push(customFile);
|
|
newArr = newArr.concat(arr);
|
|
self.overwriteFiles = newArr;
|
|
self.renderOverwriteCards();
|
|
self.saveOverwriteSort();
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(line => line.remove());
|
|
});
|
|
|
|
card.addEventListener('touchstart', function(e) {
|
|
if (card.dataset.index === 'custom') return;
|
|
if (e.touches.length !== 1) return;
|
|
|
|
var target = e.target;
|
|
var isButton = target.closest('.overwrite-refresh-btn, .overwrite-gear-btn, .overwrite-del-btn, .oc-switch');
|
|
if (isButton) return;
|
|
|
|
self.overwriteDrag.touchDraggingCard = card;
|
|
self.overwriteDrag.touchDraggingMoved = false;
|
|
self.overwriteDrag.isTouchDragging = false;
|
|
|
|
self.overwriteDrag.startTouch = {
|
|
clientX: e.touches[0].clientX,
|
|
clientY: e.touches[0].clientY
|
|
};
|
|
|
|
self.overwriteDrag.touchTimer = setTimeout(function() {
|
|
if (!self.overwriteDrag.touchDraggingMoved && self.overwriteDrag.touchDraggingCard === card) {
|
|
self.overwriteDrag.isTouchDragging = true;
|
|
self.overwriteDrag.dragging = card;
|
|
self.overwriteDrag.startIndex = parseInt(card.dataset.index);
|
|
card.classList.add('dragging');
|
|
|
|
if (navigator.vibrate) {
|
|
navigator.vibrate(50);
|
|
}
|
|
}
|
|
}, 500);
|
|
});
|
|
|
|
card.addEventListener('touchmove', function(e) {
|
|
if (card.dataset.index === 'custom') return;
|
|
if (e.touches.length !== 1) return;
|
|
|
|
var touch = e.touches[0];
|
|
|
|
if (!self.overwriteDrag.touchDraggingMoved && self.overwriteDrag.startTouch) {
|
|
var dx = Math.abs(touch.clientX - self.overwriteDrag.startTouch.clientX);
|
|
var dy = Math.abs(touch.clientY - self.overwriteDrag.startTouch.clientY);
|
|
if (dx > 15 || dy > 15) {
|
|
self.overwriteDrag.touchDraggingMoved = true;
|
|
}
|
|
}
|
|
|
|
if (self.overwriteDrag.isTouchDragging) {
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(line => line.remove());
|
|
|
|
var elementBelow = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
var targetCard = null;
|
|
|
|
if (elementBelow) {
|
|
targetCard = elementBelow.closest('.sub-card.overwrite-item');
|
|
}
|
|
|
|
if (targetCard && targetCard !== card && targetCard.dataset.index !== 'custom') {
|
|
var rect = targetCard.getBoundingClientRect();
|
|
var midX = rect.left + rect.width / 2;
|
|
|
|
var line = document.createElement('div');
|
|
line.className = 'overwrite-drag-line';
|
|
|
|
if (touch.clientX < midX) {
|
|
list.insertBefore(line, targetCard);
|
|
} else {
|
|
if (targetCard.nextSibling) {
|
|
list.insertBefore(line, targetCard.nextSibling);
|
|
} else {
|
|
list.appendChild(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
var listRect = list.getBoundingClientRect();
|
|
var scrollZone = 60;
|
|
if (touch.clientX - listRect.left < scrollZone && list.scrollLeft > 0) {
|
|
list.scrollLeft -= 10;
|
|
} else if (listRect.right - touch.clientX < scrollZone) {
|
|
list.scrollLeft += 10;
|
|
}
|
|
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
card.addEventListener('touchend', function(e) {
|
|
if (self.overwriteDrag.touchTimer) {
|
|
clearTimeout(self.overwriteDrag.touchTimer);
|
|
self.overwriteDrag.touchTimer = null;
|
|
}
|
|
|
|
var wasInDraggingMode = self.overwriteDrag.isTouchDragging;
|
|
|
|
if (wasInDraggingMode) {
|
|
card.classList.remove('dragging');
|
|
|
|
var line = list.querySelector('.overwrite-drag-line');
|
|
if (line) {
|
|
var from = parseInt(card.dataset.index);
|
|
var allCards = Array.from(list.querySelectorAll('.sub-card.overwrite-item'));
|
|
var fileCards = allCards.filter(c => c.dataset && typeof c.dataset.index !== 'undefined' && c.dataset.index !== 'custom');
|
|
var lineRect = line.getBoundingClientRect();
|
|
var lineCenterX = lineRect.left + lineRect.width / 2;
|
|
|
|
var to = fileCards.length;
|
|
for (var i = 0; i < fileCards.length; i++) {
|
|
var rect = fileCards[i].getBoundingClientRect();
|
|
var cardMid = rect.left + rect.width / 2;
|
|
if (lineCenterX < cardMid) {
|
|
to = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!(from === to || from + 1 === to)) {
|
|
var arr = self.overwriteFiles.filter(function(f){ return f.name !== 'openclash_custom_overwrite.sh'; });
|
|
var customFile = self.overwriteFiles.find(function(f){ return f.name === 'openclash_custom_overwrite.sh'; });
|
|
var moved = arr.splice(from, 1)[0];
|
|
arr.splice(to > from ? to - 1 : to, 0, moved);
|
|
var newArr = [];
|
|
if (customFile) newArr.push(customFile);
|
|
newArr = newArr.concat(arr);
|
|
self.overwriteFiles = newArr;
|
|
self.renderOverwriteCards();
|
|
self.saveOverwriteSort();
|
|
}
|
|
}
|
|
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(function(l){ l.remove(); });
|
|
|
|
self.overwriteDrag.dragging = null;
|
|
self.overwriteDrag.startIndex = null;
|
|
self.overwriteDrag.isTouchDragging = false;
|
|
|
|
if (self.overwriteDrag.touchDraggingMoved) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
}
|
|
|
|
self.overwriteDrag.touchDraggingMoved = false;
|
|
self.overwriteDrag.touchDraggingCard = null;
|
|
self.overwriteDrag.startTouch = null;
|
|
});
|
|
|
|
card.addEventListener('touchcancel', function() {
|
|
if (self.overwriteDrag.touchTimer) {
|
|
clearTimeout(self.overwriteDrag.touchTimer);
|
|
self.overwriteDrag.touchTimer = null;
|
|
}
|
|
|
|
if (self.overwriteDrag.isTouchDragging) {
|
|
card.classList.remove('dragging');
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(line => line.remove());
|
|
self.overwriteDrag.dragging = null;
|
|
self.overwriteDrag.startIndex = null;
|
|
self.saveOverwriteSort();
|
|
}
|
|
|
|
self.overwriteDrag.isTouchDragging = false;
|
|
self.overwriteDrag.touchDraggingMoved = false;
|
|
self.overwriteDrag.touchDraggingCard = null;
|
|
self.overwriteDrag.startTouch = null;
|
|
});
|
|
|
|
list.appendChild(card);
|
|
});
|
|
|
|
list.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
|
|
const cards = Array.from(list.querySelectorAll('.sub-card.overwrite-item'));
|
|
if (cards.length === 0) return;
|
|
|
|
const realCards = cards.slice(2);
|
|
if (realCards.length === 0) return;
|
|
|
|
const x = e.clientX;
|
|
|
|
let insertBefore = null;
|
|
for (let i = 0; i < realCards.length; i++) {
|
|
const rect = realCards[i].getBoundingClientRect();
|
|
if (x < rect.left + rect.width / 2) {
|
|
insertBefore = realCards[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
const line = document.createElement('div');
|
|
line.className = 'overwrite-drag-line';
|
|
if (insertBefore) {
|
|
list.insertBefore(line, insertBefore);
|
|
} else {
|
|
list.appendChild(line);
|
|
}
|
|
|
|
const lines = Array.from(list.querySelectorAll('.overwrite-drag-line'));
|
|
lines.forEach(l => {
|
|
if (l !== line) l.remove();
|
|
});
|
|
});
|
|
|
|
list.addEventListener('dragleave', function(e) {
|
|
if (!list.contains(e.relatedTarget)) {
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(line => line.remove());
|
|
}
|
|
});
|
|
|
|
list.addEventListener('drop', function(e) {
|
|
e.preventDefault();
|
|
const dragging = ConfigEditor.overwriteDrag.dragging;
|
|
if (!dragging) return;
|
|
const from = parseInt(dragging.dataset.index);
|
|
const lines = Array.from(list.querySelectorAll('.overwrite-drag-line'));
|
|
if (lines.length === 0) return;
|
|
const line = lines[0];
|
|
const allCards = Array.from(list.querySelectorAll('.sub-card.overwrite-item'));
|
|
const fileCards = allCards.filter(c => typeof c.dataset.index !== 'undefined' && c.dataset.index !== 'custom');
|
|
let nextNode = line.nextSibling;
|
|
while (nextNode && !(nextNode.classList && nextNode.classList.contains('sub-card') && typeof nextNode.dataset.index !== 'undefined' && nextNode.dataset.index !== 'custom')) {
|
|
nextNode = nextNode.nextSibling;
|
|
}
|
|
let to = fileCards.indexOf(nextNode);
|
|
if (to === -1) to = fileCards.length;
|
|
if (from === to || from + 1 === to) {
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(line => line.remove());
|
|
ConfigEditor.saveOverwriteSort();
|
|
return;
|
|
}
|
|
const arr = ConfigEditor.overwriteFiles.filter((f, i) => f.name !== 'openclash_custom_overwrite.sh');
|
|
const customFile = ConfigEditor.overwriteFiles.find(f => f.name === 'openclash_custom_overwrite.sh');
|
|
const moved = arr.splice(from, 1)[0];
|
|
arr.splice(to > from ? to - 1 : to, 0, moved);
|
|
let newArr = [];
|
|
if (customFile) newArr.push(customFile);
|
|
newArr = newArr.concat(arr);
|
|
ConfigEditor.overwriteFiles = newArr;
|
|
ConfigEditor.renderOverwriteCards();
|
|
ConfigEditor.saveOverwriteSort();
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(line => line.remove());
|
|
});
|
|
|
|
list.addEventListener('touchmove', function(e) {
|
|
if (!ConfigEditor.overwriteDrag.isTouchDragging) return;
|
|
const touch = e.touches[0];
|
|
const cards = Array.from(list.querySelectorAll('.sub-card.overwrite-item'));
|
|
if (cards.length === 0) return;
|
|
const realCards = cards.slice(2);
|
|
if (realCards.length === 0) return;
|
|
|
|
let insertBefore = null;
|
|
for (let i = 0; i < realCards.length; i++) {
|
|
const rect = realCards[i].getBoundingClientRect();
|
|
if (touch.clientX < rect.left + rect.width / 2) {
|
|
insertBefore = realCards[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(line => line.remove());
|
|
|
|
if (insertBefore) {
|
|
const line = document.createElement('div');
|
|
line.className = 'overwrite-drag-line';
|
|
list.insertBefore(line, insertBefore);
|
|
} else {
|
|
const line = document.createElement('div');
|
|
line.className = 'overwrite-drag-line';
|
|
list.appendChild(line);
|
|
}
|
|
});
|
|
|
|
list.addEventListener('touchend', function(e) {
|
|
var dragging = ConfigEditor.overwriteDrag.dragging;
|
|
if (!dragging) {
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(function(line){ line.remove(); });
|
|
return;
|
|
}
|
|
var from = parseInt(dragging.dataset.index);
|
|
|
|
var line = list.querySelector('.overwrite-drag-line');
|
|
if (!line) {
|
|
ConfigEditor.overwriteDrag.dragging = null;
|
|
ConfigEditor.overwriteDrag.startIndex = null;
|
|
ConfigEditor.overwriteDrag.isTouchDragging = false;
|
|
return;
|
|
}
|
|
|
|
var allCards = Array.from(list.querySelectorAll('.sub-card.overwrite-item'));
|
|
var fileCards = allCards.filter(function(c){ return c.dataset && typeof c.dataset.index !== 'undefined' && c.dataset.index !== 'custom'; });
|
|
|
|
var lineRect = line.getBoundingClientRect();
|
|
var lineCenterX = lineRect.left + lineRect.width / 2;
|
|
|
|
var to = fileCards.length;
|
|
for (var i = 0; i < fileCards.length; i++) {
|
|
var rect = fileCards[i].getBoundingClientRect();
|
|
var cardMid = rect.left + rect.width / 2;
|
|
if (lineCenterX < cardMid) {
|
|
to = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!(from === to || from + 1 === to)) {
|
|
var arr = ConfigEditor.overwriteFiles.filter(function(f){ return f.name !== 'openclash_custom_overwrite.sh'; });
|
|
var customFile = ConfigEditor.overwriteFiles.find(function(f){ return f.name === 'openclash_custom_overwrite.sh'; });
|
|
var moved = arr.splice(from, 1)[0];
|
|
arr.splice(to > from ? to - 1 : to, 0, moved);
|
|
var newArr = [];
|
|
if (customFile) newArr.push(customFile);
|
|
newArr = newArr.concat(arr);
|
|
ConfigEditor.overwriteFiles = newArr;
|
|
ConfigEditor.renderOverwriteCards();
|
|
ConfigEditor.saveOverwriteSort();
|
|
} else {
|
|
ConfigEditor.saveOverwriteSort();
|
|
}
|
|
|
|
list.querySelectorAll('.overwrite-drag-line').forEach(function(l){ l.remove(); });
|
|
ConfigEditor.overwriteDrag.dragging = null;
|
|
ConfigEditor.overwriteDrag.startIndex = null;
|
|
ConfigEditor.overwriteDrag.isTouchDragging = false;
|
|
});
|
|
|
|
bar.style.display = self.isOverwrite ? 'block' : 'none';
|
|
},
|
|
|
|
saveOverwriteSort: function() {
|
|
var self = this;
|
|
var reqs = [];
|
|
this.overwriteFiles.forEach(function(file, idx) {
|
|
var name = file.name || (file.path ? file.path.split('/').pop() : '');
|
|
if (name === 'openclash_custom_overwrite.sh') return;
|
|
var sub = self.overwriteSubInfo[name] || {};
|
|
var formData = new FormData();
|
|
formData.append('filename', name);
|
|
formData.append('type', sub.type || 'file');
|
|
formData.append('url', sub.url || '');
|
|
formData.append('update_days', sub.update_days || '');
|
|
formData.append('update_hour', sub.update_hour || '');
|
|
formData.append('param', sub.param || '');
|
|
formData.append('config', self.parseOverwriteConfigValue(sub.config).join('\n'));
|
|
formData.append('order', idx);
|
|
reqs.push(fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
|
|
method: 'POST',
|
|
body: formData
|
|
}));
|
|
});
|
|
Promise.all(reqs).then(function() {
|
|
self.loadOverwriteSubInfo();
|
|
});
|
|
},
|
|
|
|
getOverwriteConfigFiles: function() {
|
|
var rawList = [];
|
|
if (window.configFiles && Array.isArray(window.configFiles)) {
|
|
rawList = window.configFiles;
|
|
} else if (window.ConfigFileManager && Array.isArray(window.ConfigFileManager.configList)) {
|
|
rawList = window.ConfigFileManager.configList;
|
|
}
|
|
|
|
return rawList.map(function(file) {
|
|
if (typeof file === 'string') {
|
|
return {
|
|
name: file,
|
|
path: file
|
|
};
|
|
}
|
|
return {
|
|
name: file.name || file.filename || file.path || '',
|
|
path: file.path || file.filepath || file.name || ''
|
|
};
|
|
}).filter(function(file) {
|
|
return !!file.path;
|
|
});
|
|
},
|
|
|
|
parseOverwriteConfigValue: function(value) {
|
|
if (!value) return [];
|
|
if (Array.isArray(value)) {
|
|
return value.filter(function(item) { return !!item; });
|
|
}
|
|
return String(value).split(/[\r\n,;]+/).map(function(item) {
|
|
return item.trim();
|
|
}).filter(function(item) {
|
|
return !!item;
|
|
});
|
|
},
|
|
|
|
renderOverwriteConfigDropdown: function(configValue, dropdownId) {
|
|
var files = this.getOverwriteConfigFiles();
|
|
var selected = this.parseOverwriteConfigValue(configValue);
|
|
var selectedMap = {};
|
|
selected.forEach(function(item) {
|
|
selectedMap[item] = true;
|
|
});
|
|
|
|
if (!selected.length) {
|
|
selectedMap['all'] = true;
|
|
}
|
|
|
|
var allChecked = !!selectedMap['all'];
|
|
|
|
var allOptionHtml = `
|
|
<label class="overwrite-config-option${allChecked ? ' selected' : ''}" data-path="all" data-all-option="1">
|
|
<span class="overwrite-config-option-left">
|
|
<input type="checkbox" value="all"${allChecked ? ' checked' : ''}>
|
|
<span class="overwrite-config-option-name" title="<%:Use For All Config File%>"><%:Use For All Config File%></span>
|
|
</span>
|
|
<span class="overwrite-config-option-state">${allChecked ? '✓' : ''}</span>
|
|
</label>
|
|
`;
|
|
|
|
var fileOptionsHtml = files.length ? files.map(function(file) {
|
|
var checked = (!allChecked && (selectedMap[file.path] || selectedMap[file.name])) ? ' checked' : '';
|
|
var selectedClass = checked ? ' selected' : '';
|
|
var disabledClass = allChecked ? ' disabled-by-all' : '';
|
|
var stateText = checked ? '✓' : '';
|
|
var disabled = allChecked;
|
|
return `
|
|
<label class="overwrite-config-option${selectedClass}${disabledClass}" data-path="${file.path}">
|
|
<span class="overwrite-config-option-left">
|
|
<input type="checkbox" value="${file.path}"${checked}${disabled ? ' disabled' : ''}>
|
|
<span class="overwrite-config-option-name" title="${file.name}">${file.name}</span>
|
|
</span>
|
|
<span class="overwrite-config-option-state">${stateText}</span>
|
|
</label>
|
|
`;
|
|
}).join('') : '<div class="overwrite-config-option"><span class="overwrite-config-option-name"><%:No config files found%></span></div>';
|
|
|
|
var optionsHtml = allOptionHtml + fileOptionsHtml;
|
|
|
|
return `
|
|
<div class="overwrite-config-dropdown form-select-wrapper" id="${dropdownId}">
|
|
<button type="button" class="overwrite-config-dropdown-btn form-select">
|
|
<span class="overwrite-config-dropdown-text"><%:Use For All Config File%></span>
|
|
<span class="overwrite-config-dropdown-arrow"></span>
|
|
</button>
|
|
<div class="overwrite-config-dropdown-panel">
|
|
${optionsHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
updateOverwriteConfigDropdownLabel: function(dropdown) {
|
|
if (!dropdown) return;
|
|
var textEl = dropdown.querySelector('.overwrite-config-dropdown-text');
|
|
if (!textEl) return;
|
|
var checked = dropdown.querySelectorAll('.overwrite-config-option input[type="checkbox"]:checked');
|
|
if (!checked.length) {
|
|
textEl.textContent = '<%:Use For All Config File%>';
|
|
return;
|
|
}
|
|
if (checked.length === 1) {
|
|
var oneName = checked[0].closest('.overwrite-config-option').querySelector('.overwrite-config-option-name');
|
|
textEl.textContent = oneName ? oneName.textContent : '<%:1 file selected%>';
|
|
return;
|
|
}
|
|
textEl.textContent = checked.length + ' <%:files selected%>';
|
|
},
|
|
|
|
bindOverwriteConfigDropdown: function(container) {
|
|
if (!container || container.dataset.inited === '1') return;
|
|
container.dataset.inited = '1';
|
|
|
|
var self = this;
|
|
var btn = container.querySelector('.overwrite-config-dropdown-btn');
|
|
var panel = container.querySelector('.overwrite-config-dropdown-panel');
|
|
|
|
function syncAllExclusiveState() {
|
|
var allInput = container.querySelector('.overwrite-config-option input[type="checkbox"][value="all"]');
|
|
var allOption = allInput ? allInput.closest('.overwrite-config-option') : null;
|
|
var allState = allOption ? allOption.querySelector('.overwrite-config-option-state') : null;
|
|
var allSelected = !!(allInput && allInput.checked);
|
|
|
|
if (allOption) {
|
|
allOption.classList.toggle('selected', allSelected);
|
|
}
|
|
if (allState) {
|
|
allState.textContent = allSelected ? '✓' : '';
|
|
}
|
|
|
|
container.querySelectorAll('.overwrite-config-option input[type="checkbox"]').forEach(function(input) {
|
|
if (input.value === 'all') return;
|
|
var option = input.closest('.overwrite-config-option');
|
|
var state = option ? option.querySelector('.overwrite-config-option-state') : null;
|
|
if (allSelected) {
|
|
input.checked = false;
|
|
}
|
|
input.disabled = allSelected;
|
|
if (option) {
|
|
option.classList.toggle('selected', input.checked);
|
|
option.classList.toggle('disabled-by-all', allSelected);
|
|
option.setAttribute('aria-disabled', allSelected ? 'true' : 'false');
|
|
}
|
|
if (state) {
|
|
state.textContent = input.checked ? '✓' : '';
|
|
}
|
|
});
|
|
}
|
|
|
|
this.updateOverwriteConfigDropdownLabel(container);
|
|
syncAllExclusiveState();
|
|
this.updateOverwriteConfigDropdownLabel(container);
|
|
|
|
if (btn && !btn.disabled) {
|
|
btn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
container.classList.toggle('open');
|
|
});
|
|
}
|
|
|
|
container.querySelectorAll('.overwrite-config-option input[type="checkbox"]').forEach(function(input) {
|
|
input.addEventListener('change', function() {
|
|
if (input.value === 'all') {
|
|
syncAllExclusiveState();
|
|
} else if (input.checked) {
|
|
var allInput = container.querySelector('.overwrite-config-option input[type="checkbox"][value="all"]');
|
|
if (allInput) {
|
|
allInput.checked = false;
|
|
}
|
|
syncAllExclusiveState();
|
|
}
|
|
|
|
var option = input.closest('.overwrite-config-option');
|
|
var state = option ? option.querySelector('.overwrite-config-option-state') : null;
|
|
if (option) {
|
|
option.classList.toggle('selected', input.checked);
|
|
}
|
|
if (state) {
|
|
state.textContent = input.checked ? '✓' : '';
|
|
}
|
|
self.updateOverwriteConfigDropdownLabel(container);
|
|
});
|
|
});
|
|
|
|
if (panel) {
|
|
panel.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
});
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (!container.contains(e.target)) {
|
|
container.classList.remove('open');
|
|
}
|
|
});
|
|
},
|
|
|
|
getOverwriteConfigSelection: function(root, dropdownId) {
|
|
var dropdown = root.querySelector('#' + dropdownId);
|
|
if (!dropdown) return [];
|
|
var selected = Array.from(dropdown.querySelectorAll('.overwrite-config-option input[type="checkbox"]:checked')).map(function(input) {
|
|
return input.value;
|
|
}).filter(function(value) {
|
|
return !!value;
|
|
});
|
|
if (selected.indexOf('all') !== -1) {
|
|
return ['all'];
|
|
}
|
|
return selected;
|
|
},
|
|
|
|
renderOverwriteSubForm: function(options) {
|
|
var name = options.name || '';
|
|
var sub = options.sub || {};
|
|
var readonly = !!options.readonly;
|
|
var showTabs = !!options.showTabs;
|
|
var activeTab = options.activeTab || 'file';
|
|
var fileConfigDropdown = this.renderOverwriteConfigDropdown(sub.config || '', 'overwrite-upload-config-dropdown');
|
|
var subscribeConfigDropdown = this.renderOverwriteConfigDropdown(sub.config || '', 'overwrite-subscribe-config-dropdown');
|
|
|
|
var tabsHtml = showTabs ? `
|
|
<div class="upload-mode-selector">
|
|
<div class="mode-tabs">
|
|
<button type="button" class="mode-tab${activeTab==='file'?' active':''}" id="overwrite-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${activeTab==='subscribe'?' active':''}" id="overwrite-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>
|
|
` : '';
|
|
|
|
var fileContent = `
|
|
<form id="overwrite-upload-form-file" style="display:${activeTab==='file'?'block':'none'};">
|
|
<div class="form-group" style="margin-bottom:20px;">
|
|
<label for="overwrite-upload-config"><%:Config File%>:</label>
|
|
${fileConfigDropdown}
|
|
</div>
|
|
<div class="upload-zone" id="overwrite-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 txt,conf files, max size 10MB%></p>
|
|
</div>
|
|
<input type="file" id="overwrite-upload-file-input" accept=".txt,.conf,*" style="display: none;">
|
|
</div>
|
|
<div class="filename-input-container" style="margin-top:20px;">
|
|
<label for="overwrite-upload-filename-input"><%:File Name%>:</label>
|
|
<input type="text" id="overwrite-upload-filename-input" placeholder="<%:Please enter a filename%>" class="form-input" value="${name}" ${readonly ? 'readonly' : ''}/>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
var subscribeContent = `
|
|
<form id="overwrite-upload-form-subscribe" class="subscribe-form">
|
|
<div class="form-group">
|
|
<label><%:File Name%>:</label>
|
|
<input type="text" class="form-input" name="filename" id="overwrite-subscribe-filename" value="${name}" ${readonly ? 'readonly' : ''} placeholder="<%:Please enter a filename%>">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="overwrite-subscribe-config"><%:Config File%>:</label>
|
|
${subscribeConfigDropdown}
|
|
</div>
|
|
<div class="form-group">
|
|
<label><%:Type%>:</label>
|
|
<div class="form-select-wrapper">
|
|
<select class="form-select" name="type" id="overwrite-subscribe-type" ${readonly ? 'disabled' : ''}>
|
|
<option value="file" ${(sub.type === 'file' || !sub.type) ? 'selected' : ''}>file</option>
|
|
<option value="http" ${sub.type === 'http' ? 'selected' : ''}>http</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-group" id="overwrite-subscribe-url-group" style="display:${sub.type === 'http' ? 'block' : 'none'};">
|
|
<label><%:Subscription URL%>:</label>
|
|
<input type="text" class="form-input" name="url" id="overwrite-subscribe-url" value="${sub.url||''}">
|
|
</div>
|
|
<div class="form-group" id="overwrite-subscribe-update-group" style="display:${sub.type === 'http' ? 'block' : 'none'};">
|
|
<label><%:Update Time%>:</label>
|
|
<div class="update-row">
|
|
<div class="form-select-wrapper">
|
|
<select class="form-select" name="update_days" id="overwrite-subscribe-update-days">
|
|
<option value="off" ${sub.update_days==='off'?'selected':''}><%:OFF%></option>
|
|
<option value="*" ${sub.update_days==='*'?'selected':''}><%:Every Day%></option>
|
|
<option value="1" ${sub.update_days==='1'?'selected':''}><%:Every Monday%></option>
|
|
<option value="2" ${sub.update_days==='2'?'selected':''}><%:Every Tuesday%></option>
|
|
<option value="3" ${sub.update_days==='3'?'selected':''}><%:Every Wednesday%></option>
|
|
<option value="4" ${sub.update_days==='4'?'selected':''}><%:Every Thursday%></option>
|
|
<option value="5" ${sub.update_days==='5'?'selected':''}><%:Every Friday%></option>
|
|
<option value="6" ${sub.update_days==='6'?'selected':''}><%:Every Saturday%></option>
|
|
<option value="0" ${sub.update_days==='0'?'selected':''}><%:Every Sunday%></option>
|
|
</select>
|
|
</div>
|
|
<div class="form-select-wrapper">
|
|
<select class="form-select" name="update_hour" id="overwrite-subscribe-update-hour">
|
|
<option value="off" ${sub.update_hour==='off'?'selected':''}><%:OFF%></option>
|
|
${[...Array(24).keys()].map(h=>`<option value="${h}" ${sub.update_hour==h?'selected':''}>${h}:00</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group" id="overwrite-subscribe-param-group" style="display:'block';">
|
|
<label><%:Environment variable%>:</label>
|
|
<input type="text" class="form-input" name="param" id="overwrite-subscribe-param" value="${sub.param||''}" placeholder="key1=value1;key2=value2">
|
|
</div>
|
|
<div class="upload-progress" id="overwrite-upload-progress" style="display:none;">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="overwrite-progress-fill" style="width:0%;"></div>
|
|
</div>
|
|
<div class="progress-text" id="overwrite-progress-text">0%</div>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
return `
|
|
${tabsHtml}
|
|
<div class="config-upload-content" id="overwrite-upload-content-file" style="${activeTab==='file'?'':'display:none;'}">
|
|
${fileContent}
|
|
</div>
|
|
<div class="config-upload-content" id="overwrite-upload-content-subscribe" style="${activeTab==='subscribe'?'':'display:none;'}">
|
|
${subscribeContent}
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
showAddOverwritemodel: function() {
|
|
var self = this;
|
|
if (document.getElementById('overwrite-add-model')) return;
|
|
|
|
var ocDiv = document.createElement('div');
|
|
ocDiv.className = 'oc';
|
|
var mainOc = document.querySelector('.oc[data-darkmode="true"]');
|
|
if (mainOc) {
|
|
ocDiv.setAttribute('data-darkmode', 'true');
|
|
}
|
|
|
|
var overlay = document.createElement('div');
|
|
overlay.className = 'config-upload-model-overlay show';
|
|
overlay.style.zIndex = 10001;
|
|
overlay.id = 'overwrite-add-model';
|
|
|
|
var model = document.createElement('div');
|
|
model.className = 'config-upload-model';
|
|
|
|
model.innerHTML = `
|
|
<div class="config-upload-header">
|
|
<div class="config-upload-title">
|
|
<span><%:Add Overwrite Module%></span>
|
|
</div>
|
|
<div class="config-upload-actions">
|
|
<button type="button" class="icon-btn" id="overwrite-add-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">
|
|
${self.renderOverwriteSubForm({name:'', sub:{}, readonly:false, showTabs:true, activeTab:'file'})}
|
|
</div>
|
|
<div class="config-upload-footer">
|
|
<div class="config-upload-status">
|
|
<span id="overwrite-upload-status-text"><%:Ready to add file%></span>
|
|
</div>
|
|
<div class="config-upload-buttons">
|
|
<button type="button" class="btn cancel-btn" id="overwrite-upload-cancel"><%:Cancel%></button>
|
|
<button type="button" class="btn upload-btn" id="overwrite-upload-submit"><%:Add%></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
overlay.appendChild(model);
|
|
ocDiv.appendChild(overlay);
|
|
document.body.appendChild(ocDiv);
|
|
|
|
this.bindOverwriteConfigDropdown(model.querySelector('#overwrite-upload-config-dropdown'));
|
|
this.bindOverwriteConfigDropdown(model.querySelector('#overwrite-subscribe-config-dropdown'));
|
|
|
|
var tabFile = model.querySelector('#overwrite-upload-mode-file');
|
|
var tabSub = model.querySelector('#overwrite-upload-mode-subscribe');
|
|
var contentFile = model.querySelector('#overwrite-upload-content-file');
|
|
var contentSub = model.querySelector('#overwrite-upload-content-subscribe');
|
|
tabFile.onclick = function() {
|
|
tabFile.classList.add('active');
|
|
tabSub.classList.remove('active');
|
|
contentFile.style.display = 'block';
|
|
contentSub.style.display = 'none';
|
|
};
|
|
tabSub.onclick = function() {
|
|
tabFile.classList.remove('active');
|
|
tabSub.classList.add('active');
|
|
contentFile.style.display = 'none';
|
|
contentSub.style.display = 'block';
|
|
};
|
|
|
|
var typeSelect = model.querySelector('#overwrite-subscribe-type');
|
|
var urlGroup = model.querySelector('#overwrite-subscribe-url-group');
|
|
var updateGroup = model.querySelector('#overwrite-subscribe-update-group');
|
|
if (typeSelect) {
|
|
if (typeSelect.value === 'http') {
|
|
urlGroup.style.display = 'block';
|
|
updateGroup.style.display = 'block';
|
|
} else {
|
|
urlGroup.style.display = 'none';
|
|
updateGroup.style.display = 'none';
|
|
}
|
|
typeSelect.addEventListener('change', function() {
|
|
if (typeSelect.value === 'http') {
|
|
urlGroup.style.display = 'block';
|
|
updateGroup.style.display = 'block';
|
|
} else {
|
|
urlGroup.style.display = 'none';
|
|
updateGroup.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
var urlInput = model.querySelector('#overwrite-subscribe-url');
|
|
var filenameInputSub = model.querySelector('#overwrite-subscribe-filename');
|
|
if (urlInput && filenameInputSub) {
|
|
urlInput.addEventListener('input', function() {
|
|
if (!filenameInputSub.value.trim() && urlInput.value.trim()) {
|
|
try {
|
|
var url = urlInput.value.trim();
|
|
var name = url.split('?')[0].split('/').pop();
|
|
if (name && /^[\w\.\-\_]+$/.test(name)) {
|
|
filenameInputSub.value = name;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
});
|
|
}
|
|
|
|
model.querySelector('#overwrite-add-close').onclick = function() {
|
|
document.body.removeChild(ocDiv);
|
|
};
|
|
overlay.onclick = function(e) {
|
|
if (e.target === overlay) {
|
|
document.body.removeChild(ocDiv);
|
|
}
|
|
};
|
|
model.querySelector('#overwrite-upload-cancel').onclick = function() {
|
|
document.body.removeChild(ocDiv);
|
|
};
|
|
|
|
var uploadZone = model.querySelector('#overwrite-upload-zone');
|
|
var fileInput = model.querySelector('#overwrite-upload-file-input');
|
|
var filenameInput = model.querySelector('#overwrite-upload-filename-input');
|
|
var statusText = model.querySelector('#overwrite-upload-status-text');
|
|
var selectedFile = null;
|
|
|
|
uploadZone.onclick = function() {
|
|
fileInput.click();
|
|
};
|
|
fileInput.onchange = function(e) {
|
|
if (e.target.files.length > 0) {
|
|
selectedFile = e.target.files[0];
|
|
uploadZone.classList.add('has-file');
|
|
uploadZone.querySelector('.upload-primary').textContent = '<%:File selected:%> ' + selectedFile.name;
|
|
uploadZone.querySelector('.upload-secondary').textContent = '<%:Size:%> ' + (selectedFile.size/1024/1024).toFixed(2) + ' MB';
|
|
filenameInput.value = selectedFile.name;
|
|
}
|
|
};
|
|
|
|
var submitBtn = model.querySelector('#overwrite-upload-submit');
|
|
function validateAddOverwriteForm() {
|
|
if (tabFile.classList.contains('active')) {
|
|
const filename = filenameInput.value.trim();
|
|
submitBtn.disabled = !(selectedFile && filename && filename !== 'openclash_custom_overwrite.sh');
|
|
} else {
|
|
const filename = model.querySelector('#overwrite-subscribe-filename').value.trim();
|
|
const type = model.querySelector('#overwrite-subscribe-type').value;
|
|
const url = model.querySelector('#overwrite-subscribe-url').value.trim();
|
|
let valid = !!filename && filename !== 'openclash_custom_overwrite.sh';
|
|
if (type === 'http') {
|
|
valid = valid && !!url && /^https?:\/\/[^ \n|]+$/.test(url);
|
|
}
|
|
submitBtn.disabled = !valid;
|
|
}
|
|
}
|
|
validateAddOverwriteForm();
|
|
|
|
filenameInput.addEventListener('input', validateAddOverwriteForm);
|
|
|
|
if (model.querySelector('#overwrite-subscribe-filename')) {
|
|
model.querySelector('#overwrite-subscribe-filename').addEventListener('input', validateAddOverwriteForm);
|
|
}
|
|
if (model.querySelector('#overwrite-subscribe-type')) {
|
|
model.querySelector('#overwrite-subscribe-type').addEventListener('change', validateAddOverwriteForm);
|
|
}
|
|
if (model.querySelector('#overwrite-subscribe-url')) {
|
|
model.querySelector('#overwrite-subscribe-url').addEventListener('input', validateAddOverwriteForm);
|
|
}
|
|
tabFile.addEventListener('click', function() {
|
|
setTimeout(validateAddOverwriteForm, 0);
|
|
});
|
|
tabSub.addEventListener('click', function() {
|
|
setTimeout(validateAddOverwriteForm, 0);
|
|
});
|
|
|
|
|
|
fileInput.addEventListener('change', validateAddOverwriteForm);
|
|
|
|
model.querySelector('#overwrite-upload-submit').onclick = function() {
|
|
if (tabFile.classList.contains('active')) {
|
|
var filename = filenameInput.value.trim();
|
|
if (!filename) {
|
|
alert('<%:Please enter a filename%>');
|
|
return;
|
|
}
|
|
if (filename === 'openclash_custom_overwrite.sh') {
|
|
alert('<%:openclash_custom_overwrite.sh already exists and cannot be added again%>');
|
|
return;
|
|
}
|
|
if (selectedFile) {
|
|
var reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
var fileContent = e.target.result;
|
|
var selectedConfigPaths = self.getOverwriteConfigSelection(model, 'overwrite-upload-config-dropdown');
|
|
var formData = new FormData();
|
|
formData.append('filename', filename);
|
|
formData.append('config_file', fileContent);
|
|
formData.append('order', self.overwriteFileCardCount + 1);
|
|
formData.append('enable', '0');
|
|
formData.append('config', selectedConfigPaths.join('\n'));
|
|
|
|
fetch('/cgi-bin/luci/admin/services/openclash/upload_overwrite', {
|
|
method: 'POST',
|
|
body: formData
|
|
}).then(r=>r.json()).then(function(data){
|
|
if (data.status === 'success') {
|
|
statusText.textContent = '<%:Upload successful%>';
|
|
document.body.removeChild(ocDiv);
|
|
self.loadOverwriteFiles();
|
|
setTimeout(function() {
|
|
self.currentConfigFile = '/etc/openclash/overwrite/' + filename;
|
|
self.isOverwrite = true;
|
|
self.showOverwrite(self.currentConfigFile);
|
|
}, 300);
|
|
} else {
|
|
statusText.textContent = '<%:Upload failed:%> ' + (data.message || '');
|
|
}
|
|
});
|
|
};
|
|
reader.onerror = function() {
|
|
statusText.textContent = '<%:Failed to read file%>';
|
|
};
|
|
reader.readAsText(selectedFile, 'UTF-8');
|
|
} else {
|
|
alert('<%:No Specify Upload File%>');
|
|
return;
|
|
}
|
|
} else {
|
|
var form = model.querySelector('#overwrite-upload-form-subscribe');
|
|
var filename = form.querySelector('#overwrite-subscribe-filename').value.trim();
|
|
var url = form.querySelector('#overwrite-subscribe-url').value.trim();
|
|
var update_days = form.querySelector('#overwrite-subscribe-update-days').value;
|
|
var update_hour = form.querySelector('#overwrite-subscribe-update-hour').value;
|
|
var type = form.querySelector('#overwrite-subscribe-type').value;
|
|
var param = form.querySelector('#overwrite-subscribe-param').value.trim();
|
|
var selectedConfigPaths = self.getOverwriteConfigSelection(model, 'overwrite-subscribe-config-dropdown');
|
|
if (!filename) {
|
|
alert('<%:Please enter a filename%>');
|
|
return;
|
|
}
|
|
if (filename === 'openclash_custom_overwrite.sh') {
|
|
alert('<%:openclash_custom_overwrite.sh already exists and cannot be added again%>');
|
|
return;
|
|
}
|
|
if (type === 'http' && !url) {
|
|
alert('<%:Please enter subscription URL%>');
|
|
return;
|
|
}
|
|
if (type === 'http' && !/^https?:\/\/[^ \n|]+$/.test(url)) {
|
|
alert('<%:Invalid subscription URL format, only single HTTP/HTTPS link is supported%>');
|
|
return;
|
|
}
|
|
var formData = new FormData();
|
|
formData.append('filename', filename);
|
|
formData.append('type', type);
|
|
formData.append('param', param);
|
|
formData.append('order', self.overwriteFileCardCount + 1);
|
|
formData.append('enable', '0');
|
|
formData.append('config', selectedConfigPaths.join('\n'));
|
|
if (type === 'http') {
|
|
formData.append('url', url);
|
|
formData.append('update_days', update_days);
|
|
formData.append('update_hour', update_hour);
|
|
}
|
|
var progressContainer = form.querySelector('#overwrite-upload-progress');
|
|
var progressFill = form.querySelector('#overwrite-progress-fill');
|
|
var progressText = form.querySelector('#overwrite-progress-text');
|
|
if (type === 'http') {
|
|
statusText.textContent = '<%:Downloading subscription content...%>';
|
|
progressContainer.style.display = 'block';
|
|
var progress = 0;
|
|
var interval = setInterval(function() {
|
|
progress = Math.min(progress + Math.random() * 20, 90);
|
|
progressFill.style.width = progress + '%';
|
|
progressText.textContent = '<%:Downloading...%> ' + Math.floor(progress) + '%';
|
|
}, 200);
|
|
}
|
|
fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
|
|
method: 'POST',
|
|
body: formData
|
|
}).then(r=>r.json()).then(function(data){
|
|
if (type === 'http') {
|
|
clearInterval(interval);
|
|
if (data.status !== 'success') {
|
|
progressFill.style.width = '0%';
|
|
progressText.textContent = '<%:Download failed%> 0%';
|
|
} else {
|
|
progressFill.style.width = '100%';
|
|
progressText.textContent = '<%:Download completed%> 100%';
|
|
}
|
|
}
|
|
if (data.status === 'success') {
|
|
statusText.textContent = '<%:Subscription added successfully%>';
|
|
} else {
|
|
statusText.textContent = '<%:Failed to save subscription info:%> ' + (data.message || '');
|
|
}
|
|
setTimeout(function() {
|
|
document.body.removeChild(ocDiv);
|
|
self.loadOverwriteFiles();
|
|
setTimeout(function() {
|
|
self.currentConfigFile = '/etc/openclash/overwrite/' + filename;
|
|
self.isOverwrite = true;
|
|
self.showOverwrite(self.currentConfigFile);
|
|
}, 300);
|
|
}, 800);
|
|
});
|
|
}
|
|
};
|
|
},
|
|
|
|
showOverwriteSubmodel: function(name, sub, filePath) {
|
|
var self = this;
|
|
if (document.getElementById('overwrite-sub-model')) return;
|
|
|
|
var ocDiv = document.createElement('div');
|
|
ocDiv.className = 'oc';
|
|
var mainOc = document.querySelector('.oc[data-darkmode="true"]');
|
|
if (mainOc) {
|
|
ocDiv.setAttribute('data-darkmode', 'true');
|
|
}
|
|
|
|
var overlay = document.createElement('div');
|
|
overlay.className = 'config-upload-model-overlay show';
|
|
overlay.style.zIndex = 10001;
|
|
overlay.id = 'overwrite-sub-model';
|
|
|
|
var model = document.createElement('div');
|
|
model.className = 'config-upload-model';
|
|
|
|
model.innerHTML = `
|
|
<div class="config-upload-header">
|
|
<div class="config-upload-title">
|
|
<span><%:Overwrite Module Edit%></span>
|
|
</div>
|
|
<div class="config-upload-actions">
|
|
<button type="button" class="icon-btn" id="overwrite-sub-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">
|
|
${self.renderOverwriteSubForm({name:name, sub:sub, readonly:false, showTabs:false, activeTab:'subscribe'})}
|
|
</div>
|
|
<div class="config-upload-footer">
|
|
<div class="config-upload-status">
|
|
<span id="overwrite-edit-status-text"><%:Ready to edit%></span>
|
|
</div>
|
|
<div class="config-upload-buttons">
|
|
<button type="button" class="btn cancel-btn" id="overwrite-edit-cancel"><%:Cancel%></button>
|
|
<button type="button" class="btn upload-btn" id="overwrite-edit-submit"><%:Save%></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
overlay.appendChild(model);
|
|
ocDiv.appendChild(overlay);
|
|
document.body.appendChild(ocDiv);
|
|
|
|
this.bindOverwriteConfigDropdown(model.querySelector('#overwrite-subscribe-config-dropdown'));
|
|
|
|
var typeSelect = model.querySelector('#overwrite-subscribe-type');
|
|
var urlGroup = model.querySelector('#overwrite-subscribe-url-group');
|
|
var updateGroup = model.querySelector('#overwrite-subscribe-update-group');
|
|
if (typeSelect) {
|
|
if (typeSelect.value === 'http') {
|
|
urlGroup.style.display = 'block';
|
|
updateGroup.style.display = 'block';
|
|
} else {
|
|
urlGroup.style.display = 'none';
|
|
updateGroup.style.display = 'none';
|
|
}
|
|
typeSelect.addEventListener('change', function() {
|
|
if (typeSelect.value === 'http') {
|
|
urlGroup.style.display = 'block';
|
|
updateGroup.style.display = 'block';
|
|
} else {
|
|
urlGroup.style.display = 'none';
|
|
updateGroup.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
model.querySelector('#overwrite-sub-close').onclick = function() {
|
|
document.body.removeChild(ocDiv);
|
|
};
|
|
overlay.onclick = function(e) {
|
|
if (e.target === overlay) {
|
|
document.body.removeChild(ocDiv);
|
|
}
|
|
};
|
|
model.querySelector('#overwrite-edit-cancel').onclick = function() {
|
|
document.body.removeChild(ocDiv);
|
|
};
|
|
|
|
var submitBtn = model.querySelector('#overwrite-edit-submit');
|
|
|
|
function validateEditOverwriteForm() {
|
|
const form = model.querySelector('#overwrite-upload-form-subscribe');
|
|
const filename = form.querySelector('#overwrite-subscribe-filename').value.trim();
|
|
const type = form.querySelector('#overwrite-subscribe-type').value;
|
|
const url = form.querySelector('#overwrite-subscribe-url').value.trim();
|
|
let valid = !!filename && filename !== 'openclash_custom_overwrite.sh';
|
|
if (type === 'http') {
|
|
valid = valid && !!url && /^https?:\/\/[^ \n|]+$/.test(url);
|
|
}
|
|
submitBtn.disabled = !valid;
|
|
}
|
|
|
|
validateEditOverwriteForm();
|
|
|
|
model.querySelector('#overwrite-subscribe-filename').addEventListener('input', validateEditOverwriteForm);
|
|
model.querySelector('#overwrite-subscribe-type').addEventListener('change', validateEditOverwriteForm);
|
|
model.querySelector('#overwrite-subscribe-url').addEventListener('input', validateEditOverwriteForm);
|
|
|
|
model.querySelector('#overwrite-edit-submit').onclick = function() {
|
|
var form = model.querySelector('#overwrite-upload-form-subscribe');
|
|
var newName = form.querySelector('#overwrite-subscribe-filename').value.trim();
|
|
var type = form.querySelector('#overwrite-subscribe-type').value;
|
|
var url = form.querySelector('#overwrite-subscribe-url').value.trim();
|
|
var param = form.querySelector('#overwrite-subscribe-param').value.trim();
|
|
var selectedConfigPaths = self.getOverwriteConfigSelection(model, 'overwrite-subscribe-config-dropdown');
|
|
|
|
if (!newName) {
|
|
alert('<%:Please enter a filename%>');
|
|
return;
|
|
}
|
|
if (newName === 'openclash_custom_overwrite.sh') {
|
|
alert('<%:openclash_custom_overwrite.sh already exists and cannot be added again%>');
|
|
return;
|
|
}
|
|
if (type === 'http') {
|
|
if (!url) {
|
|
alert('<%:Please enter subscription URL%>');
|
|
return;
|
|
}
|
|
if (!/^https?:\/\/[^ \n|]+$/.test(url)) {
|
|
alert('<%:Invalid subscription URL format, only single HTTP/HTTPS link is supported%>');
|
|
return;
|
|
}
|
|
}
|
|
|
|
var formData = new FormData(form);
|
|
if (type !== 'http') {
|
|
formData.delete('url');
|
|
formData.delete('update_days');
|
|
formData.delete('update_hour');
|
|
}
|
|
formData.delete('config');
|
|
formData.append('config', selectedConfigPaths.join('\n'));
|
|
if (newName !== name) {
|
|
formData.append('old_filename', name);
|
|
}
|
|
if (typeof sub.enable !== 'undefined') {
|
|
formData.append('enable', sub.enable);
|
|
} else {
|
|
formData.append('enable', '0');
|
|
}
|
|
formData.append('order', sub.order || 1);
|
|
|
|
var progressContainer = form.querySelector('#overwrite-upload-progress');
|
|
var progressFill = form.querySelector('#overwrite-progress-fill');
|
|
var progressText = form.querySelector('#overwrite-progress-text');
|
|
var statusText = model.querySelector('#overwrite-edit-status-text');
|
|
if (type === 'http') {
|
|
statusText.textContent = '<%:Downloading subscription content...%>';
|
|
progressContainer.style.display = 'block';
|
|
var progress = 0;
|
|
var interval = setInterval(function() {
|
|
progress = Math.min(progress + Math.random() * 20, 90);
|
|
progressFill.style.width = progress + '%';
|
|
progressText.textContent = '<%:Downloading...%> ' + Math.floor(progress) + '%';
|
|
}, 200);
|
|
}
|
|
fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
|
|
method: 'POST',
|
|
body: formData
|
|
}).then(r=>r.json()).then(function(data){
|
|
if (type === 'http') {
|
|
clearInterval(interval);
|
|
progressFill.style.width = '100%';
|
|
progressText.textContent = '<%:Download completed%> 100%';
|
|
}
|
|
if (data.status === 'success') {
|
|
if (type === 'http') {
|
|
statusText.textContent = '<%:Subscription saved successfully%>';
|
|
} else {
|
|
statusText.textContent = '<%:Saved successfully%>';
|
|
}
|
|
setTimeout(function() {
|
|
document.body.removeChild(ocDiv);
|
|
|
|
if (newName !== name) {
|
|
self.currentConfigFile = '/etc/openclash/overwrite/' + newName;
|
|
}
|
|
|
|
self.loadOverwriteFiles();
|
|
setTimeout(function() {
|
|
self.loadConfigContent();
|
|
var configNameElement = document.getElementById('config-file-name');
|
|
if (configNameElement) {
|
|
configNameElement.textContent = self.formatDisplayName(self.currentConfigFile);
|
|
}
|
|
}, 300);
|
|
|
|
}, 800);
|
|
} else {
|
|
statusText.textContent = '<%:Failed to save subscription info:%> ' + (data.message || '');
|
|
}
|
|
});
|
|
};
|
|
},
|
|
|
|
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();
|
|
|
|
if (window.OverwriteSubscribeManager && typeof window.OverwriteSubscribeManager.load === 'function') {
|
|
window.OverwriteSubscribeManager.load(true);
|
|
}
|
|
try {
|
|
window.dispatchEvent(new Event('oc-overwrite-updated'));
|
|
} catch (e) {}
|
|
},
|
|
|
|
updateZoom: function(newZoom) {
|
|
this.currentZoom = newZoom;
|
|
|
|
if (this.editorInstance) {
|
|
var cmWrapper = this.model.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);
|
|
},
|
|
|
|
isPointOnTextContent: function(el, clientX, clientY) {
|
|
if (!el) return false;
|
|
|
|
var textWalker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
|
|
var textNode;
|
|
while ((textNode = textWalker.nextNode())) {
|
|
if (!textNode.nodeValue || !textNode.nodeValue.trim()) {
|
|
continue;
|
|
}
|
|
var range = document.createRange();
|
|
range.selectNodeContents(textNode);
|
|
var rects = range.getClientRects();
|
|
for (var i = 0; i < rects.length; i++) {
|
|
var rect = rects[i];
|
|
if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
makeDraggable: function() {
|
|
var self = this;
|
|
var header = this.model.querySelector('.config-editor-header');
|
|
var startX, startY, startLeft, startTop;
|
|
var isDragging = false;
|
|
|
|
header.addEventListener('mousedown', function(e) {
|
|
var target = e.target && e.target.nodeType === 1 ? e.target : e.target.parentElement;
|
|
if (target && target.closest('.config-editor-actions')) {
|
|
return;
|
|
}
|
|
var textEl = target ? target.closest('#editTitle, #config-file-name') : null;
|
|
if (textEl && self.isPointOnTextContent(textEl, e.clientX, e.clientY)) {
|
|
return;
|
|
}
|
|
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
|
|
var rect = self.model.getBoundingClientRect();
|
|
startLeft = rect.left;
|
|
startTop = rect.top;
|
|
|
|
self.model.style.position = 'fixed';
|
|
self.model.style.left = startLeft + 'px';
|
|
self.model.style.top = startTop + 'px';
|
|
self.model.style.margin = '0';
|
|
self.model.style.transform = 'none';
|
|
self.model.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 modelRect = self.model.getBoundingClientRect();
|
|
var maxLeft = window.innerWidth - modelRect.width;
|
|
var maxTop = window.innerHeight - modelRect.height;
|
|
|
|
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
|
|
newTop = Math.max(0, Math.min(newTop, maxTop));
|
|
|
|
self.model.style.left = newLeft + 'px';
|
|
self.model.style.top = newTop + 'px';
|
|
}
|
|
|
|
function onMouseUp() {
|
|
isDragging = false;
|
|
|
|
setTimeout(function() {
|
|
self.model.style.transition = 'all 0.3s ease';
|
|
}, 50);
|
|
|
|
document.removeEventListener('mousemove', onMouseMove);
|
|
document.removeEventListener('mouseup', onMouseUp);
|
|
}
|
|
|
|
header.addEventListener('touchstart', function(e) {
|
|
var target = e.target && e.target.nodeType === 1 ? e.target : e.target.parentElement;
|
|
if (target && target.closest('.config-editor-actions')) {
|
|
return;
|
|
}
|
|
var touch = e.touches && e.touches[0] ? e.touches[0] : null;
|
|
var textEl = target ? target.closest('#editTitle, #config-file-name') : null;
|
|
if (touch && textEl && self.isPointOnTextContent(textEl, touch.clientX, touch.clientY)) {
|
|
return;
|
|
}
|
|
if (e.touches.length !== 1) return;
|
|
|
|
isDragging = true;
|
|
startX = e.touches[0].clientX;
|
|
startY = e.touches[0].clientY;
|
|
|
|
var rect = self.model.getBoundingClientRect();
|
|
startLeft = rect.left;
|
|
startTop = rect.top;
|
|
|
|
self.model.style.position = 'fixed';
|
|
self.model.style.left = startLeft + 'px';
|
|
self.model.style.top = startTop + 'px';
|
|
self.model.style.margin = '0';
|
|
self.model.style.transform = 'none';
|
|
self.model.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 modelRect = self.model.getBoundingClientRect();
|
|
var maxLeft = window.innerWidth - modelRect.width;
|
|
var maxTop = window.innerHeight - modelRect.height;
|
|
|
|
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
|
|
newTop = Math.max(0, Math.min(newTop, maxTop));
|
|
|
|
self.model.style.left = newLeft + 'px';
|
|
self.model.style.top = newTop + 'px';
|
|
|
|
e.preventDefault();
|
|
}
|
|
|
|
function onTouchEnd() {
|
|
isDragging = false;
|
|
setTimeout(function() {
|
|
self.model.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.model.getBoundingClientRect();
|
|
startWidth = rect.width;
|
|
startHeight = rect.height;
|
|
|
|
self.model.style.transition = 'none';
|
|
self.model.style.width = startWidth + 'px';
|
|
self.model.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.model.style.width = newWidth + 'px';
|
|
self.model.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.model.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.model.getBoundingClientRect();
|
|
startWidth = rect.width;
|
|
startHeight = rect.height;
|
|
|
|
self.model.style.transition = 'none';
|
|
self.model.style.width = startWidth + 'px';
|
|
self.model.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.model.style.width = newWidth + 'px';
|
|
self.model.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.model.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> |