Skip to content

Commit

Permalink
[feature] Initial support for basic media library functions
Browse files Browse the repository at this point in the history
  • Loading branch information
SkyD666 committed Jul 5, 2024
1 parent 19b9dba commit 48da0dd
Show file tree
Hide file tree
Showing 48 changed files with 2,651 additions and 550 deletions.
9 changes: 6 additions & 3 deletions 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-beta49"
versionName = "1.1-beta50"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down Expand Up @@ -136,8 +136,8 @@ composeCompiler {
}

tasks.withType(KotlinCompile::class.java).configureEach {
kotlinOptions {
freeCompilerArgs += listOf(
compilerOptions {
freeCompilerArgs.addAll(
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
Expand All @@ -150,6 +150,7 @@ tasks.withType(KotlinCompile::class.java).configureEach {
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
)
}
}
Expand Down Expand Up @@ -186,10 +187,12 @@ dependencies {
implementation("androidx.preference:preference-ktx:1.2.1")

implementation("com.google.android.material:material:1.12.0")
implementation("com.google.accompanist:accompanist-permissions:0.34.0")

implementation("com.google.dagger:hilt-android:2.51.1")
ksp("com.google.dagger:hilt-android-compiler:2.51.1")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
implementation("androidx.profileinstaller:profileinstaller:1.3.1")

implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:okhttp-coroutines-jvm:5.0.0-alpha.12")
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"
tools:node="remove" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
Expand All @@ -24,14 +27,15 @@
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage"
tools:node="remove" />
tools:node="replace" />

<application
android:name=".App"
android:allowBackup="true"
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"
Expand Down
22 changes: 19 additions & 3 deletions app/src/main/java/com/skyd/anivu/ext/FileExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,36 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.skyd.anivu.R
import com.skyd.anivu.ui.component.showToast
import java.io.File

fun File.toUri(context: Context): Uri = FileProvider.getUriForFile(
context, "${context.packageName}.fileprovider", this
)

fun File.toUri(context: Context): Uri {
return try {
FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", this)
} catch (e: IllegalArgumentException) {
toUri()
}
}

fun File.deleteRecursivelyExclude(hook: (File) -> Boolean = { true }): Boolean =
walkBottomUp().fold(true) { res, it ->
(it != this && hook(it) && (it.delete() || !it.exists())) && res
}

fun File.getMimeType(): String? {
var type: String? = null
val extension = path.substringAfterLast(".", "")
if (extension.isNotBlank()) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
}
return type
}

fun File.savePictureToMediaStore(context: Context, autoDelete: Boolean = true) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues()
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/com/skyd/anivu/ext/FlowExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean

fun <T> Flow<T>.catchMap(transform: FlowCollector<T>.(Throwable) -> T): Flow<T> =
catch { emit(transform(it)) }
catch {
it.printStackTrace()
emit(transform(it))
}

fun <T> concat(flow1: Flow<T>, flow2: Flow<T>): Flow<T> = flow {
emitAll(flow1)
Expand Down
98 changes: 98 additions & 0 deletions app/src/main/java/com/skyd/anivu/ext/IOExt.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.skyd.anivu.ext

import android.content.ActivityNotFoundException
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.webkit.URLUtil
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import com.skyd.anivu.R
import com.skyd.anivu.appContext
import com.skyd.anivu.ui.component.showToast
Expand All @@ -19,6 +23,7 @@ import java.io.InputStream
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.LinkedList


fun Uri.copyTo(target: File): File {
Expand Down Expand Up @@ -90,6 +95,99 @@ private fun Uri.openChooser(context: Context, action: String, chooserTitle: Char
}
}

fun Uri.toDocumentFile(context: Context, isTree: Boolean = false): DocumentFile? {
val uriString = toString()
return if (URLUtil.isFileUrl(uriString) || uriString.startsWith("/")) {
DocumentFile.fromFile(File(uriString))
} else {
if (isTree) DocumentFile.fromTreeUri(context, this)
else DocumentFile.fromSingleUri(context, this)
}
}

fun Uri.traverseDirectoryEntries(
contentResolver: ContentResolver,
projection: Array<String>,
onEach: (Uri, Cursor) -> Unit
) {
var childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
this,
DocumentsContract.getTreeDocumentId(this)
)

// Keep track of our directory hierarchy
val dirNodes: MutableList<Uri> = LinkedList()
dirNodes.add(childrenUri)

while (dirNodes.isNotEmpty()) {
childrenUri = dirNodes.removeAt(0) // get the item from top
contentResolver.query(
childrenUri,
(arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_MIME_TYPE
) + projection).distinct().toTypedArray(),
null,
null,
null
)?.use { c ->
while (c.moveToNext()) {
val docId = c.getString(0)
val mime = c.getString(1)
if (DocumentsContract.Document.MIME_TYPE_DIR == mime) {
val newNode = DocumentsContract.buildChildDocumentsUriUsingTree(this, docId)
dirNodes.add(newNode)
}
onEach(childrenUri, c)
}
}
}
}

fun Uri.listFiles(
contentResolver: ContentResolver,
): List<Uri> {
val result = mutableListOf<Uri>()

contentResolver.query(
this,
arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID),
null,
null,
null
)?.use { c ->
while (c.moveToNext()) {
val docId = c.getString(0)
result.add(DocumentsContract.buildDocumentUriUsingTree(this, docId))
}
}

return result
}

/**
* Needs tree uri
*/
fun Uri.findFile(contentResolver: ContentResolver, name: String): Uri? {
contentResolver.query(
this,
arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
),
null,
null,
null
)?.use { c ->
while (c.moveToNext()) {
val docId = c.getString(0)
if (name != c.getString(1)) continue
return DocumentsContract.buildDocumentUriUsingTree(this, docId)
}
}
return null
}

