From 5a0fd2b5389b23f6e5bb65434bdcf127ca2c5551 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Thu, 1 Nov 2018 20:55:32 +0100 Subject: [PATCH] Introduce FancyMenu. A menu-item like menu which gives us better layout control. --- .../android/vanilla/FileSystemAdapter.java | 10 +- .../android/vanilla/LibraryActivity.java | 65 ++--- .../android/vanilla/LibraryPagerAdapter.java | 28 +- .../android/vanilla/ui/FancyMenu.java | 213 ++++++++++++++ .../android/vanilla/ui/FancyMenuItem.java | 272 ++++++++++++++++++ app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/translatable.xml | 1 + 7 files changed, 542 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenu.java create mode 100644 app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenuItem.java diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/FileSystemAdapter.java b/app/src/main/java/ch/blinkenlights/android/vanilla/FileSystemAdapter.java index c4f2f17c..cb4b1311 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/FileSystemAdapter.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/FileSystemAdapter.java @@ -454,17 +454,17 @@ public class FileSystemAdapter } /** - * Context menu of a row: this was dispatched by LibraryPAgerAdapter + * Context menu of a row: this was dispatched by LibraryPagerAdapter * - * @param menu the context menu to populate * @param intent likely created by createData() */ - public void onCreateContextMenu(ContextMenu menu, Intent intent) { + public boolean onCreateFancyMenu(Intent intent) { String path = intent.getStringExtra(LibraryAdapter.DATA_FILE); boolean isParentRow = (path != null && pointsToParentFolder(new File(path))); if (!isParentRow) - mActivity.onCreateContextMenu(menu, intent); - // else: no context menu + return mActivity.onCreateFancyMenu(intent); + // else: no context menu, but consume event. + return true; } } diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryActivity.java b/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryActivity.java index 02854f15..bd7e29df 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryActivity.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryActivity.java @@ -24,6 +24,8 @@ package ch.blinkenlights.android.vanilla; import ch.blinkenlights.android.medialibrary.MediaLibrary; +import ch.blinkenlights.android.vanilla.ui.FancyMenu; +import ch.blinkenlights.android.vanilla.ui.FancyMenuItem; import android.app.AlertDialog; import android.content.DialogInterface; @@ -67,6 +69,7 @@ public class LibraryActivity implements DialogInterface.OnClickListener , DialogInterface.OnDismissListener , SearchView.OnQueryTextListener + , FancyMenu.Callback { @@ -626,56 +629,59 @@ public class LibraryActivity * @param menu The menu to create. * @param rowData Data for the adapter row. */ - public void onCreateContextMenu(ContextMenu menu, Intent rowData) { - // Add to -> Playlist is always available. - SubMenu subAddTo = menu.addSubMenu(0, CTX_MENU_NOOP, 10, R.string.add_to); - subAddTo.add(0, CTX_MENU_ADD_TO_PLAYLIST, 0, R.string.playlist).setIntent(rowData); + public boolean onCreateFancyMenu(Intent rowData) { + FancyMenu fm = new FancyMenu(this, this); + fm.show(getFragmentManager(), "LibraryActivityContext"); + + // Add to playlist is always available. + fm.addSpacer(20); + fm.add(CTX_MENU_ADD_TO_PLAYLIST, 20, R.drawable.folder, R.string.add_to_playlist).setIntent(rowData); if (rowData.getLongExtra(LibraryAdapter.DATA_ID, LibraryAdapter.INVALID_ID) == LibraryAdapter.HEADER_ID) { - menu.setHeaderTitle(getString(R.string.all_songs)); - menu.add(0, CTX_MENU_PLAY_ALL, 0, R.string.play_all).setIntent(rowData); - menu.add(0, CTX_MENU_ENQUEUE_ALL, 0, R.string.enqueue_all).setIntent(rowData); + fm.setHeaderTitle(getString(R.string.all_songs)); + fm.add(CTX_MENU_PLAY_ALL, 10, R.drawable.folder, R.string.play_all).setIntent(rowData); + fm.add(CTX_MENU_ENQUEUE_ALL, 10, R.drawable.folder, R.string.enqueue_all).setIntent(rowData); } else { int type = rowData.getIntExtra(LibraryAdapter.DATA_TYPE, MediaUtils.TYPE_INVALID); - menu.setHeaderTitle(rowData.getStringExtra(LibraryAdapter.DATA_TITLE)); + fm.setHeaderTitle(rowData.getStringExtra(LibraryAdapter.DATA_TITLE)); if (type != MediaUtils.TYPE_FILE) - subAddTo.add(0, CTX_MENU_ADD_TO_HOMESCREEN, 0, R.string.homescreen).setIntent(rowData); + fm.add(CTX_MENU_ADD_TO_HOMESCREEN, 20, R.drawable.folder, R.string.add_to_homescreen).setIntent(rowData); if (FileUtils.canDispatchIntent(rowData)) - menu.add(0, CTX_MENU_OPEN_EXTERNAL, 0, R.string.open).setIntent(rowData); + fm.add(CTX_MENU_OPEN_EXTERNAL, 10, R.drawable.folder, R.string.open).setIntent(rowData); - menu.add(0, CTX_MENU_PLAY, 0, R.string.play).setIntent(rowData); - if (type <= MediaUtils.TYPE_SONG) { - menu.add(0, CTX_MENU_PLAY_ALL, 0, R.string.play_all).setIntent(rowData); - } - menu.add(0, CTX_MENU_ENQUEUE_AS_NEXT, 0, R.string.enqueue_as_next).setIntent(rowData); - menu.add(0, CTX_MENU_ENQUEUE, 0, R.string.enqueue).setIntent(rowData); + fm.add(CTX_MENU_PLAY, 0, R.drawable.folder, R.string.play).setIntent(rowData); + if (type <= MediaUtils.TYPE_SONG) + fm.add(CTX_MENU_PLAY_ALL, 1, R.drawable.folder, R.string.play_all).setIntent(rowData); + + fm.add(CTX_MENU_ENQUEUE_AS_NEXT, 1, R.drawable.folder, R.string.enqueue_as_next).setIntent(rowData); + fm.add(CTX_MENU_ENQUEUE, 1, R.drawable.folder, R.string.enqueue).setIntent(rowData); if (type == MediaUtils.TYPE_PLAYLIST) { - menu.add(0, CTX_MENU_RENAME_PLAYLIST, 0, R.string.rename).setIntent(rowData); + fm.add(CTX_MENU_RENAME_PLAYLIST, 0, R.drawable.folder, R.string.rename).setIntent(rowData); } else if (rowData.getBooleanExtra(LibraryAdapter.DATA_EXPANDABLE, false)) { - menu.add(0, CTX_MENU_EXPAND, 0, R.string.expand).setIntent(rowData); + fm.add(CTX_MENU_EXPAND, 2, R.drawable.folder, R.string.expand).setIntent(rowData); } if (type == MediaUtils.TYPE_SONG || type == MediaUtils.TYPE_ALBUM) { - SubMenu subMoreFrom = menu.addSubMenu(0, CTX_MENU_NOOP, 0, R.string.more_from_current); - subMoreFrom.add(0, CTX_MENU_MORE_FROM_ARTIST, 0, R.string.artist).setIntent(rowData); + fm.addSpacer(30); + fm.add(CTX_MENU_MORE_FROM_ARTIST, 30, R.drawable.folder, R.string.more_from_artist).setIntent(rowData); if (type == MediaUtils.TYPE_SONG) { - subMoreFrom.add(0, CTX_MENU_MORE_FROM_ALBUM, 0, R.string.album).setIntent(rowData); - subMoreFrom.add(0, CTX_MENU_SHOW_DETAILS, 0, R.string.details).setIntent(rowData); - + fm.add(CTX_MENU_MORE_FROM_ALBUM, 30, R.drawable.folder, R.string.more_from_album).setIntent(rowData); + fm.add(CTX_MENU_SHOW_DETAILS, 99, R.drawable.folder, R.string.details).setIntent(rowData); if (PluginUtils.checkPlugins(this)) { // not part of submenu: just last item in normal menu. - menu.add(0, CTX_MENU_PLUGINS, 99, R.string.plugins).setIntent(rowData); + fm.add(CTX_MENU_PLUGINS, 99, R.drawable.folder, R.string.plugins).setIntent(rowData); } } } - - menu.add(0, CTX_MENU_DELETE, 90, R.string.delete).setIntent(rowData); + fm.addSpacer(90); + fm.add(CTX_MENU_DELETE, 90, R.drawable.folder, R.string.delete).setIntent(rowData); } + return true; } /** @@ -690,11 +696,7 @@ public class LibraryActivity } @Override - public boolean onContextItemSelected(MenuItem item) - { - if (item.getGroupId() != 0) - return super.onContextItemSelected(item); - + public boolean onFancyItemSelected(FancyMenuItem item) { final Intent intent = item.getIntent(); switch (item.getItemId()) { case CTX_MENU_NOOP: @@ -795,7 +797,6 @@ public class LibraryActivity default: return super.onContextItemSelected(item); } - return true; } diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java b/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java index 57511ffb..2fda62df 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java @@ -50,7 +50,7 @@ public class LibraryPagerAdapter extends PagerAdapter implements Handler.Callback , ViewPager.OnPageChangeListener - , View.OnCreateContextMenuListener + , AdapterView.OnItemLongClickListener , AdapterView.OnItemClickListener { /** @@ -324,7 +324,7 @@ public class LibraryPagerAdapter } view = (ListView)inflater.inflate(R.layout.listview, null); - view.setOnCreateContextMenuListener(this); + view.setOnItemLongClickListener(this); view.setOnItemClickListener(this); view.setTag(type); @@ -852,18 +852,24 @@ public class LibraryPagerAdapter } @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) - { - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuInfo; - View targetView = info.targetView; - Intent intent = info.id == LibraryAdapter.HEADER_ID ? createHeaderIntent(targetView) : mCurrentAdapter.createData(targetView); - int type = (Integer)((View)targetView.getParent()).getTag(); + /** + * Dispatch long click event of a row. + * + * @param parent the parent adapter view + * @param view the long clicked view + * @param position row position + * @param id id of the long clicked row + * + * @return true if the event was consumed + */ + public boolean onItemLongClick (AdapterView parent, View view, int position, long id) { + Intent intent = id == LibraryAdapter.HEADER_ID ? createHeaderIntent(view) : mCurrentAdapter.createData(view); + int type = (Integer)((View)view.getParent()).getTag(); if (type == MediaUtils.TYPE_FILE) { - mFilesAdapter.onCreateContextMenu(menu, intent); - } else { - mActivity.onCreateContextMenu(menu, intent); + return mFilesAdapter.onCreateFancyMenu(intent); } + return mActivity.onCreateFancyMenu(intent); } @Override diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenu.java b/app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenu.java new file mode 100644 index 00000000..bd293527 --- /dev/null +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenu.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2018 Adrian Ulrich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package ch.blinkenlights.android.vanilla.ui; + +import ch.blinkenlights.android.vanilla.R; +import ch.blinkenlights.android.vanilla.ThemeHelper; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import android.widget.ImageView; + +import java.util.ArrayList; +import android.util.Log; + + +public class FancyMenu extends DialogFragment + implements DialogInterface.OnClickListener { + /** + * Title to use for this FancyMenu + */ + private String mTitle; + /** + * Context of this instance + */ + private Context mContext; + /** + * Callback to call + */ + private FancyMenu.Callback mCallback; + /** + * List of all items and possible children + */ + private ArrayList> mItems; + /** + * The built adapter used by the visible dialog + */ + private FancyMenu.Adapter mAdapter; + /** + * The callback interface to implement + */ + public interface Callback { + boolean onFancyItemSelected(FancyMenuItem item); + } + + /** + * Create a new FancyMenu instance. + * + * @param context the context to use + */ + public FancyMenu(Context context, FancyMenu.Callback callback) { + mContext = context; + mCallback = callback; + mItems = new ArrayList>(); + } + + /** + * Set title of this FancyMenu + * + * @param title the title to set + */ + public void setHeaderTitle(String title) { + mTitle = title; + } + + /** + * Adds a new item to the menu + * + * @param id the id which identifies this object. + * @param order how to sort this item + * @param drawable icon drawable to use + * @param textRes id of the text resource to use as label + */ + public FancyMenuItem add(int id, int order, int drawable, int textRes) { + CharSequence text = mContext.getResources().getString(textRes); + return addInternal(id, order, drawable, text, false); + } + + /** + * Adds a new item to the menu + * + * @param id the id which identifies this object. + * @param order how to sort this item + * @param drawable icon drawable to use + * @param text string label + */ + public FancyMenuItem add(int id, int order, int drawable, CharSequence text) { + return addInternal(id, order, drawable, text, false); + } + + /** + * Adds a new spacer item + * + * @param order how to sort this item + */ + public FancyMenuItem addSpacer(int order) { + return addInternal(0, order, 0, null, true); + } + + private FancyMenuItem addInternal(int id, int order, int icon, CharSequence text, boolean spacer) { + FancyMenuItem item = new FancyMenuItem(mContext, id) + .setIcon(icon) + .setTitle(text) + .setIsSpacer(spacer); + + while (order >= mItems.size()) { + mItems.add(new ArrayList()); + } + mItems.get(order).add(item); + return item; + } + + /** + * Called when this dialog is about to be shown. + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // spacers look awful on holo themes + final boolean usesSpacers = !ThemeHelper.usesHoloTheme(); + + // The adaper will back this list + mAdapter = new FancyMenu.Adapter(mContext, 0); + for (ArrayList sub : mItems) { + for (FancyMenuItem item : sub ) { + if (usesSpacers || !item.isSpacer()) { + mAdapter.add(item); + } + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle(mTitle) + .setAdapter(mAdapter, this); + + return builder.create(); + } + + /** + * Callback for click events on the displayed dialog + */ + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + + FancyMenuItem item = mAdapter.getItem(which); + if (!item.isSpacer()) { + mCallback.onFancyItemSelected(item); + } + } + + + /** + * Adapter class backing all menu entries + */ + private class Adapter extends ArrayAdapter { + private LayoutInflater mInflater; + + Adapter(Context context, int resource) { + super(context, resource); + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public View getView(int pos, View convertView, ViewGroup parent) { + FancyMenuItem item = (FancyMenuItem)getItem(pos); + int res = (item.isSpacer() ? R.layout.fancymenu_spacer : R.layout.fancymenu_row); + + View row = (View)mInflater.inflate(res, parent, false); + TextView text = (TextView)row.findViewById(R.id.text); + ImageView icon = (ImageView)row.findViewById(R.id.icon); + + if (item.isSpacer()) { + text.setText(item.getTitle()); + } else { + text.setText(item.getTitle()); + icon.setImageDrawable(item.getIcon()); + } + return row; + } + } + +} diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenuItem.java b/app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenuItem.java new file mode 100644 index 00000000..f387bcc9 --- /dev/null +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenuItem.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2018 Adrian Ulrich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package ch.blinkenlights.android.vanilla.ui; + +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.view.ContextMenu; +import android.view.MenuItem; +import android.view.ActionProvider; +import android.view.SubMenu; +import android.view.View; + +public class FancyMenuItem implements MenuItem { + private int mId; + private int mIconRes; + private CharSequence mTitle; + private Intent mIntent; + private boolean mSpacer; + private Drawable mIcon; + private Context mContext; + + FancyMenuItem(Context context, int id) { + mContext = context; + mId = id; + } + + /** + * Returns whether or not this is a spacer + * + * @return true if this is a spacer + */ + public boolean isSpacer() { + return mSpacer; + } + + public FancyMenuItem setIsSpacer(boolean spacer) { + mSpacer = spacer; + return this; + } + + @Override + public CharSequence getTitle() { + return mTitle; + } + + @Override + public Drawable getIcon() { + if (mIconRes != 0) + return mContext.getResources().getDrawable(mIconRes); + return mIcon; + } + + @Override + public FancyMenuItem setIcon(Drawable drawable) { + mIcon = drawable; + return this; + } + + @Override + public FancyMenuItem setIcon(int res) { + mIconRes = res; + return this; + } + + @Override + public FancyMenuItem setIntent(Intent intent) { + mIntent = intent; + return this; + } + + @Override + public FancyMenuItem setTitle(CharSequence title) { + mTitle = title; + return this; + } + + @Override + public FancyMenuItem setTitle(int title) { + return setTitle(mContext.getResources().getString(title)); + } + + @Override + public Intent getIntent() { + return mIntent; + } + + @Override + public int getItemId() { + return mId; + } + + // Dummy functions to satisfy MenuItem interface. + @Override + public ActionProvider getActionProvider() { + return null; + } + + @Override + public CharSequence getTitleCondensed() { + return null; + } + + @Override + public ContextMenu.ContextMenuInfo getMenuInfo() { + return null; + } + + @Override + public FancyMenuItem setActionProvider(ActionProvider actionProvider) { + return this; + } + + @Override + public FancyMenuItem setActionView(int view) { + return this; + } + + @Override + public FancyMenuItem setAlphabeticShortcut(char alphaChar) { + return this; + } + + @Override + public FancyMenuItem setCheckable(boolean checkable) { + return this; + } + + @Override + public FancyMenuItem setChecked(boolean checked) { + return this; + } + + @Override + public FancyMenuItem setEnabled(boolean enabled) { + return this; + } + + @Override + public FancyMenuItem setNumericShortcut(char numericChar) { + return this; + } + + @Override + public FancyMenuItem setOnActionExpandListener(MenuItem.OnActionExpandListener listener) { + return this; + } + + @Override + public FancyMenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener menuItemClickListener) { + return this; + } + + @Override + public FancyMenuItem setShortcut(char numericChar, char alphaChar) { + return this; + } + + @Override + public FancyMenuItem setShowAsActionFlags(int actionEnum) { + return this; + } + + @Override + public FancyMenuItem setTitleCondensed(CharSequence text) { + return this; + } + + @Override + public FancyMenuItem setVisible(boolean visible) { + return this; + } + + @Override + public MenuItem setActionView(View view) { + return this; + } + + @Override + public SubMenu getSubMenu() { + return null; + } + + @Override + public View getActionView() { + return null; + } + + @Override + public boolean collapseActionView() { + return false; + } + + @Override + public boolean expandActionView() { + return false; + } + + @Override + public boolean hasSubMenu() { + return false; + } + + @Override + public boolean isActionViewExpanded() { + return false; + } + + @Override + public boolean isCheckable() { + return false; + } + + @Override + public boolean isChecked() { + return false; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean isVisible() { + return true; + } + + @Override + public char getAlphabeticShortcut() { + return 0; + } + + @Override + public char getNumericShortcut() { + return 0; + } + + @Override + public int getGroupId() { + return 0; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void setShowAsAction(int actionEnum) { + } +} diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index f75c1f17..ea9caa76 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -7,6 +7,7 @@ 10sp 14sp 56sp + 38sp 4dip 8dp diff --git a/app/src/main/res/values/translatable.xml b/app/src/main/res/values/translatable.xml index e8105d20..a001fe73 100644 --- a/app/src/main/res/values/translatable.xml +++ b/app/src/main/res/values/translatable.xml @@ -68,6 +68,7 @@ THE SOFTWARE. Rename Save as playlist… Add to playlist… + Add to homescreen… Add to… Playlist Homescreen