BackupRestoreSettingsFragment: add UI options to import/export subscriptions

* create SubscriptionsImportExportHelper to share common code used in
  SubscriptionFragment and BackupRestoreSettingsFragment
* Add UI options for import/export in BackupRestoreSettingsFragment
This commit is contained in:
evermind 2026-02-13 01:40:11 +01:00
parent c2b698491b
commit 941edd7aef
6 changed files with 130 additions and 55 deletions

View File

@ -1,6 +1,5 @@
package org.schabi.newpipe.local.subscription
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
@ -14,8 +13,6 @@ import android.view.View
import android.view.ViewGroup
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.ViewModelProvider
@ -26,9 +23,6 @@ import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.GroupieViewHolder
import io.reactivex.rxjava3.disposables.CompositeDisposable
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
import org.schabi.newpipe.databinding.DialogTitleBinding
@ -52,10 +46,6 @@ import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
import org.schabi.newpipe.local.subscription.item.GroupsHeader
import org.schabi.newpipe.local.subscription.item.Header
import org.schabi.newpipe.local.subscription.item.ImportSubscriptionsHintPlaceholderItem
import org.schabi.newpipe.local.subscription.workers.SubscriptionExportWorker
import org.schabi.newpipe.local.subscription.workers.SubscriptionImportInput
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture
@ -69,6 +59,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private lateinit var viewModel: SubscriptionViewModel
private lateinit var subscriptionManager: SubscriptionManager
private lateinit var importExportHelper: SubscriptionsImportExportHelper
private val disposables: CompositeDisposable = CompositeDisposable()
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
@ -77,11 +68,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private lateinit var feedGroupsSortMenuItem: GroupsHeader
private val subscriptionsSection = Section()
private val requestExportLauncher =
registerForActivityResult(StartActivityForResult(), this::requestExportResult)
private val requestImportLauncher =
registerForActivityResult(StartActivityForResult(), this::requestImportResult)
@State
@JvmField
var itemsListState: Parcelable? = null
@ -101,6 +87,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun onAttach(context: Context) {
super.onAttach(context)
subscriptionManager = SubscriptionManager(requireContext())
importExportHelper = SubscriptionsImportExportHelper(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -141,7 +128,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
// -- Import --
val importSubMenu = menu.addSubMenu(R.string.import_from)
addMenuItemToSubmenu(importSubMenu, R.string.previous_export) { onImportPreviousSelected() }
addMenuItemToSubmenu(importSubMenu, R.string.previous_export) { importExportHelper.onImportPreviousSelected() }
.setIcon(R.drawable.ic_backup)
for (service in ServiceList.all()) {
@ -159,7 +146,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
// -- Export --
val exportSubMenu = menu.addSubMenu(R.string.export_to)
addMenuItemToSubmenu(exportSubMenu, R.string.file) { onExportSelected() }
addMenuItemToSubmenu(exportSubMenu, R.string.file) { importExportHelper.onExportSelected() }
.setIcon(R.drawable.ic_save)
}
@ -195,48 +182,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId)
}
private fun onImportPreviousSelected() {
NoFileManagerSafeGuard.launchSafe(
requestImportLauncher,
StoredFileHelper.getPicker(activity, JSON_MIME_TYPE),
TAG,
requireContext()
)
}
private fun onExportSelected() {
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
val exportName = "newpipe_subscriptions_$date.json"
NoFileManagerSafeGuard.launchSafe(
requestExportLauncher,
StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null),
TAG,
requireContext()
)
}
private fun openReorderDialog() {
FeedGroupReorderDialog().show(parentFragmentManager, null)
}
private fun requestExportResult(result: ActivityResult) {
val data = result.data?.data
if (data != null && result.resultCode == Activity.RESULT_OK) {
SubscriptionExportWorker.schedule(activity, data)
}
}
private fun requestImportResult(result: ActivityResult) {
val data = result.data?.dataString
if (data != null && result.resultCode == Activity.RESULT_OK) {
ImportConfirmationDialog.show(
this,
SubscriptionImportInput.PreviousExportMode(data)
)
}
}
// ////////////////////////////////////////////////////////////////////////
// Fragment Views
// ////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,82 @@
package org.schabi.newpipe.local.subscription
import android.app.Activity
import android.content.Context
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.fragment.app.Fragment
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import org.schabi.newpipe.local.subscription.SubscriptionFragment.Companion.JSON_MIME_TYPE
import org.schabi.newpipe.local.subscription.workers.SubscriptionExportWorker
import org.schabi.newpipe.local.subscription.workers.SubscriptionImportInput
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
import org.schabi.newpipe.streams.io.StoredFileHelper
/**
* This class has to be created in onAttach() or onCreate().
*
* It contains registerForActivityResult calls and those
* calls are only allowed before a fragment/activity is created.
*/
class SubscriptionsImportExportHelper(
val fragment: Fragment
) {
val context: Context = fragment.requireContext()
companion object {
val TAG: String =
SubscriptionsImportExportHelper::class.java.simpleName + "@" + Integer.toHexString(
hashCode()
)
}
private val requestExportLauncher =
fragment.registerForActivityResult(StartActivityForResult(), this::requestExportResult)
private val requestImportLauncher =
fragment.registerForActivityResult(StartActivityForResult(), this::requestImportResult)
private fun requestExportResult(result: ActivityResult) {
val data = result.data?.data
if (data != null && result.resultCode == Activity.RESULT_OK) {
SubscriptionExportWorker.schedule(context, data)
}
}
private fun requestImportResult(result: ActivityResult) {
val data = result.data?.dataString
if (data != null && result.resultCode == Activity.RESULT_OK) {
ImportConfirmationDialog.show(
fragment,
SubscriptionImportInput.PreviousExportMode(data)
)
}
}
fun onExportSelected() {
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
val exportName = "newpipe_subscriptions_$date.json"
NoFileManagerSafeGuard.launchSafe(
requestExportLauncher,
StoredFileHelper.getNewPicker(
context,
exportName,
JSON_MIME_TYPE,
null
),
TAG,
context
)
}
fun onImportPreviousSelected() {
NoFileManagerSafeGuard.launchSafe(
requestImportLauncher,
StoredFileHelper.getPicker(context, JSON_MIME_TYPE),
TAG,
context
)
}
}

