@@ -3,6 +3,7 @@ package com.itsaky.androidide.fragments.sidebar
33import android.app.Activity
44import android.content.ClipData
55import android.content.Context
6+ import android.graphics.drawable.Drawable
67import android.net.Uri
78import android.view.DragEvent
89import android.view.View
@@ -23,6 +24,11 @@ internal class FileTreeDropController(
2324) {
2425
2526 private data class DropTarget (val node : TreeNode ? , val file : File )
27+ private sealed interface ImportResult {
28+ data class Success (val count : Int ) : ImportResult
29+ data class PartialSuccess (val count : Int , val error : Throwable ) : ImportResult
30+ data class Failure (val error : Throwable ) : ImportResult
31+ }
2632
2733 val nodeBinder = FileTreeViewHolder .ExternalDropHandler { node, file, view ->
2834 bindDropTarget(view, DropTarget (node, file))
@@ -88,33 +94,40 @@ internal class FileTreeDropController(
8894 } finally {
8995 dragPermissions?.release()
9096 }
91- }) { copiedCount, error ->
92- if (activity.isFinishing || activity.isDestroyed) {
93- return @executeAsyncProvideError
94- }
97+ }) { result, catastrophicError ->
98+ if (activity.isFinishing || activity.isDestroyed) return @executeAsyncProvideError
9599
96- if (error != null ) {
97- onDropFailed(
98- error.cause?.message
99- ? : error.message
100- ? : context.getString(R .string.msg_file_tree_drop_import_failed)
101- )
102- return @executeAsyncProvideError
103- }
100+ fun Throwable.toErrorMsg () = cause?.message ? : message ? : context.getString(R .string.msg_file_tree_drop_import_failed)
104101
105- val importedCount = copiedCount ? : 0
106- if (importedCount <= 0 ) {
107- onDropFailed(context.getString(R .string.msg_file_tree_drop_no_files))
102+ if (catastrophicError != null ) {
103+ onDropFailed(catastrophicError.toErrorMsg())
108104 return @executeAsyncProvideError
109105 }
110106
111- onDropCompleted(target.node, target.file, importedCount)
107+ when (result) {
108+ is ImportResult .Success -> {
109+ if (result.count > 0 ) {
110+ onDropCompleted(target.node, target.file, result.count)
111+ } else {
112+ onDropFailed(context.getString(R .string.msg_file_tree_drop_no_files))
113+ }
114+ }
115+ is ImportResult .PartialSuccess -> {
116+ onDropCompleted(target.node, target.file, result.count)
117+ onDropFailed(result.error.toErrorMsg())
118+ }
119+ is ImportResult .Failure -> {
120+ onDropFailed(result.error.toErrorMsg())
121+ }
122+
123+ else -> {}
124+ }
112125 }
113126
114127 return true
115128 }
116129
117- private fun copyDroppedFiles (context : Context , clipData : ClipData , targetFile : File ): Int {
130+ private fun copyDroppedFiles (context : Context , clipData : ClipData , targetFile : File ): ImportResult {
118131 val targetDirectory = resolveTargetDirectory(targetFile)
119132 require(targetDirectory.exists() && targetDirectory.isDirectory) {
120133 context.getString(R .string.msg_file_tree_drop_destination_missing)
@@ -127,21 +140,35 @@ internal class FileTreeDropController(
127140 continue
128141 }
129142
130- val sourceName =
131- uri.getFileName(context).ifBlank { context.getString(R .string.msg_file_tree_drop_default_name) }
132- val destinationFile = createAvailableTargetFile(targetDirectory, sourceName)
143+ val defaultName = context.getString(R .string.msg_file_tree_drop_default_name)
144+ val rawName = uri.getFileName(context).ifBlank { defaultName }
145+
146+ var sanitizedName = rawName.substringAfterLast(' /' ).substringAfterLast(' \\ ' )
133147
134- UriFileImporter .copyUriToFile(
135- context = context,
136- uri = uri,
137- destinationFile = destinationFile,
138- onOpenFailed = { IllegalStateException (context.getString(R .string.msg_file_tree_drop_read_failed, sourceName)) },
139- )
148+ if (sanitizedName == " ." || sanitizedName == " .." || sanitizedName.isBlank()) {
149+ sanitizedName = defaultName
150+ }
151+
152+ val destinationFile = createAvailableTargetFile(targetDirectory, sanitizedName)
140153
141- importedCount++
154+ try {
155+ UriFileImporter .copyUriToFile(
156+ context = context,
157+ uri = uri,
158+ destinationFile = destinationFile,
159+ onOpenFailed = { IllegalStateException (context.getString(R .string.msg_file_tree_drop_read_failed, sanitizedName)) },
160+ )
161+ importedCount++
162+ } catch (e: Exception ) {
163+ return if (importedCount > 0 ) {
164+ ImportResult .PartialSuccess (importedCount, e)
165+ } else {
166+ ImportResult .Failure (e)
167+ }
168+ }
142169 }
143170
144- return importedCount
171+ return ImportResult . Success ( importedCount)
145172 }
146173
147174 private fun resolveTargetDirectory (targetFile : File ): File {
@@ -165,15 +192,25 @@ internal class FileTreeDropController(
165192 }
166193
167194 private fun showDropHighlight (view : View ) {
195+ if (view.getTag(R .id.filetree_drop_target_tag) == null ) {
196+ view.setTag(R .id.filetree_drop_target_tag, view.background ? : " NULL_BG" )
197+ }
198+
168199 val baseColor = ContextCompat .getColor(activity, R .color.teal_200)
169200 view.setBackgroundColor((baseColor and 0x00FFFFFF ) or (64 shl 24 ))
170201 }
171202
172203 private fun clearDropHighlight (view : View ) {
173- view.background = null
204+ val savedBg = view.getTag(R .id.filetree_drop_target_tag)
205+ if (savedBg != null ) {
206+ view.background = if (savedBg == " NULL_BG" ) null else savedBg as ? Drawable
207+
208+ view.setTag(R .id.filetree_drop_target_tag, null )
209+ }
174210 }
175211
176212 private fun DragEvent.hasDroppedContent (): Boolean {
213+ if (localState != null ) return false
177214 return clipDescription != null || (clipData?.itemCount ? : 0 ) > 0
178215 }
179216
0 commit comments