diff --git a/app.py b/app.py index a799a14..eaaf4b6 100644 --- a/app.py +++ b/app.py @@ -2612,6 +2612,44 @@ const I18N = { '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. ', + 'flash.saving':'Speichere...', 'flash.testing':'Teste Verbindung – Schreibzugriff wird geprüft...', + 'flash.ok_test':'✓ Verbindung OK – Lesen & Schreiben erfolgreich', + 'flash.fail_timeout':'✗ Test fehlgeschlagen (Server-Timeout)', + 'flash.no_source':'Keine Quelle ausgewählt – bitte mindestens eine Quelle anhaken.', + 'flash.no_device':'Bitte zuerst ein Gerät wählen.', + 'flash.port_as_target':'Port bereits als Ziel konfiguriert!', + 'flash.port_as_source':'Port bereits als Quelle konfiguriert!', + 'flash.port_src_dup':'Port bereits als Quelle hinzugefügt!', + 'flash.no_server':'Server-Adresse fehlt', + 'flash.no_shares':'Verbunden, aber keine Freigaben gefunden', + 'flash.no_name':'Name fehlt', 'flash.no_share':'Bitte eine Freigabe wählen', + 'flash.wifi_connecting':'Verbinde... (bis 30s)', + 'flash.wifi_started':'Gestartet. Neue IP erscheint oben.', + 'flash.ap_saved':'Gespeichert! Hotspot startet neu.', + 'flash.connect_btn':'Verbinde...', + 'flash.src_added':'Quelle Port ${p} hinzugefügt.', + 'flash.dst_saved':'Port ${p} als Ziel gespeichert.', + 'flash.int_saved':'Interner Speicher als Ziel gespeichert.', + 'flash.no_ssid':'Bitte SSID eingeben', + 'flash.pw_short':'Min. 8 Zeichen', + 'btn.testing':'Teste...', 'btn.checking':'Prüfe...', + 'src.none':'Noch keine Quelle konfiguriert.', + 'src.copy_cb':'Kopieren', + 'ut.none':'Noch keine Fernziele konfiguriert', + 'expl.no_port':'Kein Port konfiguriert', 'expl.no_dev':'Gerät nicht verbunden', + 'expl.loading':'Lade...', 'expl.empty_drive':'Laufwerk leer', 'expl.empty_folder':'Ordner leer', + 'expl.conn_err':'Verbindungsfehler', + 'confirm.del_target':'"${n}" wirklich löschen?', + 'confirm.wg_install':'wireguard + wireguard-tools + openresolv jetzt per apt-get installieren?\n\nDauer: ca. 30–90 Sekunden.', + 'confirm.wg_uninstall':'WireGuard wirklich deinstallieren?\n\nDer aktive VPN-Tunnel wird vorher getrennt.\nDie Konfigurationsdatei bleibt erhalten.', + 'confirm.reboot':'Gerät jetzt neu starten?\n\nDas Web-Interface ist für ca. 30 Sekunden nicht erreichbar.', + 'confirm.update':'Update auf v${v} installieren?\n\nDas Web-Interface ist für ca. 10 Sekunden nicht erreichbar.', + 'reboot.wait':'↺ Gerät startet neu – bitte warten...', + 'wg.installing':'Starte Installation...', 'wg.removing':'Deinstalliere...', + 'wg.connecting':'Verbinde VPN...', 'wg.disconnecting':'Trenne VPN...', + 'wg.disc_failed':'Trennen fehlgeschlagen', 'wg.cfg_empty':'Konfiguration ist leer', + 'wg.saving':'Speichere...', 'wg.saved':'✓ Konfiguration gespeichert', + 'copy.cfg_saved':'Gespeichert!', }, en: { 'wifi.connecting':'Connecting...', 'wifi.none':'No Wi-Fi', 'wifi.connected':'Connected', @@ -2622,35 +2660,105 @@ const I18N = { '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. ', + 'flash.saving':'Saving...', 'flash.testing':'Testing connection – checking write access...', + 'flash.ok_test':'✓ Connection OK – Read & Write successful', + 'flash.fail_timeout':'✗ Test failed (server timeout)', + 'flash.no_source':'No source selected – please check at least one source.', + 'flash.no_device':'Please select a device first.', + 'flash.port_as_target':'Port already configured as target!', + 'flash.port_as_source':'Port already configured as source!', + 'flash.port_src_dup':'Port already added as source!', + 'flash.no_server':'Server address missing', + 'flash.no_shares':'Connected, but no shares found', + 'flash.no_name':'Name missing', 'flash.no_share':'Please select a share', + 'flash.wifi_connecting':'Connecting... (up to 30s)', + 'flash.wifi_started':'Started. New IP will appear above.', + 'flash.ap_saved':'Saved! Hotspot restarting.', + 'flash.connect_btn':'Connecting...', + 'flash.src_added':'Source port ${p} added.', + 'flash.dst_saved':'Port ${p} saved as target.', + 'flash.int_saved':'Internal storage saved as target.', + 'flash.no_ssid':'Please enter SSID', + 'flash.pw_short':'Min. 8 characters', + 'btn.testing':'Testing...', 'btn.checking':'Checking...', + 'src.none':'No sources configured yet.', + 'src.copy_cb':'Copy', + 'ut.none':'No remote targets configured', + 'expl.no_port':'No port configured', 'expl.no_dev':'Device not connected', + 'expl.loading':'Loading...', 'expl.empty_drive':'Drive empty', 'expl.empty_folder':'Folder empty', + 'expl.conn_err':'Connection error', + 'confirm.del_target':'Really delete "${n}"?', + 'confirm.wg_install':'Install wireguard + wireguard-tools + openresolv via apt-get?\n\nDuration: approx. 30–90 seconds.', + 'confirm.wg_uninstall':'Really uninstall WireGuard?\n\nThe active VPN tunnel will be disconnected first.\nThe configuration file will be kept.', + 'confirm.reboot':'Restart device now?\n\nThe web interface will be unavailable for approx. 30 seconds.', + 'confirm.update':'Install update v${v}?\n\nThe web interface will be unavailable for approx. 10 seconds.', + 'reboot.wait':'↺ Device restarting – please wait...', + 'wg.installing':'Starting installation...', 'wg.removing':'Uninstalling...', + 'wg.connecting':'Connecting VPN...', 'wg.disconnecting':'Disconnecting VPN...', + 'wg.disc_failed':'Disconnect failed', 'wg.cfg_empty':'Configuration is empty', + 'wg.saving':'Saving...', 'wg.saved':'✓ Configuration saved', + 'copy.cfg_saved':'Saved!', } }; 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', + // Card titles + 'Kopierstatus':'Copy Status', 'USB Ports & Datei-Explorer':'USB Ports & File Explorer', + 'Kopier-Einstellungen':'Copy Settings', 'Fernkopie - NAS / SMB':'Remote Copy - NAS / SMB', + 'WiFi-Einstellungen':'Wi-Fi Settings', 'WireGuard VPN':'WireGuard VPN', + 'System':'System', 'Logs':'Logs', + // Section headers + 'Ordnerstruktur':'Folder Structure', 'Dateifilter':'File Filter', + 'Duplikate':'Duplicates', 'Integrität & Aufräumen':'Integrity & Cleanup', + 'Heimnetz':'Home Network', 'Hotspot (AP)':'Hotspot (AP)', + 'Konfiguration':'Configuration', 'Fernkopie':'Remote Copy', + 'Weitere verbundene Geräte':'Other Connected Devices', + 'Schritt 1 – Server-Verbindung':'Step 1 – Server Connection', + 'Schritt 2 – Freigabe & Details':'Step 2 – Share & Details', + // Labels + 'Bezeichnung':'Label', 'Zieltyp':'Target Type', 'Datumsformat':'Date Format', + 'Uhrzeit im Ordnernamen':'Add Time To Folder Name', + 'Unterordner pro Quelle':'Subfolder Per Source', 'Automatisch kopieren':'Auto Copy', + 'Nur diese Typen kopieren (leer = alle)':'Only These Types (empty = all)', + 'Systemdateien ausschließen':'Exclude System Files', + 'Wenn Zieldatei bereits existiert':'When Target File Exists', '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' + 'Netzwerk (SSID)':'Network (SSID)', 'Passwort':'Password', + 'Hotspot-Name (SSID)':'Hotspot Name (SSID)', 'Passwort (min. 8 Zeichen)':'Password (min. 8 chars)', + 'Port lernen - Gerät wählen':'Learn Port - Select Device', + 'Beim Start automatisch verbinden':'Connect Automatically On Startup', + 'WireGuard Konfiguration (.conf)':'WireGuard Configuration (.conf)', + 'Server (IP / Hostname)':'Server (IP / Hostname)', 'Benutzer':'User', + 'Freigabe wählen':'Select Share', 'Name (Anzeigename)':'Name (display name)', + 'Ziel-Ordner auf dem NAS':'Destination Folder on NAS', + 'Als festes Ziel speichern':'Save As Fixed Target', 'SMB-Freigabe':'SMB Share', + 'Interner Speicher vom Raspberry Pi':'Raspberry Pi Internal Storage', + // Buttons + 'Kopieren starten':'Start Copy', 'Abbrechen':'Cancel', 'Speichern':'Save', + 'Speichern & Neustart':'Save & Restart', 'Verbinden & Speichern':'Connect & Save', + 'NAS-Ziel hinzufügen':'Add NAS Target', 'Quelle hinzufügen':'Add Source', + 'Nach Update suchen':'Check For Update', 'Gerät neu starten':'Restart Device', + 'Deinstallieren':'Uninstall', 'Installieren':'Install', + 'Freigeben':'Share', 'Freigabe stoppen':'Stop Share', + 'Alle':'All', '✕ Alle':'✕ All', '📷 Fotos':'📷 Photos', '🎬 Videos':'🎬 Videos', + 'Konfiguration speichern':'Save Configuration', + // Options + '- Gerät einstecken, dann hier wählen -':'- Insert device, then select here -', + 'USB-Laufwerk':'USB Drive', 'Interner Speicher':'Internal Storage', + 'Überspringen (empfohlen)':'Skip (recommended)', 'Überschreiben':'Overwrite', + 'Umbenennen (_1, _2 ...)':'Rename (_1, _2 ...)', + 'JJJJ-MM-TT (2024-01-15)':'YYYY-MM-DD (2024-01-15)', + 'JJJJ/MM/TT (Unterordner)':'YYYY/MM/DD (Subfolder)', + 'JJJJMMTT (20240115)':'YYYYMMDD (20240115)', + 'TT-MM-JJJJ (15-01-2024)':'DD-MM-YYYY (15-01-2024)', + // States / info + 'Kein Port konfiguriert':'No port configured', 'Gerät nicht verbunden':'Device not connected', + 'Noch keine Einträge':'No entries yet', + 'Port konfigurieren und Gerät verbinden':'Configure port and connect device', + 'Nicht freigegeben':'Not shared', + // Role tags & misc + '▼ Ziel':'▼ Target', '⬇ Ziel':'▼ Target', 'Ziel':'Target', + 'Fotos':'Photos', 'Videos':'Videos', }; function t(k){return (I18N[cfg.ui_lang||'de']&&I18N[cfg.ui_lang||'de'][k])||I18N.de[k]||k;} function applyLang(){ @@ -2671,7 +2779,14 @@ function applyLang(){ '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-user':['(leer = anonym)','(empty = anonymous)'], + 'ut-pass':['(leer = kein Passwort)','(empty = no password)'], 'ut-name':['z.B. Heimserver NAS','e.g. Home server NAS'], + 'ut-dest':['PiCopy','PiCopy'], + 'c-fmt':['%Y-%m-%d','%Y-%m-%d'], + 'c-filter':['jpg, raw, mp4, mov ...','jpg, raw, mp4, mov ...'], + 'ap-ssid':['PiCopy','PiCopy'], + 'ap-pw':['PiCopy,','PiCopy,'], }; Object.entries(ph).forEach(([id,v])=>{const el=$(id); if(el) el.placeholder=lang==='en'?v[1]:v[0];}); } @@ -2797,14 +2912,14 @@ function renderSources(){ `; }).join('') + (ports.length === 0 - ? '
Noch keine Quelle konfiguriert.
' + ? '
'+t('src.none')+'
' : ''); renderExplorerTabs(); } @@ -2850,9 +2965,9 @@ function renderSlot(r, port, label){ if(port){ pp.textContent='Port '+port+(label?' | '+label:''); if(dev){ dot.className='dot on'; pi.textContent=(dev.label||dev.device)+(dev.size?' | '+dev.size:'')+(dev.mount?' | '+dev.mount:''); } - else { dot.className='dot off'; pi.textContent='Gerät nicht verbunden'; } + else { dot.className='dot off'; pi.textContent=t('expl.no_dev'); } } else { - dot.className='dot off'; pp.textContent='-'; pi.textContent='Kein Port konfiguriert'; + dot.className='dot off'; pp.textContent='-'; pi.textContent=t('expl.no_port'); } if(lb && !lb.dataset.dirty) lb.value=label||''; } @@ -2905,14 +3020,14 @@ function renderUnassigned(){ async function addSource(){ const port=$('src-select').value, label=$('src-label').value.trim(); - if(!port){flash('src-flash','err','Bitte zuerst ein Gerät wählen.');return;} - if((cfg.dest_type||'usb')!=='internal' && port===cfg.dest_port){flash('src-flash','err','Port bereits als Ziel konfiguriert!');return;} - if((cfg.source_ports||[]).some(sp=>sp.port===port)){flash('src-flash','err','Port bereits als Quelle hinzugefügt!');return;} + if(!port){flash('src-flash','err',t('flash.no_device'));return;} + if((cfg.dest_type||'usb')!=='internal' && port===cfg.dest_port){flash('src-flash','err',t('flash.port_as_target'));return;} + if((cfg.source_ports||[]).some(sp=>sp.port===port)){flash('src-flash','err',t('flash.port_src_dup'));return;} cfg.source_ports = [...(cfg.source_ports||[]), {port, label}]; selectedPortSet.add(port); await api('/config','POST',cfg); $('src-label').value=''; - flash('src-flash','ok','✓ Quelle Port '+port+' hinzugefügt.'); + flash('src-flash','ok','✓ '+t('flash.src_added').replace('${p}',port)); renderSources(); populateSel(); renderUnassigned(); } @@ -2934,17 +3049,17 @@ async function assignPort(role){ cfg[lk]=cfg.internal_dest_label; $(lid).dataset.dirty=''; await api('/config','POST',cfg); - flash(fid,'ok','✓ Interner Speicher als Ziel gespeichert.'); + flash(fid,'ok','✓ '+t('flash.int_saved')); renderSlot('dst',cfg.dest_port,cfg.dest_label); renderExplorerTabs(); expl.reload(); return; } - if(!port){flash(fid,'err','Bitte zuerst ein Gerät wählen.');return;} - if((cfg.source_ports||[]).some(sp=>sp.port===port)){flash(fid,'err','Port bereits als Quelle konfiguriert!');return;} + if(!port){flash(fid,'err',t('flash.no_device'));return;} + if((cfg.source_ports||[]).some(sp=>sp.port===port)){flash(fid,'err',t('flash.port_as_source'));return;} cfg.dest_type='usb'; cfg[pk]=port; cfg[lk]=label; $(lid).dataset.dirty=''; await api('/config','POST',cfg); - flash(fid,'ok','✓ Port '+port+' als Ziel gespeichert.'); + flash(fid,'ok','✓ '+t('flash.dst_saved').replace('${p}',port)); renderSlot('dst',cfg.dest_port,cfg.dest_label); populateSel(); renderUnassigned(); } @@ -2957,7 +3072,7 @@ async function startCopy(){ _dismissed=false; if(_autoDismissTimer){ clearTimeout(_autoDismissTimer); _autoDismissTimer=null; } const ports=[...(cfg.source_ports||[]).map(sp=>sp.port).filter(p=>selectedPortSet.has(p))]; - if(!ports.length){flash('copy-hint','warn','Keine Quelle ausgewählt – bitte mindestens eine Quelle anhaken.');return;} + if(!ports.length){flash('copy-hint','warn',t('flash.no_source'));return;} const r=await api('/copy/start','POST',{ports}); if(r.error) flash('copy-hint','warn',r.error); else $('copy-hint').style.display='none'; @@ -2994,7 +3109,7 @@ async function saveCopyCfg(){ cfg.verify_checksum=$('c-verify').checked; cfg.delete_source=$('c-delsrc').checked; await api('/config','POST',cfg); - const m=$('copy-cfg-msg'); m.style.display='block'; + const m=$('copy-cfg-msg'); m.textContent=t('copy.cfg_saved'); m.style.display='block'; setTimeout(()=>m.style.display='none',2500); } function setFilter(v){ $('c-filter').value=v; } @@ -3012,19 +3127,19 @@ async function scanNets(){ function pickNet(s){$('w-ssid').value=s;$('net-list').style.display='none';$('w-pw').focus();} async function connectWifi(){ const ssid=$('w-ssid').value.trim(),pw=$('w-pw').value; - if(!ssid){flash('wifi-flash','err','Bitte SSID eingeben');return;} - flash('wifi-flash','ok','Verbinde... (bis 30s)'); + if(!ssid){flash('wifi-flash','err',t('flash.no_ssid'));return;} + flash('wifi-flash','ok',t('flash.wifi_connecting')); const r=await api('/wifi/connect','POST',{ssid,password:pw}); if(r.error) flash('wifi-flash','err',r.error); - else flash('wifi-flash','ok','Gestartet. Neue IP erscheint oben.'); + else flash('wifi-flash','ok',t('flash.wifi_started')); } async function saveAP(){ const s=$('ap-ssid').value.trim(),p=$('ap-pw').value; - if(!s){flash('ap-flash','err','Name fehlt');return;} - if(p.length<8){flash('ap-flash','err','Min. 8 Zeichen');return;} + if(!s){flash('ap-flash','err',t('flash.no_name'));return;} + if(p.length<8){flash('ap-flash','err',t('flash.pw_short'));return;} const r=await api('/wifi/ap','POST',{ssid:s,password:p}); if(r.error) flash('ap-flash','err',r.error); - else flash('ap-flash','ok','Gespeichert! Hotspot startet neu.'); + else flash('ap-flash','ok',t('flash.ap_saved')); } // -- Upload-Ziele -------------------------------------------------------------- @@ -3033,7 +3148,7 @@ let utTargets=[], _utConn={}; async function loadUTs(){utTargets=await api('/upload/targets');renderUTs();applyLang();} function renderUTs(){ const el=$('ut-list'); - if(!utTargets.length){el.innerHTML='
Noch keine Fernziele konfiguriert
';return;} + if(!utTargets.length){el.innerHTML='
'+t('ut.none')+'
';return;} el.innerHTML=utTargets.map(t=>`
🖧 @@ -3063,7 +3178,7 @@ function utToggleForm(){ } async function utConnect(){ const host=$('ut-host').value.trim(); - if(!host){flash('ut-form-flash','err','Server-Adresse fehlt');return;} + if(!host){flash('ut-form-flash','err',t('flash.no_server'));return;} const btn=$('ut-connect-btn'); btn.disabled=true; btn.textContent='Verbinde...'; $('ut-form-flash').style.display='none'; @@ -3072,7 +3187,7 @@ async function utConnect(){ }); btn.disabled=false; btn.innerHTML='🔗 Verbinden & Freigaben laden'; if(r.error){flash('ut-form-flash','err','✗ '+r.error);return;} - if(!r.shares||!r.shares.length){flash('ut-form-flash','warn','Verbunden, aber keine Freigaben gefunden');return;} + if(!r.shares||!r.shares.length){flash('ut-form-flash','warn',t('flash.no_shares'));return;} _utConn={host, user:$('ut-user').value.trim(), pass:$('ut-pass').value}; $('ut-share-sel').innerHTML=r.shares.map(s=>``).join(''); if(!$('ut-name').value) $('ut-name').value=host; @@ -3085,38 +3200,38 @@ function utBack(){ async function utSave(){ const name=$('ut-name').value.trim(), dest=$('ut-dest').value.trim()||'PiCopy'; const share=$('ut-share-sel').value; - if(!name){flash('ut-form-flash','err','Name fehlt');return;} - if(!share){flash('ut-form-flash','err','Bitte eine Freigabe wählen');return;} + if(!name){flash('ut-form-flash','err',t('flash.no_name'));return;} + if(!share){flash('ut-form-flash','err',t('flash.no_share'));return;} const body={type:'smb',name,dest_path:dest,share, host:_utConn.host, user:_utConn.user, pass:_utConn.pass}; - flash('ut-form-flash','warn','Speichere...'); + flash('ut-form-flash','warn',t('flash.saving')); const r=await api('/upload/targets','POST',body); if(r.error){flash('ut-form-flash','err',r.error);return;} - flash('ut-form-flash','warn','Teste Verbindung - Schreibzugriff wird geprüft...'); + flash('ut-form-flash','warn',t('flash.testing')); try{ - const t=await api('/upload/targets/'+r.id+'/test','POST'); - if(t.ok){flash('ut-form-flash','ok','✓ Verbindung OK - Lesen & Schreiben erfolgreich');utToggleForm();await loadUTs();} - else flash('ut-form-flash','err','✗ '+(t.error||'Test fehlgeschlagen')); - }catch(e){flash('ut-form-flash','err','✗ Test fehlgeschlagen (Server-Timeout)');} + const tr=await api('/upload/targets/'+r.id+'/test','POST'); + if(tr.ok){flash('ut-form-flash','ok',t('flash.ok_test'));utToggleForm();await loadUTs();} + else flash('ut-form-flash','err','✗ '+(tr.error||t('flash.fail_timeout').slice(2))); + }catch(e){flash('ut-form-flash','err',t('flash.fail_timeout'));} } async function utTest(id){ const btn=$('ut-test-'+id), res=$('ut-test-result-'+id); - btn.disabled=true; btn.textContent='Teste...'; + btn.disabled=true; btn.textContent=t('btn.testing'); res.style.display='none'; const r=await api('/upload/targets/'+id+'/test','POST'); btn.disabled=false; btn.innerHTML='🔍 Test'; res.style.display='block'; if(r.ok){ res.style.background='rgba(52,211,153,.12)'; res.style.color='var(--grn)'; - res.textContent='✓ Verbindung OK - Lesen & Schreiben erfolgreich'; + res.textContent=t('flash.ok_test'); } else { res.style.background='rgba(248,113,113,.1)'; res.style.color='var(--red)'; - res.textContent='✗ ' + (r.error||'Test fehlgeschlagen'); + res.textContent='✗ ' + (r.error||t('flash.fail_timeout').slice(2)); } } async function utToggle(id){await api('/upload/targets/'+id+'/toggle','POST');await loadUTs();} async function utDel(id,name){ - if(!confirm('"'+name+'" wirklich löschen?'))return; + if(!confirm(t('confirm.del_target').replace('${n}',name)))return; await api('/upload/targets/'+id,'DELETE');await loadUTs(); } @@ -3171,19 +3286,19 @@ const expl={ port=cfg.source_ports&&cfg.source_ports[idx]?cfg.source_ports[idx].port:null; } const body=$('expl-body'), bread=$('expl-bread'); - if(!port){body.innerHTML='
Kein Port konfiguriert
';bread.innerHTML='';return;} + if(!port){body.innerHTML='
'+t('expl.no_port')+'
';bread.innerHTML='';return;} const dev=port==='__internal__' - ? {usb_port:'__internal__',label:'Interner Speicher',device:'internal'} + ? {usb_port:'__internal__',label:t('Interner Speicher')||'Interner Speicher',device:'internal'} : devs.find(d=>d.usb_port===port); - if(!dev){body.innerHTML='
Gerät nicht verbunden
';bread.innerHTML='Port '+port+' - nicht verbunden';return;} - body.innerHTML='
Lade...
'; + if(!dev){body.innerHTML='
'+t('expl.no_dev')+'
';bread.innerHTML='Port '+port+'';return;} + body.innerHTML='
'+t('expl.loading')+'
'; try{ const data=await api('/browse?port='+encodeURIComponent(port)+'&path='+encodeURIComponent(path)); if(data.error){body.innerHTML='
⚠ '+data.error+'
';return;} this.paths[this.role]=data.path||''; // role z.B. 'src_0', 'dst' this._bread(data.path||'',dev.label||dev.device); this._list(data.entries||[],data.path||''); - }catch(e){body.innerHTML='
Verbindungsfehler
';} + }catch(e){body.innerHTML='
'+t('expl.conn_err')+'
';} }, _bread(path,label){ const el=$('expl-bread'); @@ -3204,8 +3319,8 @@ const expl={ const par=cur.includes('/')?cur.substring(0,cur.lastIndexOf('/')):''; h+=`
<-..
`; } - if(!entries.length&&!cur){body.innerHTML='
Laufwerk leer
';return;} - if(!entries.length){body.innerHTML=h+'
Ordner leer
';return;} + if(!entries.length&&!cur){body.innerHTML='
'+t('expl.empty_drive')+'
';return;} + if(!entries.length){body.innerHTML=h+'
'+t('expl.empty_folder')+'
';return;} entries.forEach(e=>{ const ico=e.dir?'📁':fileIcon(e.name); const np=(cur?cur+'/':'')+e.name; @@ -3411,11 +3526,7 @@ async function pollUpdate() { async function installUpdate() { const u = await api('/update/status'); const latest = (u.latest || '?'); - if (!confirm( - 'Update auf v' + latest + ' installieren?\n\n' + - 'PiCopy lädt die neue Version herunter und startet neu.\n' + - 'Das Web-Interface ist für ca. 10 Sekunden nicht erreichbar.' - )) return; + if (!confirm(t('confirm.update').replace('${v}',latest))) return; $('upd-badge').innerHTML = '↓ Installiere...'; $('upd-badge').style.pointerEvents = 'none'; @@ -3467,9 +3578,9 @@ async function checkUpdate() { } async function rebootDevice() { - if (!confirm('Gerät jetzt neu starten?\n\nDas Web-Interface ist für ca. 30 Sekunden nicht erreichbar.')) return; + if (!confirm(t('confirm.reboot'))) return; try { await api('/system/reboot', 'POST'); } catch(e) {} - document.body.innerHTML = '
↺ Gerät startet neu - bitte warten...
'; + document.body.innerHTML = '
'+t('reboot.wait')+'
'; setTimeout(async function waitForRestart() { try { await fetch('/api/update/status'); location.reload(); } catch(e) { setTimeout(waitForRestart, 2000); } @@ -3478,37 +3589,37 @@ async function rebootDevice() { // -- WireGuard VPN ------------------------------------------------------------- async function wgInstall(){ - if(!confirm('wireguard + wireguard-tools jetzt per apt-get installieren?\n\nDauer: ca. 30-90 Sekunden.'))return; - flash('wg-flash','ok','Starte Installation...'); + if(!confirm(t('confirm.wg_install')))return; + flash('wg-flash','ok',t('wg.installing')); const r=await api('/wireguard/install','POST'); if(r.error) flash('wg-flash','err',r.error); } async function wgUninstall(){ - if(!confirm('WireGuard wirklich deinstallieren?\n\nDer aktive VPN-Tunnel wird vorher getrennt.\nDie Konfigurationsdatei bleibt erhalten.'))return; - flash('wg-flash','ok','Deinstalliere...'); + if(!confirm(t('confirm.wg_uninstall')))return; + flash('wg-flash','ok',t('wg.removing')); const r=await api('/wireguard/uninstall','POST'); if(r.error) flash('wg-flash','err',r.error); } async function wgConnect(){ $('wg-btn-connect').disabled=true; - flash('wg-flash','ok','Verbinde VPN...'); + flash('wg-flash','ok',t('wg.connecting')); await api('/wireguard/connect','POST'); } async function wgDisconnect(){ $('wg-btn-disconnect').disabled=true; - flash('wg-flash','ok','Trenne VPN...'); + flash('wg-flash','ok',t('wg.disconnecting')); const r=await api('/wireguard/disconnect','POST'); - if(!r.ok) flash('wg-flash','err','Trennen fehlgeschlagen'); + if(!r.ok) flash('wg-flash','err',t('wg.disc_failed')); } async function wgSaveConfig(){ const content=$('wg-config').value.trim(); - if(!content){flash('wg-flash','err','Konfiguration ist leer');return;} - if(!content.includes('[Interface]')){flash('wg-flash','err','Ungültige Konfiguration - [Interface] fehlt');return;} + if(!content){flash('wg-flash','err',t('wg.cfg_empty'));return;} + if(!content.includes('[Interface]')){flash('wg-flash','err','[Interface] fehlt');return;} const auto=$('wg-auto').checked; - flash('wg-flash','ok','Speichere...'); + flash('wg-flash','ok',t('wg.saving')); const r=await api('/wireguard/config','POST',{content,auto}); if(r.error){flash('wg-flash','err',r.error);return;} - flash('wg-flash','ok','✓ Konfiguration gespeichert'); + flash('wg-flash','ok',t('wg.saved')); } async function loadWgConfig(){ try{ diff --git a/version.txt b/version.txt index 46354d7..970b756 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.0.52 +1.0.54