From 8ce809d30f4b90ebbf0ed08467bbfaeaccc0f847 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Thu, 1 Dec 2022 13:48:08 +0100 Subject: [PATCH 1/6] Add import export settings --- .../github/libretube/obj/ImportPlaylist.kt | 8 ++ .../libretube/obj/ImportPlaylistFile.kt | 7 ++ .../ui/base/BasePreferenceFragment.kt | 7 ++ .../ui/preferences/AdvancedSettings.kt | 52 ---------- .../ui/preferences/BackupRestoreSettings.kt | 98 +++++++++++++++++++ .../ui/preferences/InstanceSettings.kt | 47 --------- .../libretube/ui/preferences/MainSettings.kt | 44 +++------ app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/advanced_settings.xml | 14 --- .../main/res/xml/import_export_settings.xml | 49 ++++++++++ app/src/main/res/xml/settings.xml | 6 ++ 11 files changed, 195 insertions(+), 141 deletions(-) create mode 100644 app/src/main/java/com/github/libretube/obj/ImportPlaylist.kt create mode 100644 app/src/main/java/com/github/libretube/obj/ImportPlaylistFile.kt create mode 100644 app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt create mode 100644 app/src/main/res/xml/import_export_settings.xml diff --git a/app/src/main/java/com/github/libretube/obj/ImportPlaylist.kt b/app/src/main/java/com/github/libretube/obj/ImportPlaylist.kt new file mode 100644 index 000000000..fc25c8789 --- /dev/null +++ b/app/src/main/java/com/github/libretube/obj/ImportPlaylist.kt @@ -0,0 +1,8 @@ +package com.github.libretube.obj + +data class ImportPlaylist( + val name: String? = null, + val type: String? = null, + val visibility: String? = null, + val videos: List = listOf() +) diff --git a/app/src/main/java/com/github/libretube/obj/ImportPlaylistFile.kt b/app/src/main/java/com/github/libretube/obj/ImportPlaylistFile.kt new file mode 100644 index 000000000..43f9ff4a0 --- /dev/null +++ b/app/src/main/java/com/github/libretube/obj/ImportPlaylistFile.kt @@ -0,0 +1,7 @@ +package com.github.libretube.obj + +data class ImportPlaylistFile( + val format: String? = null, + val version: Int? = null, + val playlists: List? = null +) diff --git a/app/src/main/java/com/github/libretube/ui/base/BasePreferenceFragment.kt b/app/src/main/java/com/github/libretube/ui/base/BasePreferenceFragment.kt index 6ac596c4d..fe2eb9a39 100644 --- a/app/src/main/java/com/github/libretube/ui/base/BasePreferenceFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/base/BasePreferenceFragment.kt @@ -1,6 +1,7 @@ package com.github.libretube.ui.base import android.os.Bundle +import androidx.fragment.app.Fragment import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.Preference @@ -70,4 +71,10 @@ open class BasePreferenceFragment : PreferenceFragmentCompat() { else -> super.onDisplayPreferenceDialog(preference) } } + + fun Fragment?.runOnUiThread(action: () -> Unit) { + this ?: return + if (!isAdded) return // Fragment not attached to an Activity + activity?.runOnUiThread(action) + } } diff --git a/app/src/main/java/com/github/libretube/ui/preferences/AdvancedSettings.kt b/app/src/main/java/com/github/libretube/ui/preferences/AdvancedSettings.kt index 6835dd7f5..e1ef6be6a 100644 --- a/app/src/main/java/com/github/libretube/ui/preferences/AdvancedSettings.kt +++ b/app/src/main/java/com/github/libretube/ui/preferences/AdvancedSettings.kt @@ -1,49 +1,18 @@ package com.github.libretube.ui.preferences -import android.net.Uri import android.os.Bundle -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.preference.ListPreference import androidx.preference.Preference import com.github.libretube.R import com.github.libretube.constants.PreferenceKeys -import com.github.libretube.obj.BackupFile import com.github.libretube.ui.activities.SettingsActivity import com.github.libretube.ui.base.BasePreferenceFragment -import com.github.libretube.ui.dialogs.BackupDialog -import com.github.libretube.util.BackupHelper import com.github.libretube.util.ImageHelper import com.github.libretube.util.PreferenceHelper import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.time.LocalDate -import java.time.LocalTime class AdvancedSettings : BasePreferenceFragment() { - // backup and restore database - private lateinit var getBackupFile: ActivityResultLauncher - private lateinit var createBackupFile: ActivityResultLauncher - private var backupFile = BackupFile() - - override fun onCreate(savedInstanceState: Bundle?) { - getBackupFile = - registerForActivityResult( - ActivityResultContracts.GetContent() - ) { uri: Uri? -> - BackupHelper(requireContext()).restoreAdvancedBackup(uri) - } - - createBackupFile = registerForActivityResult( - CreateDocument("application/json") - ) { uri: Uri? -> - BackupHelper(requireContext()).advancedBackup(uri, backupFile) - } - - super.onCreate(savedInstanceState) - } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.advanced_settings, rootKey) @@ -61,22 +30,6 @@ class AdvancedSettings : BasePreferenceFragment() { showResetDialog() true } - - val advancesBackup = findPreference("backup") - advancesBackup?.setOnPreferenceClickListener { - BackupDialog { - backupFile = it - createBackupFile.launch(getBackupFileName()) - } - .show(childFragmentManager, null) - true - } - - val restoreAdvancedBackup = findPreference("restore") - restoreAdvancedBackup?.setOnPreferenceClickListener { - getBackupFile.launch("application/json") - true - } } private fun showResetDialog() { @@ -95,9 +48,4 @@ class AdvancedSettings : BasePreferenceFragment() { } .show() } - - private fun getBackupFileName(): String { - val time = LocalTime.now().toString().split(".").firstOrNull() - return "libretube-backup-${LocalDate.now()}-$time.json" - } } diff --git a/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt new file mode 100644 index 000000000..04d115e91 --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt @@ -0,0 +1,98 @@ +package com.github.libretube.ui.preferences + +import android.net.Uri +import android.os.Bundle +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument +import androidx.preference.Preference +import com.github.libretube.R +import com.github.libretube.constants.PreferenceKeys +import com.github.libretube.obj.BackupFile +import com.github.libretube.ui.base.BasePreferenceFragment +import com.github.libretube.ui.dialogs.BackupDialog +import com.github.libretube.util.BackupHelper +import com.github.libretube.util.ImportHelper +import java.time.LocalDate +import java.time.LocalTime + +class BackupRestoreSettings : BasePreferenceFragment() { + + // backup and restore database + private lateinit var getBackupFile: ActivityResultLauncher + private lateinit var createBackupFile: ActivityResultLauncher + private var backupFile = BackupFile() + + /** + * result listeners for importing and exporting subscriptions + */ + private lateinit var getSubscriptionsFile: ActivityResultLauncher + private lateinit var createSubscriptionsFile: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + getSubscriptionsFile = + registerForActivityResult( + ActivityResultContracts.GetContent() + ) { uri: Uri? -> + ImportHelper(requireActivity()).importSubscriptions(uri) + } + createSubscriptionsFile = registerForActivityResult( + CreateDocument("application/json") + ) { uri: Uri? -> + ImportHelper(requireActivity()).exportSubscriptions(uri) + } + + getBackupFile = + registerForActivityResult( + ActivityResultContracts.GetContent() + ) { uri: Uri? -> + BackupHelper(requireContext()).restoreAdvancedBackup(uri) + } + + createBackupFile = registerForActivityResult( + CreateDocument("application/json") + ) { uri: Uri? -> + BackupHelper(requireContext()).advancedBackup(uri, backupFile) + } + + super.onCreate(savedInstanceState) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.import_export_settings, rootKey) + + val importSubscriptions = findPreference(PreferenceKeys.IMPORT_SUBS) + importSubscriptions?.setOnPreferenceClickListener { + // check StorageAccess + getSubscriptionsFile.launch("*/*") + true + } + + val exportSubscriptions = findPreference(PreferenceKeys.EXPORT_SUBS) + exportSubscriptions?.setOnPreferenceClickListener { + createSubscriptionsFile.launch("subscriptions.json") + true + } + + val advancesBackup = findPreference("backup") + advancesBackup?.setOnPreferenceClickListener { + BackupDialog { + backupFile = it + createBackupFile.launch(getBackupFileName()) + } + .show(childFragmentManager, null) + true + } + + val restoreAdvancedBackup = findPreference("restore") + restoreAdvancedBackup?.setOnPreferenceClickListener { + getBackupFile.launch("application/json") + true + } + } + + private fun getBackupFileName(): String { + val time = LocalTime.now().toString().split(".").firstOrNull() + return "libretube-backup-${LocalDate.now()}-$time.json" + } +} diff --git a/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt b/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt index 0c34906fb..211b7e177 100644 --- a/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt +++ b/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt @@ -1,12 +1,7 @@ package com.github.libretube.ui.preferences -import android.net.Uri import android.os.Bundle import android.widget.Toast -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.result.contract.ActivityResultContracts.CreateDocument -import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.preference.ListPreference import androidx.preference.Preference @@ -22,33 +17,10 @@ import com.github.libretube.ui.dialogs.CustomInstanceDialog import com.github.libretube.ui.dialogs.DeleteAccountDialog import com.github.libretube.ui.dialogs.LoginDialog import com.github.libretube.ui.dialogs.LogoutDialog -import com.github.libretube.util.ImportHelper import com.github.libretube.util.PreferenceHelper class InstanceSettings : BasePreferenceFragment() { - /** - * result listeners for importing and exporting subscriptions - */ - private lateinit var getContent: ActivityResultLauncher - private lateinit var createFile: ActivityResultLauncher - - override fun onCreate(savedInstanceState: Bundle?) { - getContent = - registerForActivityResult( - ActivityResultContracts.GetContent() - ) { uri: Uri? -> - ImportHelper(requireActivity()).importSubscriptions(uri) - } - createFile = registerForActivityResult( - CreateDocument("application/json") - ) { uri: Uri? -> - ImportHelper(requireActivity()).exportSubscriptions(uri) - } - - super.onCreate(savedInstanceState) - } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.instance_settings, rootKey) @@ -138,19 +110,6 @@ class InstanceSettings : BasePreferenceFragment() { newFragment.show(childFragmentManager, DeleteAccountDialog::class.java.name) true } - - val importSubscriptions = findPreference(PreferenceKeys.IMPORT_SUBS) - importSubscriptions?.setOnPreferenceClickListener { - // check StorageAccess - getContent.launch("*/*") - true - } - - val exportSubscriptions = findPreference(PreferenceKeys.EXPORT_SUBS) - exportSubscriptions?.setOnPreferenceClickListener { - createFile.launch("subscriptions.json") - true - } } private fun initCustomInstances(instancePref: ListPreference) { @@ -201,10 +160,4 @@ class InstanceSettings : BasePreferenceFragment() { PreferenceHelper.setToken("") Toast.makeText(context, getString(R.string.loggedout), Toast.LENGTH_SHORT).show() } - - private fun Fragment?.runOnUiThread(action: () -> Unit) { - this ?: return - if (!isAdded) return // Fragment not attached to an Activity - activity?.runOnUiThread(action) - } } diff --git a/app/src/main/java/com/github/libretube/ui/preferences/MainSettings.kt b/app/src/main/java/com/github/libretube/ui/preferences/MainSettings.kt index f67495781..c87ca340b 100644 --- a/app/src/main/java/com/github/libretube/ui/preferences/MainSettings.kt +++ b/app/src/main/java/com/github/libretube/ui/preferences/MainSettings.kt @@ -23,65 +23,52 @@ class MainSettings : BasePreferenceFragment() { val general = findPreference("general") general?.setOnPreferenceClickListener { - val newFragment = GeneralSettings() - navigateToSettingsFragment(newFragment) - true + navigateToSettingsFragment(GeneralSettings()) } val instance = findPreference("instance") instance?.setOnPreferenceClickListener { - val newFragment = InstanceSettings() - navigateToSettingsFragment(newFragment) - true + navigateToSettingsFragment(InstanceSettings()) } val appearance = findPreference("appearance") appearance?.setOnPreferenceClickListener { - val newFragment = AppearanceSettings() - navigateToSettingsFragment(newFragment) - true + navigateToSettingsFragment(AppearanceSettings()) } val sponsorBlock = findPreference("sponsorblock") sponsorBlock?.setOnPreferenceClickListener { - val newFragment = SponsorBlockSettings() - navigateToSettingsFragment(newFragment) - true + navigateToSettingsFragment(SponsorBlockSettings()) } val player = findPreference("player") player?.setOnPreferenceClickListener { - val newFragment = PlayerSettings() - navigateToSettingsFragment(newFragment) - true + navigateToSettingsFragment(PlayerSettings()) } val audioVideo = findPreference("audio_video") audioVideo?.setOnPreferenceClickListener { - val newFragment = AudioVideoSettings() - navigateToSettingsFragment(newFragment) - true + navigateToSettingsFragment(AudioVideoSettings()) } val history = findPreference("history") history?.setOnPreferenceClickListener { - val newFragment = HistorySettings() - navigateToSettingsFragment(newFragment) - true + navigateToSettingsFragment(HistorySettings()) } val notifications = findPreference("notifications") notifications?.setOnPreferenceClickListener { - val newFragment = NotificationSettings() - navigateToSettingsFragment(newFragment) - true + navigateToSettingsFragment(NotificationSettings()) + } + + val backupRestore = findPreference("backup_restore") + backupRestore?.setOnPreferenceClickListener { + navigateToSettingsFragment(BackupRestoreSettings()) } val advanced = findPreference("advanced") advanced?.setOnPreferenceClickListener { - val newFragment = AdvancedSettings() - navigateToSettingsFragment(newFragment) - true + navigateToSettingsFragment(AdvancedSettings()) } val update = findPreference("update") @@ -131,9 +118,10 @@ class MainSettings : BasePreferenceFragment() { } } - private fun navigateToSettingsFragment(newFragment: Fragment) { + private fun navigateToSettingsFragment(newFragment: Fragment): Boolean { parentFragmentManager.beginTransaction() .replace(R.id.settings, newFragment) .commitNow() + return true } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a229cf42b..0d835fa5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -403,6 +403,10 @@ Tap twice at the left or right to rewind or forward the player position. You\'re all caught up You\'ve seen all new videos + Import playlists + Export playlists + App Backup + Import & export subscriptions, playlists, ... Download Service diff --git a/app/src/main/res/xml/advanced_settings.xml b/app/src/main/res/xml/advanced_settings.xml index 04a6529a9..c9a298603 100644 --- a/app/src/main/res/xml/advanced_settings.xml +++ b/app/src/main/res/xml/advanced_settings.xml @@ -39,20 +39,6 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 5a0b51e07..d6edb9183 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -50,6 +50,12 @@ app:key="notifications" app:title="@string/notifications" /> + + Date: Thu, 1 Dec 2022 14:09:57 +0100 Subject: [PATCH 2/6] Add UI functionality for importing and exporting playlists --- .../github/libretube/api/PlaylistsHelper.kt | 9 ++ .../libretube/constants/PreferenceKeys.kt | 2 - .../ui/preferences/BackupRestoreSettings.kt | 38 +++++- .../com/github/libretube/util/ImportHelper.kt | 120 ++++++++++++------ app/src/main/res/values/strings.xml | 3 +- .../main/res/xml/import_export_settings.xml | 8 +- 6 files changed, 127 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt index 8b8beff9a..48d41f693 100644 --- a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt +++ b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt @@ -14,6 +14,7 @@ import com.github.libretube.extensions.awaitQuery import com.github.libretube.extensions.toLocalPlaylistItem import com.github.libretube.extensions.toStreamItem import com.github.libretube.extensions.toastFromMainThread +import com.github.libretube.obj.ImportPlaylist import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.ProxyHelper import retrofit2.HttpException @@ -175,6 +176,14 @@ object PlaylistsHelper { ) } + suspend fun importPlaylists(playlist: List) { + + } + + suspend fun exportPlaylists(): List { + + } + fun getPrivateType(): PlaylistType { return if (loggedIn()) PlaylistType.PRIVATE else PlaylistType.LOCAL } diff --git a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt index b12ddee17..8836fec16 100644 --- a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt +++ b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt @@ -49,8 +49,6 @@ object PreferenceKeys { const val CLEAR_CUSTOM_INSTANCES = "clearCustomInstances" const val LOGIN_REGISTER = "login_register" const val DELETE_ACCOUNT = "delete_account" - const val IMPORT_SUBS = "import_from_yt" - const val EXPORT_SUBS = "export_subs" /** * Player diff --git a/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt index 04d115e91..5dee1296e 100644 --- a/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt +++ b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt @@ -7,7 +7,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.preference.Preference import com.github.libretube.R -import com.github.libretube.constants.PreferenceKeys import com.github.libretube.obj.BackupFile import com.github.libretube.ui.base.BasePreferenceFragment import com.github.libretube.ui.dialogs.BackupDialog @@ -29,19 +28,35 @@ class BackupRestoreSettings : BasePreferenceFragment() { private lateinit var getSubscriptionsFile: ActivityResultLauncher private lateinit var createSubscriptionsFile: ActivityResultLauncher + /** + * result listeners for importing and exporting playlists + */ + private lateinit var getPlaylistsFile: ActivityResultLauncher + private lateinit var createPlaylistsFile: ActivityResultLauncher + override fun onCreate(savedInstanceState: Bundle?) { getSubscriptionsFile = registerForActivityResult( ActivityResultContracts.GetContent() - ) { uri: Uri? -> + ) { uri -> ImportHelper(requireActivity()).importSubscriptions(uri) } createSubscriptionsFile = registerForActivityResult( CreateDocument("application/json") - ) { uri: Uri? -> + ) { uri -> ImportHelper(requireActivity()).exportSubscriptions(uri) } + getPlaylistsFile = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + ImportHelper(requireActivity()).importPlaylists(uri) + } + + createPlaylistsFile = registerForActivityResult( + CreateDocument("application/json") + ) { uri -> + ImportHelper(requireActivity()).exportPlaylists(uri) + } + getBackupFile = registerForActivityResult( ActivityResultContracts.GetContent() @@ -61,19 +76,30 @@ class BackupRestoreSettings : BasePreferenceFragment() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.import_export_settings, rootKey) - val importSubscriptions = findPreference(PreferenceKeys.IMPORT_SUBS) + val importSubscriptions = findPreference("import_subscriptions") importSubscriptions?.setOnPreferenceClickListener { - // check StorageAccess getSubscriptionsFile.launch("*/*") true } - val exportSubscriptions = findPreference(PreferenceKeys.EXPORT_SUBS) + val exportSubscriptions = findPreference("export_subscriptions") exportSubscriptions?.setOnPreferenceClickListener { createSubscriptionsFile.launch("subscriptions.json") true } + val importPlaylists = findPreference("import_playlists") + importPlaylists?.setOnPreferenceClickListener { + getPlaylistsFile.launch("*/*") + true + } + + val exportPlaylists = findPreference("export_playlists") + exportPlaylists?.setOnPreferenceClickListener { + createPlaylistsFile.launch("subscriptions.json") + true + } + val advancesBackup = findPreference("backup") advancesBackup?.setOnPreferenceClickListener { BackupDialog { diff --git a/app/src/main/java/com/github/libretube/util/ImportHelper.kt b/app/src/main/java/com/github/libretube/util/ImportHelper.kt index 525bb95dd..562ae031a 100644 --- a/app/src/main/java/com/github/libretube/util/ImportHelper.kt +++ b/app/src/main/java/com/github/libretube/util/ImportHelper.kt @@ -6,10 +6,12 @@ import android.util.Log import android.widget.Toast import com.fasterxml.jackson.databind.ObjectMapper import com.github.libretube.R +import com.github.libretube.api.PlaylistsHelper import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.SubscriptionHelper import com.github.libretube.extensions.TAG import com.github.libretube.extensions.toastFromMainThread +import com.github.libretube.obj.ImportPlaylistFile import com.github.libretube.obj.NewPipeSubscription import com.github.libretube.obj.NewPipeSubscriptions import kotlinx.coroutines.CoroutineScope @@ -36,12 +38,10 @@ class ImportHelper( } } catch (e: IllegalArgumentException) { Log.e(TAG(), e.toString()) - Toast.makeText( - activity, + activity.toastFromMainThread( activity.getString(R.string.unsupported_file_format) + - " (${activity.contentResolver.getType(uri)}", - Toast.LENGTH_SHORT - ).show() + " (${activity.contentResolver.getType(uri)}" + ) } catch (e: Exception) { Log.e(TAG(), e.toString()) Toast.makeText(activity, e.localizedMessage, Toast.LENGTH_SHORT).show() @@ -55,12 +55,7 @@ class ImportHelper( return when (val fileType = activity.contentResolver.getType(uri)) { "application/json", "application/octet-stream" -> { // NewPipe subscriptions format - val mapper = ObjectMapper() - val json = activity.contentResolver.openInputStream(uri)?.use { - it.bufferedReader().use { reader -> reader.readText() } - }.orEmpty() - - val subscriptions = mapper.readValue(json, NewPipeSubscriptions::class.java) + val subscriptions = ObjectMapper().readValue(uri.readText(), NewPipeSubscriptions::class.java) subscriptions.subscriptions.orEmpty().map { it.url!!.replace("https://www.youtube.com/channel/", "") } @@ -84,40 +79,85 @@ class ImportHelper( */ fun exportSubscriptions(uri: Uri?) { if (uri == null) return - try { - val mapper = ObjectMapper() - val token = PreferenceHelper.getToken() - runBlocking { - val subs = if (token != "") { - RetrofitInstance.authApi.subscriptions(token) - } else { - RetrofitInstance.authApi.unauthenticatedSubscriptions( - SubscriptionHelper.getFormattedLocalSubscriptions() - ) - } - val newPipeChannels = mutableListOf() - subs.forEach { - newPipeChannels += NewPipeSubscription( - name = it.name, - service_id = 0, - url = "https://www.youtube.com" + it.url - ) - } - - val newPipeSubscriptions = NewPipeSubscriptions( - subscriptions = newPipeChannels + runBlocking { + val subs = if (PreferenceHelper.getToken() != "") { + RetrofitInstance.authApi.subscriptions(PreferenceHelper.getToken()) + } else { + RetrofitInstance.authApi.unauthenticatedSubscriptions( + SubscriptionHelper.getFormattedLocalSubscriptions() ) + } + val newPipeChannels = mutableListOf() + subs.forEach { + newPipeChannels += NewPipeSubscription( + name = it.name, + service_id = 0, + url = "https://www.youtube.com" + it.url + ) + } - val data = mapper.writeValueAsBytes(newPipeSubscriptions) + val newPipeSubscriptions = NewPipeSubscriptions( + subscriptions = newPipeChannels + ) - activity.contentResolver.openFileDescriptor(uri, "w")?.use { - FileOutputStream(it.fileDescriptor).use { fileOutputStream -> - fileOutputStream.write(data) - } + uri.write(newPipeSubscriptions) + + activity.toastFromMainThread(R.string.exportsuccess) + } + } + + /** + * Import Playlists + */ + fun importPlaylists(uri: Uri?) { + if (uri == null) return + + val playlistFile = ObjectMapper().readValue(uri.readText(), ImportPlaylistFile::class.java) + + playlistFile.playlists.orEmpty().forEach { + CoroutineScope(Dispatchers.IO).launch { + playlistFile.playlists?.let { + PlaylistsHelper.importPlaylists(it) } } - } catch (e: Exception) { - e.printStackTrace() + } + + activity.toastFromMainThread(R.string.importsuccess) + } + + /** + * Export Playlists + */ + fun exportPlaylists(uri: Uri?) { + if (uri == null) return + + runBlocking { + val playlists = PlaylistsHelper.exportPlaylists() + val playlistFile = ImportPlaylistFile( + format = "Piped", + version = 1, + playlists = playlists + ) + + uri.write(playlistFile) + + activity.toastFromMainThread(R.string.exportsuccess) + } + } + + private fun Uri.readText(): String { + return activity.contentResolver.openInputStream(this)?.use { + it.bufferedReader().use { reader -> reader.readText() } + }.orEmpty() + } + + private fun Uri.write(text: Any) { + activity.contentResolver.openFileDescriptor(this, "w")?.use { + FileOutputStream(it.fileDescriptor).use { fileOutputStream -> + fileOutputStream.write( + ObjectMapper().writeValueAsBytes(text) + ) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d835fa5b..bab5c6600 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -406,7 +406,8 @@ Import playlists Export playlists App Backup - Import & export subscriptions, playlists, ... + Import & export subscriptions, playlists, … + Successfully exported! Download Service diff --git a/app/src/main/res/xml/import_export_settings.xml b/app/src/main/res/xml/import_export_settings.xml index b7b5333cb..fcd65c2b1 100644 --- a/app/src/main/res/xml/import_export_settings.xml +++ b/app/src/main/res/xml/import_export_settings.xml @@ -7,12 +7,12 @@ @@ -22,12 +22,12 @@ From 5616934817397ee5fb08c0520b7fc0691eb087f8 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Thu, 1 Dec 2022 14:37:44 +0100 Subject: [PATCH 3/6] Add functionality for exporting and importing playlists --- .../github/libretube/api/PlaylistsHelper.kt | 69 +++++++++++++++---- .../libretube/ui/fragments/HomeFragment.kt | 2 +- .../libretube/ui/fragments/LibraryFragment.kt | 2 +- .../ui/fragments/PlaylistFragment.kt | 2 +- .../com/github/libretube/util/PlayingQueue.kt | 4 +- .../libretube/workers/NotificationWorker.kt | 5 +- 6 files changed, 60 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt index 48d41f693..035a01eb9 100644 --- a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt +++ b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt @@ -6,17 +6,21 @@ import com.github.libretube.R import com.github.libretube.api.obj.Playlist import com.github.libretube.api.obj.PlaylistId import com.github.libretube.api.obj.Playlists +import com.github.libretube.constants.YOUTUBE_FRONTEND_URL import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.obj.LocalPlaylist import com.github.libretube.enums.PlaylistType import com.github.libretube.extensions.TAG import com.github.libretube.extensions.awaitQuery +import com.github.libretube.extensions.toID import com.github.libretube.extensions.toLocalPlaylistItem import com.github.libretube.extensions.toStreamItem import com.github.libretube.extensions.toastFromMainThread import com.github.libretube.obj.ImportPlaylist import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.ProxyHelper +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking import retrofit2.HttpException import java.io.IOException @@ -47,9 +51,9 @@ object PlaylistsHelper { return playlists } - suspend fun getPlaylist(playlistType: PlaylistType, playlistId: String): Playlist { + suspend fun getPlaylist(playlistId: String): Playlist { // load locally stored playlists with the auth api - return when (playlistType) { + return when (getPlaylistType()) { PlaylistType.PRIVATE -> RetrofitInstance.authApi.getPlaylist(playlistId) PlaylistType.PUBLIC -> RetrofitInstance.api.getPlaylist(playlistId) PlaylistType.LOCAL -> { @@ -66,7 +70,10 @@ object PlaylistsHelper { } } - suspend fun createPlaylist(playlistName: String, appContext: Context, onSuccess: () -> Unit) { + suspend fun createPlaylist( + playlistName: String, + appContext: Context + ): String? { if (!loggedIn()) { awaitQuery { DatabaseHolder.Database.localPlaylistsDao().createPlaylist( @@ -76,8 +83,9 @@ object PlaylistsHelper { ) ) } - onSuccess.invoke() - return + return awaitQuery { + DatabaseHolder.Database.localPlaylistsDao().getAll() + }.last().playlist.id.toString() } val response = try { RetrofitInstance.authApi.createPlaylist( @@ -86,18 +94,17 @@ object PlaylistsHelper { ) } catch (e: IOException) { appContext.toastFromMainThread(R.string.unknown_error) - return + return null } catch (e: HttpException) { Log.e(TAG(), e.toString()) appContext.toastFromMainThread(R.string.server_error) - return + return null } if (response.playlistId != null) { appContext.toastFromMainThread(R.string.playlistCreated) - onSuccess.invoke() - } else { - appContext.toastFromMainThread(R.string.unknown_error) + return response.playlistId!! } + return null } suspend fun addToPlaylist(playlistId: String, videoId: String): Boolean { @@ -176,19 +183,51 @@ object PlaylistsHelper { ) } - suspend fun importPlaylists(playlist: List) { - + suspend fun importPlaylists(appContext: Context, playlists: List) { + for (playlist in playlists) { + val playlistId = createPlaylist(playlist.name!!, appContext) ?: continue + runBlocking { + val tasks = playlist.videos.map { videoId -> + async { addToPlaylist(playlistId, videoId.substringAfter("=")) } + } + tasks.forEach { + it.await() + } + } + } } suspend fun exportPlaylists(): List { - + val playlists = getPlaylists() + val importLists = mutableListOf() + runBlocking { + val tasks = playlists.map { + async { + val list = getPlaylist(it.id!!) + importLists.add( + ImportPlaylist( + name = list.name, + type = "playlist", + visibility = "private", + videos = list.relatedStreams.orEmpty().map { + YOUTUBE_FRONTEND_URL + it.url!!.toID() + } + ) + ) + } + } + tasks.forEach { + it.await() + } + } + return importLists } - fun getPrivateType(): PlaylistType { + fun getPlaylistType(): PlaylistType { return if (loggedIn()) PlaylistType.PRIVATE else PlaylistType.LOCAL } - fun getPrivateType(playlistId: String): PlaylistType { + fun getPlaylistType(playlistId: String): PlaylistType { if (playlistId.all { it.isDigit() }) return PlaylistType.LOCAL if (playlistId.matches(pipedPlaylistRegex)) return PlaylistType.PRIVATE return PlaylistType.PUBLIC diff --git a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt index 9417f44a7..f872acfeb 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt @@ -108,7 +108,7 @@ class HomeFragment : BaseFragment() { runOnUiThread { makeVisible(binding.playlistsRV, binding.playlistsTV) binding.playlistsRV.layoutManager = LinearLayoutManager(context) - binding.playlistsRV.adapter = PlaylistsAdapter(playlists.toMutableList(), PlaylistsHelper.getPrivateType()) + binding.playlistsRV.adapter = PlaylistsAdapter(playlists.toMutableList(), PlaylistsHelper.getPlaylistType()) binding.playlistsRV.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { diff --git a/app/src/main/java/com/github/libretube/ui/fragments/LibraryFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/LibraryFragment.kt index a38494316..ee8bce5ea 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/LibraryFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/LibraryFragment.kt @@ -123,7 +123,7 @@ class LibraryFragment : BaseFragment() { val playlistsAdapter = PlaylistsAdapter( playlists.toMutableList(), - PlaylistsHelper.getPrivateType() + PlaylistsHelper.getPlaylistType() ) // listen for playlists to become deleted diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlaylistFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlaylistFragment.kt index 591a873b3..3ab9b72e2 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlaylistFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlaylistFragment.kt @@ -99,7 +99,7 @@ class PlaylistFragment : BaseFragment() { binding.playlistScrollview.visibility = View.GONE lifecycleScope.launchWhenCreated { val response = try { - PlaylistsHelper.getPlaylist(playlistType, playlistId!!) + PlaylistsHelper.getPlaylist(playlistId!!) } catch (e: IOException) { println(e) Log.e(TAG(), "IOException, you might not have internet connection") diff --git a/app/src/main/java/com/github/libretube/util/PlayingQueue.kt b/app/src/main/java/com/github/libretube/util/PlayingQueue.kt index e6171bb5b..530ce68f2 100644 --- a/app/src/main/java/com/github/libretube/util/PlayingQueue.kt +++ b/app/src/main/java/com/github/libretube/util/PlayingQueue.kt @@ -110,8 +110,8 @@ object PlayingQueue { fun insertPlaylist(playlistId: String, newCurrentStream: StreamItem) { CoroutineScope(Dispatchers.IO).launch { try { - val playlistType = PlaylistsHelper.getPrivateType(playlistId) - val playlist = PlaylistsHelper.getPlaylist(playlistType, playlistId) + val playlistType = PlaylistsHelper.getPlaylistType(playlistId) + val playlist = PlaylistsHelper.getPlaylist(playlistId) add( *playlist.relatedStreams .orEmpty() diff --git a/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt b/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt index 7d5e9b801..d1fb9e742 100644 --- a/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt +++ b/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt @@ -82,12 +82,9 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) : var success = true runBlocking { - val task = async { - SubscriptionHelper.getFeed() - } // fetch the users feed val videoFeed = try { - task.await() + SubscriptionHelper.getFeed() } catch (e: Exception) { success = false return@runBlocking From b4100bd27c846059f41da9db5934cc329a1627f0 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Thu, 1 Dec 2022 14:43:19 +0100 Subject: [PATCH 4/6] fix some UI issues at the import/export page --- .../github/libretube/ui/dialogs/CreatePlaylistDialog.kt | 7 +++---- .../libretube/ui/preferences/BackupRestoreSettings.kt | 2 +- .../main/java/com/github/libretube/util/ImportHelper.kt | 2 +- app/src/main/res/xml/import_export_settings.xml | 3 +-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/CreatePlaylistDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/CreatePlaylistDialog.kt index 253925485..0053e60f3 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/CreatePlaylistDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/CreatePlaylistDialog.kt @@ -31,10 +31,9 @@ class CreatePlaylistDialog( val listName = binding.playlistName.text.toString() if (listName != "") { lifecycleScope.launchWhenCreated { - PlaylistsHelper.createPlaylist(listName, requireContext().applicationContext) { - onSuccess.invoke() - dismiss() - } + PlaylistsHelper.createPlaylist(listName, requireContext().applicationContext) + onSuccess.invoke() + dismiss() } } else { Toast.makeText(context, R.string.emptyPlaylistName, Toast.LENGTH_LONG).show() diff --git a/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt index 5dee1296e..7db39b5e8 100644 --- a/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt +++ b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt @@ -96,7 +96,7 @@ class BackupRestoreSettings : BasePreferenceFragment() { val exportPlaylists = findPreference("export_playlists") exportPlaylists?.setOnPreferenceClickListener { - createPlaylistsFile.launch("subscriptions.json") + createPlaylistsFile.launch("playlists.json") true } diff --git a/app/src/main/java/com/github/libretube/util/ImportHelper.kt b/app/src/main/java/com/github/libretube/util/ImportHelper.kt index 562ae031a..ec2ee9ff3 100644 --- a/app/src/main/java/com/github/libretube/util/ImportHelper.kt +++ b/app/src/main/java/com/github/libretube/util/ImportHelper.kt @@ -117,7 +117,7 @@ class ImportHelper( playlistFile.playlists.orEmpty().forEach { CoroutineScope(Dispatchers.IO).launch { playlistFile.playlists?.let { - PlaylistsHelper.importPlaylists(it) + PlaylistsHelper.importPlaylists(activity, it) } } } diff --git a/app/src/main/res/xml/import_export_settings.xml b/app/src/main/res/xml/import_export_settings.xml index fcd65c2b1..a0ee3759e 100644 --- a/app/src/main/res/xml/import_export_settings.xml +++ b/app/src/main/res/xml/import_export_settings.xml @@ -21,9 +21,8 @@ + app:title="@string/import_playlists" /> Date: Thu, 1 Dec 2022 15:05:57 +0100 Subject: [PATCH 5/6] fix duplicated importing --- .../java/com/github/libretube/api/PlaylistsHelper.kt | 3 ++- .../java/com/github/libretube/util/ImportHelper.kt | 10 ++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt index 035a01eb9..1ab307f4f 100644 --- a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt +++ b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt @@ -185,6 +185,7 @@ object PlaylistsHelper { suspend fun importPlaylists(appContext: Context, playlists: List) { for (playlist in playlists) { + Log.e("playlist", playlist.toString()) val playlistId = createPlaylist(playlist.name!!, appContext) ?: continue runBlocking { val tasks = playlist.videos.map { videoId -> @@ -210,7 +211,7 @@ object PlaylistsHelper { type = "playlist", visibility = "private", videos = list.relatedStreams.orEmpty().map { - YOUTUBE_FRONTEND_URL + it.url!!.toID() + YOUTUBE_FRONTEND_URL + "/watch?v=" + it.url!!.toID() } ) ) diff --git a/app/src/main/java/com/github/libretube/util/ImportHelper.kt b/app/src/main/java/com/github/libretube/util/ImportHelper.kt index ec2ee9ff3..cd0ab71a1 100644 --- a/app/src/main/java/com/github/libretube/util/ImportHelper.kt +++ b/app/src/main/java/com/github/libretube/util/ImportHelper.kt @@ -114,15 +114,13 @@ class ImportHelper( val playlistFile = ObjectMapper().readValue(uri.readText(), ImportPlaylistFile::class.java) - playlistFile.playlists.orEmpty().forEach { - CoroutineScope(Dispatchers.IO).launch { - playlistFile.playlists?.let { - PlaylistsHelper.importPlaylists(activity, it) - } + CoroutineScope(Dispatchers.IO).launch { + playlistFile.playlists?.let { + PlaylistsHelper.importPlaylists(activity, it) } } - activity.toastFromMainThread(R.string.importsuccess) + activity.toastFromMainThread(R.string.success) } /** From 53ebd3363f5270ffc3508c0ca22a4c8d5f97da27 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Thu, 1 Dec 2022 15:17:46 +0100 Subject: [PATCH 6/6] use the new route --- .../github/libretube/api/PlaylistsHelper.kt | 51 ++++++++++--------- .../github/libretube/api/obj/PlaylistId.kt | 1 + .../libretube/workers/NotificationWorker.kt | 1 - 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt index 1ab307f4f..736e83594 100644 --- a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt +++ b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt @@ -107,23 +107,26 @@ object PlaylistsHelper { return null } - suspend fun addToPlaylist(playlistId: String, videoId: String): Boolean { + suspend fun addToPlaylist(playlistId: String, vararg videoIds: String): Boolean { if (!loggedIn()) { - val localPlaylistItem = RetrofitInstance.api.getStreams(videoId).toLocalPlaylistItem(playlistId, videoId) - awaitQuery { - // avoid duplicated videos in a playlist - DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByVideoId(playlistId, videoId) + val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll() + .first { it.playlist.id.toString() == playlistId } - // add the new video to the database - DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem) - val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll() - .first { it.playlist.id.toString() == playlistId } + for (videoId in videoIds) { + val localPlaylistItem = RetrofitInstance.api.getStreams(videoId).toLocalPlaylistItem(playlistId, videoId) + awaitQuery { + // avoid duplicated videos in a playlist + DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByVideoId(playlistId, videoId) - if (localPlaylist.playlist.thumbnailUrl == "") { - // set the new playlist thumbnail URL - localPlaylistItem.thumbnailUrl?.let { - localPlaylist.playlist.thumbnailUrl = it - DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(localPlaylist.playlist) + // add the new video to the database + DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem) + + if (localPlaylist.playlist.thumbnailUrl == "") { + // set the new playlist thumbnail URL + localPlaylistItem.thumbnailUrl?.let { + localPlaylist.playlist.thumbnailUrl = it + DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(localPlaylist.playlist) + } } } } @@ -132,7 +135,10 @@ object PlaylistsHelper { return RetrofitInstance.authApi.addToPlaylist( token, - PlaylistId(playlistId, videoId) + PlaylistId( + playlistId = playlistId, + videoIds = videoIds.toList() + ) ).message == "ok" } @@ -185,16 +191,13 @@ object PlaylistsHelper { suspend fun importPlaylists(appContext: Context, playlists: List) { for (playlist in playlists) { - Log.e("playlist", playlist.toString()) val playlistId = createPlaylist(playlist.name!!, appContext) ?: continue - runBlocking { - val tasks = playlist.videos.map { videoId -> - async { addToPlaylist(playlistId, videoId.substringAfter("=")) } - } - tasks.forEach { - it.await() - } - } + addToPlaylist( + playlistId, + *playlist.videos.map { + it.substringAfter("=") + }.toTypedArray() + ) } } diff --git a/app/src/main/java/com/github/libretube/api/obj/PlaylistId.kt b/app/src/main/java/com/github/libretube/api/obj/PlaylistId.kt index 088a105c2..ea10202a4 100644 --- a/app/src/main/java/com/github/libretube/api/obj/PlaylistId.kt +++ b/app/src/main/java/com/github/libretube/api/obj/PlaylistId.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties data class PlaylistId( var playlistId: String? = null, var videoId: String? = null, + var videoIds: List? = null, var newName: String? = null, var index: Int = -1 ) diff --git a/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt b/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt index d1fb9e742..44021adee 100644 --- a/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt +++ b/app/src/main/java/com/github/libretube/workers/NotificationWorker.kt @@ -17,7 +17,6 @@ import com.github.libretube.extensions.toID import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.views.TimePickerPreference import com.github.libretube.util.PreferenceHelper -import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import java.time.LocalTime