import * as React from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { ConfigScreenStyles } from '../configScreenStyles'; import { View, StyleSheet } from 'react-native'; import { Banner, Text, Button, ProgressBar, List, Divider } from 'react-native-paper'; import { _, _n } from '@joplin/lib/locale'; import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService'; import InstalledPluginBox from './InstalledPluginBox'; import SearchPlugins from './SearchPlugins'; import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import useRepoApi from './utils/useRepoApi'; import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi'; import PluginInfoModal from './PluginInfoModal'; import usePluginCallbacks from './utils/usePluginCallbacks'; interface Props { themeId: number; styles: ConfigScreenStyles; pluginSettings: SerializedPluginSettings; settingsSearchQuery?: string; updatePluginStates: (settingValue: PluginSettings)=> void; shouldShowBasedOnSearchQuery: ((relatedText: string|string[])=> boolean)|null; } // Text used for determining whether to display the setting or not. const searchInputSearchText = () => [_('Search'), _('Plugin search')]; export const getSearchText = () => { const plugins = PluginService.instance().plugins; const searchText = []; for (const key in plugins) { const plugin = plugins[key]; searchText.push(plugin.manifest.name); } searchText.push(...searchInputSearchText()); return searchText; }; // Loaded plugins: All plugins with available manifests. const useLoadedPluginIds = () => { const getLoadedPlugins = useCallback(() => { return PluginService.instance().pluginIds; }, []); const [loadedPluginIds, setLoadedPluginIds] = useState(getLoadedPlugins); useEffect(() => { const { remove } = PluginService.instance().addLoadedPluginsChangeListener(() => { setLoadedPluginIds(getLoadedPlugins()); }); return () => { remove(); }; }, [getLoadedPlugins]); return loadedPluginIds; }; const styles = StyleSheet.create({ installedPluginsContainer: { marginLeft: 8, marginRight: 8, marginBottom: 10, }, }); const PluginStates: React.FC = props => { const [repoApiError, setRepoApiError] = useState(null); const [repoApiLoaded, setRepoApiLoaded] = useState(false); const [reloadRepoCounter, setRepoReloadCounter] = useState(0); const [updatablePluginIds, setUpdatablePluginIds] = useState>({}); const [shownInDialogItem, setShownInDialogItem] = useState(null); const onRepoApiLoaded = useCallback(async (repoApi: RepositoryApi) => { const manifests = Object.values(PluginService.instance().plugins) .filter(plugin => !plugin.builtIn && !plugin.devMode) .map(plugin => { return plugin.manifest; }); const updatablePluginIds = await repoApi.canBeUpdatedPlugins(manifests); const conv: Record = {}; for (const id of updatablePluginIds) { conv[id] = true; } setRepoApiLoaded(true); setUpdatablePluginIds(conv); }, []); const repoApi = useRepoApi({ setRepoApiError, onRepoApiLoaded, reloadRepoCounter }); const reloadPluginRepo = useCallback(() => { setRepoReloadCounter(reloadRepoCounter + 1); }, [reloadRepoCounter, setRepoReloadCounter]); const renderRepoApiStatus = () => { if (repoApiLoaded) { if (!repoApi.isUsingDefaultContentUrl) { const url = new URL(repoApi.contentBaseUrl); return ( {_('Content provided by: %s', url.hostname)} ); } return null; } if (repoApiError) { return {_('Plugin repository failed to load')} ; } else { return ; } }; const onShowPluginInfo = useCallback((event: ItemEvent) => { setShownInDialogItem(event.item); }, []); const onPluginDialogClosed = useCallback(() => { setShownInDialogItem(null); }, []); const pluginSettings = useMemo(() => { return PluginService.instance().unserializePluginSettings(props.pluginSettings); }, [props.pluginSettings]); const { callbacks: pluginCallbacks, updatingPluginIds, installingPluginIds } = usePluginCallbacks({ pluginSettings, updatePluginStates: props.updatePluginStates, repoApi, }); const installedPluginCards = []; const pluginService = PluginService.instance(); const pluginIds = useLoadedPluginIds(); for (const pluginId of pluginIds) { const plugin = pluginService.plugins[pluginId]; if (!props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(plugin.manifest.name)) { installedPluginCards.push( , ); } } const showSearch = ( !props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(searchInputSearchText()) ); const [searchQuery, setSearchQuery] = useState(''); const searchAccordion = ( ); const isSearching = !!props.shouldShowBasedOnSearchQuery; // Don't include the number of installed plugins when searching -- only a few of the total // may be shown by the search. const installedAccordionDescription = !isSearching ? _n('You currently have %d plugin installed.', 'You currently have %d plugins installed.', pluginIds.length, pluginIds.length) : null; // Using a different wrapper prevents the installed item group from being openable when // there are no plugins: const InstalledItemWrapper = pluginIds.length ? List.Accordion : List.Item; return ( {renderRepoApiStatus()} {installedPluginCards} {showSearch ? searchAccordion : null} ); }; export default PluginStates;