feat: add notes

This commit is contained in:
kagura 2024-11-04 15:17:31 +08:00
parent 5d4dd31b70
commit c3d8f75ee6
6 changed files with 235 additions and 91 deletions

View file

@ -4,15 +4,28 @@ import androidx.appcompat.app.AppCompatActivity
import com.dazuoye.filemanager.fileSystem.DeleteHelper.Companion.delete
import java.io.File
open class BaseActivity: AppCompatActivity() {
/**
* BaseActivity 继承自 AppCompatActivity提供了一些基础功能
* 主要功能是在销毁 Activity 时清理缓存和释放系统资源
*/
open class BaseActivity : AppCompatActivity() {
/**
* Activity 销毁时调用的方法
* 此方法用于删除缓存中的 "clipboard" 文件并触发垃圾回收
*/
override fun onDestroy() {
super.onDestroy()
val clipFile = File(this.cacheDir,"clipboard")
if (clipFile.exists()){
// 创建一个指向缓存目录中 "clipboard" 文件的引用
val clipFile = File(this.cacheDir, "clipboard")
// 如果 "clipboard" 文件存在,调用 delete 方法删除文件
if (clipFile.exists()) {
delete(clipFile.path)
}
// 显式调用垃圾回收器,提示系统进行内存回收
System.gc()
}
}
}

View file

@ -13,26 +13,36 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
// 扩展 Context定义一个 DataStore用于存储应用设置
val Context.settingStore: DataStore<Preferences> by preferencesDataStore(name = "app_settings")
/**
* SettingStorage 类用于管理和访问应用的设置存储
* 它封装了 DataStore 访问逻辑支持异步获取和设置偏好数据
*/
class SettingStorage(private val context: Context) {
val hideExtension = booleanPreferencesKey("hide_extension")
val hideHiddenFile = booleanPreferencesKey("hide_hidden_file")
/**
* 获取给定偏好设置键的值
* @param key 偏好设置键
* @return 偏好设置的值如果未设置则返回 null
*/
fun <T> get(key: Preferences.Key<T>): T? = runBlocking {
// 使用 DataStore 获取偏好数据,并获取与给定键关联的值
context.settingStore.data
.map { value -> value[key] }
.first() // 获取流的第一个值
}
fun <T> get(key: Preferences.Key<T>): T? =
runBlocking {
context.settingStore.data
.map { value ->
value[key]
}
.first()
}
fun <T> set(key: Preferences.Key<T>, value: T) =
CoroutineScope(Dispatchers.Default).launch {
context.settingStore.edit {
it[key] = value
}
}
}
/**
* 设置给定偏好设置键的值
* @param key 偏好设置键
* @param value 要存储的值
*/
fun <T> set(key: Preferences.Key<T>, value: T) = CoroutineScope(Dispatchers.Default).launch {
// 使用 DataStore 更新偏好数据
context.settingStore.edit { it[key] = value }
}
}

View file