View File

@ -26,6 +26,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.subscription.SubscriptionsImportExportHelper;
import org.schabi.newpipe.settings.export.BackupFileLocator;
import org.schabi.newpipe.settings.export.ImportExportManager;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
@ -54,8 +55,15 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
this::requestExportPathResult);
private SubscriptionsImportExportHelper importExportHelper;
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
importExportHelper = new SubscriptionsImportExportHelper(this);
}
@Override
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
@Nullable final String rootKey) {
@ -117,6 +125,21 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
alertDialog.show();
return true;
});
final Preference exportSubsPreference =
requirePreference(R.string.export_subscriptions_key);
exportSubsPreference.setOnPreferenceClickListener(reference -> {
importExportHelper.onExportSelected();
return true;
});
final Preference importSubsPreference =
requirePreference(R.string.import_subscriptions_key);
importSubsPreference.setOnPreferenceClickListener(preference -> {
importExportHelper.onImportPreviousSelected();
return true;
});
}
private void requestExportPathResult(final ActivityResult result) {

View File

@ -412,6 +412,8 @@
<string name="import_export_data_path">import_export_data_path</string>
<string name="import_data">import_data</string>
<string name="export_data">export_data</string>
<string name="import_subscriptions_key">import_subscriptions_key</string>
<string name="export_subscriptions_key">export_subscriptions_key</string>
<string name="clear_cookie_key">clear_cookie</string>

View File

@ -509,6 +509,11 @@
<string name="export_ongoing">Exporting…</string>
<string name="import_file_title">Import file</string>
<string name="previous_export">Previous export</string>
<string name="import_subscriptions_title">Import subscriptions"</string>
<string name="export_subscriptions_title">Export subscriptions</string>
<string name="import_subscriptions_summary">Import subscriptions from a previous .json export"</string>
<string name="export_subscriptions_summary">Export your subscriptions to a .json file</string>
<string name="import_from_previous_export">Import from previous export</string>
<string name="subscriptions_import_unsuccessful">Could not import subscriptions</string>
<string name="subscriptions_export_unsuccessful">Could not export subscriptions</string>
<string name="import_youtube_instructions">Import YouTube subscriptions from Google takeout:

View File

@ -22,4 +22,18 @@
android:summary="@string/reset_settings_summary"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
<Preference
android:key="@string/export_subscriptions_key"
android:title="@string/export_subscriptions_title"
android:summary="@string/export_subscriptions_summary"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
<Preference
android:key="@string/import_subscriptions_key"
android:title="@string/import_subscriptions_title"
android:summary="@string/import_subscriptions_summary"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
</PreferenceScreen>