diff --git a/.androidide/editor/openedFiles.json.bak b/.androidide/editor/openedFiles.json.bak new file mode 100644 index 0000000..e69de29 diff --git a/filetree/src/main/kotlin/com/zyron/filetree/callback/FileTreeDiffCallback.kt b/filetree/src/main/kotlin/com/zyron/filetree/callback/FileTreeDiffCallback.kt new file mode 100644 index 0000000..d895b37 --- /dev/null +++ b/filetree/src/main/kotlin/com/zyron/filetree/callback/FileTreeDiffCallback.kt @@ -0,0 +1,48 @@ +/** + * Copyright 2024 Zyron Official. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zyron.filetree.callback + +import androidx.recyclerview.widget.DiffUtil +import com.zyron.filetree.datamodel.FileTreeNode + +/** + * A DiffUtil.ItemCallback implementation for comparing FileTreeNode objects. + */ +class FileTreeDiffCallback : DiffUtil.ItemCallback() { + + /** + * Determines if two FileTreeNode items represent the same file by comparing their absolute paths. + * + * @param oldItem The previous FileTreeNode item. + * @param newItem The new FileTreeNode item. + * @return True if the two items represent the same file, false otherwise. + */ + override fun areItemsTheSame(oldItem: FileTreeNode, newItem: FileTreeNode): Boolean { + return oldItem.file.absolutePath == newItem.file.absolutePath + } + + /** + * Determines if the contents of two FileTreeNode items are the same. + * + * @param oldItem The previous FileTreeNode item. + * @param newItem The new FileTreeNode item. + * @return True if the contents of the two items are the same, false otherwise. + */ + override fun areContentsTheSame(oldItem: FileTreeNode, newItem: FileTreeNode): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/filetree/src/main/kotlin/com/zyron/filetree/datamodel/FileTreeNode.kt b/filetree/src/main/kotlin/com/zyron/filetree/datamodel/FileTreeNode.kt new file mode 100644 index 0000000..5abb1dd --- /dev/null +++ b/filetree/src/main/kotlin/com/zyron/filetree/datamodel/FileTreeNode.kt @@ -0,0 +1,61 @@ +/** + * Copyright 2024 Zyron Official. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zyron.filetree.datamodel + +import java.io.File + +/** + * Represents a node in a file tree, corresponding to a file or directory. + * + * @property file The file or directory represented by this node. + * @property parent The parent node of this node in the file tree. + * @property level The depth level of this node in the file tree. + */ +data class FileTreeNode(var file: File, var parent: FileTreeNode? = null, var level: Int = 0) { + var isExpanded: Boolean = false + var childrenStartIndex: Int = 0 + var childrenEndIndex: Int = 0 + var childrenLoaded: Boolean = false + + /** + * Sorts the children of this node, separating directories from files and sorting them alphabetically. + * + * @return A list of FileTreeNode objects representing the sorted children. + */ + fun sortNode(): List { + val children = file.listFiles() + ?.asSequence() + ?.partition { it.isDirectory } + ?.let { (directories, files) -> + (directories.sortedBy { it.name.lowercase() } + files.sortedBy { it.name.lowercase() }) + } ?: emptyList() + return children.map { childFile -> + FileTreeNode(file = childFile, parent = this, level = level + 1) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + other as FileTreeNode + return file.absolutePath == other.file.absolutePath + } + + override fun hashCode(): Int { + return file.absolutePath.hashCode() + } +} \ No newline at end of file diff --git a/filetree/src/main/kotlin/com/zyron/filetree/events/FileTreeEventListener.kt b/filetree/src/main/kotlin/com/zyron/filetree/events/FileTreeEventListener.kt new file mode 100644 index 0000000..552e99c --- /dev/null +++ b/filetree/src/main/kotlin/com/zyron/filetree/events/FileTreeEventListener.kt @@ -0,0 +1,63 @@ +/** + * Copyright 2024 Zyron Official. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zyron.filetree.events + +import java.io.File + +/** + * Interface to listen for events within a file tree, including clicks and updates. + */ +interface FileTreeEventListener { + + /** + * Called when a file is clicked. + * + * @param file The file that was clicked. + */ + fun onFileClick(file: File) + + /** + * Called when a folder is clicked. + * + * @param folder The folder that was clicked. + */ + fun onFolderClick(folder: File) + + /** + * Called when a file is long-clicked. + * + * @param file The file that was long-clicked. + * @return True if the long-click event was handled, false otherwise. + */ + fun onFileLongClick(file: File): Boolean + + /** + * Called when a folder is long-clicked. + * + * @param folder The folder that was long-clicked. + * @return True if the long-click event was handled, false otherwise. + */ + fun onFolderLongClick(folder: File): Boolean + + /** + * Called when the file tree view is updated. + * + * @param startPosition The starting position of the update. + * @param itemCount The number of items updated. + */ + fun onFileTreeViewUpdated(startPosition: Int, itemCount: Int) +} \ No newline at end of file diff --git a/filetree/src/main/kotlin/com/zyron/filetree/map/ConcurrentFileMap.kt b/filetree/src/main/kotlin/com/zyron/filetree/map/ConcurrentFileMap.kt new file mode 100644 index 0000000..17db241 --- /dev/null +++ b/filetree/src/main/kotlin/com/zyron/filetree/map/ConcurrentFileMap.kt @@ -0,0 +1,127 @@ +/** + * Copyright 2024 Zyron Official. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zyron.filetree.map + +import com.zyron.filetree.datamodel.FileTreeNode +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +/** + * Manages a cache of file nodes for efficient access and manipulation using concurrency. + * + * @param nodes A list of root nodes to be processed and cached. + * @param maxSize The maximum size of the cache before trimming. + */ +class ConcurrentFileMap(private val nodes: List, private val maxSize: Int = 100) : Runnable { + + companion object { + val concurrentFileMap: MutableMap> = ConcurrentHashMap() + } + + private val cache = ConcurrentHashMap>(maxSize) + private val executor: ScheduledExecutorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()) + + init { + // Schedule cache trimming at fixed intervals + executor.scheduleAtFixedRate(::trimCache, 10, 10, TimeUnit.SECONDS) + } + + /** + * Retrieves a list of child nodes for a given file node from the cache. + * + * @param node The parent file node. + * @return A list of child file nodes, or null if the node is not in the cache. + */ + fun get(node: FileTreeNode): List? { + return cache[node] + } + + /** + * Adds a file node and its children to the cache. + * + * @param node The parent file node. + * @param result The list of child file nodes. + */ + fun put(node: FileTreeNode, result: List) { + cache[node] = result + if (cache.size > maxSize) { + trimCache() + } + } + + /** + * Clears the entire cache. + */ + fun clear() { + cache.clear() + } + + /** + * Processes a list of file nodes asynchronously to populate the cache. + * + * @param nodes The list of file nodes to process. + */ + private fun processNodes(nodes: List) { + val futures = mutableListOf>() + for (node in nodes) { + val future = executor.submit { + val nodeList = node.sortNode() + put(node, nodeList) + processNodes(nodeList) + } + futures.add(future) + } + futures.forEach { it.get() } + } + + /** + * Trims the cache to the specified maximum size by removing excess entries. + */ + private fun trimCache() { + if (cache.size > maxSize) { + val keysToRemove = cache.keys.take(cache.size - maxSize) + for (key in keysToRemove) { + cache.remove(key) + } + } + } + + /** + * Entry point for processing nodes when this Runnable is executed. + */ + override fun run() { + processNodes(nodes) + } + + /** + * Shuts down the executor service and cancels all running tasks. + */ + fun shutdown() { + executor.shutdown() + try { + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + executor.shutdownNow() + } + } catch (ex: InterruptedException) { + executor.shutdownNow() + Thread.currentThread().interrupt() + } + } +} \ No newline at end of file diff --git a/filetree/src/main/kotlin/com/zyron/filetree/map/FileMapManager.kt b/filetree/src/main/kotlin/com/zyron/filetree/map/FileMapManager.kt new file mode 100644 index 0000000..d02f6a1 --- /dev/null +++ b/filetree/src/main/kotlin/com/zyron/filetree/map/FileMapManager.kt @@ -0,0 +1,65 @@ +/** + * Copyright 2024 Zyron Official. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zyron.filetree.map + +import com.zyron.filetree.datamodel.FileTreeNode +import java.util.concurrent.Executors +import java.util.concurrent.Future +import android.util.Log + +/** + * Manages the mapping of file nodes using a single-threaded executor. + */ +object FileMapManager { + private var fileCacheFuture: Future<*>? = null + private val executor = Executors.newSingleThreadExecutor() + + /** + * Stops the current file mapping task, if one is running. + */ + fun stopFileMapping() { + fileCacheFuture?.cancel(true) + fileCacheFuture = null + Log.i(this::class.java.simpleName, "FileMapping stopped") + } + + /** + * Starts the file mapping process on a separate thread. + * + * @param nodes The list of root nodes to process. + * @param priority Optional priority setting for the mapping thread. + */ + fun startFileMapping(nodes: List, priority: Int? = null) { + if (fileCacheFuture == null) { + fileCacheFuture = executor.submit { + try { + val fileCache = ConcurrentFileMap(nodes) + fileCache.run() + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + } catch (e: Exception) { + Log.e(this::class.java.simpleName, "Error in FileMapping", e) + } + }.apply { + priority?.let { (this as Thread).priority = it } + } + Log.i(this::class.java.simpleName, "FileMapping started") + } else { + Log.e(this::class.java.simpleName, "FileMapping is already running; this might cause issues") + } + } +} \ No newline at end of file diff --git a/filetree/src/main/res/anim/default_anim.xml b/filetree/src/main/res/anim/default_anim.xml new file mode 100644 index 0000000..6c36622 --- /dev/null +++ b/filetree/src/main/res/anim/default_anim.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/filetree/src/main/res/anim/default_animation.xml b/filetree/src/main/res/anim/default_animation.xml new file mode 100644 index 0000000..068b7b4 --- /dev/null +++ b/filetree/src/main/res/anim/default_animation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/filetree/src/main/res/anim/fall_down.xml b/filetree/src/main/res/anim/fall_down.xml new file mode 100644 index 0000000..c4259d4 --- /dev/null +++ b/filetree/src/main/res/anim/fall_down.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/filetree/src/main/res/anim/fall_down_animation.xml b/filetree/src/main/res/anim/fall_down_animation.xml new file mode 100644 index 0000000..d2c4669 --- /dev/null +++ b/filetree/src/main/res/anim/fall_down_animation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/filetree/src/main/res/anim/rotate_in.xml b/filetree/src/main/res/anim/rotate_in.xml new file mode 100644 index 0000000..0bc00bb --- /dev/null +++ b/filetree/src/main/res/anim/rotate_in.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/filetree/src/main/res/anim/rotate_in_animation.xml b/filetree/src/main/res/anim/rotate_in_animation.xml new file mode 100644 index 0000000..d23db5b --- /dev/null +++ b/filetree/src/main/res/anim/rotate_in_animation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/filetree/src/main/res/anim/scale_up.xml b/filetree/src/main/res/anim/scale_up.xml new file mode 100644 index 0000000..612680d --- /dev/null +++ b/filetree/src/main/res/anim/scale_up.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/filetree/src/main/res/anim/scale_up_animation.xml b/filetree/src/main/res/anim/scale_up_animation.xml new file mode 100644 index 0000000..c4e50d7 --- /dev/null +++ b/filetree/src/main/res/anim/scale_up_animation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/filetree/src/main/res/anim/slide_in.xml b/filetree/src/main/res/anim/slide_in.xml new file mode 100644 index 0000000..f967a6f --- /dev/null +++ b/filetree/src/main/res/anim/slide_in.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/filetree/src/main/res/anim/slide_in_animation.xml b/filetree/src/main/res/anim/slide_in_animation.xml new file mode 100644 index 0000000..6943667 --- /dev/null +++ b/filetree/src/main/res/anim/slide_in_animation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/filetree/src/main/res/drawable/ic_chevron.xml b/filetree/src/main/res/drawable/ic_chevron.xml new file mode 100644 index 0000000..9aed824 --- /dev/null +++ b/filetree/src/main/res/drawable/ic_chevron.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/filetree/src/main/res/drawable/item_activated_background.xml b/filetree/src/main/res/drawable/item_activated_background.xml new file mode 100644 index 0000000..81ec2b6 --- /dev/null +++ b/filetree/src/main/res/drawable/item_activated_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/filetree/src/main/res/drawable/item_view_background.xml b/filetree/src/main/res/drawable/item_view_background.xml new file mode 100644 index 0000000..17fdfd9 --- /dev/null +++ b/filetree/src/main/res/drawable/item_view_background.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/filetree/src/main/res/values/attrs.xml b/filetree/src/main/res/values/attrs.xml new file mode 100644 index 0000000..223fe47 --- /dev/null +++ b/filetree/src/main/res/values/attrs.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file