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
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity
android:name=".ui.extensions.export.PickerActivity"
android:exported="true" />

<provider
android:name="androidx.core.content.FileProvider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import android.os.Bundle
import android.view.View
import androidx.core.net.toUri
import androidx.core.view.iterator
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.viewModelScope
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceGroup
import androidx.preference.SwitchPreferenceCompat
import dev.brahmkshatriya.echo.R
import dev.brahmkshatriya.echo.common.Extension
import dev.brahmkshatriya.echo.common.clients.ExtensionClient
import dev.brahmkshatriya.echo.common.clients.PlaylistEditClient
import dev.brahmkshatriya.echo.common.models.ExtensionType
import dev.brahmkshatriya.echo.common.models.ImageHolder
import dev.brahmkshatriya.echo.common.models.ImportType
Expand All @@ -28,18 +32,24 @@ import dev.brahmkshatriya.echo.common.settings.SettingSwitch
import dev.brahmkshatriya.echo.common.settings.SettingTextInput
import dev.brahmkshatriya.echo.extensions.ExtensionUtils.extensionPrefId
import dev.brahmkshatriya.echo.extensions.ExtensionUtils.toSettings
import dev.brahmkshatriya.echo.extensions.builtin.unified.UnifiedExtension
import dev.brahmkshatriya.echo.playback.PlayerService.Companion.STREAM_QUALITY
import dev.brahmkshatriya.echo.playback.PlayerService.Companion.streamQualities
import dev.brahmkshatriya.echo.ui.extensions.export.ExportPlaylistUtils.Companion.deserializePlaylists
import dev.brahmkshatriya.echo.ui.extensions.export.ExportPlaylistUtils.Companion.serializePlaylists
import dev.brahmkshatriya.echo.ui.extensions.export.PickerActivity
import dev.brahmkshatriya.echo.ui.settings.BaseSettingsFragment
import dev.brahmkshatriya.echo.utils.ContextUtils.observe
import dev.brahmkshatriya.echo.utils.Serializer.getSerialized
import dev.brahmkshatriya.echo.utils.Serializer.putSerialized
import dev.brahmkshatriya.echo.utils.ui.prefs.ClickPreference
import dev.brahmkshatriya.echo.utils.ui.prefs.LoadingPreference
import dev.brahmkshatriya.echo.utils.ui.prefs.MaterialListPreference
import dev.brahmkshatriya.echo.utils.ui.prefs.MaterialMultipleChoicePreference
import dev.brahmkshatriya.echo.utils.ui.prefs.MaterialSliderPreference
import dev.brahmkshatriya.echo.utils.ui.prefs.MaterialTextInputPreference
import dev.brahmkshatriya.echo.utils.ui.prefs.TransitionPreference
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.activityViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
Expand Down Expand Up @@ -154,6 +164,32 @@ class ExtensionInfoFragment : BaseSettingsFragment() {
this@ExtensionPreference, extension, isLoginClient
)
screen.addPreference(infoPreference)

val client = extension.instance.value().getOrThrow()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove, client is not needed here

if (extension.id == UnifiedExtension.metadata.id) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why limit it to unified extension?

ClickPreference(context).apply {
key = "export_playlists"
title = getString(R.string.export_playlists)
summary = getString(R.string.export_playlists_summary)
layoutResource = R.layout.preference
isIconSpaceReserved = false
setOnClickListener {
exportPlaylists(client)
}
screen.addPreference(this)
}
ClickPreference(context).apply {
key = "import_playlists"
title = getString(R.string.import_playlists)
summary = getString(R.string.import_playlists_summary)
layoutResource = R.layout.preference
isIconSpaceReserved = false
setOnClickListener {
importPlaylists(client)
}
screen.addPreference(this)
}
}
if (extension.type == ExtensionType.MUSIC) MaterialListPreference(context).apply {
key = STREAM_QUALITY
title = getString(R.string.stream_quality)
Expand All @@ -170,6 +206,48 @@ class ExtensionInfoFragment : BaseSettingsFragment() {
}
}

private fun exportPlaylists(client: ExtensionClient) {
val context = preferenceManager.context
if (client is PlaylistEditClient) {
PickerActivity.openFolder(context) { uri ->
val directory = DocumentFile.fromTreeUri(context, uri) ?: run {
throw Exception("Failed to get directory from URI")
}

val file = directory.createFile("application/json", "playlists") ?: run {
throw Exception("Failed to create file")
}

viewModel.viewModelScope.launch {
try {
context.contentResolver.openOutputStream(file.uri)?.use { outputStream ->
outputStream.write(client.serializePlaylists().toByteArray())
}
} catch (e: Exception) {
file.delete()
throw e
}
}
}
}
}

private fun importPlaylists(client: ExtensionClient) {
val context = preferenceManager.context
if (client is PlaylistEditClient) {
PickerActivity.openFile(context) { uri ->
context.contentResolver.openInputStream(uri)?.use { inputStream ->
inputStream.bufferedReader().use { reader ->
val text = reader.readText()
viewModel.viewModelScope.launch {
client.deserializePlaylists(text)
}
}
} ?: throw Exception("Cannot open input stream for $uri")
}
}
}

