diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 251cf9bd6..ad38281f1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -94,6 +94,9 @@ + + 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") + } + } + } + private fun Setting.addPreferenceTo(preferenceGroup: PreferenceGroup) { val context = preferenceGroup.context when (this) { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extensions/export/ExportPlaylist.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extensions/export/ExportPlaylist.kt new file mode 100644 index 000000000..2626eb60d --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extensions/export/ExportPlaylist.kt @@ -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, +) + +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>() + exportPlaylists.forEach { + val playlist = this.createPlaylist(it.title, it.description) + this.addTracksToPlaylist(playlist, emptyList(), 0, it.tracks) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extensions/export/PickerActivity.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extensions/export/PickerActivity.kt new file mode 100644 index 000000000..4533dc52b --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extensions/export/PickerActivity.kt @@ -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 + } + + 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 + 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() + } + } + + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/ui/prefs/ClickPreference.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/ui/prefs/ClickPreference.kt new file mode 100644 index 000000000..ded88e6b8 --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/utils/ui/prefs/ClickPreference.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d3b77417..712386c2b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,6 +100,12 @@ Equalizer Open the device equalizer settings + Export Playlists + Export playlists to external file + + Import Playlists + Import playlists from external file + Stream Quality Select the default quality of the stream %1$s specific stream quality, will override the default stream quality selection