Redesign tv shows details, to not use tabs or expandable lists in episodes list

First version, to be improved visually
This commit is contained in:
Synced Synapse 2016-12-15 20:10:55 +00:00
parent d993126c7e
commit 43371b9480
18 changed files with 894 additions and 1033 deletions

View File

@ -200,6 +200,23 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment
*/ */
abstract protected String getListSyncType(); abstract protected String getListSyncType();
/**
* Should return the {@link LibrarySyncService} syncID if this fragment
* synchronizes a single item. The itemId that should be synced must returned by {@link #getSyncItemID()}
* @return {@link LibrarySyncService} SyncID if syncing a single item. Null if not aplicable
*/
protected String getSyncID() {
return null;
}
/**
* Should return the item ID for SyncID returned by {@link #getSyncID()}
* @return -1 if not used.
*/
protected int getSyncItemID() {
return -1;
}
/** /**
* Event bus post. Called when the syncing process ended * Event bus post. Called when the syncing process ended
* *
@ -238,10 +255,8 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment
@Override @Override
public void onServiceConnected(LibrarySyncService librarySyncService) { public void onServiceConnected(LibrarySyncService librarySyncService) {
if(SyncUtils.isLibrarySyncing( HostInfo hostInfo = HostManager.getInstance(getActivity()).getHostInfo();
librarySyncService, if(SyncUtils.isLibrarySyncing(librarySyncService, hostInfo, getListSyncType())) {
HostManager.getInstance(getActivity()).getHostInfo(),
getListSyncType())) {
showRefreshAnimation(); showRefreshAnimation();
} }
} }
@ -263,6 +278,13 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment
// Start the syncing process // Start the syncing process
Intent syncIntent = new Intent(this.getActivity(), LibrarySyncService.class); Intent syncIntent = new Intent(this.getActivity(), LibrarySyncService.class);
syncIntent.putExtra(getListSyncType(), true); syncIntent.putExtra(getListSyncType(), true);
String syncID = getSyncID();
int itemId = getSyncItemID();
if ((syncID != null) && (itemId != -1)) {
syncIntent.putExtra(syncID, itemId);
}
getActivity().startService(syncIntent); getActivity().startService(syncIntent);
} }

View File

@ -208,11 +208,11 @@ public class MoviesActivity extends BaseActivity
selectedMovieTitle = vh.movieTitle; selectedMovieTitle = vh.movieTitle;
selectedMovieId = vh.movieId; selectedMovieId = vh.movieId;
final MovieDetailsFragment movieDetailsFragment = MovieDetailsFragment.newInstance(vh);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions // Set up transitions
if (Utils.isLollipopOrLater()) { if (Utils.isLollipopOrLater()) {
final MovieDetailsFragment movieDetailsFragment = MovieDetailsFragment.newInstance(vh);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() {
@Override @Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
@ -230,31 +230,24 @@ public class MoviesActivity extends BaseActivity
} }
}; };
movieDetailsFragment.setEnterSharedElementCallback(seCallback); movieDetailsFragment.setEnterSharedElementCallback(seCallback);
movieDetailsFragment.setEnterTransition(
movieDetailsFragment.setEnterTransition(TransitionInflater TransitionInflater.from(this)
.from(this) .inflateTransition(R.transition.media_details));
.inflateTransition(R.transition.media_details));
movieDetailsFragment.setReturnTransition(null); movieDetailsFragment.setReturnTransition(null);
Transition changeImageTransition = TransitionInflater.from( Transition changeImageTransition =
this).inflateTransition(R.transition.change_image); TransitionInflater.from(this).inflateTransition(R.transition.change_image);
movieDetailsFragment.setSharedElementReturnTransition(changeImageTransition); movieDetailsFragment.setSharedElementReturnTransition(changeImageTransition);
movieDetailsFragment.setSharedElementEnterTransition(changeImageTransition); movieDetailsFragment.setSharedElementEnterTransition(changeImageTransition);
fragTrans.replace(R.id.fragment_container, movieDetailsFragment) fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName());
.addToBackStack(null)
.addSharedElement(vh.artView, vh.artView.getTransitionName())
.commit();
} else { } else {
MovieDetailsFragment movieDetailsFragment = MovieDetailsFragment.newInstance(vh);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0,
R.anim.fragment_list_popenter, 0); R.anim.fragment_list_popenter, 0);
fragTrans.replace(R.id.fragment_container, movieDetailsFragment)
.addToBackStack(null)
.commit();
} }
fragTrans.replace(R.id.fragment_container, movieDetailsFragment)
.addToBackStack(null)
.commit();
setupActionBar(selectedMovieTitle); setupActionBar(selectedMovieTitle);
} }

View File

@ -16,28 +16,49 @@
package org.xbmc.kore.ui; package org.xbmc.kore.ui;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.provider.BaseColumns;
import android.support.v4.view.ViewPager; import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.DisplayMetrics;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import com.astuetz.PagerSlidingTabStrip; import android.widget.GridLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import org.xbmc.kore.R; 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.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.LogUtils;
import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils; import org.xbmc.kore.utils.Utils;
import java.util.ArrayList;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.InjectView; import butterknife.InjectView;
/** /**
* Container for the TV Show overview and Episodes list * Presents a TV Show overview
*/ */
public class TVShowDetailsFragment extends Fragment { public class TVShowDetailsFragment extends AbstractDetailsFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(TVShowDetailsFragment.class); private static final String TAG = LogUtils.makeLogTag(TVShowDetailsFragment.class);
public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME";
@ -51,13 +72,54 @@ public class TVShowDetailsFragment extends Fragment {
public static final String BUNDLE_KEY_PLOT = "plot"; public static final String BUNDLE_KEY_PLOT = "plot";
public static final String BUNDLE_KEY_GENRES = "genres"; public static final String BUNDLE_KEY_GENRES = "genres";
public interface OnSeasonSelectedListener {
void onSeasonSelected(int tvshowId, int season);
}
// Activity listener
private OnSeasonSelectedListener listenerActivity;
// Loader IDs
private static final int LOADER_TVSHOW = 0,
LOADER_SEASONS = 1,
LOADER_CAST = 2;
// Displayed movie id // Displayed movie id
private int tvshowId = -1; private int tvshowId = -1;
private String tvshowTitle;
private TabsAdapter tabsAdapter; // Controls whether a automatic sync refresh has been issued for this show
private static boolean hasIssuedOutdatedRefresh = false;
@InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip; @InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;
@InjectView(R.id.pager) ViewPager viewPager;
// // Buttons
// @InjectView(R.id.go_to_imdb) ImageButton imdbButton;
// Detail views
@InjectView(R.id.media_panel) ScrollView mediaPanel;
@InjectView(R.id.art) ImageView mediaArt;
@InjectView(R.id.poster) ImageView mediaPoster;
@InjectView(R.id.media_title) TextView mediaTitle;
@InjectView(R.id.media_undertitle) TextView mediaUndertitle;
@InjectView(R.id.rating) TextView mediaRating;
@InjectView(R.id.max_rating) TextView mediaMaxRating;
@InjectView(R.id.premiered) TextView mediaPremiered;
@InjectView(R.id.genres) TextView mediaGenres;
@InjectView(R.id.media_description) TextView mediaDescription;
@InjectView(R.id.cast_list) GridLayout videoCastList;
@InjectView(R.id.seasons_title) TextView seasonsListTitle;
@InjectView(R.id.seasons_list) GridLayout seasonsList;
@InjectView(R.id.media_description_container) LinearLayout mediaDescriptionContainer;
@InjectView(R.id.show_all) ImageView mediaShowAll;
private boolean isDescriptionExpanded = false;
/** /**
* Create a new instance of this, initialized to show tvshowId * Create a new instance of this, initialized to show tvshowId
@ -84,61 +146,452 @@ public class TVShowDetailsFragment extends Fragment {
} }
@Override @Override
public void onCreate(Bundle savedInstanceState) { @TargetApi(21)
super.onCreate(savedInstanceState); protected View createView(LayoutInflater inflater, ViewGroup container) {
} Bundle bundle = getArguments();
tvshowId = bundle.getInt(BUNDLE_KEY_TVSHOWID, -1);
@Override if (tvshowId == -1) {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // There's nothing to show
tvshowId = getArguments().getInt(BUNDLE_KEY_TVSHOWID, -1);
if ((container == null) || (tvshowId == -1)) {
// We're not being shown or there's nothing to show
return null; return null;
} }
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_default_view_pager, container, false); ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_tvshow_overview, container, false);
ButterKnife.inject(this, root); ButterKnife.inject(this, root);
long baseFragmentId = tvshowId * 10; //UIUtils.setSwipeRefreshLayoutColorScheme(swipeRefreshLayout);
tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager())
.addTab(TVShowOverviewFragment.class, getArguments(), R.string.tvshow_overview,
baseFragmentId)
.addTab(TVShowEpisodeListFragment.class, getArguments(),
R.string.tvshow_episodes, baseFragmentId + 1);
viewPager.setAdapter(tabsAdapter); // Setup dim the fanart when scroll changes. Full dim on 4 * iconSize dp
pagerTabStrip.setViewPager(viewPager); Resources resources = getActivity().getResources();
final int pixelsToTransparent = 4 * resources.getDimensionPixelSize(R.dimen.default_icon_size);
mediaPanel.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
float y = mediaPanel.getScrollY();
float newAlpha = Math.min(1, Math.max(0, 1 - (y / pixelsToTransparent)));
mediaArt.setAlpha(newAlpha);
}
});
tvshowTitle = bundle.getString(BUNDLE_KEY_TITLE);
mediaTitle.setText(tvshowTitle);
setMediaUndertitle(bundle.getInt(BUNDLE_KEY_EPISODE), bundle.getInt(BUNDLE_KEY_WATCHEDEPISODES));
setMediaPremiered(bundle.getString(BUNDLE_KEY_PREMIERED), bundle.getString(BUNDLE_KEY_STUDIO));
mediaGenres.setText(bundle.getString(BUNDLE_KEY_GENRES));
setMediaRating(bundle.getDouble(BUNDLE_KEY_RATING));
mediaDescription.setText(bundle.getString(BUNDLE_KEY_PLOT));
if(Utils.isLollipopOrLater()) {
mediaPoster.setTransitionName(getArguments().getString(POSTER_TRANS_NAME));
}
// Pad main content view to overlap with bottom system bar
// UIUtils.setPaddingForSystemBars(getActivity(), mediaPanel, false, false, true);
// mediaPanel.setClipToPadding(false);
return root; return root;
} }
@Override
protected String getSyncType() {
return LibrarySyncService.SYNC_SINGLE_TVSHOW;
}
@Override
protected String getSyncID() {
return LibrarySyncService.SYNC_TVSHOWID;
}
@Override
protected int getSyncItemID() {
return tvshowId;
}
@Override
protected SwipeRefreshLayout getSwipeRefreshLayout() {
return swipeRefreshLayout;
}
@Override
protected void onDownload() { }
@Override @Override
public void onActivityCreated (Bundle savedInstanceState) { public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false);
hasIssuedOutdatedRefresh = false;
// Start the loaders
getLoaderManager().initLoader(LOADER_TVSHOW, null, this);
getLoaderManager().initLoader(LOADER_SEASONS, null, this);
getLoaderManager().initLoader(LOADER_CAST, null, this);
} }
public Fragment getCurrentTabFragment() { @Override
return tabsAdapter.getItem(viewPager.getCurrentItem()); public void onAttach(Context activity) {
super.onAttach(activity);
try {
listenerActivity = (OnSeasonSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnSeasonSelectedListener");
}
} }
@Override
public void onDetach() {
super.onDetach();
listenerActivity = null;
}
@Override
protected void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_TVSHOW, null, this);
getLoaderManager().restartLoader(LOADER_SEASONS, null, this);
getLoaderManager().restartLoader(LOADER_CAST, null, this);
}
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_TVSHOW:
uri = MediaContract.TVShows.buildTVShowUri(getHostInfo().getId(), tvshowId);
return new CursorLoader(getActivity(), uri,
TVShowDetailsQuery.PROJECTION, null, null, null);
case LOADER_SEASONS:
// Load seasons
uri = MediaContract.Seasons.buildTVShowSeasonsListUri(getHostInfo().getId(), tvshowId);
return new CursorLoader(getActivity(), uri,
SeasonsListQuery.PROJECTION, null, null, SeasonsListQuery.SORT);
case LOADER_CAST:
uri = MediaContract.TVShowCast.buildTVShowCastListUri(getHostInfo().getId(), tvshowId);
return new CursorLoader(getActivity(), uri,
TVShowCastListQuery.PROJECTION, null, null, TVShowCastListQuery.SORT);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
LogUtils.LOGD(TAG, "onLoadFinished");
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_TVSHOW:
displayTVShowDetails(cursor);
checkOutdatedTVShowDetails(cursor);
break;
case LOADER_SEASONS:
displaySeasonList(cursor);
break;
case LOADER_CAST:
displayCastList(cursor);
break;
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
// /**
// * Callbacks for button bar
// */
// @OnClick(R.id.go_to_imdb)
// public void onImdbClicked(View v) {
// String imdbNumber = (String)v.getTag();
//
// if (imdbNumber != null) {
// Utils.openImdbForMovie(getActivity(), imdbNumber);
// }
// }
/**
* Display the tv show details
*
* @param cursor Cursor with the data
*/
private void displayTVShowDetails(Cursor cursor) {
LogUtils.LOGD(TAG, "displayTVShowDetails");
cursor.moveToFirst();
tvshowTitle = cursor.getString(TVShowDetailsQuery.TITLE);
mediaTitle.setText(tvshowTitle);
int numEpisodes = cursor.getInt(TVShowDetailsQuery.EPISODE),
watchedEpisodes = cursor.getInt(TVShowDetailsQuery.WATCHEDEPISODES);
setMediaUndertitle(numEpisodes, watchedEpisodes);
setMediaPremiered(cursor.getString(TVShowDetailsQuery.PREMIERED), cursor.getString(TVShowDetailsQuery.STUDIO));
mediaGenres.setText(cursor.getString(TVShowDetailsQuery.GENRES));
setMediaRating(cursor.getDouble(TVShowDetailsQuery.RATING));
mediaDescription.setText(cursor.getString(TVShowDetailsQuery.PLOT));
Resources resources = getActivity().getResources();
final int maxLines = resources.getInteger(R.integer.description_max_lines);
TypedArray styledAttributes = getActivity().getTheme().obtainStyledAttributes(new int[] {
R.attr.iconExpand,
R.attr.iconCollapse
});
final int iconCollapseResId =
styledAttributes.getResourceId(styledAttributes.getIndex(0), R.drawable.ic_expand_less_white_24dp);
final int iconExpandResId =
styledAttributes.getResourceId(styledAttributes.getIndex(1), R.drawable.ic_expand_more_white_24dp);
styledAttributes.recycle();
mediaDescriptionContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isDescriptionExpanded) {
mediaDescription.setMaxLines(Integer.MAX_VALUE);
mediaShowAll.setImageResource(iconExpandResId);
} else {
mediaDescription.setMaxLines(maxLines);
mediaShowAll.setImageResource(iconCollapseResId);
}
isDescriptionExpanded = !isDescriptionExpanded;
}
});
// // IMDB button
// imdbButton.setTag(cursor.getString(TVShowDetailsQuery.IMDBNUMBER));
// Images
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int posterWidth = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_width);
int posterHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_height);
UIUtils.loadImageWithCharacterAvatar(getActivity(), getHostManager(),
cursor.getString(TVShowDetailsQuery.THUMBNAIL), tvshowTitle,
mediaPoster, posterWidth, posterHeight);
int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height);
UIUtils.loadImageIntoImageview(getHostManager(),
cursor.getString(TVShowDetailsQuery.FANART),
mediaArt, displayMetrics.widthPixels, artHeight);
}
private void setMediaUndertitle(int numEpisodes, int watchedEpisodes) {
String episodes = String.format(getString(R.string.num_episodes),
numEpisodes, numEpisodes - watchedEpisodes);
mediaUndertitle.setText(episodes);
}
private void setMediaPremiered(String premiered, String studio) {
mediaPremiered.setText(String.format(getString(R.string.premiered), premiered) + " | " + studio);
}
private void setMediaRating(double rating) {
if (rating > 0) {
mediaRating.setVisibility(View.VISIBLE);
mediaMaxRating.setVisibility(View.VISIBLE);
mediaRating.setText(String.format("%01.01f", rating));
mediaMaxRating.setText(getString(R.string.max_rating_video));
} else {
mediaRating.setVisibility(View.INVISIBLE);
mediaMaxRating.setVisibility(View.INVISIBLE);
}
}
/**
* Display the cast details
*
* @param cursor Cursor with the data
*/
private void displayCastList(Cursor cursor) {
// Transform the cursor into a List<VideoType.Cast>
if (cursor.moveToFirst()) {
ArrayList<VideoType.Cast> castArrayList = new ArrayList<VideoType.Cast>(cursor.getCount());
do {
castArrayList.add(new VideoType.Cast(cursor.getString(TVShowCastListQuery.NAME),
cursor.getInt(TVShowCastListQuery.ORDER),
cursor.getString(TVShowCastListQuery.ROLE),
cursor.getString(TVShowCastListQuery.THUMBNAIL)));
} while (cursor.moveToNext());
UIUtils.setupCastInfo(getActivity(), castArrayList, videoCastList,
AllCastActivity.buildLaunchIntent(getActivity(), tvshowTitle, castArrayList));
}
}
/**
* Display the seasons list
*
* @param cursor Cursor with the data
*/
private void displaySeasonList(Cursor cursor) {
if (cursor.moveToFirst()) {
HostManager hostManager = HostManager.getInstance(getActivity());
View.OnClickListener seasonListClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
listenerActivity.onSeasonSelected(tvshowId, (int)v.getTag());
}
};
// Get the art dimensions
Resources resources = getActivity().getResources();
int artWidth = (int)(resources.getDimension(R.dimen.seasonlist_art_width) /
UIUtils.IMAGE_RESIZE_FACTOR);
int artHeight = (int)(resources.getDimension(R.dimen.seasonlist_art_heigth) /
UIUtils.IMAGE_RESIZE_FACTOR);
seasonsList.removeAllViews();
do {
int seasonNumber = cursor.getInt(SeasonsListQuery.SEASON);
String thumbnail = cursor.getString(SeasonsListQuery.THUMBNAIL);
int numEpisodes = cursor.getInt(SeasonsListQuery.EPISODE);
int watchedEpisodes = cursor.getInt(SeasonsListQuery.WATCHEDEPISODES);
View seasonView = LayoutInflater.from(getActivity()).inflate(R.layout.grid_item_season, seasonsList, false);
ImageView seasonPictureView = (ImageView) seasonView.findViewById(R.id.art);
TextView seasonNumberView = (TextView) seasonView.findViewById(R.id.season);
TextView seasonEpisodesView = (TextView) seasonView.findViewById(R.id.episodes);
seasonNumberView.setText(String.format(getActivity().getString(R.string.season_number), seasonNumber));
seasonEpisodesView.setText(String.format(getActivity().getString(R.string.num_episodes),
numEpisodes, numEpisodes - watchedEpisodes));
UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager,
thumbnail,
String.valueOf(seasonNumber),
seasonPictureView, artWidth, artHeight);
seasonView.setTag(seasonNumber);
seasonView.setOnClickListener(seasonListClickListener);
seasonsList.addView(seasonView);
} while (cursor.moveToNext());
} else {
// No seasons, hide views
seasonsListTitle.setVisibility(View.GONE);
seasonsList.setVisibility(View.INVISIBLE);
}
}
/**
* Checks wether we should refresh the TV Show details with the info on XBMC
* The details will be updated if the last update is older than what is configured in the
* settings
*
* @param cursor Cursor with the data
*/
private void checkOutdatedTVShowDetails(Cursor cursor) {
if (hasIssuedOutdatedRefresh)
return;
cursor.moveToFirst();
long lastUpdated = cursor.getLong(TVShowDetailsQuery.UPDATED);
if (System.currentTimeMillis() > lastUpdated + Settings.DB_UPDATE_INTERVAL) {
// Trigger a silent refresh
hasIssuedOutdatedRefresh = true;
startSync(true);
}
}
/**
* Returns the shared element if visible
* @return View if visible, null otherwise
*/
public View getSharedElement() { public View getSharedElement() {
View view = getView(); if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) {
if (view == null) return mediaPoster;
return null;
//Note: this works as R.id.poster is only used in TVShowOverviewFragment.
//If the same id is used in other fragments in the TabsAdapter we
//need to check which fragment is currently displayed
View artView = view.findViewById(R.id.poster);
View scrollView = view.findViewById(R.id.media_panel);
if (( artView != null ) &&
( scrollView != null ) &&
UIUtils.isViewInBounds(scrollView, artView)) {
return artView;
} }
return null; return null;
} }
/**
* TV Show details query parameters.
*/
private interface TVShowDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.TVShows.TITLE,
MediaContract.TVShows.THUMBNAIL,
MediaContract.TVShows.FANART,
MediaContract.TVShows.PREMIERED,
MediaContract.TVShows.STUDIO,
MediaContract.TVShows.EPISODE,
MediaContract.TVShows.WATCHEDEPISODES,
MediaContract.TVShows.RATING,
MediaContract.TVShows.PLOT,
MediaContract.TVShows.PLAYCOUNT,
MediaContract.TVShows.IMDBNUMBER,
MediaContract.TVShows.GENRES,
MediaContract.SyncColumns.UPDATED,
};
int ID = 0;
int TITLE = 1;
int THUMBNAIL = 2;
int FANART = 3;
int PREMIERED = 4;
int STUDIO = 5;
int EPISODE = 6;
int WATCHEDEPISODES = 7;
int RATING = 8;
int PLOT = 9;
int PLAYCOUNT = 10;
int IMDBNUMBER = 11;
int GENRES = 12;
int UPDATED = 13;
}
/**
* Seasons list query parameters.
*/
private interface SeasonsListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Seasons.SEASON,
MediaContract.Seasons.THUMBNAIL,
MediaContract.Seasons.EPISODE,
MediaContract.Seasons.WATCHEDEPISODES
};
String SORT = MediaContract.Seasons.SEASON + " ASC";
int ID = 0;
int SEASON = 1;
int THUMBNAIL = 2;
int EPISODE = 3;
int WATCHEDEPISODES = 4;
}
/**
* Movie cast list query parameters.
*/
public interface TVShowCastListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.TVShowCast.NAME,
MediaContract.TVShowCast.ORDER,
MediaContract.TVShowCast.ROLE,
MediaContract.TVShowCast.THUMBNAIL,
};
String SORT = MediaContract.TVShowCast.ORDER + " ASC";
int ID = 0;
int NAME = 1;
int ORDER = 2;
int ROLE = 3;
int THUMBNAIL = 4;
}
} }