@ -5,34 +5,52 @@ import java.io.FileInputStream
import java.io.FileOutputStream
import java.nio.channels.FileChannel
/**
* PasteHelper 类提供了静态方法用于复制文件和目录
*/
class PasteHelper {
companion object {
/**
* 复制目录及其所有内容到目标目录
* @param sourceDir 源目录
* @param destDir 目标目录
* 如果目标目录不存在则创建它
* 如果源目录不存在或参数不是目录则抛出异常
*/
fun copyDirectory(sourceDir: File, destDir: File) {
// creates the destination directory if it does not exist
// 如果目标目录不存在,则创建它
if (!destDir.exists()) {
destDir.mkdirs()
}
// throws exception if the source does not exist
// 如果源目录不存在,则抛出异常
require(sourceDir.exists()) { "sourceDir does not exist" }
// throws exception if the arguments are not directories
// 如果参数不是目录,则抛出异常
require(!(sourceDir.isFile || destDir.isFile)) { "Either sourceDir or destDir is not a directory" }
// 调用内部方法递归复制目录
copyDirectoryImpl(sourceDir, destDir)
}
/**
* 实现目录复制的内部方法
* @param sourceDir 源目录
* @param destDir 目标目录
* 遍历源目录中的所有文件和子目录并递归复制它们
*/
private fun copyDirectoryImpl(sourceDir: File, destDir: File) {
val items = sourceDir.listFiles()
if (items != null && items.isNotEmpty()) {
for (anItem: File in items) {
if (anItem.isDirectory) {
val newDir = File(destDir, anItem.name)
newDir.mkdir()
// copy the directory (recursive call)
newDir.mkdir() // 创建子目录
// 递归复制目录
copyDirectory(anItem, newDir)
} else {
// copy the file
// 复制单个文件
val destFile = File(destDir, anItem.name)
copySingleFile(anItem, destFile)
}
@ -40,21 +58,30 @@ class PasteHelper {
}
}
/**
* 复制单个文件到目标位置
* @param sourceFile 源文件
* @param destFile 目标文件
* 如果目标文件不存在则创建它
*/
private fun copySingleFile(sourceFile: File, destFile: File) {
if (!destFile.exists()) {
destFile.createNewFile()
destFile.createNewFile() // 创建目标文件
}
var sourceChannel: FileChannel? = null
var destChannel: FileChannel? = null
try {
// 使用文件通道进行文件复制
sourceChannel = FileInputStream(sourceFile).channel
destChannel = FileOutputStream(destFile).channel
sourceChannel.transferTo(0, sourceChannel.size(), destChannel)
} finally {
// 关闭文件通道,释放资源
sourceChannel?.close()
destChannel?.close()
}
}
}
}
}

View file

@ -36,84 +36,115 @@ import com.dazuoye.filemanager.fileSystem.byTypeFileLister.MusicLister
import com.dazuoye.filemanager.fileSystem.byTypeFileLister.VideoLister
import com.dazuoye.filemanager.main_page
/**
* RequirePermissionActivity 继承自 ComponentActivity用于请求用户的存储权限
* 如果权限已经被授予应用将导航到主页面并初始化系统
*/
class RequirePermissionActivity : ComponentActivity() {
/**
* Activity 创建时调用的方法
* 该方法启用全屏模式设置状态栏颜色并构建一个界面来提示用户授予存储权限
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
enableEdgeToEdge() // 启用全屏边到边显示
val activity = this
window.statusBarColor = getColor(R.color.WhiteSmoke)
window.statusBarColor = getColor(R.color.WhiteSmoke) // 设置状态栏颜色
// 使用 Jetpack Compose 构建 UI
setContent {
Column(
modifier = Modifier
.statusBarsPadding()
.fillMaxHeight(0.9f)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
.statusBarsPadding() // 为状态栏留出空间
.fillMaxHeight(0.9f) // 设置列高度为 90%
.fillMaxWidth(), // 填满宽度
horizontalAlignment = Alignment.CenterHorizontally, // 水平居中
verticalArrangement = Arrangement.Center // 垂直居中
) {
// 显示提示文本,根据系统版本显示不同的提示信息
Text(
text = getString(
if (VERSION.SDK_INT >= VERSION_CODES.R) {
R.string.require_manage_storage
R.string.require_manage_storage // Android R 及以上需要的权限
} else {
R.string.require_permission_readwrite
R.string.require_permission_readwrite // 较低版本需要的权限
}
),
modifier = Modifier.padding(vertical = 10.dp),
fontSize = 30.sp
modifier = Modifier.padding(vertical = 10.dp), // 设置垂直内边距
fontSize = 30.sp // 设置字体大小
)
// 按钮,用于请求用户授予存储权限
Button(
onClick = { // Ask for permission
onClick = { // 按钮点击事件
if (VERSION.SDK_INT >= VERSION_CODES.R) {
// Android R 及以上版本
if (!Environment.isExternalStorageManager()) {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
intent.setData(uri)
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) // 清空栈顶
startActivity(intent)
}
} else { // for legacy system
val permissions =
arrayOf(permission.READ_EXTERNAL_STORAGE, permission.WRITE_EXTERNAL_STORAGE)
} else {
// 针对较旧版本的系统请求权限
val permissions = arrayOf(
permission.READ_EXTERNAL_STORAGE,
permission.WRITE_EXTERNAL_STORAGE
)
ActivityCompat.requestPermissions(
activity, permissions, 100
)
}
},
colors = ButtonColors(
containerColor = Color(0xFF039BE5),
contentColor = Color.White,
disabledContainerColor = Color.Gray,
disabledContentColor = Color.White
containerColor = Color(0xFF039BE5), // 按钮背景颜色
contentColor = Color.White, // 按钮文本颜色
disabledContainerColor = Color.Gray, // 按钮禁用状态背景颜色
disabledContentColor = Color.White // 按钮禁用状态文本颜色
)
) {
// 按钮文本
Text(text = getString(R.string.give_permission))
}
}
}
}
/**
* Activity 恢复时调用的方法
* 检查是否已经授予权限如果是则导航到主页面并初始化系统
*/
override fun onResume() {
super.onResume()
if (checkPermissions(this)) {
val intent = Intent(this, main_page::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) // 清空栈顶
startActivity(intent)
initSystem()
initSystem() // 初始化系统
finish()
}
}
}
/**
* 检查应用是否已获得所需的存储权限
* @param context 上下文用于访问权限检查方法
* @return Boolean 如果权限已被授予返回 true否则返回 false
*/
fun checkPermissions(context: Context): Boolean {
// Check storage permission
// 对于 Android R 及以上版本,检查是否具有管理所有文件的权限
if (VERSION.SDK_INT >= VERSION_CODES.R) {
// Check manage storage on R+
if (!Environment.isExternalStorageManager()) {
return false
}
} else {
val permissions = arrayOf(permission.READ_EXTERNAL_STORAGE, permission.WRITE_EXTERNAL_STORAGE)
// 对于较低版本,逐个检查读写权限
val permissions = arrayOf(
permission.READ_EXTERNAL_STORAGE,
permission.WRITE_EXTERNAL_STORAGE
)
permissions.forEach {
if (context.checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED) {
return false
@ -123,9 +154,12 @@ fun checkPermissions(context: Context): Boolean {
return true
}
/**
* 初始化系统的资源列表如图像视频音乐和文档
*/
fun initSystem() {
ImageLister.instance.initialize()
VideoLister.instance.initialize()
MusicLister.instance.initialize()
DocumentLister.instance.initialize()
ImageLister.instance.initialize() // 初始化图像列表
VideoLister.instance.initialize() // 初始化视频列表
MusicLister.instance.initialize() // 初始化音乐列表
DocumentLister.instance.initialize() // 初始化文档列表
}

View file

@ -4,33 +4,12 @@ import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AlertDialog.Builder
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -52,20 +31,32 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
/**
* 用于在搜索结果中显示文件列表的类
* @param context 上下文对象
* @param searchTypeName 搜索类型名称用于显示在UI中
* @param searchRegex 搜索的正则表达式
*/
class SearchFileColumn(
val context: Context,
private val searchTypeName: String,
private val searchRegex: Regex?
) {
// 保存搜索到的文件列表
private val fileList = mutableStateListOf<WrappedFile>()
// 搜索输入的文本状态
private val searchText = mutableStateOf("")
/**
* Composable函数用于绘制搜索文件的主界面
*/
@Composable
fun Draw() {
var list by remember { mutableStateOf<List<String>>(emptyList()) }
var isOkay by remember { mutableStateOf(false) }
var sortByTime by remember { mutableStateOf(true) }
// 当搜索结果或排序方式改变时,重新加载并排序文件列表
LaunchedEffect(isOkay, list, sortByTime) {
isOkay = false
fileList.clear()
@ -78,6 +69,7 @@ class SearchFileColumn(
isOkay = true
}
// 主界面布局,包含返回按钮、标题和排序按钮
Column(
modifier = Modifier
.fillMaxSize()
@ -91,6 +83,7 @@ class SearchFileColumn(
.padding(horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 返回按钮,点击时返回主页面
IconButton(
onClick = {
val intent = Intent(
@ -106,6 +99,7 @@ class SearchFileColumn(
)
}
// 显示搜索标题或搜索结果
Text(
text = if (list.isEmpty()) {
context.getString(R.string.search_here, searchTypeName)
@ -122,6 +116,7 @@ class SearchFileColumn(
Spacer(modifier = Modifier.weight(1f))
// 排序按钮,切换按时间或大小排序
IconButton(
onClick = {
sortByTime = !sortByTime
@ -148,20 +143,23 @@ class SearchFileColumn(
), "sortMethod"
)
}
}
// 加载或显示搜索结果
if (!isOkay) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// 显示“加载中”的文本
Text(
text = context.getString(R.string.loading),
fontSize = 34.sp
)
}
} else {
// 显示搜索结果的文件列表
DrawColumns(
fileList,
searchText = searchText.value,
@ -186,6 +184,7 @@ class SearchFileColumn(
}
}
) {
// 文件点击事件,打开对应的文件
val file = File(it)
if (file.isFile) {
val uri = FileProvider.getUriForFile(
@ -203,6 +202,13 @@ class SearchFileColumn(
}
}
/**
* Composable函数用于绘制文件列表
* @param fileList 文件列表
* @param searchText 搜索输入文本
* @param onSearch 搜索按钮点击事件
* @param onItemClick 文件点击事件
*/
@Composable
private fun DrawColumns(
fileList: List<WrappedFile>,
@ -213,7 +219,7 @@ class SearchFileColumn(
LazyColumn(
modifier = Modifier.padding(vertical = 5.dp)
) {
// 最顶上那个
// 搜索输入框
item {
Row(
modifier = Modifier
@ -223,6 +229,7 @@ class SearchFileColumn(
) {
var searchInput by remember { mutableStateOf(searchText) }
// 搜索输入框
TextField(
value = searchInput,
maxLines = 1,
@ -238,6 +245,7 @@ class SearchFileColumn(
),
textStyle = TextStyle(fontSize = 18.sp),
trailingIcon = {
// 点击图标进行搜索
Image(
ImageVector.vectorResource(R.drawable.ic_search),
context.getString(R.string.search),
@ -245,6 +253,7 @@ class SearchFileColumn(
.clickable {
if (searchInput.isNotEmpty()) {
if (searchInput == "." || searchInput == "..") {
// 输入非法时提示错误
Toast
.makeText(
context,
@ -269,7 +278,7 @@ class SearchFileColumn(
}
}
// 下面的内容
// 显示每个文件的视图
items(fileList) { file ->
FileSingleView(
file,
@ -279,12 +288,16 @@ class SearchFileColumn(
}
}
/**
* Composable函数用于绘制单个文件项
* @param file 文件对象
* @param onItemClick 文件点击事件
*/
@Composable
private fun FileSingleView(
file: WrappedFile,
onItemClick: ((String) -> Unit)? = null
) {
Row(
modifier = Modifier
.fillMaxWidth()
@ -299,6 +312,7 @@ class SearchFileColumn(
},
verticalAlignment = Alignment.CenterVertically,
) {
// 显示文件图标
Image(
ImageVector.vectorResource(
when (file.mime.split('/').first()) {
@ -317,12 +331,14 @@ class SearchFileColumn(
.padding(horizontal = 15.dp)
.fillMaxWidth(0.8f)
) {
// 文件名称
Text(
text = file.name,
fontSize = 24.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// 文件最后修改时间
Text(
text = file.getModifiedTimeString(context),
fontSize = 15.sp,
@ -333,6 +349,7 @@ class SearchFileColumn(
Spacer(Modifier.weight(1f))
// 显示文件信息的按钮
IconButton(
onClick = {
showFileInfoAlert(context, file.path)
@ -346,6 +363,11 @@ class SearchFileColumn(
}
}
/**
* 显示文件详细信息的弹窗
* @param context 上下文对象
* @param file 文件路径
*/
fun showFileInfoAlert(context: Context, file: String) {
val f = File(file)
if (!f.exists()) {
@ -369,4 +391,4 @@ class SearchFileColumn(
}
.show()
}
}
}

View file

@ -4,18 +4,30 @@ use jni::JNIEnv;
use std::{fs, i64};
use walkdir::WalkDir;
/// 获取指定文件夹的总大小,并将其格式化为字符串后返回给 Java
///
/// # 参数
/// - `env`: JNI 环境对象,用于与 Java 交互
/// - `_`: `JClass` 类型,表示调用此方法的 Java 类(未使用)
/// - `input`: Java 传递的字符串,代表文件夹路径
///
/// # 返回
/// - `jstring`: 表示文件夹大小的格式化字符串,如果发生错误,返回错误信息
#[no_mangle]
pub extern "system" fn Java_com_dazuoye_filemanager_utils_FSHelper_getFolderSizeNative<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
input: JString<'local>,
) -> jstring {
// 将 Java 字符串转换为 Rust 字符串
let dir: String = env
.get_string(&input)
.expect("failed to parse input")
.into();
// 检查目录是否存在
if !fs::exists(&dir).expect(format!("Cannot stat {dir}").as_str()) {
// 如果目录不存在,返回相应的错误信息
return env
.new_string(format!("{} not exists!", dir))
.expect("Couldn't create java string!")
@ -24,25 +36,29 @@ pub extern "system" fn Java_com_dazuoye_filemanager_utils_FSHelper_getFolderSize
// 从这里保证文件至少存在了
let mut size: u64 = 0;
// 使用 WalkDir 遍历目录中的所有文件和子目录
for entry in WalkDir::new(dir) {
match entry {
Ok(item) => {
// 尝试获取每个文件的元数据并累加其大小
match item.metadata() {
Ok(metadata) => size += metadata.len(),
Err(e) => {
// 处理元数据获取失败的情况
eprintln!("Error getting metadata for item: {e:?}");
continue;
}
}
},
Err(e) => {
// 处理遍历目录时的错误
eprintln!("Error walking directory: {:?}", e);
continue;
}
}
}
// 将计算出的文件夹大小格式化为字符串并返回给 Java
let output = env
.new_string(format_size(size))
.expect("Couldn't create java string!");
@ -50,48 +66,70 @@ pub extern "system" fn Java_com_dazuoye_filemanager_utils_FSHelper_getFolderSize
output.into_raw()
}
/// 获取指定文件夹的总大小(以字节为单位),返回给 Java
///
/// # 参数
/// - `env`: JNI 环境对象,用于与 Java 交互
/// - `_`: `JClass` 类型,表示调用此方法的 Java 类(未使用)
/// - `input`: Java 传递的字符串,代表文件夹路径
///
/// # 返回
/// - `jlong`: 文件夹的总大小(以字节为单位),如果发生错误则返回 0
#[no_mangle]
pub extern "system" fn Java_com_dazuoye_filemanager_utils_FSHelper_getFolderSizeBytesNative<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
input: JString<'local>,
) -> jlong {
// 将 Java 字符串转换为 Rust 字符串
let dir: String = env
.get_string(&input)
.expect("failed to parse input")
.into();
// 检查目录是否存在
if !fs::exists(&dir).expect(format!("Cannot stat {}", dir).as_str()) {
return 0;
}
// 从这里保证文件至少存在了
let mut size: u64 = 0;
// 使用 WalkDir 遍历目录中的所有文件和子目录
for entry in WalkDir::new(dir) {
match entry {
Ok(item) => {
// 尝试获取每个文件的元数据并累加其大小
match item.metadata() {
Ok(metadata) => size += metadata.len(),
Err(e) => {
// 处理元数据获取失败的情况
eprintln!("Error getting metadata for item: {:?}", e);
continue;
}
}
},
Err(e) => {
// 处理遍历目录时的错误
eprintln!("Error walking directory: {:?}", e);
continue;
}
}
}
// 将文件夹大小转换为 `jlong`,如果溢出则返回 `i64::MAX`
match i64::try_from(size) {
Ok(compatible) => compatible,
Err(_) => i64::MAX
}
}
/// 格式化文件大小为更易读的字符串格式
///
/// # 参数
/// - `size`: 文件大小,以字节为单位
///
/// # 返回
/// - `String`: 格式化后的文件大小字符串(带单位,如 "KB", "MB" 等)
fn format_size(size: u64) -> String {
// 定义单位
let units = ["B", "KB", "MB", "GB", "TB", "PB"];
@ -106,4 +144,4 @@ fn format_size(size: u64) -> String {
// 保留两位小数格式化输出
format!("{:.2} {}", size, units[unit_index])
}
}