-Modified BaseLocalItemFragment to no longer cache items when going into background.

-Refactored and restructured all LocalItem related fragments and dialogs.
-Added error logging to unmonitored single-use observables.
-Modified playlist metadata query to return by alphabetical order.
-Removed sending toast when playlist is renamed or deleted as it is obvious.
-Removed unused code in main fragment.
This commit is contained in:
John Zhen Mo 2018-01-30 16:01:11 -08:00
parent 75a58d6381
commit 225b43ca3c
16 changed files with 647 additions and 647 deletions

View File

@ -63,6 +63,7 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
" FROM " + PLAYLIST_TABLE +
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
" ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID +
" GROUP BY " + JOIN_PLAYLIST_ID)
" GROUP BY " + JOIN_PLAYLIST_ID +
" ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
}

View File

@ -229,10 +229,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name);
fragment.useAsFrontPage(true);
return fragment;
} else if (setMainPage.equals(getString(R.string.bookmark_page_key))) {
final BookmarkFragment fragment = new BookmarkFragment();
fragment.useAsFrontPage(true);
return fragment;
} else {
return new BlankFragment();
}

View File

@ -1,8 +1,7 @@
package org.schabi.newpipe.fragments.local;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@ -12,86 +11,44 @@ import android.view.MenuInflater;
import android.view.View;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.list.ListViewContract;
import org.schabi.newpipe.util.StateSaver;
import java.util.List;
import java.util.Queue;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
/**
* This fragment is design to be used with persistent data such as
* {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained
* in the list adapter to avoid extra writes when the it exits or re-enters its lifecycle.
*
* This fragment destroys its adapter and views when {@link Fragment#onDestroyView()} is
* called and is memory efficient when in backstack.
* */
public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, StateSaver.WriteRead {
implements ListViewContract<I, N> {
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
protected View headerRootView;
protected View footerRootView;
protected LocalItemListAdapter itemListAdapter;
protected RecyclerView itemsList;
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
// Lifecycle - Creation
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
itemListAdapter = new LocalItemListAdapter(activity);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onDestroy() {
super.onDestroy();
StateSaver.onDestroy(savedState);
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
protected StateSaver.SavedState savedState;
@Override
public String generateSuffix() {
// Naive solution, but it's good for now (the items don't change)
return "." + itemListAdapter.getItemsList().size() + ".list";
}
@Override
public void writeTo(Queue<Object> objectsToSave) {
objectsToSave.add(itemListAdapter.getItemsList());
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
itemListAdapter.getItemsList().clear();
itemListAdapter.getItemsList().addAll((List<LocalItem>) savedObjects.poll());
}
@Override
public void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
super.onRestoreInstanceState(bundle);
savedState = StateSaver.tryToRestore(bundle, this);
}
/*//////////////////////////////////////////////////////////////////////////
// Init
// Lifecycle - View
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
@ -113,8 +70,9 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(getListLayoutManager());
itemListAdapter.setFooter(getListFooter());
itemListAdapter.setHeader(getListHeader());
itemListAdapter = new LocalItemListAdapter(activity);
itemListAdapter.setHeader(headerRootView = getListHeader());
itemListAdapter.setFooter(footerRootView = getListFooter());
itemsList.setAdapter(itemListAdapter);
}
@ -125,12 +83,13 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
// Lifecycle - Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
"], inflater = [" + inflater + "]");
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
@ -143,27 +102,48 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
}
}
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle - Destruction
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onDestroyView() {
super.onDestroyView();
itemsList = null;
itemListAdapter = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
resetFragment();
}
@Override
public void showLoading() {
super.showLoading();
// animateView(itemsList, false, 400);
animateView(itemsList, false, 200);
if (headerRootView != null) animateView(headerRootView, false, 200);
}
@Override
public void hideLoading() {
super.hideLoading();
animateView(itemsList, true, 300);
animateView(itemsList, true, 200);
if (headerRootView != null) animateView(headerRootView, true, 200);
}
@Override
public void showError(String message, boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
animateView(itemsList, false, 200);
if (headerRootView != null) animateView(headerRootView, false, 200);
}
@Override
@ -181,4 +161,18 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
public void handleNextItems(N result) {
isLoading.set(false);
}
/*//////////////////////////////////////////////////////////////////////////
// Error handling
//////////////////////////////////////////////////////////////////////////*/
protected void resetFragment() {
if (itemListAdapter != null) itemListAdapter.clearStreamItemList();
}
@Override
protected boolean onError(Throwable exception) {
resetFragment();
return super.onError(exception);
}
}

View File

@ -11,6 +11,7 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -38,7 +39,6 @@ import java.util.concurrent.TimeUnit;
import icepick.State;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.subjects.PublishSubject;
@ -51,8 +51,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private View headerRootLayout;
private TextView headerTitleView;
private TextView headerStreamCount;
private View playlistControl;
private View playlistControl;
private View headerPlayAllButton;
private View headerPopupButton;
private View headerBackgroundButton;
@ -66,10 +66,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private ItemTouchHelper itemTouchHelper;
/* Used for independent events */
private CompositeDisposable disposables = new CompositeDisposable();
private Subscription databaseSubscription;
private LocalPlaylistManager playlistManager;
private Subscription databaseSubscription;
private PublishSubject<Long> debouncedSaveSignal;
private Disposable debouncedSaver;
@ -81,13 +79,14 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
// Fragment LifeCycle - Creation
///////////////////////////////////////////////////////////////////////////
@Override
public void onAttach(Context context) {
super.onAttach(context);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(context));
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
debouncedSaveSignal = PublishSubject.create();
}
@Override
@ -97,53 +96,23 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return inflater.inflate(R.layout.fragment_playlist, container, false);
}
@Override
public void onResume() {
super.onResume();
debouncedSaveSignal = PublishSubject.create();
debouncedSaver = getDebouncedSaver();
}
@Override
public void onPause() {
super.onPause();
if (debouncedSaveSignal != null) debouncedSaveSignal.onComplete();
if (debouncedSaver != null) debouncedSaver.dispose();
debouncedSaveSignal = null;
debouncedSaver = null;
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
if (disposables != null) disposables.clear();
super.onDestroyView();
}
@Override
public void onDestroy() {
if (disposables != null) disposables.dispose();
if (databaseSubscription != null) databaseSubscription.cancel();
disposables = null;
databaseSubscription = null;
playlistManager = null;
super.onDestroy();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Views
// Fragment Lifecycle - Views
///////////////////////////////////////////////////////////////////////////
@Override
public void setTitle(final String title) {
super.setTitle(title);
if (headerTitleView != null) {
headerTitleView.setText(title);
}
}
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
setFragmentTitle(name);
setTitle(name);
}
@Override
@ -155,6 +124,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
headerTitleView.setSelected(true);
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
playlistControl = headerRootLayout.findViewById(R.id.playlist_control);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
@ -167,6 +137,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
protected void initListeners() {
super.initListeners();
headerTitleView.setOnClickListener(view -> createRenameDialog());
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
@ -192,9 +164,236 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder);
}
});
headerTitleView.setOnClickListener(view -> createRenameDialog());
}
///////////////////////////////////////////////////////////////////////////
// Fragment Lifecycle - Loading
///////////////////////////////////////////////////////////////////////////
@Override
public void showLoading() {
super.showLoading();
animateView(headerRootLayout, false, 200);
animateView(playlistControl, false, 200);
}
@Override
public void hideLoading() {
super.hideLoading();
animateView(headerRootLayout, true, 200);
animateView(playlistControl, true, 200);
}
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
if (debouncedSaver != null) debouncedSaver.dispose();
debouncedSaver = getDebouncedSaver();
playlistManager.getPlaylistStreams(playlistId)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistObserver());
}
///////////////////////////////////////////////////////////////////////////
// Fragment Lifecycle - Destruction
///////////////////////////////////////////////////////////////////////////
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (databaseSubscription != null) databaseSubscription.cancel();
if (debouncedSaver != null) debouncedSaver.dispose();
databaseSubscription = null;
debouncedSaver = null;
itemTouchHelper = null;
}
@Override
public void onDestroy() {
super.onDestroy();
if (debouncedSaveSignal != null) debouncedSaveSignal.onComplete();
debouncedSaveSignal = null;
playlistManager = null;
}
///////////////////////////////////////////////////////////////////////////
// Playlist Stream Loader
///////////////////////////////////////////////////////////////////////////
private Subscriber<List<PlaylistStreamEntry>> getPlaylistObserver() {
return new Subscriber<List<PlaylistStreamEntry>>() {
@Override
public void onSubscribe(Subscription s) {
showLoading();
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = s;
databaseSubscription.request(1);
}
@Override
public void onNext(List<PlaylistStreamEntry> streams) {
// Do not allow saving while the result is being updated
if (debouncedSaver != null) debouncedSaver.dispose();
handleResult(streams);
debouncedSaver = getDebouncedSaver();
if (databaseSubscription != null) databaseSubscription.request(1);
}
@Override
public void onError(Throwable exception) {
LocalPlaylistFragment.this.onError(exception);
}
@Override
public void onComplete() {}
};
}
@Override
public void handleResult(@NonNull List<PlaylistStreamEntry> result) {
super.handleResult(result);
itemListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
return;
}
itemListAdapter.addItems(result);
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
setVideoCount(itemListAdapter.getItemsList().size());
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
hideLoading();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected void resetFragment() {
super.resetFragment();
if (databaseSubscription != null) databaseSubscription.cancel();
}
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Local Playlist", R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Playlist Metadata/Streams Manipulation
//////////////////////////////////////////////////////////////////////////*/
private void createRenameDialog() {
if (playlistId == null || name == null || getContext() == null) return;
final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
EditText nameEdit = dialogView.findViewById(R.id.playlist_name);
nameEdit.setText(name);
nameEdit.setSelection(nameEdit.getText().length());
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext())
.setTitle(R.string.rename_playlist)
.setView(dialogView)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.create, (dialogInterface, i) ->
changePlaylistName(nameEdit.getText().toString())
);
dialogBuilder.show();
}
private void changePlaylistName(final String name) {
this.name = name;
setTitle(name);
Log.e(TAG, "Updating playlist id=[" + playlistId +
"] with new name=[" + name + "] items");
playlistManager.renamePlaylist(playlistId, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> {/*Do nothing on success*/}, this::onError);
}
private void changeThumbnailUrl(final String thumbnailUrl) {
final Toast successToast = Toast.makeText(getActivity(),
R.string.playlist_thumbnail_change_success,
Toast.LENGTH_SHORT);
Log.e(TAG, "Updating playlist id=[" + playlistId +
"] with new thumbnail url=[" + thumbnailUrl + "]");
playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignore -> successToast.show(), this::onError);
}
private void deleteItem(final PlaylistStreamEntry item) {
itemListAdapter.removeItem(item);
setVideoCount(itemListAdapter.getItemsList().size());
saveDebounced();
}
private void saveDebounced() {
debouncedSaveSignal.onNext(System.currentTimeMillis());
}
private Disposable getDebouncedSaver() {
return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> saveJoin());
}
private void saveJoin() {
final List<LocalItem> items = itemListAdapter.getItemsList();
List<Long> streamIds = new ArrayList<>(items.size());
for (final LocalItem item : items) {
if (item instanceof PlaylistStreamEntry) {
streamIds.add(((PlaylistStreamEntry) item).streamId);
}
}
Log.e(TAG, "Updating playlist id=[" + playlistId +
"] with [" + streamIds.size() + "] items");
playlistManager.updateJoin(playlistId, streamIds)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {/*Do nothing on success*/}, this::onError);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void showStreamDialog(final PlaylistStreamEntry item) {
final Context context = getContext();
final Activity activity = getActivity();
@ -236,9 +435,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
changeThumbnailUrl(item.thumbnailUrl);
break;
case 6:
itemListAdapter.removeItem(item);
setVideoCount(itemListAdapter.getItemsList().size());
saveDebounced();
deleteItem(item);
break;
default:
break;
@ -281,124 +478,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
};
}
private void resetFragment() {
if (disposables != null) disposables.clear();
if (databaseSubscription != null) databaseSubscription.cancel();
if (itemListAdapter != null) itemListAdapter.clearStreamItemList();
}
///////////////////////////////////////////////////////////////////////////
// Loader
///////////////////////////////////////////////////////////////////////////
@Override
public void showLoading() {
super.showLoading();
animateView(headerRootLayout, false, 200);
animateView(itemsList, false, 100);
}
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
resetFragment();
playlistManager.getPlaylistStreams(playlistId)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistObserver());
}
private Subscriber<List<PlaylistStreamEntry>> getPlaylistObserver() {
return new Subscriber<List<PlaylistStreamEntry>>() {
@Override
public void onSubscribe(Subscription s) {
showLoading();
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = s;
databaseSubscription.request(1);
}
@Override
public void onNext(List<PlaylistStreamEntry> streams) {
handleResult(streams);
if (databaseSubscription != null) databaseSubscription.request(1);
}
@Override
public void onError(Throwable exception) {
LocalPlaylistFragment.this.onError(exception);
}
@Override
public void onComplete() {
}
};
}
@Override
public void handleResult(@NonNull List<PlaylistStreamEntry> result) {
super.handleResult(result);
itemListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
return;
}
animateView(headerRootLayout, true, 100);
animateView(itemsList, true, 300);
itemListAdapter.addItems(result);
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
setVideoCount(itemListAdapter.getItemsList().size());
playlistControl.setVisibility(View.VISIBLE);
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
hideLoading();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected boolean onError(Throwable exception) {
resetFragment();
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Local Playlist", R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void setInitialData(long playlistId, String name) {
this.playlistId = playlistId;
this.name = !TextUtils.isEmpty(name) ? name : "";
}
private void setFragmentTitle(final String title) {
if (activity != null && activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setTitle(title);
}
if (headerTitleView != null) {
headerTitleView.setText(title);
}
}
private void setVideoCount(final long count) {
if (activity != null && headerStreamCount != null) {
headerStreamCount.setText(Localization.localizeStreamCount(activity, count));
@ -419,71 +503,5 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
return new SinglePlayQueue(streamInfoItems, index);
}
private void createRenameDialog() {
if (playlistId == null || name == null || getContext() == null) return;
final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
EditText nameEdit = dialogView.findViewById(R.id.playlist_name);
nameEdit.setText(name);
nameEdit.setSelection(nameEdit.getText().length());
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext())
.setTitle(R.string.rename_playlist)
.setView(dialogView)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.create, (dialogInterface, i) -> {
name = nameEdit.getText().toString();
setFragmentTitle(name);
final LocalPlaylistManager playlistManager =
new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
final Toast successToast = Toast.makeText(getActivity(),
R.string.playlist_rename_success,
Toast.LENGTH_SHORT);
playlistManager.renamePlaylist(playlistId, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> successToast.show());
});
dialogBuilder.show();
}
private void changeThumbnailUrl(final String thumbnailUrl) {
final Toast successToast = Toast.makeText(getActivity(),
R.string.playlist_thumbnail_change_success,
Toast.LENGTH_SHORT);
playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignore -> successToast.show());
}
private void saveDebounced() {
debouncedSaveSignal.onNext(System.currentTimeMillis());
}
private Disposable getDebouncedSaver() {
return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> saveJoin());
}
private void saveJoin() {
final List<LocalItem> items = itemListAdapter.getItemsList();
List<Long> streamIds = new ArrayList<>(items.size());
for (final LocalItem item : items) {
if (item instanceof PlaylistStreamEntry) {
streamIds.add(((PlaylistStreamEntry) item).streamId);
}
}
playlistManager.updateJoin(playlistId, streamIds)
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
}
}