View File

@ -15,7 +15,7 @@
*/ */
package org.xbmc.kore.ui; package org.xbmc.kore.ui;
import android.app.Activity; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Resources; import android.content.res.Resources;
@ -25,198 +25,128 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.provider.BaseColumns; import android.provider.BaseColumns;
import android.support.v4.app.LoaderManager; import android.support.annotation.Nullable;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.CursorTreeAdapter;
import android.widget.ExpandableListView;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.CursorAdapter;
import org.xbmc.kore.R; import org.xbmc.kore.R;
import org.xbmc.kore.Settings; import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager; 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.PlaylistType;
import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.MediaPlayerUtils; import org.xbmc.kore.utils.MediaPlayerUtils;
import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.util.HashMap;
import butterknife.ButterKnife;
import butterknife.InjectView;
/** /**
* Presents a list of episodes for a TV show * Presents a list of episodes for a TV show season
*/ */
public class TVShowEpisodeListFragment extends AbstractDetailsFragment public class TVShowEpisodeListFragment extends AbstractCursorListFragment {
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(TVShowEpisodeListFragment.class); private static final String TAG = LogUtils.makeLogTag(TVShowEpisodeListFragment.class);
public interface OnEpisodeSelectedListener { public interface OnEpisodeSelectedListener {
public void onEpisodeSelected(int tvshowId, int episodeId); void onEpisodeSelected(EpisodeViewHolder vh);
} }
public static final String TVSHOWID = "tvshow_id"; public static final String TVSHOWID = "tvshow_id";
public static final String SEASON = "season"; public static final String TVSHOWSEASON = "season";
private final String BUNDLE_SAVEDINSTANCE_LISTPOSITION = "lposition";
private final String BUNDLE_SAVEDINSTANCE_ITEMPOSITION = "iposition";
private final String BUNDLE_SAVEDINSTANCE_GROUPSEXPANDED = "groupsexpanded";
// Loader IDs. Must be -1 to differentiate from group position
private static final int LOADER_SEASONS = -1;
private int listPosition = 0;
private int itemPosition = 0;
private HashMap<Integer, Boolean> groupsExpanded = new HashMap<>();
private HashMap<Integer, Boolean> childCursorsLoading;
private boolean isReturning; // used to determine if we are returning to this fragment and need to restore the state
// Displayed show id // Displayed show id
private int tvshowId = -1; private int tvshowId = -1;
private int tvshowSeason = -1;
// Activity listener // Activity listener
private OnEpisodeSelectedListener listenerActivity; private OnEpisodeSelectedListener listenerActivity;
private SeasonsEpisodesAdapter adapter; /**
* Create a new instance of this, initialized to show tvshowId
*/
@TargetApi(21)
public static TVShowEpisodeListFragment newInstance(int tvshowId, int season) {
TVShowEpisodeListFragment fragment = new TVShowEpisodeListFragment();
@InjectView(R.id.list) ExpandableListView seasonsEpisodesListView; Bundle args = new Bundle();
@InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout; args.putInt(TVSHOWID, tvshowId);
@InjectView(android.R.id.empty) TextView emptyView; args.putInt(TVSHOWSEASON, season);
fragment.setArguments(args);
return fragment;
}
@Override @Override
protected View createView(LayoutInflater inflater, ViewGroup container) { protected String getListSyncType() { return LibrarySyncService.SYNC_SINGLE_TVSHOW; }
@Override
protected String getSyncID() { return LibrarySyncService.SYNC_TVSHOWID; };
@Override
protected int getSyncItemID() { return tvshowId; };
@TargetApi(16) @Nullable @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = super.onCreateView(inflater, container, savedInstanceState);
tvshowId = getArguments().getInt(TVSHOWID, -1); tvshowId = getArguments().getInt(TVSHOWID, -1);
if (tvshowId == -1) { tvshowSeason = getArguments().getInt(TVSHOWSEASON, -1);
if ((tvshowId == -1) || (tvshowSeason == -1)) {
// There's nothing to show // There's nothing to show
return null; return null;
} }
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_tvshow_episodes_list, container, false);
ButterKnife.inject(this, root);
// Configure the adapter and start the loader
adapter = new SeasonsEpisodesAdapter(getActivity());
seasonsEpisodesListView.setAdapter(adapter);
return root; return root;
} }
@Override @Override
protected String getSyncType() { protected void onListItemClicked(View view) {
return LibrarySyncService.SYNC_SINGLE_TVSHOW; // Get the movie id from the tag
EpisodeViewHolder tag = (EpisodeViewHolder) view.getTag();
// Notify the activity
listenerActivity.onEpisodeSelected(tag);
}
@Override
protected CursorAdapter createAdapter() {
return new SeasonsEpisodesAdapter(getActivity());
} }
@Override @Override
protected String getSyncID() { protected CursorLoader createCursorLoader() {
return LibrarySyncService.SYNC_TVSHOWID; HostInfo hostInfo = HostManager.getInstance(getActivity()).getHostInfo();
} Uri uri = MediaContract.Episodes.buildTVShowSeasonEpisodesListUri(hostInfo.getId(), tvshowId, tvshowSeason);
@Override // Filters
protected int getSyncItemID() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
return tvshowId; StringBuilder selection = new StringBuilder();
} if (preferences.getBoolean(Settings.KEY_PREF_TVSHOWS_FILTER_HIDE_WATCHED, Settings.DEFAULT_PREF_TVSHOWS_FILTER_HIDE_WATCHED)) {
selection.append(MediaContract.EpisodesColumns.PLAYCOUNT)
@Override .append("=0");
protected SwipeRefreshLayout getSwipeRefreshLayout() {
return swipeRefreshLayout;
}
@Override
protected void onDownload() {
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
seasonsEpisodesListView.setEmptyView(emptyView);
seasonsEpisodesListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
@Override
public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
if (parent.isGroupExpanded(groupPosition)) {
parent.collapseGroup(groupPosition);
groupsExpanded.remove(groupPosition);
} else {
parent.expandGroup(groupPosition);
groupsExpanded.put(groupPosition, true);
}
return true;
}
});
seasonsEpisodesListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
// Get the movie id from the tag
EpisodeViewHolder tag = (EpisodeViewHolder) v.getTag();
// Notify the activity
listenerActivity.onEpisodeSelected(tvshowId, tag.episodeId);
return true;
}
});
setHasOptionsMenu(true);
if (savedInstanceState != null) {
listPosition = savedInstanceState.getInt(BUNDLE_SAVEDINSTANCE_LISTPOSITION, 0);
itemPosition = savedInstanceState.getInt(BUNDLE_SAVEDINSTANCE_ITEMPOSITION, 0);
groupsExpanded = (HashMap) savedInstanceState.getSerializable(BUNDLE_SAVEDINSTANCE_GROUPSEXPANDED);
isReturning = true;
} }
initLoader(); return new CursorLoader(getActivity(), uri,
EpisodesListQuery.PROJECTION, selection.toString(), null, EpisodesListQuery.SORT);
} }
@Override @Override
public void onAttach(Activity activity) { public void onAttach(Context context) {
super.onAttach(activity); super.onAttach(context);
try { try {
listenerActivity = (OnEpisodeSelectedListener) activity; listenerActivity = (OnEpisodeSelectedListener) context;
} catch (ClassCastException e) { } catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnEpisodeSelectedListener"); throw new ClassCastException(context.toString() + " must implement OnEpisodeSelectedListener");
} }
} }
@Override
public void onPause() {
super.onPause();
//Save scroll position
listPosition = seasonsEpisodesListView.getFirstVisiblePosition();
View itemView = seasonsEpisodesListView.getChildAt(0);
if (itemView != null) {
itemPosition = itemView.getTop();
} else {
itemPosition = 0;
}
isReturning = true;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(BUNDLE_SAVEDINSTANCE_ITEMPOSITION, itemPosition);
outState.putInt(BUNDLE_SAVEDINSTANCE_LISTPOSITION, listPosition);
outState.putSerializable(BUNDLE_SAVEDINSTANCE_GROUPSEXPANDED, groupsExpanded);
}
@Override @Override
public void onDetach() { public void onDetach() {
super.onDetach(); super.onDetach();
@ -250,7 +180,7 @@ public class TVShowEpisodeListFragment extends AbstractDetailsFragment
preferences.edit() preferences.edit()
.putBoolean(Settings.KEY_PREF_TVSHOW_EPISODES_FILTER_HIDE_WATCHED, item.isChecked()) .putBoolean(Settings.KEY_PREF_TVSHOW_EPISODES_FILTER_HIDE_WATCHED, item.isChecked())
.apply(); .apply();
getLoaderManager().restartLoader(LOADER_SEASONS, null, this); refreshList();
break; break;
default: default:
break; break;
@ -259,161 +189,6 @@ public class TVShowEpisodeListFragment extends AbstractDetailsFragment
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
protected void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_SEASONS, null, this);
}
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
if (!isAdded()) {
LogUtils.LOGD(TAG, "Trying to create a loader, but the fragment isn't added. " +
"Loader Id: " + id);
return null;
}
Uri uri;
StringBuilder selection = new StringBuilder();
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
boolean tvshowEpisodesFilterHideWatched =
preferences.getBoolean(Settings.KEY_PREF_TVSHOW_EPISODES_FILTER_HIDE_WATCHED, Settings.DEFAULT_PREF_TVSHOW_EPISODES_FILTER_HIDE_WATCHED);
switch (id) {
case LOADER_SEASONS:
// Load seasons
uri = MediaContract.Seasons.buildTVShowSeasonsListUri(getHostInfo().getId(), tvshowId);
// Filters
if (tvshowEpisodesFilterHideWatched) {
selection.append(MediaContract.SeasonsColumns.WATCHEDEPISODES)
.append("!=")
.append(MediaContract.SeasonsColumns.EPISODE);
}
return new CursorLoader(getActivity(), uri,
SeasonsListQuery.PROJECTION, selection.toString(), null, SeasonsListQuery.SORT);
default:
// Load episodes for a season. Season is in bundle
int season = bundle.getInt(SEASON);
uri = MediaContract.Episodes.buildTVShowSeasonEpisodesListUri(getHostInfo().getId(), tvshowId, season);
// Filters
if (tvshowEpisodesFilterHideWatched) {
selection.append(MediaContract.EpisodesColumns.PLAYCOUNT)
.append("=0");
}
return new CursorLoader(getActivity(), uri,
EpisodesListQuery.PROJECTION, selection.toString(), null, EpisodesListQuery.SORT);
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
switch (cursorLoader.getId()) {
case LOADER_SEASONS:
adapter.setGroupCursor(cursor);
setState(cursor);
// To prevent the empty text from appearing on the first load, set it now
emptyView.setText(getString(R.string.no_episodes_found));
break;
default:
// Check if the group cursor is set before setting the children cursor
// Somehow, when popping the back stack, the children cursor are reloaded first...
if (adapter.getCursor() != null) {
int id = cursorLoader.getId();
adapter.setChildrenCursor(id, cursor);
childCursorsLoading.remove(id);
if (isReturning && childCursorsLoading.isEmpty()) {
isReturning = false;
//All previous expanded child cursors loaded. Now we can finally restore the list position
seasonsEpisodesListView.setSelectionFromTop(listPosition, itemPosition);
}
}
break;
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
switch (cursorLoader.getId()) {
case LOADER_SEASONS:
adapter.setGroupCursor(null);
break;
default:
// Check if the group cursor is set before setting the children cursor
// Somehow, when popping the back stack, the children cursor are reloaded first...
if (adapter.getCursor() != null) {
try {
adapter.setChildrenCursor(cursorLoader.getId(), null);
} catch (NullPointerException exc) {
// Errrr... Adapter expired?
LogUtils.LOGW(TAG, "Adapter expired.");
}
}
break;
}
}
private void setState(Cursor cursor) {
if (cursor.getCount() == 1) {
seasonsEpisodesListView.expandGroup(0);
} else if (cursor.getCount() > 0) {
cursor.moveToFirst();
do {
int position = cursor.getPosition();
// Expand the first season that has unseen episodes
int unwatched = cursor.getInt(SeasonsListQuery.EPISODE) - cursor.getInt(SeasonsListQuery.WATCHEDEPISODES);
if (groupsExpanded.isEmpty() && (unwatched > 0)) {
seasonsEpisodesListView.expandGroup(position);
groupsExpanded.put(position, true);
break;
}
if (groupsExpanded.get(position) != null) {
seasonsEpisodesListView.expandGroup(position);
}
} while (cursor.moveToNext());
}
}
private void initLoader() {
childCursorsLoading = new HashMap<>();
for (int id : groupsExpanded.keySet()) {
childCursorsLoading.put(id, true);
}
getLoaderManager().initLoader(LOADER_SEASONS, null, this);
}
/**
* Seasons list query parameters.
*/
private interface SeasonsListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Seasons.SEASON,
MediaContract.Seasons.THUMBNAIL,
MediaContract.Seasons.EPISODE,
MediaContract.Seasons.WATCHEDEPISODES
};
String SORT = MediaContract.Seasons.SEASON + " ASC";
final int ID = 0;
final int SEASON = 1;
final int THUMBNAIL = 2;
final int EPISODE = 3;
final int WATCHEDEPISODES = 4;
}
/** /**
* Episodes list query parameters. * Episodes list query parameters.
*/ */
@ -431,111 +206,76 @@ public class TVShowEpisodeListFragment extends AbstractDetailsFragment
String SORT = MediaContract.Episodes.EPISODE + " ASC"; String SORT = MediaContract.Episodes.EPISODE + " ASC";
final int ID = 0; int ID = 0;
final int EPISODEID = 1; int EPISODEID = 1;
final int EPISODE = 2; int EPISODE = 2;
final int THUMBNAIL = 3; int THUMBNAIL = 3;
final int PLAYCOUNT = 4; int PLAYCOUNT = 4;
final int TITLE = 5; int TITLE = 5;
final int RUNTIME = 6; int RUNTIME = 6;
final int FIRSTAIRED = 7; int FIRSTAIRED = 7;
} }
/**
* Adapter for the {@link android.widget.ExpandableListView}
* Manages the seasons and episodes list
*/
private class SeasonsEpisodesAdapter extends CursorTreeAdapter {
private int themeAccentColor; private class SeasonsEpisodesAdapter extends CursorAdapter {
private int iconCollapseResId,
iconExpandResId;
private HostManager hostManager; private HostManager hostManager;
private int artWidth, artHeight; private int artWidth, artHeight;
private int themeAccentColor;
public SeasonsEpisodesAdapter(Context context) { public SeasonsEpisodesAdapter(Context context) {
// Cursor will be set vir CursorLoader super(context, null, false);
super(null, context);
// Get the default accent color // Get the default accent color
Resources.Theme theme = context.getTheme(); Resources.Theme theme = context.getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] {
R.attr.colorAccent, R.attr.colorAccent
R.attr.iconCollapse,
R.attr.iconExpand,
}); });
themeAccentColor = styledAttributes.getColor(styledAttributes.getIndex(0), getResources().getColor(R.color.accent_default)); themeAccentColor = styledAttributes.getColor(styledAttributes.getIndex(0), getResources().getColor(R.color.accent_default));
iconCollapseResId = styledAttributes.getResourceId(styledAttributes.getIndex(1), R.drawable.ic_expand_less_white_24dp);
iconExpandResId = styledAttributes.getResourceId(styledAttributes.getIndex(2), R.drawable.ic_expand_more_white_24dp);
styledAttributes.recycle(); styledAttributes.recycle();
this.hostManager = HostManager.getInstance(context); this.hostManager = HostManager.getInstance(context);
// Get the art dimensions // Get the art dimensions
Resources resources = context.getResources(); Resources resources = context.getResources();
artWidth = (int)(resources.getDimension(R.dimen.seasonlist_art_width) / artWidth = (int)(resources.getDimension(R.dimen.episodelist_art_width) /
UIUtils.IMAGE_RESIZE_FACTOR); UIUtils.IMAGE_RESIZE_FACTOR);
artHeight = (int)(resources.getDimension(R.dimen.seasonlist_art_heigth) / artHeight = (int)(resources.getDimension(R.dimen.episodelist_art_heigth) /
UIUtils.IMAGE_RESIZE_FACTOR); UIUtils.IMAGE_RESIZE_FACTOR);
} }
/** {@inheritDoc} */
@Override @Override
public View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) { public View newView(Context context, final Cursor cursor, ViewGroup parent) {
return LayoutInflater.from(context).inflate(R.layout.list_item_season, parent, false);
}
@Override
public View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) {
final View view = LayoutInflater.from(context) final View view = LayoutInflater.from(context)
.inflate(R.layout.list_item_episode, parent, false); .inflate(R.layout.list_item_episode, parent, false);
// Setup View holder pattern // Setup View holder pattern
EpisodeViewHolder viewHolder = new EpisodeViewHolder(); EpisodeViewHolder viewHolder = new EpisodeViewHolder();
viewHolder.container = (RelativeLayout)view.findViewById(R.id.container);
viewHolder.titleView = (TextView)view.findViewById(R.id.title); viewHolder.titleView = (TextView)view.findViewById(R.id.title);
viewHolder.detailsView = (TextView)view.findViewById(R.id.details); viewHolder.detailsView = (TextView)view.findViewById(R.id.details);
viewHolder.episodenumberView = (TextView)view.findViewById(R.id.episode_number); viewHolder.episodenumberView = (TextView)view.findViewById(R.id.episode_number);
viewHolder.contextMenuView = (ImageView)view.findViewById(R.id.list_context_menu); viewHolder.contextMenuView = (ImageView)view.findViewById(R.id.list_context_menu);
viewHolder.checkmarkView = (ImageView)view.findViewById(R.id.checkmark); viewHolder.checkmarkView = (ImageView)view.findViewById(R.id.checkmark);
viewHolder.artView = (ImageView)view.findViewById(R.id.art);
view.setTag(viewHolder); view.setTag(viewHolder);
return view; return view;
} }
/** {@inheritDoc} */
@Override @Override
public void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) { public void bindView(View view, Context context, Cursor cursor) {
TextView seasonView = (TextView)view.findViewById(R.id.season);
TextView episodesView = (TextView)view.findViewById(R.id.episodes);
ImageView artView = (ImageView)view.findViewById(R.id.art);
seasonView.setText(String.format(context.getString(R.string.season_number),
cursor.getInt(SeasonsListQuery.SEASON)));
int numEpisodes = cursor.getInt(SeasonsListQuery.EPISODE),
watchedEpisodes = cursor.getInt(SeasonsListQuery.WATCHEDEPISODES);
episodesView.setText(String.format(context.getString(R.string.num_episodes),
numEpisodes, numEpisodes - watchedEpisodes));
UIUtils.loadImageWithCharacterAvatar(context, hostManager,
cursor.getString(SeasonsListQuery.THUMBNAIL),
String.valueOf(cursor.getInt(SeasonsListQuery.SEASON)),
artView, artWidth, artHeight);
ImageView indicator = (ImageView)view.findViewById(R.id.status_indicator);
if (isExpanded) {
indicator.setImageResource(iconCollapseResId);
} else {
indicator.setImageResource(iconExpandResId);
}
}
@Override
public void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
final EpisodeViewHolder viewHolder = (EpisodeViewHolder)view.getTag(); final EpisodeViewHolder viewHolder = (EpisodeViewHolder)view.getTag();
// Save the episode id // Save the episode id
viewHolder.episodeId = cursor.getInt(EpisodesListQuery.EPISODEID); viewHolder.episodeId = cursor.getInt(EpisodesListQuery.EPISODEID);
viewHolder.title = cursor.getString(EpisodesListQuery.TITLE);
if(Utils.isLollipopOrLater()) {
viewHolder.artView.setTransitionName("a"+viewHolder.episodeId);
}
viewHolder.episodenumberView.setText( viewHolder.episodenumberView.setText(
String.format(context.getString(R.string.episode_number), String.format(context.getString(R.string.episode_number),
@ -555,49 +295,30 @@ public class TVShowEpisodeListFragment extends AbstractDetailsFragment
viewHolder.checkmarkView.setVisibility(View.INVISIBLE); viewHolder.checkmarkView.setVisibility(View.INVISIBLE);
} }
UIUtils.loadImageWithCharacterAvatar(context, hostManager,
cursor.getString(EpisodesListQuery.THUMBNAIL), viewHolder.title,
viewHolder.artView, artWidth, artHeight);
// For the popupmenu // For the popupmenu
ImageView contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); ImageView contextMenu = (ImageView)view.findViewById(R.id.list_context_menu);
contextMenu.setTag(viewHolder); contextMenu.setTag(viewHolder);
contextMenu.setOnClickListener(contextlistItemMenuClickListener); contextMenu.setOnClickListener(contextlistItemMenuClickListener);
} }
@Override
public Cursor getChildrenCursor(Cursor groupCursor) {
// Check if the fragment is attached to avoid IllegalStateException...
if (!isAdded()) return null;
// Start the episodes loader
final int season = groupCursor.getInt(SeasonsListQuery.SEASON);
Bundle bundle = new Bundle();
bundle.putInt(SEASON, season);
int groupPositon = groupCursor.getPosition();
// The season id will be passed in a bundle to the loadermanager, and the group id
// will be used as the loader's id
LoaderManager loaderManager = getLoaderManager();
if ((loaderManager.getLoader(groupPositon) == null) ||
(loaderManager.getLoader(groupPositon).isReset())) {
loaderManager.initLoader(groupPositon, bundle, TVShowEpisodeListFragment.this);
} else {
loaderManager.restartLoader(groupPositon, bundle, TVShowEpisodeListFragment.this);
}
return null;
}
} }
/** /**
* View holder pattern, only for episodes * View holder pattern, only for episodes
*/ */
private static class EpisodeViewHolder { public static class EpisodeViewHolder {
RelativeLayout container;
TextView titleView; TextView titleView;
TextView detailsView; TextView detailsView;
TextView episodenumberView; TextView episodenumberView;
ImageView contextMenuView; ImageView contextMenuView;
ImageView checkmarkView; ImageView checkmarkView;
// ImageView artView; ImageView artView;
int episodeId; int episodeId;
String title;
} }
private View.OnClickListener contextlistItemMenuClickListener = new View.OnClickListener() { private View.OnClickListener contextlistItemMenuClickListener = new View.OnClickListener() {

View File

@ -1,419 +0,0 @@
/*
* Copyright 2015 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.
*/
package org.xbmc.kore.ui;
import android.annotation.TargetApi;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.BaseColumns;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.GridLayout;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
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.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.util.ArrayList;
import butterknife.ButterKnife;
import butterknife.InjectView;
/**
* Presents a TV Show overview
*/
public class TVShowOverviewFragment extends AbstractDetailsFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(TVShowOverviewFragment.class);
public static final String TVSHOWID = "tvshow_id";
// Loader IDs
private static final int LOADER_TVSHOW = 0,
LOADER_CAST = 1;
// Displayed movie id
private int tvshowId = -1;
private String tvshowTitle;
private ArrayList<VideoType.Cast> castArrayList;
// Controls whether a automatic sync refresh has been issued for this show
private static boolean hasIssuedOutdatedRefresh = false;
@InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;
// // Buttons
// @InjectView(R.id.go_to_imdb) ImageButton imdbButton;
// Detail views
@InjectView(R.id.media_panel) ScrollView mediaPanel;
@InjectView(R.id.art) ImageView mediaArt;
@InjectView(R.id.poster) ImageView mediaPoster;
@InjectView(R.id.media_title) TextView mediaTitle;
@InjectView(R.id.media_undertitle) TextView mediaUndertitle;
@InjectView(R.id.rating) TextView mediaRating;
@InjectView(R.id.max_rating) TextView mediaMaxRating;
@InjectView(R.id.premiered) TextView mediaPremiered;
@InjectView(R.id.genres) TextView mediaGenres;
@InjectView(R.id.media_description) TextView mediaDescription;
@InjectView(R.id.cast_list) GridLayout videoCastList;
/**
* Create a new instance of this, initialized to show tvshowId
*/
public static TVShowOverviewFragment newInstance(int tvshowId) {
TVShowOverviewFragment fragment = new TVShowOverviewFragment();
Bundle args = new Bundle();
args.putInt(TVSHOWID, tvshowId);
fragment.setArguments(args);
return fragment;
}
@Override
@TargetApi(21)
protected View createView(LayoutInflater inflater, ViewGroup container) {
Bundle bundle = getArguments();
tvshowId = bundle.getInt(TVShowDetailsFragment.BUNDLE_KEY_TVSHOWID, -1);
if (tvshowId == -1) {
// There's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_tvshow_overview, container, false);
ButterKnife.inject(this, root);
//UIUtils.setSwipeRefreshLayoutColorScheme(swipeRefreshLayout);
// Setup dim the fanart when scroll changes. Full dim on 4 * iconSize dp
Resources resources = getActivity().getResources();
final int pixelsToTransparent = 4 * resources.getDimensionPixelSize(R.dimen.default_icon_size);
mediaPanel.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
float y = mediaPanel.getScrollY();
float newAlpha = Math.min(1, Math.max(0, 1 - (y / pixelsToTransparent)));
mediaArt.setAlpha(newAlpha);
}
});
tvshowTitle = bundle.getString(TVShowDetailsFragment.BUNDLE_KEY_TITLE);
mediaTitle.setText(tvshowTitle);
setMediaUndertitle(bundle.getInt(TVShowDetailsFragment.BUNDLE_KEY_EPISODE), bundle.getInt(TVShowDetailsFragment.BUNDLE_KEY_WATCHEDEPISODES));
setMediaPremiered(bundle.getString(TVShowDetailsFragment.BUNDLE_KEY_PREMIERED), bundle.getString(TVShowDetailsFragment.BUNDLE_KEY_STUDIO));
mediaGenres.setText(bundle.getString(TVShowDetailsFragment.BUNDLE_KEY_GENRES));
setMediaRating(bundle.getDouble(TVShowDetailsFragment.BUNDLE_KEY_RATING));
mediaDescription.setText(bundle.getString(TVShowDetailsFragment.BUNDLE_KEY_PLOT));
if(Utils.isLollipopOrLater()) {
mediaPoster.setTransitionName(getArguments().getString(TVShowDetailsFragment.POSTER_TRANS_NAME));
}
// Pad main content view to overlap with bottom system bar
// UIUtils.setPaddingForSystemBars(getActivity(), mediaPanel, false, false, true);
// mediaPanel.setClipToPadding(false);
return root;
}
@Override
protected String getSyncType() {
return LibrarySyncService.SYNC_SINGLE_TVSHOW;
}
@Override
protected String getSyncID() {
return LibrarySyncService.SYNC_TVSHOWID;
}
@Override
protected int getSyncItemID() {
return tvshowId;
}
@Override
protected SwipeRefreshLayout getSwipeRefreshLayout() {
return swipeRefreshLayout;
}
@Override
protected void onDownload() {
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
hasIssuedOutdatedRefresh = false;
// Start the loaders
getLoaderManager().initLoader(LOADER_TVSHOW, null, this);
getLoaderManager().initLoader(LOADER_CAST, null, this);
}
@Override
protected void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_TVSHOW, null, this);
getLoaderManager().restartLoader(LOADER_CAST, null, this);
}
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_TVSHOW:
uri = MediaContract.TVShows.buildTVShowUri(getHostInfo().getId(), tvshowId);
return new CursorLoader(getActivity(), uri,
TVShowDetailsQuery.PROJECTION, null, null, null);
case LOADER_CAST:
uri = MediaContract.TVShowCast.buildTVShowCastListUri(getHostInfo().getId(), tvshowId);
return new CursorLoader(getActivity(), uri,
TVShowCastListQuery.PROJECTION, null, null, TVShowCastListQuery.SORT);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
LogUtils.LOGD(TAG, "onLoadFinished");
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_TVSHOW:
displayTVShowDetails(cursor);
checkOutdatedTVShowDetails(cursor);
break;
case LOADER_CAST:
displayCastList(cursor);
break;
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
// /**
// * Callbacks for button bar
// */
// @OnClick(R.id.go_to_imdb)
// public void onImdbClicked(View v) {
// String imdbNumber = (String)v.getTag();
//
// if (imdbNumber != null) {
// Utils.openImdbForMovie(getActivity(), imdbNumber);
// }
// }
/**
* Display the tv show details
*
* @param cursor Cursor with the data
*/
private void displayTVShowDetails(Cursor cursor) {
LogUtils.LOGD(TAG, "displayTVShowDetails");
cursor.moveToFirst();
tvshowTitle = cursor.getString(TVShowDetailsQuery.TITLE);
mediaTitle.setText(tvshowTitle);
int numEpisodes = cursor.getInt(TVShowDetailsQuery.EPISODE),
watchedEpisodes = cursor.getInt(TVShowDetailsQuery.WATCHEDEPISODES);
setMediaUndertitle(numEpisodes, watchedEpisodes);
setMediaPremiered(cursor.getString(TVShowDetailsQuery.PREMIERED), cursor.getString(TVShowDetailsQuery.STUDIO));
mediaGenres.setText(cursor.getString(TVShowDetailsQuery.GENRES));
setMediaRating(cursor.getDouble(TVShowDetailsQuery.RATING));
mediaDescription.setText(cursor.getString(TVShowDetailsQuery.PLOT));
// // IMDB button
// imdbButton.setTag(cursor.getString(TVShowDetailsQuery.IMDBNUMBER));
// Images
Resources resources = getActivity().getResources();
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int posterWidth = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_width);
int posterHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_height);
UIUtils.loadImageWithCharacterAvatar(getActivity(), getHostManager(),
cursor.getString(TVShowDetailsQuery.THUMBNAIL), tvshowTitle,
mediaPoster, posterWidth, posterHeight);
int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height);
UIUtils.loadImageIntoImageview(getHostManager(),
cursor.getString(TVShowDetailsQuery.FANART),
mediaArt, displayMetrics.widthPixels, artHeight);
}
private void setMediaUndertitle(int numEpisodes, int watchedEpisodes) {
String episodes = String.format(getString(R.string.num_episodes),
numEpisodes, numEpisodes - watchedEpisodes);
mediaUndertitle.setText(episodes);
}
private void setMediaPremiered(String premiered, String studio) {
mediaPremiered.setText(String.format(getString(R.string.premiered),
premiered) + " | " + studio);
}
private void setMediaRating(double rating) {
if (rating > 0) {
mediaRating.setVisibility(View.VISIBLE);
mediaMaxRating.setVisibility(View.VISIBLE);
mediaRating.setText(String.format("%01.01f", rating));
mediaMaxRating.setText(getString(R.string.max_rating_video));
} else {
mediaRating.setVisibility(View.INVISIBLE);
mediaMaxRating.setVisibility(View.INVISIBLE);
}
}
/**
* Display the cast details
*
* @param cursor Cursor with the data
*/
private void displayCastList(Cursor cursor) {
// Transform the cursor into a List<VideoType.Cast>
if (cursor.moveToFirst()) {
castArrayList = new ArrayList<VideoType.Cast>(cursor.getCount());
do {
castArrayList.add(new VideoType.Cast(cursor.getString(TVShowCastListQuery.NAME),
cursor.getInt(TVShowCastListQuery.ORDER),
cursor.getString(TVShowCastListQuery.ROLE),
cursor.getString(TVShowCastListQuery.THUMBNAIL)));
} while (cursor.moveToNext());
UIUtils.setupCastInfo(getActivity(), castArrayList, videoCastList,
AllCastActivity.buildLaunchIntent(getActivity(), tvshowTitle, castArrayList));
}
}
/**
* Checks wether we should refresh the TV Show details with the info on XBMC
* The details will be updated if the last update is older than what is configured in the
* settings
*
* @param cursor Cursor with the data
*/
private void checkOutdatedTVShowDetails(Cursor cursor) {
if (hasIssuedOutdatedRefresh)
return;
cursor.moveToFirst();
long lastUpdated = cursor.getLong(TVShowDetailsQuery.UPDATED);
if (System.currentTimeMillis() > lastUpdated + Settings.DB_UPDATE_INTERVAL) {
// Trigger a silent refresh
hasIssuedOutdatedRefresh = true;
startSync(true);
}
}
/**
* TV Show details query parameters.
*/
private interface TVShowDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.TVShows.TITLE,
MediaContract.TVShows.THUMBNAIL,
MediaContract.TVShows.FANART,
MediaContract.TVShows.PREMIERED,
MediaContract.TVShows.STUDIO,
MediaContract.TVShows.EPISODE,
MediaContract.TVShows.WATCHEDEPISODES,
MediaContract.TVShows.RATING,
MediaContract.TVShows.PLOT,
MediaContract.TVShows.PLAYCOUNT,
MediaContract.TVShows.IMDBNUMBER,
MediaContract.TVShows.GENRES,
MediaContract.SyncColumns.UPDATED,
};
final int ID = 0;
final int TITLE = 1;
final int THUMBNAIL = 2;
final int FANART = 3;
final int PREMIERED = 4;
final int STUDIO = 5;
final int EPISODE = 6;
final int WATCHEDEPISODES = 7;
final int RATING = 8;
final int PLOT = 9;
final int PLAYCOUNT = 10;
final int IMDBNUMBER = 11;
final int GENRES = 12;
final int UPDATED = 13;
}
/**
* Movie cast list query parameters.
*/
public interface TVShowCastListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.TVShowCast.NAME,
MediaContract.TVShowCast.ORDER,
MediaContract.TVShowCast.ROLE,
MediaContract.TVShowCast.THUMBNAIL,
};
String SORT = MediaContract.TVShowCast.ORDER + " ASC";
final int ID = 0;
final int NAME = 1;
final int ORDER = 2;
final int ROLE = 3;
final int THUMBNAIL = 4;
}
}

