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:
64
app.py
64
app.py
@@ -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">↻</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">▲ Quelle ${i+1}${sp.label?' – '+sp.label:''}</div>
|
<div class="role-tag src">▲ 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}')">✕</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">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}')">▲ ${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>';}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.0.23
|
1.0.24
|
||||||
Reference in New Issue
Block a user