diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 5b1cf48e54c..cc7ab12bd93 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -73,6 +73,8 @@ import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.settings.drawer_items.DrawerItem; +import org.schabi.newpipe.settings.drawer_items.DrawerItemManager; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.KioskTranslator; @@ -88,7 +90,9 @@ import org.schabi.newpipe.views.FocusOverlayView; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; @@ -103,19 +107,27 @@ public class MainActivity extends AppCompatActivity { private DrawerLayoutBinding drawerLayoutBinding; private ToolbarLayoutBinding toolbarLayoutBinding; + private DrawerItemManager drawerItemManager; + private List drawerItemList = new ArrayList<>(); + private ActionBarDrawerToggle toggle; private boolean servicesShown = false; private BroadcastReceiver broadcastReceiver; - private static final int ITEM_ID_SUBSCRIPTIONS = -1; - private static final int ITEM_ID_FEED = -2; - private static final int ITEM_ID_BOOKMARKS = -3; - private static final int ITEM_ID_DOWNLOADS = -4; - private static final int ITEM_ID_HISTORY = -5; - private static final int ITEM_ID_SETTINGS = 0; - private static final int ITEM_ID_ABOUT = 1; + static final int ITEM_ID_BLANK = -1; + static final int ITEM_ID_SETTINGS = -2; + static final int ITEM_ID_ABOUT = -3; + static final int ITEM_ID_BOOKMARKS = -4; + static final int ITEM_ID_FEED = -5; + static final int ITEM_ID_SUBSCRIPTIONS = -6; + static final int ITEM_ID_DOWNLOADS = -7; + static final int ITEM_ID_HISTORY = -8; + static final int ITEM_ID_DEFAULT_KIOSK = -9; + static final int ITEM_ID_KIOSK = -10; + static final int ITEM_ID_CHANNEL = -11; + static final int ITEM_ID_PLAYLIST = -12; private static final int ORDER = 0; @@ -166,44 +178,8 @@ protected void onCreate(final Bundle savedInstanceState) { } private void setupDrawer() throws Exception { - //Tabs - final int currentServiceId = ServiceHelper.getSelectedServiceId(this); - final StreamingService service = NewPipe.getService(currentServiceId); - - int kioskId = 0; - - for (final String ks : service.getKioskList().getAvailableKiosks()) { - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator - .getTranslatedKioskName(ks, this)) - .setIcon(KioskTranslator.getKioskIcon(ks, this)); - kioskId++; - } - - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, - R.string.tab_subscriptions) - .setIcon(R.drawable.ic_tv); - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title) - .setIcon(R.drawable.ic_rss_feed); - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks) - .setIcon(R.drawable.ic_bookmark); - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads) - .setIcon(R.drawable.ic_file_download); - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) - .setIcon(R.drawable.ic_history); - - //Settings and About - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) - .setIcon(R.drawable.ic_settings); - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) - .setIcon(R.drawable.ic_info_outline); + drawerItemManager = DrawerItemManager.getManager(this); + setupDrawerItems(); toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(), toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close); @@ -232,6 +208,184 @@ public void onDrawerClosed(final View drawerView) { setupDrawerHeader(); } + private void setupDrawerItems() { + refreshDrawerItems(); + } + + private void drawerAddItem( + final int group, + final int menuId, + final int titleId, + final int iconRes) { + final String title = getResources().getString(titleId); + + drawerAddItem(group, menuId, title, iconRes); + } + + private void drawerAddItem( + final int group, + final int menuId, + final String title, + final int iconRes) { + drawerLayoutBinding.navigation.getMenu().add( + group, + menuId, + ORDER, + title) + .setIcon(iconRes); + } + + private List getUnusedKioskIdList( + final Map kioskMap, + final List drawerItems) { + final List kioskMapKeys = new ArrayList<>(kioskMap.keySet()); + + for (int i = 0; i < drawerItems.size(); i++) { + final DrawerItem drawerItem = drawerItems.get(i); + if (drawerItem.getDrawerItemId() != ITEM_ID_KIOSK) { + continue; + } + + final DrawerItem.KioskDrawerItem kioskDrawerItem = + (DrawerItem.KioskDrawerItem) drawerItem; + final int kioskServiceId = kioskDrawerItem.getKioskServiceId(); + final int serviceId = ServiceHelper.getSelectedServiceId(this); + if (kioskServiceId != serviceId) { + continue; + } + final String kioskId = kioskDrawerItem.getKioskId(); + kioskMapKeys.remove(kioskId); + } + return kioskMapKeys; + } + + private void fillDynamicDrawerContent() { + final Map kioskMap = getKioskIdsMappedToMenuIds(); + final List unusedKioskIdList = getUnusedKioskIdList(kioskMap, drawerItemList); + final int serviceId = ServiceHelper.getSelectedServiceId(this); + int defaultKioskCount = 0; + + for (int i = 0; i < drawerItemList.size(); i++) { + final DrawerItem drawerItem = drawerItemList.get(i); + + switch (drawerItem.getDrawerItemId()) { + case ITEM_ID_BLANK: + // don't add blank DrawerItems + break; + case ITEM_ID_KIOSK: + final DrawerItem.KioskDrawerItem kioskDrawerItem + = (DrawerItem.KioskDrawerItem) drawerItem; + + if (kioskDrawerItem.getKioskServiceId() != serviceId) { + continue; + } + + drawerAddItem( + R.id.menu_tabs_group, + kioskMap.get(kioskDrawerItem.getKioskId()), + drawerItem.getDrawerItemName(this), + drawerItem.getDrawerItemIconRes(this)); + break; + case ITEM_ID_DEFAULT_KIOSK: + // limits number of visible Kiosks to numbers of actual Kiosks + if (defaultKioskCount >= unusedKioskIdList.size()) { + continue; + } + final String kioskId = unusedKioskIdList.get(defaultKioskCount); + drawerAddItem( + R.id.menu_tabs_group, + kioskMap.get(unusedKioskIdList.get(defaultKioskCount)), + KioskTranslator.getTranslatedKioskName(kioskId, this), + KioskTranslator.getKioskIcon(kioskId, this)); + defaultKioskCount++; + break; + default: + drawerAddItem( + R.id.menu_tabs_group, + drawerItem.getDrawerItemId(), + drawerItem.getDrawerItemName(this), + drawerItem.getDrawerItemIconRes(this)); + } + } + } + + private void fillCantMissingDrawerItems() { + //Settings and About + drawerAddItem( + R.id.menu_options_about_group, + ITEM_ID_SETTINGS, + R.string.settings, + R.drawable.ic_settings); + drawerAddItem( + R.id.menu_options_about_group, + ITEM_ID_ABOUT, + R.string.tab_about, + R.drawable.ic_info_outline); + } + + private void updateVisibilityOfHistroySection() { + final SharedPreferences sharedPreferences = PreferenceManager. + getDefaultSharedPreferences(this); + final boolean isHistoryEnabled = sharedPreferences.getBoolean( + getString(R.string.enable_watch_history_key), true); + final MenuItem menuItem = drawerLayoutBinding + .navigation + .getMenu() + .findItem(ITEM_ID_HISTORY); + if (menuItem != null) { + drawerLayoutBinding.navigation.getMenu().findItem(ITEM_ID_HISTORY) + .setVisible(isHistoryEnabled); + } + } + + private void fillDrawer() { + fillDynamicDrawerContent(); + fillCantMissingDrawerItems(); + updateVisibilityOfHistroySection(); + } + + private void clearDrawer() { + drawerLayoutBinding.navigation.getMenu().clear(); + } + + private void refreshDrawerItems() { + drawerItemList.clear(); + drawerItemList.addAll(drawerItemManager.getDrawerItems()); + + clearDrawer(); + fillDrawer(); + } + + private HashMap getKioskIdsMappedToMenuIds() { + final int serviceId = ServiceHelper.getSelectedServiceId(this); + final StreamingService service; + final HashMap kioskList = new HashMap<>(); + final List ids = getKioskIdList(); + try { + service = NewPipe.getService(serviceId); + for (int i = 0; i < ids.size(); i++) { + final String kioskId = ids.get(i); + kioskList.put(kioskId, i); + } + } catch (final ExtractionException e) { + e.printStackTrace(); + } + return kioskList; + } + + private List getKioskIdList() { + final int serviceId = ServiceHelper.getSelectedServiceId(this); + final StreamingService service; + final List kioskList = new ArrayList<>(); + try { + service = NewPipe.getService(serviceId); + kioskList.addAll(service.getKioskList().getAvailableKiosks()); + } catch (final ExtractionException e) { + e.printStackTrace(); + } + return kioskList; + } + private boolean drawerItemSelected(final MenuItem item) { switch (item.getGroupId()) { case R.id.menu_services_group: @@ -239,7 +393,7 @@ private boolean drawerItemSelected(final MenuItem item) { break; case R.id.menu_tabs_group: try { - tabSelected(item); + drawerItemTabsGroupSelected(item); } catch (final Exception e) { ErrorActivity.reportUiErrorInSnackbar(this, "Selecting main page tab", e); } @@ -265,7 +419,7 @@ private void changeService(final MenuItem item) { .setChecked(true); } - private void tabSelected(final MenuItem item) throws ExtractionException { + private void drawerItemTabsGroupSelected(final MenuItem item) throws ExtractionException { switch (item.getItemId()) { case ITEM_ID_SUBSCRIPTIONS: NavigationHelper.openSubscriptionFragment(getSupportFragmentManager()); @@ -341,7 +495,7 @@ private void toggleServices() { showServices(); } else { try { - showTabs(); + showDrawerItems(); } catch (final Exception e) { ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e); } @@ -414,46 +568,10 @@ public void onNothingSelected(final AdapterView parent) { menuItem.setActionView(spinner); } - private void showTabs() throws ExtractionException { + private void showDrawerItems() throws ExtractionException { drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down); - //Tabs - final int currentServiceId = ServiceHelper.getSelectedServiceId(this); - final StreamingService service = NewPipe.getService(currentServiceId); - - int kioskId = 0; - - for (final String ks : service.getKioskList().getAvailableKiosks()) { - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, kioskId, ORDER, - KioskTranslator.getTranslatedKioskName(ks, this)) - .setIcon(KioskTranslator.getKioskIcon(ks, this)); - kioskId++; - } - - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions) - .setIcon(R.drawable.ic_tv); - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title) - .setIcon(R.drawable.ic_rss_feed); - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks) - .setIcon(R.drawable.ic_bookmark); - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads) - .setIcon(R.drawable.ic_file_download); - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) - .setIcon(R.drawable.ic_history); - - //Settings and About - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) - .setIcon(R.drawable.ic_settings); - drawerLayoutBinding.navigation.getMenu() - .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) - .setIcon(R.drawable.ic_info_outline); + refreshDrawerItems(); } @Override @@ -511,10 +629,7 @@ protected void onResume() { NavigationHelper.openMainActivity(this); } - final boolean isHistoryEnabled = sharedPreferences.getBoolean( - getString(R.string.enable_watch_history_key), true); - drawerLayoutBinding.navigation.getMenu().findItem(ITEM_ID_HISTORY) - .setVisible(isHistoryEnabled); + setupDrawerItems(); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/settings/drawer_items/AddDrawerItemDialog.java b/app/src/main/java/org/schabi/newpipe/settings/drawer_items/AddDrawerItemDialog.java new file mode 100644 index 00000000000..6bc14f7baa9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/drawer_items/AddDrawerItemDialog.java @@ -0,0 +1,99 @@ +package org.schabi.newpipe.settings.drawer_items; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatImageView; + +import org.schabi.newpipe.R; + +public final class AddDrawerItemDialog { + private final AlertDialog dialog; + + public AddDrawerItemDialog(@NonNull final Context context, + @NonNull final ChooseDrawerItemListItem[] items, + @NonNull final DialogInterface.OnClickListener actions) { + + dialog = new AlertDialog.Builder(context) + .setTitle(context.getString(R.string.drawer_item_choose)) + .setAdapter(new DialogListAdapter(context, items), actions) + .create(); + } + + public void show() { + dialog.show(); + } + + public static final class ChooseDrawerItemListItem { + public final int drawerItemId; + final String itemName; + @DrawableRes + final int itemIcon; + + public ChooseDrawerItemListItem(final Context context, final DrawerItem drawerItem) { + this(drawerItem.getDrawerItemId(), drawerItem.getDrawerItemName(context), + drawerItem.getDrawerItemIconRes(context)); + } + + public ChooseDrawerItemListItem(final int drawerItemId, final String itemName, + @DrawableRes final int itemIcon) { + this.drawerItemId = drawerItemId; + this.itemName = itemName; + this.itemIcon = itemIcon; + } + } + + private static final class DialogListAdapter extends BaseAdapter { + private final LayoutInflater inflater; + private final ChooseDrawerItemListItem[] items; + + @DrawableRes + private final int fallbackIcon; + + private DialogListAdapter(final Context context, final ChooseDrawerItemListItem[] items) { + this.inflater = LayoutInflater.from(context); + this.items = items; + this.fallbackIcon = R.drawable.ic_whatshot; + } + + @Override + public int getCount() { + return items.length; + } + + @Override + public ChooseDrawerItemListItem getItem(final int position) { + return items[position]; + } + + @Override + public long getItemId(final int position) { + return getItem(position).drawerItemId; + } + + @Override + public View getView(final int position, final View view, final ViewGroup parent) { + View convertView = view; + if (convertView == null) { + convertView = inflater.inflate(R.layout.list_choose_tabs_dialog, parent, false); + } + + final ChooseDrawerItemListItem item = getItem(position); + final AppCompatImageView drawerItemIconView = convertView.findViewById(R.id.tabIcon); + final TextView drawerItemNameView = convertView.findViewById(R.id.tabName); + + drawerItemIconView.setImageResource(item.itemIcon > 0 ? item.itemIcon : fallbackIcon); + drawerItemNameView.setText(item.itemName); + + return convertView; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/drawer_items/ChooseDrawerItemsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/drawer_items/ChooseDrawerItemsFragment.java new file mode 100644 index 00000000000..460d0f22bbb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/drawer_items/ChooseDrawerItemsFragment.java @@ -0,0 +1,464 @@ +package org.schabi.newpipe.settings.drawer_items; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.error.ErrorActivity; +import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.settings.SelectChannelFragment; +import org.schabi.newpipe.settings.SelectKioskFragment; +import org.schabi.newpipe.settings.SelectPlaylistFragment; +import org.schabi.newpipe.util.ThemeHelper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ChooseDrawerItemsFragment extends Fragment { + private static final int MENU_ITEM_RESTORE_ID = 123456; + + private DrawerItemManager drawerItemManager; + + private final List drawerItemList = new ArrayList<>(); + private SelectedDrawerItemsAdapter selectedDrawerItemsAdapter; + + /*////////////////////////////////////////////////////////////////////////// + // Lifecycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate(final @Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + drawerItemManager = DrawerItemManager.getManager(requireContext()); + updateDrawerItemList(); + + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_choose_tabs, container, false); + } + + @Override + public void onViewCreated(@NonNull final View rootView, + @Nullable final Bundle savedInstanceState) { + super.onViewCreated(rootView, savedInstanceState); + + initButton(rootView); + + final RecyclerView listSelectedDrawerItems = rootView.findViewById(R.id.selectedTabs); + listSelectedDrawerItems.setLayoutManager(new LinearLayoutManager(requireContext())); + + final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(listSelectedDrawerItems); + + selectedDrawerItemsAdapter = + new SelectedDrawerItemsAdapter(requireContext(), itemTouchHelper); + listSelectedDrawerItems.setAdapter(selectedDrawerItemsAdapter); + } + + @Override + public void onResume() { + super.onResume(); + updateTitle(); + } + + @Override + public void onPause() { + super.onPause(); + saveChanges(); + } + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, + R.string.restore_defaults); + restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), + R.drawable.ic_settings_backup_restore)); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == MENU_ITEM_RESTORE_ID) { + restoreDefaults(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void updateDrawerItemList() { + drawerItemList.clear(); + drawerItemList.addAll(drawerItemManager.getDrawerItems()); + } + + private void initButton(final View rootView) { + final FloatingActionButton fab = rootView.findViewById(R.id.addTabsButton); + fab.setOnClickListener(v -> { + final AddDrawerItemDialog.ChooseDrawerItemListItem[] availabledrawerItems = + getAvailableDrawerItems(requireContext()); + + if (availabledrawerItems.length == 0) { + return; + } + + final Dialog.OnClickListener actionListener = ((dialog, which) -> { + final AddDrawerItemDialog.ChooseDrawerItemListItem selected + = availabledrawerItems[which]; + addDrawerItem(selected.drawerItemId); + }); + + new AddDrawerItemDialog(requireContext(), availabledrawerItems, actionListener).show(); + }); + } + + private void updateTitle() { + if (getActivity() instanceof AppCompatActivity) { + final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.drawer_content); + } + } + } + + private void restoreDefaults() { + new AlertDialog.Builder(requireContext(), ThemeHelper.getDialogTheme(requireContext())) + .setTitle(R.string.restore_defaults) + .setMessage(R.string.restore_defaults_confirmation) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.yes, (dialog, which) -> { + drawerItemManager.resetDrawerItems(); + updateDrawerItemList(); + selectedDrawerItemsAdapter.notifyDataSetChanged(); + }) + .show(); + } + + private void addDrawerItem(final DrawerItem drawerItem) { + if (drawerItemList.get(0).getDrawerItemId() == DrawerItem.ITEM_ID_BLANK) { + drawerItemList.remove(0); + } + drawerItemList.add(drawerItem); + selectedDrawerItemsAdapter.notifyDataSetChanged(); + } + + private void addDrawerItem(final int drawerItemId) { + final DrawerItem.Type type = DrawerItem.typeFrom(drawerItemId); + + if (type == null) { + ErrorActivity.reportErrorInSnackbar(requireContext(), + new ErrorInfo( + new IllegalStateException("DrawerItem id not found: " + drawerItemId), + UserAction.SOMETHING_ELSE, "Choosing DrawerItems on settings")); + return; + } + + switch (type) { + case KIOSK: + final SelectKioskFragment selectKioskFragment = new SelectKioskFragment(); + selectKioskFragment.setOnSelectedListener((serviceId, kioskId, kioskName) -> + addDrawerItem(new DrawerItem.KioskDrawerItem(serviceId, kioskId))); + selectKioskFragment.show(getParentFragmentManager(), "select_kiosk"); + return; + case CHANNEL: + final SelectChannelFragment selectChannelFragment = new SelectChannelFragment(); + selectChannelFragment.setOnSelectedListener((serviceId, url, name) -> + addDrawerItem(new DrawerItem.ChannelDrawerItem(serviceId, url, name))); + selectChannelFragment.show(getParentFragmentManager(), "select_channel"); + return; + case PLAYLIST: + final SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment(); + selectPlaylistFragment.setOnSelectedListener( + new SelectPlaylistFragment.OnSelectedListener() { + @Override + public void onLocalPlaylistSelected(final long id, final String name) { + addDrawerItem(new DrawerItem.PlaylistDrawerItem(id, name)); + } + + @Override + public void onRemotePlaylistSelected( + final int serviceId, final String url, final String name) { + addDrawerItem( + new DrawerItem.PlaylistDrawerItem(serviceId, url, name)); + } + }); + selectPlaylistFragment.show(getParentFragmentManager(), "select_playlist"); + return; + default: + addDrawerItem(type.getDrawerItem()); + break; + } + } + + private void saveChanges() { + drawerItemManager.saveDrawerItems(drawerItemList); + } + + private AddDrawerItemDialog.ChooseDrawerItemListItem[] getAvailableDrawerItems( + final Context context) { + final ArrayList returnList + = new ArrayList<>(); + + for (final DrawerItem.Type type : DrawerItem.Type.values()) { + final DrawerItem drawerItem = type.getDrawerItem(); + switch (type) { + case BLANK: + //dont show blank pages + break; + case DOWNLOADS: + returnList.add(new AddDrawerItemDialog.ChooseDrawerItemListItem( + drawerItem.getDrawerItemId(), + getString(R.string.download), + drawerItem.getDrawerItemIconRes(context))); + break; + case KIOSK: + returnList.add(new AddDrawerItemDialog.ChooseDrawerItemListItem( + drawerItem.getDrawerItemId(), + getString(R.string.kiosk_page_summary), + R.drawable.ic_whatshot)); + break; + case CHANNEL: + returnList.add(new AddDrawerItemDialog.ChooseDrawerItemListItem( + drawerItem.getDrawerItemId(), + getString(R.string.channel_page_summary), + drawerItem.getDrawerItemIconRes(context))); + break; + case DEFAULT_KIOSK: + returnList.add(new AddDrawerItemDialog.ChooseDrawerItemListItem( + drawerItem.getDrawerItemId(), + getString(R.string.default_kiosk_page_summary), + R.drawable.ic_whatshot)); + break; + case PLAYLIST: + returnList.add(new AddDrawerItemDialog.ChooseDrawerItemListItem( + drawerItem.getDrawerItemId(), + getString(R.string.playlist_page_summary), + drawerItem.getDrawerItemIconRes(context))); + break; + default: + if (!drawerItemList.contains(drawerItem)) { + returnList.add( + new AddDrawerItemDialog + .ChooseDrawerItemListItem(context, drawerItem)); + } + break; + } + } + + return returnList.toArray(new AddDrawerItemDialog.ChooseDrawerItemListItem[0]); + } + + /*////////////////////////////////////////////////////////////////////////// + // List Handling + //////////////////////////////////////////////////////////////////////////*/ + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, + ItemTouchHelper.START | ItemTouchHelper.END) { + @Override + public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView, + final int viewSize, + final int viewSizeOutOfBounds, + final int totalSize, + final long msSinceStartScroll) { + final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, + viewSizeOutOfBounds, totalSize, msSinceStartScroll); + final int minimumAbsVelocity = Math.max(12, + Math.abs(standardSpeed)); + return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); + } + + @Override + public boolean onMove(final RecyclerView recyclerView, + final RecyclerView.ViewHolder source, + final RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType() + || selectedDrawerItemsAdapter == null) { + return false; + } + + final int sourceIndex = source.getAdapterPosition(); + final int targetIndex = target.getAdapterPosition(); + selectedDrawerItemsAdapter.swapItems(sourceIndex, targetIndex); + return true; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return true; + } + + @Override + public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) { + final int position = viewHolder.getAdapterPosition(); + drawerItemList.remove(position); + selectedDrawerItemsAdapter.notifyItemRemoved(position); + + if (drawerItemList.isEmpty()) { + drawerItemList.add(DrawerItem.Type.BLANK.getDrawerItem()); + selectedDrawerItemsAdapter.notifyItemInserted(0); + } + } + }; + } + + private class SelectedDrawerItemsAdapter + extends RecyclerView.Adapter { + private final LayoutInflater inflater; + private ItemTouchHelper itemTouchHelper; + + SelectedDrawerItemsAdapter(final Context context, final ItemTouchHelper itemTouchHelper) { + this.itemTouchHelper = itemTouchHelper; + this.inflater = LayoutInflater.from(context); + } + + public void swapItems(final int fromPosition, final int toPosition) { + Collections.swap(drawerItemList, fromPosition, toPosition); + notifyItemMoved(fromPosition, toPosition); + } + + @NonNull + @Override + public SelectedDrawerItemsAdapter.TabViewHolder onCreateViewHolder( + @NonNull final ViewGroup parent, final int viewType) { + final View view = inflater.inflate(R.layout.list_choose_tabs, parent, false); + return new SelectedDrawerItemsAdapter.TabViewHolder(view); + } + + @Override + public void onBindViewHolder( + @NonNull final SelectedDrawerItemsAdapter.TabViewHolder holder, + final int position) { + holder.bind(position, holder); + } + + @Override + public int getItemCount() { + return drawerItemList.size(); + } + + class TabViewHolder extends RecyclerView.ViewHolder { + private AppCompatImageView drawerItemIconView; + private TextView drawerItemNameView; + private ImageView handle; + + TabViewHolder(final View itemView) { + super(itemView); + + drawerItemNameView = itemView.findViewById(R.id.tabName); + drawerItemIconView = itemView.findViewById(R.id.tabIcon); + handle = itemView.findViewById(R.id.handle); + } + + @SuppressLint("ClickableViewAccessibility") + void bind(final int position, final TabViewHolder holder) { + handle.setOnTouchListener(getOnTouchListener(holder)); + + final DrawerItem drawerItem = drawerItemList.get(position); + final DrawerItem.Type type = DrawerItem.typeFrom(drawerItem.getDrawerItemId()); + + if (type == null) { + return; + } + + final String drawerItemName; + switch (type) { + case BLANK: + drawerItemName = getString(R.string.blank_page_summary); + break; + case DEFAULT_KIOSK: + drawerItemName = getString(R.string.default_kiosk_page_summary); + break; + case KIOSK: + drawerItemName = + NewPipe.getNameOfService(((DrawerItem.KioskDrawerItem) drawerItem) + .getKioskServiceId()) + "/" + + drawerItem.getDrawerItemName(requireContext()); + break; + case CHANNEL: + drawerItemName = + NewPipe.getNameOfService(((DrawerItem.ChannelDrawerItem) drawerItem) + .getChannelServiceId()) + "/" + + drawerItem.getDrawerItemName(requireContext()); + break; + case PLAYLIST: + final int serviceId = ((DrawerItem.PlaylistDrawerItem) drawerItem) + .getPlaylistServiceId(); + final String serviceName = serviceId == -1 + ? getString(R.string.local) + : NewPipe.getNameOfService(serviceId); + drawerItemName = + serviceName + "/" + drawerItem.getDrawerItemName(requireContext()); + break; + default: + drawerItemName = drawerItem.getDrawerItemName(requireContext()); + break; + } + + drawerItemNameView.setText(drawerItemName); + drawerItemIconView.setImageResource( + drawerItem.getDrawerItemIconRes(requireContext())); + } + + @SuppressLint("ClickableViewAccessibility") + private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) { + return (view, motionEvent) -> { + if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + if (itemTouchHelper != null && getItemCount() > 1) { + itemTouchHelper.startDrag(item); + return true; + } + } + return false; + }; + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/drawer_items/DrawerItem.java b/app/src/main/java/org/schabi/newpipe/settings/drawer_items/DrawerItem.java new file mode 100644 index 00000000000..f3259463aa7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/drawer_items/DrawerItem.java @@ -0,0 +1,645 @@ +package org.schabi.newpipe.settings.drawer_items; + +import android.content.Context; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonSink; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.fragments.BlankFragment; +import org.schabi.newpipe.fragments.list.channel.ChannelFragment; +import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment; +import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; +import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; +import org.schabi.newpipe.local.bookmark.BookmarkFragment; +import org.schabi.newpipe.local.feed.FeedFragment; +import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; +import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; +import org.schabi.newpipe.local.subscription.SubscriptionFragment; +import org.schabi.newpipe.error.ErrorActivity; +import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.util.KioskTranslator; +import org.schabi.newpipe.util.ServiceHelper; + +import java.util.Objects; + +public abstract class DrawerItem { + + // this HAS TO be the same as in MainActivity.java + static final int ITEM_ID_BLANK = -1; + static final int ITEM_ID_SETTINGS = -2; + static final int ITEM_ID_ABOUT = -3; + static final int ITEM_ID_BOOKMARKS = -4; + static final int ITEM_ID_FEED = -5; + static final int ITEM_ID_SUBSCRIPTIONS = -6; + static final int ITEM_ID_DOWNLOADS = -7; + static final int ITEM_ID_HISTORY = -8; + static final int ITEM_ID_DEFAULT_KIOSK = -9; + static final int ITEM_ID_KIOSK = -10; + static final int ITEM_ID_CHANNEL = -11; + static final int ITEM_ID_PLAYLIST = -12; + + private static final String JSON_DRAWER_ITEM_ID_KEY = "drawer_item_id"; + + DrawerItem() { } + + DrawerItem(@NonNull final JsonObject jsonObject) { + readDataFromJson(jsonObject); + } + + /*////////////////////////////////////////////////////////////////////////// + // DrawerItem Handling + //////////////////////////////////////////////////////////////////////////*/ + + @Nullable + public static DrawerItem from(@NonNull final JsonObject jsonObject) { + final int drawerItemId = jsonObject.getInt(DrawerItem.JSON_DRAWER_ITEM_ID_KEY, -1); + + return from(drawerItemId, jsonObject); + } + + @Nullable + public static DrawerItem from(final int drawerItemId) { + return from(drawerItemId, null); + } + + @Nullable + public static DrawerItem.Type typeFrom(final int drawerItemId) { + for (final DrawerItem.Type available : DrawerItem.Type.values()) { + if (available.getDrawerItemId() == drawerItemId) { + return available; + } + } + return null; + } + + @Nullable + private static DrawerItem from(final int drawerItemId, @Nullable final JsonObject jsonObject) { + final DrawerItem.Type type = typeFrom(drawerItemId); + + if (type == null) { + return null; + } + + if (jsonObject != null) { + switch (type) { + case KIOSK: + return new KioskDrawerItem(jsonObject); + case CHANNEL: + return new ChannelDrawerItem(jsonObject); + case PLAYLIST: + return new PlaylistDrawerItem(jsonObject); + } + } + + return type.getDrawerItem(); + } + + public abstract int getDrawerItemId(); + + public abstract String getDrawerItemName(Context context); + + @DrawableRes + public abstract int getDrawerItemIconRes(Context context); + + /** + * Return a instance of the fragment that this DrawerItem represent. + * + * @param context Android app context + * @return the fragment this DrawerItem represents + */ + public abstract Fragment getFragment(Context context) throws ExtractionException; + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + + return obj instanceof DrawerItem && obj.getClass().equals(this.getClass()) + && ((DrawerItem) obj).getDrawerItemId() == this.getDrawerItemId(); + } + + /*////////////////////////////////////////////////////////////////////////// + // JSON Handling + //////////////////////////////////////////////////////////////////////////*/ + + public void writeJsonOn(final JsonSink jsonSink) { + jsonSink.object(); + + jsonSink.value(JSON_DRAWER_ITEM_ID_KEY, getDrawerItemId()); + writeDataToJson(jsonSink); + + jsonSink.end(); + } + + protected void writeDataToJson(final JsonSink writerSink) { + // No-op + } + + protected void readDataFromJson(final JsonObject jsonObject) { + // No-op + } + + /*////////////////////////////////////////////////////////////////////////// + // Implementations + //////////////////////////////////////////////////////////////////////////*/ + + public enum Type { + BLANK(new BlankDrawerItem()), + DOWNLOADS(new DownloadDrawerItem()), + DEFAULT_KIOSK(new DefaultKioskDrawerItem()), + SUBSCRIPTIONS(new SubscriptionsDrawerItem()), + FEED(new FeedDrawerItem()), + BOOKMARKS(new BookmarksDrawerItem()), + HISTORY(new HistoryDrawerItem()), + KIOSK(new KioskDrawerItem()), + CHANNEL(new ChannelDrawerItem()), + PLAYLIST(new PlaylistDrawerItem()); + + private DrawerItem drawerItem; + + Type(final DrawerItem drawerItem) { + this.drawerItem = drawerItem; + } + + public int getDrawerItemId() { + return drawerItem.getDrawerItemId(); + } + + public DrawerItem getDrawerItem() { + return drawerItem; + } + } + + public static class BlankDrawerItem extends DrawerItem { + public static final int ID = ITEM_ID_BLANK; + + @Override + public int getDrawerItemId() { + return ID; + } + + @Override + public String getDrawerItemName(final Context context) { + return "NewPipe"; //context.getString(R.string.blank_page_summary); + } + + @DrawableRes + @Override + public int getDrawerItemIconRes(final Context context) { + return R.drawable.ic_crop_portrait; + } + + @Override + public BlankFragment getFragment(final Context context) { + return new BlankFragment(); + } + } + + public static class SubscriptionsDrawerItem extends DrawerItem { + public static final int ID = ITEM_ID_SUBSCRIPTIONS; + + @Override + public int getDrawerItemId() { + return ID; + } + + @Override + public String getDrawerItemName(final Context context) { + return context.getString(R.string.tab_subscriptions); + } + + @DrawableRes + @Override + public int getDrawerItemIconRes(final Context context) { + return R.drawable.ic_tv; + } + + @Override + public SubscriptionFragment getFragment(final Context context) { + return new SubscriptionFragment(); + } + } + + public static class DownloadDrawerItem extends DrawerItem { + public static final int ID = ITEM_ID_DOWNLOADS; + + @Override + public int getDrawerItemId() { + return ID; + } + + @Override + public String getDrawerItemName(final Context context) { + return context.getString(R.string.download); + } + + @Override + public int getDrawerItemIconRes(final Context context) { + return R.drawable.ic_file_download; + } + + @Override + public Fragment getFragment(final Context context) { + return new FeedFragment(); + } + } + + public static class FeedDrawerItem extends DrawerItem { + public static final int ID = ITEM_ID_FEED; + + @Override + public int getDrawerItemId() { + return ID; + } + + @Override + public String getDrawerItemName(final Context context) { + return context.getString(R.string.fragment_feed_title); + } + + @DrawableRes + @Override + public int getDrawerItemIconRes(final Context context) { + return R.drawable.ic_rss_feed; + } + + @Override + public FeedFragment getFragment(final Context context) { + return new FeedFragment(); + } + } + + public static class BookmarksDrawerItem extends DrawerItem { + public static final int ID = ITEM_ID_BOOKMARKS; + + @Override + public int getDrawerItemId() { + return ID; + } + + @Override + public String getDrawerItemName(final Context context) { + return context.getString(R.string.tab_bookmarks); + } + + @DrawableRes + @Override + public int getDrawerItemIconRes(final Context context) { + return R.drawable.ic_bookmark; + } + + @Override + public BookmarkFragment getFragment(final Context context) { + return new BookmarkFragment(); + } + } + + public static class HistoryDrawerItem extends DrawerItem { + public static final int ID = ITEM_ID_HISTORY; + + @Override + public int getDrawerItemId() { + return ID; + } + + @Override + public String getDrawerItemName(final Context context) { + return context.getString(R.string.title_activity_history); + } + + @DrawableRes + @Override + public int getDrawerItemIconRes(final Context context) { + return R.drawable.ic_history; + } + + @Override + public StatisticsPlaylistFragment getFragment(final Context context) { + return new StatisticsPlaylistFragment(); + } + } + + public static class KioskDrawerItem extends DrawerItem { + public static final int ID = ITEM_ID_KIOSK; + private static final String JSON_KIOSK_SERVICE_ID_KEY = "service_id"; + private static final String JSON_KIOSK_ID_KEY = "kiosk_id"; + private int kioskServiceId; + private String kioskId; + + private KioskDrawerItem() { + this(-1, ""); + } + + public KioskDrawerItem(final int kioskServiceId, final String kioskId) { + this.kioskServiceId = kioskServiceId; + this.kioskId = kioskId; + } + + public KioskDrawerItem(final JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public int getDrawerItemId() { + return ID; + } + + @Override + public String getDrawerItemName(final Context context) { + return KioskTranslator.getTranslatedKioskName(kioskId, context); + } + + @DrawableRes + @Override + public int getDrawerItemIconRes(final Context context) { + final int kioskIcon = KioskTranslator.getKioskIcon(kioskId, context); + + if (kioskIcon <= 0) { + throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\""); + } + + return kioskIcon; + } + + @Override + public KioskFragment getFragment(final Context context) throws ExtractionException { + return KioskFragment.getInstance(kioskServiceId, kioskId); + } + + @Override + protected void writeDataToJson(final JsonSink writerSink) { + writerSink.value(JSON_KIOSK_SERVICE_ID_KEY, kioskServiceId) + .value(JSON_KIOSK_ID_KEY, kioskId); + } + + @Override + protected void readDataFromJson(final JsonObject jsonObject) { + kioskServiceId = jsonObject.getInt(JSON_KIOSK_SERVICE_ID_KEY, -1); + kioskId = jsonObject.getString(JSON_KIOSK_ID_KEY, ""); + } + + @Override + public boolean equals(final Object obj) { + return super.equals(obj) && kioskServiceId == ((KioskDrawerItem) obj).kioskServiceId + && Objects.equals(kioskId, ((KioskDrawerItem) obj).kioskId); + } + + public int getKioskServiceId() { + return kioskServiceId; + } + + public String getKioskId() { + return kioskId; + } + } + + public static class ChannelDrawerItem extends DrawerItem { + public static final int ID = ITEM_ID_CHANNEL; + private static final String JSON_CHANNEL_SERVICE_ID_KEY = "channel_service_id"; + private static final String JSON_CHANNEL_URL_KEY = "channel_url"; + private static final String JSON_CHANNEL_NAME_KEY = "channel_name"; + private int channelServiceId; + private String channelUrl; + private String channelName; + + private ChannelDrawerItem() { + this(-1, "", ""); + } + + public ChannelDrawerItem(final int channelServiceId, final String channelUrl, + final String channelName) { + this.channelServiceId = channelServiceId; + this.channelUrl = channelUrl; + this.channelName = channelName; + } + + public ChannelDrawerItem(final JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public int getDrawerItemId() { + return ID; + } + + @Override + public String getDrawerItemName(final Context context) { + return channelName; + } + + @DrawableRes + @Override + public int getDrawerItemIconRes(final Context context) { + return R.drawable.ic_tv; + } + + @Override + public ChannelFragment getFragment(final Context context) { + return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName); + } + + @Override + protected void writeDataToJson(final JsonSink writerSink) { + writerSink.value(JSON_CHANNEL_SERVICE_ID_KEY, channelServiceId) + .value(JSON_CHANNEL_URL_KEY, channelUrl) + .value(JSON_CHANNEL_NAME_KEY, channelName); + } + + @Override + protected void readDataFromJson(final JsonObject jsonObject) { + channelServiceId = jsonObject.getInt(JSON_CHANNEL_SERVICE_ID_KEY, -1); + channelUrl = jsonObject.getString(JSON_CHANNEL_URL_KEY, ""); + channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, ""); + } + + @Override + public boolean equals(final Object obj) { + return super.equals(obj) + && channelServiceId == ((ChannelDrawerItem) obj).channelServiceId + && Objects.equals(channelUrl, ((ChannelDrawerItem) obj).channelUrl) + && Objects.equals(channelName, ((ChannelDrawerItem) obj).channelName); + } + + public int getChannelServiceId() { + return channelServiceId; + } + + public String getChannelUrl() { + return channelUrl; + } + + public String getChannelName() { + return channelName; + } + } + + public static class DefaultKioskDrawerItem extends DrawerItem { + public static final int ID = ITEM_ID_DEFAULT_KIOSK; + + @Override + public int getDrawerItemId() { + return ID; + } + + @Override + public String getDrawerItemName(final Context context) { + return KioskTranslator.getTranslatedKioskName(getDefaultKioskId(context), context); + } + + @DrawableRes + @Override + public int getDrawerItemIconRes(final Context context) { + return KioskTranslator.getKioskIcon(getDefaultKioskId(context), context); + } + + @Override + public DefaultKioskFragment getFragment(final Context context) { + return new DefaultKioskFragment(); + } + + public String getDefaultKioskId(final Context context) { + final int kioskServiceId = ServiceHelper.getSelectedServiceId(context); + + String kioskId = ""; + try { + final StreamingService service = NewPipe.getService(kioskServiceId); + kioskId = service.getKioskList().getDefaultKioskId(); + } catch (final ExtractionException e) { + ErrorActivity.reportErrorInSnackbar(context, new ErrorInfo(e, + UserAction.REQUESTED_KIOSK, "Loading default kiosk for selected service")); + } + return kioskId; + } + } + + public static class PlaylistDrawerItem extends DrawerItem { + public static final int ID = ITEM_ID_PLAYLIST; + private static final String JSON_PLAYLIST_SERVICE_ID_KEY = "playlist_service_id"; + private static final String JSON_PLAYLIST_URL_KEY = "playlist_url"; + private static final String JSON_PLAYLIST_NAME_KEY = "playlist_name"; + private static final String JSON_PLAYLIST_ID_KEY = "playlist_id"; + private static final String JSON_PLAYLIST_TYPE_KEY = "playlist_type"; + private int playlistServiceId; + private String playlistUrl; + private String playlistName; + private long playlistId; + private LocalItem.LocalItemType playlistType; + + private PlaylistDrawerItem() { + this(-1, ""); + } + + public PlaylistDrawerItem(final long playlistId, final String playlistName) { + this.playlistName = playlistName; + this.playlistId = playlistId; + this.playlistType = LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM; + this.playlistServiceId = -1; + this.playlistUrl = ""; + } + + public PlaylistDrawerItem(final int playlistServiceId, final String playlistUrl, + final String playlistName) { + this.playlistServiceId = playlistServiceId; + this.playlistUrl = playlistUrl; + this.playlistName = playlistName; + this.playlistType = LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM; + this.playlistId = -1; + } + + public PlaylistDrawerItem(final JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public int getDrawerItemId() { + return ID; + } + + @Override + public String getDrawerItemName(final Context context) { + return playlistName; + } + + @DrawableRes + @Override + public int getDrawerItemIconRes(final Context context) { + return R.drawable.ic_bookmark; + } + + @Override + public Fragment getFragment(final Context context) { + if (playlistType == LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM) { + return LocalPlaylistFragment.getInstance(playlistId, playlistName); + + } else { // playlistType == LocalItemType.PLAYLIST_REMOTE_ITEM + return PlaylistFragment.getInstance(playlistServiceId, playlistUrl, playlistName); + } + } + + @Override + protected void writeDataToJson(final JsonSink writerSink) { + writerSink.value(JSON_PLAYLIST_SERVICE_ID_KEY, playlistServiceId) + .value(JSON_PLAYLIST_URL_KEY, playlistUrl) + .value(JSON_PLAYLIST_NAME_KEY, playlistName) + .value(JSON_PLAYLIST_ID_KEY, playlistId) + .value(JSON_PLAYLIST_TYPE_KEY, playlistType.toString()); + } + + @Override + protected void readDataFromJson(final JsonObject jsonObject) { + playlistServiceId = jsonObject.getInt(JSON_PLAYLIST_SERVICE_ID_KEY, -1); + playlistUrl = jsonObject.getString(JSON_PLAYLIST_URL_KEY, ""); + playlistName = jsonObject.getString(JSON_PLAYLIST_NAME_KEY, ""); + playlistId = jsonObject.getInt(JSON_PLAYLIST_ID_KEY, -1); + playlistType = LocalItem.LocalItemType.valueOf( + jsonObject.getString(JSON_PLAYLIST_TYPE_KEY, + LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM.toString()) + ); + } + + @Override + public boolean equals(final Object obj) { + if (!(super.equals(obj) + && Objects.equals(playlistType, ((PlaylistDrawerItem) obj).playlistType) + && Objects.equals(playlistName, ((PlaylistDrawerItem) obj).playlistName))) { + return false; // base objects are different + } + + return (playlistId == ((PlaylistDrawerItem) obj).playlistId) // local + || (playlistServiceId == ((PlaylistDrawerItem) obj).playlistServiceId // remote + && Objects.equals(playlistUrl, ((PlaylistDrawerItem) obj).playlistUrl)); + } + + public int getPlaylistServiceId() { + return playlistServiceId; + } + + public String getPlaylistUrl() { + return playlistUrl; + } + + public String getPlaylistName() { + return playlistName; + } + + public long getPlaylistId() { + return playlistId; + } + + public LocalItem.LocalItemType getPlaylistType() { + return playlistType; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/drawer_items/DrawerItemManager.java b/app/src/main/java/org/schabi/newpipe/settings/drawer_items/DrawerItemManager.java new file mode 100644 index 00000000000..61bdbee6729 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/drawer_items/DrawerItemManager.java @@ -0,0 +1,88 @@ +package org.schabi.newpipe.settings.drawer_items; + +import android.content.Context; +import android.content.SharedPreferences; +import android.widget.Toast; + +import androidx.preference.PreferenceManager; + +import org.schabi.newpipe.R; + +import java.util.List; + +public final class DrawerItemManager { + private final SharedPreferences sharedPreferences; + private final String savedDrawerItemsKey; + private final Context context; + private SavedDrawerItemsChangeListener savedDrawerItemsChangeListener; + private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; + + private DrawerItemManager(final Context context) { + this.context = context; + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + this.savedDrawerItemsKey = context.getString(R.string.saved_drawer_items_key); + } + + public static DrawerItemManager getManager(final Context context) { + return new DrawerItemManager(context); + } + + public void saveDrawerItems(final List drawerItemList) { + final String jsonToSave = DrawerItemsJsonHelper.getJsonToSave(drawerItemList); + sharedPreferences.edit().putString(savedDrawerItemsKey, jsonToSave).apply(); + } + + public List getDrawerItems() { + final String savedJson = sharedPreferences.getString(savedDrawerItemsKey, null); + try { + return DrawerItemsJsonHelper.getDawerItemsFromJson(savedJson); + } catch (final DrawerItemsJsonHelper.InvalidJsonException e) { + Toast.makeText(context, R.string.saved_drawer_items_invalid_json, Toast.LENGTH_SHORT) + .show(); + return getDefaultDrawerItems(); + } + } + + public void resetDrawerItems() { + sharedPreferences.edit().remove(savedDrawerItemsKey).apply(); + } + + public List getDefaultDrawerItems() { + return DrawerItemsJsonHelper.getDefaultDrawerItems(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Listener + //////////////////////////////////////////////////////////////////////////*/ + + public void setSavedDrawerItemsListener(final SavedDrawerItemsChangeListener listener) { + if (preferenceChangeListener != null) { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener); + } + savedDrawerItemsChangeListener = listener; + preferenceChangeListener = getPreferenceChangeListener(); + sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener); + } + + public void unsetSavedDrawerItemsListener() { + if (preferenceChangeListener != null) { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener); + } + savedDrawerItemsChangeListener = null; + preferenceChangeListener = null; + } + + private SharedPreferences.OnSharedPreferenceChangeListener getPreferenceChangeListener() { + return (sp, key) -> { + if (key.equals(savedDrawerItemsKey)) { + if (savedDrawerItemsChangeListener != null) { + savedDrawerItemsChangeListener.onDrawerItemsChanged(); + } + } + }; + } + + public interface SavedDrawerItemsChangeListener { + void onDrawerItemsChanged(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/drawer_items/DrawerItemsJsonHelper.java b/app/src/main/java/org/schabi/newpipe/settings/drawer_items/DrawerItemsJsonHelper.java new file mode 100644 index 00000000000..74951c26617 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/drawer_items/DrawerItemsJsonHelper.java @@ -0,0 +1,128 @@ +package org.schabi.newpipe.settings.drawer_items; + +import androidx.annotation.Nullable; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import com.grack.nanojson.JsonStringWriter; +import com.grack.nanojson.JsonWriter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public final class DrawerItemsJsonHelper { + private static final String JSON_DRAWER_ITEMS_ARRAY_KEY = "drawer_items"; + + private static final List FALLBACK_INITIAL_DRAWER_ITEM_LIST + = Collections.unmodifiableList( + Arrays.asList( + // we need that many to show all a available kiosks + DrawerItem.Type.DEFAULT_KIOSK.getDrawerItem(), + DrawerItem.Type.DEFAULT_KIOSK.getDrawerItem(), + DrawerItem.Type.DEFAULT_KIOSK.getDrawerItem(), + DrawerItem.Type.DEFAULT_KIOSK.getDrawerItem(), + DrawerItem.Type.SUBSCRIPTIONS.getDrawerItem(), + DrawerItem.Type.FEED.getDrawerItem(), + DrawerItem.Type.BOOKMARKS.getDrawerItem(), + DrawerItem.Type.DOWNLOADS.getDrawerItem(), + DrawerItem.Type.HISTORY.getDrawerItem())); + + private DrawerItemsJsonHelper() { } + /** + * Try to reads the passed JSON and returns the list of DrawerItems if no error were + * encountered. + *

+ * If the JSON is null or empty, or the list of DrawerItems that it represents is empty, the + * {@link #getDefaultDrawerItems fallback list} will be returned. + *

+ * DrawerItems with invalid ids (i.e. not in the {@link DrawerItem.Type} enum) will be ignored. + * + * @param drawerItemsJson a JSON string got from {@link #getJsonToSave(List)}. + * @return a list of {@link DrawerItem drawerItems}. + * @throws DrawerItemsJsonHelper.InvalidJsonException if the JSON string is not valid + */ + public static List getDawerItemsFromJson(@Nullable final String drawerItemsJson) + throws InvalidJsonException { + if (drawerItemsJson == null || drawerItemsJson.isEmpty()) { + return getDefaultDrawerItems(); + } + + final List returnDrawerItems = new ArrayList<>(); + + final JsonObject outerJsonObject; + try { + outerJsonObject = JsonParser.object().from(drawerItemsJson); + + if (!outerJsonObject.has(JSON_DRAWER_ITEMS_ARRAY_KEY)) { + throw new InvalidJsonException( + "JSON doesn't contain \"" + JSON_DRAWER_ITEMS_ARRAY_KEY + "\" array"); + } + + final JsonArray tabsArray = outerJsonObject.getArray(JSON_DRAWER_ITEMS_ARRAY_KEY); + + for (final Object o : tabsArray) { + if (!(o instanceof JsonObject)) { + continue; + } + + final DrawerItem drawerItem = DrawerItem.from((JsonObject) o); + + if (drawerItem != null) { + returnDrawerItems.add(drawerItem); + } + } + } catch (final JsonParserException e) { + throw new InvalidJsonException(e); + } + + if (returnDrawerItems.isEmpty()) { + return getDefaultDrawerItems(); + } + + return returnDrawerItems; + } + + /** + * Get a JSON representation from a list of tabs. + * + * @param drawerItemList a list of {@link DrawerItem drawerItem}. + * @return a JSON string representing the list of drawerItems + */ + public static String getJsonToSave(@Nullable final List drawerItemList) { + final JsonStringWriter jsonWriter = JsonWriter.string(); + jsonWriter.object(); + + jsonWriter.array(JSON_DRAWER_ITEMS_ARRAY_KEY); + if (drawerItemList != null) { + for (final DrawerItem tab : drawerItemList) { + tab.writeJsonOn(jsonWriter); + } + } + jsonWriter.end(); + + jsonWriter.end(); + return jsonWriter.done(); + } + + public static List getDefaultDrawerItems() { + return FALLBACK_INITIAL_DRAWER_ITEM_LIST; + } + + public static final class InvalidJsonException extends Exception { + private InvalidJsonException() { + super(); + } + + private InvalidJsonException(final String message) { + super(message); + } + + private InvalidJsonException(final Throwable cause) { + super(cause); + } + } +} diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 769a31b5205..97edef4800b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -157,6 +157,7 @@ 履歴 履歴 このアイテムを検索履歴から削除しますか? + パネルのコンテンツ メインページのコンテンツ 空白ページ Kioskページ @@ -380,6 +381,7 @@ アプリの更新通知 外部 SD カードにダウンロードできません。ダウンロードフォルダーの場所をリセットしますか\? 保存されたタブを読み込めないため、デフォルトのタブを使用します + パネルに表示されるタブ メインページに表示されるタブ 新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します 従量制課金ネットワークの割り込み diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 9261dfae166..ad40a50444c 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -11,6 +11,7 @@ service @string/youtube + saved_sections_key saved_tabs_key @@ -251,6 +252,7 @@ youtube_restricted_mode_enabled enable_search_history enable_watch_history + panel_content main_page_content enable_playback_resume enable_playback_state_lists diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 225ccd126f9..6016bea4e1d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,6 +34,7 @@ Subscriptions Bookmarked Playlists Choose Tab + Choose Section Background Popup Add To @@ -235,6 +236,7 @@ Filename cannot be empty An error occurred: %1$s No streams available to download + Couldn not read saved sections, so using default ones Could not read saved tabs, so using default ones Restore defaults Do you want to restore defaults? @@ -378,6 +380,8 @@ Last Played Most Played + Content of the Drawer + What is shown on the Drawer Content of main page What tabs are shown on the main page Swipe items to remove them diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index 23b782ffde7..f00790b9b2f 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -33,6 +33,14 @@ app:singleLineTitle="false" app:iconSpaceReserved="false" /> + +