View File

@ -1,7 +1,6 @@
package org.schabi.newpipe.fragments.local;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -25,6 +24,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@ -63,26 +64,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
playlistAdapter = new LocalItemListAdapter(getActivity());
}
@Override
public void onDestroy() {
super.onDestroy();
if (playlistReactor != null) playlistReactor.dispose();
playlistReactor = null;
playlistRecyclerView = null;
playlistAdapter = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Views
// LifeCycle - Creation
//////////////////////////////////////////////////////////////////////////*/
@Override
@ -95,52 +77,44 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
playlistRecyclerView = view.findViewById(R.id.playlist_list);
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
playlistRecyclerView.setAdapter(playlistAdapter);
final LocalPlaylistManager playlistManager =
new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
playlistAdapter = new LocalItemListAdapter(getActivity());
playlistAdapter.setSelectedListener(new OnLocalItemGesture<LocalItem>() {
@Override
public void selected(LocalItem selectedItem) {
if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null)
return;
final long playlistId = ((PlaylistMetadataEntry) selectedItem).uid;
@SuppressLint("ShowToast")
final Toast successToast = Toast.makeText(getContext(), R.string.playlist_add_stream_success,
Toast.LENGTH_SHORT);
playlistManager.appendToPlaylist(playlistId, getStreams())
.observeOn(AndroidSchedulers.mainThread())
.doOnDispose(successToast::show)
.subscribe(ignored -> {});
getDialog().dismiss();
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem,
getStreams());
}
});
playlistRecyclerView = view.findViewById(R.id.playlist_list);
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
playlistRecyclerView.setAdapter(playlistAdapter);
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
playlistReactor = playlistManager.getPlaylists()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(metadataEntries -> {
if (metadataEntries.isEmpty()) {
openCreatePlaylistDialog();
return;
}
.subscribe(this::onPlaylistsReceived);
}
if (playlistAdapter != null) {
playlistAdapter.clearStreamItemList();
playlistAdapter.addItems(metadataEntries);
}
if (playlistRecyclerView != null) {
playlistRecyclerView.setVisibility(View.VISIBLE);
}
});
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle - Destruction
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onDestroyView() {
super.onDestroyView();
if (playlistReactor != null) playlistReactor.dispose();
playlistReactor = null;
playlistRecyclerView = null;
playlistAdapter = null;
}
/*//////////////////////////////////////////////////////////////////////////
@ -153,4 +127,33 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG);
getDialog().dismiss();
}
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) {
if (playlists.isEmpty()) {
openCreatePlaylistDialog();
return;
}
if (playlistAdapter != null && playlistRecyclerView != null) {
playlistAdapter.clearStreamItemList();
playlistAdapter.addItems(playlists);
playlistRecyclerView.setVisibility(View.VISIBLE);
}
}
private void onPlaylistSelected(@NonNull LocalPlaylistManager manager,
@NonNull PlaylistMetadataEntry playlist,
@Nonnull List<StreamEntity> streams) {
if (getStreams() == null) return;
@SuppressLint("ShowToast")
final Toast successToast = Toast.makeText(getContext(),
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
manager.appendToPlaylist(playlist.uid, streams)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> successToast.show());
getDialog().dismiss();
}
}

View File

@ -1,17 +1,14 @@
package org.schabi.newpipe.fragments.local.bookmark;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@ -19,29 +16,25 @@ import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.local.LocalItemListAdapter;
import org.schabi.newpipe.fragments.local.BaseLocalListFragment;
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
import org.schabi.newpipe.fragments.local.OnLocalItemGesture;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.Collections;
import java.util.List;
import icepick.State;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public final class BookmarkFragment
extends BaseLocalListFragment<List<PlaylistMetadataEntry>, Void> {
public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEntry>> {
private View watchHistoryButton;
private View mostWatchedButton;
private LocalItemListAdapter itemListAdapter;
private RecyclerView itemsList;
@State
protected Parcelable itemsListState;
@ -50,23 +43,14 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
private LocalPlaylistManager localPlaylistManager;
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
// Fragment LifeCycle - Creation
///////////////////////////////////////////////////////////////////////////
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser && activity != null && activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setTitle(R.string.tab_bookmarks);
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
itemListAdapter = new LocalItemListAdapter(activity);
localPlaylistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(context));
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
localPlaylistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
disposables = new CompositeDisposable();
}
@Nullable
@ -74,62 +58,37 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
Bundle savedInstanceState) {
if (activity.getSupportActionBar() != null) {
if (activity != null && activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
activity.setTitle(R.string.tab_subscriptions);
}
activity.setTitle(R.string.tab_bookmarks);
if(useAsFrontPage) {
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
return inflater.inflate(R.layout.fragment_bookmarks, container, false);
}
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
if (disposables != null) disposables.clear();
if (databaseSubscription != null) databaseSubscription.cancel();
super.onDestroyView();
}
@Override
public void onDestroy() {
if (disposables != null) disposables.dispose();
if (databaseSubscription != null) databaseSubscription.cancel();
disposables = null;
databaseSubscription = null;
localPlaylistManager = null;
super.onDestroy();
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) setTitle(getString(R.string.tab_bookmarks));
}
///////////////////////////////////////////////////////////////////////////
// Fragment Views
// Fragment LifeCycle - Views
///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
}
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(new LinearLayoutManager(activity));
@Override
protected View getListHeader() {
final View headerRootLayout = activity.getLayoutInflater()
.inflate(R.layout.bookmark_header, itemsList, false);
watchHistoryButton = headerRootLayout.findViewById(R.id.watchHistory);
mostWatchedButton = headerRootLayout.findViewById(R.id.mostWatched);
itemListAdapter.setHeader(headerRootLayout);
itemsList.setAdapter(itemListAdapter);
return headerRootLayout;
}
@Override
@ -168,41 +127,51 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
});
}
private void showDeleteDialog(final PlaylistMetadataEntry item) {
new AlertDialog.Builder(activity)
.setTitle(item.name)
.setMessage(R.string.delete_playlist_prompt)
.setCancelable(true)
.setPositiveButton(R.string.delete, (dialog, i) -> {
final Toast deleteSuccessful = Toast.makeText(getContext(),
R.string.playlist_delete_success, Toast.LENGTH_SHORT);
disposables.add(localPlaylistManager.deletePlaylist(item.uid)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> deleteSuccessful.show()));
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void resetFragment() {
if (disposables != null) disposables.clear();
if (itemListAdapter != null) itemListAdapter.clearStreamItemList();
}
///////////////////////////////////////////////////////////////////////////
// Subscriptions Loader
// Fragment LifeCycle - Loading
///////////////////////////////////////////////////////////////////////////
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
resetFragment();
localPlaylistManager.getPlaylists()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscriptionSubscriber());
}
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Destruction
///////////////////////////////////////////////////////////////////////////
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (disposables != null) disposables.clear();
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = null;
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.dispose();
disposables = null;
localPlaylistManager = null;
itemsListState = null;
}
///////////////////////////////////////////////////////////////////////////
// Subscriptions Loader
///////////////////////////////////////////////////////////////////////////
private Subscriber<List<PlaylistMetadataEntry>> getSubscriptionSubscriber() {
return new Subscriber<List<PlaylistMetadataEntry>>() {
@Override
@ -238,55 +207,58 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
if (result.isEmpty()) {
showEmptyState();
} else {
itemListAdapter.addItems(infoItemsOf(result));
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
hideLoading();
return;
}
itemListAdapter.addItems(result);
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
hideLoading();
}
private List<PlaylistMetadataEntry> infoItemsOf(List<PlaylistMetadataEntry> playlists) {
Collections.sort(playlists, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name));
return playlists;
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animateView(itemsList, false, 100);
}
@Override
public void hideLoading() {
super.hideLoading();
animateView(itemsList, true, 200);
}
@Override
public void showEmptyState() {
super.showEmptyState();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected boolean onError(Throwable exception) {
resetFragment();
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Bookmark", R.string.general_error);
return true;
}
@Override
protected void resetFragment() {
super.resetFragment();
if (disposables != null) disposables.clear();
}
///////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////
private void showDeleteDialog(final PlaylistMetadataEntry item) {
new AlertDialog.Builder(activity)
.setTitle(item.name)
.setMessage(R.string.delete_playlist_prompt)
.setCancelable(true)
.setPositiveButton(R.string.delete, (dialog, i) ->
disposables.add(deletePlaylist(item.uid))
)
.setNegativeButton(R.string.cancel, null)
.show();
}
private Disposable deletePlaylist(final long playlistId) {
return localPlaylistManager.deletePlaylist(playlistId)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> {/*Do nothing on success*/},
throwable -> Log.e(TAG, "Playlist deletion failed, id=["
+ playlistId + "]")
);
}
}

