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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ dependencies {
implementation(libs.kotlinx.immutable.collections)
implementation(libs.truetype.parser)
implementation(libs.fsaf)
implementation(libs.storage.util)
}

detekt {
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
android:minSdkVersion="30"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />

<application
android:name=".App"
android:allowBackup="true"
android:appCategory="video"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
Expand Down
104 changes: 72 additions & 32 deletions app/src/main/java/live/mehiz/mpvkt/ui/home/FilePickerScreen.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package live.mehiz.mpvkt.ui.home

import android.net.Uri
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
Expand Down Expand Up @@ -39,11 +40,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.github.k1rakishou.fsaf.FileManager
import com.github.k1rakishou.fsaf.file.AbstractFile
import hendrawd.storageutil.library.StorageUtil
import `is`.xyz.mpv.Utils
import live.mehiz.mpvkt.R
import live.mehiz.mpvkt.presentation.Screen
Expand All @@ -52,21 +51,21 @@ import live.mehiz.mpvkt.ui.player.imageExtensions
import live.mehiz.mpvkt.ui.player.videoExtensions
import live.mehiz.mpvkt.ui.theme.spacing
import live.mehiz.mpvkt.ui.utils.FilesComparator
import org.koin.compose.koinInject
import java.io.File
import java.lang.Long.signum
import java.text.StringCharacterIterator
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlin.math.abs

data class FilePickerScreen(val uri: String) : Screen() {
data class FilePickerScreen(val path: String? = null) : Screen() {

@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val fileManager = koinInject<FileManager>()
val context = LocalContext.current
Scaffold(
topBar = {
Expand All @@ -80,34 +79,75 @@ data class FilePickerScreen(val uri: String) : Screen() {
)
},
) { paddingValues ->
FilePicker(
directory = fileManager.fromUri(uri.toUri())!!,
onNavigate = { newFile ->
if (fileManager.isFile(newFile)) {
HomeScreen.playFile(newFile.getFullPath(), context)
return@FilePicker
}
navigator.push(FilePickerScreen(newFile.getFullPath()))
},
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
)
if (path == null) {
StoragePicker(
onNavigate = { device -> navigator.push(FilePickerScreen(device.absolutePath)) },
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
)
} else {
FilePicker(
directory = File(path),
onNavigate = { newFile ->
if (newFile.isFile) {
HomeScreen.playFile(Uri.fromFile(newFile).toString(), context)
return@FilePicker
}
navigator.push(FilePickerScreen(newFile.absolutePath))
},
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
)
}
}
}

@Composable
fun StoragePicker(
modifier: Modifier = Modifier,
onNavigate: (File) -> Unit,
) {
val context = LocalContext.current
val deviceList = StorageUtil.getStorageDirectories(context)
.map { File(it) }
.filter { it.exists() }

LazyColumn(modifier) {
itemsIndexed(deviceList, key = { _, file -> file.absolutePath }) { index, file ->
FileListing(
name = file.absolutePath,
isDirectory = true,
lastModified = null,
length = null,
modifier = Modifier.background(
if (index % 2 == 0) {
MaterialTheme.colorScheme.surfaceContainerLow
} else {
MaterialTheme.colorScheme.surfaceContainerHigh
},
),
items = null,
onClick = { onNavigate(file) },
)
}
}
}

@Composable
fun FilePicker(
directory: AbstractFile,
directory: File,
modifier: Modifier = Modifier,
onNavigate: (AbstractFile) -> Unit,
onNavigate: (File) -> Unit,
) {
val navigator = LocalNavigator.currentOrThrow
val fileManager = koinInject<FileManager>()
val fileList = fileManager.listFiles(directory).filterNot {
!Utils.MEDIA_EXTENSIONS.contains(fileManager.getName(it).substringAfterLast('.')) &&
fileManager.isFile(it) || fileManager.getName(it).startsWith('.')
}.sortedWith(FilesComparator(fileManager))
val fileList = directory.listFiles { file ->
val name = file.name
if (name.startsWith('.')) return@listFiles false
if (file.isDirectory) return@listFiles true
file.isFile && Utils.MEDIA_EXTENSIONS.contains(name.substringAfterLast('.').lowercase(Locale.ENGLISH))
}?.sortedWith(FilesComparator()) ?: emptyList()

LazyColumn(modifier) {
item {
Expand All @@ -120,20 +160,20 @@ data class FilePickerScreen(val uri: String) : Screen() {
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainerLow),
)
}
itemsIndexed(fileList, key = { _, file -> fileManager.getName(file) }) { index, file ->
itemsIndexed(fileList, key = { _, file -> file.name }) { index, file ->
FileListing(
name = fileManager.getName(file),
isDirectory = fileManager.isDirectory(file),
lastModified = fileManager.lastModified(file),
length = if (fileManager.isFile(file)) fileManager.getLength(file) else null,
name = file.name,
isDirectory = file.isDirectory,
lastModified = file.lastModified(),
length = if (file.isFile) file.length() else null,
modifier = Modifier.background(
if (index % 2 == 1) {
MaterialTheme.colorScheme.surfaceContainerLow
} else {
MaterialTheme.colorScheme.surfaceContainerHigh
},
),
items = if (fileManager.isDirectory(file)) fileManager.listFiles(file).size else null,
items = if (file.isDirectory) file.listFiles()?.size else null,
onClick = { onNavigate(file) },
)
}
Expand Down
57 changes: 33 additions & 24 deletions app/src/main/java/live/mehiz/mpvkt/ui/home/HomeScreen.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package live.mehiz.mpvkt.ui.home

import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
Expand All @@ -11,7 +16,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Link
Expand All @@ -36,10 +40,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.github.k1rakishou.fsaf.FileManager
import `is`.xyz.mpv.Utils.PROTOCOLS
import live.mehiz.mpvkt.R
import live.mehiz.mpvkt.presentation.Screen
Expand Down Expand Up @@ -108,31 +112,36 @@ object HomeScreen : Screen() {
Text(text = stringResource(R.string.home_open_url))
}
}
val documentPicker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument(),
) {
if (it == null) return@rememberLauncherForActivityResult
playFile(it.toString(), context)
}
OutlinedButton(
onClick = { documentPicker.launch(arrayOf("*/*")) },
) {
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.smaller),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.Default.FileOpen, null)
Text(text = stringResource(R.string.home_pick_file))
val manageStoragePermission = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
navigator.push(FilePickerScreen())
}
}
val fileManager = FileManager(context)
val directoryPicker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocumentTree(),
) {
if (it == null) return@rememberLauncherForActivityResult
navigator.push(FilePickerScreen(fileManager.fromUri(it)!!.getFullPath()))
val readStoragePermission = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
navigator.push(FilePickerScreen())
}
}
OutlinedButton(onClick = { directoryPicker.launch(null) }) {
OutlinedButton(onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
manageStoragePermission.launch(
Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
"package:${context.packageName}".toUri(),
)
)
} else {
navigator.push(FilePickerScreen())
}
} else {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
readStoragePermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} else {
navigator.push(FilePickerScreen())
}
}
}) {
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.smaller),
verticalAlignment = Alignment.CenterVertically,
Expand Down
15 changes: 6 additions & 9 deletions app/src/main/java/live/mehiz/mpvkt/ui/utils/FilesComparator.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
package live.mehiz.mpvkt.ui.utils

