Remove MultiLang
This commit is contained in:
460
app.py
460
app.py
@@ -59,7 +59,6 @@ DEFAULT_CONFIG = {
|
|||||||
'dest_port': None, 'dest_label': '',
|
'dest_port': None, 'dest_label': '',
|
||||||
'dest_type': 'usb', 'internal_dest_label': 'Interner Speicher',
|
'dest_type': 'usb', 'internal_dest_label': 'Interner Speicher',
|
||||||
'internal_share_enabled': False,
|
'internal_share_enabled': False,
|
||||||
'ui_lang': 'de',
|
|
||||||
'folder_format': '%Y-%m-%d', 'add_time': True,
|
'folder_format': '%Y-%m-%d', 'add_time': True,
|
||||||
'subfolder': True, 'auto_copy': True,
|
'subfolder': True, 'auto_copy': True,
|
||||||
'file_filter': '', 'exclude_system': True,
|
'file_filter': '', 'exclude_system': True,
|
||||||
@@ -1975,11 +1974,7 @@ 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{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)}
|
.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{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}
|
.topbar-wifi~.topbar-wifi,.wdot{width:7px;height:7px;border-radius:50%;transition:.3s;flex-shrink: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.c{background:var(--grn);box-shadow:0 0 6px var(--grn)}
|
||||||
.wdot.a{background:var(--pur)}
|
.wdot.a{background:var(--pur)}
|
||||||
.wdot.d{background:var(--brd2)}
|
.wdot.d{background:var(--brd2)}
|
||||||
@@ -2155,18 +2150,14 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
|||||||
<div class="logo-dot"></div>
|
<div class="logo-dot"></div>
|
||||||
PiCopy
|
PiCopy
|
||||||
</div>
|
</div>
|
||||||
<div id="upd-badge" class="upd-badge" onclick="installUpdate()" title="Klicken zum Installieren" data-title-i18n="title.install">
|
<div id="upd-badge" class="upd-badge" onclick="installUpdate()" title="Klicken zum Installieren">
|
||||||
↑ <span id="upd-version"></span> verfügbar
|
↑ <span id="upd-version"></span> verfügbar
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-wifi">
|
<div class="topbar-wifi">
|
||||||
<div class="wdot d" id="wdot"></div>
|
<div class="wdot d" id="wdot"></div>
|
||||||
<span id="wifi-label" data-i18n="wifi.connecting">Verbinde...</span>
|
<span id="wifi-label">Verbinde...</span>
|
||||||
<span id="wifi-ip"></span>
|
<span id="wifi-ip"></span>
|
||||||
</div>
|
</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 id="vpn-pill" class="topbar-wifi" style="display:none">
|
||||||
<div class="wdot d" id="vpn-dot"></div>
|
<div class="wdot d" id="vpn-dot"></div>
|
||||||
<span id="vpn-label" style="font-weight:600;color:var(--txt)">VPN</span>
|
<span id="vpn-label" style="font-weight:600;color:var(--txt)">VPN</span>
|
||||||
@@ -2182,7 +2173,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
|||||||
<div class="card-icon blue">▶</div>
|
<div class="card-icon blue">▶</div>
|
||||||
<span class="card-title">Kopierstatus</span>
|
<span class="card-title">Kopierstatus</span>
|
||||||
<span class="card-sub" id="st-time"></span>
|
<span class="card-sub" id="st-time"></span>
|
||||||
<button id="st-dismiss" onclick="dismissStatus()" title="Meldung schließen" data-title-i18n="title.dismiss" style="display:none;margin-left:.5rem;background:transparent;border:1px solid var(--brd2);color:var(--sub);border-radius:.35rem;padding:.18rem .45rem;cursor:pointer;font-size:.8rem;line-height:1;transition:.15s" onmouseover="this.style.color='var(--txt)'" onmouseout="this.style.color='var(--sub)'">✕</button>
|
<button id="st-dismiss" onclick="dismissStatus()" title="Meldung schließen" style="display:none;margin-left:.5rem;background:transparent;border:1px solid var(--brd2);color:var(--sub);border-radius:.35rem;padding:.18rem .45rem;cursor:pointer;font-size:.8rem;line-height:1;transition:.15s" onmouseover="this.style.color='var(--txt)'" onmouseout="this.style.color='var(--sub)'">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="st-headline st-idle" id="st-text">Bereit</div>
|
<div class="st-headline st-idle" id="st-text">Bereit</div>
|
||||||
@@ -2311,7 +2302,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
|||||||
<div class="expl-bar">
|
<div class="expl-bar">
|
||||||
<div id="src-tabs" style="display:contents"></div>
|
<div id="src-tabs" style="display:contents"></div>
|
||||||
<button class="etab" id="etab-dst" onclick="expl.switchRole('dst')">⬇ Ziel</button>
|
<button class="etab" id="etab-dst" onclick="expl.switchRole('dst')">⬇ Ziel</button>
|
||||||
<button class="expl-reload" onclick="expl.reload()" title="Neu laden" data-title-i18n="title.reload">↻</button>
|
<button class="expl-reload" onclick="expl.reload()" title="Neu laden">↻</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="expl-bread" id="expl-bread"></div>
|
<div class="expl-bread" id="expl-bread"></div>
|
||||||
<div class="expl-scroll" id="expl-body">
|
<div class="expl-scroll" id="expl-body">
|
||||||
@@ -2461,8 +2452,8 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="tab-strip">
|
<div class="tab-strip">
|
||||||
<div class="tab on" data-tab="tc" onclick="swTab('tc','ta')" data-i18n="Heimnetz">Heimnetz</div>
|
<div class="tab on" data-tab="tc" onclick="swTab('tc','ta')">Heimnetz</div>
|
||||||
<div class="tab" data-tab="ta" onclick="swTab('ta','tc')" data-i18n="Hotspot (AP)">Hotspot (AP)</div>
|
<div class="tab" data-tab="ta" onclick="swTab('ta','tc')">Hotspot (AP)</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tc" class="tpane on">
|
<div id="tc" class="tpane on">
|
||||||
<div style="font-size:.8rem;color:var(--sub);margin-bottom:.75rem;line-height:1.5">Heimnetz für die Router-Verbindung. Ohne Verbindung startet PiCopy automatisch einen eigenen Hotspot.</div>
|
<div style="font-size:.8rem;color:var(--sub);margin-bottom:.75rem;line-height:1.5">Heimnetz für die Router-Verbindung. Ohne Verbindung startet PiCopy automatisch einen eigenen Hotspot.</div>
|
||||||
@@ -2470,7 +2461,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
|||||||
<label>Netzwerk (SSID)</label>
|
<label>Netzwerk (SSID)</label>
|
||||||
<div style="display:flex;gap:.4rem">
|
<div style="display:flex;gap:.4rem">
|
||||||
<input type="text" id="w-ssid" placeholder="WLAN-Name" style="flex:1">
|
<input type="text" id="w-ssid" placeholder="WLAN-Name" style="flex:1">
|
||||||
<button class="btn ghost" onclick="scanNets()" title="Netzwerke suchen" data-title-i18n="title.scan_nets">🔍</button>
|
<button class="btn ghost" onclick="scanNets()" title="Netzwerke suchen">🔍</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="net-list" class="net-list" style="display:none"></div>
|
<div id="net-list" class="net-list" style="display:none"></div>
|
||||||
@@ -2484,9 +2475,9 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
|||||||
<div class="qr-row">
|
<div class="qr-row">
|
||||||
<canvas id="hotspot-qr" width="116" height="116"></canvas>
|
<canvas id="hotspot-qr" width="116" height="116"></canvas>
|
||||||
<div>
|
<div>
|
||||||
<div class="qr-title" data-i18n="qr.title">Direkt öffnen</div>
|
<div class="qr-title">Direkt öffnen</div>
|
||||||
<div class="qr-url" id="hotspot-qr-url">http://10.42.0.1:8080</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 class="qr-sub">Im PiCopy-Hotspot mit dem Handy scannen und die Oberfläche öffnen.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2525,7 +2516,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
|||||||
<span style="font-size:1.3rem;flex-shrink:0" id="wg-pkg-icon">⏳</span>
|
<span style="font-size:1.3rem;flex-shrink:0" id="wg-pkg-icon">⏳</span>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight:600;font-size:.87rem" id="wg-pkg-title">Installiere WireGuard...</div>
|
<div style="font-weight:600;font-size:.87rem" id="wg-pkg-title">Installiere WireGuard...</div>
|
||||||
<div style="font-size:.76rem;color:var(--sub);margin-top:.1rem" data-i18n="wg.apt_running">apt-get läuft – bitte warten (bis 60 s)</div>
|
<div style="font-size:.76rem;color:var(--sub);margin-top:.1rem">apt-get läuft – bitte warten (bis 60 s)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2556,7 +2547,7 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
|||||||
<label class="tog"><input type="checkbox" id="wg-auto"><span>Beim Start automatisch verbinden</span></label>
|
<label class="tog"><input type="checkbox" id="wg-auto"><span>Beim Start automatisch verbinden</span></label>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn pri" onclick="wgSaveConfig()">✓ Konfiguration speichern</button>
|
<button class="btn pri" onclick="wgSaveConfig()">✓ Konfiguration speichern</button>
|
||||||
<button class="btn danger" onclick="wgUninstall()" style="margin-left:auto" title="wireguard-Paket entfernen" data-title-i18n="title.wg_rm">✕ Deinstallieren</button>
|
<button class="btn danger" onclick="wgUninstall()" style="margin-left:auto" title="wireguard-Paket entfernen">✕ Deinstallieren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2602,243 +2593,6 @@ const api = async (p, m='GET', b=null) => {
|
|||||||
return (await fetch('/api'+p,o)).json();
|
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. ',
|
|
||||||
'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',
|
|
||||||
'wg.invalid_cfg':'[Interface] fehlt',
|
|
||||||
'wg.action_install':'Installiere', 'wg.action_remove':'Deinstalliere',
|
|
||||||
'copy.cfg_saved':'Gespeichert!',
|
|
||||||
'copy.error_prefix':'Fehler: ',
|
|
||||||
'status.active':'Aktiv', 'status.inactive':'Inaktiv',
|
|
||||||
'status.configured':'Konfiguriert', 'status.not_cfg':'Nicht konfiguriert',
|
|
||||||
'size.free':' frei',
|
|
||||||
'confirm.samba':'Samba installieren und /opt/picopy/internal als Netzwerkfreigabe PiCopy bereitstellen?\n\nDie Freigabe ist im Netzwerk lesbar erreichbar.',
|
|
||||||
'share.activating':'Aktiviere Freigabe...', 'share.deactivating':'Deaktiviere Freigabe...',
|
|
||||||
'share.active_ok':'✓ Freigabe aktiv', 'share.deactive_ok':'✓ Freigabe deaktiviert',
|
|
||||||
'update.available':'Update v${v} verfügbar – über das Badge oben installieren.',
|
|
||||||
'update.error_prefix':'Fehler: ', 'update.current':'PiCopy ist aktuell.',
|
|
||||||
'update.installing':'↓ Installiere...',
|
|
||||||
'btn.connect_load':'🔗 Verbinden & Freigaben laden',
|
|
||||||
'ut.toggle_close':'✕ Abbrechen', 'ut.toggle_open':'+ NAS-Ziel hinzufügen',
|
|
||||||
'check_update':'Nach Update suchen',
|
|
||||||
'title.install':'Klicken zum Installieren', 'title.dismiss':'Meldung schließen',
|
|
||||||
'title.reload':'Neu laden', 'title.scan_nets':'Netzwerke suchen',
|
|
||||||
'title.wg_rm':'wireguard-Paket entfernen',
|
|
||||||
'wg.apt_running':'apt-get läuft – bitte warten (bis 60 s)',
|
|
||||||
'wg.not_installed':'Nicht installiert',
|
|
||||||
'Heimnetz':'Heimnetz',
|
|
||||||
},
|
|
||||||
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. ',
|
|
||||||
'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',
|
|
||||||
'wg.invalid_cfg':'[Interface] missing',
|
|
||||||
'wg.action_install':'Installing', 'wg.action_remove':'Uninstalling',
|
|
||||||
'copy.cfg_saved':'Saved!',
|
|
||||||
'copy.error_prefix':'Error: ',
|
|
||||||
'status.active':'Active', 'status.inactive':'Inactive',
|
|
||||||
'status.configured':'Configured', 'status.not_cfg':'Not configured',
|
|
||||||
'size.free':' free',
|
|
||||||
'confirm.samba':'Install Samba and share /opt/picopy/internal as network share PiCopy?\n\nThe share will be accessible read-only on the network.',
|
|
||||||
'share.activating':'Activating share...', 'share.deactivating':'Deactivating share...',
|
|
||||||
'share.active_ok':'✓ Share active', 'share.deactive_ok':'✓ Share deactivated',
|
|
||||||
'update.available':'Update v${v} available – install via the badge above.',
|
|
||||||
'update.error_prefix':'Error: ', 'update.current':'PiCopy is up to date.',
|
|
||||||
'update.installing':'↓ Installing...',
|
|
||||||
'btn.connect_load':'🔗 Connect & Load Shares',
|
|
||||||
'ut.toggle_close':'✕ Cancel', 'ut.toggle_open':'+ Add NAS Target',
|
|
||||||
'check_update':'Check For Update',
|
|
||||||
'title.install':'Click to install', 'title.dismiss':'Close message',
|
|
||||||
'title.reload':'Reload', 'title.scan_nets':'Scan networks',
|
|
||||||
'title.wg_rm':'Remove WireGuard package',
|
|
||||||
'wg.apt_running':'apt-get running – please wait (up to 60 s)',
|
|
||||||
'wg.not_installed':'Not installed',
|
|
||||||
'Heimnetz':'Home Network',
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const STATIC_EN = {
|
|
||||||
// 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',
|
|
||||||
'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(){
|
|
||||||
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-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];});
|
|
||||||
document.querySelectorAll('[data-title-i18n]').forEach(el=>{el.title=t(el.dataset.titleI18n);});
|
|
||||||
}
|
|
||||||
async function setLang(lang){
|
|
||||||
cfg.ui_lang=lang;
|
|
||||||
await api('/config','POST',cfg);
|
|
||||||
applyLang();
|
|
||||||
poll();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawHotspotQR(){
|
function drawHotspotQR(){
|
||||||
const url='http://10.42.0.1:8080';
|
const url='http://10.42.0.1:8080';
|
||||||
@@ -2932,7 +2686,6 @@ async function refreshDevices(){
|
|||||||
renderSlot('dst', cfg.dest_port, cfg.dest_label);
|
renderSlot('dst', cfg.dest_port, cfg.dest_label);
|
||||||
renderUnassigned();
|
renderUnassigned();
|
||||||
populateSel();
|
populateSel();
|
||||||
applyLang();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedPortSet = new Set();
|
let selectedPortSet = new Set();
|
||||||
@@ -2955,14 +2708,14 @@ function renderSources(){
|
|||||||
</div>
|
</div>
|
||||||
<label style="margin-left:auto;display:flex;align-items:center;gap:.3rem;font-size:.76rem;cursor:pointer;flex-shrink:0;white-space:nowrap">
|
<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)">
|
<input type="checkbox" ${chk} onchange="toggleSrc('${sp.port}',this.checked)">
|
||||||
${t('src.copy_cb')}
|
${'Kopieren'}
|
||||||
</label>
|
</label>
|
||||||
<button class="btn sm danger" style="margin-left:.4rem;flex-shrink:0"
|
<button class="btn sm danger" style="margin-left:.4rem;flex-shrink:0"
|
||||||
onclick="removeSource('${sp.port}')">✕</button>
|
onclick="removeSource('${sp.port}')">✕</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('') + (ports.length === 0
|
}).join('') + (ports.length === 0
|
||||||
? '<div style="color:var(--sub);font-size:.83rem;margin-bottom:.5rem">'+t('src.none')+'</div>'
|
? '<div style="color:var(--sub);font-size:.83rem;margin-bottom:.5rem">'+'Noch keine Quelle konfiguriert.'+'</div>'
|
||||||
: '');
|
: '');
|
||||||
renderExplorerTabs();
|
renderExplorerTabs();
|
||||||
}
|
}
|
||||||
@@ -3008,9 +2761,9 @@ function renderSlot(r, port, label){
|
|||||||
if(port){
|
if(port){
|
||||||
pp.textContent='Port '+port+(label?' | '+label:'');
|
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:''); }
|
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=t('expl.no_dev'); }
|
else { dot.className='dot off'; pi.textContent='Gerät nicht verbunden'; }
|
||||||
} else {
|
} else {
|
||||||
dot.className='dot off'; pp.textContent='-'; pi.textContent=t('expl.no_port');
|
dot.className='dot off'; pp.textContent='-'; pi.textContent='Kein Port konfiguriert';
|
||||||
}
|
}
|
||||||
if(lb && !lb.dataset.dirty) lb.value=label||'';
|
if(lb && !lb.dataset.dirty) lb.value=label||'';
|
||||||
}
|
}
|
||||||
@@ -3063,14 +2816,14 @@ function renderUnassigned(){
|
|||||||
|
|
||||||
async function addSource(){
|
async function addSource(){
|
||||||
const port=$('src-select').value, label=$('src-label').value.trim();
|
const port=$('src-select').value, label=$('src-label').value.trim();
|
||||||
if(!port){flash('src-flash','err',t('flash.no_device'));return;}
|
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',t('flash.port_as_target'));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',t('flash.port_src_dup'));return;}
|
if((cfg.source_ports||[]).some(sp=>sp.port===port)){flash('src-flash','err','Port bereits als Quelle hinzugefügt!');return;}
|
||||||
cfg.source_ports = [...(cfg.source_ports||[]), {port, label}];
|
cfg.source_ports = [...(cfg.source_ports||[]), {port, label}];
|
||||||
selectedPortSet.add(port);
|
selectedPortSet.add(port);
|
||||||
await api('/config','POST',cfg);
|
await api('/config','POST',cfg);
|
||||||
$('src-label').value='';
|
$('src-label').value='';
|
||||||
flash('src-flash','ok','✓ '+t('flash.src_added').replace('${p}',port));
|
flash('src-flash','ok','✓ '+'Quelle Port ${p} hinzugefügt.'.replace('${p}',port));
|
||||||
renderSources(); populateSel(); renderUnassigned();
|
renderSources(); populateSel(); renderUnassigned();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3092,17 +2845,17 @@ async function assignPort(role){
|
|||||||
cfg[lk]=cfg.internal_dest_label;
|
cfg[lk]=cfg.internal_dest_label;
|
||||||
$(lid).dataset.dirty='';
|
$(lid).dataset.dirty='';
|
||||||
await api('/config','POST',cfg);
|
await api('/config','POST',cfg);
|
||||||
flash(fid,'ok','✓ '+t('flash.int_saved'));
|
flash(fid,'ok','✓ '+'Interner Speicher als Ziel gespeichert.');
|
||||||
renderSlot('dst',cfg.dest_port,cfg.dest_label);
|
renderSlot('dst',cfg.dest_port,cfg.dest_label);
|
||||||
renderExplorerTabs(); expl.reload();
|
renderExplorerTabs(); expl.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(!port){flash(fid,'err',t('flash.no_device'));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',t('flash.port_as_source'));return;}
|
if((cfg.source_ports||[]).some(sp=>sp.port===port)){flash(fid,'err','Port bereits als Quelle konfiguriert!');return;}
|
||||||
cfg.dest_type='usb';
|
cfg.dest_type='usb';
|
||||||
cfg[pk]=port; cfg[lk]=label; $(lid).dataset.dirty='';
|
cfg[pk]=port; cfg[lk]=label; $(lid).dataset.dirty='';
|
||||||
await api('/config','POST',cfg);
|
await api('/config','POST',cfg);
|
||||||
flash(fid,'ok','✓ '+t('flash.dst_saved').replace('${p}',port));
|
flash(fid,'ok','✓ '+'Port ${p} als Ziel gespeichert.'.replace('${p}',port));
|
||||||
renderSlot('dst',cfg.dest_port,cfg.dest_label);
|
renderSlot('dst',cfg.dest_port,cfg.dest_label);
|
||||||
populateSel(); renderUnassigned();
|
populateSel(); renderUnassigned();
|
||||||
}
|
}
|
||||||
@@ -3115,7 +2868,7 @@ async function startCopy(){
|
|||||||
_dismissed=false;
|
_dismissed=false;
|
||||||
if(_autoDismissTimer){ clearTimeout(_autoDismissTimer); _autoDismissTimer=null; }
|
if(_autoDismissTimer){ clearTimeout(_autoDismissTimer); _autoDismissTimer=null; }
|
||||||
const ports=[...(cfg.source_ports||[]).map(sp=>sp.port).filter(p=>selectedPortSet.has(p))];
|
const ports=[...(cfg.source_ports||[]).map(sp=>sp.port).filter(p=>selectedPortSet.has(p))];
|
||||||
if(!ports.length){flash('copy-hint','warn',t('flash.no_source'));return;}
|
if(!ports.length){flash('copy-hint','warn','Keine Quelle ausgewählt – bitte mindestens eine Quelle anhaken.');return;}
|
||||||
const r=await api('/copy/start','POST',{ports});
|
const r=await api('/copy/start','POST',{ports});
|
||||||
if(r.error) flash('copy-hint','warn',r.error);
|
if(r.error) flash('copy-hint','warn',r.error);
|
||||||
else $('copy-hint').style.display='none';
|
else $('copy-hint').style.display='none';
|
||||||
@@ -3141,7 +2894,6 @@ async function loadCfg(){
|
|||||||
$('w-ssid').value=cfg.wifi_ssid||''; $('ap-ssid').value=cfg.ap_ssid||'PiCopy';
|
$('w-ssid').value=cfg.wifi_ssid||''; $('ap-ssid').value=cfg.ap_ssid||'PiCopy';
|
||||||
$('dst-type').value=cfg.dest_type||'usb';
|
$('dst-type').value=cfg.dest_type||'usb';
|
||||||
onDestTypeChange(false);
|
onDestTypeChange(false);
|
||||||
applyLang();
|
|
||||||
}
|
}
|
||||||
async function saveCopyCfg(){
|
async function saveCopyCfg(){
|
||||||
cfg.folder_format=$('c-fmt').value; cfg.add_time=$('c-time').checked;
|
cfg.folder_format=$('c-fmt').value; cfg.add_time=$('c-time').checked;
|
||||||
@@ -3152,7 +2904,7 @@ async function saveCopyCfg(){
|
|||||||
cfg.verify_checksum=$('c-verify').checked;
|
cfg.verify_checksum=$('c-verify').checked;
|
||||||
cfg.delete_source=$('c-delsrc').checked;
|
cfg.delete_source=$('c-delsrc').checked;
|
||||||
await api('/config','POST',cfg);
|
await api('/config','POST',cfg);
|
||||||
const m=$('copy-cfg-msg'); m.textContent=t('copy.cfg_saved'); m.style.display='block';
|
const m=$('copy-cfg-msg'); m.textContent='Gespeichert!'; m.style.display='block';
|
||||||
setTimeout(()=>m.style.display='none',2500);
|
setTimeout(()=>m.style.display='none',2500);
|
||||||
}
|
}
|
||||||
function setFilter(v){ $('c-filter').value=v; }
|
function setFilter(v){ $('c-filter').value=v; }
|
||||||
@@ -3170,28 +2922,28 @@ async function scanNets(){
|
|||||||
function pickNet(s){$('w-ssid').value=s;$('net-list').style.display='none';$('w-pw').focus();}
|
function pickNet(s){$('w-ssid').value=s;$('net-list').style.display='none';$('w-pw').focus();}
|
||||||
async function connectWifi(){
|
async function connectWifi(){
|
||||||
const ssid=$('w-ssid').value.trim(),pw=$('w-pw').value;
|
const ssid=$('w-ssid').value.trim(),pw=$('w-pw').value;
|
||||||
if(!ssid){flash('wifi-flash','err',t('flash.no_ssid'));return;}
|
if(!ssid){flash('wifi-flash','err','Bitte SSID eingeben');return;}
|
||||||
flash('wifi-flash','ok',t('flash.wifi_connecting'));
|
flash('wifi-flash','ok','Verbinde... (bis 30s)');
|
||||||
const r=await api('/wifi/connect','POST',{ssid,password:pw});
|
const r=await api('/wifi/connect','POST',{ssid,password:pw});
|
||||||
if(r.error) flash('wifi-flash','err',r.error);
|
if(r.error) flash('wifi-flash','err',r.error);
|
||||||
else flash('wifi-flash','ok',t('flash.wifi_started'));
|
else flash('wifi-flash','ok','Gestartet. Neue IP erscheint oben.');
|
||||||
}
|
}
|
||||||
async function saveAP(){
|
async function saveAP(){
|
||||||
const s=$('ap-ssid').value.trim(),p=$('ap-pw').value;
|
const s=$('ap-ssid').value.trim(),p=$('ap-pw').value;
|
||||||
if(!s){flash('ap-flash','err',t('flash.no_name'));return;}
|
if(!s){flash('ap-flash','err','Name fehlt');return;}
|
||||||
if(p.length<8){flash('ap-flash','err',t('flash.pw_short'));return;}
|
if(p.length<8){flash('ap-flash','err','Min. 8 Zeichen');return;}
|
||||||
const r=await api('/wifi/ap','POST',{ssid:s,password:p});
|
const r=await api('/wifi/ap','POST',{ssid:s,password:p});
|
||||||
if(r.error) flash('ap-flash','err',r.error);
|
if(r.error) flash('ap-flash','err',r.error);
|
||||||
else flash('ap-flash','ok',t('flash.ap_saved'));
|
else flash('ap-flash','ok','Gespeichert! Hotspot startet neu.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Upload-Ziele --------------------------------------------------------------
|
// -- Upload-Ziele --------------------------------------------------------------
|
||||||
let utTargets=[], _utConn={};
|
let utTargets=[], _utConn={};
|
||||||
|
|
||||||
async function loadUTs(){utTargets=await api('/upload/targets');renderUTs();applyLang();}
|
async function loadUTs(){utTargets=await api('/upload/targets');renderUTs();}
|
||||||
function renderUTs(){
|
function renderUTs(){
|
||||||
const el=$('ut-list');
|
const el=$('ut-list');
|
||||||
if(!utTargets.length){el.innerHTML='<div class="empty">'+t('ut.none')+'</div>';return;}
|
if(!utTargets.length){el.innerHTML='<div class="empty">'+'Noch keine Fernziele konfiguriert'+'</div>';return;}
|
||||||
el.innerHTML=utTargets.map(t=>`
|
el.innerHTML=utTargets.map(t=>`
|
||||||
<div class="ut-row ${t.enabled?'on':''}">
|
<div class="ut-row ${t.enabled?'on':''}">
|
||||||
<span class="ut-ico">🖧</span>
|
<span class="ut-ico">🖧</span>
|
||||||
@@ -3201,7 +2953,7 @@ function renderUTs(){
|
|||||||
</div>
|
</div>
|
||||||
<div class="ut-acts">
|
<div class="ut-acts">
|
||||||
<button class="btn sm ghost" id="ut-test-${t.id}" onclick="utTest('${t.id}')">🔍 Test</button>
|
<button class="btn sm ghost" id="ut-test-${t.id}" onclick="utTest('${t.id}')">🔍 Test</button>
|
||||||
<button class="btn sm ${t.enabled?'grn':'ghost'}" onclick="utToggle('${t.id}')">${t.enabled?t('status.active'):t('status.inactive')}</button>
|
<button class="btn sm ${t.enabled?'grn':'ghost'}" onclick="utToggle('${t.id}')">${t.enabled?'Aktiv':'Inaktiv'}</button>
|
||||||
<button class="btn sm danger" onclick="utDel('${t.id}','${t.name}')">✕</button>
|
<button class="btn sm danger" onclick="utDel('${t.id}','${t.name}')">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="ut-test-result-${t.id}" style="display:none;font-size:.76rem;margin-top:.35rem;padding:.3rem .5rem;border-radius:.35rem"></div>
|
<div id="ut-test-result-${t.id}" style="display:none;font-size:.76rem;margin-top:.35rem;padding:.3rem .5rem;border-radius:.35rem"></div>
|
||||||
@@ -3210,7 +2962,7 @@ function renderUTs(){
|
|||||||
function utToggleForm(){
|
function utToggleForm(){
|
||||||
const f=$('ut-form'),b=$('ut-add-btn'),show=f.style.display==='none';
|
const f=$('ut-form'),b=$('ut-add-btn'),show=f.style.display==='none';
|
||||||
f.style.display=show?'block':'none';
|
f.style.display=show?'block':'none';
|
||||||
b.innerHTML=show?t('ut.toggle_close'):t('ut.toggle_open');
|
b.innerHTML=show?'✕ Abbrechen':'+ NAS-Ziel hinzufügen';
|
||||||
if(show){
|
if(show){
|
||||||
$('ut-step1').style.display=''; $('ut-step2').style.display='none';
|
$('ut-step1').style.display=''; $('ut-step2').style.display='none';
|
||||||
['ut-host','ut-user','ut-pass','ut-name'].forEach(id=>{$(id).value='';});
|
['ut-host','ut-user','ut-pass','ut-name'].forEach(id=>{$(id).value='';});
|
||||||
@@ -3221,16 +2973,16 @@ function utToggleForm(){
|
|||||||
}
|
}
|
||||||
async function utConnect(){
|
async function utConnect(){
|
||||||
const host=$('ut-host').value.trim();
|
const host=$('ut-host').value.trim();
|
||||||
if(!host){flash('ut-form-flash','err',t('flash.no_server'));return;}
|
if(!host){flash('ut-form-flash','err','Server-Adresse fehlt');return;}
|
||||||
const btn=$('ut-connect-btn');
|
const btn=$('ut-connect-btn');
|
||||||
btn.disabled=true; btn.textContent=t('flash.connect_btn');
|
btn.disabled=true; btn.textContent='Verbinde...';
|
||||||
$('ut-form-flash').style.display='none';
|
$('ut-form-flash').style.display='none';
|
||||||
const r=await api('/upload/browse','POST',{
|
const r=await api('/upload/browse','POST',{
|
||||||
host, user:$('ut-user').value.trim(), pass:$('ut-pass').value
|
host, user:$('ut-user').value.trim(), pass:$('ut-pass').value
|
||||||
});
|
});
|
||||||
btn.disabled=false; btn.innerHTML=t('btn.connect_load');
|
btn.disabled=false; btn.innerHTML='🔗 Verbinden & Freigaben laden';
|
||||||
if(r.error){flash('ut-form-flash','err','✗ '+r.error);return;}
|
if(r.error){flash('ut-form-flash','err','✗ '+r.error);return;}
|
||||||
if(!r.shares||!r.shares.length){flash('ut-form-flash','warn',t('flash.no_shares'));return;}
|
if(!r.shares||!r.shares.length){flash('ut-form-flash','warn','Verbunden, aber keine Freigaben gefunden');return;}
|
||||||
_utConn={host, user:$('ut-user').value.trim(), pass:$('ut-pass').value};
|
_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('');
|
$('ut-share-sel').innerHTML=r.shares.map(s=>`<option value="${s}">${s}</option>`).join('');
|
||||||
if(!$('ut-name').value) $('ut-name').value=host;
|
if(!$('ut-name').value) $('ut-name').value=host;
|
||||||
@@ -3243,38 +2995,38 @@ function utBack(){
|
|||||||
async function utSave(){
|
async function utSave(){
|
||||||
const name=$('ut-name').value.trim(), dest=$('ut-dest').value.trim()||'PiCopy';
|
const name=$('ut-name').value.trim(), dest=$('ut-dest').value.trim()||'PiCopy';
|
||||||
const share=$('ut-share-sel').value;
|
const share=$('ut-share-sel').value;
|
||||||
if(!name){flash('ut-form-flash','err',t('flash.no_name'));return;}
|
if(!name){flash('ut-form-flash','err','Name fehlt');return;}
|
||||||
if(!share){flash('ut-form-flash','err',t('flash.no_share'));return;}
|
if(!share){flash('ut-form-flash','err','Bitte eine Freigabe wählen');return;}
|
||||||
const body={type:'smb',name,dest_path:dest,share,
|
const body={type:'smb',name,dest_path:dest,share,
|
||||||
host:_utConn.host, user:_utConn.user, pass:_utConn.pass};
|
host:_utConn.host, user:_utConn.user, pass:_utConn.pass};
|
||||||
flash('ut-form-flash','warn',t('flash.saving'));
|
flash('ut-form-flash','warn','Speichere...');
|
||||||
const r=await api('/upload/targets','POST',body);
|
const r=await api('/upload/targets','POST',body);
|
||||||
if(r.error){flash('ut-form-flash','err',r.error);return;}
|
if(r.error){flash('ut-form-flash','err',r.error);return;}
|
||||||
flash('ut-form-flash','warn',t('flash.testing'));
|
flash('ut-form-flash','warn','Teste Verbindung – Schreibzugriff wird geprüft...');
|
||||||
try{
|
try{
|
||||||
const tr=await api('/upload/targets/'+r.id+'/test','POST');
|
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();}
|
if(tr.ok){flash('ut-form-flash','ok','✓ Verbindung OK – Lesen & Schreiben erfolgreich');utToggleForm();await loadUTs();}
|
||||||
else flash('ut-form-flash','err','✗ '+(tr.error||t('flash.fail_timeout').slice(2)));
|
else flash('ut-form-flash','err','✗ '+(tr.error||'✗ Test fehlgeschlagen (Server-Timeout)'.slice(2)));
|
||||||
}catch(e){flash('ut-form-flash','err',t('flash.fail_timeout'));}
|
}catch(e){flash('ut-form-flash','err','✗ Test fehlgeschlagen (Server-Timeout)');}
|
||||||
}
|
}
|
||||||
async function utTest(id){
|
async function utTest(id){
|
||||||
const btn=$('ut-test-'+id), res=$('ut-test-result-'+id);
|
const btn=$('ut-test-'+id), res=$('ut-test-result-'+id);
|
||||||
btn.disabled=true; btn.textContent=t('btn.testing');
|
btn.disabled=true; btn.textContent='Teste...';
|
||||||
res.style.display='none';
|
res.style.display='none';
|
||||||
const r=await api('/upload/targets/'+id+'/test','POST');
|
const r=await api('/upload/targets/'+id+'/test','POST');
|
||||||
btn.disabled=false; btn.innerHTML='🔍 Test';
|
btn.disabled=false; btn.innerHTML='🔍 Test';
|
||||||
res.style.display='block';
|
res.style.display='block';
|
||||||
if(r.ok){
|
if(r.ok){
|
||||||
res.style.background='rgba(52,211,153,.12)'; res.style.color='var(--grn)';
|
res.style.background='rgba(52,211,153,.12)'; res.style.color='var(--grn)';
|
||||||
res.textContent=t('flash.ok_test');
|
res.textContent='✓ Verbindung OK – Lesen & Schreiben erfolgreich';
|
||||||
} else {
|
} else {
|
||||||
res.style.background='rgba(248,113,113,.1)'; res.style.color='var(--red)';
|
res.style.background='rgba(248,113,113,.1)'; res.style.color='var(--red)';
|
||||||
res.textContent='✗ ' + (r.error||t('flash.fail_timeout').slice(2));
|
res.textContent='✗ ' + (r.error||'✗ Test fehlgeschlagen (Server-Timeout)'.slice(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function utToggle(id){await api('/upload/targets/'+id+'/toggle','POST');await loadUTs();}
|
async function utToggle(id){await api('/upload/targets/'+id+'/toggle','POST');await loadUTs();}
|
||||||
async function utDel(id,name){
|
async function utDel(id,name){
|
||||||
if(!confirm(t('confirm.del_target').replace('${n}',name)))return;
|
if(!confirm('"${n}" wirklich löschen?'.replace('${n}',name)))return;
|
||||||
await api('/upload/targets/'+id,'DELETE');await loadUTs();
|
await api('/upload/targets/'+id,'DELETE');await loadUTs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3282,17 +3034,17 @@ async function updateInternalShareBox(state=null){
|
|||||||
if(!$('internal-share-box'))return;
|
if(!$('internal-share-box'))return;
|
||||||
const s=state||await api('/internal-share/status');
|
const s=state||await api('/internal-share/status');
|
||||||
const btn=$('internal-share-btn'), detail=$('internal-share-detail');
|
const btn=$('internal-share-btn'), detail=$('internal-share-detail');
|
||||||
const free=s.free!=null?fmtBytes(s.free)+t('size.free'):'';
|
const free=s.free!=null?fmtBytes(s.free)+' frei':'';
|
||||||
if(s.pkg_running){
|
if(s.pkg_running){
|
||||||
btn.disabled=true; btn.textContent=t('share.install');
|
btn.disabled=true; btn.textContent='Installiere...';
|
||||||
detail.textContent=t('share.installing')+free;
|
detail.textContent='Samba wird installiert. '+free;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
btn.disabled=false;
|
btn.disabled=false;
|
||||||
btn.textContent=s.enabled?t('share.on'):t('share.off');
|
btn.textContent=s.enabled?'Freigabe stoppen':'Freigeben';
|
||||||
const status=s.enabled
|
const status=s.enabled
|
||||||
? ((s.active?t('status.active'):t('status.configured'))+' | \\\\'+(location.hostname||'picopy')+'\\PiCopy')
|
? ((s.active?'Aktiv':'Konfiguriert')+' | \\\\'+(location.hostname||'picopy')+'\\PiCopy')
|
||||||
: t('share.inactive');
|
: 'Nicht freigegeben';
|
||||||
detail.textContent=status+(free?' | '+free:'');
|
detail.textContent=status+(free?' | '+free:'');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3300,12 +3052,12 @@ async function toggleInternalShare(){
|
|||||||
const current=await api('/internal-share/status');
|
const current=await api('/internal-share/status');
|
||||||
const enable=!current.enabled;
|
const enable=!current.enabled;
|
||||||
if(enable && !current.installed){
|
if(enable && !current.installed){
|
||||||
if(!confirm(t('confirm.samba')))return;
|
if(!confirm('Samba installieren und /opt/picopy/internal als Netzwerkfreigabe PiCopy bereitstellen?\n\nDie Freigabe ist im Netzwerk lesbar erreichbar.'))return;
|
||||||
}
|
}
|
||||||
flash('internal-share-flash','ok',enable?t('share.activating'):t('share.deactivating'));
|
flash('internal-share-flash','ok',enable?'Aktiviere Freigabe...':'Deaktiviere Freigabe...');
|
||||||
const r=await api('/internal-share','POST',{enabled:enable});
|
const r=await api('/internal-share','POST',{enabled:enable});
|
||||||
if(r.error){flash('internal-share-flash','err',r.error);return;}
|
if(r.error){flash('internal-share-flash','err',r.error);return;}
|
||||||
flash('internal-share-flash','ok',enable?t('share.active_ok'):t('share.deactive_ok'));
|
flash('internal-share-flash','ok',enable?'✓ Freigabe aktiv':'✓ Freigabe deaktiviert');
|
||||||
updateInternalShareBox(r.status);
|
updateInternalShareBox(r.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3329,19 +3081,19 @@ const expl={
|
|||||||
port=cfg.source_ports&&cfg.source_ports[idx]?cfg.source_ports[idx].port:null;
|
port=cfg.source_ports&&cfg.source_ports[idx]?cfg.source_ports[idx].port:null;
|
||||||
}
|
}
|
||||||
const body=$('expl-body'), bread=$('expl-bread');
|
const body=$('expl-body'), bread=$('expl-bread');
|
||||||
if(!port){body.innerHTML='<div class="expl-empty">'+t('expl.no_port')+'</div>';bread.innerHTML='';return;}
|
if(!port){body.innerHTML='<div class="expl-empty">'+'Kein Port konfiguriert'+'</div>';bread.innerHTML='';return;}
|
||||||
const dev=port==='__internal__'
|
const dev=port==='__internal__'
|
||||||
? {usb_port:'__internal__',label:t('Interner Speicher')||'Interner Speicher',device:'internal'}
|
? {usb_port:'__internal__',label:'Interner Speicher',device:'internal'}
|
||||||
: devs.find(d=>d.usb_port===port);
|
: devs.find(d=>d.usb_port===port);
|
||||||
if(!dev){body.innerHTML='<div class="expl-empty">'+t('expl.no_dev')+'</div>';bread.innerHTML='<span style="color:var(--sub)">Port '+port+'</span>';return;}
|
if(!dev){body.innerHTML='<div class="expl-empty">'+'Gerät nicht verbunden'+'</div>';bread.innerHTML='<span style="color:var(--sub)">Port '+port+'</span>';return;}
|
||||||
body.innerHTML='<div class="expl-empty">'+t('expl.loading')+'</div>';
|
body.innerHTML='<div class="expl-empty">'+'Lade...'+'</div>';
|
||||||
try{
|
try{
|
||||||
const data=await api('/browse?port='+encodeURIComponent(port)+'&path='+encodeURIComponent(path));
|
const data=await api('/browse?port='+encodeURIComponent(port)+'&path='+encodeURIComponent(path));
|
||||||
if(data.error){body.innerHTML='<div class="expl-empty">⚠ '+data.error+'</div>';return;}
|
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.paths[this.role]=data.path||''; // role z.B. 'src_0', 'dst'
|
||||||
this._bread(data.path||'',dev.label||dev.device);
|
this._bread(data.path||'',dev.label||dev.device);
|
||||||
this._list(data.entries||[],data.path||'');
|
this._list(data.entries||[],data.path||'');
|
||||||
}catch(e){body.innerHTML='<div class="expl-empty">'+t('expl.conn_err')+'</div>';}
|
}catch(e){body.innerHTML='<div class="expl-empty">'+'Verbindungsfehler'+'</div>';}
|
||||||
},
|
},
|
||||||
_bread(path,label){
|
_bread(path,label){
|
||||||
const el=$('expl-bread');
|
const el=$('expl-bread');
|
||||||
@@ -3362,8 +3114,8 @@ const expl={
|
|||||||
const par=cur.includes('/')?cur.substring(0,cur.lastIndexOf('/')):'';
|
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>`;
|
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">'+t('expl.empty_drive')+'</div>';return;}
|
if(!entries.length&&!cur){body.innerHTML='<div class="expl-empty">'+'Laufwerk leer'+'</div>';return;}
|
||||||
if(!entries.length){body.innerHTML=h+'<div class="expl-empty">'+t('expl.empty_folder')+'</div>';return;}
|
if(!entries.length){body.innerHTML=h+'<div class="expl-empty">'+'Ordner leer'+'</div>';return;}
|
||||||
entries.forEach(e=>{
|
entries.forEach(e=>{
|
||||||
const ico=e.dir?'📁':fileIcon(e.name);
|
const ico=e.dir?'📁':fileIcon(e.name);
|
||||||
const np=(cur?cur+'/':'')+e.name;
|
const np=(cur?cur+'/':'')+e.name;
|
||||||
@@ -3427,7 +3179,7 @@ async function poll(){
|
|||||||
vp.style.display='none';
|
vp.style.display='none';
|
||||||
} else if(!v.installed){
|
} else if(!v.installed){
|
||||||
ni.style.display='block'; pp.style.display='none'; ui.style.display='none';
|
ni.style.display='block'; pp.style.display='none'; ui.style.display='none';
|
||||||
$('wg-status-sub').textContent=t('wg.not_installed')||'Nicht installiert';
|
$('wg-status-sub').textContent='Nicht installiert'||'Nicht installiert';
|
||||||
vp.style.display='none';
|
vp.style.display='none';
|
||||||
if(v.pkg_error) flash('wg-flash','err',v.pkg_error);
|
if(v.pkg_error) flash('wg-flash','err',v.pkg_error);
|
||||||
} else {
|
} else {
|
||||||
@@ -3436,26 +3188,26 @@ async function poll(){
|
|||||||
const bc=$('wg-btn-connect'),bd=$('wg-btn-disconnect');
|
const bc=$('wg-btn-connect'),bd=$('wg-btn-disconnect');
|
||||||
if(v.connected){
|
if(v.connected){
|
||||||
vp.style.display='flex'; vdot.className='wdot c';
|
vp.style.display='flex'; vdot.className='wdot c';
|
||||||
vl.textContent=t('vpn.active'); vi.textContent=v.ip||'';
|
vl.textContent='VPN aktiv'; vi.textContent=v.ip||'';
|
||||||
wgd.className='wdot c'; wgl.textContent=t('wifi.connected');
|
wgd.className='wdot c'; wgl.textContent='Verbunden';
|
||||||
wgdet.textContent=v.ip?(v.ip+(v.peer?' | peer ...'+v.peer.slice(-8):'')):'';
|
wgdet.textContent=v.ip?(v.ip+(v.peer?' | peer ...'+v.peer.slice(-8):'')):'';
|
||||||
bc.style.display='none'; bd.style.display=''; bd.disabled=false;
|
bc.style.display='none'; bd.style.display=''; bd.disabled=false;
|
||||||
$('wg-status-sub').textContent=v.ip||'';
|
$('wg-status-sub').textContent=v.ip||'';
|
||||||
} else {
|
} else {
|
||||||
vp.style.display=v.has_config?'flex':'none';
|
vp.style.display=v.has_config?'flex':'none';
|
||||||
vdot.className='wdot d'; vl.textContent='VPN'; vi.textContent='';
|
vdot.className='wdot d'; vl.textContent='VPN'; vi.textContent='';
|
||||||
wgd.className='wdot d'; wgl.textContent=t('vpn.disconnected');
|
wgd.className='wdot d'; wgl.textContent='Getrennt';
|
||||||
wgdet.textContent=v.error||'';
|
wgdet.textContent=v.error||'';
|
||||||
bc.style.display=v.has_config?'':'none'; bc.disabled=false; bd.style.display='none';
|
bc.style.display=v.has_config?'':'none'; bc.disabled=false; bd.style.display='none';
|
||||||
$('wg-status-sub').textContent=v.has_config?t('status.configured'):t('status.not_cfg');
|
$('wg-status-sub').textContent=v.has_config?'Konfiguriert':'Nicht konfiguriert';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// WiFi
|
// WiFi
|
||||||
const wd=$('wdot'),wl=$('wifi-label'),wi=$('wifi-ip');
|
const wd=$('wdot'),wl=$('wifi-label'),wi=$('wifi-ip');
|
||||||
if(w.mode==='client'){wd.className='wdot c';wl.textContent=w.ssid||t('wifi.connected');wi.textContent=w.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=t('wifi.hotspot')+(w.ssid||'PiCopy');wi.textContent='10.42.0.1';}
|
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=t('wifi.none');wi.textContent='';}
|
else{wd.className='wdot d';wl.textContent='Kein WLAN';wi.textContent='';}
|
||||||
const qrBox=$('hotspot-qr-box');
|
const qrBox=$('hotspot-qr-box');
|
||||||
if(qrBox){qrBox.style.display=w.mode==='ap'?'block':'none'; if(w.mode==='ap')drawHotspotQR();}
|
if(qrBox){qrBox.style.display=w.mode==='ap'?'block':'none'; if(w.mode==='ap')drawHotspotQR();}
|
||||||
// Copy
|
// Copy
|
||||||
@@ -3466,22 +3218,22 @@ async function poll(){
|
|||||||
if(c.running){
|
if(c.running){
|
||||||
const ph=c.phase||'copy';
|
const ph=c.phase||'copy';
|
||||||
if(ph==='verify'){
|
if(ph==='verify'){
|
||||||
tx.className='st-headline st-run'; tx.textContent=t('copy.verify')+c.progress+'%';
|
tx.className='st-headline st-run'; tx.textContent='Verifiziere... '+c.progress+'%';
|
||||||
pw.style.display='block'; pf.className='prog-fill'; pf.style.width=c.progress+'%';
|
pw.style.display='block'; pf.className='prog-fill'; pf.style.width=c.progress+'%';
|
||||||
pp.style.display=''; pp.textContent=c.progress+'%';
|
pp.style.display=''; pp.textContent=c.progress+'%';
|
||||||
pfiles.style.display=''; pfiles.textContent=c.done+' / '+c.total+t('copy.checked');
|
pfiles.style.display=''; pfiles.textContent=c.done+' / '+c.total+' geprüft';
|
||||||
pbytes.style.display='none'; eta.style.display='none'; spd.style.display='none';
|
pbytes.style.display='none'; eta.style.display='none'; spd.style.display='none';
|
||||||
cf.textContent=c.current||'';
|
cf.textContent=c.current||'';
|
||||||
} else if(ph==='delete'){
|
} else if(ph==='delete'){
|
||||||
tx.className='st-headline st-run'; tx.textContent=t('copy.delete');
|
tx.className='st-headline st-run'; tx.textContent='Quelle wird geleert...';
|
||||||
pw.style.display='none'; pp.style.display='none'; pfiles.style.display='none';
|
pw.style.display='none'; pp.style.display='none'; pfiles.style.display='none';
|
||||||
pbytes.style.display='none'; eta.style.display='none'; spd.style.display='none';
|
pbytes.style.display='none'; eta.style.display='none'; spd.style.display='none';
|
||||||
cf.textContent='';
|
cf.textContent='';
|
||||||
} else {
|
} else {
|
||||||
tx.className='st-headline st-run'; tx.textContent=t('copy.copying')+c.progress+'%';
|
tx.className='st-headline st-run'; tx.textContent='Kopiert... '+c.progress+'%';
|
||||||
pw.style.display='block'; pf.className='prog-fill'; pf.style.width=c.progress+'%';
|
pw.style.display='block'; pf.className='prog-fill'; pf.style.width=c.progress+'%';
|
||||||
pp.style.display=''; pp.textContent=c.progress+'%';
|
pp.style.display=''; pp.textContent=c.progress+'%';
|
||||||
pfiles.style.display=''; pfiles.textContent=c.done+' / '+c.total+t('copy.files');
|
pfiles.style.display=''; pfiles.textContent=c.done+' / '+c.total+' Dateien';
|
||||||
if(c.bytes_total>0){pbytes.style.display='';pbytes.textContent=fmtBytes(c.bytes_done)+' / '+fmtBytes(c.bytes_total);}else pbytes.style.display='none';
|
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 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:'';
|
const s=fmtSpd(c.speed_bps); spd.style.display=s?'':'none'; spd.textContent=s?'⚡ '+s:'';
|
||||||
@@ -3492,13 +3244,13 @@ async function poll(){
|
|||||||
bS.style.display=''; bC.style.display='none'; cf.textContent='';
|
bS.style.display=''; bC.style.display='none'; cf.textContent='';
|
||||||
eta.style.display='none'; spd.style.display='none'; pfiles.style.display='none'; pbytes.style.display='none'; pp.style.display='none';
|
eta.style.display='none'; spd.style.display='none'; pfiles.style.display='none'; pbytes.style.display='none'; pp.style.display='none';
|
||||||
if(c.error){
|
if(c.error){
|
||||||
tx.className='st-headline st-err'; tx.textContent=t('copy.error_prefix')+c.error;
|
tx.className='st-headline st-err'; tx.textContent='Fehler: '+c.error;
|
||||||
pf.className='prog-fill err'; pw.style.display='block'; pf.style.width='100%';
|
pf.className='prog-fill err'; pw.style.display='block'; pf.style.width='100%';
|
||||||
sum.textContent=''; time.textContent='';
|
sum.textContent=''; time.textContent='';
|
||||||
}else if(c.last_copy && !_dismissed){
|
}else if(c.last_copy && !_dismissed){
|
||||||
tx.className='st-headline st-ok'; tx.textContent=t('copy.done');
|
tx.className='st-headline st-ok'; tx.textContent='✓ Abgeschlossen';
|
||||||
pf.className='prog-fill done'; pw.style.display='block'; pf.style.width='100%';
|
pf.className='prog-fill done'; pw.style.display='block'; pf.style.width='100%';
|
||||||
sum.textContent=c.total+t('copy.files')+' | '+fmtBytes(c.bytes_total);
|
sum.textContent=c.total+' Dateien'+' | '+fmtBytes(c.bytes_total);
|
||||||
time.textContent=new Date(c.last_copy).toLocaleString('de-DE');
|
time.textContent=new Date(c.last_copy).toLocaleString('de-DE');
|
||||||
$('st-dismiss').style.display='';
|
$('st-dismiss').style.display='';
|
||||||
// Auto-dismiss nach 5 Minuten
|
// Auto-dismiss nach 5 Minuten
|
||||||
@@ -3508,7 +3260,7 @@ async function poll(){
|
|||||||
_autoDismissTimer=setTimeout(dismissStatus, remaining*1000);
|
_autoDismissTimer=setTimeout(dismissStatus, remaining*1000);
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
tx.className='st-headline st-idle'; tx.textContent=t('copy.ready');
|
tx.className='st-headline st-idle'; tx.textContent='Bereit';
|
||||||
pw.style.display='none'; sum.textContent=''; time.textContent='';
|
pw.style.display='none'; sum.textContent=''; time.textContent='';
|
||||||
$('st-dismiss').style.display='none';
|
$('st-dismiss').style.display='none';
|
||||||
}
|
}
|
||||||
@@ -3546,7 +3298,7 @@ let _dismissed = false, _autoDismissTimer = null;
|
|||||||
function dismissStatus(){
|
function dismissStatus(){
|
||||||
_dismissed = true;
|
_dismissed = true;
|
||||||
if(_autoDismissTimer){ clearTimeout(_autoDismissTimer); _autoDismissTimer=null; }
|
if(_autoDismissTimer){ clearTimeout(_autoDismissTimer); _autoDismissTimer=null; }
|
||||||
$('st-text').className='st-headline st-idle'; $('st-text').textContent=t('copy.ready');
|
$('st-text').className='st-headline st-idle'; $('st-text').textContent='Bereit';
|
||||||
$('prog-wrap').style.display='none';
|
$('prog-wrap').style.display='none';
|
||||||
$('st-summary').textContent=''; $('st-time').textContent='';
|
$('st-summary').textContent=''; $('st-time').textContent='';
|
||||||
$('st-dismiss').style.display='none';
|
$('st-dismiss').style.display='none';
|
||||||
@@ -3569,9 +3321,9 @@ async function pollUpdate() {
|
|||||||
async function installUpdate() {
|
async function installUpdate() {
|
||||||
const u = await api('/update/status');
|
const u = await api('/update/status');
|
||||||
const latest = (u.latest || '?');
|
const latest = (u.latest || '?');
|
||||||
if (!confirm(t('confirm.update').replace('${v}',latest))) return;
|
if (!confirm('Update auf v${v} installieren?\n\nDas Web-Interface ist für ca. 10 Sekunden nicht erreichbar.'.replace('${v}',latest))) return;
|
||||||
|
|
||||||
$('upd-badge').innerHTML = t('update.installing');
|
$('upd-badge').innerHTML = '↓ Installiere...';
|
||||||
$('upd-badge').style.pointerEvents = 'none';
|
$('upd-badge').style.pointerEvents = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -3591,7 +3343,7 @@ async function installUpdate() {
|
|||||||
|
|
||||||
async function checkUpdate() {
|
async function checkUpdate() {
|
||||||
const btn = event.currentTarget;
|
const btn = event.currentTarget;
|
||||||
btn.disabled = true; btn.innerHTML = '🔍 '+t('btn.checking');
|
btn.disabled = true; btn.innerHTML = '🔍 '+'Prüfe...';
|
||||||
try {
|
try {
|
||||||
await api('/update/check', 'POST');
|
await api('/update/check', 'POST');
|
||||||
// Warten bis der Server-Check abgeschlossen ist (max 15 s, alle 500 ms)
|
// Warten bis der Server-Check abgeschlossen ist (max 15 s, alle 500 ms)
|
||||||
@@ -3604,26 +3356,26 @@ async function checkUpdate() {
|
|||||||
await pollUpdate(); // Badge sofort aktualisieren
|
await pollUpdate(); // Badge sofort aktualisieren
|
||||||
const fl = $('sys-update-flash');
|
const fl = $('sys-update-flash');
|
||||||
if (u.available && u.latest) {
|
if (u.available && u.latest) {
|
||||||
fl.className = 'flash warn'; fl.textContent = t('update.available').replace('${v}',u.latest);
|
fl.className = 'flash warn'; fl.textContent = 'Update v${v} verfügbar – über das Badge oben installieren.'.replace('${v}',u.latest);
|
||||||
} else if (u.error) {
|
} else if (u.error) {
|
||||||
fl.className = 'flash err'; fl.textContent = t('update.error_prefix') + u.error;
|
fl.className = 'flash err'; fl.textContent = 'Fehler: ' + u.error;
|
||||||
} else {
|
} else {
|
||||||
fl.className = 'flash ok'; fl.textContent = t('update.current');
|
fl.className = 'flash ok'; fl.textContent = 'PiCopy ist aktuell.';
|
||||||
}
|
}
|
||||||
fl.style.display = 'block';
|
fl.style.display = 'block';
|
||||||
if (fl.className.includes('ok')) setTimeout(() => fl.style.display = 'none', 3500);
|
if (fl.className.includes('ok')) setTimeout(() => fl.style.display = 'none', 3500);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
const fl = $('sys-update-flash');
|
const fl = $('sys-update-flash');
|
||||||
fl.className = 'flash err'; fl.textContent = t('expl.conn_err'); fl.style.display = 'block';
|
fl.className = 'flash err'; fl.textContent = 'Verbindungsfehler'; fl.style.display = 'block';
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false; btn.innerHTML = '🔍 '+t('check_update');
|
btn.disabled = false; btn.innerHTML = '🔍 '+'Nach Update suchen';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rebootDevice() {
|
async function rebootDevice() {
|
||||||
if (!confirm(t('confirm.reboot'))) return;
|
if (!confirm('Gerät jetzt neu starten?\n\nDas Web-Interface ist für ca. 30 Sekunden nicht erreichbar.')) return;
|
||||||
try { await api('/system/reboot', 'POST'); } catch(e) {}
|
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">'+t('reboot.wait')+'</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">'+'↺ Gerät startet neu – bitte warten...'+'</div>';
|
||||||
setTimeout(async function waitForRestart() {
|
setTimeout(async function waitForRestart() {
|
||||||
try { await fetch('/api/update/status'); location.reload(); }
|
try { await fetch('/api/update/status'); location.reload(); }
|
||||||
catch(e) { setTimeout(waitForRestart, 2000); }
|
catch(e) { setTimeout(waitForRestart, 2000); }
|
||||||
@@ -3632,37 +3384,37 @@ async function rebootDevice() {
|
|||||||
|
|
||||||
// -- WireGuard VPN -------------------------------------------------------------
|
// -- WireGuard VPN -------------------------------------------------------------
|
||||||
async function wgInstall(){
|
async function wgInstall(){
|
||||||
if(!confirm(t('confirm.wg_install')))return;
|
if(!confirm('wireguard + wireguard-tools + openresolv jetzt per apt-get installieren?\n\nDauer: ca. 30–90 Sekunden.'))return;
|
||||||
flash('wg-flash','ok',t('wg.installing'));
|
flash('wg-flash','ok','Starte Installation...');
|
||||||
const r=await api('/wireguard/install','POST');
|
const r=await api('/wireguard/install','POST');
|
||||||
if(r.error) flash('wg-flash','err',r.error);
|
if(r.error) flash('wg-flash','err',r.error);
|
||||||
}
|
}
|
||||||
async function wgUninstall(){
|
async function wgUninstall(){
|
||||||
if(!confirm(t('confirm.wg_uninstall')))return;
|
if(!confirm('WireGuard wirklich deinstallieren?\n\nDer aktive VPN-Tunnel wird vorher getrennt.\nDie Konfigurationsdatei bleibt erhalten.'))return;
|
||||||
flash('wg-flash','ok',t('wg.removing'));
|
flash('wg-flash','ok','Deinstalliere...');
|
||||||
const r=await api('/wireguard/uninstall','POST');
|
const r=await api('/wireguard/uninstall','POST');
|
||||||
if(r.error) flash('wg-flash','err',r.error);
|
if(r.error) flash('wg-flash','err',r.error);
|
||||||
}
|
}
|
||||||
async function wgConnect(){
|
async function wgConnect(){
|
||||||
$('wg-btn-connect').disabled=true;
|
$('wg-btn-connect').disabled=true;
|
||||||
flash('wg-flash','ok',t('wg.connecting'));
|
flash('wg-flash','ok','Verbinde VPN...');
|
||||||
await api('/wireguard/connect','POST');
|
await api('/wireguard/connect','POST');
|
||||||
}
|
}
|
||||||
async function wgDisconnect(){
|
async function wgDisconnect(){
|
||||||
$('wg-btn-disconnect').disabled=true;
|
$('wg-btn-disconnect').disabled=true;
|
||||||
flash('wg-flash','ok',t('wg.disconnecting'));
|
flash('wg-flash','ok','Trenne VPN...');
|
||||||
const r=await api('/wireguard/disconnect','POST');
|
const r=await api('/wireguard/disconnect','POST');
|
||||||
if(!r.ok) flash('wg-flash','err',t('wg.disc_failed'));
|
if(!r.ok) flash('wg-flash','err','Trennen fehlgeschlagen');
|
||||||
}
|
}
|
||||||
async function wgSaveConfig(){
|
async function wgSaveConfig(){
|
||||||
const content=$('wg-config').value.trim();
|
const content=$('wg-config').value.trim();
|
||||||
if(!content){flash('wg-flash','err',t('wg.cfg_empty'));return;}
|
if(!content){flash('wg-flash','err','Konfiguration ist leer');return;}
|
||||||
if(!content.includes('[Interface]')){flash('wg-flash','err',t('wg.invalid_cfg'));return;}
|
if(!content.includes('[Interface]')){flash('wg-flash','err','[Interface] fehlt');return;}
|
||||||
const auto=$('wg-auto').checked;
|
const auto=$('wg-auto').checked;
|
||||||
flash('wg-flash','ok',t('wg.saving'));
|
flash('wg-flash','ok','Speichere...');
|
||||||
const r=await api('/wireguard/config','POST',{content,auto});
|
const r=await api('/wireguard/config','POST',{content,auto});
|
||||||
if(r.error){flash('wg-flash','err',r.error);return;}
|
if(r.error){flash('wg-flash','err',r.error);return;}
|
||||||
flash('wg-flash','ok',t('wg.saved'));
|
flash('wg-flash','ok','✓ Konfiguration gespeichert');
|
||||||
}
|
}
|
||||||
async function loadWgConfig(){
|
async function loadWgConfig(){
|
||||||
try{
|
try{
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.0.55
|
1.0.56
|
||||||
Reference in New Issue
Block a user