2021-01-07 18:30:53 +02:00
|
|
|
import * as React from 'react';
|
|
|
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
|
|
import SearchInput, { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
|
|
|
|
import styled from 'styled-components';
|
|
|
|
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
|
|
|
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
|
|
|
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
|
|
|
import PluginBox, { InstallState } from './PluginBox';
|
2021-01-24 20:45:42 +02:00
|
|
|
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
2021-01-07 18:30:53 +02:00
|
|
|
import { _ } from '@joplin/lib/locale';
|
|
|
|
import useOnInstallHandler from './useOnInstallHandler';
|
2021-09-21 17:22:19 +02:00
|
|
|
import { themeStyle } from '@joplin/lib/theme';
|
2021-01-07 18:30:53 +02:00
|
|
|
|
|
|
|
const Root = styled.div`
|
|
|
|
`;
|
|
|
|
|
|
|
|
const ResultsRoot = styled.div`
|
|
|
|
display: flex;
|
|
|
|
flex-wrap: wrap;
|
|
|
|
`;
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
themeId: number;
|
|
|
|
searchQuery: string;
|
|
|
|
onSearchQueryChange(event: OnChangeEvent): void;
|
|
|
|
pluginSettings: PluginSettings;
|
|
|
|
onPluginSettingsChange(event: any): void;
|
|
|
|
renderDescription: Function;
|
2021-01-09 15:14:39 +02:00
|
|
|
maxWidth: number;
|
2021-01-20 00:58:09 +02:00
|
|
|
repoApi(): RepositoryApi;
|
|
|
|
disabled: boolean;
|
2021-01-07 18:30:53 +02:00
|
|
|
}
|
|
|
|
|
2021-09-01 13:17:20 +02:00
|
|
|
function sortManifestResults(results: PluginManifest[]): PluginManifest[] {
|
|
|
|
return results.sort((m1, m2) => {
|
|
|
|
if (m1._recommended && !m2._recommended) return -1;
|
|
|
|
if (!m1._recommended && m2._recommended) return +1;
|
|
|
|
return m1.name.toLowerCase() < m2.name.toLowerCase() ? -1 : +1;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-01-07 18:30:53 +02:00
|
|
|
export default function(props: Props) {
|
|
|
|
const [searchStarted, setSearchStarted] = useState(false);
|
|
|
|
const [manifests, setManifests] = useState<PluginManifest[]>([]);
|
2021-01-20 00:58:09 +02:00
|
|
|
const asyncSearchQueue = useRef(new AsyncActionQueue(10));
|
2021-01-07 18:30:53 +02:00
|
|
|
const [installingPluginsIds, setInstallingPluginIds] = useState<Record<string, boolean>>({});
|
|
|
|
const [searchResultCount, setSearchResultCount] = useState(null);
|
|
|
|
|
2021-01-20 00:58:09 +02:00
|
|
|
const onInstall = useOnInstallHandler(setInstallingPluginIds, props.pluginSettings, props.repoApi, props.onPluginSettingsChange, false);
|
2021-01-07 18:30:53 +02:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setSearchResultCount(null);
|
|
|
|
asyncSearchQueue.current.push(async () => {
|
|
|
|
if (!props.searchQuery) {
|
|
|
|
setManifests([]);
|
|
|
|
setSearchResultCount(null);
|
|
|
|
} else {
|
2021-01-20 00:58:09 +02:00
|
|
|
const r = await props.repoApi().search(props.searchQuery);
|
2021-09-01 13:17:20 +02:00
|
|
|
setManifests(sortManifestResults(r));
|
2021-01-07 18:30:53 +02:00
|
|
|
setSearchResultCount(r.length);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}, [props.searchQuery]);
|
|
|
|
|
|
|
|
const onChange = useCallback((event: OnChangeEvent) => {
|
|
|
|
setSearchStarted(true);
|
|
|
|
props.onSearchQueryChange(event);
|
|
|
|
}, [props.onSearchQueryChange]);
|
|
|
|
|
|
|
|
const onSearchButtonClick = useCallback(() => {
|
|
|
|
setSearchStarted(false);
|
|
|
|
props.onSearchQueryChange({ value: '' });
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
function installState(pluginId: string): InstallState {
|
|
|
|
const settings = props.pluginSettings[pluginId];
|
|
|
|
if (settings && !settings.deleted) return InstallState.Installed;
|
|
|
|
if (installingPluginsIds[pluginId]) return InstallState.Installing;
|
|
|
|
return InstallState.NotInstalled;
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderResults(query: string, manifests: PluginManifest[]) {
|
|
|
|
if (query && !manifests.length) {
|
|
|
|
if (searchResultCount === null) return ''; // Search in progress
|
|
|
|
return props.renderDescription(props.themeId, _('No results'));
|
|
|
|
} else {
|
|
|
|
const output = [];
|
|
|
|
|
|
|
|
for (const manifest of manifests) {
|
|
|
|
output.push(<PluginBox
|
|
|
|
key={manifest.id}
|
|
|
|
manifest={manifest}
|
|
|
|
themeId={props.themeId}
|
2021-01-24 20:45:42 +02:00
|
|
|
isCompatible={PluginService.instance().isCompatible(manifest.app_min_version)}
|
2021-01-07 18:30:53 +02:00
|
|
|
onInstall={onInstall}
|
|
|
|
installState={installState(manifest.id)}
|
|
|
|
/>);
|
|
|
|
}
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-21 17:22:19 +02:00
|
|
|
const renderContentSourceInfo = () => {
|
|
|
|
if (props.repoApi().isUsingDefaultContentUrl) return null;
|
|
|
|
const theme = themeStyle(props.themeId);
|
|
|
|
const url = new URL(props.repoApi().contentBaseUrl);
|
|
|
|
return <div style={{ ...theme.textStyleMinor, marginTop: 5, fontSize: theme.fontSize }}>{_('Content provided by %s', url.hostname)}</div>;
|
|
|
|
};
|
|
|
|
|
2021-01-07 18:30:53 +02:00
|
|
|
return (
|
|
|
|
<Root>
|
2021-01-09 15:14:39 +02:00
|
|
|
<div style={{ marginBottom: 10, width: props.maxWidth }}>
|
2021-01-07 18:30:53 +02:00
|
|
|
<SearchInput
|
|
|
|
inputRef={null}
|
|
|
|
value={props.searchQuery}
|
|
|
|
onChange={onChange}
|
|
|
|
onSearchButtonClick={onSearchButtonClick}
|
|
|
|
searchStarted={searchStarted}
|
2021-05-15 16:04:10 +02:00
|
|
|
placeholder={props.disabled ? _('Please wait...') : _('Search for plugins...')}
|
2021-01-20 00:58:09 +02:00
|
|
|
disabled={props.disabled}
|
2021-01-07 18:30:53 +02:00
|
|
|
/>
|
2021-09-21 17:22:19 +02:00
|
|
|
{renderContentSourceInfo()}
|
2021-01-07 18:30:53 +02:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<ResultsRoot>
|
|
|
|
{renderResults(props.searchQuery, manifests)}
|
|
|
|
</ResultsRoot>
|
|
|
|
</Root>
|
|
|
|
);
|
|
|
|
}
|