Merge pull request #7194 from KalleStruik/add-to-playlist-in-share

Add a "add to playlist" option in the share menu
This commit is contained in:
litetex 2021-10-13 20:34:07 +02:00 committed by GitHub
commit 4af49ee5a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 264 additions and 180 deletions

View file

@ -9,8 +9,8 @@ import android.widget.PopupMenu;
import androidx.fragment.app.FragmentManager;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.NavigationHelper;
@ -47,13 +47,15 @@ public final class QueueItemMenuUtil {
false);
return true;
case R.id.menu_item_append_playlist:
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(
Collections.singletonList(item)
PlaylistDialog.createCorrespondingDialog(
context,
Collections.singletonList(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"
)
);
PlaylistAppendDialog.onPlaylistFound(context,
() -> d.show(fragmentManager, "QueueItemMenuUtil@append_playlist"),
() -> PlaylistCreationDialog.newInstance(d)
.show(fragmentManager, "QueueItemMenuUtil@append_playlist"));
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),

View file

@ -1,5 +1,8 @@
package org.schabi.newpipe;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
import android.annotation.SuppressLint;
import android.app.IntentService;
import android.content.Context;
@ -30,6 +33,7 @@ import androidx.core.widget.TextViewCompat;
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.download.DownloadDialog;
@ -56,6 +60,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
@ -69,14 +74,15 @@ import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.urlfinder.UrlFinder;
import org.schabi.newpipe.views.FocusOverlayView;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import icepick.Icepick;
@ -89,9 +95,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
/**
* Get the url from the intent and open it in the chosen preferred player.
*/
@ -107,6 +110,7 @@ public class RouterActivity extends AppCompatActivity {
protected String currentUrl;
private StreamingService currentService;
private boolean selectionIsDownload = false;
private boolean selectionIsAddToPlaylist = false;
private AlertDialog alertDialogChoice = null;
@Override
@ -350,7 +354,7 @@ public class RouterActivity extends AppCompatActivity {
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
.setPositiveButton(R.string.always, dialogButtonsClickListener)
.setOnDismissListener((dialog) -> {
if (!selectionIsDownload) {
if (!selectionIsDownload && !selectionIsAddToPlaylist) {
finish();
}
})
@ -446,6 +450,10 @@ public class RouterActivity extends AppCompatActivity {
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
getString(R.string.background_player_key), getString(R.string.background_player),
R.drawable.ic_headset);
final AdapterChoiceItem addToPlaylist = new AdapterChoiceItem(
getString(R.string.add_to_playlist_key), getString(R.string.add_to_playlist),
R.drawable.ic_add);
if (linkType == LinkType.STREAM) {
if (isExtVideoEnabled) {
@ -482,6 +490,10 @@ public class RouterActivity extends AppCompatActivity {
getString(R.string.download),
R.drawable.ic_file_download));
// Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can
// not be added to a playlist
returnList.add(addToPlaylist);
} else {
returnList.add(showInfo);
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
@ -547,6 +559,12 @@ public class RouterActivity extends AppCompatActivity {
return;
}
if (selectedChoiceKey.equals(getString(R.string.add_to_playlist_key))) {
selectionIsAddToPlaylist = true;
openAddToPlaylistDialog();
return;
}
// stop and bypass FetcherService if InfoScreen was selected since
// StreamDetailFragment can fetch data itself
if (selectedChoiceKey.equals(getString(R.string.show_info_key))) {
@ -572,6 +590,41 @@ public class RouterActivity extends AppCompatActivity {
finish();
}
private void openAddToPlaylistDialog() {
// Getting the stream info usually takes a moment
// Notifying the user here to ensure that no confusion arises
Toast.makeText(
getApplicationContext(),
getString(R.string.processing_may_take_a_moment),
Toast.LENGTH_SHORT)
.show();
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
info -> PlaylistDialog.createCorrespondingDialog(
getThemeWrapperContext(),
Collections.singletonList(new StreamEntity(info)),
playlistDialog -> {
playlistDialog.setOnDismissListener(dialog -> finish());
playlistDialog.show(
this.getSupportFragmentManager(),
"addToPlaylistDialog"
);
}
),
throwable -> handleError(this, new ErrorInfo(
throwable,
UserAction.REQUESTED_STREAM,
"Tried to add " + currentUrl + " to a playlist",
currentService.getServiceId())
)
)
);
}
@SuppressLint("CheckResult")
private void openDownloadDialog() {
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)

View file

@ -52,6 +52,7 @@ import com.squareup.picasso.Callback;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.error.ErrorActivity;
@ -73,8 +74,7 @@ import org.schabi.newpipe.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
@ -99,6 +99,7 @@ import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
@ -444,12 +445,11 @@ public final class VideoDetailFragment
break;
case R.id.detail_controls_playlist_append:
if (getFM() != null && currentInfo != null) {
final PlaylistAppendDialog d = PlaylistAppendDialog.fromStreamInfo(currentInfo);
disposables.add(
PlaylistAppendDialog.onPlaylistFound(getContext(),
() -> d.show(getFM(), TAG),
() -> PlaylistCreationDialog.newInstance(d).show(getFM(), TAG)
PlaylistDialog.createCorrespondingDialog(
getContext(),
Collections.singletonList(new StreamEntity(currentInfo)),
dialog -> dialog.show(getFM(), TAG)
)
);
}

View file

@ -1,6 +1,5 @@
package org.schabi.newpipe.local.dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -17,20 +16,14 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.local.LocalItemListAdapter;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.OnClickGesture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
public final class PlaylistAppendDialog extends PlaylistDialog {
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
@ -40,47 +33,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
public static Disposable onPlaylistFound(
final Context context, final Runnable onSuccess, final Runnable onFailed
) {
final LocalPlaylistManager playlistManager =
new LocalPlaylistManager(NewPipeDatabase.getInstance(context));
return playlistManager.hasPlaylists()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(hasPlaylists -> {
if (hasPlaylists) {
onSuccess.run();
} else {
onFailed.run();
}
});
}
public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) {
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
dialog.setInfo(Collections.singletonList(new StreamEntity(info)));
return dialog;
}
public static PlaylistAppendDialog fromStreamInfoItems(final List<StreamInfoItem> items) {
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
final List<StreamEntity> entities = new ArrayList<>(items.size());
for (final StreamInfoItem item : items) {
entities.add(new StreamEntity(item));
}
dialog.setInfo(entities);
return dialog;
}
public static PlaylistAppendDialog fromPlayQueueItems(final List<PlayQueueItem> items) {
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
final List<StreamEntity> entities = new ArrayList<>(items.size());
for (final PlayQueueItem item : items) {
entities.add(new StreamEntity(item));
}
dialog.setInfo(entities);
return dialog;
public PlaylistAppendDialog(final List<StreamEntity> streamEntities) {
super(streamEntities);
}
/*//////////////////////////////////////////////////////////////////////////
@ -104,11 +58,15 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
@Override
public void selected(final LocalItem selectedItem) {
if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) {
if (!(selectedItem instanceof PlaylistMetadataEntry)
|| getStreamEntities() == null) {
return;
}
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem,
getStreams());
onPlaylistSelected(
playlistManager,
(PlaylistMetadataEntry) selectedItem,
getStreamEntities()
);
}
});
@ -146,11 +104,17 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
//////////////////////////////////////////////////////////////////////////*/
public void openCreatePlaylistDialog() {
if (getStreams() == null || !isAdded()) {
if (getStreamEntities() == null || !isAdded()) {
return;
}
PlaylistCreationDialog.newInstance(getStreams()).show(getParentFragmentManager(), TAG);
final PlaylistCreationDialog playlistCreationDialog =
new PlaylistCreationDialog(getStreamEntities());
// Move the dismissListener to the new dialog.
playlistCreationDialog.setOnDismissListener(this.getOnDismissListener());
this.setOnDismissListener(null);
playlistCreationDialog.show(getParentFragmentManager(), TAG);
requireDialog().dismiss();
}
@ -165,7 +129,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
@NonNull final PlaylistMetadataEntry playlist,
@NonNull final List<StreamEntity> streams) {
if (getStreams() == null) {
if (getStreamEntities() == null) {
return;
}

View file

@ -7,29 +7,22 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AlertDialog.Builder;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
public final class PlaylistCreationDialog extends PlaylistDialog {
public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) {
final PlaylistCreationDialog dialog = new PlaylistCreationDialog();
dialog.setInfo(streams);
return dialog;
}
public static PlaylistCreationDialog newInstance(final PlaylistAppendDialog appendDialog) {
final PlaylistCreationDialog dialog = new PlaylistCreationDialog();
dialog.setInfo(appendDialog.getStreams());
return dialog;
public PlaylistCreationDialog(final List<StreamEntity> streamEntities) {
super(streamEntities);
}
/*//////////////////////////////////////////////////////////////////////////
@ -39,16 +32,18 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
if (getStreams() == null) {
if (getStreamEntities() == null) {
return super.onCreateDialog(savedInstanceState);
}
final DialogEditTextBinding dialogBinding
= DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext()));
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext())
final Builder dialogBuilder = new Builder(requireContext(),
ThemeHelper.getDialogTheme(requireContext()))
.setTitle(R.string.create_playlist)
.setView(dialogBinding.getRoot())
.setCancelable(true)
@ -61,11 +56,10 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
R.string.playlist_creation_success,
Toast.LENGTH_SHORT);
playlistManager.createPlaylist(name, getStreams())
playlistManager.createPlaylist(name, getStreamEntities())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> successToast.show());
});
return dialogBuilder.create();
}
}

View file

@ -1,6 +1,8 @@
package org.schabi.newpipe.local.dialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.Window;
@ -8,23 +10,29 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.util.StateSaver;
import java.util.List;
import java.util.Queue;
import java.util.function.Consumer;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
@Nullable
private DialogInterface.OnDismissListener onDismissListener = null;
private List<StreamEntity> streamEntities;
private org.schabi.newpipe.util.SavedState savedState;
protected void setInfo(final List<StreamEntity> entities) {
this.streamEntities = entities;
}
protected List<StreamEntity> getStreams() {
return streamEntities;
public PlaylistDialog(final List<StreamEntity> streamEntities) {
this.streamEntities = streamEntities;
}
/*//////////////////////////////////////////////////////////////////////////
@ -43,6 +51,10 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
StateSaver.onDestroy(savedState);
}
public List<StreamEntity> getStreamEntities() {
return streamEntities;
}
@NonNull
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
@ -55,6 +67,14 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
return dialog;
}
@Override
public void onDismiss(@NonNull final DialogInterface dialog) {
super.onDismiss(dialog);
if (onDismissListener != null) {
onDismissListener.onDismiss(dialog);
}
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@ -84,4 +104,47 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
savedState, outState, this);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Getter + Setter
//////////////////////////////////////////////////////////////////////////*/
@Nullable
public DialogInterface.OnDismissListener getOnDismissListener() {
return onDismissListener;
}
public void setOnDismissListener(
@Nullable final DialogInterface.OnDismissListener onDismissListener
) {
this.onDismissListener = onDismissListener;
}
/*//////////////////////////////////////////////////////////////////////////
// Dialog creation
//////////////////////////////////////////////////////////////////////////*/
/**
* Creates a {@link PlaylistAppendDialog} when playlists exists,
* otherwise a {@link PlaylistCreationDialog}.
*
* @param context context used for accessing the database
* @param streamEntities used for crating the dialog
* @param onExec execution that should occur after a dialog got created, e.g. showing it
* @return Disposable
*/
public static Disposable createCorrespondingDialog(
final Context context,
final List<StreamEntity> streamEntities,
final Consumer<PlaylistDialog> onExec
) {
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
.hasPlaylists()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(hasPlaylists ->
onExec.accept(hasPlaylists
? new PlaylistAppendDialog(streamEntities)
: new PlaylistCreationDialog(streamEntities))
);
}
}

View file

@ -23,11 +23,11 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -43,6 +43,7 @@ import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
import java.util.stream.Collectors;
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
@ -452,12 +453,12 @@ public final class PlayQueueActivity extends AppCompatActivity
}
}
private void openPlaylistAppendDialog(final List<PlayQueueItem> playlist) {
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(playlist);
PlaylistAppendDialog.onPlaylistFound(getApplicationContext(),
() -> d.show(getSupportFragmentManager(), TAG),
() -> PlaylistCreationDialog.newInstance(d).show(getSupportFragmentManager(), TAG));
private void openPlaylistAppendDialog(final List<PlayQueueItem> playQueueItems) {
PlaylistDialog.createCorrespondingDialog(
getApplicationContext(),
playQueueItems.stream().map(StreamEntity::new).collect(Collectors.toList()),
dialog -> dialog.show(getSupportFragmentManager(), TAG)
);
}
////////////////////////////////////////////////////////////////////////////

View file

@ -8,9 +8,10 @@ import androidx.fragment.app.Fragment;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.external_communication.KoreUtils;
@ -81,14 +82,16 @@ public enum StreamDialogEntry {
delete(R.string.delete, (fragment, item) -> {
}), // has to be set manually
append_playlist(R.string.append_playlist, (fragment, item) -> {
final PlaylistAppendDialog d = PlaylistAppendDialog
.fromStreamInfoItems(Collections.singletonList(item));
PlaylistAppendDialog.onPlaylistFound(fragment.getContext(),
() -> d.show(fragment.getParentFragmentManager(), "StreamDialogEntry@append_playlist"),
() -> PlaylistCreationDialog.newInstance(d)
.show(fragment.getParentFragmentManager(), "StreamDialogEntry@create_playlist")
append_playlist(R.string.add_to_playlist, (fragment, item) -> {
PlaylistDialog.createCorrespondingDialog(
fragment.getContext(),
Collections.singletonList(new StreamEntity(item)),
dialog -> dialog.show(
fragment.getParentFragmentManager(),
"StreamDialogEntry@"
+ (dialog instanceof PlaylistAppendDialog ? "append" : "create")
+ "_playlist"
)
);
}),