feat: Unterstützung für die Auswahl mehrerer Quellgeräte hinzugefügt und Versionsnummer auf 1.0.24 erhöht

This commit is contained in:
2026-05-09 12:01:32 +02:00
parent e06c464fb0
commit 6abc1e23c7
2 changed files with 55 additions and 13 deletions

64
app.py
View File

@@ -983,9 +983,13 @@ def r_start():
return jsonify(error='Abbruch wird noch abgeschlossen - bitte kurz warten und erneut versuchen.'), 400 return jsonify(error='Abbruch wird noch abgeschlossen - bitte kurz warten und erneut versuchen.'), 400
cfg = load_cfg() cfg = load_cfg()
devs = usb_devices() devs = usb_devices()
body = request.get_json(force=True) or {}
wanted_ports = body.get('ports') # None = alle konfigurierten Quellen
src_ports = _resolve_source_ports(cfg) src_ports = _resolve_source_ports(cfg)
srcs = [next((d for d in devs if d['usb_port'] == sp['port']), None) for sp in src_ports] srcs = [next((d for d in devs if d['usb_port'] == sp['port']), None) for sp in src_ports]
srcs = [s for s in srcs if s is not None] srcs = [s for s in srcs if s is not None]
if wanted_ports is not None:
srcs = [s for s in srcs if s['usb_port'] in wanted_ports]
if not srcs: return jsonify(error='Keine Quellgeräte gefunden (Ports nicht verbunden)'), 400 if not srcs: return jsonify(error='Keine Quellgeräte gefunden (Ports nicht verbunden)'), 400
dst = next((d for d in devs if d['usb_port'] == cfg.get('dest_port')), None) dst = next((d for d in devs if d['usb_port'] == cfg.get('dest_port')), None)
if not dst: return jsonify(error='Zielgerät nicht gefunden'), 400 if not dst: return jsonify(error='Zielgerät nicht gefunden'), 400
@@ -1726,9 +1730,9 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
<!-- File Explorer --> <!-- File Explorer -->
<div class="expl-wrap"> <div class="expl-wrap">
<div class="expl-bar"> <div class="expl-bar">
<button class="etab on" id="etab-src" onclick="expl.switchRole('src')">⬆ Quelle</button> <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">-></button> <button class="expl-reload" onclick="expl.reload()" title="Neu laden">&#8635;</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">
@@ -2004,6 +2008,8 @@ async function refreshDevices(){
populateSel(); populateSel();
} }
let selectedPortSet = new Set();
function renderSources(){ function renderSources(){
const ports = cfg.source_ports || []; const ports = cfg.source_ports || [];
$('sources-list').innerHTML = ports.map((sp, i) => { $('sources-list').innerHTML = ports.map((sp, i) => {
@@ -2011,6 +2017,7 @@ function renderSources(){
const info = dev const info = dev
? (dev.label||dev.device) + (dev.size ? ' | '+dev.size : '') ? (dev.label||dev.device) + (dev.size ? ' | '+dev.size : '')
: 'Gerät nicht verbunden'; : 'Gerät nicht verbunden';
const chk = selectedPortSet.has(sp.port) ? 'checked' : '';
return `<div class="port-slot ${dev?'src-on':''}" style="margin-bottom:.5rem"> return `<div class="port-slot ${dev?'src-on':''}" style="margin-bottom:.5rem">
<div class="role-tag src">&#9650; Quelle ${i+1}${sp.label?' '+sp.label:''}</div> <div class="role-tag src">&#9650; Quelle ${i+1}${sp.label?' '+sp.label:''}</div>
<div class="port-display"> <div class="port-display">
@@ -2019,13 +2026,36 @@ function renderSources(){
<div class="port-path">Port ${sp.port}</div> <div class="port-path">Port ${sp.port}</div>
<div class="port-info">${info}</div> <div class="port-info">${info}</div>
</div> </div>
<button class="btn sm danger" style="margin-left:auto;flex-shrink:0" <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
</label>
<button class="btn sm danger" style="margin-left:.4rem;flex-shrink:0"
onclick="removeSource('${sp.port}')">&#10005;</button> onclick="removeSource('${sp.port}')">&#10005;</button>
</div> </div>
</div>`; </div>`;
}).join('') + (ports.length === 0 }).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">Noch keine Quelle konfiguriert.</div>'
: ''); : '');
renderExplorerTabs();
}
function toggleSrc(port, on){
if(on) selectedPortSet.add(port); else selectedPortSet.delete(port);
}
function renderExplorerTabs(){
const ports = cfg.source_ports || [];
$('src-tabs').innerHTML = ports.map((sp, i) => {
const r = 'src_'+i;
const label = sp.label || ('Quelle '+(i+1));
return `<button class="etab ${expl.role===r?'on':''}" id="etab-${r}"
onclick="expl.switchRole('${r}')">&#9650; ${label}</button>`;
}).join('');
// Fallback: falls aktive Rolle nicht mehr existiert
if(expl.role!=='dst' && !ports.some((_,i)=>expl.role==='src_'+i)){
expl.role = ports.length>0 ? 'src_0' : 'dst';
}
} }
function renderSlot(r, port, label){ function renderSlot(r, port, label){
@@ -2081,6 +2111,7 @@ async function addSource(){
if(port===cfg.dest_port){flash('src-flash','err','Port bereits als Ziel konfiguriert!');return;} if(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((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);
await api('/config','POST',cfg); await api('/config','POST',cfg);
$('src-label').value=''; $('src-label').value='';
flash('src-flash','ok','✓ Quelle Port '+port+' hinzugefügt.'); flash('src-flash','ok','✓ Quelle Port '+port+' hinzugefügt.');
@@ -2089,6 +2120,7 @@ async function addSource(){
async function removeSource(port){ async function removeSource(port){
cfg.source_ports = (cfg.source_ports||[]).filter(sp=>sp.port!==port); cfg.source_ports = (cfg.source_ports||[]).filter(sp=>sp.port!==port);
selectedPortSet.delete(port);
await api('/config','POST',cfg); await api('/config','POST',cfg);
renderSources(); populateSel(); renderUnassigned(); renderSources(); populateSel(); renderUnassigned();
} }
@@ -2113,7 +2145,9 @@ async function assignPort(role){
async function startCopy(){ async function startCopy(){
_dismissed=false; _dismissed=false;
if(_autoDismissTimer){ clearTimeout(_autoDismissTimer); _autoDismissTimer=null; } if(_autoDismissTimer){ clearTimeout(_autoDismissTimer); _autoDismissTimer=null; }
const r=await api('/copy/start','POST'); 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;}
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';
} }
@@ -2126,6 +2160,8 @@ async function loadCfg(){
if(!cfg.source_ports) cfg.source_ports=[]; if(!cfg.source_ports) cfg.source_ports=[];
if(cfg.source_ports.length===0 && cfg.source_port) if(cfg.source_ports.length===0 && cfg.source_port)
cfg.source_ports=[{port:cfg.source_port, label:cfg.source_label||''}]; cfg.source_ports=[{port:cfg.source_port, label:cfg.source_label||''}];
// Alle konfigurierten Quellen standardmäßig ausgewählt
selectedPortSet = new Set(cfg.source_ports.map(sp=>sp.port));
$('c-fmt').value=cfg.folder_format||'%Y-%m-%d'; $('c-fmt').value=cfg.folder_format||'%Y-%m-%d';
$('c-time').checked=!!cfg.add_time; $('c-sub').checked=!!cfg.subfolder; $('c-auto').checked=!!cfg.auto_copy; $('c-time').checked=!!cfg.add_time; $('c-sub').checked=!!cfg.subfolder; $('c-auto').checked=!!cfg.auto_copy;
$('c-filter').value=cfg.file_filter||''; $('c-filter').value=cfg.file_filter||'';
@@ -2251,17 +2287,23 @@ async function utDel(id,name){
// -- File Explorer ------------------------------------------------------------- // -- File Explorer -------------------------------------------------------------
const expl={ const expl={
role:'src', paths:{src:'',dst:''}, role:'src_0', paths:{dst:''},
switchRole(r){ switchRole(r){
this.role=r; this.role=r;
$('etab-src').classList.toggle('on',r==='src'); document.querySelectorAll('.etab').forEach(t=>t.classList.remove('on'));
$('etab-dst').classList.toggle('on',r==='dst'); const tab=$('etab-'+r); if(tab) tab.classList.add('on');
this.load(this.paths[r]); this.load(this.paths[r]||'');
}, },
reload(){this.load(this.paths[this.role]);}, reload(){this.load(this.paths[this.role]||'');},
navigate(p){this.load(p);}, navigate(p){this.load(p);},
async load(path=''){ async load(path=''){
const port=this.role==='src'?(cfg.source_ports&&cfg.source_ports[0]?.port):cfg.dest_port; let port;
if(this.role==='dst'){
port=cfg.dest_port;
} else {
const idx=parseInt(this.role.replace('src_',''),10);
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">Kein Port konfiguriert</div>';bread.innerHTML='';return;} if(!port){body.innerHTML='<div class="expl-empty">Kein Port konfiguriert</div>';bread.innerHTML='';return;}
const dev=devs.find(d=>d.usb_port===port); const dev=devs.find(d=>d.usb_port===port);
@@ -2270,7 +2312,7 @@ const expl={
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||''; 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">Verbindungsfehler</div>';} }catch(e){body.innerHTML='<div class="expl-empty">Verbindungsfehler</div>';}

View File

@@ -1 +1 @@
1.0.23 1.0.24