Files
cfg-editor-PRO/confadj.html
2025-11-04 17:54:51 +00:00

296 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Config/Text File Editor Upload • Edit • Download (Tall, Fixed)</title>
<style>
:root{
--bg:#0b0f14; --panel:#121922; --muted:#1a2430; --text:#e6edf3; --sub:#9fb0c3;
--accent:#4aa3ff; --accent-2:#22c55e; --warn:#f59e0b; --err:#ef4444; --border:#243241;
}
*{box-sizing:border-box}
body{margin:0; background:var(--bg); color:var(--text); font:14px/1.45 system-ui, Segoe UI, Roboto, Ubuntu, Arial, sans-serif}
header{display:flex; align-items:center; gap:.75rem; padding:10px 14px; background:linear-gradient(180deg,var(--panel),#0e1420); border-bottom:1px solid var(--border)}
h1{font-size:16px; margin:0; font-weight:600}
.spacer{flex:1}
button,.btn,input[type=file],select{border:1px solid var(--border); background:var(--muted); color:var(--text); border-radius:10px; padding:8px 12px; cursor:pointer}
button:hover,.btn:hover{background:#223046}
button.primary{background:var(--accent); color:#06121f; border-color:#2d7cd6; font-weight:600}
.toolbar{display:flex; gap:.5rem; flex-wrap:wrap; padding:10px 14px; border-bottom:1px solid var(--border); background:#0f1622}
.filelabel{padding:8px 12px; border:1px dashed var(--border); border-radius:10px; background:var(--panel)}
.meta{margin-left:8px; color:var(--sub)}
/* Taller viewport for editor */
.editor-wrap{position:relative; height:calc(100dvh - 160px);} /* header+toolbar+statusbar */
.editor{display:grid; grid-template-columns: 64px 1fr; height:100%;}
.gutter{user-select:none; background:#0a0f18; border-right:1px solid var(--border); color:#5d7089; padding:8px 6px; overflow:hidden; font:12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; text-align:right; white-space:pre}
textarea#text{border:0; outline:none; background:#0b111a; color:#e6edf3; padding:8px 10px; font:12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; resize:none; width:100%; height:100%}
.dropzone{border:2px dashed var(--border); margin:12px; padding:24px; border-radius:12px; text-align:center; color:var(--sub)}
.dropzone.drag{border-color:var(--accent); color:var(--accent)}
.statusbar{display:flex; gap:10px; align-items:center; padding:8px 12px; border-top:1px solid var(--border); background:#0e1520; color:var(--sub);}
.statusbar input, .statusbar select{padding:6px 8px; border-radius:8px; border:1px solid var(--border); background:#0b111a; color:#e6edf3}
.find{display:flex; gap:6px; align-items:center}
.pill{font-size:11px; padding:2px 6px; border-radius:999px; border:1px solid var(--border); background:#0b111a; color:#9fb0c3}
.hidden{display:none !important}
</style>
</head>
<body>
<header>
<h1>Config/Text File Editor (Tall)</h1>
<div class="spacer"></div>
<button id="downloadBtn" class="primary" title="Download (Ctrl/Cmd+S)">Download</button>
</header>
<div class="toolbar">
<label class="filelabel" for="file">Choose file…</label>
<input id="file" type="file" accept=".cfg,.conf,.txt,.json,.ini,.yaml,.yml,.sh,.rsc,.cnf,.env,*/*" hidden>
<button id="pasteBtn">Paste</button>
<button id="wrapBtn">Toggle wrap</button>
<label>Line endings:
<select id="eolSel">
<option value="auto">Auto</option>
<option value="lf">LF (\n)</option>
<option value="crlf">CRLF (\r\n)</option>
</select>
</label>
<span id="meta" class="meta">No file</span>
</div>
<div id="drop" class="dropzone">Drop a text file here</div>
<div class="editor-wrap hidden" id="editorWrap">
<div class="editor">
<div id="gutter" class="gutter"></div>
<textarea id="text" spellcheck="false" placeholder="Open a file to begin…"></textarea>
</div>
</div>
<div class="statusbar">
<div class="find">
<input id="findInput" placeholder="Find… (Enter)">
<input id="replaceInput" placeholder="Replace with…">
<button id="findNextBtn">Find next</button>
<button id="replaceBtn">Replace</button>
<button id="replaceAllBtn">Replace all</button>
<span id="matchCount" class="pill">0 matches</span>
</div>
<div class="spacer"></div>
<span id="pos" class="pill">Ln 1, Col 1</span>
</div>
<script>
(function(){
'use strict';
// State
let filename = 'untitled.txt';
let originalEOL = '\\n'; // default LF
let wrap = false;
const $ = s => document.querySelector(s);
const $$ = s => Array.from(document.querySelectorAll(s));
const fileInput = $('#file');
const pasteBtn = $('#pasteBtn');
const wrapBtn = $('#wrapBtn');
const eolSel = $('#eolSel');
const drop = $('#drop');
const editorWrap = $('#editorWrap');
const gutter = $('#gutter');
const ta = $('#text');
const meta = $('#meta');
const downloadBtn = $('#downloadBtn');
const findInput = $('#findInput');
const replaceInput = $('#replaceInput');
const findNextBtn = $('#findNextBtn');
const replaceBtn = $('#replaceBtn');
const replaceAllBtn = $('#replaceAllBtn');
const matchCount = $('#matchCount');
const pos = $('#pos');
// Utilities
function detectEOL(s){
const crlf = (s.match(/\\r\\n/g)||[]).length;
const lf = (s.match(/(?<!\\r)\\n/g)||[]).length;
return crlf > lf ? '\\r\\n' : '\\n';
}
function setEditorVisible(v){
editorWrap.classList.toggle('hidden', !v);
drop.classList.toggle('hidden', v);
}
function updateMeta(size){ meta.textContent = filename + (size!==undefined ? `${size} bytes` : ''); }
function setEOLMode(mode){
if (mode==='auto'){ originalEOL = detectEOL(ta.value); return; }
originalEOL = (mode==='crlf') ? '\\r\\n' : '\\n';
}
function normalizeToEOL(s, eol){
return s.replace(/\\r?\\n/g, eol);
}
function getDownloadName(){
const dot = filename.lastIndexOf('.');
if (dot > 0){
return filename.slice(0, dot) + '-edited' + filename.slice(dot);
}
return filename + '-edited';
}
function saveToLocal(){
try { localStorage.setItem('editor:'+filename, ta.value); } catch {}
}
function maybeLoadFromLocal(){
try {
const v = localStorage.getItem('editor:'+filename);
if (v != null) { ta.value = v; renderGutter(); }
} catch {}
}
// File handling
fileInput.addEventListener('change', async (e)=>{
if (!e.target.files.length) return;
const f = e.target.files[0];
filename = f.name;
const buf = await f.arrayBuffer();
let text;
try {
// Attempt decode as UTF-8; fallback naive
text = new TextDecoder('utf-8', {fatal:false}).decode(new Uint8Array(buf));
} catch { text = String(buf); }
originalEOL = detectEOL(text);
ta.value = text;
renderGutter();
setEditorVisible(true);
updateMeta(f.size);
eolSel.value = 'auto';
maybeLoadFromLocal();
ta.focus();
saveToLocal();
});
// Drag & drop
;['dragenter','dragover'].forEach(ev=> drop.addEventListener(ev, (e)=>{ e.preventDefault(); drop.classList.add('drag'); }));
;['dragleave','drop'].forEach(ev=> drop.addEventListener(ev, (e)=>{ e.preventDefault(); drop.classList.remove('drag'); }));
drop.addEventListener('drop', async (e)=>{
const f = e.dataTransfer.files?.[0]; if (!f) return;
fileInput.files = e.dataTransfer.files; // so change handler can reuse logic
const evt = new Event('change'); fileInput.dispatchEvent(evt);
});
pasteBtn.addEventListener('click', async ()=>{
try{
const text = await navigator.clipboard.readText();
if (!text) return alert('Clipboard is empty.');
filename = 'pasted.txt';
ta.value = text;
originalEOL = detectEOL(text);
setEditorVisible(true);
renderGutter();
updateMeta(text.length);
eolSel.value = 'auto';
ta.focus();
saveToLocal();
}catch{ alert('Clipboard permission denied. Paste with Ctrl/Cmd+V into the editor.'); }
});
eolSel.addEventListener('change', ()=>{
setEOLMode(eolSel.value);
});
wrapBtn.addEventListener('click', ()=>{
wrap = !wrap;
ta.style.whiteSpace = wrap ? 'pre-wrap' : 'pre';
});
// Editor behavior
function renderGutter(){
const lines = ta.value.split(/\\r?\\n/).length;
let s = '';
for (let i=1;i<=lines;i++){ s += (i + '\\n'); }
gutter.textContent = s;
gutter.scrollTop = ta.scrollTop;
}
ta.addEventListener('input', ()=>{ renderGutter(); saveToLocal(); highlightFind(); updateCaretPos(); });
ta.addEventListener('scroll', ()=>{ gutter.scrollTop = ta.scrollTop; });
ta.addEventListener('keyup', updateCaretPos);
ta.addEventListener('click', updateCaretPos);
function updateCaretPos(){
const start = ta.selectionStart;
const pre = ta.value.slice(0, start);
const line = pre.split(/\\r?\\n/).length;
const lastBreak = Math.max(pre.lastIndexOf('\\n'), pre.lastIndexOf('\\r'));
const col = start - (lastBreak >= 0 ? lastBreak : -1);
pos.textContent = `Ln ${line}, Col ${col}`;
}
// Find / Replace (simple)
function highlightFind(){
const q = findInput.value;
if (!q){ matchCount.textContent = '0 matches'; return; }
const re = new RegExp(q.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g');
const m = ta.value.match(re);
matchCount.textContent = (m? m.length : 0) + ' matches';
}
findInput.addEventListener('input', highlightFind);
findInput.addEventListener('keydown', (e)=>{ if (e.key==='Enter') findNext(); });
findNextBtn.addEventListener('click', findNext);
function findNext(){
const q = findInput.value;
if (!q) return;
const start = ta.selectionEnd;
const idx = ta.value.indexOf(q, start);
const pos = idx >= 0 ? idx : ta.value.indexOf(q, 0);
if (pos >= 0){
ta.focus();
ta.setSelectionRange(pos, pos+q.length);
ta.scrollTop = ta.scrollHeight * (pos / ta.value.length);
updateCaretPos();
}
highlightFind();
}
replaceBtn.addEventListener('click', ()=>{
const q = findInput.value; if (!q) return;
const r = replaceInput.value ?? '';
if (ta.selectionStart !== ta.selectionEnd && ta.value.slice(ta.selectionStart, ta.selectionEnd) === q){
const before = ta.value.slice(0, ta.selectionStart);
const after = ta.value.slice(ta.selectionEnd);
const pos0 = ta.selectionStart;
ta.value = before + r + after;
ta.setSelectionRange(pos0, pos0 + r.length);
renderGutter(); saveToLocal(); highlightFind(); updateCaretPos();
} else {
findNext();
}
});
replaceAllBtn.addEventListener('click', ()=>{
const q = findInput.value; if (!q) return;
const r = replaceInput.value ?? '';
const re = new RegExp(q.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g');
ta.value = ta.value.replace(re, r);
renderGutter(); saveToLocal(); highlightFind(); updateCaretPos();
});
// Download
function doDownload(){
let text = ta.value;
const mode = eolSel.value;
if (mode==='lf') text = normalizeToEOL(text, '\\n');
else if (mode==='crlf') text = normalizeToEOL(text, '\\r\\n');
else text = normalizeToEOL(text, originalEOL); // auto preserves original
const blob = new Blob([text], {type:'text/plain'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = getDownloadName();
a.click();
setTimeout(()=>URL.revokeObjectURL(a.href), 1200);
}
downloadBtn.addEventListener('click', doDownload);
window.addEventListener('keydown', (e)=>{
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase()==='s'){ e.preventDefault(); doDownload(); }
});
// Init
setEditorVisible(false);
ta.style.whiteSpace = 'pre'; // default no wrap
renderGutter();
updateCaretPos();
})();
</script>
</body>
</html>