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
11 changes: 8 additions & 3 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ android {
minSdk = 30
targetSdk = 36
versionCode = 1
versionName = "1.0"
versionName = "1.0.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -51,6 +51,8 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.material3)
implementation(libs.androidx.compose.foundation)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand All @@ -67,4 +69,8 @@ dependencies {

implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.viewmodel.ktx)

// navigation
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose.v270)
}
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>

</manifest>
144 changes: 10 additions & 134 deletions app/src/main/java/com/eggetteluo/folder/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,155 +1,31 @@
package com.eggetteluo.folder

import android.Manifest
import android.os.Bundle
import android.os.Environment
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import com.eggetteluo.folder.model.FileItem
import androidx.navigation.compose.rememberNavController
import com.eggetteluo.folder.ui.navigation.AppNavGraph
import com.eggetteluo.folder.ui.theme.FolderTheme
import com.eggetteluo.folder.viewModel.FileViewModel
import com.permissionx.guolindev.PermissionX
import java.io.File

class MainActivity : FragmentActivity() {

private val fileViewModel: FileViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
FolderTheme {
val hasPermission by fileViewModel.hasPermission.collectAsState()

Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
if (hasPermission) {
FileExplorerScreen(fileViewModel)
} else {
PermissionGuideScreen {
requestStoragePermission(this@MainActivity) {
fileViewModel.onPermissionGranted()
}
}
}
}
}
}
}
}
}

@Composable
fun FileExplorerScreen(viewModel: FileViewModel) {
val fileList by viewModel.fileList.collectAsState()
val currentPath by viewModel.currentPath.collectAsState()

// 处理安卓物理返回键:如果不在根目录,点击返回键则跳回上一级
BackHandler(enabled = currentPath.absolutePath != Environment.getExternalStorageDirectory().absolutePath) {
viewModel.navigateBack()
}

Column {
Surface(tonalElevation = 4.dp, modifier = Modifier.fillMaxWidth()) {
Text(
text = "当前路径: ${currentPath.absolutePath}",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.labelSmall
)
}

if (fileList.isEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("文件夹为空", color = Color.Gray)
}
} else {
LazyColumn {
items(fileList) { file ->
FileRow(file) {
if (file.isDirectory) {
viewModel.loadFiles(File(file.path))
}
FolderTheme {
val navController = rememberNavController()
Box(
modifier = Modifier.fillMaxSize()
) {
AppNavGraph(navController = navController)
}
}
}
}
}
}

@Composable
fun FileRow(file: FileItem, onClick: () -> Unit) {
ListItem(
modifier = Modifier.clickable { onClick() },
headlineContent = { Text(file.name) },
supportingContent = {
if (!file.isDirectory) {
Text("${file.size / 1024} KB")
}
},
leadingContent = {
Icon(
imageVector = if (file.isDirectory) Icons.Default.Folder else Icons.Default.Description,
contentDescription = null,
tint = if (file.isDirectory) Color(0xFFFFCA28) else Color.Gray
)
}
)
}

@Composable
fun PermissionGuideScreen(onButtonClick: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("需要“所有文件访问权限”才能管理文件")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onButtonClick) {
Text("去授权")
}
}
}

fun requestStoragePermission(activity: FragmentActivity, onPermissionGranted: () -> Unit) {
PermissionX.init(activity)
.permissions(Manifest.permission.MANAGE_EXTERNAL_STORAGE) // 请求所有文件访问权限
.onExplainRequestReason { scope, deniedList ->
scope.showRequestReasonDialog(
deniedList,
"文件管理器需要访问所有文件以进行管理,请在设置中允许权限",
"确定", "取消"
)
}
.onForwardToSettings { scope, deniedList ->
scope.showForwardToSettingsDialog(
deniedList,
"您需要去设置界面手动开启文件访问权限",
"去设置", "取消"
)
}
.request { allGranted, _, _ ->
if (allGranted) {
onPermissionGranted() // 权限获取成功,去刷新文件列表
} else {
// 处理权限被拒绝的情况,比如弹一个 Toast
}
}
}
16 changes: 16 additions & 0 deletions app/src/main/java/com/eggetteluo/folder/model/FileIconTheme.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.eggetteluo.folder.model

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector

/**
* 文件图标主题模型
* 用于封装文件在列表项中展示的图标向量和颜色方案
*
* @property icon 图标的 ImageVector(如 Icons.Default.Folder)
* @property color 图标的着色(如 Color.Yellow)
*/
data class FileIconTheme(
val icon: ImageVector,
val color: Color
)
17 changes: 11 additions & 6 deletions app/src/main/java/com/eggetteluo/folder/model/FileItem.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.eggetteluo.folder.model

data class FileItem(
val name: String,
val path: String,
val isDirectory: Boolean,
val size: Long,
val lastModified: Long
)
val name: String, // 文件名
val path: String, // 文件的真实物理绝对路径
val isDirectory: Boolean, // 是否为文件夹
val size: Long, // 文件大小 (字节)
val lastModified: Long, // 最后修改时间戳
val createdAt: Long = 0L, // 创建时间
val canRead: Boolean, // 是否可读
val canWrite: Boolean, // 是否可写
val isHidden: Boolean, // 是否是隐藏文件
val extension: String = "" // 后缀名 (如 "jpg", "pdf")
)
Loading