View File

@ -42,18 +42,22 @@ import java.util.Map;
*/ */
public class TVShowsActivity extends BaseActivity public class TVShowsActivity extends BaseActivity
implements TVShowListFragment.OnTVShowSelectedListener, implements TVShowListFragment.OnTVShowSelectedListener,
TVShowEpisodeListFragment.OnEpisodeSelectedListener { TVShowDetailsFragment.OnSeasonSelectedListener,
TVShowEpisodeListFragment.OnEpisodeSelectedListener {
private static final String TAG = LogUtils.makeLogTag(TVShowsActivity.class); private static final String TAG = LogUtils.makeLogTag(TVShowsActivity.class);
public static final String TVSHOWID = "tvshow_id"; public static final String TVSHOWID = "tvshow_id";
public static final String TVSHOWTITLE = "tvshow_title"; public static final String TVSHOWTITLE = "tvshow_title";
public static final String EPISODEID = "episode_id"; public static final String EPISODEID = "episode_id";
public static final String SEASON = "season";
public static final String SEASONTITLE = "season_title";
private int selectedTVShowId = -1; private int selectedTVShowId = -1;
private String selectedTVShowTitle = null; private String selectedTVShowTitle = null;
private int selectedSeason = -1;
private String selectedSeasonTitle = null;
private int selectedEpisodeId = -1; private int selectedEpisodeId = -1;
private TVShowDetailsFragment tvshowDetailsFragment;
private boolean clearSharedElements; private boolean clearSharedElements;
private NavigationDrawerFragment navigationDrawerFragment; private NavigationDrawerFragment navigationDrawerFragment;
@ -108,6 +112,8 @@ public class TVShowsActivity extends BaseActivity
selectedTVShowId = savedInstanceState.getInt(TVSHOWID, -1); selectedTVShowId = savedInstanceState.getInt(TVSHOWID, -1);
selectedTVShowTitle = savedInstanceState.getString(TVSHOWTITLE, null); selectedTVShowTitle = savedInstanceState.getString(TVSHOWTITLE, null);
selectedEpisodeId = savedInstanceState.getInt(EPISODEID, -1); selectedEpisodeId = savedInstanceState.getInt(EPISODEID, -1);
selectedSeason = savedInstanceState.getInt(SEASON, -1);
selectedSeasonTitle = savedInstanceState.getString(SEASONTITLE, null);
} }
setupActionBar(selectedTVShowTitle); setupActionBar(selectedTVShowTitle);
@ -124,6 +130,8 @@ public class TVShowsActivity extends BaseActivity
outState.putInt(TVSHOWID, selectedTVShowId); outState.putInt(TVSHOWID, selectedTVShowId);
outState.putString(TVSHOWTITLE, selectedTVShowTitle); outState.putString(TVSHOWTITLE, selectedTVShowTitle);
outState.putInt(EPISODEID, selectedEpisodeId); outState.putInt(EPISODEID, selectedEpisodeId);
outState.putInt(SEASON, selectedSeason);
outState.putString(SEASONTITLE, selectedSeasonTitle);
} }
@Override @Override
@ -152,6 +160,12 @@ public class TVShowsActivity extends BaseActivity
if (selectedEpisodeId != -1) { if (selectedEpisodeId != -1) {
selectedEpisodeId = -1; selectedEpisodeId = -1;
getSupportFragmentManager().popBackStack(); getSupportFragmentManager().popBackStack();
setupActionBar(selectedSeasonTitle);
return true;
} else if (selectedSeason != -1) {
selectedSeason = -1;
getSupportFragmentManager().popBackStack();
setupActionBar(selectedTVShowTitle);
return true; return true;
} else if (selectedTVShowId != -1) { } else if (selectedTVShowId != -1) {
selectedTVShowId = -1; selectedTVShowId = -1;
@ -173,6 +187,10 @@ public class TVShowsActivity extends BaseActivity
// If we are showing episode or show details in portrait, clear selected and show action bar // If we are showing episode or show details in portrait, clear selected and show action bar
if (selectedEpisodeId != -1) { if (selectedEpisodeId != -1) {
selectedEpisodeId = -1; selectedEpisodeId = -1;
setupActionBar(selectedSeasonTitle);
} else if (selectedSeason != -1) {
selectedSeason = -1;
setupActionBar(selectedTVShowTitle);
} else if (selectedTVShowId != -1) { } else if (selectedTVShowId != -1) {
selectedTVShowId = -1; selectedTVShowId = -1;
selectedTVShowTitle = null; selectedTVShowTitle = null;
@ -182,19 +200,19 @@ public class TVShowsActivity extends BaseActivity
} }
private boolean drawerIndicatorIsArrow = false; private boolean drawerIndicatorIsArrow = false;
private void setupActionBar(String tvshowTitle) { private void setupActionBar(String title) {
Toolbar toolbar = (Toolbar)findViewById(R.id.default_toolbar); Toolbar toolbar = (Toolbar)findViewById(R.id.default_toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
if (actionBar == null) return; if (actionBar == null) return;
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
if (tvshowTitle != null) { if (title != null) {
if (!drawerIndicatorIsArrow) { if (!drawerIndicatorIsArrow) {
navigationDrawerFragment.animateDrawerToggle(true); navigationDrawerFragment.animateDrawerToggle(true);
drawerIndicatorIsArrow = true; drawerIndicatorIsArrow = true;
} }
actionBar.setTitle(tvshowTitle); actionBar.setTitle(title);
} else { } else {
if (drawerIndicatorIsArrow) { if (drawerIndicatorIsArrow) {
navigationDrawerFragment.animateDrawerToggle(false); navigationDrawerFragment.animateDrawerToggle(false);
@ -207,7 +225,7 @@ public class TVShowsActivity extends BaseActivity
/** /**
* Callback from tvshows list fragment when a show is selected. * Callback from tvshows list fragment when a show is selected.
* Switch fragment in portrait * Switch fragment in portrait
* @param vh * @param vh view holder
*/ */
@TargetApi(21) @TargetApi(21)
public void onTVShowSelected(TVShowListFragment.ViewHolder vh) { public void onTVShowSelected(TVShowListFragment.ViewHolder vh) {
@ -215,7 +233,7 @@ public class TVShowsActivity extends BaseActivity
selectedTVShowTitle = vh.tvshowTitle; selectedTVShowTitle = vh.tvshowTitle;
// Replace list fragment // Replace list fragment
tvshowDetailsFragment = TVShowDetailsFragment.newInstance(vh); final TVShowDetailsFragment tvshowDetailsFragment = TVShowDetailsFragment.newInstance(vh);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions // Set up transitions
@ -238,55 +256,71 @@ public class TVShowsActivity extends BaseActivity
}; };
tvshowDetailsFragment.setEnterSharedElementCallback(seCallback); tvshowDetailsFragment.setEnterSharedElementCallback(seCallback);
tvshowDetailsFragment.setEnterTransition(TransitionInflater tvshowDetailsFragment.setEnterTransition(
.from(this) TransitionInflater.from(this)
.inflateTransition(R.transition.media_details)); .inflateTransition(R.transition.media_details));
tvshowDetailsFragment.setReturnTransition(null); tvshowDetailsFragment.setReturnTransition(null);
Transition changeImageTransition = TransitionInflater.from( Transition changeImageTransition =
this).inflateTransition(R.transition.change_image); TransitionInflater.from(this)
.inflateTransition(R.transition.change_image);
tvshowDetailsFragment.setSharedElementReturnTransition(changeImageTransition); tvshowDetailsFragment.setSharedElementReturnTransition(changeImageTransition);
tvshowDetailsFragment.setSharedElementEnterTransition(changeImageTransition); tvshowDetailsFragment.setSharedElementEnterTransition(changeImageTransition);
fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName()); fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName());
} else { } else {
fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0);
R.anim.fragment_list_popenter, 0);
} }
fragTrans.replace(R.id.fragment_container, tvshowDetailsFragment) fragTrans.replace(R.id.fragment_container, tvshowDetailsFragment)
.addToBackStack(null) .addToBackStack(null)
.commit(); .commit();
setupActionBar(selectedTVShowTitle); setupActionBar(selectedTVShowTitle);
} }
/** /**
* Callback from tvshow episodes list when a episode is selected * Callback from tvshow details when a season is selected
* @param tvshowId Show id of the episode, should be the same as {@link #selectedTVShowId} * @param tvshowId tv show id
* @param episodeId Episode id * @param seasonId season number
*/ */
@TargetApi(21) public void onSeasonSelected(int tvshowId, int seasonId) {
public void onEpisodeSelected(int tvshowId, int episodeId) { selectedSeason = seasonId;
selectedEpisodeId = episodeId;
// Replace fragment
TVShowEpisodeListFragment fragment =
TVShowEpisodeListFragment.newInstance(selectedTVShowId, seasonId);
getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0)
.replace(R.id.fragment_container, fragment)
.addToBackStack(null)
.commit();
selectedSeasonTitle = String.format(getString(R.string.season_number), seasonId);
setupActionBar(selectedSeasonTitle);
}
/**
* Callback from tvshow episodes list when a episode is selected
* @param vh view holder
*/
public void onEpisodeSelected(TVShowEpisodeListFragment.EpisodeViewHolder vh) {
selectedEpisodeId = vh.episodeId;
// Replace list fragment // Replace list fragment
TVShowEpisodeDetailsFragment fragment = TVShowEpisodeDetailsFragment fragment =
TVShowEpisodeDetailsFragment.newInstance(tvshowId, episodeId); TVShowEpisodeDetailsFragment.newInstance(selectedTVShowId, selectedEpisodeId);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions // Set up transitions
if (Utils.isLollipopOrLater()) { if (Utils.isLollipopOrLater()) {
fragment.setEnterTransition(TransitionInflater fragment.setEnterTransition(
.from(this) TransitionInflater.from(this).inflateTransition(R.transition.media_details));
.inflateTransition(R.transition.media_details));
fragment.setReturnTransition(null); fragment.setReturnTransition(null);
} else { } else {
fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0);
R.anim.fragment_list_popenter, 0);
} }
fragTrans.replace(R.id.fragment_container, fragment) fragTrans.replace(R.id.fragment_container, fragment)
.addToBackStack(null) .addToBackStack(null)
.commit(); .commit();
setupActionBar(selectedTVShowTitle); setupActionBar(selectedTVShowTitle);
} }
} }

