Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>

<service
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/raival/compose/file/explorer/App.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.raival.compose.file.explorer

import android.app.Application
import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import android.content.Context
import android.os.Environment
import android.os.Process
Expand Down Expand Up @@ -48,6 +52,10 @@ import java.util.concurrent.atomic.AtomicInteger
import kotlin.system.exitProcess

class App : Application(), coil3.SingletonImageLoader.Factory {

// Share mode
var isShareMode: Boolean by mutableStateOf(false)
var shareUris: List<Uri> by mutableStateOf(emptyList())
companion object {
lateinit var appContext: Context

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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<Uri>(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<Uri>(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
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -253,3 +352,49 @@ 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<android.net.Uri>,
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 originalName = getSharedFileName(context, uri) ?: "shared_${System.currentTimeMillis()}"
val destFile = getUniqueFile(destDir, originalName)
context.contentResolver.openInputStream(uri)?.use { input ->
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
}
9 changes: 9 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -453,4 +453,13 @@
<string name="close_tab_on_back_nav">Back navigation to close tabs</string>
<string name="remember_last_session">Remember last session</string>
<string name="confirm_before_exit">Confirm before exiting the app</string>
<string name="save_shared_file">Save File</string>
<string name="save_here">Save here</string>
<string name="saving_file">Saving...</string>
<string name="file_saved_successfully">File saved successfully</string>
<string name="failed_to_save_file">Failed to save file</string>
<string name="destination_folder">Destination folder</string>
<string name="no_files_received">No files received</string>
<string name="unknown_file">Unknown file</string>
<string name="shared_files_count">%d shared files</string>
</resources>