Skip to content

Commit

Permalink
[FEATURE] Blacklist Functionality Added
Browse files Browse the repository at this point in the history
- Users can now easily blacklist a folder by long-pressing it in the FolderView.
- A Blacklist dialog has been added for easier management.

[FEATURE] Trashcan Feature Implemented
- Users on Android 11 and above can now toggle the trashcan feature on/off.

[FEATURE] Fetch Artwork from MediaStore Toggle Added
- This feature is currently under development and requires further polishing.

[FEATURE] Recent Playlist Size Customization
- Users can now specify the maximum number of tracks that the recent playlist can hold.
This commit fixes #32 #28 #27 and partially #25
  • Loading branch information
iZakirSheikh committed Aug 8, 2023
1 parent 5ebf1b4 commit 3b72a20
Show file tree
Hide file tree
Showing 12 changed files with 766 additions and 80 deletions.
42 changes: 28 additions & 14 deletions app/src/main/java/com/prime/media/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import com.prime.media.core.db.findAudio
import com.prime.media.core.db.toMediaItem
import com.prime.media.core.playback.Remote
import com.primex.core.MetroGreen
import com.primex.core.OrientRed
import com.primex.core.Text
import com.primex.preferences.Key
import com.primex.preferences.Preferences
Expand Down Expand Up @@ -336,21 +337,34 @@ class MainActivity : ComponentActivity(), SystemFacade {
}

override fun launchEqualizer(id: Int) {
if (id == AudioEffect.ERROR_BAD_VALUE) {
Toast.makeText(this, "No Session Id", Toast.LENGTH_LONG).show();
return
}
val res = kotlin.runCatching {
startActivity(
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
putExtra(AudioEffect.EXTRA_AUDIO_SESSION, id)
putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC);
}
)
lifecycleScope.launch {
if (id == AudioEffect.ERROR_BAD_VALUE)
return@launch show(R.string.error_msg, R.string.error)
val result = kotlin.runCatching {
startActivity(
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
putExtra(AudioEffect.EXTRA_AUDIO_SESSION, id)
putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC);
}
)
}
if (!result.isFailure)
return@launch
val res = channel.show(
message = R.string.equalizer_3rd_party_not_found_msg,
action = R.string.launch,
accent = Color.OrientRed,
duration = Duration.Short
)
if (res != Channel.Result.ActionPerformed)
return@launch
runCatching {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("market://search?q=equalizer")
startActivity(intent)
}
}
if (res.exceptionOrNull() is ActivityNotFoundException)
Toast.makeText(this, "There is no equalizer", Toast.LENGTH_SHORT).show();
}

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
Expand Down
150 changes: 150 additions & 0 deletions app/src/main/java/com/prime/media/core/compose/Preferences.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.prime.media.core.compose

import android.R
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Slider
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.primex.core.composableOrNull
import com.primex.core.rememberState
import com.primex.material2.Label
import com.primex.material2.Preference
import com.primex.material2.Text


@Composable
private fun TextButtons(
modifier: Modifier = Modifier,
onConfirmClick: () -> Unit,
onCancelClick: () -> Unit
) {

Row(
modifier = modifier,
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
TextButton(onClick = onCancelClick) {
Label(text = androidx.compose.ui.res.stringResource(id = R.string.cancel))
}

TextButton(onClick = onConfirmClick) {
Label(text = androidx.compose.ui.res.stringResource(id = R.string.ok))
}
}
}

@Composable
fun SliderPreference2(
title: CharSequence,
defaultValue: Float,
onValueChange: (Float) -> Unit,
modifier: Modifier = Modifier,
steps: Int = 0,
enabled: Boolean = true,
singleLineTitle: Boolean = true,
iconSpaceReserved: Boolean = true,
icon: ImageVector? = null,
summery: CharSequence? = null,
forceVisible: Boolean = false,
iconChange: ImageVector? = null,
preview: CharSequence? = null,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
) {

val revealable =
@Composable {
val startPadding = (if (iconSpaceReserved) 24.dp + 16.dp else 0.dp) + 8.dp
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = startPadding)
) {
// place slider
var value by rememberState(initial = defaultValue)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
if (iconChange != null)
Icon(
imageVector = iconChange,
contentDescription = null,
)
Slider(
value = value,
onValueChange = {
value = it
},
valueRange = valueRange,
steps = steps,
modifier = Modifier.weight(1f)
)
if (iconChange != null) {
Icon(
imageVector = iconChange,
contentDescription = null,
modifier = Modifier.scale(1.5f)
)
}
}

