diff options
Diffstat (limited to 'app')
89 files changed, 2945 insertions, 2568 deletions
diff --git a/app/build.gradle b/app/build.gradle index cefe7224a..b2d6a5600 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ plugins { id('com.android.application') id('com.getkeepsafe.dexcount') - id('com.github.triplet.play') version '2.7.5' apply false + id('com.github.triplet.play') version '3.4.0' apply false } apply from: "../common.gradle" apply from: "../playFlavor.gradle" @@ -149,6 +149,8 @@ dependencies { implementation 'com.github.mfietz:fyydlin:v0.5.0' implementation 'com.github.ByteHamster:SearchPreference:v2.0.0' implementation 'com.github.skydoves:balloon:1.1.5' + implementation 'it.xabaras.android:recyclerview-swipedecorator:1.2.3' + implementation 'com.annimon:stream:1.2.2' // Non-free dependencies: playImplementation 'com.google.android.play:core:1.8.0' @@ -168,8 +170,8 @@ dependencies { if (project.hasProperty("antennaPodPlayPublisherCredentials")) { apply plugin: 'com.github.triplet.play' play { - track = 'alpha' - serviceAccountCredentials = file(antennaPodPlayPublisherCredentials) + track.set('alpha') + serviceAccountCredentials.set(file(antennaPodPlayPublisherCredentials)) } } diff --git a/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java b/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java index f1d71b07a..32ad5a694 100644 --- a/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java +++ b/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java @@ -20,7 +20,7 @@ import static java.util.Collections.singletonList; /** * Test class for GpodnetService */ -@Ignore +@Ignore("Needs valid credentials to run") @RunWith(AndroidJUnit4.class) public class GPodnetServiceTest { diff --git a/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java b/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java deleted file mode 100644 index 09730ee4e..000000000 --- a/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package de.test.antennapod.ui; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import androidx.preference.PreferenceManager; -import androidx.test.rule.ActivityTestRule; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.playback.PlayerStatus; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.danoeh.antennapod.fragment.QueueFragment; -import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; -import de.test.antennapod.EspressoTestUtils; -import de.test.antennapod.IgnoreOnCi; -import org.awaitility.Awaitility; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.matcher.ViewMatchers.isRoot; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static de.test.antennapod.EspressoTestUtils.waitForView; - -/** - * User interface tests for changing the playback speed. - */ -@RunWith(AndroidJUnit4.class) -@IgnoreOnCi -public class SpeedChangeTest { - - @Rule - public ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(MainActivity.class, false, false); - private UITestUtils uiTestUtils; - private String[] availableSpeeds; - private PlaybackController controller; - - @Before - public void setUp() throws Exception { - EspressoTestUtils.clearPreferences(); - EspressoTestUtils.clearDatabase(); - EspressoTestUtils.setLastNavFragment(QueueFragment.TAG); - - Context context = getInstrumentation().getTargetContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().putBoolean(UserPreferences.PREF_FOLLOW_QUEUE, true).commit(); - - uiTestUtils = new UITestUtils(context); - uiTestUtils.setup(); - uiTestUtils.setMediaFileName("30sec.mp3"); - uiTestUtils.addLocalFeedData(true); - - List<FeedItem> queue = DBReader.getQueue(); - PlaybackPreferences.writeMediaPlaying(queue.get(0).getMedia(), PlayerStatus.PAUSED, false); - availableSpeeds = new String[] {"1.00", "2.00", "3.00"}; - UserPreferences.setPlaybackSpeedArray(Arrays.asList(1.0f, 2.0f, 3.0f)); - - EspressoTestUtils.tryKillPlaybackService(); - activityRule.launchActivity(new Intent().putExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, true)); - controller = new PlaybackController(activityRule.getActivity()) { - @Override - public void loadMediaInfo() { - // Do nothing - } - }; - controller.init(); - controller.getMedia(); // To load media - } - - @After - public void tearDown() throws Exception { - uiTestUtils.tearDown(); - controller.release(); - } - - @Test - public void testChangeSpeedServiceNotRunning() { - clickThroughSpeeds(); - } - - @Test - public void testChangeSpeedPlaying() { - onView(isRoot()).perform(waitForView(withId(R.id.butPlay), 1000)); - controller.playPause(); - Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() - -> controller.getStatus() == PlayerStatus.PLAYING); - clickThroughSpeeds(); - } - - @Test - public void testChangeSpeedPaused() { - onView(isRoot()).perform(waitForView(withId(R.id.butPlay), 1000)); - controller.playPause(); - Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() - -> controller.getStatus() == PlayerStatus.PLAYING); - controller.playPause(); - Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() - -> controller.getStatus() == PlayerStatus.PAUSED); - clickThroughSpeeds(); - } - - private void clickThroughSpeeds() { - onView(isRoot()).perform(waitForView(withText(availableSpeeds[0]), 1000)); - onView(withId(R.id.txtvPlaybackSpeed)).check(matches(withText(availableSpeeds[0]))); - onView(withId(R.id.butPlaybackSpeed)).perform(click()); - onView(isRoot()).perform(waitForView(withText(availableSpeeds[1]), 1000)); - onView(withId(R.id.txtvPlaybackSpeed)).check(matches(withText(availableSpeeds[1]))); - onView(withId(R.id.butPlaybackSpeed)).perform(click()); - onView(isRoot()).perform(waitForView(withText(availableSpeeds[2]), 1000)); - onView(withId(R.id.txtvPlaybackSpeed)).check(matches(withText(availableSpeeds[2]))); - onView(withId(R.id.butPlaybackSpeed)).perform(click()); - onView(isRoot()).perform(waitForView(withText(availableSpeeds[0]), 1000)); - onView(withId(R.id.txtvPlaybackSpeed)).check(matches(withText(availableSpeeds[0]))); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/ui/VideoplayerActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/VideoplayerActivityTest.java deleted file mode 100644 index 0dae22db1..000000000 --- a/app/src/androidTest/java/de/test/antennapod/ui/VideoplayerActivityTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.test.antennapod.ui; - -import android.content.Intent; -import androidx.test.filters.MediumTest; -import androidx.test.rule.ActivityTestRule; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.VideoplayerActivity; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.withId; - -/** - * Test class for VideoplayerActivity - */ -@MediumTest -@Ignore -public class VideoplayerActivityTest { - - @Rule - public ActivityTestRule<VideoplayerActivity> activityTestRule = new ActivityTestRule<>(VideoplayerActivity.class, false, false); - - /** - * Test if activity can be started. - */ - @Test - public void testStartActivity() throws Exception { - activityTestRule.launchActivity(new Intent()); - onView(withId(R.id.videoPlayerContainer)).check(matches(isDisplayed())); - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 074ea0c1c..47648f9d3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" package="de.danoeh.antennapod" android:installLocation="auto"> <uses-permission android:name="android.permission.INTERNET"/> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" + tools:ignore="ScopedStorage" /> <uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> @@ -194,10 +196,6 @@ </intent-filter> </activity> <activity - android:name=".activity.OpmlFeedChooserActivity" - android:label="@string/opml_import_label"> - </activity> - <activity android:name=".activity.BugReportActivity" android:label="@string/bug_report_title"> <meta-data @@ -304,6 +302,19 @@ </intent-filter> <intent-filter> + <action android:name="android.intent.action.VIEW" /> + + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + + <data android:host="antennapod.org" /> + <data android:host="www.antennapod.org" /> + <data android:pathPrefix="/deeplink/subscribe" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + </intent-filter> + + <intent-filter> <action android:name="android.intent.action.SEND"/> <category android:name="android.intent.category.DEFAULT"/> @@ -353,4 +364,11 @@ android:resource="@xml/actions" /> </application> + <queries> + <intent> + <action android:name="android.intent.action.SEND" /> + <data android:mimeType="text/*" /> + </intent> + </queries> + </manifest> diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java deleted file mode 100644 index 376074525..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java +++ /dev/null @@ -1,144 +0,0 @@ -package de.danoeh.antennapod.activity; - -import android.content.Intent; -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import android.util.SparseBooleanArray; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.ListView; - -import java.util.ArrayList; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.export.opml.OpmlElement; -import de.danoeh.antennapod.core.preferences.UserPreferences; - -/** - * Displays the feeds that the OPML-Importer has read and lets the user choose - * which feeds he wants to import. - */ -public class OpmlFeedChooserActivity extends AppCompatActivity { - public static final String EXTRA_SELECTED_ITEMS = "de.danoeh.antennapod.selectedItems"; - private static final String TAG = "OpmlFeedChooserActivity"; - private Button butConfirm; - private Button butCancel; - private ListView feedlist; - private ArrayAdapter<String> listAdapter; - - private MenuItem selectAll; - private MenuItem deselectAll; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - - setContentView(R.layout.opml_selection); - butConfirm = findViewById(R.id.butConfirm); - butCancel = findViewById(R.id.butCancel); - feedlist = findViewById(R.id.feedlist); - - feedlist.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - listAdapter = new ArrayAdapter<>(this, - android.R.layout.simple_list_item_multiple_choice, - getTitleList()); - - feedlist.setAdapter(listAdapter); - feedlist.setOnItemClickListener((parent, view, position, id) -> { - SparseBooleanArray checked = feedlist.getCheckedItemPositions(); - int checkedCount = 0; - for (int i = 0; i < checked.size(); i++) { - if (checked.valueAt(i)) { - checkedCount++; - } - } - if(checkedCount == listAdapter.getCount()) { - selectAll.setVisible(false); - deselectAll.setVisible(true); - } else { - deselectAll.setVisible(false); - selectAll.setVisible(true); - } - }); - - butCancel.setOnClickListener(v -> { - setResult(RESULT_CANCELED); - finish(); - }); - - butConfirm.setOnClickListener(v -> { - Intent intent = new Intent(); - SparseBooleanArray checked = feedlist.getCheckedItemPositions(); - - int checkedCount = 0; - // Get number of checked items - for (int i = 0; i < checked.size(); i++) { - if (checked.valueAt(i)) { - checkedCount++; - } - } - int[] selection = new int[checkedCount]; - for (int i = 0, collected = 0; collected < checkedCount; i++) { - if (checked.valueAt(i)) { - selection[collected] = checked.keyAt(i); - collected++; - } - } - intent.putExtra(EXTRA_SELECTED_ITEMS, selection); - setResult(RESULT_OK, intent); - finish(); - }); - - } - - private List<String> getTitleList() { - List<String> result = new ArrayList<>(); - if (OpmlImportHolder.getReadElements() != null) { - for (OpmlElement element : OpmlImportHolder.getReadElements()) { - result.add(element.getText()); - } - } - return result; - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.opml_selection_options, menu); - selectAll = menu.findItem(R.id.select_all_item); - deselectAll = menu.findItem(R.id.deselect_all_item); - deselectAll.setVisible(false); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.select_all_item: - selectAll.setVisible(false); - selectAllItems(true); - deselectAll.setVisible(true); - return true; - case R.id.deselect_all_item: - deselectAll.setVisible(false); - selectAllItems(false); - selectAll.setVisible(true); - return true; - default: - return false; - } - } - - private void selectAllItems(boolean b) { - for (int i = 0; i < feedlist.getCount(); i++) { - feedlist.setItemChecked(i, b); - } - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java index d4e9ee5d9..2b8064270 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java @@ -7,17 +7,32 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.util.Log; +import android.util.SparseBooleanArray; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.OpmlFeedQueuer; -import de.danoeh.antennapod.asynctask.OpmlImportWorker; import de.danoeh.antennapod.core.export.opml.OpmlElement; +import de.danoeh.antennapod.core.export.opml.OpmlReader; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.databinding.OpmlSelectionBinding; +import de.danoeh.antennapod.model.feed.Feed; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; import org.apache.commons.io.ByteOrderMark; import org.apache.commons.io.input.BOMInputStream; import org.apache.commons.lang3.ArrayUtils; @@ -26,6 +41,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.ArrayList; +import java.util.List; /** * Activity for Opml Import. @@ -34,13 +50,73 @@ public class OpmlImportActivity extends AppCompatActivity { private static final String TAG = "OpmlImportBaseActivity"; private static final int PERMISSION_REQUEST_READ_EXTERNAL_STORAGE = 5; @Nullable private Uri uri; + OpmlSelectionBinding viewBinding; + private ArrayAdapter<String> listAdapter; + private MenuItem selectAll; + private MenuItem deselectAll; + private ArrayList<OpmlElement> readElements; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { setTheme(UserPreferences.getTheme()); super.onCreate(savedInstanceState); getSupportActionBar().setDisplayHomeAsUpEnabled(true); + viewBinding = OpmlSelectionBinding.inflate(getLayoutInflater()); + setContentView(viewBinding.getRoot()); + viewBinding.feedlist.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + viewBinding.feedlist.setOnItemClickListener((parent, view, position, id) -> { + SparseBooleanArray checked = viewBinding.feedlist.getCheckedItemPositions(); + int checkedCount = 0; + for (int i = 0; i < checked.size(); i++) { + if (checked.valueAt(i)) { + checkedCount++; + } + } + if (checkedCount == listAdapter.getCount()) { + selectAll.setVisible(false); + deselectAll.setVisible(true); + } else { + deselectAll.setVisible(false); + selectAll.setVisible(true); + } + }); + viewBinding.butCancel.setOnClickListener(v -> { + setResult(RESULT_CANCELED); + finish(); + }); + viewBinding.butConfirm.setOnClickListener(v -> { + viewBinding.progressBar.setVisibility(View.VISIBLE); + Completable.fromAction(() -> { + DownloadRequester requester = DownloadRequester.getInstance(); + SparseBooleanArray checked = viewBinding.feedlist.getCheckedItemPositions(); + for (int i = 0; i < checked.size(); i++) { + if (!checked.valueAt(i)) { + continue; + } + OpmlElement element = readElements.get(checked.keyAt(i)); + Feed feed = new Feed(element.getXmlUrl(), null, element.getText()); + try { + requester.downloadFeed(getApplicationContext(), feed); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + () -> { + viewBinding.progressBar.setVisibility(View.GONE); + Intent intent = new Intent(OpmlImportActivity.this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + }, e -> { + viewBinding.progressBar.setVisibility(View.GONE); + Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); + }); + }); Uri uri = getIntent().getData(); if (uri != null && uri.toString().startsWith("/")) { @@ -54,39 +130,6 @@ public class OpmlImportActivity extends AppCompatActivity { importUri(uri); } - /** - * Handles the choices made by the user in the OpmlFeedChooserActivity and - * starts the OpmlFeedQueuer if necessary. - */ - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - Log.d(TAG, "Received result"); - if (resultCode == RESULT_CANCELED) { - Log.d(TAG, "Activity was cancelled"); - finish(); - } else { - int[] selected = data.getIntArrayExtra(OpmlFeedChooserActivity.EXTRA_SELECTED_ITEMS); - if (selected != null && selected.length > 0) { - OpmlFeedQueuer queuer = new OpmlFeedQueuer(this, selected) { - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - Intent intent = new Intent(OpmlImportActivity.this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP - | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - } - - }; - queuer.executeAsync(); - } else { - Log.d(TAG, "No items were selected"); - } - } - } - void importUri(@Nullable Uri uri) { if (uri == null) { new AlertDialog.Builder(this) @@ -108,6 +151,52 @@ public class OpmlImportActivity extends AppCompatActivity { startImport(); } + private List<String> getTitleList() { + List<String> result = new ArrayList<>(); + if (readElements != null) { + for (OpmlElement element : readElements) { + result.add(element.getText()); + } + } + return result; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.opml_selection_options, menu); + selectAll = menu.findItem(R.id.select_all_item); + deselectAll = menu.findItem(R.id.deselect_all_item); + deselectAll.setVisible(false); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == R.id.select_all_item) { + selectAll.setVisible(false); + selectAllItems(true); + deselectAll.setVisible(true); + return true; + } else if (itemId == R.id.deselect_all_item) { + deselectAll.setVisible(false); + selectAllItems(false); + selectAll.setVisible(true); + return true; + } else if (itemId == android.R.id.home) { + finish(); + } + return false; + } + + private void selectAllItems(boolean b) { + for (int i = 0; i < viewBinding.feedlist.getCount(); i++) { + viewBinding.feedlist.setItemChecked(i, b); + } + } + private void requestPermission() { String[] permissions = { android.Manifest.permission.READ_EXTERNAL_STORAGE }; ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_READ_EXTERNAL_STORAGE); @@ -132,37 +221,37 @@ public class OpmlImportActivity extends AppCompatActivity { /** Starts the import process. */ private void startImport() { - try { + viewBinding.progressBar.setVisibility(View.VISIBLE); + + Observable.fromCallable(() -> { InputStream opmlFileStream = getContentResolver().openInputStream(uri); BOMInputStream bomInputStream = new BOMInputStream(opmlFileStream); ByteOrderMark bom = bomInputStream.getBOM(); String charsetName = (bom == null) ? "UTF-8" : bom.getCharsetName(); Reader reader = new InputStreamReader(bomInputStream, charsetName); - - OpmlImportWorker importWorker = new OpmlImportWorker(this, reader) { - - @Override - protected void onPostExecute(ArrayList<OpmlElement> result) { - super.onPostExecute(result); - if (result != null) { - Log.d(TAG, "Parsing was successful"); - OpmlImportHolder.setReadElements(result); - startActivityForResult(new Intent( - OpmlImportActivity.this, - OpmlFeedChooserActivity.class), 0); - } else { - Log.d(TAG, "Parser error occurred"); - } - } - }; - importWorker.executeAsync(); - } catch (Exception e) { - Log.d(TAG, Log.getStackTraceString(e)); - String message = getString(R.string.opml_reader_error); - new AlertDialog.Builder(this) - .setMessage(message + " " + e.getMessage()) - .setPositiveButton(android.R.string.ok, null) - .show(); - } + OpmlReader opmlReader = new OpmlReader(); + ArrayList<OpmlElement> result = opmlReader.readDocument(reader); + reader.close(); + return result; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> { + viewBinding.progressBar.setVisibility(View.GONE); + Log.d(TAG, "Parsing was successful"); + readElements = result; + listAdapter = new ArrayAdapter<>(OpmlImportActivity.this, + android.R.layout.simple_list_item_multiple_choice, + getTitleList()); + viewBinding.feedlist.setAdapter(listAdapter); + }, e -> { + viewBinding.progressBar.setVisibility(View.GONE); + AlertDialog.Builder alert = new AlertDialog.Builder(this); + alert.setTitle(R.string.error_label); + alert.setMessage(getString(R.string.opml_reader_error) + e.getMessage()); + alert.setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); + alert.create().show(); + }); } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java deleted file mode 100644 index dc5570dc0..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.danoeh.antennapod.activity; - -import java.util.ArrayList; - -import de.danoeh.antennapod.core.export.opml.OpmlElement; - -/** - * Hold infos gathered by Ompl-Import - * <p/> - * Created with IntelliJ IDEA. - * User: ligi - * Date: 1/23/13 - * Time: 2:15 PM - */ -public class OpmlImportHolder { - - private OpmlImportHolder(){} - - private static ArrayList<OpmlElement> readElements; - - public static ArrayList<OpmlElement> getReadElements() { - return readElements; - } - - public static void setReadElements(ArrayList<OpmlElement> _readElements) { - readElements = _readElements; - } - - -} - diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java index cd72e34e8..157ad246f 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -3,15 +3,16 @@ package de.danoeh.antennapod.activity; import android.content.Intent; import android.os.Build; import android.os.Bundle; +import android.provider.Settings; +import android.view.Menu; +import android.view.MenuItem; +import android.view.inputmethod.InputMethodManager; + import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceFragmentCompat; -import android.provider.Settings; -import android.view.Menu; -import android.view.MenuItem; - import com.bytehamster.lib.preferencesearch.SearchPreferenceResult; import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener; @@ -26,6 +27,7 @@ import de.danoeh.antennapod.fragment.preferences.NetworkPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.NotificationPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.StoragePreferencesFragment; +import de.danoeh.antennapod.fragment.preferences.SwipePreferencesFragment; import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragment; /** @@ -79,33 +81,35 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe prefFragment = new PlaybackPreferencesFragment(); } else if (screen == R.xml.preferences_notifications) { prefFragment = new NotificationPreferencesFragment(); + } else if (screen == R.xml.preferences_swipe) { + prefFragment = new SwipePreferencesFragment(); } return prefFragment; } public static int getTitleOfPage(int preferences) { - switch (preferences) { - case R.xml.preferences_network: - return R.string.network_pref; - case R.xml.preferences_autodownload: - return R.string.pref_automatic_download_title; - case R.xml.preferences_playback: - return R.string.playback_pref; - case R.xml.preferences_storage: - return R.string.storage_pref; - case R.xml.preferences_import_export: - return R.string.import_export_pref; - case R.xml.preferences_user_interface: - return R.string.user_interface_label; - case R.xml.preferences_gpodder: - return R.string.gpodnet_main_label; - case R.xml.preferences_notifications: - return R.string.notification_pref_fragment; - case R.xml.feed_settings: - return R.string.feed_settings_label; - default: - return R.string.settings_label; + if (preferences == R.xml.preferences_network) { + return R.string.network_pref; + } else if (preferences == R.xml.preferences_autodownload) { + return R.string.pref_automatic_download_title; + } else if (preferences == R.xml.preferences_playback) { + return R.string.playback_pref; + } else if (preferences == R.xml.preferences_storage) { + return R.string.storage_pref; + } else if (preferences == R.xml.preferences_import_export) { + return R.string.import_export_pref; + } else if (preferences == R.xml.preferences_user_interface) { + return R.string.user_interface_label; + } else if (preferences == R.xml.preferences_gpodder) { + return R.string.gpodnet_main_label; + } else if (preferences == R.xml.preferences_notifications) { + return R.string.notification_pref_fragment; + } else if (preferences == R.xml.feed_settings) { + return R.string.feed_settings_label; + } else if (preferences == R.xml.preferences_swipe) { + return R.string.swipeactions_label; } + return R.string.settings_label; } public PreferenceFragmentCompat openScreen(int screen) { @@ -136,6 +140,10 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe if (getSupportFragmentManager().getBackStackEntryCount() == 0) { finish(); } else { + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + if (imm.isActive()) { + imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_NOT_ALWAYS); + } getSupportFragmentManager().popBackStack(); } return true; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java index 774b97454..674b5f86e 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java @@ -3,11 +3,20 @@ package de.danoeh.antennapod.adapter; import android.app.Activity; import android.view.ContextMenu; import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; + +import org.apache.commons.lang3.ArrayUtils; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.model.feed.FeedItem; @@ -15,24 +24,20 @@ import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.fragment.ItemPagerFragment; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; -import org.apache.commons.lang3.ArrayUtils; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; /** * List adapter for the list of new episodes. */ -public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemViewHolder> +public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHolder> implements View.OnCreateContextMenuListener { private final WeakReference<MainActivity> mainActivityRef; private List<FeedItem> episodes = new ArrayList<>(); - private FeedItem selectedItem; + private FeedItem longPressedItem; + int longPressedPosition = 0; // used to init actionMode public EpisodeItemListAdapter(MainActivity mainActivity) { - super(); + super(mainActivity); this.mainActivityRef = new WeakReference<>(mainActivity); setHasStableIds(true); } @@ -63,19 +68,35 @@ public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemView FeedItem item = episodes.get(pos); holder.bind(item); - holder.itemView.setOnLongClickListener(v -> { - selectedItem = item; - return false; - }); + holder.itemView.setOnClickListener(v -> { MainActivity activity = mainActivityRef.get(); - if (activity != null) { + if (activity != null && !inActionMode()) { long[] ids = FeedItemUtil.getIds(episodes); int position = ArrayUtils.indexOf(ids, item.getId()); activity.loadChildFragment(ItemPagerFragment.newInstance(ids, position)); + } else { + toggleSelection(pos); } }); holder.itemView.setOnCreateContextMenuListener(this); + holder.itemView.setOnLongClickListener(v -> { + longPressedItem = item; + longPressedPosition = pos; + return false; + }); + + if (inActionMode()) { + holder.secondaryActionButton.setVisibility(View.GONE); + holder.selectCheckBox.setOnClickListener(v -> { + toggleSelection(pos); + }); + holder.selectCheckBox.setChecked(isSelected(pos)); + holder.selectCheckBox.setVisibility(View.VISIBLE); + } else { + holder.selectCheckBox.setVisibility(View.GONE); + } + afterBindViewHolder(holder, pos); holder.hideSeparatorIfNecessary(); } @@ -106,6 +127,7 @@ public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemView * Instead, we tell the adapter to use partial binding by calling {@link #notifyItemChanged(int, Object)}. * We actually ignore the payload and always do a full bind but calling the partial bind method ensures * that ViewHolders are always re-used. + * * @param position Position of the item that has changed */ public void notifyItemChangedCompat(int position) { @@ -113,8 +135,8 @@ public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemView } @Nullable - public FeedItem getSelectedItem() { - return selectedItem; + public FeedItem getLongPressedItem() { + return longPressedItem; } @Override @@ -139,8 +161,37 @@ public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemView @Override public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { MenuInflater inflater = mainActivityRef.get().getMenuInflater(); - inflater.inflate(R.menu.feeditemlist_context, menu); - menu.setHeaderTitle(selectedItem.getTitle()); - FeedItemMenuHandler.onPrepareMenu(menu, selectedItem, R.id.skip_episode_item); + if (inActionMode()) { + inflater.inflate(R.menu.multi_select_context_popup, menu); + } else { + inflater.inflate(R.menu.feeditemlist_context, menu); + menu.setHeaderTitle(longPressedItem.getTitle()); + FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item); + } + } + + public boolean onContextItemSelected(MenuItem item) { + if (item.getItemId() == R.id.multi_select) { + startSelectMode(longPressedPosition); + return true; + } else if (item.getItemId() == R.id.select_all_above) { + setSelected(0, longPressedPosition, true); + return true; + } else if (item.getItemId() == R.id.select_all_below) { + setSelected(longPressedPosition + 1, getItemCount(), true); + return true; + } + return false; } + + public List<FeedItem> getSelectedItems() { + List<FeedItem> items = new ArrayList<>(); + for (int i = 0; i < getItemCount(); i++) { + if (isSelected(i)) { + items.add(getItem(i)); + } + } + return items; + } + } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java index 01712ea29..6055582a3 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java @@ -6,10 +6,11 @@ import android.view.ContextMenu; import android.view.MenuInflater; import android.view.MotionEvent; import android.view.View; -import androidx.recyclerview.widget.ItemTouchHelper; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; /** @@ -18,13 +19,13 @@ import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; public class QueueRecyclerAdapter extends EpisodeItemListAdapter { private static final String TAG = "QueueRecyclerAdapter"; - private final ItemTouchHelper itemTouchHelper; + private final SwipeActions swipeActions; private boolean dragDropEnabled; - public QueueRecyclerAdapter(MainActivity mainActivity, ItemTouchHelper itemTouchHelper) { + public QueueRecyclerAdapter(MainActivity mainActivity, SwipeActions swipeActions) { super(mainActivity); - this.itemTouchHelper = itemTouchHelper; + this.swipeActions = swipeActions; dragDropEnabled = ! (UserPreferences.isQueueKeepSorted() || UserPreferences.isQueueLocked()); } @@ -39,12 +40,12 @@ public class QueueRecyclerAdapter extends EpisodeItemListAdapter { View.OnTouchListener startDragTouchListener = (v1, event) -> { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { Log.d(TAG, "startDrag()"); - itemTouchHelper.startDrag(holder); + swipeActions.startDrag(holder); } return false; }; - if (!dragDropEnabled) { + if (!dragDropEnabled || inActionMode()) { holder.dragHandle.setVisibility(View.GONE); holder.dragHandle.setOnTouchListener(null); holder.coverHolder.setOnTouchListener(null); @@ -63,11 +64,17 @@ public class QueueRecyclerAdapter extends EpisodeItemListAdapter { inflater.inflate(R.menu.queue_context, menu); super.onCreateContextMenu(menu, v, menuInfo); - final boolean keepSorted = UserPreferences.isQueueKeepSorted(); - if (getItem(0).getId() == getSelectedItem().getId() || keepSorted) { + if (!inActionMode()) { + menu.findItem(R.id.multi_select).setVisible(true); + final boolean keepSorted = UserPreferences.isQueueKeepSorted(); + if (getItem(0).getId() == getLongPressedItem().getId() || keepSorted) { + menu.findItem(R.id.move_to_top_item).setVisible(false); + } + if (getItem(getItemCount() - 1).getId() == getLongPressedItem().getId() || keepSorted) { + menu.findItem(R.id.move_to_bottom_item).setVisible(false); + } + } else { menu.findItem(R.id.move_to_top_item).setVisible(false); - } - if (getItem(getItemCount() - 1).getId() == getSelectedItem().getId() || keepSorted) { menu.findItem(R.id.move_to_bottom_item).setVisible(false); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java new file mode 100644 index 000000000..43f749ff3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java @@ -0,0 +1,174 @@ +package de.danoeh.antennapod.adapter; + +import android.app.Activity; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import androidx.recyclerview.widget.RecyclerView; + +import de.danoeh.antennapod.R; + +import java.util.HashSet; + +/** + * Used by Recyclerviews that need to provide ability to select items. + */ +abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> { + private ActionMode actionMode; + private final HashSet<Long> selectedIds = new HashSet<>(); + private final Activity activity; + private OnSelectModeListener onSelectModeListener; + + public SelectableAdapter(Activity activity) { + this.activity = activity; + } + + public void startSelectMode(int pos) { + if (inActionMode()) { + endSelectMode(); + } + + if (onSelectModeListener != null) { + onSelectModeListener.onStartSelectMode(); + } + + selectedIds.clear(); + selectedIds.add(getItemId(pos)); + notifyDataSetChanged(); + + actionMode = activity.startActionMode(new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.multi_select_options, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + updateTitle(); + toggleSelectAllIcon(menu.findItem(R.id.select_toggle), false); + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (item.getItemId() == R.id.select_toggle) { + boolean allSelected = selectedIds.size() == getItemCount(); + setSelected(0, getItemCount(), !allSelected); + toggleSelectAllIcon(item, !allSelected); + updateTitle(); + return true; + } + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + callOnEndSelectMode(); + actionMode = null; + selectedIds.clear(); + notifyDataSetChanged(); + } + }); + updateTitle(); + } + + /** + * End action mode if currently in select mode, otherwise do nothing + */ + public void endSelectMode() { + if (inActionMode()) { + callOnEndSelectMode(); + actionMode.finish(); + } + } + + public boolean isSelected(int pos) { + return selectedIds.contains(getItemId(pos)); + } + + /** + * Set the selected state of item at given position + * + * @param pos the position to select + * @param selected true for selected state and false for unselected + */ + public void setSelected(int pos, boolean selected) { + if (selected) { + selectedIds.add(getItemId(pos)); + } else { + selectedIds.remove(getItemId(pos)); + } + updateTitle(); + } + + /** + * Set the selected state of item for a given range + * + * @param startPos start position of range, inclusive + * @param endPos end position of range, inclusive + * @param selected indicates the selection state + * @throws IllegalArgumentException if start and end positions are not valid + */ + public void setSelected(int startPos, int endPos, boolean selected) throws IllegalArgumentException { + for (int i = startPos; i < endPos && i < getItemCount(); i++) { + setSelected(i, selected); + } + notifyItemRangeChanged(startPos, (endPos - startPos)); + } + + protected void toggleSelection(int pos) { + setSelected(pos, !isSelected(pos)); + notifyItemChanged(pos); + + if (selectedIds.size() == 0) { + endSelectMode(); + } + } + + public boolean inActionMode() { + return actionMode != null; + } + + public int getSelectedCount() { + return selectedIds.size(); + } + + private void toggleSelectAllIcon(MenuItem selectAllItem, boolean allSelected) { + if (allSelected) { + selectAllItem.setIcon(R.drawable.ic_select_none); + selectAllItem.setTitle(R.string.deselect_all_label); + } else { + selectAllItem.setIcon(R.drawable.ic_select_all); + selectAllItem.setTitle(R.string.select_all_label); + } + } + + private void updateTitle() { + if (actionMode == null) { + return; + } + actionMode.setTitle(activity.getResources() + .getQuantityString(R.plurals.num_selected_label, selectedIds.size(), + selectedIds.size(), getItemCount())); + } + + public void setOnSelectModeListener(OnSelectModeListener onSelectModeListener) { + this.onSelectModeListener = onSelectModeListener; + } + + private void callOnEndSelectMode() { + if (onSelectModeListener != null) { + onSelectModeListener.onEndSelectMode(); + } + } + + public interface OnSelectModeListener { + void onStartSelectMode(); + + void onEndSelectMode(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/AddToQueueActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/AddToQueueActionButton.java deleted file mode 100644 index b362a5a1d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/AddToQueueActionButton.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; -import androidx.annotation.DrawableRes; -import androidx.annotation.StringRes; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.model.feed.FeedItem; - -class AddToQueueActionButton extends ItemActionButton { - - AddToQueueActionButton(FeedItem item) { - super(item); - } - - @Override - @StringRes - public int getLabel() { - return R.string.add_to_queue_label; - } - - @Override - @DrawableRes - public int getDrawable() { - return R.drawable.ic_add; - } - - @Override - public void onClick(Context context) { - MobileDownloadHelper.confirmMobileDownload(context, item); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java index afa86c9d7..dedf8e5e6 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java @@ -3,7 +3,6 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; -import android.widget.Toast; import de.danoeh.antennapod.R; import de.danoeh.antennapod.model.feed.FeedItem; @@ -35,8 +34,8 @@ public class CancelDownloadActionButton extends ItemActionButton { FeedMedia media = item.getMedia(); DownloadRequester.getInstance().cancelDownload(context, media); if (UserPreferences.isEnableAutodownload()) { - DBWriter.setFeedItemAutoDownload(media.getItem(), false); - Toast.makeText(context, R.string.download_canceled_autodownload_enabled_msg, Toast.LENGTH_LONG).show(); + item.setAutoDownload(false); + DBWriter.setFeedItem(item); } } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java index c3e979dd8..7b922154e 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java @@ -21,9 +21,9 @@ import de.danoeh.antennapod.core.util.NetworkUtils; public class DownloadActionButton extends ItemActionButton { private boolean isInQueue; - public DownloadActionButton(FeedItem item, boolean isInQueue) { + public DownloadActionButton(FeedItem item) { super(item); - this.isInQueue = isInQueue; + this.isInQueue = item.isTagged(FeedItem.TAG_QUEUE);; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java index 12150293f..ad4f35786 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java @@ -33,7 +33,7 @@ public abstract class ItemActionButton { } @NonNull - public static ItemActionButton forItem(@NonNull FeedItem item, boolean isInQueue, boolean allowStream) { + public static ItemActionButton forItem(@NonNull FeedItem item) { final FeedMedia media = item.getMedia(); if (media == null) { return new MarkAsPlayedActionButton(item); @@ -48,13 +48,10 @@ public abstract class ItemActionButton { return new PlayActionButton(item); } else if (isDownloadingMedia) { return new CancelDownloadActionButton(item); - } else if (UserPreferences.isStreamOverDownload() && allowStream) { + } else if (UserPreferences.isStreamOverDownload()) { return new StreamActionButton(item); - } else if (MobileDownloadHelper.userAllowedMobileDownloads() - || !MobileDownloadHelper.userChoseAddToQueue() || isInQueue) { - return new DownloadActionButton(item, isInQueue); } else { - return new AddToQueueActionButton(item); + return new DownloadActionButton(item); } } diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java deleted file mode 100644 index a80e3d59b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java +++ /dev/null @@ -1,62 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; - -import java.util.Arrays; - -import de.danoeh.antennapod.activity.OpmlImportHolder; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.export.opml.OpmlElement; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.core.storage.DownloadRequestException; -import de.danoeh.antennapod.core.storage.DownloadRequester; - -/** Queues items for download in the background. */ -public class OpmlFeedQueuer extends AsyncTask<Void, Void, Void> { - private final Context context; - private ProgressDialog progDialog; - private final int[] selection; - - public OpmlFeedQueuer(Context context, int[] selection) { - super(); - this.context = context; - this.selection = Arrays.copyOf(selection, selection.length); - } - - @Override - protected void onPostExecute(Void result) { - progDialog.dismiss(); - } - - @Override - protected void onPreExecute() { - progDialog = new ProgressDialog(context); - progDialog.setMessage(context.getString(R.string.processing_label)); - progDialog.setCancelable(false); - progDialog.setIndeterminate(true); - progDialog.show(); - } - - @Override - protected Void doInBackground(Void... params) { - DownloadRequester requester = DownloadRequester.getInstance(); - for (int selected : selection) { - OpmlElement element = OpmlImportHolder.getReadElements().get(selected); - Feed feed = new Feed(element.getXmlUrl(), null, - element.getText()); - try { - requester.downloadFeed(context.getApplicationContext(), feed); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - } - return null; - } - - public void executeAsync() { - executeOnExecutor(THREAD_POOL_EXECUTOR); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java deleted file mode 100644 index e037eb392..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java +++ /dev/null @@ -1,93 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; -import androidx.appcompat.app.AlertDialog; -import android.util.Log; - -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.io.Reader; -import java.util.ArrayList; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.export.opml.OpmlElement; -import de.danoeh.antennapod.core.export.opml.OpmlReader; - -public class OpmlImportWorker extends AsyncTask<Void, Void, ArrayList<OpmlElement>> { - private static final String TAG = "OpmlImportWorker"; - - private final Context context; - private Exception exception; - private ProgressDialog progDialog; - - private final Reader reader; - - public OpmlImportWorker(Context context, Reader reader) { - super(); - this.context = context; - this.reader = reader; - } - - @Override - protected ArrayList<OpmlElement> doInBackground(Void... params) { - Log.d(TAG, "Starting background work"); - - if (reader == null) { - return null; - } - - OpmlReader opmlReader = new OpmlReader(); - try { - ArrayList<OpmlElement> result = opmlReader.readDocument(reader); - reader.close(); - return result; - } catch (XmlPullParserException e) { - e.printStackTrace(); - exception = e; - return null; - } catch (IOException e) { - e.printStackTrace(); - exception = e; - return null; - } - - } - - @Override - protected void onPostExecute(ArrayList<OpmlElement> result) { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - progDialog.dismiss(); - if (exception != null) { - Log.d(TAG, "An error occurred while trying to parse the opml document"); - AlertDialog.Builder alert = new AlertDialog.Builder(context); - alert.setTitle(R.string.error_label); - alert.setMessage(context.getString(R.string.opml_reader_error) - + exception.getMessage()); - alert.setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); - alert.create().show(); - } - } - - @Override - protected void onPreExecute() { - progDialog = new ProgressDialog(context); - progDialog.setMessage(context.getString(R.string.please_wait)); - progDialog.setIndeterminate(true); - progDialog.setCancelable(false); - progDialog.show(); - } - - public void executeAsync() { - executeOnExecutor(THREAD_POOL_EXECUTOR); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java deleted file mode 100644 index 508ce74f4..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ /dev/null @@ -1,466 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import androidx.annotation.IdRes; -import androidx.annotation.NonNull; -import androidx.annotation.PluralsRes; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.collection.ArrayMap; -import androidx.fragment.app.Fragment; -import com.google.android.material.snackbar.Snackbar; -import com.leinardi.android.speeddial.SpeedDialView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.DownloadRequestException; -import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.util.FeedItemPermutors; -import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.model.feed.SortOrder; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class EpisodesApplyActionFragment extends Fragment implements Toolbar.OnMenuItemClickListener { - - public static final String TAG = "EpisodeActionFragment"; - - public static final int ACTION_ADD_TO_QUEUE = 1; - public static final int ACTION_REMOVE_FROM_QUEUE = 2; - private static final int ACTION_MARK_PLAYED = 4; - private static final int ACTION_MARK_UNPLAYED = 8; - public static final int ACTION_DOWNLOAD = 16; - public static final int ACTION_DELETE = 32; - public 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 static 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 final Map<Long, FeedItem> idMap = new ArrayMap<>(); - private final List<FeedItem> episodes = new ArrayList<>(); - private int actions; - private final List<String> titles = new ArrayList<>(); - private final LongList checkedIds = new LongList(); - - private ListView mListView; - private ArrayAdapter<String> mAdapter; - private SpeedDialView mSpeedDialView; - private Toolbar toolbar; - - 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, int actions) { - EpisodesApplyActionFragment f = new EpisodesApplyActionFragment(); - f.episodes.addAll(items); - for (FeedItem episode : items) { - f.idMap.put(episode.getId(), episode); - } - f.actions = actions; - return f; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.episodes_apply_action_fragment, container, false); - - toolbar = view.findViewById(R.id.toolbar); - toolbar.inflateMenu(R.menu.episodes_apply_action_options); - toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); - toolbar.setOnMenuItemClickListener(this); - refreshToolbarState(); - - mListView = view.findViewById(android.R.id.list); - mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - mListView.setOnItemClickListener((listView, view1, position, rowId) -> { - long id = episodes.get(position).getId(); - if (checkedIds.contains(id)) { - checkedIds.remove(id); - } else { - checkedIds.add(id); - } - refreshCheckboxes(); - }); - mListView.setOnItemLongClickListener((adapterView, view12, position, id) -> { - new AlertDialog.Builder(getActivity()) - .setItems(R.array.batch_long_press_options, (dialogInterface, item) -> { - int direction; - if (item == 0) { - direction = -1; - } else { - direction = 1; - } - - int currentPosition = position + direction; - while (currentPosition >= 0 && currentPosition < episodes.size()) { - long id1 = episodes.get(currentPosition).getId(); - if (!checkedIds.contains(id1)) { - checkedIds.add(id1); - } - currentPosition += direction; - } - refreshCheckboxes(); - }).show(); - return true; - }); - - titles.clear(); - for (FeedItem episode : episodes) { - titles.add(episode.getTitle()); - } - - mAdapter = new ArrayAdapter<>(getActivity(), - R.layout.simple_list_item_multiple_choice_on_start, titles); - mListView.setAdapter(mAdapter); - - // 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.setOnChangeListener(new SpeedDialView.OnChangeListener() { - @Override - public boolean onMainActionSelected() { - return false; - } - - @Override - public void onToggleChanged(boolean open) { - if (open && checkedIds.size() == 0) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, - Snackbar.LENGTH_SHORT); - mSpeedDialView.close(); - } - } - }); - 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; - }); - refreshCheckboxes(); - return view; - } - - public void refreshToolbarState() { - MenuItem selectAllItem = toolbar.getMenu().findItem(R.id.select_toggle); - if (checkedIds.size() == episodes.size()) { - selectAllItem.setIcon(R.drawable.ic_select_none); - selectAllItem.setTitle(R.string.deselect_all_label); - } else { - selectAllItem.setIcon(R.drawable.ic_select_all); - selectAllItem.setTitle(R.string.select_all_label); - } - } - - private static final Map<Integer, SortOrder> menuItemIdToSortOrder; - static { - Map<Integer, SortOrder> map = new ArrayMap<>(); - map.put(R.id.sort_title_a_z, SortOrder.EPISODE_TITLE_A_Z); - map.put(R.id.sort_title_z_a, SortOrder.EPISODE_TITLE_Z_A); - map.put(R.id.sort_date_new_old, SortOrder.DATE_NEW_OLD); - map.put(R.id.sort_date_old_new, SortOrder.DATE_OLD_NEW); - map.put(R.id.sort_duration_long_short, SortOrder.DURATION_LONG_SHORT); - map.put(R.id.sort_duration_short_long, SortOrder.DURATION_SHORT_LONG); - menuItemIdToSortOrder = Collections.unmodifiableMap(map); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - @StringRes int resId = 0; - switch (item.getItemId()) { - case R.id.select_options: - return true; - case R.id.select_toggle: - if (checkedIds.size() == episodes.size()) { - checkNone(); - } else { - checkAll(); - } - return true; - case R.id.check_all: - checkAll(); - resId = R.string.selected_all_label; - break; - case R.id.check_none: - checkNone(); - resId = R.string.deselected_all_label; - break; - case R.id.check_played: - checkPlayed(true); - resId = R.string.selected_played_label; - break; - case R.id.check_unplayed: - checkPlayed(false); - resId = R.string.selected_unplayed_label; - break; - case R.id.check_downloaded: - checkDownloaded(true); - resId = R.string.selected_downloaded_label; - break; - case R.id.check_not_downloaded: - checkDownloaded(false); - resId = R.string.selected_not_downloaded_label; - break; - case R.id.check_queued: - checkQueued(true); - resId = R.string.selected_queued_label; - break; - case R.id.check_not_queued: - checkQueued(false); - resId = R.string.selected_not_queued_label; - break; - case R.id.check_has_media: - checkWithMedia(); - resId = R.string.selected_has_media_label; - break; - default: // handle various sort options - SortOrder sortOrder = menuItemIdToSortOrder.get(item.getItemId()); - if (sortOrder != null) { - sort(sortOrder); - return true; - } - } - if (resId != 0) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer(resId, Snackbar.LENGTH_SHORT); - return true; - } else { - return false; - } - } - - private void sort(@NonNull SortOrder sortOrder) { - FeedItemPermutors.getPermutor(sortOrder) - .reorder(episodes); - refreshTitles(); - refreshCheckboxes(); - } - - private void checkAll() { - for (FeedItem episode : episodes) { - if (!checkedIds.contains(episode.getId())) { - checkedIds.add(episode.getId()); - } - } - refreshCheckboxes(); - } - - private void checkNone() { - checkedIds.clear(); - refreshCheckboxes(); - } - - private void checkPlayed(boolean isPlayed) { - for (FeedItem episode : episodes) { - if (episode.isPlayed() == isPlayed) { - if (!checkedIds.contains(episode.getId())) { - checkedIds.add(episode.getId()); - } - } else { - if (checkedIds.contains(episode.getId())) { - checkedIds.remove(episode.getId()); - } - } - } - refreshCheckboxes(); - } - - private void checkDownloaded(boolean isDownloaded) { - for (FeedItem episode : episodes) { - if (episode.hasMedia() && episode.getMedia().isDownloaded() == isDownloaded) { - if (!checkedIds.contains(episode.getId())) { - checkedIds.add(episode.getId()); - } - } else { - if (checkedIds.contains(episode.getId())) { - checkedIds.remove(episode.getId()); - } - } - } - refreshCheckboxes(); - } - - private void checkQueued(boolean isQueued) { - for (FeedItem episode : episodes) { - if (episode.isTagged(FeedItem.TAG_QUEUE) == isQueued) { - checkedIds.add(episode.getId()); - } else { - checkedIds.remove(episode.getId()); - } - } - refreshCheckboxes(); - } - - private void checkWithMedia() { - for (FeedItem episode : episodes) { - if (episode.hasMedia()) { - checkedIds.add(episode.getId()); - } else { - checkedIds.remove(episode.getId()); - } - } - refreshCheckboxes(); - } - - private void refreshTitles() { - titles.clear(); - for (FeedItem episode : episodes) { - titles.add(episode.getTitle()); - } - mAdapter.notifyDataSetChanged(); - } - - private void refreshCheckboxes() { - for (int i = 0; i < episodes.size(); i++) { - FeedItem episode = episodes.get(i); - boolean checked = checkedIds.contains(episode.getId()); - mListView.setItemChecked(i, checked); - } - refreshToolbarState(); - toolbar.setTitle(getResources().getQuantityString(R.plurals.num_selected_label, - checkedIds.size(), checkedIds.size())); - } - - private void queueChecked() { - // Check if an episode actually contains any media files before adding it to queue - LongList toQueue = new LongList(checkedIds.size()); - for (FeedItem episode : episodes) { - if (checkedIds.contains(episode.getId()) && episode.hasMedia()) { - toQueue.add(episode.getId()); - } - } - DBWriter.addQueueItem(getActivity(), true, toQueue.toArray()); - close(R.plurals.added_to_queue_batch_label, toQueue.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(R.plurals.marked_read_batch_label, checkedIds.size()); - } - - private void markedCheckedUnplayed() { - DBWriter.markItemPlayed(FeedItem.UNPLAYED, checkedIds.toArray()); - close(R.plurals.marked_unread_batch_label, checkedIds.size()); - } - - private void downloadChecked() { - // download the check episodes in the same order as they are currently displayed - List<FeedItem> toDownload = new ArrayList<>(checkedIds.size()); - for (FeedItem episode : episodes) { - if (checkedIds.contains(episode.getId()) && episode.hasMedia() && !episode.getFeed().isLocalFeed()) { - toDownload.add(episode); - } - } - try { - DownloadRequester.getInstance().downloadMedia(getActivity(), true, toDownload.toArray(new FeedItem[0])); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DownloadRequestErrorDialogCreator.newRequestErrorDialog(getActivity(), e.getMessage()); - } - close(R.plurals.downloading_batch_label, toDownload.size()); - } - - private void deleteChecked() { - int countHasMedia = 0; - int countNoMedia = 0; - for (long id : checkedIds.toArray()) { - FeedItem episode = idMap.get(id); - if (episode.hasMedia() && episode.getMedia().isDownloaded()) { - countHasMedia++; - DBWriter.deleteFeedMediaOfItem(getActivity(), episode.getMedia().getId()); - } else { - countNoMedia++; - } - } - closeMore(R.plurals.deleted_multi_episode_batch_label, countNoMedia, countHasMedia); - } - - private void close(@PluralsRes int msgId, int numItems) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - getResources().getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG); - getActivity().getSupportFragmentManager().popBackStack(); - } - - private void closeMore(@PluralsRes int msgId, int countNoMedia, int countHasMedia) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - getResources().getQuantityString(msgId, - (countHasMedia + countNoMedia), - (countHasMedia + countNoMedia), countHasMedia), - Snackbar.LENGTH_LONG); - getActivity().getSupportFragmentManager().popBackStack(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java index 195891499..3186cbe2e 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java @@ -10,11 +10,9 @@ import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import android.widget.Button; import android.widget.CheckBox; -import android.widget.SeekBar; import android.widget.TextView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.view.PlaybackSpeedSeekBar; @@ -63,11 +61,7 @@ public class PlaybackControlsDialog extends DialogFragment { dialog = new AlertDialog.Builder(getContext()) .setTitle(R.string.audio_controls) .setView(R.layout.audio_controls) - .setPositiveButton(R.string.close_label, (dialog1, which) -> { - final SeekBar left = dialog.findViewById(R.id.volume_left); - final SeekBar right = dialog.findViewById(R.id.volume_right); - UserPreferences.setVolume(left.getProgress(), right.getProgress()); - }).create(); + .setPositiveButton(R.string.close_label, null).create(); return dialog; } @@ -79,10 +73,6 @@ public class PlaybackControlsDialog extends DialogFragment { speedSeekBar.setProgressChangedListener(speed -> txtvPlaybackSpeed.setText(String.format(Locale.getDefault(), "%.2fx", speed))); - final SeekBar barLeftVolume = dialog.findViewById(R.id.volume_left); - barLeftVolume.setProgress(UserPreferences.getLeftVolumePercentage()); - final SeekBar barRightVolume = dialog.findViewById(R.id.volume_right); - barRightVolume.setProgress(UserPreferences.getRightVolumePercentage()); final CheckBox stereoToMono = dialog.findViewById(R.id.stereo_to_mono); stereoToMono.setChecked(UserPreferences.stereoToMono()); if (controller != null && !controller.canDownmix()) { @@ -91,10 +81,6 @@ public class PlaybackControlsDialog extends DialogFragment { stereoToMono.setText(getString(R.string.stereo_to_mono) + " [" + sonicOnly + "]"); } - if (UserPreferences.useExoplayer()) { - barRightVolume.setEnabled(false); - } - final CheckBox skipSilence = dialog.findViewById(R.id.skipSilence); skipSilence.setChecked(UserPreferences.isSkipSilence()); if (!UserPreferences.useExoplayer()) { @@ -106,39 +92,6 @@ public class PlaybackControlsDialog extends DialogFragment { UserPreferences.setSkipSilence(isChecked); controller.setSkipSilence(isChecked); }); - - barLeftVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - controller.setVolume( - Converter.getVolumeFromPercentage(progress), - Converter.getVolumeFromPercentage(barRightVolume.getProgress())); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }); - barRightVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - controller.setVolume( - Converter.getVolumeFromPercentage(barLeftVolume.getProgress()), - Converter.getVolumeFromPercentage(progress)); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }); stereoToMono.setOnCheckedChangeListener((buttonView, isChecked) -> { UserPreferences.stereoToMono(isChecked); if (controller != null) { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java index fa5c2d8c3..691bd65e8 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java @@ -153,10 +153,13 @@ public class SleepTimerDialog extends DialogFragment { return; } try { + long time = Long.parseLong(etxtTime.getText().toString()); + if (time == 0) { + throw new NumberFormatException("Timer must not be zero"); + } SleepTimerPreferences.setLastTimer(etxtTime.getText().toString(), spTimeUnit.getSelectedItemPosition()); - long time = SleepTimerPreferences.timerMillis(); if (controller != null) { - controller.setSleepTimer(time); + controller.setSleepTimer(SleepTimerPreferences.timerMillis()); } closeKeyboard(content); } catch (NumberFormatException e) { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java new file mode 100644 index 000000000..53c2697ce --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java @@ -0,0 +1,188 @@ +package de.danoeh.antennapod.dialog; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.gridlayout.widget.GridLayout; + +import com.annimon.stream.Stream; + +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.FeeditemlistItemBinding; +import de.danoeh.antennapod.databinding.SwipeactionsDialogBinding; +import de.danoeh.antennapod.databinding.SwipeactionsPickerBinding; +import de.danoeh.antennapod.databinding.SwipeactionsPickerItemBinding; +import de.danoeh.antennapod.databinding.SwipeactionsRowBinding; +import de.danoeh.antennapod.fragment.EpisodesFragment; +import de.danoeh.antennapod.fragment.FeedItemlistFragment; +import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.fragment.swipeactions.SwipeAction; +import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; +import de.danoeh.antennapod.ui.common.ThemeUtils; + +public class SwipeActionsDialog { + private static final int LEFT = 1; + private static final int RIGHT = 0; + + private final Context context; + private final String tag; + + private SwipeAction rightAction; + private SwipeAction leftAction; + private List<SwipeAction> keys; + + public SwipeActionsDialog(Context context, String tag) { + this.context = context; + this.tag = tag; + } + + public void show(Callback prefsChanged) { + SwipeActions.Actions actions = SwipeActions.getPrefsWithDefaults(context, tag); + leftAction = actions.left; + rightAction = actions.right; + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + + keys = SwipeActions.swipeActions; + + String forFragment = ""; + switch (tag) { + /*case InboxFragment.TAG: + forFragment = context.getString(R.string.inbox_label); + break;*/ + case EpisodesFragment.TAG: + forFragment = context.getString(R.string.episodes_label); + break; + case FeedItemlistFragment.TAG: + forFragment = context.getString(R.string.feeds_label); + break; + case QueueFragment.TAG: + forFragment = context.getString(R.string.queue_label); + keys = Stream.of(keys).filter(a -> !a.getId().equals(SwipeAction.ADD_TO_QUEUE) + && !a.getId().equals(SwipeAction.REMOVE_FROM_INBOX)).toList(); + break; + default: break; + } + + if (!tag.equals(QueueFragment.TAG)) { + keys = Stream.of(keys).filter(a -> !a.getId().equals(SwipeAction.REMOVE_FROM_QUEUE)).toList(); + } + + builder.setTitle(context.getString(R.string.swipeactions_label) + " - " + forFragment); + SwipeactionsDialogBinding viewBinding = SwipeactionsDialogBinding.inflate(LayoutInflater.from(context)); + builder.setView(viewBinding.getRoot()); + + viewBinding.enableSwitch.setOnCheckedChangeListener((compoundButton, b) -> { + viewBinding.actionLeftContainer.getRoot().setAlpha(b ? 1.0f : 0.4f); + viewBinding.actionRightContainer.getRoot().setAlpha(b ? 1.0f : 0.4f); + }); + + viewBinding.enableSwitch.setChecked(SwipeActions.isSwipeActionEnabled(context, tag)); + + setupSwipeDirectionView(viewBinding.actionLeftContainer, LEFT); + setupSwipeDirectionView(viewBinding.actionRightContainer, RIGHT); + + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + savePrefs(tag, rightAction.getId(), leftAction.getId()); + saveActionsEnabledPrefs(viewBinding.enableSwitch.isChecked()); + prefsChanged.onCall(); + }); + + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } + + private void setupSwipeDirectionView(SwipeactionsRowBinding view, int direction) { + SwipeAction action = direction == LEFT ? leftAction : rightAction; + + view.swipeDirectionLabel.setText(direction == LEFT ? R.string.swipe_left : R.string.swipe_right); + view.swipeActionLabel.setText(action.getTitle(context)); + populateMockEpisode(view.mockEpisode); + if (direction == RIGHT && view.previewContainer.getChildAt(0) != view.swipeIcon) { + view.previewContainer.removeView(view.swipeIcon); + view.previewContainer.addView(view.swipeIcon, 0); + } + + view.swipeIcon.setImageResource(action.getActionIcon()); + view.swipeIcon.setColorFilter(ThemeUtils.getColorFromAttr(context, action.getActionColor())); + + view.changeButton.setOnClickListener(v -> showPicker(view, direction)); + view.previewContainer.setOnClickListener(v -> showPicker(view, direction)); + } + + private void showPicker(SwipeactionsRowBinding view, int direction) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(direction == LEFT ? R.string.swipe_left : R.string.swipe_right); + + SwipeactionsPickerBinding picker = SwipeactionsPickerBinding.inflate(LayoutInflater.from(context)); + builder.setView(picker.getRoot()); + builder.setNegativeButton(R.string.cancel_label, null); + AlertDialog dialog = builder.show(); + + for (int i = 0; i < keys.size(); i++) { + final int actionIndex = i; + SwipeAction action = keys.get(actionIndex); + SwipeactionsPickerItemBinding item = SwipeactionsPickerItemBinding.inflate(LayoutInflater.from(context)); + item.swipeActionLabel.setText(action.getTitle(context)); + + Drawable icon = DrawableCompat.wrap(AppCompatResources.getDrawable(context, action.getActionIcon())); + DrawableCompat.setTintMode(icon, PorterDuff.Mode.SRC_ATOP); + if ((direction == LEFT && leftAction == action) || (direction == RIGHT && rightAction == action)) { + DrawableCompat.setTint(icon, ThemeUtils.getColorFromAttr(context, action.getActionColor())); + item.swipeActionLabel.setTextColor(ThemeUtils.getColorFromAttr(context, action.getActionColor())); + } else { + DrawableCompat.setTint(icon, ThemeUtils.getColorFromAttr(context, R.attr.action_icon_color)); + } + item.swipeIcon.setImageDrawable(icon); + + item.getRoot().setOnClickListener(v -> { + if (direction == LEFT) { + leftAction = keys.get(actionIndex); + } else { + rightAction = keys.get(actionIndex); + } + setupSwipeDirectionView(view, direction); + dialog.dismiss(); + }); + GridLayout.LayoutParams param = new GridLayout.LayoutParams( + GridLayout.spec(GridLayout.UNDEFINED, GridLayout.BASELINE), + GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f)); + param.width = 0; + picker.pickerGridLayout.addView(item.getRoot(), param); + } + picker.pickerGridLayout.setColumnCount(2); + picker.pickerGridLayout.setRowCount((keys.size() + 1) / 2); + } + + private void populateMockEpisode(FeeditemlistItemBinding view) { + view.container.setAlpha(0.3f); + view.secondaryActionButton.secondaryActionButton.setVisibility(View.GONE); + view.dragHandle.setVisibility(View.GONE); + view.statusUnread.setText("███"); + view.txtvTitle.setText("███████"); + view.txtvPosition.setText("█████"); + } + + private void savePrefs(String tag, String right, String left) { + SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE); + prefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + tag, right + "," + left).apply(); + } + + private void saveActionsEnabledPrefs(Boolean enabled) { + SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE); + prefs.edit().putBoolean(SwipeActions.KEY_PREFIX_NO_ACTION + tag, enabled).apply(); + } + + public interface Callback { + void onCall(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java index c6927c69f..6011872cf 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java @@ -131,8 +131,8 @@ public class VariableSpeedDialog extends DialogFragment { }); holder.chip.setOnClickListener(v -> { if (controller != null) { + dismiss(); controller.setPlaybackSpeed(speed); - notifyDataSetChanged(); } }); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java index a84c34b7e..168133c7a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java @@ -75,7 +75,6 @@ public class AudioPlayerFragment extends Fragment implements public static final int POS_COVER = 0; public static final int POS_DESCRIPTION = 1; private static final int NUM_CONTENT_FRAGMENTS = 2; - private static final float EPSILON = 0.001f; PlaybackSpeedIndicatorView butPlaybackSpeed; TextView txtvPlaybackSpeed; @@ -136,7 +135,7 @@ public class AudioPlayerFragment extends Fragment implements setupLengthTextView(); setupControlButtons(); - setupPlaybackSpeedButton(); + butPlaybackSpeed.setOnClickListener(v -> new VariableSpeedDialog().show(getChildFragmentManager(), null)); sbPosition.setOnSeekBarChangeListener(this); pager = root.findViewById(R.id.pager); @@ -244,40 +243,6 @@ public class AudioPlayerFragment extends Fragment implements }); } - private void setupPlaybackSpeedButton() { - butPlaybackSpeed.setOnClickListener(v -> { - if (controller == null) { - return; - } - List<Float> availableSpeeds = UserPreferences.getPlaybackSpeedArray(); - float currentSpeed = controller.getCurrentPlaybackSpeedMultiplier(); - - int newSpeedIndex = 0; - while (newSpeedIndex < availableSpeeds.size() - && availableSpeeds.get(newSpeedIndex) < currentSpeed + EPSILON) { - newSpeedIndex++; - } - - float newSpeed; - if (availableSpeeds.size() == 0) { - newSpeed = 1.0f; - } else if (newSpeedIndex == availableSpeeds.size()) { - newSpeed = availableSpeeds.get(0); - } else { - newSpeed = availableSpeeds.get(newSpeedIndex); - } - - controller.setPlaybackSpeed(newSpeed); - loadMediaInfo(false); - }); - butPlaybackSpeed.setOnLongClickListener(v -> { - new VariableSpeedDialog().show(getChildFragmentManager(), null); - return true; - }); - butPlaybackSpeed.setVisibility(View.VISIBLE); - txtvPlaybackSpeed.setVisibility(View.VISIBLE); - } - protected void updatePlaybackSpeedButton(Playable media) { if (butPlaybackSpeed == null || controller == null) { return; @@ -286,8 +251,6 @@ public class AudioPlayerFragment extends Fragment implements String speedStr = new DecimalFormat("0.00").format(speed); txtvPlaybackSpeed.setText(speedStr); butPlaybackSpeed.setSpeed(speed); - butPlaybackSpeed.setVisibility(View.VISIBLE); - txtvPlaybackSpeed.setVisibility(View.VISIBLE); } private void loadMediaInfo(boolean includingChapters) { @@ -550,21 +513,21 @@ public class AudioPlayerFragment extends Fragment implements if (feedItem != null && FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), feedItem)) { return true; } - switch (item.getItemId()) { - case R.id.disable_sleeptimer_item: // Fall-through - case R.id.set_sleeptimer_item: - new SleepTimerDialog().show(getChildFragmentManager(), "SleepTimerDialog"); - return true; - case R.id.audio_controls: - PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(); - dialog.show(getChildFragmentManager(), "playback_controls"); - return true; - case R.id.open_feed_item: - if (feedItem != null) { - Intent intent = MainActivity.getIntentToOpenFeed(getContext(), feedItem.getFeedId()); - startActivity(intent); - } - return true; + + final int itemId = item.getItemId(); + if (itemId == R.id.disable_sleeptimer_item || itemId == R.id.set_sleeptimer_item) { + new SleepTimerDialog().show(getChildFragmentManager(), "SleepTimerDialog"); + return true; + } else if (itemId == R.id.audio_controls) { + PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(); + dialog.show(getChildFragmentManager(), "playback_controls"); + return true; + } else if (itemId == R.id.open_feed_item) { + if (feedItem != null) { + Intent intent = MainActivity.getIntentToOpenFeed(getContext(), feedItem.getFeedId()); + startActivity(intent); + } + return true; } return false; } 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 66f85d84d..74661d240 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.fragment; import android.os.Bundle; import android.util.Log; +import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -12,6 +13,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; + +import com.google.android.material.snackbar.Snackbar; +import com.leinardi.android.speeddial.SpeedDialView; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; @@ -22,15 +27,15 @@ import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; -import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; import de.danoeh.antennapod.view.EmptyViewHandler; import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; @@ -45,13 +50,11 @@ import org.greenrobot.eventbus.ThreadMode; import java.util.ArrayList; import java.util.List; -import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_ADD_TO_QUEUE; -import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_DELETE; - /** * Displays all completed downloads and provides a button to delete them. */ -public class CompletedDownloadsFragment extends Fragment { +public class CompletedDownloadsFragment extends Fragment implements + EpisodeItemListAdapter.OnSelectModeListener { private static final String TAG = CompletedDownloadsFragment.class.getSimpleName(); @@ -64,6 +67,8 @@ public class CompletedDownloadsFragment extends Fragment { private boolean isUpdatingFeeds = false; + private SpeedDialView speedDialView; + @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -74,9 +79,38 @@ public class CompletedDownloadsFragment extends Fragment { recyclerView = root.findViewById(R.id.recyclerView); recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); adapter = new CompletedDownloadsListAdapter((MainActivity) getActivity()); + adapter.setOnSelectModeListener(this); recyclerView.setAdapter(adapter); progressBar = root.findViewById(R.id.progLoading); + speedDialView = root.findViewById(R.id.fabSD); + speedDialView.inflate(R.menu.episodes_apply_action_speeddial); + speedDialView.removeActionItemById(R.id.download_batch); + speedDialView.removeActionItemById(R.id.mark_read_batch); + speedDialView.removeActionItemById(R.id.mark_unread_batch); + speedDialView.removeActionItemById(R.id.remove_from_queue_batch); + speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() { + @Override + public boolean onMainActionSelected() { + return false; + } + + @Override + public void onToggleChanged(boolean open) { + if (open && adapter.getSelectedCount() == 0) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, + Snackbar.LENGTH_SHORT); + speedDialView.close(); + } + } + }); + speedDialView.setOnActionSelectedListener(actionItem -> { + new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), adapter.getSelectedItems()) + .handleAction(actionItem.getId()); + adapter.endSelectMode(); + return true; + }); + addEmptyView(); EventBus.getDefault().register(this); return root; @@ -85,6 +119,7 @@ public class CompletedDownloadsFragment extends Fragment { @Override public void onDestroyView() { EventBus.getDefault().unregister(this); + adapter.endSelectMode(); super.onDestroyView(); } @@ -105,17 +140,12 @@ public class CompletedDownloadsFragment extends Fragment { @Override public void onPrepareOptionsMenu(@NonNull Menu menu) { menu.findItem(R.id.clear_logs_item).setVisible(false); - menu.findItem(R.id.episode_actions).setVisible(items.size() > 0); isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); } @Override public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.episode_actions) { - ((MainActivity) requireActivity()) - .loadChildFragment(EpisodesApplyActionFragment.newInstance(items, ACTION_DELETE | ACTION_ADD_TO_QUEUE)); - return true; - } else if (item.getItemId() == R.id.refresh_item) { + if (item.getItemId() == R.id.refresh_item) { AutoUpdateManager.runImmediate(requireContext()); return true; } @@ -135,11 +165,15 @@ public class CompletedDownloadsFragment extends Fragment { @Override public boolean onContextItemSelected(@NonNull MenuItem item) { - FeedItem selectedItem = adapter.getSelectedItem(); + FeedItem selectedItem = adapter.getLongPressedItem(); if (selectedItem == null) { Log.i(TAG, "Selected item at current position was null, ignoring selection"); return super.onContextItemSelected(item); } + if (adapter.onContextItemSelected(item)) { + return true; + } + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); } @@ -151,7 +185,6 @@ public class CompletedDownloadsFragment extends Fragment { emptyView.attachToRecyclerView(recyclerView); } - @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); @@ -221,6 +254,17 @@ public class CompletedDownloadsFragment extends Fragment { }, error -> Log.e(TAG, Log.getStackTraceString(error))); } + @Override + public void onStartSelectMode() { + speedDialView.setVisibility(View.VISIBLE); + } + + @Override + public void onEndSelectMode() { + speedDialView.close(); + speedDialView.setVisibility(View.GONE); + } + private static class CompletedDownloadsListAdapter extends EpisodeItemListAdapter { public CompletedDownloadsListAdapter(MainActivity mainActivity) { @@ -232,5 +276,13 @@ public class CompletedDownloadsFragment extends Fragment { DeleteActionButton actionButton = new DeleteActionButton(getItem(pos)); actionButton.configure(holder.secondaryActionButton, holder.secondaryActionIcon, getActivity()); } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + if (!inActionMode()) { + menu.findItem(R.id.multi_select).setVisible(true); + } + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java index 1f6067125..ddbf6c078 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -11,7 +11,6 @@ import android.view.MenuItem; import android.view.View; import android.widget.ListView; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -23,7 +22,7 @@ import de.danoeh.antennapod.adapter.DownloadLogAdapter; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloadLogEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; -import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.service.download.DownloadStatus; @@ -32,8 +31,8 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; @@ -111,13 +110,11 @@ public class DownloadLogFragment extends ListFragment { DownloadRequest downloadRequest = ((Downloader) item).getDownloadRequest(); DownloadRequester.getInstance().cancelDownload(getActivity(), downloadRequest.getSource()); - if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA - && UserPreferences.isEnableAutodownload()) { + if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { FeedMedia media = DBReader.getFeedMedia(downloadRequest.getFeedfileId()); - DBWriter.setFeedItemAutoDownload(media.getItem(), false); - - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - R.string.download_canceled_autodownload_enabled_msg, Toast.LENGTH_SHORT); + FeedItem feedItem = media.getItem(); + feedItem.setAutoDownload(false); + DBWriter.setFeedItem(feedItem); } } else if (item instanceof DownloadStatus) { DownloadStatus status = (DownloadStatus) item; @@ -164,7 +161,6 @@ public class DownloadLogFragment extends ListFragment { @Override public void onPrepareOptionsMenu(@NonNull Menu menu) { - menu.findItem(R.id.episode_actions).setVisible(false); menu.findItem(R.id.clear_logs_item).setVisible(!downloadLog.isEmpty()); isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java index 1ca5d524b..cfa226f8f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java @@ -18,7 +18,6 @@ import com.google.android.material.tabs.TabLayoutMediator; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; public class EpisodesFragment extends PagedToolbarFragment { @@ -45,7 +44,6 @@ public class EpisodesFragment extends PagedToolbarFragment { Toolbar toolbar = rootView.findViewById(R.id.toolbar); toolbar.setTitle(R.string.episodes_label); toolbar.inflateMenu(R.menu.episodes); - MenuItemUtils.setupSearchItem(toolbar.getMenu(), (MainActivity) getActivity(), 0, ""); displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; if (savedInstanceState != null) { displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java index e88ef432c..6d63e4ab2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -25,6 +25,7 @@ import de.danoeh.antennapod.core.event.FeedListUpdateEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; import org.greenrobot.eventbus.EventBus; @@ -47,7 +48,6 @@ import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -121,47 +121,48 @@ public abstract class EpisodesListFragment extends Fragment { @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (!super.onOptionsItemSelected(item)) { - switch (item.getItemId()) { - case R.id.refresh_item: - AutoUpdateManager.runImmediate(requireContext()); - return true; - case R.id.mark_all_read_item: - ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getActivity(), - R.string.mark_all_read_label, - R.string.mark_all_read_confirmation_msg) { - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - DBWriter.markAllItemsRead(); - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - R.string.mark_all_read_msg, Toast.LENGTH_SHORT); - } - }; - markAllReadConfirmationDialog.createNewDialog().show(); - return true; - case R.id.remove_all_new_flags_item: - ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getActivity(), - R.string.remove_all_new_flags_label, - R.string.remove_all_new_flags_confirmation_msg) { - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - DBWriter.removeAllNewFlags(); - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - R.string.removed_all_new_flags_msg, Toast.LENGTH_SHORT); - } - }; - removeAllNewFlagsConfirmationDialog.createNewDialog().show(); - return true; - default: - return false; - } - } else { + if (super.onOptionsItemSelected(item)) { + return true; + } + final int itemId = item.getItemId(); + if (itemId == R.id.refresh_item) { + AutoUpdateManager.runImmediate(requireContext()); + return true; + } else if (itemId == R.id.mark_all_read_item) { + ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getActivity(), + R.string.mark_all_read_label, + R.string.mark_all_read_confirmation_msg) { + + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + dialog.dismiss(); + DBWriter.markAllItemsRead(); + ((MainActivity) getActivity()).showSnackbarAbovePlayer( + R.string.mark_all_read_msg, Toast.LENGTH_SHORT); + } + }; + markAllReadConfirmationDialog.createNewDialog().show(); + return true; + } else if (itemId == R.id.remove_all_new_flags_item) { + ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getActivity(), + R.string.remove_all_new_flags_label, + R.string.remove_all_new_flags_confirmation_msg) { + + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + dialog.dismiss(); + DBWriter.removeAllNewFlags(); + ((MainActivity) getActivity()).showSnackbarAbovePlayer( + R.string.removed_all_new_flags_msg, Toast.LENGTH_SHORT); + } + }; + removeAllNewFlagsConfirmationDialog.createNewDialog().show(); + return true; + } else if (itemId == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); return true; } + return false; } @Override @@ -177,11 +178,11 @@ public abstract class EpisodesListFragment extends Fragment { return true; // avoids that the position is reset when we need it in the submenu } - if (listAdapter.getSelectedItem() == null) { + if (listAdapter.getLongPressedItem() == null) { Log.i(TAG, "Selected item or listAdapter was null, ignoring selection"); return super.onContextItemSelected(item); } - FeedItem selectedItem = listAdapter.getSelectedItem(); + FeedItem selectedItem = listAdapter.getLongPressedItem(); return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); } @@ -203,6 +204,7 @@ public abstract class EpisodesListFragment extends Fragment { } SwipeRefreshLayout swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); + swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); swipeRefreshLayout.setOnRefreshListener(() -> { AutoUpdateManager.runImmediate(requireContext()); new Handler(Looper.getMainLooper()).postDelayed(() -> swipeRefreshLayout.setRefreshing(false), diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index de27ff0af..fb3b8d136 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -1,13 +1,14 @@ package de.danoeh.antennapod.fragment; import android.content.Context; -import android.content.res.Configuration; import android.content.Intent; +import android.content.res.Configuration; import android.graphics.LightingColorFilter; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.util.Log; +import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; @@ -18,6 +19,7 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatDrawableManager; @@ -30,23 +32,34 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.CollapsingToolbarLayout; +import com.google.android.material.snackbar.Snackbar; import com.joanzapata.iconify.Iconify; import com.joanzapata.iconify.widget.IconTextView; +import com.leinardi.android.speeddial.SpeedDialView; + +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; +import org.apache.commons.lang3.Validate; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; +import java.util.Set; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; +import de.danoeh.antennapod.core.event.FavoritesEvent; import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.FeedListUpdateEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; +import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.feed.FeedEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.glide.FastBlurTransformation; import de.danoeh.antennapod.core.service.download.DownloadService; @@ -58,13 +71,16 @@ import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil; -import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.dialog.FilterDialog; import de.danoeh.antennapod.dialog.RemoveFeedDialog; import de.danoeh.antennapod.dialog.RenameFeedDialog; +import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; +import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.menuhandler.FeedMenuHandler; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; import de.danoeh.antennapod.view.ToolbarIconTintManager; import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; @@ -72,24 +88,18 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.Validate; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.List; -import java.util.Set; /** * Displays a list of FeedItems. */ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItemClickListener, - Toolbar.OnMenuItemClickListener { - private static final String TAG = "ItemlistFragment"; + Toolbar.OnMenuItemClickListener, EpisodeItemListAdapter.OnSelectModeListener { + public static final String TAG = "ItemlistFragment"; private static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id"; private static final String KEY_UP_ARROW = "up_arrow"; private FeedItemListAdapter adapter; + private SwipeActions swipeActions; private MoreContentListFooterUtil nextPageLoader; private ProgressBar progressBar; @@ -105,7 +115,8 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem private ImageButton butShowSettings; private View header; private Toolbar toolbar; - private ToolbarIconTintManager iconTintManager; + private SpeedDialView speedDialView; + private boolean displayUpArrow; private long feedID; @@ -158,6 +169,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); progressBar = root.findViewById(R.id.progLoading); + progressBar.setVisibility(View.VISIBLE); txtvTitle = root.findViewById(R.id.txtvTitle); txtvAuthor = root.findViewById(R.id.txtvAuthor); imgvBackground = root.findViewById(R.id.imgvBackground); @@ -171,7 +183,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem AppBarLayout appBar = root.findViewById(R.id.appBar); CollapsingToolbarLayout collapsingToolbar = root.findViewById(R.id.collapsing_toolbar); - iconTintManager = new ToolbarIconTintManager(getContext(), toolbar, collapsingToolbar) { + ToolbarIconTintManager iconTintManager = new ToolbarIconTintManager(getContext(), toolbar, collapsingToolbar) { @Override protected void doTint(Context themedContext) { toolbar.getMenu().findItem(R.id.sort_items) @@ -211,6 +223,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem EventBus.getDefault().register(this); SwipeRefreshLayout swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); + swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); swipeRefreshLayout.setOnRefreshListener(() -> { try { DBTasks.forceRefreshFeed(requireContext(), feed, true); @@ -222,6 +235,31 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem }); loadItems(); + + // Init action UI (via a FAB Speed Dial) + speedDialView = root.findViewById(R.id.fabSD); + speedDialView.inflate(R.menu.episodes_apply_action_speeddial); + speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() { + @Override + public boolean onMainActionSelected() { + return false; + } + + @Override + public void onToggleChanged(boolean open) { + if (open && adapter.getSelectedCount() == 0) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, + Snackbar.LENGTH_SHORT); + speedDialView.close(); + } + } + }); + speedDialView.setOnActionSelectedListener(actionItem -> { + new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), adapter.getSelectedItems()) + .handleAction(actionItem.getId()); + adapter.endSelectMode(); + return true; + }); return root; } @@ -233,6 +271,9 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem if (disposable != null) { disposable.dispose(); } + if (adapter != null) { + adapter.endSelectMode(); + } adapter = null; } @@ -253,8 +294,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem if (feed == null) { return; } - MenuItemUtils.setupSearchItem(toolbar.getMenu(), (MainActivity) getActivity(), feedID, feed.getTitle()); - toolbar.getMenu().findItem(R.id.share_link_item).setVisible(feed.getLink() != null); toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed.getLink() != null); @@ -272,9 +311,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem @Override public boolean onMenuItemClick(MenuItem item) { - if (item.getItemId() == R.id.action_search) { - item.getActionView().post(() -> iconTintManager.updateTint()); - } if (feed == null) { ((MainActivity) getActivity()).showSnackbarAbovePlayer( R.string.please_wait_for_data, Toast.LENGTH_LONG); @@ -291,37 +327,31 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem if (feedMenuHandled) { return true; } - switch (item.getItemId()) { - case R.id.episode_actions: - int actions = EpisodesApplyActionFragment.ACTION_ALL; - if (feed.isLocalFeed()) { - // turn off download and delete actions for local feed - actions ^= EpisodesApplyActionFragment.ACTION_DOWNLOAD; - actions ^= EpisodesApplyActionFragment.ACTION_DELETE; - } - EpisodesApplyActionFragment fragment = EpisodesApplyActionFragment - .newInstance(feed.getItems(), actions); - ((MainActivity) getActivity()).loadChildFragment(fragment); - return true; - case R.id.rename_item: - new RenameFeedDialog(getActivity(), feed).show(); - return true; - case R.id.remove_item: - RemoveFeedDialog.show(getContext(), feed, () -> - ((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null)); - return true; - default: - return false; + final int itemId = item.getItemId(); + if (itemId == R.id.rename_item) { + new RenameFeedDialog(getActivity(), feed).show(); + return true; + } else if (itemId == R.id.remove_item) { + RemoveFeedDialog.show(getContext(), feed, () -> + ((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null)); + return true; + } else if (itemId == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(feed.getId(), feed.getTitle())); + return true; } + return false; } @Override public boolean onContextItemSelected(@NonNull MenuItem item) { - FeedItem selectedItem = adapter.getSelectedItem(); + FeedItem selectedItem = adapter.getLongPressedItem(); if (selectedItem == null) { Log.i(TAG, "Selected item at current position was null, ignoring selection"); return super.onContextItemSelected(item); } + if (adapter.onContextItemSelected(item)) { + return true; + } return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); } @@ -393,6 +423,34 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem } } + @Subscribe(threadMode = ThreadMode.MAIN) + public void favoritesChanged(FavoritesEvent event) { + updateUi(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onQueueChanged(QueueEvent event) { + updateUi(); + } + + @Override + public void onStartSelectMode() { + swipeActions.detach(); + if (feed.isLocalFeed()) { + speedDialView.removeActionItemById(R.id.download_batch); + speedDialView.removeActionItemById(R.id.delete_batch); + } + speedDialView.setVisibility(View.VISIBLE); + refreshToolbarState(); + } + + @Override + public void onEndSelectMode() { + speedDialView.close(); + speedDialView.setVisibility(View.GONE); + swipeActions.attachTo(recyclerView); + } + private void updateUi() { loadItems(); updateSyncProgressBarVisibility(); @@ -433,11 +491,14 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem if (adapter == null) { recyclerView.setAdapter(null); adapter = new FeedItemListAdapter((MainActivity) getActivity()); + adapter.setOnSelectModeListener(this); recyclerView.setAdapter(adapter); + swipeActions = new SwipeActions(this, TAG).attachTo(recyclerView); } progressBar.setVisibility(View.GONE); if (feed != null) { adapter.updateItems(feed.getItems()); + swipeActions.setFilter(feed.getItemFilter()); } refreshToolbarState(); @@ -551,7 +612,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem if (disposable != null) { disposable.dispose(); } - progressBar.setVisibility(View.VISIBLE); disposable = Observable.fromCallable(this::loadData) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -592,5 +652,13 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem protected void beforeBindViewHolder(EpisodeItemViewHolder holder, int pos) { holder.coverHolder.setVisibility(View.GONE); } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + if (!inActionMode()) { + menu.findItem(R.id.multi_select).setVisible(true); + } + } } -}
\ No newline at end of file +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java index 9e57497bf..dbc7f2ae3 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -1,7 +1,5 @@ package de.danoeh.antennapod.fragment; -import android.content.Context; -import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -16,7 +14,6 @@ import androidx.preference.PreferenceFragmentCompat; import androidx.preference.SwitchPreferenceCompat; import androidx.recyclerview.widget.RecyclerView; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.settings.SkipIntroEndingChangedEvent; import de.danoeh.antennapod.core.event.settings.SpeedPresetChangedEvent; import de.danoeh.antennapod.core.event.settings.VolumeAdaptionChangedEvent; @@ -384,8 +381,6 @@ public class FeedSettingsFragment extends Fragment { feedPreferences.setAutoDownload(checked); DBWriter.setFeedPreferences(feedPreferences); updateAutoDownloadEnabled(); - ApplyToEpisodesDialog dialog = new ApplyToEpisodesDialog(getActivity(), checked); - dialog.createNewDialog().show(); pref.setChecked(checked); return false; }); @@ -417,22 +412,5 @@ public class FeedSettingsFragment extends Fragment { return false; }); } - - private class ApplyToEpisodesDialog extends ConfirmationDialog { - private final boolean autoDownload; - - ApplyToEpisodesDialog(Context context, boolean autoDownload) { - super(context, R.string.auto_download_apply_to_items_title, - R.string.auto_download_apply_to_items_message); - this.autoDownload = autoDownload; - setPositiveText(R.string.yes); - setNegativeText(R.string.no); - } - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - DBWriter.setFeedsItemsAutoDownload(feed, autoDownload); - } - } } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index c83ed4722..5a2061a5f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -349,7 +349,7 @@ public class ItemFragment extends Fragment { if (DownloadRequester.getInstance().isDownloadingFile(media)) { actionButton2 = new CancelDownloadActionButton(item); } else if (!media.isDownloaded()) { - actionButton2 = new DownloadActionButton(item, false); + actionButton2 = new DownloadActionButton(item); } else { actionButton2 = new DeleteActionButton(item); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java index 98ba59980..7acb94ab3 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java @@ -143,43 +143,42 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS } private boolean onFeedContextMenuClicked(Feed feed, MenuItem item) { - switch (item.getItemId()) { - case R.id.remove_all_new_flags_item: - ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getContext(), - R.string.remove_all_new_flags_label, - R.string.remove_all_new_flags_confirmation_msg) { - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - DBWriter.removeFeedNewFlag(feed.getId()); - } - }; - removeAllNewFlagsConfirmationDialog.createNewDialog().show(); - return true; - case R.id.mark_all_read_item: - ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getContext(), - R.string.mark_all_read_label, - R.string.mark_all_read_confirmation_msg) { - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - DBWriter.markFeedRead(feed.getId()); - } - }; - markAllReadConfirmationDialog.createNewDialog().show(); - return true; - case R.id.rename_item: - new RenameFeedDialog(getActivity(), feed).show(); - return true; - case R.id.remove_item: - RemoveFeedDialog.show(getContext(), feed, () -> { - ((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null); - }); - return true; - default: - return super.onContextItemSelected(item); + final int itemId = item.getItemId(); + if (itemId == R.id.remove_all_new_flags_item) { + ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getContext(), + R.string.remove_all_new_flags_label, + R.string.remove_all_new_flags_confirmation_msg) { + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + dialog.dismiss(); + DBWriter.removeFeedNewFlag(feed.getId()); + } + }; + removeAllNewFlagsConfirmationDialog.createNewDialog().show(); + return true; + } else if (itemId == R.id.mark_all_read_item) { + ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getContext(), + R.string.mark_all_read_label, + R.string.mark_all_read_confirmation_msg) { + + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + dialog.dismiss(); + DBWriter.markFeedRead(feed.getId()); + } + }; + markAllReadConfirmationDialog.createNewDialog().show(); + return true; + } else if (itemId == R.id.rename_item) { + new RenameFeedDialog(getActivity(), feed).show(); + return true; + } else if (itemId == R.id.remove_item) { + RemoveFeedDialog.show(getContext(), feed, () -> { + ((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null); + }); + return true; } + return super.onContextItemSelected(item); } @Subscribe(threadMode = ThreadMode.MAIN) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index 84b0e97c1..5e3d36c03 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -172,7 +172,7 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI @Override public boolean onContextItemSelected(@NonNull MenuItem item) { - FeedItem selectedItem = adapter.getSelectedItem(); + FeedItem selectedItem = adapter.getLongPressedItem(); if (selectedItem == null) { Log.i(TAG, "Selected item at current position was null, ignoring selection"); return super.onContextItemSelected(item); 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 80f666fa2..ed99cb2dd 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -24,8 +24,11 @@ import androidx.recyclerview.widget.SimpleItemAnimator; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.google.android.material.snackbar.Snackbar; +import com.leinardi.android.speeddial.SpeedDialView; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; import de.danoeh.antennapod.adapter.QueueRecyclerAdapter; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.DownloadEvent; @@ -35,6 +38,9 @@ import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler; +import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -44,11 +50,10 @@ import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; -import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; import de.danoeh.antennapod.view.EmptyViewHandler; import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; @@ -63,14 +68,11 @@ import org.greenrobot.eventbus.ThreadMode; import java.util.List; import java.util.Locale; -import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_DELETE; -import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_DOWNLOAD; -import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_REMOVE_FROM_QUEUE; - /** * Shows all items in the queue. */ -public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickListener { +public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickListener, + EpisodeItemListAdapter.OnSelectModeListener { public static final String TAG = "QueueFragment"; private static final String KEY_UP_ARROW = "up_arrow"; @@ -90,9 +92,11 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi private static final String PREF_SHOW_LOCK_WARNING = "show_lock_warning"; private Disposable disposable; - private ItemTouchHelper itemTouchHelper; + private SwipeActions swipeActions; private SharedPreferences prefs; + private SpeedDialView speedDialView; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -230,21 +234,27 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi } } - private void resetViewState() { - recyclerAdapter = null; - } - @Override public void onDestroyView() { super.onDestroyView(); - resetViewState(); + if (recyclerAdapter != null) { + recyclerAdapter.endSelectMode(); + } + recyclerAdapter = null; } private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = () -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds(); private void refreshToolbarState() { - MenuItemUtils.refreshLockItem(getActivity(), toolbar.getMenu()); + final MenuItem queueLock = toolbar.getMenu().findItem(R.id.queue_lock); + if (UserPreferences.isQueueLocked()) { + queueLock.setTitle(de.danoeh.antennapod.R.string.unlock_queue); + queueLock.setIcon(R.drawable.ic_lock_open); + } else { + queueLock.setTitle(de.danoeh.antennapod.R.string.lock_queue); + queueLock.setIcon(R.drawable.ic_lock_closed); + } boolean keepSorted = UserPreferences.isQueueKeepSorted(); toolbar.getMenu().findItem(R.id.queue_sort_random).setVisible(!keepSorted); @@ -255,82 +265,79 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi @Override public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case R.id.queue_lock: - toggleQueueLock(); - return true; - case R.id.refresh_item: - AutoUpdateManager.runImmediate(requireContext()); - return true; - case R.id.clear_queue: - // make sure the user really wants to clear the queue - ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), - R.string.clear_queue_label, - R.string.clear_queue_confirmation_msg) { - - @Override - public void onConfirmButtonPressed( - DialogInterface dialog) { - dialog.dismiss(); - DBWriter.clearQueue(); - } - }; - conDialog.createNewDialog().show(); - return true; - case R.id.episode_actions: - ((MainActivity) requireActivity()).loadChildFragment( - EpisodesApplyActionFragment.newInstance(queue, - ACTION_DELETE | ACTION_REMOVE_FROM_QUEUE | ACTION_DOWNLOAD)); - return true; - case R.id.queue_sort_episode_title_asc: - setSortOrder(SortOrder.EPISODE_TITLE_A_Z); - return true; - case R.id.queue_sort_episode_title_desc: - setSortOrder(SortOrder.EPISODE_TITLE_Z_A); - return true; - case R.id.queue_sort_date_asc: - setSortOrder(SortOrder.DATE_OLD_NEW); - return true; - case R.id.queue_sort_date_desc: - setSortOrder(SortOrder.DATE_NEW_OLD); - return true; - case R.id.queue_sort_duration_asc: - setSortOrder(SortOrder.DURATION_SHORT_LONG); - return true; - case R.id.queue_sort_duration_desc: - setSortOrder(SortOrder.DURATION_LONG_SHORT); - return true; - case R.id.queue_sort_feed_title_asc: - setSortOrder(SortOrder.FEED_TITLE_A_Z); - return true; - case R.id.queue_sort_feed_title_desc: - setSortOrder(SortOrder.FEED_TITLE_Z_A); - return true; - case R.id.queue_sort_random: - setSortOrder(SortOrder.RANDOM); - return true; - case R.id.queue_sort_smart_shuffle_asc: - setSortOrder(SortOrder.SMART_SHUFFLE_OLD_NEW); - return true; - case R.id.queue_sort_smart_shuffle_desc: - setSortOrder(SortOrder.SMART_SHUFFLE_NEW_OLD); - return true; - case R.id.queue_keep_sorted: - boolean keepSortedOld = UserPreferences.isQueueKeepSorted(); - boolean keepSortedNew = !keepSortedOld; - UserPreferences.setQueueKeepSorted(keepSortedNew); - if (keepSortedNew) { - SortOrder sortOrder = UserPreferences.getQueueKeepSortedOrder(); - DBWriter.reorderQueue(sortOrder, true); - } - if (recyclerAdapter != null) { - recyclerAdapter.updateDragDropEnabled(); + final int itemId = item.getItemId(); + if (itemId == R.id.queue_lock) { + toggleQueueLock(); + return true; + } else if (itemId == R.id.refresh_item) { + AutoUpdateManager.runImmediate(requireContext()); + return true; + } else if (itemId == R.id.clear_queue) { + // make sure the user really wants to clear the queue + ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), + R.string.clear_queue_label, + R.string.clear_queue_confirmation_msg) { + + @Override + public void onConfirmButtonPressed( + DialogInterface dialog) { + dialog.dismiss(); + DBWriter.clearQueue(); } - refreshToolbarState(); - return true; - default: - return false; + }; + conDialog.createNewDialog().show(); + return true; + } else if (itemId == R.id.queue_sort_episode_title_asc) { + setSortOrder(SortOrder.EPISODE_TITLE_A_Z); + return true; + } else if (itemId == R.id.queue_sort_episode_title_desc) { + setSortOrder(SortOrder.EPISODE_TITLE_Z_A); + return true; + } else if (itemId == R.id.queue_sort_date_asc) { + setSortOrder(SortOrder.DATE_OLD_NEW); + return true; + } else if (itemId == R.id.queue_sort_date_desc) { + setSortOrder(SortOrder.DATE_NEW_OLD); + return true; + } else if (itemId == R.id.queue_sort_duration_asc) { + setSortOrder(SortOrder.DURATION_SHORT_LONG); + return true; + } else if (itemId == R.id.queue_sort_duration_desc) { + setSortOrder(SortOrder.DURATION_LONG_SHORT); + return true; + } else if (itemId == R.id.queue_sort_feed_title_asc) { + setSortOrder(SortOrder.FEED_TITLE_A_Z); + return true; + } else if (itemId == R.id.queue_sort_feed_title_desc) { + setSortOrder(SortOrder.FEED_TITLE_Z_A); + return true; + } else if (itemId == R.id.queue_sort_random) { + setSortOrder(SortOrder.RANDOM); + return true; + } else if (itemId == R.id.queue_sort_smart_shuffle_asc) { + setSortOrder(SortOrder.SMART_SHUFFLE_OLD_NEW); + return true; + } else if (itemId == R.id.queue_sort_smart_shuffle_desc) { + setSortOrder(SortOrder.SMART_SHUFFLE_NEW_OLD); + return true; + } else if (itemId == R.id.queue_keep_sorted) { + boolean keepSortedOld = UserPreferences.isQueueKeepSorted(); + boolean keepSortedNew = !keepSortedOld; + UserPreferences.setQueueKeepSorted(keepSortedNew); + if (keepSortedNew) { + SortOrder sortOrder = UserPreferences.getQueueKeepSortedOrder(); + DBWriter.reorderQueue(sortOrder, true); + } + if (recyclerAdapter != null) { + recyclerAdapter.updateDragDropEnabled(); + } + refreshToolbarState(); + return true; + } else if (itemId == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); + return true; } + return false; } private void toggleQueueLock() { @@ -391,7 +398,7 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi if (!isVisible() || recyclerAdapter == null) { return false; } - FeedItem selectedItem = recyclerAdapter.getSelectedItem(); + FeedItem selectedItem = recyclerAdapter.getLongPressedItem(); if (selectedItem == null) { Log.i(TAG, "Selected item was null, ignoring selection"); return super.onContextItemSelected(item); @@ -402,24 +409,25 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi Log.i(TAG, "Selected item no longer exist, ignoring selection"); return super.onContextItemSelected(item); } + if (recyclerAdapter.onContextItemSelected(item)) { + return true; + } - switch(item.getItemId()) { - case R.id.move_to_top_item: - queue.add(0, queue.remove(position)); - recyclerAdapter.notifyItemMoved(position, 0); - DBWriter.moveQueueItemToTop(selectedItem.getId(), true); - return true; - case R.id.move_to_bottom_item: - queue.add(queue.size()-1, queue.remove(position)); - recyclerAdapter.notifyItemMoved(position, queue.size()-1); - DBWriter.moveQueueItemToBottom(selectedItem.getId(), true); - return true; - default: - return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); + final int itemId = item.getItemId(); + if (itemId == R.id.move_to_top_item) { + queue.add(0, queue.remove(position)); + recyclerAdapter.notifyItemMoved(position, 0); + DBWriter.moveQueueItemToTop(selectedItem.getId(), true); + return true; + } else if (itemId == R.id.move_to_bottom_item) { + queue.add(queue.size() - 1, queue.remove(position)); + recyclerAdapter.notifyItemMoved(position, queue.size() - 1); + DBWriter.moveQueueItemToBottom(selectedItem.getId(), true); + return true; } + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); @@ -432,7 +440,6 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi } ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); toolbar.inflateMenu(R.menu.queue); - MenuItemUtils.setupSearchItem(toolbar.getMenu(), (MainActivity) getActivity(), 0, ""); refreshToolbarState(); infoBar = root.findViewById(R.id.info_bar); @@ -445,89 +452,16 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi registerForContextMenu(recyclerView); SwipeRefreshLayout swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); + swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); swipeRefreshLayout.setOnRefreshListener(() -> { AutoUpdateManager.runImmediate(requireContext()); new Handler(Looper.getMainLooper()).postDelayed(() -> swipeRefreshLayout.setRefreshing(false), getResources().getInteger(R.integer.swipe_to_refresh_duration_in_ms)); }); - itemTouchHelper = new ItemTouchHelper( - new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, - ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { - - // Position tracking whilst dragging - int dragFrom = -1; - int dragTo = -1; - - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, - RecyclerView.ViewHolder target) { - int fromPosition = viewHolder.getAdapterPosition(); - int toPosition = target.getAdapterPosition(); - - // Update tracked position - if (dragFrom == -1) { - dragFrom = fromPosition; - } - dragTo = toPosition; - - int from = viewHolder.getAdapterPosition(); - int to = target.getAdapterPosition(); - Log.d(TAG, "move(" + from + ", " + to + ") in memory"); - if (from >= queue.size() || to >= queue.size() || from < 0 || to < 0) { - return false; - } - queue.add(to, queue.remove(from)); - recyclerAdapter.notifyItemMoved(from, to); - return true; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { - if (disposable != null) { - disposable.dispose(); - } - final int position = viewHolder.getAdapterPosition(); - Log.d(TAG, "remove(" + position + ")"); - final FeedItem item = queue.get(position); - DBWriter.removeQueueItem(getActivity(), true, item); - - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - getResources().getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1), - Snackbar.LENGTH_LONG) - .setAction(getString(R.string.undo), v -> - DBWriter.addQueueItemAt(getActivity(), item.getId(), position, false)); - } - - @Override - public boolean isLongPressDragEnabled() { - return false; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return !UserPreferences.isQueueLocked(); - } - - @Override - public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - // Check if drag finished - if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) { - reallyMoved(dragFrom, dragTo); - } - - dragFrom = dragTo = -1; - } - - private void reallyMoved(int from, int to) { - // Write drag operation to database - Log.d(TAG, "Write to database move(" + from + ", " + to + ")"); - DBWriter.moveQueueItem(from, to, true); - } - } - ); - itemTouchHelper.attachToRecyclerView(recyclerView); + swipeActions = new QueueSwipeActions(); + swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.QUEUED)); + swipeActions.attachTo(recyclerView); emptyView = new EmptyViewHandler(getContext()); emptyView.attachToRecyclerView(recyclerView); @@ -538,6 +472,32 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi progLoading = root.findViewById(R.id.progLoading); progLoading.setVisibility(View.VISIBLE); + speedDialView = root.findViewById(R.id.fabSD); + speedDialView.inflate(R.menu.episodes_apply_action_speeddial); + speedDialView.removeActionItemById(R.id.mark_read_batch); + speedDialView.removeActionItemById(R.id.mark_unread_batch); + speedDialView.removeActionItemById(R.id.add_to_queue_batch); + speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() { + @Override + public boolean onMainActionSelected() { + return false; + } + + @Override + public void onToggleChanged(boolean open) { + if (open && recyclerAdapter.getSelectedCount() == 0) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, + Snackbar.LENGTH_SHORT); + speedDialView.close(); + } + } + }); + speedDialView.setOnActionSelectedListener(actionItem -> { + new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), recyclerAdapter.getSelectedItems()) + .handleAction(actionItem.getId()); + recyclerAdapter.endSelectMode(); + return true; + }); return root; } @@ -548,10 +508,11 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi } private void onFragmentLoaded(final boolean restoreScrollPosition) { - if (queue != null && queue.size() > 0) { + if (queue != null) { if (recyclerAdapter == null) { MainActivity activity = (MainActivity) getActivity(); - recyclerAdapter = new QueueRecyclerAdapter(activity, itemTouchHelper); + recyclerAdapter = new QueueRecyclerAdapter(activity, swipeActions); + recyclerAdapter.setOnSelectModeListener(this); recyclerView.setAdapter(recyclerAdapter); emptyView.updateAdapter(recyclerAdapter); } @@ -615,4 +576,92 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi } }, error -> Log.e(TAG, Log.getStackTraceString(error))); } + + @Override + public void onStartSelectMode() { + swipeActions.detach(); + speedDialView.setVisibility(View.VISIBLE); + refreshToolbarState(); + infoBar.setVisibility(View.GONE); + } + + @Override + public void onEndSelectMode() { + speedDialView.close(); + speedDialView.setVisibility(View.GONE); + infoBar.setVisibility(View.VISIBLE); + swipeActions.attachTo(recyclerView); + } + + private class QueueSwipeActions extends SwipeActions { + + // Position tracking whilst dragging + int dragFrom = -1; + int dragTo = -1; + + public QueueSwipeActions() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, QueueFragment.this, TAG); + } + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { + int fromPosition = viewHolder.getBindingAdapterPosition(); + int toPosition = target.getBindingAdapterPosition(); + + // Update tracked position + if (dragFrom == -1) { + dragFrom = fromPosition; + } + dragTo = toPosition; + + int from = viewHolder.getBindingAdapterPosition(); + int to = target.getBindingAdapterPosition(); + Log.d(TAG, "move(" + from + ", " + to + ") in memory"); + if (from >= queue.size() || to >= queue.size() || from < 0 || to < 0) { + return false; + } + queue.add(to, queue.remove(from)); + recyclerAdapter.notifyItemMoved(from, to); + return true; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + if (disposable != null) { + disposable.dispose(); + } + + //SwipeActions + super.onSwiped(viewHolder, direction); + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return !UserPreferences.isQueueLocked(); + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + // Check if drag finished + if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) { + reallyMoved(dragFrom, dragTo); + } + + dragFrom = dragTo = -1; + } + + private void reallyMoved(int from, int to) { + // Write drag operation to database + Log.d(TAG, "Write to database move(" + from + ", " + to + ")"); + DBWriter.moveQueueItem(from, to, true); + } + + } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java index 0394b5987..f8326d9c1 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java @@ -1,6 +1,8 @@ package de.danoeh.antennapod.fragment; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.util.Pair; import android.view.LayoutInflater; @@ -55,6 +57,7 @@ public class SearchFragment extends Fragment { private static final String ARG_QUERY = "query"; private static final String ARG_FEED = "feed"; private static final String ARG_FEED_NAME = "feedName"; + private static final int SEARCH_DEBOUNCE_INTERVAL = 1500; private EpisodeItemListAdapter adapter; private FeedSearchResultAdapter adapterFeeds; @@ -64,27 +67,35 @@ public class SearchFragment extends Fragment { private EpisodeItemListRecyclerView recyclerView; private List<FeedItem> results; private Chip chip; + private SearchView searchView; + private Handler automaticSearchDebouncer; + private long lastQueryChange = 0; /** * Create a new SearchFragment that searches all feeds. */ - public static SearchFragment newInstance(String query) { - if (query == null) { - query = ""; - } + public static SearchFragment newInstance() { SearchFragment fragment = new SearchFragment(); Bundle args = new Bundle(); - args.putString(ARG_QUERY, query); args.putLong(ARG_FEED, 0); fragment.setArguments(args); return fragment; } /** + * Create a new SearchFragment that searches all feeds with pre-defined query. + */ + public static SearchFragment newInstance(String query) { + SearchFragment fragment = newInstance(); + fragment.getArguments().putString(ARG_QUERY, query); + return fragment; + } + + /** * Create a new SearchFragment that searches one specific feed. */ - public static SearchFragment newInstance(String query, long feed, String feedTitle) { - SearchFragment fragment = newInstance(query); + public static SearchFragment newInstance(long feed, String feedTitle) { + SearchFragment fragment = newInstance(); fragment.getArguments().putLong(ARG_FEED, feed); fragment.getArguments().putString(ARG_FEED_NAME, feedTitle); return fragment; @@ -94,12 +105,7 @@ public class SearchFragment extends Fragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); - } - - @Override - public void onStart() { - super.onStart(); - search(); + automaticSearchDebouncer = new Handler(Looper.getMainLooper()); } @Override @@ -134,13 +140,19 @@ public class SearchFragment extends Fragment { emptyViewHandler.attachToRecyclerView(recyclerView); emptyViewHandler.setIcon(R.drawable.ic_search); emptyViewHandler.setTitle(R.string.search_status_no_results); + emptyViewHandler.setMessage(R.string.type_to_search); EventBus.getDefault().register(this); chip = layout.findViewById(R.id.feed_title_chip); chip.setOnCloseIconClickListener(v -> { getArguments().putLong(ARG_FEED, 0); - search(); + searchWithProgressBar(); }); + chip.setVisibility((getArguments().getLong(ARG_FEED, 0) == 0) ? View.GONE : View.VISIBLE); + chip.setText(getArguments().getString(ARG_FEED_NAME, "")); + if (getArguments().getString(ARG_QUERY, null) != null) { + search(); + } return layout; } @@ -157,21 +169,30 @@ public class SearchFragment extends Fragment { MenuItem item = toolbar.getMenu().findItem(R.id.action_search); item.expandActionView(); - final SearchView sv = (SearchView) item.getActionView(); - sv.setQueryHint(getString(R.string.search_label)); - sv.clearFocus(); - sv.setQuery(getArguments().getString(ARG_QUERY), false); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + searchView = (SearchView) item.getActionView(); + searchView.setQueryHint(getString(R.string.search_label)); + searchView.requestFocus(); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - getArguments().putString(ARG_QUERY, s); - search(); + searchView.clearFocus(); + searchWithProgressBar(); return true; } @Override public boolean onQueryTextChange(String s) { + automaticSearchDebouncer.removeCallbacksAndMessages(null); + if (s.isEmpty() || s.endsWith(" ") || (lastQueryChange != 0 + && System.currentTimeMillis() > lastQueryChange + SEARCH_DEBOUNCE_INTERVAL)) { + search(); + } else { + automaticSearchDebouncer.postDelayed(() -> { + search(); + lastQueryChange = 0; // Don't search instantly with first symbol after some pause + }, SEARCH_DEBOUNCE_INTERVAL / 2); + } + lastQueryChange = System.currentTimeMillis(); return false; } }); @@ -191,7 +212,7 @@ public class SearchFragment extends Fragment { @Override public boolean onContextItemSelected(@NonNull MenuItem item) { - FeedItem selectedItem = adapter.getSelectedItem(); + FeedItem selectedItem = adapter.getLongPressedItem(); if (selectedItem == null) { Log.i(TAG, "Selected item at current position was null, ignoring selection"); return super.onContextItemSelected(item); @@ -256,12 +277,17 @@ public class SearchFragment extends Fragment { search(); } + private void searchWithProgressBar() { + progressBar.setVisibility(View.VISIBLE); + emptyViewHandler.hide(); + search(); + } + private void search() { if (disposable != null) { disposable.dispose(); } - progressBar.setVisibility(View.VISIBLE); - emptyViewHandler.hide(); + chip.setVisibility((getArguments().getLong(ARG_FEED, 0) == 0) ? View.GONE : View.VISIBLE); disposable = Observable.fromCallable(this::performSearch) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -271,19 +297,24 @@ public class SearchFragment extends Fragment { adapter.updateItems(results.first); if (getArguments().getLong(ARG_FEED, 0) == 0) { adapterFeeds.updateData(results.second); - chip.setVisibility(View.GONE); } else { adapterFeeds.updateData(Collections.emptyList()); - chip.setText(getArguments().getString(ARG_FEED_NAME, "")); } - String query = getArguments().getString(ARG_QUERY); - emptyViewHandler.setMessage(getString(R.string.no_results_for_query, query)); + + if (searchView.getQuery().toString().isEmpty()) { + emptyViewHandler.setMessage(R.string.type_to_search); + } else { + emptyViewHandler.setMessage(getString(R.string.no_results_for_query, searchView.getQuery())); + } }, error -> Log.e(TAG, Log.getStackTraceString(error))); } @NonNull private Pair<List<FeedItem>, List<Feed>> performSearch() { - String query = getArguments().getString(ARG_QUERY); + String query = searchView.getQuery().toString(); + if (query.isEmpty()) { + return new Pair<>(Collections.emptyList(), Collections.emptyList()); + } long feed = getArguments().getLong(ARG_FEED); List<FeedItem> items = FeedSearcher.searchFeedItems(getContext(), query, feed); List<Feed> feeds = FeedSearcher.searchFeeds(getContext(), query); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java index 367eb4aaf..d1d114a50 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -39,6 +39,7 @@ import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.FeedListUpdateEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadService; @@ -51,7 +52,6 @@ import de.danoeh.antennapod.dialog.RemoveFeedDialog; import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; import de.danoeh.antennapod.dialog.FeedSortDialog; import de.danoeh.antennapod.dialog.RenameFeedDialog; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -148,6 +148,7 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem feedsFilteredMsg.setOnClickListener((l) -> SubscriptionsFilterDialog.showDialog(requireContext())); SwipeRefreshLayout swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); + swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); swipeRefreshLayout.setOnRefreshListener(() -> { AutoUpdateManager.runImmediate(requireContext()); new Handler(Looper.getMainLooper()).postDelayed(() -> swipeRefreshLayout.setRefreshing(false), @@ -172,31 +173,33 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem @Override public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case R.id.refresh_item: - AutoUpdateManager.runImmediate(requireContext()); - return true; - case R.id.subscriptions_filter: - SubscriptionsFilterDialog.showDialog(requireContext()); - return true; - case R.id.subscriptions_sort: - FeedSortDialog.showDialog(requireContext()); - return true; - case R.id.subscription_num_columns_2: - setColumnNumber(2); - return true; - case R.id.subscription_num_columns_3: - setColumnNumber(3); - return true; - case R.id.subscription_num_columns_4: - setColumnNumber(4); - return true; - case R.id.subscription_num_columns_5: - setColumnNumber(5); - return true; - default: - return false; + final int itemId = item.getItemId(); + if (itemId == R.id.refresh_item) { + AutoUpdateManager.runImmediate(requireContext()); + return true; + } else if (itemId == R.id.subscriptions_filter) { + SubscriptionsFilterDialog.showDialog(requireContext()); + return true; + } else if (itemId == R.id.subscriptions_sort) { + FeedSortDialog.showDialog(requireContext()); + return true; + } else if (itemId == R.id.subscription_num_columns_2) { + setColumnNumber(2); + return true; + } else if (itemId == R.id.subscription_num_columns_3) { + setColumnNumber(3); + return true; + } else if (itemId == R.id.subscription_num_columns_4) { + setColumnNumber(4); + return true; + } else if (itemId == R.id.subscription_num_columns_5) { + setColumnNumber(5); + return true; + } else if (itemId == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); + return true; } + return false; } private void setColumnNumber(int columns) { @@ -315,28 +318,27 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem Feed feed = selectedFeed; selectedFeed = null; - switch (item.getItemId()) { - case R.id.remove_all_new_flags_item: - displayConfirmationDialog( - R.string.remove_all_new_flags_label, - R.string.remove_all_new_flags_confirmation_msg, - () -> DBWriter.removeFeedNewFlag(feed.getId())); - return true; - case R.id.mark_all_read_item: - displayConfirmationDialog( - R.string.mark_all_read_label, - R.string.mark_all_read_confirmation_msg, - () -> DBWriter.markFeedRead(feed.getId())); - return true; - case R.id.rename_item: - new RenameFeedDialog(getActivity(), feed).show(); - return true; - case R.id.remove_item: - RemoveFeedDialog.show(getContext(), feed, null); - return true; - default: - return super.onContextItemSelected(item); + final int itemId = item.getItemId(); + if (itemId == R.id.remove_all_new_flags_item) { + displayConfirmationDialog( + R.string.remove_all_new_flags_label, + R.string.remove_all_new_flags_confirmation_msg, + () -> DBWriter.removeFeedNewFlag(feed.getId())); + return true; + } else if (itemId == R.id.mark_all_read_item) { + displayConfirmationDialog( + R.string.mark_all_read_label, + R.string.mark_all_read_confirmation_msg, + () -> DBWriter.markFeedRead(feed.getId())); + return true; + } else if (itemId == R.id.rename_item) { + new RenameFeedDialog(getActivity(), feed).show(); + return true; + } else if (itemId == R.id.remove_item) { + RemoveFeedDialog.show(getContext(), feed, null); + return true; } + return super.onContextItemSelected(item); } private <T> void displayConfirmationDialog(@StringRes int title, @StringRes int message, Callable<? extends T> task) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java new file mode 100644 index 000000000..028d2fff4 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java @@ -0,0 +1,130 @@ +package de.danoeh.antennapod.fragment.actions; + +import android.util.Log; + +import androidx.annotation.PluralsRes; + +import com.google.android.material.snackbar.Snackbar; + +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.model.feed.FeedItem; + +public class EpisodeMultiSelectActionHandler { + private static final String TAG = "EpisodeSelectHandler"; + private final MainActivity activity; + private final List<FeedItem> selectedItems; + + public EpisodeMultiSelectActionHandler(MainActivity activity, List<FeedItem> selectedItems) { + this.activity = activity; + this.selectedItems = selectedItems; + } + + public void handleAction(int id) { + if (id == R.id.add_to_queue_batch) { + queueChecked(); + } else if (id == R.id.remove_from_queue_batch) { + removeFromQueueChecked(); + } else if (id == R.id.mark_read_batch) { + markedCheckedPlayed(); + } else if (id == R.id.mark_unread_batch) { + markedCheckedUnplayed(); + } else if (id == R.id.download_batch) { + downloadChecked(); + } else if (id == R.id.delete_batch) { + deleteChecked(); + } else { + Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + id); + } + } + + private void queueChecked() { + // Check if an episode actually contains any media files before adding it to queue + LongList toQueue = new LongList(selectedItems.size()); + for (FeedItem episode : selectedItems) { + if (episode.hasMedia()) { + toQueue.add(episode.getId()); + } + } + DBWriter.addQueueItem(activity, true, toQueue.toArray()); + showMessage(R.plurals.added_to_queue_batch_label, toQueue.size()); + } + + private void removeFromQueueChecked() { + long[] checkedIds = getSelectedIds(); + DBWriter.removeQueueItem(activity, true, checkedIds); + showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.length); + } + + private void markedCheckedPlayed() { + long[] checkedIds = getSelectedIds(); + DBWriter.markItemPlayed(FeedItem.PLAYED, checkedIds); + showMessage(R.plurals.marked_read_batch_label, checkedIds.length); + } + + private void markedCheckedUnplayed() { + long[] checkedIds = getSelectedIds(); + DBWriter.markItemPlayed(FeedItem.UNPLAYED, checkedIds); + showMessage(R.plurals.marked_unread_batch_label, checkedIds.length); + } + + private void downloadChecked() { + // download the check episodes in the same order as they are currently displayed + List<FeedItem> toDownload = new ArrayList<>(selectedItems.size()); + for (FeedItem episode : selectedItems) { + if (episode.hasMedia() && !episode.getFeed().isLocalFeed()) { + toDownload.add(episode); + } + } + try { + DownloadRequester.getInstance().downloadMedia(activity, true, toDownload.toArray(new FeedItem[0])); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(activity, e.getMessage()); + } + showMessage(R.plurals.downloading_batch_label, toDownload.size()); + } + + private void deleteChecked() { + int countHasMedia = 0; + int countNoMedia = 0; + for (FeedItem feedItem : selectedItems) { + if (feedItem.hasMedia() && feedItem.getMedia().isDownloaded()) { + countHasMedia++; + DBWriter.deleteFeedMediaOfItem(activity, feedItem.getMedia().getId()); + } else { + countNoMedia++; + } + } + showMessageMore(R.plurals.deleted_multi_episode_batch_label, countNoMedia, countHasMedia); + } + + private void showMessage(@PluralsRes int msgId, int numItems) { + activity.showSnackbarAbovePlayer(activity.getResources() + .getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG); + } + + private void showMessageMore(@PluralsRes int msgId, int countNoMedia, int countHasMedia) { + activity.showSnackbarAbovePlayer(activity.getResources() + .getQuantityString(msgId, + (countHasMedia + countNoMedia), + (countHasMedia + countNoMedia), countHasMedia), + Snackbar.LENGTH_LONG); + } + + private long[] getSelectedIds() { + long[] checkedIds = new long[selectedItems.size()]; + for (int i = 0; i < selectedItems.size(); ++i) { + checkedIds[i] = selectedItems.get(i).getId(); + } + return checkedIds; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java index baf4c7c57..cc09acbca 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java @@ -148,5 +148,8 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_notifications)); config.index(R.xml.feed_settings) .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.feed_settings)); + config.index(R.xml.preferences_swipe) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_user_interface)) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_swipe)); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java index 42b0cb96f..e5617b8ea 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.fragment.preferences; import android.os.Bundle; -import androidx.appcompat.app.AlertDialog; import androidx.preference.PreferenceFragmentCompat; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.PreferenceActivity; @@ -43,22 +42,6 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { return true; } ); - findPreference(UserPreferences.PREF_IMAGE_CACHE_SIZE).setOnPreferenceChangeListener( - (preference, o) -> { - if (o instanceof String) { - int newValue = Integer.parseInt((String) o) * 1024 * 1024; - if (newValue != UserPreferences.getImageCacheSize()) { - AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity()); - dialog.setTitle(android.R.string.dialog_alert_title); - dialog.setMessage(R.string.pref_restart_required); - dialog.setPositiveButton(android.R.string.ok, null); - dialog.show(); - } - return true; - } - return false; - } - ); findPreference(PREF_IMPORT_EXPORT).setOnPreferenceClickListener( preference -> { ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_import_export); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java new file mode 100644 index 000000000..3d9709f74 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java @@ -0,0 +1,37 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.os.Bundle; +import androidx.preference.PreferenceFragmentCompat; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.PreferenceActivity; +import de.danoeh.antennapod.dialog.SwipeActionsDialog; +import de.danoeh.antennapod.fragment.FeedItemlistFragment; +import de.danoeh.antennapod.fragment.QueueFragment; + +public class SwipePreferencesFragment extends PreferenceFragmentCompat { + private static final String PREF_SWIPE_FEED = "prefSwipeFeed"; + private static final String PREF_SWIPE_QUEUE = "prefSwipeQueue"; + //private static final String PREF_SWIPE_INBOX = "prefSwipeInbox"; + //private static final String PREF_SWIPE_EPISODES = "prefSwipeEpisodes"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_swipe); + + findPreference(PREF_SWIPE_FEED).setOnPreferenceClickListener(preference -> { + new SwipeActionsDialog(requireContext(), FeedItemlistFragment.TAG).show(() -> { }); + return true; + }); + findPreference(PREF_SWIPE_QUEUE).setOnPreferenceClickListener(preference -> { + new SwipeActionsDialog(requireContext(), QueueFragment.TAG).show(() -> { }); + return true; + }); + } + + @Override + public void onStart() { + super.onStart(); + ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.swipeactions_label); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java index 4d1b79965..7c79d0962 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java @@ -21,6 +21,7 @@ import org.greenrobot.eventbus.EventBus; import java.util.List; public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat { + private static final String PREF_SWIPE = "prefSwipe"; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -98,6 +99,11 @@ public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat { FeedSortDialog.showDialog(requireContext()); return true; })); + findPreference(PREF_SWIPE) + .setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_swipe); + return true; + }); if (Build.VERSION.SDK_INT >= 26) { findPreference(UserPreferences.PREF_EXPANDED_NOTIFICATION).setVisible(false); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java new file mode 100644 index 000000000..514ba9764 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.fragment.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class AddToQueueSwipeAction implements SwipeAction { + + @Override + public String getId() { + return ADD_TO_QUEUE; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_playlist; + } + + @Override + public int getActionColor() { + return R.attr.colorAccent; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.add_to_queue_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + if (!item.isTagged(FeedItem.TAG_QUEUE)) { + DBWriter.addQueueItem(fragment.requireContext(), item); + } else { + new RemoveFromQueueSwipeAction().performAction(item, fragment, filter); + } + } + + @Override + public boolean willRemove(FeedItemFilter filter) { + return filter.showQueued || filter.showNew; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java new file mode 100644 index 000000000..2458657a0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java @@ -0,0 +1,43 @@ +package de.danoeh.antennapod.fragment.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class MarkFavoriteSwipeAction implements SwipeAction { + + @Override + public String getId() { + return MARK_FAV; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_star; + } + + @Override + public int getActionColor() { + return R.attr.icon_yellow; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.add_to_favorite_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + DBWriter.toggleFavoriteItem(item); + } + + @Override + public boolean willRemove(FeedItemFilter filter) { + return filter.showIsFavorite || filter.showNotFavorite; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkPlayedSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkPlayedSwipeAction.java new file mode 100644 index 000000000..b820d8a65 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkPlayedSwipeAction.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.fragment.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class MarkPlayedSwipeAction implements SwipeAction { + + @Override + public String getId() { + return MARK_PLAYED; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_mark_played; + } + + @Override + public int getActionColor() { + return R.attr.icon_gray; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.mark_read_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + int togglePlayState = + item.getPlayState() != FeedItem.PLAYED ? FeedItem.PLAYED : FeedItem.UNPLAYED; + FeedItemMenuHandler.markReadWithUndo(fragment, + item, togglePlayState, willRemove(filter)); + } + + @Override + public boolean willRemove(FeedItemFilter filter) { + return filter.showUnplayed || filter.showPlayed; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromInboxSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromInboxSwipeAction.java new file mode 100644 index 000000000..9852269fb --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromInboxSwipeAction.java @@ -0,0 +1,44 @@ +package de.danoeh.antennapod.fragment.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class RemoveFromInboxSwipeAction implements SwipeAction { + + @Override + public String getId() { + return REMOVE_FROM_INBOX; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_check; + } + + @Override + public int getActionColor() { + return R.attr.icon_purple; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.remove_new_flag_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + FeedItemMenuHandler.markReadWithUndo(fragment, + item, FeedItem.UNPLAYED, willRemove(filter)); + } + + @Override + public boolean willRemove(FeedItemFilter filter) { + return filter.showUnplayed; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java new file mode 100644 index 000000000..87cf97f56 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.fragment.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import com.google.android.material.snackbar.Snackbar; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class RemoveFromQueueSwipeAction implements SwipeAction { + + @Override + public String getId() { + return REMOVE_FROM_QUEUE; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_playlist_remove; + } + + @Override + public int getActionColor() { + return R.attr.colorAccent; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.remove_from_queue_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + int position = DBReader.getQueueIDList().indexOf(item.getId()); + + DBWriter.removeQueueItem(fragment.requireActivity(), true, item); + + if (willRemove(filter)) { + ((MainActivity) fragment.requireActivity()).showSnackbarAbovePlayer( + fragment.getResources().getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1), + Snackbar.LENGTH_LONG) + .setAction(fragment.getString(R.string.undo), v -> + DBWriter.addQueueItemAt(fragment.requireActivity(), item.getId(), position, false)); + } + } + + @Override + public boolean willRemove(FeedItemFilter filter) { + return filter.showQueued || filter.showNotQueued; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/ShowFirstSwipeDialogAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/ShowFirstSwipeDialogAction.java new file mode 100644 index 000000000..7d626134d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/ShowFirstSwipeDialogAction.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.fragment.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class ShowFirstSwipeDialogAction implements SwipeAction { + + @Override + public String getId() { + return "SHOW_FIRST_SWIPE_DIALOG"; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_settings; + } + + @Override + public int getActionColor() { + return R.attr.icon_gray; + } + + @Override + public String getTitle(Context context) { + return ""; + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + //handled in SwipeActions + } + + @Override + public boolean willRemove(FeedItemFilter filter) { + return false; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/StartDownloadSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/StartDownloadSwipeAction.java new file mode 100644 index 000000000..2c0110822 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/StartDownloadSwipeAction.java @@ -0,0 +1,44 @@ +package de.danoeh.antennapod.fragment.swipeactions; + +import android.content.Context; +import androidx.fragment.app.Fragment; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.adapter.actionbutton.DownloadActionButton; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class StartDownloadSwipeAction implements SwipeAction { + + @Override + public String getId() { + return START_DOWNLOAD; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_download; + } + + @Override + public int getActionColor() { + return R.attr.icon_green; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.download_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + if (!item.isDownloaded() && !item.getFeed().isLocalFeed()) { + new DownloadActionButton(item) + .onClick(fragment.requireContext()); + } + } + + @Override + public boolean willRemove(FeedItemFilter filter) { + return false; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeAction.java new file mode 100644 index 000000000..e6d002b2b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeAction.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.fragment.swipeactions; + +import android.content.Context; + +import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public interface SwipeAction { + + String ADD_TO_QUEUE = "ADD_TO_QUEUE"; + String REMOVE_FROM_INBOX = "REMOVE_FROM_INBOX"; + String START_DOWNLOAD = "START_DOWNLOAD"; + String MARK_FAV = "MARK_FAV"; + String MARK_PLAYED = "MARK_PLAYED"; + String REMOVE_FROM_QUEUE = "REMOVE_FROM_QUEUE"; + + String getId(); + + String getTitle(Context context); + + @DrawableRes + int getActionIcon(); + + @AttrRes + int getActionColor(); + + void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter); + + boolean willRemove(FeedItemFilter filter); +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java new file mode 100644 index 000000000..50c7c1ae5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java @@ -0,0 +1,258 @@ +package de.danoeh.antennapod.fragment.swipeactions; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Canvas; + +import androidx.annotation.NonNull; +import androidx.core.graphics.ColorUtils; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.annimon.stream.Stream; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.dialog.SwipeActionsDialog; +import de.danoeh.antennapod.fragment.EpisodesFragment; +import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; +import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator; + +public class SwipeActions extends ItemTouchHelper.SimpleCallback implements LifecycleObserver { + public static final String PREF_NAME = "SwipeActionsPrefs"; + public static final String KEY_PREFIX_SWIPEACTIONS = "PrefSwipeActions"; + public static final String KEY_PREFIX_NO_ACTION = "PrefNoSwipeAction"; + + public static final List<SwipeAction> swipeActions = Collections.unmodifiableList( + Arrays.asList(new AddToQueueSwipeAction(), new RemoveFromInboxSwipeAction(), + new StartDownloadSwipeAction(), new MarkFavoriteSwipeAction(), + new MarkPlayedSwipeAction(), new RemoveFromQueueSwipeAction()) + ); + + private final Fragment fragment; + private final String tag; + private FeedItemFilter filter = null; + + Actions actions; + boolean swipeOutEnabled = true; + int swipedOutTo = 0; + private final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(this); + + public SwipeActions(int dragDirs, Fragment fragment, String tag) { + super(dragDirs, ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT); + this.fragment = fragment; + this.tag = tag; + reloadPreference(); + fragment.getLifecycle().addObserver(this); + } + + public SwipeActions(Fragment fragment, String tag) { + this(0, fragment, tag); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + public void reloadPreference() { + actions = getPrefs(fragment.requireContext(), tag); + } + + public void setFilter(FeedItemFilter filter) { + this.filter = filter; + } + + public SwipeActions attachTo(RecyclerView recyclerView) { + itemTouchHelper.attachToRecyclerView(recyclerView); + return this; + } + + public void detach() { + itemTouchHelper.attachToRecyclerView(null); + } + + private static Actions getPrefs(Context context, String tag, String defaultActions) { + SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + String prefsString = prefs.getString(KEY_PREFIX_SWIPEACTIONS + tag, defaultActions); + + return new Actions(prefsString); + } + + private static Actions getPrefs(Context context, String tag) { + return getPrefs(context, tag, ""); + } + + public static Actions getPrefsWithDefaults(Context context, String tag) { + String defaultActions; + switch (tag) { + /*case InboxFragment.TAG: + defaultActions = new int[] {ADD_TO_QUEUE, MARK_UNPLAYED}; + break;*/ + case QueueFragment.TAG: + defaultActions = SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE; + break; + default: + case EpisodesFragment.TAG: + defaultActions = SwipeAction.MARK_FAV + "," + SwipeAction.START_DOWNLOAD; + break; + } + + return getPrefs(context, tag, defaultActions); + } + + public static boolean isSwipeActionEnabled(Context context, String tag) { + SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + return prefs.getBoolean(KEY_PREFIX_NO_ACTION + tag, true); + } + + private boolean isSwipeActionEnabled() { + return isSwipeActionEnabled(fragment.requireContext(), tag); + } + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { + return false; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int swipeDir) { + if (!actions.hasActions()) { + //open settings dialog if no prefs are set + new SwipeActionsDialog(fragment.requireContext(), tag).show(this::reloadPreference); + return; + } + + FeedItem item = ((EpisodeItemViewHolder) viewHolder).getFeedItem(); + + (swipeDir == ItemTouchHelper.RIGHT ? actions.right : actions.left) + .performAction(item, fragment, filter); + } + + @Override + public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + float dx, float dy, int actionState, boolean isCurrentlyActive) { + SwipeAction right; + SwipeAction left; + if (actions.hasActions()) { + right = actions.right; + left = actions.left; + } else { + right = left = new ShowFirstSwipeDialogAction(); + } + + //check if it will be removed + boolean rightWillRemove = right.willRemove(filter); + boolean leftWillRemove = left.willRemove(filter); + boolean wontLeave = (dx > 0 && !rightWillRemove) || (dx < 0 && !leftWillRemove); + + //Limit swipe if it's not removed + int maxMovement = recyclerView.getWidth() * 2 / 5; + float sign = dx > 0 ? 1 : -1; + float limitMovement = Math.min(maxMovement, sign * dx); + float displacementPercentage = limitMovement / maxMovement; + + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && wontLeave) { + swipeOutEnabled = false; + + boolean swipeThresholdReached = displacementPercentage == 1; + + // Move slower when getting near the maxMovement + dx = sign * maxMovement * (float) Math.sin((Math.PI / 2) * displacementPercentage); + + if (isCurrentlyActive) { + int dir = dx > 0 ? ItemTouchHelper.RIGHT : ItemTouchHelper.LEFT; + swipedOutTo = swipeThresholdReached ? dir : 0; + } + } else { + swipeOutEnabled = true; + } + + //add color and icon + Context context = fragment.requireContext(); + int themeColor = ThemeUtils.getColorFromAttr(context, android.R.attr.windowBackground); + int actionColor = ThemeUtils.getColorFromAttr(context, + dx > 0 ? right.getActionColor() : left.getActionColor()); + RecyclerViewSwipeDecorator.Builder builder = new RecyclerViewSwipeDecorator.Builder( + c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive) + .addSwipeRightActionIcon(right.getActionIcon()) + .addSwipeLeftActionIcon(left.getActionIcon()) + .addSwipeRightBackgroundColor(ThemeUtils.getColorFromAttr(context, R.attr.background_elevated)) + .addSwipeLeftBackgroundColor(ThemeUtils.getColorFromAttr(context, R.attr.background_elevated)) + .setActionIconTint( + ColorUtils.blendARGB(themeColor, + actionColor, + Math.max(0.5f, displacementPercentage))); + builder.create().decorate(); + + + super.onChildDraw(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive); + } + + @Override + public float getSwipeEscapeVelocity(float defaultValue) { + return swipeOutEnabled ? defaultValue : Float.MAX_VALUE; + } + + @Override + public float getSwipeVelocityThreshold(float defaultValue) { + return swipeOutEnabled ? defaultValue : 0; + } + + @Override + public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { + return swipeOutEnabled ? 0.6f : 1.0f; + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + + if (swipedOutTo != 0) { + onSwiped(viewHolder, swipedOutTo); + swipedOutTo = 0; + } + } + + @Override + public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + if (!isSwipeActionEnabled()) { + return makeMovementFlags(getDragDirs(recyclerView, viewHolder), 0); + } else { + return super.getMovementFlags(recyclerView, viewHolder); + } + } + + public void startDrag(EpisodeItemViewHolder holder) { + itemTouchHelper.startDrag(holder); + } + + public static class Actions { + public SwipeAction right = null; + public SwipeAction left = null; + + public Actions(String prefs) { + String[] actions = prefs.split(","); + if (actions.length == 2) { + this.right = Stream.of(swipeActions) + .filter(a -> a.getId().equals(actions[0])).single();; + this.left = Stream.of(swipeActions) + .filter(a -> a.getId().equals(actions[1])).single(); + } + } + + public boolean hasActions() { + return right != null && left != null; + } + } +} 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 d478c581d..c272af7d5 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -6,25 +6,26 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; -import com.google.android.material.snackbar.Snackbar; - import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; + +import com.google.android.material.snackbar.Snackbar; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ShareUtils; import de.danoeh.antennapod.dialog.ShareDialog; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; /** * Handles interactions with the FeedItemMenu. @@ -65,14 +66,6 @@ public class FeedItemMenuHandler { setItemVisibility(menu, R.id.mark_unread_item, selectedItem.isPlayed()); setItemVisibility(menu, R.id.reset_position, hasMedia && selectedItem.getMedia().getPosition() != 0); - if (!UserPreferences.isEnableAutodownload() || fileDownloaded || selectedItem.getFeed().isLocalFeed()) { - setItemVisibility(menu, R.id.activate_auto_download, false); - setItemVisibility(menu, R.id.deactivate_auto_download, false); - } else { - setItemVisibility(menu, R.id.activate_auto_download, !selectedItem.getAutoDownload()); - setItemVisibility(menu, R.id.deactivate_auto_download, selectedItem.getAutoDownload()); - } - // Display proper strings when item has no media if (hasMedia) { setItemTitle(menu, R.id.mark_read_item, R.string.mark_read_label); @@ -149,81 +142,60 @@ public class FeedItemMenuHandler { @NonNull FeedItem selectedItem) { @NonNull Context context = fragment.requireContext(); - switch (menuItemId) { - case R.id.skip_episode_item: - IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SKIP_CURRENT_EPISODE); - break; - case R.id.remove_item: - DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); - break; - case R.id.remove_new_flag_item: - removeNewFlagWithUndo(fragment, selectedItem); - break; - case R.id.mark_read_item: - selectedItem.setPlayed(true); - DBWriter.markItemPlayed(selectedItem, FeedItem.PLAYED, true); - if (GpodnetPreferences.loggedIn()) { - FeedMedia media = selectedItem.getMedia(); - // not all items have media, Gpodder only cares about those that do - if (media != null) { - EpisodeAction actionPlay = new EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY) - .currentTimestamp() - .started(media.getDuration() / 1000) - .position(media.getDuration() / 1000) - .total(media.getDuration() / 1000) - .build(); - SyncService.enqueueEpisodeAction(context, actionPlay); - } - } - break; - case R.id.mark_unread_item: - selectedItem.setPlayed(false); - DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, false); - if (GpodnetPreferences.loggedIn() && selectedItem.getMedia() != null) { - EpisodeAction actionNew = new EpisodeAction.Builder(selectedItem, EpisodeAction.NEW) + if (menuItemId == R.id.skip_episode_item) { + IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SKIP_CURRENT_EPISODE); + } else if (menuItemId == R.id.remove_item) { + DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); + } else if (menuItemId == R.id.remove_new_flag_item) { + removeNewFlagWithUndo(fragment, selectedItem); + } else if (menuItemId == R.id.mark_read_item) { + selectedItem.setPlayed(true); + DBWriter.markItemPlayed(selectedItem, FeedItem.PLAYED, true); + if (GpodnetPreferences.loggedIn()) { + FeedMedia media = selectedItem.getMedia(); + // not all items have media, Gpodder only cares about those that do + if (media != null) { + EpisodeAction actionPlay = new EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY) .currentTimestamp() + .started(media.getDuration() / 1000) + .position(media.getDuration() / 1000) + .total(media.getDuration() / 1000) .build(); - SyncService.enqueueEpisodeAction(context, actionNew); - } - break; - case R.id.add_to_queue_item: - DBWriter.addQueueItem(context, selectedItem); - break; - case R.id.remove_from_queue_item: - DBWriter.removeQueueItem(context, true, selectedItem); - break; - case R.id.add_to_favorites_item: - DBWriter.addFavoriteItem(selectedItem); - break; - case R.id.remove_from_favorites_item: - DBWriter.removeFavoriteItem(selectedItem); - break; - case R.id.reset_position: - selectedItem.getMedia().setPosition(0); - if (PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == selectedItem.getMedia().getId()) { - PlaybackPreferences.writeNoMediaPlaying(); - IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE); + SyncService.enqueueEpisodeAction(context, actionPlay); } - DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, true); - break; - case R.id.activate_auto_download: - selectedItem.setAutoDownload(true); - DBWriter.setFeedItemAutoDownload(selectedItem, true); - break; - case R.id.deactivate_auto_download: - selectedItem.setAutoDownload(false); - DBWriter.setFeedItemAutoDownload(selectedItem, false); - break; - case R.id.visit_website_item: - IntentUtils.openInBrowser(context, FeedItemUtil.getLinkWithFallback(selectedItem)); - break; - case R.id.share_item: - ShareDialog shareDialog = ShareDialog.newInstance(selectedItem); - shareDialog.show((fragment.getActivity().getSupportFragmentManager()), "ShareEpisodeDialog"); - break; - default: - Log.d(TAG, "Unknown menuItemId: " + menuItemId); - return false; + } + } else if (menuItemId == R.id.mark_unread_item) { + selectedItem.setPlayed(false); + DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, false); + if (GpodnetPreferences.loggedIn() && selectedItem.getMedia() != null) { + EpisodeAction actionNew = new EpisodeAction.Builder(selectedItem, EpisodeAction.NEW) + .currentTimestamp() + .build(); + SyncService.enqueueEpisodeAction(context, actionNew); + } + } else if (menuItemId == R.id.add_to_queue_item) { + DBWriter.addQueueItem(context, selectedItem); + } else if (menuItemId == R.id.remove_from_queue_item) { + DBWriter.removeQueueItem(context, true, selectedItem); + } else if (menuItemId == R.id.add_to_favorites_item) { + DBWriter.addFavoriteItem(selectedItem); + } else if (menuItemId == R.id.remove_from_favorites_item) { + DBWriter.removeFavoriteItem(selectedItem); + } else if (menuItemId == R.id.reset_position) { + selectedItem.getMedia().setPosition(0); + if (PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == selectedItem.getMedia().getId()) { + PlaybackPreferences.writeNoMediaPlaying(); + IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE); + } + DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, true); + } else if (menuItemId == R.id.visit_website_item) { + IntentUtils.openInBrowser(context, FeedItemUtil.getLinkWithFallback(selectedItem)); + } else if (menuItemId == R.id.share_item) { + ShareDialog shareDialog = ShareDialog.newInstance(selectedItem); + shareDialog.show((fragment.getActivity().getSupportFragmentManager()), "ShareEpisodeDialog"); + } else { + Log.d(TAG, "Unknown menuItemId: " + menuItemId); + return false; } // Refresh menu state @@ -236,15 +208,16 @@ public class FeedItemMenuHandler { * Undo is useful for Remove new flag, given there is no UI to undo it otherwise * ,i.e., there is (context) menu item for add new flag */ - public static void removeNewFlagWithUndo(@NonNull Fragment fragment, FeedItem item) { + public static void markReadWithUndo(@NonNull Fragment fragment, FeedItem item, + int playState, boolean showSnackbar) { if (item == null) { return; } - Log.d(TAG, "removeNewFlagWithUndo(" + item.getId() + ")"); + Log.d(TAG, "markReadWithUndo(" + item.getId() + ")"); // we're marking it as unplayed since the user didn't actually play it // but they don't want it considered 'NEW' anymore - DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); + DBWriter.markItemPlayed(playState, item.getId()); final Handler h = new Handler(fragment.requireContext().getMainLooper()); final Runnable r = () -> { @@ -254,15 +227,40 @@ public class FeedItemMenuHandler { } }; + int playStateStringRes; + switch (playState) { + default: + case FeedItem.UNPLAYED: + if (item.getPlayState() == FeedItem.NEW) { + //was new + playStateStringRes = R.string.removed_new_flag_label; + } else { + //was played + playStateStringRes = R.string.marked_as_unplayed_label; + } + break; + case FeedItem.PLAYED: + playStateStringRes = R.string.marked_as_played_label; + break; + } + + int duration = Snackbar.LENGTH_LONG; + + if (showSnackbar) { + ((MainActivity) fragment.getActivity()).showSnackbarAbovePlayer( + playStateStringRes, duration) + .setAction(fragment.getString(R.string.undo), v -> { + DBWriter.markItemPlayed(item.getPlayState(), item.getId()); + // don't forget to cancel the thing that's going to remove the media + h.removeCallbacks(r); + }); + } + + h.postDelayed(r, (int) Math.ceil(duration * 1.05f)); + } - Snackbar snackbar = ((MainActivity) fragment.getActivity()).showSnackbarAbovePlayer( - R.string.removed_new_flag_label, Snackbar.LENGTH_LONG) - .setAction(fragment.getString(R.string.undo), v -> { - DBWriter.markItemPlayed(FeedItem.NEW, item.getId()); - // don't forget to cancel the thing that's going to remove the media - h.removeCallbacks(r); - }); - h.postDelayed(r, (int) Math.ceil(snackbar.getDuration() * 1.05f)); + public static void removeNewFlagWithUndo(@NonNull Fragment fragment, FeedItem item) { + markReadWithUndo(fragment, item, FeedItem.UNPLAYED, false); } } diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java index ed0cac05d..fded5fb34 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -60,44 +60,36 @@ public class FeedMenuHandler { */ public static boolean onOptionsItemClicked(final Context context, final MenuItem item, final Feed selectedFeed) throws DownloadRequestException { - switch (item.getItemId()) { - case R.id.refresh_item: - DBTasks.forceRefreshFeed(context, selectedFeed, true); - break; - case R.id.refresh_complete_item: - DBTasks.forceRefreshCompleteFeed(context, selectedFeed); - break; - case R.id.sort_items: - showSortDialog(context, selectedFeed); - break; - case R.id.filter_items: - showFilterDialog(context, selectedFeed); - break; - case R.id.mark_all_read_item: - ConfirmationDialog conDialog = new ConfirmationDialog(context, - R.string.mark_all_read_label, - R.string.mark_all_read_feed_confirmation_msg) { - - @Override - public void onConfirmButtonPressed( - DialogInterface dialog) { - dialog.dismiss(); - DBWriter.markFeedRead(selectedFeed.getId()); - } - }; - conDialog.createNewDialog().show(); - break; - case R.id.visit_website_item: - IntentUtils.openInBrowser(context, selectedFeed.getLink()); - break; - case R.id.share_link_item: - ShareUtils.shareFeedlink(context, selectedFeed); - break; - case R.id.share_download_url_item: - ShareUtils.shareFeedDownloadLink(context, selectedFeed); - break; - default: - return false; + final int itemId = item.getItemId(); + if (itemId == R.id.refresh_item) { + DBTasks.forceRefreshFeed(context, selectedFeed, true); + } else if (itemId == R.id.refresh_complete_item) { + DBTasks.forceRefreshCompleteFeed(context, selectedFeed); + } else if (itemId == R.id.sort_items) { + showSortDialog(context, selectedFeed); + } else if (itemId == R.id.filter_items) { + showFilterDialog(context, selectedFeed); + } else if (itemId == R.id.mark_all_read_item) { + ConfirmationDialog conDialog = new ConfirmationDialog(context, + R.string.mark_all_read_label, + R.string.mark_all_read_feed_confirmation_msg) { + + @Override + public void onConfirmButtonPressed( + DialogInterface dialog) { + dialog.dismiss(); + DBWriter.markFeedRead(selectedFeed.getId()); + } + }; + conDialog.createNewDialog().show(); + } else if (itemId == R.id.visit_website_item) { + IntentUtils.openInBrowser(context, selectedFeed.getLink()); + } else if (itemId == R.id.share_link_item) { + ShareUtils.shareFeedlink(context, selectedFeed); + } else if (itemId == R.id.share_download_url_item) { + ShareUtils.shareFeedDownloadLink(context, selectedFeed); + } else { + return false; } return true; } diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java deleted file mode 100644 index b42244160..000000000 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java +++ /dev/null @@ -1,99 +0,0 @@ -package de.danoeh.antennapod.menuhandler; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.view.Menu; -import android.view.MenuItem; -import androidx.appcompat.view.menu.MenuItemImpl; -import androidx.appcompat.widget.SearchView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.fragment.SearchFragment; - -import java.util.HashMap; -import java.util.Map; - -/** - * Utilities for menu items. - */ -public class MenuItemUtils extends de.danoeh.antennapod.core.menuhandler.MenuItemUtils { - - public static void refreshLockItem(Context context, Menu menu) { - final MenuItem queueLock = menu.findItem(R.id.queue_lock); - if (UserPreferences.isQueueLocked()) { - queueLock.setTitle(de.danoeh.antennapod.R.string.unlock_queue); - queueLock.setIcon(R.drawable.ic_lock_open); - } else { - queueLock.setTitle(de.danoeh.antennapod.R.string.lock_queue); - queueLock.setIcon(R.drawable.ic_lock_closed); - } - } - - public static void setupSearchItem(Menu menu, MainActivity activity, long feedId, String feedTitle) { - MenuItem searchItem = menu.findItem(R.id.action_search); - final SearchView sv = (SearchView) searchItem.getActionView(); - sv.setBackgroundColor(ThemeUtils.getColorFromAttr(activity, android.R.attr.windowBackground)); - sv.setQueryHint(activity.getString(R.string.search_label)); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - activity.loadChildFragment(SearchFragment.newInstance(s, feedId, feedTitle)); - searchItem.collapseActionView(); - return true; - } - - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); - searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { - private final Map<Integer, Integer> oldShowAsActionState = new HashMap<>(); - - @Override - public boolean onMenuItemActionExpand(MenuItem clickedItem) { - oldShowAsActionState.clear(); - for (int i = 0; i < menu.size(); i++) { - MenuItem item = menu.getItem(i); - if (item.getItemId() != searchItem.getItemId()) { - oldShowAsActionState.put(item.getItemId(), getShowAsActionFlag(item)); - item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - } - } - return true; - } - - @Override - public boolean onMenuItemActionCollapse(MenuItem collapsedItem) { - for (int i = 0; i < menu.size(); i++) { - MenuItem item = menu.getItem(i); - if (item.getItemId() != searchItem.getItemId() - && oldShowAsActionState.containsKey(item.getItemId())) { - item.setShowAsAction(oldShowAsActionState.get(item.getItemId())); - } - } - return true; - } - }); - } - - @SuppressLint("RestrictedApi") - private static int getShowAsActionFlag(MenuItem item) { - if (!(item instanceof MenuItemImpl)) { - return MenuItemImpl.SHOW_AS_ACTION_NEVER; - } - MenuItemImpl itemImpl = ((MenuItemImpl) item); - if (itemImpl.requiresActionButton()) { - return MenuItemImpl.SHOW_AS_ACTION_ALWAYS; - } else if (itemImpl.requestsActionButton()) { - return MenuItemImpl.SHOW_AS_ACTION_IF_ROOM; - } else if (itemImpl.showsTextAsAction()) { - return MenuItemImpl.SHOW_AS_ACTION_WITH_TEXT; - } else { - return MenuItemImpl.SHOW_AS_ACTION_NEVER; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java index 03a8edbf0..84c738632 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java @@ -11,6 +11,9 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.fragment.swipeactions.SwipeAction; +import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; public class PreferenceUpgrader { private static final String PREF_CONFIGURED_VERSION = "version_code"; @@ -28,12 +31,12 @@ public class PreferenceUpgrader { AutoUpdateManager.restartUpdateAlarm(context); CrashReportWriter.getFile().delete(); - upgrade(oldVersion); + upgrade(oldVersion, context); upgraderPrefs.edit().putInt(PREF_CONFIGURED_VERSION, newVersion).apply(); } } - private static void upgrade(int oldVersion) { + private static void upgrade(int oldVersion, Context context) { if (oldVersion == -1) { //New installation if (UserPreferences.getUsageCountingDateMillis() < 0) { @@ -104,5 +107,10 @@ public class PreferenceUpgrader { String.valueOf(KeyEvent.KEYCODE_MEDIA_PREVIOUS)).apply(); } } + if (oldVersion < 2040000) { + SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE); + prefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + QueueFragment.TAG, + SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE).apply(); + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java b/app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java index 9355c0c15..ae6e88c45 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java +++ b/app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.view; import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.graphics.Color; @@ -15,10 +16,15 @@ import android.view.View; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; + +import androidx.core.content.ContextCompat; import androidx.core.util.Consumer; import androidx.core.view.ViewCompat; + import com.google.android.material.snackbar.Snackbar; + import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.NetworkUtils; @@ -94,6 +100,19 @@ public class ShownotesWebView extends WebView implements View.OnLongClickListene selectedUrl = r.getExtra(); showContextMenu(); return true; + } else if (r != null && r.getType() == HitTestResult.EMAIL_TYPE) { + Log.d(TAG, "E-Mail of webview was long-pressed. Extra: " + r.getExtra()); + ClipboardManager clipboardManager = ContextCompat.getSystemService(this.getContext(), + ClipboardManager.class); + if (clipboardManager != null) { + clipboardManager.setPrimaryClip(ClipData.newPlainText("AntennaPod", r.getExtra())); + } + if (this.getContext() instanceof MainActivity) { + ((MainActivity) this.getContext()).showSnackbarAbovePlayer( + getResources().getString(R.string.copied_to_clipboard), + Snackbar.LENGTH_SHORT); + } + return true; } selectedUrl = null; return false; @@ -104,33 +123,28 @@ public class ShownotesWebView extends WebView implements View.OnLongClickListene return false; } - switch (item.getItemId()) { - case R.id.open_in_browser_item: - IntentUtils.openInBrowser(getContext(), selectedUrl); - break; - case R.id.share_url_item: - ShareUtils.shareLink(getContext(), selectedUrl); - break; - case R.id.copy_url_item: - ClipData clipData = ClipData.newPlainText(selectedUrl, selectedUrl); - android.content.ClipboardManager cm = (android.content.ClipboardManager) getContext() - .getSystemService(Context.CLIPBOARD_SERVICE); - cm.setPrimaryClip(clipData); - Snackbar s = Snackbar.make(this, R.string.copied_url_msg, Snackbar.LENGTH_LONG); - ViewCompat.setElevation(s.getView(), 100); - s.show(); - break; - case R.id.go_to_position_item: - if (Timeline.isTimecodeLink(selectedUrl) && timecodeSelectedListener != null) { - timecodeSelectedListener.accept(Timeline.getTimecodeLinkTime(selectedUrl)); - } else { - Log.e(TAG, "Selected go_to_position_item, but URL was no timecode link: " + selectedUrl); - } - break; - default: - selectedUrl = null; - return false; - + final int itemId = item.getItemId(); + if (itemId == R.id.open_in_browser_item) { + IntentUtils.openInBrowser(getContext(), selectedUrl); + } else if (itemId == R.id.share_url_item) { + ShareUtils.shareLink(getContext(), selectedUrl); + } else if (itemId == R.id.copy_url_item) { + ClipData clipData = ClipData.newPlainText(selectedUrl, selectedUrl); + ClipboardManager cm = (ClipboardManager) getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(clipData); + Snackbar s = Snackbar.make(this, R.string.copied_url_msg, Snackbar.LENGTH_LONG); + ViewCompat.setElevation(s.getView(), 100); + s.show(); + } else if (itemId == R.id.go_to_position_item) { + if (Timeline.isTimecodeLink(selectedUrl) && timecodeSelectedListener != null) { + timecodeSelectedListener.accept(Timeline.getTimecodeLinkTime(selectedUrl)); + } else { + Log.e(TAG, "Selected go_to_position_item, but URL was no timecode link: " + selectedUrl); + } + } else { + selectedUrl = null; + return false; } selectedUrl = null; return true; diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java index 1ea9d71f9..02d45b2a0 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java @@ -7,13 +7,16 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckBox; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import androidx.cardview.widget.CardView; import androidx.recyclerview.widget.RecyclerView; + import com.joanzapata.iconify.Iconify; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.CoverLoader; @@ -31,8 +34,8 @@ import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.ui.common.CircularProgressBar; +import de.danoeh.antennapod.ui.common.ThemeUtils; /** * Holds the view which shows FeedItems. @@ -60,6 +63,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { private final TextView separatorIcons; private final View leftPadding; public final CardView coverHolder; + public final CheckBox selectCheckBox; private final MainActivity activity; private FeedItem item; @@ -91,6 +95,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { coverHolder = itemView.findViewById(R.id.coverHolder); leftPadding = itemView.findViewById(R.id.left_padding); itemView.setTag(this); + selectCheckBox = itemView.findViewById(R.id.selectCheckBox); } public void bind(FeedItem item) { @@ -105,7 +110,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { isInQueue.setVisibility(item.isTagged(FeedItem.TAG_QUEUE) ? View.VISIBLE : View.GONE); container.setAlpha(item.isPlayed() ? 0.5f : 1.0f); - ItemActionButton actionButton = ItemActionButton.forItem(item, true, true); + ItemActionButton actionButton = ItemActionButton.forItem(item); actionButton.configure(secondaryActionButton, secondaryActionIcon, activity); secondaryActionButton.setFocusable(false); diff --git a/app/src/main/res/layout-sw720dp/main.xml b/app/src/main/res/layout-sw720dp/main.xml index 79b7213e0..fe5a86d24 100644 --- a/app/src/main/res/layout-sw720dp/main.xml +++ b/app/src/main/res/layout-sw720dp/main.xml @@ -1,28 +1,29 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:orientation="horizontal" - android:layout_width="match_parent" - android:layout_height="match_parent"> + 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" + android:orientation="horizontal" + tools:viewBindingIgnore="true"> <FrameLayout - android:id="@+id/navDrawerFragment" - android:layout_width="300dp" - android:layout_height="match_parent" - android:layout_gravity="start" - android:orientation="vertical" /> + android:id="@+id/navDrawerFragment" + android:layout_width="300dp" + android:layout_height="match_parent" + android:layout_gravity="start" + android:orientation="vertical" /> <View - android:layout_width="1dp" - android:layout_height="match_parent" - android:background="?android:attr/listDivider" /> + android:layout_width="1dp" + android:layout_height="match_parent" + android:background="?android:attr/listDivider" /> <androidx.coordinatorlayout.widget.CoordinatorLayout - android:id="@+id/overview_coordinator_layout" - android:layout_width="match_parent" - android:layout_height="match_parent"> + android:id="@+id/overview_coordinator_layout" + android:layout_width="match_parent" + android:layout_height="match_parent"> <FrameLayout android:id="@+id/main_view" @@ -33,14 +34,14 @@ tools:background="@android:color/holo_red_dark" /> <FrameLayout - android:elevation="8dp" android:id="@+id/audioplayerFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:background="?android:attr/windowBackground" + android:elevation="8dp" android:visibility="gone" app:layout_behavior="de.danoeh.antennapod.view.LockableBottomSheetBehavior" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> -</LinearLayout>
\ No newline at end of file +</LinearLayout> diff --git a/app/src/main/res/layout/audio_controls.xml b/app/src/main/res/layout/audio_controls.xml index 2c9665aad..0bfa4f521 100644 --- a/app/src/main/res/layout/audio_controls.xml +++ b/app/src/main/res/layout/audio_controls.xml @@ -1,9 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" + android:orientation="vertical" android:padding="16dp"> <LinearLayout @@ -17,7 +17,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" - android:layout_marginBottom="8dp"/> + android:layout_marginBottom="8dp" /> <LinearLayout android:layout_width="match_parent" @@ -27,9 +27,9 @@ <TextView android:layout_width="0dp" android:layout_height="wrap_content" - style="@style/AntennaPod.TextView.ListItemPrimaryTitle" android:layout_weight="1" - android:text="@string/playback_speed" /> + android:text="@string/playback_speed" + style="@style/AntennaPod.TextView.ListItemPrimaryTitle" /> <TextView android:id="@+id/txtvPlaybackSpeed" @@ -42,67 +42,14 @@ <de.danoeh.antennapod.view.PlaybackSpeedSeekBar android:id="@+id/speed_seek_bar" android:layout_width="match_parent" - android:layout_height="wrap_content"/> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="12dp" - android:layout_marginBottom="4dp" - style="@style/AntennaPod.TextView.ListItemPrimaryTitle" - android:text="@string/volume" /> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginLeft="8dp" - android:layout_marginStart="8dp" - android:orientation="horizontal" - android:gravity="center"> - - <TextView - android:id="@+id/txtvLeft" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/left_short" /> - - <SeekBar - android:id="@+id/volume_left" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:max="100" /> - - </LinearLayout> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:layout_marginLeft="8dp" - android:layout_marginStart="8dp" - android:orientation="horizontal" - android:gravity="center"> - - <TextView - android:id="@+id/txtvRight" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/right_short" /> - - <SeekBar - android:id="@+id/volume_right" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:max="100" /> - - </LinearLayout> + android:layout_height="wrap_content" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" - style="@style/AntennaPod.TextView.ListItemPrimaryTitle" - android:text="@string/audio_effects" /> + android:text="@string/audio_effects" + style="@style/AntennaPod.TextView.ListItemPrimaryTitle" /> <CheckBox android:id="@+id/skipSilence" @@ -115,5 +62,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/stereo_to_mono" /> + </LinearLayout> + </ScrollView> diff --git a/app/src/main/res/layout/episode_filter_dialog.xml b/app/src/main/res/layout/episode_filter_dialog.xml index 43d074bfb..9661a8e72 100644 --- a/app/src/main/res/layout/episode_filter_dialog.xml +++ b/app/src/main/res/layout/episode_filter_dialog.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" @@ -7,7 +8,7 @@ <RadioGroup android:id="@+id/radio_filter_group" - android:layout_width="fill_parent" + android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> @@ -22,19 +23,21 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/episode_filters_exclude" /> + </RadioGroup> <EditText android:id="@+id/etxtEpisodeFilterText" - android:layout_width="fill_parent" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" - android:lines="8" - android:minLines="1" - android:maxLines="20" - android:scrollbars="vertical" - android:hint="@string/episode_filters_hint" + android:cursorVisible="true" android:focusable="true" android:focusableInTouchMode="true" - android:cursorVisible="true" /> + android:hint="@string/episode_filters_hint" + android:lines="8" + android:maxLines="20" + android:minLines="1" + android:scrollbars="vertical" /> + </LinearLayout> diff --git a/app/src/main/res/layout/episodes_apply_action_fragment.xml b/app/src/main/res/layout/episodes_apply_action_fragment.xml deleted file mode 100644 index 78827a12a..000000000 --- a/app/src/main/res/layout/episodes_apply_action_fragment.xml +++ /dev/null @@ -1,59 +0,0 @@ -<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"> - - <androidx.appcompat.widget.Toolbar - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?attr/actionBarSize" - android:theme="?attr/actionBarTheme" - android:layout_alignParentTop="true" - app:navigationIcon="?homeAsUpIndicator" - android:id="@+id/toolbar"/> - - <ListView - android:id="@android:id/list" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/toolbar" - android:layout_marginTop="0dp" /> - - <com.leinardi.android.speeddial.SpeedDialOverlayLayout - android:id="@+id/fabSDOverlay" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:importantForAccessibility="no" - android:layout_below="@id/toolbar" /> - <!-- 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_open_elevation" - tools:ignore="UnusedAttribute" > - - <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" - android:accessibilityTraversalBefore="@android:id/list" - android:contentDescription="@string/apply_action" /> - </ScrollView> - -</RelativeLayout> diff --git a/app/src/main/res/layout/feed_item_list_fragment.xml b/app/src/main/res/layout/feed_item_list_fragment.xml index 6dc484e2f..734ce64dd 100644 --- a/app/src/main/res/layout/feed_item_list_fragment.xml +++ b/app/src/main/res/layout/feed_item_list_fragment.xml @@ -1,51 +1,53 @@ <?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout - 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"> + 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"> <com.google.android.material.appbar.AppBarLayout - android:id="@+id/appBar" - android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:id="@+id/appBar" + android:layout_width="match_parent" + android:layout_height="wrap_content"> <com.google.android.material.appbar.CollapsingToolbarLayout - android:id="@+id/collapsing_toolbar" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="?android:attr/windowBackground" - app:contentScrim="?android:attr/windowBackground" - app:scrimAnimationDuration="200" - app:layout_scrollFlags="scroll|exitUntilCollapsed"> + android:id="@+id/collapsing_toolbar" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?android:attr/windowBackground" + app:contentScrim="?android:attr/windowBackground" + app:scrimAnimationDuration="200" + app:layout_scrollFlags="scroll|exitUntilCollapsed"> <ImageView - android:id="@+id/imgvBackground" - style="@style/BigBlurryBackground" - android:background="@color/image_readability_tint" - android:layout_width="match_parent" - android:layout_height="232dp" - app:layout_collapseMode="parallax" - app:layout_collapseParallaxMultiplier="0.6"/> + android:id="@+id/imgvBackground" + android:layout_width="match_parent" + android:layout_height="232dp" + android:background="@color/image_readability_tint" + style="@style/BigBlurryBackground" + app:layout_collapseMode="parallax" + app:layout_collapseParallaxMultiplier="0.6" /> - <include layout="@layout/feeditemlist_header" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="bottom" - app:layout_collapseMode="parallax" - app:layout_collapseParallaxMultiplier="0.6" /> + <include + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + layout="@layout/feeditemlist_header" + app:layout_collapseMode="parallax" + app:layout_collapseParallaxMultiplier="0.6" /> <androidx.appcompat.widget.Toolbar - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?attr/actionBarSize" - android:theme="?attr/actionBarTheme" - android:layout_alignParentTop="true" - android:id="@+id/toolbar" - app:navigationIcon="?homeAsUpIndicator" - app:layout_collapseMode="pin"/> + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?attr/actionBarSize" + android:theme="?attr/actionBarTheme" + android:layout_alignParentTop="true" + app:navigationIcon="?homeAsUpIndicator" + app:layout_collapseMode="pin" /> </com.google.android.material.appbar.CollapsingToolbarLayout> + </com.google.android.material.appbar.AppBarLayout> <androidx.swiperefreshlayout.widget.SwipeRefreshLayout @@ -54,27 +56,30 @@ android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> - <de.danoeh.antennapod.view.EpisodeItemListRecyclerView - android:id="@+id/recyclerView" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:paddingHorizontal="@dimen/additional_horizontal_spacing" /> + <de.danoeh.antennapod.view.EpisodeItemListRecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingHorizontal="@dimen/additional_horizontal_spacing" /> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> <ProgressBar - android:id="@+id/progLoading" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:indeterminateOnly="true" - android:visibility="gone"/> + android:id="@+id/progLoading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:indeterminateOnly="true" + android:visibility="gone" /> <include - layout="@layout/more_content_list_footer" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="bottom" - android:visibility="gone"/> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:visibility="gone" + layout="@layout/more_content_list_footer" /> + + <include + layout="@layout/multi_select_speed_dial" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/feeditemlist_item.xml b/app/src/main/res/layout/feeditemlist_item.xml index 37b88d1b5..b876f079d 100644 --- a/app/src/main/res/layout/feeditemlist_item.xml +++ b/app/src/main/res/layout/feeditemlist_item.xml @@ -1,177 +1,187 @@ <?xml version="1.0" encoding="utf-8"?> <FrameLayout - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content"> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:ignore="MergeRootFrame"> <!-- This parent FrameLayout is necessary because RecyclerView's ItemAnimator changes alpha values, which conflicts with our played state indicator. --> - <LinearLayout - android:id="@+id/container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:gravity="center_vertical" - android:baselineAligned="false" - android:paddingStart="12dp" - android:paddingLeft="12dp" - android:paddingEnd="0dp" - android:paddingRight="0dp" - tools:ignore="UselessParent"> + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical" + android:baselineAligned="false" + android:paddingStart="12dp" + android:paddingLeft="12dp" + android:paddingEnd="0dp" + android:paddingRight="0dp" + tools:ignore="UselessParent"> <LinearLayout - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:id="@+id/left_padding" - android:minWidth="4dp"> + android:id="@+id/left_padding" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:minWidth="4dp"> + <ImageView - android:id="@+id/drag_handle" - android:layout_width="16dp" - android:layout_height="match_parent" - android:importantForAccessibility="no" - android:scaleType="fitCenter" - app:srcCompat="?attr/dragview_background" - android:paddingStart="0dp" - android:paddingLeft="0dp" - android:paddingEnd="4dp" - android:paddingRight="4dp" - tools:src="@drawable/ic_drag_darktheme" - tools:background="@android:color/holo_green_dark"/> + android:id="@+id/drag_handle" + android:layout_width="16dp" + android:layout_height="match_parent" + android:importantForAccessibility="no" + android:scaleType="fitCenter" + android:paddingStart="0dp" + android:paddingLeft="0dp" + android:paddingEnd="4dp" + android:paddingRight="4dp" + app:srcCompat="?attr/dragview_background" + tools:src="@drawable/ic_drag_darktheme" + tools:background="@android:color/holo_green_dark" /> + + <!-- Needs to have the same width as the action button. Otherwise, the screen jumps around. --> + <CheckBox + android:id="@+id/selectCheckBox" + android:layout_width="60dp" + android:layout_height="match_parent" + android:visibility="gone" /> </LinearLayout> <androidx.cardview.widget.CardView - android:layout_width="@dimen/thumbnail_length_queue_item" - android:layout_height="@dimen/thumbnail_length_queue_item" - android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" - android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" - android:layout_marginRight="@dimen/listitem_threeline_textleftpadding" - android:layout_marginEnd="@dimen/listitem_threeline_textleftpadding" - android:id="@+id/coverHolder" - app:cardBackgroundColor="@color/non_square_icon_background" - app:cardCornerRadius="4dp" - app:cardPreventCornerOverlap="false" - app:cardElevation="0dp"> + android:id="@+id/coverHolder" + android:layout_width="@dimen/thumbnail_length_queue_item" + android:layout_height="@dimen/thumbnail_length_queue_item" + android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" + android:layout_marginRight="@dimen/listitem_threeline_textleftpadding" + android:layout_marginEnd="@dimen/listitem_threeline_textleftpadding" + app:cardBackgroundColor="@color/non_square_icon_background" + app:cardCornerRadius="4dp" + app:cardPreventCornerOverlap="false" + app:cardElevation="0dp"> <RelativeLayout - android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_width="match_parent" + android:layout_height="match_parent"> <TextView - android:id="@+id/txtvPlaceholder" - android:layout_width="@dimen/thumbnail_length_queue_item" - android:layout_height="@dimen/thumbnail_length_queue_item" - android:layout_centerVertical="true" - android:gravity="center" - android:background="@color/light_gray" - android:maxLines="3" - android:padding="2dp" - android:ellipsize="end"/> + android:id="@+id/txtvPlaceholder" + android:layout_width="@dimen/thumbnail_length_queue_item" + android:layout_height="@dimen/thumbnail_length_queue_item" + android:layout_centerVertical="true" + android:gravity="center" + android:background="@color/light_gray" + android:maxLines="3" + android:padding="2dp" + android:ellipsize="end" /> + <ImageView - android:id="@+id/imgvCover" - android:layout_width="@dimen/thumbnail_length_queue_item" - android:layout_height="@dimen/thumbnail_length_queue_item" - android:layout_centerVertical="true" - android:importantForAccessibility="no" - tools:src="@tools:sample/avatars"/> + android:id="@+id/imgvCover" + android:layout_width="@dimen/thumbnail_length_queue_item" + android:layout_height="@dimen/thumbnail_length_queue_item" + android:layout_centerVertical="true" + android:importantForAccessibility="no" + tools:src="@tools:sample/avatars" /> </RelativeLayout> + </androidx.cardview.widget.CardView> <LinearLayout - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" - android:layout_marginRight="@dimen/listitem_threeline_textrightpadding" - android:layout_marginEnd="@dimen/listitem_threeline_textrightpadding" - android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" - android:layout_weight="1" - tools:background="@android:color/holo_red_dark" - android:orientation="vertical"> + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + android:layout_marginRight="@dimen/listitem_threeline_textrightpadding" + android:layout_marginEnd="@dimen/listitem_threeline_textrightpadding" + android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" + android:layout_weight="1" + android:orientation="vertical" + tools:background="@android:color/holo_red_dark"> <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/status" - android:orientation="horizontal" - android:gravity="center_vertical"> + android:id="@+id/status" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical"> <TextView - android:text="@string/new_label" - style="@style/AntennaPod.TextView.UnreadIndicator" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:id="@+id/statusUnread" - android:layout_marginRight="4dp" - android:layout_marginEnd="4dp" - tools:text="@sample/episodes.json/data/status_label"/> + android:id="@+id/statusUnread" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/new_label" + android:layout_marginRight="4dp" + android:layout_marginEnd="4dp" + style="@style/AntennaPod.TextView.UnreadIndicator" + tools:text="@sample/episodes.json/data/status_label" /> <ImageView - android:layout_width="14sp" - android:layout_height="14sp" - app:srcCompat="@drawable/ic_videocam" - android:contentDescription="@string/media_type_video_label" - android:id="@+id/ivIsVideo"/> + android:id="@+id/ivIsVideo" + android:layout_width="14sp" + android:layout_height="14sp" + android:contentDescription="@string/media_type_video_label" + app:srcCompat="@drawable/ic_videocam" /> <ImageView - android:layout_width="14sp" - android:layout_height="14sp" - app:srcCompat="@drawable/ic_star" - android:contentDescription="@string/is_favorite_label" - android:id="@+id/isFavorite"/> + android:id="@+id/isFavorite" + android:layout_width="14sp" + android:layout_height="14sp" + android:contentDescription="@string/is_favorite_label" + app:srcCompat="@drawable/ic_star" /> <ImageView - android:layout_width="14sp" - android:layout_height="14sp" - app:srcCompat="@drawable/ic_playlist" - android:contentDescription="@string/in_queue_label" - android:id="@+id/ivInPlaylist"/> + android:id="@+id/ivInPlaylist" + android:layout_width="14sp" + android:layout_height="14sp" + android:contentDescription="@string/in_queue_label" + app:srcCompat="@drawable/ic_playlist" /> <TextView - android:id="@+id/separatorIcons" - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginLeft="4dp" - android:layout_marginStart="4dp" - android:layout_marginRight="4dp" - android:layout_marginEnd="4dp" - android:text="·" - android:importantForAccessibility="no" - tools:background="@android:color/holo_blue_light"/> + android:id="@+id/separatorIcons" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" + android:layout_marginRight="4dp" + android:layout_marginEnd="4dp" + android:text="·" + android:importantForAccessibility="no" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + tools:background="@android:color/holo_blue_light" /> <TextView - android:id="@+id/txtvPubDate" - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginRight="4dp" - android:layout_marginEnd="4dp" - tools:text="@sample/episodes.json/data/published_at"/> + android:id="@+id/txtvPubDate" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="4dp" + android:layout_marginEnd="4dp" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + tools:text="@sample/episodes.json/data/published_at" /> <TextView - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginRight="4dp" - android:layout_marginEnd="4dp" - android:text="·" - android:importantForAccessibility="no" - tools:background="@android:color/holo_blue_light"/> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="4dp" + android:layout_marginEnd="4dp" + android:text="·" + android:importantForAccessibility="no" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + tools:background="@android:color/holo_blue_light" /> <TextView - android:id="@+id/size" - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_marginRight="4dp" - android:layout_marginEnd="4dp" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="10 MB"/> + android:id="@+id/size" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="4dp" + android:layout_marginEnd="4dp" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + tools:text="10 MB" /> </LinearLayout> @@ -181,55 +191,58 @@ Keep this in mind when changing the order of this layout! --> <TextView - android:id="@+id/txtvTitle" - style="@style/AntennaPod.TextView.ListItemPrimaryTitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - tools:text="@sample/episodes.json/data/title" - android:importantForAccessibility="no" - android:ellipsize="end" - tools:background="@android:color/holo_blue_light"/> + android:id="@+id/txtvTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + android:ellipsize="end" + style="@style/AntennaPod.TextView.ListItemPrimaryTitle" + tools:text="@sample/episodes.json/data/title" + tools:background="@android:color/holo_blue_light" /> <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/progress" - android:orientation="horizontal" - android:gravity="center_vertical"> + android:id="@+id/progress" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical"> <TextView - android:id="@+id/txtvPosition" - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="0dp" - tools:text="00:42:23" - tools:background="@android:color/holo_blue_light"/> + android:id="@+id/txtvPosition" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="0dp" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + tools:text="00:42:23" + tools:background="@android:color/holo_blue_light" /> <ProgressBar - android:id="@+id/progressBar" - style="?attr/progressBarTheme" - android:layout_width="0dp" - android:layout_weight="1" - android:layout_height="4dp" - android:max="100" - android:layout_margin="4dp" - tools:background="@android:color/holo_blue_light"/> + android:id="@+id/progressBar" + android:layout_width="0dp" + android:layout_height="4dp" + android:layout_weight="1" + android:max="100" + android:layout_margin="4dp" + style="?attr/progressBarTheme" + tools:background="@android:color/holo_blue_light" /> <TextView - android:id="@+id/txtvDuration" - style="@style/AntennaPod.TextView.ListItemSecondaryTitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="0dp" - tools:text="@sample/episodes.json/data/duration" - tools:background="@android:color/holo_blue_light"/> + android:id="@+id/txtvDuration" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="0dp" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + tools:text="@sample/episodes.json/data/duration" + tools:background="@android:color/holo_blue_light" /> </LinearLayout> </LinearLayout> - <include layout="@layout/secondary_action"/> + <include + android:id="@+id/secondaryActionButton" + layout="@layout/secondary_action" /> </LinearLayout> + </FrameLayout> diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml index 48195a176..10b560faf 100644 --- a/app/src/main/res/layout/main.xml +++ b/app/src/main/res/layout/main.xml @@ -1,18 +1,21 @@ <?xml version="1.0" encoding="utf-8"?> <androidx.drawerlayout.widget.DrawerLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/drawer_layout" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:ignore="InconsistentLayout"> - <!-- InconsistentLayout: Tablet layout does not have a drawer --> + 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:id="@+id/drawer_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="InconsistentLayout" + tools:viewBindingIgnore="true"> + <!-- InconsistentLayout: Tablet layout does not have a drawer --> + <!-- viewBindingIgnore: Configurations for main.xml must + agree on the root element's ID --> <androidx.coordinatorlayout.widget.CoordinatorLayout - android:id="@+id/overview_coordinator_layout" - android:layout_width="match_parent" - android:layout_height="match_parent"> + android:id="@+id/overview_coordinator_layout" + android:layout_width="match_parent" + android:layout_height="match_parent"> <FrameLayout android:id="@+id/main_view" @@ -23,21 +26,21 @@ tools:background="@android:color/holo_red_dark" /> <FrameLayout - android:elevation="8dp" android:id="@+id/audioplayerFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:background="?android:attr/windowBackground" + android:elevation="8dp" android:visibility="gone" app:layout_behavior="de.danoeh.antennapod.view.LockableBottomSheetBehavior" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> <FrameLayout - android:id="@+id/navDrawerFragment" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="start" - android:orientation="vertical" /> + android:id="@+id/navDrawerFragment" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="start" + android:orientation="vertical" /> -</androidx.drawerlayout.widget.DrawerLayout>
\ No newline at end of file +</androidx.drawerlayout.widget.DrawerLayout> diff --git a/app/src/main/res/layout/multi_select_speed_dial.xml b/app/src/main/res/layout/multi_select_speed_dial.xml new file mode 100644 index 000000000..0451471bc --- /dev/null +++ b/app/src/main/res/layout/multi_select_speed_dial.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <com.leinardi.android.speeddial.SpeedDialOverlayLayout + android:id="@+id/fabSDOverlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:importantForAccessibility="no" /> + + <!-- 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_gravity="bottom|right" + android:layout_alignParentBottom="true" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:elevation="@dimen/sd_open_elevation"> + + <com.leinardi.android.speeddial.SpeedDialView + android:id="@+id/fabSD" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:accessibilityTraversalBefore="@android:id/list" + android:contentDescription="@string/apply_action" + android:visibility="gone" + app:sdMainFabClosedSrc="@drawable/ic_fab_edit" + app:sdOverlayLayout="@id/fabSDOverlay" /> + + </ScrollView> + +</merge> diff --git a/app/src/main/res/layout/opml_selection.xml b/app/src/main/res/layout/opml_selection.xml index 1f1d72d76..735c7f87e 100644 --- a/app/src/main/res/layout/opml_selection.xml +++ b/app/src/main/res/layout/opml_selection.xml @@ -1,39 +1,44 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent"> + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> <Button - android:id="@+id/butConfirm" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentBottom="true" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - style="@style/Widget.MaterialComponents.Button.TextButton" - android:layout_margin="8dp" - android:text="@string/confirm_label"/> + android:id="@+id/butConfirm" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:layout_margin="8dp" + android:text="@string/confirm_label" + style="@style/Widget.MaterialComponents.Button.TextButton" /> <Button - android:id="@+id/butCancel" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentBottom="true" - android:layout_toLeftOf="@+id/butConfirm" - android:layout_toStartOf="@+id/butConfirm" - style="@style/Widget.MaterialComponents.Button.TextButton" - android:layout_margin="8dp" - android:text="@string/cancel_label"/> + android:id="@+id/butCancel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_toLeftOf="@+id/butConfirm" + android:layout_toStartOf="@+id/butConfirm" + android:layout_margin="8dp" + android:text="@string/cancel_label" + style="@style/Widget.MaterialComponents.Button.TextButton" /> <ListView - android:id="@+id/feedlist" - android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_above="@id/butConfirm" - android:layout_alignParentTop="true" - tools:listitem="@android:layout/simple_list_item_multiple_choice"> - </ListView> + android:id="@+id/feedlist" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_above="@id/butConfirm" + android:layout_alignParentTop="true" + tools:listitem="@android:layout/simple_list_item_multiple_choice" /> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" /> </RelativeLayout> diff --git a/app/src/main/res/layout/queue_fragment.xml b/app/src/main/res/layout/queue_fragment.xml index 3bcd4819f..292b1bb45 100644 --- a/app/src/main/res/layout/queue_fragment.xml +++ b/app/src/main/res/layout/queue_fragment.xml @@ -1,42 +1,44 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools"> +<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"> <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="?attr/actionBarSize" android:theme="?attr/actionBarTheme" android:layout_alignParentTop="true" - app:title="@string/queue_label" - android:id="@+id/toolbar"/> + app:title="@string/queue_label" /> <TextView - android:layout_below="@id/toolbar" android:id="@+id/info_bar" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_below="@id/toolbar" android:textSize="12sp" android:layout_marginTop="-8dp" android:layout_marginLeft="72dp" android:layout_marginStart="72dp" android:layout_marginBottom="4dp" - tools:text="12 Episodes - Time remaining: 12 hours"/> + tools:text="12 Episodes - Time remaining: 12 hours" /> <View android:id="@+id/divider" android:layout_width="match_parent" android:layout_height="1dp" android:layout_below="@id/info_bar" - android:background="?android:attr/listDivider"/> + android:background="?android:attr/listDivider" /> <androidx.swiperefreshlayout.widget.SwipeRefreshLayout - android:id="@+id/swipeRefresh" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_below="@id/divider"> + android:id="@+id/swipeRefresh" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_below="@id/divider"> <de.danoeh.antennapod.view.EpisodeItemListRecyclerView android:id="@+id/recyclerView" @@ -54,4 +56,7 @@ android:indeterminateOnly="true" android:visibility="gone" /> + <include + layout="@layout/multi_select_speed_dial" /> + </RelativeLayout> diff --git a/app/src/main/res/layout/search_fragment.xml b/app/src/main/res/layout/search_fragment.xml index 28d4778f7..5745cf655 100644 --- a/app/src/main/res/layout/search_fragment.xml +++ b/app/src/main/res/layout/search_fragment.xml @@ -1,51 +1,54 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_height="match_parent" - android:layout_width="match_parent"> + 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"> <androidx.appcompat.widget.Toolbar - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?attr/actionBarSize" - android:theme="?attr/actionBarTheme" - app:navigationIcon="?homeAsUpIndicator" - app:title="@string/search_label" - android:id="@+id/toolbar"/> + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?attr/actionBarSize" + android:theme="?attr/actionBarTheme" + app:navigationIcon="?homeAsUpIndicator" + app:title="@string/search_label" /> <com.google.android.material.chip.Chip - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@id/toolbar" - android:id="@+id/feed_title_chip" - android:layout_marginLeft="10dp" - android:layout_marginRight="0dp" - app:closeIconVisible="true"/> + android:id="@+id/feed_title_chip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/toolbar" + android:layout_marginLeft="10dp" + android:layout_marginRight="0dp" + android:visibility="gone" + app:closeIconVisible="true" /> <ProgressBar - android:layout_centerInParent="true" - android:id="@+id/progressBar" - style="?android:attr/progressBarStyle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center"/> + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:layout_gravity="center" + android:visibility="gone" + style="?android:attr/progressBarStyle" /> <androidx.recyclerview.widget.RecyclerView - android:layout_below="@id/feed_title_chip" - android:id="@+id/recyclerViewFeeds" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingLeft="12dp" - android:paddingRight="12dp" - android:clipToPadding="false"/> + android:id="@+id/recyclerViewFeeds" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/feed_title_chip" + android:paddingLeft="12dp" + android:paddingRight="12dp" + android:clipToPadding="false" /> <de.danoeh.antennapod.view.EpisodeItemListRecyclerView - android:id="@+id/recyclerView" - android:layout_below="@id/recyclerViewFeeds" - android:layout_marginTop="-4dp" - android:paddingTop="12dp" - android:paddingHorizontal="@dimen/additional_horizontal_spacing" - android:layout_width="match_parent" - android:layout_height="match_parent"/> + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_below="@id/recyclerViewFeeds" + android:layout_marginTop="-4dp" + android:paddingTop="12dp" + android:paddingHorizontal="@dimen/additional_horizontal_spacing" /> + </RelativeLayout> diff --git a/app/src/main/res/layout/simple_list_fragment.xml b/app/src/main/res/layout/simple_list_fragment.xml index 989566499..6ea3ab54b 100644 --- a/app/src/main/res/layout/simple_list_fragment.xml +++ b/app/src/main/res/layout/simple_list_fragment.xml @@ -1,29 +1,33 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent"> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> <androidx.appcompat.widget.Toolbar - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?attr/actionBarSize" - android:theme="?attr/actionBarTheme" - android:layout_alignParentTop="true" - android:id="@+id/toolbar"/> + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?attr/actionBarSize" + android:theme="?attr/actionBarTheme" + android:layout_alignParentTop="true" /> <de.danoeh.antennapod.view.EpisodeItemListRecyclerView - android:layout_width="match_parent" - android:layout_height="match_parent" - android:paddingHorizontal="@dimen/additional_horizontal_spacing" - android:layout_below="@id/toolbar" - android:id="@+id/recyclerView"/> + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingHorizontal="@dimen/additional_horizontal_spacing" + android:layout_below="@id/toolbar" /> <ProgressBar - android:id="@+id/progLoading" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerInParent="true" - android:indeterminateOnly="true" - android:visibility="gone"/> + android:id="@+id/progLoading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:indeterminateOnly="true" + android:visibility="gone" /> + + <include + layout="@layout/multi_select_speed_dial" /> </RelativeLayout> diff --git a/app/src/main/res/layout/subscription_item.xml b/app/src/main/res/layout/subscription_item.xml index 7fa738f12..ec918fdac 100644 --- a/app/src/main/res/layout/subscription_item.xml +++ b/app/src/main/res/layout/subscription_item.xml @@ -2,32 +2,32 @@ <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" xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:foreground="?attr/selectableItemBackground"> <de.danoeh.antennapod.ui.common.SquareImageView android:id="@+id/imgvCover" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:scaleType="fitCenter" + android:layout_width="match_parent" + android:layout_height="match_parent" android:background="@color/non_square_icon_background" - tools:src="@mipmap/ic_launcher_round" - squareImageView:direction="width"/> + android:scaleType="fitCenter" + squareImageView:direction="width" + tools:src="@mipmap/ic_launcher_round" /> <com.joanzapata.iconify.widget.IconTextView android:id="@+id/txtvTitle" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/non_square_icon_background" - android:layout_alignLeft="@+id/imgvCover" - android:layout_alignRight="@+id/imgvCover" android:layout_alignStart="@+id/imgvCover" - android:layout_alignEnd="@+id/imgvCover" + android:layout_alignLeft="@+id/imgvCover" android:layout_alignTop="@+id/imgvCover" + android:layout_alignEnd="@+id/imgvCover" + android:layout_alignRight="@+id/imgvCover" android:layout_alignBottom="@+id/imgvCover" + android:background="@color/non_square_icon_background" android:ellipsize="end" android:gravity="center" android:textColor="?android:attr/textColorPrimary" @@ -37,12 +37,13 @@ android:id="@+id/triangleCountView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" android:layout_alignParentTop="true" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" app:backgroundColor="?attr/colorSecondary" app:corner="rightTop" app:primaryText="Test" app:primaryTextColor="?attr/colorOnSecondary" app:primaryTextSize="12sp" /> + </RelativeLayout> diff --git a/app/src/main/res/layout/swipeactions_dialog.xml b/app/src/main/res/layout/swipeactions_dialog.xml new file mode 100644 index 000000000..a1f0b7ae6 --- /dev/null +++ b/app/src/main/res/layout/swipeactions_dialog.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="16dp"> + + <androidx.appcompat.widget.SwitchCompat + android:id="@+id/enableSwitch" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:checked="true" + android:text="@string/enable_swipeactions" /> + + <include + android:id="@+id/actionLeftContainer" + layout="@layout/swipeactions_row" /> + + <include + android:id="@+id/actionRightContainer" + layout="@layout/swipeactions_row" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/swipeactions_picker.xml b/app/src/main/res/layout/swipeactions_picker.xml new file mode 100644 index 000000000..e473888b2 --- /dev/null +++ b/app/src/main/res/layout/swipeactions_picker.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.gridlayout.widget.GridLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:grid="http://schemas.android.com/apk/res-auto" + android:id="@+id/pickerGridLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp" + grid:columnCount="2" + grid:alignmentMode="alignBounds" /> diff --git a/app/src/main/res/layout/swipeactions_picker_item.xml b/app/src/main/res/layout/swipeactions_picker_item.xml new file mode 100644 index 000000000..b497efb14 --- /dev/null +++ b/app/src/main/res/layout/swipeactions_picker_item.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:grid="http://schemas.android.com/apk/res-auto" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center" + android:background="?attr/selectableItemBackground" + android:padding="8dp"> + + <ImageView + android:id="@+id/swipeIcon" + android:layout_width="48dp" + android:layout_height="48dp" + android:padding="8dp" + app:srcCompat="@drawable/ic_add" /> + + <TextView + android:id="@+id/swipeActionLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/add_to_queue_label" + android:textSize="14sp" + android:textAlignment="center" + android:textColor="?android:attr/textColorPrimary" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/swipeactions_row.xml b/app/src/main/res/layout/swipeactions_row.xml new file mode 100644 index 000000000..df55d3f89 --- /dev/null +++ b/app/src/main/res/layout/swipeactions_row.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + 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="wrap_content" + android:orientation="vertical"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:layout_marginBottom="8dp"> + + <TextView + android:id="@+id/swipeDirectionLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/swipe_left" + android:layout_alignParentTop="true" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:textColor="?android:attr/textColorPrimary" + android:textSize="18sp" /> + + <TextView + android:id="@+id/swipeActionLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_below="@+id/swipeDirectionLabel" + android:textSize="14sp" + tools:text="@string/add_to_queue_label" /> + + <Button + android:id="@+id/changeButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:text="@string/change_setting" + style="@style/Widget.MaterialComponents.Button.TextButton" /> + + </RelativeLayout> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?attr/background_elevated" /> + + <LinearLayout + android:id="@+id/previewContainer" + android:layout_width="match_parent" + android:layout_height="76dp" + android:gravity="center" + android:foreground="?attr/selectableItemBackground" + android:orientation="horizontal"> + + <include + android:id="@+id/mockEpisode" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="0.7" + layout="@layout/feeditemlist_item" /> + + <ImageView + android:id="@+id/swipeIcon" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="0.3" + android:background="?attr/background_elevated" + android:padding="22dp" + app:srcCompat="@drawable/ic_add" /> + + </LinearLayout> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?attr/background_elevated" /> + +</LinearLayout> diff --git a/app/src/main/res/menu/downloads.xml b/app/src/main/res/menu/downloads.xml index 142f251fc..54469a101 100644 --- a/app/src/main/res/menu/downloads.xml +++ b/app/src/main/res/menu/downloads.xml @@ -2,13 +2,6 @@ <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item - android:id="@+id/episode_actions" - android:menuCategory="container" - android:title="@string/multi_select" - android:icon="@drawable/ic_check_multiple" - android:visible="false" - app:showAsAction="ifRoom" /> - <item android:id="@+id/clear_logs_item" android:menuCategory="container" android:title="@string/clear_history_label" diff --git a/app/src/main/res/menu/episodes.xml b/app/src/main/res/menu/episodes.xml index 22121c905..7a311aedb 100644 --- a/app/src/main/res/menu/episodes.xml +++ b/app/src/main/res/menu/episodes.xml @@ -6,8 +6,7 @@ <item android:id="@+id/action_search" android:icon="@drawable/ic_search" - custom:showAsAction="collapseActionView|always" - custom:actionViewClass="androidx.appcompat.widget.SearchView" + custom:showAsAction="always" android:title="@string/search_label"/> <item diff --git a/app/src/main/res/menu/episodes_apply_action_options.xml b/app/src/main/res/menu/episodes_apply_action_options.xml deleted file mode 100644 index 221ec4d59..000000000 --- a/app/src/main/res/menu/episodes_apply_action_options.xml +++ /dev/null @@ -1,58 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto"> - - <item - android:id="@+id/sort" - android:icon="@drawable/ic_sort" - android:title="@string/sort" - app:showAsAction="always"> - <menu> - <item android:id="@+id/sort_title_a_z" - android:title="@string/sort_title_a_z"/> - <item android:id="@+id/sort_title_z_a" - android:title="@string/sort_title_z_a"/> - <item android:id="@+id/sort_date_new_old" - android:title="@string/sort_date_new_old"/> - <item android:id="@+id/sort_date_old_new" - android:title="@string/sort_date_old_new"/> - <item android:id="@+id/sort_duration_short_long" - android:title="@string/sort_duration_short_long"/> - <item android:id="@+id/sort_duration_long_short" - android:title="@string/sort_duration_long_short"/> - </menu> - </item> - - <item - android:id="@+id/select_options" - android:icon="@drawable/ic_filter" - android:title="@string/filter" - app:showAsAction="always"> - - <menu> - <item android:id="@+id/check_all" - android:title="@string/all_label"/> - <item android:id="@+id/check_none" - android:title="@string/select_none_label"/> - <item android:id="@+id/check_played" - android:title="@string/played_label"/> - <item android:id="@+id/check_unplayed" - android:title="@string/unplayed_label"/> - <item android:id="@+id/check_downloaded" - android:title="@string/downloaded_label"/> - <item android:id="@+id/check_not_downloaded" - android:title="@string/not_downloaded_label"/> - <item android:id="@+id/check_queued" - android:title="@string/queued_label"/> - <item android:id="@+id/check_not_queued" - android:title="@string/not_queued_label"/> - <item android:id="@+id/check_has_media" - android:title="@string/has_media"/> - </menu> - </item> - - <item - android:id="@+id/select_toggle" - android:title="@string/select_all_label" - app:showAsAction="always"/> -</menu> diff --git a/app/src/main/res/menu/episodes_apply_action_speeddial.xml b/app/src/main/res/menu/episodes_apply_action_speeddial.xml index a2f509ec5..c9bc4b4df 100644 --- a/app/src/main/res/menu/episodes_apply_action_speeddial.xml +++ b/app/src/main/res/menu/episodes_apply_action_speeddial.xml @@ -14,21 +14,21 @@ android:title="@string/download_label" /> <item android:id="@+id/mark_unread_batch" - android:icon="@drawable/ic_cancel" + android:icon="@drawable/ic_mark_unplayed" android:title="@string/mark_unread_label" /> <item android:id="@+id/mark_read_batch" - android:icon="@drawable/ic_check" + android:icon="@drawable/ic_mark_played" android:title="@string/mark_read_label" /> <item android:id="@+id/remove_from_queue_batch" - android:icon="@drawable/ic_remove" + android:icon="@drawable/ic_playlist_remove" android:title="@string/remove_from_queue_label" /> <item android:id="@+id/add_to_queue_batch" - android:icon="@drawable/ic_add" + android:icon="@drawable/ic_playlist" android:title="@string/add_to_queue_label" /> </menu> diff --git a/app/src/main/res/menu/feeditem_options.xml b/app/src/main/res/menu/feeditem_options.xml index 5b33539e1..70400fe55 100644 --- a/app/src/main/res/menu/feeditem_options.xml +++ b/app/src/main/res/menu/feeditem_options.xml @@ -50,18 +50,6 @@ custom:showAsAction="collapseActionView" android:title="@string/reset_position"> </item> - - <item - android:id="@+id/activate_auto_download" - custom:showAsAction="collapseActionView" - android:title="@string/activate_auto_download"> - </item> - <item - android:id="@+id/deactivate_auto_download" - custom:showAsAction="collapseActionView" - android:title="@string/deactivate_auto_download"> - </item> - <item android:id="@+id/visit_website_item" android:icon="@drawable/ic_web" diff --git a/app/src/main/res/menu/feeditemlist_context.xml b/app/src/main/res/menu/feeditemlist_context.xml index 84e45d4c9..f6edec0b4 100644 --- a/app/src/main/res/menu/feeditemlist_context.xml +++ b/app/src/main/res/menu/feeditemlist_context.xml @@ -1,7 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> - <menu xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:id="@id/skip_episode_item" android:menuCategory="container" @@ -16,6 +14,7 @@ android:id="@+id/mark_read_item" android:menuCategory="container" android:title="@string/mark_read_label" /> + <item android:id="@+id/mark_unread_item" android:menuCategory="container" @@ -25,10 +24,12 @@ android:id="@+id/add_to_queue_item" android:menuCategory="container" android:title="@string/add_to_queue_label" /> + <item android:id="@+id/remove_from_queue_item" android:menuCategory="container" android:title="@string/remove_from_queue_label" /> + <item android:id="@+id/remove_item" android:menuCategory="container" @@ -38,6 +39,7 @@ android:id="@+id/add_to_favorites_item" android:menuCategory="container" android:title="@string/add_to_favorite_label" /> + <item android:id="@+id/remove_from_favorites_item" android:menuCategory="container" @@ -49,20 +51,13 @@ android:title="@string/reset_position" /> <item - android:id="@+id/activate_auto_download" - android:menuCategory="container" - android:title="@string/activate_auto_download" /> - <item - android:id="@+id/deactivate_auto_download" + android:id="@+id/share_item" android:menuCategory="container" - android:title="@string/deactivate_auto_download" /> + android:title="@string/share_label" /> <item - android:id="@+id/visit_website_item" + android:id="@+id/multi_select" android:menuCategory="container" - android:title="@string/visit_website_label" /> - <item - android:id="@+id/share_item" - android:menuCategory="container" - android:title="@string/share_label" /> + android:title="@string/multi_select" + android:visible="false" /> </menu>
\ No newline at end of file diff --git a/app/src/main/res/menu/feedlist.xml b/app/src/main/res/menu/feedlist.xml index 85e7a95ba..12cbc2e5e 100644 --- a/app/src/main/res/menu/feedlist.xml +++ b/app/src/main/res/menu/feedlist.xml @@ -33,18 +33,10 @@ <item android:id="@+id/action_search" android:icon="@drawable/ic_search" - custom:showAsAction="always|collapseActionView" - custom:actionViewClass="androidx.appcompat.widget.SearchView" + custom:showAsAction="always" android:title="@string/search_label"/> <item - android:id="@+id/episode_actions" - android:menuCategory="container" - android:icon="@drawable/ic_check_multiple" - android:title="@string/multi_select" - custom:showAsAction="collapseActionView"> - </item> - <item android:id="@+id/visit_website_item" android:icon="@drawable/ic_web" android:menuCategory="container" diff --git a/app/src/main/res/menu/multi_select_context_popup.xml b/app/src/main/res/menu/multi_select_context_popup.xml new file mode 100644 index 000000000..730b01016 --- /dev/null +++ b/app/src/main/res/menu/multi_select_context_popup.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/select_all_above" + android:title="@string/select_all_above"> + </item> + <item + android:id="@+id/select_all_below" + android:title="@string/select_all_below"> + </item> +</menu>
\ No newline at end of file diff --git a/app/src/main/res/menu/multi_select_options.xml b/app/src/main/res/menu/multi_select_options.xml new file mode 100644 index 000000000..5cb2b7602 --- /dev/null +++ b/app/src/main/res/menu/multi_select_options.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/select_toggle" + android:title="@string/select_all_label" + app:showAsAction="always"/> +</menu> diff --git a/app/src/main/res/menu/queue.xml b/app/src/main/res/menu/queue.xml index adf44b8b1..2d341d8e4 100644 --- a/app/src/main/res/menu/queue.xml +++ b/app/src/main/res/menu/queue.xml @@ -19,8 +19,7 @@ <item android:id="@+id/action_search" android:icon="@drawable/ic_search" - custom:showAsAction="collapseActionView|ifRoom" - custom:actionViewClass="androidx.appcompat.widget.SearchView" + custom:showAsAction="ifRoom" android:title="@string/search_label"/> <item @@ -115,10 +114,4 @@ android:title="@string/clear_queue_label" custom:showAsAction="collapseActionView" android:icon="@drawable/ic_check"/> - - <item - android:id="@+id/episode_actions" - custom:showAsAction="collapseActionView" - android:title="@string/multi_select" /> - </menu> diff --git a/app/src/main/res/menu/subscriptions.xml b/app/src/main/res/menu/subscriptions.xml index b1cc89eb6..9276db562 100644 --- a/app/src/main/res/menu/subscriptions.xml +++ b/app/src/main/res/menu/subscriptions.xml @@ -1,15 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto"> - + <item + android:id="@+id/action_search" + android:icon="@drawable/ic_search" + custom:showAsAction="always" + android:title="@string/search_label"/> <item android:id="@+id/refresh_item" android:title="@string/refresh_label" android:menuCategory="container" custom:showAsAction="always" android:icon="@drawable/ic_refresh"/> - - <item android:id="@+id/subscriptions_filter" android:title="@string/filter" diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index cb8f9e082..03da6c669 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -3,4 +3,5 @@ <integer name="subscriptions_default_num_of_columns">3</integer> <integer name="nav_drawer_screen_size_percent">80</integer> <integer name="swipe_to_refresh_duration_in_ms">750</integer> + <integer name="swipe_refresh_distance">300</integer> </resources>
\ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 523c7cd0f..d528945c7 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -31,7 +31,7 @@ android:key="prefScreenGpodder" android:title="@string/synchronization_pref" android:summary="@string/synchronization_sum" - android:icon="@drawable/ic_star" /> + android:icon="@drawable/ic_cloud" /> <Preference android:key="prefScreenStorage" diff --git a/app/src/main/res/xml/preferences_storage.xml b/app/src/main/res/xml/preferences_storage.xml index d3d570e6c..89e8c4cf5 100644 --- a/app/src/main/res/xml/preferences_storage.xml +++ b/app/src/main/res/xml/preferences_storage.xml @@ -6,13 +6,6 @@ <Preference android:title="@string/choose_data_directory" android:key="prefChooseDataDir"/> - <ListPreference - android:entryValues="@array/image_cache_size_values" - android:entries="@array/image_cache_size_options" - android:title="@string/pref_image_cache_size_title" - android:key="prefImageCacheSize" - android:summary="@string/pref_image_cache_size_sum" - android:defaultValue="100"/> <SwitchPreferenceCompat android:defaultValue="false" android:enabled="true" diff --git a/app/src/main/res/xml/preferences_swipe.xml b/app/src/main/res/xml/preferences_swipe.xml new file mode 100644 index 000000000..eb238ac14 --- /dev/null +++ b/app/src/main/res/xml/preferences_swipe.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> + + <Preference + android:key="prefSwipeFeed" + android:title="@string/feeds_label"/> + <Preference + android:key="prefSwipeQueue" + android:title="@string/queue_label"/> + +</PreferenceScreen> diff --git a/app/src/main/res/xml/preferences_user_interface.xml b/app/src/main/res/xml/preferences_user_interface.xml index f8e80cdff..0b2707a18 100644 --- a/app/src/main/res/xml/preferences_user_interface.xml +++ b/app/src/main/res/xml/preferences_user_interface.xml @@ -78,5 +78,9 @@ android:title="@string/pref_back_button_behavior_title" android:summary="@string/pref_back_button_behavior_sum" android:defaultValue="default"/> + <Preference + android:key="prefSwipe" + android:summary="@string/swipeactions_summary" + android:title="@string/swipeactions_label"/> </PreferenceCategory> </PreferenceScreen> |