2024-03-11 17:02:15 +02:00
|
|
|
import * as React from 'react';
|
|
|
|
|
|
|
|
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
|
|
|
import { _ } from '@joplin/lib/locale';
|
|
|
|
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
|
|
|
import { useCallback, useMemo, useState } from 'react';
|
2024-08-02 15:51:49 +02:00
|
|
|
import { FlatList, Platform, StyleSheet, View } from 'react-native';
|
2024-06-15 11:00:21 +02:00
|
|
|
import { TextInput } from 'react-native-paper';
|
2024-03-11 17:02:15 +02:00
|
|
|
import PluginBox, { InstallState } from './PluginBox';
|
2024-06-04 10:57:52 +02:00
|
|
|
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
|
|
|
import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
2024-03-11 17:02:15 +02:00
|
|
|
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
2024-04-25 15:02:10 +02:00
|
|
|
import openWebsiteForPlugin from './utils/openWebsiteForPlugin';
|
2024-06-04 10:57:52 +02:00
|
|
|
import { PluginCallback, PluginCallbacks } from './utils/usePluginCallbacks';
|
|
|
|
import InstalledPluginBox from './InstalledPluginBox';
|
2024-06-15 11:00:21 +02:00
|
|
|
import SectionLabel from './SectionLabel';
|
2024-03-11 17:02:15 +02:00
|
|
|
|
|
|
|
interface Props {
|
|
|
|
themeId: number;
|
2024-06-04 10:57:52 +02:00
|
|
|
pluginSettings: PluginSettings;
|
2024-03-11 17:02:15 +02:00
|
|
|
repoApiInitialized: boolean;
|
|
|
|
onUpdatePluginStates: (states: PluginSettings)=> void;
|
|
|
|
repoApi: RepositoryApi;
|
2024-06-04 10:57:52 +02:00
|
|
|
|
|
|
|
installingPluginIds: Record<string, boolean>;
|
|
|
|
updatingPluginIds: Record<string, boolean>;
|
|
|
|
updatablePluginIds: Record<string, boolean>;
|
|
|
|
|
|
|
|
callbacks: PluginCallbacks;
|
|
|
|
onShowPluginInfo: PluginCallback;
|
|
|
|
|
|
|
|
searchQuery: string;
|
|
|
|
setSearchQuery: (newQuery: string)=> void;
|
2024-03-11 17:02:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface SearchResultRecord {
|
|
|
|
id: string;
|
|
|
|
item: PluginItem;
|
|
|
|
installState: InstallState;
|
|
|
|
}
|
|
|
|
|
2024-06-04 10:57:52 +02:00
|
|
|
const styles = StyleSheet.create({
|
|
|
|
container: {
|
|
|
|
flexDirection: 'column',
|
|
|
|
margin: 12,
|
2024-06-15 11:00:21 +02:00
|
|
|
marginBottom: 0,
|
2024-06-04 10:57:52 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2024-06-15 11:00:21 +02:00
|
|
|
|
2024-03-11 17:02:15 +02:00
|
|
|
const PluginSearch: React.FC<Props> = props => {
|
2024-06-04 10:57:52 +02:00
|
|
|
const { searchQuery, setSearchQuery } = props;
|
2024-03-11 17:02:15 +02:00
|
|
|
const [searchResultManifests, setSearchResultManifests] = useState<PluginManifest[]>([]);
|
|
|
|
|
|
|
|
useAsyncEffect(async event => {
|
|
|
|
if (!searchQuery || !props.repoApiInitialized) {
|
|
|
|
setSearchResultManifests([]);
|
|
|
|
} else {
|
|
|
|
const results = await props.repoApi.search(searchQuery);
|
|
|
|
if (event.cancelled) return;
|
|
|
|
|
|
|
|
setSearchResultManifests(results);
|
|
|
|
}
|
|
|
|
}, [searchQuery, props.repoApi, setSearchResultManifests, props.repoApiInitialized]);
|
|
|
|
|
|
|
|
const pluginSettings = useMemo(() => {
|
|
|
|
return { ...PluginService.instance().unserializePluginSettings(props.pluginSettings) };
|
|
|
|
}, [props.pluginSettings]);
|
|
|
|
|
|
|
|
const searchResults: SearchResultRecord[] = useMemo(() => {
|
|
|
|
return searchResultManifests.map(manifest => {
|
|
|
|
const settings = pluginSettings[manifest.id];
|
|
|
|
|
|
|
|
let installState = InstallState.NotInstalled;
|
|
|
|
if (settings && !settings.deleted) {
|
|
|
|
installState = InstallState.Installed;
|
|
|
|
}
|
2024-06-04 10:57:52 +02:00
|
|
|
if (props.installingPluginIds[manifest.id]) {
|
2024-03-11 17:02:15 +02:00
|
|
|
installState = InstallState.Installing;
|
|
|
|
}
|
|
|
|
|
|
|
|
const item: PluginItem = {
|
|
|
|
manifest,
|
2024-06-04 10:57:52 +02:00
|
|
|
installed: !!settings,
|
2024-03-11 17:02:15 +02:00
|
|
|
enabled: settings && settings.enabled,
|
|
|
|
deleted: settings && !settings.deleted,
|
|
|
|
devMode: false,
|
|
|
|
builtIn: false,
|
|
|
|
hasBeenUpdated: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: manifest.id,
|
|
|
|
item,
|
|
|
|
installState,
|
|
|
|
};
|
|
|
|
});
|
2024-06-04 10:57:52 +02:00
|
|
|
}, [searchResultManifests, props.installingPluginIds, pluginSettings]);
|
2024-03-11 17:02:15 +02:00
|
|
|
|
|
|
|
|
2024-06-04 10:57:52 +02:00
|
|
|
const onInstall = props.callbacks.onInstall;
|
2024-03-11 17:02:15 +02:00
|
|
|
const renderResult = useCallback(({ item }: { item: SearchResultRecord }) => {
|
|
|
|
const manifest = item.item.manifest;
|
|
|
|
|
2024-06-04 10:57:52 +02:00
|
|
|
if (item.installState === InstallState.Installed && PluginService.instance().isPluginLoaded(manifest.id)) {
|
|
|
|
return (
|
|
|
|
<InstalledPluginBox
|
|
|
|
pluginId={manifest.id}
|
|
|
|
themeId={props.themeId}
|
|
|
|
pluginSettings={props.pluginSettings}
|
|
|
|
updatablePluginIds={props.updatablePluginIds}
|
|
|
|
updatingPluginIds={props.updatingPluginIds}
|
|
|
|
showInstalledChip={true}
|
|
|
|
callbacks={props.callbacks}
|
|
|
|
onShowPluginInfo={props.onShowPluginInfo}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return (
|
|
|
|
<PluginBox
|
|
|
|
themeId={props.themeId}
|
|
|
|
key={manifest.id}
|
|
|
|
item={item.item}
|
|
|
|
installState={item.installState}
|
|
|
|
showInstalledChip={false}
|
|
|
|
isCompatible={PluginService.instance().isCompatible(manifest)}
|
|
|
|
onInstall={onInstall}
|
|
|
|
onAboutPress={openWebsiteForPlugin}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}, [onInstall, props.themeId, props.pluginSettings, props.updatingPluginIds, props.updatablePluginIds, props.onShowPluginInfo, props.callbacks]);
|
|
|
|
|
2024-06-15 11:00:21 +02:00
|
|
|
const onClearSearch = useCallback(() => {
|
|
|
|
setSearchQuery('');
|
|
|
|
}, [setSearchQuery]);
|
2024-06-04 10:57:52 +02:00
|
|
|
|
2024-06-15 11:00:21 +02:00
|
|
|
const renderSearchButton = () => {
|
|
|
|
if (searchQuery) {
|
|
|
|
return <TextInput.Icon onPress={onClearSearch} accessibilityLabel={_('Clear search')} icon='close' />;
|
|
|
|
} else {
|
|
|
|
return <TextInput.Icon icon='magnify' aria-hidden={true} importantForAccessibility='no-hide-descendants'/>;
|
|
|
|
}
|
2024-06-04 10:57:52 +02:00
|
|
|
};
|
2024-03-11 17:02:15 +02:00
|
|
|
|
2024-08-02 15:51:49 +02:00
|
|
|
// scrollEnabled seems to have a different effect on web, when compared with native:
|
|
|
|
// https://github.com/necolas/react-native-web/issues/1042#issuecomment-407157580
|
|
|
|
// When not provided on web, scrolling the parent element doesn't work.
|
|
|
|
const scrollEnabled = Platform.OS === 'web';
|
|
|
|
|
2024-03-11 17:02:15 +02:00
|
|
|
return (
|
2024-06-04 10:57:52 +02:00
|
|
|
<View style={styles.container}>
|
|
|
|
<TextInput
|
2024-03-11 17:02:15 +02:00
|
|
|
testID='searchbar'
|
2024-06-04 10:57:52 +02:00
|
|
|
mode='outlined'
|
2024-06-15 11:00:21 +02:00
|
|
|
right={renderSearchButton()}
|
|
|
|
placeholder={_('Search for plugins...')}
|
2024-03-11 17:02:15 +02:00
|
|
|
onChangeText={setSearchQuery}
|
|
|
|
value={searchQuery}
|
|
|
|
editable={props.repoApiInitialized}
|
|
|
|
/>
|
2024-06-15 11:00:21 +02:00
|
|
|
<SectionLabel visible={!!searchQuery.length}>{_('Results (%d):', searchResults.length)}</SectionLabel>
|
2024-03-11 17:02:15 +02:00
|
|
|
<FlatList
|
|
|
|
data={searchResults}
|
|
|
|
renderItem={renderResult}
|
|
|
|
keyExtractor={item => item.id}
|
2024-08-02 15:51:49 +02:00
|
|
|
scrollEnabled={scrollEnabled}
|
2024-03-11 17:02:15 +02:00
|
|
|
/>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default PluginSearch;
|