fun Uri.isLocal(): Boolean = URLUtil.isFileUrl(toString()) || URLUtil.isContentUrl(toString())

fun Uri.isNetwork(): Boolean = URLUtil.isNetworkUrl(toString())
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.skyd.anivu.model.preference.data.OpmlExportDirPreference
import com.skyd.anivu.model.preference.data.autodelete.AutoDeleteArticleBeforePreference
import com.skyd.anivu.model.preference.data.autodelete.AutoDeleteArticleFrequencyPreference
import com.skyd.anivu.model.preference.data.autodelete.UseAutoDeletePreference
import com.skyd.anivu.model.preference.data.medialib.MediaLibLocationPreference
import com.skyd.anivu.model.preference.player.HardwareDecodePreference
import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference
import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference
Expand Down Expand Up @@ -74,5 +75,6 @@ fun Preferences.toSettings(): Settings {
autoDeleteArticleFrequency = AutoDeleteArticleFrequencyPreference.fromPreferences(this),
autoDeleteArticleBefore = AutoDeleteArticleBeforePreference.fromPreferences(this),
opmlExportDir = OpmlExportDirPreference.fromPreferences(this),
mediaLibLocation = MediaLibLocationPreference.fromPreferences(this),
)
}
33 changes: 33 additions & 0 deletions app/src/main/java/com/skyd/anivu/model/bean/MediaGroupBean.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.skyd.anivu.model.bean

import com.skyd.anivu.R
import com.skyd.anivu.appContext
import com.skyd.anivu.base.BaseBean

open class MediaGroupBean(
open val name: String,
) : BaseBean {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is MediaGroupBean) return false
if (this === DefaultMediaGroup || other === DefaultMediaGroup) return false
return name == other.name
}

override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + (this === DefaultMediaGroup).hashCode()
return result
}

object DefaultMediaGroup :
MediaGroupBean(appContext.getString(R.string.default_media_group)) {
private fun readResolve(): Any = DefaultMediaGroup
override val name: String
get() = appContext.getString(R.string.default_media_group)
}

