2021-01-07 18:30:53 +02:00
import * as React from 'react' ;
2024-06-14 20:36:44 +02:00
import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
2021-01-07 18:30:53 +02:00
import PluginService , { defaultPluginSetting , Plugins , PluginSetting , PluginSettings } from '@joplin/lib/services/plugins/PluginService' ;
import { _ } from '@joplin/lib/locale' ;
import styled from 'styled-components' ;
import SearchPlugins from './SearchPlugins' ;
2024-03-09 13:03:57 +02:00
import PluginBox , { UpdateState } from './PluginBox' ;
2021-10-03 17:00:49 +02:00
import Button , { ButtonLevel , ButtonSize } from '../../../Button/Button' ;
2021-01-07 18:30:53 +02:00
import bridge from '../../../../services/bridge' ;
import produce from 'immer' ;
import { OnChangeEvent } from '../../../lib/SearchInput/SearchInput' ;
2024-03-09 13:03:57 +02:00
import { PluginItem , ItemEvent , OnPluginSettingChangeEvent } from '@joplin/lib/components/shared/config/plugins/types' ;
2024-03-29 14:40:54 +02:00
import RepositoryApi , { InstallMode } from '@joplin/lib/services/plugins/RepositoryApi' ;
2024-04-03 19:51:09 +02:00
import Setting , { AppType } from '@joplin/lib/models/Setting' ;
2024-03-09 13:03:57 +02:00
import useOnInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler' ;
import useOnDeleteHandler from '@joplin/lib/components/shared/config/plugins/useOnDeleteHandler' ;
2023-07-27 17:05:56 +02:00
import Logger from '@joplin/utils/Logger' ;
2021-05-15 16:04:10 +02:00
import StyledMessage from '../../../style/StyledMessage' ;
import StyledLink from '../../../style/StyledLink' ;
2024-08-02 15:49:15 +02:00
import SettingHeader from '../SettingHeader' ;
import SettingDescription from '../SettingDescription' ;
2021-01-07 18:30:53 +02:00
const { space } = require ( 'styled-system' ) ;
2021-05-15 16:04:10 +02:00
const logger = Logger . create ( 'PluginState' ) ;
2023-02-05 13:32:28 +02:00
const maxWidth = 320 ;
2021-01-09 15:14:39 +02:00
2021-01-07 18:30:53 +02:00
const Root = styled . div `
display : flex ;
flex - direction : column ;
` ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-12-20 17:08:43 +02:00
const UserPluginsRoot = styled . div < any > `
2021-01-07 18:30:53 +02:00
$ { space }
display : flex ;
flex - wrap : wrap ;
` ;
2021-01-09 15:14:39 +02:00
const ToolsButton = styled ( Button ) `
2021-01-20 00:58:09 +02:00
margin - right : 6px ;
2021-01-09 15:14:39 +02:00
` ;
2021-01-07 18:30:53 +02:00
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-12-20 17:08:43 +02:00
const RepoApiErrorMessage = styled ( StyledMessage ) < any > `
2021-05-15 16:04:10 +02:00
max - width : $ { props = > props . maxWidth } px ;
margin - bottom : 10px ;
` ;
2021-01-07 18:30:53 +02:00
interface Props {
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-07 18:30:53 +02:00
value : any ;
themeId : number ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2021-01-07 18:30:53 +02:00
onChange : Function ;
}
2021-01-20 00:58:09 +02:00
let repoApi_ : RepositoryApi = null ;
function repoApi ( ) : RepositoryApi {
if ( repoApi_ ) return repoApi_ ;
2024-04-03 19:51:09 +02:00
const appInfo = { type : AppType . Desktop , version : PluginService.instance ( ) . appVersion } ;
repoApi_ = RepositoryApi . ofDefaultJoplinRepo ( Setting . value ( 'tempDir' ) , appInfo , InstallMode . Default ) ;
2021-01-20 00:58:09 +02:00
// repoApi_ = new RepositoryApi('/Users/laurent/src/joplin-plugins-test', Setting.value('tempDir'));
return repoApi_ ;
}
2021-01-07 18:30:53 +02:00
function usePluginItems ( plugins : Plugins , settings : PluginSettings ) : PluginItem [ ] {
return useMemo ( ( ) = > {
const output : PluginItem [ ] = [ ] ;
for ( const pluginId in plugins ) {
const plugin = plugins [ pluginId ] ;
const setting : PluginSetting = {
. . . defaultPluginSetting ( ) ,
. . . settings [ pluginId ] ,
} ;
output . push ( {
2021-01-24 20:45:42 +02:00
manifest : plugin.manifest ,
2024-06-04 10:57:52 +02:00
installed : true ,
2021-01-07 18:30:53 +02:00
enabled : setting.enabled ,
deleted : setting.deleted ,
devMode : plugin.devMode ,
2023-12-22 13:31:57 +02:00
builtIn : plugin.builtIn ,
2021-01-20 00:58:09 +02:00
hasBeenUpdated : setting.hasBeenUpdated ,
2021-01-07 18:30:53 +02:00
} ) ;
}
output . sort ( ( a : PluginItem , b : PluginItem ) = > {
2021-01-24 20:45:42 +02:00
return a . manifest . name < b . manifest . name ? - 1 : + 1 ;
2021-01-07 18:30:53 +02:00
} ) ;
return output ;
} , [ plugins , settings ] ) ;
}
export default function ( props : Props ) {
const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
2021-01-20 00:58:09 +02:00
const [ manifestsLoaded , setManifestsLoaded ] = useState < boolean > ( false ) ;
const [ updatingPluginsIds , setUpdatingPluginIds ] = useState < Record < string , boolean > > ( { } ) ;
const [ canBeUpdatedPluginIds , setCanBeUpdatedPluginIds ] = useState < Record < string , boolean > > ( { } ) ;
2021-05-15 16:04:10 +02:00
const [ repoApiError , setRepoApiError ] = useState < Error > ( null ) ;
const [ fetchManifestTime , setFetchManifestTime ] = useState < number > ( Date . now ( ) ) ;
2021-01-07 18:30:53 +02:00
const pluginService = PluginService . instance ( ) ;
const pluginSettings = useMemo ( ( ) = > {
return pluginService . unserializePluginSettings ( props . value ) ;
2022-08-19 13:10:04 +02:00
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
2021-01-07 18:30:53 +02:00
} , [ props . value ] ) ;
2021-01-20 00:58:09 +02:00
const pluginItems = usePluginItems ( pluginService . plugins , pluginSettings ) ;
useEffect ( ( ) = > {
let cancelled = false ;
async function fetchManifests() {
2021-05-15 16:04:10 +02:00
setManifestsLoaded ( false ) ;
setRepoApiError ( null ) ;
let loadError : Error = null ;
try {
2021-06-01 11:09:46 +02:00
await repoApi ( ) . initialize ( ) ;
2021-05-15 16:04:10 +02:00
} catch ( error ) {
logger . error ( error ) ;
loadError = error ;
}
2021-01-20 00:58:09 +02:00
if ( cancelled ) return ;
2021-05-15 16:04:10 +02:00
if ( loadError ) {
setManifestsLoaded ( false ) ;
setRepoApiError ( loadError ) ;
} else {
setManifestsLoaded ( true ) ;
}
2021-01-20 00:58:09 +02:00
}
void fetchManifests ( ) ;
return ( ) = > {
cancelled = true ;
} ;
2021-05-15 16:04:10 +02:00
} , [ fetchManifestTime ] ) ;
2021-01-20 00:58:09 +02:00
useEffect ( ( ) = > {
if ( ! manifestsLoaded ) return ( ) = > { } ;
let cancelled = false ;
async function fetchPluginIds() {
2023-12-11 15:58:45 +02:00
// Built-in plugins can't be updated from the main repoApi
const nonDefaultPlugins = pluginItems
2023-12-22 13:31:57 +02:00
. filter ( plugin = > ! plugin . builtIn )
. map ( p = > p . manifest ) ;
2023-12-11 15:58:45 +02:00
2024-04-03 19:51:09 +02:00
const pluginIds = await repoApi ( ) . canBeUpdatedPlugins ( nonDefaultPlugins ) ;
2021-01-20 00:58:09 +02:00
if ( cancelled ) return ;
2023-12-11 15:58:45 +02:00
2021-01-20 00:58:09 +02:00
const conv : Record < string , boolean > = { } ;
2023-12-11 15:58:45 +02:00
for ( const id of pluginIds ) {
conv [ id ] = true ;
}
2021-01-20 00:58:09 +02:00
setCanBeUpdatedPluginIds ( conv ) ;
}
void fetchPluginIds ( ) ;
return ( ) = > {
cancelled = true ;
} ;
2023-03-17 10:50:51 +02:00
} , [ manifestsLoaded , pluginItems , pluginService . appVersion ] ) ;
2021-01-20 00:58:09 +02:00
2021-01-27 01:56:35 +02:00
const onToggle = useCallback ( ( event : ItemEvent ) = > {
const item = event . item ;
2021-01-07 18:30:53 +02:00
const newSettings = produce ( pluginSettings , ( draft : PluginSettings ) = > {
2021-01-24 20:45:42 +02:00
if ( ! draft [ item . manifest . id ] ) draft [ item . manifest . id ] = defaultPluginSetting ( ) ;
draft [ item . manifest . id ] . enabled = ! draft [ item . manifest . id ] . enabled ;
2021-01-07 18:30:53 +02:00
} ) ;
props . onChange ( { value : pluginService.serializePluginSettings ( newSettings ) } ) ;
2022-08-19 13:10:04 +02:00
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
2021-01-07 18:30:53 +02:00
} , [ pluginSettings , props . onChange ] ) ;
2021-01-09 15:14:39 +02:00
const onInstall = useCallback ( async ( ) = > {
2021-11-01 09:38:06 +02:00
const result = await bridge ( ) . showOpenDialog ( {
2021-01-09 15:14:39 +02:00
filters : [ { name : 'Joplin Plugin Archive' , extensions : [ 'jpl' ] } ] ,
} ) ;
2021-01-07 18:30:53 +02:00
2021-01-09 15:14:39 +02:00
const filePath = result && result . length ? result [ 0 ] : null ;
if ( ! filePath ) return ;
2021-01-07 18:30:53 +02:00
2021-01-09 15:14:39 +02:00
const plugin = await pluginService . installPlugin ( filePath ) ;
2021-01-07 18:30:53 +02:00
2021-01-09 15:14:39 +02:00
const newSettings = produce ( pluginSettings , ( draft : PluginSettings ) = > {
draft [ plugin . manifest . id ] = defaultPluginSetting ( ) ;
} ) ;
2021-01-07 18:30:53 +02:00
2021-01-09 15:14:39 +02:00
props . onChange ( { value : pluginService.serializePluginSettings ( newSettings ) } ) ;
2022-08-19 13:10:04 +02:00
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
2021-01-09 15:14:39 +02:00
} , [ pluginSettings , props . onChange ] ) ;
2021-01-22 00:57:09 +02:00
const onBrowsePlugins = useCallback ( ( ) = > {
2024-03-14 20:40:47 +02:00
void bridge ( ) . openExternal ( 'https://joplinapp.org/plugins/' ) ;
2021-01-22 00:57:09 +02:00
} , [ ] ) ;
2021-01-20 00:58:09 +02:00
const onPluginSettingsChange = useCallback ( ( event : OnPluginSettingChangeEvent ) = > {
props . onChange ( { value : pluginService.serializePluginSettings ( event . value ) } ) ;
2024-03-09 13:03:57 +02:00
} , [ pluginService , props . onChange ] ) ;
2021-01-20 00:58:09 +02:00
2024-06-14 20:36:44 +02:00
const pluginSettingsRef = useRef ( pluginSettings ) ;
pluginSettingsRef . current = pluginSettings ;
const onDelete = useOnDeleteHandler ( pluginSettingsRef , onPluginSettingsChange , false ) ;
const onUpdate = useOnInstallHandler ( setUpdatingPluginIds , pluginSettingsRef , repoApi , onPluginSettingsChange , true ) ;
2021-01-20 00:58:09 +02:00
2021-01-09 15:14:39 +02:00
const onToolsClick = useCallback ( async ( ) = > {
2021-01-22 00:57:09 +02:00
const template = [
{
label : _ ( 'Browse all plugins' ) ,
click : onBrowsePlugins ,
} ,
{
label : _ ( 'Install from file' ) ,
click : onInstall ,
} ,
] ;
2021-01-09 15:14:39 +02:00
const menu = bridge ( ) . Menu . buildFromTemplate ( template ) ;
2024-11-08 17:32:05 +02:00
menu . popup ( { window : bridge ( ) . mainWindow ( ) } ) ;
2021-01-22 00:57:09 +02:00
} , [ onInstall , onBrowsePlugins ] ) ;
2021-01-07 18:30:53 +02:00
const onSearchQueryChange = useCallback ( ( event : OnChangeEvent ) = > {
setSearchQuery ( event . value ) ;
} , [ ] ) ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-07 18:30:53 +02:00
const onSearchPluginSettingsChange = useCallback ( ( event : any ) = > {
props . onChange ( { value : pluginService.serializePluginSettings ( event . value ) } ) ;
2022-08-19 13:10:04 +02:00
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
2021-01-07 18:30:53 +02:00
} , [ props . onChange ] ) ;
function renderCells ( items : PluginItem [ ] ) {
const output = [ ] ;
for ( const item of items ) {
if ( item . deleted ) continue ;
2021-01-24 20:45:42 +02:00
const isUpdating = updatingPluginsIds [ item . manifest . id ] ;
const onUpdateHandler = canBeUpdatedPluginIds [ item . manifest . id ] ? onUpdate : null ;
2021-01-20 00:58:09 +02:00
let updateState = UpdateState . Idle ;
if ( onUpdateHandler ) updateState = UpdateState . CanUpdate ;
if ( isUpdating ) updateState = UpdateState . Updating ;
if ( item . hasBeenUpdated ) updateState = UpdateState . HasBeenUpdated ;
2021-01-07 18:30:53 +02:00
output . push ( < PluginBox
2021-01-24 20:45:42 +02:00
key = { item . manifest . id }
2021-01-07 18:30:53 +02:00
item = { item }
themeId = { props . themeId }
2021-01-20 00:58:09 +02:00
updateState = { updateState }
2024-04-03 19:51:09 +02:00
isCompatible = { PluginService . instance ( ) . isCompatible ( item . manifest ) }
2021-01-07 18:30:53 +02:00
onDelete = { onDelete }
onToggle = { onToggle }
2021-01-20 00:58:09 +02:00
onUpdate = { onUpdateHandler }
2021-01-07 18:30:53 +02:00
/ > ) ;
}
return output ;
}
function renderUserPlugins ( pluginItems : PluginItem [ ] ) {
const allDeleted = ! pluginItems . find ( it = > it . deleted !== true ) ;
if ( ! pluginItems . length || allDeleted ) {
return (
< UserPluginsRoot mb = { '10px' } >
2024-08-02 15:49:15 +02:00
< SettingDescription text = { _ ( 'You do not have any installed plugin.' ) } / >
2021-01-07 18:30:53 +02:00
< / UserPluginsRoot >
) ;
} else {
2023-12-22 13:31:57 +02:00
const nonDefaultPlugins = pluginItems . filter ( item = > ! item . builtIn ) ;
const defaultPlugins = pluginItems . filter ( item = > item . builtIn ) ;
2021-01-07 18:30:53 +02:00
return (
2023-12-11 15:58:45 +02:00
< >
< UserPluginsRoot >
{ renderCells ( nonDefaultPlugins ) }
< / UserPluginsRoot >
< UserPluginsRoot >
{ renderCells ( defaultPlugins ) }
< / UserPluginsRoot >
< / >
2021-01-07 18:30:53 +02:00
) ;
}
}
2021-01-20 00:58:09 +02:00
function renderSearchArea() {
return (
2021-05-15 16:04:10 +02:00
< div style = { { marginBottom : 0 } } >
2021-01-07 18:30:53 +02:00
< SearchPlugins
2021-01-20 00:58:09 +02:00
disabled = { ! manifestsLoaded }
2021-01-09 15:14:39 +02:00
maxWidth = { maxWidth }
2021-01-07 18:30:53 +02:00
themeId = { props . themeId }
searchQuery = { searchQuery }
pluginSettings = { pluginSettings }
onSearchQueryChange = { onSearchQueryChange }
onPluginSettingsChange = { onSearchPluginSettingsChange }
2021-01-20 00:58:09 +02:00
repoApi = { repoApi }
2021-01-07 18:30:53 +02:00
/ >
< / div >
2021-01-20 00:58:09 +02:00
) ;
}
2021-05-15 16:04:10 +02:00
function renderRepoApiError() {
if ( ! repoApiError ) return null ;
2021-08-28 16:45:27 +02:00
return < RepoApiErrorMessage maxWidth = { maxWidth } type = "error" > { _ ( 'Could not connect to plugin repository.' ) } < br / > < br / > - < StyledLink href = "#" onClick = { ( ) = > { setFetchManifestTime ( Date . now ( ) ) ; } } > { _ ( 'Try again' ) } < / StyledLink > < br / > < br / > - < StyledLink href = "#" onClick = { onBrowsePlugins } > { _ ( 'Browse all plugins' ) } < / StyledLink > < / RepoApiErrorMessage > ;
2021-05-15 16:04:10 +02:00
}
2021-01-20 00:58:09 +02:00
function renderBottomArea() {
if ( searchQuery ) return null ;
2021-01-07 18:30:53 +02:00
2021-01-20 00:58:09 +02:00
return (
2021-01-09 15:14:39 +02:00
< div >
2021-05-15 16:04:10 +02:00
{ renderRepoApiError ( ) }
2021-01-09 15:14:39 +02:00
< div style = { { display : 'flex' , flexDirection : 'row' , maxWidth } } >
2021-10-03 17:00:49 +02:00
< ToolsButton size = { ButtonSize . Small } tooltip = { _ ( 'Plugin tools' ) } iconName = "fas fa-cog" level = { ButtonLevel . Secondary } onClick = { onToolsClick } / >
2021-01-09 15:14:39 +02:00
< div style = { { display : 'flex' , flex : 1 } } >
2024-08-02 15:49:15 +02:00
< SettingHeader text = { _ ( 'Manage your plugins' ) } / >
2021-01-09 15:14:39 +02:00
< / div >
< / div >
{ renderUserPlugins ( pluginItems ) }
< / div >
2021-01-20 00:58:09 +02:00
) ;
}
return (
< Root >
{ renderSearchArea ( ) }
{ renderBottomArea ( ) }
2021-01-07 18:30:53 +02:00
< / Root >
) ;
}