val manager = LocalFocusManager.current
val onCancelClick = {
if (!forceVisible)
manager.clearFocus(true)
}
val onConfirmClick = {
if (!forceVisible)
manager.clearFocus(true)
onValueChange(value)
}
TextButtons(
onCancelClick = onCancelClick,
onConfirmClick = onConfirmClick,
modifier = Modifier.fillMaxWidth()
)
}
}

Preference(
modifier = modifier,
title = title,
enabled = enabled,
singleLineTitle = singleLineTitle,
iconSpaceReserved = iconSpaceReserved,
icon = icon,
forceVisible = forceVisible,
summery = summery,
widget = composableOrNull(preview != null) {
Text(
text = preview ?: "",
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Bold,
modifier = Modifier
.size(60.dp)
.wrapContentSize(Alignment.Center),
textAlign = TextAlign.Center
)
},
revealable = revealable
)
}
64 changes: 64 additions & 0 deletions app/src/main/java/com/prime/media/core/db/ContentResolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -876,4 +876,68 @@ suspend fun ContentResolver.delete(activity: ComponentActivity, vararg uri: Uri)
// Launch the activity for result using the IntentSenderRequest object
launcher.launch(intentSenderRequest)
}
}

/**
* A simple extension method that trashes files insetead of deleting them.
* @see delete
*/
@RequiresApi(Build.VERSION_CODES.R)
suspend fun ContentResolver.trash(activity: ComponentActivity, vararg uri: Uri): Int {
if (uri.isEmpty()) return -1 // error
return suspendCoroutine { continuation ->
// Create a lazy ActivityResultLauncher object
var launcher: ActivityResultLauncher<IntentSenderRequest>? = null
// Assign result to launcher in such a way tha it allows us to
// unregister later.
val contract = ActivityResultContracts.StartIntentSenderForResult()
launcher = activity.registerActivityResultLauncher(contract) {
// unregister launcher
launcher?.unregister()
// user cancelled
if (it.resultCode == Activity.RESULT_CANCELED) {
continuation.resume(-3 /*cancelled*/)
return@registerActivityResultLauncher
}
// some unknown error occurred
if (it.resultCode != Activity.RESULT_OK) {
continuation.resume(-1 /*Error*/)
return@registerActivityResultLauncher
}
// construct which ids have been removed.
val ids = uri.joinToString(", ") {
"${ContentUris.parseId(it)}"
}
// assuming that all uri's are content uris.
val parent = ContentUris.removeId(uri[0])
// check how many are still there; maybe some error occurred while deleting some files.
val projection = arrayOf(MediaStore.MediaColumns._ID)
val count =
query(
parent,
projection,
"${MediaStore.MediaColumns._ID} IN ($ids)",
null,
null
).use { it?.count ?: -1 }
// resume with how many files have been trashed or error code.
continuation.resume(if (count > 0) uri.size - count else if (count == 0) uri.size else count)
}
val request = MediaStore.createTrashRequest(this, uri.toList(), true).intentSender
// Create an IntentSenderRequest object from the IntentSender object
val intentSenderRequest = IntentSenderRequest.Builder(request).build()
// Launch the activity for result using the IntentSenderRequest object
launcher.launch(intentSenderRequest)
}
}

/**
* A simple extension method that trashes files instead of deleting them.
* @see delete
*/
@RequiresApi(Build.VERSION_CODES.R)
fun ContentResolver.trash(activity: Activity, vararg uri: Uri): Int {
val deleteRequest = MediaStore.createTrashRequest(this, uri.toList(), true).intentSender
activity.startIntentSenderForResult(deleteRequest, 100, null, 0, 0, 0)
return -2 // dialog is about to be shown.
}
12 changes: 8 additions & 4 deletions app/src/main/java/com/prime/media/core/playback/Playback.kt
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,13 @@ private val PREF_KEY_ORDERS = stringPreferenceKey(
}
}
)
private val PREF_KEY_RECENT_PLAYLIST_LIMIT =
intPreferenceKey( "_max_recent_size", defaultValue = 50)