View File

@ -184,6 +184,7 @@
style="@style/TextAppearance.Media.Info" style="@style/TextAppearance.Media.Info"
android:paddingLeft="@dimen/default_padding" android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding" android:paddingRight="@dimen/default_padding"
android:paddingBottom="0dp"
android:maxLines="@integer/description_max_lines" android:maxLines="@integer/description_max_lines"
android:ellipsize="@null"/> android:ellipsize="@null"/>
<ImageView <ImageView

View File

@ -84,26 +84,6 @@
android:contentDescription="@string/poster" android:contentDescription="@string/poster"
android:scaleType="centerCrop"/> android:scaleType="centerCrop"/>
<!-- Buttons-->
<!--<LinearLayout-->
<!--android:id="@+id/media_actions_bar"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="@dimen/buttonbar_height"-->
<!--android:layout_below="@id/media_undertitle"-->
<!--android:paddingLeft="@dimen/default_padding"-->
<!--android:paddingRight="@dimen/default_padding"-->
<!--android:orientation="horizontal"-->
<!--style="@style/ButtonBar"-->
<!--android:background="?attr/contentBackgroundDimColor">-->
<!--<ImageButton-->
<!--android:id="@+id/go_to_imdb"-->
<!--android:layout_width="@dimen/buttonbar_button_width"-->
<!--android:layout_height="match_parent"-->
<!--style="@style/Widget.Button.Borderless"-->
<!--android:src="?attr/iconWWW"-->
<!--android:contentDescription="@string/imdb"/>-->
<!--</LinearLayout>-->
<RelativeLayout <RelativeLayout
android:id="@+id/media_details" android:id="@+id/media_details"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -156,37 +136,81 @@
android:text="@string/max_rating_video"/> android:text="@string/max_rating_video"/>
</RelativeLayout> </RelativeLayout>
<TextView <LinearLayout
android:id="@+id/media_description" android:id="@+id/media_description_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/media_details" android:layout_below="@id/media_details"
style="@style/TextAppearance.Media.Info" android:orientation="vertical"
android:paddingLeft="@dimen/default_padding" android:background="?attr/contentBackgroundColor">
android:paddingRight="@dimen/default_padding" <TextView
android:background="?attr/contentBackgroundColor"/> android:id="@+id/media_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Info"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:paddingBottom="0dp"
android:maxLines="@integer/description_max_lines"
android:ellipsize="@null"/>
<ImageView
android:id="@+id/show_all"
android:layout_width="@dimen/small_icon_size"
android:layout_height="@dimen/small_icon_size"
android:layout_gravity="end"
android:layout_marginRight="@dimen/small_padding"
android:layout_marginEnd="@dimen/small_padding"
android:src="?attr/iconExpand"
android:background="?android:attr/selectableItemBackground"/>
</LinearLayout>
<View <View
android:id="@+id/separator" android:id="@+id/separator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_below="@id/media_description" android:layout_below="@id/media_description_container"
style="@style/DefaultDividerH"/> style="@style/DefaultDividerH"/>
<TextView <TextView
android:id="@+id/cast_title" android:id="@+id/seasons_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/separator" android:layout_below="@id/separator"
style="@style/TextAppearance.Media.Title" style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/default_padding" android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding" android:paddingRight="@dimen/default_padding"
android:paddingTop="@dimen/default_padding"
android:text="@string/tvshow_seasons"/>
<!--
TODO: Review the column count.
Currently set to 1 in all cases as any other value isn't working
-->
<GridLayout
android:id="@+id/seasons_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/seasons_title"
android:columnCount="@integer/seasons_grid_view_columns"
android:orientation="horizontal"
android:useDefaultMargins="true"/>
<TextView
android:id="@+id/cast_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/seasons_list"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:paddingTop="@dimen/default_padding"
android:text="@string/cast"/> android:text="@string/cast"/>
<GridLayout <GridLayout
android:id="@+id/cast_list" android:id="@+id/cast_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_below="@id/cast_title" android:layout_below="@id/cast_title"
android:columnCount="@integer/cast_grid_view_columns" android:columnCount="@integer/cast_grid_view_columns"
android:orientation="horizontal"/> android:orientation="horizontal"/>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015 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"
android:focusable="true"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground">
<RelativeLayout
android:id="@+id/container"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/art"
android:layout_width="@dimen/seasonlist_art_width"
android:layout_height="@dimen/seasonlist_art_heigth"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/poster"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/season"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/art"
android:layout_toEndOf="@id/art"
style="@style/TextAppearance.Medialist.Title"
android:paddingTop="@dimen/small_padding"/>
<TextView
android:id="@+id/episodes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/season"
android:layout_alignStart="@id/season"
android:layout_below="@id/season"
style="@style/TextAppearance.Medialist.Details"/>
</RelativeLayout>
</android.support.v7.widget.CardView>

