2024-03-11 17:02:15 +02:00
|
|
|
import * as React from 'react';
|
2024-06-04 10:57:52 +02:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
2024-03-11 17:02:15 +02:00
|
|
|
import { ConfigScreenStyles } from '../configScreenStyles';
|
2024-06-04 10:57:52 +02:00
|
|
|
import { View, StyleSheet } from 'react-native';
|
2024-06-15 11:00:21 +02:00
|
|
|
import { Banner, Text, Button, ProgressBar, Divider } from 'react-native-paper';
|
|
|
|
import { _ } from '@joplin/lib/locale';
|
2024-04-27 12:45:39 +02:00
|
|
|
import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
2024-06-04 10:57:52 +02:00
|
|
|
import InstalledPluginBox from './InstalledPluginBox';
|
2024-03-11 17:02:15 +02:00
|
|
|
import SearchPlugins from './SearchPlugins';
|
2024-06-04 10:57:52 +02:00
|
|
|
import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
2024-03-11 17:02:15 +02:00
|
|
|
import useRepoApi from './utils/useRepoApi';
|
|
|
|
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
2024-06-04 10:57:52 +02:00
|
|
|
import PluginInfoModal from './PluginInfoModal';
|
|
|
|
import usePluginCallbacks from './utils/usePluginCallbacks';
|
2024-06-14 20:38:50 +02:00
|
|
|
import BetaChip from '../../../BetaChip';
|
2024-06-15 11:00:21 +02:00
|
|
|
import SectionLabel from './SectionLabel';
|
2024-03-11 17:02:15 +02:00
|
|
|
|
|
|
|
interface Props {
|
|
|
|
themeId: number;
|
|
|
|
styles: ConfigScreenStyles;
|
2024-04-27 12:45:39 +02:00
|
|
|
pluginSettings: SerializedPluginSettings;
|
2024-03-11 17:02:15 +02:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2024-04-27 12:45:39 +02:00
|
|
|
// Loaded plugins: All plugins with available manifests.
|
2024-05-02 18:05:25 +02:00
|
|
|
const useLoadedPluginIds = () => {
|
|
|
|
const getLoadedPlugins = useCallback(() => {
|
|
|
|
return PluginService.instance().pluginIds;
|
|
|
|
}, []);
|
|
|
|
const [loadedPluginIds, setLoadedPluginIds] = useState(getLoadedPlugins);
|
2024-04-27 12:45:39 +02:00
|
|
|
|
2024-06-04 10:57:52 +02:00
|
|
|
useEffect(() => {
|
|
|
|
const { remove } = PluginService.instance().addLoadedPluginsChangeListener(() => {
|
2024-05-02 18:05:25 +02:00
|
|
|
setLoadedPluginIds(getLoadedPlugins());
|
2024-06-04 10:57:52 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
remove();
|
|
|
|
};
|
|
|
|
}, [getLoadedPlugins]);
|
2024-04-27 12:45:39 +02:00
|
|
|
|
|
|
|
return loadedPluginIds;
|
|
|
|
};
|
|
|
|
|
2024-06-04 10:57:52 +02:00
|
|
|
const styles = StyleSheet.create({
|
|
|
|
installedPluginsContainer: {
|
2024-06-15 11:00:21 +02:00
|
|
|
marginLeft: 12,
|
|
|
|
marginRight: 12,
|
2024-06-04 10:57:52 +02:00
|
|
|
marginBottom: 10,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2024-03-11 17:02:15 +02:00
|
|
|
const PluginStates: React.FC<Props> = props => {
|
|
|
|
const [repoApiError, setRepoApiError] = useState(null);
|
|
|
|
const [repoApiLoaded, setRepoApiLoaded] = useState(false);
|
|
|
|
const [reloadRepoCounter, setRepoReloadCounter] = useState(0);
|
|
|
|
const [updatablePluginIds, setUpdatablePluginIds] = useState<Record<string, boolean>>({});
|
2024-06-04 10:57:52 +02:00
|
|
|
const [shownInDialogItem, setShownInDialogItem] = useState<PluginItem|null>(null);
|
2024-03-11 17:02:15 +02:00
|
|
|
|
|
|
|
const onRepoApiLoaded = useCallback(async (repoApi: RepositoryApi) => {
|
|
|
|
const manifests = Object.values(PluginService.instance().plugins)
|
|
|
|
.filter(plugin => !plugin.builtIn && !plugin.devMode)
|
|
|
|
.map(plugin => {
|
|
|
|
return plugin.manifest;
|
|
|
|
});
|
2024-04-03 19:51:09 +02:00
|
|
|
const updatablePluginIds = await repoApi.canBeUpdatedPlugins(manifests);
|
2024-03-11 17:02:15 +02:00
|
|
|
|
|
|
|
const conv: Record<string, boolean> = {};
|
|
|
|
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 (
|
|
|
|
<Banner visible={true} icon='alert'>{_('Content provided by: %s', url.hostname)}</Banner>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (repoApiError) {
|
|
|
|
return <View style={{ flexDirection: 'row' }}>
|
|
|
|
<Text>{_('Plugin repository failed to load')}</Text>
|
|
|
|
<Button onPress={reloadPluginRepo}>{_('Retry')}</Button>
|
|
|
|
</View>;
|
|
|
|
} else {
|
2024-08-02 15:51:49 +02:00
|
|
|
// The progress bar needs to be wrapped in a View to have the correct height on web.
|
|
|
|
return <View>
|
|
|
|
<ProgressBar accessibilityLabel={_('Loading...')} indeterminate={true} />
|
|
|
|
</View>;
|
2024-03-11 17:02:15 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-06-04 10:57:52 +02:00
|
|
|
const onShowPluginInfo = useCallback((event: ItemEvent) => {
|
|
|
|
setShownInDialogItem(event.item);
|
2024-03-11 17:02:15 +02:00
|
|
|
}, []);
|
|
|
|
|
2024-06-04 10:57:52 +02:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
|
2024-03-11 17:02:15 +02:00
|
|
|
const installedPluginCards = [];
|
|
|
|
const pluginService = PluginService.instance();
|
2024-04-27 12:45:39 +02:00
|
|
|
|
2024-06-15 11:00:21 +02:00
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
const isPluginSearching = !!searchQuery;
|
|
|
|
|
2024-05-02 18:05:25 +02:00
|
|
|
const pluginIds = useLoadedPluginIds();
|
2024-04-27 12:45:39 +02:00
|
|
|
for (const pluginId of pluginIds) {
|
|
|
|
const plugin = pluginService.plugins[pluginId];
|
2024-03-11 17:02:15 +02:00
|
|
|
|
2024-06-15 11:00:21 +02:00
|
|
|
const matchesGlobalSearch = !props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(plugin.manifest.name);
|
|
|
|
const showCard = !isPluginSearching && matchesGlobalSearch;
|
|
|
|
if (showCard) {
|
2024-03-11 17:02:15 +02:00
|
|
|
installedPluginCards.push(
|
2024-06-04 10:57:52 +02:00
|
|
|
<InstalledPluginBox
|
2024-04-27 12:45:39 +02:00
|
|
|
key={`plugin-${pluginId}`}
|
2024-04-25 15:02:10 +02:00
|
|
|
themeId={props.themeId}
|
2024-04-27 12:45:39 +02:00
|
|
|
pluginId={pluginId}
|
2024-06-04 10:57:52 +02:00
|
|
|
pluginSettings={pluginSettings}
|
2024-03-11 17:02:15 +02:00
|
|
|
updatablePluginIds={updatablePluginIds}
|
2024-06-04 10:57:52 +02:00
|
|
|
updatingPluginIds={updatingPluginIds}
|
|
|
|
showInstalledChip={false}
|
|
|
|
onShowPluginInfo={onShowPluginInfo}
|
|
|
|
callbacks={pluginCallbacks}
|
2024-03-11 17:02:15 +02:00
|
|
|
/>,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const showSearch = (
|
2024-03-29 14:40:54 +02:00
|
|
|
!props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(searchInputSearchText())
|
2024-03-11 17:02:15 +02:00
|
|
|
);
|
|
|
|
|
2024-06-15 11:00:21 +02:00
|
|
|
const searchSection = (
|
|
|
|
<SearchPlugins
|
|
|
|
pluginSettings={pluginSettings}
|
|
|
|
themeId={props.themeId}
|
|
|
|
onUpdatePluginStates={props.updatePluginStates}
|
|
|
|
installingPluginIds={installingPluginIds}
|
|
|
|
callbacks={pluginCallbacks}
|
|
|
|
repoApiInitialized={repoApiLoaded}
|
|
|
|
repoApi={repoApi}
|
|
|
|
updatingPluginIds={updatingPluginIds}
|
|
|
|
updatablePluginIds={updatablePluginIds}
|
|
|
|
onShowPluginInfo={onShowPluginInfo}
|
|
|
|
|
|
|
|
searchQuery={searchQuery}
|
|
|
|
setSearchQuery={setSearchQuery}
|
|
|
|
/>
|
2024-03-11 17:02:15 +02:00
|
|
|
);
|
|
|
|
|
2024-06-15 11:00:21 +02:00
|
|
|
const isSearching = !!props.shouldShowBasedOnSearchQuery || isPluginSearching;
|
2024-06-14 20:38:16 +02:00
|
|
|
|
2024-03-11 17:02:15 +02:00
|
|
|
return (
|
|
|
|
<View>
|
|
|
|
{renderRepoApiStatus()}
|
2024-06-14 20:38:50 +02:00
|
|
|
<Banner visible={true} elevation={0} icon={() => <BetaChip size={13}/>}>
|
|
|
|
<Text>Plugin support on mobile is still in beta. Plugins may cause performance issues. Some have only partial support for Joplin mobile.</Text>
|
|
|
|
</Banner>
|
|
|
|
<Divider/>
|
|
|
|
|
2024-06-15 11:00:21 +02:00
|
|
|
{showSearch ? searchSection : null}
|
|
|
|
<View style={styles.installedPluginsContainer}>
|
|
|
|
<SectionLabel visible={!isSearching}>
|
|
|
|
{pluginIds.length ? _('Installed (%d):', pluginIds.length) : _('No plugins are installed.')}
|
|
|
|
</SectionLabel>
|
|
|
|
{installedPluginCards}
|
|
|
|
</View>
|
|
|
|
|
2024-06-04 10:57:52 +02:00
|
|
|
<PluginInfoModal
|
|
|
|
themeId={props.themeId}
|
|
|
|
pluginSettings={pluginSettings}
|
|
|
|
updatablePluginIds={updatablePluginIds}
|
|
|
|
updatingPluginIds={updatingPluginIds}
|
|
|
|
installingPluginIds={installingPluginIds}
|
|
|
|
item={shownInDialogItem}
|
|
|
|
visible={!!shownInDialogItem}
|
|
|
|
onModalDismiss={onPluginDialogClosed}
|
|
|
|
pluginCallbacks={pluginCallbacks}
|
|
|
|
/>
|
2024-06-15 11:00:21 +02:00
|
|
|
<Divider/>
|
2024-03-11 17:02:15 +02:00
|
|
|
</View>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default PluginStates;
|