companion object {
fun MediaGroupBean.isDefaultGroup(): Boolean = this === DefaultMediaGroup
}
}
29 changes: 9 additions & 20 deletions app/src/main/java/com/skyd/anivu/model/bean/VideoBean.kt
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
package com.skyd.anivu.model.bean

import android.content.Context
import android.os.Parcelable
import com.skyd.anivu.base.BaseBean
import com.skyd.anivu.ext.toUri
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import com.skyd.anivu.ext.getMimeType
import java.io.File

@Parcelize
data class VideoBean(
val displayName: String? = null,
val file: File,
) : BaseBean, Parcelable {
fun isMedia(context: Context): Boolean = context.contentResolver
.getType(file.toUri(context))?.startsWith("video/") == true

@IgnoredOnParcel
val name: String = file.name.orEmpty()

@IgnoredOnParcel
val size: Long = file.length()

@IgnoredOnParcel
val date: Long = file.lastModified()

@IgnoredOnParcel
) : BaseBean {
var name: String = file.name
var mimetype: String = 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
}
4 changes: 4 additions & 0 deletions app/src/main/java/com/skyd/anivu/model/preference/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.skyd.anivu.model.preference.data.OpmlExportDirPreference
import com.skyd.anivu.model.preference.data.autodelete.AutoDeleteArticleBeforePreference
import com.skyd.anivu.model.preference.data.autodelete.AutoDeleteArticleFrequencyPreference
import com.skyd.anivu.model.preference.data.autodelete.UseAutoDeletePreference
import com.skyd.anivu.model.preference.data.medialib.MediaLibLocationPreference
import com.skyd.anivu.model.preference.player.HardwareDecodePreference
import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference
import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference
Expand All @@ -54,6 +55,7 @@ import com.skyd.anivu.ui.local.LocalFeedTopBarTonalElevation
import com.skyd.anivu.ui.local.LocalHardwareDecode
import com.skyd.anivu.ui.local.LocalHideEmptyDefault
import com.skyd.anivu.ui.local.LocalIgnoreUpdateVersion
import com.skyd.anivu.ui.local.LocalMediaLibLocation
import com.skyd.anivu.ui.local.LocalNavigationBarLabel
import com.skyd.anivu.ui.local.LocalOpmlExportDir
import com.skyd.anivu.ui.local.LocalPickImageMethod
Expand Down Expand Up @@ -106,6 +108,7 @@ data class Settings(
val autoDeleteArticleFrequency: Long = AutoDeleteArticleFrequencyPreference.default,
val autoDeleteArticleBefore: Long = AutoDeleteArticleBeforePreference.default,
val opmlExportDir: String = OpmlExportDirPreference.default,
val mediaLibLocation: String = MediaLibLocationPreference.default,
)

@Composable
Expand Down Expand Up @@ -152,6 +155,7 @@ fun SettingsProvider(
LocalAutoDeleteArticleFrequency provides settings.autoDeleteArticleFrequency,
LocalAutoDeleteArticleBefore provides settings.autoDeleteArticleBefore,
LocalOpmlExportDir provides settings.opmlExportDir,
LocalMediaLibLocation provides settings.mediaLibLocation,
) {
content()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.skyd.anivu.model.preference.data

import android.content.Context
import android.os.Environment
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
import com.skyd.anivu.base.BasePreference
import com.skyd.anivu.ext.dataStore
import com.skyd.anivu.ext.put
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

object FilePickerLocationPreference : BasePreference<String> {
private const val FILE_PICKER_LOCATION = "filePickerLocation"

override val default: String = Environment.getExternalStorageDirectory().absolutePath

val key = stringPreferencesKey(FILE_PICKER_LOCATION)

fun put(context: Context, scope: CoroutineScope, value: String) {
scope.launch(Dispatchers.IO) {
context.dataStore.put(key, value)
}
}

override fun fromPreferences(preferences: Preferences): String = preferences[key] ?: default
}
Loading

0 comments on commit 48da0dd

Please sign in to comment.