View File

@ -15,69 +15,89 @@
limitations under the License. limitations under the License.
--> -->
<RelativeLayout <android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container" xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="@dimen/small_padding"> card_view:cardElevation="@dimen/default_card_elevation"
card_view:cardBackgroundColor="?attr/appCardBackgroundColor">
<ImageView
android:id="@+id/list_context_menu"
android:layout_width="@dimen/default_icon_size"
android:layout_height="@dimen/default_icon_size"
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"/>
<RelativeLayout <RelativeLayout
android:id="@+id/relativelayout_checkmark_epnumber" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/seasonlist_art_width" android:id="@+id/container"
android:layout_height="match_parent" android:layout_width="match_parent"
android:layout_alignParentStart="true" android:layout_height="wrap_content">
android:layout_alignParentLeft="true">
<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"/>
<TextView
android:id="@+id/episode_number"
android:layout_width="@dimen/default_icon_size"
android:layout_height="wrap_content"
style="@style/TextAppearance.Medialist.Title"
android:paddingRight="@dimen/small_padding"
android:layout_alignTop="@id/art"
android:layout_toRightOf="@id/art"
android:layout_toEndOf="@id/art"/>
<ImageView <ImageView
android:id="@+id/checkmark" android:id="@+id/checkmark"
android:layout_width="@dimen/default_icon_size" android:layout_width="@dimen/default_icon_size"
android:layout_height="match_parent" android:layout_height="@dimen/default_icon_size"
android:layout_alignParentLeft="true" android:layout_below="@id/episode_number"
android:layout_alignParentStart="true" android:layout_alignLeft="@id/episode_number"
android:layout_marginTop="@dimen/small_padding" android:layout_alignStart="@id/episode_number"
android:contentDescription="@string/seen" android:padding="@dimen/default_icon_padding"
android:src="?attr/iconSeen"/> android:src="?attr/iconSeen"
android:contentDescription="@string/seen"/>
<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 <TextView
android:id="@+id/episode_number" android:id="@+id/title"
android:layout_width="@dimen/seasonlist_art_width" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignTop="@id/episode_number"
android:layout_toRightOf="@id/episode_number"
android:layout_toLeftOf="@id/list_context_menu"
android:layout_toEndOf="@id/episode_number"
android:layout_toStartOf="@id/list_context_menu"
style="@style/TextAppearance.Medialist.Title" style="@style/TextAppearance.Medialist.Title"
android:gravity="end"/> android:paddingTop="@dimen/small_padding"
android:paddingLeft="@dimen/small_padding"
android:paddingRight="@dimen/small_padding"/>
<TextView
android:id="@+id/details"
android:layout_width="wrap_content"
android:layout_height="@dimen/default_icon_size"
android:layout_alignLeft="@id/title"
android:layout_alignStart="@id/title"
android:layout_below="@id/title"
style="@style/TextAppearance.Medialist.Details"
android:paddingLeft="@dimen/small_padding"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:gravity="center_vertical"/>
</RelativeLayout> </RelativeLayout>
</android.support.v7.widget.CardView>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/relativelayout_checkmark_epnumber"
android:layout_toRightOf="@id/relativelayout_checkmark_epnumber"
android:layout_toLeftOf="@id/list_context_menu"
android:layout_toEndOf="@id/relativelayout_checkmark_epnumber"
android:layout_toStartOf="@id/list_context_menu"
style="@style/TextAppearance.Medialist.Title"
android:paddingTop="@dimen/small_padding"/>
<TextView
android:id="@+id/details"
android:layout_width="wrap_content"
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"
android:paddingBottom="@dimen/small_padding"/>
</RelativeLayout>