/**
* A simple extension fun that adds items to recent.
*/
private suspend fun Playlists.addToRecent(item: MediaItem) {
private suspend fun Playlists.addToRecent(item: MediaItem, limit: Long) {

val playlistId =
get(PLAYLIST_RECENT)?.id ?: insert(Playlist(name = PLAYLIST_RECENT))
Expand All @@ -114,8 +116,6 @@ private suspend fun Playlists.addToRecent(item: MediaItem) {
}

else -> {
// check the limit in this case
val limit = 200L
// delete above member
delete(playlistId, limit)
insert(Member(item, playlistId, 0))
Expand Down Expand Up @@ -167,6 +167,9 @@ class Playback : MediaLibraryService(), Callback, Player.Listener {
* The root of the playing queue
*/
const val ROOT_QUEUE = com.prime.media.core.playback.ROOT_QUEUE

@JvmField
val PREF_KEY_RECENT_PLAYLIST_LIMIT = com.prime.media.core.playback.PREF_KEY_RECENT_PLAYLIST_LIMIT
}

/**
Expand Down Expand Up @@ -293,7 +296,8 @@ class Playback : MediaLibraryService(), Callback, Player.Listener {
// save current index in preference
preferences[PREF_KEY_INDEX] = player.currentMediaItemIndex
if (mediaItem != null) {
GlobalScope.launch { playlists.addToRecent(mediaItem) }
val limit = preferences.value(PREF_KEY_RECENT_PLAYLIST_LIMIT)
GlobalScope.launch { playlists.addToRecent(mediaItem, limit.toLong()) }
session.notifyChildrenChanged(ROOT_QUEUE, 0, null)
}
}
Expand Down
15 changes: 8 additions & 7 deletions app/src/main/java/com/prime/media/directory/store/Audios.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import com.prime.media.core.db.albumUri
import com.prime.media.core.db.key
import com.prime.media.core.db.toMediaItem
import com.prime.media.core.util.toMember
import com.prime.media.settings.Settings
import com.primex.core.*
import com.primex.material2.*
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -70,6 +71,8 @@ import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.random.Random.Default.nextInt
import com.primex.core.Text
import com.primex.preferences.Preferences
import com.primex.preferences.value

private const val TAG = "AudiosViewModel"

Expand All @@ -84,6 +87,7 @@ class AudiosViewModel @Inject constructor(
private val repository: Repository,
private val toaster: Channel,
private val remote: Remote,
private val preferences: Preferences
) : DirectoryViewModel<Audio>(handle) {

companion object {
Expand Down Expand Up @@ -420,10 +424,6 @@ class AudiosViewModel @Inject constructor(
viewModelScope.launch {
val focused = focused.toLongOrNull() ?: return@launch
val res = repository.toggleFav(focused)
toaster.show(
if (res) "Added to favourite" else "Removed from favourite",
"Favourite"
)
}
}

Expand Down Expand Up @@ -502,10 +502,11 @@ class AudiosViewModel @Inject constructor(
}
// consume selected
clear()
val isTrashEnabled = preferences.value(Settings.TRASH_CAN_ENABLED)
val res = show(
buildTextResource(R.string.delete_files_warning_msg, list.size),
buildTextResource(if (isTrashEnabled) R.string.trash_files_warning_msg else R.string.delete_files_warning_msg, list.size),
buildTextResource(R.string.alert),
buildTextResource(R.string.delete),
buildTextResource(if (isTrashEnabled) R.string.trash else R.string.delete),
Icons.Outlined.WarningAmber,
accent = Color.Rose,
Channel.Duration.Long
Expand All @@ -522,7 +523,7 @@ class AudiosViewModel @Inject constructor(
it.toLongOrNull() ?: 0
)
}
val result = repository.delete(activity, *uris.toTypedArray())
val result = repository.delete(activity, *uris.toTypedArray(), trash = isTrashEnabled)
// handle error code
// show appropriate message
when (result) {
Expand Down
Loading

0 comments on commit 3b72a20

Please sign in to comment.