1- // Çift panel (split-pane) — sağ tarafta ikinci bir bağlı sunucuyu gezdirir ve
2- // sol (aktif) panelle arasında sürükle-bırak/düğme ile aktarım yapar.
3- // Aktarım var olan /api/transfer-remote (sunucudan sunucuya relay) ile yapılır.
1+ // Çift panel (split-pane) — sağ tarafta İKİNCİ BİR SUNUCUYU ya da BU BİLGİSAYARI
2+ // (yerel, yalnızca masaüstü uygulamasında) gezdirir ve sol (aktif) sunucuyla arasında
3+ // dosya aktarır.
4+ // • Sunucu ↔ sunucu: /api/transfer-remote (relay)
5+ // • Yerel → sunucu: /api/upload-local (yükleme)
6+ // • Sunucu → yerel: window.desktop.downloadToDir (indirme, masaüstü)
47import { $ , escapeHtml , toast } from "./dom.js" ;
58import { connections , activeConnId , session , cwd } from "./state.js" ;
69import { navigate , joinPath , fmtSize , fmtDate , checkedItems } from "./explorer.js" ;
710import { runTransfer } from "./transfer-remote.js" ;
811import { enqueueTransfer } from "./transfer-queue.js" ;
912
13+ const LOCAL = "__local__" ;
1014let isOpen = false ;
11- const dp = { connId : null , tok : null , cwd : "/" , items : [ ] } ;
15+ // mode: "remote" | "local"
16+ const dp = { mode : "remote" , connId : null , tok : null , cwd : "/" , parent : null , items : [ ] } ;
1217
18+ const isDesktop = ( ) => ! ! ( window . desktop && window . desktop . isDesktop && window . desktop . listDir ) ;
1319function listConns ( ) { return connections . filter ( ( c ) => c . session ) ; }
1420function connLabel ( c ) { return ( c . info && ( c . info . name || `${ c . info . username } @${ c . info . host } ` ) ) || c . id ; }
1521function activeConn ( ) { return connections . find ( ( c ) => c . id === activeConnId ) ; }
1622function activeLabel ( ) { const c = activeConn ( ) ; return c ? connLabel ( c ) : "aktif sunucu" ; }
23+ function rightLabel ( ) {
24+ if ( dp . mode === "local" ) return "💻 Bu Bilgisayar" ;
25+ return dp . connId ? connLabel ( connections . find ( ( c ) => c . id === dp . connId ) || { } ) : "—" ;
26+ }
1727
1828// İki panelin kim olduğunu ve ne yapılacağını açıkça yaz.
1929function setHint ( ) {
2030 const el = $ ( "dp-xfer-hint" ) ;
2131 if ( ! el ) return ;
22- const right = dp . connId ? connLabel ( connections . find ( ( c ) => c . id === dp . connId ) || { } ) : "—" ;
23- const same = dp . tok && activeConn ( ) && activeConn ( ) . session === dp . tok ;
32+ const same = dp . mode === "remote" && dp . tok && activeConn ( ) && activeConn ( ) . session === dp . tok ;
2433 el . innerHTML =
2534 `<span class="dp-side"><b>◀ Sol</b> (senin gezginin): ${ escapeHtml ( activeLabel ( ) ) } </span>` +
26- `<span class="dp-side"><b>Sağ ▶</b>: ${ escapeHtml ( right ) } </span>` +
35+ `<span class="dp-side"><b>Sağ ▶</b>: ${ escapeHtml ( rightLabel ( ) ) } </span>` +
2736 ( same
2837 ? `<span class="dp-warn">⚠ İki panel aynı sunucu — aktarım için farklı bir sunucu seç.</span>`
2938 : `<span class="dp-tip">Aktarmak için dosyaları <b>kutucukla seç</b>, sonra <b>📥 Soldan al</b> / <b>📤 Sola gönder</b>.</span>` ) ;
3039}
3140
32- async function listDir ( tok , path ) {
33- const res = await fetch ( "/api/list?path=" + encodeURIComponent ( path ) , { headers : tok ? { "x-session" : tok } : { } } ) ;
41+ async function listRemote ( tok , p ) {
42+ const res = await fetch ( "/api/list?path=" + encodeURIComponent ( p ) , { headers : tok ? { "x-session" : tok } : { } } ) ;
3443 if ( ! res . ok ) { let e = "Listelenemedi" ; try { e = ( await res . json ( ) ) . error || e ; } catch ( _ ) { } throw new Error ( e ) ; }
3544 return res . json ( ) ;
3645}
3746
3847function populateSelect ( ) {
3948 const sel = $ ( "dp-conn" ) ;
4049 const conns = listConns ( ) ;
41- sel . innerHTML = conns . map ( ( c ) => `<option value="${ escapeHtml ( c . id ) } ">${ escapeHtml ( connLabel ( c ) ) } </option>` ) . join ( "" ) ;
42- // Var olan seçimi koru; yoksa aktif olmayan ilk bağlantıyı seç.
43- if ( dp . connId && conns . some ( ( c ) => c . id === dp . connId ) ) sel . value = dp . connId ;
50+ let html = conns . map ( ( c ) => `<option value="${ escapeHtml ( c . id ) } ">${ escapeHtml ( connLabel ( c ) ) } </option>` ) . join ( "" ) ;
51+ if ( isDesktop ( ) ) html += `<option value="${ LOCAL } ">💻 Bu Bilgisayar (yerel)</option>` ;
52+ sel . innerHTML = html ;
53+ // Var olan seçimi koru; yoksa aktif olmayan ilk sunucu, o da yoksa yerel.
54+ if ( dp . mode === "local" ) sel . value = LOCAL ;
55+ else if ( dp . connId && conns . some ( ( c ) => c . id === dp . connId ) ) sel . value = dp . connId ;
4456 else {
4557 const pref = conns . find ( ( c ) => c . id !== activeConnId ) || conns [ 0 ] ;
4658 if ( pref ) sel . value = pref . id ;
59+ else if ( isDesktop ( ) ) sel . value = LOCAL ;
4760 }
4861}
4962
5063async function setConn ( id ) {
64+ if ( id === LOCAL ) {
65+ dp . mode = "local" ; dp . connId = null ; dp . tok = null ;
66+ let home = "/" ; try { home = await window . desktop . homeDir ( ) ; } catch ( _ ) { }
67+ dp . cwd = home ;
68+ setHint ( ) ;
69+ return loadLocal ( home ) ;
70+ }
5171 const c = connections . find ( ( x ) => x . id === id ) ;
5272 if ( ! c ) return ;
53- dp . connId = id ; dp . tok = c . session ; dp . cwd = c . homePath || "/" ;
73+ dp . mode = "remote" ; dp . connId = id ; dp . tok = c . session ; dp . cwd = c . homePath || "/" ; dp . parent = null ;
5474 setHint ( ) ;
5575 await loadDp ( ) ;
5676}
5777
5878async function loadDp ( ) {
79+ if ( dp . mode === "local" ) return loadLocal ( dp . cwd ) ;
5980 const list = $ ( "dp-list" ) ;
6081 list . innerHTML = `<div class="dk-msg">Yükleniyor…</div>` ;
6182 if ( ! dp . tok ) { list . innerHTML = `<div class="dk-msg">Sağ panel için bir sunucu seç.</div>` ; renderBread ( ) ; return ; }
6283 try {
63- const data = await listDir ( dp . tok , dp . cwd ) ;
84+ const data = await listRemote ( dp . tok , dp . cwd ) ;
6485 dp . cwd = data . path ;
65- dp . items = data . items || [ ] ;
86+ dp . items = ( data . items || [ ] ) . map ( ( i ) => ( { name : i . name , type : i . type , size : i . size , mtime : i . mtime } ) ) ;
6687 render ( ) ;
6788 } catch ( e ) { list . innerHTML = `<div class="dk-msg">Hata: ${ escapeHtml ( e . message ) } </div>` ; }
6889}
6990
91+ async function loadLocal ( target ) {
92+ const list = $ ( "dp-list" ) ;
93+ list . innerHTML = `<div class="dk-msg">Yükleniyor…</div>` ;
94+ let r ;
95+ try { r = await window . desktop . listDir ( target ) ; } catch ( e ) { r = { ok : false , error : e . message } ; }
96+ if ( ! r || ! r . ok ) { list . innerHTML = `<div class="dk-msg">Klasör açılamadı: ${ escapeHtml ( ( r && r . error ) || "?" ) } </div>` ; return ; }
97+ dp . cwd = r . path ; dp . parent = r . parent ;
98+ const sep = r . path . includes ( "\\" ) ? "\\" : "/" ;
99+ dp . items = ( r . entries || [ ] ) . map ( ( e ) => ( {
100+ name : e . name , type : e . isDir ? "dir" : "file" , size : e . size || 0 , mtime : e . mtime || 0 ,
101+ abs : r . path . replace ( / [ \\ / ] + $ / , "" ) + sep + e . name ,
102+ } ) ) ;
103+ render ( ) ;
104+ }
105+
70106function renderBread ( ) {
71107 const bc = $ ( "dp-bread" ) ;
72108 if ( ! bc ) return ;
73109 bc . innerHTML = "" ;
110+ if ( dp . mode === "local" ) {
111+ // Yerel: yol metni + (varsa) üst klasör kısayolu — segment yeniden kurmak yerine güvenli.
112+ const span = document . createElement ( "span" ) ;
113+ span . className = "crumb dp-localpath" ;
114+ span . textContent = "💻 " + dp . cwd ;
115+ bc . appendChild ( span ) ;
116+ return ;
117+ }
74118 const mk = ( label , path ) => {
75119 const s = document . createElement ( "span" ) ;
76120 s . className = "crumb" ;
@@ -110,37 +154,75 @@ function render() {
110154 cb . _item = item ;
111155 cb . addEventListener ( "click" , ( e ) => e . stopPropagation ( ) ) ;
112156 if ( item . type === "dir" || item . type === "link" ) {
113- row . addEventListener ( "dblclick" , ( ) => { dp . cwd = joinPath ( dp . cwd , item . name ) ; loadDp ( ) ; } ) ;
157+ row . addEventListener ( "dblclick" , ( ) => {
158+ if ( dp . mode === "local" ) loadLocal ( item . abs ) ;
159+ else { dp . cwd = joinPath ( dp . cwd , item . name ) ; loadDp ( ) ; }
160+ } ) ;
114161 }
115162 list . appendChild ( row ) ;
116163 } ) ;
117164}
118165
119- function dpSelectedPaths ( ) {
120- return Array . from ( $ ( "dp-list" ) . querySelectorAll ( ".dp-check:checked" ) ) . map ( ( cb ) => joinPath ( dp . cwd , cb . _item . name ) ) ;
166+ // Seçili sağ-panel öğeleri (item nesneleri)
167+ function dpSelectedItems ( ) {
168+ return Array . from ( $ ( "dp-list" ) . querySelectorAll ( ".dp-check:checked" ) ) . map ( ( cb ) => cb . _item ) ;
121169}
122170
123- // Sol (aktif) → Sağ (dp)
171+ // ---- 📥 Soldan al: sol (aktif sunucu) seçilenleri → sağ panele ----
124172function transferLeftToRight ( ) {
173+ const items = checkedItems ( ) ;
174+ if ( ! items . length ) return toast ( "Sol panelde aktarılacak öğe seç (kutucukla)." , true ) ;
175+
176+ if ( dp . mode === "local" ) {
177+ // Sunucu → yerel klasöre indir (masaüstü)
178+ const destDir = dp . cwd ;
179+ enqueueTransfer ( `İndir → 💻 ${ destDir } (${ items . length } )` , async ( ) => {
180+ let ok = 0 , fail = 0 ;
181+ for ( const it of items ) {
182+ const full = joinPath ( cwd , it . name ) ;
183+ const isDir = it . type === "dir" ;
184+ const url = `${ location . origin } /api/${ isDir ? "download-folder" : "download" } ?session=${ encodeURIComponent ( session ) } &path=${ encodeURIComponent ( full ) } ` ;
185+ const name = isDir ? it . name + ".tar.gz" : it . name ;
186+ try {
187+ const r = await window . desktop . downloadToDir ( url , destDir , name ) ;
188+ if ( r && r . ok ) ok ++ ; else fail ++ ;
189+ } catch ( _ ) { fail ++ ; }
190+ }
191+ toast ( fail ? `${ ok } indirildi, ${ fail } başarısız` : `${ ok } öğe indirildi → ${ destDir } ` , ! ! fail ) ;
192+ loadLocal ( destDir ) ;
193+ } ) ;
194+ return ;
195+ }
196+
197+ // Sunucu → sunucu
125198 if ( ! dp . tok ) return toast ( "Önce sağ panel için sunucu seç." , true ) ;
126- const sel = checkedItems ( ) . map ( ( it ) => joinPath ( cwd , it . name ) ) ;
127- if ( ! sel . length ) return toast ( "Sol panelde aktarılacak öğe seç (kutucukla)." , true ) ;
128- const c = connections . find ( ( x ) => x . id === activeConnId ) ;
199+ const c = activeConn ( ) ;
129200 if ( c && c . session === dp . tok ) return toast ( "İki panel aynı sunucu — farklı bir sunucu seç." , true ) ;
130- const rightName = connLabel ( connections . find ( ( x ) => x . id === dp . connId ) || { } ) ;
131- enqueueTransfer ( `Soldan al → ${ rightName } (${ sel . length } )` , async ( ) => {
201+ const sel = items . map ( ( it ) => joinPath ( cwd , it . name ) ) ;
202+ enqueueTransfer ( `Soldan al → ${ rightLabel ( ) } (${ sel . length } )` , async ( ) => {
132203 await runTransfer ( session , dp . tok , dp . cwd , sel ) ;
133204 loadDp ( ) ;
134205 } ) ;
135206}
136207
137- // Sağ (dp) → Sol (aktif)
208+ // ---- 📤 Sola gönder: sağ panel seçilenleri → sol (aktif sunucu) ----
138209function transferRightToLeft ( ) {
210+ const items = dpSelectedItems ( ) ;
211+ if ( ! items . length ) return toast ( "Sağ panelde aktarılacak öğe seç (kutucukla)." , true ) ;
212+
213+ if ( dp . mode === "local" ) {
214+ // Yerel → aktif sunucunun açık klasörüne yükle
215+ const abs = items . map ( ( it ) => it . abs ) . filter ( Boolean ) ;
216+ enqueueTransfer ( `Yükle → ${ activeLabel ( ) } (${ abs . length } )` , ( ) =>
217+ import ( "./local-explorer.js" ) . then ( ( m ) => m . uploadLocalPaths ( abs , abs ) ) ) ;
218+ return ;
219+ }
220+
221+ // Sunucu → sunucu
139222 if ( ! dp . tok ) return toast ( "Önce sağ panel için sunucu seç." , true ) ;
140- const sel = dpSelectedPaths ( ) ;
141- if ( ! sel . length ) return toast ( "Sağ panelde aktarılacak öğe seç (kutucukla)." , true ) ;
142- const c = connections . find ( ( x ) => x . id === activeConnId ) ;
223+ const c = activeConn ( ) ;
143224 if ( c && c . session === dp . tok ) return toast ( "İki panel aynı sunucu — farklı bir sunucu seç." , true ) ;
225+ const sel = items . map ( ( it ) => joinPath ( dp . cwd , it . name ) ) ;
144226 enqueueTransfer ( `Sola gönder → ${ activeLabel ( ) } (${ sel . length } )` , async ( ) => {
145227 await runTransfer ( dp . tok , session , cwd , sel ) ;
146228 navigate ( cwd , false ) ;
@@ -155,11 +237,10 @@ function toggle() {
155237 pane . hidden = ! isOpen ;
156238 $ ( "btn-split" ) . classList . toggle ( "active" , isOpen ) ;
157239 if ( ! isOpen ) return ;
158- // Sol tarafta panel (dashboard) açıksa dosya gezginine geç — yoksa kullanıcı
159- // kendi dosyalarını göremez ve aktarım için seçim yapamaz.
240+ // Sol tarafta panel (dashboard) açıksa dosya gezginine geç.
160241 import ( "./dashboard.js" ) . then ( ( m ) => m . showFilesView ( ) ) . catch ( ( ) => { } ) ;
161- if ( listConns ( ) . length < 2 )
162- toast ( "Çift panel için ikinci bir sunucuya bağlan (yeni sekmede). Tek sunucu arasında aktarım yapılamaz. " , true ) ;
242+ if ( listConns ( ) . length < 2 && ! isDesktop ( ) )
243+ toast ( "Çift panel için ikinci bir sunucuya bağlan (yeni sekmede)." , true ) ;
163244 setHint ( ) ;
164245 populateSelect ( ) ;
165246 const id = $ ( "dp-conn" ) . value ;
@@ -173,6 +254,7 @@ export function initDualPane() {
173254 $ ( "dp-close" ) . addEventListener ( "click" , ( ) => { if ( isOpen ) toggle ( ) ; } ) ;
174255 $ ( "dp-refresh" ) . addEventListener ( "click" , loadDp ) ;
175256 $ ( "dp-up" ) . addEventListener ( "click" , ( ) => {
257+ if ( dp . mode === "local" ) { if ( dp . parent ) loadLocal ( dp . parent ) ; return ; }
176258 if ( dp . cwd === "/" ) return ;
177259 dp . cwd = dp . cwd . replace ( / \/ [ ^ / ] + \/ ? $ / , "" ) || "/" ;
178260 loadDp ( ) ;
0 commit comments