View File

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015 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.
-->
<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/seasonlist_art_width"
android:layout_height="@dimen/seasonlist_art_heigth"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/poster"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/season"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/art"
android:layout_toEndOf="@id/art"
style="@style/TextAppearance.Medialist.Title"
android:paddingTop="@dimen/small_padding"/>
<TextView
android:id="@+id/episodes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/season"
android:layout_alignStart="@id/season"
android:layout_below="@id/season"
style="@style/TextAppearance.Medialist.Details"/>
<ImageView
android:id="@+id/status_indicator"
android:layout_width="@dimen/default_icon_size"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="@dimen/small_padding"/>
</RelativeLayout>

View File

@ -17,4 +17,6 @@
<resources> <resources>
<integer name="cast_grid_view_columns">5</integer> <integer name="cast_grid_view_columns">5</integer>
<integer name="cast_grid_view_rows">2</integer> <integer name="cast_grid_view_rows">2</integer>
<integer name="seasons_grid_view_columns">1</integer>
</resources> </resources>

View File

@ -17,4 +17,6 @@
<resources> <resources>
<integer name="cast_grid_view_columns">6</integer> <integer name="cast_grid_view_columns">6</integer>
<integer name="cast_grid_view_rows">2</integer> <integer name="cast_grid_view_rows">2</integer>
<integer name="seasons_grid_view_columns">1</integer>
</resources> </resources>