View File

@ -6,7 +6,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import java.util.Collections;
import java.util.List;
public class MostPlayedFragment extends StatisticsPlaylistFragment {
public final class MostPlayedFragment extends StatisticsPlaylistFragment {
@Override
protected String getName() {
return getString(R.string.title_most_played);

View File

@ -32,13 +32,9 @@ import java.util.List;
import icepick.State;
import io.reactivex.android.schedulers.AndroidSchedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class StatisticsPlaylistFragment
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
private View headerRootLayout;
private View playlistControl;
private View headerPlayAllButton;
private View headerPopupButton;
private View headerBackgroundButton;
@ -59,13 +55,13 @@ public abstract class StatisticsPlaylistFragment
protected abstract List<StreamStatisticsEntry> processResult(final List<StreamStatisticsEntry> results);
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
// Fragment LifeCycle - Creation
///////////////////////////////////////////////////////////////////////////
@Override
public void onAttach(Context context) {
super.onAttach(context);
recordManager = new HistoryRecordManager(context);
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
recordManager = new HistoryRecordManager(getContext());
}
@Override
@ -75,46 +71,23 @@ public abstract class StatisticsPlaylistFragment
return inflater.inflate(R.layout.fragment_playlist, container, false);
}
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
if (databaseSubscription != null) databaseSubscription.cancel();
super.onDestroyView();
}
@Override
public void onDestroy() {
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = null;
recordManager = null;
super.onDestroy();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Views
// Fragment LifeCycle - Views
///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
setFragmentTitle(getName());
setTitle(getName());
}
@Override
protected View getListHeader() {
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control,
final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control,
itemsList, false);
playlistControl = headerRootLayout.findViewById(R.id.playlist_control);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
return headerRootLayout;
}
@ -139,9 +112,124 @@ public abstract class StatisticsPlaylistFragment
}
}
});
}
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Loading
///////////////////////////////////////////////////////////////////////////
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
recordManager.getStreamStatistics()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHistoryObserver());
}
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Destruction
///////////////////////////////////////////////////////////////////////////
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = null;
}
@Override
public void onDestroy() {
super.onDestroy();
recordManager = null;
itemsListState = null;
}
///////////////////////////////////////////////////////////////////////////
// Statistics Loader
///////////////////////////////////////////////////////////////////////////
private Subscriber<List<StreamStatisticsEntry>> getHistoryObserver() {
return new Subscriber<List<StreamStatisticsEntry>>() {
@Override
public void onSubscribe(Subscription s) {
showLoading();
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = s;
databaseSubscription.request(1);
}
@Override
public void onNext(List<StreamStatisticsEntry> streams) {
handleResult(streams);
if (databaseSubscription != null) databaseSubscription.request(1);
}
@Override
public void onError(Throwable exception) {
StatisticsPlaylistFragment.this.onError(exception);
}
@Override
public void onComplete() {
}
};
}
@Override
public void handleResult(@NonNull List<StreamStatisticsEntry> result) {
super.handleResult(result);
itemListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
return;
}
itemListAdapter.addItems(processResult(result));
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
hideLoading();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected void resetFragment() {
super.resetFragment();
if (databaseSubscription != null) databaseSubscription.cancel();
}
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "History Statistics", R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void showStreamDialog(final StreamStatisticsEntry item) {
final Context context = getContext();
final Activity activity = getActivity();
@ -182,113 +270,6 @@ public abstract class StatisticsPlaylistFragment
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
}
private void resetFragment() {
if (databaseSubscription != null) databaseSubscription.cancel();
if (itemListAdapter != null) itemListAdapter.clearStreamItemList();
}
///////////////////////////////////////////////////////////////////////////
// Loader
///////////////////////////////////////////////////////////////////////////
@Override
public void showLoading() {
super.showLoading();
animateView(headerRootLayout, false, 200);
animateView(itemsList, false, 100);
}
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
resetFragment();
recordManager.getStreamStatistics()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHistoryObserver());
}
private Subscriber<List<StreamStatisticsEntry>> getHistoryObserver() {
return new Subscriber<List<StreamStatisticsEntry>>() {
@Override
public void onSubscribe(Subscription s) {
showLoading();
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = s;
databaseSubscription.request(1);
}
@Override
public void onNext(List<StreamStatisticsEntry> streams) {
handleResult(streams);
if (databaseSubscription != null) databaseSubscription.request(1);
}
@Override
public void onError(Throwable exception) {
StatisticsPlaylistFragment.this.onError(exception);
}
@Override
public void onComplete() {
}
};
}
@Override
public void handleResult(@NonNull List<StreamStatisticsEntry> result) {
super.handleResult(result);
itemListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
return;
}
animateView(headerRootLayout, true, 100);
animateView(itemsList, true, 300);
itemListAdapter.addItems(processResult(result));
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
playlistControl.setVisibility(View.VISIBLE);
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
hideLoading();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected boolean onError(Throwable exception) {
resetFragment();
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "History Statistics", R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void setFragmentTitle(final String title) {
if (activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setTitle(title);
}
}
private PlayQueue getPlayQueue() {
return getPlayQueue(0);
}

View File

@ -6,7 +6,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import java.util.Collections;
import java.util.List;
public class WatchHistoryFragment extends StatisticsPlaylistFragment {
public final class WatchHistoryFragment extends StatisticsPlaylistFragment {
@Override
protected String getName() {
return getString(R.string.title_watch_history);

View File

@ -14,6 +14,7 @@ import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -173,10 +174,19 @@ public abstract class HistoryFragment<E> extends BaseFragment
final Disposable deletion = delete(itemsToDelete)
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
.subscribe(
ignored -> Log.d(TAG, "Clear history deleted [" +
itemsToDelete.size() + "] items."),
error -> Log.e(TAG, "Clear history delete step failed", error)
);
final Disposable cleanUp = historyRecordManager.removeOrphanedRecords()
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
.subscribe(
ignored -> Log.d(TAG, "Clear history deleted orphaned stream records"),
error -> Log.e(TAG, "Clear history remove orphaned records failed", error)
);
disposables.addAll(deletion, cleanUp);
makeSnackbar(R.string.history_cleared);

View File

@ -7,6 +7,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -83,17 +84,25 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
.setCancelable(true)
.setNeutralButton(R.string.cancel, null)
.setPositiveButton(R.string.delete_one, (dialog, i) -> {
final Single<Integer> onDelete = historyRecordManager
final Disposable onDelete = historyRecordManager
.deleteSearches(Collections.singleton(item))
.observeOn(AndroidSchedulers.mainThread());
disposables.add(onDelete.subscribe());
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
ignored -> {/*successful*/},
error -> Log.e(TAG, "Search history Delete One failed:", error)
);
disposables.add(onDelete);
makeSnackbar(R.string.item_deleted);
})
.setNegativeButton(R.string.delete_all, (dialog, i) -> {
final Single<Integer> onDeleteAll = historyRecordManager
final Disposable onDeleteAll = historyRecordManager
.deleteSearchHistory(item.getSearch())
.observeOn(AndroidSchedulers.mainThread());
disposables.add(onDeleteAll.subscribe());
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
ignored -> {/*successful*/},
error -> Log.e(TAG, "Search history Delete All failed:", error)
);
disposables.add(onDeleteAll);
makeSnackbar(R.string.item_deleted);
})
.show();
@ -112,8 +121,7 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
protected class SearchHistoryAdapter extends HistoryEntryAdapter<SearchHistoryEntry, ViewHolder> {
public SearchHistoryAdapter(Context context) {
SearchHistoryAdapter(Context context) {
super(context);
}

View File

@ -8,6 +8,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -29,6 +30,7 @@ import java.util.List;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
public class WatchedHistoryFragment extends HistoryFragment<StreamHistoryEntry> {
@ -85,17 +87,25 @@ public class WatchedHistoryFragment extends HistoryFragment<StreamHistoryEntry>
.setCancelable(true)
.setNeutralButton(R.string.cancel, null)
.setPositiveButton(R.string.delete_one, (dialog, i) -> {
final Single<Integer> onDelete = historyRecordManager
final Disposable onDelete = historyRecordManager
.deleteStreamHistory(Collections.singleton(item))
.observeOn(AndroidSchedulers.mainThread());
disposables.add(onDelete.subscribe());
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
ignored -> {/*successful*/},
error -> Log.e(TAG, "Watch history Delete One failed:", error)
);
disposables.add(onDelete);
makeSnackbar(R.string.item_deleted);
})
.setNegativeButton(R.string.delete_all, (dialog, i) -> {
final Single<Integer> onDeleteAll = historyRecordManager
final Disposable onDeleteAll = historyRecordManager
.deleteStreamHistory(item.streamId)
.observeOn(AndroidSchedulers.mainThread());
disposables.add(onDeleteAll.subscribe());
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
ignored -> {/*successful*/},
error -> Log.e(TAG, "Watch history Delete All failed:", error)
);
disposables.add(onDeleteAll);
makeSnackbar(R.string.item_deleted);
})
.show();

View File

@ -676,7 +676,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
}
// TODO: update exoplayer to 2.6.x in order to register view count on repeated streams
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe());
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete()
.subscribe(
ignored -> {/* successful */},
error -> Log.e(TAG, "Player onViewed() failure: ", error)
));
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
}
@ -844,7 +848,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
.observeOn(AndroidSchedulers.mainThread())
.onErrorComplete()
.subscribe();
.subscribe(
ignored -> {/* successful */},
error -> Log.e(TAG, "savePlaybackState() failure: ", error)
);
databaseUpdateReactor.add(stateSaver);
}

