Füge Fortschritts- und Geschwindigkeitsanzeigen für den Kopiervorgang hinzu und verbessere die USB-Port-Anzeige
This commit is contained in:
165
app.py
165
app.py
@@ -50,6 +50,8 @@ copy_state = {
|
|||||||
'running': False, 'progress': 0,
|
'running': False, 'progress': 0,
|
||||||
'total': 0, 'done': 0, 'current': '',
|
'total': 0, 'done': 0, 'current': '',
|
||||||
'error': None, 'last_copy': None, 'logs': [],
|
'error': None, 'last_copy': None, 'logs': [],
|
||||||
|
'bytes_total': 0, 'bytes_done': 0,
|
||||||
|
'start_ts': None, 'eta_sec': None, 'speed_bps': 0,
|
||||||
}
|
}
|
||||||
copy_lock = threading.Lock()
|
copy_lock = threading.Lock()
|
||||||
|
|
||||||
@@ -242,6 +244,24 @@ def wifi_monitor():
|
|||||||
# ── USB Geräteerkennung ───────────────────────────────────────────────────────
|
# ── USB Geräteerkennung ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
def usb_port_of(dev_name):
|
def usb_port_of(dev_name):
|
||||||
|
"""Gibt den physischen USB-Port-Pfad zurück (z.B. '2-2').
|
||||||
|
Primär via udevadm, Fallback via sysfs."""
|
||||||
|
# Primär: udevadm (zuverlässiger)
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
['udevadm', 'info', '-q', 'path', '-n', f'/dev/{dev_name}'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if r.returncode == 0:
|
||||||
|
port = None
|
||||||
|
for seg in r.stdout.strip().split('/'):
|
||||||
|
if re.fullmatch(r'\d+-[\d.]+', seg):
|
||||||
|
port = seg
|
||||||
|
if port:
|
||||||
|
return port
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Fallback: sysfs readlink
|
||||||
try:
|
try:
|
||||||
real = Path(f'/sys/block/{dev_name}').resolve()
|
real = Path(f'/sys/block/{dev_name}').resolve()
|
||||||
port = None
|
port = None
|
||||||
@@ -309,13 +329,22 @@ def add_log(msg):
|
|||||||
copy_state['logs'].append({'t': datetime.now().strftime('%H:%M:%S'), 'm': msg})
|
copy_state['logs'].append({'t': datetime.now().strftime('%H:%M:%S'), 'm': msg})
|
||||||
copy_state['logs'] = copy_state['logs'][-200:]
|
copy_state['logs'] = copy_state['logs'][-200:]
|
||||||
|
|
||||||
|
def _fmt_bytes(b):
|
||||||
|
if b < 1024: return f'{b} B'
|
||||||
|
if b < 1024**2: return f'{b/1024:.1f} KB'
|
||||||
|
if b < 1024**3: return f'{b/1024**2:.1f} MB'
|
||||||
|
return f'{b/1024**3:.2f} GB'
|
||||||
|
|
||||||
|
|
||||||
def do_copy(src_dev, dst_dev, cfg):
|
def do_copy(src_dev, dst_dev, cfg):
|
||||||
src_mp = dst_mp = None
|
src_mp = dst_mp = None
|
||||||
src_owned = dst_owned = False
|
src_owned = dst_owned = False
|
||||||
try:
|
try:
|
||||||
with copy_lock:
|
with copy_lock:
|
||||||
copy_state.update(running=True, progress=0, error=None,
|
copy_state.update(running=True, progress=0, error=None,
|
||||||
done=0, total=0, logs=[], current='')
|
done=0, total=0, logs=[], current='',
|
||||||
|
bytes_total=0, bytes_done=0,
|
||||||
|
start_ts=time.time(), eta_sec=None, speed_bps=0)
|
||||||
save_state()
|
save_state()
|
||||||
add_log('Kopiervorgang gestartet')
|
add_log('Kopiervorgang gestartet')
|
||||||
|
|
||||||
@@ -344,9 +373,11 @@ def do_copy(src_dev, dst_dev, cfg):
|
|||||||
src_path = Path(src_mp)
|
src_path = Path(src_mp)
|
||||||
files = [f for f in src_path.rglob('*') if f.is_file()]
|
files = [f for f in src_path.rglob('*') if f.is_file()]
|
||||||
total = len(files)
|
total = len(files)
|
||||||
|
bytes_total = sum(f.stat().st_size for f in files)
|
||||||
with copy_lock:
|
with copy_lock:
|
||||||
copy_state['total'] = total
|
copy_state['total'] = total
|
||||||
add_log(f'{total} Dateien gefunden')
|
copy_state['bytes_total'] = bytes_total
|
||||||
|
add_log(f'{total} Dateien gefunden ({_fmt_bytes(bytes_total)})')
|
||||||
save_state()
|
save_state()
|
||||||
|
|
||||||
for i, f in enumerate(files):
|
for i, f in enumerate(files):
|
||||||
@@ -356,11 +387,22 @@ def do_copy(src_dev, dst_dev, cfg):
|
|||||||
return
|
return
|
||||||
dst_f = dst_dir / f.relative_to(src_path)
|
dst_f = dst_dir / f.relative_to(src_path)
|
||||||
dst_f.parent.mkdir(parents=True, exist_ok=True)
|
dst_f.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fsize = f.stat().st_size
|
||||||
shutil.copy2(f, dst_f)
|
shutil.copy2(f, dst_f)
|
||||||
with copy_lock:
|
with copy_lock:
|
||||||
copy_state.update(done=i+1,
|
copy_state['bytes_done'] += fsize
|
||||||
progress=int((i+1)/total*100) if total else 100,
|
bd = copy_state['bytes_done']
|
||||||
current=str(f.name))
|
bt = copy_state['bytes_total']
|
||||||
|
elapsed = time.time() - copy_state['start_ts']
|
||||||
|
speed = bd / elapsed if elapsed > 1 else 0
|
||||||
|
eta = int((bt - bd) / speed) if speed > 0 and bt > bd else 0
|
||||||
|
copy_state.update(
|
||||||
|
done=i+1,
|
||||||
|
progress=int((i+1)/total*100) if total else 100,
|
||||||
|
current=str(f.name),
|
||||||
|
speed_bps=int(speed),
|
||||||
|
eta_sec=eta,
|
||||||
|
)
|
||||||
if (i+1) % 20 == 0:
|
if (i+1) % 20 == 0:
|
||||||
save_state()
|
save_state()
|
||||||
|
|
||||||
@@ -618,7 +660,7 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e
|
|||||||
.btn-row{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.8rem}
|
.btn-row{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.8rem}
|
||||||
.prog-wrap{margin:.6rem 0 .3rem;height:6px;background:var(--bg);border-radius:3px;overflow:hidden}
|
.prog-wrap{margin:.6rem 0 .3rem;height:6px;background:var(--bg);border-radius:3px;overflow:hidden}
|
||||||
.prog-bar{height:100%;background:var(--acc);border-radius:3px;transition:width .4s ease}
|
.prog-bar{height:100%;background:var(--acc);border-radius:3px;transition:width .4s ease}
|
||||||
.prog-info{font-size:.78rem;color:var(--mut);min-height:1.1rem}
|
.prog-info{font-size:.78rem;color:var(--mut);min-height:1.1rem;font-family:ui-monospace,monospace}
|
||||||
.st-ok{color:var(--grn)}.st-run{color:var(--acc)}.st-err{color:var(--red)}.st-idle{color:var(--mut)}
|
.st-ok{color:var(--grn)}.st-run{color:var(--acc)}.st-err{color:var(--red)}.st-idle{color:var(--mut)}
|
||||||
.field{margin-bottom:.8rem}
|
.field{margin-bottom:.8rem}
|
||||||
.field label{display:block;font-size:.81rem;color:var(--mut);margin-bottom:.3rem}
|
.field label{display:block;font-size:.81rem;color:var(--mut);margin-bottom:.3rem}
|
||||||
@@ -656,8 +698,8 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e
|
|||||||
.pdot{width:10px;height:10px;border-radius:50%;flex-shrink:0;transition:.3s}
|
.pdot{width:10px;height:10px;border-radius:50%;flex-shrink:0;transition:.3s}
|
||||||
.pdot.on{background:var(--grn);box-shadow:0 0 6px var(--grn)}
|
.pdot.on{background:var(--grn);box-shadow:0 0 6px var(--grn)}
|
||||||
.pdot.off{background:var(--brd)}
|
.pdot.off{background:var(--brd)}
|
||||||
.port-dev-name{font-weight:600;font-size:.9rem;line-height:1.3}
|
.port-path{font-weight:700;font-size:1rem;font-family:ui-monospace,monospace;color:var(--txt);line-height:1.3}
|
||||||
.port-dev-sub{font-size:.73rem;color:var(--mut);font-family:monospace;margin-top:.1rem}
|
.port-dev-info{font-size:.75rem;color:var(--mut);margin-top:.15rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
.port-hint{font-size:.73rem;color:var(--mut);margin-top:.65rem;padding:.5rem .65rem;background:var(--bg);border-radius:.4rem;border-left:3px solid var(--brd)}
|
.port-hint{font-size:.73rem;color:var(--mut);margin-top:.65rem;padding:.5rem .65rem;background:var(--bg);border-radius:.4rem;border-left:3px solid var(--brd)}
|
||||||
|
|
||||||
/* ── Port + Explorer grid ── */
|
/* ── Port + Explorer grid ── */
|
||||||
@@ -723,6 +765,10 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e
|
|||||||
<div class="prog-bar" id="prog-bar" style="width:0%"></div>
|
<div class="prog-bar" id="prog-bar" style="width:0%"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="prog-info" class="prog-info"></div>
|
<div id="prog-info" class="prog-info"></div>
|
||||||
|
<div id="eta-row" style="display:none;margin-top:.4rem;display:none">
|
||||||
|
<span id="eta-badge" style="display:inline-flex;align-items:center;gap:.35rem;font-size:.8rem;padding:.2rem .55rem;border-radius:9999px;border:1px solid var(--brd);color:var(--mut)"></span>
|
||||||
|
<span id="speed-badge" style="display:inline-flex;align-items:center;gap:.35rem;font-size:.8rem;padding:.2rem .55rem;border-radius:9999px;border:1px solid var(--brd);color:var(--mut);margin-left:.4rem"></span>
|
||||||
|
</div>
|
||||||
<div id="st-summary" style="font-size:.82rem;color:var(--mut);margin-top:.3rem"></div>
|
<div id="st-summary" style="font-size:.82rem;color:var(--mut);margin-top:.3rem"></div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button id="btn-start" class="btn pri" onclick="startCopy()">▶ Kopieren starten</button>
|
<button id="btn-start" class="btn pri" onclick="startCopy()">▶ Kopieren starten</button>
|
||||||
@@ -743,18 +789,18 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e
|
|||||||
<div class="port-status">
|
<div class="port-status">
|
||||||
<div class="pdot off" id="src-dot"></div>
|
<div class="pdot off" id="src-dot"></div>
|
||||||
<div style="min-width:0">
|
<div style="min-width:0">
|
||||||
<div class="port-dev-name" id="src-dev-name">Nicht verbunden</div>
|
<div class="port-path" id="src-port-path">—</div>
|
||||||
<div class="port-dev-sub" id="src-dev-sub">Kein Port konfiguriert</div>
|
<div class="port-dev-info" id="src-dev-info">Kein Port konfiguriert</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Bezeichnung (frei wählbar)</label>
|
<label>Eigene Bezeichnung für diesen Port</label>
|
||||||
<input type="text" id="src-label" placeholder="z.B. Kamera-Stick">
|
<input type="text" id="src-label" placeholder="z.B. linker USB-3 Port">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Port zuweisen — verbundenes Gerät wählen</label>
|
<label>Physischen Port lernen (Gerät einstecken → wählen)</label>
|
||||||
<select id="src-select">
|
<select id="src-select">
|
||||||
<option value="">— Gerät einstecken & hier wählen —</option>
|
<option value="">— Gerät einstecken, dann hier wählen —</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn sec" style="width:100%" onclick="assignPort('source')">✓ Als feste Quelle speichern</button>
|
<button class="btn sec" style="width:100%" onclick="assignPort('source')">✓ Als feste Quelle speichern</button>
|
||||||
@@ -768,18 +814,18 @@ h2{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08e
|
|||||||
<div class="port-status">
|
<div class="port-status">
|
||||||
<div class="pdot off" id="dst-dot"></div>
|
<div class="pdot off" id="dst-dot"></div>
|
||||||
<div style="min-width:0">
|
<div style="min-width:0">
|
||||||
<div class="port-dev-name" id="dst-dev-name">Nicht verbunden</div>
|
<div class="port-path" id="dst-port-path">—</div>
|
||||||
<div class="port-dev-sub" id="dst-dev-sub">Kein Port konfiguriert</div>
|
<div class="port-dev-info" id="dst-dev-info">Kein Port konfiguriert</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Bezeichnung (frei wählbar)</label>
|
<label>Eigene Bezeichnung für diesen Port</label>
|
||||||
<input type="text" id="dst-label" placeholder="z.B. Backup-Laufwerk">
|
<input type="text" id="dst-label" placeholder="z.B. rechter USB-3 Port">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Port zuweisen — verbundenes Gerät wählen</label>
|
<label>Physischen Port lernen (Gerät einstecken → wählen)</label>
|
||||||
<select id="dst-select">
|
<select id="dst-select">
|
||||||
<option value="">— Gerät einstecken & hier wählen —</option>
|
<option value="">— Gerät einstecken, dann hier wählen —</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn pri" style="width:100%" onclick="assignPort('dest')">✓ Als festes Ziel speichern</button>
|
<button class="btn pri" style="width:100%" onclick="assignPort('dest')">✓ Als festes Ziel speichern</button>
|
||||||
@@ -898,31 +944,38 @@ function renderPortSlots() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSlot(role, port, label) {
|
function renderSlot(role, port, label) {
|
||||||
const isSrc = role === 'src';
|
const isSrc = role === 'src';
|
||||||
const dev = devs.find(d => d.usb_port === port);
|
const dev = devs.find(d => d.usb_port === port);
|
||||||
const dot = $(role+'-dot'), nameEl=$(role+'-dev-name'), subEl=$(role+'-dev-sub');
|
const dot = $(role+'-dot');
|
||||||
const slotEl = $('slot-'+role), lblEl=$(role+'-label');
|
const pathEl = $(role+'-port-path');
|
||||||
|
const infoEl = $(role+'-dev-info');
|
||||||
|
const slotEl = $('slot-'+role);
|
||||||
|
const lblEl = $(role+'-label');
|
||||||
|
|
||||||
slotEl.classList.toggle('has-src', isSrc && !!port);
|
slotEl.classList.toggle('has-src', isSrc && !!port);
|
||||||
slotEl.classList.toggle('has-dst', !isSrc && !!port);
|
slotEl.classList.toggle('has-dst', !isSrc && !!port);
|
||||||
if (dev) {
|
|
||||||
dot.className = 'pdot on';
|
if (port) {
|
||||||
nameEl.textContent = dev.label || dev.device;
|
// Port is configured — show port path PROMINENTLY
|
||||||
subEl.textContent = 'Port '+port+(dev.size?' · '+dev.size:'')+(dev.mount?' · '+dev.mount:'');
|
pathEl.textContent = 'Port ' + port + (label ? ' · ' + label : '');
|
||||||
} else if (port) {
|
if (dev) {
|
||||||
dot.className = 'pdot off';
|
dot.className = 'pdot on';
|
||||||
nameEl.textContent = label || 'Nicht verbunden';
|
infoEl.textContent = (dev.label||dev.device) + (dev.size?' · '+dev.size:'') + (dev.mount?' · '+dev.mount:'');
|
||||||
subEl.textContent = 'Konfiguriert: Port '+port+(label?' · '+label:'');
|
} else {
|
||||||
|
dot.className = 'pdot off';
|
||||||
|
infoEl.textContent = 'Gerät nicht verbunden';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dot.className = 'pdot off';
|
dot.className = 'pdot off';
|
||||||
nameEl.textContent = 'Nicht verbunden';
|
pathEl.textContent = '—';
|
||||||
subEl.textContent = 'Kein Port konfiguriert';
|
infoEl.textContent = 'Kein Port konfiguriert';
|
||||||
}
|
}
|
||||||
if (lblEl && !lblEl.dataset.dirty) lblEl.value = label || '';
|
if (lblEl && !lblEl.dataset.dirty) lblEl.value = label || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateSelects() {
|
function populateSelects() {
|
||||||
const opts = devs.map(d =>
|
const opts = devs.map(d =>
|
||||||
`<option value="${d.usb_port}">${d.label||d.device} — Port ${d.usb_port||'?'} (${d.size})</option>`
|
`<option value="${d.usb_port}">Port ${d.usb_port||'?'} — ${d.label||d.device} (${d.size})</option>`
|
||||||
).join('');
|
).join('');
|
||||||
['src-select','dst-select'].forEach(id => {
|
['src-select','dst-select'].forEach(id => {
|
||||||
const el=$(id), prev=el.value;
|
const el=$(id), prev=el.value;
|
||||||
@@ -955,7 +1008,7 @@ async function assignPort(role) {
|
|||||||
cfg[isSrc?'source_label':'dest_label'] = label;
|
cfg[isSrc?'source_label':'dest_label'] = label;
|
||||||
$(lblId).dataset.dirty='';
|
$(lblId).dataset.dirty='';
|
||||||
await api('/config','POST',cfg);
|
await api('/config','POST',cfg);
|
||||||
flash(fId,'ok','Gespeichert — Port '+port+' ist jetzt feste '+(isSrc?'Quelle':'Ziel')+'.');
|
flash(fId,'ok','Port '+port+' dauerhaft als '+(isSrc?'Quelle':'Ziel')+' gespeichert.');
|
||||||
renderPortSlots(); renderUnassigned();
|
renderPortSlots(); renderUnassigned();
|
||||||
expl.reload();
|
expl.reload();
|
||||||
}
|
}
|
||||||
@@ -1131,10 +1184,23 @@ async function poll() {
|
|||||||
if(c.running){
|
if(c.running){
|
||||||
txt.className='st-run';txt.textContent='Kopiert… '+c.progress+'%';
|
txt.className='st-run';txt.textContent='Kopiert… '+c.progress+'%';
|
||||||
wrap.style.display='block';bar.style.width=c.progress+'%';
|
wrap.style.display='block';bar.style.width=c.progress+'%';
|
||||||
info.textContent=c.current?c.done+' / '+c.total+' — '+c.current:'';
|
// Datei + Bytes-Info
|
||||||
|
const byteInfo = c.bytes_total>0 ? fmtBytes(c.bytes_done)+' / '+fmtBytes(c.bytes_total) : '';
|
||||||
|
info.textContent = (c.current ? c.done+' / '+c.total+' — '+c.current : '') + (byteInfo ? ' ('+byteInfo+')' : '');
|
||||||
|
// ETA + Speed Badges
|
||||||
|
const etaRow=$('eta-row'), etaBadge=$('eta-badge'), speedBadge=$('speed-badge');
|
||||||
|
const etaTxt=fmtETA(c.eta_sec), spdTxt=fmtSpeed(c.speed_bps);
|
||||||
|
if(etaTxt||spdTxt){
|
||||||
|
etaRow.style.display='flex'; etaRow.style.alignItems='center';
|
||||||
|
etaBadge.innerHTML = etaTxt ? '⏱ '+etaTxt : '';
|
||||||
|
etaBadge.style.display = etaTxt ? '' : 'none';
|
||||||
|
speedBadge.innerHTML = spdTxt ? '⚡ '+spdTxt : '';
|
||||||
|
speedBadge.style.display = spdTxt ? '' : 'none';
|
||||||
|
} else { etaRow.style.display='none'; }
|
||||||
sum.textContent='';bS.style.display='none';bC.style.display='';
|
sum.textContent='';bS.style.display='none';bC.style.display='';
|
||||||
} else {
|
} else {
|
||||||
bS.style.display='';bC.style.display='none';info.textContent='';
|
bS.style.display='';bC.style.display='none';info.textContent='';
|
||||||
|
$('eta-row').style.display='none';
|
||||||
if(c.error){txt.className='st-err';txt.textContent='Fehler: '+c.error;wrap.style.display='none';sum.textContent='';}
|
if(c.error){txt.className='st-err';txt.textContent='Fehler: '+c.error;wrap.style.display='none';sum.textContent='';}
|
||||||
else if(c.last_copy){txt.className='st-ok';txt.textContent='✓ Abgeschlossen';wrap.style.display='block';bar.style.width='100%';sum.textContent=c.total+' Dateien · '+new Date(c.last_copy).toLocaleString('de-DE');}
|
else if(c.last_copy){txt.className='st-ok';txt.textContent='✓ Abgeschlossen';wrap.style.display='block';bar.style.width='100%';sum.textContent=c.total+' Dateien · '+new Date(c.last_copy).toLocaleString('de-DE');}
|
||||||
else{txt.className='st-idle';txt.textContent='Bereit';wrap.style.display='none';sum.textContent='';}
|
else{txt.className='st-idle';txt.textContent='Bereit';wrap.style.display='none';sum.textContent='';}
|
||||||
@@ -1144,6 +1210,29 @@ async function poll() {
|
|||||||
} catch(e){}
|
} catch(e){}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtETA(sec) {
|
||||||
|
if (!sec || sec <= 0) return '';
|
||||||
|
if (sec < 60) return 'noch < 1 Min.';
|
||||||
|
const m = Math.round(sec / 60);
|
||||||
|
if (m < 60) return 'noch ca. ' + m + ' Min.';
|
||||||
|
const h = Math.floor(m / 60), rm = m % 60;
|
||||||
|
return 'noch ca. ' + h + ' Std.' + (rm ? ' ' + rm + ' Min.' : '');
|
||||||
|
}
|
||||||
|
function fmtSpeed(bps) {
|
||||||
|
if (!bps || bps <= 0) return '';
|
||||||
|
if (bps < 1024) return bps + ' B/s';
|
||||||
|
if (bps < 1048576) return (bps/1024).toFixed(1) + ' KB/s';
|
||||||
|
if (bps < 1073741824) return (bps/1048576).toFixed(1) + ' MB/s';
|
||||||
|
return (bps/1073741824).toFixed(2) + ' GB/s';
|
||||||
|
}
|
||||||
|
function fmtBytes(b) {
|
||||||
|
if (!b) return '';
|
||||||
|
if (b < 1024) return b + ' B';
|
||||||
|
if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
|
||||||
|
if (b < 1073741824) return (b/1048576).toFixed(1) + ' MB';
|
||||||
|
return (b/1073741824).toFixed(2) + ' GB';
|
||||||
|
}
|
||||||
|
|
||||||
function flash(id,cls,msg){const el=$(id);el.className='flash '+cls;el.textContent=msg;el.style.display='block';if(cls==='ok')setTimeout(()=>el.style.display='none',3500);}
|
function flash(id,cls,msg){const el=$(id);el.className='flash '+cls;el.textContent=msg;el.style.display='block';if(cls==='ok')setTimeout(()=>el.style.display='none',3500);}
|
||||||
|
|
||||||
(async()=>{
|
(async()=>{
|
||||||
|
|||||||
Reference in New Issue
Block a user