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' ;
import { Banner , Text , Button , ProgressBar , List , Divider } from 'react-native-paper' ;
import { _ , _n } 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-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 : {
marginLeft : 8 ,
marginRight : 8 ,
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-06-04 10:57:52 +02:00
return < ProgressBar accessibilityLabel = { _ ( 'Loading...' ) } indeterminate = { true } / > ;
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-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
if ( ! props . shouldShowBasedOnSearchQuery || props . shouldShowBasedOnSearchQuery ( plugin . manifest . name ) ) {
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-04 10:57:52 +02:00
const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
const searchAccordion = (
< List.Accordion
title = { _ ( 'Install new plugins' ) }
description = { _ ( 'Browse and install community plugins.' ) }
id = 'search'
>
< 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 }
/ >
< / List.Accordion >
2024-03-11 17:02:15 +02:00
) ;
2024-06-04 10:57:52 +02:00
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 ;
2024-06-14 20:38:16 +02:00
// 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 ;
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-04 10:57:52 +02:00
< List.AccordionGroup >
2024-06-14 20:38:16 +02:00
< InstalledItemWrapper
2024-06-04 10:57:52 +02:00
title = { _ ( 'Installed plugins' ) }
description = { installedAccordionDescription }
id = 'installed'
>
< View style = { styles . installedPluginsContainer } >
{ installedPluginCards }
< / View >
2024-06-14 20:38:16 +02:00
< / InstalledItemWrapper >
2024-06-04 10:57:52 +02:00
< Divider / >
{ showSearch ? searchAccordion : null }
< Divider / >
< / List.AccordionGroup >
< PluginInfoModal
themeId = { props . themeId }
pluginSettings = { pluginSettings }
updatablePluginIds = { updatablePluginIds }
updatingPluginIds = { updatingPluginIds }
installingPluginIds = { installingPluginIds }
item = { shownInDialogItem }
visible = { ! ! shownInDialogItem }
onModalDismiss = { onPluginDialogClosed }
pluginCallbacks = { pluginCallbacks }
/ >
2024-03-11 17:02:15 +02:00
< / View >
) ;
} ;
export default PluginStates ;