-
-
Notifications
You must be signed in to change notification settings - Fork 151
Basic functionality for export/import playlists #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -154,6 +164,32 @@ class ExtensionInfoFragment : BaseSettingsFragment() { | |
| this@ExtensionPreference, extension, isLoginClient | ||
| ) | ||
| screen.addPreference(infoPreference) | ||
|
|
||
| val client = extension.instance.value().getOrThrow() | ||
| if (extension.id == UnifiedExtension.metadata.id) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
@@ -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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
|
||
| 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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you should put the picker type data inside a Bundle, |
||||
| 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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be created inside
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to put the code in
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Try using this
|
||||
|
|
||||
| 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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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