From 7fd9cbd9bc05226dd5a48ed3f613803b2a30e060 Mon Sep 17 00:00:00 2001 From: sictiru Date: Sun, 12 Feb 2023 12:00:28 -0800 Subject: [PATCH 01/31] #1644 Home activity keyboard shortcuts --- .../main/java/com/newsblur/activity/Main.java | 88 +++++++++++++++---- ...ragment.java => FeedSelectorFragment.java} | 8 +- .../com/newsblur/keyboard/KeyboardManager.kt | 64 ++++++++++++++ .../app/src/main/res/layout/activity_main.xml | 2 +- 4 files changed, 141 insertions(+), 21 deletions(-) rename clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/{FeedIntelligenceSelectorFragment.java => FeedSelectorFragment.java} (89%) create mode 100644 clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java index ca80a55de..14fbd7b7b 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java @@ -6,14 +6,11 @@ import static com.newsblur.service.NBSyncReceiver.UPDATE_REBUILD; import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS; import android.content.Intent; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.os.Bundle; import android.os.Trace; import android.preference.PreferenceManager; - -import androidx.appcompat.widget.PopupMenu; -import androidx.fragment.app.FragmentManager; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.text.Editable; import android.text.TextWatcher; import android.view.KeyEvent; @@ -22,13 +19,21 @@ import android.view.View; import android.view.View.OnKeyListener; import android.widget.AbsListView; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.PopupMenu; +import androidx.fragment.app.FragmentManager; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + import com.newsblur.R; import com.newsblur.database.BlurDatabaseHelper; import com.newsblur.databinding.ActivityMainBinding; import com.newsblur.delegate.MainContextMenuDelegate; import com.newsblur.delegate.MainContextMenuDelegateImpl; -import com.newsblur.fragment.FeedIntelligenceSelectorFragment; +import com.newsblur.fragment.FeedSelectorFragment; import com.newsblur.fragment.FolderListFragment; +import com.newsblur.keyboard.KeyboardEvent; +import com.newsblur.keyboard.KeyboardListener; +import com.newsblur.keyboard.KeyboardManager; import com.newsblur.service.BootReceiver; import com.newsblur.service.NBSyncService; import com.newsblur.util.AppConstants; @@ -45,7 +50,7 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint -public class Main extends NbActivity implements StateChangedListener, SwipeRefreshLayout.OnRefreshListener, AbsListView.OnScrollListener, PopupMenu.OnMenuItemClickListener { +public class Main extends NbActivity implements StateChangedListener, SwipeRefreshLayout.OnRefreshListener, AbsListView.OnScrollListener, PopupMenu.OnMenuItemClickListener, KeyboardListener { @Inject FeedUtils feedUtils; @@ -55,10 +60,12 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre public static final String EXTRA_FORCE_SHOW_FEED_ID = "force_show_feed_id"; - private FolderListFragment folderFeedList; + private FolderListFragment folderFeedList; + private FeedSelectorFragment feedSelectorFragment; private boolean wasSwipeEnabled = false; private ActivityMainBinding binding; private MainContextMenuDelegate contextMenuDelegate; + private KeyboardManager keyboardManager; @Override public void onCreate(Bundle savedInstanceState) { @@ -69,7 +76,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre getWindow().setBackgroundDrawableResource(android.R.color.transparent); binding = ActivityMainBinding.inflate(getLayoutInflater()); contextMenuDelegate = new MainContextMenuDelegateImpl(this, dbHelper); - setContentView(binding.getRoot()); + keyboardManager = new KeyboardManager(); + setContentView(binding.getRoot()); // set the status bar to an generic loading message when the activity is first created so // that something is displayed while the service warms up @@ -82,7 +90,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre FragmentManager fragmentManager = getSupportFragmentManager(); folderFeedList = (FolderListFragment) fragmentManager.findFragmentByTag("folderFeedListFragment"); - ((FeedIntelligenceSelectorFragment) fragmentManager.findFragmentByTag("feedIntelligenceSelector")).setState(folderFeedList.currentState); + feedSelectorFragment = ((FeedSelectorFragment) fragmentManager.findFragmentByTag("feedIntelligenceSelector")); + feedSelectorFragment.setState(folderFeedList.currentState); // make sure the interval sync is scheduled, since we are the root Activity BootReceiver.scheduleSyncService(this); @@ -127,12 +136,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre // Check whether it's a shortcut intent String shortcutExtra = getIntent().getStringExtra(ShortcutUtils.SHORTCUT_EXTRA); if (shortcutExtra != null && shortcutExtra.startsWith(ShortcutUtils.SHORTCUT_ALL_STORIES)) { - Intent intent = new Intent(this, AllStoriesItemsList.class); - intent.putExtra(ItemsList.EXTRA_FEED_SET, FeedSet.allFeeds()); - if (shortcutExtra.equals(ShortcutUtils.SHORTCUT_ALL_STORIES_SEARCH)) { - intent.putExtra(ItemsList.EXTRA_VISIBLE_SEARCH, true); - } - startActivity(intent); + boolean isAllStoriesSearch = shortcutExtra.equals(ShortcutUtils.SHORTCUT_ALL_STORIES_SEARCH); + openAllStories(isAllStoriesSearch); } Trace.endSection(); @@ -177,6 +182,7 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre updateStatusIndicators(); folderFeedList.pushUnreadCounts(); folderFeedList.checkOpenFolderPreferences(); + keyboardManager.addListener(this); triggerSync(); } @@ -211,7 +217,15 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre if ((updateType & UPDATE_METADATA) != 0) { folderFeedList.hasUpdated(); } - } + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY) { + // TODO check if overrides BACK action + return keyboardManager.onKeyUp(keyCode, event); + } else return super.onKeyUp(keyCode, event); + } public void updateUnreadCounts(int neutCount, int posiCount) { binding.mainUnreadCountNeutText.setText(Integer.toString(neutCount)); @@ -325,4 +339,46 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre } folderFeedList.setSearchQuery(q); } + + private void openAllStories(boolean isAllStoriesSearch) { + Intent intent = new Intent(this, AllStoriesItemsList.class); + intent.putExtra(ItemsList.EXTRA_FEED_SET, FeedSet.allFeeds()); + intent.putExtra(ItemsList.EXTRA_VISIBLE_SEARCH, isAllStoriesSearch); + startActivity(intent); + } + + private void switchViewStateLeft() { + StateFilter currentState = folderFeedList.currentState; + if (currentState.equals(StateFilter.SAVED)) { + feedSelectorFragment.setState(StateFilter.BEST); + } else if (currentState.equals(StateFilter.BEST)) { + feedSelectorFragment.setState(StateFilter.SOME); + } else if (currentState.equals(StateFilter.SOME)) { + feedSelectorFragment.setState(StateFilter.ALL); + } + } + + private void switchViewStateRight() { + StateFilter currentState = folderFeedList.currentState; + if (currentState.equals(StateFilter.ALL)) { + feedSelectorFragment.setState(StateFilter.SOME); + } else if (currentState.equals(StateFilter.SOME)) { + feedSelectorFragment.setState(StateFilter.BEST); + } else if (currentState.equals(StateFilter.BEST)) { + feedSelectorFragment.setState(StateFilter.SAVED); + } + } + + @Override + public void onKeyboardEvent(@NonNull KeyboardEvent event) { + if (event instanceof KeyboardEvent.AddFeed) { + onClickAddButton(); + } else if (event instanceof KeyboardEvent.OpenAllStories) { + openAllStories(false); + } else if (event instanceof KeyboardEvent.SwitchViewLeft) { + switchViewStateLeft(); + } else if (event instanceof KeyboardEvent.SwitchViewRight) { + switchViewStateRight(); + } + } } diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/FeedIntelligenceSelectorFragment.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/FeedSelectorFragment.java similarity index 89% rename from clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/FeedIntelligenceSelectorFragment.java rename to clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/FeedSelectorFragment.java index f95d364e2..2ba596652 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/FeedIntelligenceSelectorFragment.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/FeedSelectorFragment.java @@ -11,10 +11,10 @@ import com.newsblur.view.StateToggleButton; import com.newsblur.view.StateToggleButton.StateChangedListener; import com.newsblur.util.StateFilter; -public class FeedIntelligenceSelectorFragment extends Fragment implements StateChangedListener { - +public class FeedSelectorFragment extends Fragment implements StateChangedListener { + private StateToggleButton button; - + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View v = inflater.inflate(R.layout.fragment_intelligenceselector, null); @@ -27,7 +27,7 @@ public class FeedIntelligenceSelectorFragment extends Fragment implements StateC public void changedState(StateFilter state) { ((StateChangedListener) getActivity()).changedState(state); } - + public void setState(StateFilter state) { button.setState(state); } diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt new file mode 100644 index 000000000..6cc89e483 --- /dev/null +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt @@ -0,0 +1,64 @@ +package com.newsblur.keyboard + +import android.view.KeyEvent + +class KeyboardManager { + + private var listener: KeyboardListener? = null + + fun addListener(listener: KeyboardListener) { + this.listener = listener + } + + fun removeListener() { + this.listener = null + } + + /** + * @return Return true to prevent this event from being propagated + * further, or false to indicate that you have not handled + */ + fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean = when (keyCode) { + KeyEvent.KEYCODE_E -> { + handleKeycodeE(event) + } + KeyEvent.KEYCODE_A -> { + handleKeycodeA(event) + } + KeyEvent.KEYCODE_DPAD_RIGHT -> { + listener?.onKeyboardEvent(KeyboardEvent.SwitchViewRight) + true + } + KeyEvent.KEYCODE_DPAD_LEFT -> { + listener?.onKeyboardEvent(KeyboardEvent.SwitchViewLeft) + true + } + else -> false + } + + private fun handleKeycodeE(event: KeyEvent): Boolean = if (event.isAltPressed) { + listener?.onKeyboardEvent(KeyboardEvent.OpenAllStories) + true + } else false + + private fun handleKeycodeA(event: KeyEvent): Boolean = if (event.isAltPressed) { + listener?.onKeyboardEvent(KeyboardEvent.AddFeed) + true + } else false +} + +interface KeyboardListener { + + fun onKeyboardEvent(event: KeyboardEvent) +} + +sealed class KeyboardEvent { + + object OpenAllStories : KeyboardEvent() + + object AddFeed : KeyboardEvent() + + object SwitchViewRight : KeyboardEvent() + + object SwitchViewLeft : KeyboardEvent() +} \ No newline at end of file diff --git a/clients/android/NewsBlur/app/src/main/res/layout/activity_main.xml b/clients/android/NewsBlur/app/src/main/res/layout/activity_main.xml index 5f7e9a5e2..c4ee1186e 100644 --- a/clients/android/NewsBlur/app/src/main/res/layout/activity_main.xml +++ b/clients/android/NewsBlur/app/src/main/res/layout/activity_main.xml @@ -103,7 +103,7 @@ to be defined first so that other things can be placed above it. --> Date: Mon, 13 Feb 2023 08:52:21 -0800 Subject: [PATCH 02/31] #1644 Story keyboard shortcuts --- .../main/java/com/newsblur/activity/Main.java | 29 ++++-- .../java/com/newsblur/activity/Reading.kt | 74 +++++++++++---- .../newsblur/fragment/ReadingItemFragment.kt | 84 +++++++++++------ .../com/newsblur/keyboard/KeyboardEvent.kt | 38 ++++++++ .../com/newsblur/keyboard/KeyboardManager.kt | 89 ++++++++++++++++--- .../main/java/com/newsblur/util/UIUtils.java | 5 ++ .../src/main/res/layout/activity_reading.xml | 3 +- .../app/src/main/res/values/strings.xml | 8 +- 8 files changed, 264 insertions(+), 66 deletions(-) create mode 100644 clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardEvent.kt diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java index 14fbd7b7b..eccf00b22 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java @@ -6,7 +6,6 @@ import static com.newsblur.service.NBSyncReceiver.UPDATE_REBUILD; import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS; import android.content.Intent; -import android.content.res.Configuration; import android.graphics.Bitmap; import android.os.Bundle; import android.os.Trace; @@ -186,7 +185,13 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre triggerSync(); } - @Override + @Override + protected void onPause() { + keyboardManager.removeListener(); + super.onPause(); + } + + @Override public void changedState(StateFilter state) { if ( !( (state == StateFilter.ALL) || (state == StateFilter.SOME) || @@ -219,12 +224,24 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre } } + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (KeyboardManager.hasHardwareKeyboard(this)) { + boolean isKnownKeyCode = keyboardManager.isKnownKeyCode(keyCode, true); + if (isKnownKeyCode) return true; + else return super.onKeyDown(keyCode, event); + } + return super.onKeyDown(keyCode, event); + } + @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY) { - // TODO check if overrides BACK action - return keyboardManager.onKeyUp(keyCode, event); - } else return super.onKeyUp(keyCode, event); + if (KeyboardManager.hasHardwareKeyboard(this)) { + boolean handledKeyCode = keyboardManager.onKeyUp(keyCode, event); + if (handledKeyCode) return true; + else return super.onKeyUp(keyCode, event); + } + return super.onKeyUp(keyCode, event); } public void updateUnreadCounts(int neutCount, int posiCount) { diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt index 2af892d3a..787d5660a 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt @@ -23,6 +23,9 @@ import com.newsblur.di.IconLoader import com.newsblur.domain.Story import com.newsblur.fragment.ReadingItemFragment import com.newsblur.fragment.ReadingPagerFragment +import com.newsblur.keyboard.KeyboardEvent +import com.newsblur.keyboard.KeyboardListener +import com.newsblur.keyboard.KeyboardManager import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_REBUILD import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STATUS import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY @@ -42,7 +45,7 @@ import javax.inject.Inject import kotlin.math.abs @AndroidEntryPoint -abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListener { +abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListener, KeyboardListener { @Inject lateinit var feedUtils: FeedUtils @@ -84,6 +87,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene private var isMultiWindowModeHack = false private val pageHistory = mutableListOf() + private val keyboardManager = KeyboardManager() private lateinit var volumeKeyNavigation: VolumeKeyNavigation private lateinit var intelState: StateFilter @@ -165,10 +169,12 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene // the correct session, but that can be delayed by sync backup, so we try here to // reduce UI lag, or in case somehow we got redisplayed in a zero-story state feedUtils.prepareReadingSession(fs, false) + keyboardManager.addListener(this) } override fun onPause() { super.onPause() + keyboardManager.removeListener() if (isMultiWindowModeHack) { isMultiWindowModeHack = false } else { @@ -737,6 +743,10 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene return if (isVolumeKeyNavigationEvent(keyCode)) { processVolumeKeyNavigationEvent(keyCode) true + } else if (KeyboardManager.hasHardwareKeyboard(this)) { + val isKnownKeyCode = keyboardManager.isKnownKeyCode(keyCode) + if (isKnownKeyCode) true + else super.onKeyDown(keyCode, event) } else { super.onKeyDown(keyCode, event) } @@ -748,24 +758,32 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene private fun processVolumeKeyNavigationEvent(keyCode: Int) { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && volumeKeyNavigation == VolumeKeyNavigation.DOWN_NEXT || keyCode == KeyEvent.KEYCODE_VOLUME_UP && volumeKeyNavigation == VolumeKeyNavigation.UP_NEXT) { - if (pager == null) return - val nextPosition = pager!!.currentItem + 1 - if (nextPosition < readingAdapter!!.count) { - try { - pager!!.currentItem = nextPosition - } catch (e: Exception) { - // Just in case cursor changes. - } - } + nextStory() } else { - if (pager == null) return - val nextPosition = pager!!.currentItem - 1 - if (nextPosition >= 0) { - try { - pager!!.currentItem = nextPosition - } catch (e: Exception) { - // Just in case cursor changes. - } + previousStory() + } + } + + private fun nextStory() { + if (pager == null) return + val nextPosition = pager!!.currentItem + 1 + if (nextPosition < readingAdapter!!.count) { + try { + pager!!.currentItem = nextPosition + } catch (e: Exception) { + // Just in case cursor changes. + } + } + } + + private fun previousStory() { + if (pager == null) return + val nextPosition = pager!!.currentItem - 1 + if (nextPosition >= 0) { + try { + pager!!.currentItem = nextPosition + } catch (e: Exception) { + // Just in case cursor changes. } } } @@ -774,6 +792,10 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene // Required to prevent the default sound playing when the volume key is pressed return if (isVolumeKeyNavigationEvent(keyCode)) { true + } else if (KeyboardManager.hasHardwareKeyboard(this)) { + val handledKeyCode = keyboardManager.onKeyUp(keyCode, event) + if (handledKeyCode) true + else super.onKeyUp(keyCode, event) } else { super.onKeyUp(keyCode, event) } @@ -797,6 +819,22 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene if (isActive) feedUtils.markStoryAsRead(story, this@Reading) } + override fun onKeyboardEvent(event: KeyboardEvent) { + when (event) { + KeyboardEvent.NextStory -> nextStory() + KeyboardEvent.PreviousStory -> previousStory() + KeyboardEvent.NextUnreadStory -> nextUnread() + KeyboardEvent.OpenInBrowser -> readingFragment?.openBrowser() + KeyboardEvent.OpenStoryTrainer -> readingFragment?.openStoryTrainer() + KeyboardEvent.SaveUnsaveStory -> readingFragment?.switchStorySavedState(true) + KeyboardEvent.ScrollToComments -> readingFragment?.scrollToComments() + KeyboardEvent.ShareStory -> readingFragment?.openShareDialog() + KeyboardEvent.ToggleReadUnread -> readingFragment?.switchMarkStoryReadState(true) + KeyboardEvent.ToggleTextView -> readingFragment?.switchSelectedViewMode() + else -> {} + } + } + companion object { const val EXTRA_FEEDSET = "feed_set" const val EXTRA_STORY_HASH = "story_hash" diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt index 909b1e6a3..4178f5a38 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt @@ -12,13 +12,16 @@ import android.util.Log import android.view.* import android.view.ContextMenu.ContextMenuInfo import android.webkit.WebView.HitTestResult +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip +import com.google.android.material.snackbar.Snackbar import com.newsblur.R import com.newsblur.activity.FeedItemsList import com.newsblur.activity.Reading @@ -198,10 +201,10 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.storyContextMenuButton.setOnClickListener { onClickMenuButton() } - readingItemActionsBinding.markReadStoryButton.setOnClickListener { clickMarkStoryRead() } - readingItemActionsBinding.trainStoryButton.setOnClickListener { clickTrain() } - readingItemActionsBinding.saveStoryButton.setOnClickListener { clickSave() } - readingItemActionsBinding.shareStoryButton.setOnClickListener { clickShare() } + readingItemActionsBinding.markReadStoryButton.setOnClickListener { switchMarkStoryReadState() } + readingItemActionsBinding.trainStoryButton.setOnClickListener { openStoryTrainer() } + readingItemActionsBinding.saveStoryButton.setOnClickListener { switchStorySavedState() } + readingItemActionsBinding.shareStoryButton.setOnClickListener { openShareDialog() } } override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenuInfo?) { @@ -298,8 +301,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) { R.id.menu_reading_original -> { - val uri = Uri.parse(story!!.permalink) - UIUtils.handleUri(requireContext(), uri) + openBrowser() true } R.id.menu_reading_sharenewsblur -> { @@ -408,7 +410,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { R.id.menu_intel -> { // check against training on feedless stories if (story!!.feedId != "0") { - clickTrain() + openStoryTrainer() } true } @@ -421,9 +423,19 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { } } - private fun clickMarkStoryRead() { - if (story!!.read) feedUtils.markStoryUnread(story!!, requireContext()) - else feedUtils.markStoryAsRead(story!!, requireContext()) + fun switchMarkStoryReadState(notifyUser: Boolean = false) { + story?.let { + val msg = if (it.read) { + feedUtils.markStoryUnread(it, requireContext()) + getString(R.string.story_unread) + } + else { + feedUtils.markStoryAsRead(it, requireContext()) + getString(R.string.story_read) + } + if (notifyUser) UIUtils.showSnackBar(binding.root, msg) + } ?: Log.e(this.javaClass.name, "Error switching null story read state.") + } private fun updateMarkStoryReadState() { @@ -437,7 +449,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { sampledQueue?.add { updateStoryReadTitleState.invoke() } ?: updateStoryReadTitleState.invoke() } - private fun clickTrain() { + fun openStoryTrainer() { val intelFrag = StoryIntelTrainerFragment.newInstance(story, fs) intelFrag.show(requireActivity().supportFragmentManager, StoryIntelTrainerFragment::class.java.name) } @@ -446,19 +458,25 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { readingItemActionsBinding.trainStoryButton.visibility = if (story!!.feedId == "0") View.GONE else View.VISIBLE } - private fun clickSave() { - if (story!!.starred) { - feedUtils.setStorySaved(story!!.storyHash, false, requireContext()) - } else { - feedUtils.setStorySaved(story!!.storyHash, true, requireContext()) - } + fun switchStorySavedState(notifyUser: Boolean = false) { + story?.let { + val msg = if (it.starred) { + feedUtils.setStorySaved(it.storyHash, false, requireContext()) + getString(R.string.story_saved) + } else { + feedUtils.setStorySaved(it.storyHash, true, requireContext()) + getString(R.string.story_unsaved) + } + if (notifyUser) UIUtils.showSnackBar(binding.root, msg) + + } ?: Log.e(this.javaClass.name, "Error switching null story saved state.") } private fun updateSaveButton() { readingItemActionsBinding.saveStoryButton.setText(if (story!!.starred) R.string.unsave_this else R.string.save_this) } - private fun clickShare() { + fun openShareDialog() { val newFragment: DialogFragment = ShareDialogFragment.newInstance(story, sourceUserId) newFragment.show(parentFragmentManager, "dialog") } @@ -530,17 +548,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { val intelFrag = StoryIntelTrainerFragment.newInstance(story, fs) intelFrag.show(parentFragmentManager, StoryIntelTrainerFragment::class.java.name) }) - binding.readingItemTitle.setOnClickListener(object : View.OnClickListener { - override fun onClick(v: View) { - try { - UIUtils.handleUri(requireContext(), Uri.parse(story!!.permalink)) - } catch (t: Throwable) { - // we don't actually know if the user will successfully be able to open whatever string - // was in the permalink or if the Intent could throw errors - Log.e(this.javaClass.name, "Error opening story by permalink URL.", t) - } - } - }) + binding.readingItemTitle.setOnClickListener { openBrowser() } setupTagsAndIntel() } @@ -989,6 +997,24 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { reloadStoryContent() } + fun openBrowser() { + story?.let { + val uri = Uri.parse(it.permalink) + UIUtils.handleUri(requireContext(), uri) + } ?: Log.e(this.javaClass.name, "Error opening null story by permalink URL.") + } + + fun scrollToComments() { + val targetView = if (readingItemActionsBinding.readingFriendCommentHeader.isVisible) { + readingItemActionsBinding.readingFriendCommentContainer + } else if (readingItemActionsBinding.readingPublicCommentHeader.isVisible) { + readingItemActionsBinding.readingPublicCommentContainer + } else null + targetView?.let { + it.parent.requestChildFocus(targetView, it) + } + } + companion object { private const val BUNDLE_SCROLL_POS_REL = "scrollStateRel" diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardEvent.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardEvent.kt new file mode 100644 index 000000000..f0319d699 --- /dev/null +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardEvent.kt @@ -0,0 +1,38 @@ +package com.newsblur.keyboard + +interface KeyboardListener { + + fun onKeyboardEvent(event: KeyboardEvent) +} + +sealed class KeyboardEvent { + + /** + * Keyboard events for Home + */ + object OpenAllStories : KeyboardEvent() + object AddFeed : KeyboardEvent() + object SwitchViewRight : KeyboardEvent() + object SwitchViewLeft : KeyboardEvent() + + /** + * Keyboard events for Reading + */ + object NextStory : KeyboardEvent() + object PreviousStory : KeyboardEvent() + object ToggleTextView : KeyboardEvent() + object NextUnreadStory : KeyboardEvent() + object ToggleReadUnread : KeyboardEvent() + object SaveUnsaveStory : KeyboardEvent() + object OpenInBrowser : KeyboardEvent() + object ShareStory : KeyboardEvent() + object ScrollToComments : KeyboardEvent() + object OpenStoryTrainer : KeyboardEvent() + + /** + * TODO + * disable descendant focusability + * add back in the space and arrows key codes + * write tests + */ +} diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt index 6cc89e483..cfe6ea975 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt @@ -1,5 +1,7 @@ package com.newsblur.keyboard +import android.content.Context +import android.content.res.Configuration import android.view.KeyEvent class KeyboardManager { @@ -19,6 +21,9 @@ class KeyboardManager { * further, or false to indicate that you have not handled */ fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean = when (keyCode) { + /** + * Home events + */ KeyEvent.KEYCODE_E -> { handleKeycodeE(event) } @@ -33,6 +38,46 @@ class KeyboardManager { listener?.onKeyboardEvent(KeyboardEvent.SwitchViewLeft) true } + /** + * Story events + */ + KeyEvent.KEYCODE_J -> { + listener?.onKeyboardEvent(KeyboardEvent.PreviousStory) + true + } + KeyEvent.KEYCODE_K -> { + listener?.onKeyboardEvent(KeyboardEvent.NextStory) + true + } + KeyEvent.KEYCODE_N -> { + listener?.onKeyboardEvent(KeyboardEvent.NextUnreadStory) + true + } + KeyEvent.KEYCODE_W -> { + listener?.onKeyboardEvent(KeyboardEvent.ToggleTextView) + true + } + KeyEvent.KEYCODE_U, KeyEvent.KEYCODE_M -> { + listener?.onKeyboardEvent(KeyboardEvent.ToggleReadUnread) + true + } + KeyEvent.KEYCODE_S -> { + if (event.isShiftPressed) listener?.onKeyboardEvent(KeyboardEvent.ShareStory) + else listener?.onKeyboardEvent(KeyboardEvent.SaveUnsaveStory) + true + } + KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_V -> { + listener?.onKeyboardEvent(KeyboardEvent.OpenInBrowser) + true + } + KeyEvent.KEYCODE_C -> { + listener?.onKeyboardEvent(KeyboardEvent.ScrollToComments) + true + } + KeyEvent.KEYCODE_T -> { + listener?.onKeyboardEvent(KeyboardEvent.OpenStoryTrainer) + true + } else -> false } @@ -45,20 +90,42 @@ class KeyboardManager { listener?.onKeyboardEvent(KeyboardEvent.AddFeed) true } else false -} -interface KeyboardListener { + fun isKnownKeyCode(keyCode: Int, overridePad: Boolean = false): Boolean { + val isShortcutKey = isShortcutKeyCode(keyCode) + return if (overridePad) isShortcutKey && isPadKeyCode(keyCode) + else isShortcutKey + } - fun onKeyboardEvent(event: KeyboardEvent) -} + private fun isPadKeyCode(keyCode: Int) = when (keyCode) { + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT, + -> true + else -> false + } -sealed class KeyboardEvent { + private fun isShortcutKeyCode(keyCode: Int) = when (keyCode) { + KeyEvent.KEYCODE_E, + KeyEvent.KEYCODE_A, + KeyEvent.KEYCODE_J, + KeyEvent.KEYCODE_K, + KeyEvent.KEYCODE_N, + KeyEvent.KEYCODE_U, + KeyEvent.KEYCODE_M, + KeyEvent.KEYCODE_S, + KeyEvent.KEYCODE_O, + KeyEvent.KEYCODE_V, + KeyEvent.KEYCODE_C, + KeyEvent.KEYCODE_T, + KeyEvent.KEYCODE_W, + -> true + else -> false + } - object OpenAllStories : KeyboardEvent() + companion object { - object AddFeed : KeyboardEvent() - - object SwitchViewRight : KeyboardEvent() - - object SwitchViewLeft : KeyboardEvent() + @JvmStatic + fun hasHardwareKeyboard(context: Context) = + context.resources.configuration.keyboard == Configuration.KEYBOARD_QWERTY + } } \ No newline at end of file diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java index 3f85e59fe..785d49375 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java @@ -44,6 +44,7 @@ import androidx.core.content.ContextCompat; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.color.MaterialColors; +import com.google.android.material.snackbar.Snackbar; import com.newsblur.NbApplication; import com.newsblur.R; import com.newsblur.activity.*; @@ -599,4 +600,8 @@ public class UIUtils { context.sendBroadcast(intent); } } + + public static void showSnackBar(View view, String message) { + Snackbar.make(view, message, 750).show(); + } } diff --git a/clients/android/NewsBlur/app/src/main/res/layout/activity_reading.xml b/clients/android/NewsBlur/app/src/main/res/layout/activity_reading.xml index f0d9b67c9..d5bbb5b2e 100644 --- a/clients/android/NewsBlur/app/src/main/res/layout/activity_reading.xml +++ b/clients/android/NewsBlur/app/src/main/res/layout/activity_reading.xml @@ -2,7 +2,8 @@ + android:layout_height="match_parent" + android:descendantFocusability="blocksDescendants"> diff --git a/clients/android/NewsBlur/app/src/main/res/values/strings.xml b/clients/android/NewsBlur/app/src/main/res/values/strings.xml index 3f0be3cca..7f5556e99 100644 --- a/clients/android/NewsBlur/app/src/main/res/values/strings.xml +++ b/clients/android/NewsBlur/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - + NewsBlur @@ -739,4 +739,10 @@ Permissions is required for posting notifications Notifications permission must be added manually in the app\'s settings before trying again to enable notifications + + Story marked as saved + Story marked as unsaved + Story marked as read + Story marked as unread + From de77e11100629cc57da644a51e9795c069e45f2c Mon Sep 17 00:00:00 2001 From: sictiru Date: Sun, 26 Feb 2023 17:44:17 -0800 Subject: [PATCH 03/31] #1644 Keyboard shortcuts --- .../main/java/com/newsblur/activity/Main.java | 20 ++++++---- .../java/com/newsblur/activity/Reading.kt | 5 +++ .../newsblur/fragment/ReadingItemFragment.kt | 7 +++- .../fragment/StoryIntelTrainerFragment.java | 20 +++++----- .../com/newsblur/keyboard/KeyboardEvent.kt | 2 + .../com/newsblur/keyboard/KeyboardManager.kt | 37 ++++++++++++------- .../main/java/com/newsblur/util/UIUtils.java | 2 +- .../app/src/main/res/layout/activity_main.xml | 4 +- .../app/src/main/res/values/strings.xml | 4 ++ .../app/src/main/res/values/styles.xml | 22 +++++++++++ .../app/src/main/res/values/theme.xml | 6 +++ 11 files changed, 93 insertions(+), 36 deletions(-) diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java index eccf00b22..a9a488615 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Main.java @@ -19,6 +19,7 @@ import android.view.View.OnKeyListener; import android.widget.AbsListView; import androidx.annotation.NonNull; +import androidx.annotation.StringRes; import androidx.appcompat.widget.PopupMenu; import androidx.fragment.app.FragmentManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -227,7 +228,7 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (KeyboardManager.hasHardwareKeyboard(this)) { - boolean isKnownKeyCode = keyboardManager.isKnownKeyCode(keyCode, true); + boolean isKnownKeyCode = keyboardManager.isKnownKeyCode(keyCode); if (isKnownKeyCode) return true; else return super.onKeyDown(keyCode, event); } @@ -367,25 +368,30 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre private void switchViewStateLeft() { StateFilter currentState = folderFeedList.currentState; if (currentState.equals(StateFilter.SAVED)) { - feedSelectorFragment.setState(StateFilter.BEST); + setAndNotifySelectorState(StateFilter.BEST, R.string.focused_stories); } else if (currentState.equals(StateFilter.BEST)) { - feedSelectorFragment.setState(StateFilter.SOME); + setAndNotifySelectorState(StateFilter.SOME, R.string.unread_stories); } else if (currentState.equals(StateFilter.SOME)) { - feedSelectorFragment.setState(StateFilter.ALL); + setAndNotifySelectorState(StateFilter.ALL, R.string.all_stories); } } private void switchViewStateRight() { StateFilter currentState = folderFeedList.currentState; if (currentState.equals(StateFilter.ALL)) { - feedSelectorFragment.setState(StateFilter.SOME); + setAndNotifySelectorState(StateFilter.SOME, R.string.unread_stories); } else if (currentState.equals(StateFilter.SOME)) { - feedSelectorFragment.setState(StateFilter.BEST); + setAndNotifySelectorState(StateFilter.BEST, R.string.focused_stories); } else if (currentState.equals(StateFilter.BEST)) { - feedSelectorFragment.setState(StateFilter.SAVED); + setAndNotifySelectorState(StateFilter.SAVED, R.string.saved_stories); } } + private void setAndNotifySelectorState(StateFilter state, @StringRes int notifyMsgRes) { + feedSelectorFragment.setState(state); + UIUtils.showSnackBar(binding.getRoot(), getString(notifyMsgRes)); + } + @Override public void onKeyboardEvent(@NonNull KeyboardEvent event) { if (event instanceof KeyboardEvent.AddFeed) { diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt index 787d5660a..d3244e6e4 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/activity/Reading.kt @@ -22,6 +22,7 @@ import com.newsblur.databinding.ActivityReadingBinding import com.newsblur.di.IconLoader import com.newsblur.domain.Story import com.newsblur.fragment.ReadingItemFragment +import com.newsblur.fragment.ReadingItemFragment.Companion.VERTICAL_SCROLL_DISTANCE_DP import com.newsblur.fragment.ReadingPagerFragment import com.newsblur.keyboard.KeyboardEvent import com.newsblur.keyboard.KeyboardListener @@ -831,6 +832,10 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene KeyboardEvent.ShareStory -> readingFragment?.openShareDialog() KeyboardEvent.ToggleReadUnread -> readingFragment?.switchMarkStoryReadState(true) KeyboardEvent.ToggleTextView -> readingFragment?.switchSelectedViewMode() + KeyboardEvent.PageDown -> + readingFragment?.scrollVerticallyBy(UIUtils.dp2px(this, VERTICAL_SCROLL_DISTANCE_DP)) + KeyboardEvent.PageUp -> + readingFragment?.scrollVerticallyBy(UIUtils.dp2px(this, -VERTICAL_SCROLL_DISTANCE_DP)) else -> {} } } diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt index 4178f5a38..017b3773e 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/ReadingItemFragment.kt @@ -12,7 +12,6 @@ import android.util.Log import android.view.* import android.view.ContextMenu.ContextMenuInfo import android.webkit.WebView.HitTestResult -import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat @@ -21,7 +20,6 @@ import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip -import com.google.android.material.snackbar.Snackbar import com.newsblur.R import com.newsblur.activity.FeedItemsList import com.newsblur.activity.Reading @@ -1015,8 +1013,13 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener { } } + fun scrollVerticallyBy(dy: Int) { + binding.readingScrollview.smoothScrollBy(0, dy) + } + companion object { private const val BUNDLE_SCROLL_POS_REL = "scrollStateRel" + const val VERTICAL_SCROLL_DISTANCE_DP = 240 @JvmStatic fun newInstance(story: Story?, feedTitle: String?, feedFaviconColor: String?, feedFaviconFade: String?, feedFaviconBorder: String?, faviconText: String?, faviconUrl: String?, classifier: Classifier?, displayFeedDetails: Boolean, sourceUserId: String?): ReadingItemFragment { diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/StoryIntelTrainerFragment.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/StoryIntelTrainerFragment.java index af1b966b4..40f4f0638 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/StoryIntelTrainerFragment.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/fragment/StoryIntelTrainerFragment.java @@ -2,17 +2,16 @@ package com.newsblur.fragment; import java.util.Map; -import android.app.Activity; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import android.text.InputType; import android.text.TextUtils; import android.view.Gravity; -import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.widget.TextView; @@ -57,6 +56,7 @@ public class StoryIntelTrainerFragment extends DialogFragment { return fragment; } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -64,9 +64,7 @@ public class StoryIntelTrainerFragment extends DialogFragment { fs = (FeedSet) getArguments().getSerializable("feedset"); classifier = dbHelper.getClassifierForFeed(story.feedId); - final Activity activity = getActivity(); - LayoutInflater inflater = LayoutInflater.from(activity); - View v = inflater.inflate(R.layout.dialog_trainstory, null); + View v = getLayoutInflater().inflate(R.layout.dialog_trainstory, null); binding = DialogTrainstoryBinding.bind(v); // set up the special title training box for the title from this story and the associated buttons @@ -111,7 +109,7 @@ public class StoryIntelTrainerFragment extends DialogFragment { // scan trained title fragments for this feed and see if any apply to this story for (Map.Entry rule : classifier.title.entrySet()) { if (story.title.indexOf(rule.getKey()) >= 0) { - View row = inflater.inflate(R.layout.include_intel_row, null); + View row = getLayoutInflater().inflate(R.layout.include_intel_row, null); TextView label = (TextView) row.findViewById(R.id.intel_row_label); label.setText(rule.getKey()); UIUtils.setupIntelDialogRow(row, classifier.title, rule.getKey()); @@ -121,7 +119,7 @@ public class StoryIntelTrainerFragment extends DialogFragment { // list all tags for this story, trained or not for (String tag : story.tags) { - View row = inflater.inflate(R.layout.include_intel_row, null); + View row = getLayoutInflater().inflate(R.layout.include_intel_row, null); TextView label = (TextView) row.findViewById(R.id.intel_row_label); label.setText(tag); UIUtils.setupIntelDialogRow(row, classifier.tags, tag); @@ -131,7 +129,7 @@ public class StoryIntelTrainerFragment extends DialogFragment { // there is a single author per story if (!TextUtils.isEmpty(story.authors)) { - View rowAuthor = inflater.inflate(R.layout.include_intel_row, null); + View rowAuthor = getLayoutInflater().inflate(R.layout.include_intel_row, null); TextView labelAuthor = (TextView) rowAuthor.findViewById(R.id.intel_row_label); labelAuthor.setText(story.authors); UIUtils.setupIntelDialogRow(rowAuthor, classifier.authors, story.authors); @@ -142,13 +140,13 @@ public class StoryIntelTrainerFragment extends DialogFragment { // there is a single feed to be trained, but it is a bit odd in that the label is the title and // the intel identifier is the feed ID - View rowFeed = inflater.inflate(R.layout.include_intel_row, null); + View rowFeed = getLayoutInflater().inflate(R.layout.include_intel_row, null); TextView labelFeed = (TextView) rowFeed.findViewById(R.id.intel_row_label); labelFeed.setText(feedUtils.getFeedTitle(story.feedId)); UIUtils.setupIntelDialogRow(rowFeed, classifier.feeds, story.feedId); binding.existingFeedIntelContainer.addView(rowFeed); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); + AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(R.string.story_intel_dialog_title); builder.setView(v); @@ -164,7 +162,7 @@ public class StoryIntelTrainerFragment extends DialogFragment { if ((newTitleTraining != null) && (!TextUtils.isEmpty(binding.intelTitleSelection.getSelection()))) { classifier.title.put(binding.intelTitleSelection.getSelection(), newTitleTraining); } - feedUtils.updateClassifier(story.feedId, classifier, fs, activity); + feedUtils.updateClassifier(story.feedId, classifier, fs, requireActivity()); StoryIntelTrainerFragment.this.dismiss(); } }); diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardEvent.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardEvent.kt index f0319d699..627582d6c 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardEvent.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardEvent.kt @@ -28,6 +28,8 @@ sealed class KeyboardEvent { object ShareStory : KeyboardEvent() object ScrollToComments : KeyboardEvent() object OpenStoryTrainer : KeyboardEvent() + object PageDown: KeyboardEvent() + object PageUp: KeyboardEvent() /** * TODO diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt b/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt index cfe6ea975..09607440a 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/keyboard/KeyboardManager.kt @@ -41,11 +41,13 @@ class KeyboardManager { /** * Story events */ - KeyEvent.KEYCODE_J -> { + KeyEvent.KEYCODE_J, + KeyEvent.KEYCODE_DPAD_DOWN -> { listener?.onKeyboardEvent(KeyboardEvent.PreviousStory) true } - KeyEvent.KEYCODE_K -> { + KeyEvent.KEYCODE_K, + KeyEvent.KEYCODE_DPAD_UP -> { listener?.onKeyboardEvent(KeyboardEvent.NextStory) true } @@ -53,10 +55,6 @@ class KeyboardManager { listener?.onKeyboardEvent(KeyboardEvent.NextUnreadStory) true } - KeyEvent.KEYCODE_W -> { - listener?.onKeyboardEvent(KeyboardEvent.ToggleTextView) - true - } KeyEvent.KEYCODE_U, KeyEvent.KEYCODE_M -> { listener?.onKeyboardEvent(KeyboardEvent.ToggleReadUnread) true @@ -78,6 +76,18 @@ class KeyboardManager { listener?.onKeyboardEvent(KeyboardEvent.OpenStoryTrainer) true } + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_NUMPAD_ENTER -> { + if (event.isShiftPressed) { + listener?.onKeyboardEvent(KeyboardEvent.ToggleTextView) + true + } else false + } + KeyEvent.KEYCODE_SPACE -> { + if (event.isShiftPressed) listener?.onKeyboardEvent(KeyboardEvent.PageUp) + else listener?.onKeyboardEvent(KeyboardEvent.PageDown) + true + } else -> false } @@ -91,15 +101,17 @@ class KeyboardManager { true } else false - fun isKnownKeyCode(keyCode: Int, overridePad: Boolean = false): Boolean { - val isShortcutKey = isShortcutKeyCode(keyCode) - return if (overridePad) isShortcutKey && isPadKeyCode(keyCode) - else isShortcutKey - } + fun isKnownKeyCode(keyCode: Int): Boolean = + isShortcutKeyCode(keyCode) && isSpecialKeyCode(keyCode) - private fun isPadKeyCode(keyCode: Int) = when (keyCode) { + private fun isSpecialKeyCode(keyCode: Int) = when (keyCode) { KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_NUMPAD_ENTER, + KeyEvent.KEYCODE_SPACE, -> true else -> false } @@ -117,7 +129,6 @@ class KeyboardManager { KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_T, - KeyEvent.KEYCODE_W, -> true else -> false } diff --git a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java index 785d49375..73f81652b 100644 --- a/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java +++ b/clients/android/NewsBlur/app/src/main/java/com/newsblur/util/UIUtils.java @@ -602,6 +602,6 @@ public class UIUtils { } public static void showSnackBar(View view, String message) { - Snackbar.make(view, message, 750).show(); + Snackbar.make(view, message, 600).show(); } } diff --git a/clients/android/NewsBlur/app/src/main/res/layout/activity_main.xml b/clients/android/NewsBlur/app/src/main/res/layout/activity_main.xml index c4ee1186e..630e1e991 100644 --- a/clients/android/NewsBlur/app/src/main/res/layout/activity_main.xml +++ b/clients/android/NewsBlur/app/src/main/res/layout/activity_main.xml @@ -1,10 +1,10 @@ - + android:animateLayoutChanges="true" + android:descendantFocusability="blocksDescendants"> Story marked as read Story marked as unread + Unread stories + Focused stories + Saved stories + diff --git a/clients/android/NewsBlur/app/src/main/res/values/styles.xml b/clients/android/NewsBlur/app/src/main/res/values/styles.xml index 5f1a41c64..9be5daad0 100644 --- a/clients/android/NewsBlur/app/src/main/res/values/styles.xml +++ b/clients/android/NewsBlur/app/src/main/res/values/styles.xml @@ -46,6 +46,7 @@ @color/bar_background @color/gray30 + + @@ -540,4 +542,24 @@ 40dp + + + + + + + + diff --git a/clients/android/NewsBlur/app/src/main/res/values/theme.xml b/clients/android/NewsBlur/app/src/main/res/values/theme.xml index 79b7b99f7..49f5618f9 100644 --- a/clients/android/NewsBlur/app/src/main/res/values/theme.xml +++ b/clients/android/NewsBlur/app/src/main/res/values/theme.xml @@ -61,6 +61,8 @@ @font/whitney @style/circleProgressIndicator @style/toggleButton + @style/materialSnackBarTheme + @style/materialSnackBarTextView