import com.github.k1rakishou.fsaf.FileManager
import com.github.k1rakishou.fsaf.file.AbstractFile
import java.io.File

/**
* Sorts files/directories alphabetically while giving directories priority
* credit goes to mpv-android
*/
class FilesComparator(
private val fileManager: FileManager
) : Comparator<AbstractFile> {
override fun compare(o1: AbstractFile?, o2: AbstractFile?): Int {
val iso1ADirectory = fileManager.isDirectory(o1!!)
val iso2ADirectory = fileManager.isDirectory(o2!!)
class FilesComparator : Comparator<File> {
override fun compare(o1: File?, o2: File?): Int {
val iso1ADirectory = o1!!.isDirectory
val iso2ADirectory = o2!!.isDirectory
if (iso1ADirectory != iso2ADirectory) return if (iso2ADirectory) 1 else -1
return fileManager.getName(o1).compareTo(fileManager.getName(o2), ignoreCase = true)
return o1.name.compareTo(o2.name, ignoreCase = true)
}
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ kotlinx-immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collec

truetype-parser = { module = "io.github.yubyf:truetypeparser-light", version = "2.1.4" }
fsaf = { module = "com.github.K1rakishou:Fuck-Storage-Access-Framework", version = "1.1.3" }
storage-util = { module = "com.github.hendrawd:StorageUtil", version = "1.1.0" }

about-libs-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "about-libs" }
about-libs-ui-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "about-libs" }
Expand Down
Loading