Comment on lines +209 to +250
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move all this code to the viewmodel itself, and instead of passing the client, use the extension from extensionFlow

private fun Setting.addPreferenceTo(preferenceGroup: PreferenceGroup) {
val context = preferenceGroup.context
when (this) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package dev.brahmkshatriya.echo.ui.extensions.export

import dev.brahmkshatriya.echo.common.clients.PlaylistEditClient
import dev.brahmkshatriya.echo.common.models.Track
import dev.brahmkshatriya.echo.common.models.Feed.Companion.loadAll
import dev.brahmkshatriya.echo.utils.Serializer.toData
import dev.brahmkshatriya.echo.utils.Serializer.toJson
import kotlinx.serialization.Serializable

@Serializable
data class ExportPlaylist(
val title: String,
val description: String? = null,
val tracks: List<Track>,
)

class ExportPlaylistUtils {
companion object {
suspend fun PlaylistEditClient.serializePlaylists(): String {
val playlists = this.listEditablePlaylists(null)
val exportPlaylists = playlists.map { ExportPlaylist(
it.first.title,
it.first.description,
this.loadTracks(it.first).loadAll(),
) }
return exportPlaylists.toJson()
}

suspend fun PlaylistEditClient.deserializePlaylists(playlistJson: String) {
val exportPlaylists = playlistJson.toData<List<ExportPlaylist>>()
exportPlaylists.forEach {
val playlist = this.createPlaylist(it.title, it.description)
this.addTracksToPlaylist(playlist, emptyList(), 0, it.tracks)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package dev.brahmkshatriya.echo.ui.extensions.export

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity

class PickerActivity : AppCompatActivity() {
sealed interface PickerType {
object Folder : PickerType
object File : PickerType
}
Comment on lines +11 to +14
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use an enum class instead


interface Callback {
fun onSelected(uri: Uri)
fun onCancelled() {}
}

companion object {
private var callback: Callback? = null
private var pickerType: PickerType? = null

fun openFolder(context: Context, callback: Callback) {
open(context, PickerType.Folder, callback)
}

fun openFolder(context: Context, onSelected: (Uri) -> Unit) {
openFolder(context, object : Callback {
override fun onSelected(uri: Uri) = onSelected(uri)
})
}

fun openFile(context: Context, callback: Callback) {
open(context, PickerType.File, callback)
}

fun openFile(context: Context, onSelected: (Uri) -> Unit) {
openFile(context, object : Callback {
override fun onSelected(uri: Uri) = onSelected(uri)
})
}

private fun open(context: Context, pickerType: PickerType, callback: Callback) {
this.callback = callback
this.pickerType = pickerType
Comment on lines +46 to +47
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should put the picker type data inside a Bundle,
and the callback should removed

Intent(context, PickerActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(this)
}
}
}

private val pickerLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
try {
when {
result.resultCode == RESULT_OK && result.data?.data != null -> {
callback?.onSelected(result.data!!.data!!)
}
else -> callback?.onCancelled()
}
} finally {
callback = null
pickerType = null
finish()
}
}
Comment on lines +55 to +70
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be created inside open function, and use runCatching instead of try, catch, finally

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to put the code in open function but AI gives me the following warning
registerForActivityResult must be called during the component's initialization phase (before or during onCreate), not from a static context like the companion object. The current implementation will likely crash because we're trying to register for activity result from a non-activity context.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try using this

fun <I, O> ComponentActivity.registerActivityResultLauncher(


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = when (pickerType) {
PickerType.Folder -> Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
PickerType.File -> Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/json"))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
null -> {
callback?.onCancelled()
finish()
return
}
}
pickerLauncher.launch(intent)
}

override fun onDestroy() {
callback?.onCancelled()
callback = null
pickerType = null
super.onDestroy()
}
Comment on lines +93 to +98
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wont be required after the pickerLaucher changes

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.brahmkshatriya.echo.utils.ui.prefs

import android.content.Context
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder

open class ClickPreference(context:Context) : Preference(context) {
private var onClickListener: (() -> Unit)? = null

fun setOnClickListener(listener: () -> Unit) {
onClickListener = listener
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)

val listener = onClickListener ?: return
holder.itemView.setOnClickListener {
listener.invoke()
}
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
<string name="equalizer">Equalizer</string>
<string name="equalizer_summary">Open the device equalizer settings</string>

<string name="export_playlists">Export Playlists</string>
<string name="export_playlists_summary">Export playlists to external file</string>

<string name="import_playlists">Import Playlists</string>
<string name="import_playlists_summary">Import playlists from external file</string>

<string name="stream_quality">Stream Quality</string>
<string name="stream_quality_summary">Select the default quality of the stream</string>
<string name="x_specific_quality_summary">%1$s specific stream quality, will override the default stream quality selection</string>
Expand Down