🚀 Add new text selection actions (Share/Web)

Added new actions to text selection toolbar. Share action shares selected text to the app you want to. Web Search action searches selected text in your browser.

Partly resolves: #46
This commit is contained in:
Acclorite 2024-08-14 21:12:29 +03:00
parent a830be1403
commit 3fdc289720
6 changed files with 162 additions and 6 deletions

View file

@ -29,8 +29,10 @@ import ua.acclorite.book_story.R
private const val MENU_ITEM_COPY = 0 private const val MENU_ITEM_COPY = 0
private const val MENU_ITEM_TRANSLATE = 1 private const val MENU_ITEM_SHARE = 1
private const val MENU_ITEM_DICTIONARY = 2 private const val MENU_ITEM_WEB = 2
private const val MENU_ITEM_TRANSLATE = 3
private const val MENU_ITEM_DICTIONARY = 4
/** /**
* Custom Text ActionMode callback. Used in pair with [CustomSelectionToolbar]. Follow [TextToolbar] for more info. * Custom Text ActionMode callback. Used in pair with [CustomSelectionToolbar]. Follow [TextToolbar] for more info.
@ -39,6 +41,8 @@ private class CustomTextActionModeCallback(
private val context: Context, private val context: Context,
var rect: Rect = Rect.Zero, var rect: Rect = Rect.Zero,
var onCopyRequested: (() -> Unit)? = null, var onCopyRequested: (() -> Unit)? = null,
var onShareRequested: (() -> Unit)? = null,
var onWebSearchRequested: (() -> Unit)? = null,
var onTranslateRequested: (() -> Unit)? = null, var onTranslateRequested: (() -> Unit)? = null,
var onDictionaryRequested: (() -> Unit)? = null var onDictionaryRequested: (() -> Unit)? = null
) : ActionMode.Callback { ) : ActionMode.Callback {
@ -51,13 +55,23 @@ private class CustomTextActionModeCallback(
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
} }
onShareRequested?.let {
menu.add(0, MENU_ITEM_SHARE, 1, context.getString(R.string.share))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
onWebSearchRequested?.let {
menu.add(0, MENU_ITEM_WEB, 2, context.getString(R.string.web_search))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
onTranslateRequested?.let { onTranslateRequested?.let {
menu.add(0, MENU_ITEM_TRANSLATE, 1, context.getString(R.string.translate)) menu.add(0, MENU_ITEM_TRANSLATE, 3, context.getString(R.string.translate))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
} }
onDictionaryRequested?.let { onDictionaryRequested?.let {
menu.add(0, MENU_ITEM_DICTIONARY, 2, context.getString(R.string.dictionary)) menu.add(0, MENU_ITEM_DICTIONARY, 4, context.getString(R.string.dictionary))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
} }
@ -71,6 +85,8 @@ private class CustomTextActionModeCallback(
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
when (item!!.itemId) { when (item!!.itemId) {
MENU_ITEM_COPY -> onCopyRequested?.invoke() MENU_ITEM_COPY -> onCopyRequested?.invoke()
MENU_ITEM_SHARE -> onShareRequested?.invoke()
MENU_ITEM_WEB -> onWebSearchRequested?.invoke()
MENU_ITEM_TRANSLATE -> onTranslateRequested?.invoke() MENU_ITEM_TRANSLATE -> onTranslateRequested?.invoke()
MENU_ITEM_DICTIONARY -> onDictionaryRequested?.invoke() MENU_ITEM_DICTIONARY -> onDictionaryRequested?.invoke()
else -> return false else -> return false
@ -116,12 +132,15 @@ private class FloatingTextActionModeCallback(
} }
/** /**
* Custom Selection Toolbar. Used in pair with [CustomSelectionContainer] to display custom toolbar. * Custom Selection Toolbar.
* Used in pair with [CustomSelectionContainer] to display custom toolbar.
*/ */
private class CustomSelectionToolbar( private class CustomSelectionToolbar(
private val view: View, private val view: View,
context: Context, context: Context,
private val onCopyRequest: (() -> Unit)?, private val onCopyRequest: (() -> Unit)?,
private val onShareRequest: ((String) -> Unit)?,
private val onWebSearchRequest: ((String) -> Unit)?,
private val onTranslateRequest: ((String) -> Unit)?, private val onTranslateRequest: ((String) -> Unit)?,
private val onDictionaryRequest: ((String) -> Unit)? private val onDictionaryRequest: ((String) -> Unit)?
) : TextToolbar { ) : TextToolbar {
@ -145,6 +164,36 @@ private class CustomSelectionToolbar(
onCopyRequested?.invoke() onCopyRequested?.invoke()
onCopyRequest?.invoke() onCopyRequest?.invoke()
} }
callback.onShareRequested = {
val previousClipboard = clipboardManager.primaryClip
onCopyRequested?.invoke()
val currentClipboard = clipboardManager.text
onShareRequest?.invoke(currentClipboard.toString())
if (previousClipboard != null) {
clipboardManager.setPrimaryClip(
previousClipboard
)
} else {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
}
}
callback.onWebSearchRequested = {
val previousClipboard = clipboardManager.primaryClip
onCopyRequested?.invoke()
val currentClipboard = clipboardManager.text
onWebSearchRequest?.invoke(currentClipboard.toString())
if (previousClipboard != null) {
clipboardManager.setPrimaryClip(
previousClipboard
)
} else {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
}
}
callback.onTranslateRequested = { callback.onTranslateRequested = {
val previousClipboard = clipboardManager.primaryClip val previousClipboard = clipboardManager.primaryClip
onCopyRequested?.invoke() onCopyRequested?.invoke()
@ -205,6 +254,8 @@ private class CustomSelectionToolbar(
@Composable @Composable
fun CustomSelectionContainer( fun CustomSelectionContainer(
onCopyRequested: (() -> Unit), onCopyRequested: (() -> Unit),
onShareRequested: ((String) -> Unit),
onWebSearchRequested: ((String) -> Unit),
onTranslateRequested: ((String) -> Unit), onTranslateRequested: ((String) -> Unit),
onDictionaryRequested: ((String) -> Unit), onDictionaryRequested: ((String) -> Unit),
content: @Composable (toolbarHidden: Boolean) -> Unit content: @Composable (toolbarHidden: Boolean) -> Unit
@ -220,6 +271,12 @@ fun CustomSelectionContainer(
onCopyRequest = { onCopyRequest = {
onCopyRequested() onCopyRequested()
}, },
onShareRequest = {
onShareRequested(it)
},
onWebSearchRequest = {
onWebSearchRequested(it)
},
onTranslateRequest = { onTranslateRequest = {
onTranslateRequested(it) onTranslateRequested(it)
}, },

View file

@ -274,6 +274,36 @@ private fun ReaderScreen(
).show() ).show()
} }
}, },
onShareRequested = { textToShare ->
onEvent(
ReaderEvent.OnOpenShareApp(
textToShare = textToShare,
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_share_app),
Toast.LENGTH_SHORT
).show()
}
)
)
},
onWebSearchRequested = { textToSearch ->
onEvent(
ReaderEvent.OnOpenWebBrowser(
textToSearch = textToSearch,
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_browser),
Toast.LENGTH_SHORT
).show()
}
)
)
},
onTranslateRequested = { textToTranslate -> onTranslateRequested = { textToTranslate ->
onEvent( onEvent(
ReaderEvent.OnOpenTranslator( ReaderEvent.OnOpenTranslator(
@ -298,7 +328,7 @@ private fun ReaderScreen(
noAppsFound = { noAppsFound = {
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.error_no_browser), context.getString(R.string.error_no_dictionary),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }

View file

@ -51,6 +51,18 @@ sealed class ReaderEvent {
val noAppsFound: () -> Unit val noAppsFound: () -> Unit
) : ReaderEvent() ) : ReaderEvent()
data class OnOpenShareApp(
val textToShare: String,
val context: ComponentActivity,
val noAppsFound: () -> Unit
) : ReaderEvent()
data class OnOpenWebBrowser(
val textToSearch: String,
val context: ComponentActivity,
val noAppsFound: () -> Unit
) : ReaderEvent()
data class OnOpenDictionary( data class OnOpenDictionary(
val textToDefine: String, val textToDefine: String,
val context: ComponentActivity, val context: ComponentActivity,

View file

@ -360,6 +360,55 @@ class ReaderViewModel @Inject constructor(
} }
} }
is ReaderEvent.OnOpenShareApp -> {
launch {
val shareIntent = Intent()
shareIntent.action = Intent.ACTION_SEND
shareIntent.type = "text/plain"
shareIntent.putExtra(
Intent.EXTRA_SUBJECT,
event.context.getString(R.string.app_name)
)
shareIntent.putExtra(
Intent.EXTRA_TEXT,
event.textToShare.trim()
)
var shareFailure = false
shareIntent.launchActivity(event.context, createChooser = true) {
shareFailure = true
}
if (!shareFailure) {
return@launch
}
event.noAppsFound()
}
}
is ReaderEvent.OnOpenWebBrowser -> {
launch {
val browserIntent = Intent()
browserIntent.action = Intent.ACTION_WEB_SEARCH
browserIntent.putExtra(
SearchManager.QUERY,
event.textToSearch
)
var browserFailure = false
browserIntent.launchActivity(event.context) {
browserFailure = true
}
if (!browserFailure) {
return@launch
}
event.noAppsFound()
}
}
is ReaderEvent.OnOpenDictionary -> { is ReaderEvent.OnOpenDictionary -> {
launch { launch {
val dictionaryIntent = Intent() val dictionaryIntent = Intent()

View file

@ -101,7 +101,9 @@
<string name="error_query">Помилка: %1$s</string> <string name="error_query">Помилка: %1$s</string>
<string name="error_file_empty">Цей файл пустий або пошкоджений.</string> <string name="error_file_empty">Цей файл пустий або пошкоджений.</string>
<string name="error_no_translator">Не знайдено жодного перекладача.</string> <string name="error_no_translator">Не знайдено жодного перекладача.</string>
<string name="error_no_dictionary">Не знайдено жодного словника.</string>
<string name="error_no_browser">Не знайдено жодного браузера.</string> <string name="error_no_browser">Не знайдено жодного браузера.</string>
<string name="error_no_share_app">Не знайдено застосунка щоб поділитися.</string>
<string name="error_no_text"> <string name="error_no_text">
Вміст книги пустий. Будь ласка, перевірте вміст книги та оновіть її. Вміст книги пустий. Будь ласка, перевірте вміст книги та оновіть її.
</string> </string>
@ -160,6 +162,8 @@
<string name="download">Завантажити</string> <string name="download">Завантажити</string>
<string name="done">Готово</string> <string name="done">Готово</string>
<string name="enable">Увімкнути</string> <string name="enable">Увімкнути</string>
<string name="web_search">Веб-пошук</string>
<string name="share">Поділитися</string>
<!-- Items --> <!-- Items -->
<string name="files">файли</string> <string name="files">файли</string>

View file

@ -118,7 +118,9 @@
<string name="error_query">Error: %1$s</string> <string name="error_query">Error: %1$s</string>
<string name="error_file_empty">This file is empty or corrupted.</string> <string name="error_file_empty">This file is empty or corrupted.</string>
<string name="error_no_translator">No translator app found.</string> <string name="error_no_translator">No translator app found.</string>
<string name="error_no_dictionary">No dictionary app found.</string>
<string name="error_no_browser">No browser app found.</string> <string name="error_no_browser">No browser app found.</string>
<string name="error_no_share_app">No app to share found.</string>
<string name="error_no_text"> <string name="error_no_text">
The book content is empty. Please, make sure that the book content is not empty and update it. The book content is empty. Please, make sure that the book content is not empty and update it.
</string> </string>
@ -176,6 +178,8 @@
<string name="download">Download</string> <string name="download">Download</string>
<string name="done">Done</string> <string name="done">Done</string>
<string name="enable">Enable</string> <string name="enable">Enable</string>
<string name="web_search">Web search</string>
<string name="share">Share</string>
<!-- Items --> <!-- Items -->
<string name="files">files</string> <string name="files">files</string>