From 7af173ba9102a5e058dccf99b7c5baa053e04752 Mon Sep 17 00:00:00 2001
From: Ahmed Nagi <144544047+be-at@users.noreply.github.com>
Date: Sun, 22 Feb 2026 03:50:11 +0000
Subject: [PATCH 1/2] feat: add share-to feature
---
app/src/main/AndroidManifest.xml | 10 ++
.../com/raival/compose/file/explorer/App.kt | 8 ++
.../file/explorer/screen/main/MainActivity.kt | 45 +++++-
.../main/tab/files/ui/BottomOptionsBar.kt | 130 ++++++++++++++++++
app/src/main/res/values/strings.xml | 9 ++
5 files changed, 195 insertions(+), 7 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4f2ecd3d..b9ce70c9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -159,6 +159,16 @@
+
+
+
+
+
+
+
+
+
+
by mutableStateOf(emptyList())
companion object {
lateinit var appContext: Context
diff --git a/app/src/main/java/com/raival/compose/file/explorer/screen/main/MainActivity.kt b/app/src/main/java/com/raival/compose/file/explorer/screen/main/MainActivity.kt
index f45052e2..ba442662 100644
--- a/app/src/main/java/com/raival/compose/file/explorer/screen/main/MainActivity.kt
+++ b/app/src/main/java/com/raival/compose/file/explorer/screen/main/MainActivity.kt
@@ -36,6 +36,8 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Velocity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
+import android.content.Intent
+import android.net.Uri
import com.raival.compose.file.explorer.App.Companion.globalClass
import com.raival.compose.file.explorer.R
import com.raival.compose.file.explorer.base.BaseActivity
@@ -115,6 +117,9 @@ class MainActivity : BaseActivity() {
mainActivityManager.checkForUpdate()
if (hasIntent()) {
handleIntent()
+ if (mainActivityState.tabs.isEmpty()) {
+ mainActivityManager.loadStartupTabs()
+ }
} else {
if (mainActivityState.tabs.isEmpty()) {
mainActivityManager.loadStartupTabs()
@@ -343,17 +348,43 @@ class MainActivity : BaseActivity() {
}
private fun hasIntent(): Boolean {
- return intent isNot null && intent!!.hasExtra(HOME_SCREEN_SHORTCUT_EXTRA_KEY)
+ if (intent == null) return false
+ val action = intent!!.action
+ return intent!!.hasExtra(HOME_SCREEN_SHORTCUT_EXTRA_KEY)
+ || action == Intent.ACTION_SEND
+ || action == Intent.ACTION_SEND_MULTIPLE
}
private fun handleIntent() {
intent?.let {
- if (it.hasExtra(HOME_SCREEN_SHORTCUT_EXTRA_KEY)) {
- globalClass.mainActivityManager.jumpToFile(
- file = LocalFileHolder(File(it.getStringExtra(HOME_SCREEN_SHORTCUT_EXTRA_KEY)!!)),
- context = this
- )
- intent = null
+ when (it.action) {
+ Intent.ACTION_SEND -> {
+ @Suppress("DEPRECATION")
+ val uri = it.getParcelableExtra(Intent.EXTRA_STREAM)
+ if (uri != null) {
+ globalClass.shareUris = listOf(uri)
+ globalClass.isShareMode = true
+ }
+ intent = null
+ }
+ Intent.ACTION_SEND_MULTIPLE -> {
+ @Suppress("DEPRECATION")
+ val uris = it.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
+ if (!uris.isNullOrEmpty()) {
+ globalClass.shareUris = uris
+ globalClass.isShareMode = true
+ }
+ intent = null
+ }
+ else -> {
+ if (it.hasExtra(HOME_SCREEN_SHORTCUT_EXTRA_KEY)) {
+ globalClass.mainActivityManager.jumpToFile(
+ file = LocalFileHolder(File(it.getStringExtra(HOME_SCREEN_SHORTCUT_EXTRA_KEY)!!)),
+ context = this
+ )
+ intent = null
+ }
+ }
}
}
}
diff --git a/app/src/main/java/com/raival/compose/file/explorer/screen/main/tab/files/ui/BottomOptionsBar.kt b/app/src/main/java/com/raival/compose/file/explorer/screen/main/tab/files/ui/BottomOptionsBar.kt
index 8beb07e7..9d187890 100644
--- a/app/src/main/java/com/raival/compose/file/explorer/screen/main/tab/files/ui/BottomOptionsBar.kt
+++ b/app/src/main/java/com/raival/compose/file/explorer/screen/main/tab/files/ui/BottomOptionsBar.kt
@@ -5,7 +5,20 @@ import androidx.compose.animation.expandIn
import androidx.compose.animation.shrinkOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
+import android.app.Activity
+import android.provider.OpenableColumns
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.rounded.SaveAlt
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.style.TextOverflow
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -54,6 +67,92 @@ import com.raival.compose.file.explorer.screen.main.tab.files.task.CopyTask
fun BottomOptionsBar(tab: FilesTab) {
val state = tab.bottomOptionsBarState.collectAsState().value
+ // ── Share mode: Save here banner ──────────────────────────────────────────
+ if (globalClass.isShareMode && globalClass.shareUris.isNotEmpty()) {
+ val context = LocalContext.current
+ val coroutineScope = rememberCoroutineScope()
+ var isSaving by remember { mutableStateOf(false) }
+ val fileLabel = if (globalClass.shareUris.size == 1)
+ getSharedFileName(context, globalClass.shareUris[0]) ?: context.getString(R.string.unknown_file)
+ else context.getString(R.string.shared_files_count, globalClass.shareUris.size)
+
+ HorizontalDivider()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 10.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = fileLabel,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ androidx.compose.material3.OutlinedButton(
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(6.dp),
+ onClick = {
+ globalClass.isShareMode = false
+ globalClass.shareUris = emptyList()
+ }
+ ) {
+ Text(text = stringResource(R.string.cancel))
+ }
+ androidx.compose.material3.Button(
+ modifier = Modifier.weight(1f),
+ enabled = !isSaving && tab.activeFolder.canWrite,
+ shape = RoundedCornerShape(6.dp),
+ onClick = {
+ coroutineScope.launch(Dispatchers.IO) {
+ isSaving = true
+ val ok = saveSharedFilesToFolder(context, globalClass.shareUris, tab)
+ isSaving = false
+ if (ok) {
+ globalClass.isShareMode = false
+ globalClass.shareUris = emptyList()
+ globalClass.showMsg(R.string.file_saved_successfully)
+ tab.reloadFiles()
+ } else {
+ globalClass.showMsg(R.string.failed_to_save_file)
+ }
+ }
+ }
+ ) {
+ if (isSaving) {
+ androidx.compose.material3.CircularProgressIndicator(
+ modifier = Modifier.size(16.dp),
+ color = MaterialTheme.colorScheme.onPrimary,
+ strokeWidth = 2.dp
+ )
+ Space(size = 8.dp)
+ Text(text = stringResource(R.string.saving_file))
+ } else {
+ Icon(
+ modifier = Modifier.size(16.dp),
+ imageVector = Icons.Rounded.SaveAlt,
+ contentDescription = null
+ )
+ Space(size = 8.dp)
+ Text(text = stringResource(R.string.save_here))
+ }
+ }
+ } // end Row
+ }
+ }
+ // ─────────────────────────────────────────────────────────────────────────
+
AnimatedVisibility(
visible = state.showQuickOptions && tab.selectedFiles.isNotEmpty(),
enter = expandIn(expandFrom = Alignment.TopCenter) + slideInVertically(
@@ -253,3 +352,34 @@ fun RowScope.BottomOptionsBarButton(
view()
}
}
+
+private fun getSharedFileName(context: android.content.Context, uri: android.net.Uri): String? {
+ context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ val idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ if (idx >= 0) return cursor.getString(idx)
+ }
+ }
+ return uri.lastPathSegment
+}
+
+private fun saveSharedFilesToFolder(
+ context: android.content.Context,
+ uris: List,
+ tab: FilesTab
+): Boolean {
+ return try {
+ val localFolder = tab.activeFolder
+ as? com.raival.compose.file.explorer.screen.main.tab.files.holder.LocalFileHolder
+ ?: return false
+ val destDir = localFolder.file
+ if (!destDir.exists() || !destDir.isDirectory) return false
+ uris.forEach { uri ->
+ val name = getSharedFileName(context, uri) ?: "shared_${System.currentTimeMillis()}"
+ context.contentResolver.openInputStream(uri)?.use { input ->
+ java.io.File(destDir, name).outputStream().use { input.copyTo(it) }
+ }
+ }
+ true
+ } catch (e: Exception) { false }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 388efca7..a4a9550b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -453,4 +453,13 @@
Back navigation to close tabs
Remember last session
Confirm before exiting the app
+ Save File
+ Save here
+ Saving...
+ File saved successfully
+ Failed to save file
+ Destination folder
+ No files received
+ Unknown file
+ %d shared files
From 4aa1ef03327709c200c4674ef2c781874b396f13 Mon Sep 17 00:00:00 2001
From: Ahmed Nagi <144544047+be-at@users.noreply.github.com>
Date: Sun, 22 Feb 2026 08:46:24 +0000
Subject: [PATCH 2/2] handle duplicate filenames
---
.../main/tab/files/ui/BottomOptionsBar.kt | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/app/src/main/java/com/raival/compose/file/explorer/screen/main/tab/files/ui/BottomOptionsBar.kt b/app/src/main/java/com/raival/compose/file/explorer/screen/main/tab/files/ui/BottomOptionsBar.kt
index 9d187890..235437e5 100644
--- a/app/src/main/java/com/raival/compose/file/explorer/screen/main/tab/files/ui/BottomOptionsBar.kt
+++ b/app/src/main/java/com/raival/compose/file/explorer/screen/main/tab/files/ui/BottomOptionsBar.kt
@@ -375,11 +375,26 @@ private fun saveSharedFilesToFolder(
val destDir = localFolder.file
if (!destDir.exists() || !destDir.isDirectory) return false
uris.forEach { uri ->
- val name = getSharedFileName(context, uri) ?: "shared_${System.currentTimeMillis()}"
+ val originalName = getSharedFileName(context, uri) ?: "shared_${System.currentTimeMillis()}"
+ val destFile = getUniqueFile(destDir, originalName)
context.contentResolver.openInputStream(uri)?.use { input ->
- java.io.File(destDir, name).outputStream().use { input.copyTo(it) }
+ destFile.outputStream().use { input.copyTo(it) }
}
}
true
} catch (e: Exception) { false }
}
+
+private fun getUniqueFile(dir: java.io.File, fileName: String): java.io.File {
+ val dot = fileName.lastIndexOf('.')
+ val name = if (dot != -1) fileName.substring(0, dot) else fileName
+ val ext = if (dot != -1) fileName.substring(dot) else ""
+
+ var file = java.io.File(dir, fileName)
+ var counter = 1
+ while (file.exists()) {
+ file = java.io.File(dir, "${name} Copy($counter)${ext}")
+ counter++
+ }
+ return file
+}
\ No newline at end of file