diff options
48 files changed, 518 insertions, 230 deletions
diff --git a/app/build.gradle b/app/build.gradle index bcd7a381d..3299ab715 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,6 +14,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled true + vectorDrawables.useSupportLibrary true // Version code schema: // "1.2.3-SNAPSHOT" -> 1020300 // "1.2.3-RC4" -> 1020304 @@ -158,6 +159,7 @@ dependencies { } implementation "com.github.shts:TriangleLabelView:$triangleLabelViewVersion" + implementation 'com.leinardi.android:speed-dial:1.0.2' // 1.0.2 uses support 27.1.1 ; newer versions use 28.0.0; implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java index b80482c53..27d76116d 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java @@ -7,6 +7,8 @@ import android.preference.PreferenceManager; import android.test.InstrumentationTestCase; import android.util.Log; +import org.awaitility.Awaitility; + import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -24,8 +26,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.PodDBAdapter; - -import org.awaitility.Awaitility; +import de.danoeh.antennapod.core.util.Consumer; /** * Test class for DBWriter @@ -574,29 +575,16 @@ public class DBWriterTest extends InstrumentationTestCase { public void testRemoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException { final int NUM_ITEMS = 10; final Context context = getInstrumentation().getTargetContext(); - Feed feed = new Feed("url", null, "title"); - feed.setItems(new ArrayList<>()); - for (int i = 0; i < NUM_ITEMS; i++) { - FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed); - feed.getItems().add(item); - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feed); - adapter.close(); + Feed feed = createTestFeed(NUM_ITEMS); - for (FeedItem item : feed.getItems()) { - assertTrue(item.getId() != 0); - } for (int removeIndex = 0; removeIndex < NUM_ITEMS; removeIndex++) { final FeedItem item = feed.getItems().get(removeIndex); - adapter = PodDBAdapter.getInstance(); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setQueue(feed.getItems()); adapter.close(); - DBWriter.removeQueueItem(context, item, false).get(TIMEOUT, TimeUnit.SECONDS); + DBWriter.removeQueueItem(context, false, item).get(TIMEOUT, TimeUnit.SECONDS); adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor queue = adapter.getQueueIDCursor(); @@ -616,6 +604,43 @@ public class DBWriterTest extends InstrumentationTestCase { } } + public void testRemoveQueueItemMultipleItems() throws InterruptedException, ExecutionException, TimeoutException { + // Setup test data + // + final int NUM_ITEMS = 5; + final int NUM_IN_QUEUE = NUM_ITEMS - 1; // the last one not in queue for boundary condition + final Context context = getInstrumentation().getTargetContext(); + Feed feed = createTestFeed(NUM_ITEMS); + + List<FeedItem> itemsToAdd = feed.getItems().subList(0, NUM_IN_QUEUE); + withPodDB(adapter -> adapter.setQueue(itemsToAdd) ); + + // Actual tests + // + + // Use array rather than List to make codes more succinct + Long[] itemIds = toItemIds(feed.getItems()).toArray(new Long[0]); + + DBWriter.removeQueueItem(context, false, + itemIds[1], itemIds[3]).get(TIMEOUT, TimeUnit.SECONDS); + assertQueueByItemIds("Average case - 2 items removed successfully", + itemIds[0], itemIds[2]); + + DBWriter.removeQueueItem(context, false).get(TIMEOUT, TimeUnit.SECONDS); + assertQueueByItemIds("Boundary case - no items supplied. queue should see no change", + itemIds[0], itemIds[2]); + + DBWriter.removeQueueItem(context, false, + itemIds[0], itemIds[4], -1L).get(TIMEOUT, TimeUnit.SECONDS); + assertQueueByItemIds("Boundary case - items not in queue ignored", + itemIds[2]); + + DBWriter.removeQueueItem(context, false, + itemIds[2], -1L).get(TIMEOUT, TimeUnit.SECONDS); + assertQueueByItemIds("Boundary case - invalid itemIds ignored"); // the queue is empty + + } + public void testMoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException { final int NUM_ITEMS = 10; Feed feed = new Feed("url", null, "title"); @@ -713,4 +738,53 @@ public class DBWriterTest extends InstrumentationTestCase { assertTrue(item.isPlayed()); } } + + private static Feed createTestFeed(int numItems) { + Feed feed = new Feed("url", null, "title"); + feed.setItems(new ArrayList<>()); + for (int i = 0; i < numItems; i++) { + FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed); + feed.getItems().add(item); + } + + withPodDB(adapter -> adapter.setCompleteFeed(feed)); + + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + } + return feed; + } + + private static void withPodDB(Consumer<PodDBAdapter> action) { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + try { + adapter.open(); + action.accept(adapter); + } finally { + adapter.close(); + } + } + + private static void assertQueueByItemIds( + String message, + long... itemIdsExpected + ) { + List<FeedItem> queue = DBReader.getQueue(); + List<Long> itemIdsActualList = toItemIds(queue); + List<Long> itemIdsExpectedList = new ArrayList<Long>(itemIdsExpected.length); + for (long id : itemIdsExpected) { + itemIdsExpectedList.add(id); + } + + assertEquals(message, itemIdsExpectedList, itemIdsActualList); + } + + private static List<Long> toItemIds(List<FeedItem> items) { + List<Long> itemIds = new ArrayList<Long>(items.size()); + for(FeedItem item : items) { + itemIds.add(item.getId()); + } + return itemIds; + } + } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java index 07a64cde8..02071e355 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java @@ -4,9 +4,17 @@ import android.app.AlertDialog; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.support.annotation.IdRes; +import android.support.annotation.NonNull; +import android.support.annotation.PluralsRes; +import android.support.annotation.StringRes; +import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.util.ArrayMap; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -14,11 +22,12 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.ListView; -import android.widget.Toast; + +import com.leinardi.android.speeddial.SpeedDialView; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -35,22 +44,42 @@ public class EpisodesApplyActionFragment extends Fragment { public static final String TAG = "EpisodeActionFragment"; - public static final int ACTION_QUEUE = 1; - private static final int ACTION_MARK_PLAYED = 2; - private static final int ACTION_MARK_UNPLAYED = 4; - private static final int ACTION_DOWNLOAD = 8; - public static final int ACTION_REMOVE = 16; - private static final int ACTION_ALL = ACTION_QUEUE | ACTION_MARK_PLAYED | ACTION_MARK_UNPLAYED - | ACTION_DOWNLOAD | ACTION_REMOVE; + public static final int ACTION_ADD_TO_QUEUE = 1; + private static final int ACTION_REMOVE_FROM_QUEUE = 2; + private static final int ACTION_MARK_PLAYED = 4; + private static final int ACTION_MARK_UNPLAYED = 8; + private static final int ACTION_DOWNLOAD = 16; + public static final int ACTION_DELETE = 32; + private static final int ACTION_ALL = ACTION_ADD_TO_QUEUE | ACTION_REMOVE_FROM_QUEUE + | ACTION_MARK_PLAYED | ACTION_MARK_UNPLAYED | ACTION_DOWNLOAD | ACTION_DELETE; + + /** + * Specify an action (defined by #flag) 's UI bindings. + * + * Includes: the menu / action item and the actual logic + */ + private class ActionBinding { + int flag; + @IdRes + final int actionItemId; + @NonNull + final Runnable action; + + ActionBinding(int flag, @IdRes int actionItemId, @NonNull Runnable action) { + this.flag = flag; + this.actionItemId = actionItemId; + this.action = action; + } + } + + private final List<? extends ActionBinding> actionBindings; private ListView mListView; private ArrayAdapter<String> mAdapter; - private Button btnAddToQueue; - private Button btnMarkAsPlayed; - private Button btnMarkAsUnplayed; - private Button btnDownload; - private Button btnDelete; + private SpeedDialView mSpeedDialView; + @NonNull + private CharSequence actionBarTitleOriginal = ""; private final Map<Long,FeedItem> idMap = new ArrayMap<>(); private final List<FeedItem> episodes = new ArrayList<>(); @@ -60,6 +89,23 @@ public class EpisodesApplyActionFragment extends Fragment { private MenuItem mSelectToggle; + public EpisodesApplyActionFragment() { + actionBindings = Arrays.asList( + new ActionBinding(ACTION_ADD_TO_QUEUE, + R.id.add_to_queue_batch, this::queueChecked), + new ActionBinding(ACTION_REMOVE_FROM_QUEUE, + R.id.remove_from_queue_batch, this::removeFromQueueChecked), + new ActionBinding(ACTION_MARK_PLAYED, + R.id.mark_read_batch, this::markedCheckedPlayed), + new ActionBinding(ACTION_MARK_UNPLAYED, + R.id.mark_unread_batch, this::markedCheckedUnplayed), + new ActionBinding(ACTION_DOWNLOAD, + R.id.download_batch, this::downloadChecked), + new ActionBinding(ACTION_DELETE, + R.id.delete_batch, this::deleteChecked) + ); + } + public static EpisodesApplyActionFragment newInstance(List<FeedItem> items) { return newInstance(items, ACTION_ALL); } @@ -124,57 +170,58 @@ public class EpisodesApplyActionFragment extends Fragment { } mAdapter = new ArrayAdapter<>(getActivity(), - android.R.layout.simple_list_item_multiple_choice, titles); + R.layout.simple_list_item_multiple_choice_on_start, titles); mListView.setAdapter(mAdapter); - checkAll(); - int lastVisibleDiv = 0; - btnAddToQueue = view.findViewById(R.id.btnAddToQueue); - if((actions & ACTION_QUEUE) != 0) { - btnAddToQueue.setOnClickListener(v -> queueChecked()); - lastVisibleDiv = R.id.divider1; - } else { - btnAddToQueue.setVisibility(View.GONE); - view.findViewById(R.id.divider1).setVisibility(View.GONE); - } - btnMarkAsPlayed = view.findViewById(R.id.btnMarkAsPlayed); - if((actions & ACTION_MARK_PLAYED) != 0) { - btnMarkAsPlayed.setOnClickListener(v -> markedCheckedPlayed()); - lastVisibleDiv = R.id.divider2; - } else { - btnMarkAsPlayed.setVisibility(View.GONE); - view.findViewById(R.id.divider2).setVisibility(View.GONE); - } - btnMarkAsUnplayed = view.findViewById(R.id.btnMarkAsUnplayed); - if((actions & ACTION_MARK_UNPLAYED) != 0) { - btnMarkAsUnplayed.setOnClickListener(v -> markedCheckedUnplayed()); - lastVisibleDiv = R.id.divider3; - } else { - btnMarkAsUnplayed.setVisibility(View.GONE); - view.findViewById(R.id.divider3).setVisibility(View.GONE); - } - btnDownload = view.findViewById(R.id.btnDownload); - if((actions & ACTION_DOWNLOAD) != 0) { - btnDownload.setOnClickListener(v -> downloadChecked()); - lastVisibleDiv = R.id.divider4; - } else { - btnDownload.setVisibility(View.GONE); - view.findViewById(R.id.divider4).setVisibility(View.GONE); - } - btnDelete = view.findViewById(R.id.btnDelete); - if((actions & ACTION_REMOVE) != 0) { - btnDelete.setOnClickListener(v -> deleteChecked()); - } else { - btnDelete.setVisibility(View.GONE); - if(lastVisibleDiv > 0) { - view.findViewById(lastVisibleDiv).setVisibility(View.GONE); + saveActionBarTitle(); // needed when we dynamically change the title based on selection + + // Init action UI (via a FAB Speed Dial) + mSpeedDialView = view.findViewById(R.id.fabSD); + mSpeedDialView.inflate(R.menu.episodes_apply_action_speeddial); + + // show only specified actions, and bind speed dial UIs to the actual logic + for (ActionBinding binding : actionBindings) { + if ((actions & binding.flag) == 0) { + mSpeedDialView.removeActionItemById(binding.actionItemId); } } + mSpeedDialView.setOnActionSelectedListener(actionItem -> { + ActionBinding selectedBinding = null; + for (ActionBinding binding : actionBindings) { + if (actionItem.getId() == binding.actionItemId) { + selectedBinding = binding; + break; + } + } + if (selectedBinding != null) { + selectedBinding.action.run(); + } else { + Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + actionItem.getId()); + } + return true; + }); + + showSpeedDialIfAnyChecked(); + return view; } @Override + public void onStop() { + restoreActionBarTitle(); // it might have been changed to "N selected". Restore original. + super.onStop(); + } + + private void showSpeedDialIfAnyChecked() { + mSpeedDialView.setVisibility(checkedIds.size() > 0 ? View.VISIBLE : View.GONE); + } + + private void hideSpeedDial() { + mSpeedDialView.setVisibility(View.GONE); + } + + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.episodes_apply_action_options, menu); @@ -196,11 +243,9 @@ public class EpisodesApplyActionFragment extends Fragment { int[] icon = new int[1]; if (checkedIds.size() == episodes.size()) { - icon[0] = R.attr.ic_check_box; - } else if (checkedIds.size() == 0) { - icon[0] = R.attr.ic_check_box_outline; + icon[0] = R.attr.ic_select_none; } else { - icon[0] = R.attr.ic_indeterminate_check_box; + icon[0] = R.attr.ic_select_all; } TypedArray a = getActivity().obtainStyledAttributes(icon); @@ -212,7 +257,7 @@ public class EpisodesApplyActionFragment extends Fragment { @Override public boolean onOptionsItemSelected(MenuItem item) { - int resId = 0; + @StringRes int resId = 0; switch(item.getItemId()) { case R.id.select_options: return true; @@ -272,7 +317,8 @@ public class EpisodesApplyActionFragment extends Fragment { return true; } if(resId != 0) { - Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show(); + Snackbar.make(getActivity().findViewById(R.id.content), resId, Snackbar.LENGTH_SHORT) + .show(); return true; } else { return false; @@ -410,21 +456,56 @@ public class EpisodesApplyActionFragment extends Fragment { mListView.setItemChecked(i, checked); } ActivityCompat.invalidateOptionsMenu(EpisodesApplyActionFragment.this.getActivity()); + showSpeedDialIfAnyChecked(); + updateActionBarTitle(); + } + + private void saveActionBarTitle() { + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) { + CharSequence title = actionBar.getTitle(); + if (title == null) { + title = ""; + } + actionBarTitleOriginal = title; + } + } + + private void restoreActionBarTitle() { + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(actionBarTitleOriginal); + } + } + + private void updateActionBarTitle() { + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) { + CharSequence title = checkedIds.size() > 0 ? + getString(R.string.num_selected_label, checkedIds.size()) : + actionBarTitleOriginal; + actionBar.setTitle(title); + } } private void queueChecked() { DBWriter.addQueueItem(getActivity(), true, checkedIds.toArray()); - close(); + close(R.plurals.added_to_queue_batch_label, checkedIds.size()); + } + + private void removeFromQueueChecked() { + DBWriter.removeQueueItem(getActivity(), true, checkedIds.toArray()); + close(R.plurals.removed_from_queue_batch_label, checkedIds.size()); } private void markedCheckedPlayed() { DBWriter.markItemPlayed(FeedItem.PLAYED, checkedIds.toArray()); - close(); + close(R.plurals.marked_read_batch_label, checkedIds.size()); } private void markedCheckedUnplayed() { DBWriter.markItemPlayed(FeedItem.UNPLAYED, checkedIds.toArray()); - close(); + close(R.plurals.marked_unread_batch_label, checkedIds.size()); } private void downloadChecked() { @@ -441,7 +522,7 @@ public class EpisodesApplyActionFragment extends Fragment { e.printStackTrace(); DownloadRequestErrorDialogCreator.newRequestErrorDialog(getActivity(), e.getMessage()); } - close(); + close(R.plurals.downloading_batch_label, checkedIds.size()); } private void deleteChecked() { @@ -451,10 +532,18 @@ public class EpisodesApplyActionFragment extends Fragment { DBWriter.deleteFeedMediaOfItem(getActivity(), episode.getMedia().getId()); } } - close(); + close(R.plurals.deleted_episode_batch_label, checkedIds.size()); } - private void close() { + private void close(@PluralsRes int msgId, int numItems) { + if (numItems > 0) { + Snackbar.make(getActivity().findViewById(R.id.content), + getResources().getQuantityString(msgId, numItems, numItems), + Snackbar.LENGTH_LONG + ) + .setAction(android.R.string.ok, v -> {}) + .show(); + } getActivity().getSupportFragmentManager().popBackStack(); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java index ddf9044ab..b52fd444f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -149,7 +149,7 @@ public class CompletedDownloadsFragment extends ListFragment { switch (item.getItemId()) { case R.id.episode_actions: EpisodesApplyActionFragment fragment = EpisodesApplyActionFragment - .newInstance(items, EpisodesApplyActionFragment.ACTION_REMOVE | EpisodesApplyActionFragment.ACTION_QUEUE); + .newInstance(items, EpisodesApplyActionFragment.ACTION_DELETE | EpisodesApplyActionFragment.ACTION_ADD_TO_QUEUE); ((MainActivity) getActivity()).loadChildFragment(fragment); return true; default: diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index 763bc487b..0e8f2f083 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -431,7 +431,7 @@ public class QueueFragment extends Fragment { final FeedItem item = queue.get(position); final boolean isRead = item.isPlayed(); DBWriter.markItemPlayed(FeedItem.PLAYED, false, item.getId()); - DBWriter.removeQueueItem(getActivity(), item, true); + DBWriter.removeQueueItem(getActivity(), true, item); Snackbar snackbar = Snackbar.make(root, getString(R.string.marked_as_read_label), Snackbar.LENGTH_LONG); snackbar.setAction(getString(R.string.undo), v -> { DBWriter.addQueueItemAt(getActivity(), item.getId(), position, false); diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java index 321b9c7bb..3f8d88af7 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -199,7 +199,7 @@ public class FeedItemMenuHandler { DBWriter.addQueueItem(context, selectedItem); break; case R.id.remove_from_queue_item: - DBWriter.removeQueueItem(context, selectedItem, true); + DBWriter.removeQueueItem(context, true, selectedItem); break; case R.id.add_to_favorites_item: DBWriter.addFavoriteItem(selectedItem); diff --git a/app/src/main/res/layout/downloaded_episodeslist_item.xml b/app/src/main/res/layout/downloaded_episodeslist_item.xml index 66ae6c180..65a08251f 100644 --- a/app/src/main/res/layout/downloaded_episodeslist_item.xml +++ b/app/src/main/res/layout/downloaded_episodeslist_item.xml @@ -92,7 +92,7 @@ android:layout_height="match_parent" android:background="?attr/selectableItemBackground" android:clickable="false" - android:contentDescription="@string/remove_episode_lable" + android:contentDescription="@string/delete_episode_label" android:focusable="false" android:focusableInTouchMode="false" android:src="?attr/content_discard" diff --git a/app/src/main/res/layout/episodes_apply_action_fragment.xml b/app/src/main/res/layout/episodes_apply_action_fragment.xml index e9a2e2e23..984e960d8 100644 --- a/app/src/main/res/layout/episodes_apply_action_fragment.xml +++ b/app/src/main/res/layout/episodes_apply_action_fragment.xml @@ -1,115 +1,50 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> - - <LinearLayout - android:id="@+id/bottomBar" - android:layout_width="match_parent" - android:layout_height="68dp" - android:layout_alignParentBottom="true" - android:gravity="center_vertical" - android:orientation="horizontal" - android:padding="4dp"> - - <Button - android:id="@+id/btnAddToQueue" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:background="@android:color/transparent" - android:drawableTop="?attr/content_new" - android:text="@string/add_to_queue_label" - android:textSize="10sp" /> - - <View - android:id="@+id/divider1" - android:layout_width="1dp" - android:layout_height="match_parent" - android:layout_margin="4dp" - android:background="?android:attr/listDivider" - tools:background="@android:color/holo_red_dark" /> - - <Button - android:id="@+id/btnMarkAsPlayed" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:background="@android:color/transparent" - android:drawableTop="?attr/navigation_accept" - android:text="@string/mark_read_label" - android:textSize="10sp" /> - - <View - android:id="@+id/divider2" - android:layout_width="1dp" - android:layout_height="match_parent" - android:layout_margin="4dp" - android:background="?android:attr/listDivider" - tools:background="@android:color/holo_red_dark" /> - - <Button - android:id="@+id/btnMarkAsUnplayed" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:background="@android:color/transparent" - android:drawableTop="?attr/navigation_cancel" - android:text="@string/mark_unread_label" - android:textSize="10sp" /> - - <View - android:id="@+id/divider3" - android:layout_width="1dp" - android:layout_height="match_parent" - android:layout_margin="4dp" - android:background="?android:attr/listDivider" - tools:background="@android:color/holo_red_dark" /> - - <Button - android:id="@+id/btnDownload" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:background="@android:color/transparent" - android:drawableTop="?attr/av_download" - android:text="@string/download_label" - android:textSize="10sp" /> - - <View - android:id="@+id/divider4" - android:layout_width="1dp" - android:layout_height="match_parent" - android:layout_margin="4dp" - android:background="?android:attr/listDivider" - tools:background="@android:color/holo_red_dark" /> - - <Button - android:id="@+id/btnDelete" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:background="@android:color/transparent" - android:drawableTop="?attr/content_discard" - android:text="@string/remove_episode_lable" - android:textSize="10sp" /> - - </LinearLayout> - - <View - android:id="@+id/divider" - android:layout_width="match_parent" - android:layout_height="1dp" - android:layout_above="@id/bottomBar" - android:background="?android:attr/listDivider" - android:paddingBottom="4dp" - tools:background="@android:color/holo_red_dark" /> - <ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_above="@id/divider"/> + android:layout_alignParentTop="true" + android:layout_marginTop="0dp" /> + + <com.leinardi.android.speeddial.SpeedDialOverlayLayout + android:id="@+id/fabSDOverlay" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + <!-- The FAB SpeedDial + 1. MUST be placed at the bottom of the layout xml to ensure it is at the front, + clickable on Pre-Lollipop devices (that do not support elevation). + See: https://stackoverflow.com/a/2614402 + 2. ScrollView is needed to ensure the vertical list of speed dials are + accessible when screen height is small, eg., landscape mode on most phones. + --> + <ScrollView + android:id="@+id/fabSDScrollCtr" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:elevation="@dimen/sd_close_elevation" + tools:ignore="UnusedAttribute"> + <!-- android:elevation: + 1. Needs to match the speed dial's minimal elevation, + or the speed dial can't be clicked at all + --> + <com.leinardi.android.speeddial.SpeedDialView + android:id="@+id/fabSD" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:sdMainFabClosedSrc="@drawable/ic_fab_edit" + app:sdOverlayLayout="@id/fabSDOverlay" + android:layout_marginEnd="16dp" + android:layout_marginRight="16dp" + android:layout_marginBottom="16dp" + /> + </ScrollView> </RelativeLayout> diff --git a/app/src/main/res/layout/simple_list_item_multiple_choice_on_start.xml b/app/src/main/res/layout/simple_list_item_multiple_choice_on_start.xml new file mode 100644 index 000000000..3c03f71ea --- /dev/null +++ b/app/src/main/res/layout/simple_list_item_multiple_choice_on_start.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Based on simple_list_item_multiple_choice.xml + from The Android Open Source Project + This one puts the check box at the start of the view (rather than at the end). + --> +<!-- Copyright (C) 2008 The Android Open Source Project + + 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. +--> + +<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="?android:attr/listPreferredItemHeightSmall" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:gravity="center_vertical" + android:drawableStart="?android:attr/listChoiceIndicatorMultiple" + android:drawableLeft="?android:attr/listChoiceIndicatorMultiple" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingRight="?android:attr/listPreferredItemPaddingRight" + /> diff --git a/app/src/main/res/menu/episodes_apply_action_speeddial.xml b/app/src/main/res/menu/episodes_apply_action_speeddial.xml new file mode 100644 index 000000000..39083e41b --- /dev/null +++ b/app/src/main/res/menu/episodes_apply_action_speeddial.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- the order is opposite of the typical menu: + catered to FAB speed dial, which somehow shows the item in reverse. + E.g., item @id/delete_batch is the first in the xml, + visually it will be shown at the bottom of the list of actions. + --> + <item android:id="@+id/delete_batch" + android:icon="?attr/content_discard" + android:title="@string/delete_episode_label" + /> + <item android:id="@+id/download_batch" + android:icon="?attr/av_download" + android:title="@string/download_label" + /> + <item android:id="@+id/mark_unread_batch" + android:icon="?attr/navigation_cancel" + android:title="@string/mark_unread_label" + /> + <item + android:id="@+id/mark_read_batch" + android:icon="?attr/navigation_accept" + android:title="@string/mark_read_label" + /> + <item android:id="@+id/remove_from_queue_batch" + android:icon="?attr/content_remove_from_queue" + android:title="@string/remove_from_queue_label" + /> + <item + android:id="@+id/add_to_queue_batch" + android:icon="?attr/content_new" + android:title="@string/add_to_queue_label" + /> +</menu> diff --git a/app/src/main/res/menu/opml_selection_options.xml b/app/src/main/res/menu/opml_selection_options.xml index 26d2a0519..8b3310dc2 100644 --- a/app/src/main/res/menu/opml_selection_options.xml +++ b/app/src/main/res/menu/opml_selection_options.xml @@ -4,14 +4,14 @@ <item android:id="@id/select_all_item" - android:icon="?attr/ic_check_box_outline" + android:icon="?attr/ic_select_all" android:title="@string/select_all_label" custom:showAsAction="ifRoom"> </item> <item android:id="@id/deselect_all_item" - android:icon="?attr/ic_check_box" + android:icon="?attr/ic_select_none" android:title="@string/deselect_all_label" custom:showAsAction="ifRoom"> </item> diff --git a/app/src/main/res/values-w300dp/dimens-fabspeeddial.xml b/app/src/main/res/values-w300dp/dimens-fabspeeddial.xml new file mode 100644 index 000000000..e531395c0 --- /dev/null +++ b/app/src/main/res/values-w300dp/dimens-fabspeeddial.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- increase FAB speed dial label's max width if the screen is wide enough + (300dp ~ 1.875 inch, devices with 3.5-inch screens have a width of ~ 1.9in + so the setup is applicable for most phones) + --> + <dimen name="sd_label_max_width">240dp</dimen> +</resources> diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index c31dbc83d..7fe93a162 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -898,7 +898,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { final List<FeedItem> queue = taskManager.getQueue(); if (QueueAccess.ItemListAccess(queue).contains(item.getId())) { // don't know if it actually matters to not autodownload when smart mark as played is triggered - DBWriter.removeQueueItem(PlaybackService.this, item, ended); + DBWriter.removeQueueItem(PlaybackService.this, ended, item); } } catch (InterruptedException e) { e.printStackTrace(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index ab55bd3c0..ad20bfa0a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -7,7 +7,6 @@ import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.util.Log; -import io.reactivex.annotations.NonNull; import org.shredzone.flattr4j.model.Flattr; import java.io.File; @@ -49,6 +48,7 @@ import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.danoeh.antennapod.core.util.flattr.FlattrThing; import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing; import de.greenrobot.event.EventBus; +import io.reactivex.annotations.NonNull; /** * Provides methods for writing data to AntennaPod's database. @@ -89,7 +89,7 @@ public class DBWriter { boolean result = deleteFeedMediaSynchronous(context, media); if (result && UserPreferences.shouldDeleteRemoveFromQueue()) { - DBWriter.removeQueueItemSynchronous(context, media.getItem(), false); + DBWriter.removeQueueItemSynchronous(context, false, media.getItem().getId()); } } }); @@ -423,30 +423,58 @@ public class DBWriter { /** * Removes a FeedItem object from the queue. - * - * @param context A context that is used for opening a database connection. - * @param item FeedItem that should be removed. + * @param context A context that is used for opening a database connection. * @param performAutoDownload true if an auto-download process should be started after the operation. + * @param item FeedItem that should be removed. */ public static Future<?> removeQueueItem(final Context context, - final FeedItem item, final boolean performAutoDownload) { - return dbExec.submit(() -> removeQueueItemSynchronous(context, item, performAutoDownload)); + final boolean performAutoDownload, final FeedItem item) { + return dbExec.submit(() -> removeQueueItemSynchronous(context, performAutoDownload, item.getId())); + } + + public static Future<?> removeQueueItem(final Context context, final boolean performAutoDownload, + final long... itemIds) { + return dbExec.submit(() -> removeQueueItemSynchronous(context, performAutoDownload, itemIds)); } private static void removeQueueItemSynchronous(final Context context, - final FeedItem item, final boolean performAutoDownload) { + final boolean performAutoDownload, + final long... itemIds) { + if (itemIds.length < 1) { + return; + } final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); final List<FeedItem> queue = DBReader.getQueue(adapter); if (queue != null) { - int position = queue.indexOf(item); - if (position >= 0) { - queue.remove(position); + boolean queueModified = false; + List<QueueEvent> events = new ArrayList<>(); + List<FeedItem> updatedItems = new ArrayList<>(); + for (long itemId : itemIds) { + int position = indexInItemList(queue, itemId); + if (position >= 0) { + final FeedItem item = DBReader.getFeedItem(itemId); + if (item == null) { + Log.e(TAG, "removeQueueItem - item in queue but somehow cannot be loaded." + + " Item ignored. It should never happen. id:" + itemId); + continue; + } + queue.remove(position); + item.removeTag(FeedItem.TAG_QUEUE); + events.add(QueueEvent.removed(item)); + updatedItems.add(item); + queueModified = true; + } else { + Log.v(TAG, "removeQueueItem - item not in queue:" + itemId); + } + } + if (queueModified) { adapter.setQueue(queue); - item.removeTag(FeedItem.TAG_QUEUE); - EventBus.getDefault().post(QueueEvent.removed(item)); - EventBus.getDefault().post(FeedItemEvent.updated(item)); + for (QueueEvent event : events) { + EventBus.getDefault().post(event); + } + EventBus.getDefault().post(FeedItemEvent.updated(updatedItems)); } else { Log.w(TAG, "Queue was not modified by call to removeQueueItem"); } @@ -786,12 +814,17 @@ public class DBWriter { } private static boolean itemListContains(List<FeedItem> items, long itemId) { - for (FeedItem item : items) { + return indexInItemList(items, itemId) >= 0; + } + + private static int indexInItemList(List<FeedItem> items, long itemId) { + for (int i = 0; i < items.size(); i ++) { + FeedItem item = items.get(i); if (item.getId() == itemId) { - return true; + return i; } } - return false; + return -1; } /** diff --git a/core/src/main/res/drawable-hdpi/ic_check_box_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_check_box_grey600_24dp.png Binary files differdeleted file mode 100644 index 187a426a8..000000000 --- a/core/src/main/res/drawable-hdpi/ic_check_box_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-hdpi/ic_check_box_outline_blank_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_check_box_outline_blank_grey600_24dp.png Binary files differdeleted file mode 100644 index 076773bca..000000000 --- a/core/src/main/res/drawable-hdpi/ic_check_box_outline_blank_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-hdpi/ic_check_box_outline_blank_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_check_box_outline_blank_white_24dp.png Binary files differdeleted file mode 100644 index ecaf0d5be..000000000 --- a/core/src/main/res/drawable-hdpi/ic_check_box_outline_blank_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-hdpi/ic_check_box_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_check_box_white_24dp.png Binary files differdeleted file mode 100644 index 5ce64cc5f..000000000 --- a/core/src/main/res/drawable-hdpi/ic_check_box_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-hdpi/ic_indeterminate_check_box_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_indeterminate_check_box_grey600_24dp.png Binary files differdeleted file mode 100644 index e56fbf224..000000000 --- a/core/src/main/res/drawable-hdpi/ic_indeterminate_check_box_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-hdpi/ic_indeterminate_check_box_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_indeterminate_check_box_white_24dp.png Binary files differdeleted file mode 100644 index dccf44930..000000000 --- a/core/src/main/res/drawable-hdpi/ic_indeterminate_check_box_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-mdpi/ic_check_box_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_check_box_grey600_24dp.png Binary files differdeleted file mode 100644 index d5bdfa433..000000000 --- a/core/src/main/res/drawable-mdpi/ic_check_box_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-mdpi/ic_check_box_outline_blank_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_check_box_outline_blank_grey600_24dp.png Binary files differdeleted file mode 100644 index aefe5b6c1..000000000 --- a/core/src/main/res/drawable-mdpi/ic_check_box_outline_blank_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-mdpi/ic_check_box_outline_blank_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_check_box_outline_blank_white_24dp.png Binary files differdeleted file mode 100644 index a3a588c64..000000000 --- a/core/src/main/res/drawable-mdpi/ic_check_box_outline_blank_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-mdpi/ic_check_box_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_check_box_white_24dp.png Binary files differdeleted file mode 100644 index dc94cdbf4..000000000 --- a/core/src/main/res/drawable-mdpi/ic_check_box_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-mdpi/ic_indeterminate_check_box_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_indeterminate_check_box_grey600_24dp.png Binary files differdeleted file mode 100644 index 0e6ce58e3..000000000 --- a/core/src/main/res/drawable-mdpi/ic_indeterminate_check_box_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-mdpi/ic_indeterminate_check_box_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_indeterminate_check_box_white_24dp.png Binary files differdeleted file mode 100644 index c496b4648..000000000 --- a/core/src/main/res/drawable-mdpi/ic_indeterminate_check_box_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xhdpi/ic_check_box_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_check_box_grey600_24dp.png Binary files differdeleted file mode 100644 index e46ab71b4..000000000 --- a/core/src/main/res/drawable-xhdpi/ic_check_box_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xhdpi/ic_check_box_outline_blank_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_check_box_outline_blank_grey600_24dp.png Binary files differdeleted file mode 100644 index bb15a903a..000000000 --- a/core/src/main/res/drawable-xhdpi/ic_check_box_outline_blank_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xhdpi/ic_check_box_outline_blank_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_check_box_outline_blank_white_24dp.png Binary files differdeleted file mode 100644 index 336682497..000000000 --- a/core/src/main/res/drawable-xhdpi/ic_check_box_outline_blank_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xhdpi/ic_check_box_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_check_box_white_24dp.png Binary files differdeleted file mode 100644 index cdb4a3181..000000000 --- a/core/src/main/res/drawable-xhdpi/ic_check_box_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xhdpi/ic_indeterminate_check_box_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_indeterminate_check_box_grey600_24dp.png Binary files differdeleted file mode 100644 index 3b0d3aa1a..000000000 --- a/core/src/main/res/drawable-xhdpi/ic_indeterminate_check_box_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xhdpi/ic_indeterminate_check_box_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_indeterminate_check_box_white_24dp.png Binary files differdeleted file mode 100644 index 57e478e9f..000000000 --- a/core/src/main/res/drawable-xhdpi/ic_indeterminate_check_box_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xxhdpi/ic_check_box_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_check_box_grey600_24dp.png Binary files differdeleted file mode 100644 index 7093f28d5..000000000 --- a/core/src/main/res/drawable-xxhdpi/ic_check_box_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xxhdpi/ic_check_box_outline_blank_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_check_box_outline_blank_grey600_24dp.png Binary files differdeleted file mode 100644 index 050e6cd6c..000000000 --- a/core/src/main/res/drawable-xxhdpi/ic_check_box_outline_blank_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xxhdpi/ic_check_box_outline_blank_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_check_box_outline_blank_white_24dp.png Binary files differdeleted file mode 100644 index 56d380575..000000000 --- a/core/src/main/res/drawable-xxhdpi/ic_check_box_outline_blank_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xxhdpi/ic_check_box_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_check_box_white_24dp.png Binary files differdeleted file mode 100644 index ba9af5265..000000000 --- a/core/src/main/res/drawable-xxhdpi/ic_check_box_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xxhdpi/ic_indeterminate_check_box_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_indeterminate_check_box_grey600_24dp.png Binary files differdeleted file mode 100644 index 2e7d39a5a..000000000 --- a/core/src/main/res/drawable-xxhdpi/ic_indeterminate_check_box_grey600_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable-xxhdpi/ic_indeterminate_check_box_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_indeterminate_check_box_white_24dp.png Binary files differdeleted file mode 100644 index ec4981f5c..000000000 --- a/core/src/main/res/drawable-xxhdpi/ic_indeterminate_check_box_white_24dp.png +++ /dev/null diff --git a/core/src/main/res/drawable/ic_fab_edit.xml b/core/src/main/res/drawable/ic_fab_edit.xml new file mode 100644 index 000000000..cb2e394b0 --- /dev/null +++ b/core/src/main/res/drawable/ic_fab_edit.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" + android:viewportHeight="24.0" android:viewportWidth="24.0" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FFFFFFFF" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_remove_grey600.xml b/core/src/main/res/drawable/ic_remove_grey600.xml new file mode 100644 index 000000000..5a6b2af6b --- /dev/null +++ b/core/src/main/res/drawable/ic_remove_grey600.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" + android:viewportHeight="24.0" android:viewportWidth="24.0" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FF757575" android:pathData="M19,13H5v-2h14v2z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_remove_white.xml b/core/src/main/res/drawable/ic_remove_white.xml new file mode 100644 index 000000000..d812091fb --- /dev/null +++ b/core/src/main/res/drawable/ic_remove_white.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" + android:viewportHeight="24.0" android:viewportWidth="24.0" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FFFFFFFF" android:pathData="M19,13H5v-2h14v2z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_select_all_grey600.xml b/core/src/main/res/drawable/ic_select_all_grey600.xml new file mode 100644 index 000000000..96e9a2de5 --- /dev/null +++ b/core/src/main/res/drawable/ic_select_all_grey600.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF757575" + android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_select_all_white.xml b/core/src/main/res/drawable/ic_select_all_white.xml new file mode 100644 index 000000000..0fc49c923 --- /dev/null +++ b/core/src/main/res/drawable/ic_select_all_white.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_select_none_grey600.xml b/core/src/main/res/drawable/ic_select_none_grey600.xml new file mode 100644 index 000000000..8057f73ca --- /dev/null +++ b/core/src/main/res/drawable/ic_select_none_grey600.xml @@ -0,0 +1,11 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF757575" + android:pathData="m3,5l2,0l0,-2c-1.1,0 -2,0.9 -2,2zm0,8l2,0l0,-2l-2,0l0,2zm4,8l2,0l0,-2l-2,0l0,2zm-4,-12l2,0l0,-2l-2,0l0,2zm10,-6l-2,0l0,2l2,0l0,-2zm6,0l0,2l2,0c0,-1.1 -0.9,-2 -2,-2zm-14,18l0,-2l-2,0c0,1.1 0.9,2 2,2zm-2,-4l2,0l0,-2l-2,0l0,2zm6,-14l-2,0l0,2l2,0l0,-2zm2,18l2,0l0,-2l-2,0l0,2zm8,-8l2,0l0,-2l-2,0l0,2zm0,8c1.1,0 2,-0.9 2,-2l-2,0l0,2zm0,-12l2,0l0,-2l-2,0l0,2zm0,8l2,0l0,-2l-2,0l0,2zm-4,4l2,0l0,-2l-2,0l0,2zm0,-16l2,0l0,-2l-2,0l0,2z"/> + <path android:fillColor="#FF757575" + android:pathData="M17,8.41L15.59,7 12,10.59 8.41,7 7,8.41 10.59,12 7,15.59 8.41,17 12,13.41 15.59,17 17,15.59 13.41,12z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_select_none_white.xml b/core/src/main/res/drawable/ic_select_none_white.xml new file mode 100644 index 000000000..3e1f6b884 --- /dev/null +++ b/core/src/main/res/drawable/ic_select_none_white.xml @@ -0,0 +1,11 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="m3,5l2,0l0,-2c-1.1,0 -2,0.9 -2,2zm0,8l2,0l0,-2l-2,0l0,2zm4,8l2,0l0,-2l-2,0l0,2zm-4,-12l2,0l0,-2l-2,0l0,2zm10,-6l-2,0l0,2l2,0l0,-2zm6,0l0,2l2,0c0,-1.1 -0.9,-2 -2,-2zm-14,18l0,-2l-2,0c0,1.1 0.9,2 2,2zm-2,-4l2,0l0,-2l-2,0l0,2zm6,-14l-2,0l0,2l2,0l0,-2zm2,18l2,0l0,-2l-2,0l0,2zm8,-8l2,0l0,-2l-2,0l0,2zm0,8c1.1,0 2,-0.9 2,-2l-2,0l0,2zm0,-12l2,0l0,-2l-2,0l0,2zm0,8l2,0l0,-2l-2,0l0,2zm-4,4l2,0l0,-2l-2,0l0,2zm0,-16l2,0l0,-2l-2,0l0,2z"/> + <path android:fillColor="#FFFFFFFF" + android:pathData="M17,8.41L15.59,7 12,10.59 8.41,7 7,8.41 10.59,12 7,15.59 8.41,17 12,13.41 15.59,17 17,15.59 13.41,12z"/> +</vector> diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml index 48e55768e..5311d6cd2 100644 --- a/core/src/main/res/values/attrs.xml +++ b/core/src/main/res/values/attrs.xml @@ -12,6 +12,7 @@ <attr name="av_rewind" format="reference"/> <attr name="content_discard" format="reference"/> <attr name="content_new" format="reference"/> + <attr name="content_remove_from_queue" format="reference"/> <attr name="storage" format="reference"/> <attr name="statistics" format="reference"/> <attr name="feed" format="reference"/> @@ -47,9 +48,8 @@ <attr name="ic_sleep" format="reference"/> <attr name="ic_sleep_off" format="reference"/> <attr name="checkbox_multiple" format="reference"/> - <attr name="ic_check_box" format="reference"/> - <attr name="ic_check_box_outline" format="reference"/> - <attr name="ic_indeterminate_check_box" format="reference"/> + <attr name="ic_select_all" format="reference"/> + <attr name="ic_select_none" format="reference"/> <attr name="ic_sort" format="reference"/> <attr name="ic_sd_storage" format="reference"/> <attr name="ic_create_new_folder" format="reference"/> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 3b03f2af7..3d730516e 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -113,6 +113,7 @@ <item quantity="one">1 day after finishing</item> <item quantity="other">%d days after finishing</item> </plurals> + <string name="num_selected_label">%d selected</string> <!-- 'Add Feed' Activity labels --> <string name="feedurl_label">Feed URL</string> @@ -165,6 +166,10 @@ <!-- actions on feeditems --> <string name="download_label">Download</string> + <plurals name="downloading_batch_label"> + <item quantity="one">Downloading %d episode.</item> + <item quantity="other">Downloading %d episodes.</item> + </plurals> <string name="play_label">Play</string> <string name="pause_label">Pause</string> <string name="stop_label">Stop</string> @@ -172,15 +177,35 @@ <string name="remove_label">Remove</string> <string name="delete_label">Delete</string> <string name="delete_failed">Unable to delete file. Rebooting the device could help.</string> - <string name="remove_episode_lable">Remove Episode</string> + <string name="delete_episode_label">Delete Episode</string> + <plurals name="deleted_episode_batch_label"> + <item quantity="one">%d episode deleted.</item> + <item quantity="other">%d episodes deleted.</item> + </plurals> <string name="mark_as_seen_label">Mark as seen</string> <string name="marked_as_seen_label">Marked as seen</string> <string name="mark_read_label">Mark as played</string> <string name="marked_as_read_label">Marked as played</string> + <plurals name="marked_read_batch_label"> + <item quantity="one">%d episode marked as played.</item> + <item quantity="other">%d episodes marked as played.</item> + </plurals> <string name="mark_unread_label">Mark as unplayed</string> + <plurals name="marked_unread_batch_label"> + <item quantity="one">%d episode marked as unplayed.</item> + <item quantity="other">%d episodes marked as unplayed.</item> + </plurals> <string name="add_to_queue_label">Add to Queue</string> <string name="added_to_queue_label">Added to Queue</string> + <plurals name="added_to_queue_batch_label"> + <item quantity="one">%d episode added to queue.</item> + <item quantity="other">%d episodes added to queue.</item> + </plurals> <string name="remove_from_queue_label">Remove from Queue</string> + <plurals name="removed_from_queue_batch_label"> + <item quantity="one">%d episode removed from queue.</item> + <item quantity="other">%d episodes removed from queue.</item> + </plurals> <string name="add_to_favorite_label">Add to Favorites</string> <string name="added_to_favorites">Added to Favorites</string> <string name="remove_from_favorite_label">Remove from Favorites</string> diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml index 363a69a63..e9a7a2f64 100644 --- a/core/src/main/res/values/styles.xml +++ b/core/src/main/res/values/styles.xml @@ -26,6 +26,7 @@ <item type="attr" name="av_rewind">@drawable/ic_fast_rewind_grey600_24dp</item> <item type="attr" name="content_discard">@drawable/ic_delete_grey600_24dp</item> <item type="attr" name="content_new">@drawable/ic_add_grey600_24dp</item> + <item type="attr" name="content_remove_from_queue">@drawable/ic_remove_grey600</item> <item type="attr" name="feed">@drawable/ic_feed_grey600_24dp</item> <item type="attr" name="location_web_site">@drawable/ic_web_grey600_24dp</item> <item type="attr" name="navigation_accept">@drawable/ic_done_grey600_24dp</item> @@ -60,9 +61,8 @@ <item type="attr" name="ic_filter">@drawable/ic_filter_grey600_24dp</item> <item type="attr" name="ic_sleep">@drawable/ic_sleep_grey600_24dp</item> <item type="attr" name="ic_sleep_off">@drawable/ic_sleep_off_grey600_24dp</item> - <item type="attr" name="ic_check_box">@drawable/ic_check_box_grey600_24dp</item> - <item type="attr" name="ic_check_box_outline">@drawable/ic_check_box_outline_blank_grey600_24dp</item> - <item type="attr" name="ic_indeterminate_check_box">@drawable/ic_indeterminate_check_box_grey600_24dp</item> + <item type="attr" name="ic_select_all">@drawable/ic_select_all_grey600</item> + <item type="attr" name="ic_select_none">@drawable/ic_select_none_grey600</item> <item type="attr" name="ic_sort">@drawable/ic_sort_grey600_24dp</item> <item type="attr" name="ic_sd_storage">@drawable/ic_sd_storage_grey600_36dp</item> <item type="attr" name="ic_create_new_folder">@drawable/ic_create_new_folder_grey600_24dp</item> @@ -110,6 +110,7 @@ <item type="attr" name="av_rewind">@drawable/ic_fast_rewind_white_24dp</item> <item type="attr" name="content_discard">@drawable/ic_delete_white_24dp</item> <item type="attr" name="content_new">@drawable/ic_add_white_24dp</item> + <item type="attr" name="content_remove_from_queue">@drawable/ic_remove_white</item> <item type="attr" name="feed">@drawable/ic_feed_white_24dp</item> <item type="attr" name="location_web_site">@drawable/ic_web_white_24dp</item> <item type="attr" name="navigation_accept">@drawable/ic_done_white_24dp</item> @@ -144,9 +145,8 @@ <item type="attr" name="ic_filter">@drawable/ic_filter_white_24dp</item> <item type="attr" name="ic_sleep">@drawable/ic_sleep_white_24dp</item> <item type="attr" name="ic_sleep_off">@drawable/ic_sleep_off_white_24dp</item> - <item type="attr" name="ic_check_box">@drawable/ic_check_box_white_24dp</item> - <item type="attr" name="ic_check_box_outline">@drawable/ic_check_box_outline_blank_white_24dp</item> - <item type="attr" name="ic_indeterminate_check_box">@drawable/ic_indeterminate_check_box_white_24dp</item> + <item type="attr" name="ic_select_all">@drawable/ic_select_all_white</item> + <item type="attr" name="ic_select_none">@drawable/ic_select_none_white</item> <item type="attr" name="ic_sort">@drawable/ic_sort_white_24dp</item> <item type="attr" name="ic_sd_storage">@drawable/ic_sd_storage_white_36dp</item> <item type="attr" name="ic_create_new_folder">@drawable/ic_create_new_folder_white_24dp</item> @@ -214,6 +214,7 @@ <item type="attr" name="av_rewind">@drawable/ic_fast_rewind_grey600_24dp</item> <item type="attr" name="content_discard">@drawable/ic_delete_grey600_24dp</item> <item type="attr" name="content_new">@drawable/ic_add_grey600_24dp</item> + <item type="attr" name="content_remove_from_queue">@drawable/ic_remove_grey600</item> <item type="attr" name="feed">@drawable/ic_feed_grey600_24dp</item> <item type="attr" name="location_web_site">@drawable/ic_web_grey600_24dp</item> <item type="attr" name="navigation_accept">@drawable/ic_done_grey600_24dp</item> @@ -248,9 +249,8 @@ <item type="attr" name="ic_filter">@drawable/ic_filter_grey600_24dp</item> <item type="attr" name="ic_sleep">@drawable/ic_sleep_grey600_24dp</item> <item type="attr" name="ic_sleep_off">@drawable/ic_sleep_off_grey600_24dp</item> - <item type="attr" name="ic_check_box">@drawable/ic_check_box_grey600_24dp</item> - <item type="attr" name="ic_check_box_outline">@drawable/ic_check_box_outline_blank_grey600_24dp</item> - <item type="attr" name="ic_indeterminate_check_box">@drawable/ic_indeterminate_check_box_grey600_24dp</item> + <item type="attr" name="ic_select_all">@drawable/ic_select_all_grey600</item> + <item type="attr" name="ic_select_none">@drawable/ic_select_none_grey600</item> <item type="attr" name="ic_sort">@drawable/ic_sort_grey600_24dp</item> <item type="attr" name="ic_sd_storage">@drawable/ic_sd_storage_grey600_36dp</item> <item type="attr" name="ic_create_new_folder">@drawable/ic_create_new_folder_grey600_24dp</item> @@ -298,6 +298,7 @@ <item type="attr" name="content_discard">@drawable/ic_delete_white_24dp</item> <item type="attr" name="content_new">@drawable/ic_add_white_24dp</item> <item type="attr" name="feed">@drawable/ic_feed_white_24dp</item> + <item type="attr" name="content_remove_from_queue">@drawable/ic_remove_white</item> <item type="attr" name="location_web_site">@drawable/ic_web_white_24dp</item> <item type="attr" name="navigation_accept">@drawable/ic_done_white_24dp</item> <item type="attr" name="navigation_cancel">@drawable/ic_cancel_white_24dp</item> @@ -331,9 +332,8 @@ <item type="attr" name="ic_filter">@drawable/ic_filter_white_24dp</item> <item type="attr" name="ic_sleep">@drawable/ic_sleep_white_24dp</item> <item type="attr" name="ic_sleep_off">@drawable/ic_sleep_off_white_24dp</item> - <item type="attr" name="ic_check_box">@drawable/ic_check_box_white_24dp</item> - <item type="attr" name="ic_check_box_outline">@drawable/ic_check_box_outline_blank_white_24dp</item> - <item type="attr" name="ic_indeterminate_check_box">@drawable/ic_indeterminate_check_box_white_24dp</item> + <item type="attr" name="ic_select_all">@drawable/ic_select_all_white</item> + <item type="attr" name="ic_select_none">@drawable/ic_select_none_white</item> <item type="attr" name="ic_sort">@drawable/ic_sort_white_24dp</item> <item type="attr" name="ic_sd_storage">@drawable/ic_sd_storage_white_36dp</item> <item type="attr" name="ic_create_new_folder">@drawable/ic_create_new_folder_white_24dp</item> |