feat(SearchOptions): add Chrome policy warning for search engine selection removal

This commit is contained in:
alexsparkes
2025-10-28 19:41:30 +00:00
parent e1ad5d490b
commit 0e852e8a89
5 changed files with 115 additions and 329 deletions

View File

@@ -144,3 +144,40 @@ h4 {
pointer-events: none;
transition: 0.4s ease-in-out;
}
// Warning banner (used in Search settings and potentially others)
.itemWarning {
padding: 10px 20px;
display: flex;
flex-flow: row;
gap: 15px;
align-items: center;
.text {
display: flex;
flex-flow: column;
.header {
font-weight: 600;
margin-bottom: 5px;
}
}
svg {
@include themed {
background-image: t($slightGradient);
box-shadow: t($boxShadow);
}
padding: 7px;
border-radius: 100%;
font-size: 24px;
min-width: 24px;
}
@include themed {
background: t($modal-sidebar);
border-radius: t($borderRadius);
box-shadow: 0 0 0 1px t($modal-sidebarActive);
}
}

View File

@@ -1,94 +1,20 @@
/* global chrome */
import variables from 'config/variables';
import { memo, createRef, useEffect, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { MdSearch, MdMic, MdScreenSearchDesktop } from 'react-icons/md';
import { BsGoogle, BsBing } from 'react-icons/bs';
import { SiDuckduckgo, SiBaidu, SiNaver } from 'react-icons/si';
import { FaYandex, FaYahoo } from 'react-icons/fa';
import { memo, createRef, useEffect, useState, useCallback } from 'react';
import { MdSearch, MdMic } from 'react-icons/md';
import { Tooltip } from 'components/Elements';
import { Autocomplete as AutocompleteInput } from './components/autocomplete';
import EventBus from 'utils/eventbus';
import './search.scss';
import searchEngines from './search_engines.json';
function Search() {
const [url, setURL] = useState('');
const [query, setQuery] = useState('');
const [microphone, setMicrophone] = useState(null);
const [suggestions, setSuggestions] = useState([]);
const [searchDropdown, setSearchDropdown] = useState(false);
const [classList] = useState(
localStorage.getItem('widgetStyle') === 'legacy' ? 'searchIcons old' : 'searchIcons',
);
const [currentSearch, setCurrentSearch] = useState('');
useEffect(() => {
EventBus.on('refresh', (data) => {
if (data === 'search') {
init();
}
});
init();
if (localStorage.getItem('searchFocus') === 'true') {
const element = document.getElementById('searchtext');
if (element) {
element.focus();
}
}
return () => {
EventBus.off('refresh');
};
}, [init]);
const micIcon = createRef();
const customText = variables
.getMessage('modals.main.settings.sections.search.custom')
.split(' ')[0];
function init() {
let _url;
let _query = 'q';
const setting = localStorage.getItem('searchEngine');
const info = searchEngines.find((i) => i.settingsName === setting);
if (info !== undefined) {
_url = info.url;
if (info.query) {
_query = info.query;
}
}
if (setting === 'custom') {
const custom = localStorage.getItem('customSearchEngine');
if (custom !== null) {
_url = custom;
}
}
if (localStorage.getItem('voiceSearch') === 'true') {
setMicrophone(
<button
className="navbarButton"
onClick={startSpeechRecognition}
ref={micIcon}
aria-label="Microphone Search"
>
<MdMic className="micIcon" />
</button>,
);
}
setURL(_url);
setQuery(_query);
setCurrentSearch(info ? info.name : 'Custom');
}
function startSpeechRecognition() {
const startSpeechRecognition = useCallback(() => {
const voiceSearch = new window.webkitSpeechRecognition();
voiceSearch.start();
@@ -108,98 +34,67 @@ function Search() {
setTimeout(() => {
variables.stats.postEvent('feature', 'Voice search');
window.location.href = url + `?${query}=` + searchText.value;
// Use Chrome Search API - respects user's default search engine
if (chrome && chrome.search && chrome.search.query) {
chrome.search.query({
text: searchText.value,
disposition: 'CURRENT_TAB',
}).catch((error) => {
console.error('Search API error:', error);
// Fallback to Google search if API fails
window.location.href = `https://www.google.com/search?q=${encodeURIComponent(searchText.value)}`;
});
} else {
// Fallback for browsers without chrome.search API
window.location.href = `https://www.google.com/search?q=${encodeURIComponent(searchText.value)}`;
}
}, 1000);
};
}
}, [micIcon]);
const init = useCallback(() => {
if (localStorage.getItem('voiceSearch') === 'true') {
setMicrophone(
<button
className="navbarButton"
onClick={startSpeechRecognition}
ref={micIcon}
aria-label="Microphone Search"
>
<MdMic className="micIcon" />
</button>,
);
}
}, [micIcon, startSpeechRecognition]);
useEffect(() => {
init();
if (localStorage.getItem('searchFocus') === 'true') {
const element = document.getElementById('searchtext');
if (element) {
element.focus();
}
}
}, [init]);
function searchButton(e) {
e.preventDefault();
const value = e.target.value || document.getElementById('searchtext').value || 'mue fast';
variables.stats.postEvent('feature', 'Search');
window.location.href = url + `?${query}=` + value;
}
async function getSuggestions(input) {
if (input === '') {
setSuggestions([]);
return;
}
const results = await (
await fetch(`${variables.constants.API_URL}/search/autocomplete?q=${input}`)
).json();
try {
setSuggestions(results.suggestions.splice(0, 5));
} catch (e) {
console.error(e);
// ignore error if empty
}
}
const getSuggestionsDebounced = useDebouncedCallback(getSuggestions, 100);
// const getSuggestionsDebounced = getSuggestions;
/**
* If the user selects a search engine from the dropdown menu, the function will set the state of the
* search engine to the selected search engine.
* @param {string} name - The name of the search engine
* @param {boolean} custom - If the search engine is custom
*/
function setSearch(name, custom) {
let _url;
let _query = 'q';
const info = searchEngines.find((i) => i.name === name);
if (info !== undefined) {
_url = info.url;
if (info.query) {
_query = info.query;
}
}
if (custom) {
const customSetting = localStorage.getItem('customSearchEngine');
if (customSetting !== null) {
_url = customSetting;
} else {
_url = url;
}
// Use Chrome Search API - respects user's default search engine
if (chrome && chrome.search && chrome.search.query) {
chrome.search.query({
text: value,
disposition: 'CURRENT_TAB',
}).catch((error) => {
console.error('Search API error:', error);
// Fallback to Google search if API fails
window.location.href = `https://www.google.com/search?q=${encodeURIComponent(value)}`;
});
} else {
localStorage.setItem('searchEngine', info.settingsName);
}
setURL(_url);
setQuery(_query);
setCurrentSearch(name);
setSearchDropdown(false);
}
/**
* Gets the icon for the search engine dropdown.
* @param {string} name - The name of the search engine.
* @returns A React component.
*/
function getSearchDropdownicon(name) {
switch (name) {
case 'Google':
return <BsGoogle />;
case 'DuckDuckGo':
return <SiDuckduckgo />;
case 'Bing':
return <BsBing />;
case 'Yahoo':
case 'Yahoo! JAPAN':
return <FaYahoo />;
case 'Яндекс':
return <FaYandex />;
case '百度':
return <SiBaidu />;
case 'NAVER':
return <SiNaver />;
default:
return <MdScreenSearchDesktop />;
// Fallback for browsers without chrome.search API
window.location.href = `https://www.google.com/search?q=${encodeURIComponent(value)}`;
}
}
@@ -207,21 +102,6 @@ function Search() {
<div className="searchComponents">
<div className="searchMain">
<div className={classList}>
{localStorage.getItem('searchDropdown') === 'true' ? (
<Tooltip
title={variables.getMessage('modals.main.settings.sections.search.search_engine')}
>
<button
className="navbarButton"
aria-label="Search Engine"
onClick={() => setSearchDropdown(!searchDropdown)}
>
{getSearchDropdownicon(currentSearch)}
</button>
</Tooltip>
) : (
''
)}
<Tooltip
title={variables.getMessage('modals.main.settings.sections.search.voice_search')}
>
@@ -236,44 +116,14 @@ function Search() {
</button>
</Tooltip>
</div>
<AutocompleteInput
<input
type="text"
placeholder={variables.getMessage('widgets.search')}
id="searchtext"
suggestions={suggestions}
onChange={(e) => getSuggestionsDebounced(e)}
onClick={searchButton}
className="searchInput"
/>
</form>
</div>
<div>
{localStorage.getItem('searchDropdown') === 'true' && searchDropdown === true && (
<div className="searchDropdown">
{searchEngines.map(({ name }, key) => {
return (
<span
className={
'searchDropdownList' +
(currentSearch === name ? ' searchDropdownListActive' : '')
}
onClick={() => setSearch(name)}
key={key}
>
{name}
</span>
);
})}
<span
className={
'searchDropdownList' +
(currentSearch === customText ? ' searchDropdownListActive' : '')
}
onClick={() => setSearch(customText, 'custom')}
>
{customText}
</span>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,72 +1,32 @@
import variables from 'config/variables';
import { PureComponent } from 'react';
import { toast } from 'react-toastify';
import { TextField } from '@mui/material';
import { MdOutlineWarning } from 'react-icons/md';
import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings';
import { Dropdown, Checkbox } from 'components/Form/Settings';
import { Checkbox } from 'components/Form/Settings';
import EventBus from 'utils/eventbus';
import searchEngines from '../search_engines.json';
class SearchOptions extends PureComponent {
constructor() {
super();
this.state = {
customEnabled: false,
customValue: localStorage.getItem('customSearchEngine') || '',
};
}
resetSearch() {
localStorage.removeItem('customSearchEngine');
this.setState({
customValue: '',
});
toast(variables.getMessage('toasts.reset'));
}
componentDidMount() {
if (localStorage.getItem('searchEngine') === 'custom') {
this.setState({
customEnabled: true,
});
} else {
localStorage.removeItem('customSearchEngine');
}
}
componentDidUpdate() {
if (this.state.customEnabled === true && this.state.customValue !== '') {
localStorage.setItem('customSearchEngine', this.state.customValue);
}
EventBus.emit('refresh', 'search');
}
setSearchEngine(input) {
if (input === 'custom') {
this.setState({
customEnabled: true,
});
} else {
this.setState({
customEnabled: false,
});
localStorage.setItem('searchEngine', input);
}
EventBus.emit('refresh', 'search');
}
render() {
const SEARCH_SECTION = 'modals.main.settings.sections.search';
const ChromePolicyWarning = () => {
return (
<div className="itemWarning" style={{ marginBottom: '20px' }}>
<MdOutlineWarning />
<div className="text">
<span className="header">Search Engine Selection Removed</span>
<span>{variables.getMessage(`${SEARCH_SECTION}.chrome_policy_warning`)}</span>
</div>
</div>
);
};
const AdditionalOptions = () => {
return (
<Row>
<Row final={true}>
<Content
title={variables.getMessage('modals.main.settings.additional_settings')}
subtitle={variables.getMessage(`${SEARCH_SECTION}.additional`)}
@@ -80,74 +40,12 @@ class SearchOptions extends PureComponent {
category="search"
/>
) : null}
<Checkbox
name="searchDropdown"
text={variables.getMessage(`${SEARCH_SECTION}.dropdown`)}
category="search"
element=".other"
/>
<Checkbox
name="searchFocus"
text={variables.getMessage(`${SEARCH_SECTION}.focus`)}
category="search"
element=".other"
/>
<Checkbox
name="autocomplete"
text={variables.getMessage(`${SEARCH_SECTION}.autocomplete`)}
category="search"
/>
</Action>
</Row>
);
};
const SearchEngineSelection = () => {
return (
<Row final={!this.state.customEnabled}>
<Content
title={variables.getMessage(`${SEARCH_SECTION}.search_engine`)}
subtitle={variables.getMessage(
'modals.main.settings.sections.search.search_engine_subtitle',
)}
/>
<Action>
<Dropdown
name="searchEngine"
onChange={(value) => this.setSearchEngine(value)}
items={[
...searchEngines.map((engine) => ({
value: engine.settingsName,
text: engine.name,
})),
{
value: 'custom',
text: variables.getMessage(`${SEARCH_SECTION}.custom`),
},
]}
/>
</Action>
</Row>
);
};
const CustomOptions = () => {
return (
<Row final={true}>
<Content title={variables.getMessage(`${SEARCH_SECTION}.custom`)} />
<Action>
<TextField
label={variables.getMessage(`${SEARCH_SECTION}.custom`)}
value={this.state.customValue}
onInput={(e) => this.setState({ customValue: e.target.value })}
varient="outlined"
InputLabelProps={{ shrink: true }}
/>
<p style={{ marginTop: '0px' }}>
<span className="link" onClick={() => this.resetSearch()}>
{variables.getMessage('modals.main.settings.buttons.reset')}
</span>
</p>
</Action>
</Row>
);
@@ -162,9 +60,8 @@ class SearchOptions extends PureComponent {
visibilityToggle={true}
/>
<PreferencesWrapper setting="searchBar" category="widgets" visibilityToggle={true}>
<ChromePolicyWarning />
<AdditionalOptions />
<SearchEngineSelection />
{this.state.customEnabled && CustomOptions()}
</PreferencesWrapper>
</>
);

View File

@@ -285,7 +285,8 @@
"autocomplete_provider_subtitle": "Search engine to use for autocomplete dropdown results",
"voice_search": "Voice search",
"dropdown": "Search Dropdown",
"focus": "Focus on tab open"
"focus": "Focus on tab open",
"chrome_policy_warning": "In version 7.2, the ability to choose your search engine within Mue was removed to comply with new Chrome Web Store guidelines. Mue now uses your browser's default search engine. You can change this in Chrome Settings."
},
"weather": {
"title": "Weather",