514 lines
23 KiB
HTML
514 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>appRobotFileservice</title>
|
||
<link rel="stylesheet" href="/index.css" />
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app-header">
|
||
<h1>appRobotFileservice</h1>
|
||
<span class="subtitle">Datei-Browser · GCodeFiles/</span>
|
||
</div>
|
||
|
||
<div class="sections">
|
||
|
||
<!-- ===== LINKE SPALTE: Programm-Liste ===== -->
|
||
<div class="section">
|
||
<h2>
|
||
Programme
|
||
<span class="badge" id="prog-count">–</span>
|
||
</h2>
|
||
|
||
<div id="breadcrumb" class="breadcrumb"></div>
|
||
|
||
<div class="controls">
|
||
<button id="btn-refresh" title="Aktualisieren">⟳</button>
|
||
<button id="btn-new-file" title="Neue Datei erstellen">📄+</button>
|
||
<button id="btn-new-folder" title="Neuen Ordner erstellen">📁+</button>
|
||
<button id="btn-delete-selected" title="Ausgewähltes löschen" disabled>🗑</button>
|
||
</div>
|
||
|
||
<div class="status-line" id="list-status"></div>
|
||
<div id="program-list"><span class="empty-hint">Lade…</span></div>
|
||
</div>
|
||
|
||
<!-- ===== RECHTE SPALTE: Aktives Programm + Inhalt ===== -->
|
||
<div class="section">
|
||
<h2>
|
||
Aktives Programm
|
||
<span class="badge" id="active-name">–</span>
|
||
</h2>
|
||
|
||
<!-- Info-Zeile -->
|
||
<div class="active-bar" id="active-bar" style="display:none">
|
||
<div class="kv"><span class="k">ID</span><span class="v" id="ai-id">–</span></div>
|
||
<div class="sep"></div>
|
||
<div class="kv"><span class="k">Zeilen</span><span class="v" id="ai-count">–</span></div>
|
||
<div class="sep"></div>
|
||
<div class="kv"><span class="k">Cursor</span><span class="v" id="ai-cursor">–</span></div>
|
||
<div class="sep"></div>
|
||
<div class="kv"><span class="k">Version</span><span class="v" id="ai-version">–</span></div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button id="btn-first" disabled title="Erste Zeile">|◀ First</button>
|
||
<button id="btn-prev" disabled title="Schritt zurück">◀ Prev</button>
|
||
<button id="btn-next" disabled title="Schritt vor">Next ▶</button>
|
||
<button id="btn-last" disabled title="Letzte Zeile">Last ▶|</button>
|
||
<button id="btn-clear" disabled title="Programm leeren">✕ Clear</button>
|
||
<a id="btn-download" class="dl-link" style="display:none" download>⬇ .gcode</a>
|
||
</div>
|
||
|
||
<div class="status-line" id="active-status"></div>
|
||
|
||
<div class="table-wrap">
|
||
<table id="lines-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="idx">#</th>
|
||
<th class="line">G-Code (gespeichert in Grad)</th>
|
||
<th class="ts">Zeitstempel</th>
|
||
<th class="del-col"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="lines-body">
|
||
<tr><td colspan="4" class="empty-hint" style="padding:10px">Kein aktives Programm</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="edit-bar" class="edit-bar" style="display:none">
|
||
<button id="btn-cancel-delete">✗ Abbrechen</button>
|
||
<button id="btn-save-delete" class="danger-confirm">✓ Speichern</button>
|
||
<span id="edit-bar-info" class="edit-bar-info"></span>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- .sections -->
|
||
|
||
<script>
|
||
// ─── Konfiguration ───────────────────────────────────────────────────────────
|
||
const API = ''; // leer → gleicher Origin; überschreib mit 'http://…:2100'
|
||
const REFRESH_MS = 5000; // Auto-Refresh-Intervall
|
||
|
||
// ─── Zustand ─────────────────────────────────────────────────────────────────
|
||
let currentActiveId = null;
|
||
let selectedId = null; // in der Liste ausgewähltes Item (unabhängig vom aktiven Programm)
|
||
let selectedType = null; // 'file' | 'folder'
|
||
let pendingDeletes = new Set(); // Indices markierter Zeilen (noch nicht gespeichert)
|
||
let cachedState = null; // letzter bekannter Serverzustand (für Abbrechen)
|
||
let currentDir = ''; // aktuelles Verzeichnis (relativer Pfad, z. B. 'training/run1')
|
||
let currentPath = []; // Segmente des currentDir als Array (für Breadcrumb)
|
||
|
||
// ─── DOM-Referenzen ───────────────────────────────────────────────────────────
|
||
const elProgCount = document.getElementById('prog-count');
|
||
const elProgList = document.getElementById('program-list');
|
||
const elListStatus = document.getElementById('list-status');
|
||
const elActiveName = document.getElementById('active-name');
|
||
const elActiveBar = document.getElementById('active-bar');
|
||
const elAiId = document.getElementById('ai-id');
|
||
const elAiCount = document.getElementById('ai-count');
|
||
const elAiCursor = document.getElementById('ai-cursor');
|
||
const elAiVersion = document.getElementById('ai-version');
|
||
const elLinesBody = document.getElementById('lines-body');
|
||
const elActStatus = document.getElementById('active-status');
|
||
const elBtnFirst = document.getElementById('btn-first');
|
||
const elBtnPrev = document.getElementById('btn-prev');
|
||
const elBtnNext = document.getElementById('btn-next');
|
||
const elBtnLast = document.getElementById('btn-last');
|
||
const elBtnClear = document.getElementById('btn-clear');
|
||
const elBtnDownload = document.getElementById('btn-download');
|
||
const elBtnDeleteSelected = document.getElementById('btn-delete-selected');
|
||
|
||
// ─── API-Helpers ─────────────────────────────────────────────────────────────
|
||
async function apiFetch(method, path, body) {
|
||
const opts = { method, headers: {} };
|
||
if (body !== undefined) {
|
||
opts.headers['content-type'] = 'application/json';
|
||
opts.body = JSON.stringify(body);
|
||
}
|
||
const res = await fetch(API + path, opts);
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw Object.assign(new Error(data.message || res.statusText), { code: data.code, status: res.status });
|
||
return data;
|
||
}
|
||
|
||
function setStatus(el, msg, isErr = false) {
|
||
el.textContent = msg;
|
||
el.className = 'status-line' + (isErr ? ' err' : '');
|
||
}
|
||
|
||
// ─── Zeitstempel lesbar machen ───────────────────────────────────────────────
|
||
function fmtTs(raw) {
|
||
if (!raw) return '';
|
||
const n = Number(raw);
|
||
if (!Number.isFinite(n) || n < 1e12) return raw;
|
||
return new Date(n).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'medium' });
|
||
}
|
||
|
||
// ─── Zeile in Code + Timestamp zerlegen ──────────────────────────────────────
|
||
function parseLine(line) {
|
||
const idx = line.indexOf(';');
|
||
if (idx === -1) return { code: line.trim(), ts: '' };
|
||
return { code: line.slice(0, idx).trim(), ts: fmtTs(line.slice(idx + 1).trim()) };
|
||
}
|
||
|
||
// ─── Selection-State aktualisieren ──────────────────────────────────────────
|
||
function setSelected(id, type) {
|
||
selectedId = id;
|
||
selectedType = type;
|
||
// Alle Items neu markieren
|
||
elProgList.querySelectorAll('.prog-item').forEach(el => {
|
||
el.classList.toggle('is-selected', el.dataset.id === id);
|
||
});
|
||
elBtnDeleteSelected.disabled = !id;
|
||
}
|
||
|
||
// ─── Programmliste rendern ───────────────────────────────────────────────────
|
||
function renderProgList(programs, folders, activeId) {
|
||
const total = folders.length + programs.length;
|
||
elProgCount.textContent = total;
|
||
if (!total) {
|
||
elProgList.innerHTML = '<span class="empty-hint">Keine Programme gefunden</span>';
|
||
elBtnDeleteSelected.disabled = true;
|
||
return;
|
||
}
|
||
|
||
// Ordner zuerst, dann Dateien
|
||
const folderHtml = folders.map(f => `
|
||
<div class="prog-item is-folder" data-id="${esc(f.id)}" data-type="folder">
|
||
<span class="prog-name" title="${esc(f.name)}">📁 ${esc(f.name)}</span>
|
||
</div>`).join('');
|
||
|
||
const fileHtml = programs.map(p => {
|
||
const isActive = p.id === activeId;
|
||
const isSel = p.id === selectedId;
|
||
return `<div class="prog-item${isActive ? ' is-active' : ''}${isSel ? ' is-selected' : ''}" data-id="${esc(p.id)}" data-type="file">
|
||
<span class="prog-name" title="${esc(p.name)}">📄 ${esc(p.name)}</span>
|
||
<span class="prog-id">${esc(p.id)}</span>
|
||
<span class="prog-count">${p.lineCount} Z.</span>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
elProgList.innerHTML = folderHtml + fileHtml;
|
||
|
||
// Klick → auswählen; Datei laden; Ordner navigieren
|
||
elProgList.querySelectorAll('.prog-item').forEach(el => {
|
||
el.addEventListener('click', () => {
|
||
if (el.dataset.type === 'folder') {
|
||
navigateInto(el.dataset.id);
|
||
} else {
|
||
setSelected(el.dataset.id, el.dataset.type);
|
||
loadProgram(el.dataset.id);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ─── Edit-Bar (Abbrechen / Speichern) ───────────────────────────────────────
|
||
const elEditBar = document.getElementById('edit-bar');
|
||
const elEditBarInfo = document.getElementById('edit-bar-info');
|
||
|
||
function updateEditBar() {
|
||
const n = pendingDeletes.size;
|
||
if (n === 0) {
|
||
elEditBar.style.display = 'none';
|
||
} else {
|
||
elEditBar.style.display = 'flex';
|
||
elEditBarInfo.textContent = `${n} Zeile${n === 1 ? '' : 'n'} zum Löschen markiert`;
|
||
document.getElementById('btn-save-delete').disabled = false; // nach erfolgreichem Save zurücksetzen
|
||
}
|
||
}
|
||
|
||
function markDelete(index) {
|
||
if (pendingDeletes.has(index)) {
|
||
pendingDeletes.delete(index);
|
||
} else {
|
||
pendingDeletes.add(index);
|
||
}
|
||
// Nur die betroffene Zeile neu stylen statt ganzer Tabelle
|
||
const row = elLinesBody.querySelector(`tr[data-index="${index}"]`);
|
||
if (row) row.classList.toggle('pending-delete', pendingDeletes.has(index));
|
||
updateEditBar();
|
||
}
|
||
|
||
// ─── Zeilen-Tabelle rendern ──────────────────────────────────────────────────
|
||
function renderLines(state) {
|
||
cachedState = state;
|
||
const { programId, cursor, lineCount, lines = [], version } = state;
|
||
currentActiveId = programId;
|
||
|
||
elActiveName.textContent = programId || '–';
|
||
const hasProgram = !!programId;
|
||
|
||
// Info-Bar
|
||
if (hasProgram) {
|
||
elActiveBar.style.display = 'flex';
|
||
elAiId.textContent = programId;
|
||
elAiCount.textContent = lineCount;
|
||
elAiCursor.textContent = cursor;
|
||
elAiVersion.textContent = version;
|
||
} else {
|
||
elActiveBar.style.display = 'none';
|
||
}
|
||
|
||
// Stepping-Buttons
|
||
const hasLines = lineCount > 0;
|
||
elBtnFirst.disabled = !hasProgram || !hasLines || cursor === 0;
|
||
elBtnPrev.disabled = !hasProgram || !hasLines || cursor === 0;
|
||
elBtnNext.disabled = !hasProgram || !hasLines || cursor >= lineCount - 1;
|
||
elBtnLast.disabled = !hasProgram || !hasLines || cursor >= lineCount - 1;
|
||
elBtnClear.disabled = !hasProgram;
|
||
|
||
// Download-Link
|
||
if (programId) {
|
||
elBtnDownload.href = `/api/programs/${encodeURIComponent(programId)}/download`;
|
||
elBtnDownload.download = `${programId}.gcode`;
|
||
elBtnDownload.style.display = '';
|
||
} else {
|
||
elBtnDownload.style.display = 'none';
|
||
}
|
||
|
||
// Tabelle
|
||
if (!lines.length) {
|
||
elLinesBody.innerHTML = `<tr><td colspan="4" class="empty-hint" style="padding:10px">${
|
||
hasProgram ? 'Programm ist leer' : 'Kein aktives Programm — links ein Programm anklicken'
|
||
}</td></tr>`;
|
||
updateEditBar();
|
||
return;
|
||
}
|
||
|
||
elLinesBody.innerHTML = lines.map((line, i) => {
|
||
const { code, ts } = parseLine(line);
|
||
const isCursor = i === cursor;
|
||
const isPending = pendingDeletes.has(i);
|
||
return `<tr class="${isCursor ? 'cursor-row' : ''}${isPending ? ' pending-delete' : ''}" data-index="${i}">
|
||
<td class="idx">${i}</td>
|
||
<td class="line">${isCursor ? '▶ ' : ''}${esc(code)}</td>
|
||
<td class="ts">${esc(ts)}</td>
|
||
<td class="del-col"><button class="btn-row-del" data-index="${i}" title="Zeile löschen">🗑</button></td>
|
||
</tr>`;
|
||
}).join('');
|
||
|
||
// Delete-Buttons verdrahten
|
||
elLinesBody.querySelectorAll('.btn-row-del').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
markDelete(Number(btn.dataset.index));
|
||
});
|
||
});
|
||
|
||
// Zur Cursor-Zeile scrollen (nur wenn kein edit-Modus aktiv)
|
||
if (pendingDeletes.size === 0) {
|
||
const cursorRow = elLinesBody.querySelector('.cursor-row');
|
||
if (cursorRow) cursorRow.scrollIntoView({ block: 'nearest' });
|
||
}
|
||
|
||
updateEditBar();
|
||
}
|
||
|
||
// ─── Breadcrumb ──────────────────────────────────────────────────────────────
|
||
function renderBreadcrumb() {
|
||
const el = document.getElementById('breadcrumb');
|
||
let html = `<span class="bc-seg" data-depth="-1">GCodeFiles</span>`;
|
||
currentPath.forEach((seg, i) => {
|
||
html += ` / <span class="bc-seg" data-depth="${i}">${esc(seg)}</span>`;
|
||
});
|
||
el.innerHTML = html;
|
||
el.querySelectorAll('.bc-seg').forEach(span => {
|
||
span.addEventListener('click', () => {
|
||
const depth = Number(span.dataset.depth);
|
||
if (depth === -1) { currentPath = []; currentDir = ''; }
|
||
else { currentPath = currentPath.slice(0, depth + 1); currentDir = currentPath.join('/'); }
|
||
selectedId = null; selectedType = null;
|
||
elBtnDeleteSelected.disabled = true;
|
||
refresh();
|
||
});
|
||
});
|
||
}
|
||
|
||
// ─── In Ordner navigieren ────────────────────────────────────────────────────
|
||
function navigateInto(folderName) {
|
||
currentPath.push(folderName);
|
||
currentDir = currentPath.join('/');
|
||
selectedId = null; selectedType = null;
|
||
elBtnDeleteSelected.disabled = true;
|
||
refresh();
|
||
}
|
||
|
||
// ─── Daten laden ─────────────────────────────────────────────────────────────
|
||
async function refresh() {
|
||
if (pendingDeletes.size > 0) return; // edit-Modus schützen
|
||
const dirQ = currentDir ? `?dir=${encodeURIComponent(currentDir)}` : '';
|
||
try {
|
||
const [programs, folders, active] = await Promise.all([
|
||
apiFetch('GET', `/api/programs${dirQ}`),
|
||
apiFetch('GET', `/api/folders${dirQ}`),
|
||
apiFetch('GET', '/api/active'),
|
||
]);
|
||
renderBreadcrumb();
|
||
renderProgList(programs.programs || [], folders.folders || [], active.programId);
|
||
renderLines(active);
|
||
setStatus(elListStatus, '');
|
||
setStatus(elActStatus, '');
|
||
} catch (err) {
|
||
setStatus(elListStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Programm laden (FLoad-Äquivalent) ───────────────────────────────────────
|
||
async function loadProgram(id, dir) {
|
||
setStatus(elActStatus, `Lade '${id}'…`);
|
||
try {
|
||
const state = await apiFetch('PUT', '/api/active', { id, dir: dir ?? currentDir });
|
||
renderLines(state);
|
||
await refresh();
|
||
setStatus(elActStatus, `'${id}' geladen`);
|
||
} catch (err) {
|
||
setStatus(elActStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Stepping ────────────────────────────────────────────────────────────────
|
||
async function step(endpoint) {
|
||
setStatus(elActStatus, '…');
|
||
try {
|
||
const r = await apiFetch('POST', `/api/active/${endpoint}`);
|
||
setStatus(elActStatus, `Cursor → ${r.cursor}`);
|
||
await refresh();
|
||
} catch (err) {
|
||
setStatus(elActStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Clear ───────────────────────────────────────────────────────────────────
|
||
async function clearProgram() {
|
||
if (!currentActiveId) return;
|
||
if (!confirm(`Programm '${currentActiveId}' wirklich leeren?`)) return;
|
||
try {
|
||
const r = await apiFetch('POST', '/api/active/clear');
|
||
renderLines(r);
|
||
setStatus(elActStatus, 'Programm geleert');
|
||
} catch (err) {
|
||
setStatus(elActStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Zeilen-Löschung: Abbrechen / Speichern ─────────────────────────────────
|
||
function cancelDeletes() {
|
||
pendingDeletes.clear();
|
||
if (cachedState) renderLines(cachedState);
|
||
else updateEditBar();
|
||
}
|
||
|
||
async function saveDeletes() {
|
||
const indices = [...pendingDeletes].sort((a, b) => b - a); // höchste zuerst → kein Index-Shift
|
||
setStatus(elActStatus, `Lösche ${indices.length} Zeile(n)…`);
|
||
document.getElementById('btn-save-delete').disabled = true;
|
||
try {
|
||
for (const i of indices) {
|
||
await apiFetch('DELETE', `/api/active/lines/${i}`);
|
||
}
|
||
pendingDeletes.clear();
|
||
setStatus(elActStatus, `${indices.length} Zeile(n) gelöscht`);
|
||
await refresh();
|
||
} catch (err) {
|
||
setStatus(elActStatus, `Fehler: ${err.message}`, true);
|
||
document.getElementById('btn-save-delete').disabled = false;
|
||
}
|
||
}
|
||
|
||
// ─── Neue Datei anlegen ──────────────────────────────────────────────────────
|
||
async function createFile() {
|
||
const name = prompt('Name der neuen Datei (ohne .gcode):');
|
||
if (!name || !name.trim()) return;
|
||
setStatus(elListStatus, `Erstelle '${name.trim()}'…`);
|
||
try {
|
||
const meta = await apiFetch('POST', '/api/programs', { name: name.trim(), dir: currentDir });
|
||
setStatus(elListStatus, `'${meta.id}' erstellt`);
|
||
await loadProgram(meta.id, currentDir);
|
||
} catch (err) {
|
||
setStatus(elListStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Neuen Ordner anlegen ────────────────────────────────────────────────────
|
||
async function createFolder() {
|
||
const name = prompt('Name des neuen Ordners:');
|
||
if (!name || !name.trim()) return;
|
||
const slug = name.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
|
||
if (!slug) { alert('Ungültiger Ordnername.'); return; }
|
||
setStatus(elListStatus, `Erstelle Ordner '${slug}'…`);
|
||
try {
|
||
await apiFetch('POST', '/api/folders', { name: slug, dir: currentDir });
|
||
setStatus(elListStatus, `Ordner '${slug}' erstellt`);
|
||
await refresh();
|
||
} catch (err) {
|
||
setStatus(elListStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Auswahl löschen (Toolbar-🗑) ────────────────────────────────────────────
|
||
async function deleteSelected() {
|
||
if (!selectedId) return;
|
||
const dirQ = currentDir ? `?dir=${encodeURIComponent(currentDir)}` : '';
|
||
if (selectedType === 'folder') {
|
||
if (!confirm(`Ordner '${selectedId}' und seinen gesamten Inhalt wirklich löschen?`)) return;
|
||
setStatus(elListStatus, `Lösche Ordner '${selectedId}'…`);
|
||
try {
|
||
await apiFetch('DELETE', `/api/folders/${encodeURIComponent(selectedId)}${dirQ}`);
|
||
setStatus(elListStatus, `Ordner '${selectedId}' gelöscht`);
|
||
selectedId = null; selectedType = null;
|
||
elBtnDeleteSelected.disabled = true;
|
||
await refresh();
|
||
} catch (err) {
|
||
setStatus(elListStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
return;
|
||
}
|
||
if (!confirm(`Programm '${selectedId}' wirklich löschen?`)) return;
|
||
setStatus(elListStatus, `Lösche '${selectedId}'…`);
|
||
try {
|
||
await apiFetch('DELETE', `/api/programs/${encodeURIComponent(selectedId)}${dirQ}`);
|
||
setStatus(elListStatus, `'${selectedId}' gelöscht`);
|
||
selectedId = null; selectedType = null;
|
||
elBtnDeleteSelected.disabled = true;
|
||
await refresh();
|
||
} catch (err) {
|
||
setStatus(elListStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Event-Listener ──────────────────────────────────────────────────────────
|
||
document.getElementById('btn-refresh').addEventListener('click', refresh);
|
||
document.getElementById('btn-new-file').addEventListener('click', createFile);
|
||
document.getElementById('btn-new-folder').addEventListener('click', createFolder);
|
||
document.getElementById('btn-cancel-delete').addEventListener('click', cancelDeletes);
|
||
document.getElementById('btn-save-delete').addEventListener('click', saveDeletes);
|
||
elBtnDeleteSelected.addEventListener('click', deleteSelected);
|
||
elBtnFirst.addEventListener('click', () => step('first'));
|
||
elBtnPrev .addEventListener('click', () => step('prev'));
|
||
elBtnNext .addEventListener('click', () => step('next'));
|
||
elBtnLast .addEventListener('click', () => step('last'));
|
||
elBtnClear.addEventListener('click', clearProgram);
|
||
|
||
// Collapse-Verhalten (wie im appRobotHoming)
|
||
document.querySelectorAll('.section h2').forEach(h2 => {
|
||
h2.addEventListener('click', () => h2.closest('.section').classList.toggle('collapsed'));
|
||
});
|
||
|
||
// ─── HTML escapen ────────────────────────────────────────────────────────────
|
||
function esc(s) {
|
||
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// ─── Start ───────────────────────────────────────────────────────────────────
|
||
refresh();
|
||
setInterval(refresh, REFRESH_MS);
|
||
</script>
|
||
</body>
|
||
</html>
|