Bug 1826473 – add new search engine settings screens
parent
76c1c11bfd
commit
e73a630bd5
@ -0,0 +1,20 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.search.ext
|
||||
|
||||
import mozilla.components.browser.state.search.SearchEngine
|
||||
import mozilla.components.browser.state.state.SearchState
|
||||
import mozilla.components.browser.state.state.availableSearchEngines
|
||||
|
||||
/**
|
||||
* The list of search engine shortcuts to be available for quick search menu.
|
||||
*/
|
||||
val SearchState.searchEngineShortcuts: List<SearchEngine>
|
||||
get() = (
|
||||
regionSearchEngines + additionalSearchEngines + availableSearchEngines +
|
||||
customSearchEngines + applicationSearchEngines
|
||||
).filter {
|
||||
!disabledSearchEngineIds.contains(it.id)
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.settings.search
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.getPreferenceKey
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
|
||||
/**
|
||||
* A [Fragment] that allows user to set the default search engine.
|
||||
*/
|
||||
class DefaultSearchEngineFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.default_search_engine_preferences, rootKey)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
view?.hideKeyboard()
|
||||
showToolbar(getString(R.string.preferences_default_search_engine))
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
when (preference.key) {
|
||||
getPreferenceKey(R.string.pref_key_add_search_engine) -> {
|
||||
val directions = DefaultSearchEngineFragmentDirections
|
||||
.actionDefaultEngineFragmentToSaveSearchEngineFragment(null)
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
}
|
||||
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.settings.search
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.browser.icons.IconRequest
|
||||
import mozilla.components.feature.search.ext.createSearchEngine
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
import org.mozilla.fenix.databinding.FragmentSaveSearchEngineBinding
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
|
||||
/**
|
||||
* A [Fragment] that allows user to add a new search engine.
|
||||
*/
|
||||
@SuppressWarnings("LargeClass", "TooManyFunctions")
|
||||
class SaveSearchEngineFragment : Fragment(R.layout.fragment_save_search_engine) {
|
||||
|
||||
private val args by navArgs<SaveSearchEngineFragmentArgs>()
|
||||
private val searchEngine by lazy {
|
||||
requireComponents.core.store.state.search.customSearchEngines.find { engine ->
|
||||
engine.id == args.searchEngineIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
private var _binding: FragmentSaveSearchEngineBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val inputListener = object : TextWatcher {
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
val bothFieldsHaveInput = binding.editEngineName.text?.isNotBlank() == true &&
|
||||
binding.editSearchString.text?.isNotBlank() == true
|
||||
binding.saveButton.isEnabled = bothFieldsHaveInput
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int,
|
||||
) = Unit
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) =
|
||||
Unit
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
_binding = FragmentSaveSearchEngineBinding.bind(view)
|
||||
|
||||
binding.saveButton.apply {
|
||||
isEnabled = false
|
||||
setOnClickListener { createCustomEngine() }
|
||||
}
|
||||
|
||||
binding.editEngineName.addTextChangedListener(inputListener)
|
||||
binding.editSearchString.addTextChangedListener(inputListener)
|
||||
|
||||
searchEngine?.let {
|
||||
val url = it.resultUrls[0]
|
||||
binding.editEngineName.setText(it.name)
|
||||
binding.editSearchString.setText(url.toEditableUrl())
|
||||
}
|
||||
|
||||
binding.customSearchEnginesLearnMoreWrapper.setOnClickListener {
|
||||
(activity as HomeActivity).openToBrowserAndLoad(
|
||||
searchTermOrURL = SupportUtils.getSumoURLForTopic(
|
||||
requireContext(),
|
||||
SupportUtils.SumoTopic.CUSTOM_SEARCH_ENGINES,
|
||||
),
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromSaveSearchEngineFragment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (args.searchEngineIdentifier != null) {
|
||||
showToolbar(getString(R.string.search_engine_edit_custom_search_engine_title))
|
||||
} else {
|
||||
showToolbar(getString(R.string.search_engine_add_custom_search_engine_title))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
private fun createCustomEngine() {
|
||||
binding.customSearchEngineNameField.error = ""
|
||||
binding.customSearchEngineSearchStringField.error = ""
|
||||
|
||||
val name = binding.editEngineName.text?.toString()?.trim() ?: ""
|
||||
val searchString = binding.editSearchString.text?.toString() ?: ""
|
||||
|
||||
if (checkForErrors(name, searchString)) {
|
||||
return
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch(Main) {
|
||||
val result = withContext(IO) {
|
||||
SearchStringValidator.isSearchStringValid(
|
||||
requireComponents.core.client,
|
||||
searchString,
|
||||
)
|
||||
}
|
||||
|
||||
when (result) {
|
||||
SearchStringValidator.Result.CannotReach -> {
|
||||
binding.customSearchEngineSearchStringField.error = resources
|
||||
.getString(R.string.search_add_custom_engine_error_cannot_reach, name)
|
||||
}
|
||||
|
||||
SearchStringValidator.Result.Success -> {
|
||||
val searchEngine = createSearchEngine(
|
||||
name,
|
||||
searchString.toSearchUrl(),
|
||||
requireComponents.core.icons.loadIcon(IconRequest(searchString))
|
||||
.await().bitmap,
|
||||
isGeneral = true,
|
||||
)
|
||||
|
||||
requireComponents.useCases.searchUseCases.addSearchEngine(searchEngine)
|
||||
|
||||
val successMessage = resources
|
||||
.getString(R.string.search_add_custom_engine_success_message, name)
|
||||
|
||||
view?.also {
|
||||
FenixSnackbar.make(
|
||||
view = it,
|
||||
duration = FenixSnackbar.LENGTH_SHORT,
|
||||
isDisplayedWithBrowserToolbar = false,
|
||||
)
|
||||
.setText(successMessage)
|
||||
.show()
|
||||
}
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForErrors(name: String, searchString: String): Boolean {
|
||||
return when {
|
||||
name.isEmpty() -> {
|
||||
binding.customSearchEngineNameField.error = resources
|
||||
.getString(R.string.search_add_custom_engine_error_empty_name)
|
||||
true
|
||||
}
|
||||
searchString.isEmpty() -> {
|
||||
binding.customSearchEngineSearchStringField.error =
|
||||
resources.getString(R.string.search_add_custom_engine_error_empty_search_string)
|
||||
true
|
||||
}
|
||||
!searchString.contains("%s") -> {
|
||||
binding.customSearchEngineSearchStringField.error =
|
||||
resources.getString(R.string.search_add_custom_engine_error_missing_template)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toEditableUrl(): String {
|
||||
return replace("{searchTerms}", "%s")
|
||||
}
|
||||
|
||||
private fun String.toSearchUrl(): String {
|
||||
return replace("%s", "{searchTerms}")
|
||||
}
|
@ -0,0 +1,316 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.settings.search
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.CheckboxDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.browser.state.search.SearchEngine
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.SearchState
|
||||
import mozilla.components.browser.state.state.availableSearchEngines
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.lib.state.ext.observeAsComposableState
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.ContextualMenu
|
||||
import org.mozilla.fenix.compose.MenuItem
|
||||
import org.mozilla.fenix.compose.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Top-level UI for search shortcuts settings.
|
||||
*
|
||||
* @param categoryTitle is used for displaying a title above the search shortcut list.
|
||||
* @param store [BrowserStore] used to listen for changes to [SearchState].
|
||||
* @param onCheckboxClicked Invoked when the user clicks on the checkbox of a search engine item.
|
||||
* @param onEditEngineClicked Invoked when the user clicks on the edit item of the three dot menu.
|
||||
* @param onDeleteEngineClicked Invoked when the user clicks on the delete item of the three dot menu.
|
||||
* @param onAddEngineClicked Invoked when the user clicks on the add search engine button.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun SearchEngineShortcuts(
|
||||
categoryTitle: String,
|
||||
store: BrowserStore,
|
||||
onCheckboxClicked: (SearchEngine, Boolean) -> Unit,
|
||||
onEditEngineClicked: (SearchEngine) -> Unit,
|
||||
onDeleteEngineClicked: (SearchEngine) -> Unit,
|
||||
onAddEngineClicked: () -> Unit,
|
||||
) {
|
||||
val searchState = store.observeAsComposableState { it.search }.value ?: SearchState()
|
||||
val searchEngines = with(searchState) {
|
||||
regionSearchEngines + additionalSearchEngines + availableSearchEngines + customSearchEngines
|
||||
}
|
||||
val disabledShortcutsIds = searchState.disabledSearchEngineIds
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.background(color = FirefoxTheme.colors.layer1)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
item {
|
||||
Title(title = categoryTitle)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
items(
|
||||
items = searchEngines,
|
||||
key = { engine -> engine.id },
|
||||
) {
|
||||
SearchItem(
|
||||
engine = it,
|
||||
name = it.name,
|
||||
isEnabled = !disabledShortcutsIds.contains(it.id),
|
||||
onCheckboxClicked = onCheckboxClicked,
|
||||
onEditEngineClicked = onEditEngineClicked,
|
||||
onDeleteEngineClicked = onDeleteEngineClicked,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
AddEngineButton(onAddEngineClicked = onAddEngineClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(title: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = FirefoxTheme.colors.textAccent,
|
||||
fontWeight = FontWeight.W400,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
style = FirefoxTheme.typography.headline8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
private fun SearchItem(
|
||||
engine: SearchEngine,
|
||||
name: String,
|
||||
isEnabled: Boolean,
|
||||
onCheckboxClicked: (SearchEngine, Boolean) -> Unit,
|
||||
onEditEngineClicked: (SearchEngine) -> Unit,
|
||||
onDeleteEngineClicked: (SearchEngine) -> Unit,
|
||||
) {
|
||||
val isMenuExpanded: MutableState<Boolean> = remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 56.dp)
|
||||
.padding(start = 4.dp),
|
||||
) {
|
||||
Checkbox(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
checked = isEnabled,
|
||||
onCheckedChange = { onCheckboxClicked.invoke(engine, it) },
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = FirefoxTheme.colors.formSelected,
|
||||
uncheckedColor = FirefoxTheme.colors.formDefault,
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.size(24.dp),
|
||||
bitmap = engine.icon.asImageBitmap(),
|
||||
contentDescription = stringResource(
|
||||
id = R.string.search_engine_icon_content_description,
|
||||
engine.name,
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(vertical = 8.dp)
|
||||
.weight(1f),
|
||||
text = name,
|
||||
style = FirefoxTheme.typography.subtitle1,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
)
|
||||
|
||||
if (engine.type == SearchEngine.Type.CUSTOM) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
isMenuExpanded.value = true
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_menu),
|
||||
contentDescription = stringResource(id = R.string.content_description_menu),
|
||||
tint = FirefoxTheme.colors.iconPrimary,
|
||||
)
|
||||
|
||||
ContextualMenu(
|
||||
showMenu = isMenuExpanded.value,
|
||||
onDismissRequest = { isMenuExpanded.value = false },
|
||||
menuItems = listOf(
|
||||
MenuItem(
|
||||
stringResource(R.string.search_engine_edit),
|
||||
color = FirefoxTheme.colors.textWarning,
|
||||
) {
|
||||
onEditEngineClicked(engine)
|
||||
},
|
||||
MenuItem(
|
||||
stringResource(R.string.search_engine_delete),
|
||||
color = FirefoxTheme.colors.textWarning,
|
||||
) {
|
||||
onDeleteEngineClicked(engine)
|
||||
},
|
||||
),
|
||||
offset = DpOffset(x = 0.dp, y = (-24).dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddEngineButton(
|
||||
onAddEngineClicked: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 56.dp)
|
||||
.padding(start = 4.dp)
|
||||
.clickable { onAddEngineClicked() },
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(68.dp))
|
||||
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
painter = painterResource(id = R.drawable.ic_new),
|
||||
contentDescription = stringResource(
|
||||
id = R.string.search_engine_add_custom_search_engine_button_content_description,
|
||||
),
|
||||
tint = FirefoxTheme.colors.iconPrimary,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(vertical = 8.dp)
|
||||
.weight(1f),
|
||||
text = stringResource(id = R.string.search_engine_add_custom_search_engine_title),
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
style = FirefoxTheme.typography.subtitle1,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@LightDarkPreview
|
||||
@Composable
|
||||
private fun SearchEngineShortcutsPreview() {
|
||||
FirefoxTheme {
|
||||
SearchEngineShortcuts(
|
||||
categoryTitle = stringResource(id = R.string.preferences_category_engines_in_search_menu),
|
||||
store = BrowserStore(
|
||||
initialState = BrowserState(
|
||||
search = SearchState(
|
||||
regionSearchEngines = generateFakeEnginesList(),
|
||||
disabledSearchEngineIds = listOf("8", "9"),
|
||||
),
|
||||
),
|
||||
),
|
||||
onCheckboxClicked = { _, _ -> },
|
||||
onEditEngineClicked = {},
|
||||
onDeleteEngineClicked = {},
|
||||
onAddEngineClicked = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateFakeEnginesList(): List<SearchEngine> {
|
||||
val dummyBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
|
||||
dummyBitmap.eraseColor(Color.BLUE)
|
||||
|
||||
return listOf(
|
||||
generateFakeEngines("1", "Google"),
|
||||
generateFakeEngines("2", "Bing"),
|
||||
generateFakeEngines("3", "Bing"),
|
||||
generateFakeEngines("4", "Amazon.com"),
|
||||
generateFakeEngines("5", "DuckDuckGo"),
|
||||
generateFakeEngines("6", "Qwant"),
|
||||
generateFakeEngines("7", "eBay"),
|
||||
generateFakeEngines("8", "Reddit"),
|
||||
generateFakeEngines("9", "YouTube"),
|
||||
generateFakeEngines("10", "Yandex", SearchEngine.Type.CUSTOM),
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateFakeEngines(
|
||||
id: String,
|
||||
name: String,
|
||||
type: SearchEngine.Type = SearchEngine.Type.BUNDLED,
|
||||
): SearchEngine {
|
||||
return SearchEngine(
|
||||
id = id,
|
||||
name = name,
|
||||
icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888).apply {
|
||||
eraseColor(Color.BLUE)
|
||||
},
|
||||
type = type,
|
||||
isGeneral = true,
|
||||
)
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.settings.search
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.MainScope
|
||||
import mozilla.components.browser.state.search.SearchEngine
|
||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.databinding.FragmentSearchShortcutsBinding
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.getRootView
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
import org.mozilla.fenix.theme.Theme
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
|
||||
/**
|
||||
* A [Fragment] that allows user to select what search engine shortcuts will be visible in the quick
|
||||
* search menu.
|
||||
*/
|
||||
class SearchShortcutsFragment : Fragment(R.layout.fragment_search_shortcuts) {
|
||||
|
||||
private var _binding: FragmentSearchShortcutsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = FragmentSearchShortcutsBinding.inflate(
|
||||
inflater,
|
||||
container,
|
||||
false,
|
||||
)
|
||||
|
||||
binding.root.setContent {
|
||||
FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) {
|
||||
SearchEngineShortcuts(
|
||||
getString(R.string.preferences_category_engines_in_search_menu),
|
||||
requireComponents.core.store,
|
||||
onEditEngineClicked = {
|
||||
navigateToSaveEngineFragment(it)
|
||||
},
|
||||
onCheckboxClicked = { engine, isEnabled ->
|
||||
requireContext().components.useCases.searchUseCases
|
||||
.updateDisabledSearchEngineIds(
|
||||
engine.id,
|
||||
isEnabled,
|
||||
)
|
||||
},
|
||||
onDeleteEngineClicked = {
|
||||
deleteSearchEngine(requireContext(), it)
|
||||
},
|
||||
onAddEngineClicked = {
|
||||
navigateToSaveEngineFragment()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private fun navigateToSaveEngineFragment(engine: SearchEngine? = null) {
|
||||
val directions = SearchShortcutsFragmentDirections
|
||||
.actionSearchShortcutsFragmentToSaveSearchEngineFragment(engine?.id)
|
||||
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
||||
private fun deleteSearchEngine(
|
||||
context: Context,
|
||||
engine: SearchEngine,
|
||||
) {
|
||||
context.components.useCases.searchUseCases.removeSearchEngine(engine)
|
||||
|
||||
MainScope().allowUndo(
|
||||
view = context.getRootView()!!,
|
||||
message = context
|
||||
.getString(R.string.search_delete_search_engine_success_message, engine.name),
|
||||
undoActionTitle = context.getString(R.string.snackbar_deleted_undo),
|
||||
onCancel = {
|
||||
context.components.useCases.searchUseCases.addSearchEngine(engine)
|
||||
},
|
||||
operation = {},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
view?.hideKeyboard()
|
||||
showToolbar(getString(R.string.preferences_manage_search_shortcuts))
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="false" android:color="?attr/textDisabled" />
|
||||
<item android:color="?attr/textOnColorPrimary"/>
|
||||
</selector>
|
@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<ScrollView android:id="@+id/search_engine_scrollview"
|
||||
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"
|
||||
tools:context="org.mozilla.fenix.settings.search.AddSearchEngineFragment"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<LinearLayout
|
||||
android:id="@+id/search_engine_wrapper"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:contentDescription="@string/search_add_custom_engine_form_description"
|
||||
android:importantForAutofill="noExcludeDescendants">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/search_add_custom_engine_name_label"
|
||||
android:textColor="?attr/textSecondary"
|
||||
android:textFontWeight="@integer/font_weight_light"
|
||||
android:textSize="12sp"
|
||||
tools:targetApi="p" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/custom_search_engine_name_field"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="8dp"
|
||||
app:hintEnabled="false"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_engine_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="@dimen/accessibility_min_height"
|
||||
android:hint="@string/search_add_custom_engine_name_hint_2"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/search_add_custom_engine_url_label"
|
||||
android:textColor="?attr/textSecondary"
|
||||
android:textFontWeight="@integer/font_weight_light"
|
||||
android:textSize="12sp"
|
||||
tools:targetApi="p" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/custom_search_engine_search_string_field"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:hintTextColor="?attr/textSecondary"
|
||||
android:textColorHint="?attr/textSecondary"
|
||||
app:hintTextAppearance="@style/EngineTextField"
|
||||
app:hintEnabled="false"
|
||||
android:paddingBottom="8dp"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_search_string"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="@dimen/accessibility_min_height"
|
||||
android:hint="@string/search_add_custom_engine_search_string_hint_2"
|
||||
android:inputType="text"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/custom_search_engines_learn_more_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:minHeight="@dimen/accessibility_min_height"
|
||||
android:paddingBottom="16dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/search_add_custom_engine_search_string_example"
|
||||
app:lineHeight="18sp"
|
||||
android:labelFor="@id/edit_search_string"
|
||||
android:textColor="@android:color/tertiary_text_dark" />
|
||||
|
||||
<org.mozilla.fenix.utils.LinkTextView
|
||||
android:id="@+id/custom_search_engines_learn_more"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/exceptions_empty_message_learn_more_link"
|
||||
android:textColor="?accent"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintTop_toBottomOf="@id/exceptions_empty_message" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/save_button"
|
||||
style="@style/PositiveButton"
|
||||
android:backgroundTint="@color/button_state_list"
|
||||
android:text="@string/search_custom_engine_save_button"
|
||||
android:textColor="@color/text_on_color_state_list_text_color"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:visibility="gone"/>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<PreferenceCategory
|
||||
android:title="@string/preferences_category_select_default_search_engine"
|
||||
android:selectable="false"
|
||||
app:iconSpaceReserved="false"
|
||||
android:layout="@layout/preference_category_no_icon_style">
|
||||
<org.mozilla.fenix.settings.search.RadioSearchEngineListPreference
|
||||
android:key="@string/pref_key_search_engine_list"
|
||||
android:selectable="false"/>
|
||||
<Preference
|
||||
android:key="@string/pref_key_add_search_engine"
|
||||
android:title="@string/search_engine_add_custom_search_engine_title"
|
||||
android:layout="@layout/preference_search_add_engine"/>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<PreferenceCategory
|
||||
android:title="Search Engines"
|
||||
android:selectable="false"
|
||||
app:iconSpaceReserved="false"
|
||||
android:layout="@layout/preference_category_no_icon_style">
|
||||
<Preference
|
||||
app:iconSpaceReserved="false"
|
||||
android:key="@string/pref_key_default_search_engine"
|
||||
android:title="@string/preferences_default_search_engine"/>
|
||||
<Preference
|
||||
app:iconSpaceReserved="false"
|
||||
android:key="@string/pref_key_manage_search_shortcuts"
|
||||
android:title="@string/preferences_manage_search_shortcuts"
|
||||
android:summary="@string/preferences_manage_search_shortcuts_summary"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/preferences_search_address_bar"
|
||||
android:selectable="false"
|
||||
app:iconSpaceReserved="false"
|
||||
android:layout="@layout/preference_category_no_icon_style">
|
||||
<SwitchPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_key_show_search_engine_shortcuts"
|
||||
android:title="@string/preferences_show_search_engines" />
|
||||
<SwitchPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_key_enable_autocomplete_urls"
|
||||
android:title="@string/preferences_enable_autocomplete_urls" />
|
||||
<SwitchPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_key_show_clipboard_suggestions"
|
||||
android:title="@string/preferences_show_clipboard_suggestions" />
|
||||
<SwitchPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_key_search_browsing_history"
|
||||
android:title='@string/preferences_search_browsing_history' />
|
||||
<SwitchPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_key_search_bookmarks"
|
||||
android:title='@string/preferences_search_bookmarks' />
|
||||
<SwitchPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_key_search_synced_tabs"
|
||||
android:title='@string/preferences_search_synced_tabs' />
|
||||
<SwitchPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_key_show_voice_search"
|
||||
android:title="@string/preferences_show_voice_search" />
|
||||
<SwitchPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_key_show_search_suggestions"
|
||||
android:title="@string/preferences_show_search_suggestions" />
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:dependency="@string/pref_key_show_search_suggestions"
|
||||
android:key="@string/pref_key_show_search_suggestions_in_private"
|
||||
android:layout="@layout/checkbox_left_preference"
|
||||
android:title="@string/preferences_show_search_suggestions_in_private"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
Loading…
Reference in New Issue