commit 3242c9f4a02a30f8728e6beb10894857ccc6be84 Author: Kagura Date: Thu Oct 10 12:46:38 2024 +0800 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..b3405b3 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +My Application \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..f2f5c22 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..6806f5a --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,53 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..d4b7acc --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0ad17cb --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..f80ec5c --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,82 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) + +} + +android { + namespace = "com.example.myapplication" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.myapplication" + minSdk = 29 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + viewBinding = true + compose = true + buildConfig = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.activity) + implementation(libs.constraintlayout) + implementation(libs.core.ktx) + implementation(libs.navigation.fragment) + implementation(libs.navigation.ui) + implementation(libs.lifecycle.runtime.ktx) + implementation(libs.activity.compose) + implementation(platform(libs.compose.bom)) + implementation(libs.ui) + implementation(libs.ui.graphics) + implementation(libs.ui.tooling.preview) + implementation(libs.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation(libs.ui.test.junit4) + debugImplementation(libs.ui.tooling) + debugImplementation(libs.ui.test.manifest) + + + implementation(libs.commons.io) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/myapplication/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/myapplication/ExampleInstrumentedTest.java new file mode 100644 index 0000000..982ba51 --- /dev/null +++ b/app/src/androidTest/java/com/example/myapplication/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.myapplication; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.myapplication", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3ed858b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_camera-playstore.png b/app/src/main/ic_camera-playstore.png new file mode 100644 index 0000000..4f2e4d2 Binary files /dev/null and b/app/src/main/ic_camera-playstore.png differ diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..3a76d6f Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/ic_picture1-playstore.png b/app/src/main/ic_picture1-playstore.png new file mode 100644 index 0000000..4f2e4d2 Binary files /dev/null and b/app/src/main/ic_picture1-playstore.png differ diff --git a/app/src/main/ic_wechat-playstore.png b/app/src/main/ic_wechat-playstore.png new file mode 100644 index 0000000..bf3fd5a Binary files /dev/null and b/app/src/main/ic_wechat-playstore.png differ diff --git a/app/src/main/java/com/example/myapplication/adapters/Document.kt b/app/src/main/java/com/example/myapplication/adapters/Document.kt new file mode 100644 index 0000000..2691501 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/adapters/Document.kt @@ -0,0 +1,33 @@ +package com.example.myapplication.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.TextView +import com.example.myapplication.R +import java.io.File + +class DocumentModel(document: File) { + val name: String = document.name + + init { + if (!document.isFile) { + throw RuntimeException("No such document") + } + } +} + +class DocumentAdapter(context: Context, list: ArrayList) : + ArrayAdapter(context, 0, list) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val listView = convertView ?: LayoutInflater.from(context).inflate( + R.layout.document_card_item, parent, false + ) + val model = getItem(position) ?: throw RuntimeException() + val card = listView.findViewById(R.id.iconButton) + card.text = model.name + return listView + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/adapters/Image.kt b/app/src/main/java/com/example/myapplication/adapters/Image.kt new file mode 100644 index 0000000..bc23924 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/adapters/Image.kt @@ -0,0 +1,43 @@ +package com.example.myapplication.adapters + +import android.content.Context +import android.graphics.Bitmap +import android.media.ThumbnailUtils +import android.util.Size +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AbsListView.LayoutParams +import android.widget.ArrayAdapter +import android.widget.GridView +import android.widget.ImageView +import android.widget.TextView +import com.example.myapplication.R +import java.io.File + +class ImageModel(image: File) { + val name: String = image.name + var thumbnail: Bitmap + + init { + if (!image.isFile) { + throw RuntimeException("No such Image") + } + thumbnail = ThumbnailUtils.createImageThumbnail(image, Size(512, 512), null) + } +} + +class ImageAdapter(context: Context, list: ArrayList) : + ArrayAdapter(context, 0, list) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val listView = convertView ?: LayoutInflater.from(context).inflate( + R.layout.picture_card_item, parent, false + ) + val model = getItem(position) ?: throw RuntimeException() + listView.findViewById(R.id.pictureCardImage).setImageBitmap(model.thumbnail) + listView.findViewById(R.id.pictureCardText).text = model.name + listView.setLayoutParams(LayoutParams(GridView.AUTO_FIT, 530)) + + return listView + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/adapters/Music.kt b/app/src/main/java/com/example/myapplication/adapters/Music.kt new file mode 100644 index 0000000..916d134 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/adapters/Music.kt @@ -0,0 +1,33 @@ +package com.example.myapplication.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.TextView +import com.example.myapplication.R +import java.io.File + +class MusicModel(music: File) { + val name: String = music.nameWithoutExtension // 歌曲就不放扩展名了 + + init { + if (!music.isFile) { + throw RuntimeException("No such Video") + } + } +} + +class MusicAdapter(context: Context, list: ArrayList) : + ArrayAdapter(context, 0, list) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val listView = convertView ?: LayoutInflater.from(context).inflate( + R.layout.music_card_item, parent, false + ) + val model = getItem(position) ?: throw RuntimeException() + val card = listView.findViewById(R.id.iconButton) + card.text = model.name + return listView + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/adapters/Video.kt b/app/src/main/java/com/example/myapplication/adapters/Video.kt new file mode 100644 index 0000000..97fde5e --- /dev/null +++ b/app/src/main/java/com/example/myapplication/adapters/Video.kt @@ -0,0 +1,46 @@ +package com.example.myapplication.adapters + +import android.content.Context +import android.graphics.Bitmap +import android.media.ThumbnailUtils +import android.util.Size +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AbsListView.LayoutParams +import android.widget.ArrayAdapter +import android.widget.GridView +import android.widget.ImageView +import android.widget.TextView +import com.example.myapplication.R +import java.io.File + +class VideoModel(video: File) { + val name: String = video.name + var thumbnail: Bitmap + + init { + if (!video.isFile) { + throw RuntimeException("No such Video") + } + thumbnail = ThumbnailUtils.createVideoThumbnail( + video, Size(854, 480) // 考虑到视频多16:9 + , null + ) + } +} + +class VideoAdapter(context: Context, list: ArrayList) : + ArrayAdapter(context, 0, list) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val listView = convertView ?: LayoutInflater.from(context).inflate( + R.layout.picture_card_item, parent, false + ) + val model = getItem(position) ?: throw RuntimeException() + listView.findViewById(R.id.pictureCardImage).setImageBitmap(model.thumbnail) + listView.findViewById(R.id.pictureCardText).text = model.name + listView.setLayoutParams(LayoutParams(GridView.AUTO_FIT, 530)) + + return listView + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/compose/PasteHelper.kt b/app/src/main/java/com/example/myapplication/compose/PasteHelper.kt new file mode 100644 index 0000000..88f5896 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/compose/PasteHelper.kt @@ -0,0 +1,60 @@ +package com.example.myapplication.compose + +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.nio.channels.FileChannel + +class PasteHelper { + companion object{ + 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) + } + + 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) + copyDirectory(anItem, newDir) + } else { + // copy the file + val destFile = File(destDir, anItem.name) + copySingleFile(anItem, destFile) + } + } + } + } + + private fun copySingleFile(sourceFile: File, destFile: File){ + if (!destFile.exists()) { + 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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/compose/RequirePermissionActivity.kt b/app/src/main/java/com/example/myapplication/compose/RequirePermissionActivity.kt new file mode 100644 index 0000000..33c6332 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/compose/RequirePermissionActivity.kt @@ -0,0 +1,145 @@ +package com.example.myapplication.compose + +import android.Manifest.permission +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import android.os.Bundle +import android.os.Environment +import android.provider.Settings +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.app.ActivityCompat +import com.example.myapplication.BuildConfig +import com.example.myapplication.R +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister +import com.example.myapplication.fileSystem.byTypeFileLister.ImageLister +import com.example.myapplication.fileSystem.byTypeFileLister.MusicLister +import com.example.myapplication.fileSystem.byTypeFileLister.VideoLister +import com.example.myapplication.main_page + +class RequirePermissionActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + val activity = this + window.statusBarColor = getColor(R.color.WhiteSmoke) + setContent { + Column( + modifier = Modifier.statusBarsPadding() + .fillMaxHeight(0.9f) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = getString( + if (VERSION.SDK_INT >= VERSION_CODES.R) { + R.string.require_manage_storage + } else { + R.string.require_permission_readwrite + } + ), + modifier = Modifier.padding(vertical = 10.dp), + fontSize = 30.sp + ) + Button( + onClick = { // Ask for permission + if (VERSION.SDK_INT >= VERSION_CODES.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) + startActivity(intent) + } + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + val perm33 = arrayOf(permission.READ_MEDIA_AUDIO,permission.READ_MEDIA_VIDEO,permission.READ_MEDIA_IMAGES) + ActivityCompat.requestPermissions( + activity, perm33, 101 + ) + } + } else { // for legacy system + 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 + ) + ) { + Text(text = getString(R.string.give_permission)) + } + } + } + + } + + override fun onResume() { + super.onResume() + if (checkPermissions(this)) { + val intent = Intent(this, main_page::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + initSystem() + finish() + } + } +} + +fun checkPermissions(context: Context): Boolean { + // Check storage permission + if (VERSION.SDK_INT >= VERSION_CODES.R) { + // Check manage storage on R+ + if (!Environment.isExternalStorageManager()) { + return false + } + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + val perm33 = arrayOf(permission.READ_MEDIA_AUDIO,permission.READ_MEDIA_VIDEO,permission.READ_MEDIA_IMAGES) + perm33.forEach { + if (context.checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED) { + return false + } + } + } + } else { + val permissions = arrayOf(permission.READ_EXTERNAL_STORAGE, permission.WRITE_EXTERNAL_STORAGE) + permissions.forEach { + if (context.checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED) { + return false + } + } + } + return true +} + +fun initSystem(){ + ImageLister.instance.initialize() + VideoLister.instance.initialize() + MusicLister.instance.initialize() + DocumentLister.instance.initialize() +} diff --git a/app/src/main/java/com/example/myapplication/compose/ViewFileActivity.kt b/app/src/main/java/com/example/myapplication/compose/ViewFileActivity.kt new file mode 100644 index 0000000..ba659e7 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/compose/ViewFileActivity.kt @@ -0,0 +1,39 @@ +package com.example.myapplication.compose + +import android.os.Bundle +import android.os.Environment +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.example.myapplication.R +import com.example.myapplication.compose.ui.FileColumn +import java.io.File + +class ViewFileActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + var path = + intent.extras?.getString("folder") ?: Environment.getExternalStorageDirectory().path + val file = File(path) + if (!file.isDirectory){ + path = Environment.getExternalStorageDirectory().path + } + enableEdgeToEdge() + window.statusBarColor = getColor(R.color.WhiteSmoke) + setContent { + Surface( + modifier = Modifier + .fillMaxSize() + .background(Color(getColor(R.color.WhiteSmoke))) + ){ + FileColumn(this).Draw(path) + } + } + } +} + diff --git a/app/src/main/java/com/example/myapplication/compose/ui/FileColumn.kt b/app/src/main/java/com/example/myapplication/compose/ui/FileColumn.kt new file mode 100644 index 0000000..9539c9c --- /dev/null +++ b/app/src/main/java/com/example/myapplication/compose/ui/FileColumn.kt @@ -0,0 +1,693 @@ +package com.example.myapplication.compose.ui + +import android.content.ClipData +import android.content.ClipDescription +import android.content.Context +import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.draganddrop.dragAndDropSource +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.foundation.gestures.detectTapGestures +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.DragAndDropTransferData +import androidx.compose.ui.draganddrop.mimeTypes +import androidx.compose.ui.draganddrop.toAndroidDragEvent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.FileProvider +import com.example.myapplication.R +import com.example.myapplication.compose.PasteHelper +import com.example.myapplication.fileSystem.CutHelper +import com.example.myapplication.fileSystem.WrappedFile +import com.example.myapplication.fileSystem.WrappedFile.Type +import com.example.myapplication.main_page +import com.example.myapplication.utils.AlertHelper +import com.example.myapplication.utils.ClipHelper +import org.apache.commons.io.IOUtils +import java.io.File +import java.io.FileNotFoundException + +class FileColumn(val context: Context) { + private val fileList = mutableStateListOf() + + @Composable + fun Draw(startFolder: String) { + var path by remember { mutableStateOf(startFolder) } + var shouldUpdate by remember { mutableStateOf(false) } + val cwd = File(path) + if (!cwd.isDirectory) { + return + } + var isOkay by remember { mutableStateOf(false) } + + LaunchedEffect(path, shouldUpdate) { + isOkay = false + fileList.clear() + cwd.listFiles()?.forEach { f -> + fileList.add(WrappedFile(f)) + } + isOkay = true + } + + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .navigationBarsPadding() + .background(Color(context.getColor(R.color.WhiteSmoke))) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { + val intent = Intent( + context, + main_page::class.java + ) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + context.startActivity(intent) + } + ) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_left_arrow), "back" + ) + } + + Text( + text = path, + fontSize = 24.sp, + modifier = Modifier.padding(start = 16.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + } + 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, path, update = { shouldUpdate = !shouldUpdate }) { + if (it == "/storage/emulated") { + return@DrawColumns + } + val file = File(it) + if (file.isDirectory) { + path = it + } else if (file.isFile) { + val uri = FileProvider.getUriForFile( + context, + context.packageName + ".provider", + file + ) + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(uri, WrappedFile(file).mime) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + context.startActivity(intent) + } + } + } + } + } + + private companion object{ + var dropTarget: String? = null + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + private fun DrawColumns( + fileList: List, + cwd: String, + parent: String? = null, + update: (() -> Unit)? = null, + onItemClick: ((String) -> Unit)? = null + ) { + val dragAndDropCallBack = remember { + object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + val target = File(dropTarget ?: return false) + if (!target.exists()){ + return false.also { + dropTarget = null + } + } + val data = event.toAndroidDragEvent() + .clipData.getItemAt(0).text + if (!data.startsWith("Drag:")) { + return false.also { + dropTarget = null + } + } + + val source = File(data.split(':').last()) + if (!source.exists()) { + return false.also { + dropTarget = null + } + } + + if (source.path == target.path){ + dropTarget = null + return false + } + + if (target.isFile) { + if (source.isFile) { + val dir = source.parent ?: return false + val f = File("$dir/合并文件夹") + if (!f.exists()){ + f.mkdir() + } + ClipHelper.getInstance(context).copy(source,context) + val sourceUri = ClipHelper.getInstance(context).paste() ?: return false + val inputStream = try { + context.contentResolver.openInputStream(sourceUri) + }catch (e: FileNotFoundException) { + return false.also { + dropTarget = null + } + } + if (inputStream != null) { + val actualFile = File(f, source.name) + actualFile.writeBytes(IOUtils.toByteArray(inputStream)) + inputStream.close() + } + ClipHelper.getInstance(context).copy(target,context) + val targetUri = ClipHelper.getInstance(context).paste() ?: return false + val inputStream2 = context.contentResolver.openInputStream(targetUri) + if (inputStream2 != null) { + val actualFile = File(f, target.name) + actualFile.writeBytes(IOUtils.toByteArray(inputStream2)) + inputStream2.close() + } + Toast.makeText( + context, + "已放入 $dir/合并文件夹", + Toast.LENGTH_SHORT + ).show() + } else if (source.isDirectory) { + ClipHelper.getInstance(context).copy(target,context) + val sourceUri = ClipHelper.getInstance(context).paste() ?: return false.also { + dropTarget = null + } + val inputStream = try { + context.contentResolver.openInputStream(sourceUri) + }catch (e: FileNotFoundException) { + return false.also { + dropTarget = null + } + } + if (inputStream != null) { + val actualFile = File(source, target.name) + actualFile.writeBytes(IOUtils.toByteArray(inputStream)) + inputStream.close() + } + Toast.makeText( + context, + "已放入 ${source.path}", + Toast.LENGTH_SHORT + ).show() + } + } else if (target.isDirectory) { + if (source.isFile) { + ClipHelper.getInstance(context).copy(source,context) + val sourceUri = ClipHelper.getInstance(context).paste() ?: return false.also { + dropTarget = null + } + val inputStream = try { + context.contentResolver.openInputStream(sourceUri) + }catch (e: FileNotFoundException) { + return false.also { + dropTarget = null + } + } + if (inputStream != null) { + val actualFile = File(target, source.name) + actualFile.writeBytes(IOUtils.toByteArray(inputStream)) + inputStream.close() + } + Toast.makeText( + context, + "已放入 ${target.path}", + Toast.LENGTH_SHORT + ).show() + } + } + update?.invoke() + dropTarget = null + return true + } + } + } + + LazyColumn( + modifier = Modifier.padding(vertical = 5.dp) + ) { + // 最顶上那个 + if (parent == null) { + val parts = cwd.split('/') + if (parts.lastIndex != 0) { + val prevDir = StringBuilder() + for (i in 0.. + FileSingleView( + file, cwd, + update = update, + dragAndDrop = Modifier + .dragAndDropSource { + detectTapGestures(onLongPress = { + startTransfer( + DragAndDropTransferData( + ClipData.newPlainText( + "Drag", "Drag:${file.path}" + ) + ) + ) + }) + } + , + dragAndDropCallBack = dragAndDropCallBack, + onItemClick = onItemClick + ) + } + } + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + private fun FileSingleView( + file: WrappedFile, + cwd: String, + forceIcon: ImageVector? = null, + forceName: String? = null, + forceParent: String? = null, + update: (() -> Unit)? = null, + dragAndDrop: Modifier = Modifier, + dragAndDropCallBack: DragAndDropTarget? = null, + onItemClick: ((String) -> Unit)? = null + ) { + val openFileDialog = remember { mutableIntStateOf(3) } + when (openFileDialog.intValue) { + 0 -> AskForName( + onDismissRequest = { openFileDialog.intValue = 3 }, + onConfirmation = { + if (it.isEmpty()) { + Toast.makeText( + context, + context.getString(R.string.error_need_input_name), + Toast.LENGTH_SHORT + ).show() + } else { + val f = File("$cwd/$it") + if (f.exists()) { + Toast.makeText( + context, + context.getString(R.string.error_already_exist), + Toast.LENGTH_SHORT + ).show() + } else { + f.mkdir() + } + } + update?.invoke() + openFileDialog.intValue = 3 + }, + dir = cwd, + isDirectory = true + ) + + 1 -> AskForName( + onDismissRequest = { openFileDialog.intValue = 3 }, + onConfirmation = { + if (it.isEmpty()) { + Toast.makeText( + context, + context.getString(R.string.error_need_input_name), + Toast.LENGTH_SHORT + ).show() + } else { + val f = File("$cwd/$it") + if (f.exists()) { + Toast.makeText( + context, + context.getString(R.string.error_already_exist), + Toast.LENGTH_SHORT + ).show() + } else { + f.createNewFile() + } + } + update?.invoke() + openFileDialog.intValue = 3 + }, + dir = cwd, + isDirectory = false + ) + + else -> {} + } + + Row( + modifier = Modifier + .fillMaxWidth() + .border( + width = Dp.Hairline, + color = Color.Gray, + shape = RectangleShape + ) + .padding(vertical = 3.dp) + .clickable { + onItemClick?.invoke(file.path) + }.then( + if (dragAndDropCallBack != null) { + Modifier.dragAndDropTarget( + shouldStartDragAndDrop = { event -> + val result = event + .mimeTypes() + .contains(ClipDescription.MIMETYPE_TEXT_PLAIN) + if (result) { + dropTarget = file.path + } + result + }, target = dragAndDropCallBack + ) + }else{ + Modifier + } + ) + + , + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + forceIcon ?: ImageVector.vectorResource( + when (file.mime.split('/').first()) { + "dir" -> R.drawable.type_directory + "image" -> R.drawable.type_image + "video" -> R.drawable.type_video + "audio" -> R.drawable.type_audio + else -> R.drawable.type_file + } + ), file.mime, + modifier = Modifier + .padding(horizontal = 8.dp) + .then(dragAndDrop) + ) + Column( + modifier = Modifier + .padding(horizontal = 15.dp) + .fillMaxWidth(0.8f) + ) { + Text( + text = forceName ?: file.name, + fontSize = 24.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = forceParent ?: file.getModifiedTimeString(context), + fontSize = 15.sp, + color = Color.Gray, + maxLines = 1 + ) + } + + Spacer(Modifier.weight(1f)) + + if (forceName == null) { + IconButton( + onClick = { + if (file.type == Type.FILE) { // 普通文件 + AlertHelper.showNoPasteAlert(context, + onCopy = { + val f = File(file.path) + if (f.isFile) { + ClipHelper.getInstance(context).copy(f, context) + } + }, + onPaste = { + + }, + onDelete = { + AlertHelper.showDeleteAlert(context, file.path) { + update?.invoke() + } + }, + onCut = { + CutHelper.cut(context, File(file.path)) + update?.invoke() + }, + onInfo = { + AlertHelper.showFileInfoAlert(context, file.path) + } + ) + } else { // 普通文件夹 + AlertHelper.showNoPasteAlert(context, + onCopy = { + val f = File(file.path) + if (f.isDirectory) { + ClipHelper.getInstance(context).copyFolder(f.path) + } + }, + onPaste = { + + }, + onDelete = { + AlertHelper.showDeleteAlert(context, file.path) { + update?.invoke() + } + }, + onCut = { + val f = File(file.path) + if (f.isDirectory) { + CutHelper.cutFolder(context, f) + } + update?.invoke() + }, + onInfo = { + AlertHelper.showFileInfoAlert(context, file.path) + } + ) + } + }, + modifier = Modifier.padding(horizontal = 10.dp) + ) { + Image( + ImageVector.vectorResource(R.drawable.outline_info_24), "info" + ) + } + } else { // 最上面那个按钮 + IconButton( + onClick = { + AlertHelper.showOnlyPasteInfoNewAlert(context, + onPaste = { + val uri = ClipHelper.getInstance(context).paste() + if (uri != null) { + val name = uri.path?.split('/')?.last() ?: "somePastedItem" + val ext = name.split('.').last() + var actualFile = File(cwd, name) + while (actualFile.exists()) { + actualFile = File("${actualFile.path}_paste.$ext") + } + val inputStream = context.contentResolver.openInputStream(uri) + if (inputStream != null) { + actualFile.writeBytes(IOUtils.toByteArray(inputStream)) + inputStream.close() + // 刷新 + update?.invoke() + } + } else { + val pasteDir = ClipHelper.getInstance(context).pasteFolder() + if (pasteDir != null) { + val sourceDir = File(pasteDir) + if (sourceDir.isDirectory) { + var destDir = File("$cwd/${sourceDir.name}") + while (destDir.exists()) { + destDir = File("${destDir.path}_paste") + } + destDir.mkdir() + PasteHelper.copyDirectory(sourceDir, destDir) + } + update?.invoke() + } else { + Toast.makeText( + context, + context.getString(R.string.error_nothing_to_paste), + Toast.LENGTH_SHORT + ) + .show() + } + } + }, + onInfo = { + AlertHelper.showFileInfoAlert(context, cwd) + }, + onNewFile = { + openFileDialog.intValue = 1 + }, + onNewFolder = { + openFileDialog.intValue = 0 + } + ) + }, + modifier = Modifier.padding(horizontal = 10.dp) + ) { + Image( + ImageVector.vectorResource(R.drawable.outline_info_i_24), "info" + ) + } + } + } + } + + @Composable + fun AskForName( + onDismissRequest: () -> Unit, + onConfirmation: (String) -> Unit, + dir: String, + isDirectory: Boolean + ) { + var input by remember { mutableStateOf("") } + AlertDialog( + icon = { + Icon( + ImageVector.vectorResource(R.drawable.baseline_question_mark_24), + contentDescription = "Ask" + ) + }, + title = { + Text(text = context.getString(R.string.input_name)) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = context.getString( + if (isDirectory) { + R.string.create_directory + } else { + R.string.create_file + }, dir + ), + modifier = Modifier.padding(vertical = 5.dp) + ) + + TextField( + value = input, + onValueChange = { input = it }, + maxLines = 1 + ) + } + + }, + onDismissRequest = { + onDismissRequest() + }, + confirmButton = { + TextButton( + onClick = { + onConfirmation(input) + } + ) { + Text(context.getString(R.string.confirm)) + } + }, + dismissButton = { + TextButton( + onClick = { + onDismissRequest() + } + ) { + Text(context.getString(R.string.cancel)) + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/document_page.kt b/app/src/main/java/com/example/myapplication/document_page.kt new file mode 100644 index 0000000..c9ede47 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/document_page.kt @@ -0,0 +1,284 @@ +package com.example.myapplication + +import android.content.ActivityNotFoundException +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.os.Environment +import android.util.Log +import android.view.View +import android.widget.AdapterView +import android.widget.GridView +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AlertDialog.Builder +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsCompat.Type +import com.example.myapplication.adapters.DocumentAdapter +import com.example.myapplication.adapters.DocumentModel +import com.example.myapplication.fileSystem.CutHelper +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister.Companion.instance +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister.Companion.regex +import com.example.myapplication.utils.AlertHelper +import com.example.myapplication.utils.ClipHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.apache.commons.io.IOUtils +import java.io.File + +class document_page : AppCompatActivity() { + private var documentList = listOf() + private val pasteDir = "${Environment.getExternalStorageDirectory().path}/Documents/pasted" + private var listOrderType = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + this.enableEdgeToEdge() + setContentView(R.layout.document_page) + ViewCompat.setOnApplyWindowInsetsListener( + findViewById(R.id.main) + ) { v: View, insets: WindowInsetsCompat -> + val systemBars = + insets.getInsets(Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + // 设置排序按钮的点击事件 + val sortImageView = findViewById(R.id.sortDocumentView) + sortImageView.setOnClickListener { v: View? -> showSortOptions() } + + // 设置左箭头的点击事件,返回上一级页面 + val leftArrowImageView = findViewById(R.id.leftArrowImageView) + leftArrowImageView.setOnClickListener { v: View? -> + val intent = + Intent( + this@document_page, + main_page::class.java + ) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + startActivity(intent) + finish() + } + + val searchImageView = findViewById(R.id.searchDocumentView) + searchImageView.setOnClickListener { v: View? -> + val intent = + Intent( + this@document_page, + document_page_search::class.java + ) + startActivity(intent) // 跳转到搜索页面 + } + + findViewById(R.id.refreshData).setOnClickListener { _ -> + update() + } + + val documentGrid: GridView = findViewById(R.id.DocumentGrid) + + documentGrid.onItemClickListener = + AdapterView.OnItemClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long -> + val file = File(documentList[position]) + if (file.isFile) { + val uri = FileProvider.getUriForFile( + this, + applicationContext.packageName + ".provider", + file + ) + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(uri, when(file.extension){ + "xls" -> "application/vnd.ms-excel" + "xlsx" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + "doc" -> "application/msword" + "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + "ppt" -> "application/vnd.ms-powerpoint" + "pptx" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation" + "txt" -> "text/plain" + "htm","html" -> "text/html" + "pdf" -> "application/pdf" + + else -> "application/*" + }) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + try { startActivity(intent) } + catch (e: ActivityNotFoundException){ + Toast.makeText(baseContext,"没有安装可以打开此类型文件的应用",Toast.LENGTH_SHORT).show() + } + } + } + documentGrid.onItemLongClickListener = + AdapterView.OnItemLongClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long -> + showDocumentOperation( + position + ) + true + } + + // 最后在后台刷新 + CoroutineScope(Dispatchers.Default).launch { + val loadingTextView = findViewById(R.id.LoadingBlankText) + val defaultText = loadingTextView.text + launch { loadingText(loadingTextView,defaultText) } + documentList = instance.dateOrderedList() + val models = ArrayList() + for (path in documentList) { + models.add(DocumentModel(File(path))) + } + runOnUiThread { + val adapter = DocumentAdapter(this@document_page, models) + val grid = findViewById(R.id.DocumentGrid) + grid.setAdapter(adapter) + findViewById(R.id.LoadingBlankText).visibility = View.GONE + } + } + } + + private fun showDocumentOperation(position: Int) { + AlertHelper.showItemAlert(this, + onCopy = { + val file = File(documentList[position]) + if (file.isFile) { + ClipHelper.getInstance(this).copy(file, this) + } + }, + onPaste = { + val uri = ClipHelper.getInstance(this).paste() + if (uri != null) { + val dir = File(pasteDir) + if (!dir.exists()) { + dir.mkdir() + } + val name = uri.path?.split('/')?.last() ?: "somePastedItem" + val ext = name.split('.').last() + if (!"$ext.".matches(regex)){ + Toast.makeText(this, getString(R.string.error_nothing_to_paste), Toast.LENGTH_SHORT) + .show() + return@showItemAlert + } + val actualFile = File(dir, "${name}_paste.$ext") + val inputStream = contentResolver.openInputStream(uri) + if (inputStream != null) { + actualFile.writeBytes(IOUtils.toByteArray(inputStream)) + inputStream.close() + // 刷新 + update() + } + } else { + Toast.makeText(this, getString(R.string.error_nothing_to_paste), Toast.LENGTH_SHORT) + .show() + } + }, + onDelete = { + AlertHelper.showDeleteAlert(this, documentList[position]) { + update() + } + }, + onCut = { + CutHelper.cut(this, File(documentList[position])) + update() + }, + onInfo = { + AlertHelper.showFileInfoAlert(this, documentList[position]) + } + ) + } + + private fun showSortOptions() { + val loadingTextView = findViewById(R.id.LoadingBlankText) + // 创建对话框构建者 + val builder = Builder(this@document_page) + builder.setTitle("选择排序方式") + .setItems( + arrayOf("按时间排序(默认)", "按大小排序") + ) { dialog: DialogInterface?, which: Int -> + when (which) { + 0 -> { + listOrderType = 0 + runOnUiThread{ + loadingTextView.visibility = View.VISIBLE + } + CoroutineScope(Dispatchers.IO).launch { loadingText(loadingTextView,loadingTextView.text) } + update { + runOnUiThread{ + loadingTextView.visibility = View.GONE + Toast.makeText( + this@document_page, + "已选择按时间排序", + Toast.LENGTH_SHORT + ).show() + } + } + } + + 1 -> { + listOrderType = 1 + runOnUiThread{ + loadingTextView.visibility = View.VISIBLE + } + CoroutineScope(Dispatchers.IO).launch { loadingText(loadingTextView,loadingTextView.text) } + update { + runOnUiThread{ + loadingTextView.visibility = View.GONE + Toast.makeText( + this@document_page, + "已选择按大小排序", + Toast.LENGTH_SHORT + ).show() + } + } + } + } + } + .setNegativeButton( + "取消" + ) { dialog: DialogInterface, which: Int -> dialog.dismiss() } + .show() + } + + private fun update(runSomethingMore: (()->Unit)? = null) { + instance.initialize { + documentList = when (listOrderType) { + 0 -> instance.dateOrderedList() + 1 -> instance.sizeOrderedList() + else -> listOf() + } + val models = ArrayList() + for (path in documentList) { + models.add(DocumentModel(File(path))) + } + val adapter = DocumentAdapter(this, models) + runOnUiThread { + val grid = findViewById(R.id.DocumentGrid) + grid.setAdapter(adapter) + } + runSomethingMore?.invoke() + } + } + + private fun loadingText( + loadingTextView: TextView, + defaultText: CharSequence, + dots: String = "" + ) { + Thread.sleep(500) + val next = if (dots.length > 3) { + "" + } else { + "$dots." + } + runOnUiThread { + loadingTextView.text = "$defaultText$next" + } + + if (loadingTextView.visibility != View.GONE) { + loadingText(loadingTextView, defaultText, next) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/document_page_search.java b/app/src/main/java/com/example/myapplication/document_page_search.java new file mode 100644 index 0000000..35765db --- /dev/null +++ b/app/src/main/java/com/example/myapplication/document_page_search.java @@ -0,0 +1,44 @@ +package com.example.myapplication; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.ImageView; +import android.widget.SearchView; +import android.widget.Toast; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; + +public class document_page_search extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.document_page_search); + // 设置左箭头的点击事件,返回上一级页面 + ImageView leftArrowImageView = findViewById(R.id.leftArrowImageView); + leftArrowImageView.setOnClickListener(v -> { + Intent intent = new Intent(document_page_search.this, document_page.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + }); + SearchView searchView = findViewById(R.id.searchDocument); // 确保使用正确的 ID + + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + // 处理搜索提交 + Toast.makeText(document_page_search.this, "搜索: " + query, Toast.LENGTH_SHORT).show(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + // 处理搜索文本变化 + // 可以在这里添加过滤逻辑 + return false; + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/fileSystem/DeleteHelper.kt b/app/src/main/java/com/example/myapplication/fileSystem/DeleteHelper.kt new file mode 100644 index 0000000..78d2fb1 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/fileSystem/DeleteHelper.kt @@ -0,0 +1,53 @@ +package com.example.myapplication.fileSystem + +import android.content.Context +import android.os.Environment +import com.example.myapplication.compose.PasteHelper +import com.example.myapplication.utils.ClipHelper +import java.io.File + +class DeleteHelper { + companion object{ + fun delete(path: String){ + val file = File(path) + if (file.isFile){ + file.delete() + }else if (file.isDirectory){ + file.deleteRecursively() + } + } + } +} + +class CutHelper{ + companion object { + fun cut(context: Context, file: File){ + val cacheDir = File("${Environment.getExternalStorageDirectory()}/.copy") + if (!cacheDir.exists()){ + cacheDir.mkdir() + } + + if (file.exists()) { + val tempFile = File("${Environment.getExternalStorageDirectory()}/.copy", file.name) + val bytes = file.readBytes() + tempFile.writeBytes(bytes) + ClipHelper.getInstance(context).copy(tempFile, context) + DeleteHelper.delete(file.path) + } + } + + fun cutFolder(context: Context,folder: File){ + val cacheDir = File("${Environment.getExternalStorageDirectory()}/.copy") + if (!cacheDir.exists()){ + cacheDir.mkdir() + } + + if (folder.exists()) { + val tempFolder = File("${Environment.getExternalStorageDirectory()}/.copy", folder.name) + PasteHelper.copyDirectory(folder,tempFolder) + ClipHelper.getInstance(context).copyFolder(tempFolder.path) + DeleteHelper.delete(folder.path) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/fileSystem/SystemStorageInfo.kt b/app/src/main/java/com/example/myapplication/fileSystem/SystemStorageInfo.kt new file mode 100644 index 0000000..0bb369f --- /dev/null +++ b/app/src/main/java/com/example/myapplication/fileSystem/SystemStorageInfo.kt @@ -0,0 +1,39 @@ +package com.example.myapplication.fileSystem + +import android.app.usage.StorageStatsManager +import android.content.Context +import android.os.Environment +import android.os.storage.StorageManager +import java.io.IOException + +class SystemStorageInfo(private val context: Context) { + private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager + private val storageStatsManager = + context.getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager + + private var totalStorage = 1uL + private var freeStorage = 0uL + + fun getTotalStorageSize(): ULong { + try { + val uuid = storageManager.getUuidForPath(Environment.getDataDirectory()) + totalStorage = storageStatsManager.getTotalBytes(uuid).toULong() + return totalStorage + } catch (e: IOException) { + return 1uL + } + } + + fun getFreeStorageSize(): ULong { + try { + val uuid = storageManager.getUuidForPath(Environment.getDataDirectory()) + freeStorage = storageStatsManager.getFreeBytes(uuid).toULong() + return freeStorage + } catch (e: IOException) { + return 0uL + } + } + + fun getUsedPercentage(): Int = 100 - Math.round(freeStorage.toDouble() * 100 / totalStorage.toDouble()).toInt() +} + diff --git a/app/src/main/java/com/example/myapplication/fileSystem/WrappedFile.kt b/app/src/main/java/com/example/myapplication/fileSystem/WrappedFile.kt new file mode 100644 index 0000000..7b06124 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/fileSystem/WrappedFile.kt @@ -0,0 +1,155 @@ +package com.example.myapplication.fileSystem + +import android.content.Context +import android.icu.text.DecimalFormat +import android.text.format.DateFormat +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister +import com.example.myapplication.fileSystem.byTypeFileLister.ImageLister +import com.example.myapplication.fileSystem.byTypeFileLister.MusicLister +import com.example.myapplication.fileSystem.byTypeFileLister.VideoLister +import java.io.File +import java.net.URLConnection +import java.nio.file.Files +import java.nio.file.attribute.BasicFileAttributes +import java.sql.Date +import java.sql.Timestamp +import java.time.Instant + +class WrappedFile(private val f: File, skipCalculateDirectorySize: Boolean = false) { + companion object { + fun getSizeString(size: ULong): String { + var sizeFirst = size + var sizeLast = 0 + var unit = SizeUnit.B + + while (sizeFirst > 1024u && unit != SizeUnit.GB) { + sizeLast = size.mod(1024u).toInt() + sizeFirst /= 1024u + unit = when (unit) { + SizeUnit.B -> SizeUnit.KB + SizeUnit.KB -> SizeUnit.MB + SizeUnit.MB -> SizeUnit.GB + else -> SizeUnit.B + } + } + val s: Double = sizeFirst.toDouble() + sizeLast / 1000.0 + return "${DecimalFormat("#.##").format(s)} ${getUnit(unit)}" + } + + private fun getUnit(unit: SizeUnit): String = when (unit) { + SizeUnit.B -> "B" + SizeUnit.KB -> "KB" + SizeUnit.MB -> "MB" + SizeUnit.GB -> "GB" + } + + fun guessMime(ext: String): String { + // Known ext + val dotExt = ".$ext" + if (dotExt.matches(ImageLister.regex)) { + return "image/*" + } else if (dotExt.matches(VideoLister.regex)) { + return "video/*" + } else if (dotExt.matches(MusicLister.regex)) { + return "audio/*" + } else if (dotExt.matches(DocumentLister.regex)) { + return when (ext) { + "xls" -> "application/vnd.ms-excel" + "xlsx" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + "doc" -> "application/msword" + "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + "ppt" -> "application/vnd.ms-powerpoint" + "pptx" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation" + "txt" -> "text/plain" + "htm", "html" -> "text/html" + "pdf" -> "application/pdf" + else -> "application/*" + } + } else return URLConnection.guessContentTypeFromName("f.$ext") ?: "text/plain" + } + } + + enum class Type { + FILE, + DIRECTORY + } + + private enum class SizeUnit { + B, + KB, + MB, + GB + } + + val name: String + val nameWithoutExt: String + val path: String + val type: Type + val lastModifiedTime: Instant + val mime: String + + private var isSizeCalculated = true + var size: Long + private set + + init { + if (!f.exists()) { + throw RuntimeException("Cannot find file") + } + name = f.name + nameWithoutExt = f.nameWithoutExtension + path = f.path + type = when (f.isDirectory) { + true -> Type.DIRECTORY + false -> Type.FILE + } + + size = if (type == Type.FILE) { + f.length() + } else { + if (skipCalculateDirectorySize) { + isSizeCalculated = false + 0 + } else { + getFolderSize(f) + } + } + + val attr: BasicFileAttributes = Files.readAttributes( + f.toPath(), + BasicFileAttributes::class.java + ) + lastModifiedTime = attr.lastModifiedTime().toInstant() + + mime = if (f.isDirectory){ + "dir" + }else { + guessMime(f.extension) + } + } + + fun getSizeString(): String { + if (size == 0L) { + if (type == Type.DIRECTORY) { + if (!isSizeCalculated){ + // Calculate Size + size = getFolderSize(f) + isSizeCalculated = true + }else{ + return "0B" + } + } else { + return "未知" + } + } + return getSizeString(size.toULong()) + } + + fun getModifiedTimeString(context: Context): String { + val ts = Timestamp.from(lastModifiedTime) + val df = DateFormat.getDateFormat(context) + val tf = DateFormat.getTimeFormat(context) + val date = Date(ts.time) + return df.format(date) + " " + tf.format(date) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/DocumentAdapter.kt b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/DocumentAdapter.kt new file mode 100644 index 0000000..c7fbe4b --- /dev/null +++ b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/DocumentAdapter.kt @@ -0,0 +1,59 @@ +package com.example.myapplication.fileSystem.byTypeFileLister + +import android.os.Environment +import com.example.myapplication.fileSystem.WrappedFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File + +class DocumentLister : Lister() { + companion object { + val instance by lazy { DocumentLister() } + val directories = listOf("Documents", "Download") + val regex = "\\.((xls|doc|ppt)(x|)|txt|htm(l|)|pdf)".toRegex() + } + + val documentList = mutableListOf() + + fun initialize(onFinished: (() -> Unit)? = null) { + documentList.clear() + CoroutineScope(Dispatchers.IO).launch { + directories.forEach { dir -> + walkDir( + File("${Environment.getExternalStorageDirectory().path}/${dir}"), + documentList, + regex + ) + } + onFinished?.invoke() + } + return + } + + fun dateOrderedList(): List { + val wrappedFileList = mutableListOf() + documentList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.sortBy { it.lastModifiedTime } + val result = mutableListOf() + wrappedFileList.forEach { result.add(it.path) } + return result + } + + fun sizeOrderedList(): List { + val wrappedFileList = mutableListOf() + documentList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.sortBy { it.size } + val result = mutableListOf() + wrappedFileList.forEach { result.add(it.path) } + return result + } + + fun getFullSize(): ULong { + var size = 0UL + val wrappedFileList = mutableListOf() + documentList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.forEach { size += it.size.toUInt() } + return size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/ImageLister.kt b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/ImageLister.kt new file mode 100644 index 0000000..cd21a02 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/ImageLister.kt @@ -0,0 +1,59 @@ +package com.example.myapplication.fileSystem.byTypeFileLister + +import android.os.Environment +import com.example.myapplication.fileSystem.WrappedFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File + +class ImageLister private constructor() : Lister() { + companion object { + val instance by lazy { ImageLister() } + val directories = listOf("DCIM", "Pictures", "Download") + val regex = "\\.(jpg|png|jpeg|webp)".toRegex() + } + + val imageList = mutableListOf() + + fun initialize(onFinished: (() -> Unit)? = null) { + imageList.clear() + CoroutineScope(Dispatchers.IO).launch { + directories.forEach { imageDirectory -> + walkDir( + File("${Environment.getExternalStorageDirectory().path}/${imageDirectory}"), + imageList, + regex + ) + } + onFinished?.invoke() + } + return + } + + fun dateOrderedList(): List { + val wrappedFileList = mutableListOf() + imageList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.sortBy { it.lastModifiedTime } + val result = mutableListOf() + wrappedFileList.forEach { result.add(it.path) } + return result + } + + fun sizeOrderedList(): List { + val wrappedFileList = mutableListOf() + imageList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.sortBy { it.size } + val result = mutableListOf() + wrappedFileList.forEach { result.add(it.path) } + return result + } + + fun getFullSize(): ULong { + var size = 0UL + val wrappedFileList = mutableListOf() + imageList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.forEach { size += it.size.toUInt() } + return size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/Lister.kt b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/Lister.kt new file mode 100644 index 0000000..b5aac21 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/Lister.kt @@ -0,0 +1,4 @@ +package com.example.myapplication.fileSystem.byTypeFileLister + +abstract class Lister { +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/MusicLister.kt b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/MusicLister.kt new file mode 100644 index 0000000..f3d31c8 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/MusicLister.kt @@ -0,0 +1,59 @@ +package com.example.myapplication.fileSystem.byTypeFileLister + +import android.os.Environment +import com.example.myapplication.fileSystem.WrappedFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File + +class MusicLister : Lister() { + companion object { + val instance by lazy { MusicLister() } + val directories = listOf("Recordings", "Download", "Audiobooks", "Music", "Podcasts", "Ringtones") + val regex = "\\.(mp3|ogg|aac|wav)".toRegex() + } + + val musicList = mutableListOf() + + fun initialize(onFinished: (() -> Unit)? = null) { + musicList.clear() + CoroutineScope(Dispatchers.IO).launch { + directories.forEach { dir -> + walkDir( + File("${Environment.getExternalStorageDirectory().path}/${dir}"), + musicList, + regex + ) + } + onFinished?.invoke() + } + return + } + + fun dateOrderedList(): List { + val wrappedFileList = mutableListOf() + musicList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.sortBy { it.lastModifiedTime } + val result = mutableListOf() + wrappedFileList.forEach { result.add(it.path) } + return result + } + + fun sizeOrderedList(): List { + val wrappedFileList = mutableListOf() + musicList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.sortBy { it.size } + val result = mutableListOf() + wrappedFileList.forEach { result.add(it.path) } + return result + } + + fun getFullSize(): ULong { + var size = 0UL + val wrappedFileList = mutableListOf() + musicList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.forEach { size += it.size.toUInt() } + return size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/VideoLister.kt b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/VideoLister.kt new file mode 100644 index 0000000..5e97380 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/VideoLister.kt @@ -0,0 +1,59 @@ +package com.example.myapplication.fileSystem.byTypeFileLister + +import android.os.Environment +import com.example.myapplication.fileSystem.WrappedFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File + +class VideoLister : Lister() { + companion object { + val instance by lazy { VideoLister() } + val directories = listOf("DCIM", "Download", "Movies") + val regex = "\\.(mp4|avi|video|webm)".toRegex() + } + + val videoList = mutableListOf() + + fun initialize(onFinished: (() -> Unit)? = null) { + videoList.clear() + CoroutineScope(Dispatchers.IO).launch { + directories.forEach { dir -> + walkDir( + File("${Environment.getExternalStorageDirectory().path}/${dir}"), + videoList, + regex + ) + } + onFinished?.invoke() + } + return + } + + fun dateOrderedList(): List { + val wrappedFileList = mutableListOf() + videoList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.sortBy { it.lastModifiedTime } + val result = mutableListOf() + wrappedFileList.forEach { result.add(it.path) } + return result + } + + fun sizeOrderedList(): List { + val wrappedFileList = mutableListOf() + videoList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.sortBy { it.size } + val result = mutableListOf() + wrappedFileList.forEach { result.add(it.path) } + return result + } + + fun getFullSize(): ULong { + var size = 0UL + val wrappedFileList = mutableListOf() + videoList.forEach { wrappedFileList.add(WrappedFile(File(it))) } + wrappedFileList.forEach { size += it.size.toUInt() } + return size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/Walker.kt b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/Walker.kt new file mode 100644 index 0000000..c78528a --- /dev/null +++ b/app/src/main/java/com/example/myapplication/fileSystem/byTypeFileLister/Walker.kt @@ -0,0 +1,20 @@ +package com.example.myapplication.fileSystem.byTypeFileLister + +import java.io.File + +fun Lister.walkDir(directory: File,list: MutableList,pattern: Regex ,ignoreDotFile: Boolean = true){ + if (!directory.exists() || !directory.isDirectory){ + return + } + directory.listFiles()?.forEach { + if (ignoreDotFile && !it.name.startsWith(".")) { + if (it.isDirectory) { + walkDir(it, list, pattern) + } else if (it.isFile) { + if (it.name.contains(pattern)) { + list.add(it.path) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/fileSystem/getFolderSize.kt b/app/src/main/java/com/example/myapplication/fileSystem/getFolderSize.kt new file mode 100644 index 0000000..b007767 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/fileSystem/getFolderSize.kt @@ -0,0 +1,33 @@ +package com.example.myapplication.fileSystem + +import java.io.File + +fun getFolderSize(folder: File, level: Int = 0): Long { + if (!folder.exists() || !folder.isDirectory) { + return 0 + } + + if (level >= 5) { // 限制递归层数防止过度递归 + return 1.shl(63) + } + + var size = 0L + + val file = folder.listFiles() + folder.listFiles()?.forEach { content -> + size += if (content.isFile) { + content.length() + } else if (content.isDirectory) { + getFolderSize(content, level + 1).let { + if (size > 0 || it > 0) { + it + } else { + -it + } + } + } else { + 0 + } + } + return size +} diff --git a/app/src/main/java/com/example/myapplication/main_page.java b/app/src/main/java/com/example/myapplication/main_page.java new file mode 100644 index 0000000..9d0486f --- /dev/null +++ b/app/src/main/java/com/example/myapplication/main_page.java @@ -0,0 +1,129 @@ +package com.example.myapplication; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Environment; +import android.view.View; +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import com.example.myapplication.compose.ViewFileActivity; +import com.example.myapplication.fileSystem.DeleteHelper; +import java.io.File; + +public class main_page extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.main_page); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + + ButtonClickerHandler buttonClickerHandler = new ButtonClickerHandler(this); + + findViewById(R.id.MainPagePictureButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPageVideoButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPageMusicButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPageDocumentButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPageStorageButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPageDownloadButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPageAllFilesButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPageDownloadButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPageDocumentsButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPageRecordingButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPageDCIMButton).setOnClickListener(buttonClickerHandler); + findViewById(R.id.MainPagePicturesButton).setOnClickListener(buttonClickerHandler); + } + + @Override protected void onDestroy() { + // 把剪切的缓存文件删掉 + File cacheDir = new File(Environment.getExternalStorageDirectory() + "/.copy"); + if (cacheDir.exists()) { + DeleteHelper.Companion.delete(cacheDir.getPath()); + } + super.onDestroy(); + } + + public class ButtonClickerHandler implements View.OnClickListener { + private final Context context; + + ButtonClickerHandler(Context context) { + this.context = context; + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.MainPagePictureButton) { + Intent intent = new Intent(context, picture_page.class); + startActivity(intent); + } + if (view.getId() == R.id.MainPageVideoButton) { + Intent intent = new Intent(context, video_page.class); + startActivity(intent); + } + if (view.getId() == R.id.MainPageMusicButton) { + Intent intent = new Intent(context, music_page.class); + startActivity(intent); + } + if (view.getId() == R.id.MainPageDocumentButton) { + Intent intent = new Intent(context, document_page.class); + startActivity(intent); + } + if (view.getId() == R.id.MainPageStorageButton) { + Intent intent = new Intent(context, store_page.class); + startActivity(intent); + } + if (view.getId() == R.id.MainPageAllFilesButton) { + Intent intent = new Intent(context, ViewFileActivity.class); + startActivity(intent); + } + if (view.getId() == R.id.MainPageDCIMButton) { + Intent intent = new Intent(context, ViewFileActivity.class); + Bundle bundle = new Bundle(); + bundle.putString("folder", Environment.getExternalStorageDirectory().getPath() + "/DCIM"); + intent.putExtras(bundle); + startActivity(intent); + } + if (view.getId() == R.id.MainPageDownloadButton) { + Intent intent = new Intent(context, ViewFileActivity.class); + Bundle bundle = new Bundle(); + bundle.putString("folder", + Environment.getExternalStorageDirectory().getPath() + "/Download"); + intent.putExtras(bundle); + startActivity(intent); + } + if (view.getId() == R.id.MainPageDocumentsButton) { + Intent intent = new Intent(context, ViewFileActivity.class); + Bundle bundle = new Bundle(); + bundle.putString("folder", + Environment.getExternalStorageDirectory().getPath() + "/Documents"); + intent.putExtras(bundle); + startActivity(intent); + } + if (view.getId() == R.id.MainPageRecordingButton) { + Intent intent = new Intent(context, ViewFileActivity.class); + Bundle bundle = new Bundle(); + bundle.putString("folder", + Environment.getExternalStorageDirectory().getPath() + "/Recordings"); + intent.putExtras(bundle); + startActivity(intent); + } + if (view.getId() == R.id.MainPagePicturesButton) { + Intent intent = new Intent(context, ViewFileActivity.class); + Bundle bundle = new Bundle(); + bundle.putString("folder", + Environment.getExternalStorageDirectory().getPath() + "/Pictures"); + intent.putExtras(bundle); + startActivity(intent); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/music_page.kt b/app/src/main/java/com/example/myapplication/music_page.kt new file mode 100644 index 0000000..67a29a9 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/music_page.kt @@ -0,0 +1,269 @@ +package com.example.myapplication + +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.os.Environment +import android.util.Log +import android.view.View +import android.widget.AdapterView +import android.widget.GridView +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AlertDialog.Builder +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsCompat.Type +import com.example.myapplication.adapters.MusicAdapter +import com.example.myapplication.adapters.MusicModel +import com.example.myapplication.fileSystem.CutHelper +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister.Companion +import com.example.myapplication.fileSystem.byTypeFileLister.MusicLister.Companion.instance +import com.example.myapplication.fileSystem.byTypeFileLister.MusicLister.Companion.regex +import com.example.myapplication.utils.AlertHelper +import com.example.myapplication.utils.ClipHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.apache.commons.io.IOUtils +import java.io.File + +class music_page : AppCompatActivity() { + private var musicList = listOf() + private val pasteDir = "${Environment.getExternalStorageDirectory().path}/Music/pasted" + private var listOrderType = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + this.enableEdgeToEdge() + setContentView(R.layout.music_page) + ViewCompat.setOnApplyWindowInsetsListener( + findViewById(R.id.main) + ) { v: View, insets: WindowInsetsCompat -> + val systemBars = + insets.getInsets(Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + // 设置排序按钮的点击事件 + val sortImageView = findViewById(R.id.sortMusicView) + sortImageView.setOnClickListener { v: View? -> showSortOptions() } + + // 设置左箭头的点击事件,返回上一级页面 + val leftArrowImageView = findViewById(R.id.leftArrowImageView) + leftArrowImageView.setOnClickListener { v: View? -> + val intent = + Intent( + this@music_page, + main_page::class.java + ) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + startActivity(intent) + } + + val searchImageView = findViewById(R.id.searchMusicView) + searchImageView.setOnClickListener { v: View? -> + val intent = + Intent( + this@music_page, + music_page_search::class.java + ) + startActivity(intent) // 跳转到搜索页面 + } + + findViewById(R.id.refreshData).setOnClickListener { _ -> + update() + } + + val musicGrid: GridView = findViewById(R.id.MusicGrid) + + musicGrid.onItemClickListener = + AdapterView.OnItemClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long -> + val file = File(musicList[position]) + if (file.isFile) { + val uri = FileProvider.getUriForFile( + this, + applicationContext.packageName + ".provider", + file + ) + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(uri, "audio/*") + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + startActivity(intent) + } + } + musicGrid.onItemLongClickListener = + AdapterView.OnItemLongClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long -> + showMusicOperation( + position + ) + true + } + + // 最后在后台刷新 + CoroutineScope(Dispatchers.Default).launch { + val loadingTextView = findViewById(R.id.LoadingBlankText) + val defaultText = loadingTextView.text + launch { loadingText(loadingTextView,defaultText) } + musicList = instance.dateOrderedList() + val models = ArrayList() + for (path in musicList) { + models.add(MusicModel(File(path))) + } + runOnUiThread { + val adapter = MusicAdapter(this@music_page, models) + val grid = findViewById(R.id.MusicGrid) + grid.setAdapter(adapter) + findViewById(R.id.LoadingBlankText).visibility = View.GONE + } + } + } + + private fun showMusicOperation(position: Int) { + AlertHelper.showItemAlert(this, + onCopy = { + val file = File(musicList[position]) + if (file.isFile) { + ClipHelper.getInstance(this).copy(file, this) + } + }, + onPaste = { + val uri = ClipHelper.getInstance(this).paste() + if (uri != null) { + val dir = File(pasteDir) + if (!dir.exists()) { + dir.mkdir() + } + val name = uri.path?.split('/')?.last() ?: "somePastedItem" + val ext = name.split('.').last() + if (!"$ext.".matches(DocumentLister.regex)){ + Toast.makeText(this, getString(R.string.error_nothing_to_paste), Toast.LENGTH_SHORT) + .show() + return@showItemAlert + } + val actualFile = File(dir, "${name}_paste.$ext") + val inputStream = contentResolver.openInputStream(uri) + if (inputStream != null) { + actualFile.writeBytes(IOUtils.toByteArray(inputStream)) + inputStream.close() + // 刷新 + update() + } + } else { + Toast.makeText(this, getString(R.string.error_nothing_to_paste), Toast.LENGTH_SHORT) + .show() + } + }, + onDelete = { + AlertHelper.showDeleteAlert(this, musicList[position]) { + update() + } + }, + onCut = { + CutHelper.cut(this, File(musicList[position])) + update() + }, + onInfo = { + AlertHelper.showFileInfoAlert(this, musicList[position]) + } + ) + } + + private fun showSortOptions() { + val loadingTextView = findViewById(R.id.LoadingBlankText) + // 创建对话框构建者 + val builder = Builder(this@music_page) + builder.setTitle("选择排序方式") + .setItems( + arrayOf("按时间排序(默认)", "按大小排序") + ) { dialog: DialogInterface?, which: Int -> + when (which) { + 0 -> { + listOrderType = 0 + runOnUiThread{ + loadingTextView.visibility = View.VISIBLE + } + CoroutineScope(Dispatchers.IO).launch { loadingText(loadingTextView,loadingTextView.text) } + update { + runOnUiThread{ + loadingTextView.visibility = View.GONE + Toast.makeText( + this@music_page, + "已选择按时间排序", + Toast.LENGTH_SHORT + ).show() + } + } + } + + 1 -> { + listOrderType = 1 + runOnUiThread{ + loadingTextView.visibility = View.VISIBLE + } + CoroutineScope(Dispatchers.IO).launch { loadingText(loadingTextView,loadingTextView.text) } + update { + runOnUiThread{ + loadingTextView.visibility = View.GONE + Toast.makeText( + this@music_page, + "已选择按大小排序", + Toast.LENGTH_SHORT + ).show() + } + } + } + } + } + .setNegativeButton( + "取消" + ) { dialog: DialogInterface, which: Int -> dialog.dismiss() } + .show() + } + + private fun update(runSomethingMore: (()->Unit)? = null) { + instance.initialize { + musicList = when (listOrderType) { + 0 -> instance.dateOrderedList() + 1 -> instance.sizeOrderedList() + else -> listOf() + } + val models = ArrayList() + for (path in musicList) { + models.add(MusicModel(File(path))) + } + val adapter = MusicAdapter(this, models) + runOnUiThread { + val grid = findViewById(R.id.MusicGrid) + grid.setAdapter(adapter) + } + runSomethingMore?.invoke() + } + } + + private fun loadingText( + loadingTextView: TextView, + defaultText: CharSequence, + dots: String = "" + ) { + Thread.sleep(500) + val next = if (dots.length > 3) { + "" + } else { + "$dots." + } + runOnUiThread { + loadingTextView.text = "$defaultText$next" + } + + if (loadingTextView.visibility != View.GONE) { + loadingText(loadingTextView, defaultText, next) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/music_page_search.java b/app/src/main/java/com/example/myapplication/music_page_search.java new file mode 100644 index 0000000..ebc1027 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/music_page_search.java @@ -0,0 +1,43 @@ +package com.example.myapplication; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.ImageView; +import android.widget.SearchView; +import android.widget.Toast; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; + +public class music_page_search extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.music_page_search); + // 设置左箭头的点击事件,返回上一级页面 + ImageView leftArrowImageView = findViewById(R.id.leftArrowImageView); + leftArrowImageView.setOnClickListener(v -> { + Intent intent = new Intent(music_page_search.this, music_page.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + }); + SearchView searchView = findViewById(R.id.searchMusic); // 确保使用正确的 ID + + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + // 处理搜索提交 + Toast.makeText(music_page_search.this, "搜索: " + query, Toast.LENGTH_SHORT).show(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + // 处理搜索文本变化 + // 可以在这里添加过滤逻辑 + return false; + } + }); + } +} diff --git a/app/src/main/java/com/example/myapplication/picture_page.kt b/app/src/main/java/com/example/myapplication/picture_page.kt new file mode 100644 index 0000000..c010fad --- /dev/null +++ b/app/src/main/java/com/example/myapplication/picture_page.kt @@ -0,0 +1,270 @@ +package com.example.myapplication + +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.os.Environment +import android.view.View +import android.widget.AdapterView +import android.widget.GridView +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AlertDialog.Builder +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsCompat.Type +import com.example.myapplication.adapters.ImageAdapter +import com.example.myapplication.adapters.ImageModel +import com.example.myapplication.fileSystem.CutHelper +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister.Companion +import com.example.myapplication.fileSystem.byTypeFileLister.ImageLister.Companion.instance +import com.example.myapplication.fileSystem.byTypeFileLister.ImageLister.Companion.regex +import com.example.myapplication.utils.AlertHelper +import com.example.myapplication.utils.ClipHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.apache.commons.io.IOUtils +import java.io.File + +class picture_page : AppCompatActivity() { + private var imageList = listOf() + private val pasteDir = "${Environment.getExternalStorageDirectory().path}/Pictures/pasted" + private var imageListOrderType = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + this.enableEdgeToEdge() + setContentView(R.layout.picture_page) + ViewCompat.setOnApplyWindowInsetsListener( + findViewById(R.id.main) + ) { v: View, insets: WindowInsetsCompat -> + val systemBars = + insets.getInsets(Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + // 设置排序按钮的点击事件 + val sortImageView = findViewById(R.id.sortImageView) + sortImageView.setOnClickListener { v: View? -> showSortOptions() } + + // 设置左箭头的点击事件,返回上一级页面 + val leftArrowImageView = findViewById(R.id.leftArrowImageView) + leftArrowImageView.setOnClickListener { v: View? -> + val intent = + Intent( + this@picture_page, + main_page::class.java + ) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + startActivity(intent) + finish() + } + + // 设置搜索按钮的点击事件,跳转到 picture_page_search 页面 + val searchImageView = findViewById(R.id.searchImageView) + searchImageView.setOnClickListener { v: View? -> + val intent = + Intent( + this@picture_page, + picture_page_search::class.java + ) + startActivity(intent) // 跳转到搜索页面 + } + + findViewById(R.id.refreshData).setOnClickListener { _ -> + update() + } + + val pictureGrid: GridView = findViewById(R.id.PicturePageGrid) + + pictureGrid.onItemClickListener = + AdapterView.OnItemClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long -> + val file = File(imageList[position]) + if (file.isFile) { + val uri = FileProvider.getUriForFile( + this, + applicationContext.packageName + ".provider", + file + ) + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(uri, "image/*") + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + startActivity(intent) + } + } + pictureGrid.onItemLongClickListener = + AdapterView.OnItemLongClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long -> + showImageOperation( + position + ) + true + } + + // 最后在后台刷新 + CoroutineScope(Dispatchers.Default).launch { + val loadingTextView = findViewById(R.id.LoadingBlankText) + val defaultText = loadingTextView.text + launch { loadingText(loadingTextView,defaultText) } + imageList = instance.dateOrderedList() + val imageModels = ArrayList() + for (path in imageList) { + imageModels.add(ImageModel(File(path))) + } + runOnUiThread { + val adapter = ImageAdapter(this@picture_page, imageModels) + val grid = findViewById(R.id.PicturePageGrid) + grid.setAdapter(adapter) + findViewById(R.id.LoadingBlankText).visibility = View.GONE + } + } + } + + private fun showImageOperation(position: Int) { + AlertHelper.showItemAlert(this, + onCopy = { + val file = File(imageList[position]) + if (file.isFile) { + ClipHelper.getInstance(this).copy(file, this) + } + }, + onPaste = { + val uri = ClipHelper.getInstance(this).paste() + if (uri != null) { + val dir = File(pasteDir) + if (!dir.exists()) { + dir.mkdir() + } + val name = uri.path?.split('/')?.last() ?: "somePastedItem" + val ext = name.split('.').last() + if (!"$ext.".matches(DocumentLister.regex)){ + Toast.makeText(this, getString(R.string.error_nothing_to_paste), Toast.LENGTH_SHORT) + .show() + return@showItemAlert + } + val actualFile = File(dir, "${name}_paste.$ext") + val inputStream = contentResolver.openInputStream(uri) + if (inputStream != null) { + actualFile.writeBytes(IOUtils.toByteArray(inputStream)) + inputStream.close() + // 刷新 + update() + } + } else { + Toast.makeText(this, getString(R.string.error_nothing_to_paste), Toast.LENGTH_SHORT) + .show() + } + }, + onDelete = { + AlertHelper.showDeleteAlert(this, imageList[position]) { + update() + } + }, + onCut = { + CutHelper.cut(this, File(imageList[position])) + update() + }, + onInfo = { + AlertHelper.showFileInfoAlert(this, imageList[position]) + } + ) + } + + private fun showSortOptions() { + val loadingTextView = findViewById(R.id.LoadingBlankText) + // 创建对话框构建者 + val builder = Builder(this@picture_page) + builder.setTitle("选择排序方式") + .setItems( + arrayOf("按时间排序(默认)", "按大小排序") + ) { dialog: DialogInterface?, which: Int -> + when (which) { + 0 -> { + imageListOrderType = 0 + runOnUiThread{ + loadingTextView.visibility = View.VISIBLE + } + CoroutineScope(Dispatchers.IO).launch { loadingText(loadingTextView,loadingTextView.text) } + update { + runOnUiThread{ + loadingTextView.visibility = View.GONE + Toast.makeText( + this@picture_page, + "已选择按时间排序", + Toast.LENGTH_SHORT + ).show() + } + } + } + + 1 -> { + imageListOrderType = 1 + runOnUiThread{ + loadingTextView.visibility = View.VISIBLE + } + CoroutineScope(Dispatchers.IO).launch { loadingText(loadingTextView,loadingTextView.text) } + update { + runOnUiThread{ + loadingTextView.visibility = View.GONE + Toast.makeText( + this@picture_page, + "已选择按大小排序", + Toast.LENGTH_SHORT + ).show() + } + } + } + } + } + .setNegativeButton( + "取消" + ) { dialog: DialogInterface, which: Int -> dialog.dismiss() } + .show() + } + + private fun update(runSomethingMore: (()->Unit)? = null) { + instance.initialize { + imageList = when (imageListOrderType) { + 0 -> instance.dateOrderedList() + 1 -> instance.sizeOrderedList() + else -> listOf() + } + val imageModels = ArrayList() + for (path in imageList) { + imageModels.add(ImageModel(File(path))) + } + val adapter = ImageAdapter(this, imageModels) + runOnUiThread { + val grid = findViewById(R.id.PicturePageGrid) + grid.setAdapter(adapter) + } + runSomethingMore?.invoke() + } + } + + private fun loadingText( + loadingTextView: TextView, + defaultText: CharSequence, + dots: String = "" + ) { + Thread.sleep(500) + val next = if (dots.length > 3) { + "" + } else { + "$dots." + } + runOnUiThread { + loadingTextView.text = "$defaultText$next" + } + + if (loadingTextView.visibility != View.GONE) { + loadingText(loadingTextView, defaultText, next) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/picture_page_search.java b/app/src/main/java/com/example/myapplication/picture_page_search.java new file mode 100644 index 0000000..932f6fd --- /dev/null +++ b/app/src/main/java/com/example/myapplication/picture_page_search.java @@ -0,0 +1,43 @@ +package com.example.myapplication; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.ImageView; +import android.widget.SearchView; +import android.widget.Toast; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; + +public class picture_page_search extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.picture_page_search); + // 设置左箭头的点击事件,返回上一级页面 + ImageView leftArrowImageView = findViewById(R.id.leftArrowImageView); + leftArrowImageView.setOnClickListener(v -> { + Intent intent = new Intent(picture_page_search.this, picture_page.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + }); + SearchView searchView = findViewById(R.id.searchPicture); // 确保使用正确的 ID + + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + // 处理搜索提交 + Toast.makeText(picture_page_search.this, "搜索: " + query, Toast.LENGTH_SHORT).show(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + // 处理搜索文本变化 + // 可以在这里添加过滤逻辑 + return false; + } + }); + } +} diff --git a/app/src/main/java/com/example/myapplication/store_page.kt b/app/src/main/java/com/example/myapplication/store_page.kt new file mode 100644 index 0000000..e43a718 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/store_page.kt @@ -0,0 +1,72 @@ +package com.example.myapplication + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import com.example.myapplication.fileSystem.SystemStorageInfo +import com.example.myapplication.fileSystem.WrappedFile +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister +import com.example.myapplication.fileSystem.byTypeFileLister.ImageLister +import com.example.myapplication.fileSystem.byTypeFileLister.MusicLister +import com.example.myapplication.fileSystem.byTypeFileLister.VideoLister +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class store_page : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + this.enableEdgeToEdge() + setContentView(R.layout.store_page) + val leftArrowImageView = findViewById(R.id.leftArrowImageView) + leftArrowImageView.setOnClickListener { v: View? -> + val intent = Intent( + this@store_page, + main_page::class.java + ) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + startActivity(intent) + finish() + } + + val systemStorageInfo = SystemStorageInfo(this) + val tv = findViewById(R.id.storageText) + tv.text = getString( + R.string.used_storage, + WrappedFile.getSizeString(systemStorageInfo.getTotalStorageSize() - systemStorageInfo.getFreeStorageSize()), + WrappedFile.getSizeString(systemStorageInfo.getTotalStorageSize()) + ) + (findViewById(R.id.progressText) as TextView).text = + getString( + R.string.used_storage_percentage, + systemStorageInfo.getUsedPercentage() + ) + (findViewById(R.id.progressBar) as ProgressBar).progress = + systemStorageInfo.getUsedPercentage() + + CoroutineScope(Dispatchers.Main).launch { + val imageSize = ImageLister.instance.getFullSize() + val imageSizeString = WrappedFile.getSizeString(imageSize) + val videoSize = VideoLister.instance.getFullSize() + val videoSizeString = WrappedFile.getSizeString(videoSize) + val musicSize = MusicLister.instance.getFullSize() + val musicSizeString = WrappedFile.getSizeString(musicSize) + val documentSize = DocumentLister.instance.getFullSize() + val documentSizeString = WrappedFile.getSizeString(documentSize) + val otherSize = systemStorageInfo.getTotalStorageSize() - systemStorageInfo.getFreeStorageSize() - imageSize - musicSize - documentSize + val otherSizeString = WrappedFile.getSizeString(otherSize) + runOnUiThread{ + findViewById(R.id.pictureStorage).text = imageSizeString + findViewById(R.id.videoStorage).text = videoSizeString + findViewById(R.id.audioStorage).text = musicSizeString + findViewById(R.id.documentStorage).text = documentSizeString + findViewById(R.id.appStorage).text = otherSizeString + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/utils/AlertHelper.kt b/app/src/main/java/com/example/myapplication/utils/AlertHelper.kt new file mode 100644 index 0000000..ee770d8 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/utils/AlertHelper.kt @@ -0,0 +1,167 @@ +package com.example.myapplication.utils + +import android.content.Context +import android.content.DialogInterface +import androidx.appcompat.app.AlertDialog.Builder +import com.example.myapplication.R +import com.example.myapplication.fileSystem.DeleteHelper +import com.example.myapplication.fileSystem.WrappedFile +import java.io.File + +class AlertHelper { + companion object { + fun showItemAlert( + context: Context, + onCopy: () -> Unit, + onPaste: () -> Unit, + onDelete: () -> Unit, + onCut: () -> Unit, + onInfo: () -> Unit + ) { + val builder = Builder(context) + builder.setTitle(context.getString(R.string.select_action)) + .setItems( + arrayOf( + context.getString(R.string.action_copy), + context.getString(R.string.action_paste), + context.getString(R.string.action_delete), + context.getString(R.string.action_cut), + context.getString(R.string.action_info) + ) + ) { _: DialogInterface?, which: Int -> + when (which) { + 0 -> { + onCopy() + } + + 1 -> { + onPaste() + } + + 2 -> { + onDelete() + } + + 3 -> { + onCut() + } + + 4 -> { + onInfo() + } + } + } + .setNegativeButton(context.getString(R.string.action_cancel)) + { dialog: DialogInterface, which: Int -> dialog.dismiss() } + .show() + } + + fun showNoPasteAlert( + context: Context, + onCopy: () -> Unit, + onPaste: () -> Unit, + onDelete: () -> Unit, + onCut: () -> Unit, + onInfo: () -> Unit + ) { + val builder = Builder(context) + builder.setTitle(context.getString(R.string.select_action)) + .setItems( + arrayOf( + context.getString(R.string.action_copy), + context.getString(R.string.action_delete), + context.getString(R.string.action_cut), + context.getString(R.string.action_info) + ) + ) { _: DialogInterface?, which: Int -> + when (which) { + 0 -> { + onCopy() + } + + 1 -> { + onDelete() + } + + 2 -> { + onCut() + } + + 3 -> { + onInfo() + } + } + } + .setNegativeButton(context.getString(R.string.action_cancel)) + { dialog: DialogInterface, which: Int -> dialog.dismiss() } + .show() + } + + fun showOnlyPasteInfoNewAlert( + context: Context, + onPaste: () -> Unit, + onInfo: () -> Unit, + onNewFile: () -> Unit, + onNewFolder: () -> Unit + ) { + val builder = Builder(context) + builder.setTitle(context.getString(R.string.select_action)) + .setItems( + arrayOf( + context.getString(R.string.action_paste), + context.getString(R.string.action_info), + context.getString(R.string.action_new_file), + context.getString(R.string.action_new_folder) + ) + ) { _: DialogInterface?, which: Int -> + when (which) { + 0 -> onPaste() + 1 -> onInfo() + 2-> onNewFile() + 3-> onNewFolder() + } + } + .setNegativeButton(context.getString(R.string.action_cancel)) + { dialog: DialogInterface, which: Int -> dialog.dismiss() } + .show() + } + + fun showDeleteAlert(context: Context, file: String, onConfirm: (() -> Unit)?) { + val builder = Builder(context) + builder.setTitle(context.getString(R.string.confirm_to_delete)) + .setMessage(context.getString(R.string.confirm_to_delete_file, file)) + .setPositiveButton(context.getString(R.string.confirm)) { _, _ -> + DeleteHelper.delete(file) + onConfirm?.invoke() + } + .setNegativeButton(context.getString(R.string.cancel)) { dialog, _ -> + dialog.dismiss() + } + .show() + } + + fun showFileInfoAlert(context: Context, file: String) { + val f = File(file) + if (!f.exists()) { + return + } + val wrappedFile = WrappedFile(f) + + val builder = Builder(context) + builder.setTitle(context.getString(R.string.file_info)) + .setMessage( + context.getString( + R.string.file_info_text, + wrappedFile.name, + wrappedFile.path, + wrappedFile.getSizeString(), + wrappedFile.getModifiedTimeString(context) + ) + ) + .setNegativeButton(context.getString(R.string.okay)) { dialog, _ -> + dialog.dismiss() + } + .show() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/utils/ClipHelper.kt b/app/src/main/java/com/example/myapplication/utils/ClipHelper.kt new file mode 100644 index 0000000..25d06db --- /dev/null +++ b/app/src/main/java/com/example/myapplication/utils/ClipHelper.kt @@ -0,0 +1,68 @@ +package com.example.myapplication.utils + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import androidx.core.content.FileProvider +import com.example.myapplication.BuildConfig +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.internal.synchronized +import java.io.File + +class ClipHelper private constructor(context: Context) { + companion object { + private const val label = "${BuildConfig.APPLICATION_ID}\$ClipHelper" + private const val ENCODE_LABEL = "ClipHelper" + + private var instance: ClipHelper? = null + + @OptIn(InternalCoroutinesApi::class) + fun getInstance(context: Context): ClipHelper = + instance ?: synchronized(this) { + instance ?: ClipHelper(context).also { instance = it } + } + } + + private val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + private val contentResolver: ContentResolver = context.contentResolver + + fun copy(file: File, context: Context) { + val uri = FileProvider.getUriForFile( + context, + context.applicationContext.packageName + ".provider", + file + ) + val clip = ClipData.newUri(contentResolver, label, uri) + clipboard.setPrimaryClip(clip) + } + + fun paste(): Uri? { + val clip = clipboard.primaryClip + clip?.run { + val item: ClipData.Item = getItemAt(0) + return item.uri + } + return null + } + + fun copyFolder(folder: String){ + val clip = ClipData.newPlainText("SingleFolderCopy","$ENCODE_LABEL:${folder}") + clipboard.setPrimaryClip(clip) + } + + fun pasteFolder(): String? { + val clip = clipboard.primaryClip + val content = clip?.run { + val item: ClipData.Item = getItemAt(0) + item.text + } + if (content != null){ + if (content.startsWith(ENCODE_LABEL)){ + return content.split(':').last() + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/video_page.kt b/app/src/main/java/com/example/myapplication/video_page.kt new file mode 100644 index 0000000..35e2a39 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/video_page.kt @@ -0,0 +1,271 @@ +package com.example.myapplication + +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.os.Environment +import android.view.View +import android.widget.AdapterView +import android.widget.GridView +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AlertDialog.Builder +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsCompat.Type +import com.example.myapplication.adapters.ImageAdapter +import com.example.myapplication.adapters.ImageModel +import com.example.myapplication.adapters.VideoAdapter +import com.example.myapplication.adapters.VideoModel +import com.example.myapplication.fileSystem.CutHelper +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister +import com.example.myapplication.fileSystem.byTypeFileLister.DocumentLister.Companion +import com.example.myapplication.fileSystem.byTypeFileLister.VideoLister.Companion.regex +import com.example.myapplication.fileSystem.byTypeFileLister.VideoLister.Companion.instance +import com.example.myapplication.utils.AlertHelper +import com.example.myapplication.utils.ClipHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.apache.commons.io.IOUtils +import java.io.File + +class video_page : AppCompatActivity() { + private var videoList = listOf() + private val pasteDir = "${Environment.getExternalStorageDirectory().path}/Movies/pasted" + private var videoListOrderType = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + this.enableEdgeToEdge() + setContentView(R.layout.video_page) + ViewCompat.setOnApplyWindowInsetsListener( + findViewById(R.id.main) + ) { v: View, insets: WindowInsetsCompat -> + val systemBars = + insets.getInsets(Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + // 设置排序按钮的点击事件 + val sortImageView = findViewById(R.id.sortVideoView) + sortImageView.setOnClickListener { v: View? -> showSortOptions() } + + // 设置左箭头的点击事件,返回上一级页面 + val leftArrowImageView = findViewById(R.id.leftArrowImageView) + leftArrowImageView.setOnClickListener { v: View? -> + val intent = + Intent( + this@video_page, + main_page::class.java + ) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + startActivity(intent) + finish() + } + + val searchImageView = findViewById(R.id.searchVideoView) + searchImageView.setOnClickListener { v: View? -> + val intent = + Intent( + this@video_page, + video_page_search::class.java + ) + startActivity(intent) // 跳转到搜索页面 + } + + findViewById(R.id.refreshData).setOnClickListener { _ -> + update() + } + + val videoGrid: GridView = findViewById(R.id.VideoGrid) + + videoGrid.onItemClickListener = + AdapterView.OnItemClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long -> + val file = File(videoList[position]) + if (file.isFile) { + val uri = FileProvider.getUriForFile( + this, + applicationContext.packageName + ".provider", + file + ) + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(uri, "video/*") + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + startActivity(intent) + } + } + videoGrid.onItemLongClickListener = + AdapterView.OnItemLongClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long -> + showVideoOperation( + position + ) + true + } + + // 最后在后台刷新 + CoroutineScope(Dispatchers.Default).launch { + val loadingTextView = findViewById(R.id.LoadingBlankText) + val defaultText = loadingTextView.text + launch { loadingText(loadingTextView,defaultText) } + videoList = instance.dateOrderedList() + val videoModels = ArrayList() + for (path in videoList) { + videoModels.add(VideoModel(File(path))) + } + runOnUiThread { + val adapter = VideoAdapter(this@video_page, videoModels) + val grid = findViewById(R.id.VideoGrid) + grid.setAdapter(adapter) + findViewById(R.id.LoadingBlankText).visibility = View.GONE + } + } + } + + private fun showVideoOperation(position: Int) { + AlertHelper.showItemAlert(this, + onCopy = { + val file = File(videoList[position]) + if (file.isFile) { + ClipHelper.getInstance(this).copy(file, this) + } + }, + onPaste = { + val uri = ClipHelper.getInstance(this).paste() + if (uri != null) { + val dir = File(pasteDir) + if (!dir.exists()) { + dir.mkdir() + } + val name = uri.path?.split('/')?.last() ?: "somePastedItem" + val ext = name.split('.').last() + if (!"$ext.".matches(DocumentLister.regex)){ + Toast.makeText(this, getString(R.string.error_nothing_to_paste), Toast.LENGTH_SHORT) + .show() + return@showItemAlert + } + val actualFile = File(dir, "${name}_paste.$ext") + val inputStream = contentResolver.openInputStream(uri) + if (inputStream != null) { + actualFile.writeBytes(IOUtils.toByteArray(inputStream)) + inputStream.close() + // 刷新 + update() + } + } else { + Toast.makeText(this, getString(R.string.error_nothing_to_paste), Toast.LENGTH_SHORT) + .show() + } + }, + onDelete = { + AlertHelper.showDeleteAlert(this, videoList[position]) { + update() + } + }, + onCut = { + CutHelper.cut(this, File(videoList[position])) + update() + }, + onInfo = { + AlertHelper.showFileInfoAlert(this, videoList[position]) + } + ) + } + + private fun showSortOptions() { + val loadingTextView = findViewById(R.id.LoadingBlankText) + // 创建对话框构建者 + val builder = Builder(this@video_page) + builder.setTitle("选择排序方式") + .setItems( + arrayOf("按时间排序(默认)", "按大小排序") + ) { dialog: DialogInterface?, which: Int -> + when (which) { + 0 -> { + videoListOrderType = 0 + runOnUiThread{ + loadingTextView.visibility = View.VISIBLE + } + CoroutineScope(Dispatchers.IO).launch { loadingText(loadingTextView,loadingTextView.text) } + update { + runOnUiThread{ + loadingTextView.visibility = View.GONE + Toast.makeText( + this@video_page, + "已选择按时间排序", + Toast.LENGTH_SHORT + ).show() + } + } + } + + 1 -> { + videoListOrderType = 1 + runOnUiThread{ + loadingTextView.visibility = View.VISIBLE + } + CoroutineScope(Dispatchers.IO).launch { loadingText(loadingTextView,loadingTextView.text) } + update { + runOnUiThread{ + loadingTextView.visibility = View.GONE + Toast.makeText( + this@video_page, + "已选择按大小排序", + Toast.LENGTH_SHORT + ).show() + } + } + } + } + } + .setNegativeButton( + "取消" + ) { dialog: DialogInterface, which: Int -> dialog.dismiss() } + .show() + } + + private fun update(runSomethingMore: (()->Unit)? = null) { + instance.initialize { + videoList = when (videoListOrderType) { + 0 -> instance.dateOrderedList() + 1 -> instance.sizeOrderedList() + else -> listOf() + } + val videoModels = ArrayList() + for (path in videoList) { + videoModels.add(VideoModel(File(path))) + } + val adapter = VideoAdapter(this, videoModels) + runOnUiThread { + val grid = findViewById(R.id.VideoGrid) + grid.setAdapter(adapter) + } + runSomethingMore?.invoke() + } + } + + private fun loadingText( + loadingTextView: TextView, + defaultText: CharSequence, + dots: String = "" + ) { + Thread.sleep(500) + val next = if (dots.length > 3) { + "" + } else { + "$dots." + } + runOnUiThread { + loadingTextView.text = "$defaultText$next" + } + + if (loadingTextView.visibility != View.GONE) { + loadingText(loadingTextView, defaultText, next) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/video_page_search.java b/app/src/main/java/com/example/myapplication/video_page_search.java new file mode 100644 index 0000000..89210b6 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/video_page_search.java @@ -0,0 +1,43 @@ +package com.example.myapplication; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.ImageView; +import android.widget.SearchView; +import android.widget.Toast; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; + +public class video_page_search extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.video_page_search); + // 设置左箭头的点击事件,返回上一级页面 + ImageView leftArrowImageView = findViewById(R.id.leftArrowImageView); + leftArrowImageView.setOnClickListener(v -> { + Intent intent = new Intent(video_page_search.this, video_page.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + }); + SearchView searchView = findViewById(R.id.searchVideo); // 确保使用正确的 ID + + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + // 处理搜索提交 + Toast.makeText(video_page_search.this, "搜索: " + query, Toast.LENGTH_SHORT).show(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + // 处理搜索文本变化 + // 可以在这里添加过滤逻辑 + return false; + } + }); + } +} diff --git a/app/src/main/res/drawable/baseline_camera_alt_24.xml b/app/src/main/res/drawable/baseline_camera_alt_24.xml new file mode 100644 index 0000000..e684cac --- /dev/null +++ b/app/src/main/res/drawable/baseline_camera_alt_24.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/baseline_photo_library_24.xml b/app/src/main/res/drawable/baseline_photo_library_24.xml new file mode 100644 index 0000000..6d08994 --- /dev/null +++ b/app/src/main/res/drawable/baseline_photo_library_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_question_mark_24.xml b/app/src/main/res/drawable/baseline_question_mark_24.xml new file mode 100644 index 0000000..850b78a --- /dev/null +++ b/app/src/main/res/drawable/baseline_question_mark_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_refresh_24.xml b/app/src/main/res/drawable/baseline_refresh_24.xml new file mode 100644 index 0000000..e58b7a9 --- /dev/null +++ b/app/src/main/res/drawable/baseline_refresh_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_settings_24.xml b/app/src/main/res/drawable/baseline_settings_24.xml new file mode 100644 index 0000000..f30d78c --- /dev/null +++ b/app/src/main/res/drawable/baseline_settings_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_storage_24.xml b/app/src/main/res/drawable/baseline_storage_24.xml new file mode 100644 index 0000000..288dba6 --- /dev/null +++ b/app/src/main/res/drawable/baseline_storage_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_roundcorner.xml b/app/src/main/res/drawable/bg_roundcorner.xml new file mode 100644 index 0000000..1e84282 --- /dev/null +++ b/app/src/main/res/drawable/bg_roundcorner.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circular_progress_bar.xml b/app/src/main/res/drawable/circular_progress_bar.xml new file mode 100644 index 0000000..c7426ea --- /dev/null +++ b/app/src/main/res/drawable/circular_progress_bar.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_app.xml b/app/src/main/res/drawable/ic_app.xml new file mode 100644 index 0000000..1ab42a8 --- /dev/null +++ b/app/src/main/res/drawable/ic_app.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_drop_down.xml b/app/src/main/res/drawable/ic_arrow_drop_down.xml new file mode 100644 index 0000000..20425dd --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_drop_down.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_circle_blue.xml b/app/src/main/res/drawable/ic_circle_blue.xml new file mode 100644 index 0000000..1535691 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle_blue.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_circle_green.xml b/app/src/main/res/drawable/ic_circle_green.xml new file mode 100644 index 0000000..f430300 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle_green.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_circle_orange.xml b/app/src/main/res/drawable/ic_circle_orange.xml new file mode 100644 index 0000000..1b151a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle_orange.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_circle_red.xml b/app/src/main/res/drawable/ic_circle_red.xml new file mode 100644 index 0000000..000dd98 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle_red.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_circle_yellow.xml b/app/src/main/res/drawable/ic_circle_yellow.xml new file mode 100644 index 0000000..fdd493b --- /dev/null +++ b/app/src/main/res/drawable/ic_circle_yellow.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_document.xml b/app/src/main/res/drawable/ic_document.xml new file mode 100644 index 0000000..7f91357 --- /dev/null +++ b/app/src/main/res/drawable/ic_document.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 0000000..d8ab46f --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_install_package.xml b/app/src/main/res/drawable/ic_install_package.xml new file mode 100644 index 0000000..a766488 --- /dev/null +++ b/app/src/main/res/drawable/ic_install_package.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..196e181 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..fd905ca --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_left_arrow.xml b/app/src/main/res/drawable/ic_left_arrow.xml new file mode 100644 index 0000000..6385964 --- /dev/null +++ b/app/src/main/res/drawable/ic_left_arrow.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_music.xml b/app/src/main/res/drawable/ic_music.xml new file mode 100644 index 0000000..f5a15b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_music.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_no_document.xml b/app/src/main/res/drawable/ic_no_document.xml new file mode 100644 index 0000000..686fb57 --- /dev/null +++ b/app/src/main/res/drawable/ic_no_document.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_photo.xml b/app/src/main/res/drawable/ic_photo.xml new file mode 100644 index 0000000..ec9c5d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml new file mode 100644 index 0000000..538b7e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_play.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_radio.xml b/app/src/main/res/drawable/ic_radio.xml new file mode 100644 index 0000000..f19576f --- /dev/null +++ b/app/src/main/res/drawable/ic_radio.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..09acb24 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_storage.xml b/app/src/main/res/drawable/ic_storage.xml new file mode 100644 index 0000000..c53b515 --- /dev/null +++ b/app/src/main/res/drawable/ic_storage.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_video.xml b/app/src/main/res/drawable/ic_video.xml new file mode 100644 index 0000000..fc28967 --- /dev/null +++ b/app/src/main/res/drawable/ic_video.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/music_icon.xml b/app/src/main/res/drawable/music_icon.xml new file mode 100644 index 0000000..5c024ac --- /dev/null +++ b/app/src/main/res/drawable/music_icon.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_arrow_upward_32.xml b/app/src/main/res/drawable/outline_arrow_upward_32.xml new file mode 100644 index 0000000..4d6a8f5 --- /dev/null +++ b/app/src/main/res/drawable/outline_arrow_upward_32.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_info_24.xml b/app/src/main/res/drawable/outline_info_24.xml new file mode 100644 index 0000000..8400276 --- /dev/null +++ b/app/src/main/res/drawable/outline_info_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_info_i_24.xml b/app/src/main/res/drawable/outline_info_i_24.xml new file mode 100644 index 0000000..78457e2 --- /dev/null +++ b/app/src/main/res/drawable/outline_info_i_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_lab_profile_24.xml b/app/src/main/res/drawable/rounded_lab_profile_24.xml new file mode 100644 index 0000000..d7f5caf --- /dev/null +++ b/app/src/main/res/drawable/rounded_lab_profile_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/search_border.xml b/app/src/main/res/drawable/search_border.xml new file mode 100644 index 0000000..a1a44a9 --- /dev/null +++ b/app/src/main/res/drawable/search_border.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/select_border.xml b/app/src/main/res/drawable/select_border.xml new file mode 100644 index 0000000..8841f75 --- /dev/null +++ b/app/src/main/res/drawable/select_border.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/type_audio.xml b/app/src/main/res/drawable/type_audio.xml new file mode 100644 index 0000000..6ca72b4 --- /dev/null +++ b/app/src/main/res/drawable/type_audio.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/type_directory.xml b/app/src/main/res/drawable/type_directory.xml new file mode 100644 index 0000000..5d7afd8 --- /dev/null +++ b/app/src/main/res/drawable/type_directory.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/type_file.xml b/app/src/main/res/drawable/type_file.xml new file mode 100644 index 0000000..c109448 --- /dev/null +++ b/app/src/main/res/drawable/type_file.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/type_image.xml b/app/src/main/res/drawable/type_image.xml new file mode 100644 index 0000000..85a2b29 --- /dev/null +++ b/app/src/main/res/drawable/type_image.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/type_video.xml b/app/src/main/res/drawable/type_video.xml new file mode 100644 index 0000000..7e302b0 --- /dev/null +++ b/app/src/main/res/drawable/type_video.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/layout/document_card_item.xml b/app/src/main/res/layout/document_card_item.xml new file mode 100644 index 0000000..b6d853f --- /dev/null +++ b/app/src/main/res/layout/document_card_item.xml @@ -0,0 +1,17 @@ + + + diff --git a/app/src/main/res/layout/document_page.xml b/app/src/main/res/layout/document_page.xml new file mode 100644 index 0000000..861b378 --- /dev/null +++ b/app/src/main/res/layout/document_page.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/document_page_search.xml b/app/src/main/res/layout/document_page_search.xml new file mode 100644 index 0000000..648b52e --- /dev/null +++ b/app/src/main/res/layout/document_page_search.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/main_page.xml b/app/src/main/res/layout/main_page.xml new file mode 100644 index 0000000..34fe523 --- /dev/null +++ b/app/src/main/res/layout/main_page.xml @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + + + + + + + + +