Add next episodes section to tv show details screen

This commit is contained in:
Synced Synapse 2016-12-16 19:14:06 +00:00
parent 04d901cf03
commit 661908c922
9 changed files with 326 additions and 14 deletions

View File

@ -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()

View File

@ -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;

View File

@ -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);

View File

@ -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<Cursor> {
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<Cursor> 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.
*/

View File

@ -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

View File

@ -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.
*/

View File

@ -173,7 +173,7 @@
style="@style/DefaultDividerH"/>
<TextView
android:id="@+id/seasons_title"
android:id="@+id/next_episode_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/separator"
@ -181,6 +181,26 @@
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:paddingTop="@dimen/default_padding"
android:text="@string/tvshow_next_episode"/>
<GridLayout
android:id="@+id/next_episode_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/next_episode_title"
android:columnCount="@integer/seasons_grid_view_columns"
android:orientation="horizontal"
android:useDefaultMargins="true"/>
<TextView
android:id="@+id/seasons_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/next_episode_list"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:paddingTop="@dimen/default_padding"
android:text="@string/tvshow_seasons"/>
<!--

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 Synced Synapse. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
card_view:cardElevation="@dimen/default_card_elevation"
card_view:cardBackgroundColor="?attr/appCardBackgroundColor">
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/art"
android:layout_width="@dimen/episodelist_art_width"
android:layout_height="@dimen/episodelist_art_heigth"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/poster"
android:scaleType="centerCrop"/>
<ImageView
android:id="@+id/list_context_menu"
android:layout_width="@dimen/default_icon_size"
android:layout_height="@dimen/default_icon_size"
android:layout_alignTop="@id/art"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:padding="@dimen/default_icon_padding"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconOverflow"
android:contentDescription="@string/action_options"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/art"
android:layout_toEndOf="@id/art"
android:layout_toLeftOf="@id/list_context_menu"
android:layout_toStartOf="@id/list_context_menu"
android:layout_alignTop="@id/art"
style="@style/TextAppearance.Medialist.Title"/>
<TextView
android:id="@+id/details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/title"
android:layout_alignStart="@id/title"
android:layout_below="@id/title"
style="@style/TextAppearance.Medialist.Details"/>
<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/title"
android:layout_alignStart="@id/title"
android:layout_below="@id/details"
android:layout_alignParentBottom="true"
style="@style/TextAppearance.Medialist.OtherInfo"/>
</RelativeLayout>
</android.support.v7.widget.CardView>

View File

@ -288,6 +288,7 @@
<string name="tvshow_overview">Overview</string>
<string name="tvshow_episodes">Episodes</string>
<string name="tvshow_seasons">Seasons</string>
<string name="tvshow_next_episode">Next Episodes</string>
<string name="addon_overview">Overview</string>
<string name="addon_content">Content</string>