From 661908c9228c3ad59be8c0b404ae7ea9b0bc056b Mon Sep 17 00:00:00 2001 From: Synced Synapse Date: Fri, 16 Dec 2016 19:14:06 +0000 Subject: [PATCH] Add next episodes section to tv show details screen --- .../org/xbmc/kore/provider/MediaContract.java | 16 ++ .../org/xbmc/kore/provider/MediaProvider.java | 4 +- .../org/xbmc/kore/ui/SettingsFragment.java | 1 + .../xbmc/kore/ui/TVShowDetailsFragment.java | 169 +++++++++++++++++- .../org/xbmc/kore/ui/TVShowsActivity.java | 39 +++- .../org/xbmc/kore/utils/SelectionBuilder.java | 7 + .../res/layout/fragment_tvshow_overview.xml | 22 ++- .../res/layout/list_item_next_episode.xml | 81 +++++++++ app/src/main/res/values/strings.xml | 1 + 9 files changed, 326 insertions(+), 14 deletions(-) create mode 100644 app/src/main/res/layout/list_item_next_episode.xml diff --git a/app/src/main/java/org/xbmc/kore/provider/MediaContract.java b/app/src/main/java/org/xbmc/kore/provider/MediaContract.java index 8417122..5c732f5 100644 --- a/app/src/main/java/org/xbmc/kore/provider/MediaContract.java +++ b/app/src/main/java/org/xbmc/kore/provider/MediaContract.java @@ -27,6 +27,11 @@ public class MediaContract { public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY); + /** + * LIMIT query to incluse in URIs so that it translate to a Limit By in the query + */ + public static final String LIMIT_QUERY = "limit"; + /** * Paths to tables */ @@ -405,6 +410,17 @@ public class MediaContract { .build(); } + /** Build {@link Uri} for tvshows list with a limit */ + public static Uri buildTVShowEpisodesListUri(long hostId, long tvshowId, int limit) { + if (limit <= 0) return buildTVShowEpisodesListUri(hostId, tvshowId); + + return TVShows.buildTVShowUri(hostId, tvshowId) + .buildUpon() + .appendPath(PATH_EPISODES) + .appendQueryParameter(MediaContract.LIMIT_QUERY, String.valueOf(limit)) + .build(); + } + /** Build {@link Uri} for tvshows for a season list. */ public static Uri buildTVShowSeasonEpisodesListUri(long hostId, long tvshowId, long season) { return Seasons.buildTVShowSeasonUri(hostId, tvshowId, season).buildUpon() diff --git a/app/src/main/java/org/xbmc/kore/provider/MediaProvider.java b/app/src/main/java/org/xbmc/kore/provider/MediaProvider.java index 71b9c65..6643d34 100644 --- a/app/src/main/java/org/xbmc/kore/provider/MediaProvider.java +++ b/app/src/main/java/org/xbmc/kore/provider/MediaProvider.java @@ -330,8 +330,10 @@ public class MediaProvider extends ContentProvider { default: { // Most cases are handled with simple SelectionBuilder final SelectionBuilder builder = buildQuerySelection(uri, match); + String limit = uri.getQueryParameter(MediaContract.LIMIT_QUERY); + cursor = builder.where(selection, selectionArgs) - .query(db, projection, sortOrder); + .query(db, projection, sortOrder, limit); } } return cursor; diff --git a/app/src/main/java/org/xbmc/kore/ui/SettingsFragment.java b/app/src/main/java/org/xbmc/kore/ui/SettingsFragment.java index 0c0d355..c538091 100644 --- a/app/src/main/java/org/xbmc/kore/ui/SettingsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/SettingsFragment.java @@ -73,6 +73,7 @@ public class SettingsFragment extends PreferenceFragment if (getPreferenceManager().getSharedPreferences().getStringSet(Settings.getNavDrawerItemsPrefKey(hostId), null) != null) { Class iterClass = sideMenuItens.getClass(); try { + @SuppressWarnings("unchecked") Method m = iterClass.getDeclaredMethod("onSetInitialValue", boolean.class, Object.class); m.setAccessible(true); m.invoke(sideMenuItens, true, null); diff --git a/app/src/main/java/org/xbmc/kore/ui/TVShowDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/TVShowDetailsFragment.java index a62730a..f626c25 100644 --- a/app/src/main/java/org/xbmc/kore/ui/TVShowDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/TVShowDetailsFragment.java @@ -29,12 +29,14 @@ import android.support.v4.content.Loader; import android.support.v4.widget.SwipeRefreshLayout; import android.util.DisplayMetrics; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.GridLayout; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.ScrollView; import android.widget.TextView; @@ -43,10 +45,12 @@ import org.xbmc.kore.R; import org.xbmc.kore.Settings; import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; +import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.jsonrpc.type.VideoType; import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.MediaPlayerUtils; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; @@ -62,6 +66,8 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment implements LoaderManager.LoaderCallbacks { private static final String TAG = LogUtils.makeLogTag(TVShowDetailsFragment.class); + private static final int NEXT_EPISODES_COUNT = 2; + public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; public static final String BUNDLE_KEY_TVSHOWID = "tvshow_id"; public static final String BUNDLE_KEY_TITLE = "title"; @@ -73,17 +79,19 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment public static final String BUNDLE_KEY_PLOT = "plot"; public static final String BUNDLE_KEY_GENRES = "genres"; - public interface OnSeasonSelectedListener { + public interface TVShowDetailsActionListener { void onSeasonSelected(int tvshowId, int season); + void onNextEpisodeSelected(int episodeId); } // Activity listener - private OnSeasonSelectedListener listenerActivity; + private TVShowDetailsActionListener listenerActivity; // Loader IDs private static final int LOADER_TVSHOW = 0, - LOADER_SEASONS = 1, - LOADER_CAST = 2; + LOADER_NEXT_EPISODES = 1, + LOADER_SEASONS = 2, + LOADER_CAST = 3; // Displayed movie id private int tvshowId = -1; @@ -114,6 +122,9 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment @InjectView(R.id.media_description) TextView mediaDescription; @InjectView(R.id.cast_list) GridLayout videoCastList; + @InjectView(R.id.next_episode_title) TextView nextEpisodeTitle; + @InjectView(R.id.next_episode_list) GridLayout nextEpisodeList; + @InjectView(R.id.seasons_title) TextView seasonsListTitle; @InjectView(R.id.seasons_list) GridLayout seasonsList; @@ -224,6 +235,7 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment // Start the loaders getLoaderManager().initLoader(LOADER_TVSHOW, null, this); + getLoaderManager().initLoader(LOADER_NEXT_EPISODES, null, this); getLoaderManager().initLoader(LOADER_SEASONS, null, this); getLoaderManager().initLoader(LOADER_CAST, null, this); } @@ -232,9 +244,9 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment public void onAttach(Context activity) { super.onAttach(activity); try { - listenerActivity = (OnSeasonSelectedListener) activity; + listenerActivity = (TVShowDetailsActionListener) activity; } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() + " must implement OnSeasonSelectedListener"); + throw new ClassCastException(activity.toString() + " must implement TVShowDetailsActionListener"); } } @@ -248,6 +260,7 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment protected void onSyncProcessEnded(MediaSyncEvent event) { if (event.status == MediaSyncEvent.STATUS_SUCCESS) { getLoaderManager().restartLoader(LOADER_TVSHOW, null, this); + getLoaderManager().restartLoader(LOADER_NEXT_EPISODES, null, this); getLoaderManager().restartLoader(LOADER_SEASONS, null, this); getLoaderManager().restartLoader(LOADER_CAST, null, this); } @@ -266,6 +279,12 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment uri = MediaContract.TVShows.buildTVShowUri(getHostInfo().getId(), tvshowId); return new CursorLoader(getActivity(), uri, TVShowDetailsQuery.PROJECTION, null, null, null); + case LOADER_NEXT_EPISODES: + // Load seasons + uri = MediaContract.Episodes.buildTVShowEpisodesListUri(getHostInfo().getId(), tvshowId, NEXT_EPISODES_COUNT); + String selection = MediaContract.EpisodesColumns.PLAYCOUNT + "=0"; + return new CursorLoader(getActivity(), uri, + NextEpisodesListQuery.PROJECTION, selection, null, NextEpisodesListQuery.SORT); case LOADER_SEASONS: // Load seasons uri = MediaContract.Seasons.buildTVShowSeasonsListUri(getHostInfo().getId(), tvshowId); @@ -284,12 +303,15 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment @Override public void onLoadFinished(Loader cursorLoader, Cursor cursor) { LogUtils.LOGD(TAG, "onLoadFinished"); - if (cursor != null && cursor.getCount() > 0) { + if (cursor != null) { switch (cursorLoader.getId()) { case LOADER_TVSHOW: displayTVShowDetails(cursor); checkOutdatedTVShowDetails(cursor); break; + case LOADER_NEXT_EPISODES: + displayNextEpisodeList(cursor); + break; case LOADER_SEASONS: displaySeasonList(cursor); break; @@ -427,6 +449,104 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment } } + private View.OnClickListener contextlistItemMenuClickListener = new View.OnClickListener() { + @Override + public void onClick(final View v) { + final PlaylistType.Item playListItem = new PlaylistType.Item(); + playListItem.episodeid = (int)v.getTag(); + + final PopupMenu popupMenu = new PopupMenu(getActivity(), v); + popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_play: + MediaPlayerUtils.play(TVShowDetailsFragment.this, playListItem); + return true; + case R.id.action_queue: + MediaPlayerUtils.queue(TVShowDetailsFragment.this, playListItem, PlaylistType.GetPlaylistsReturnType.VIDEO); + return true; + } + return false; + } + }); + popupMenu.show(); + } + }; + + /** + * Display next episode list + * + * @param cursor Cursor with the data + */ + private void displayNextEpisodeList(Cursor cursor) { + if (cursor.moveToFirst()) { + nextEpisodeTitle.setVisibility(View.VISIBLE); + nextEpisodeList.setVisibility(View.VISIBLE); + + HostManager hostManager = HostManager.getInstance(getActivity()); + + View.OnClickListener episodeClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + listenerActivity.onNextEpisodeSelected((int)v.getTag()); + } + }; + + // Get the art dimensions + Resources resources = getActivity().getResources(); + int artWidth = (int)(resources.getDimension(R.dimen.episodelist_art_width) / + UIUtils.IMAGE_RESIZE_FACTOR); + int artHeight = (int)(resources.getDimension(R.dimen.episodelist_art_heigth) / + UIUtils.IMAGE_RESIZE_FACTOR); + + nextEpisodeList.removeAllViews(); + do { + int episodeId = cursor.getInt(NextEpisodesListQuery.EPISODEID); + String title = cursor.getString(NextEpisodesListQuery.TITLE); + String seasonEpisode = String.format(getString(R.string.season_episode), + cursor.getInt(NextEpisodesListQuery.SEASON), + cursor.getInt(NextEpisodesListQuery.EPISODE)); + int runtime = cursor.getInt(NextEpisodesListQuery.RUNTIME) / 60; + String duration = runtime > 0 ? + String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) + + " | " + cursor.getString(NextEpisodesListQuery.FIRSTAIRED) : + cursor.getString(NextEpisodesListQuery.FIRSTAIRED); + String thumbnail = cursor.getString(NextEpisodesListQuery.THUMBNAIL); + + View episodeView = LayoutInflater.from(getActivity()) + .inflate(R.layout.list_item_next_episode, nextEpisodeList, false); + + ImageView artView = (ImageView)episodeView.findViewById(R.id.art); + TextView titleView = (TextView)episodeView.findViewById(R.id.title); + TextView detailsView = (TextView)episodeView.findViewById(R.id.details); + TextView durationView = (TextView)episodeView.findViewById(R.id.duration); + + titleView.setText(title); + detailsView.setText(seasonEpisode); + durationView.setText(duration); + + UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager, + thumbnail, title, + artView, artWidth, artHeight); + episodeView.setTag(episodeId); + episodeView.setOnClickListener(episodeClickListener); + + // For the popupmenu + ImageView contextMenu = (ImageView)episodeView.findViewById(R.id.list_context_menu); + contextMenu.setTag(episodeId); + contextMenu.setOnClickListener(contextlistItemMenuClickListener); + + nextEpisodeList.addView(episodeView); + } while (cursor.moveToNext()); + } else { + // No episodes, hide views + nextEpisodeTitle.setVisibility(View.GONE); + nextEpisodeList.setVisibility(View.GONE); + } + } + /** * Display the seasons list * @@ -434,6 +554,9 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment */ private void displaySeasonList(Cursor cursor) { if (cursor.moveToFirst()) { + seasonsListTitle.setVisibility(View.VISIBLE); + seasonsList.setVisibility(View.VISIBLE); + HostManager hostManager = HostManager.getInstance(getActivity()); View.OnClickListener seasonListClickListener = new View.OnClickListener() { @@ -482,9 +605,8 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment } else { // No seasons, hide views seasonsListTitle.setVisibility(View.GONE); - seasonsList.setVisibility(View.INVISIBLE); + seasonsList.setVisibility(View.GONE); } - } /** @@ -557,6 +679,35 @@ public class TVShowDetailsFragment extends AbstractDetailsFragment int UPDATED = 13; } + /** + * Next episodes list query parameters. + */ + private interface NextEpisodesListQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.Episodes.EPISODEID, + MediaContract.Episodes.SEASON, + MediaContract.Episodes.EPISODE, + MediaContract.Episodes.THUMBNAIL, + MediaContract.Episodes.PLAYCOUNT, + MediaContract.Episodes.TITLE, + MediaContract.Episodes.RUNTIME, + MediaContract.Episodes.FIRSTAIRED, + }; + + String SORT = MediaContract.Episodes.EPISODEID + " ASC"; + + int ID = 0; + int EPISODEID = 1; + int SEASON = 2; + int EPISODE = 3; + int THUMBNAIL = 4; + int PLAYCOUNT = 5; + int TITLE = 6; + int RUNTIME = 7; + int FIRSTAIRED = 8; + } + /** * Seasons list query parameters. */ diff --git a/app/src/main/java/org/xbmc/kore/ui/TVShowsActivity.java b/app/src/main/java/org/xbmc/kore/ui/TVShowsActivity.java index d6b4992..8448416 100644 --- a/app/src/main/java/org/xbmc/kore/ui/TVShowsActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/TVShowsActivity.java @@ -42,7 +42,7 @@ import java.util.Map; */ public class TVShowsActivity extends BaseActivity implements TVShowListFragment.OnTVShowSelectedListener, - TVShowDetailsFragment.OnSeasonSelectedListener, + TVShowDetailsFragment.TVShowDetailsActionListener, TVShowEpisodeListFragment.OnEpisodeSelectedListener { private static final String TAG = LogUtils.makeLogTag(TVShowsActivity.class); @@ -160,7 +160,10 @@ public class TVShowsActivity extends BaseActivity if (selectedEpisodeId != -1) { selectedEpisodeId = -1; getSupportFragmentManager().popBackStack(); - setupActionBar(selectedSeasonTitle); + if (selectedSeason != -1) + setupActionBar(selectedSeasonTitle); + else + setupActionBar(selectedTVShowTitle); return true; } else if (selectedSeason != -1) { selectedSeason = -1; @@ -187,7 +190,10 @@ public class TVShowsActivity extends BaseActivity // If we are showing episode or show details in portrait, clear selected and show action bar if (selectedEpisodeId != -1) { selectedEpisodeId = -1; - setupActionBar(selectedSeasonTitle); + if (selectedSeason != -1) + setupActionBar(selectedSeasonTitle); + else + setupActionBar(selectedTVShowTitle); } else if (selectedSeason != -1) { selectedSeason = -1; setupActionBar(selectedTVShowTitle); @@ -297,6 +303,33 @@ public class TVShowsActivity extends BaseActivity setupActionBar(selectedSeasonTitle); } + /** + * Callback from tvshow details when a episode is selected + * @param episodeId episode id + */ + public void onNextEpisodeSelected(int episodeId) { + selectedEpisodeId = episodeId; + + // Replace list fragment + TVShowEpisodeDetailsFragment fragment = + TVShowEpisodeDetailsFragment.newInstance(selectedTVShowId, selectedEpisodeId); + FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); + + // Set up transitions + if (Utils.isLollipopOrLater()) { + fragment.setEnterTransition( + TransitionInflater.from(this).inflateTransition(R.transition.media_details)); + fragment.setReturnTransition(null); + } else { + fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0); + } + + fragTrans.replace(R.id.fragment_container, fragment) + .addToBackStack(null) + .commit(); + setupActionBar(selectedTVShowTitle); + } + /** * Callback from tvshow episodes list when a episode is selected * @param vh view holder diff --git a/app/src/main/java/org/xbmc/kore/utils/SelectionBuilder.java b/app/src/main/java/org/xbmc/kore/utils/SelectionBuilder.java index 5081ea1..60f9399 100644 --- a/app/src/main/java/org/xbmc/kore/utils/SelectionBuilder.java +++ b/app/src/main/java/org/xbmc/kore/utils/SelectionBuilder.java @@ -158,6 +158,13 @@ public class SelectionBuilder { return query(db, columns, mGroupBy.toString(), null, orderBy, null); } + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String orderBy, String limit) { + return query(db, columns, mGroupBy.toString(), null, orderBy, limit); + } + /** * Execute query using the current internal state as {@code WHERE} clause. */ diff --git a/app/src/main/res/layout/fragment_tvshow_overview.xml b/app/src/main/res/layout/fragment_tvshow_overview.xml index 35ecad3..d4cdab2 100644 --- a/app/src/main/res/layout/fragment_tvshow_overview.xml +++ b/app/src/main/res/layout/fragment_tvshow_overview.xml @@ -173,7 +173,7 @@ style="@style/DefaultDividerH"/> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0406378..afa2d93 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -288,6 +288,7 @@ Overview Episodes Seasons + Next Episodes Overview Content