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..736e83594 100644 --- a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt +++ b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt @@ -6,16 +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 @@ -46,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 -> { @@ -65,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( @@ -75,8 +83,9 @@ object PlaylistsHelper { ) ) } - onSuccess.invoke() - return + return awaitQuery { + DatabaseHolder.Database.localPlaylistsDao().getAll() + }.last().playlist.id.toString() } val response = try { RetrofitInstance.authApi.createPlaylist( @@ -85,37 +94,39 @@ 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 { + 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) + } } } } @@ -124,7 +135,10 @@ object PlaylistsHelper { return RetrofitInstance.authApi.addToPlaylist( token, - PlaylistId(playlistId, videoId) + PlaylistId( + playlistId = playlistId, + videoIds = videoIds.toList() + ) ).message == "ok" } @@ -175,11 +189,49 @@ object PlaylistsHelper { ) } - fun getPrivateType(): PlaylistType { + suspend fun importPlaylists(appContext: Context, playlists: List) { + for (playlist in playlists) { + val playlistId = createPlaylist(playlist.name!!, appContext) ?: continue + addToPlaylist( + playlistId, + *playlist.videos.map { + it.substringAfter("=") + }.toTypedArray() + ) + } + } + + 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 + "/watch?v=" + it.url!!.toID() + } + ) + ) + } + } + tasks.forEach { + it.await() + } + } + return importLists + } + + 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/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/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/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/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/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/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..7db39b5e8 --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt @@ -0,0 +1,124 @@ +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.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 + + /** + * 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 -> + ImportHelper(requireActivity()).importSubscriptions(uri) + } + createSubscriptionsFile = registerForActivityResult( + CreateDocument("application/json") + ) { 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() + ) { 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("import_subscriptions") + importSubscriptions?.setOnPreferenceClickListener { + getSubscriptionsFile.launch("*/*") + true + } + + 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("playlists.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/java/com/github/libretube/util/ImportHelper.kt b/app/src/main/java/com/github/libretube/util/ImportHelper.kt index 525bb95dd..cd0ab71a1 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,83 @@ 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 newPipeSubscriptions = NewPipeSubscriptions( + subscriptions = newPipeChannels + ) + + 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) + + CoroutineScope(Dispatchers.IO).launch { + playlistFile.playlists?.let { + PlaylistsHelper.importPlaylists(activity, it) + } + } + + activity.toastFromMainThread(R.string.success) + } + + /** + * 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) ) - - val data = mapper.writeValueAsBytes(newPipeSubscriptions) - - activity.contentResolver.openFileDescriptor(uri, "w")?.use { - FileOutputStream(it.fileDescriptor).use { fileOutputStream -> - fileOutputStream.write(data) - } - } } - } catch (e: Exception) { - e.printStackTrace() } } } 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..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 @@ -82,12 +81,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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a229cf42b..bab5c6600 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -403,6 +403,11 @@ 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, … + Successfully exported! 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" /> + +