Refactored AbstractDetailsFragment

This introduces the MVC model to details fragments. It moves as much
view and control code out of the concrete fragments into the abstract
classes.

   * Added UML class and sequence diagrams under doc/diagrams to clarify
     the new setup

   * Introduces new abstract classes
      * AbstractFragment class to hold the DataHolder
      * AbstractInfoFragment class to display media information
      * AbstractAddtionalInfoFragment class to allow *InfoFragments
        to add additional UI elements and propagate refresh requests.
        See for an example TVShowInfoFragment which adds
        TVShowProgressFragment to display next episodes and season
        progression.

   * Introduces new RefreshItem class to encapsulate all refresh
     functionality from AbstractDetailsFragment

   * Introduces new SharedElementTransition utility class to encapsulate
     all shared element transition code

   * Introduces new CastFragment class to encapsulate all code for
     displaying casts reducing code duplication

   * Introduces DataHolder class to replace passing the ViewHolder from
     the *ListFragment to the *DetailsFragment or *InfoFragment

   * Refactored AbstractDetailsFragment into two classes:

     o AbstractDetailsFragment: for fragments requiring a tab bar
     o AbstractInfoFragment:    for fragments showing media information

     We used to use <NAME>DetailsFragments for both fragments that show
     generic
     info about some media item and fragments that hold all details for
     some media item.
     For example, artist details showed artist info and used tabs to
     show artist albums and songs as well. Now Details fragments are
     used to show all details, Info fragments only show media item
     information like description, title, rating, etc.

   * Moved swiperefreshlayout code from AbstractCursorListFragment to
     AbstractListFragment
This commit is contained in:
Martijn Brekhof 2016-12-30 09:27:24 +01:00
parent d3a68b73c9
commit 412931b8db
67 changed files with 5914 additions and 7153 deletions

View File

@ -49,8 +49,8 @@ public class RestoreSearchQueryViewPagerTest {
private final int ARTIST_SEARCH_QUERY_LIST_SIZE = 2; private final int ARTIST_SEARCH_QUERY_LIST_SIZE = 2;
private final String ALBUMS_SEARCH_QUERY = "tes"; private final String ALBUMS_SEARCH_QUERY = "tes";
private final int ALBUM_SEARCH_QUERY_LIST_SIZE = 3; private final int ALBUM_SEARCH_QUERY_LIST_SIZE = 3;
private final int ARTIST_COMPLETE_LIST_SIZE = 228; private final int ARTIST_COMPLETE_LIST_SIZE = 229;
private final int ALBUM_COMPLETE_LIST_SIZE = 234; private final int ALBUM_COMPLETE_LIST_SIZE = 235;
private LoaderIdlingResource loaderIdlingResource; private LoaderIdlingResource loaderIdlingResource;

View File

@ -0,0 +1,21 @@
/*
* Copyright 2017 Martijn Brekhof. 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;
abstract public class AbstractAdditionalInfoFragment extends AbstractFragment {
abstract protected void refresh();
}

View File

@ -27,7 +27,6 @@ import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.support.v4.view.MenuItemCompat; import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.SearchView; import android.support.v7.widget.SearchView;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -55,19 +54,15 @@ import de.greenrobot.event.EventBus;
public abstract class AbstractCursorListFragment extends AbstractListFragment public abstract class AbstractCursorListFragment extends AbstractListFragment
implements LoaderManager.LoaderCallbacks<Cursor>, implements LoaderManager.LoaderCallbacks<Cursor>,
SyncUtils.OnServiceListener, SyncUtils.OnServiceListener,
SearchView.OnQueryTextListener, SearchView.OnQueryTextListener {
SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = LogUtils.makeLogTag(AbstractCursorListFragment.class); private static final String TAG = LogUtils.makeLogTag(AbstractCursorListFragment.class);
private final String BUNDLE_KEY_SEARCH_QUERY = "search_query"; private final String BUNDLE_KEY_SEARCH_QUERY = "search_query";
private ServiceConnection serviceConnection; private ServiceConnection serviceConnection;
private HostInfo hostInfo;
private EventBus bus; private EventBus bus;
private CursorAdapter adapter;
// Loader IDs // Loader IDs
private static final int LOADER = 0; private static final int LOADER = 0;
@ -89,13 +84,6 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment
View root = super.onCreateView(inflater, container, savedInstanceState); View root = super.onCreateView(inflater, container, savedInstanceState);
bus = EventBus.getDefault(); bus = EventBus.getDefault();
HostManager hostManager = HostManager.getInstance(getActivity());
hostInfo = hostManager.getHostInfo();
swipeRefreshLayout.setEnabled(true);
swipeRefreshLayout.setOnRefreshListener(this);
adapter = (CursorAdapter) getAdapter();
if (savedInstanceState != null) { if (savedInstanceState != null) {
savedSearchFilter = savedInstanceState.getString(BUNDLE_KEY_SEARCH_QUERY); savedSearchFilter = savedInstanceState.getString(BUNDLE_KEY_SEARCH_QUERY);
@ -177,22 +165,6 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
/**
* Swipe refresh layout callback
*/
/** {@inheritDoc} */
@Override
public void onRefresh() {
if (hostInfo != null) {
showRefreshAnimation();
onSwipeRefresh();
} else {
swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getActivity(), R.string.no_xbmc_configured, Toast.LENGTH_SHORT)
.show();
}
}
/** /**
* Should return the {@link LibrarySyncService} SyncType that * Should return the {@link LibrarySyncService} SyncType that
* this list initiates * this list initiates
@ -237,7 +209,7 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment
} }
if (event.syncType.equals(getListSyncType())) { if (event.syncType.equals(getListSyncType())) {
swipeRefreshLayout.setRefreshing(false); hideRefreshAnimation();
if (event.status == MediaSyncEvent.STATUS_SUCCESS) { if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
refreshList(); refreshList();
if (!silentSync) { if (!silentSync) {
@ -273,10 +245,10 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment
this.supportsSearch = supportsSearch; this.supportsSearch = supportsSearch;
} }
protected void onSwipeRefresh() { @Override
LogUtils.LOGD(TAG, "Swipe, starting sync for: " + getListSyncType()); public void onRefresh() {
// Start the syncing process showRefreshAnimation();
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(); String syncID = getSyncID();
@ -327,16 +299,18 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment
@Override @Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
loaderLoading = false; loaderLoading = false;
adapter.swapCursor(cursor); ((CursorAdapter) getAdapter()).swapCursor(cursor);
// To prevent the empty text from appearing on the first load, set it now if (TextUtils.isEmpty(searchFilter)) {
emptyView.setText(getString(R.string.swipe_down_to_refresh)); // To prevent the empty text from appearing on the first load, set it now
emptyView.setText(getString(R.string.swipe_down_to_refresh));
}
loaderLoading = false; loaderLoading = false;
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public void onLoaderReset(Loader<Cursor> cursorLoader) { public void onLoaderReset(Loader<Cursor> cursorLoader) {
adapter.swapCursor(null); ((CursorAdapter) getAdapter()).swapCursor(null);
} }
/** /**

View File

@ -1,321 +0,0 @@
/*
* Copyright 2015 Martijn Brekhof. 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.Manifest;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v13.app.FragmentCompat;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiException;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.service.library.SyncUtils;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import butterknife.OnClick;
import butterknife.Optional;
import de.greenrobot.event.EventBus;
abstract public class AbstractDetailsFragment extends Fragment
implements SwipeRefreshLayout.OnRefreshListener,
SyncUtils.OnServiceListener {
private static final String TAG = LogUtils.makeLogTag(AbstractDetailsFragment.class);
private HostManager hostManager;
private HostInfo hostInfo;
private EventBus bus;
private String syncType;
private SwipeRefreshLayout swipeRefreshLayout;
private ServiceConnection serviceConnection;
// Used to hide the refresh animation when a silent refresh is issued
private boolean silentRefresh = false;
abstract protected View createView(LayoutInflater inflater, ViewGroup container);
/**
* Should return {@link LibrarySyncService} SyncType that
* this fragment initiates
* @return {@link LibrarySyncService} SyncType
*/
abstract protected String getSyncType();
/**
* 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
*/
abstract protected String getSyncID();
/**
* Should return the item ID for SyncID returned by {@link #getSyncID()}
* @return -1 if not used.
*/
abstract protected int getSyncItemID();
/**
* Should return the SwipeRefreshLayout if the fragment's view uses one.
* Used to notify the user if a sync for synctype returned by {@link #getSyncType()}
* is currently in progress
* @return
*/
abstract protected SwipeRefreshLayout getSwipeRefreshLayout();
/**
* When the view created in {@link #createView(LayoutInflater, ViewGroup)} contains a button
* with resource identifier R.id.download this will be called to initiate the download
*/
abstract protected void onDownload();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
bus = EventBus.getDefault();
hostManager = HostManager.getInstance(getActivity());
hostInfo = hostManager.getHostInfo();
syncType = getSyncType();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (container == null) {
// We're not being shown or there's nothing to show
return null;
}
View view = createView(inflater, container);
if( view != null ) {
swipeRefreshLayout = getSwipeRefreshLayout();
if( swipeRefreshLayout != null ) {
swipeRefreshLayout.setOnRefreshListener(this);
}
}
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onStart() {
super.onStart();
serviceConnection = SyncUtils.connectToLibrarySyncService(getActivity(), this);
}
@Override
public void onResume() {
bus.register(this);
super.onResume();
}
@Override
public void onPause() {
bus.unregister(this);
super.onPause();
}
@Override
public void onStop() {
super.onStop();
SyncUtils.disconnectFromLibrarySyncService(getActivity(), serviceConnection);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.refresh_item, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_refresh:
onRefresh();
}
return super.onOptionsItemSelected(item);
}
@Optional
@OnClick(R.id.download)
public void onDownloadClicked(View v) {
boolean hasStoragePermission =
ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
if (!hasStoragePermission) {
requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
Utils.PERMISSION_REQUEST_WRITE_STORAGE);
return;
}
if (Settings.allowedDownloadNetworkTypes(getActivity()) != 0) {
onDownload();
} else {
Toast.makeText(getActivity(), R.string.no_connection_type_selected, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
switch (requestCode) {
case Utils.PERMISSION_REQUEST_WRITE_STORAGE:
// If request is cancelled, the result arrays are empty.
if ((grantResults.length > 0) &&
(grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
onDownloadClicked(null);
} else {
Toast.makeText(getActivity(), R.string.write_storage_permission_denied, Toast.LENGTH_SHORT)
.show();
}
break;
}
}
protected void startSync(boolean silentRefresh) {
this.silentRefresh = silentRefresh;
LogUtils.LOGD(TAG, "Starting syc. Silent? " + silentRefresh);
if (getHostInfo() != null) {
if ((swipeRefreshLayout != null) && (!silentRefresh)) {
UIUtils.showRefreshAnimation(swipeRefreshLayout);
}
// Start the syncing process
Intent syncIntent = new Intent(this.getActivity(), LibrarySyncService.class);
if (syncType != null) {
syncIntent.putExtra(syncType, true);
}
String syncID = getSyncID();
int itemId = getSyncItemID();
if ((syncID != null) && (itemId != -1)) {
syncIntent.putExtra(syncID, itemId);
}
Bundle syncExtras = new Bundle();
syncExtras.putBoolean(LibrarySyncService.SILENT_SYNC, silentRefresh);
syncIntent.putExtra(LibrarySyncService.SYNC_EXTRAS, syncExtras);
getActivity().startService(syncIntent);
} else {
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
Toast.makeText(getActivity(), R.string.no_xbmc_configured, Toast.LENGTH_SHORT)
.show();
}
}
/**
* Swipe refresh layout callback
*/
/** {@inheritDoc} */
@Override
public void onRefresh () {
startSync(false);
}
/**
* Event bus post. Called when the syncing process ended
*
* @param event Refreshes data
*/
public void onEventMainThread(MediaSyncEvent event) {
if ((syncType == null) || (! event.syncType.equals(syncType)))
return;
boolean silentSync = false;
if (event.syncExtras != null) {
silentSync = event.syncExtras.getBoolean(LibrarySyncService.SILENT_SYNC, false);
}
if( swipeRefreshLayout != null ) {
swipeRefreshLayout.setRefreshing(false);
}
onSyncProcessEnded(event);
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
if (!silentSync) {
Toast.makeText(getActivity(),
R.string.sync_successful, Toast.LENGTH_SHORT)
.show();
}
} else if (!silentSync) {
String msg = (event.errorCode == ApiException.API_ERROR) ?
String.format(getString(R.string.error_while_syncing), event.errorMessage) :
getString(R.string.unable_to_connect_to_xbmc);
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onServiceConnected(LibrarySyncService librarySyncService) {
if (syncType == null)
return;
if (!silentRefresh &&
(swipeRefreshLayout != null) &&
SyncUtils.isLibrarySyncing(librarySyncService,
HostManager.getInstance(getActivity()).getHostInfo(),
syncType)) {
LogUtils.LOGD(TAG, "Showing refresh animation");
UIUtils.showRefreshAnimation(swipeRefreshLayout);
}
}
/**
* Called when sync process for type set through {@link #getSyncType()} ends
* @param event
*/
abstract protected void onSyncProcessEnded(MediaSyncEvent event);
protected HostManager getHostManager() {
return hostManager;
}
protected HostInfo getHostInfo() {
return hostInfo;
}
}

View File

@ -0,0 +1,183 @@
/*
* Copyright 2017 Martijn Brekhof. 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.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
public class AbstractFragment extends Fragment {
private AbstractInfoFragment.DataHolder dataHolder;
public void setDataHolder(AbstractInfoFragment.DataHolder dataHolder) {
this.dataHolder = dataHolder;
Bundle bundle = getArguments();
if (bundle == null) {
setArguments(dataHolder.getBundle());
} else {
bundle.putAll(dataHolder.getBundle());
}
}
public AbstractInfoFragment.DataHolder getDataHolder() {
return dataHolder;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if( this.dataHolder == null ) {
this.dataHolder = new AbstractInfoFragment.DataHolder(-1);
}
this.dataHolder.setBundle(getArguments());
}
public static class DataHolder {
static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME";
static final String BUNDLE_KEY_ID = "id";
static final String BUNDLE_KEY_TITLE = "title";
static final String BUNDLE_KEY_UNDERTITLE = "undertitle";
static final String BUNDLE_KEY_DESCRIPTION = "description";
static final String BUNDLE_KEY_DETAILS = "details";
static final String BUNDLE_KEY_POSTERURL = "poster";
static final String BUNDLE_KEY_FANARTURL = "fanart";
static final String BUNDLE_KEY_SQUAREPOSTER = "squareposter";
static final String BUNDLE_KEY_RATING = "rating";
static final String BUNDLE_KEY_MAXRATING = "maxrating";
static final String BUNDLE_KEY_VOTES = "votes";
private Bundle bundle;
private DataHolder() {}
public DataHolder(Bundle bundle) {
setBundle(bundle);
}
public DataHolder(int itemId) {
bundle = new Bundle();
bundle.putInt(BUNDLE_KEY_ID, itemId);
}
public void setBundle(Bundle bundle) {
this.bundle = bundle;
}
public void setPosterTransitionName(String posterTransitionName) {
bundle.putString(POSTER_TRANS_NAME, posterTransitionName);
}
public void setSquarePoster(boolean squarePoster) {
bundle.putBoolean(BUNDLE_KEY_SQUAREPOSTER, squarePoster);
}
public void setRating(double rating) {
bundle.putDouble(BUNDLE_KEY_RATING, rating);
}
public void setMaxRating(int maxRating) {
bundle.putInt(BUNDLE_KEY_MAXRATING, maxRating);
}
public void setVotes(int votes) {
bundle.putInt(BUNDLE_KEY_VOTES, votes);
}
public void setPosterUrl(String posterUrl) {
bundle.putString(BUNDLE_KEY_POSTERURL, posterUrl);
}
public void setTitle(String title) {
bundle.putString(BUNDLE_KEY_TITLE, title);
}
public void setUndertitle(String underTitle) {
bundle.putString(BUNDLE_KEY_UNDERTITLE, underTitle);
}
public void setDescription(String description) {
bundle.putString(BUNDLE_KEY_DESCRIPTION, description);
}
public void setDetails(String details) {
bundle.putString(BUNDLE_KEY_DETAILS, details);
}
public void setFanArtUrl(String fanArtUrl) {
bundle.putString(BUNDLE_KEY_FANARTURL, fanArtUrl);
}
public void setId(int id) {
bundle.putInt(BUNDLE_KEY_ID, id);
}
public String getPosterTransitionName() {
return bundle.getString(POSTER_TRANS_NAME);
}
public boolean getSquarePoster() {
return bundle.getBoolean(BUNDLE_KEY_SQUAREPOSTER);
}
public double getRating() {
return bundle.getDouble(BUNDLE_KEY_RATING);
}
public int getMaxRating() {
return bundle.getInt(BUNDLE_KEY_MAXRATING);
}
public int getVotes() {
return bundle.getInt(BUNDLE_KEY_VOTES);
}
public String getPosterUrl() {
return bundle.getString(BUNDLE_KEY_POSTERURL);
}
public String getTitle() {
return bundle.getString(BUNDLE_KEY_TITLE);
}
public String getUnderTitle() {
return bundle.getString(BUNDLE_KEY_UNDERTITLE);
}
public String getDescription() {
return bundle.getString(BUNDLE_KEY_DESCRIPTION);
}
public String getDetails() {
return bundle.getString(BUNDLE_KEY_DETAILS);
}
public String getFanArtUrl() {
return bundle.getString(BUNDLE_KEY_FANARTURL);
}
public int getId() {
return bundle.getInt(BUNDLE_KEY_ID);
}
public Bundle getBundle() {
return bundle;
}
}
}

View File

@ -0,0 +1,597 @@
/*
* Copyright 2015 Martijn Brekhof. 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.Manifest;
import android.annotation.TargetApi;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
import com.melnykov.fab.ObservableScrollView;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.service.library.SyncUtils;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.SharedElementTransition;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.util.Locale;
import at.blogc.android.views.ExpandableTextView;
import butterknife.ButterKnife;
import butterknife.InjectView;
import static android.view.View.GONE;
abstract public class AbstractInfoFragment extends AbstractFragment
implements SwipeRefreshLayout.OnRefreshListener,
SyncUtils.OnServiceListener,
SharedElementTransition.SharedElement {
private static final String TAG = LogUtils.makeLogTag(AbstractInfoFragment.class);
// Detail views
@InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;
@InjectView(R.id.media_panel) ScrollView panelScrollView;
@InjectView(R.id.art) ImageView artImageView;
@InjectView(R.id.poster) ImageView posterImageView;
@InjectView(R.id.media_title) TextView titleTextView;
@InjectView(R.id.media_undertitle) TextView underTitleTextView;
@InjectView(R.id.rating_container) LinearLayout ratingContainer;
@InjectView(R.id.rating) TextView ratingTextView;
@InjectView(R.id.rating_votes) TextView ratingVotesTextView;
@InjectView(R.id.max_rating) TextView maxRatingTextView;
@InjectView(R.id.media_details_right) TextView detailsRightTextView;
@InjectView(R.id.media_details) LinearLayout mediaDetailsContainer;
@InjectView(R.id.media_action_download) ImageButton downloadButton;
@InjectView(R.id.media_action_pin_unpin) ImageButton pinUnpinButton;
@InjectView(R.id.media_action_add_to_playlist) ImageButton addToPlaylistButton;
@InjectView(R.id.media_action_seen) ImageButton seenButton;
@InjectView(R.id.media_action_go_to_imdb) ImageButton imdbButton;
@InjectView(R.id.media_actions_bar) LinearLayout mediaActionsBar;
@InjectView(R.id.media_description) ExpandableTextView descriptionExpandableTextView;
@InjectView(R.id.media_description_container) LinearLayout descriptionContainer;
@InjectView(R.id.show_all) ImageView expansionImage;
@InjectView(R.id.fab) ImageButton fabButton;
@InjectView(R.id.exit_transition_view) View exitTransitionView;
private HostManager hostManager;
private HostInfo hostInfo;
private ServiceConnection serviceConnection;
private RefreshItem refreshItem;
private boolean expandDescription;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
/**
* Use {@link #setDataHolder(DataHolder)}
* to provide the required info after creating a new instance of this Fragment
*/
public AbstractInfoFragment() {
super();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hostManager = HostManager.getInstance(getActivity());
hostInfo = hostManager.getHostInfo();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (container == null) {
// We're not being shown or there's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_info, container, false);
ButterKnife.inject(this, root);
// 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);
panelScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
float y = panelScrollView.getScrollY();
float newAlpha = Math.min(1, Math.max(0, 1 - (y / pixelsToTransparent)));
artImageView.setAlpha(newAlpha);
}
});
DataHolder dataHolder = getDataHolder();
if(!dataHolder.getSquarePoster()) {
posterImageView.getLayoutParams().width =
resources.getDimensionPixelSize(R.dimen.detail_poster_width_nonsquare);
posterImageView.getLayoutParams().height =
resources.getDimensionPixelSize(R.dimen.detail_poster_height_nonsquare);
}
if(getRefreshItem() != null) {
swipeRefreshLayout.setOnRefreshListener(this);
} else {
swipeRefreshLayout.setEnabled(false);
}
FloatingActionButton fab = (FloatingActionButton)fabButton;
fab.attachToScrollView((ObservableScrollView) panelScrollView);
if(Utils.isLollipopOrLater()) {
posterImageView.setTransitionName(dataHolder.getPosterTransitionName());
}
if (savedInstanceState == null) {
FragmentManager fragmentManager = getChildFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.media_additional_info);
if (fragment == null) {
fragment = getAdditionalInfoFragment();
if (fragment != null) {
fragmentManager.beginTransaction()
.add(R.id.media_additional_info, fragment)
.commit();
}
}
}
if(setupMediaActionBar()) {
mediaActionsBar.setVisibility(View.VISIBLE);
}
if(setupFAB(fabButton)) {
fabButton.setVisibility(View.VISIBLE);
}
updateView(dataHolder);
return root;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.refresh_item, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public void onStart() {
super.onStart();
serviceConnection = SyncUtils.connectToLibrarySyncService(getActivity(), this);
}
@Override
public void onResume() {
// Force the exit view to invisible
exitTransitionView.setVisibility(View.INVISIBLE);
if ( refreshItem != null ) {
refreshItem.register();
}
super.onResume();
}
@Override
public void onPause() {
if ( refreshItem != null ) {
refreshItem.unregister();
}
super.onPause();
}
@Override
public void onStop() {
super.onStop();
SyncUtils.disconnectFromLibrarySyncService(getActivity(), serviceConnection);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_refresh:
onRefresh();
}
return super.onOptionsItemSelected(item);
}
/**
* Swipe refresh layout callback
*/
/** {@inheritDoc} */
@Override
public void onRefresh () {
if (getRefreshItem() == null) {
Toast.makeText(getActivity(), R.string.Refreshing_not_implemented_for_this_item,
Toast.LENGTH_SHORT).show();
swipeRefreshLayout.setRefreshing(false);
return;
}
refreshItem.setSwipeRefreshLayout(swipeRefreshLayout);
refreshItem.startSync(false);
}
@Override
public void onServiceConnected(LibrarySyncService librarySyncService) {
if (getRefreshItem() == null) {
return;
}
if (SyncUtils.isLibrarySyncing(librarySyncService,
HostManager.getInstance(getActivity()).getHostInfo(),
refreshItem.getSyncType())) {
UIUtils.showRefreshAnimation(swipeRefreshLayout);
refreshItem.setSwipeRefreshLayout(swipeRefreshLayout);
refreshItem.register();
}
}
protected void setFabButtonState(boolean enable) {
if(enable) {
fabButton.setVisibility(View.VISIBLE);
} else {
fabButton.setVisibility(GONE);
}
}
protected void fabActionPlayItem(PlaylistType.Item item) {
if (item == null) {
Toast.makeText(getActivity(), R.string.no_item_available_to_play, Toast.LENGTH_SHORT).show();
return;
}
Player.Open action = new Player.Open(item);
action.execute(HostManager.getInstance(getActivity()).getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Check whether we should switch to the remote
boolean switchToRemote = PreferenceManager
.getDefaultSharedPreferences(getActivity())
.getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
if (switchToRemote) {
int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
switch (requestCode) {
case Utils.PERMISSION_REQUEST_WRITE_STORAGE:
// If request is cancelled, the result arrays are empty.
if ((grantResults.length > 0) &&
(grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
downloadButton.performClick();
} else {
Toast.makeText(getActivity(), R.string.write_storage_permission_denied, Toast.LENGTH_SHORT)
.show();
}
break;
}
}
@Override
@TargetApi(21)
public boolean isSharedElementVisible() {
return UIUtils.isViewInBounds(panelScrollView, posterImageView);
}
protected void refreshAdditionInfoFragment() {
Fragment fragment = getChildFragmentManager().findFragmentById(R.id.media_additional_info);
if (fragment != null)
((AbstractAdditionalInfoFragment) fragment).refresh();
}
protected HostManager getHostManager() {
return hostManager;
}
protected HostInfo getHostInfo() {
return hostInfo;
}
/**
* Call this when you are ready to provide the titleTextView, undertitle, details, descriptionExpandableTextView, etc. etc.
*/
protected void updateView(DataHolder dataHolder) {
titleTextView.setText(dataHolder.getTitle());
underTitleTextView.setText(dataHolder.getUnderTitle());
detailsRightTextView.setText(dataHolder.getDetails());
if (!TextUtils.isEmpty(dataHolder.getDescription())) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.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();
descriptionContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
descriptionExpandableTextView.toggle();
expansionImage.setImageResource(descriptionExpandableTextView.isExpanded() ? iconCollapseResId : iconExpandResId);
}
});
descriptionExpandableTextView.setText(dataHolder.getDescription());
if (expandDescription) {
descriptionExpandableTextView.expand();
expansionImage.setImageResource(iconExpandResId);
}
descriptionContainer.setVisibility(View.VISIBLE);
} else {
descriptionContainer.setVisibility(GONE);
}
// Images
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
Resources resources = getActivity().getResources();
if (dataHolder.getPosterUrl() != null) {
int posterWidth;
int posterHeight;
if (dataHolder.getSquarePoster()) {
posterWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_square);
posterHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_square);
} else {
posterWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_nonsquare);
posterHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_nonsquare);
}
UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager,
dataHolder.getPosterUrl(), dataHolder.getTitle(),
posterImageView, posterWidth, posterHeight);
} else {
posterImageView.setVisibility(GONE);
int padding = getActivity().getResources().getDimensionPixelSize(R.dimen.default_padding);
titleTextView.setPadding(padding, padding, 0, 0);
underTitleTextView.setPadding(padding, padding, 0, 0);
}
int artHeight = resources.getDimensionPixelOffset(R.dimen.detail_art_height);
int artWidth = displayMetrics.widthPixels;
UIUtils.loadImageIntoImageview(hostManager,
TextUtils.isEmpty(dataHolder.getFanArtUrl()) ?
dataHolder.getPosterUrl() : dataHolder.getFanArtUrl(),
artImageView, artWidth, artHeight);
if (dataHolder.getRating() > 0) {
ratingTextView.setText(String.format(Locale.getDefault(), "%01.01f", dataHolder.getRating()));
if (dataHolder.getMaxRating() > 0) {
maxRatingTextView.setText(String.format(getString(R.string.max_rating),
String.valueOf(dataHolder.getMaxRating())));
}
if (dataHolder.getVotes() > 0 ) {
ratingVotesTextView.setText(String.format(getString(R.string.votes),
String.valueOf(dataHolder.getVotes())));
}
ratingContainer.setVisibility(View.VISIBLE);
} else if (TextUtils.isEmpty(dataHolder.getDetails())) {
mediaDetailsContainer.setVisibility(View.GONE);
}
}
/**
* Setting a listener for downloads will add the download button to the UI
* @param listener to be called when user clicks the download button. Note that the View passed
* into onClick from {@link android.view.View.OnClickListener} will be null
* when the user is asked for storage permissions
*/
protected void setOnDownloadListener(final View.OnClickListener listener) {
downloadButton.setVisibility(View.VISIBLE);
downloadButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (checkStoragePermission()) {
if (Settings.allowedDownloadNetworkTypes(getActivity()) != 0) {
listener.onClick(view);
setButtonState(downloadButton, true);
} else {
Toast.makeText(getActivity(), R.string.no_connection_type_selected, Toast.LENGTH_SHORT).show();
}
}
}
});
}
protected void setOnAddToPlaylistListener(View.OnClickListener listener) {
addToPlaylistButton.setVisibility(View.VISIBLE);
addToPlaylistButton.setOnClickListener(listener);
}
protected void setOnGoToImdbListener(View.OnClickListener listener) {
imdbButton.setVisibility(View.VISIBLE);
imdbButton.setOnClickListener(listener);
}
/**
* Use {@link #setSeenButtonState(boolean)} to set the state of the seen button
* @param listener
*/
protected void setOnSeenListener(final View.OnClickListener listener) {
setupToggleButton(seenButton, listener);
}
protected void setOnPinClickedListener(final View.OnClickListener listener) {
setupToggleButton(pinUnpinButton, listener);
}
/**
* Uses colors to show to the user the item has been downloaded
* @param state true if item has been watched/listened too, false otherwise
*/
protected void setDownloadButtonState(boolean state) {
setButtonState(downloadButton, state);
}
/**
* Uses colors to show the seen state to the user
* @param state true if item has been watched/listened too, false otherwise
*/
protected void setSeenButtonState(boolean state) {
setToggleButtonState(seenButton, state);
}
protected void setPinButtonState(boolean state) {
setToggleButtonState(pinUnpinButton, state);
}
private void setButtonState(ImageButton button, boolean state) {
if (state) {
UIUtils.highlightImageView(getActivity(), button);
} else {
button.clearColorFilter();
}
}
private void setToggleButtonState(ImageButton button, boolean state) {
setButtonState(button, state);
button.setTag(state);
}
private void setupToggleButton(final ImageButton button, final View.OnClickListener listener) {
button.setVisibility(View.VISIBLE);
button.setTag(false);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
listener.onClick(view);
// Boldly invert the state. We depend on the observer to correct the state
// if Kodi or other service didn't honour our request
setToggleButtonState(button, ! (boolean) button.getTag());
}
});
}
private boolean checkStoragePermission() {
boolean hasStoragePermission =
ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
if (!hasStoragePermission) {
requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
Utils.PERMISSION_REQUEST_WRITE_STORAGE);
return false;
}
return true;
}
protected RefreshItem getRefreshItem() {
if (refreshItem == null) {
refreshItem = createRefreshItem();
}
return refreshItem;
}
protected void setExpandDescription(boolean expandDescription) {
this.expandDescription = expandDescription;
}
abstract protected AbstractAdditionalInfoFragment getAdditionalInfoFragment();
/**
* Called when user commands the information to be renewed. Either through a swipe down
* or a menu call.
* <br/>
* Note, that {@link AbstractAdditionalInfoFragment#refresh()} will be called for an
* additional fragment, if available, automatically.
* @return
*/
abstract protected RefreshItem createRefreshItem();
/**
* Called when the media action bar actions are available and
* you can use {@link #setOnAddToPlaylistListener(View.OnClickListener)},
* {@link #setOnSeenListener(View.OnClickListener)},
* {@link #setOnDownloadListener(View.OnClickListener)},
* {@link #setOnGoToImdbListener(View.OnClickListener)},
* and {@link #setOnPinClickedListener(View.OnClickListener)} to enable
* one or more actions.
* @return true if media action bar should be visible, false otherwise
*/
abstract protected boolean setupMediaActionBar();
/**
* Called when the fab button is available
* @return true to enable the Floating Action Button, false otherwise
*/
abstract protected boolean setupFAB(ImageButton FAB);
}

View File

@ -43,7 +43,8 @@ import org.xbmc.kore.utils.Utils;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.InjectView; import butterknife.InjectView;
public abstract class AbstractListFragment extends Fragment { public abstract class AbstractListFragment extends Fragment implements
SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = LogUtils.makeLogTag(AbstractListFragment.class); private static final String TAG = LogUtils.makeLogTag(AbstractListFragment.class);
private BaseAdapter adapter; private BaseAdapter adapter;
@ -58,6 +59,12 @@ public abstract class AbstractListFragment extends Fragment {
abstract protected AdapterView.OnItemClickListener createOnItemClickListener(); abstract protected AdapterView.OnItemClickListener createOnItemClickListener();
abstract protected BaseAdapter createAdapter(); abstract protected BaseAdapter createAdapter();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
adapter = createAdapter();
}
@TargetApi(16) @TargetApi(16)
@Nullable @Nullable
@Override @Override
@ -65,13 +72,10 @@ public abstract class AbstractListFragment extends Fragment {
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_generic_media_list, container, false); ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_generic_media_list, container, false);
ButterKnife.inject(this, root); ButterKnife.inject(this, root);
swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setOnRefreshListener(this);
gridView.setEmptyView(emptyView); gridView.setEmptyView(emptyView);
gridView.setOnItemClickListener(createOnItemClickListener()); gridView.setOnItemClickListener(createOnItemClickListener());
// Configure the adapter and start the loader
adapter = createAdapter();
gridView.setAdapter(adapter); gridView.setAdapter(adapter);
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -99,7 +103,7 @@ public abstract class AbstractListFragment extends Fragment {
gridView.getViewTreeObserver().removeGlobalOnLayoutListener(this); gridView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} }
//Make sure menu is updated if it was already created //Make sure menu is update d if it was already created
getActivity().invalidateOptionsMenu(); getActivity().invalidateOptionsMenu();
} }
}); });
@ -184,6 +188,10 @@ public abstract class AbstractListFragment extends Fragment {
}); });
} }
public void hideRefreshAnimation() {
swipeRefreshLayout.setRefreshing(false);
}
public BaseAdapter getAdapter() { public BaseAdapter getAdapter() {
return adapter; return adapter;
} }

View File

@ -0,0 +1,101 @@
/*
* Copyright 2015 Martijn Brekhof. 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.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.astuetz.PagerSlidingTabStrip;
import org.xbmc.kore.R;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.SharedElementTransition;
import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.UIUtils;
import butterknife.ButterKnife;
import butterknife.InjectView;
abstract public class AbstractTabsFragment extends AbstractFragment
implements SharedElementTransition.SharedElement {
private static final String TAG = LogUtils.makeLogTag(AbstractTabsFragment.class);
@InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip;
@InjectView(R.id.pager) ViewPager viewPager;
/**
* Use {@link #setDataHolder(AbstractInfoFragment.DataHolder)} to provide the required info
* after creating a new instance of this Fragment
*/
public AbstractTabsFragment() {
super();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (container == null) {
// We're not being shown or there's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_default_view_pager, container, false);
ButterKnife.inject(this, root);
viewPager.setAdapter(createTabsAdapter(getDataHolder()));
pagerTabStrip.setViewPager(viewPager);
return root;
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false);
}
@Override
public boolean isSharedElementVisible() {
View view = getView();
if (view == null)
return false;
//Note: this works as R.id.poster is only used in *InfoFragment.
//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 true;
}
return false;
}
/**
* Called to get the TabsAdapter that should be connected to the ViewPager
* @param dataHolder the data passed to the *DetailsFragment
* @return
*/
abstract protected TabsAdapter createTabsAdapter(AbstractInfoFragment.DataHolder dataHolder);
}

View File

@ -0,0 +1,193 @@
/*
* Copyright 2017 Martijn Brekhof. 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.generic;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.BaseColumns;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridLayout;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.type.VideoType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.sections.video.AllCastActivity;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import java.util.ArrayList;
public class CastFragment extends AbstractAdditionalInfoFragment implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(CastFragment.class);
private static final String BUNDLE_ITEMID = "itemid";
private static final String BUNDLE_TITLE = "title";
private static final String BUNDLE_LOADER_TYPE = "loadertype";
public static enum TYPE {
TVSHOW,
MOVIE
}
public void setArgs(int itemId, String title, TYPE type) {
Bundle bundle = new Bundle();
bundle.putInt(BUNDLE_ITEMID, itemId);
bundle.putString(BUNDLE_TITLE, title);
bundle.putInt(BUNDLE_LOADER_TYPE, type.ordinal());
setArguments(bundle);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_cast, container, false);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Bundle bundle = getArguments();
getLoaderManager().initLoader(bundle.getInt(BUNDLE_LOADER_TYPE), null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
int hostId = HostManager.getInstance(getActivity()).getHostInfo().getId();
Uri uri;
int itemId = getArguments().getInt(BUNDLE_ITEMID);
if (id == TYPE.MOVIE.ordinal()) {
uri = MediaContract.MovieCast.buildMovieCastListUri(hostId, itemId);
return new CursorLoader(getActivity(), uri,
MovieCastListQuery.PROJECTION, null, null, MovieCastListQuery.SORT);
} else {
uri = MediaContract.TVShowCast.buildTVShowCastListUri(hostId, itemId);
return new CursorLoader(getActivity(), uri,
TVShowCastListQuery.PROJECTION, null, null, TVShowCastListQuery.SORT);
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (! cursor.moveToFirst()) {
return;
}
ArrayList<VideoType.Cast> castArrayList;
int id = getArguments().getInt(BUNDLE_LOADER_TYPE);
if (id == TYPE.MOVIE.ordinal()) {
castArrayList = createMovieCastList(cursor);
} else {
castArrayList = createTVShowCastList(cursor);
}
UIUtils.setupCastInfo(getActivity(), castArrayList,
(GridLayout) getView().findViewById(R.id.cast_list),
AllCastActivity.buildLaunchIntent(getActivity(),
getArguments().getString(BUNDLE_TITLE),
castArrayList));
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
private ArrayList<VideoType.Cast> createMovieCastList(Cursor cursor) {
ArrayList<VideoType.Cast> castArrayList = new ArrayList<>(cursor.getCount());
do {
castArrayList.add(new VideoType.Cast(cursor.getString(MovieCastListQuery.NAME),
cursor.getInt(MovieCastListQuery.ORDER),
cursor.getString(MovieCastListQuery.ROLE),
cursor.getString(MovieCastListQuery.THUMBNAIL)));
} while (cursor.moveToNext());
return castArrayList;
}
private ArrayList<VideoType.Cast> createTVShowCastList(Cursor cursor) {
ArrayList<VideoType.Cast> castArrayList = new ArrayList<>(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());
return castArrayList;
}
@Override
public void refresh() {
getLoaderManager().restartLoader(getArguments().getInt(BUNDLE_LOADER_TYPE),
null, this);
}
/**
* Movie cast list query parameters.
*/
public interface MovieCastListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.MovieCast.NAME,
MediaContract.MovieCast.ORDER,
MediaContract.MovieCast.ROLE,
MediaContract.MovieCast.THUMBNAIL,
};
String SORT = MediaContract.MovieCast.ORDER + " ASC";
int ID = 0;
int NAME = 1;
int ORDER = 2;
int ROLE = 3;
int THUMBNAIL = 4;
}
/**
* Movie cast list query parameters.
*/
public interface TVShowCastListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.MovieCast.NAME,
MediaContract.MovieCast.ORDER,
MediaContract.MovieCast.ROLE,
MediaContract.MovieCast.THUMBNAIL,
};
String SORT = MediaContract.TVShowCast.ORDER + " ASC";
int ID = 0;
int NAME = 1;
int ORDER = 2;
int ROLE = 3;
int THUMBNAIL = 4;
}
}

View File

@ -0,0 +1,187 @@
/*
* Copyright 2017 Martijn Brekhof. 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.generic;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.widget.Toast;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiException;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import de.greenrobot.event.EventBus;
public class RefreshItem {
private final String TAG = LogUtils.makeLogTag(RefreshItem.class);
public interface RefreshItemListener {
void onSyncProcessEnded(MediaSyncEvent event);
}
private RefreshItemListener listener;
private String syncType;
private String syncID;
private int itemId;
private Context context;
private SwipeRefreshLayout swipeRefreshLayout;
private RefreshItem() {}
/**
* RefreshItem can be used to refresh the information for one or more items.
* If you want to sync a single item you will need to use {@link #setSyncItem(String, int)}
* to set the item that needs to be refreshed.
* @param context
* @param syncType {@link LibrarySyncService} SyncType
*/
public RefreshItem(Context context, String syncType) {
if (syncType == null) {
throw new IllegalArgumentException("Argument syncType can not be null");
}
this.syncType = syncType;
this.context = context;
}
/**
* Sets the item that needs to be refreshed. Only required if you want to refresh a single item.
* @param syncID {@link LibrarySyncService} syncID if you want to refresh a single item.
* @param itemId the item ID of the single item (set with syncID) you want to refresh.
*/
public void setSyncItem(String syncID, int itemId) {
this.syncID = syncID;
this.itemId = itemId;
}
/**
* @return {@link LibrarySyncService} SyncType
*/
public String getSyncType() {
return syncType;
}
/**
* Specifiy a listener if you want to be notified when the synchronization has finished.
* @param listener
*/
public void setListener(RefreshItemListener listener) {
this.listener = listener;
}
/**
* If you use a SwipeRefreshLayout you can let RefreshItem manage the refresh animation
* by passing in the reference to SwipeRefreshLayout in your View
* @param swipeRefreshLayout
*/
public void setSwipeRefreshLayout(SwipeRefreshLayout swipeRefreshLayout) {
this.swipeRefreshLayout = swipeRefreshLayout;
}
public void startSync(boolean silentRefresh) {
LogUtils.LOGD(TAG, "Starting sync. Silent? " + silentRefresh);
HostInfo hostInfo = HostManager.getInstance(context).getHostInfo();
if (hostInfo != null) {
register();
if ((swipeRefreshLayout != null) && (!silentRefresh)) {
UIUtils.showRefreshAnimation(swipeRefreshLayout);
}
// Start the syncing process
Intent syncIntent = new Intent(context, LibrarySyncService.class);
syncIntent.putExtra(syncType, true);
if ((syncID != null) && (itemId != -1)) {
syncIntent.putExtra(syncID, itemId);
}
Bundle syncExtras = new Bundle();
syncExtras.putBoolean(LibrarySyncService.SILENT_SYNC, silentRefresh);
syncIntent.putExtra(LibrarySyncService.SYNC_EXTRAS, syncExtras);
context.startService(syncIntent);
} else {
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
Toast.makeText(context, R.string.no_xbmc_configured, Toast.LENGTH_SHORT)
.show();
}
}
/**
* Event bus post. Called when the syncing process ended
*
* @param event Refreshes data
*/
public void onEventMainThread(MediaSyncEvent event) {
unregister();
if (! event.syncType.equals(syncType))
return;
boolean silentRefresh = false;
if (event.syncExtras != null) {
silentRefresh = event.syncExtras.getBoolean(LibrarySyncService.SILENT_SYNC, false);
}
if( swipeRefreshLayout != null ) {
swipeRefreshLayout.setRefreshing(false);
}
if (listener != null) {
listener.onSyncProcessEnded(event);
}
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
if (!silentRefresh) {
Toast.makeText(context,
R.string.sync_successful, Toast.LENGTH_SHORT)
.show();
}
} else if (!silentRefresh) {
String msg = (event.errorCode == ApiException.API_ERROR) ?
String.format(context.getString(R.string.error_while_syncing), event.errorMessage) :
context.getString(R.string.unable_to_connect_to_xbmc);
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
}
public void unregister() {
EventBus eventBus = EventBus.getDefault();
if ( eventBus.isRegistered(this) ) {
eventBus.unregister(this);
}
}
public void register() {
EventBus eventBus = EventBus.getDefault();
if ( ! eventBus.isRegistered(this) ) {
eventBus.register(this);
}
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2015 Synced Synapse. All rights reserved. * Copyright 2016 Synced Synapse. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,363 +15,43 @@
*/ */
package org.xbmc.kore.ui.sections.addon; package org.xbmc.kore.ui.sections.addon;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
import com.melnykov.fab.ObservableScrollView;
import org.xbmc.kore.R; import org.xbmc.kore.R;
import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.ui.AbstractTabsFragment;
import org.xbmc.kore.host.HostManager; import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.jsonrpc.ApiCallback; import org.xbmc.kore.ui.sections.file.MediaFileListFragment;
import org.xbmc.kore.jsonrpc.method.Addons;
import org.xbmc.kore.jsonrpc.type.AddonType;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.Utils;
import java.util.Collections; public class AddonDetailsFragment extends AbstractTabsFragment {
import java.util.HashSet;
import java.util.Set;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
/**
* Presents addon details
*/
public class AddonDetailsFragment extends SharedElementFragment {
private static final String TAG = LogUtils.makeLogTag(AddonDetailsFragment.class); private static final String TAG = LogUtils.makeLogTag(AddonDetailsFragment.class);
public static final String BUNDLE_KEY_ADDONID = "addon_id"; public Bundle contentArgs(Bundle details) {
public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(details);
public static final String BUNDLE_KEY_NAME = "name"; String name = dataHolder.getTitle();
public static final String BUNDLE_KEY_AUTHOR = "author"; String path = details.getString(AddonInfoFragment.BUNDLE_KEY_ADDONID);
public static final String BUNDLE_KEY_SUMMARY = "summary";
public static final String BUNDLE_KEY_VERSION = "version";
public static final String BUNDLE_KEY_DESCRIPTION = "description";
public static final String BUNDLE_KEY_FANART = "fanart";
public static final String BUNDLE_KEY_POSTER = "poster";
public static final String BUNDLE_KEY_ENABLED = "enabled";
public static final String BUNDLE_KEY_BROWSABLE = "browsable";
private HostManager hostManager; MediaFileListFragment.FileLocation rootPath = new MediaFileListFragment.FileLocation(name, "plugin://" + path, true);
private HostInfo hostInfo; rootPath.setRootDir(true);
details.putParcelable(MediaFileListFragment.ROOT_PATH, rootPath);
/** details.putBoolean(MediaFileListFragment.DELAY_LOAD, true);
* Handler on which to post RPC callbacks return details;
*/
private Handler callbackHandler = new Handler();
// Displayed addon id
private String addonId;
// Buttons
@InjectView(R.id.fab) ImageButton fabButton;
@InjectView(R.id.enable_disable) ImageButton enabledButton;
@InjectView(R.id.pin_unpin) ImageView pinButton;
// 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.author) TextView mediaAuthor;
@InjectView(R.id.version) TextView mediaVersion;
@InjectView(R.id.media_description) TextView mediaDescription;
/**
* Create a new instance of this, initialized to show the addon addonId
*/
@TargetApi(21)
public static AddonDetailsFragment newInstance(AddonListFragment.ViewHolder vh) {
AddonDetailsFragment fragment = new AddonDetailsFragment();
Bundle args = new Bundle();
args.putString(BUNDLE_KEY_ADDONID, vh.addonId);
args.putString(BUNDLE_KEY_NAME, vh.addonName);
args.putString(BUNDLE_KEY_AUTHOR, vh.author);
args.putString(BUNDLE_KEY_VERSION, vh.version);
args.putString(BUNDLE_KEY_SUMMARY, vh.summary);
args.putString(BUNDLE_KEY_DESCRIPTION, vh.description);
args.putString(BUNDLE_KEY_FANART, vh.fanart);
args.putString(BUNDLE_KEY_POSTER, vh.poster);
args.putBoolean(BUNDLE_KEY_ENABLED, vh.enabled);
args.putBoolean(BUNDLE_KEY_BROWSABLE, vh.browsable);
if( Utils.isLollipopOrLater()) {
args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName());
}
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@TargetApi(21)
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Bundle bundle = getArguments();
addonId = bundle.getString(BUNDLE_KEY_ADDONID, null);
if ((container == null) || (addonId == null)) {
// We're not being shown or there's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_addon_details, container, false);
ButterKnife.inject(this, root);
hostManager = HostManager.getInstance(getActivity());
hostInfo = hostManager.getHostInfo();
// 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);
}
});
FloatingActionButton fab = (FloatingActionButton)fabButton;
fab.attachToScrollView((ObservableScrollView) mediaPanel);
if(Utils.isLollipopOrLater()) {
mediaPoster.setTransitionName(bundle.getString(POSTER_TRANS_NAME));
}
mediaTitle.setText(bundle.getString(BUNDLE_KEY_NAME));
mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_SUMMARY));
mediaAuthor.setText(bundle.getString(BUNDLE_KEY_AUTHOR));
mediaVersion.setText(bundle.getString(BUNDLE_KEY_VERSION));
mediaDescription.setText(bundle.getString(BUNDLE_KEY_DESCRIPTION));
setImages(bundle.getString(BUNDLE_KEY_POSTER), bundle.getString(BUNDLE_KEY_FANART));
setupEnableButton(bundle.getBoolean(BUNDLE_KEY_ENABLED, false));
if (bundle.getBoolean(BUNDLE_KEY_BROWSABLE, true))
updatePinButton();
// Pad main content view to overlap with bottom system bar
// UIUtils.setPaddingForSystemBars(getActivity(), mediaPanel, false, false, true);
// mediaPanel.setClipToPadding(false);
return root;
} }
@Override @Override
public void onActivityCreated (Bundle savedInstanceState) { public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false); setHasOptionsMenu(false);
updateEnabledButton();
} }
@Override @Override
public void onResume() { protected TabsAdapter createTabsAdapter(AbstractInfoFragment.DataHolder dataHolder) {
super.onResume(); long baseFragmentId = 1000;
} Bundle args = getArguments();
return new TabsAdapter(getActivity(), getChildFragmentManager())
@Override .addTab(AddonInfoFragment.class, args, R.string.addon_overview, baseFragmentId++)
public void onPause() { .addTab(MediaFileListFragment.class, contentArgs(args), R.string.addon_content, baseFragmentId++)
super.onPause(); ;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// outState.putInt(ADDONID, addonId);
}
/**
* Callbacks for button bar
*/
@OnClick(R.id.fab)
public void onFabClicked(View v) {
Addons.ExecuteAddon action = new Addons.ExecuteAddon(addonId);
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
// Do nothing
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.enable_disable)
public void onEnabledClicked(View v) {
final Boolean isEnabled = (v.getTag() == null)? false : (Boolean)v.getTag();
Addons.SetAddonEnabled action = new Addons.SetAddonEnabled(addonId, !isEnabled);
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
int messageResId = (!isEnabled) ? R.string.addon_enabled : R.string.addon_disabled;
Toast.makeText(getActivity(), messageResId, Toast.LENGTH_SHORT).show();
setupEnableButton(!isEnabled);
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
Toast.makeText(getActivity(),
String.format(getString(R.string.general_error_executing_action), description),
Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
private void setImages(String poster, String fanart) {
Resources resources = getActivity().getResources();
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height),
artWidth = displayMetrics.widthPixels;
int posterWidth = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_width);
int posterHeight = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_height);
UIUtils.loadImageIntoImageview(hostManager,
TextUtils.isEmpty(fanart)? poster : fanart,
mediaArt, artWidth, artHeight);
UIUtils.loadImageIntoImageview(hostManager,
poster,
mediaPoster, posterWidth, posterHeight);
}
private void setupEnableButton(boolean enabled) {
// Enabled button
if (enabled) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{
R.attr.colorAccent});
enabledButton.setColorFilter(styledAttributes.getColor(0,
getActivity().getResources().getColor(R.color.accent_default)));
styledAttributes.recycle();
fabButton.setVisibility(View.VISIBLE);
} else {
enabledButton.clearColorFilter();
fabButton.setVisibility(View.GONE);
}
enabledButton.setTag(enabled);
}
/**
* Returns the shared element if visible
* @return View if visible, null otherwise
*/
@Override
public View getSharedElement() {
if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) {
return mediaPoster;
}
return null;
}
private void updateEnabledButton() {
// Get the addon details, this is done asyhnchronously
String[] properties = new String[] {
AddonType.Fields.ENABLED
};
Addons.GetAddonDetails action = new Addons.GetAddonDetails(addonId, properties);
action.execute(hostManager.getConnection(), new ApiCallback<AddonType.Details>() {
@Override
public void onSuccess(AddonType.Details result) {
if (!isAdded()) return;
setupEnableButton(result.enabled);
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
Toast.makeText(getActivity(),
String.format(getString(R.string.error_getting_addon_info), description),
Toast.LENGTH_SHORT).show();
}
}, callbackHandler);
}
@OnClick(R.id.pin_unpin)
public void onPinClicked(View v) {
final boolean isBookmarked = (v.getTag() == null)? true : !(Boolean)v.getTag();
String name = mediaTitle.getText().toString();
String path = addonId;
SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE);
Set<String> bookmarks = new HashSet<>(prefs.getStringSet("bookmarked", Collections.<String>emptySet()));
if (isBookmarked)
bookmarks.add(path);
else
bookmarks.remove(path);
prefs.edit()
.putStringSet("bookmarked", bookmarks)
.putString("name_" + path, name)
.apply();
Toast.makeText(getActivity(), isBookmarked? R.string.addon_pinned : R.string.addon_unpinned, Toast.LENGTH_SHORT).show();
setupPinButton(isBookmarked);
}
private void setupPinButton(boolean bookmarked) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes =
theme.obtainStyledAttributes(new int[] {R.attr.defaultButtonColorFilter, R.attr.colorAccent});
Resources resources = getActivity().getResources();
// Bookmarked button
if (bookmarked) {
pinButton.setColorFilter(styledAttributes.getColor(styledAttributes.getIndex(1),
resources.getColor(R.color.accent_default)));
} else {
pinButton.setColorFilter(styledAttributes.getColor(styledAttributes.getIndex(0),
resources.getColor(R.color.white)));
}
styledAttributes.recycle();
pinButton.setTag(bookmarked);
pinButton.setVisibility(View.VISIBLE);
}
private void updatePinButton() {
SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE);
Set<String> bookmarked = prefs.getStringSet("bookmarked", Collections.<String>emptySet());
setupPinButton(bookmarked.contains(addonId));
} }
} }

View File

@ -0,0 +1,194 @@
/*
* 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.sections.addon;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.method.Addons;
import org.xbmc.kore.jsonrpc.type.AddonType;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.utils.LogUtils;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Presents addon details
*/
public class AddonInfoFragment extends AbstractInfoFragment {
private static final String TAG = LogUtils.makeLogTag(AddonInfoFragment.class);
public static final String BUNDLE_KEY_ADDONID = "addonid";
public static final String BUNDLE_KEY_BROWSABLE = "browsable";
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
private String addonId;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addonId = getDataHolder().getBundle().getString(BUNDLE_KEY_ADDONID, null);
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false);
}
@Override
protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() {
return null;
}
@Override
protected RefreshItem createRefreshItem() {
return null;
}
@Override
protected boolean setupMediaActionBar() {
boolean browsable = getDataHolder().getBundle().getBoolean(BUNDLE_KEY_BROWSABLE, true);
if (browsable) {
setupPinButton();
}
setupEnabledButton();
return true;
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Addons.ExecuteAddon action = new Addons.ExecuteAddon(addonId);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
// Do nothing
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
});
return true;
}
private void setupEnabledButton() {
setOnSeenListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final Boolean isEnabled = (v.getTag() == null)? false : (Boolean)v.getTag();
Addons.SetAddonEnabled action = new Addons.SetAddonEnabled(addonId, !isEnabled);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
int messageResId = (!isEnabled) ? R.string.addon_enabled : R.string.addon_disabled;
Toast.makeText(getActivity(), messageResId, Toast.LENGTH_SHORT).show();
setSeenButtonState(!isEnabled);
setFabButtonState(!isEnabled);
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
Toast.makeText(getActivity(),
String.format(getString(R.string.general_error_executing_action), description),
Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
});
// Get the addon details, this is done asyhnchronously
String[] properties = new String[] {
AddonType.Fields.ENABLED
};
Addons.GetAddonDetails action = new Addons.GetAddonDetails(
addonId, properties);
action.execute(getHostManager().getConnection(), new ApiCallback<AddonType.Details>() {
@Override
public void onSuccess(AddonType.Details result) {
if (!isAdded()) return;
setSeenButtonState(result.enabled);
setFabButtonState(result.enabled);
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
Toast.makeText(getActivity(),
String.format(getString(R.string.error_getting_addon_info), description),
Toast.LENGTH_SHORT).show();
}
}, callbackHandler);
}
private void setupPinButton() {
setOnPinClickedListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final boolean isBookmarked = (view.getTag() == null) ? true : !(Boolean) view.getTag();
String name = getDataHolder().getTitle();
String path = addonId;
SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE);
Set<String> bookmarks = new HashSet<>(prefs.getStringSet("bookmarked", Collections.<String>emptySet()));
if (isBookmarked)
bookmarks.add(path);
else
bookmarks.remove(path);
prefs.edit()
.putStringSet("bookmarked", bookmarks)
.putString("name_" + path, name)
.apply();
Toast.makeText(getActivity(), isBookmarked ? R.string.addon_pinned : R.string.addon_unpinned, Toast.LENGTH_SHORT).show();
setPinButtonState(!isBookmarked);
}
});
SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE);
Set<String> bookmarked = prefs.getStringSet("bookmarked", Collections.<String>emptySet());
setPinButtonState(bookmarked.contains(addonId));
}
}

View File

@ -18,96 +18,44 @@ package org.xbmc.kore.ui.sections.addon;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.astuetz.PagerSlidingTabStrip;
import org.xbmc.kore.R; import org.xbmc.kore.R;
import org.xbmc.kore.ui.AbstractTabsFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.sections.file.MediaFileListFragment; import org.xbmc.kore.ui.sections.file.MediaFileListFragment;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.TabsAdapter; import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.UIUtils;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import butterknife.ButterKnife; public class AddonListContainerFragment extends AbstractTabsFragment {
import butterknife.InjectView;
/**
* Container for the TV Show overview and Episodes list
*/
public class AddonListContainerFragment extends Fragment {
private static final String TAG = LogUtils.makeLogTag(AddonListContainerFragment.class); private static final String TAG = LogUtils.makeLogTag(AddonListContainerFragment.class);
private TabsAdapter tabsAdapter;
@InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip;
@InjectView(R.id.pager) ViewPager viewPager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (container == null) {
// We're not being shown or there's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_default_view_pager, container, false);
ButterKnife.inject(this, root);
tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager());
SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE);
Set<String> bookmarked = prefs.getStringSet("bookmarked", Collections.<String>emptySet());
long baseFragmentId = 70 + bookmarked.size() * 100;
tabsAdapter.addTab(AddonListFragment.class, new Bundle(), R.string.addons, baseFragmentId);
for (String path: bookmarked) {
String name = prefs.getString("name_" + path, "Content");
Bundle addon = new Bundle();
addon.putString(AddonDetailsFragment.BUNDLE_KEY_NAME, name);
addon.putParcelable(MediaFileListFragment.ROOT_PATH, new MediaFileListFragment.FileLocation(name, "plugin://" + path, true));
tabsAdapter.addTab(MediaFileListFragment.class, addon, name, ++baseFragmentId);
}
viewPager.setAdapter(tabsAdapter);
pagerTabStrip.setViewPager(viewPager);
return root;
}
@Override @Override
public void onActivityCreated (Bundle savedInstanceState) { public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false); setHasOptionsMenu(false);
} }
public Fragment getCurrentTabFragment() { @Override
return tabsAdapter.getItem(viewPager.getCurrentItem()); protected TabsAdapter createTabsAdapter(AbstractInfoFragment.DataHolder dataHolder) {
} Bundle arguments = dataHolder.getBundle();
if (arguments == null)
arguments = new Bundle();
public View getSharedElement() { TabsAdapter tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager());
View view = getView(); SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE);
if (view == null) Set<String> bookmarked = prefs.getStringSet("bookmarked", Collections.<String>emptySet());
return null; long baseFragmentId = 70 + bookmarked.size() * 100;
tabsAdapter.addTab(AddonListFragment.class, new Bundle(), R.string.addons, baseFragmentId);
//Note: this works as R.id.poster is only used in TVShowOverviewFragment. for (String path: bookmarked) {
//If the same id is used in other fragments in the TabsAdapter we String name = prefs.getString("name_" + path, "Content");
//need to check which fragment is currently displayed arguments.putParcelable(MediaFileListFragment.ROOT_PATH,
View artView = view.findViewById(R.id.poster); new MediaFileListFragment.FileLocation(name, "plugin://" + path, true));
View scrollView = view.findViewById(R.id.media_panel); tabsAdapter.addTab(MediaFileListFragment.class, arguments, name, ++baseFragmentId);
if (( artView != null ) &&
( scrollView != null ) &&
UIUtils.isViewInBounds(scrollView, artView)) {
return artView;
} }
return null; return tabsAdapter;
} }
} }

View File

@ -21,14 +21,12 @@ import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.support.v4.app.Fragment;
import android.support.v4.widget.SwipeRefreshLayout;
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.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.GridView; import android.widget.BaseAdapter;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -38,6 +36,8 @@ import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback; import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.method.Addons; import org.xbmc.kore.jsonrpc.method.Addons;
import org.xbmc.kore.jsonrpc.type.AddonType; import org.xbmc.kore.jsonrpc.type.AddonType;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.AbstractListFragment;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils; import org.xbmc.kore.utils.Utils;
@ -46,58 +46,27 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import butterknife.ButterKnife;
import butterknife.InjectView;
/** /**
* Fragment that presents the movie list * Fragment that presents the movie list
*/ */
public class AddonListFragment extends Fragment public class AddonListFragment extends AbstractListFragment {
implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = LogUtils.makeLogTag(AddonListFragment.class); private static final String TAG = LogUtils.makeLogTag(AddonListFragment.class);
public interface OnAddonSelectedListener { public interface OnAddonSelectedListener {
public void onAddonSelected(ViewHolder vh); void onAddonSelected(ViewHolder vh);
} }
// Activity listener // Activity listener
private OnAddonSelectedListener listenerActivity; private OnAddonSelectedListener listenerActivity;
private HostManager hostManager;
@InjectView(R.id.list) GridView addonsGridView;
@InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;
@InjectView(android.R.id.empty) TextView emptyView;
/** /**
* Handler on which to post RPC callbacks * Handler on which to post RPC callbacks
*/ */
private Handler callbackHandler = new Handler(); private Handler callbackHandler = new Handler();
private AddonsAdapter adapter = null;
@Override @Override
public void onCreate(Bundle savedInstanceState) { protected AdapterView.OnItemClickListener createOnItemClickListener() {
super.onCreate(savedInstanceState); return new AdapterView.OnItemClickListener() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_generic_media_list, container, false);
ButterKnife.inject(this, root);
hostManager = HostManager.getInstance(getActivity());
swipeRefreshLayout.setOnRefreshListener(this);
emptyView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onRefresh();
}
});
addonsGridView.setEmptyView(emptyView);
addonsGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// Get the movie id from the tag // Get the movie id from the tag
@ -105,27 +74,21 @@ public class AddonListFragment extends Fragment
// Notify the activity // Notify the activity
listenerActivity.onAddonSelected(tag); listenerActivity.onAddonSelected(tag);
} }
}); };
if (adapter == null) {
adapter = new AddonsAdapter(getActivity(), R.layout.grid_item_addon);
}
addonsGridView.setAdapter(adapter);
// Pad main content view to overlap with bottom system bar
// UIUtils.setPaddingForSystemBars(getActivity(), addonsGridView, false, false, true);
// addonsGridView.setClipToPadding(false);
return root;
} }
@Override
protected BaseAdapter createAdapter() {
return new AddonsAdapter(getActivity(), R.layout.grid_item_addon);
}
@Override @Override
public void onActivityCreated (Bundle savedInstanceState) { public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false); setHasOptionsMenu(false);
if (adapter.getCount() == 0) if (getAdapter().getCount() == 0)
callGetAddonsAndSetup(); callGetAddonsAndSetup();
} }
@ -145,26 +108,12 @@ public class AddonListFragment extends Fragment
listenerActivity = null; listenerActivity = null;
} }
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
/**
* Swipe refresh layout callback
*/
/** {@inheritDoc} */
@Override @Override
public void onRefresh () { public void onRefresh () {
if (hostManager.getHostInfo() != null) { if (HostManager.getInstance(getActivity()).getHostInfo() != null) {
callGetAddonsAndSetup(); callGetAddonsAndSetup();
} else { } else {
swipeRefreshLayout.setRefreshing(false); hideRefreshAnimation();
Toast.makeText(getActivity(), R.string.no_xbmc_configured, Toast.LENGTH_SHORT) Toast.makeText(getActivity(), R.string.no_xbmc_configured, Toast.LENGTH_SHORT)
.show(); .show();
} }
@ -179,7 +128,9 @@ public class AddonListFragment extends Fragment
* Get the addons list and setup the gridview * Get the addons list and setup the gridview
*/ */
private void callGetAddonsAndSetup() { private void callGetAddonsAndSetup() {
swipeRefreshLayout.setRefreshing(true); final AddonsAdapter adapter = (AddonsAdapter) getAdapter();
showRefreshAnimation();
// Get the addon list, this is done asyhnchronously // Get the addon list, this is done asyhnchronously
String[] properties = new String[] { String[] properties = new String[] {
AddonType.Fields.NAME, AddonType.Fields.VERSION, AddonType.Fields.SUMMARY, AddonType.Fields.NAME, AddonType.Fields.VERSION, AddonType.Fields.SUMMARY,
@ -189,54 +140,60 @@ public class AddonListFragment extends Fragment
AddonType.Fields.RATING, AddonType.Fields.ENABLED AddonType.Fields.RATING, AddonType.Fields.ENABLED
}; };
Addons.GetAddons action = new Addons.GetAddons(properties); Addons.GetAddons action = new Addons.GetAddons(properties);
action.execute(hostManager.getConnection(), new ApiCallback<List<AddonType.Details>>() { action.execute(HostManager.getInstance(getActivity()).getConnection(),
@Override new ApiCallback<List<AddonType.Details>>() {
public void onSuccess(List<AddonType.Details> result) { @Override
if (!isAdded()) return; public void onSuccess(List<AddonType.Details> result) {
for (AddonType.Details addon : result) { if (!isAdded()) return;
String regex = "\\[.*?\\]";
addon.name = addon.name.replaceAll(regex, "");
addon.description = addon.description.replaceAll(regex, "");
addon.summary = addon.summary.replaceAll(regex, "");
addon.author = addon.author.replaceAll(regex, "");
}
Collections.sort(result, new AddonNameComparator());
adapter.clear();
for (AddonType.Details addon : result) {
if (addon.type.equals(AddonType.Types.UNKNOWN) ||
addon.type.equals(AddonType.Types.XBMC_PYTHON_PLUGINSOURCE) ||
addon.type.equals(AddonType.Types.XBMC_PYTHON_SCRIPT) ||
addon.type.equals(AddonType.Types.XBMC_ADDON_AUDIO) ||
addon.type.equals(AddonType.Types.XBMC_ADDON_EXECUTABLE) ||
addon.type.equals(AddonType.Types.XBMC_ADDON_VIDEO) ||
addon.type.equals(AddonType.Types.XBMC_ADDON_IMAGE)) {
adapter.add(addon);
}
}
// To prevent the empty text from appearing on the first load, set it now
emptyView.setText(getString(R.string.no_addons_found_refresh));
adapter.notifyDataSetChanged();
swipeRefreshLayout.setRefreshing(false);
}
@Override for (AddonType.Details addon : result) {
public void onError(int errorCode, String description) { String regex = "\\[.*?\\]";
if (!isAdded()) return; addon.name = addon.name.replaceAll(regex, "");
addon.description = addon.description.replaceAll(regex, "");
addon.summary = addon.summary.replaceAll(regex, "");
addon.author = addon.author.replaceAll(regex, "");
}
Collections.sort(result, new AddonNameComparator());
// To prevent the empty text from appearing on the first load, set it now adapter.clear();
emptyView.setText(getString(R.string.no_addons_found_refresh)); for (AddonType.Details addon : result) {
Toast.makeText(getActivity(), if (addon.type.equals(AddonType.Types.UNKNOWN) ||
String.format(getString(R.string.error_getting_addon_info), description), addon.type.equals(AddonType.Types.XBMC_PYTHON_PLUGINSOURCE) ||
Toast.LENGTH_SHORT).show(); addon.type.equals(AddonType.Types.XBMC_PYTHON_SCRIPT) ||
swipeRefreshLayout.setRefreshing(false); addon.type.equals(AddonType.Types.XBMC_ADDON_AUDIO) ||
} addon.type.equals(AddonType.Types.XBMC_ADDON_EXECUTABLE) ||
}, callbackHandler); addon.type.equals(AddonType.Types.XBMC_ADDON_VIDEO) ||
addon.type.equals(AddonType.Types.XBMC_ADDON_IMAGE)) {
adapter.add(addon);
}
}
adapter.notifyDataSetChanged();
hideRefreshAnimation();
if(adapter.getCount() == 0) {
getEmptyView().setText(R.string.no_addons_found_refresh);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
Toast.makeText(getActivity(),
String.format(getString(R.string.error_getting_addon_info), description),
Toast.LENGTH_SHORT).show();
hideRefreshAnimation();
}
}, callbackHandler);
} }
private class AddonsAdapter extends ArrayAdapter<AddonType.Details> { private class AddonsAdapter extends ArrayAdapter<AddonType.Details> {
private HostManager hostManager; private HostManager hostManager;
private int artWidth, artHeight; private int artWidth, artHeight;
private String author;
private String version;
public AddonsAdapter(Context context, int resource) { public AddonsAdapter(Context context, int resource) {
super(context, resource); super(context, resource);
@ -246,8 +203,11 @@ public class AddonListFragment extends Fragment
// Use the same dimensions as in the details fragment, so that it hits Picasso's cache when // Use the same dimensions as in the details fragment, so that it hits Picasso's cache when
// the user transitions to that fragment, avoiding another call and imediatelly showing the image // the user transitions to that fragment, avoiding another call and imediatelly showing the image
Resources resources = context.getResources(); Resources resources = context.getResources();
artWidth = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_width);; artWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_square);
artHeight = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_height);; artHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_square);
author = context.getString(R.string.author);
version = context.getString(R.string.version);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@ -269,27 +229,26 @@ public class AddonListFragment extends Fragment
final ViewHolder viewHolder = (ViewHolder)convertView.getTag(); final ViewHolder viewHolder = (ViewHolder)convertView.getTag();
AddonType.Details addonDetails = this.getItem(position); AddonType.Details addonDetails = this.getItem(position);
// Save the movie id viewHolder.dataHolder.setTitle(addonDetails.name);
viewHolder.addonId = addonDetails.addonid; viewHolder.dataHolder.setDescription(addonDetails.description);
viewHolder.addonName = addonDetails.name; viewHolder.dataHolder.setUndertitle(addonDetails.summary);
viewHolder.author = addonDetails.author; viewHolder.dataHolder.setFanArtUrl(addonDetails.fanart);
viewHolder.description = addonDetails.description; viewHolder.dataHolder.setPosterUrl(addonDetails.thumbnail);
viewHolder.summary = addonDetails.summary; viewHolder.dataHolder.setDetails(author + " " + addonDetails.author + "\n" +
viewHolder.version = addonDetails.version; version + " " +addonDetails.version);
viewHolder.fanart = addonDetails.fanart; viewHolder.dataHolder.getBundle().putString(AddonInfoFragment.BUNDLE_KEY_ADDONID, addonDetails.addonid);
viewHolder.poster = addonDetails.thumbnail; viewHolder.dataHolder.getBundle().putBoolean(AddonInfoFragment.BUNDLE_KEY_BROWSABLE,
viewHolder.enabled = addonDetails.enabled; AddonType.Types.XBMC_PYTHON_PLUGINSOURCE.equals(addonDetails.type));
viewHolder.browsable = AddonType.Types.XBMC_PYTHON_PLUGINSOURCE.equals(addonDetails.type);
viewHolder.titleView.setText(viewHolder.addonName); viewHolder.titleView.setText(viewHolder.dataHolder.getTitle());
viewHolder.detailsView.setText(addonDetails.summary); viewHolder.detailsView.setText(addonDetails.summary);
UIUtils.loadImageWithCharacterAvatar(getContext(), hostManager, UIUtils.loadImageWithCharacterAvatar(getContext(), hostManager,
addonDetails.thumbnail, viewHolder.addonName, addonDetails.thumbnail, viewHolder.dataHolder.getTitle(),
viewHolder.artView, artWidth, artHeight); viewHolder.artView, artWidth, artHeight);
if(Utils.isLollipopOrLater()) { if(Utils.isLollipopOrLater()) {
viewHolder.artView.setTransitionName("a"+viewHolder.addonId); viewHolder.artView.setTransitionName("a"+addonDetails.addonid);
} }
return convertView; return convertView;
} }
@ -303,15 +262,6 @@ public class AddonListFragment extends Fragment
TextView detailsView; TextView detailsView;
ImageView artView; ImageView artView;
String addonId; AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0);
String addonName;
String summary;
String author;
String version;
String description;
String fanart;
String poster;
Boolean enabled;
Boolean browsable;
} }
} }

View File

@ -1,145 +0,0 @@
/*
* 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.
*/
package org.xbmc.kore.ui.sections.addon;
import android.annotation.TargetApi;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.astuetz.PagerSlidingTabStrip;
import org.xbmc.kore.R;
import org.xbmc.kore.ui.sections.file.MediaFileListFragment;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import butterknife.ButterKnife;
import butterknife.InjectView;
/**
* Container for the TV Show overview and Episodes list
*/
public class AddonOverviewFragment extends SharedElementFragment {
private static final String TAG = LogUtils.makeLogTag(AddonOverviewFragment.class);
private TabsAdapter tabsAdapter;
@InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip;
@InjectView(R.id.pager) ViewPager viewPager;
/**
* Create a new instance of this, initialized to show the addon addonId
*/
@TargetApi(21)
public static AddonOverviewFragment newInstance(AddonListFragment.ViewHolder vh) {
AddonOverviewFragment fragment = new AddonOverviewFragment();
Bundle args = new Bundle();
args.putString(AddonDetailsFragment.BUNDLE_KEY_ADDONID, vh.addonId);
args.putString(AddonDetailsFragment.BUNDLE_KEY_NAME, vh.addonName);
args.putString(AddonDetailsFragment.BUNDLE_KEY_AUTHOR, vh.author);
args.putString(AddonDetailsFragment.BUNDLE_KEY_VERSION, vh.version);
args.putString(AddonDetailsFragment.BUNDLE_KEY_SUMMARY, vh.summary);
args.putString(AddonDetailsFragment.BUNDLE_KEY_DESCRIPTION, vh.description);
args.putString(AddonDetailsFragment.BUNDLE_KEY_FANART, vh.fanart);
args.putString(AddonDetailsFragment.BUNDLE_KEY_POSTER, vh.poster);
args.putBoolean(AddonDetailsFragment.BUNDLE_KEY_ENABLED, vh.enabled);
args.putBoolean(AddonDetailsFragment.BUNDLE_KEY_BROWSABLE, vh.browsable);
if( Utils.isLollipopOrLater()) {
args.putString(AddonDetailsFragment.POSTER_TRANS_NAME, vh.artView.getTransitionName());
}
fragment.setArguments(args);
return fragment;
}
public Bundle contentArgs(Bundle details) {
String name = details.getString(AddonDetailsFragment.BUNDLE_KEY_NAME, "Content");
String path = details.getString(AddonDetailsFragment.BUNDLE_KEY_ADDONID);
Bundle content = new Bundle();
content.putString(AddonDetailsFragment.BUNDLE_KEY_NAME, name);
MediaFileListFragment.FileLocation rootPath = new MediaFileListFragment.FileLocation(name, "plugin://" + path, true);
rootPath.setRootDir(true);
content.putParcelable(MediaFileListFragment.ROOT_PATH, rootPath);
content.putBoolean(MediaFileListFragment.DELAY_LOAD, true);
return content;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Bundle args = getArguments();
if ((container == null) || (args == null)) {
// We're not being shown or there's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_default_view_pager, container, false);
ButterKnife.inject(this, root);
long baseFragmentId = 1000;
tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager())
.addTab(AddonDetailsFragment.class, args, R.string.addon_overview, baseFragmentId++)
.addTab(MediaFileListFragment.class, contentArgs(args), R.string.addon_content, baseFragmentId++)
;
viewPager.setAdapter(tabsAdapter);
pagerTabStrip.setViewPager(viewPager);
return root;
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false);
}
public Fragment getCurrentTabFragment() {
return tabsAdapter.getItem(viewPager.getCurrentItem());
}
@Override
public View getSharedElement() {
View view = getView();
if (view == null)
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;
}
}

View File

@ -18,27 +18,24 @@ package org.xbmc.kore.ui.sections.addon;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout; import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View;
import android.view.Window; import android.view.Window;
import org.xbmc.kore.R; import org.xbmc.kore.R;
import org.xbmc.kore.ui.AbstractFragment;
import org.xbmc.kore.ui.BaseActivity; import org.xbmc.kore.ui.BaseActivity;
import org.xbmc.kore.ui.generic.NavigationDrawerFragment; import org.xbmc.kore.ui.generic.NavigationDrawerFragment;
import org.xbmc.kore.ui.sections.remote.RemoteActivity; import org.xbmc.kore.ui.sections.remote.RemoteActivity;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.SharedElementTransition;
import org.xbmc.kore.utils.Utils; import org.xbmc.kore.utils.Utils;
import java.util.List;
import java.util.Map;
/** /**
* Controls the presentation of Addons information (list, details) * Controls the presentation of Addons information (list, details)
* All the information is presented by specific fragments * All the information is presented by specific fragments
@ -49,13 +46,14 @@ public class AddonsActivity extends BaseActivity
public static final String ADDONID = "addon_id"; public static final String ADDONID = "addon_id";
public static final String ADDONTITLE = "addon_title"; public static final String ADDONTITLE = "addon_title";
public static final String LISTFRAGMENT_TAG = "addonlist";
private String selectedAddonId; private String selectedAddonId;
private String selectedAddonTitle; private String selectedAddonTitle;
private NavigationDrawerFragment navigationDrawerFragment; private NavigationDrawerFragment navigationDrawerFragment;
private boolean clearSharedElements; private SharedElementTransition sharedElementTransition = new SharedElementTransition();
@TargetApi(21) @TargetApi(21)
@Override @Override
@ -72,54 +70,28 @@ public class AddonsActivity extends BaseActivity
.findFragmentById(R.id.navigation_drawer); .findFragmentById(R.id.navigation_drawer);
navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout)); navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout));
Fragment fragment;
if (savedInstanceState == null) { if (savedInstanceState == null) {
AddonListContainerFragment addonListFragment = new AddonListContainerFragment(); fragment = new AddonListContainerFragment();
// Setup animations
if (Utils.isLollipopOrLater()) {
//Fade added to prevent shared element from disappearing very shortly at the start of the transition.
Transition fade = TransitionInflater
.from(this)
.inflateTransition(android.R.transition.fade);
addonListFragment.setExitTransition(fade);
addonListFragment.setReenterTransition(fade);
addonListFragment.setSharedElementReturnTransition(TransitionInflater.from(
this).inflateTransition(R.transition.change_image));
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() {
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
if (clearSharedElements) {
names.clear();
sharedElements.clear();
clearSharedElements = false;
}
}
};
addonListFragment.setExitSharedElementCallback(seCallback);
}
getSupportFragmentManager() getSupportFragmentManager()
.beginTransaction() .beginTransaction()
.add(R.id.fragment_container, addonListFragment) .add(R.id.fragment_container, fragment, LISTFRAGMENT_TAG)
.commit(); .commit();
} else { } else {
fragment = getSupportFragmentManager().findFragmentByTag(LISTFRAGMENT_TAG);
selectedAddonId = savedInstanceState.getString(ADDONID, null); selectedAddonId = savedInstanceState.getString(ADDONID, null);
selectedAddonTitle = savedInstanceState.getString(ADDONTITLE, null); selectedAddonTitle = savedInstanceState.getString(ADDONTITLE, null);
} }
if (Utils.isLollipopOrLater()) {
sharedElementTransition.setupExitTransition(this, fragment);
}
setupActionBar(selectedAddonTitle); setupActionBar(selectedAddonTitle);
} }
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override @Override
protected void onSaveInstanceState (Bundle outState) { protected void onSaveInstanceState (Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
@ -205,48 +177,26 @@ public class AddonsActivity extends BaseActivity
*/ */
@TargetApi(21) @TargetApi(21)
public void onAddonSelected(AddonListFragment.ViewHolder vh) { public void onAddonSelected(AddonListFragment.ViewHolder vh) {
selectedAddonId = vh.addonId; Bundle bundle = vh.dataHolder.getBundle();
selectedAddonTitle = vh.addonName; selectedAddonId = bundle.getString(AddonInfoFragment.BUNDLE_KEY_ADDONID);
selectedAddonTitle = vh.dataHolder.getTitle();
// Replace list fragment // Replace list fragment
final SharedElementFragment addonDetailsFragment = final AbstractFragment addonDetailsFragment =
vh.browsable bundle.getBoolean(AddonInfoFragment.BUNDLE_KEY_BROWSABLE)
? AddonOverviewFragment.newInstance(vh) ? new AddonDetailsFragment()
: AddonDetailsFragment.newInstance(vh) : new AddonInfoFragment()
; ;
addonDetailsFragment.setDataHolder(vh.dataHolder);
vh.dataHolder.setSquarePoster(true);
vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName());
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions // Set up transitions
if (Utils.isLollipopOrLater()) { if (Utils.isLollipopOrLater()) {
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { sharedElementTransition.setupEnterTransition(this, fragTrans, addonDetailsFragment,
@Override vh.artView);
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
//On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements
// for the reentering fragment. We use this to determine if we are returning and if
// we should clear the shared element lists. Note that, clearing must be done in the reentering fragment
// as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might
// be a v4 support package bug.
if (addonDetailsFragment.isVisible()) {
View sharedView = addonDetailsFragment.getSharedElement();
if (sharedView == null) { // shared element not visible
clearSharedElements = true;
}
}
}
};
addonDetailsFragment.setEnterSharedElementCallback(seCallback);
addonDetailsFragment.setEnterTransition(TransitionInflater
.from(this)
.inflateTransition(R.transition.media_details));
addonDetailsFragment.setReturnTransition(null);
Transition changeImageTransition = TransitionInflater.from(
this).inflateTransition(R.transition.change_image);
addonDetailsFragment.setSharedElementReturnTransition(changeImageTransition);
addonDetailsFragment.setSharedElementEnterTransition(changeImageTransition);
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);

View File

@ -1,677 +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.sections.audio;
import android.annotation.TargetApi;
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.Handler;
import android.preference.PreferenceManager;
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.text.TextUtils;
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.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
import com.melnykov.fab.ObservableScrollView;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.ApiMethod;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.ui.AbstractDetailsFragment;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.util.ArrayList;
import java.util.Locale;
import at.blogc.android.views.ExpandableTextView;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
/**
* Presents movie details
*/
public class AlbumDetailsFragment extends AbstractDetailsFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(AlbumDetailsFragment.class);
public static final String BUNDLE_KEY_ALBUMID = "album_id";
public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME";
public static final String BUNDLE_KEY_ALBUMARTIST = "album_artist";
public static final String BUNDLE_KEY_ALBUMTITLE = "album_title";
public static final String BUNDLE_KEY_ALBUMGENRE = "album_genre";
public static final String BUNDLE_KEY_ALBUMYEAR = "album_year";
public static final String BUNDLE_KEY_ALBUMRATING = "album_rating";
// Loader IDs
private static final int LOADER_ALBUM = 0,
LOADER_SONGS = 1;
private HostManager hostManager;
private HostInfo hostInfo;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
// Displayed album id
private int albumId = -1;
// Album information
private String albumDisplayArtist;
private String albumTitle;
private ArrayList<FileDownloadHelper.SongInfo> songInfoList = null;
@InjectView(R.id.exit_transition_view) View exitTransitionView;
// Buttons
@InjectView(R.id.fab) ImageButton fabButton;
@InjectView(R.id.add_to_playlist) ImageButton addToPlaylistButton;
@InjectView(R.id.download) ImageButton downloadButton;
// 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.year) TextView mediaYear;
@InjectView(R.id.media_description_container) LinearLayout mediaDescriptionContainer;
@InjectView(R.id.media_description) ExpandableTextView mediaDescription;
@InjectView(R.id.show_all) ImageView mediaShowAll;
@InjectView(R.id.song_list) LinearLayout songListView;
/**
* Create a new instance of this, initialized to show the album albumId
*/
@TargetApi(21)
public static AlbumDetailsFragment newInstance(AlbumListFragment.ViewHolder vh) {
AlbumDetailsFragment fragment = new AlbumDetailsFragment();
Bundle args = new Bundle();
args.putInt(BUNDLE_KEY_ALBUMID, vh.albumId);
args.putString(BUNDLE_KEY_ALBUMTITLE, vh.albumTitle);
args.putString(BUNDLE_KEY_ALBUMARTIST, vh.albumArtist);
args.putString(BUNDLE_KEY_ALBUMGENRE, vh.albumGenre);
args.putInt(BUNDLE_KEY_ALBUMYEAR, vh.albumYear);
args.putDouble(BUNDLE_KEY_ALBUMRATING, vh.albumRating);
if( Utils.isLollipopOrLater()) {
args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName());
}
fragment.setArguments(args);
return fragment;
}
@TargetApi(21)
@Override
protected View createView(LayoutInflater inflater, ViewGroup container) {
albumId = getArguments().getInt(BUNDLE_KEY_ALBUMID, -1);
if ((container == null) || (albumId == -1)) {
// We're not being shown or there's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_album_details, container, false);
ButterKnife.inject(this, root);
hostManager = HostManager.getInstance(getActivity());
hostInfo = hostManager.getHostInfo();
// 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);
}
});
FloatingActionButton fab = (FloatingActionButton)fabButton;
fab.attachToScrollView((ObservableScrollView) mediaPanel);
Bundle bundle = getArguments();
if(Utils.isLollipopOrLater()) {
mediaPoster.setTransitionName(bundle.getString(POSTER_TRANS_NAME));
}
mediaTitle.setText(bundle.getString(BUNDLE_KEY_ALBUMTITLE));
mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_ALBUMARTIST));
setMediaYear(bundle.getString(BUNDLE_KEY_ALBUMGENRE), bundle.getInt(BUNDLE_KEY_ALBUMYEAR));
setMediaRating(bundle.getDouble(BUNDLE_KEY_ALBUMRATING));
// 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 null;
}
@Override
protected String getSyncID() {
return null;
}
@Override
protected int getSyncItemID() {
return 0;
}
@Override
protected SwipeRefreshLayout getSwipeRefreshLayout() {
return null;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Start the loaders
getLoaderManager().initLoader(LOADER_ALBUM, null, this);
setHasOptionsMenu(false);
}
@Override
public void onResume() {
// Force the exit view to invisible
exitTransitionView.setVisibility(View.INVISIBLE);
super.onResume();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_ALBUM:
uri = MediaContract.Albums.buildAlbumUri(hostInfo.getId(), albumId);
return new CursorLoader(getActivity(), uri,
AlbumDetailsQuery.PROJECTION, null, null, null);
case LOADER_SONGS:
uri = MediaContract.Songs.buildAlbumSongsListUri(hostInfo.getId(), albumId);
return new CursorLoader(getActivity(), uri,
AlbumSongsListQuery.PROJECTION, null, null, AlbumSongsListQuery.SORT);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_ALBUM:
displayAlbumDetails(cursor);
getLoaderManager().initLoader(LOADER_SONGS, null, this);
break;
case LOADER_SONGS:
displaySongsList(cursor);
break;
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
/**
* Callbacks for button bar
*/
private ApiCallback<String> defaultStringActionCallback = ApiMethod.getDefaultActionCallback();
@OnClick(R.id.fab)
public void onFabClicked(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.albumid = albumId;
Player.Open action = new Player.Open(item);
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Check whether we should switch to the remote
boolean switchToRemote = PreferenceManager
.getDefaultSharedPreferences(getActivity())
.getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
if (switchToRemote) {
int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.add_to_playlist)
public void onAddToPlaylistClicked(View v) {
addToPlaylist(TYPE_ALBUM, albumId);
}
@Override
protected void onDownload() {
UIUtils.downloadSongs(getActivity(), songInfoList, hostInfo, callbackHandler);
}
@Override
protected void onSyncProcessEnded(MediaSyncEvent event) {
}
/**
* Display the album details
*
* @param cursor Cursor with the data
*/
private void displayAlbumDetails(Cursor cursor) {
final Resources resources = getActivity().getResources();
cursor.moveToFirst();
albumTitle = cursor.getString(AlbumDetailsQuery.TITLE);
albumDisplayArtist = cursor.getString(AlbumDetailsQuery.DISPLAYARTIST);
mediaTitle.setText(albumTitle);
mediaUndertitle.setText(albumDisplayArtist);
setMediaYear(cursor.getString(AlbumDetailsQuery.GENRE), cursor.getInt(AlbumDetailsQuery.YEAR));
double rating = cursor.getDouble(AlbumDetailsQuery.RATING);
if (rating > 0) {
mediaRating.setVisibility(View.VISIBLE);
mediaMaxRating.setVisibility(View.VISIBLE);
setMediaRating(rating);
} else {
mediaRating.setVisibility(View.GONE);
mediaMaxRating.setVisibility(View.GONE);
}
String description = cursor.getString(AlbumDetailsQuery.DESCRIPTION);
if (!TextUtils.isEmpty(description)) {
mediaDescription.setVisibility(View.VISIBLE);
mediaDescription.setText(description);
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.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) {
mediaDescription.toggle();
mediaShowAll.setImageResource(mediaDescription.isExpanded() ? iconCollapseResId: iconExpandResId);
}
});
} else {
mediaDescriptionContainer.setVisibility(View.GONE);
}
// Images
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
String fanart = cursor.getString(AlbumDetailsQuery.FANART),
poster = cursor.getString(AlbumDetailsQuery.THUMBNAIL);
int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height),
artWidth = displayMetrics.widthPixels;
int posterWidth = resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_width);
int posterHeight = resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_heigth);
UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager,
poster, albumTitle,
mediaPoster, posterWidth, posterHeight);
UIUtils.loadImageIntoImageview(hostManager,
TextUtils.isEmpty(fanart)? poster : fanart,
mediaArt, artWidth, artHeight);
}
private void setMediaRating(double rating) {
mediaRating.setText(String.format(Locale.getDefault(), "%01.01f", rating));
mediaMaxRating.setText(getString(R.string.max_rating_music));
}
private void setMediaYear(String genres, int year) {
String label = (year > 0) ?
(!TextUtils.isEmpty(genres) ?
genres + " | " + String.valueOf(year) :
String.valueOf(year)) :
genres;
mediaYear.setText(label);
}
/**
* Starts playing the song on XBMC
* @param songId song to play
*/
private void playSong(int songId) {
PlaylistType.Item item = new PlaylistType.Item();
item.songid = songId;
Player.Open action = new Player.Open(item);
action.execute(hostManager.getConnection(), defaultStringActionCallback, callbackHandler);
}
private int TYPE_ALBUM = 0,
TYPE_SONG = 1;
/**
* Adds an album or a song to the audio playlist
* @param type Album or song
* @param id albumId or songId
*/
private void addToPlaylist(final int type, final int id) {
Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
getPlaylists.execute(hostManager.getConnection(), new ApiCallback<ArrayList<PlaylistType.GetPlaylistsReturnType>>() {
@Override
public void onSuccess(ArrayList<PlaylistType.GetPlaylistsReturnType> result) {
if (!isAdded()) return;
// Ok, loop through the playlists, looking for the audio one
int audioPlaylistId = -1;
for (PlaylistType.GetPlaylistsReturnType playlist : result) {
if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.AUDIO)) {
audioPlaylistId = playlist.playlistid;
break;
}
}
// If found, add to playlist
if (audioPlaylistId != -1) {
PlaylistType.Item item = new PlaylistType.Item();
if (type == TYPE_ALBUM) {
item.albumid = id;
} else {
item.songid = id;
}
Playlist.Add action = new Playlist.Add(audioPlaylistId, item);
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
} else {
Toast.makeText(getActivity(), R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
.show();
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
View.OnClickListener songClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
playSong(((FileDownloadHelper.SongInfo)v.getTag()).songId);
}
};
private View.OnClickListener songItemMenuClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
final FileDownloadHelper.SongInfo songInfo = ((FileDownloadHelper.SongInfo)v.getTag());
final int songId = songInfo.songId;
final PopupMenu popupMenu = new PopupMenu(getActivity(), v);
popupMenu.getMenuInflater().inflate(R.menu.song_item, popupMenu.getMenu());
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_play_song:
playSong(songId);
return true;
case R.id.action_add_to_playlist:
addToPlaylist(TYPE_SONG, songId);
return true;
case R.id.download:
ArrayList<FileDownloadHelper.SongInfo> songInfoList = new ArrayList<>();
songInfoList.add(songInfo);
UIUtils.downloadSongs(getActivity(), songInfoList,
hostInfo, callbackHandler);
return true;
}
return false;
}
});
popupMenu.show();
}
};
/**
* Display the songs
*
* @param cursor Cursor with the data
*/
private void displaySongsList(Cursor cursor) {
if (cursor.moveToFirst()) {
songInfoList = new ArrayList<>(cursor.getCount());
do {
View songView = LayoutInflater.from(getActivity())
.inflate(R.layout.list_item_song, songListView, false);
TextView songTitle = (TextView)songView.findViewById(R.id.song_title);
TextView trackNumber = (TextView)songView.findViewById(R.id.track_number);
TextView details = (TextView)songView.findViewById(R.id.details);
ImageView contextMenu = (ImageView)songView.findViewById(R.id.list_context_menu);
String artist = cursor.getString(AlbumSongsListQuery.ARTIST);
// Add this song to the list
FileDownloadHelper.SongInfo songInfo = new FileDownloadHelper.SongInfo(
artist,
albumTitle,
cursor.getInt(AlbumSongsListQuery.SONGID),
cursor.getInt(AlbumSongsListQuery.TRACK),
cursor.getString(AlbumSongsListQuery.TITLE),
cursor.getString(AlbumSongsListQuery.FILE));
songInfoList.add(songInfo);
songTitle.setText(songInfo.title);
trackNumber.setText(String.valueOf(songInfo.track));
String duration = UIUtils.formatTime(cursor.getInt(AlbumSongsListQuery.DURATION));
String detailsText = TextUtils.isEmpty(artist) ? duration : duration + " | " + artist;
details.setText(detailsText);
contextMenu.setTag(songInfo);
contextMenu.setOnClickListener(songItemMenuClickListener);
songView.setTag(songInfo);
songView.setOnClickListener(songClickListener);
songListView.addView(songView);
} while (cursor.moveToNext());
if (!songInfoList.isEmpty()) {
// Check if download dir exists
FileDownloadHelper.SongInfo songInfo = new FileDownloadHelper.SongInfo
(albumDisplayArtist, albumTitle, 0, 0, null, null);
if (songInfo.downloadDirectoryExists()) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{
R.attr.colorAccent});
downloadButton.setColorFilter(
styledAttributes.getColor(0,
getActivity().getResources().getColor(R.color.accent_default)));
styledAttributes.recycle();
} else {
downloadButton.clearColorFilter();
}
}
}
}
/**
* Returns the shared element if visible
* @return View if visible, null otherwise
*/
public View getSharedElement() {
if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) {
return mediaPoster;
}
return null;
}
/**
* Album details query parameters.
*/
public interface AlbumDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Albums.TITLE,
MediaContract.Albums.DISPLAYARTIST,
MediaContract.Albums.THUMBNAIL,
MediaContract.Albums.FANART,
MediaContract.Albums.YEAR,
MediaContract.Albums.GENRE,
MediaContract.Albums.ALBUMLABEL,
MediaContract.Albums.DESCRIPTION,
MediaContract.Albums.RATING,
};
int ID = 0;
int TITLE = 1;
int DISPLAYARTIST = 2;
int THUMBNAIL = 3;
int FANART = 4;
int YEAR = 5;
int GENRE = 6;
int ALBUMLABEL = 7;
int DESCRIPTION = 8;
int RATING = 9;
}
/**
* Album songs list query parameters.
*/
public interface AlbumSongsListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Songs.TITLE,
MediaContract.Songs.TRACK,
MediaContract.Songs.DURATION,
MediaContract.Songs.FILE,
MediaContract.Songs.SONGID,
MediaContract.Songs.DISPLAYARTIST,
MediaContract.Songs.DISC
};
String SORT = MediaContract.Songs.DISC + " ASC, " + MediaContract.Songs.TRACK + " ASC";
int ID = 0;
int TITLE = 1;
int TRACK = 2;
int DURATION = 3;
int FILE = 4;
int SONGID = 5;
int ARTIST = 6;
int DISC = 7;
}
}

View File

@ -0,0 +1,266 @@
/*
* 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.sections.audio;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.BaseColumns;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import java.util.ArrayList;
/**
* Presents album details
*/
public class AlbumInfoFragment extends AbstractInfoFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(AlbumInfoFragment.class);
private static final int LOADER_ALBUM = 0;
private Handler callbackHandler = new Handler();
private AlbumSongsListFragment albumSongsListFragment;
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(LOADER_ALBUM, null, this);
setHasOptionsMenu(false);
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
int albumId = getDataHolder().getId();
int id = HostManager.getInstance(getActivity()).getHostInfo().getId();
switch (i) {
case LOADER_ALBUM:
uri = MediaContract.Albums.buildAlbumUri(id, albumId);
return new CursorLoader(getActivity(), uri,
AlbumDetailsQuery.PROJECTION, null, null, null);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_ALBUM:
cursor.moveToFirst();
DataHolder dataHolder = getDataHolder();
dataHolder.setRating(cursor.getDouble(AlbumDetailsQuery.RATING));
dataHolder.setTitle(cursor.getString(AlbumDetailsQuery.TITLE));
dataHolder.setUndertitle(cursor.getString(AlbumDetailsQuery.DISPLAYARTIST));
dataHolder.setDescription(cursor.getString(AlbumDetailsQuery.DESCRIPTION));
dataHolder.setFanArtUrl(cursor.getString(AlbumInfoFragment.AlbumDetailsQuery.FANART));
dataHolder.setPosterUrl(cursor.getString(AlbumInfoFragment.AlbumDetailsQuery.THUMBNAIL));
int year = cursor.getInt(AlbumDetailsQuery.YEAR);
String genres = cursor.getString(AlbumDetailsQuery.GENRE);
dataHolder.setDetails ( (year > 0) ?
(!TextUtils.isEmpty(genres) ?
genres + " | " + String.valueOf(year) :
String.valueOf(year)) :
genres
);
FileDownloadHelper.SongInfo songInfo = new FileDownloadHelper.SongInfo
(dataHolder.getUnderTitle(), dataHolder.getTitle(), 0, 0, null, null);
setDownloadButtonState(songInfo.downloadDirectoryExists());
updateView(dataHolder);
break;
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
@Override
protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() {
DataHolder dataHolder = getDataHolder();
albumSongsListFragment = new AlbumSongsListFragment();
albumSongsListFragment.setAlbum(dataHolder.getId(), dataHolder.getTitle());
return albumSongsListFragment;
}
@Override
protected RefreshItem createRefreshItem() {
RefreshItem refreshItem = new RefreshItem(getActivity(), LibrarySyncService.SYNC_ALL_MUSIC);
refreshItem.setListener(new RefreshItem.RefreshItemListener() {
@Override
public void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS)
getLoaderManager().restartLoader(LOADER_ALBUM, null, AlbumInfoFragment.this);
}
});
return refreshItem;
}
@Override
protected boolean setupMediaActionBar() {
setOnDownloadListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
UIUtils.downloadSongs(getActivity(), albumSongsListFragment.getSongInfoList(),
getHostInfo(), callbackHandler);
}
});
setOnAddToPlaylistListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
addToPlaylist();
}
});
return true;
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.albumid = getDataHolder().getId();
fabActionPlayItem(item);
}
});
return true;
}
private void addToPlaylist() {
Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
getPlaylists.execute(HostManager.getInstance(getActivity()).getConnection(), new ApiCallback<ArrayList<PlaylistType.GetPlaylistsReturnType>>() {
@Override
public void onSuccess(ArrayList<PlaylistType.GetPlaylistsReturnType> result) {
if (!isAdded()) return;
// Ok, loop through the playlists, looking for the audio one
int audioPlaylistId = -1;
for (PlaylistType.GetPlaylistsReturnType playlist : result) {
if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.AUDIO)) {
audioPlaylistId = playlist.playlistid;
break;
}
}
// If found, add to playlist
if (audioPlaylistId != -1) {
PlaylistType.Item item = new PlaylistType.Item();
item.albumid = getDataHolder().getId();
Playlist.Add action = new Playlist.Add(audioPlaylistId, item);
action.execute(HostManager.getInstance(getActivity()).getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
} else {
Toast.makeText(getActivity(), R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
.show();
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
/**
* Album details query parameters.
*/
public interface AlbumDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Albums.TITLE,
MediaContract.Albums.DISPLAYARTIST,
MediaContract.Albums.THUMBNAIL,
MediaContract.Albums.FANART,
MediaContract.Albums.YEAR,
MediaContract.Albums.GENRE,
MediaContract.Albums.DESCRIPTION,
MediaContract.Albums.RATING,
};
int ID = 0;
int TITLE = 1;
int DISPLAYARTIST = 2;
int THUMBNAIL = 3;
int FANART = 4;
int YEAR = 5;
int GENRE = 6;
int DESCRIPTION = 7;
int RATING = 8;
}
}

View File

@ -47,6 +47,7 @@ import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.provider.MediaDatabase; import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractCursorListFragment; import org.xbmc.kore.ui.AbstractCursorListFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
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;
@ -59,7 +60,7 @@ public class AlbumListFragment extends AbstractCursorListFragment {
private static final String TAG = LogUtils.makeLogTag(AlbumListFragment.class); private static final String TAG = LogUtils.makeLogTag(AlbumListFragment.class);
public interface OnAlbumSelectedListener { public interface OnAlbumSelectedListener {
public void onAlbumSelected(ViewHolder vh); public void onAlbumSelected(ViewHolder viewHolder);
} }
public static final String BUNDLE_KEY_GENREID = "genreid", public static final String BUNDLE_KEY_GENREID = "genreid",
@ -75,27 +76,23 @@ public class AlbumListFragment extends AbstractCursorListFragment {
protected String getListSyncType() { return LibrarySyncService.SYNC_ALL_MUSIC; } protected String getListSyncType() { return LibrarySyncService.SYNC_ALL_MUSIC; }
/** /**
* Create a new instance of this, initialized to show albums of genres * Use this to display all albums for a specific artist
* @param artistId
*/ */
public static AlbumListFragment newInstanceForGenre(final int genreId) { public void setArtist(int artistId) {
AlbumListFragment fragment = new AlbumListFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putInt(BUNDLE_KEY_GENREID, genreId); args.putInt(BUNDLE_KEY_ARTISTID, artistId);
fragment.setArguments(args); setArguments(args);
return fragment;
} }
/** /**
* Create a new instance of this, initialized to show albums of artists * Use this to display all albums for a specific genre
* @param genreId
*/ */
public static AlbumListFragment newInstanceForArtist(final int artistId) { public void setGenre(int genreId) {
AlbumListFragment fragment = new AlbumListFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putInt(BUNDLE_KEY_ARTISTID, artistId); args.putInt(BUNDLE_KEY_GENREID, genreId);
fragment.setArguments(args); setArguments(args);
return fragment;
} }
@Override @Override
@ -204,7 +201,7 @@ public class AlbumListFragment extends AbstractCursorListFragment {
} }
return new CursorLoader(getActivity(), uri, return new CursorLoader(getActivity(), uri,
AlbumListQuery.PROJECTION, selection, selectionArgs, sortOrderStr); AlbumListQuery.PROJECTION, selection, selectionArgs, sortOrderStr);
} }
@Override @Override
@ -249,21 +246,21 @@ public class AlbumListFragment extends AbstractCursorListFragment {
MediaContract.Albums.THUMBNAIL, MediaContract.Albums.THUMBNAIL,
MediaContract.Albums.YEAR, MediaContract.Albums.YEAR,
MediaContract.Albums.RATING, MediaContract.Albums.RATING,
}; };
String SORT_BY_ALBUM = MediaDatabase.sortCommonTokens(MediaContract.Albums.TITLE) + " ASC"; String SORT_BY_ALBUM = MediaDatabase.sortCommonTokens(MediaContract.Albums.TITLE) + " ASC";
String SORT_BY_ARTIST = MediaDatabase.sortCommonTokens(MediaContract.Albums.DISPLAYARTIST) + " ASC"; String SORT_BY_ARTIST = MediaDatabase.sortCommonTokens(MediaContract.Albums.DISPLAYARTIST) + " ASC";
String SORT_BY_ARTIST_YEAR = MediaDatabase.sortCommonTokens(MediaContract.Albums.DISPLAYARTIST) String SORT_BY_ARTIST_YEAR = MediaDatabase.sortCommonTokens(MediaContract.Albums.DISPLAYARTIST)
+ " ASC, " + MediaContract.Albums.YEAR + " ASC"; + " ASC, " + MediaContract.Albums.YEAR + " ASC";
final int ID = 0; int ID = 0;
final int ALBUMID = 1; int ALBUMID = 1;
final int TITLE = 2; int TITLE = 2;
final int DISPLAYARTIST = 3; int DISPLAYARTIST = 3;
final int GENRE = 4; int GENRE = 4;
final int THUMBNAIL = 5; int THUMBNAIL = 5;
final int YEAR = 6; int YEAR = 6;
final int RATING = 7; int RATING = 7;
} }
private class AlbumsAdapter extends CursorAdapter { private class AlbumsAdapter extends CursorAdapter {
@ -279,8 +276,8 @@ public class AlbumListFragment extends AbstractCursorListFragment {
// Use the same dimensions as in the details fragment, so that it hits Picasso's cache when // Use the same dimensions as in the details fragment, so that it hits Picasso's cache when
// the user transitions to that fragment, avoiding another call and imediatelly showing the image // the user transitions to that fragment, avoiding another call and imediatelly showing the image
Resources resources = context.getResources(); Resources resources = context.getResources();
artWidth = (int) resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_width); artWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_square);
artHeight = (int) resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_heigth); artHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_square);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@ -306,34 +303,33 @@ public class AlbumListFragment extends AbstractCursorListFragment {
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, Cursor cursor) {
final ViewHolder viewHolder = (ViewHolder)view.getTag(); final ViewHolder viewHolder = (ViewHolder)view.getTag();
viewHolder.albumId = cursor.getInt(AlbumListQuery.ALBUMID); viewHolder.dataHolder.setId(cursor.getInt(AlbumListQuery.ALBUMID));
viewHolder.albumTitle = cursor.getString(AlbumListQuery.TITLE); viewHolder.dataHolder.setTitle(cursor.getString(AlbumListQuery.TITLE));
viewHolder.albumArtist = cursor.getString(AlbumListQuery.DISPLAYARTIST); viewHolder.dataHolder.setUndertitle(cursor.getString(AlbumListQuery.DISPLAYARTIST));
viewHolder.albumGenre = cursor.getString(AlbumListQuery.GENRE);
viewHolder.albumYear = cursor.getInt(AlbumListQuery.YEAR);
viewHolder.albumRating = cursor.getDouble(AlbumListQuery.RATING);
viewHolder.titleView.setText(viewHolder.albumTitle); viewHolder.titleView.setText(viewHolder.dataHolder.getTitle());
viewHolder.artistView.setText(cursor.getString(AlbumListQuery.DISPLAYARTIST)); viewHolder.artistView.setText(viewHolder.dataHolder.getUnderTitle());
int year = cursor.getInt(AlbumListQuery.YEAR); int year = cursor.getInt(AlbumListQuery.YEAR);
String genres = cursor.getString(AlbumListQuery.GENRE); String genres = cursor.getString(AlbumListQuery.GENRE);
String desc = (genres != null) ? String desc = (genres != null) ?
((year > 0) ? genres + " | " + year : genres) : ((year > 0) ? genres + " | " + year : genres) :
String.valueOf(year); String.valueOf(year);
viewHolder.dataHolder.setDescription(desc);
viewHolder.genresView.setText(desc); viewHolder.genresView.setText(desc);
String thumbnail = cursor.getString(AlbumListQuery.THUMBNAIL); viewHolder.dataHolder.setPosterUrl(cursor.getString(AlbumListQuery.THUMBNAIL));
UIUtils.loadImageWithCharacterAvatar(context, hostManager, UIUtils.loadImageWithCharacterAvatar(context, hostManager,
thumbnail, viewHolder.albumTitle, viewHolder.dataHolder.getPosterUrl(),
viewHolder.artView, artWidth, artHeight); viewHolder.dataHolder.getTitle(),
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(albumlistItemMenuClickListener); contextMenu.setOnClickListener(albumlistItemMenuClickListener);
if(Utils.isLollipopOrLater()) { if (Utils.isLollipopOrLater()) {
viewHolder.artView.setTransitionName("a"+viewHolder.albumId); viewHolder.artView.setTransitionName("al"+viewHolder.dataHolder.getId());
} }
} }
} }
@ -346,13 +342,7 @@ public class AlbumListFragment extends AbstractCursorListFragment {
TextView artistView; TextView artistView;
TextView genresView; TextView genresView;
ImageView artView; ImageView artView;
AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0);
int albumId;
String albumTitle;
String albumArtist;
int albumYear;
String albumGenre;
double albumRating;
} }
private View.OnClickListener albumlistItemMenuClickListener = new View.OnClickListener() { private View.OnClickListener albumlistItemMenuClickListener = new View.OnClickListener() {
@ -361,7 +351,7 @@ public class AlbumListFragment extends AbstractCursorListFragment {
final ViewHolder viewHolder = (ViewHolder)v.getTag(); final ViewHolder viewHolder = (ViewHolder)v.getTag();
final PlaylistType.Item playListItem = new PlaylistType.Item(); final PlaylistType.Item playListItem = new PlaylistType.Item();
playListItem.albumid = viewHolder.albumId; playListItem.albumid = viewHolder.dataHolder.getId();
final PopupMenu popupMenu = new PopupMenu(getActivity(), v); final PopupMenu popupMenu = new PopupMenu(getActivity(), v);
popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu()); popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu());

View File

@ -0,0 +1,418 @@
/*
* 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.sections.audio;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.BaseColumns;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.ApiMethod;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.MediaPlayerUtils;
import org.xbmc.kore.utils.UIUtils;
import java.util.ArrayList;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
/**
* Fragment that presents the songs list
*/
public class AlbumSongsListFragment extends AbstractAdditionalInfoFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(AlbumSongsListFragment.class);
public static final String BUNDLE_KEY_ALBUMID = "albumid";
public static final String BUNDLE_KEY_ALBUMTITLE = "albumtitle";
private static final int LOADER = 0;
private int albumId = -1;
private String albumTitle = "";
private Handler callbackHandler = new Handler();
private ArrayList<FileDownloadHelper.SongInfo> songInfoList;
/**
* Use this to display all songs for a specific album
* @param albumId
*/
public void setAlbum(int albumId, String albumTitle) {
Bundle args = new Bundle();
args.putInt(BUNDLE_KEY_ALBUMID, albumId);
args.putString(BUNDLE_KEY_ALBUMTITLE, albumTitle);
setArguments(args);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Bundle arguments = getArguments();
if (arguments != null) {
albumId = arguments.getInt(BUNDLE_KEY_ALBUMID, -1);
albumTitle = arguments.getString(BUNDLE_KEY_ALBUMTITLE, "");
}
getLoaderManager().initLoader(LOADER, null, this);
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
LinearLayout linearLayout = new LinearLayout(getActivity());
linearLayout.setOrientation(LinearLayout.VERTICAL);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT);
linearLayout.setLayoutParams(lp);
return linearLayout;
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri;
HostInfo hostInfo = HostManager.getInstance(getActivity()).getHostInfo();
int hostId = hostInfo != null ? hostInfo.getId() : -1;
uri = MediaContract.Songs.buildAlbumSongsListUri(hostId, albumId);
return new CursorLoader(getActivity(), uri, AlbumSongsListQuery.PROJECTION, null,
null, AlbumSongsListQuery.SORT);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (! data.moveToFirst()) {
Toast.makeText(getActivity(), R.string.no_songs_found_refresh,
Toast.LENGTH_SHORT).show();
return;
}
displaySongs(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
/**
* Returns all songs displayed by this fragment
* @return
*/
public ArrayList<FileDownloadHelper.SongInfo> getSongInfoList() {
return songInfoList;
}
private void displaySongs(Cursor cursor) {
songInfoList = new ArrayList<>(cursor.getCount());
LinearLayout listView = (LinearLayout) getView();
do {
View songView = LayoutInflater.from(getActivity())
.inflate(R.layout.list_item_song, listView, false);
TextView songTitle = (TextView)songView.findViewById(R.id.song_title);
TextView trackNumber = (TextView)songView.findViewById(R.id.track_number);
TextView details = (TextView)songView.findViewById(R.id.details);
ImageView contextMenu = (ImageView)songView.findViewById(R.id.list_context_menu);
String artist = cursor.getString(AlbumSongsListQuery.ARTIST);
// Add this song to the list
FileDownloadHelper.SongInfo songInfo = new FileDownloadHelper.SongInfo(
artist,
albumTitle,
cursor.getInt(AlbumSongsListQuery.SONGID),
cursor.getInt(AlbumSongsListQuery.TRACK),
cursor.getString(AlbumSongsListQuery.TITLE),
cursor.getString(AlbumSongsListQuery.FILE));
songInfoList.add(songInfo);
songTitle.setText(songInfo.title);
trackNumber.setText(String.valueOf(songInfo.track));
String duration = UIUtils.formatTime(cursor.getInt(AlbumSongsListQuery.DURATION));
String detailsText = TextUtils.isEmpty(artist) ? duration : duration + " | " + artist;
details.setText(detailsText);
contextMenu.setTag(songInfo);
contextMenu.setOnClickListener(songItemMenuClickListener);
songView.setTag(songInfo);
songView.setOnClickListener(songClickListener);
listView.addView(songView);
} while (cursor.moveToNext());
}
View.OnClickListener songClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
playSong(((FileDownloadHelper.SongInfo)v.getTag()).songId);
}
};
private ApiCallback<String> defaultStringActionCallback = ApiMethod.getDefaultActionCallback();
private void playSong(int songId) {
PlaylistType.Item item = new PlaylistType.Item();
item.songid = songId;
Player.Open action = new Player.Open(item);
action.execute(HostManager.getInstance(getActivity()).getConnection(),
defaultStringActionCallback, callbackHandler);
}
private View.OnClickListener songItemMenuClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
final FileDownloadHelper.SongInfo songInfo = ((FileDownloadHelper.SongInfo)v.getTag());
final int songId = songInfo.songId;
final PopupMenu popupMenu = new PopupMenu(getActivity(), v);
popupMenu.getMenuInflater().inflate(R.menu.song_item, popupMenu.getMenu());
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_play_song:
playSong(songId);
return true;
case R.id.action_add_to_playlist:
addToPlaylist(songId);
return true;
case R.id.download:
ArrayList<FileDownloadHelper.SongInfo> songInfoList = new ArrayList<>();
songInfoList.add(songInfo);
UIUtils.downloadSongs(getActivity(), songInfoList,
HostManager.getInstance(getActivity()).getHostInfo(), callbackHandler);
return true;
}
return false;
}
});
popupMenu.show();
}
};
private void addToPlaylist(final int id) {
Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
getPlaylists.execute(HostManager.getInstance(getActivity()).getConnection(), new ApiCallback<ArrayList<PlaylistType.GetPlaylistsReturnType>>() {
@Override
public void onSuccess(ArrayList<PlaylistType.GetPlaylistsReturnType> result) {
if (!isAdded()) return;
// Ok, loop through the playlists, looking for the audio one
int audioPlaylistId = -1;
for (PlaylistType.GetPlaylistsReturnType playlist : result) {
if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.AUDIO)) {
audioPlaylistId = playlist.playlistid;
break;
}
}
// If found, add to playlist
if (audioPlaylistId != -1) {
PlaylistType.Item item = new PlaylistType.Item();
item.songid = id;
Playlist.Add action = new Playlist.Add(audioPlaylistId, item);
action.execute(HostManager.getInstance(getActivity()).getConnection(),
new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
} else {
Toast.makeText(getActivity(), R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
.show();
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@Override
public void refresh() {
getLoaderManager().restartLoader(LOADER, null, this);
}
/**
* Album songs list query parameters.
*/
public interface AlbumSongsListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Songs.TITLE,
MediaContract.Songs.TRACK,
MediaContract.Songs.DURATION,
MediaContract.Songs.FILE,
MediaContract.Songs.SONGID,
MediaContract.Songs.DISPLAYARTIST,
MediaContract.Songs.DISC
};
String SORT = MediaContract.Songs.DISC + " ASC, " + MediaContract.Songs.TRACK + " ASC";
int ID = 0;
int TITLE = 1;
int TRACK = 2;
int DURATION = 3;
int FILE = 4;
int SONGID = 5;
int ARTIST = 6;
int DISC = 7;
}
private class AlbumSongsAdapter extends CursorAdapter {
public AlbumSongsAdapter(Context context) {
super(context, null, false);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
View view = LayoutInflater.from(context)
.inflate(R.layout.list_item_song, viewGroup, false);
// Setup View holder pattern
ViewHolder viewHolder = new ViewHolder();
viewHolder.trackNumber = (TextView)view.findViewById(R.id.track_number);
viewHolder.title = (TextView)view.findViewById(R.id.song_title);
viewHolder.details = (TextView)view.findViewById(R.id.details);
viewHolder.contextMenu = (ImageView)view.findViewById(R.id.list_context_menu);
viewHolder.songInfo = new FileDownloadHelper.SongInfo();
view.setTag(viewHolder);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
String artist = cursor.getString(AlbumSongsListQuery.ARTIST);
ViewHolder vh = (ViewHolder) view.getTag();
vh.title.setText(cursor.getString(AlbumSongsListQuery.TITLE));
vh.songInfo.artist = artist;
vh.songInfo.album = albumTitle;
vh.songInfo.songId = cursor.getInt(AlbumSongsListQuery.SONGID);
vh.songInfo.title = cursor.getString(AlbumSongsListQuery.TITLE);
vh.songInfo.fileName = cursor.getString(AlbumSongsListQuery.FILE);
vh.songInfo.track = cursor.getInt(AlbumSongsListQuery.TRACK);
vh.trackNumber.setText(String.valueOf(vh.songInfo.track));
String duration = UIUtils.formatTime(cursor.getInt(AlbumSongsListQuery.DURATION));
String detailsText = TextUtils.isEmpty(artist) ? duration : duration + " | " + artist;
vh.details.setText(detailsText);
vh.contextMenu.setTag(vh);
vh.contextMenu.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
showPopupMenu(v);
}
});
}
}
/**
* View holder pattern
*/
public static class ViewHolder {
TextView title;
TextView details;
TextView trackNumber;
ImageView contextMenu;
FileDownloadHelper.SongInfo songInfo;
}
private void showPopupMenu(View v) {
final ViewHolder viewHolder = (ViewHolder) v.getTag();
final PlaylistType.Item playListItem = new PlaylistType.Item();
final PopupMenu popupMenu = new PopupMenu(getActivity(), v);
popupMenu.getMenuInflater().inflate(R.menu.song_item, popupMenu.getMenu());
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_play_song:
MediaPlayerUtils.play(AlbumSongsListFragment.this, playListItem);
return true;
case R.id.action_add_to_playlist:
MediaPlayerUtils.queue(AlbumSongsListFragment.this, playListItem, PlaylistType.GetPlaylistsReturnType.AUDIO);
return true;
case R.id.download:
ArrayList<FileDownloadHelper.SongInfo> songInfoList = new ArrayList<>();
songInfoList.add(viewHolder.songInfo);
UIUtils.downloadSongs(getActivity(),
songInfoList,
HostManager.getInstance(getActivity()).getHostInfo(),
callbackHandler);
}
return false;
}
});
popupMenu.show();
}
}

View File

@ -15,108 +15,32 @@
*/ */
package org.xbmc.kore.ui.sections.audio; package org.xbmc.kore.ui.sections.audio;
import android.annotation.TargetApi;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.astuetz.PagerSlidingTabStrip;
import org.xbmc.kore.R; import org.xbmc.kore.R;
import org.xbmc.kore.ui.AbstractTabsFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.TabsAdapter; import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import butterknife.ButterKnife; public class ArtistDetailsFragment extends AbstractTabsFragment {
import butterknife.InjectView;
public class ArtistDetailsFragment extends Fragment {
private static final String TAG = LogUtils.makeLogTag(ArtistDetailsFragment.class); private static final String TAG = LogUtils.makeLogTag(ArtistDetailsFragment.class);
@InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip;
@InjectView(R.id.pager) ViewPager viewPager;
/**
* Create a new instance of this, initialized to show tvshowId
*/
@TargetApi(21)
public static ArtistDetailsFragment newInstance(ArtistListFragment.ViewHolder vh) {
ArtistDetailsFragment fragment = new ArtistDetailsFragment();
Bundle args = new Bundle();
args.putInt(ArtistOverviewFragment.BUNDLE_KEY_ARTISTID, vh.artistId);
args.putInt(AlbumListFragment.BUNDLE_KEY_ARTISTID, vh.artistId);
args.putString(ArtistOverviewFragment.BUNDLE_KEY_TITLE, vh.artistName);
args.putString(ArtistOverviewFragment.BUNDLE_KEY_FANART, vh.fanart);
args.putString(ArtistOverviewFragment.BUNDLE_KEY_DESCRIPTION, vh.description);
args.putString(ArtistOverviewFragment.BUNDLE_KEY_GENRE, vh.genres);
args.putString(ArtistOverviewFragment.BUNDLE_KEY_POSTER, vh.poster);
if( Utils.isLollipopOrLater()) {
args.putString(ArtistOverviewFragment.POSTER_TRANS_NAME, vh.artView.getTransitionName());
}
fragment.setArguments(args);
return fragment;
}
@Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { protected TabsAdapter createTabsAdapter(AbstractInfoFragment.DataHolder dataHolder) {
Bundle arguments = getArguments(); Bundle arguments = dataHolder.getBundle();
int id = arguments.getInt(ArtistOverviewFragment.BUNDLE_KEY_ARTISTID, -1); int itemId = dataHolder.getId();
long baseFragmentId = itemId * 10;
arguments.putInt(SongsListFragment.BUNDLE_KEY_ARTISTID, id); arguments.putInt(AlbumListFragment.BUNDLE_KEY_ARTISTID, itemId);
arguments.putInt(SongsListFragment.BUNDLE_KEY_ARTISTID, itemId);
if ((container == null) || (id == -1)) { return new TabsAdapter(getActivity(), getChildFragmentManager())
// We're not being shown or there's nothing to show .addTab(ArtistInfoFragment.class, arguments, R.string.info,
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_default_view_pager, container, false);
ButterKnife.inject(this, root);
long baseFragmentId = id * 10;
TabsAdapter tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager())
.addTab(ArtistOverviewFragment.class, arguments, R.string.info,
baseFragmentId) baseFragmentId)
.addTab(AlbumListFragment.class, arguments, .addTab(AlbumListFragment.class, arguments,
R.string.albums, baseFragmentId + 1) R.string.albums, baseFragmentId + 1)
.addTab(SongsListFragment.class, arguments, .addTab(SongsListFragment.class, arguments,
R.string.songs, baseFragmentId + 2); R.string.songs, baseFragmentId + 2);
viewPager.setAdapter(tabsAdapter);
pagerTabStrip.setViewPager(viewPager);
return root;
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false);
}
public View getSharedElement() {
View view = getView();
if (view == null)
return null;
//Note: this works as R.id.poster is only used in ArtistShowOverviewFragment.
//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;
} }
} }

View File

@ -0,0 +1,255 @@
/*
* 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.sections.audio;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.BaseColumns;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.View;
import android.widget.ImageButton;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.provider.MediaProvider;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.service.library.SyncMusic;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.MediaPlayerUtils;
import org.xbmc.kore.utils.UIUtils;
import java.util.ArrayList;
public class ArtistInfoFragment extends AbstractInfoFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(ArtistInfoFragment.class);
// Loader IDs
private static final int LOADER_ARTIST = 0,
LOADER_SONGS = 1;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
@Override
protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() {
return null;
}
@Override
protected RefreshItem createRefreshItem() {
RefreshItem refreshItem = new RefreshItem(getActivity(), LibrarySyncService.SYNC_ALL_MUSIC);
refreshItem.setListener(new RefreshItem.RefreshItemListener() {
@Override
public void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS)
getLoaderManager().restartLoader(LOADER_ARTIST, null, ArtistInfoFragment.this);
}
});
return refreshItem;
}
@Override
protected boolean setupMediaActionBar() {
setOnAddToPlaylistListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final PlaylistType.Item playListItem = new PlaylistType.Item();
playListItem.artistid = getDataHolder().getId();
MediaPlayerUtils.queue(ArtistInfoFragment.this, playListItem, PlaylistType.GetPlaylistsReturnType.AUDIO);
}
});
setOnDownloadListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
getLoaderManager().initLoader(LOADER_SONGS, null, ArtistInfoFragment.this);
}
});
return true;
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.artistid = getDataHolder().getId();
fabActionPlayItem(item);
}
});
return true;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setExpandDescription(true);
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(LOADER_ARTIST, null, this);
setHasOptionsMenu(false);
}
@Override
public void onPause() {
//Make sure loader is not reloaded for albums and songs when we return
//These loaders should only be activated by the user pressing the download button
getLoaderManager().destroyLoader(LOADER_SONGS);
super.onPause();
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_ARTIST:
uri = MediaContract.Artists.buildArtistUri(getHostInfo().getId(), getDataHolder().getId());
return new CursorLoader(getActivity(), uri,
DetailsQuery.PROJECTION, null, null, null);
case LOADER_SONGS:
uri = MediaContract.Songs.buildArtistSongsListUri(getHostInfo().getId(), getDataHolder().getId());
return new CursorLoader(getActivity(), uri,
SongsListQuery.PROJECTION, null, null, SongsListQuery.SORT);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_ARTIST:
cursor.moveToFirst();
FileDownloadHelper.SongInfo songInfo = new FileDownloadHelper.SongInfo(
cursor.getString(DetailsQuery.ARTIST),null, -1, -1, null, null);
setDownloadButtonState(songInfo.downloadDirectoryExists());
DataHolder dataHolder = getDataHolder();
dataHolder.setTitle(cursor.getString(DetailsQuery.ARTIST));
dataHolder.setUndertitle(cursor.getString(DetailsQuery.GENRE));
dataHolder.setDescription(cursor.getString(DetailsQuery.DESCRIPTION));
dataHolder.setPosterUrl(cursor.getString(DetailsQuery.THUMBNAIL));
dataHolder.setFanArtUrl(cursor.getString(DetailsQuery.FANART));
updateView(dataHolder);
break;
case LOADER_SONGS:
final ArrayList<FileDownloadHelper.SongInfo> songInfoList = new ArrayList<>(cursor.getCount());
if (cursor.moveToFirst()) {
do {
songInfoList.add(createSongInfo(cursor));
} while (cursor.moveToNext());
}
UIUtils.downloadSongs(getActivity(), songInfoList, getHostInfo(), callbackHandler);
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
private FileDownloadHelper.SongInfo createSongInfo(Cursor cursor) {
return new FileDownloadHelper.SongInfo(
cursor.getString(SongsListQuery.DISPLAYARTIST),
cursor.getString(SongsListQuery.ALBUMTITLE),
cursor.getInt(SongsListQuery.SONGID),
cursor.getInt(SongsListQuery.TRACK),
cursor.getString(SongsListQuery.TITLE),
cursor.getString(SongsListQuery.FILE));
}
private interface DetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Artists.ARTISTID,
MediaContract.Artists.ARTIST,
MediaContract.Artists.GENRE,
MediaContract.Artists.THUMBNAIL,
MediaContract.Artists.DESCRIPTION,
MediaContract.Artists.FANART
};
int ID = 0;
int ARTISTID = 1;
int ARTIST = 2;
int GENRE = 3;
int THUMBNAIL = 4;
int DESCRIPTION = 5;
int FANART = 6;
}
/**
* Song list query parameters.
*/
private interface SongsListQuery {
String[] PROJECTION = {
MediaDatabase.Tables.SONGS + "." + BaseColumns._ID,
MediaProvider.Qualified.SONGS_TITLE,
MediaProvider.Qualified.SONGS_TRACK,
MediaProvider.Qualified.SONGS_DURATION,
MediaProvider.Qualified.SONGS_FILE,
MediaProvider.Qualified.SONGS_SONGID,
MediaProvider.Qualified.SONGS_ALBUMID,
MediaProvider.Qualified.ALBUMS_TITLE,
MediaProvider.Qualified.SONGS_DISPLAYARTIST
};
String SORT = MediaContract.Songs.TRACK + " ASC";
int ID = 0;
int TITLE = 1;
int TRACK = 2;
int DURATION = 3;
int FILE = 4;
int SONGID = 5;
int ALBUMID = 6;
int ALBUMTITLE = 7;
int DISPLAYARTIST = 8;
}
}

View File

@ -41,6 +41,7 @@ import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.provider.MediaDatabase; import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractCursorListFragment; import org.xbmc.kore.ui.AbstractCursorListFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
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;
@ -145,8 +146,8 @@ public class ArtistListFragment extends AbstractCursorListFragment {
// Get the art dimensions // Get the art dimensions
Resources resources = context.getResources(); Resources resources = context.getResources();
artWidth = (int)(resources.getDimension(R.dimen.albumdetail_poster_width)); artWidth = (int)(resources.getDimension(R.dimen.detail_poster_width_square));
artHeight = (int)(resources.getDimension(R.dimen.albumdetail_poster_heigth)); artHeight = (int)(resources.getDimension(R.dimen.detail_poster_height_square));
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@ -160,8 +161,9 @@ public class ArtistListFragment extends AbstractCursorListFragment {
viewHolder.nameView = (TextView)view.findViewById(R.id.name); viewHolder.nameView = (TextView)view.findViewById(R.id.name);
viewHolder.genresView = (TextView)view.findViewById(R.id.genres); viewHolder.genresView = (TextView)view.findViewById(R.id.genres);
viewHolder.artView = (ImageView)view.findViewById(R.id.art); viewHolder.artView = (ImageView)view.findViewById(R.id.art);
viewHolder.contextMenu = (ImageView)view.findViewById(R.id.list_context_menu);
view.setTag(viewHolder); view.setTag(viewHolder);
return view; return view;
} }
@ -172,27 +174,25 @@ public class ArtistListFragment extends AbstractCursorListFragment {
final ViewHolder viewHolder = (ViewHolder)view.getTag(); final ViewHolder viewHolder = (ViewHolder)view.getTag();
// Save the movie id // Save the movie id
viewHolder.artistId = cursor.getInt(ArtistListQuery.ARTISTID); viewHolder.dataHolder.setId(cursor.getInt(ArtistListQuery.ARTISTID));
viewHolder.artistName = cursor.getString(ArtistListQuery.ARTIST); viewHolder.dataHolder.setTitle(cursor.getString(ArtistListQuery.ARTIST));
viewHolder.genres = cursor.getString(ArtistListQuery.GENRE); viewHolder.dataHolder.setUndertitle(cursor.getString(ArtistListQuery.GENRE));
viewHolder.description = cursor.getString(ArtistListQuery.DESCRIPTION); viewHolder.dataHolder.setDescription(cursor.getString(ArtistListQuery.DESCRIPTION));
viewHolder.fanart = cursor.getString(ArtistListQuery.FANART); viewHolder.dataHolder.setFanArtUrl(cursor.getString(ArtistListQuery.FANART));
viewHolder.nameView.setText(viewHolder.artistName); viewHolder.nameView.setText(cursor.getString(ArtistListQuery.ARTIST));
viewHolder.genresView.setText(cursor.getString(ArtistListQuery.GENRE)); viewHolder.genresView.setText(cursor.getString(ArtistListQuery.GENRE));
viewHolder.poster = cursor.getString(ArtistListQuery.THUMBNAIL); viewHolder.dataHolder.setPosterUrl(cursor.getString(ArtistListQuery.THUMBNAIL));
UIUtils.loadImageWithCharacterAvatar(context, hostManager, UIUtils.loadImageWithCharacterAvatar(context, hostManager,
viewHolder.poster, viewHolder.artistName, viewHolder.dataHolder.getPosterUrl(), viewHolder.dataHolder.getTitle(),
viewHolder.artView, artWidth, artHeight); viewHolder.artView, artWidth, artHeight);
// For the popupmenu viewHolder.contextMenu.setTag(viewHolder);
ImageView contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); viewHolder.contextMenu.setOnClickListener(artistlistItemMenuClickListener);
contextMenu.setTag(viewHolder);
contextMenu.setOnClickListener(artistlistItemMenuClickListener);
if(Utils.isLollipopOrLater()) { if (Utils.isLollipopOrLater()) {
viewHolder.artView.setTransitionName("a"+viewHolder.artistId); viewHolder.artView.setTransitionName("ar"+viewHolder.dataHolder.getId());
} }
} }
} }
@ -204,13 +204,9 @@ public class ArtistListFragment extends AbstractCursorListFragment {
TextView nameView; TextView nameView;
TextView genresView; TextView genresView;
ImageView artView; ImageView artView;
ImageView contextMenu;
int artistId; AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0);
String artistName;
String genres;
String description;
String fanart;
String poster;
} }
private View.OnClickListener artistlistItemMenuClickListener = new View.OnClickListener() { private View.OnClickListener artistlistItemMenuClickListener = new View.OnClickListener() {
@ -219,7 +215,7 @@ public class ArtistListFragment extends AbstractCursorListFragment {
final ViewHolder viewHolder = (ViewHolder)v.getTag(); final ViewHolder viewHolder = (ViewHolder)v.getTag();
final PlaylistType.Item playListItem = new PlaylistType.Item(); final PlaylistType.Item playListItem = new PlaylistType.Item();
playListItem.artistid = viewHolder.artistId; playListItem.artistid = viewHolder.dataHolder.getId();
final PopupMenu popupMenu = new PopupMenu(getActivity(), v); final PopupMenu popupMenu = new PopupMenu(getActivity(), v);
popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu()); popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu());

View File

@ -1,405 +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.sections.audio;
import android.annotation.TargetApi;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
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.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
import com.melnykov.fab.ObservableScrollView;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.provider.MediaProvider;
import org.xbmc.kore.ui.AbstractDetailsFragment;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.MediaPlayerUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.util.ArrayList;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
public class ArtistOverviewFragment extends AbstractDetailsFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(ArtistOverviewFragment.class);
public static final String BUNDLE_KEY_ARTISTID = "id";
public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME";
public static final String BUNDLE_KEY_TITLE = "title";
public static final String BUNDLE_KEY_GENRE = "genre";
public static final String BUNDLE_KEY_DESCRIPTION = "description";
public static final String BUNDLE_KEY_FANART = "fanart";
public static final String BUNDLE_KEY_POSTER = "poster";
// Loader IDs
private static final int LOADER_ARTIST = 0,
LOADER_SONGS = 1;
private HostManager hostManager;
private HostInfo hostInfo;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
private int artistId = -1;
private String artistTitle;
@InjectView(R.id.exit_transition_view) View exitTransitionView;
// Buttons
@InjectView(R.id.fab) ImageButton fabButton;
// 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.media_description) TextView mediaDescription;
@TargetApi(21)
@Override
protected View createView(LayoutInflater inflater, ViewGroup container) {
Bundle bundle = getArguments();
artistId = bundle.getInt(BUNDLE_KEY_ARTISTID, -1);
if ((container == null) || (artistId == -1)) {
// We're not being shown or there's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_artist_details, container, false);
ButterKnife.inject(this, root);
hostManager = HostManager.getInstance(getActivity());
hostInfo = hostManager.getHostInfo();
// 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);
}
});
((FloatingActionButton) fabButton).attachToScrollView((ObservableScrollView) mediaPanel);
if(Utils.isLollipopOrLater()) {
mediaPoster.setTransitionName(bundle.getString(POSTER_TRANS_NAME));
}
artistTitle = bundle.getString(BUNDLE_KEY_TITLE);
mediaTitle.setText(artistTitle);
mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_GENRE));
mediaDescription.setText(bundle.getString(BUNDLE_KEY_DESCRIPTION));
setArt(bundle.getString(BUNDLE_KEY_POSTER), bundle.getString(BUNDLE_KEY_FANART));
return root;
}
@Override
protected String getSyncType() {
return null;
}
@Override
protected String getSyncID() {
return null;
}
@Override
protected int getSyncItemID() {
return 0;
}
@Override
protected SwipeRefreshLayout getSwipeRefreshLayout() {
return null;
}
@Override
protected void onDownload() {
getLoaderManager().initLoader(LOADER_SONGS, null, this);
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false);
}
@Override
public void onResume() {
// Force the exit view to invisible
exitTransitionView.setVisibility(View.INVISIBLE);
super.onResume();
}
@Override
public void onPause() {
//Make sure loader is not reloaded for albums and songs when we return
//These loaders should only be activated by the user pressing the download button
getLoaderManager().destroyLoader(LOADER_SONGS);
super.onPause();
}
@Override
protected void onSyncProcessEnded(MediaSyncEvent event) {
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_ARTIST:
uri = MediaContract.Artists.buildArtistUri(hostInfo.getId(), artistId);
return new CursorLoader(getActivity(), uri,
DetailsQuery.PROJECTION, null, null, null);
case LOADER_SONGS:
uri = MediaContract.Songs.buildArtistSongsListUri(hostInfo.getId(), artistId);
return new CursorLoader(getActivity(), uri,
SongsListQuery.PROJECTION, null, null, SongsListQuery.SORT);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_ARTIST:
displayArtistDetails(cursor);
break;
case LOADER_SONGS:
downloadSongs(cursor);
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
@OnClick(R.id.fab)
public void onFabClicked(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.artistid = artistId;
Player.Open action = new Player.Open(item);
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Check whether we should switch to the remote
boolean switchToRemote = PreferenceManager
.getDefaultSharedPreferences(getActivity())
.getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
if (switchToRemote) {
int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.add_to_playlist)
public void onAddToPlaylistClicked(View v) {
final PlaylistType.Item playListItem = new PlaylistType.Item();
playListItem.artistid = artistId;
MediaPlayerUtils.queue(this, playListItem, PlaylistType.GetPlaylistsReturnType.AUDIO);
}
private FileDownloadHelper.SongInfo createSongInfo(Cursor cursor) {
return new FileDownloadHelper.SongInfo(
cursor.getString(SongsListQuery.DISPLAYARTIST),
cursor.getString(SongsListQuery.ALBUMTITLE),
cursor.getInt(SongsListQuery.SONGID),
cursor.getInt(SongsListQuery.TRACK),
cursor.getString(SongsListQuery.TITLE),
cursor.getString(SongsListQuery.FILE));
}
private void downloadSongs(Cursor cursor) {
DialogInterface.OnClickListener noopClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) { }
};
final ArrayList<FileDownloadHelper.SongInfo> songInfoList = new ArrayList<>(cursor.getCount());
if (cursor.moveToFirst()) {
do {
FileDownloadHelper.SongInfo songInfo = createSongInfo(cursor);
if (songInfo != null) {
songInfoList.add(songInfo);
}
} while (cursor.moveToNext());
}
UIUtils.downloadSongs(getActivity(), songInfoList, hostInfo, callbackHandler);
}
private void displayArtistDetails(Cursor cursor) {
cursor.moveToFirst();
artistTitle = cursor.getString(DetailsQuery.ARTIST);
mediaTitle.setText(artistTitle);
mediaUndertitle.setText(cursor.getString(DetailsQuery.GENRE));
mediaDescription.setText(cursor.getString(DetailsQuery.DESCRIPTION));
setArt(cursor.getString(DetailsQuery.THUMBNAIL), cursor.getString(DetailsQuery.FANART));
}
private void setArt(String poster, String fanart) {
final Resources resources = getActivity().getResources();
// Images
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height),
artWidth = displayMetrics.widthPixels;
int posterWidth = resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_width);
int posterHeight = resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_heigth);
UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager,
poster, artistTitle,
mediaPoster, posterWidth, posterHeight);
UIUtils.loadImageIntoImageview(hostManager,
TextUtils.isEmpty(fanart) ? poster : fanart,
mediaArt, artWidth, artHeight);
}
/**
* Returns the shared element if visible
* @return View if visible, null otherwise
*/
public View getSharedElement() {
if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) {
return mediaPoster;
}
return null;
}
private interface DetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Artists.ARTISTID,
MediaContract.Artists.ARTIST,
MediaContract.Artists.GENRE,
MediaContract.Artists.THUMBNAIL,
MediaContract.Artists.DESCRIPTION,
MediaContract.Artists.FANART
};
int ID = 0;
int ARTISTID = 1;
int ARTIST = 2;
int GENRE = 3;
int THUMBNAIL = 4;
int DESCRIPTION = 5;
int FANART = 6;
}
/**
* Song list query parameters.
*/
private interface SongsListQuery {
String[] PROJECTION = {
MediaDatabase.Tables.SONGS + "." + BaseColumns._ID,
MediaProvider.Qualified.SONGS_TITLE,
MediaProvider.Qualified.SONGS_TRACK,
MediaProvider.Qualified.SONGS_DURATION,
MediaProvider.Qualified.SONGS_FILE,
MediaProvider.Qualified.SONGS_SONGID,
MediaProvider.Qualified.SONGS_ALBUMID,
MediaProvider.Qualified.ALBUMS_TITLE,
MediaProvider.Qualified.SONGS_DISPLAYARTIST
};
String SORT = MediaContract.Songs.TRACK + " ASC";
int ID = 0;
int TITLE = 1;
int TRACK = 2;
int DURATION = 3;
int FILE = 4;
int SONGID = 5;
int ALBUMID = 6;
int ALBUMTITLE = 7;
int DISPLAYARTIST = 8;
}
}

View File

@ -18,36 +18,32 @@ package org.xbmc.kore.ui.sections.audio;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.SharedElementCallback;
import android.support.v4.widget.DrawerLayout; import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View;
import org.xbmc.kore.R; import org.xbmc.kore.R;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.BaseActivity; import org.xbmc.kore.ui.BaseActivity;
import org.xbmc.kore.ui.generic.NavigationDrawerFragment; import org.xbmc.kore.ui.generic.NavigationDrawerFragment;
import org.xbmc.kore.ui.sections.remote.RemoteActivity; import org.xbmc.kore.ui.sections.remote.RemoteActivity;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.SharedElementTransition;
import org.xbmc.kore.utils.Utils; import org.xbmc.kore.utils.Utils;
import java.util.List;
import java.util.Map;
/** /**
* Controls the presentation of Music information (list, details) * Controls the presentation of Music information (list, details)
* All the information is presented by specific fragments * All the information is presented by specific fragments
*/ */
public class MusicActivity extends BaseActivity public class MusicActivity extends BaseActivity
implements ArtistListFragment.OnArtistSelectedListener, implements ArtistListFragment.OnArtistSelectedListener,
AlbumListFragment.OnAlbumSelectedListener, AlbumListFragment.OnAlbumSelectedListener,
AudioGenresListFragment.OnAudioGenreSelectedListener, AudioGenresListFragment.OnAudioGenreSelectedListener,
MusicVideoListFragment.OnMusicVideoSelectedListener { MusicVideoListFragment.OnMusicVideoSelectedListener {
private static final String TAG = LogUtils.makeLogTag(MusicActivity.class); private static final String TAG = LogUtils.makeLogTag(MusicActivity.class);
public static final String ALBUMID = "album_id"; public static final String ALBUMID = "album_id";
@ -58,6 +54,7 @@ public class MusicActivity extends BaseActivity
public static final String GENRETITLE = "genre_title"; public static final String GENRETITLE = "genre_title";
public static final String MUSICVIDEOID = "music_video_id"; public static final String MUSICVIDEOID = "music_video_id";
public static final String MUSICVIDEOTITLE = "music_video_title"; public static final String MUSICVIDEOTITLE = "music_video_title";
public static final String LISTFRAGMENT_TAG = "musiclist";
private int selectedAlbumId = -1; private int selectedAlbumId = -1;
private int selectedArtistId = -1; private int selectedArtistId = -1;
@ -70,9 +67,8 @@ public class MusicActivity extends BaseActivity
private NavigationDrawerFragment navigationDrawerFragment; private NavigationDrawerFragment navigationDrawerFragment;
private MusicListFragment musicListFragment; private SharedElementTransition sharedElementTransition = new SharedElementTransition();
private boolean clearSharedElements;
@TargetApi(21) @TargetApi(21)
@Override @Override
@ -85,34 +81,17 @@ public class MusicActivity extends BaseActivity
.findFragmentById(R.id.navigation_drawer); .findFragmentById(R.id.navigation_drawer);
navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout)); navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout));
Fragment fragment;
if (savedInstanceState == null) { if (savedInstanceState == null) {
musicListFragment = new MusicListFragment(); fragment = new MusicListFragment();
// Setup animations
if (Utils.isLollipopOrLater()) {
musicListFragment.setExitTransition(null);
musicListFragment.setReenterTransition(TransitionInflater
.from(this)
.inflateTransition(android.R.transition.fade));
musicListFragment.setExitSharedElementCallback(new SharedElementCallback() {
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
// Clearing must be done in the reentering fragment
// as this is called last. Otherwise, the app will crash during transition setup. Not sure, but might
// be a v4 support package bug.
if (clearSharedElements) {
names.clear();
sharedElements.clear();
clearSharedElements = false;
}
}
});
}
getSupportFragmentManager() getSupportFragmentManager()
.beginTransaction() .beginTransaction()
.add(R.id.fragment_container, musicListFragment) .add(R.id.fragment_container, fragment, LISTFRAGMENT_TAG)
.commit(); .commit();
} else { } else {
fragment = getSupportFragmentManager().findFragmentByTag(LISTFRAGMENT_TAG);
selectedAlbumId = savedInstanceState.getInt(ALBUMID, -1); selectedAlbumId = savedInstanceState.getInt(ALBUMID, -1);
selectedArtistId = savedInstanceState.getInt(ARTISTID, -1); selectedArtistId = savedInstanceState.getInt(ARTISTID, -1);
selectedGenreId = savedInstanceState.getInt(GENREID, -1); selectedGenreId = savedInstanceState.getInt(GENREID, -1);
@ -123,6 +102,10 @@ public class MusicActivity extends BaseActivity
selectedMusicVideoTitle = savedInstanceState.getString(MUSICVIDEOTITLE, null); selectedMusicVideoTitle = savedInstanceState.getString(MUSICVIDEOTITLE, null);
} }
if (Utils.isLollipopOrLater()) {
sharedElementTransition.setupExitTransition(this, fragment);
}
setupActionBar(selectedAlbumTitle, selectedArtistName, selectedGenreTitle, selectedMusicVideoTitle); setupActionBar(selectedAlbumTitle, selectedArtistName, selectedGenreTitle, selectedMusicVideoTitle);
// // Setup system bars and content padding, allowing averlap with the bottom bar // // Setup system bars and content padding, allowing averlap with the bottom bar
@ -131,16 +114,6 @@ public class MusicActivity extends BaseActivity
// UIUtils.setPaddingForSystemBars(this, findViewById(R.id.drawer_layout), true, true, true); // UIUtils.setPaddingForSystemBars(this, findViewById(R.id.drawer_layout), true, true, true);
} }
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override @Override
protected void onSaveInstanceState (Bundle outState) { protected void onSaveInstanceState (Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
@ -270,44 +243,28 @@ public class MusicActivity extends BaseActivity
} }
@TargetApi(21) @TargetApi(21)
public void onArtistSelected(ArtistListFragment.ViewHolder viewHolder) { public void onArtistSelected(ArtistListFragment.ViewHolder vh) {
selectedArtistId = viewHolder.artistId; selectedArtistId = vh.dataHolder.getId();
selectedArtistName = viewHolder.artistName; selectedArtistName = vh.dataHolder.getTitle();
// Replace list fragment // Replace list fragment
final ArtistDetailsFragment artistDetailsFragment = ArtistDetailsFragment.newInstance(viewHolder); final ArtistDetailsFragment artistDetailsFragment = new ArtistDetailsFragment();
artistDetailsFragment.setDataHolder(vh.dataHolder);
vh.dataHolder.setSquarePoster(true);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Setup animations // Setup animations
if (Utils.isLollipopOrLater()) { if (Utils.isLollipopOrLater()) {
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName());
@Override sharedElementTransition.setupEnterTransition(this, fragTrans, artistDetailsFragment,
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { vh.artView);
View sharedView = artistDetailsFragment.getSharedElement();
if (sharedView == null) { // shared element not visible
clearSharedElements = true;
}
}
};
artistDetailsFragment.setEnterSharedElementCallback(seCallback);
artistDetailsFragment.setEnterTransition(TransitionInflater
.from(this)
.inflateTransition(R.transition.media_details));
artistDetailsFragment.setReturnTransition(null);
Transition changeImageTransition = TransitionInflater.from(
this).inflateTransition(R.transition.change_image);
artistDetailsFragment.setSharedElementReturnTransition(changeImageTransition);
artistDetailsFragment.setSharedElementEnterTransition(changeImageTransition);
fragTrans.addSharedElement(viewHolder.artView, viewHolder.artView.getTransitionName());
} else { } else {
fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0); fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0);
} }
fragTrans.replace(R.id.fragment_container, artistDetailsFragment) fragTrans.replace(R.id.fragment_container, artistDetailsFragment)
.addToBackStack(null) .addToBackStack(null)
.commit(); .commit();
navigationDrawerFragment.animateDrawerToggle(true); navigationDrawerFragment.animateDrawerToggle(true);
setupActionBar(null, selectedArtistName, null, null); setupActionBar(null, selectedArtistName, null, null);
@ -315,52 +272,27 @@ public class MusicActivity extends BaseActivity
@TargetApi(21) @TargetApi(21)
public void onAlbumSelected(AlbumListFragment.ViewHolder vh) { public void onAlbumSelected(AlbumListFragment.ViewHolder vh) {
selectedAlbumId = vh.albumId; selectedAlbumId = vh.dataHolder.getId();
selectedAlbumTitle = vh.albumTitle; selectedAlbumTitle = vh.dataHolder.getTitle();
// Replace list fragment // Replace list fragment
final AlbumDetailsFragment albumDetailsFragment = AlbumDetailsFragment.newInstance(vh); final AbstractInfoFragment albumInfoFragment = new AlbumInfoFragment();
vh.dataHolder.setSquarePoster(true);
albumInfoFragment.setDataHolder(vh.dataHolder);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions // Set up transitions
if (Utils.isLollipopOrLater()) { if (Utils.isLollipopOrLater()) {
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName());
@Override sharedElementTransition.setupEnterTransition(this, fragTrans, albumInfoFragment, vh.artView);
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
//On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements
// for the reentering fragment. We use this to determine if we are returning and if
// we should clear the shared element lists. Note that, clearing must be done in the reentering fragment
// as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might
// be a v4 support package bug.
if (albumDetailsFragment.isVisible()) {
View sharedView = albumDetailsFragment.getSharedElement();
if (sharedView == null) { // shared element not visible
clearSharedElements = true;
}
}
}
};
albumDetailsFragment.setEnterSharedElementCallback(seCallback);
albumDetailsFragment.setEnterTransition(TransitionInflater
.from(this)
.inflateTransition(R.transition.media_details));
albumDetailsFragment.setReturnTransition(null);
Transition changeImageTransition = TransitionInflater.from(
this).inflateTransition(R.transition.change_image);
albumDetailsFragment.setSharedElementReturnTransition(changeImageTransition);
albumDetailsFragment.setSharedElementEnterTransition(changeImageTransition);
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, albumDetailsFragment) fragTrans.replace(R.id.fragment_container, albumInfoFragment)
.addToBackStack(null) .addToBackStack(null)
.commit(); .commit();
setupActionBar(selectedAlbumTitle, null, null, null); setupActionBar(selectedAlbumTitle, null, null, null);
} }
@ -369,7 +301,9 @@ public class MusicActivity extends BaseActivity
selectedGenreTitle = genreTitle; selectedGenreTitle = genreTitle;
// Replace list fragment // Replace list fragment
AlbumListFragment albumListFragment = AlbumListFragment.newInstanceForGenre(genreId); AlbumListFragment albumListFragment = new AlbumListFragment();
albumListFragment.setGenre(genreId);
getSupportFragmentManager() getSupportFragmentManager()
.beginTransaction() .beginTransaction()
.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0) .setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0)
@ -381,53 +315,28 @@ public class MusicActivity extends BaseActivity
@TargetApi(21) @TargetApi(21)
public void onMusicVideoSelected(MusicVideoListFragment.ViewHolder vh) { public void onMusicVideoSelected(MusicVideoListFragment.ViewHolder vh) {
selectedMusicVideoId = vh.musicVideoId; selectedMusicVideoId = vh.dataHolder.getId();
selectedMusicVideoTitle = vh.musicVideoTitle; selectedMusicVideoTitle = vh.dataHolder.getTitle();
// Replace list fragment // Replace list fragment
final MusicVideoDetailsFragment detailsFragment = MusicVideoDetailsFragment.newInstance(vh); final MusicVideoInfoFragment musicVideoInfoFragment = new MusicVideoInfoFragment();
vh.dataHolder.setSquarePoster(true);
musicVideoInfoFragment.setDataHolder(vh.dataHolder);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions // Set up transitions
if (Utils.isLollipopOrLater()) { if (Utils.isLollipopOrLater()) {
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName());
@Override sharedElementTransition.setupEnterTransition(this, fragTrans, musicVideoInfoFragment, vh.artView);
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
//On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements
// for the reentering fragment. We use this to determine if we are returning and if
// we should clear the shared element lists. Note that, clearing must be done in the reentering fragment
// as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might
// be a v4 support package bug.
if (detailsFragment.isVisible()) {
View sharedView = detailsFragment.getSharedElement();
if (sharedView == null) { // shared element not visible
LogUtils.LOGD(TAG, "onMusicVideoSelected: setting clearedSharedElements to true");
clearSharedElements = true;
}
}
}
};
detailsFragment.setEnterSharedElementCallback(seCallback);
detailsFragment.setEnterTransition(TransitionInflater
.from(this)
.inflateTransition(R.transition.media_details));
detailsFragment.setReturnTransition(null);
Transition changeImageTransition = TransitionInflater.from(
this).inflateTransition(R.transition.change_image);
detailsFragment.setSharedElementReturnTransition(changeImageTransition);
detailsFragment.setSharedElementEnterTransition(changeImageTransition);
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, detailsFragment) fragTrans.replace(R.id.fragment_container, musicVideoInfoFragment)
.addToBackStack(null) .addToBackStack(null)
.commit(); .commit();
setupActionBar(null, null, null, selectedMusicVideoTitle); setupActionBar(null, null, null, selectedMusicVideoTitle);
} }
} }

View File

@ -1,519 +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.sections.audio;
import android.annotation.TargetApi;
import android.content.DialogInterface;
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.Handler;
import android.preference.PreferenceManager;
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.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
import com.melnykov.fab.ObservableScrollView;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractDetailsFragment;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.io.File;
import java.util.ArrayList;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
/**
* Presents music videos details
*/
public class MusicVideoDetailsFragment extends AbstractDetailsFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(MusicVideoDetailsFragment.class);
public static final String BUNDLE_KEY_ID = "music_video_id";
public static final String BUNDLE_KEY_ALBUM = "album";
public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME";
public static final String BUNDLE_KEY_ARTIST = "artist";
public static final String BUNDLE_KEY_TITLE = "title";
public static final String BUNDLE_KEY_GENRES = "genre";
public static final String BUNDLE_KEY_YEAR = "year";
public static final String BUNDLE_KEY_PLOT = "plot";
public static final String BUNDLE_KEY_RUNTIME = "runtime";
// Loader IDs
private static final int LOADER_MUSIC_VIDEO = 0;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
// Displayed music video id
private int musicVideoId = -1;
// Info for downloading the music video
private FileDownloadHelper.MusicVideoInfo musicVideoDownloadInfo = null;
// Controls whether the finished refreshing message is shown
private boolean showRefreshStatusMessage = true;
@InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;
@InjectView(R.id.exit_transition_view) View exitTransitionView;
// Buttons
@InjectView(R.id.fab) ImageButton fabButton;
@InjectView(R.id.add_to_playlist) ImageButton addToPlaylistButton;
@InjectView(R.id.download) ImageButton downloadButton;
// 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.year) TextView mediaYear;
@InjectView(R.id.genres) TextView mediaGenres;
@InjectView(R.id.media_description) TextView mediaDescription;
/**
* Create a new instance of this, initialized to show the video musicVideoId
*/
@TargetApi(21)
public static MusicVideoDetailsFragment newInstance(MusicVideoListFragment.ViewHolder vh) {
MusicVideoDetailsFragment fragment = new MusicVideoDetailsFragment();
Bundle args = new Bundle();
args.putInt(BUNDLE_KEY_ID, vh.musicVideoId);
args.putString(BUNDLE_KEY_TITLE, vh.musicVideoTitle);
args.putString(BUNDLE_KEY_ALBUM, vh.album);
args.putString(BUNDLE_KEY_ARTIST, vh.artist);
args.putString(BUNDLE_KEY_GENRES, vh.genres);
args.putString(BUNDLE_KEY_PLOT, vh.plot);
args.putInt(BUNDLE_KEY_RUNTIME, vh.runtime);
args.putInt(BUNDLE_KEY_YEAR, vh.year);
if( Utils.isLollipopOrLater()) {
args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName());
}
fragment.setArguments(args);
return fragment;
}
@TargetApi(21)
@Override
protected View createView(LayoutInflater inflater, ViewGroup container) {
Bundle bundle = getArguments();
musicVideoId = bundle.getInt(BUNDLE_KEY_ID, -1);
if (musicVideoId == -1) {
// There's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_music_video_details, 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);
}
});
FloatingActionButton fab = (FloatingActionButton)fabButton;
fab.attachToScrollView((ObservableScrollView) mediaPanel);
mediaTitle.setText(bundle.getString(BUNDLE_KEY_TITLE));
setMediaUndertitle(bundle.getString(BUNDLE_KEY_ARTIST), bundle.getString(BUNDLE_KEY_ALBUM));
setMediaYear(bundle.getInt(BUNDLE_KEY_RUNTIME), bundle.getInt(BUNDLE_KEY_YEAR));
mediaGenres.setText(bundle.getString(BUNDLE_KEY_GENRES));
mediaDescription.setText(bundle.getString(BUNDLE_KEY_PLOT));
if(Utils.isLollipopOrLater()) {
mediaPoster.setTransitionName(bundle.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;
}
@Override
protected String getSyncType() {
return LibrarySyncService.SYNC_ALL_MUSIC_VIDEOS;
}
@Override
protected String getSyncID() {
return null;
}
@Override
protected int getSyncItemID() {
return -1;
}
@Override
protected SwipeRefreshLayout getSwipeRefreshLayout() {
return swipeRefreshLayout;
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Start the loaders
getLoaderManager().initLoader(LOADER_MUSIC_VIDEO, null, this);
}
@Override
public void onResume() {
// Force the exit view to invisible
exitTransitionView.setVisibility(View.INVISIBLE);
super.onResume();
}
@Override
protected void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_MUSIC_VIDEO, null, this);
}
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_MUSIC_VIDEO:
uri = MediaContract.MusicVideos.buildMusicVideoUri(getHostInfo().getId(), musicVideoId);
return new CursorLoader(getActivity(), uri,
MusicVideoDetailsQuery.PROJECTION, null, null, null);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_MUSIC_VIDEO:
displayMusicVideoDetails(cursor);
break;
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
/**
* Callbacks for button bar
*/
@OnClick(R.id.fab)
public void onFabClicked(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.musicvideoid = musicVideoId;
Player.Open action = new Player.Open(item);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Check whether we should switch to the remote
boolean switchToRemote = PreferenceManager
.getDefaultSharedPreferences(getActivity())
.getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
if (switchToRemote) {
int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.add_to_playlist)
public void onAddToPlaylistClicked(View v) {
Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
getPlaylists.execute(getHostManager().getConnection(), new ApiCallback<ArrayList<PlaylistType.GetPlaylistsReturnType>>() {
@Override
public void onSuccess(ArrayList<PlaylistType.GetPlaylistsReturnType> result) {
if (!isAdded()) return;
// Ok, loop through the playlists, looking for the video one
int videoPlaylistId = -1;
for (PlaylistType.GetPlaylistsReturnType playlist : result) {
if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.VIDEO)) {
videoPlaylistId = playlist.playlistid;
break;
}
}
// If found, add to playlist
if (videoPlaylistId != -1) {
PlaylistType.Item item = new PlaylistType.Item();
item.musicvideoid = musicVideoId;
Playlist.Add action = new Playlist.Add(videoPlaylistId, item);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
} else {
Toast.makeText(getActivity(), R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
.show();
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@Override
protected void onDownload() {
if (musicVideoDownloadInfo == null) {
// Nothing to download
Toast.makeText(getActivity(), R.string.no_files_to_download, Toast.LENGTH_SHORT).show();
return;
}
// Check if the directory exists and whether to overwrite it
File file = new File(musicVideoDownloadInfo.getAbsoluteFilePath());
if (file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.download_file_exists)
.setPositiveButton(R.string.overwrite,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
musicVideoDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNeutralButton(R.string.download_with_new_name,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
musicVideoDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Nothing to do
}
})
.show();
} else {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
musicVideoDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
}
/**
* Display the video details
*
* @param cursor Cursor with the data
*/
private void displayMusicVideoDetails(Cursor cursor) {
cursor.moveToFirst();
String musicVideoTitle = cursor.getString(MusicVideoDetailsQuery.TITLE);
mediaTitle.setText(musicVideoTitle);
setMediaUndertitle(cursor.getString(MusicVideoDetailsQuery.ARTIST), cursor.getString(MusicVideoDetailsQuery.ALBUM));
setMediaYear(cursor.getInt(MusicVideoDetailsQuery.RUNTIME), cursor.getInt(MusicVideoDetailsQuery.YEAR));
mediaGenres.setText(cursor.getString(MusicVideoDetailsQuery.GENRES));
mediaDescription.setText(cursor.getString(MusicVideoDetailsQuery.PLOT));
// Images
Resources resources = getActivity().getResources();
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
String fanart = cursor.getString(MusicVideoDetailsQuery.FANART),
poster = cursor.getString(MusicVideoDetailsQuery.THUMBNAIL);
int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height),
artWidth = displayMetrics.widthPixels;
int posterWidth = resources.getDimensionPixelOffset(R.dimen.musicvideodetail_poster_width);
int posterHeight = resources.getDimensionPixelOffset(R.dimen.musicvideodetail_poster_width);
UIUtils.loadImageWithCharacterAvatar(getActivity(), getHostManager(),
poster, musicVideoTitle,
mediaPoster, posterWidth, posterHeight);
UIUtils.loadImageIntoImageview(getHostManager(),
TextUtils.isEmpty(fanart)? poster : fanart,
mediaArt, artWidth, artHeight);
// Setup download info
musicVideoDownloadInfo = new FileDownloadHelper.MusicVideoInfo(
musicVideoTitle, cursor.getString(MusicVideoDetailsQuery.FILE));
// Check if downloaded file exists
if (musicVideoDownloadInfo.downloadFileExists()) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{
R.attr.colorAccent});
downloadButton.setColorFilter(
styledAttributes.getColor(0,
getActivity().getResources().getColor(R.color.accent_default)));
styledAttributes.recycle();
} else {
downloadButton.clearColorFilter();
}
}
private void setMediaUndertitle(String artist, String album) {
mediaUndertitle.setText(artist + " | " + album);
}
private void setMediaYear(int runtime, int year) {
String durationYear = runtime > 0 ?
UIUtils.formatTime(runtime) + " | " +
String.valueOf(year) :
String.valueOf(year);
mediaYear.setText(durationYear);
}
/**
* Returns the shared element if visible
* @return View if visible, null otherwise
*/
public View getSharedElement() {
if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) {
return mediaPoster;
}
return null;
}
/**
* Video details query parameters.
*/
private interface MusicVideoDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.MusicVideos.TITLE,
MediaContract.MusicVideos.ALBUM,
MediaContract.MusicVideos.ARTIST,
MediaContract.MusicVideos.THUMBNAIL,
MediaContract.MusicVideos.FANART,
MediaContract.MusicVideos.YEAR,
MediaContract.MusicVideos.GENRES,
MediaContract.MusicVideos.RUNTIME,
MediaContract.MusicVideos.PLOT,
MediaContract.MusicVideos.FILE,
};
final int ID = 0;
final int TITLE = 1;
final int ALBUM = 2;
final int ARTIST = 3;
final int THUMBNAIL =4;
final int FANART = 5;
final int YEAR = 6;
final int GENRES = 7;
final int RUNTIME = 8;
final int PLOT = 9;
final int FILE = 10;
}
}

View File

@ -0,0 +1,322 @@
/*
* 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.sections.audio;
import android.content.DialogInterface;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.BaseColumns;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import java.io.File;
import java.util.ArrayList;
/**
* Presents music videos details
*/
public class MusicVideoInfoFragment extends AbstractInfoFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(MusicVideoInfoFragment.class);
// Loader IDs
private static final int LOADER_MUSIC_VIDEO = 0;
// /**
// * Handler on which to post RPC callbacks
// */
private Handler callbackHandler = new Handler();
private Cursor cursor;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setExpandDescription(true);
}
@Override
protected RefreshItem createRefreshItem() {
RefreshItem refreshItem = new RefreshItem(getActivity(),
LibrarySyncService.SYNC_ALL_MUSIC_VIDEOS);
refreshItem.setListener(new RefreshItem.RefreshItemListener() {
@Override
public void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_MUSIC_VIDEO, null,
MusicVideoInfoFragment.this);
}
}
});
return refreshItem;
}
@Override
protected boolean setupMediaActionBar() {
setOnAddToPlaylistListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
addToPlaylist(getDataHolder().getId());
}
});
setOnDownloadListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
download();
}
});
return true;
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.musicvideoid = getDataHolder().getId();
fabActionPlayItem(item);
}
});
return true;
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Start the loaders
getLoaderManager().initLoader(LOADER_MUSIC_VIDEO, null, this);
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_MUSIC_VIDEO:
uri = MediaContract.MusicVideos.buildMusicVideoUri(getHostInfo().getId(),
getDataHolder().getId());
return new CursorLoader(getActivity(), uri,
MusicVideoDetailsQuery.PROJECTION, null, null, null);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_MUSIC_VIDEO:
this.cursor = cursor;
cursor.moveToFirst();
DataHolder dataHolder = getDataHolder();
dataHolder.setFanArtUrl(cursor.getString(MusicVideoDetailsQuery.FANART));
dataHolder.setPosterUrl(cursor.getString(MusicVideoDetailsQuery.THUMBNAIL));
int runtime = cursor.getInt(MusicVideoDetailsQuery.RUNTIME);
int year = cursor.getInt(MusicVideoDetailsQuery.YEAR);
String details = runtime > 0 ?
UIUtils.formatTime(runtime) + " | " +
String.valueOf(year) :
String.valueOf(year);
dataHolder.setDetails(details + "\n" + cursor.getString(MusicVideoDetailsQuery.GENRES));
dataHolder.setTitle(cursor.getString(MusicVideoDetailsQuery.TITLE));
dataHolder.setUndertitle(cursor.getString(MusicVideoDetailsQuery.ARTIST)
+ " | " +
cursor.getString(MusicVideoDetailsQuery.ALBUM));
dataHolder.setDescription(cursor.getString(MusicVideoDetailsQuery.PLOT));
FileDownloadHelper.MusicVideoInfo musicVideoDownloadInfo = new FileDownloadHelper.MusicVideoInfo(
dataHolder.getTitle(), cursor.getString(MusicVideoDetailsQuery.FILE));
setDownloadButtonState(musicVideoDownloadInfo.downloadFileExists());
updateView(dataHolder);
break;
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
public void addToPlaylist(final int itemId) {
Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
getPlaylists.execute(getHostManager().getConnection(), new ApiCallback<ArrayList<PlaylistType.GetPlaylistsReturnType>>() {
@Override
public void onSuccess(ArrayList<PlaylistType.GetPlaylistsReturnType> result) {
if (!isAdded()) return;
// Ok, loop through the playlists, looking for the video one
int videoPlaylistId = -1;
for (PlaylistType.GetPlaylistsReturnType playlist : result) {
if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.VIDEO)) {
videoPlaylistId = playlist.playlistid;
break;
}
}
// If found, add to playlist
if (videoPlaylistId != -1) {
PlaylistType.Item item = new PlaylistType.Item();
item.musicvideoid = itemId;
Playlist.Add action = new Playlist.Add(videoPlaylistId, item);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
} else {
Toast.makeText(getActivity(), R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
.show();
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
protected void download() {
final FileDownloadHelper.MusicVideoInfo musicVideoDownloadInfo = new FileDownloadHelper.MusicVideoInfo(
getDataHolder().getTitle(), cursor.getString(MusicVideoDetailsQuery.FILE));
// Check if the directory exists and whether to overwrite it
File file = new File(musicVideoDownloadInfo.getAbsoluteFilePath());
if (file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.download_file_exists)
.setPositiveButton(R.string.overwrite,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
musicVideoDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNeutralButton(R.string.download_with_new_name,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
musicVideoDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Nothing to do
}
})
.show();
} else {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
musicVideoDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
}
@Override
protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() {
return null;
}
/**
* Video details query parameters.
*/
private interface MusicVideoDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.MusicVideos.TITLE,
MediaContract.MusicVideos.ALBUM,
MediaContract.MusicVideos.ARTIST,
MediaContract.MusicVideos.THUMBNAIL,
MediaContract.MusicVideos.FANART,
MediaContract.MusicVideos.YEAR,
MediaContract.MusicVideos.GENRES,
MediaContract.MusicVideos.RUNTIME,
MediaContract.MusicVideos.PLOT,
MediaContract.MusicVideos.FILE,
};
int ID = 0;
int TITLE = 1;
int ALBUM = 2;
int ARTIST = 3;
int THUMBNAIL =4;
int FANART = 5;
int YEAR = 6;
int GENRES = 7;
int RUNTIME = 8;
int PLOT = 9;
int FILE = 10;
}
}

View File

@ -38,6 +38,7 @@ import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.provider.MediaDatabase; import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractCursorListFragment; import org.xbmc.kore.ui.AbstractCursorListFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils; import org.xbmc.kore.utils.Utils;
@ -124,16 +125,16 @@ public class MusicVideoListFragment extends AbstractCursorListFragment {
String SORT = MediaDatabase.sortCommonTokens(MediaContract.MusicVideos.TITLE) + " ASC"; String SORT = MediaDatabase.sortCommonTokens(MediaContract.MusicVideos.TITLE) + " ASC";
final int ID = 0; int ID = 0;
final int MUSICVIDEOID = 1; int MUSICVIDEOID = 1;
final int TITLE = 2; int TITLE = 2;
final int ARTIST = 3; int ARTIST = 3;
final int ALBUM = 4; int ALBUM = 4;
final int THUMBNAIL = 5; int THUMBNAIL = 5;
final int RUNTIME = 6; int RUNTIME = 6;
final int GENRES = 7; int GENRES = 7;
final int YEAR = 8; int YEAR = 8;
final int PLOT = 9; int PLOT = 9;
} }
private static class MusicVideosAdapter extends CursorAdapter { private static class MusicVideosAdapter extends CursorAdapter {
@ -147,8 +148,8 @@ public class MusicVideoListFragment extends AbstractCursorListFragment {
// Get the art dimensions // Get the art dimensions
Resources resources = context.getResources(); Resources resources = context.getResources();
artHeight = resources.getDimensionPixelOffset(R.dimen.musicvideodetail_poster_heigth); artHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_square);
artWidth = resources.getDimensionPixelOffset(R.dimen.musicvideodetail_poster_width); artWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_square);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@ -175,31 +176,32 @@ public class MusicVideoListFragment extends AbstractCursorListFragment {
final ViewHolder viewHolder = (ViewHolder)view.getTag(); final ViewHolder viewHolder = (ViewHolder)view.getTag();
// Save the movie id // Save the movie id
viewHolder.musicVideoId = cursor.getInt(MusicVideosListQuery.MUSICVIDEOID); viewHolder.dataHolder.setId(cursor.getInt(MusicVideosListQuery.MUSICVIDEOID));
viewHolder.musicVideoTitle = cursor.getString(MusicVideosListQuery.TITLE); viewHolder.dataHolder.setTitle(cursor.getString(MusicVideosListQuery.TITLE));
viewHolder.album = cursor.getString(MusicVideosListQuery.ALBUM);
viewHolder.artist = cursor.getString(MusicVideosListQuery.ARTIST);
viewHolder.genres = cursor.getString(MusicVideosListQuery.GENRES);
viewHolder.plot = cursor.getString(MusicVideosListQuery.PLOT);
viewHolder.runtime = cursor.getInt(MusicVideosListQuery.RUNTIME);
viewHolder.year = cursor.getInt(MusicVideosListQuery.YEAR);
viewHolder.titleView.setText(viewHolder.musicVideoTitle); viewHolder.titleView.setText(viewHolder.dataHolder.getTitle());
String artistAlbum = viewHolder.artist + " | " + String artistAlbum = cursor.getString(MusicVideosListQuery.ARTIST) + " | " +
viewHolder.album; cursor.getString(MusicVideosListQuery.ALBUM);
viewHolder.artistAlbumView.setText(artistAlbum); viewHolder.artistAlbumView.setText(artistAlbum);
viewHolder.dataHolder.setUndertitle(artistAlbum);
int runtime = cursor.getInt(MusicVideosListQuery.RUNTIME);
String genres = cursor.getString(MusicVideosListQuery.GENRES);
String durationGenres = String durationGenres =
viewHolder.runtime > 0 ? runtime > 0 ?
UIUtils.formatTime(viewHolder.runtime) + " | " + viewHolder.genres : UIUtils.formatTime(runtime) + " | " + genres :
viewHolder.genres; genres;
viewHolder.durationGenresView.setText(durationGenres); viewHolder.durationGenresView.setText(durationGenres);
UIUtils.loadImageWithCharacterAvatar(context, hostManager, viewHolder.dataHolder.setDetails(durationGenres);
cursor.getString(MusicVideosListQuery.THUMBNAIL), viewHolder.musicVideoTitle,
String posterUrl = cursor.getString(MusicVideosListQuery.THUMBNAIL);
viewHolder.dataHolder.setPosterUrl(posterUrl);
UIUtils.loadImageWithCharacterAvatar(context, hostManager, posterUrl
, viewHolder.dataHolder.getTitle(),
viewHolder.artView, artWidth, artHeight); viewHolder.artView, artWidth, artHeight);
if(Utils.isLollipopOrLater()) { if(Utils.isLollipopOrLater()) {
viewHolder.artView.setTransitionName("a"+viewHolder.musicVideoId); viewHolder.artView.setTransitionName("a"+viewHolder.dataHolder.getId());
} }
} }
} }
@ -213,13 +215,6 @@ public class MusicVideoListFragment extends AbstractCursorListFragment {
TextView durationGenresView; TextView durationGenresView;
ImageView artView; ImageView artView;
int musicVideoId; AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0);
String musicVideoTitle;
String artist;
String album;
int runtime;
String genres;
int year;
String plot;
} }
} }

View File

@ -15,7 +15,6 @@
*/ */
package org.xbmc.kore.ui.sections.audio; package org.xbmc.kore.ui.sections.audio;
import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
@ -23,6 +22,8 @@ import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.provider.BaseColumns;
import android.support.annotation.Nullable;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
import android.support.v4.view.MenuItemCompat; import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.SearchView; import android.support.v7.widget.SearchView;
@ -61,17 +62,46 @@ public class SongsListFragment extends AbstractCursorListFragment {
private static final String TAG = LogUtils.makeLogTag(SongsListFragment.class); private static final String TAG = LogUtils.makeLogTag(SongsListFragment.class);
public static final String BUNDLE_KEY_ARTISTID = "artistid"; public static final String BUNDLE_KEY_ARTISTID = "artistid";
public static final String BUNDLE_KEY_ALBUMID = "albumid";
public static final String BUNDLE_KEY_ALBUMTITLE = "albumtitle";
private int artistId = -1; private int artistId = -1;
private int albumId = -1;
private String albumTitle = "";
private Handler callbackHandler = new Handler(); private Handler callbackHandler = new Handler();
/**
* Use this to display all songs for a specific artist
* @param artistId
*/
public void setArtist(int artistId) {
Bundle args = new Bundle();
args.putInt(BUNDLE_KEY_ARTISTID, artistId);
setArguments(args);
}
/**
* Use this to display all songs for a specific album
* @param albumId
*/
public void setAlbum(int albumId, String albumTitle) {
Bundle args = new Bundle();
args.putInt(BUNDLE_KEY_ALBUMID, albumId);
args.putString(BUNDLE_KEY_ALBUMTITLE, albumTitle);
setArguments(args);
}
@Override @Override
protected String getListSyncType() { return LibrarySyncService.SYNC_ALL_MUSIC; } protected String getListSyncType() { return LibrarySyncService.SYNC_ALL_MUSIC; }
@Override @Override
protected CursorAdapter createAdapter() { protected CursorAdapter createAdapter() {
return new SongsAdapter(getActivity()); if (albumId != -1 ) {
return new AlbumSongsAdapter(getActivity());
} else {
return new SongsAdapter(getActivity());
}
} }
@Override @Override
@ -86,9 +116,11 @@ public class SongsListFragment extends AbstractCursorListFragment {
HostInfo hostInfo = HostManager.getInstance(getActivity()).getHostInfo(); HostInfo hostInfo = HostManager.getInstance(getActivity()).getHostInfo();
int hostId = hostInfo != null ? hostInfo.getId() : -1; int hostId = hostInfo != null ? hostInfo.getId() : -1;
if (artistId != -1) { if (artistId != -1) { // get songs for artist
uri = MediaContract.Songs.buildArtistSongsListUri(hostId, artistId); uri = MediaContract.Songs.buildArtistSongsListUri(hostId, artistId);
} else { } else if (albumId != -1) {
uri = MediaContract.Songs.buildAlbumSongsListUri(hostId, albumId);
} else { // get all songs
uri = MediaContract.Songs.buildSongsListUri(hostId); uri = MediaContract.Songs.buildSongsListUri(hostId);
} }
@ -100,8 +132,13 @@ public class SongsListFragment extends AbstractCursorListFragment {
selectionArgs = new String[] {"%" + searchFilter + "%"}; selectionArgs = new String[] {"%" + searchFilter + "%"};
} }
return new CursorLoader(getActivity(), uri, if (albumId != -1) {
SongsListQuery.PROJECTION, selection, selectionArgs, SongsListQuery.SORT); return new CursorLoader(getActivity(), uri,
AlbumSongsListQuery.PROJECTION, selection, selectionArgs, AlbumSongsListQuery.SORT);
} else {
return new CursorLoader(getActivity(), uri,
SongsListQuery.PROJECTION, selection, selectionArgs, SongsListQuery.SORT);
}
} }
@Override @Override
@ -111,13 +148,14 @@ public class SongsListFragment extends AbstractCursorListFragment {
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
Bundle args = getArguments(); Bundle arguments = getArguments();
if (args != null) { if (arguments != null) {
artistId = getArguments().getInt(BUNDLE_KEY_ARTISTID, -1); artistId = arguments.getInt(BUNDLE_KEY_ARTISTID, -1);
albumId = arguments.getInt(BUNDLE_KEY_ALBUMID, -1);
albumTitle = arguments.getString(BUNDLE_KEY_ALBUMTITLE, "");
} }
super.onCreate(savedInstanceState);
return super.onCreateView(inflater, container, savedInstanceState);
} }
@Override @Override
@ -152,7 +190,7 @@ public class SongsListFragment extends AbstractCursorListFragment {
MediaProvider.Qualified.ALBUMS_GENRE, MediaProvider.Qualified.ALBUMS_GENRE,
MediaProvider.Qualified.ALBUMS_YEAR, MediaProvider.Qualified.ALBUMS_YEAR,
MediaProvider.Qualified.ALBUMS_THUMBNAIL MediaProvider.Qualified.ALBUMS_THUMBNAIL
}; };
String SORT = MediaDatabase.sortCommonTokens(MediaProvider.Qualified.SONGS_TITLE) + " ASC"; String SORT = MediaDatabase.sortCommonTokens(MediaProvider.Qualified.SONGS_TITLE) + " ASC";
@ -169,6 +207,33 @@ public class SongsListFragment extends AbstractCursorListFragment {
int THUMBNAIL = 10; int THUMBNAIL = 10;
} }
/**
* Album songs list query parameters.
*/
public interface AlbumSongsListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Songs.TITLE,
MediaContract.Songs.TRACK,
MediaContract.Songs.DURATION,
MediaContract.Songs.FILE,
MediaContract.Songs.SONGID,
MediaContract.Songs.DISPLAYARTIST,
MediaContract.Songs.DISC
};
String SORT = MediaContract.Songs.DISC + " ASC, " + MediaContract.Songs.TRACK + " ASC";
int ID = 0;
int TITLE = 1;
int TRACK = 2;
int DURATION = 3;
int FILE = 4;
int SONGID = 5;
int ARTIST = 6;
int DISC = 7;
}
private class SongsAdapter extends CursorAdapter { private class SongsAdapter extends CursorAdapter {
private HostManager hostManager; private HostManager hostManager;
@ -182,14 +247,14 @@ public class SongsListFragment extends AbstractCursorListFragment {
// Use the same dimensions as in the details fragment, so that it hits Picasso's cache when // Use the same dimensions as in the details fragment, so that it hits Picasso's cache when
// the user transitions to that fragment, avoiding another call and imediatelly showing the image // the user transitions to that fragment, avoiding another call and imediatelly showing the image
Resources resources = context.getResources(); Resources resources = context.getResources();
artWidth = (int) resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_width); artWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_square);
artHeight = (int) resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_heigth); artHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_square);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public View newView(Context context, final Cursor cursor, ViewGroup parent) { public View newView(Context context, Cursor cursor, ViewGroup parent) {
final View view = LayoutInflater.from(context) View view = LayoutInflater.from(context)
.inflate(R.layout.grid_item_song, parent, false); .inflate(R.layout.grid_item_song, parent, false);
// Setup View holder pattern // Setup View holder pattern
@ -199,13 +264,13 @@ public class SongsListFragment extends AbstractCursorListFragment {
viewHolder.art = (ImageView)view.findViewById(R.id.art); viewHolder.art = (ImageView)view.findViewById(R.id.art);
viewHolder.artist = (TextView)view.findViewById(R.id.artist); viewHolder.artist = (TextView)view.findViewById(R.id.artist);
viewHolder.songInfo = new FileDownloadHelper.SongInfo(); viewHolder.songInfo = new FileDownloadHelper.SongInfo();
viewHolder.contextMenu = (ImageView)view.findViewById(R.id.list_context_menu);
view.setTag(viewHolder); view.setTag(viewHolder);
return view; return view;
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@TargetApi(21)
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, Cursor cursor) {
final ViewHolder viewHolder = (ViewHolder)view.getTag(); final ViewHolder viewHolder = (ViewHolder)view.getTag();
@ -243,9 +308,62 @@ public class SongsListFragment extends AbstractCursorListFragment {
viewHolder.songInfo.track = cursor.getInt(SongsListQuery.TRACK); viewHolder.songInfo.track = cursor.getInt(SongsListQuery.TRACK);
// For the popupmenu // For the popupmenu
ImageView contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); viewHolder.contextMenu.setTag(viewHolder);
contextMenu.setTag(viewHolder); viewHolder.contextMenu.setOnClickListener(new View.OnClickListener() {
contextMenu.setOnClickListener(new View.OnClickListener() { @Override
public void onClick(final View v) {
showPopupMenu(v);
}
});
}
}
private class AlbumSongsAdapter extends CursorAdapter {
public AlbumSongsAdapter(Context context) {
super(context, null, false);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
View view = LayoutInflater.from(context)
.inflate(R.layout.list_item_song, viewGroup, false);
// Setup View holder pattern
ViewHolder viewHolder = new ViewHolder();
viewHolder.trackNumber = (TextView)view.findViewById(R.id.track_number);
viewHolder.title = (TextView)view.findViewById(R.id.song_title);
viewHolder.details = (TextView)view.findViewById(R.id.details);
viewHolder.contextMenu = (ImageView)view.findViewById(R.id.list_context_menu);
viewHolder.songInfo = new FileDownloadHelper.SongInfo();
view.setTag(viewHolder);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
String artist = cursor.getString(AlbumSongsListQuery.ARTIST);
ViewHolder vh = (ViewHolder) view.getTag();
vh.title.setText(cursor.getString(AlbumSongsListQuery.TITLE));
vh.songInfo.artist = artist;
vh.songInfo.album = albumTitle;
vh.songInfo.songId = cursor.getInt(SongsListQuery.SONGID);
vh.songInfo.title = cursor.getString(SongsListQuery.TITLE);
vh.songInfo.fileName = cursor.getString(SongsListQuery.FILE);
vh.songInfo.track = cursor.getInt(SongsListQuery.TRACK);
vh.trackNumber.setText(String.valueOf(vh.songInfo.track));
String duration = UIUtils.formatTime(cursor.getInt(AlbumSongsListQuery.DURATION));
String detailsText = TextUtils.isEmpty(artist) ? duration : duration + " | " + artist;
vh.details.setText(detailsText);
vh.contextMenu.setTag(vh);
vh.contextMenu.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(final View v) { public void onClick(final View v) {
showPopupMenu(v); showPopupMenu(v);
@ -262,6 +380,8 @@ public class SongsListFragment extends AbstractCursorListFragment {
TextView title; TextView title;
TextView details; TextView details;
TextView artist; TextView artist;
TextView trackNumber;
ImageView contextMenu;
FileDownloadHelper.SongInfo songInfo; FileDownloadHelper.SongInfo songInfo;
} }

View File

@ -474,6 +474,11 @@ public class MediaFileListFragment extends AbstractListFragment {
return p; return p;
} }
@Override
public void onRefresh() {
}
private class MediaFileListAdapter extends BaseAdapter implements ListAdapter { private class MediaFileListAdapter extends BaseAdapter implements ListAdapter {
Context ctx; Context ctx;

View File

@ -110,7 +110,7 @@ public class AllCastActivity extends BaseActivity {
@Override @Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// Get the name from the tag // Get the name from the tag
Utils.openImdbForPerson(AllCastActivity.this, (String) ((ViewHolder)view.getTag()).castName); Utils.openImdbForPerson(AllCastActivity.this, ((ViewHolder)view.getTag()).castName);
} }
}); });
@ -120,16 +120,6 @@ public class AllCastActivity extends BaseActivity {
setupActionBar(movie_tvshow_title); setupActionBar(movie_tvshow_title);
} }
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override @Override
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);

View File

@ -1,705 +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.sections.video;
import android.annotation.TargetApi;
import android.content.DialogInterface;
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.Handler;
import android.preference.PreferenceManager;
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.support.v7.app.AlertDialog;
import android.text.TextUtils;
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.ImageButton;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
import com.melnykov.fab.ObservableScrollView;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
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.ui.AbstractDetailsFragment;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.io.File;
import java.util.ArrayList;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
/**
* Presents movie details
*/
public class MovieDetailsFragment extends AbstractDetailsFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(MovieDetailsFragment.class);
public static final String BUNDLE_KEY_MOVIETITLE = "movie_title";
public static final String BUNDLE_KEY_MOVIEPLOT = "movie_plot";
public static final String BUNDLE_KEY_MOVIEID = "movie_id";
public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME";
public static final String BUNDLE_KEY_MOVIEGENRES = "movie_genres";
public static final String BUNDLE_KEY_MOVIEYEAR = "movie_year";
public static final String BUNDLE_KEY_MOVIERUNTIME = "movie_runtime";
public static final String BUNDLE_KEY_MOVIERATING = "movie_rating";
// Loader IDs
private static final int LOADER_MOVIE = 0,
LOADER_CAST = 1;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
// Displayed movie id
private int movieId = -1;
private String movieTitle;
private ArrayList<VideoType.Cast> castArrayList;
// Info for downloading the movie
private FileDownloadHelper.MovieInfo movieDownloadInfo = null;
// 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;
@InjectView(R.id.exit_transition_view) View exitTransitionView;
// Buttons
@InjectView(R.id.fab) ImageButton fabButton;
@InjectView(R.id.add_to_playlist) ImageButton addToPlaylistButton;
@InjectView(R.id.go_to_imdb) ImageButton imdbButton;
@InjectView(R.id.download) ImageButton downloadButton;
@InjectView(R.id.seen) ImageButton seenButton;
// 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.year) TextView mediaYear;
@InjectView(R.id.genres) TextView mediaGenres;
@InjectView(R.id.rating_votes) TextView mediaRatingVotes;
@InjectView(R.id.media_description) TextView mediaDescription;
@InjectView(R.id.directors) TextView mediaDirectors;
@InjectView(R.id.cast_list) GridLayout videoCastList;
/**
* Create a new instance of this, initialized to show the movie movieId
*/
@TargetApi(21)
public static MovieDetailsFragment newInstance(MovieListFragment.ViewHolder vh) {
MovieDetailsFragment fragment = new MovieDetailsFragment();
Bundle args = new Bundle();
args.putInt(BUNDLE_KEY_MOVIEID, vh.movieId);
args.putString(BUNDLE_KEY_MOVIETITLE, vh.movieTitle);
args.putString(BUNDLE_KEY_MOVIEPLOT, vh.movieTagline);
args.putString(BUNDLE_KEY_MOVIEGENRES, vh.movieGenres);
args.putInt(BUNDLE_KEY_MOVIEYEAR, vh.movieYear);
args.putInt(BUNDLE_KEY_MOVIERUNTIME, vh.movieRuntime);
args.putDouble(BUNDLE_KEY_MOVIERATING, vh.movieRating);
if( Utils.isLollipopOrLater()) {
args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName());
}
fragment.setArguments(args);
return fragment;
}
@TargetApi(21)
@Override
protected View createView(LayoutInflater inflater, ViewGroup container) {
Bundle bundle = getArguments();
movieId = bundle.getInt(BUNDLE_KEY_MOVIEID, -1);
if (movieId == -1) {
// There's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_movie_details, 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);
}
});
FloatingActionButton fab = (FloatingActionButton)fabButton;
fab.attachToScrollView((ObservableScrollView) mediaPanel);
if(Utils.isLollipopOrLater()) {
mediaPoster.setTransitionName(getArguments().getString(POSTER_TRANS_NAME));
}
mediaTitle.setText(bundle.getString(BUNDLE_KEY_MOVIETITLE));
mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_MOVIEPLOT));
mediaGenres.setText(bundle.getString(BUNDLE_KEY_MOVIEGENRES));
setMediaYear(bundle.getInt(BUNDLE_KEY_MOVIERUNTIME), bundle.getInt(BUNDLE_KEY_MOVIEYEAR));
setMediaRating(bundle.getDouble(BUNDLE_KEY_MOVIERATING));
// 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_MOVIE;
}
@Override
protected String getSyncID() {
return LibrarySyncService.SYNC_MOVIEID;
}
@Override
protected int getSyncItemID() {
return movieId;
}
@Override
protected SwipeRefreshLayout getSwipeRefreshLayout() {
return swipeRefreshLayout;
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
hasIssuedOutdatedRefresh = false;
// Start the loaders
getLoaderManager().initLoader(LOADER_MOVIE, null, this);
getLoaderManager().initLoader(LOADER_CAST, null, this);
}
@Override
public void onResume() {
// Force the exit view to invisible
exitTransitionView.setVisibility(View.INVISIBLE);
//As we make mediaPoster invisible in onStop() we need to make it visible here.
mediaPoster.setVisibility(View.VISIBLE);
super.onResume();
}
@Override
public void onStop() {
//For some reason poster is included in the bottom slide animation, by making it invisible it is not noticeable for the user
mediaPoster.setVisibility(View.INVISIBLE);
super.onStop();
}
@Override
protected void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_MOVIE, 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_MOVIE:
uri = MediaContract.Movies.buildMovieUri(getHostInfo().getId(), movieId);
return new CursorLoader(getActivity(), uri,
MovieDetailsQuery.PROJECTION, null, null, null);
case LOADER_CAST:
uri = MediaContract.MovieCast.buildMovieCastListUri(getHostInfo().getId(), movieId);
return new CursorLoader(getActivity(), uri,
MovieCastListQuery.PROJECTION, null, null, MovieCastListQuery.SORT);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_MOVIE:
displayMovieDetails(cursor);
checkOutdatedMovieDetails(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.fab)
public void onFabClicked(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.movieid = movieId;
Player.Open action = new Player.Open(item);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Check whether we should switch to the remote
boolean switchToRemote = PreferenceManager
.getDefaultSharedPreferences(getActivity())
.getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
if (switchToRemote) {
int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.add_to_playlist)
public void onAddToPlaylistClicked(View v) {
Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
getPlaylists.execute(getHostManager().getConnection(), new ApiCallback<ArrayList<PlaylistType.GetPlaylistsReturnType>>() {
@Override
public void onSuccess(ArrayList<PlaylistType.GetPlaylistsReturnType> result) {
if (!isAdded()) return;
// Ok, loop through the playlists, looking for the video one
int videoPlaylistId = -1;
for (PlaylistType.GetPlaylistsReturnType playlist : result) {
if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.VIDEO)) {
videoPlaylistId = playlist.playlistid;
break;
}
}
// If found, add to playlist
if (videoPlaylistId != -1) {
PlaylistType.Item item = new PlaylistType.Item();
item.movieid = movieId;
Playlist.Add action = new Playlist.Add(videoPlaylistId, item);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
} else {
Toast.makeText(getActivity(), R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
.show();
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.go_to_imdb)
public void onImdbClicked(View v) {
String imdbNumber = (String)v.getTag();
if (imdbNumber != null) {
Utils.openImdbForMovie(getActivity(), imdbNumber);
}
}
@OnClick(R.id.seen)
public void onSeenClicked(View v) {
// Set the playcount
Integer playcount = (Integer)v.getTag();
int newPlaycount = (playcount > 0) ? 0 : 1;
VideoLibrary.SetMovieDetails action =
new VideoLibrary.SetMovieDetails(movieId, newPlaycount, null);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Force a refresh, but don't show a message
startSync(true);
}
@Override
public void onError(int errorCode, String description) { }
}, callbackHandler);
// Change the button, to provide imeddiate feedback, even if it isn't yet stored in the db
// (will be properly updated and refreshed after the refresh callback ends)
setupSeenButton(newPlaycount);
}
@Override
protected void onDownload() {
if (movieDownloadInfo == null) {
// Nothing to download
Toast.makeText(getActivity(), R.string.no_files_to_download, Toast.LENGTH_SHORT).show();
return;
}
DialogInterface.OnClickListener noopClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) { }
};
// Check if the directory exists and whether to overwrite it
File file = new File(movieDownloadInfo.getAbsoluteFilePath());
if (file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.download_file_exists)
.setPositiveButton(R.string.overwrite,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
movieDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNeutralButton(R.string.download_with_new_name,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
movieDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
} else {
// Confirm that the user really wants to download the file
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.confirm_movie_download)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
movieDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
}
}
/**
* Display the movie details
*
* @param cursor Cursor with the data
*/
private void displayMovieDetails(Cursor cursor) {
LogUtils.LOGD(TAG, "Refreshing movie details");
cursor.moveToFirst();
movieTitle = cursor.getString(MovieDetailsQuery.TITLE);
mediaTitle.setText(movieTitle);
mediaUndertitle.setText(cursor.getString(MovieDetailsQuery.TAGLINE));
setMediaYear(cursor.getInt(MovieDetailsQuery.RUNTIME) / 60, cursor.getInt(MovieDetailsQuery.YEAR));
mediaGenres.setText(cursor.getString(MovieDetailsQuery.GENRES));
double rating = cursor.getDouble(MovieDetailsQuery.RATING);
if (rating > 0) {
mediaRating.setVisibility(View.VISIBLE);
mediaMaxRating.setVisibility(View.VISIBLE);
mediaRatingVotes.setVisibility(View.VISIBLE);
setMediaRating(rating);
String votes = cursor.getString(MovieDetailsQuery.VOTES);
mediaRatingVotes.setText((TextUtils.isEmpty(votes)) ?
"" : String.format(getString(R.string.votes), votes));
} else {
mediaRating.setVisibility(View.INVISIBLE);
mediaMaxRating.setVisibility(View.INVISIBLE);
mediaRatingVotes.setVisibility(View.INVISIBLE);
}
mediaDescription.setText(cursor.getString(MovieDetailsQuery.PLOT));
mediaDirectors.setText(cursor.getString(MovieDetailsQuery.DIRECTOR));
// IMDB button
imdbButton.setTag(cursor.getString(MovieDetailsQuery.IMDBNUMBER));
setupSeenButton(cursor.getInt(MovieDetailsQuery.PLAYCOUNT));
// 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(MovieDetailsQuery.THUMBNAIL), movieTitle,
mediaPoster, posterWidth, posterHeight);
int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height);
UIUtils.loadImageIntoImageview(getHostManager(),
cursor.getString(MovieDetailsQuery.FANART),
mediaArt, displayMetrics.widthPixels, artHeight);
// Setup movie download info
movieDownloadInfo = new FileDownloadHelper.MovieInfo(
movieTitle, cursor.getString(MovieDetailsQuery.FILE));
// Check if downloaded file exists
if (movieDownloadInfo.downloadFileExists()) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{
R.attr.colorAccent});
downloadButton.setColorFilter(
styledAttributes.getColor(0,
getActivity().getResources().getColor(R.color.accent_default)));
styledAttributes.recycle();
} else {
downloadButton.clearColorFilter();
}
}
private void setMediaRating(double rating) {
mediaRating.setText(String.format("%01.01f", rating));
mediaMaxRating.setText(getString(R.string.max_rating_video));
}
private void setMediaYear(int runtime, int year) {
String durationYear = runtime > 0 ?
String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
" | " + year :
String.valueOf(year);
mediaYear.setText(durationYear);
}
private void setupSeenButton(int playcount) {
// Seen button
if (playcount > 0) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] {
R.attr.colorAccent});
seenButton.setColorFilter(styledAttributes.getColor(0,
getActivity().getResources().getColor(R.color.accent_default)));
styledAttributes.recycle();
} else {
seenButton.clearColorFilter();
}
// Save the playcount
seenButton.setTag(playcount);
}
/**
* 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(MovieCastListQuery.NAME),
cursor.getInt(MovieCastListQuery.ORDER),
cursor.getString(MovieCastListQuery.ROLE),
cursor.getString(MovieCastListQuery.THUMBNAIL)));
} while (cursor.moveToNext());
UIUtils.setupCastInfo(getActivity(), castArrayList, videoCastList,
AllCastActivity.buildLaunchIntent(getActivity(), movieTitle, castArrayList));
}
}
/**
* Checks wether we should refresh the movie 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 checkOutdatedMovieDetails(Cursor cursor) {
if (hasIssuedOutdatedRefresh)
return;
cursor.moveToFirst();
long lastUpdated = cursor.getLong(MovieDetailsQuery.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() {
if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) {
return mediaPoster;
}
return null;
}
/**
* Movie details query parameters.
*/
private interface MovieDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Movies.TITLE,
MediaContract.Movies.TAGLINE,
MediaContract.Movies.THUMBNAIL,
MediaContract.Movies.FANART,
MediaContract.Movies.YEAR,
MediaContract.Movies.GENRES,
MediaContract.Movies.RUNTIME,
MediaContract.Movies.RATING,
MediaContract.Movies.VOTES,
MediaContract.Movies.PLOT,
MediaContract.Movies.PLAYCOUNT,
MediaContract.Movies.DIRECTOR,
MediaContract.Movies.IMDBNUMBER,
MediaContract.Movies.FILE,
MediaContract.SyncColumns.UPDATED,
};
final int ID = 0;
final int TITLE = 1;
final int TAGLINE = 2;
final int THUMBNAIL = 3;
final int FANART = 4;
final int YEAR = 5;
final int GENRES = 6;
final int RUNTIME = 7;
final int RATING = 8;
final int VOTES = 9;
final int PLOT = 10;
final int PLAYCOUNT = 11;
final int DIRECTOR = 12;
final int IMDBNUMBER = 13;
final int FILE = 14;
final int UPDATED = 15;
}
/**
* Movie cast list query parameters.
*/
public interface MovieCastListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.MovieCast.NAME,
MediaContract.MovieCast.ORDER,
MediaContract.MovieCast.ROLE,
MediaContract.MovieCast.THUMBNAIL,
};
String SORT = MediaContract.MovieCast.ORDER + " ASC";
final int ID = 0;
final int NAME = 1;
final int ORDER = 2;
final int ROLE = 3;
final int THUMBNAIL = 4;
}
}

View File

@ -0,0 +1,411 @@
/*
* 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.sections.video;
import android.content.DialogInterface;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.BaseColumns;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.CastFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.Utils;
import java.io.File;
import java.util.ArrayList;
/**
* Presents movie details
*/
public class MovieInfoFragment extends AbstractInfoFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(MovieInfoFragment.class);
// Loader IDs
private static final int LOADER_MOVIE = 0;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
// Controls whether a automatic sync refresh has been issued for this show
private static boolean hasIssuedOutdatedRefresh = false;
private Cursor cursor;
private FileDownloadHelper.MovieInfo movieDownloadInfo;
@Override
protected RefreshItem createRefreshItem() {
RefreshItem refreshItem = new RefreshItem(getActivity(),
LibrarySyncService.SYNC_SINGLE_MOVIE);
refreshItem.setSyncItem(LibrarySyncService.SYNC_MOVIEID, getDataHolder().getId());
refreshItem.setListener(new RefreshItem.RefreshItemListener() {
@Override
public void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_MOVIE, null, MovieInfoFragment.this);
}
}
});
return refreshItem;
}
@Override
protected boolean setupMediaActionBar() {
setOnDownloadListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DialogInterface.OnClickListener noopClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) { }
};
// Check if the directory exists and whether to overwrite it
File file = new File(movieDownloadInfo.getAbsoluteFilePath());
if (file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.download_file_exists)
.setPositiveButton(R.string.overwrite,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
movieDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNeutralButton(R.string.download_with_new_name,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
movieDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
} else {
// Confirm that the user really wants to download the file
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.confirm_movie_download)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
movieDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
}
}
});
setOnSeenListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Set the playcount
int playcount = cursor.getInt(MovieDetailsQuery.PLAYCOUNT);
int newPlaycount = (playcount > 0) ? 0 : 1;
VideoLibrary.SetMovieDetails action =
new VideoLibrary.SetMovieDetails(getDataHolder().getId(), newPlaycount, null);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Force a refresh, but don't show a message
getRefreshItem().startSync(true);
}
@Override
public void onError(int errorCode, String description) { }
}, callbackHandler);
}
});
setOnGoToImdbListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String imdbNumber = cursor.getString(MovieDetailsQuery.IMDBNUMBER);
if (imdbNumber != null) {
Utils.openImdbForMovie(getActivity(), imdbNumber);
}
}
});
setOnAddToPlaylistListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
getPlaylists.execute(getHostManager().getConnection(), new ApiCallback<ArrayList<PlaylistType.GetPlaylistsReturnType>>() {
@Override
public void onSuccess(ArrayList<PlaylistType.GetPlaylistsReturnType> result) {
if (!isAdded()) return;
// Ok, loop through the playlists, looking for the video one
int videoPlaylistId = -1;
for (PlaylistType.GetPlaylistsReturnType playlist : result) {
if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.VIDEO)) {
videoPlaylistId = playlist.playlistid;
break;
}
}
// If found, add to playlist
if (videoPlaylistId != -1) {
PlaylistType.Item item = new PlaylistType.Item();
item.movieid = getDataHolder().getId();
Playlist.Add action = new Playlist.Add(videoPlaylistId, item);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
} else {
Toast.makeText(getActivity(), R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
.show();
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
});
return true;
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.movieid = getDataHolder().getId();
fabActionPlayItem(item);
}
});
return true;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setExpandDescription(true);
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
hasIssuedOutdatedRefresh = false;
// Start the loaders
getLoaderManager().initLoader(LOADER_MOVIE, null, this);
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_MOVIE:
uri = MediaContract.Movies.buildMovieUri(getHostInfo().getId(), getDataHolder().getId());
return new CursorLoader(getActivity(), uri,
MovieDetailsQuery.PROJECTION, null, null, null);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_MOVIE:
cursor.moveToFirst();
this.cursor = cursor;
DataHolder dataHolder = getDataHolder();
dataHolder.setFanArtUrl(cursor.getString(MovieDetailsQuery.FANART));
dataHolder.setPosterUrl(cursor.getString(MovieDetailsQuery.THUMBNAIL));
dataHolder.setRating(cursor.getDouble(MovieDetailsQuery.RATING));
dataHolder.setMaxRating(10);
dataHolder.setVotes(cursor.getInt(MovieDetailsQuery.VOTES));
String director = cursor.getString(MovieDetailsQuery.DIRECTOR);
if (!TextUtils.isEmpty(director)) {
director = getString(R.string.directors) + " " + director;
}
int runtime = cursor.getInt(MovieDetailsQuery.RUNTIME) / 60;
String year = String.valueOf(cursor.getInt(MovieDetailsQuery.YEAR));
String durationYear = runtime > 0 ?
String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
" | " + year :
String.valueOf(year);
dataHolder.setDetails(durationYear
+ "\n" +
cursor.getString(MovieDetailsQuery.GENRES)
+"\n" +
director);
dataHolder.setTitle(cursor.getString(MovieDetailsQuery.TITLE));
dataHolder.setUndertitle(cursor.getString(MovieDetailsQuery.TAGLINE));
dataHolder.setDescription(cursor.getString(MovieDetailsQuery.PLOT));
movieDownloadInfo = new FileDownloadHelper.MovieInfo(
dataHolder.getTitle(), cursor.getString(MovieDetailsQuery.FILE));
setDownloadButtonState(movieDownloadInfo.downloadDirectoryExists());
setSeenButtonState(cursor.getInt(MovieDetailsQuery.PLAYCOUNT) > 0);
updateView(dataHolder);
checkOutdatedMovieDetails(cursor);
break;
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
@Override
protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() {
CastFragment castFragment = new CastFragment();
castFragment.setArgs(getDataHolder().getId(), getDataHolder().getTitle(),
CastFragment.TYPE.MOVIE);
return castFragment;
}
/**
* Checks wether we should refresh the movie 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 checkOutdatedMovieDetails(Cursor cursor) {
if (hasIssuedOutdatedRefresh)
return;
long lastUpdated = cursor.getLong(MovieDetailsQuery.UPDATED);
if (System.currentTimeMillis() > lastUpdated + Settings.DB_UPDATE_INTERVAL) {
// Trigger a silent refresh
hasIssuedOutdatedRefresh = true;
getRefreshItem().startSync(true);
}
}
/**
* Movie details query parameters.
*/
private interface MovieDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Movies.TITLE,
MediaContract.Movies.TAGLINE,
MediaContract.Movies.THUMBNAIL,
MediaContract.Movies.FANART,
MediaContract.Movies.YEAR,
MediaContract.Movies.GENRES,
MediaContract.Movies.RUNTIME,
MediaContract.Movies.RATING,
MediaContract.Movies.VOTES,
MediaContract.Movies.PLOT,
MediaContract.Movies.PLAYCOUNT,
MediaContract.Movies.DIRECTOR,
MediaContract.Movies.IMDBNUMBER,
MediaContract.Movies.FILE,
MediaContract.SyncColumns.UPDATED,
};
int ID = 0;
int TITLE = 1;
int TAGLINE = 2;
int THUMBNAIL = 3;
int FANART = 4;
int YEAR = 5;
int GENRES = 6;
int RUNTIME = 7;
int RATING = 8;
int VOTES = 9;
int PLOT = 10;
int PLAYCOUNT = 11;
int DIRECTOR = 12;
int IMDBNUMBER = 13;
int FILE = 14;
int UPDATED = 15;
}
}

View File

@ -44,6 +44,7 @@ import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.provider.MediaDatabase; import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractCursorListFragment; import org.xbmc.kore.ui.AbstractCursorListFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils; import org.xbmc.kore.utils.Utils;
@ -95,7 +96,7 @@ public class MovieListFragment extends AbstractCursorListFragment {
if (selection.length() != 0) if (selection.length() != 0)
selection.append(" AND "); selection.append(" AND ");
selection.append(MediaContract.MoviesColumns.PLAYCOUNT) selection.append(MediaContract.MoviesColumns.PLAYCOUNT)
.append("=0"); .append("=0");
} }
String sortOrderStr; String sortOrderStr;
@ -120,7 +121,7 @@ public class MovieListFragment extends AbstractCursorListFragment {
} }
return new CursorLoader(getActivity(), uri, return new CursorLoader(getActivity(), uri,
MovieListQuery.PROJECTION, selection.toString(), selectionArgs, sortOrderStr); MovieListQuery.PROJECTION, selection.toString(), selectionArgs, sortOrderStr);
} }
@Override @Override
@ -197,43 +198,43 @@ public class MovieListFragment extends AbstractCursorListFragment {
case R.id.action_hide_watched: case R.id.action_hide_watched:
item.setChecked(!item.isChecked()); item.setChecked(!item.isChecked());
preferences.edit() preferences.edit()
.putBoolean(Settings.KEY_PREF_MOVIES_FILTER_HIDE_WATCHED, item.isChecked()) .putBoolean(Settings.KEY_PREF_MOVIES_FILTER_HIDE_WATCHED, item.isChecked())
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_ignore_prefixes: case R.id.action_ignore_prefixes:
item.setChecked(!item.isChecked()); item.setChecked(!item.isChecked());
preferences.edit() preferences.edit()
.putBoolean(Settings.KEY_PREF_MOVIES_IGNORE_PREFIXES, item.isChecked()) .putBoolean(Settings.KEY_PREF_MOVIES_IGNORE_PREFIXES, item.isChecked())
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_sort_by_name: case R.id.action_sort_by_name:
item.setChecked(true); item.setChecked(true);
preferences.edit() preferences.edit()
.putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_NAME) .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_NAME)
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_sort_by_year: case R.id.action_sort_by_year:
item.setChecked(true); item.setChecked(true);
preferences.edit() preferences.edit()
.putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_YEAR) .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_YEAR)
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_sort_by_rating: case R.id.action_sort_by_rating:
item.setChecked(true); item.setChecked(true);
preferences.edit() preferences.edit()
.putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_RATING) .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_RATING)
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_sort_by_date_added: case R.id.action_sort_by_date_added:
item.setChecked(true); item.setChecked(true);
preferences.edit() preferences.edit()
.putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_DATE_ADDED) .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_DATE_ADDED)
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_sort_by_last_played: case R.id.action_sort_by_last_played:
@ -246,8 +247,8 @@ public class MovieListFragment extends AbstractCursorListFragment {
case R.id.action_sort_by_length: case R.id.action_sort_by_length:
item.setChecked(true); item.setChecked(true);
preferences.edit() preferences.edit()
.putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_LENGTH) .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_LENGTH)
.apply(); .apply();
refreshList(); refreshList();
break; break;
default: default:
@ -271,7 +272,7 @@ public class MovieListFragment extends AbstractCursorListFragment {
MediaContract.Movies.RUNTIME, MediaContract.Movies.RUNTIME,
MediaContract.Movies.RATING, MediaContract.Movies.RATING,
MediaContract.Movies.TAGLINE, MediaContract.Movies.TAGLINE,
}; };
String SORT_BY_NAME = MediaContract.Movies.TITLE + " ASC"; String SORT_BY_NAME = MediaContract.Movies.TITLE + " ASC";
@ -307,22 +308,21 @@ public class MovieListFragment extends AbstractCursorListFragment {
// the user transitions to that fragment, avoiding another call and imediatelly showing the image // the user transitions to that fragment, avoiding another call and imediatelly showing the image
Resources resources = context.getResources(); Resources resources = context.getResources();
artWidth = (int)(resources.getDimension(R.dimen.now_playing_poster_width) / artWidth = (int)(resources.getDimension(R.dimen.now_playing_poster_width) /
UIUtils.IMAGE_RESIZE_FACTOR); UIUtils.IMAGE_RESIZE_FACTOR);
artHeight = (int)(resources.getDimension(R.dimen.now_playing_poster_height) / artHeight = (int)(resources.getDimension(R.dimen.now_playing_poster_height) /
UIUtils.IMAGE_RESIZE_FACTOR); UIUtils.IMAGE_RESIZE_FACTOR);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public View newView(Context context, final Cursor cursor, ViewGroup parent) { public View newView(Context context, final Cursor cursor, ViewGroup parent) {
final View view = LayoutInflater.from(context) final View view = LayoutInflater.from(context)
.inflate(R.layout.grid_item_movie, parent, false); .inflate(R.layout.grid_item_movie, parent, false);
// Setup View holder pattern // Setup View holder pattern
ViewHolder viewHolder = new ViewHolder(); ViewHolder viewHolder = new ViewHolder();
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.yearView = (TextView)view.findViewById(R.id.year);
viewHolder.durationView = (TextView)view.findViewById(R.id.duration); viewHolder.durationView = (TextView)view.findViewById(R.id.duration);
viewHolder.artView = (ImageView)view.findViewById(R.id.art); viewHolder.artView = (ImageView)view.findViewById(R.id.art);
@ -337,32 +337,37 @@ public class MovieListFragment extends AbstractCursorListFragment {
final ViewHolder viewHolder = (ViewHolder)view.getTag(); final ViewHolder viewHolder = (ViewHolder)view.getTag();
// Save the movie id // Save the movie id
viewHolder.movieId = cursor.getInt(MovieListQuery.MOVIEID); viewHolder.dataHolder.setId(cursor.getInt(MovieListQuery.MOVIEID));
viewHolder.movieTitle = cursor.getString(MovieListQuery.TITLE); viewHolder.dataHolder.setTitle(cursor.getString(MovieListQuery.TITLE));
viewHolder.movieTagline = cursor.getString(MovieListQuery.TAGLINE); viewHolder.dataHolder.setUndertitle(cursor.getString(MovieListQuery.TAGLINE));
viewHolder.movieYear = cursor.getInt(MovieListQuery.YEAR);
viewHolder.movieRating = cursor.getDouble(MovieListQuery.RATING);
viewHolder.titleView.setText(viewHolder.movieTitle); int movieYear = cursor.getInt(MovieListQuery.YEAR);
viewHolder.dataHolder.setRating(cursor.getDouble(MovieListQuery.RATING));
viewHolder.dataHolder.setMaxRating(10);
viewHolder.movieGenres = cursor.getString(MovieListQuery.GENRES); viewHolder.titleView.setText(viewHolder.dataHolder.getTitle());
String details = TextUtils.isEmpty(viewHolder.movieTagline) ?
viewHolder.movieGenres : String genres = cursor.getString(MovieListQuery.GENRES);
viewHolder.movieTagline; String details = TextUtils.isEmpty(viewHolder.dataHolder.getUnderTitle()) ?
genres : viewHolder.dataHolder.getUnderTitle();
viewHolder.detailsView.setText(details); viewHolder.detailsView.setText(details);
// viewHolder.yearView.setText(String.valueOf(cursor.getInt(MovieListQuery.YEAR)));
viewHolder.movieRuntime = cursor.getInt(MovieListQuery.RUNTIME) / 60;
String duration = viewHolder.movieRuntime > 0 ?
String.format(context.getString(R.string.minutes_abbrev), String.valueOf(viewHolder.movieRuntime)) +
" | " + viewHolder.movieYear :
String.valueOf(viewHolder.movieYear);
viewHolder.durationView.setText(duration);
UIUtils.loadImageWithCharacterAvatar(context, hostManager,
cursor.getString(MovieListQuery.THUMBNAIL), viewHolder.movieTitle,
viewHolder.artView, artWidth, artHeight);
if(Utils.isLollipopOrLater()) { int runtime = cursor.getInt(MovieListQuery.RUNTIME) / 60;
viewHolder.artView.setTransitionName("a"+viewHolder.movieId); String duration = runtime > 0 ?
String.format(context.getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
" | " + movieYear :
String.valueOf(movieYear);
viewHolder.durationView.setText(duration);
viewHolder.dataHolder.setDetails(duration + "\n" + details);
viewHolder.dataHolder.setPosterUrl(cursor.getString(MovieListQuery.THUMBNAIL));
UIUtils.loadImageWithCharacterAvatar(context, hostManager,
viewHolder.dataHolder.getPosterUrl(),
viewHolder.dataHolder.getTitle(),
viewHolder.artView, artWidth, artHeight);
if (Utils.isLollipopOrLater()) {
viewHolder.artView.setTransitionName("a" + viewHolder.dataHolder.getId());
} }
} }
} }
@ -373,16 +378,9 @@ public class MovieListFragment extends AbstractCursorListFragment {
public static class ViewHolder { public static class ViewHolder {
TextView titleView; TextView titleView;
TextView detailsView; TextView detailsView;
// TextView yearView;
TextView durationView; TextView durationView;
ImageView artView; ImageView artView;
int movieId; AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0);
String movieTitle;
String movieTagline;
int movieYear;
int movieRuntime;
String movieGenres;
double movieRating;
} }
} }

View File

@ -18,15 +18,13 @@ package org.xbmc.kore.ui.sections.video;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout; import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View;
import android.view.Window; import android.view.Window;
import org.xbmc.kore.R; import org.xbmc.kore.R;
@ -34,11 +32,9 @@ import org.xbmc.kore.ui.BaseActivity;
import org.xbmc.kore.ui.generic.NavigationDrawerFragment; import org.xbmc.kore.ui.generic.NavigationDrawerFragment;
import org.xbmc.kore.ui.sections.remote.RemoteActivity; import org.xbmc.kore.ui.sections.remote.RemoteActivity;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.SharedElementTransition;
import org.xbmc.kore.utils.Utils; import org.xbmc.kore.utils.Utils;
import java.util.List;
import java.util.Map;
/** /**
* Controls the presentation of Movies information (list, details) * Controls the presentation of Movies information (list, details)
* All the information is presented by specific fragments * All the information is presented by specific fragments
@ -49,15 +45,14 @@ public class MoviesActivity extends BaseActivity
public static final String MOVIEID = "movie_id"; public static final String MOVIEID = "movie_id";
public static final String MOVIETITLE = "movie_title"; public static final String MOVIETITLE = "movie_title";
public static final String LISTFRAGMENT_TAG = "movielist";
private int selectedMovieId = -1; private int selectedMovieId = -1;
private String selectedMovieTitle; private String selectedMovieTitle;
private NavigationDrawerFragment navigationDrawerFragment; private NavigationDrawerFragment navigationDrawerFragment;
private boolean clearSharedElements; private SharedElementTransition sharedElementTransition = new SharedElementTransition();
private MovieListFragment movieListFragment;
@TargetApi(21) @TargetApi(21)
@Override @Override
@ -74,54 +69,28 @@ public class MoviesActivity extends BaseActivity
.findFragmentById(R.id.navigation_drawer); .findFragmentById(R.id.navigation_drawer);
navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout)); navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout));
Fragment fragment;
if (savedInstanceState == null) { if (savedInstanceState == null) {
movieListFragment = new MovieListFragment(); fragment = new MovieListFragment();
// Setup animations
if (Utils.isLollipopOrLater()) {
//Fade added to prevent shared element from disappearing very shortly at the start of the transition.
Transition fade = TransitionInflater
.from(this)
.inflateTransition(android.R.transition.fade);
movieListFragment.setExitTransition(fade);
movieListFragment.setReenterTransition(fade);
movieListFragment.setSharedElementReturnTransition(TransitionInflater.from(
this).inflateTransition(R.transition.change_image));
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() {
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
if (clearSharedElements) {
names.clear();
sharedElements.clear();
clearSharedElements = false;
}
}
};
movieListFragment.setExitSharedElementCallback(seCallback);
}
getSupportFragmentManager() getSupportFragmentManager()
.beginTransaction() .beginTransaction()
.add(R.id.fragment_container, movieListFragment) .add(R.id.fragment_container, fragment, LISTFRAGMENT_TAG)
.commit(); .commit();
} else { } else {
fragment = getSupportFragmentManager().findFragmentByTag(LISTFRAGMENT_TAG);
selectedMovieId = savedInstanceState.getInt(MOVIEID, -1); selectedMovieId = savedInstanceState.getInt(MOVIEID, -1);
selectedMovieTitle = savedInstanceState.getString(MOVIETITLE, null); selectedMovieTitle = savedInstanceState.getString(MOVIETITLE, null);
} }
if (Utils.isLollipopOrLater()) {
sharedElementTransition.setupExitTransition(this, fragment);
}
setupActionBar(selectedMovieTitle); setupActionBar(selectedMovieTitle);
} }
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override @Override
protected void onSaveInstanceState (Bundle outState) { protected void onSaveInstanceState (Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
@ -208,47 +177,24 @@ public class MoviesActivity extends BaseActivity
*/ */
@TargetApi(21) @TargetApi(21)
public void onMovieSelected(MovieListFragment.ViewHolder vh) { public void onMovieSelected(MovieListFragment.ViewHolder vh) {
selectedMovieTitle = vh.movieTitle; selectedMovieTitle = vh.dataHolder.getTitle();
selectedMovieId = vh.movieId; selectedMovieId = vh.dataHolder.getId();
final MovieInfoFragment movieInfoFragment = new MovieInfoFragment();
movieInfoFragment.setDataHolder(vh.dataHolder);
final MovieDetailsFragment movieDetailsFragment = MovieDetailsFragment.newInstance(vh);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions // Set up transitions
if (Utils.isLollipopOrLater()) { if (Utils.isLollipopOrLater()) {
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName());
@Override sharedElementTransition.setupEnterTransition(this, fragTrans, movieInfoFragment,
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { vh.artView);
//On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements
// for the reentering fragment. We use this to determine if we are returning and if
// we should clear the shared element lists. Note that, clearing must be done in the reentering fragment
// as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might
// be a v4 support package bug.
if (movieDetailsFragment.isVisible()) {
View sharedView = movieDetailsFragment.getSharedElement();
if (sharedView == null) { // shared element not visible
clearSharedElements = true;
}
}
}
};
movieDetailsFragment.setEnterSharedElementCallback(seCallback);
movieDetailsFragment.setEnterTransition(
TransitionInflater.from(this)
.inflateTransition(R.transition.media_details));
movieDetailsFragment.setReturnTransition(null);
Transition changeImageTransition =
TransitionInflater.from(this).inflateTransition(R.transition.change_image);
movieDetailsFragment.setSharedElementReturnTransition(changeImageTransition);
movieDetailsFragment.setSharedElementEnterTransition(changeImageTransition);
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, movieDetailsFragment) fragTrans.replace(R.id.fragment_container, movieInfoFragment)
.addToBackStack(null) .addToBackStack(null)
.commit(); .commit();

View File

@ -1,745 +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.sections.video;
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.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.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;
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.ui.AbstractDetailsFragment;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.MediaPlayerUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.util.ArrayList;
import at.blogc.android.views.ExpandableTextView;
import butterknife.ButterKnife;
import butterknife.InjectView;
/**
* Presents a TV Show overview
*/
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";
public static final String BUNDLE_KEY_PREMIERED = "premiered";
public static final String BUNDLE_KEY_STUDIO = "studio";
public static final String BUNDLE_KEY_EPISODE = "episode";
public static final String BUNDLE_KEY_WATCHEDEPISODES = "watchedepisodes";
public static final String BUNDLE_KEY_RATING = "rating";
public static final String BUNDLE_KEY_PLOT = "plot";
public static final String BUNDLE_KEY_GENRES = "genres";
public interface TVShowDetailsActionListener {
void onSeasonSelected(int tvshowId, int season);
void onNextEpisodeSelected(int episodeId);
}
// Activity listener
private TVShowDetailsActionListener listenerActivity;
// Loader IDs
private static final int LOADER_TVSHOW = 0,
LOADER_NEXT_EPISODES = 1,
LOADER_SEASONS = 2,
LOADER_CAST = 3;
// Displayed movie id
private int tvshowId = -1;
private String tvshowTitle;
// 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) ExpandableTextView 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;
@InjectView(R.id.media_description_container) LinearLayout mediaDescriptionContainer;
@InjectView(R.id.show_all) ImageView mediaShowAll;
/**
* Create a new instance of this, initialized to show tvshowId
*/
@TargetApi(21)
public static TVShowDetailsFragment newInstance(TVShowListFragment.ViewHolder vh) {
TVShowDetailsFragment fragment = new TVShowDetailsFragment();
Bundle args = new Bundle();
args.putInt(BUNDLE_KEY_TVSHOWID, vh.tvshowId);
args.putInt(BUNDLE_KEY_EPISODE, vh.episode);
args.putString(BUNDLE_KEY_GENRES, vh.genres);
args.putString(BUNDLE_KEY_PLOT, vh.plot);
args.putString(BUNDLE_KEY_PREMIERED, vh.premiered);
args.putDouble(BUNDLE_KEY_RATING, vh.rating);
args.putString(BUNDLE_KEY_STUDIO, vh.studio);
args.putString(BUNDLE_KEY_TITLE, vh.tvshowTitle);
args.putInt(BUNDLE_KEY_WATCHEDEPISODES, vh.watchedEpisodes);
if( Utils.isLollipopOrLater()) {
args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName());
}
fragment.setArguments(args);
return fragment;
}
@Override
@TargetApi(21)
protected View createView(LayoutInflater inflater, ViewGroup container) {
Bundle bundle = getArguments();
tvshowId = bundle.getInt(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(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;
}
@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_NEXT_EPISODES, null, this);
getLoaderManager().initLoader(LOADER_SEASONS, null, this);
getLoaderManager().initLoader(LOADER_CAST, null, this);
}
@Override
public void onAttach(Context activity) {
super.onAttach(activity);
try {
listenerActivity = (TVShowDetailsActionListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement TVShowDetailsActionListener");
}
}
@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_NEXT_EPISODES, 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_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);
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) {
switch (cursorLoader.getId()) {
case LOADER_TVSHOW:
displayTVShowDetails(cursor);
checkOutdatedTVShowDetails(cursor);
break;
case LOADER_NEXT_EPISODES:
displayNextEpisodeList(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();
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) {
mediaDescription.toggle();
mediaShowAll.setImageResource(mediaDescription.isExpanded() ? iconCollapseResId: iconExpandResId);
}
});
// // 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<>(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));
}
}
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
*
* @param cursor Cursor with the data
*/
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() {
@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);
ProgressBar seasonProgressBar = (ProgressBar) seasonView.findViewById(R.id.season_progress_bar);
seasonNumberView.setText(String.format(getActivity().getString(R.string.season_number), seasonNumber));
seasonEpisodesView.setText(String.format(getActivity().getString(R.string.num_episodes),
numEpisodes, numEpisodes - watchedEpisodes));
seasonProgressBar.setMax(numEpisodes);
seasonProgressBar.setProgress(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.GONE);
}
}
/**
* 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() {
if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) {
return mediaPoster;
}
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;
}
/**
* 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.
*/
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

@ -1,600 +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.sections.video;
import android.content.DialogInterface;
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.Handler;
import android.preference.PreferenceManager;
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.support.v7.app.AlertDialog;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
import com.melnykov.fab.ObservableScrollView;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractDetailsFragment;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import java.io.File;
import java.util.ArrayList;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
/**
* Presents movie details
*/
public class TVShowEpisodeDetailsFragment extends AbstractDetailsFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(TVShowEpisodeDetailsFragment.class);
public static final String TVSHOWID = "tvshow_id";
public static final String EPISODEID = "episode_id";
// Loader IDs
private static final int LOADER_EPISODE = 0;
// private static final int LOADER_CAST = 1;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
// Displayed episode
private int tvshowId = -1;
private int episodeId = -1;
// Info for downloading the episode
private FileDownloadHelper.TVShowInfo tvshowDownloadInfo = null;
@InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;
@InjectView(R.id.exit_transition_view) View exitTransitionView;
// Buttons
@InjectView(R.id.fab) ImageButton fabButton;
@InjectView(R.id.add_to_playlist) ImageButton addToPlaylistButton;
@InjectView(R.id.download) ImageButton downloadButton;
@InjectView(R.id.seen) ImageButton seenButton;
// 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.season) TextView mediaSeason;
@InjectView(R.id.media_description) TextView mediaDescription;
@InjectView(R.id.directors) TextView mediaDirectors;
// @InjectView(R.id.cast_list) GridLayout videoCastList;
// @InjectView(R.id.additional_cast_list) TextView videoAdditionalCastList;
// @InjectView(R.id.additional_cast_title) TextView videoAdditionalCastTitle;
/**
* Create a new instance of this, initialized to show the episode episodeId
*/
public static TVShowEpisodeDetailsFragment newInstance(final int tvshowId, final int episodeId) {
TVShowEpisodeDetailsFragment fragment = new TVShowEpisodeDetailsFragment();
Bundle args = new Bundle();
args.putInt(TVSHOWID, tvshowId);
args.putInt(EPISODEID, episodeId);
fragment.setArguments(args);
return fragment;
}
@Override
protected View createView(LayoutInflater inflater, ViewGroup container) {
tvshowId = getArguments().getInt(TVSHOWID, -1);
episodeId = getArguments().getInt(EPISODEID, -1);
if(episodeId == -1) {
// There's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_episode_details, 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);
}
});
FloatingActionButton fab = (FloatingActionButton)fabButton;
fab.attachToScrollView((ObservableScrollView) mediaPanel);
// 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
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Start the loaders
getLoaderManager().initLoader(LOADER_EPISODE, null, this);
// getLoaderManager().initLoader(LOADER_CAST, null, this);
}
@Override
public void onResume() {
// Force the exit view to invisible
exitTransitionView.setVisibility(View.INVISIBLE);
super.onResume();
}
@Override
protected void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_EPISODE, null, this);
}
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_EPISODE:
uri = MediaContract.Episodes.buildTVShowEpisodeUri(getHostInfo().getId(), tvshowId, episodeId);
return new CursorLoader(getActivity(), uri,
EpisodeDetailsQuery.PROJECTION, null, null, null);
// case LOADER_CAST:
// uri = MediaContract.MovieCast.buildMovieCastListUri(hostInfo.getId(), episodeId);
// return new CursorLoader(getActivity(), uri,
// MovieCastListQuery.PROJECTION, null, null, MovieCastListQuery.SORT_BY_NAME_IGNORE_ARTICLES);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_EPISODE:
displayEpisodeDetails(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.fab)
public void onFabClicked(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.episodeid = episodeId;
Player.Open action = new Player.Open(item);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Check whether we should switch to the remote
boolean switchToRemote = PreferenceManager
.getDefaultSharedPreferences(getActivity())
.getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
if (switchToRemote) {
int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.add_to_playlist)
public void onAddToPlaylistClicked(View v) {
Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
getPlaylists.execute(getHostManager().getConnection(), new ApiCallback<ArrayList<PlaylistType.GetPlaylistsReturnType>>() {
@Override
public void onSuccess(ArrayList<PlaylistType.GetPlaylistsReturnType> result) {
if (!isAdded()) return;
// Ok, loop through the playlists, looking for the video one
int videoPlaylistId = -1;
for (PlaylistType.GetPlaylistsReturnType playlist : result) {
if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.VIDEO)) {
videoPlaylistId = playlist.playlistid;
break;
}
}
// If found, add to playlist
if (videoPlaylistId != -1) {
PlaylistType.Item item = new PlaylistType.Item();
item.episodeid = episodeId;
Playlist.Add action = new Playlist.Add(videoPlaylistId, item);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
} else {
Toast.makeText(getActivity(), R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
.show();
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.seen)
public void onSeenClicked(View v) {
// Set the playcount
Integer playcount = (Integer)v.getTag();
int newPlaycount = (playcount > 0) ? 0 : 1;
VideoLibrary.SetEpisodeDetails action =
new VideoLibrary.SetEpisodeDetails(episodeId, newPlaycount, null);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
// Force a refresh, but don't show a message
if (!isAdded()) return;
startSync(true);
}
@Override
public void onError(int errorCode, String description) { }
}, callbackHandler);
// Change the button, to provide imeddiate feedback, even if it isn't yet stored in the db
// (will be properly updated and refreshed after the refresh callback ends)
setupSeenButton(newPlaycount);
}
@Override
protected void onDownload() {
if (tvshowDownloadInfo == null) {
// Nothing to download
Toast.makeText(getActivity(), R.string.no_files_to_download, Toast.LENGTH_SHORT).show();
return;
}
DialogInterface.OnClickListener noopClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) { }
};
// Check if the directory exists and whether to overwrite it
File file = new File(tvshowDownloadInfo.getAbsoluteFilePath());
if (file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.download_file_exists)
.setPositiveButton(R.string.overwrite,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
tvshowDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNeutralButton(R.string.download_with_new_name,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
tvshowDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
} else {
// Confirm that the user really wants to download the file
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.confirm_episode_download)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
tvshowDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
}
}
/**
* Display the episode details
*
* @param cursor Cursor with the data
*/
private void displayEpisodeDetails(Cursor cursor) {
cursor.moveToFirst();
mediaTitle.setText(cursor.getString(EpisodeDetailsQuery.TITLE));
mediaUndertitle.setText(cursor.getString(EpisodeDetailsQuery.SHOWTITLE));
int runtime = cursor.getInt(EpisodeDetailsQuery.RUNTIME) / 60;
String durationPremiered = runtime > 0 ?
String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
" | " + cursor.getString(EpisodeDetailsQuery.FIRSTAIRED) :
cursor.getString(EpisodeDetailsQuery.FIRSTAIRED);
mediaPremiered.setText(durationPremiered);
String season = String.format(getString(R.string.season_episode),
cursor.getInt(EpisodeDetailsQuery.SEASON),
cursor.getInt(EpisodeDetailsQuery.EPISODE));
mediaSeason.setText(season);
double rating = cursor.getDouble(EpisodeDetailsQuery.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);
}
mediaDescription.setText(cursor.getString(EpisodeDetailsQuery.PLOT));
mediaDirectors.setText(cursor.getString(EpisodeDetailsQuery.DIRECTOR));
setupSeenButton(cursor.getInt(EpisodeDetailsQuery.PLAYCOUNT));
// 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.loadImageIntoImageview(hostManager,
// cursor.getString(EpisodeDetailsQuery.THUMBNAIL),
// mediaPoster, posterWidth, posterHeight);
int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height);
UIUtils.loadImageIntoImageview(getHostManager(),
cursor.getString(EpisodeDetailsQuery.THUMBNAIL),
mediaArt, displayMetrics.widthPixels, artHeight);
// Setup movie download info
tvshowDownloadInfo = new FileDownloadHelper.TVShowInfo(
cursor.getString(EpisodeDetailsQuery.SHOWTITLE),
cursor.getInt(EpisodeDetailsQuery.SEASON),
cursor.getInt(EpisodeDetailsQuery.EPISODE),
cursor.getString(EpisodeDetailsQuery.TITLE),
cursor.getString(EpisodeDetailsQuery.FILE));
// Check if downloaded file exists
downloadButton.setVisibility(View.VISIBLE);
if (tvshowDownloadInfo.downloadFileExists()) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{
R.attr.colorAccent});
downloadButton.setColorFilter(
styledAttributes.getColor(0, getResources().getColor(R.color.accent_default)));
styledAttributes.recycle();
} else {
downloadButton.clearColorFilter();
}
}
private void setupSeenButton(int playcount) {
// Seen button
if (playcount > 0) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] {
R.attr.colorAccent});
seenButton.setColorFilter(styledAttributes.getColor(0, getResources().getColor(R.color.accent_default)));
styledAttributes.recycle();
} else {
seenButton.clearColorFilter();
}
// Save the playcount
seenButton.setTag(playcount);
}
//
// /**
// * 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()) {
// List<VideoType.Cast> castList = new ArrayList<VideoType.Cast>(cursor.getCount());
// do {
// castList.add(new VideoType.Cast(cursor.getString(MovieCastListQuery.NAME),
// cursor.getInt(MovieCastListQuery.ORDER),
// cursor.getString(MovieCastListQuery.ROLE),
// cursor.getString(MovieCastListQuery.THUMBNAIL)));
// } while (cursor.moveToNext());
//
// UIUtils.setupCastInfo(getActivity(), castList, videoCastList,
// videoAdditionalCastTitle, videoAdditionalCastList);
// }
// }
//
/**
* Episode details query parameters.
*/
private interface EpisodeDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Episodes.TITLE,
MediaContract.Episodes.SHOWTITLE,
MediaContract.Episodes.SEASON,
MediaContract.Episodes.EPISODE,
MediaContract.Episodes.THUMBNAIL,
MediaContract.Episodes.FANART,
MediaContract.Episodes.FIRSTAIRED,
MediaContract.Episodes.RUNTIME,
MediaContract.Episodes.RATING,
MediaContract.Episodes.PLOT,
MediaContract.Episodes.PLAYCOUNT,
MediaContract.Episodes.DIRECTOR,
MediaContract.Episodes.WRITER,
MediaContract.Episodes.FILE,
};
final int ID = 0;
final int TITLE = 1;
final int SHOWTITLE = 2;
final int SEASON = 3;
final int EPISODE = 4;
final int THUMBNAIL = 5;
final int FANART = 6;
final int FIRSTAIRED = 7;
final int RUNTIME = 8;
final int RATING = 9;
final int PLOT = 10;
final int PLAYCOUNT = 11;
final int DIRECTOR = 12;
final int WRITER = 13;
final int FILE = 14;
}
// /**
// * Movie cast list query parameters.
// */
// private interface MovieCastListQuery {
// String[] PROJECTION = {
// MediaContract.MovieCast.ID,
// MediaContract.MovieCast.NAME,
// MediaContract.MovieCast.ORDER,
// MediaContract.MovieCast.ROLE,
// MediaContract.MovieCast.THUMBNAIL,
// };
//
// String SORT_BY_NAME_IGNORE_ARTICLES = MediaContract.MovieCast.ORDER + " ASC";
//
// final int ID = 0;
// final int NAME = 1;
// final int ORDER = 2;
// final int ROLE = 3;
// final int THUMBNAIL = 4;
// }
}

View File

@ -0,0 +1,341 @@
/*
* 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.sections.video;
import android.content.DialogInterface;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.BaseColumns;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageButton;
import org.xbmc.kore.R;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.Utils;
import java.io.File;
/**
* Presents movie details
*/
public class TVShowEpisodeInfoFragment extends AbstractInfoFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(TVShowEpisodeInfoFragment.class);
public static final String BUNDLE_KEY_TVSHOWID = "tvshow_id";
// Loader IDs
private static final int LOADER_EPISODE = 0;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
// Displayed episode
private int tvshowId = -1;
private Cursor cursor;
FileDownloadHelper.TVShowInfo fileDownloadHelper;
public void setTvshowId(int tvshowId) {
getDataHolder().getBundle().putInt(BUNDLE_KEY_TVSHOWID, tvshowId);
}
@Override
protected RefreshItem createRefreshItem() {
RefreshItem refreshItem = new RefreshItem(getActivity(),
LibrarySyncService.SYNC_SINGLE_TVSHOW);
refreshItem.setSyncItem(LibrarySyncService.SYNC_TVSHOWID, tvshowId);
refreshItem.setListener(new RefreshItem.RefreshItemListener() {
@Override
public void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_EPISODE, null,
TVShowEpisodeInfoFragment.this);
}
}
});
return refreshItem;
}
@Override
protected boolean setupMediaActionBar() {
setOnDownloadListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
downloadEpisode();
}
});
setOnAddToPlaylistListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Utils.addToPlaylist(TVShowEpisodeInfoFragment.this, getDataHolder().getId(),
PlaylistType.GetPlaylistsReturnType.VIDEO);
}
});
setOnSeenListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Integer playcount = cursor.getInt(EpisodeDetailsQuery.PLAYCOUNT);
int newPlaycount = (playcount > 0) ? 0 : 1;
VideoLibrary.SetEpisodeDetails action =
new VideoLibrary.SetEpisodeDetails(getDataHolder().getId(), newPlaycount, null);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
// Force a refresh, but don't show a message
if (!isAdded()) return;
getRefreshItem().startSync(true);
}
@Override
public void onError(int errorCode, String description) { }
}, callbackHandler);
}
});
return true;
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.episodeid = getDataHolder().getId();
fabActionPlayItem(item);
}
});
return true;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.tvshowId = getArguments().getInt(BUNDLE_KEY_TVSHOWID, -1);
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(LOADER_EPISODE, null, this);
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_EPISODE:
uri = MediaContract.Episodes.buildTVShowEpisodeUri(getHostInfo().getId(), tvshowId,
getDataHolder().getId());
return new CursorLoader(getActivity(), uri,
EpisodeDetailsQuery.PROJECTION, null, null, null);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_EPISODE:
cursor.moveToFirst();
this.cursor = cursor;
DataHolder dataHolder = getDataHolder();
dataHolder.setPosterUrl(cursor.getString(EpisodeDetailsQuery.THUMBNAIL));
dataHolder.setRating(cursor.getDouble(EpisodeDetailsQuery.RATING));
dataHolder.setMaxRating(10);
String director = cursor.getString(EpisodeDetailsQuery.DIRECTOR);
if (!TextUtils.isEmpty(director)) {
director = getActivity().getResources().getString(R.string.directors) + " " + director;
}
int runtime = cursor.getInt(EpisodeDetailsQuery.RUNTIME) / 60;
String durationPremiered = runtime > 0 ?
String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
" | " + cursor.getString(EpisodeDetailsQuery.FIRSTAIRED) :
cursor.getString(EpisodeDetailsQuery.FIRSTAIRED);
String season = String.format(getString(R.string.season_episode),
cursor.getInt(EpisodeDetailsQuery.SEASON),
cursor.getInt(EpisodeDetailsQuery.EPISODE));
dataHolder.setDetails(durationPremiered + "\n" + season + "\n" + director);
fileDownloadHelper = new FileDownloadHelper.TVShowInfo(
cursor.getString(EpisodeDetailsQuery.SHOWTITLE),
cursor.getInt(EpisodeDetailsQuery.SEASON),
cursor.getInt(EpisodeDetailsQuery.EPISODE),
cursor.getString(EpisodeDetailsQuery.TITLE),
cursor.getString(EpisodeDetailsQuery.FILE));
setDownloadButtonState(fileDownloadHelper.downloadFileExists());
setSeenButtonState(cursor.getInt(EpisodeDetailsQuery.PLAYCOUNT) > 0);
getDataHolder().setTitle(cursor.getString(EpisodeDetailsQuery.TITLE));
getDataHolder().setUndertitle(cursor.getString(EpisodeDetailsQuery.SHOWTITLE));
setExpandDescription(true);
getDataHolder().setDescription(cursor.getString(EpisodeDetailsQuery.PLOT));
updateView(dataHolder);
break;
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
private void downloadEpisode() {
final FileDownloadHelper.TVShowInfo tvshowDownloadInfo = new FileDownloadHelper.TVShowInfo(
cursor.getString(EpisodeDetailsQuery.SHOWTITLE),
cursor.getInt(EpisodeDetailsQuery.SEASON),
cursor.getInt(EpisodeDetailsQuery.EPISODE),
cursor.getString(EpisodeDetailsQuery.TITLE),
cursor.getString(EpisodeDetailsQuery.FILE));
DialogInterface.OnClickListener noopClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) { }
};
// Check if the directory exists and whether to overwrite it
File file = new File(tvshowDownloadInfo.getAbsoluteFilePath());
if (file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.download_file_exists)
.setPositiveButton(R.string.overwrite,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
tvshowDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNeutralButton(R.string.download_with_new_name,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
tvshowDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
} else {
// Confirm that the user really wants to download the file
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.confirm_episode_download)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
tvshowDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
}
}
@Override
protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() {
return null;
}
/**
* Episode details query parameters.
*/
private interface EpisodeDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Episodes.TITLE,
MediaContract.Episodes.SHOWTITLE,
MediaContract.Episodes.SEASON,
MediaContract.Episodes.EPISODE,
MediaContract.Episodes.THUMBNAIL,
MediaContract.Episodes.FANART,
MediaContract.Episodes.FIRSTAIRED,
MediaContract.Episodes.RUNTIME,
MediaContract.Episodes.RATING,
MediaContract.Episodes.PLOT,
MediaContract.Episodes.PLAYCOUNT,
MediaContract.Episodes.DIRECTOR,
MediaContract.Episodes.WRITER,
MediaContract.Episodes.FILE,
};
int ID = 0;
int TITLE = 1;
int SHOWTITLE = 2;
int SEASON = 3;
int EPISODE = 4;
int THUMBNAIL = 5;
int FANART = 6;
int FIRSTAIRED = 7;
int RUNTIME = 8;
int RATING = 9;
int PLOT = 10;
int PLAYCOUNT = 11;
int DIRECTOR = 12;
int WRITER = 13;
int FILE = 14;
}
}

View File

@ -33,10 +33,10 @@ 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.CursorAdapter;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.PopupMenu; import android.widget.PopupMenu;
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;
@ -46,10 +46,10 @@ 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.ui.AbstractCursorListFragment; import org.xbmc.kore.ui.AbstractCursorListFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
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;
/** /**
* Presents a list of episodes for a TV show season * Presents a list of episodes for a TV show season
@ -58,7 +58,7 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment {
private static final String TAG = LogUtils.makeLogTag(TVShowEpisodeListFragment.class); private static final String TAG = LogUtils.makeLogTag(TVShowEpisodeListFragment.class);
public interface OnEpisodeSelectedListener { public interface OnEpisodeSelectedListener {
void onEpisodeSelected(EpisodeViewHolder vh); void onEpisodeSelected(int tvshowId, ViewHolder dataHolder);
} }
public static final String TVSHOWID = "tvshow_id"; public static final String TVSHOWID = "tvshow_id";
@ -109,9 +109,9 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment {
@Override @Override
protected void onListItemClicked(View view) { protected void onListItemClicked(View view) {
// Get the movie id from the tag // Get the movie id from the tag
EpisodeViewHolder tag = (EpisodeViewHolder) view.getTag(); ViewHolder tag = (ViewHolder) view.getTag();
// Notify the activity // Notify the activity
listenerActivity.onEpisodeSelected(tag); listenerActivity.onEpisodeSelected(tvshowId, tag);
} }
@ -253,7 +253,7 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment {
.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(); ViewHolder viewHolder = new ViewHolder();
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);
@ -269,15 +269,11 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment {
@TargetApi(21) @TargetApi(21)
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, Cursor cursor) {
final EpisodeViewHolder viewHolder = (EpisodeViewHolder)view.getTag(); final ViewHolder viewHolder = (ViewHolder)view.getTag();
// Save the episode id // Save the episode id
viewHolder.episodeId = cursor.getInt(EpisodesListQuery.EPISODEID); viewHolder.dataHolder.setId(cursor.getInt(EpisodesListQuery.EPISODEID));
viewHolder.title = cursor.getString(EpisodesListQuery.TITLE); viewHolder.dataHolder.setTitle(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),
@ -298,7 +294,8 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment {
} }
UIUtils.loadImageWithCharacterAvatar(context, hostManager, UIUtils.loadImageWithCharacterAvatar(context, hostManager,
cursor.getString(EpisodesListQuery.THUMBNAIL), viewHolder.title, cursor.getString(EpisodesListQuery.THUMBNAIL),
viewHolder.dataHolder.getTitle(),
viewHolder.artView, artWidth, artHeight); viewHolder.artView, artWidth, artHeight);
// For the popupmenu // For the popupmenu
@ -311,7 +308,7 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment {
/** /**
* View holder pattern, only for episodes * View holder pattern, only for episodes
*/ */
public static class EpisodeViewHolder { public static class ViewHolder {
TextView titleView; TextView titleView;
TextView detailsView; TextView detailsView;
TextView episodenumberView; TextView episodenumberView;
@ -319,17 +316,16 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment {
ImageView checkmarkView; ImageView checkmarkView;
ImageView artView; ImageView artView;
int episodeId; AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0);
String title;
} }
private View.OnClickListener contextlistItemMenuClickListener = new View.OnClickListener() { private View.OnClickListener contextlistItemMenuClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(final View v) { public void onClick(final View v) {
final EpisodeViewHolder viewHolder = (EpisodeViewHolder)v.getTag(); final ViewHolder viewHolder = (ViewHolder)v.getTag();
final PlaylistType.Item playListItem = new PlaylistType.Item(); final PlaylistType.Item playListItem = new PlaylistType.Item();
playListItem.episodeid = viewHolder.episodeId; playListItem.episodeid = viewHolder.dataHolder.getId();
final PopupMenu popupMenu = new PopupMenu(getActivity(), v); final PopupMenu popupMenu = new PopupMenu(getActivity(), v);
popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu()); popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu());

View File

@ -0,0 +1,210 @@
/*
* 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.sections.video;
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.widget.ImageButton;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.utils.LogUtils;
/**
* Presents a TV Show overview
*/
public class TVShowInfoFragment extends AbstractInfoFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(TVShowInfoFragment.class);
// Loader IDs
private static final int LOADER_TVSHOW = 0;
// Controls whether a automatic sync refresh has been issued for this show
private static boolean hasIssuedOutdatedRefresh = false;
@Override
protected RefreshItem createRefreshItem() {
RefreshItem refreshItem = new RefreshItem(getActivity(),
LibrarySyncService.SYNC_SINGLE_TVSHOW);
refreshItem.setSyncItem(LibrarySyncService.SYNC_TVSHOWID, getDataHolder().getId());
refreshItem.setListener(new RefreshItem.RefreshItemListener() {
@Override
public void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_TVSHOW, null,
TVShowInfoFragment.this);
refreshAdditionInfoFragment();
}
}
});
return refreshItem;
}
@Override
protected boolean setupMediaActionBar() {
return false;
}
@Override
protected boolean setupFAB(ImageButton FAB) {
return false;
}
@Override
protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() {
TVShowProgressFragment tvShowProgressFragment = new TVShowProgressFragment();
tvShowProgressFragment.setArgs(getDataHolder().getId(), getDataHolder().getTitle());
return tvShowProgressFragment;
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
hasIssuedOutdatedRefresh = false;
getLoaderManager().initLoader(LOADER_TVSHOW, null, this);
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri = MediaContract.TVShows.buildTVShowUri(getHostInfo().getId(), getDataHolder().getId());
return new CursorLoader(getActivity(), uri,
TVShowDetailsQuery.PROJECTION, null, null, null);
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null) {
switch (cursorLoader.getId()) {
case LOADER_TVSHOW:
cursor.moveToFirst();
DataHolder dataHolder = getDataHolder();
dataHolder.setFanArtUrl(cursor.getString(TVShowDetailsQuery.FANART));
dataHolder.setPosterUrl(cursor.getString(TVShowDetailsQuery.THUMBNAIL));
dataHolder.setRating(cursor.getDouble(TVShowDetailsQuery.RATING));
dataHolder.setMaxRating(10);
String premiered = cursor.getString(TVShowDetailsQuery.PREMIERED);
String studio = cursor.getString(TVShowDetailsQuery.STUDIO);
dataHolder.setDetails(String.format(getString(R.string.premiered), premiered) + " | " + studio +
"\n" +
cursor.getString(TVShowDetailsQuery.GENRES));
dataHolder.setTitle(cursor.getString(TVShowDetailsQuery.TITLE));
int numEpisodes = cursor.getInt(TVShowDetailsQuery.EPISODE),
watchedEpisodes = cursor.getInt(TVShowDetailsQuery.WATCHEDEPISODES);
dataHolder.setUndertitle(String.format(getString(R.string.num_episodes),
numEpisodes, numEpisodes - watchedEpisodes));
dataHolder.setDescription(cursor.getString(TVShowDetailsQuery.PLOT));
updateView(dataHolder);
checkOutdatedTVShowDetails(cursor);
break;
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
/**
* 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;
getRefreshItem().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,
};
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;
}
}

View File

@ -44,6 +44,7 @@ import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.provider.MediaDatabase; import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractCursorListFragment; import org.xbmc.kore.ui.AbstractCursorListFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils; import org.xbmc.kore.utils.Utils;
@ -96,8 +97,8 @@ public class TVShowListFragment extends AbstractCursorListFragment {
if (selection.length() != 0) if (selection.length() != 0)
selection.append(" AND "); selection.append(" AND ");
selection.append(MediaContract.TVShowsColumns.WATCHEDEPISODES) selection.append(MediaContract.TVShowsColumns.WATCHEDEPISODES)
.append("!=") .append("!=")
.append(MediaContract.TVShowsColumns.EPISODE); .append(MediaContract.TVShowsColumns.EPISODE);
} }
String sortOrderStr; String sortOrderStr;
@ -121,8 +122,8 @@ public class TVShowListFragment extends AbstractCursorListFragment {
return new CursorLoader(getActivity(), uri, return new CursorLoader(getActivity(), uri,
TVShowListQuery.PROJECTION, selection.toString(), TVShowListQuery.PROJECTION, selection.toString(),
selectionArgs, sortOrderStr); selectionArgs, sortOrderStr);
} }
@Override @Override
@ -194,43 +195,43 @@ public class TVShowListFragment extends AbstractCursorListFragment {
case R.id.action_hide_watched: case R.id.action_hide_watched:
item.setChecked(!item.isChecked()); item.setChecked(!item.isChecked());
preferences.edit() preferences.edit()
.putBoolean(Settings.KEY_PREF_TVSHOWS_FILTER_HIDE_WATCHED, item.isChecked()) .putBoolean(Settings.KEY_PREF_TVSHOWS_FILTER_HIDE_WATCHED, item.isChecked())
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_ignore_prefixes: case R.id.action_ignore_prefixes:
item.setChecked(!item.isChecked()); item.setChecked(!item.isChecked());
preferences.edit() preferences.edit()
.putBoolean(Settings.KEY_PREF_TVSHOWS_IGNORE_PREFIXES, item.isChecked()) .putBoolean(Settings.KEY_PREF_TVSHOWS_IGNORE_PREFIXES, item.isChecked())
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_sort_by_name: case R.id.action_sort_by_name:
item.setChecked(true); item.setChecked(true);
preferences.edit() preferences.edit()
.putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_NAME) .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_NAME)
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_sort_by_year: case R.id.action_sort_by_year:
item.setChecked(true); item.setChecked(true);
preferences.edit() preferences.edit()
.putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_YEAR) .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_YEAR)
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_sort_by_rating: case R.id.action_sort_by_rating:
item.setChecked(true); item.setChecked(true);
preferences.edit() preferences.edit()
.putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_RATING) .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_RATING)
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_sort_by_date_added: case R.id.action_sort_by_date_added:
item.setChecked(true); item.setChecked(true);
preferences.edit() preferences.edit()
.putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_DATE_ADDED) .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_DATE_ADDED)
.apply(); .apply();
refreshList(); refreshList();
break; break;
case R.id.action_sort_by_last_played: case R.id.action_sort_by_last_played:
@ -266,7 +267,7 @@ public class TVShowListFragment extends AbstractCursorListFragment {
MediaContract.TVShows.PLAYCOUNT, MediaContract.TVShows.PLAYCOUNT,
MediaContract.TVShows.IMDBNUMBER, MediaContract.TVShows.IMDBNUMBER,
MediaContract.TVShows.GENRES, MediaContract.TVShows.GENRES,
}; };
String SORT_BY_NAME = MediaContract.TVShows.TITLE + " ASC"; String SORT_BY_NAME = MediaContract.TVShows.TITLE + " ASC";
String SORT_BY_YEAR = MediaContract.TVShows.PREMIERED + " ASC"; String SORT_BY_YEAR = MediaContract.TVShows.PREMIERED + " ASC";
@ -305,22 +306,21 @@ public class TVShowListFragment extends AbstractCursorListFragment {
// the user transitions to that fragment, avoiding another call and imediatelly showing the image // the user transitions to that fragment, avoiding another call and imediatelly showing the image
Resources resources = context.getResources(); Resources resources = context.getResources();
artWidth = (int)(resources.getDimension(R.dimen.now_playing_poster_width) / artWidth = (int)(resources.getDimension(R.dimen.now_playing_poster_width) /
UIUtils.IMAGE_RESIZE_FACTOR); UIUtils.IMAGE_RESIZE_FACTOR);
artHeight = (int)(resources.getDimension(R.dimen.now_playing_poster_height) / artHeight = (int)(resources.getDimension(R.dimen.now_playing_poster_height) /
UIUtils.IMAGE_RESIZE_FACTOR); UIUtils.IMAGE_RESIZE_FACTOR);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public View newView(Context context, final Cursor cursor, ViewGroup parent) { public View newView(Context context, final Cursor cursor, ViewGroup parent) {
final View view = LayoutInflater.from(context) final View view = LayoutInflater.from(context)
.inflate(R.layout.grid_item_tvshow, parent, false); .inflate(R.layout.grid_item_tvshow, parent, false);
// Setup View holder pattern // Setup View holder pattern
ViewHolder viewHolder = new ViewHolder(); ViewHolder viewHolder = new ViewHolder();
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.yearView = (TextView)view.findViewById(R.id.year);
viewHolder.premieredView = (TextView)view.findViewById(R.id.premiered); viewHolder.premieredView = (TextView)view.findViewById(R.id.premiered);
viewHolder.artView = (ImageView)view.findViewById(R.id.art); viewHolder.artView = (ImageView)view.findViewById(R.id.art);
@ -335,31 +335,32 @@ public class TVShowListFragment extends AbstractCursorListFragment {
final ViewHolder viewHolder = (ViewHolder)view.getTag(); final ViewHolder viewHolder = (ViewHolder)view.getTag();
// Save the movie id // Save the movie id
viewHolder.tvshowId = cursor.getInt(TVShowListQuery.TVSHOWID); viewHolder.dataHolder.setId(cursor.getInt(TVShowListQuery.TVSHOWID));
viewHolder.tvshowTitle = cursor.getString(TVShowListQuery.TITLE); viewHolder.dataHolder.setTitle(cursor.getString(TVShowListQuery.TITLE));
viewHolder.episode = cursor.getInt(TVShowListQuery.EPISODE); viewHolder.dataHolder.setDescription(cursor.getString(TVShowListQuery.PLOT));
viewHolder.genres = cursor.getString(TVShowListQuery.GENRES); viewHolder.dataHolder.setRating(cursor.getInt(TVShowListQuery.RATING));
viewHolder.plot = cursor.getString(TVShowListQuery.PLOT); int episode = cursor.getInt(TVShowListQuery.EPISODE);
viewHolder.premiered = cursor.getString(TVShowListQuery.PREMIERED); int watchedEpisodes = cursor.getInt(TVShowListQuery.WATCHEDEPISODES);
viewHolder.rating = cursor.getInt(TVShowListQuery.RATING);
viewHolder.studio = cursor.getString(TVShowListQuery.STUDIO);
viewHolder.watchedEpisodes = cursor.getInt(TVShowListQuery.WATCHEDEPISODES);
if(Utils.isLollipopOrLater()) { viewHolder.titleView.setText(viewHolder.dataHolder.getTitle());
viewHolder.artView.setTransitionName("a"+viewHolder.tvshowId);
}
viewHolder.titleView.setText(viewHolder.tvshowTitle);
String details = String.format(context.getString(R.string.num_episodes), String details = String.format(context.getString(R.string.num_episodes),
viewHolder.episode, viewHolder.episode - viewHolder.watchedEpisodes); episode, episode - watchedEpisodes);
viewHolder.detailsView.setText(details); viewHolder.detailsView.setText(details);
viewHolder.dataHolder.setUndertitle(details);
String premiered = String.format(context.getString(R.string.premiered), String premiered = String.format(context.getString(R.string.premiered),
viewHolder.premiered); cursor.getString(TVShowListQuery.PREMIERED));
viewHolder.premieredView.setText(premiered); viewHolder.premieredView.setText(premiered);
viewHolder.dataHolder.setDetails(premiered);
viewHolder.dataHolder.setPosterUrl(cursor.getString(TVShowListQuery.THUMBNAIL));
UIUtils.loadImageWithCharacterAvatar(context, hostManager, UIUtils.loadImageWithCharacterAvatar(context, hostManager,
cursor.getString(TVShowListQuery.THUMBNAIL), viewHolder.tvshowTitle, viewHolder.dataHolder.getPosterUrl(),
viewHolder.artView, artWidth, artHeight); viewHolder.dataHolder.getTitle(),
viewHolder.artView, artWidth, artHeight);
if (Utils.isLollipopOrLater()) {
viewHolder.artView.setTransitionName("a" + viewHolder.dataHolder.getId());
}
} }
} }
@ -369,18 +370,9 @@ public class TVShowListFragment extends AbstractCursorListFragment {
public static class ViewHolder { public static class ViewHolder {
TextView titleView; TextView titleView;
TextView detailsView; TextView detailsView;
// TextView yearView;
TextView premieredView; TextView premieredView;
ImageView artView; ImageView artView;
int tvshowId; AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0);
String tvshowTitle;
String premiered;
String studio;
int episode;
int watchedEpisodes;
double rating;
String plot;
String genres;
} }
} }

View File

@ -0,0 +1,394 @@
/*
* Copyright 2017 Martijn Brekhof. 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.sections.video;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.BaseColumns;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.widget.PopupMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.CastFragment;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.MediaPlayerUtils;
import org.xbmc.kore.utils.UIUtils;
public class TVShowProgressFragment extends AbstractAdditionalInfoFragment implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(TVShowProgressFragment.class);
public static final String BUNDLE_ITEM_ID = "itemid";
public static final String BUNDLE_TITLE = "title";
private static final int NEXT_EPISODES_COUNT = 2;
private int itemId = -1;
private CastFragment castFragment;
public static final int LOADER_NEXT_EPISODES = 1,
LOADER_SEASONS = 2;
public interface TVShowProgressActionListener {
void onSeasonSelected(int tvshowId, int season);
void onNextEpisodeSelected(int tvshowId, AbstractInfoFragment.DataHolder dataHolder);
}
// Activity listener
private TVShowProgressActionListener listenerActivity;
public void setArgs(int itemId, String showTitle) {
Bundle bundle = new Bundle();
bundle.putInt(BUNDLE_ITEM_ID, itemId);
bundle.putString(BUNDLE_TITLE, showTitle);
setArguments(bundle);
}
@Override
public void refresh() {
getLoaderManager().restartLoader(LOADER_NEXT_EPISODES, null, this);
getLoaderManager().restartLoader(LOADER_SEASONS, null, this);
castFragment.refresh();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
listenerActivity = (TVShowProgressActionListener) context;
} catch (ClassCastException e) {
throw new ClassCastException(context.toString() + " must implement TVShowProgressActionListener");
}
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Bundle arguments = getArguments();
if (arguments == null) {
throw new IllegalStateException("Use setArgs to set required item id");
}
this.itemId = arguments.getInt(BUNDLE_ITEM_ID);
String title = arguments.getString(BUNDLE_TITLE);
View view = inflater.inflate(R.layout.fragment_tvshow_progress, container, false);
castFragment = new CastFragment();
castFragment.setArgs(this.itemId, title, CastFragment.TYPE.TVSHOW);
getActivity().getSupportFragmentManager()
.beginTransaction()
.add(R.id.cast_fragment, castFragment)
.commit();
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(LOADER_NEXT_EPISODES, null, this);
getLoaderManager().initLoader(LOADER_SEASONS, null, this);
}
@Override
public void onDetach() {
super.onDetach();
listenerActivity = null;
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri;
int hostId = HostManager.getInstance(getActivity()).getHostInfo().getId();
switch (id) {
case LOADER_NEXT_EPISODES:
// Load seasons
uri = MediaContract.Episodes.buildTVShowEpisodesListUri(hostId, itemId, 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(hostId, itemId);
return new CursorLoader(getActivity(), uri,
SeasonsListQuery.PROJECTION, null, null, SeasonsListQuery.SORT);
default:
return null;
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (data != null) {
switch (loader.getId()) {
case LOADER_NEXT_EPISODES:
displayNextEpisodeList(data);
break;
case LOADER_SEASONS:
displaySeasonList(data);
break;
}
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
/**
* Display next episode list
*
* @param cursor Cursor with the data
*/
@TargetApi(21)
private void displayNextEpisodeList(Cursor cursor) {
TextView nextEpisodeTitle = (TextView) getActivity().findViewById(R.id.next_episode_title);
GridLayout nextEpisodeList = (GridLayout) getActivity().findViewById(R.id.next_episode_list);
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) {
AbstractInfoFragment.DataHolder vh = (AbstractInfoFragment.DataHolder) v.getTag();
listenerActivity.onNextEpisodeSelected(itemId, vh);
}
};
// Get the art dimensions
Resources resources = getActivity().getResources();
int artWidth = (int)(resources.getDimension(R.dimen.detail_poster_width_square) /
UIUtils.IMAGE_RESIZE_FACTOR);
int artHeight = (int)(resources.getDimension(R.dimen.detail_poster_height_square) /
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);
AbstractInfoFragment.DataHolder vh = new AbstractInfoFragment.DataHolder(episodeId);
vh.setTitle(title);
vh.setUndertitle(seasonEpisode);
episodeView.setTag(vh);
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
*
* @param cursor Cursor with the data
*/
private void displaySeasonList(Cursor cursor) {
TextView seasonsListTitle = (TextView) getActivity().findViewById(R.id.seasons_title);
GridLayout seasonsList = (GridLayout) getActivity().findViewById(R.id.seasons_list);
if (cursor.moveToFirst()) {
seasonsListTitle.setVisibility(View.VISIBLE);
seasonsList.setVisibility(View.VISIBLE);
HostManager hostManager = HostManager.getInstance(getActivity());
View.OnClickListener seasonListClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
listenerActivity.onSeasonSelected(itemId, (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);
ProgressBar seasonProgressBar = (ProgressBar) seasonView.findViewById(R.id.season_progress_bar);
seasonNumberView.setText(String.format(getActivity().getString(R.string.season_number), seasonNumber));
seasonEpisodesView.setText(String.format(getActivity().getString(R.string.num_episodes),
numEpisodes, numEpisodes - watchedEpisodes));
seasonProgressBar.setMax(numEpisodes);
seasonProgressBar.setProgress(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.GONE);
}
}
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(TVShowProgressFragment.this, playListItem);
return true;
case R.id.action_queue:
MediaPlayerUtils.queue(TVShowProgressFragment.this, playListItem, PlaylistType.GetPlaylistsReturnType.VIDEO);
return true;
}
return false;
}
});
popupMenu.show();
}
};
/**
* 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.
*/
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;
}
}

View File

@ -18,34 +18,32 @@ package org.xbmc.kore.ui.sections.video;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout; import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.transition.Transition;
import android.transition.TransitionInflater; import android.transition.TransitionInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View;
import android.view.Window; import android.view.Window;
import org.xbmc.kore.R; import org.xbmc.kore.R;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.BaseActivity; import org.xbmc.kore.ui.BaseActivity;
import org.xbmc.kore.ui.generic.NavigationDrawerFragment; import org.xbmc.kore.ui.generic.NavigationDrawerFragment;
import org.xbmc.kore.ui.sections.remote.RemoteActivity; import org.xbmc.kore.ui.sections.remote.RemoteActivity;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.SharedElementTransition;
import org.xbmc.kore.utils.Utils; import org.xbmc.kore.utils.Utils;
import java.util.List;
import java.util.Map;
/** /**
* Controls the presentation of TV Shows information (list, details) * Controls the presentation of TV Shows information (list, details)
* All the information is presented by specific fragments * All the information is presented by specific fragments
*/ */
public class TVShowsActivity extends BaseActivity public class TVShowsActivity extends BaseActivity
implements TVShowListFragment.OnTVShowSelectedListener, implements TVShowListFragment.OnTVShowSelectedListener,
TVShowDetailsFragment.TVShowDetailsActionListener, TVShowProgressFragment.TVShowProgressActionListener,
TVShowEpisodeListFragment.OnEpisodeSelectedListener { TVShowEpisodeListFragment.OnEpisodeSelectedListener {
private static final String TAG = LogUtils.makeLogTag(TVShowsActivity.class); private static final String TAG = LogUtils.makeLogTag(TVShowsActivity.class);
@ -54,6 +52,7 @@ public class TVShowsActivity extends BaseActivity
public static final String EPISODEID = "episode_id"; public static final String EPISODEID = "episode_id";
public static final String SEASON = "season"; public static final String SEASON = "season";
public static final String SEASONTITLE = "season_title"; public static final String SEASONTITLE = "season_title";
public static final String LISTFRAGMENT_TAG = "tvshowlist";
private int selectedTVShowId = -1; private int selectedTVShowId = -1;
private String selectedTVShowTitle = null; private String selectedTVShowTitle = null;
@ -61,7 +60,7 @@ public class TVShowsActivity extends BaseActivity
private String selectedSeasonTitle = null; private String selectedSeasonTitle = null;
private int selectedEpisodeId = -1; private int selectedEpisodeId = -1;
private boolean clearSharedElements; private SharedElementTransition sharedElementTransition = new SharedElementTransition();
private NavigationDrawerFragment navigationDrawerFragment; private NavigationDrawerFragment navigationDrawerFragment;
@ -80,38 +79,17 @@ public class TVShowsActivity extends BaseActivity
.findFragmentById(R.id.navigation_drawer); .findFragmentById(R.id.navigation_drawer);
navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout)); navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout));
Fragment fragment;
if (savedInstanceState == null) { if (savedInstanceState == null) {
TVShowListFragment tvshowListFragment = new TVShowListFragment(); fragment = new TVShowListFragment();
// Setup animations
if (Utils.isLollipopOrLater()) {
//Fade added to prevent shared element from disappearing very shortly at the start of the transition.
Transition fade = TransitionInflater
.from(this)
.inflateTransition(android.R.transition.fade);
tvshowListFragment.setExitTransition(fade);
tvshowListFragment.setReenterTransition(fade);
tvshowListFragment.setSharedElementReturnTransition(TransitionInflater.from(
this).inflateTransition(R.transition.change_image));
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() {
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
if (clearSharedElements) {
names.clear();
sharedElements.clear();
clearSharedElements = false;
}
}
};
tvshowListFragment.setExitSharedElementCallback(seCallback);
}
getSupportFragmentManager() getSupportFragmentManager()
.beginTransaction() .beginTransaction()
.add(R.id.fragment_container, tvshowListFragment) .add(R.id.fragment_container, fragment, LISTFRAGMENT_TAG)
.commit(); .commit();
} else { } else {
fragment = getSupportFragmentManager().findFragmentByTag(LISTFRAGMENT_TAG);
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);
@ -119,6 +97,10 @@ public class TVShowsActivity extends BaseActivity
selectedSeasonTitle = savedInstanceState.getString(SEASONTITLE, null); selectedSeasonTitle = savedInstanceState.getString(SEASONTITLE, null);
} }
if (Utils.isLollipopOrLater()) {
sharedElementTransition.setupExitTransition(this, fragment);
}
setupActionBar(selectedTVShowTitle); setupActionBar(selectedTVShowTitle);
// // Setup system bars and content padding, allowing averlap with the bottom bar // // Setup system bars and content padding, allowing averlap with the bottom bar
@ -238,43 +220,19 @@ public class TVShowsActivity extends BaseActivity
*/ */
@TargetApi(21) @TargetApi(21)
public void onTVShowSelected(TVShowListFragment.ViewHolder vh) { public void onTVShowSelected(TVShowListFragment.ViewHolder vh) {
selectedTVShowId = vh.tvshowId; selectedTVShowId = vh.dataHolder.getId();
selectedTVShowTitle = vh.tvshowTitle; selectedTVShowTitle = vh.dataHolder.getTitle();
// Replace list fragment // Replace list fragment
final TVShowDetailsFragment tvshowDetailsFragment = TVShowDetailsFragment.newInstance(vh); final TVShowInfoFragment tvshowDetailsFragment = new TVShowInfoFragment();
tvshowDetailsFragment.setDataHolder(vh.dataHolder);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions // Set up transitions
if (Utils.isLollipopOrLater()) { if (Utils.isLollipopOrLater()) {
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName());
@Override sharedElementTransition.setupEnterTransition(this, fragTrans, tvshowDetailsFragment, vh.artView);
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
//On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements
// for the reentering fragment. We use this to determine if we are returning and if
// we should clear the shared element lists. Note that, clearing must be done in the reentering fragment
// as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might
// be a v4 support package bug.
if (tvshowDetailsFragment.isVisible()) {
View sharedView = tvshowDetailsFragment.getSharedElement();
if (sharedView == null) { // shared element not visible
clearSharedElements = true;
}
}
}
};
tvshowDetailsFragment.setEnterSharedElementCallback(seCallback);
tvshowDetailsFragment.setEnterTransition(
TransitionInflater.from(this)
.inflateTransition(R.transition.media_details));
tvshowDetailsFragment.setReturnTransition(null);
Transition changeImageTransition =
TransitionInflater.from(this)
.inflateTransition(R.transition.change_image);
tvshowDetailsFragment.setSharedElementReturnTransition(changeImageTransition);
tvshowDetailsFragment.setSharedElementEnterTransition(changeImageTransition);
fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName());
} else { } else {
fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0); fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0);
} }
@ -308,15 +266,16 @@ public class TVShowsActivity extends BaseActivity
/** /**
* Callback from tvshow details when a episode is selected * Callback from tvshow details when a episode is selected
* @param episodeId episode id
*/ */
@TargetApi(21) @TargetApi(21)
public void onNextEpisodeSelected(int episodeId) { public void onNextEpisodeSelected(int tvshowId,
selectedEpisodeId = episodeId; AbstractInfoFragment.DataHolder dh) {
selectedEpisodeId = dh.getId();
// Replace list fragment // Replace list fragment
TVShowEpisodeDetailsFragment fragment = TVShowEpisodeInfoFragment fragment = new TVShowEpisodeInfoFragment();
TVShowEpisodeDetailsFragment.newInstance(selectedTVShowId, selectedEpisodeId); fragment.setDataHolder(dh);
fragment.setTvshowId(tvshowId);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions // Set up transitions
@ -336,15 +295,16 @@ public class TVShowsActivity extends BaseActivity
/** /**
* Callback from tvshow episodes list when a episode is selected * Callback from tvshow episodes list when a episode is selected
* @param vh view holder
*/ */
@TargetApi(21) @TargetApi(21)
public void onEpisodeSelected(TVShowEpisodeListFragment.EpisodeViewHolder vh) { public void onEpisodeSelected(int tvshowId,
selectedEpisodeId = vh.episodeId; TVShowEpisodeListFragment.ViewHolder viewHolder) {
selectedEpisodeId = viewHolder.dataHolder.getId();
// Replace list fragment // Replace list fragment
TVShowEpisodeDetailsFragment fragment = TVShowEpisodeInfoFragment fragment = new TVShowEpisodeInfoFragment();
TVShowEpisodeDetailsFragment.newInstance(selectedTVShowId, selectedEpisodeId); fragment.setDataHolder(viewHolder.dataHolder);
fragment.setTvshowId(tvshowId);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions // Set up transitions

View File

@ -0,0 +1,118 @@
/*
* Copyright 2017 Martijn Brekhof. 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.utils;
import android.annotation.TargetApi;
import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.SharedElementCallback;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.View;
import org.xbmc.kore.R;
import java.util.List;
import java.util.Map;
public class SharedElementTransition {
private static final String TAG = LogUtils.makeLogTag(SharedElementTransition.class);
public interface SharedElement {
/**
* Returns if the shared element if visible
* @return true if visible, false otherwise
*/
boolean isSharedElementVisible();
}
private boolean clearSharedElements;
/**
* Sets up the transition for the exiting fragment
* @param fragment
*/
@TargetApi(21)
public void setupExitTransition(Context context, Fragment fragment) {
Transition fade = TransitionInflater
.from(context)
.inflateTransition(android.R.transition.fade);
fragment.setExitTransition(fade);
fragment.setReenterTransition(fade);
fragment.setExitSharedElementCallback(new SharedElementCallback() {
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
// Clearing must be done in the reentering fragment
// as this is called last. Otherwise, the app will crash during transition setup. Not sure, but might
// be a v4 support package bug.
if (clearSharedElements) {
names.clear();
sharedElements.clear();
clearSharedElements = false;
}
}
});
}
/**
* Sets up the transition for the entering fragment
* @param fragmentTransaction
* @param fragment entering fragment
* @param sharedElement must have the transition name set
*/
@TargetApi(21)
public void setupEnterTransition(Context context,
FragmentTransaction fragmentTransaction,
final Fragment fragment,
View sharedElement) {
if (!(fragment instanceof SharedElement)) {
LogUtils.LOGD(TAG, "Enter transition fragment must implement SharedElement interface");
return;
}
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() {
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
// On returning, onMapSharedElements for the exiting fragment is called before the onMapSharedElements
// for the reentering fragment. We use this to determine if we are returning and if
// we should clear the shared element lists. Note that, clearing must be done in the reentering fragment
// as this is called last. Otherwise, the app will crash during transition setup. Not sure, but might
// be a v4 support package bug.
if (fragment.isVisible() && (!((SharedElement) fragment).isSharedElementVisible())) {
// shared element not visible
clearSharedElements = true;
}
}
};
fragment.setEnterSharedElementCallback(seCallback);
fragment.setEnterTransition(TransitionInflater
.from(context)
.inflateTransition(R.transition.media_details));
fragment.setReturnTransition(null);
Transition changeImageTransition = TransitionInflater.from(
context).inflateTransition(R.transition.change_image);
fragment.setSharedElementReturnTransition(changeImageTransition);
fragment.setSharedElementEnterTransition(changeImageTransition);
fragmentTransaction.addSharedElement(sharedElement, sharedElement.getTransitionName());
}
}

View File

@ -468,7 +468,7 @@ public class UIUtils {
} }
/** /**
* Returns true if {@param view} is contained within {@param container}'s bounds. * Returns true if {@param view} is visible within {@param container}'s bounds.
*/ */
public static boolean isViewInBounds(@NonNull View container, @NonNull View view) { public static boolean isViewInBounds(@NonNull View container, @NonNull View view) {
Rect containerBounds = new Rect(); Rect containerBounds = new Rect();
@ -497,7 +497,7 @@ public class UIUtils {
// Check if any file exists and whether to overwrite it // Check if any file exists and whether to overwrite it
boolean someFilesExist = false; boolean someFilesExist = false;
for (FileDownloadHelper.SongInfo songInfo : songInfoList) { for (FileDownloadHelper.SongInfo songInfo : songInfoList) {
File file = new File(songInfoList.get(0).getAbsoluteFilePath()); File file = new File(songInfo.getAbsoluteFilePath());
if (file.exists()) { if (file.exists()) {
someFilesExist = true; someFilesExist = true;
break; break;
@ -560,4 +560,14 @@ public class UIUtils {
} }
} }
} }
public static void highlightImageView(Context context, ImageView view) {
Resources.Theme theme = context.getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{
R.attr.colorAccent});
view.setColorFilter(
styledAttributes.getColor(0,
context.getResources().getColor(R.color.accent_default)));
styledAttributes.recycle();
}
} }

View File

@ -23,8 +23,19 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.text.TextUtils; import android.text.TextUtils;
import android.widget.Toast;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.HostConnection;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -173,4 +184,61 @@ public class Utils {
return bitmap; return bitmap;
} }
public static void addToPlaylist(final Fragment fragment, final int itemId, final String playlistId) {
Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
final Context context = fragment.getContext();
final HostConnection hostConnection = HostManager.getInstance(context).getConnection();
final Handler callbackHandler = new Handler();
getPlaylists.execute(hostConnection, new ApiCallback<ArrayList<PlaylistType.GetPlaylistsReturnType>>() {
@Override
public void onSuccess(ArrayList<PlaylistType.GetPlaylistsReturnType> result) {
if (!fragment.isAdded()) return;
// Ok, loop through the playlists, looking for the video one
int videoPlaylistId = -1;
for (PlaylistType.GetPlaylistsReturnType playlist : result) {
if (playlist.type.equals(playlistId)) {
videoPlaylistId = playlist.playlistid;
break;
}
}
// If found, add to playlist
if (videoPlaylistId != -1) {
PlaylistType.Item item = new PlaylistType.Item();
item.episodeid = itemId;
Playlist.Add action = new Playlist.Add(videoPlaylistId, item);
action.execute(hostConnection, new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!fragment.isAdded()) return;
// Got an error, show toast
Toast.makeText(context, R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(int errorCode, String description) {
if (!fragment.isAdded()) return;
// Got an error, show toast
Toast.makeText(context, R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
} else {
Toast.makeText(context, R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
.show();
}
}
@Override
public void onError(int errorCode, String description) {
if (!fragment.isAdded()) return;
// Got an error, show toast
Toast.makeText(context, R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
} }

View File

@ -1,196 +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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/art"
android:layout_width="match_parent"
android:layout_height="@dimen/now_playing_art_height"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"/>
<com.melnykov.fab.ObservableScrollView
android:id="@+id/media_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true">
<!-- Addon details information -->
<RelativeLayout
android:id="@+id/media_panel_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/remote_content_hmargin"
android:layout_marginRight="@dimen/remote_content_hmargin"
android:transitionGroup="true">
<TextView
android:id="@+id/media_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/media_title_margin_top"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:paddingStart="@dimen/poster_width_plus_padding"
android:paddingEnd="@dimen/default_padding"
android:background="?attr/contentBackgroundColor"/>
<TextView
android:id="@+id/media_undertitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Subtitle"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:paddingStart="@dimen/poster_width_plus_padding"
android:paddingEnd="@dimen/default_padding"
android:layout_below="@id/media_title"
android:background="?attr/contentBackgroundColor"/>
<ImageView
android:id="@+id/poster"
android:layout_width="@dimen/albumdetail_poster_width"
android:layout_height="@dimen/albumdetail_poster_heigth"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="@dimen/default_padding"
android:layout_marginStart="@dimen/default_padding"
android:layout_alignBottom="@id/media_undertitle"
android:contentDescription="@string/poster"
android:scaleType="centerCrop"/>
<!-- Media actions 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/contentBackgroundColor">
<!--<ImageButton-->
<!--android:id="@+id/play"-->
<!--android:layout_width="@dimen/buttonbar_button_width"-->
<!--android:layout_height="match_parent"-->
<!--style="@style/Widget.Button.Borderless"-->
<!--android:src="?attr/iconPlay"-->
<!--android:contentDescription="@string/play"/>-->
<Space
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<ImageButton
android:id="@+id/pin_unpin"
android:visibility="gone"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconBookmark"
android:contentDescription="@string/enable_disable"/>
<ImageButton
android:id="@+id/enable_disable"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconSeen"
android:contentDescription="@string/enable_disable"/>
</LinearLayout>
<LinearLayout
android:id="@+id/media_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_below="@id/media_actions_bar"
android:paddingTop="@dimen/default_padding"
android:paddingBottom="@dimen/default_padding"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:background="?attr/contentBackgroundColor">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.SmallDetails"
android:paddingRight="@dimen/default_padding"
android:paddingEnd="@dimen/default_padding"
android:textColor="?attr/appTextColorSecondary"
android:text="@string/author"/>
<TextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.SmallDetails"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.SmallDetails"
android:paddingRight="@dimen/default_padding"
android:paddingEnd="@dimen/default_padding"
android:textColor="?attr/appTextColorSecondary"
android:text="@string/version"/>
<TextView
android:id="@+id/version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.SmallDetails"/>
</LinearLayout>
<View
android:id="@+id/separator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/media_details"
style="@style/DefaultDividerH"/>
<TextView
android:id="@+id/media_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/separator"
style="@style/TextAppearance.Media.Info"
android:layout_marginTop="@dimen/default_padding"
android:layout_marginBottom="@dimen/default_padding"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"/>
</RelativeLayout>
</com.melnykov.fab.ObservableScrollView>
<com.melnykov.fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="@dimen/default_padding"
android:layout_marginRight="@dimen/double_padding"
android:layout_marginEnd="@dimen/double_padding"
android:src="@drawable/ic_play_arrow_white_24dp"
app:fab_colorNormal="?attr/fabColorNormal"
app:fab_colorPressed="?attr/fabColorPressed"/>
</RelativeLayout>

View File

@ -1,241 +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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/art"
android:layout_width="match_parent"
android:layout_height="@dimen/now_playing_art_height"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"/>
<com.melnykov.fab.ObservableScrollView
android:id="@+id/media_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true">
<!-- Album details information -->
<RelativeLayout
android:id="@+id/media_panel_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/remote_content_hmargin"
android:layout_marginRight="@dimen/remote_content_hmargin"
android:transitionGroup="true">
<TextView
android:id="@+id/media_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/media_title_margin_top"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:paddingStart="@dimen/poster_width_plus_padding"
android:paddingEnd="@dimen/default_padding"
android:background="?attr/contentBackgroundColor"/>
<TextView
android:id="@+id/media_undertitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Subtitle"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:paddingStart="@dimen/poster_width_plus_padding"
android:paddingEnd="@dimen/default_padding"
android:layout_below="@id/media_title"
android:background="?attr/contentBackgroundColor"/>
<ImageView
android:id="@+id/poster"
android:layout_width="@dimen/albumdetail_poster_width"
android:layout_height="@dimen/albumdetail_poster_heigth"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="@dimen/default_padding"
android:layout_marginStart="@dimen/default_padding"
android:layout_alignBottom="@id/media_undertitle"
android:contentDescription="@string/poster"
android:scaleType="centerCrop"/>
<!-- Media actions 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/contentBackgroundColor">
<!--<ImageButton-->
<!--android:id="@+id/play"-->
<!--android:layout_width="@dimen/buttonbar_button_width"-->
<!--android:layout_height="match_parent"-->
<!--style="@style/Widget.Button.Borderless"-->
<!--android:src="?attr/iconPlay"-->
<!--android:contentDescription="@string/play"/>-->
<Space
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<ImageButton
android:id="@+id/add_to_playlist"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconAddToQueue"
android:contentDescription="@string/add_to_playlist"/>
<ImageButton
android:id="@+id/download"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconDownload"
android:contentDescription="@string/download"/>
</LinearLayout>
<RelativeLayout
android:id="@+id/media_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/media_actions_bar"
android:paddingTop="@dimen/default_padding"
android:paddingBottom="@dimen/default_padding"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:background="?attr/contentBackgroundColor">
<TextView
android:id="@+id/year"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:gravity="end"
style="@style/TextAppearance.Media.SmallDetails"/>
<!--<TextView-->
<!--android:id="@+id/genres"-->
<!--android:layout_width="wrap_content"-->
<!--android:layout_height="wrap_content"-->
<!--android:layout_below="@id/year"-->
<!--android:layout_alignParentRight="true"-->
<!--android:layout_alignWithParentIfMissing="true"-->
<!--style="@style/TextAppearance.Media.SmallDetails"/>-->
<TextView
android:id="@+id/rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
style="@style/TextAppearance.Media.SmallDetails"
android:textColor="?attr/appTextColorPrimary"
android:textStyle="bold"/>
<TextView
android:id="@+id/max_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/rating"
android:layout_toRightOf="@id/rating"
android:layout_toEndOf="@id/rating"
android:layout_toLeftOf="@id/year"
android:layout_toStartOf="@id/year"
style="@style/TextAppearance.Media.SmallDetails"
android:text="@string/max_rating_music"/>
</RelativeLayout>
<LinearLayout
android:id="@+id/media_description_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/media_details"
android:orientation="vertical"
android:background="?attr/contentBackgroundColor">
<at.blogc.android.views.ExpandableTextView
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="end"
app:animation_duration="@integer/expandable_textview_animation_duration"/>
<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_marginBottom="@dimen/small_padding"
android:layout_marginRight="@dimen/small_padding"
android:layout_marginEnd="@dimen/small_padding"
android:src="?attr/iconExpand"/>
</LinearLayout>
<View
android:id="@+id/separator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/media_description_container"
style="@style/DefaultDividerH"/>
<LinearLayout
android:id="@+id/song_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/separator"
android:layout_marginTop="@dimen/default_padding"
android:orientation="vertical"/>
</RelativeLayout>
</com.melnykov.fab.ObservableScrollView>
<com.melnykov.fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="@dimen/default_padding"
android:layout_marginRight="@dimen/double_padding"
android:layout_marginEnd="@dimen/double_padding"
android:src="@drawable/ic_play_arrow_white_24dp"
app:fab_colorNormal="?attr/fabColorNormal"
app:fab_colorPressed="?attr/fabColorPressed"/>
<!-- View that will be shown with the circularReveal when user presses the FAB -->
<View
android:id="@+id/exit_transition_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/fabColorNormal"
android:visibility="invisible"/>
</RelativeLayout>

View File

@ -1,149 +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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/art"
android:layout_width="match_parent"
android:layout_height="@dimen/now_playing_art_height"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"/>
<com.melnykov.fab.ObservableScrollView
android:id="@+id/media_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true">
<!-- Album details information -->
<RelativeLayout
android:id="@+id/media_panel_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/remote_content_hmargin"
android:layout_marginRight="@dimen/remote_content_hmargin"
android:transitionGroup="true">
<TextView
android:id="@+id/media_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/media_title_margin_top"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:paddingStart="@dimen/poster_width_plus_padding"
android:paddingEnd="@dimen/default_padding"
android:background="?attr/contentBackgroundColor"/>
<TextView
android:id="@+id/media_undertitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Subtitle"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:paddingStart="@dimen/poster_width_plus_padding"
android:paddingEnd="@dimen/default_padding"
android:layout_below="@id/media_title"
android:background="?attr/contentBackgroundColor"/>
<!-- Media actions 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">
<Space
android:id="@+id/media_actions_bar_space"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<ImageButton
android:id="@+id/add_to_playlist"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconAddToQueue"
android:contentDescription="@string/add_to_playlist"/>
<ImageButton
android:id="@+id/download"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconDownload"
android:contentDescription="@string/download"/>
</LinearLayout>
<ImageView
android:id="@+id/poster"
android:layout_width="@dimen/albumdetail_poster_width"
android:layout_height="@dimen/albumdetail_poster_heigth"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="@dimen/default_padding"
android:layout_marginStart="@dimen/default_padding"
android:layout_alignBottom="@id/media_undertitle"
android:contentDescription="@string/poster"
android:scaleType="centerCrop"/>
<TextView
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:layout_below="@id/media_actions_bar"
android:background="?attr/contentBackgroundColor"
/>
</RelativeLayout>
</com.melnykov.fab.ObservableScrollView>
<com.melnykov.fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="@dimen/default_padding"
android:layout_marginRight="@dimen/double_padding"
android:layout_marginEnd="@dimen/double_padding"
android:src="@drawable/ic_play_arrow_white_24dp"
app:fab_colorNormal="?attr/fabColorNormal"
app:fab_colorPressed="?attr/fabColorPressed"/>
<!-- View that will be shown with the circularReveal when user presses the FAB -->
<View
android:id="@+id/exit_transition_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/fabColorNormal"
android:visibility="invisible"/>
</RelativeLayout>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 Martijn Brekhof. 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/casts_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:paddingTop="@dimen/default_padding"
android:text="@string/cast"/>
<GridLayout
android:id="@+id/cast_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnCount="@integer/cast_grid_view_columns">
</GridLayout>
</LinearLayout>

View File

@ -1,274 +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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/art"
android:layout_width="match_parent"
android:layout_height="@dimen/now_playing_art_height"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"/>
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.melnykov.fab.ObservableScrollView
android:id="@+id/media_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true">
<!-- Episode details information -->
<RelativeLayout
android:id="@+id/media_panel_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/remote_content_hmargin"
android:layout_marginRight="@dimen/remote_content_hmargin"
android:transitionGroup="true">
<TextView
android:id="@+id/media_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/media_title_margin_top"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:background="?attr/contentBackgroundColor"/>
<TextView
android:id="@+id/media_undertitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Subtitle"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:layout_below="@id/media_title"
android:background="?attr/contentBackgroundColor"/>
<!-- Media actions 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/contentBackgroundColor">
<!--<ImageButton-->
<!--android:id="@+id/play"-->
<!--android:layout_width="@dimen/buttonbar_button_width"-->
<!--android:layout_height="match_parent"-->
<!--style="@style/Widget.Button.Borderless"-->
<!--android:src="?attr/iconPlay"-->
<!--android:contentDescription="@string/play"/>-->
<Space
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<ImageButton
android:id="@+id/add_to_playlist"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconAddToQueue"
android:contentDescription="@string/add_to_playlist"/>
<ImageButton
android:id="@+id/download"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconDownload"
android:contentDescription="@string/download"
android:visibility="gone"/>
<ImageButton
android:id="@+id/seen"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconSeen"
android:contentDescription="@string/seen"/>
</LinearLayout>
<RelativeLayout
android:id="@+id/media_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/media_actions_bar"
android:paddingTop="@dimen/default_padding"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:background="?attr/contentBackgroundColor">
<TextView
android:id="@+id/premiered"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
style="@style/TextAppearance.Media.SmallDetails"/>
<TextView
android:id="@+id/season"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/premiered"
android:layout_alignRight="@id/premiered"
android:layout_alignEnd="@id/premiered"
style="@style/TextAppearance.Media.SmallDetails"/>
<TextView
android:id="@+id/rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/season"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
style="@style/TextAppearance.Media.Rating"
android:layout_marginRight="@dimen/small_padding"
android:layout_marginEnd="@dimen/small_padding"/>
<TextView
android:id="@+id/max_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/season"
android:layout_alignBottom="@id/season"
android:layout_toRightOf="@id/rating"
android:layout_toLeftOf="@id/season"
android:layout_toEndOf="@id/rating"
android:layout_toStartOf="@id/season"
style="@style/TextAppearance.Media.SmallDetails"
android:text="@string/max_rating_video"/>
</RelativeLayout>
<TextView
android:id="@+id/media_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/media_details"
style="@style/TextAppearance.Media.Info"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:background="?attr/contentBackgroundColor"/>
<TextView
android:id="@+id/director_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/media_description"
style="@style/TextAppearance.Media.Info"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:text="@string/directors"
android:background="?attr/contentBackgroundColor"/>
<TextView
android:id="@+id/directors"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/director_title"
android:layout_toEndOf="@id/director_title"
android:layout_alignTop="@id/director_title"
style="@style/TextAppearance.Media.Info"
android:textColor="?attr/appTextColorSecondary"
android:background="?attr/contentBackgroundColor"/>
<View
android:id="@+id/separator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/directors"
style="@style/DefaultDividerH"/>
<!--<TextView-->
<!--android:id="@+id/cast_title"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="wrap_content"-->
<!--android:layout_below="@id/directors"-->
<!--style="@style/TextAppearance.Media.Title"-->
<!--android:paddingLeft="@dimen/default_padding"-->
<!--android:paddingRight="@dimen/default_padding"-->
<!--android:text="@string/cast"/>-->
<!--<GridLayout-->
<!--android:id="@+id/cast_list"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="match_parent"-->
<!--android:layout_below="@id/cast_title"-->
<!--android:columnCount="@integer/cast_grid_view_columns"-->
<!--android:orientation="horizontal"/>-->
<!--<TextView-->
<!--android:id="@+id/additional_cast_title"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="wrap_content"-->
<!--android:layout_below="@id/cast_list"-->
<!--style="@style/TextAppearance.Media.Title"-->
<!--android:layout_marginTop="@dimen/default_padding"-->
<!--android:paddingLeft="@dimen/default_padding"-->
<!--android:paddingRight="@dimen/default_padding"-->
<!--android:text="@string/additional_cast"/>-->
<!--<TextView-->
<!--android:id="@+id/additional_cast_list"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="wrap_content"-->
<!--android:layout_below="@id/additional_cast_title"-->
<!--style="@style/TextAppearance.Media.Info"-->
<!--android:textColor="?attr/secondaryTextColor"-->
<!--android:layout_marginBottom="@dimen/default_padding"-->
<!--android:paddingLeft="@dimen/default_padding"-->
<!--android:paddingRight="@dimen/default_padding"/>-->
</RelativeLayout>
</com.melnykov.fab.ObservableScrollView>
</android.support.v4.widget.SwipeRefreshLayout>
<com.melnykov.fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="@dimen/default_padding"
android:layout_marginRight="@dimen/double_padding"
android:layout_marginEnd="@dimen/double_padding"
android:src="@drawable/ic_play_arrow_white_24dp"
app:fab_colorNormal="?attr/fabColorNormal"
app:fab_colorPressed="?attr/fabColorPressed"/>
<!-- View that will be shown with the circularReveal when user presses the FAB -->
<View
android:id="@+id/exit_transition_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/fabColorNormal"
android:visibility="invisible"/>
</RelativeLayout>

View File

@ -40,18 +40,15 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"> android:layout_alignParentLeft="true"
android:transitionGroup="true">
<!-- Movie details information -->
<RelativeLayout <RelativeLayout
android:id="@+id/media_panel_group" android:id="@+id/media_panel_group"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/remote_content_hmargin" android:layout_marginLeft="@dimen/remote_content_hmargin"
android:layout_marginRight="@dimen/remote_content_hmargin" android:layout_marginRight="@dimen/remote_content_hmargin">
android:paddingBottom="@dimen/default_padding"
android:transitionGroup="true">
<TextView <TextView
android:id="@+id/media_title" android:id="@+id/media_title"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -77,8 +74,8 @@
<ImageView <ImageView
android:id="@+id/poster" android:id="@+id/poster"
android:layout_width="@dimen/now_playing_poster_width" android:layout_width="@dimen/detail_poster_width_square"
android:layout_height="@dimen/now_playing_poster_height" android:layout_height="@dimen/detail_poster_height_square"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_marginLeft="@dimen/default_padding" android:layout_marginLeft="@dimen/default_padding"
@ -97,173 +94,155 @@
android:paddingRight="@dimen/default_padding" android:paddingRight="@dimen/default_padding"
android:orientation="horizontal" android:orientation="horizontal"
style="@style/ButtonBar" style="@style/ButtonBar"
android:background="?attr/contentBackgroundColor"> android:background="?attr/contentBackgroundColor"
<!--<ImageButton--> android:visibility="gone">
<!--android:id="@+id/play"-->
<!--android:layout_width="@dimen/buttonbar_button_width"-->
<!--android:layout_height="match_parent"-->
<!--style="@style/Widget.Button.Borderless"-->
<!--android:src="?attr/iconPlay"-->
<!--android:contentDescription="@string/play"/>-->
<Space <Space
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
<ImageButton <ImageButton
android:id="@+id/add_to_playlist" android:id="@+id/media_action_pin_unpin"
android:visibility="gone"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconBookmark"
android:contentDescription="@string/enable_disable"/>
<ImageButton
android:id="@+id/media_action_add_to_playlist"
android:layout_width="@dimen/buttonbar_button_width" android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent" android:layout_height="match_parent"
style="@style/Widget.Button.Borderless" style="@style/Widget.Button.Borderless"
android:src="?attr/iconAddToQueue" android:src="?attr/iconAddToQueue"
android:contentDescription="@string/add_to_playlist"/> android:contentDescription="@string/add_to_playlist"
android:visibility="gone"/>
<ImageButton <ImageButton
android:id="@+id/go_to_imdb" android:id="@+id/media_action_download"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconInfoWWW"
android:contentDescription="@string/imdb"/>
<ImageButton
android:id="@+id/download"
android:layout_width="@dimen/buttonbar_button_width" android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent" android:layout_height="match_parent"
style="@style/Widget.Button.Borderless" style="@style/Widget.Button.Borderless"
android:src="?attr/iconDownload" android:src="?attr/iconDownload"
android:contentDescription="@string/download" android:contentDescription="@string/download"
/> android:visibility="gone"/>
<ImageButton <ImageButton
android:id="@+id/seen" android:id="@+id/media_action_go_to_imdb"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconInfoWWW"
android:contentDescription="@string/imdb"
android:visibility="gone"/>
<ImageButton
android:id="@+id/media_action_seen"
android:layout_width="@dimen/buttonbar_button_width" android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent" android:layout_height="match_parent"
style="@style/Widget.Button.Borderless" style="@style/Widget.Button.Borderless"
android:src="?attr/iconSeen" android:src="?attr/iconSeen"
android:contentDescription="@string/seen"/> android:contentDescription="@string/seen"
android:visibility="gone"/>
</LinearLayout> </LinearLayout>
<RelativeLayout <LinearLayout
android:id="@+id/media_details" android:id="@+id/media_details"
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_actions_bar" android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/default_padding" android:paddingTop="@dimen/default_padding"
android:paddingBottom="@dimen/default_padding" android:paddingBottom="@dimen/default_padding"
android:paddingLeft="@dimen/default_padding" android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding" android:paddingRight="@dimen/default_padding"
android:layout_below="@id/media_actions_bar"
android:background="?attr/contentBackgroundColor"> android:background="?attr/contentBackgroundColor">
<TextView
android:id="@+id/year" <LinearLayout
android:layout_width="wrap_content" android:id="@+id/rating_container"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentTop="true" android:layout_weight="3"
android:layout_alignParentRight="true" android:layout_gravity="start">
android:layout_alignParentEnd="true" <TextView
style="@style/TextAppearance.Media.SmallDetails"/> android:id="@+id/rating"
<TextView android:layout_width="wrap_content"
android:id="@+id/genres" android:layout_height="wrap_content"
android:layout_width="wrap_content" android:layout_gravity="center_horizontal"
android:layout_height="wrap_content" style="@style/TextAppearance.Media.Rating"
android:layout_below="@id/year" android:layout_marginRight="@dimen/small_padding"
android:layout_alignRight="@id/year" android:layout_marginEnd="@dimen/small_padding"/>
android:layout_alignEnd="@id/year"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/rating_votes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.SmallDetails"/>
<TextView
android:id="@+id/max_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.SmallDetails"/>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/media_details_right"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="5"
android:layout_gravity="end"
android:gravity="end"
style="@style/TextAppearance.Media.SmallDetails"/> style="@style/TextAppearance.Media.SmallDetails"/>
</LinearLayout>
<TextView <LinearLayout
android:id="@+id/rating" android:id="@+id/media_description_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/genres"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
style="@style/TextAppearance.Media.Rating"
android:layout_marginRight="@dimen/small_padding"
android:layout_marginEnd="@dimen/small_padding"/>
<TextView
android:id="@+id/rating_votes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/year"
android:layout_alignBottom="@id/year"
android:layout_toRightOf="@id/rating"
android:layout_toLeftOf="@id/year"
android:layout_toEndOf="@id/rating"
android:layout_toStartOf="@id/year"
style="@style/TextAppearance.Media.SmallDetails"/>
<TextView
android:id="@+id/max_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/genres"
android:layout_alignBottom="@id/genres"
android:layout_toRightOf="@id/rating"
android:layout_toLeftOf="@id/genres"
android:layout_toEndOf="@id/rating"
android:layout_toStartOf="@id/genres"
style="@style/TextAppearance.Media.SmallDetails"
android:text="@string/max_rating_video"/>
</RelativeLayout>
<TextView
android:id="@+id/media_description"
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" <at.blogc.android.views.ExpandableTextView
android:background="?attr/contentBackgroundColor"/> android:id="@+id/media_description"
android:layout_width="match_parent"
<TextView android:layout_height="wrap_content"
android:id="@+id/director_title" style="@style/TextAppearance.Media.Info"
android:layout_width="wrap_content" android:paddingLeft="@dimen/default_padding"
android:layout_height="wrap_content" android:paddingRight="@dimen/default_padding"
android:layout_below="@id/media_description" android:paddingBottom="0dp"
style="@style/TextAppearance.Media.Info" android:maxLines="@integer/description_max_lines"
android:paddingLeft="@dimen/default_padding" android:ellipsize="end"
android:paddingRight="@dimen/default_padding" app:animation_duration="@integer/expandable_textview_animation_duration"/>
android:text="@string/directors" <ImageView
android:background="?attr/contentBackgroundColor"/> android:id="@+id/show_all"
android:layout_width="@dimen/small_icon_size"
<TextView android:layout_height="@dimen/small_icon_size"
android:id="@+id/directors" android:layout_gravity="end"
android:layout_width="match_parent" android:layout_marginBottom="@dimen/small_padding"
android:layout_height="wrap_content" android:layout_marginRight="@dimen/small_padding"
android:layout_toRightOf="@id/director_title" android:layout_marginEnd="@dimen/small_padding"
android:layout_toEndOf="@id/director_title" android:src="?attr/iconExpand"/>
android:layout_alignTop="@id/director_title" </LinearLayout>
style="@style/TextAppearance.Media.Info"
android:textColor="?attr/appTextColorSecondary"
android:background="?attr/contentBackgroundColor"/>
<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/directors" android:layout_below="@id/media_description_container"
style="@style/DefaultDividerH"/> style="@style/DefaultDividerH"/>
<TextView <FrameLayout
android:id="@+id/cast_title" android:id="@+id/media_additional_info"
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" android:layout_marginTop="@dimen/default_padding"/>
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:text="@string/cast"/>
<GridLayout
android:id="@+id/cast_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/cast_title"
android:columnCount="@integer/cast_grid_view_columns"
android:orientation="horizontal"/>
</RelativeLayout> </RelativeLayout>
</com.melnykov.fab.ObservableScrollView> </com.melnykov.fab.ObservableScrollView>
</android.support.v4.widget.SwipeRefreshLayout> </android.support.v4.widget.SwipeRefreshLayout>
<com.melnykov.fab.FloatingActionButton <com.melnykov.fab.FloatingActionButton
@ -278,7 +257,8 @@
android:layout_marginEnd="@dimen/double_padding" android:layout_marginEnd="@dimen/double_padding"
android:src="@drawable/ic_play_arrow_white_24dp" android:src="@drawable/ic_play_arrow_white_24dp"
app:fab_colorNormal="?attr/fabColorNormal" app:fab_colorNormal="?attr/fabColorNormal"
app:fab_colorPressed="?attr/fabColorPressed"/> app:fab_colorPressed="?attr/fabColorPressed"
android:visibility="gone"/>
<!-- View that will be shown with the circularReveal when user presses the FAB --> <!-- View that will be shown with the circularReveal when user presses the FAB -->
<View <View

View File

@ -1,197 +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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/art"
android:layout_width="match_parent"
android:layout_height="@dimen/now_playing_art_height"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"/>
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.melnykov.fab.ObservableScrollView
android:id="@+id/media_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true">
<!-- Music video details information -->
<RelativeLayout
android:id="@+id/media_panel_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/remote_content_hmargin"
android:layout_marginRight="@dimen/remote_content_hmargin"
android:transitionGroup="true">
<TextView
android:id="@+id/media_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/media_title_margin_top"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:paddingStart="@dimen/poster_width_plus_padding"
android:paddingEnd="@dimen/default_padding"
android:background="?attr/contentBackgroundColor"/>
<TextView
android:id="@+id/media_undertitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Subtitle"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:paddingStart="@dimen/poster_width_plus_padding"
android:paddingEnd="@dimen/default_padding"
android:layout_below="@id/media_title"
android:background="?attr/contentBackgroundColor"/>
<ImageView
android:id="@+id/poster"
android:layout_width="@dimen/musicvideodetail_poster_width"
android:layout_height="@dimen/musicvideodetail_poster_heigth"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="@dimen/default_padding"
android:layout_marginStart="@dimen/default_padding"
android:layout_alignBottom="@id/media_undertitle"
android:contentDescription="@string/poster"
android:scaleType="centerCrop"/>
<!-- Media actions 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/contentBackgroundColor">
<!--<ImageButton-->
<!--android:id="@+id/play"-->
<!--android:layout_width="@dimen/buttonbar_button_width"-->
<!--android:layout_height="match_parent"-->
<!--style="@style/Widget.Button.Borderless"-->
<!--android:src="?attr/iconPlay"-->
<!--android:contentDescription="@string/play"/>-->
<Space
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<ImageButton
android:id="@+id/add_to_playlist"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconAddToQueue"
android:contentDescription="@string/add_to_playlist"/>
<ImageButton
android:id="@+id/download"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconDownload"
android:contentDescription="@string/download"/>
</LinearLayout>
<RelativeLayout
android:id="@+id/media_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/media_actions_bar"
android:paddingTop="@dimen/default_padding"
android:paddingBottom="@dimen/default_padding"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:background="?attr/contentBackgroundColor">
<TextView
android:id="@+id/year"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
style="@style/TextAppearance.Media.SmallDetails"/>
<TextView
android:id="@+id/genres"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_toLeftOf="@id/year"
android:layout_toStartOf="@id/year"
style="@style/TextAppearance.Media.SmallDetails"/>
</RelativeLayout>
<TextView
android:id="@+id/media_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/media_details"
style="@style/TextAppearance.Media.Info"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:background="?attr/contentBackgroundColor"/>
<View
android:id="@+id/separator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/media_description"
style="@style/DefaultDividerH"/>
</RelativeLayout>
</com.melnykov.fab.ObservableScrollView>
</android.support.v4.widget.SwipeRefreshLayout>
<com.melnykov.fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="@dimen/default_padding"
android:layout_marginRight="@dimen/double_padding"
android:layout_marginEnd="@dimen/double_padding"
android:src="@drawable/ic_play_arrow_white_24dp"
app:fab_colorNormal="?attr/fabColorNormal"
app:fab_colorPressed="?attr/fabColorPressed"/>
<!-- View that will be shown with the circularReveal when user presses the FAB -->
<View
android:id="@+id/exit_transition_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/fabColorNormal"
android:visibility="invisible"/>
</RelativeLayout>

View File

@ -1,45 +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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/contentBackgroundColor">
<include layout="@layout/empty_view"/>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ExpandableListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/small_padding"
android:paddingRight="@dimen/small_padding"
android:paddingTop="@dimen/small_padding"
android:paddingBottom="@dimen/small_padding"
android:clipToPadding="false"
android:choiceMode="none"
android:listSelector="?attr/selectableItemBackground"
android:childDivider="@android:color/transparent"
android:groupIndicator="@null"/>
</android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>

View File

@ -1,244 +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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/art"
android:layout_width="match_parent"
android:layout_height="@dimen/now_playing_art_height"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/fanart"
android:scaleType="centerCrop"/>
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/media_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true">
<!-- TV show details information -->
<RelativeLayout
android:id="@+id/media_panel_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/remote_content_hmargin"
android:layout_marginRight="@dimen/remote_content_hmargin"
android:paddingBottom="@dimen/default_padding"
android:transitionGroup="true">
<TextView
android:id="@+id/media_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/media_title_margin_top"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:paddingStart="@dimen/poster_width_plus_padding"
android:paddingEnd="@dimen/default_padding"
android:background="?attr/contentBackgroundColor"/>
<TextView
android:id="@+id/media_undertitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Subtitle"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:paddingStart="@dimen/poster_width_plus_padding"
android:paddingEnd="@dimen/default_padding"
android:layout_below="@id/media_title"
android:background="?attr/contentBackgroundColor"/>
<ImageView
android:id="@+id/poster"
android:layout_width="@dimen/now_playing_poster_width"
android:layout_height="@dimen/now_playing_poster_height"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="@dimen/default_padding"
android:layout_marginStart="@dimen/default_padding"
android:layout_alignBottom="@id/media_undertitle"
android:contentDescription="@string/poster"
android:scaleType="centerCrop"/>
<RelativeLayout
android:id="@+id/media_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/media_undertitle"
android:paddingTop="@dimen/default_padding"
android:paddingBottom="@dimen/default_padding"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:background="?attr/contentBackgroundColor">
<TextView
android:id="@+id/premiered"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
style="@style/TextAppearance.Media.SmallDetails"/>
<TextView
android:id="@+id/genres"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/premiered"
android:layout_alignRight="@id/premiered"
android:layout_alignEnd="@id/premiered"
style="@style/TextAppearance.Media.SmallDetails"/>
<TextView
android:id="@+id/rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/genres"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
style="@style/TextAppearance.Media.Rating"
android:layout_marginRight="@dimen/small_padding"
android:layout_marginEnd="@dimen/small_padding"/>
<TextView
android:id="@+id/max_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/genres"
android:layout_toRightOf="@id/rating"
android:layout_toLeftOf="@id/genres"
android:layout_toEndOf="@id/rating"
android:layout_toStartOf="@id/genres"
style="@style/TextAppearance.Media.SmallDetails"
android:text="@string/max_rating_video"/>
</RelativeLayout>
<LinearLayout
android:id="@+id/media_description_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/media_details"
android:orientation="vertical"
android:background="?attr/contentBackgroundColor">
<at.blogc.android.views.ExpandableTextView
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="end"
app:animation_duration="@integer/expandable_textview_animation_duration"/>
<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"/>
</LinearLayout>
<View
android:id="@+id/separator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/media_description_container"
style="@style/DefaultDividerH"/>
<TextView
android:id="@+id/next_episode_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/separator"
style="@style/TextAppearance.Media.Title"
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"/>
<!--
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"/>
<GridLayout
android:id="@+id/cast_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/cast_title"
android:columnCount="@integer/cast_grid_view_columns"
android:orientation="horizontal"/>
</RelativeLayout>
</ScrollView>
</android.support.v4.widget.SwipeRefreshLayout>
</RelativeLayout>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 Martijn Brekhof. 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/media_panel_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/remote_content_hmargin"
android:layout_marginRight="@dimen/remote_content_hmargin"
android:paddingBottom="@dimen/default_padding"
android:orientation="vertical">
<TextView
android:id="@+id/next_episode_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@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: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"
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"/>
<!--
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:columnCount="@integer/seasons_grid_view_columns"
android:orientation="horizontal"
android:useDefaultMargins="true"/>
<FrameLayout
android:id="@+id/cast_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

View File

@ -44,6 +44,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/TextAppearance.Medialist.Title" style="@style/TextAppearance.Medialist.Title"
android:paddingRight="@dimen/small_padding" android:paddingRight="@dimen/small_padding"
android:paddingEnd="@dimen/small_padding"
android:layout_alignTop="@id/art" android:layout_alignTop="@id/art"
android:layout_toRightOf="@id/art" android:layout_toRightOf="@id/art"
android:layout_toEndOf="@id/art"/> android:layout_toEndOf="@id/art"/>
@ -94,6 +95,7 @@
android:layout_below="@id/title" android:layout_below="@id/title"
style="@style/TextAppearance.Medialist.Details" style="@style/TextAppearance.Medialist.Details"
android:paddingLeft="@dimen/small_padding" android:paddingLeft="@dimen/small_padding"
android:paddingStart="@dimen/small_padding"
android:paddingTop="0dp" android:paddingTop="0dp"
android:paddingBottom="0dp" android:paddingBottom="0dp"
android:gravity="center_vertical"/> android:gravity="center_vertical"/>

View File

@ -54,24 +54,15 @@
<dimen name="albumlist_art_width">88dp</dimen> <dimen name="albumlist_art_width">88dp</dimen>
<dimen name="albumlist_art_heigth">88dp</dimen> <dimen name="albumlist_art_heigth">88dp</dimen>
<dimen name="albumdetail_poster_width">136dp</dimen>
<dimen name="albumdetail_poster_heigth">136dp</dimen>
<dimen name="audiogenrelist_art_width">88dp</dimen> <dimen name="audiogenrelist_art_width">88dp</dimen>
<dimen name="audiogenrelist_art_heigth">88dp</dimen> <dimen name="audiogenrelist_art_heigth">88dp</dimen>
<dimen name="musicvideolist_art_width">88dp</dimen> <dimen name="musicvideolist_art_width">88dp</dimen>
<dimen name="musicvideolist_art_heigth">88dp</dimen> <dimen name="musicvideolist_art_heigth">88dp</dimen>
<dimen name="musicvideodetail_poster_width">146dp</dimen>
<dimen name="musicvideodetail_poster_heigth">146dp</dimen>
<dimen name="addonlist_art_width">88dp</dimen> <dimen name="addonlist_art_width">88dp</dimen>
<dimen name="addonlist_art_heigth">88dp</dimen> <dimen name="addonlist_art_heigth">88dp</dimen>
<dimen name="addondetail_poster_width">146dp</dimen>
<dimen name="addondetail_poster_height">146dp</dimen>
<dimen name="channellist_art_width">88dp</dimen> <dimen name="channellist_art_width">88dp</dimen>
<dimen name="channellist_art_heigth">88dp</dimen> <dimen name="channellist_art_heigth">88dp</dimen>

View File

@ -95,8 +95,13 @@
<dimen name="albumlist_art_width">74dp</dimen> <dimen name="albumlist_art_width">74dp</dimen>
<dimen name="albumlist_art_heigth">74dp</dimen> <dimen name="albumlist_art_heigth">74dp</dimen>
<dimen name="albumdetail_poster_width">112dp</dimen> <dimen name="detail_art_height">216dp</dimen>
<dimen name="albumdetail_poster_heigth">112dp</dimen>
<dimen name="detail_poster_width_square">112dp</dimen>
<dimen name="detail_poster_height_square">112dp</dimen>
<dimen name="detail_poster_width_nonsquare">112dp</dimen>
<dimen name="detail_poster_height_nonsquare">160dp</dimen>
<dimen name="audiogenrelist_art_width">74dp</dimen> <dimen name="audiogenrelist_art_width">74dp</dimen>
<dimen name="audiogenrelist_art_heigth">74dp</dimen> <dimen name="audiogenrelist_art_heigth">74dp</dimen>
@ -104,15 +109,9 @@
<dimen name="musicvideolist_art_width">74dp</dimen> <dimen name="musicvideolist_art_width">74dp</dimen>
<dimen name="musicvideolist_art_heigth">74dp</dimen> <dimen name="musicvideolist_art_heigth">74dp</dimen>
<dimen name="musicvideodetail_poster_width">112dp</dimen>
<dimen name="musicvideodetail_poster_heigth">112dp</dimen>
<dimen name="addonlist_art_width">74dp</dimen> <dimen name="addonlist_art_width">74dp</dimen>
<dimen name="addonlist_art_heigth">74dp</dimen> <dimen name="addonlist_art_heigth">74dp</dimen>
<dimen name="addondetail_poster_width">112dp</dimen>
<dimen name="addondetail_poster_height">112dp</dimen>
<dimen name="filelist_art_width">52dp</dimen> <dimen name="filelist_art_width">52dp</dimen>
<dimen name="filelist_art_heigth">76dp</dimen> <dimen name="filelist_art_heigth">76dp</dimen>

View File

@ -195,6 +195,7 @@
<string name="episode_number">%1$d</string> <string name="episode_number">%1$d</string>
<string name="votes">%1$s votes</string> <string name="votes">%1$s votes</string>
<string name="max_rating">/%1$s</string>
<string name="max_rating_video">/10</string> <string name="max_rating_video">/10</string>
<string name="max_rating_music">/5</string> <string name="max_rating_music">/5</string>
@ -400,5 +401,9 @@
<string name="by_artist">By artist</string> <string name="by_artist">By artist</string>
<string name="by_artist_and_year">By artist and year</string> <string name="by_artist_and_year">By artist and year</string>
<string name="muted">muted</string> <string name="muted">muted</string>
<string name="no_item_available_to_play">Error: no item available to play.</string>
<string name="Refreshing_not_implemented_for_this_item">Refreshing not implemented for this item</string>
<string name="no_songs_found_refresh">No songs found\\n\\nSwipe down to refresh</string>
<string name="not_implemented">Not implemented</string>
</resources> </resources>

View File

@ -172,7 +172,6 @@
<style name="TextAppearance.Media.SmallDetails"> <style name="TextAppearance.Media.SmallDetails">
<item name="android:textAppearance">@style/TextAppearance.AppCompat.Caption</item> <item name="android:textAppearance">@style/TextAppearance.AppCompat.Caption</item>
<item name="android:textColor">?attr/appTextColorSecondary</item> <item name="android:textColor">?attr/appTextColorSecondary</item>
<item name="android:singleLine">true</item>
</style> </style>
<style name="TextAppearance.Media.Rating"> <style name="TextAppearance.Media.Rating">

View File

@ -1,105 +0,0 @@
@startuml
abstract class BaseActivity
abstract class AbstractDetailsFragment
abstract class AbstractListFragment
abstract class AbstractCursorListFragment
class MusicActivity
class Fragment
class MusicListFragment
class ArtistDetailsFragment {
# View createView(LayoutInflater inflater, ViewGroup container)
# String getSyncType()
# String getSyncID()
# int getSyncItemID()
# SwipeRefreshLayout getSwipeRefreshLayout()
# void onDownload()
# void onSyncProcessEnded(MediaSyncEvent event)
}
class AlbumDetailsFragment {
# View createView(LayoutInflater inflater, ViewGroup container)
# String getSyncType()
# String getSyncID()
# int getSyncItemID()
# SwipeRefreshLayout getSwipeRefreshLayout()
# void onDownload()
# void onSyncProcessEnded(MediaSyncEvent event)
}
class MusicVideoDetailsFragment {
# View createView(LayoutInflater inflater, ViewGroup container)
# String getSyncType()
# String getSyncID()
# int getSyncItemID()
# SwipeRefreshLayout getSwipeRefreshLayout()
# void onDownload()
# void onSyncProcessEnded(MediaSyncEvent event)
}
class ArtistListFragment {
# void onListItemClicked(View view)
# CursorLoader createCursorLoader()
# String getListSyncType()
# BaseAdapter createAdapter()
}
class AlbumListFragment {
# void onListItemClicked(View view)
# CursorLoader createCursorLoader()
# String getListSyncType()
# BaseAdapter createAdapter()
}
class SongsListFragment {
# void onListItemClicked(View view)
# CursorLoader createCursorLoader()
# String getListSyncType()
# BaseAdapter createAdapter()
}
class MusicVideoListFragment {
# void onListItemClicked(View view)
# CursorLoader createCursorLoader()
# String getListSyncType()
# BaseAdapter createAdapter()
}
class AudioGenresListFragment {
# void onListItemClicked(View view)
# CursorLoader createCursorLoader()
# String getListSyncType()
# BaseAdapter createAdapter()
}
BaseActivity <|-- MusicActivity
Fragment <|-- MusicListFragment
AbstractDetailsFragment <|-- ArtistDetailsFragment
AbstractDetailsFragment <|-- AlbumDetailsFragment
AbstractDetailsFragment <|-- MusicVideoDetailsFragment
Fragment <|-- AbstractListFragment
AbstractListFragment <|-- AbstractCursorListFragment
AbstractCursorListFragment <|-- ArtistListFragment
AbstractCursorListFragment <|-- AlbumListFragment
AbstractCursorListFragment <|-- SongsListFragment
AbstractCursorListFragment <|-- MusicVideoListFragment
AbstractCursorListFragment <|-- AudioGenresListFragment
MusicActivity *-- MusicListFragment
MusicListFragment *-- ArtistListFragment
MusicListFragment *-- AlbumListFragment
MusicListFragment *-- SongsListFragment
MusicListFragment *-- MusicVideoListFragment
MusicListFragment *-- AudioGenresListFragment
ArtistListFragment *-- ArtistDetailsFragment
AlbumListFragment *-- AlbumDetailsFragment
AudioGenresListFragment *-- AlbumDetailsFragment
MusicVideoListFragment *-- MusicVideoDetailsFragment
@enduml

View File

@ -0,0 +1,180 @@
@startuml
interface SharedElement {
Required for all fragments that
support shared element transitions
+ boolean isSharedElementVisible()
}
class DataHolder {
Holds required data used by info and details fragments
+ DataHolder(Bundle bundle)
+ DataHolder(int itemId)
+ void setBundle(Bundle bundle)
+ void setPosterTransitionName(String posterTransitionName)
+ void setSquarePoster(boolean squarePoster)
+ void setRating(double rating)
+ void setMaxRating(int maxRating)
+ void setVotes(int votes)
+ void setPosterUrl(String posterUrl)
+ void setTitle(String title)
+ void setUndertitle(String underTitle)
+ void setDescription(String description)
+ void setDetails(String details)
+ void setFanArtUrl(String fanArtUrl)
+ void setId(int id)
+ String getPosterTransitionName()
+ boolean getSquarePoster()
+ double getRating()
+ int getMaxRating()
+ int getVotes()
+ String getPosterUrl()
+ String getTitle()
+ String getUnderTitle()
+ String getDescription()
+ String getDetails()
+ String getFanArtUrl()
+ int getId()
+ Bundle getBundle()
}
abstract class AbstractFragment {
Holds the dataholder to provide quick access to required data in info
and detail fragments. This is required to provide a smooth and
responsive UX, especially when using shared element transitions.
+ void setDataHolder(AbstractInfoFragment.DataHolder dataHolder)
+ AbstractInfoFragment.DataHolder getDataHolder()
}
abstract class AbstractAdditionalInfoFragment {
Defines mandatory methods for fragments that can be added to
information fragments
+ abstract void refresh()
}
abstract class AbstractInfoFragment {
Defines a common UI for information fragments. Concrete implementations
of this class should ideally only provide the data, while the
AbstractInfoFragment contains the logic to present the UI and react to
user input.
-- Implements --
+ boolean isSharedElementVisible()
-- Generic --
Allows concrete fragment to get certain data or update the UI
--
# void refreshAdditionInfoFragment()
# HostManager getHostManager()
# HostInfo getHostInfo()
# void updateView(DataHolder dataHolder)
# RefreshItem getRefreshItem()
# void setExpandDescription(boolean expandDescription)
-- Media action bar --
Adding a listener in a concrete fragment will add the corresponding
button to the UI
--
# void setOnDownloadListener(final View.OnClickListener listener)
# void setOnAddToPlaylistListener(View.OnClickListener listener)
# void setOnGoToImdbListener(View.OnClickListener listener)
# void setOnSeenListener(final View.OnClickListener listener)
# void setOnPinClickedListener(final View.OnClickListener listener)
# void setDownloadButtonState(boolean state)
# void setSeenButtonState(boolean state)
# void setPinButtonState(boolean state)
-- Abstract methods --
Every concrete fragment is able to add additional info using an
AbstractAdditionalInfoFragment. This will be placed below the generic info.
Every concrete fragment should implement a refresh functionality.
The method setupMediaActionBar() will be called when the media action bar buttons
are available. This is where the concrete fragment should call the setOn*Listeners
to connect listeners to specific media action buttons.
The method setupFAB(ImageButton FAB) will be called to allow adding a listener.
It should return true to enable the FAB, false to disabled it.
--
# {abstract} AbstractAdditionalInfoFragment getAdditionalInfoFragment()
# {abstract} RefreshItem createRefreshItem()
# {abstract} boolean setupMediaActionBar()
# {abstract} boolean setupFAB(ImageButton FAB)
}
abstract class AbstractTabsFragment {
Defines a common UI for fragments that want to use a viewpager and a scrollable tab bar.
-- Implements --
+ boolean isSharedElementVisible()
--
# {abstract} TabsAdapter createTabsAdapter(AbstractInfoFragment.DataHolder dataHolder)
}
class ConcreteTabsFragment {
The returned TabsAdapter should hold all required tabs
# TabsAdapter createTabsAdapter(AbstractInfoFragment.DataHolder dataHolder)
}
class ConcreteInfoFragment {
Should be able to provide the required information by updating the DataHolder.
Use getDataHolder() to get the current DataHolder and update it. Use updateView(DataHolder)
to force an update of the UI with the updated DataHolder.
--
# AbstractAdditionalInfoFragment getAdditionalInfoFragment()
# RefreshItem createRefreshItem()
# boolean setupMediaActionBar()
}
class ConcreteAdditionalInfoFragment {
# void refresh()
}
abstract class AbstractListFragment {
# {abstract} AdapterView.OnItemClickListener createOnItemClickListener()
# {abstract} BaseAdapter createAdapter()
+ void hideRefreshAnimation()
+ void showRefreshAnimation()
+ BaseAdapter getAdapter()
+ TextView getEmptyView()
}
abstract class AbstractCursorListFragment {
# {abstract} void onListItemClicked(View view)
# {abstract} CursorLoader createCursorLoader()
# {abstract} String getListSyncType()
# AdapterView.OnItemClickListener createOnItemClickListener()
# String getSyncID()
# int getSyncItemID()
# void onRefresh()
+ void refreshList()
+ String getSearchFilter()
+ public void saveSearchState()
+ void onLoaderReset((Loader<Cursor> cursorLoader))
}
class ConcreteCursorListFragment {
# void onListItemClicked(View view)
# CursorLoader createCursorLoader()
# String getListSyncType()
# BaseAdapter createAdapter()
}
class ConcreteListFragment {
# AdapterView.OnItemClickListener createOnItemClickListener()
# BaseAdapter createAdapter()
# void onRefresh()
}
Fragment <|-- AbstractFragment
AbstractFragment <|-- AbstractAdditionalInfoFragment
AbstractFragment *--- DataHolder
AbstractFragment <|-- AbstractTabsFragment
AbstractFragment <|-- AbstractInfoFragment
SharedElement <|.. AbstractInfoFragment
SharedElement <|.. AbstractTabsFragment
AbstractTabsFragment <|-- ConcreteTabsFragment
AbstractInfoFragment <|-- ConcreteInfoFragment
AbstractAdditionalInfoFragment <|-- ConcreteAdditionalInfoFragment
Fragment <|-- AbstractListFragment
AbstractListFragment <|-- AbstractCursorListFragment
AbstractCursorListFragment <|-- ConcreteCursorListFragment
AbstractListFragment <|-- ConcreteListFragment
AbstractInfoFragment *--- AbstractAdditionalInfoFragment
@enduml

View File

@ -1,126 +0,0 @@
@startuml
abstract class BaseActivity {
# void onCreate(Bundle savedInstanceState)
+ void onPause()
}
class ConcreteActivity
class AppCompatActivity
AppCompatActivity <|-- BaseActivity
BaseActivity <|-- ConcreteActivity
abstract class AbstractDetailsFragment {
# {abstract} View createView(LayoutInflater inflater, ViewGroup container)
# {abstract} String getSyncType()
# {abstract} String getSyncID()
# {abstract} int getSyncItemID()
# {abstract} SwipeRefreshLayout getSwipeRefreshLayout()
# {abstract} void onDownload()
# {abstract} void onSyncProcessEnded(MediaSyncEvent event)
# void startSync(boolean silentRefresh)
+ void onCreate(@Nullable Bundle savedInstanceState)
+ View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
+ void onActivityCreated(@Nullable Bundle savedInstanceState)
+ void onStart()
+ void onResume()
+ void onPause()
+ void onStop()
+ void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
+ boolean onOptionsItemSelected(MenuItem item)
+ void onDownloadClicked(View v)
+ void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults)
+ void onRefresh ()
+ void onEventMainThread(MediaSyncEvent event)
+ void onServiceConnected(LibrarySyncService librarySyncService)
# HostManager getHostManager()
# HostInfo getHostInfo()
}
class ConcreteDetailsFragment {
# View createView(LayoutInflater inflater, ViewGroup container)
# String getSyncType()
# String getSyncID()
# int getSyncItemID()
# SwipeRefreshLayout getSwipeRefreshLayout()
# void onDownload()
# void onSyncProcessEnded(MediaSyncEvent event)
}
Fragment <|-- AbstractDetailsFragment
AbstractDetailsFragment <|-- ConcreteDetailsFragment
abstract class AbstractListFragment {
# {abstract} AdapterView.OnItemClickListener createOnItemClickListener()
# {abstract} BaseAdapter createAdapter()
+ View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
+ void onGlobalLayout()
+ void onSaveInstanceState(Bundle outState)
+ void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
+ boolean onOptionsItemSelected(MenuItem item)
- void toggleAmountOfColumns(MenuItem item)
+ void showRefreshAnimation()
+ BaseAdapter getAdapter()
+ TextView getEmptyView()
}
abstract class AbstractCursorListFragment {
# {abstract} void onListItemClicked(View view)
# {abstract} CursorLoader createCursorLoader()
# {abstract} String getListSyncType()
+ View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
+ void onActivityCreated (Bundle savedInstanceState)
+ void onStart()
+ void onResume()
+ void onPause()
+ void onStop()
+ void onSaveInstanceState(Bundle outState)
+ void onItemClick(AdapterView<?> parent, View view, int position, long id)
+ void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
+ boolean onOptionsItemSelected(MenuItem item)
+ void onRefresh()
+ void onEventMainThread(MediaSyncEvent event)
+ void onServiceConnected(LibrarySyncService librarySyncService)
+ void setSupportsSearch(boolean supportsSearch)
+ boolean onQueryTextChange(String newText)
+ boolean onQueryTextSubmit(String newText)
+ Loader<Cursor> onCreateLoader(int i, Bundle bundle)
+ void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor)
+ void onLoaderReset(Loader<Cursor> cursorLoader)
+ void saveSearchState()
+ String getSearchFilter()
+ void refreshList()
+ boolean onMenuItemActionExpand(MenuItem item)
+ boolean onMenuItemActionCollapse(MenuItem item)
+ void onClick(View v)
# AdapterView.OnItemClickListener createOnItemClickListener()
# String getSyncID()
# int getSyncItemID()
# void onSyncProcessEnded(MediaSyncEvent event)
# void onSwipeRefresh()
- void setupSearchMenuItem(Menu menu, MenuInflater inflater)
- void restartLoader()
}
class ConcreteCursorListFragment {
# void onListItemClicked(View view)
# CursorLoader createCursorLoader()
# String getListSyncType()
# BaseAdapter createAdapter()
}
class ConcreteListFragment {
# AdapterView.OnItemClickListener createOnItemClickListener()
# BaseAdapter createAdapter()
}
Fragment <|-- AbstractListFragment
AbstractListFragment <|-- AbstractCursorListFragment
AbstractCursorListFragment <|-- ConcreteCursorListFragment
AbstractListFragment <|-- ConcreteListFragment
@enduml

View File

@ -0,0 +1,98 @@
@startuml
Actor Activity
participant "AbstractListFragment" as A
participant "AbstractCursorListFragment" as B
participant "ConcreteCursorListFragment" as C
Activity -> A: onCreate
activate A
A -> C: createAdapter()
activate C
C --> A: return adapter
deactivate C
deactivate A
Activity -> B: onCreateView
activate B
B -> A: onCreateView
deactivate B
activate A
A -> A: connect this class as swipe refresh listener
A -> A: setup gridview
activate A
A -> B: createOnItemClickListener
activate B
B --> A: return click listener
deactivate B
deactivate A
A -> A: restore saved state (if any)
A -> A: enable options menu
A -> B:
deactivate A
activate B
B -> B: restore saved search query (if any)
activate B
deactivate B
deactivate B
Activity -> B: onActivityCreated
activate B
B -> LoaderManager: initialize cursor loader
activate LoaderManager
LoaderManager -> B: onCreateLoader
B -> C: createCursorLoader
activate B
activate C
C --> B: return cursorLoader
deactivate C
B --> LoaderManager: return cursorLoader
deactivate B
deactivate B
deactivate LoaderManager
Activity -> B: onStart
Activate B
B ->> LoaderManager: onStart
Activate LoaderManager
B -> B: bind to library sync service
activate B
deactivate B
deactivate B
LoaderManager ->> B: onLoadFinished
Activity -> B: onResume
Activate B
B -> B: register to eventbus
activate B
deactivate B
deactivate B
== UI active ==
...
Activity ->> B: user selects list item
activate B
B -> B: saveSearchState
activate B
deactivate B
B -> C: onListItemClicked(view)
Activate C
C -> Activity: onItemSelected
note over Activity: starts corresponding InfoFragment
deactivate C
deactivate B
== UI inactive ==
Activity -> B: onPause
activate B
B -> B: unregister from eventbus
activate B
deactivate B
deactivate B
Activity -> B: onStop
activate B
B -> B: unbind from library sync service
activate B
deactivate B
deactivate B
@enduml

View File

@ -0,0 +1,113 @@
@startuml
Actor Activity
participant "AbstractFragment" as A
participant "AbstractInfoFragment" as B
participant "ConcreteInfoFragment" as C
participant "AbstractAdditionalInfoFragment" as D
participant "ConcreteAdditionalInfoFragment" as E
participant "RefreshItem" as F
Activity -> A: onCreate
activate A
A -> A: setup DataHolder
activate A
deactivate A
deactivate A
Activity -> B: onCreateView
activate B
B -> B: set poster size to square or rectangular
B -> B: connect this class as swipe refresh listener
B -> C: getAdditionalInfoFragment()
activate C
C -> B: return AbstractAdditionalInfoFragment or null if none
deactivate C
B -> C: setupMediaActionBar()
activate C
C -> B: setOn...Listener(View.OnClickListener)
activate B
C -> B: setOn...Listener(View.OnClickListener)
C -> B: ...
note over C: setting a listener for each\naction button required, will\nmake it visible in the UI
note over B: Make button(s) visible\nin action bar and\nattach listener
B --> C
deactivate B
deactivate C
B -> C: setupFAB(ImageButton FAB)
activate C
note over C: add a listener for the FAB if needed
C -> B: return true to enable the FAB, false to disable it
B -> B: updateView(dataholder)
deactivate C
note left : updateView uses dataholder to fill views
activate B
deactivate B
B --> Activity: return view
deactivate B
Activity -> B: onStart
Activate B
B -> B: bind to library sync service
activate B
deactivate B
deactivate B
Activity -> B: onResume
Activate B
B -> B: register refresh item if it exists
activate B
deactivate B
deactivate B
== UI active ==
...
note over B: user requests a refresh
Activity -> B: onRefresh
activate B
B -> B: getRefreshItem()
B -> C: if no refreshItem then call createRefreshItem()
activate B
activate C
C -> B: return refresh item
deactivate C
deactivate B
B ->> F: startSync()
activate F
F -> F: start LibrarySyncService
B -> D: refresh()
activate D
D -> E: refresh()
activate E
E --> D
deactivate E
D --> B
deactivate D
deactivate B
...
F -> C: onSyncProcessEnded(MediaSyncEvent)
deactivate F
activate C
note over C: update dataholder
C -> B: updateView(dataholder)
deactivate C
...
== UI inactive ==
Activity -> B: onPause
activate B
B -> B: unregister refresh item if it exists
activate B
deactivate B
deactivate B
Activity -> B: onStop
activate B
B -> B: unbind from library sync service
activate B
deactivate B
deactivate B
@enduml

View File

@ -0,0 +1,358 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1080"
height="1920"
viewBox="0 0 1080 1920"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="detailsfragment.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="47.810667"
inkscape:cy="929.57115"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1280"
inkscape:window-height="751"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1"
borderlayer="true"
inkscape:showpageshadow="true"
showborder="true" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,867.63784)">
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.9477067;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11546"
width="1080"
height="1920"
x="0"
y="-867.63782" />
<rect
style="fill:#0288d1;fill-opacity:1;stroke:#ade2fe;stroke-opacity:1;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none"
id="rect3405"
width="1081"
height="145"
x="0"
y="-807.63782" />
<rect
style="fill:#0277bd;fill-opacity:1"
id="rect4228"
width="1081"
height="61"
x="0"
y="-867.63782" />
<rect
style="fill:#004a00;fill-opacity:1;stroke:#ade2fe;stroke-opacity:1;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4230"
width="1081"
height="565.71429"
x="0"
y="-662.63782" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;enable-background:accumulate"
id="path4750"
d="m 70.100471,-760.87886 4.25,4.2812 -14.5625,14.5625 34.75,0 0,6 -34.7188,0 14.5313,14.5625 -4.25,4.2187 -21.7813,-21.7812 z" />
<rect
style="fill:#4d4d4d;fill-opacity:1;stroke:#ade2fe;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6981"
width="1040.597"
height="596.02563"
x="19.701468"
y="-97.650917" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ade2fe;stroke-opacity:1;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none"
id="rect6979"
width="294.28571"
height="414.28571"
x="68.571373"
y="-319.06644" />
<rect
style="fill:#121314;fill-opacity:1;stroke:#ade2fe;stroke-width:4.00767612;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6983"
width="1040.1523"
height="421.68207"
x="19.923792"
y="501.59259" />
<rect
style="fill:#000000;fill-opacity:1"
id="rect6987"
width="1077.1428"
height="128.5714"
x="1.4285889"
y="923.79077" />
<circle
style="fill:#ffffff;fill-opacity:0;stroke:#ffffff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path7600"
cx="540.771"
cy="988.07648"
r="25.151453" />
<path
sodipodi:type="star"
style="fill:#ffffff;fill-opacity:0;stroke:#ffffff;stroke-width:27.63642311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path11538"
sodipodi:sides="3"
sodipodi:cx="254.28571"
sodipodi:cy="840.93353"
sodipodi:r1="181.80499"
sodipodi:r2="90.09066"
sodipodi:arg1="-1.0438531"
sodipodi:arg2="0.0033444541"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="M 345.71427,683.79069 344.66112,998.68441 72.48173,840.3255 Z"
inkscape:transform-center-x="6.4744758"
inkscape:transform-center-y="-0.70312468"
transform="matrix(0.15628051,0,0,0.17058614,178.22278,844.56599)" />
<path
sodipodi:type="star"
style="fill:#ffffff;fill-opacity:0;stroke:#ffffff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path11540"
sodipodi:sides="4"
sodipodi:cx="871.8484"
sodipodi:cy="988.07648"
sodipodi:r1="37.624657"
sodipodi:r2="26.60465"
sodipodi:arg1="0.78539816"
sodipodi:arg2="1.5707963"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 898.45304,1014.6811 -53.2093,0 0,-53.20927 53.2093,0 z" />
<circle
style="fill:#0288d1;fill-opacity:1;stroke:#0288d1;stroke-width:4.548;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path11542"
cx="931.42859"
cy="785.2193"
r="72.725868" />
<path
sodipodi:type="star"
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path11544"
sodipodi:sides="3"
sodipodi:cx="720"
sodipodi:cy="643.79077"
sodipodi:r1="52.450169"
sodipodi:r2="34.315464"
sodipodi:arg1="-2.0831858"
sodipodi:arg2="-1.0359882"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 694.28571,598.07649 78.16116,46.3022 -79.17946,44.53845 z"
inkscape:transform-center-x="-7.382508"
inkscape:transform-center-y="-0.5704157"
transform="matrix(0.46414272,0,0,0.39929271,595.43536,527.95863)" />
<rect
style="fill:#000000;fill-opacity:1;stroke:#ade2fe;stroke-width:3.22206235;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11548"
width="595.06366"
height="72.206512"
x="411.03958"
y="-73.741096" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="508.57144"
y="-27.637831"
id="text11550"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan11552"
x="508.57144"
y="-27.637831">Title</tspan></text>
<rect
style="fill:#000000;fill-opacity:1;stroke:#ade2fe;stroke-width:3.22206235;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11548-3"
width="595.06366"
height="72.206512"
x="411.03958"
y="23.401726" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="512.55719"
y="75.845695"
id="text11550-6"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan11552-7"
x="512.55719"
y="75.845695">Undertitle</tspan></text>
<rect
style="fill:#000000;fill-opacity:1;stroke:#ade2fe;stroke-width:4.02694607;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11548-3-5"
width="939.97308"
height="71.401627"
x="65.727768"
y="148.0899" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="433.92438"
y="195.84569"
id="text11550-6-3"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan11552-7-5"
x="433.92438"
y="195.84569">Action buttons</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="134.28571"
y="-159.06642"
id="text11620"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan11622"
x="134.28571"
y="-159.06642">Poster</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:68.84462738px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="484.48926"
y="-439.98108"
id="text11624"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan11626"
x="484.48926"
y="-439.98108">Art</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="174.28572"
y="-727.63788"
id="text11628"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan11630"
x="174.28572"
y="-727.63788">Title</tspan></text>
<path
inkscape:connector-curvature="0"
d="m 1008.5714,-751.06642 c 3.3,0 6,-2.7 6,-6 0,-3.3 -2.7,-6 -6,-6 -3.3,0 -6,2.7 -6,6 0,3.3 2.7,6 6,6 z m 0,6 c -3.3,0 -6,2.7 -6,6 0,3.3 2.7,6 6,6 3.3,0 6,-2.7 6,-6 0,-3.3 -2.7,-6 -6,-6 z m 0,18 c -3.3,0 -6,2.7 -6,6 0,3.3 2.7,6 6,6 3.3,0 6,-2.7 6,-6 0,-3.3 -2.7,-6 -6,-6 z"
id="path13804"
style="fill:#ffffff" />
<path
inkscape:connector-curvature="0"
d="m 913.81427,-754.07355 0,-14.85 -16.2,0 0,14.85 8.1,8.1 8.1,-8.1 z m -20.25,4.05 -14.85,0 0,16.2 14.85,0 8.1,-8.1 -8.1,-8.1 z m 4.05,20.25 0,14.85 16.2,0 0,-14.85 -8.1,-8.1 -8.1,8.1 z m 20.25,-20.25 -8.1,8.1 8.1,8.1 14.85,0 0,-16.2 -14.85,0 z"
id="path14002"
style="fill:#ffffff" />
<rect
style="fill:#000000;fill-opacity:1;stroke:#ade2fe;stroke-width:2.72044969;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11548-6"
width="421.27957"
height="72.708122"
x="65.074493"
y="258.86526" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="103.98576"
y="310.13141"
id="text11550-2"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan11552-9"
x="103.98576"
y="310.13141">Rating</tspan></text>
<rect
style="fill:#000000;fill-opacity:1;stroke:#ade2fe;stroke-width:2.72044969;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11548-6-1"
width="421.27957"
height="72.708122"
x="580.78876"
y="258.86523" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="739.01093"
y="304.41714"
id="text11550-2-2"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan11552-9-7"
x="739.01093"
y="304.41714">Details right</tspan></text>
<rect
style="fill:#000000;fill-opacity:1;stroke:#ade2fe;stroke-width:4.02694607;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11548-3-5-0"
width="939.97308"
height="71.401627"
x="61.442043"
y="368.0899" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="83.536552"
y="415.8457"
id="text11550-6-3-9"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan11552-7-5-3"
x="83.536552"
y="415.8457">Description</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="412.10797"
y="704.41711"
id="text11550-6-3-6"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan11552-7-5-0"
x="412.10797"
y="704.41711">Additional info</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="-437.14285"
y="249.50511"
id="text3468"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3470"
x="-437.14285"
y="249.50511" /></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB