feat: Unterstützung für mehrsprachige Benutzeroberfläche hinzugefügt; Versionsnummer auf 1.0.52 erhöht
This commit is contained in:
225
app.py
225
app.py
@@ -59,6 +59,7 @@ DEFAULT_CONFIG = {
|
||||
'dest_port': None, 'dest_label': '',
|
||||
'dest_type': 'usb', 'internal_dest_label': 'Interner Speicher',
|
||||
'internal_share_enabled': False,
|
||||
'ui_lang': 'de',
|
||||
'folder_format': '%Y-%m-%d', 'add_time': True,
|
||||
'subfolder': True, 'auto_copy': True,
|
||||
'file_filter': '', 'exclude_system': True,
|
||||
@@ -1974,6 +1975,10 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
.logo{display:flex;align-items:center;gap:.55rem;font-size:1rem;font-weight:700;letter-spacing:-.02em;color:var(--txt)}
|
||||
.logo-dot{width:8px;height:8px;border-radius:50%;background:var(--acc);box-shadow:0 0 8px var(--acc)}
|
||||
.topbar-wifi{margin-left:auto;display:flex;align-items:center;gap:.6rem;font-size:.82rem;background:var(--surf);border:1px solid var(--brd);border-radius:9999px;padding:.3rem .75rem}
|
||||
.topbar-wifi~.topbar-wifi,.topbar-wifi~.lang-toggle{margin-left:0}
|
||||
.lang-toggle{display:inline-flex;border:1px solid var(--brd);border-radius:9999px;overflow:hidden;background:var(--surf);flex-shrink:0}
|
||||
.lang-toggle button{border:0;background:transparent;color:var(--sub);font-size:.74rem;font-weight:700;padding:.32rem .55rem;cursor:pointer}
|
||||
.lang-toggle button.on{background:var(--acc);color:#fff}
|
||||
.wdot{width:7px;height:7px;border-radius:50%;transition:.3s;flex-shrink:0}
|
||||
.wdot.c{background:var(--grn);box-shadow:0 0 6px var(--grn)}
|
||||
.wdot.a{background:var(--pur)}
|
||||
@@ -2065,6 +2070,13 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
.port-path{font-family:ui-monospace,monospace;font-size:.92rem;font-weight:700;line-height:1.2}
|
||||
.port-info{font-size:.72rem;color:var(--sub);margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.hint-box{font-size:.72rem;color:var(--sub);margin-top:.65rem;padding:.45rem .65rem;background:var(--bg2);border-radius:.4rem;border-left:3px solid var(--brd2);line-height:1.5}
|
||||
.qr-box{display:none;margin-top:.75rem;padding:.65rem .75rem;background:var(--bg2);border:1px solid var(--brd);border-radius:.5rem}
|
||||
.qr-row{display:flex;gap:.75rem;align-items:center}
|
||||
.qr-row canvas{width:112px;height:112px;background:#fff;border-radius:.35rem;padding:.35rem;flex-shrink:0}
|
||||
.qr-title{font-size:.83rem;font-weight:700}
|
||||
.qr-url{font-family:ui-monospace,monospace;font-size:.76rem;color:var(--acc);margin-top:.2rem;word-break:break-all}
|
||||
.qr-sub{font-size:.72rem;color:var(--sub);margin-top:.25rem;line-height:1.4}
|
||||
@media(max-width:430px){.qr-row{align-items:flex-start}.qr-row canvas{width:96px;height:96px}}
|
||||
|
||||
/* -- Port+Explorer grid -- */
|
||||
/* pex-grid: port-pair links, explorer rechts */
|
||||
@@ -2148,9 +2160,13 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
</div>
|
||||
<div class="topbar-wifi">
|
||||
<div class="wdot d" id="wdot"></div>
|
||||
<span id="wifi-label">Verbinde...</span>
|
||||
<span id="wifi-label" data-i18n="wifi.connecting">Verbinde...</span>
|
||||
<span id="wifi-ip"></span>
|
||||
</div>
|
||||
<div class="lang-toggle" title="Language">
|
||||
<button id="lang-de" onclick="setLang('de')">DE</button>
|
||||
<button id="lang-en" onclick="setLang('en')">EN</button>
|
||||
</div>
|
||||
<div id="vpn-pill" class="topbar-wifi" style="display:none">
|
||||
<div class="wdot d" id="vpn-dot"></div>
|
||||
<span id="vpn-label" style="font-weight:600;color:var(--txt)">VPN</span>
|
||||
@@ -2464,6 +2480,16 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
</div>
|
||||
<div id="ta" class="tpane">
|
||||
<div style="font-size:.8rem;color:var(--sub);margin-bottom:.75rem;line-height:1.5">Startet automatisch wenn kein Heimnetz erreichbar ist.<br>IP im Hotspot-Modus: <b style="color:var(--txt)">10.42.0.1:8080</b></div>
|
||||
<div id="hotspot-qr-box" class="qr-box">
|
||||
<div class="qr-row">
|
||||
<canvas id="hotspot-qr" width="116" height="116"></canvas>
|
||||
<div>
|
||||
<div class="qr-title" data-i18n="qr.title">Direkt öffnen</div>
|
||||
<div class="qr-url" id="hotspot-qr-url">http://10.42.0.1:8080</div>
|
||||
<div class="qr-sub" data-i18n="qr.sub">Im PiCopy-Hotspot mit dem Handy scannen und die Oberfläche öffnen.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label>Hotspot-Name (SSID)</label><input type="text" id="ap-ssid" placeholder="PiCopy"></div>
|
||||
<div class="field"><label>Passwort (min. 8 Zeichen)</label><input type="password" id="ap-pw" placeholder="PiCopy,"></div>
|
||||
<button class="btn pri" onclick="saveAP()">✓ Speichern & Neustart</button>
|
||||
@@ -2576,6 +2602,163 @@ const api = async (p, m='GET', b=null) => {
|
||||
return (await fetch('/api'+p,o)).json();
|
||||
};
|
||||
|
||||
const I18N = {
|
||||
de: {
|
||||
'wifi.connecting':'Verbinde...', 'wifi.none':'Kein WLAN', 'wifi.connected':'Verbunden',
|
||||
'wifi.hotspot':'Hotspot: ', 'vpn.active':'VPN aktiv', 'vpn.disconnected':'Getrennt',
|
||||
'copy.ready':'Bereit', 'copy.copying':'Kopiert... ', 'copy.verify':'Verifiziere... ',
|
||||
'copy.delete':'Quelle wird geleert...', 'copy.done':'✓ Abgeschlossen', 'copy.files':' Dateien',
|
||||
'copy.checked':' geprüft', 'qr.title':'Direkt öffnen',
|
||||
'qr.sub':'Im PiCopy-Hotspot mit dem Handy scannen und die Oberfläche öffnen.',
|
||||
'share.on':'Freigabe stoppen', 'share.off':'Freigeben', 'share.inactive':'Nicht freigegeben',
|
||||
'share.install':'Installiere...', 'share.installing':'Samba wird installiert. ',
|
||||
},
|
||||
en: {
|
||||
'wifi.connecting':'Connecting...', 'wifi.none':'No Wi-Fi', 'wifi.connected':'Connected',
|
||||
'wifi.hotspot':'Hotspot: ', 'vpn.active':'VPN active', 'vpn.disconnected':'Disconnected',
|
||||
'copy.ready':'Ready', 'copy.copying':'Copying... ', 'copy.verify':'Verifying... ',
|
||||
'copy.delete':'Clearing source...', 'copy.done':'✓ Complete', 'copy.files':' files',
|
||||
'copy.checked':' checked', 'qr.title':'Open directly',
|
||||
'qr.sub':'Scan while connected to the PiCopy hotspot to open the interface.',
|
||||
'share.on':'Stop share', 'share.off':'Share', 'share.inactive':'Not shared',
|
||||
'share.install':'Installing...', 'share.installing':'Installing Samba. ',
|
||||
}
|
||||
};
|
||||
const STATIC_EN = {
|
||||
'Kopierstatus':'Copy Status', 'Quelle hinzufügen':'Add Source', 'Ziel':'Target',
|
||||
'Bezeichnung':'Label', 'Zieltyp':'Target Type', 'USB-Laufwerk':'USB Drive',
|
||||
'Interner Speicher vom Raspberry Pi':'Raspberry Pi Internal Storage',
|
||||
'Port lernen - Gerät wählen':'Learn Port - Choose Device',
|
||||
'Als festes Ziel speichern':'Save As Fixed Target', 'SMB-Freigabe':'SMB Share',
|
||||
'Kopier-Einstellungen':'Copy Settings', 'Ordnerstruktur':'Folder Structure',
|
||||
'Datumsformat':'Date Format', 'Uhrzeit im Ordnernamen':'Add Time To Folder Name',
|
||||
'Unterordner pro Quelle':'Subfolder Per Source', 'Automatisch kopieren':'Copy Automatically',
|
||||
'Dateifilter':'File Filter', 'Nur diese Typen kopieren (leer = alle)':'Only Copy These Types (empty = all)',
|
||||
'Fotos':'Photos', 'Videos':'Videos', 'Alle':'All', 'Systemdateien ausschließen':'Exclude System Files',
|
||||
'Duplikate':'Duplicates', 'Wenn Zieldatei bereits existiert':'When Target File Already Exists',
|
||||
'Überspringen (empfohlen)':'Skip (recommended)', 'Überschreiben':'Overwrite',
|
||||
'Umbenennen (_1, _2 ...)':'Rename (_1, _2 ...)', 'Integrität & Aufräumen':'Integrity & Cleanup',
|
||||
'Dateien nach Kopieren per MD5 verifizieren':'Verify Files With MD5 After Copy',
|
||||
'Quelldateien nach Kopieren löschen':'Delete Source Files After Copy',
|
||||
'Speichern':'Save', 'Fernkopie - NAS / SMB':'Remote Copy - NAS / SMB',
|
||||
'NAS-Ziel hinzufügen':'Add NAS Target', 'WiFi-Einstellungen':'Wi-Fi Settings',
|
||||
'Heimnetz':'Home Network', 'Hotspot (AP)':'Hotspot (AP)', 'Netzwerk (SSID)':'Network (SSID)',
|
||||
'Passwort':'Password', 'Verbinden & Speichern':'Connect & Save', 'Hotspot-Name (SSID)':'Hotspot Name (SSID)',
|
||||
'Passwort (min. 8 Zeichen)':'Password (min. 8 chars)', 'Speichern & Neustart':'Save & Restart',
|
||||
'WireGuard VPN':'WireGuard VPN', 'Konfiguration':'Configuration', 'Beim Start automatisch verbinden':'Connect Automatically On Startup',
|
||||
'Konfiguration speichern':'Save Configuration', 'Deinstallieren':'Uninstall', 'System':'System',
|
||||
'Nach Update suchen':'Check For Update', 'Gerät neu starten':'Restart Device', 'Logs':'Logs',
|
||||
'Noch keine Einträge':'No entries yet', 'Fernkopie':'Remote Copy', 'Kopieren starten':'Start Copy',
|
||||
'Abbrechen':'Cancel', 'Interner Speicher':'Internal Storage', 'Nicht freigegeben':'Not shared',
|
||||
'Freigeben':'Share', 'Freigabe stoppen':'Stop share'
|
||||
};
|
||||
function t(k){return (I18N[cfg.ui_lang||'de']&&I18N[cfg.ui_lang||'de'][k])||I18N.de[k]||k;}
|
||||
function applyLang(){
|
||||
const lang=cfg.ui_lang||'de';
|
||||
document.documentElement.lang=lang;
|
||||
$('lang-de').classList.toggle('on',lang==='de');
|
||||
$('lang-en').classList.toggle('on',lang==='en');
|
||||
document.querySelectorAll('[data-i18n]').forEach(el=>{el.textContent=t(el.dataset.i18n);});
|
||||
document.querySelectorAll('button,label,span,.card-title,.sec,.hint-box,.expl-empty,option').forEach(el=>{
|
||||
if(el.children.length>0) return;
|
||||
if(!el.dataset.deText) el.dataset.deText=el.textContent.trim();
|
||||
const de=el.dataset.deText;
|
||||
el.textContent=lang==='en'?(STATIC_EN[de]||de):de;
|
||||
});
|
||||
const ph = {
|
||||
'src-label':['z.B. Kamera 1 / linker Port','e.g. Camera 1 / left port'],
|
||||
'dst-label':['z.B. Zielplatte oder Interner Speicher','e.g. target drive or internal storage'],
|
||||
'w-ssid':['WLAN-Name','Wi-Fi name'],
|
||||
'w-pw':['WLAN-Passwort','Wi-Fi password'],
|
||||
'ut-host':['192.168.1.100 oder nas.local','192.168.1.100 or nas.local'],
|
||||
'ut-name':['z.B. Heimserver NAS','e.g. Home server NAS'],
|
||||
};
|
||||
Object.entries(ph).forEach(([id,v])=>{const el=$(id); if(el) el.placeholder=lang==='en'?v[1]:v[0];});
|
||||
}
|
||||
async function setLang(lang){
|
||||
cfg.ui_lang=lang;
|
||||
await api('/config','POST',cfg);
|
||||
applyLang();
|
||||
poll();
|
||||
}
|
||||
|
||||
function drawHotspotQR(){
|
||||
const url='http://10.42.0.1:8080';
|
||||
const c=$('hotspot-qr'); if(!c)return;
|
||||
$('hotspot-qr-url').textContent=url;
|
||||
drawQR(c,url);
|
||||
}
|
||||
|
||||
function drawQR(canvas,text){
|
||||
const version=2,size=25,dataCw=34,ecCw=10;
|
||||
const bytes=[...new TextEncoder().encode(text)];
|
||||
const bits=[];
|
||||
const put=(val,len)=>{for(let i=len-1;i>=0;i--)bits.push((val>>>i)&1);};
|
||||
put(4,4); put(bytes.length,8); bytes.forEach(b=>put(b,8)); put(0,Math.min(4,dataCw*8-bits.length));
|
||||
while(bits.length%8)bits.push(0);
|
||||
const data=[];
|
||||
for(let i=0;i<bits.length;i+=8)data.push(bits.slice(i,i+8).reduce((a,b)=>a*2+b,0));
|
||||
for(let p=0xec;data.length<dataCw;p=p===0xec?0x11:0xec)data.push(p);
|
||||
const gfMul=(x,y)=>{let z=0;for(let i=7;i>=0;i--){z=(z<<1)^((z>>>7)*0x11d);if((y>>>i)&1)z^=x;}return z&255;};
|
||||
const gen=Array(ecCw).fill(0); gen[ecCw-1]=1;
|
||||
for(let i=0,root=1;i<ecCw;i++,root=gfMul(root,2)){
|
||||
for(let j=0;j<ecCw;j++){
|
||||
gen[j]=gfMul(gen[j],root);
|
||||
if(j+1<ecCw)gen[j]^=gen[j+1];
|
||||
}
|
||||
}
|
||||
const rem=Array(ecCw).fill(0);
|
||||
data.forEach(d=>{
|
||||
const factor=d^rem.shift(); rem.push(0);
|
||||
for(let i=0;i<ecCw;i++)rem[i]^=gfMul(gen[i],factor);
|
||||
});
|
||||
const code=data.concat(rem);
|
||||
const m=Array.from({length:size},()=>Array(size).fill(null));
|
||||
const set=(x,y,v)=>{if(x>=0&&y>=0&&x<size&&y<size)m[y][x]=v;};
|
||||
const finder=(x,y)=>{
|
||||
for(let dy=-1;dy<=7;dy++)for(let dx=-1;dx<=7;dx++){
|
||||
const xx=x+dx,yy=y+dy;
|
||||
const on=(dx>=0&&dx<=6&&dy>=0&&dy<=6&&(dx===0||dx===6||dy===0||dy===6||(dx>=2&&dx<=4&&dy>=2&&dy<=4)));
|
||||
set(xx,yy,on?1:0);
|
||||
}
|
||||
};
|
||||
finder(0,0); finder(size-7,0); finder(0,size-7);
|
||||
for(let i=8;i<size-8;i++){set(i,6,i%2?0:1);set(6,i,i%2?0:1);}
|
||||
for(let y=16;y<=20;y++)for(let x=16;x<=20;x++)set(x,y,(x===16||x===20||y===16||y===20||(x===18&&y===18))?1:0);
|
||||
set(8,size-8,1);
|
||||
for(let i=0;i<9;i++){if(m[8][i]===null)set(8,i,0);if(m[i][8]===null)set(i,8,0);}
|
||||
for(let i=0;i<8;i++){if(m[8][size-1-i]===null)set(8,size-1-i,0);if(m[size-1-i][8]===null)set(size-1-i,8,0);}
|
||||
const dataBits=code.flatMap(b=>Array.from({length:8},(_,i)=>(b>>>(7-i))&1));
|
||||
let bi=0,up=true;
|
||||
for(let x=size-1;x>0;x-=2){
|
||||
if(x===6)x--;
|
||||
for(let yi=0;yi<size;yi++){
|
||||
const y=up?size-1-yi:yi;
|
||||
for(let dx=0;dx<2;dx++){
|
||||
const xx=x-dx;
|
||||
if(m[y][xx]!==null)continue;
|
||||
const mask=(x+y)%2===0;
|
||||
m[y][xx]=(dataBits[bi++]||0)^(mask?1:0);
|
||||
}
|
||||
}
|
||||
up=!up;
|
||||
}
|
||||
const fmt=qrFormatBits(1,0);
|
||||
for(let i=0;i<=5;i++)set(8,i,(fmt>>i)&1); set(8,7,(fmt>>6)&1); set(8,8,(fmt>>7)&1); set(7,8,(fmt>>8)&1);
|
||||
for(let i=9;i<15;i++)set(14-i,8,(fmt>>i)&1);
|
||||
for(let i=0;i<8;i++)set(size-1-i,8,(fmt>>i)&1);
|
||||
for(let i=8;i<15;i++)set(8,size-15+i,(fmt>>i)&1);
|
||||
const ctx=canvas.getContext('2d'),scale=Math.floor(canvas.width/(size+8)),off=Math.floor((canvas.width-scale*size)/2);
|
||||
ctx.fillStyle='#fff';ctx.fillRect(0,0,canvas.width,canvas.height);
|
||||
ctx.fillStyle='#0a0f1e';
|
||||
for(let y=0;y<size;y++)for(let x=0;x<size;x++)if(m[y][x])ctx.fillRect(off+x*scale,off+y*scale,scale,scale);
|
||||
}
|
||||
function qrFormatBits(ecl,mask){
|
||||
let data=(ecl<<3)|mask, rem=data;
|
||||
for(let i=0;i<10;i++)rem=(rem<<1)^(((rem>>>9)&1)?0x537:0);
|
||||
return ((data<<10)|rem)^0x5412;
|
||||
}
|
||||
|
||||
// -- Tabs ---------------------------------------------------------------------
|
||||
function swTab(show,hide){
|
||||
$(show).classList.add('on'); $(hide).classList.remove('on');
|
||||
@@ -2591,6 +2774,7 @@ async function refreshDevices(){
|
||||
renderSlot('dst', cfg.dest_port, cfg.dest_label);
|
||||
renderUnassigned();
|
||||
populateSel();
|
||||
applyLang();
|
||||
}
|
||||
|
||||
let selectedPortSet = new Set();
|
||||
@@ -2799,6 +2983,7 @@ async function loadCfg(){
|
||||
$('w-ssid').value=cfg.wifi_ssid||''; $('ap-ssid').value=cfg.ap_ssid||'PiCopy';
|
||||
$('dst-type').value=cfg.dest_type||'usb';
|
||||
onDestTypeChange(false);
|
||||
applyLang();
|
||||
}
|
||||
async function saveCopyCfg(){
|
||||
cfg.folder_format=$('c-fmt').value; cfg.add_time=$('c-time').checked;
|
||||
@@ -2845,7 +3030,7 @@ async function saveAP(){
|
||||
// -- Upload-Ziele --------------------------------------------------------------
|
||||
let utTargets=[], _utConn={};
|
||||
|
||||
async function loadUTs(){utTargets=await api('/upload/targets');renderUTs();}
|
||||
async function loadUTs(){utTargets=await api('/upload/targets');renderUTs();applyLang();}
|
||||
function renderUTs(){
|
||||
const el=$('ut-list');
|
||||
if(!utTargets.length){el.innerHTML='<div class="empty">Noch keine Fernziele konfiguriert</div>';return;}
|
||||
@@ -2941,15 +3126,15 @@ async function updateInternalShareBox(state=null){
|
||||
const btn=$('internal-share-btn'), detail=$('internal-share-detail');
|
||||
const free=s.free!=null?fmtBytes(s.free)+' frei':'';
|
||||
if(s.pkg_running){
|
||||
btn.disabled=true; btn.textContent='Installiere...';
|
||||
detail.textContent='Samba wird installiert. '+free;
|
||||
btn.disabled=true; btn.textContent=t('share.install');
|
||||
detail.textContent=t('share.installing')+free;
|
||||
return;
|
||||
}
|
||||
btn.disabled=false;
|
||||
btn.textContent=s.enabled?'Freigabe stoppen':'Freigeben';
|
||||
btn.textContent=s.enabled?t('share.on'):t('share.off');
|
||||
const status=s.enabled
|
||||
? ((s.active?'Aktiv':'Konfiguriert')+' | \\\\'+(location.hostname||'picopy')+'\\PiCopy')
|
||||
: 'Nicht freigegeben';
|
||||
: t('share.inactive');
|
||||
detail.textContent=status+(free?' | '+free:'');
|
||||
}
|
||||
|
||||
@@ -3093,15 +3278,15 @@ async function poll(){
|
||||
const bc=$('wg-btn-connect'),bd=$('wg-btn-disconnect');
|
||||
if(v.connected){
|
||||
vp.style.display='flex'; vdot.className='wdot c';
|
||||
vl.textContent='VPN aktiv'; vi.textContent=v.ip||'';
|
||||
wgd.className='wdot c'; wgl.textContent='Verbunden';
|
||||
vl.textContent=t('vpn.active'); vi.textContent=v.ip||'';
|
||||
wgd.className='wdot c'; wgl.textContent=t('wifi.connected');
|
||||
wgdet.textContent=v.ip?(v.ip+(v.peer?' | peer ...'+v.peer.slice(-8):'')):'';
|
||||
bc.style.display='none'; bd.style.display=''; bd.disabled=false;
|
||||
$('wg-status-sub').textContent=v.ip||'';
|
||||
} else {
|
||||
vp.style.display=v.has_config?'flex':'none';
|
||||
vdot.className='wdot d'; vl.textContent='VPN'; vi.textContent='';
|
||||
wgd.className='wdot d'; wgl.textContent='Getrennt';
|
||||
wgd.className='wdot d'; wgl.textContent=t('vpn.disconnected');
|
||||
wgdet.textContent=v.error||'';
|
||||
bc.style.display=v.has_config?'':'none'; bc.disabled=false; bd.style.display='none';
|
||||
$('wg-status-sub').textContent=v.has_config?'Konfiguriert':'Nicht konfiguriert';
|
||||
@@ -3110,9 +3295,11 @@ async function poll(){
|
||||
}
|
||||
// WiFi
|
||||
const wd=$('wdot'),wl=$('wifi-label'),wi=$('wifi-ip');
|
||||
if(w.mode==='client'){wd.className='wdot c';wl.textContent=w.ssid||'Verbunden';wi.textContent=w.ip||'';}
|
||||
else if(w.mode==='ap'){wd.className='wdot a';wl.textContent='Hotspot: '+(w.ssid||'PiCopy');wi.textContent='10.42.0.1';}
|
||||
else{wd.className='wdot d';wl.textContent='Kein WLAN';wi.textContent='';}
|
||||
if(w.mode==='client'){wd.className='wdot c';wl.textContent=w.ssid||t('wifi.connected');wi.textContent=w.ip||'';}
|
||||
else if(w.mode==='ap'){wd.className='wdot a';wl.textContent=t('wifi.hotspot')+(w.ssid||'PiCopy');wi.textContent='10.42.0.1';}
|
||||
else{wd.className='wdot d';wl.textContent=t('wifi.none');wi.textContent='';}
|
||||
const qrBox=$('hotspot-qr-box');
|
||||
if(qrBox){qrBox.style.display=w.mode==='ap'?'block':'none'; if(w.mode==='ap')drawHotspotQR();}
|
||||
// Copy
|
||||
const tx=$('st-text'),pf=$('prog-fill'),pw=$('prog-wrap'),pp=$('prog-pct');
|
||||
const pfiles=$('prog-files'),pbytes=$('prog-bytes'),eta=$('eta-pill'),spd=$('speed-pill');
|
||||
@@ -3121,22 +3308,22 @@ async function poll(){
|
||||
if(c.running){
|
||||
const ph=c.phase||'copy';
|
||||
if(ph==='verify'){
|
||||
tx.className='st-headline st-run'; tx.textContent='Verifiziere... '+c.progress+'%';
|
||||
tx.className='st-headline st-run'; tx.textContent=t('copy.verify')+c.progress+'%';
|
||||
pw.style.display='block'; pf.className='prog-fill'; pf.style.width=c.progress+'%';
|
||||
pp.style.display=''; pp.textContent=c.progress+'%';
|
||||
pfiles.style.display=''; pfiles.textContent=c.done+' / '+c.total+' geprüft';
|
||||
pfiles.style.display=''; pfiles.textContent=c.done+' / '+c.total+t('copy.checked');
|
||||
pbytes.style.display='none'; eta.style.display='none'; spd.style.display='none';
|
||||
cf.textContent=c.current||'';
|
||||
} else if(ph==='delete'){
|
||||
tx.className='st-headline st-run'; tx.textContent='Quelle wird geleert...';
|
||||
tx.className='st-headline st-run'; tx.textContent=t('copy.delete');
|
||||
pw.style.display='none'; pp.style.display='none'; pfiles.style.display='none';
|
||||
pbytes.style.display='none'; eta.style.display='none'; spd.style.display='none';
|
||||
cf.textContent='';
|
||||
} else {
|
||||
tx.className='st-headline st-run'; tx.textContent='Kopiert... '+c.progress+'%';
|
||||
tx.className='st-headline st-run'; tx.textContent=t('copy.copying')+c.progress+'%';
|
||||
pw.style.display='block'; pf.className='prog-fill'; pf.style.width=c.progress+'%';
|
||||
pp.style.display=''; pp.textContent=c.progress+'%';
|
||||
pfiles.style.display=''; pfiles.textContent=c.done+' / '+c.total+' Dateien';
|
||||
pfiles.style.display=''; pfiles.textContent=c.done+' / '+c.total+t('copy.files');
|
||||
if(c.bytes_total>0){pbytes.style.display='';pbytes.textContent=fmtBytes(c.bytes_done)+' / '+fmtBytes(c.bytes_total);}else pbytes.style.display='none';
|
||||
const e=fmtETA(c.eta_sec); eta.style.display=e?'':'none'; eta.textContent=e?'⏱ '+e:'';
|
||||
const s=fmtSpd(c.speed_bps); spd.style.display=s?'':'none'; spd.textContent=s?'⚡ '+s:'';
|
||||
@@ -3151,7 +3338,7 @@ async function poll(){
|
||||
pf.className='prog-fill err'; pw.style.display='block'; pf.style.width='100%';
|
||||
sum.textContent=''; time.textContent='';
|
||||
}else if(c.last_copy && !_dismissed){
|
||||
tx.className='st-headline st-ok'; tx.textContent='✓ Abgeschlossen';
|
||||
tx.className='st-headline st-ok'; tx.textContent=t('copy.done');
|
||||
pf.className='prog-fill done'; pw.style.display='block'; pf.style.width='100%';
|
||||
sum.textContent=c.total+' Dateien | '+fmtBytes(c.bytes_total);
|
||||
time.textContent=new Date(c.last_copy).toLocaleString('de-DE');
|
||||
@@ -3163,7 +3350,7 @@ async function poll(){
|
||||
_autoDismissTimer=setTimeout(dismissStatus, remaining*1000);
|
||||
}
|
||||
}else{
|
||||
tx.className='st-headline st-idle'; tx.textContent='Bereit';
|
||||
tx.className='st-headline st-idle'; tx.textContent=t('copy.ready');
|
||||
pw.style.display='none'; sum.textContent=''; time.textContent='';
|
||||
$('st-dismiss').style.display='none';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user