View File

@ -45,8 +45,8 @@
<dimen name="seasonlist_art_width">86dp</dimen> <dimen name="seasonlist_art_width">86dp</dimen>
<dimen name="seasonlist_art_heigth">124dp</dimen> <dimen name="seasonlist_art_heigth">124dp</dimen>
<dimen name="episodelist_art_width">134dp</dimen> <dimen name="episodelist_art_width">112dp</dimen>
<dimen name="episodelist_art_heigth">76dp</dimen> <dimen name="episodelist_art_heigth">100dp</dimen>
<dimen name="artistlist_art_width">88dp</dimen> <dimen name="artistlist_art_width">88dp</dimen>
<dimen name="artistlist_art_heigth">88dp</dimen> <dimen name="artistlist_art_heigth">88dp</dimen>

View File

@ -16,5 +16,7 @@
--> -->
<resources> <resources>
<integer name="cast_grid_view_columns">4</integer> <integer name="cast_grid_view_columns">4</integer>
<integer name="cast_grid_view_rows">3</integer> <integer name="cast_grid_view_rows">3</integer>
<integer name="seasons_grid_view_columns">1</integer>
</resources> </resources>

View File

@ -86,8 +86,8 @@
<dimen name="seasonlist_art_width">72dp</dimen> <dimen name="seasonlist_art_width">72dp</dimen>
<dimen name="seasonlist_art_heigth">104dp</dimen> <dimen name="seasonlist_art_heigth">104dp</dimen>
<dimen name="episodelist_art_width">112dp</dimen> <dimen name="episodelist_art_width">84dp</dimen>
<dimen name="episodelist_art_heigth">64dp</dimen> <dimen name="episodelist_art_heigth">84dp</dimen>
<dimen name="artistlist_art_width">74dp</dimen> <dimen name="artistlist_art_width">74dp</dimen>
<dimen name="artistlist_art_heigth">74dp</dimen> <dimen name="artistlist_art_heigth">74dp</dimen>

View File

@ -25,5 +25,7 @@
<integer name="cast_grid_view_rows">3</integer> <integer name="cast_grid_view_rows">3</integer>
<integer name="description_max_lines">3</integer> <integer name="description_max_lines">3</integer>
<integer name="seasons_grid_view_columns">1</integer>
<integer name="fab_animation_start_delay">100</integer> <integer name="fab_animation_start_delay">100</integer>
</resources> </resources>

View File

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