View File

@ -11,9 +11,12 @@
android:id="@+id/playlist_title_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="6dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_toLeftOf="@id/playlist_stream_count"
android:layout_toStartOf="@id/playlist_stream_count"
android:background="?attr/selectableItemBackground"
@ -26,7 +29,7 @@
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/playlist_detail_title_text_size"
tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur..." />
tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur..."/>
<TextView
android:id="@+id/playlist_stream_count"
@ -34,7 +37,7 @@
android:layout_height="wrap_content"
android:layout_alignBottom="@id/playlist_title_view"
android:layout_alignParentRight="true"
android:layout_marginRight="6dp"
android:padding="6dp"
android:ellipsize="end"
android:gravity="right|center_vertical"
android:maxLines="1"

View File

@ -119,14 +119,12 @@
<string name="subscription_page_key" translatable="false">subscription_page_key</string>
<string name="kiosk_page_key" translatable="false">kiosk_page</string>
<string name="channel_page_key" translatable="false">channel_page</string>
<string name="bookmark_page_key" translatable="false">bookmark_page</string>
<string-array name="main_page_content_pages" translatable="false">
<item>@string/blank_page_key</item>
<item>@string/kiosk_page_key</item>
<item>@string/feed_page_key</item>
<item>@string/subscription_page_key</item>
<item>@string/channel_page_key</item>
<item>@string/bookmark_page_key</item>
</string-array>
<string name="main_page_selected_service" translatable="false">main_page_selected_service</string>
<string name="main_page_selected_channel_name" translatable="false">main_page_selected_channel_name</string>

View File

@ -390,6 +390,5 @@
<string name="playlist_creation_success">Playlist successfully created</string>
<string name="playlist_add_stream_success">Added to playlist</string>
<string name="playlist_thumbnail_change_success">Playlist thumbnail changed</string>
<string name="playlist_rename_success">Playlist renamed</string>
<string name="playlist_delete_success">Playlist deleted</string>
<string name="playlist_delete_failure">Failed to delete playlist</string>
</resources>