Update confadj.html

This commit is contained in:
2025-11-04 17:54:51 +00:00
parent 3d850ca5ec
commit 8b05503c07

296
confadj.html Normal file
View File

@@ -0,0 +1,296 @@
<!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>