Allow sorting of PVR Recordings and optionally hide watched items. (#694)
* Added menu options to PVR recordings view fragment: Sorting and hide_watched option. Default to unsorted (showing the order delivered by Kodi). Default to not hide watched recordings. (#385) * Implemented sorting and filtering of PVR recordings as per user's selection. (#385)
This commit is contained in:
parent
37ef130a7c
commit
a3ed983fee
|
@ -49,7 +49,8 @@ public class Settings {
|
||||||
SORT_BY_ALBUM = 5,
|
SORT_BY_ALBUM = 5,
|
||||||
SORT_BY_ARTIST = 6,
|
SORT_BY_ARTIST = 6,
|
||||||
SORT_BY_ARTIST_YEAR = 7,
|
SORT_BY_ARTIST_YEAR = 7,
|
||||||
SORT_BY_LAST_PLAYED = 8;
|
SORT_BY_LAST_PLAYED = 8,
|
||||||
|
UNSORTED = 9;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preferences keys.
|
* Preferences keys.
|
||||||
|
@ -136,6 +137,14 @@ public class Settings {
|
||||||
public static final String KEY_PREF_TVSHOWS_SHOW_WATCHED_STATUS = "tvshows_show_watched_status";
|
public static final String KEY_PREF_TVSHOWS_SHOW_WATCHED_STATUS = "tvshows_show_watched_status";
|
||||||
public static final boolean DEFAULT_PREF_TVSHOWS_SHOW_WATCHED_STATUS = true;
|
public static final boolean DEFAULT_PREF_TVSHOWS_SHOW_WATCHED_STATUS = true;
|
||||||
|
|
||||||
|
// Filter watched pvr recordings on movie list
|
||||||
|
public static final String KEY_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED = "pvr_recordings_filter_hide_watched";
|
||||||
|
public static final boolean DEFAULT_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED = false;
|
||||||
|
|
||||||
|
// Sort order on pvr recordings
|
||||||
|
public static final String KEY_PREF_PVR_RECORDINGS_SORT_ORDER = "pvr_recordings_sort_order";
|
||||||
|
public static final int DEFAULT_PREF_PVR_RECORDINGS_SORT_ORDER = UNSORTED;
|
||||||
|
|
||||||
// Filter disabled addons on addons list
|
// Filter disabled addons on addons list
|
||||||
public static final String KEY_PREF_ADDONS_FILTER_HIDE_DISABLED = "addons_filter_hide_disabled";
|
public static final String KEY_PREF_ADDONS_FILTER_HIDE_DISABLED = "addons_filter_hide_disabled";
|
||||||
public static final boolean DEFAULT_PREF_ADDONS_FILTER_HIDE_DISABLED = false;
|
public static final boolean DEFAULT_PREF_ADDONS_FILTER_HIDE_DISABLED = false;
|
||||||
|
|
|
@ -16,12 +16,17 @@
|
||||||
package org.xbmc.kore.ui.sections.video;
|
package org.xbmc.kore.ui.sections.video;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
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.preference.PreferenceManager;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.widget.SwipeRefreshLayout;
|
import android.support.v4.widget.SwipeRefreshLayout;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
|
@ -32,6 +37,7 @@ import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.xbmc.kore.R;
|
import org.xbmc.kore.R;
|
||||||
|
import org.xbmc.kore.Settings;
|
||||||
import org.xbmc.kore.host.HostManager;
|
import org.xbmc.kore.host.HostManager;
|
||||||
import org.xbmc.kore.jsonrpc.ApiCallback;
|
import org.xbmc.kore.jsonrpc.ApiCallback;
|
||||||
import org.xbmc.kore.jsonrpc.method.PVR;
|
import org.xbmc.kore.jsonrpc.method.PVR;
|
||||||
|
@ -40,6 +46,9 @@ import org.xbmc.kore.jsonrpc.type.PVRType;
|
||||||
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 java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
|
@ -96,7 +105,7 @@ public class PVRRecordingsListFragment extends Fragment
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated (Bundle savedInstanceState) {
|
public void onActivityCreated (Bundle savedInstanceState) {
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
setHasOptionsMenu(false);
|
setHasOptionsMenu(true);
|
||||||
browseRecordings();
|
browseRecordings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +115,91 @@ public class PVRRecordingsListFragment extends Fragment
|
||||||
unbinder.unbind();
|
unbinder.unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
if (!isAdded()) {
|
||||||
|
// HACK: Fix crash reported on Play Store. Why does this is necessary is beyond me
|
||||||
|
// copied from MovieListFragment#onCreateOptionsMenu
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inflater.inflate(R.menu.pvr_recording_list, menu);
|
||||||
|
|
||||||
|
// Setup filters
|
||||||
|
MenuItem hideWatched = menu.findItem(R.id.action_hide_watched),
|
||||||
|
sortByNameAndDate = menu.findItem(R.id.action_sort_by_name_and_date_added),
|
||||||
|
sortByDateAdded = menu.findItem(R.id.action_sort_by_date_added),
|
||||||
|
unsorted = menu.findItem(R.id.action_unsorted);
|
||||||
|
|
||||||
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
|
hideWatched.setChecked(preferences.getBoolean(Settings.KEY_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED, Settings.DEFAULT_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED));
|
||||||
|
|
||||||
|
int sortOrder = preferences.getInt(Settings.KEY_PREF_PVR_RECORDINGS_SORT_ORDER, Settings.DEFAULT_PREF_PVR_RECORDINGS_SORT_ORDER);
|
||||||
|
switch (sortOrder) {
|
||||||
|
case Settings.SORT_BY_DATE_ADDED:
|
||||||
|
sortByDateAdded.setChecked(true);
|
||||||
|
break;
|
||||||
|
case Settings.SORT_BY_NAME:
|
||||||
|
sortByNameAndDate.setChecked(true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
unsorted.setChecked(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to reload the items in the list
|
||||||
|
*/
|
||||||
|
public void refreshList() {
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.action_hide_watched:
|
||||||
|
item.setChecked(!item.isChecked());
|
||||||
|
preferences.edit()
|
||||||
|
.putBoolean(Settings.KEY_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED, item.isChecked())
|
||||||
|
.apply();
|
||||||
|
refreshList();
|
||||||
|
break;
|
||||||
|
case R.id.action_sort_by_name_and_date_added:
|
||||||
|
item.setChecked(true);
|
||||||
|
preferences.edit()
|
||||||
|
.putInt(Settings.KEY_PREF_PVR_RECORDINGS_SORT_ORDER, Settings.SORT_BY_NAME)
|
||||||
|
.apply();
|
||||||
|
refreshList();
|
||||||
|
break;
|
||||||
|
case R.id.action_sort_by_date_added:
|
||||||
|
item.setChecked(true);
|
||||||
|
preferences.edit()
|
||||||
|
.putInt(Settings.KEY_PREF_PVR_RECORDINGS_SORT_ORDER, Settings.SORT_BY_DATE_ADDED)
|
||||||
|
.apply();
|
||||||
|
refreshList();
|
||||||
|
break;
|
||||||
|
case R.id.action_unsorted:
|
||||||
|
item.setChecked(true);
|
||||||
|
preferences.edit()
|
||||||
|
.putInt(Settings.KEY_PREF_PVR_RECORDINGS_SORT_ORDER, Settings.UNSORTED)
|
||||||
|
.apply();
|
||||||
|
refreshList();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Swipe refresh layout callback
|
* Swipe refresh layout callback
|
||||||
*/
|
*/
|
||||||
|
@ -134,10 +228,90 @@ public class PVRRecordingsListFragment extends Fragment
|
||||||
|
|
||||||
// To prevent the empty text from appearing on the first load, set it now
|
// To prevent the empty text from appearing on the first load, set it now
|
||||||
emptyView.setText(getString(R.string.no_recordings_found_refresh));
|
emptyView.setText(getString(R.string.no_recordings_found_refresh));
|
||||||
setupRecordingsGridview(result);
|
|
||||||
|
// As the JSON RPC API does not support sorting or filter parameters for PVR.GetRecordings
|
||||||
|
// we apply the sorting and filtering right here.
|
||||||
|
// See https://kodi.wiki/view/JSON-RPC_API/v9#PVR.GetRecordings
|
||||||
|
List<PVRType.DetailsRecording> finalResult = filter(result);
|
||||||
|
sort(finalResult);
|
||||||
|
|
||||||
|
setupRecordingsGridview(finalResult);
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<PVRType.DetailsRecording> filter(List<PVRType.DetailsRecording> itemList) {
|
||||||
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
|
boolean hideWatched = preferences.getBoolean(Settings.KEY_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED, Settings.DEFAULT_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED);
|
||||||
|
|
||||||
|
if (!hideWatched) {
|
||||||
|
return itemList;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PVRType.DetailsRecording> result = new ArrayList<>(itemList.size());
|
||||||
|
for (PVRType.DetailsRecording item:itemList) {
|
||||||
|
if (hideWatched) {
|
||||||
|
if (item.playcount > 0) {
|
||||||
|
continue; // Skip this item as it is played.
|
||||||
|
} else {
|
||||||
|
// Heuristic: Try to guess if it's play from resume timestamp.
|
||||||
|
double resumePosition = item.resume.position;
|
||||||
|
int runtime = item.runtime;
|
||||||
|
if (runtime < resumePosition) {
|
||||||
|
// Tv show duration is smaller than resume position.
|
||||||
|
// The tv show likely has been watched.
|
||||||
|
// It's still possible some minutes have not yet been watched
|
||||||
|
// at the end of the show as some minutes at the
|
||||||
|
// recording start do not belong to the show.
|
||||||
|
// Never the less skip this item.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// more conditions may be added here
|
||||||
|
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sort(List<PVRType.DetailsRecording> itemList) {
|
||||||
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
|
|
||||||
|
int sortOrder = preferences.getInt(Settings.KEY_PREF_PVR_RECORDINGS_SORT_ORDER, Settings.DEFAULT_PREF_PVR_RECORDINGS_SORT_ORDER);
|
||||||
|
|
||||||
|
Comparator<PVRType.DetailsRecording> comparator;
|
||||||
|
switch (sortOrder) {
|
||||||
|
case Settings.SORT_BY_DATE_ADDED:
|
||||||
|
// sort by recording start time descending (most current first)
|
||||||
|
// luckily the starttime is in sortable format yyyy-MM-dd hh:mm:ss
|
||||||
|
comparator = new Comparator<PVRType.DetailsRecording>() {
|
||||||
|
@Override
|
||||||
|
public int compare(PVRType.DetailsRecording a, PVRType.DetailsRecording b) {
|
||||||
|
return b.starttime.compareTo(a.starttime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Collections.sort(itemList, comparator);
|
||||||
|
break;
|
||||||
|
case Settings.SORT_BY_NAME:
|
||||||
|
// sort by recording title and start time
|
||||||
|
comparator = new Comparator<PVRType.DetailsRecording>() {
|
||||||
|
@Override
|
||||||
|
public int compare(PVRType.DetailsRecording a, PVRType.DetailsRecording b) {
|
||||||
|
int result = a.title.compareToIgnoreCase(b.title);
|
||||||
|
if (0 == result) { // note the yoda condition ;)
|
||||||
|
// sort by starttime descending (most current first)
|
||||||
|
result = b.starttime.compareTo(a.starttime);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Collections.sort(itemList, comparator);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(int errorCode, String description) {
|
public void onError(int errorCode, String description) {
|
||||||
if (!isAdded()) return;
|
if (!isAdded()) return;
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?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.
|
||||||
|
-->
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:title="@string/sort_order"
|
||||||
|
app:showAsAction="never">
|
||||||
|
<menu>
|
||||||
|
<group
|
||||||
|
android:checkableBehavior="single">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_sort_by_name_and_date_added"
|
||||||
|
android:title="@string/sort_by_name"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_sort_by_date_added"
|
||||||
|
android:title="@string/sort_by_date_added"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_unsorted"
|
||||||
|
android:title="@string/unsorted"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
</group>
|
||||||
|
</menu>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<group
|
||||||
|
android:checkableBehavior="all">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_hide_watched"
|
||||||
|
android:title="@string/hide_watched"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</menu>
|
|
@ -325,6 +325,7 @@
|
||||||
<string name="sort_by_length">Nach Länge</string>
|
<string name="sort_by_length">Nach Länge</string>
|
||||||
<string name="sort_by_date_added">Nach Hinzufügungsdatum</string>
|
<string name="sort_by_date_added">Nach Hinzufügungsdatum</string>
|
||||||
<string name="sort_by_last_played">Zuletzt gespielt</string>
|
<string name="sort_by_last_played">Zuletzt gespielt</string>
|
||||||
|
<string name="unsorted">Nicht sortiert</string>
|
||||||
<string name="ignore_prefixes">Präfixe ignorieren</string>
|
<string name="ignore_prefixes">Präfixe ignorieren</string>
|
||||||
|
|
||||||
<!-- Preferences strings -->
|
<!-- Preferences strings -->
|
||||||
|
|
|
@ -340,6 +340,7 @@
|
||||||
<string name="sort_by_length">By length</string>
|
<string name="sort_by_length">By length</string>
|
||||||
<string name="sort_by_date_added">By date added</string>
|
<string name="sort_by_date_added">By date added</string>
|
||||||
<string name="sort_by_last_played">By last played</string>
|
<string name="sort_by_last_played">By last played</string>
|
||||||
|
<string name="unsorted">Unsorted</string>
|
||||||
<string name="ignore_prefixes">Ignore prefixes</string>
|
<string name="ignore_prefixes">Ignore prefixes</string>
|
||||||
<string name="show_watched_status">Show watched status</string>
|
<string name="show_watched_status">Show watched status</string>
|
||||||
<string name="show_rating">Show rating</string>
|
<string name="show_rating">Show rating</string>
|
||||||
|
|
Loading…
Reference in New Issue