feat: Mehrsprachige Unterstützung erweitert und neue Übersetzungen hinzugefügt; Versionsnummer auf 1.0.54 erhöht
This commit is contained in:
279
app.py
279
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(){
|
||||
</div>
|
||||
<label style="margin-left:auto;display:flex;align-items:center;gap:.3rem;font-size:.76rem;cursor:pointer;flex-shrink:0;white-space:nowrap">
|
||||
<input type="checkbox" ${chk} onchange="toggleSrc('${sp.port}',this.checked)">
|
||||
Kopieren
|
||||
${t('src.copy_cb')}
|
||||
</label>
|
||||
<button class="btn sm danger" style="margin-left:.4rem;flex-shrink:0"
|
||||
onclick="removeSource('${sp.port}')">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('') + (ports.length === 0
|
||||
? '<div style="color:var(--sub);font-size:.83rem;margin-bottom:.5rem">Noch keine Quelle konfiguriert.</div>'
|
||||
? '<div style="color:var(--sub);font-size:.83rem;margin-bottom:.5rem">'+t('src.none')+'</div>'
|
||||
: '');
|
||||
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='<div class="empty">Noch keine Fernziele konfiguriert</div>';return;}
|
||||
if(!utTargets.length){el.innerHTML='<div class="empty">'+t('ut.none')+'</div>';return;}
|
||||
el.innerHTML=utTargets.map(t=>`
|
||||
<div class="ut-row ${t.enabled?'on':''}">
|
||||
<span class="ut-ico">🖧</span>
|
||||
@@ -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=>`<option value="${s}">${s}</option>`).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='<div class="expl-empty">Kein Port konfiguriert</div>';bread.innerHTML='';return;}
|
||||
if(!port){body.innerHTML='<div class="expl-empty">'+t('expl.no_port')+'</div>';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='<div class="expl-empty">Gerät nicht verbunden</div>';bread.innerHTML='<span style="color:var(--sub)">Port '+port+' - nicht verbunden</span>';return;}
|
||||
body.innerHTML='<div class="expl-empty">Lade...</div>';
|
||||
if(!dev){body.innerHTML='<div class="expl-empty">'+t('expl.no_dev')+'</div>';bread.innerHTML='<span style="color:var(--sub)">Port '+port+'</span>';return;}
|
||||
body.innerHTML='<div class="expl-empty">'+t('expl.loading')+'</div>';
|
||||
try{
|
||||
const data=await api('/browse?port='+encodeURIComponent(port)+'&path='+encodeURIComponent(path));
|
||||
if(data.error){body.innerHTML='<div class="expl-empty">⚠ '+data.error+'</div>';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='<div class="expl-empty">Verbindungsfehler</div>';}
|
||||
}catch(e){body.innerHTML='<div class="expl-empty">'+t('expl.conn_err')+'</div>';}
|
||||
},
|
||||
_bread(path,label){
|
||||
const el=$('expl-bread');
|
||||
@@ -3204,8 +3319,8 @@ const expl={
|
||||
const par=cur.includes('/')?cur.substring(0,cur.lastIndexOf('/')):'';
|
||||
h+=`<div class="expl-row up" onclick="expl.navigate('${par}')"><span class="expl-ico"><-</span><span class="expl-nm" style="color:var(--sub)">..</span><span></span><span></span></div>`;
|
||||
}
|
||||
if(!entries.length&&!cur){body.innerHTML='<div class="expl-empty">Laufwerk leer</div>';return;}
|
||||
if(!entries.length){body.innerHTML=h+'<div class="expl-empty">Ordner leer</div>';return;}
|
||||
if(!entries.length&&!cur){body.innerHTML='<div class="expl-empty">'+t('expl.empty_drive')+'</div>';return;}
|
||||
if(!entries.length){body.innerHTML=h+'<div class="expl-empty">'+t('expl.empty_folder')+'</div>';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 = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;color:#888;font-size:1rem">↺ Gerät startet neu - bitte warten...</div>';
|
||||
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;color:#888;font-size:1rem">'+t('reboot.wait')+'</div>';
|
||||
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{
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.52
|
||||
1.0.54
|
||||
|
||||
Reference in New Issue
Block a user