@@ -300,8 +300,14 @@ class ProjspecToolWindowPanel(
300300 msg[" name" ] as ? String ? : " " , so) }
301301 " deleteEntry" -> pool { fbDeleteEntry(msg[" url" ] as ? String ? : " " ,
302302 msg[" isDir" ] == true , so) }
303+ " deleteEntries" -> pool { fbDeleteEntries(msg[" items" ] as ? List <* > ? : emptyList<Any >()) }
303304 " renameEntry" -> pool { fbRenameEntry(msg[" url" ] as ? String ? : " " ,
304305 msg[" newName" ] as ? String ? : " " , so) }
306+ " paste" -> pool { fbPasteEntry(msg[" items" ] as ? List <* > ? : emptyList<Any >(),
307+ msg[" dstDir" ] as ? String ? : " " ,
308+ msg[" dstStorageOptions" ] as ? String ,
309+ msg[" mode" ] as ? String ? : " copy" ,
310+ msg[" confirmed" ] == true ) }
305311 " mkdir" -> pool { fbMkdir(msg[" parentUrl" ] as ? String ? : " " ,
306312 msg[" name" ] as ? String ? : " " , so) }
307313 " addBookmark" -> pool { fbBookmarkAdd(msg[" url" ] as ? String ? : " " ,
@@ -829,6 +835,37 @@ class ProjspecToolWindowPanel(
829835 fbBrowse(url.trimEnd(' /' ).substringBeforeLast(' /' ).ifBlank { " /" }, storageOptions, false )
830836 }
831837
838+ /* *
839+ * Delete one or more entries (multi-select). Deletes each item
840+ * individually (fire-and-forget, like [fbDeleteEntry]) and reports
841+ * per-item results so partial failures are visible in the webview.
842+ */
843+ private fun fbDeleteEntries (items : List <* >) {
844+ if (items.isEmpty()) return
845+ val results = mutableListOf<Map <String , Any ?>>()
846+ var refreshDir: String? = null
847+ var refreshSo: String? = null
848+ for (raw in items) {
849+ @Suppress(" UNCHECKED_CAST" )
850+ val item = raw as ? Map <String , Any ?> ? : continue
851+ val url = item[" url" ] as ? String ? : continue
852+ val isDir = item[" isDir" ] == true
853+ val storageOptions = item[" storageOptions" ] as ? String
854+ val so = server.parseSo(storageOptions)
855+ val error = if (server.delete(url, isDir, so) != null ) {
856+ null
857+ } else {
858+ ProjspecRunner .runFbDelete(url, isDir, storageOptions)
859+ null
860+ }
861+ results.add(mapOf (" url" to url, " error" to error))
862+ refreshDir = url.trimEnd(' /' ).substringBeforeLast(' /' ).ifBlank { " /" }
863+ refreshSo = storageOptions
864+ }
865+ deliverToFbWebview(mapOf (" type" to " deleteEntriesResult" , " results" to results))
866+ if (refreshDir != null ) fbBrowse(refreshDir, refreshSo, false )
867+ }
868+
832869 private fun fbRenameEntry (url : String , newName : String , storageOptions : String? ) {
833870 val parent = url.trimEnd(' /' ).substringBeforeLast(' /' ).ifBlank { " /" }
834871 val dst = parent.trimEnd(' /' ) + " /" + newName
@@ -837,6 +874,81 @@ class ProjspecToolWindowPanel(
837874 fbBrowse(parent, storageOptions, false )
838875 }
839876
877+ /* *
878+ * Paste one or more previously copied/cut entries into `dstDir`. `mode`
879+ * is "copy" or "cut" — "cut" maps to `move()` per item (fire-and-forget,
880+ * like [fbRenameEntry]); "copy" first checks the *aggregate* size of all
881+ * items via a single `totalSize()` call, which reports `needsConfirm`
882+ * using the same `filebrowser_copy_confirm_bytes` threshold `copy()`
883+ * itself enforces per item. If confirmation is needed and `confirmed`
884+ * wasn't already set, nothing is copied yet — `pasteNeedsConfirm` is
885+ * delivered to the webview, which re-sends this same message with
886+ * `confirmed=true` once the user accepts. Each item is then
887+ * copied/moved individually (passing `confirmed=true` through so the
888+ * per-item calls don't redundantly re-check a threshold already cleared
889+ * for the batch) and per-item results are reported.
890+ */
891+ private fun fbPasteEntry (
892+ items : List <* >,
893+ dstDir : String ,
894+ dstStorageOptions : String? ,
895+ mode : String ,
896+ confirmed : Boolean ,
897+ ) {
898+ @Suppress(" UNCHECKED_CAST" )
899+ val parsedItems = items.mapNotNull { it as ? Map <String , Any ?> }
900+ if (parsedItems.isEmpty()) return
901+ val firstSrcSo = parsedItems[0 ][" srcStorageOptions" ] as ? String
902+ val soStr = firstSrcSo ? : dstStorageOptions
903+ val so = server.parseSo(soStr)
904+
905+ if (mode != " cut" && ! confirmed) {
906+ val urls = parsedItems.mapNotNull { it[" src" ] as ? String }
907+ val tsData: Map <String , Any ?> = server.totalSize(urls, so) ? : run {
908+ val raw = ProjspecRunner .runFbTotalSize(urls, soStr)
909+ try {
910+ @Suppress(" UNCHECKED_CAST" )
911+ gson.fromJson(raw, Map ::class .java) as Map <String , Any ?>
912+ } catch (_: Exception ) {
913+ mapOf (" total_size" to null , " error" to raw, " needs_confirm" to false )
914+ }
915+ }
916+ if (tsData[" needs_confirm" ] == true ) {
917+ deliverToFbWebview(mapOf (
918+ " type" to " pasteNeedsConfirm" ,
919+ " items" to items, " dstDir" to dstDir, " dstStorageOptions" to dstStorageOptions,
920+ " mode" to mode, " totalSize" to tsData[" total_size" ],
921+ ))
922+ return
923+ }
924+ }
925+
926+ val results = mutableListOf<Map <String , Any ?>>()
927+ for (item in parsedItems) {
928+ val src = item[" src" ] as ? String ? : continue
929+ val itemSoStr = (item[" srcStorageOptions" ] as ? String ) ? : dstStorageOptions
930+ val itemSo = server.parseSo(itemSoStr) ? : so
931+ val dst = dstDir.trimEnd(' /' ) + " /" + (src.trimEnd(' /' ).substringAfterLast(' /' ))
932+ val error: Any? = if (mode == " cut" ) {
933+ if (server.move(src, dst, itemSo) == null ) ProjspecRunner .runFbMove(src, dst, itemSoStr)
934+ null
935+ } else {
936+ val data: Map <String , Any ?> = server.copy(src, dst, itemSo, true ) ? : run {
937+ val raw = ProjspecRunner .runFbCopy(src, dst, itemSoStr, true )
938+ try {
939+ @Suppress(" UNCHECKED_CAST" )
940+ gson.fromJson(raw, Map ::class .java) as Map <String , Any ?>
941+ } catch (_: Exception ) {
942+ mapOf (" error" to raw)
943+ }
944+ }
945+ data[" error" ]
946+ }
947+ results.add(mapOf (" src" to src, " dst" to dst, " error" to error))
948+ }
949+ deliverToFbWebview(mapOf (" type" to " pasteResult" , " mode" to mode, " results" to results, " error" to null ))
950+ }
951+
840952 private fun fbMkdir (parentUrl : String , name : String , storageOptions : String? ) {
841953 val newUrl = parentUrl.trimEnd(' /' ) + " /" + name
842954 val so = server.parseSo(storageOptions)
0 commit comments