Skip to content

Commit

Permalink
[feature|optimize] Change the style of media library layout; support …
Browse files Browse the repository at this point in the history
…displaying the icons of typical file types
  • Loading branch information
SkyD666 committed Jul 13, 2024
1 parent a168c50 commit 7037a15
Show file tree
Hide file tree
Showing 55 changed files with 1,544 additions and 900 deletions.
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ android {
minSdk = 24
targetSdk = 34
versionCode = 18
versionName = "1.1-beta53"
versionName = "1.1-beta54"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down Expand Up @@ -209,6 +209,7 @@ dependencies {
implementation("io.coil-kt:coil-compose:2.6.0")
implementation("io.coil-kt:coil-gif:2.6.0")
implementation("io.coil-kt:coil-svg:2.6.0")
implementation("com.airbnb.android:lottie-compose:6.4.1")
implementation("com.rometools:rome:2.1.0")
implementation("be.ceau:opml-parser:3.1.0") {
exclude(group = "net.sf.kxml", module = "kxml2")
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AniVu.Pink"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/skyd/anivu/ext/FileExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
Expand All @@ -28,6 +29,7 @@ fun File.deleteRecursivelyExclude(hook: (File) -> Boolean = { true }): Boolean =
}

fun File.getMimeType(): String? {
if (isDirectory) return DocumentsContract.Document.MIME_TYPE_DIR
var type: String? = null
val extension = path.substringAfterLast(".", "")
if (extension.isNotBlank()) {
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/skyd/anivu/model/bean/MediaGroupBean.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ open class MediaGroupBean(
return result
}

override fun toString(): String {
return if (isDefaultGroup()) "default"
else name
}

object DefaultMediaGroup :
MediaGroupBean(appContext.getString(R.string.default_media_group)) {
private fun readResolve(): Any = DefaultMediaGroup
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/com/skyd/anivu/model/bean/VideoBean.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ package com.skyd.anivu.model.bean

import com.skyd.anivu.base.BaseBean
import com.skyd.anivu.ext.getMimeType
import com.skyd.anivu.util.fileicon.getFileIcon
import java.io.File

data class VideoBean(
val displayName: String? = null,
val file: File,
) : BaseBean {
var name: String = file.name
var mimetype: String = file.getMimeType() ?: "*/*"
val mimetype: String by lazy { file.getMimeType() ?: "*/*" }
var size: Long = file.length()
var date: Long = file.lastModified()
val isMedia: Boolean = mimetype.startsWith("video/") || mimetype.startsWith("audio/")
val isDir: Boolean = file.isDirectory
val isFile: Boolean = file.isFile
val icon: Int by lazy { getFileIcon(mimetype).resourceId }
}
101 changes: 43 additions & 58 deletions app/src/main/java/com/skyd/anivu/model/repository/MediaRepository.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.skyd.anivu.model.repository

import androidx.compose.ui.util.fastFirstOrNull
import com.skyd.anivu.base.BaseRepository
import com.skyd.anivu.model.bean.MediaGroupBean
import com.skyd.anivu.model.bean.MediaGroupBean.Companion.isDefaultGroup
Expand All @@ -26,66 +27,57 @@ class MediaRepository @Inject constructor(
const val GROUP_JSON_NAME = "group.json"
}

fun requestMedias(uriPath: String, isMediaLibRoot: Boolean): Flow<List<Any>> {
fun requestGroups(uriPath: String): Flow<List<MediaGroupBean>> {
return flow {
val file = File(uriPath)
val groupJsonFile = File(uriPath, GROUP_JSON_NAME)
val mediaGroupJson = parseGroupJson(groupJsonFile)
val allGroups = mediaGroupJson?.allGroups.orEmpty()
emit(listOf(MediaGroupBean.DefaultMediaGroup) +
allGroups.map { MediaGroupBean(name = it) }.sortedBy { it.name })
}.flowOn(Dispatchers.IO)
}

val fileList = file.listFiles()
.orEmpty()
.map {
fun requestFiles(uriPath: String, group: MediaGroupBean?): Flow<List<VideoBean>> {
return flow {
val groupJsonFile = File(uriPath, GROUP_JSON_NAME)
val mediaGroupJson = parseGroupJson(groupJsonFile)
val filesInGroup = mediaGroupJson?.files.orEmpty()
val allFiles =
File(uriPath).listFiles().orEmpty().toMutableList().filter { it.exists() }
val videoList = if (group == null) {
allFiles.map {
VideoBean(
displayName = if (it.isDirectory) {
parseFolderInfoJson(File(it, FOLDER_INFO_JSON_NAME))?.displayName
} else {
null
},
displayName = null, // TODO
file = it,
)
}

if (isMediaLibRoot) {
val groupToFiles = parseGroupJsonToMap(
groupJsonFile = File(file, GROUP_JSON_NAME),
)

// Map group name to group object
val groupNameToObject = groupToFiles.keys
.map { MediaGroupBean(name = it) }
.associateBy { it.name }

// Map group object to video list
val result = mapOf(MediaGroupBean.DefaultMediaGroup to mutableListOf<VideoBean>()) +
groupNameToObject.values.map { it to mutableListOf() }

// Map (fileName, isFile) to group object
val fileNameMap = groupToFiles.values
.flatten()
.associateBy { it.fileName }
.mapKeys { it.key to it.value.isFile }

fileList.forEach { videoBean ->
// Skip config files
if (GROUP_JSON_NAME == videoBean.name) return@forEach
val fileToGroup = fileNameMap[videoBean.name to videoBean.isFile]
if (fileToGroup == null) {
result[MediaGroupBean.DefaultMediaGroup]!! += videoBean
} else {
result[groupNameToObject[fileToGroup.groupName]]!! += videoBean
} else {
if (group.isDefaultGroup()) {
allFiles.filter { file ->
filesInGroup.fastFirstOrNull { it.fileName == file.name } == null
}.map { file ->
VideoBean(
displayName = null, // TODO
file = file,
)
}
} else {
filesInGroup.filter { it.groupName == group.name }.mapNotNull {
val file = File(uriPath, it.fileName)
if (file.exists()) {
VideoBean(
displayName = null, // TODO
file = file,
)
} else null
}
}

emit(result.flatMap { (group, list) -> listOf(group) + list })
} else {
// Skip config files
emit(fileList.toMutableList().apply {
forEach {
if (it.name == GROUP_JSON_NAME) {
remove(it)
return@apply
}
}
})
}
emit(videoList.toMutableList().apply {
fastFirstOrNull { it.name.equals(FOLDER_INFO_JSON_NAME, true) }?.let { remove(it) }
fastFirstOrNull { it.name.equals(GROUP_JSON_NAME, true) }?.let { remove(it) }
})
}.flowOn(Dispatchers.IO)
}

Expand All @@ -101,7 +93,7 @@ class MediaRepository @Inject constructor(
return@flow
}
val groupJsonFile = File(uriPath, GROUP_JSON_NAME)
val mediaGroupJson = parseGroupJson(groupJsonFile)!!
val mediaGroupJson = parseGroupJson(groupJsonFile) ?: MediaGroupJson(files = emptyList())

writeGroupToJson(
groupJsonFile,
Expand Down Expand Up @@ -293,11 +285,4 @@ class MediaRepository @Inject constructor(
json.encodeToStream(formatMediaGroupJson(data), outputStream)
}
}

private fun parseFolderInfoJson(folderJsonFile: File): FolderInfo? {
if (!folderJsonFile.exists()) return null
return folderJsonFile.inputStream().use { inputStream ->
json.decodeFromStream(inputStream)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.skyd.anivu.ui.component

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition

@Composable
fun AniVuLottieAnimation(
modifier: Modifier = Modifier,
@androidx.annotation.RawRes resId: Int,
contentScale: ContentScale = ContentScale.Inside,
) {
val lottieComposition by rememberLottieComposition(LottieCompositionSpec.RawRes(resId))
val lottieProgress by animateLottieCompositionAsState(
composition = lottieComposition,
iterations = LottieConstants.IterateForever
)
LottieAnimation(
modifier = modifier,
composition = lottieComposition,
progress = { lottieProgress },
contentScale = contentScale,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.skyd.anivu.ui.component

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp

@Composable
fun AnimatedPlaceholder(
modifier: Modifier = Modifier,
@androidx.annotation.RawRes resId: Int,
tip: String,
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
AniVuLottieAnimation(resId = resId)
Text(
modifier = Modifier.padding(top = 10.dp),
text = tip,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
}
}
4 changes: 1 addition & 3 deletions app/src/main/java/com/skyd/anivu/ui/fragment/MainFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,7 @@ fun MainScreen() {
popExitTransition = { fadeOut(animationSpec = tween(170)) },
) {
composable(FEED_SCREEN_ROUTE) { FeedScreen() }
composable(MEDIA_SCREEN_ROUTE) {
MediaScreen(path = LocalMediaLibLocation.current, hasParentDir = false)
}
composable(MEDIA_SCREEN_ROUTE) { MediaScreen(path = LocalMediaLibLocation.current) }
composable(MORE_SCREEN_ROUTE) { MoreScreen() }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.InsertDriveFile
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.PhoneAndroid
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
Expand All @@ -34,6 +32,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
Expand All @@ -42,12 +41,14 @@ import com.skyd.anivu.R
import com.skyd.anivu.base.BaseComposeFragment
import com.skyd.anivu.base.mvi.getDispatcher
import com.skyd.anivu.ext.findMainNavController
import com.skyd.anivu.ext.getMimeType
import com.skyd.anivu.ext.popBackStackWithLifecycle
import com.skyd.anivu.ext.showSnackbarWithLaunchedEffect
import com.skyd.anivu.ui.component.AniVuIconButton
import com.skyd.anivu.ui.component.AniVuTopBar
import com.skyd.anivu.ui.component.AniVuTopBarStyle
import com.skyd.anivu.ui.local.LocalNavController
import com.skyd.anivu.util.fileicon.getFileIcon
import dagger.hilt.android.AndroidEntryPoint
import java.io.File
import java.io.Serializable
Expand Down Expand Up @@ -170,8 +171,11 @@ fun FilePickerScreen(
},
leadingContent = {
Icon(
imageVector = if (file.isDirectory) Icons.Outlined.Folder
else Icons.AutoMirrored.Outlined.InsertDriveFile,
painter = painterResource(
id = remember(file) {
getFileIcon(file.getMimeType() ?: "*/*").resourceId
}
),
contentDescription = null,
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import com.skyd.anivu.R
import com.skyd.anivu.model.bean.MediaGroupBean
import com.skyd.anivu.model.bean.MediaGroupBean.Companion.isDefaultGroup
import com.skyd.anivu.ui.component.dialog.TextFieldDialog
import com.skyd.anivu.ui.fragment.media.list.GroupArea
import com.skyd.anivu.ui.fragment.media.list.OptionArea

@Composable
fun EditMediaGroupSheet(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,6 @@ import com.skyd.anivu.base.mvi.MviSingleEvent
import com.skyd.anivu.model.bean.MediaGroupBean

sealed interface MediaEvent : MviSingleEvent {
sealed interface MediaListResultEvent : MediaEvent {
data class Failed(val msg: String) : MediaListResultEvent
}

sealed interface DeleteFileResultEvent : MediaEvent {
data class Failed(val msg: String) : DeleteFileResultEvent
}

sealed interface DeleteGroupResultEvent : MediaEvent {
data class Success(val timestamp: Long) : DeleteGroupResultEvent
data class Failed(val msg: String) : DeleteGroupResultEvent
Expand Down
Loading

0 comments on commit 7037a15

Please sign in to comment.