2021-01-07 18:30:53 +02:00
import * as React from 'react' ;
2021-01-20 00:58:09 +02:00
import { useCallback , useEffect , useMemo , 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' ;
2021-01-27 01:56:35 +02:00
import PluginBox , { ItemEvent , 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' ;
import { PluginItem } from './PluginBox' ;
2021-01-20 00:58:09 +02:00
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi' ;
import Setting from '@joplin/lib/models/Setting' ;
import useOnInstallHandler , { OnPluginSettingChangeEvent } from './useOnInstallHandler' ;
2021-05-15 16:04:10 +02:00
import Logger from '@joplin/lib/Logger' ;
import StyledMessage from '../../../style/StyledMessage' ;
import StyledLink from '../../../style/StyledLink' ;
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' ) ;
2021-01-20 00:58:09 +02:00
const maxWidth : number = 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 ;
` ;
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
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 {
value : any ;
themeId : number ;
onChange : Function ;
renderLabel : Function ;
renderDescription : Function ;
2021-01-09 15:14:39 +02:00
renderHeader : Function ;
2021-01-07 18:30:53 +02:00
}
2021-01-20 00:58:09 +02:00
let repoApi_ : RepositoryApi = null ;
function repoApi ( ) : RepositoryApi {
if ( repoApi_ ) return repoApi_ ;
repoApi_ = new RepositoryApi ( 'https://github.com/joplin/plugins' , Setting . value ( 'tempDir' ) ) ;
// 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 ,
2021-01-07 18:30:53 +02:00
enabled : setting.enabled ,
deleted : setting.deleted ,
devMode : plugin.devMode ,
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 ) ;
} , [ 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() {
2021-01-29 23:54:08 +02:00
const pluginIds = await repoApi ( ) . canBeUpdatedPlugins ( pluginItems . map ( p = > p . manifest ) ) ;
2021-01-20 00:58:09 +02:00
if ( cancelled ) return ;
const conv : Record < string , boolean > = { } ;
pluginIds . forEach ( id = > conv [ id ] = true ) ;
setCanBeUpdatedPluginIds ( conv ) ;
}
void fetchPluginIds ( ) ;
return ( ) = > {
cancelled = true ;
} ;
} , [ manifestsLoaded , pluginItems ] ) ;
2021-01-27 01:56:35 +02:00
const onDelete = useCallback ( async ( event : ItemEvent ) = > {
const item = event . item ;
2021-01-24 20:45:42 +02:00
const confirm = await bridge ( ) . showConfirmMessageBox ( _ ( 'Delete plugin "%s"?' , item . manifest . name ) ) ;
2021-01-07 18:30:53 +02:00
if ( ! confirm ) return ;
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 ] . deleted = true ;
2021-01-07 18:30:53 +02:00
} ) ;
props . onChange ( { value : pluginService.serializePluginSettings ( newSettings ) } ) ;
} , [ pluginSettings , props . onChange ] ) ;
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 ) } ) ;
} , [ 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 ) } ) ;
} , [ pluginSettings , props . onChange ] ) ;
2021-01-22 00:57:09 +02:00
const onBrowsePlugins = useCallback ( ( ) = > {
bridge ( ) . openExternal ( 'https://github.com/joplin/plugins/blob/master/README.md#plugins' ) ;
} , [ ] ) ;
2021-01-20 00:58:09 +02:00
const onPluginSettingsChange = useCallback ( ( event : OnPluginSettingChangeEvent ) = > {
props . onChange ( { value : pluginService.serializePluginSettings ( event . value ) } ) ;
} , [ ] ) ;
const onUpdate = useOnInstallHandler ( setUpdatingPluginIds , pluginSettings , repoApi , onPluginSettingsChange , true ) ;
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 ) ;
menu . popup ( bridge ( ) . window ( ) ) ;
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 ) ;
} , [ ] ) ;
const onSearchPluginSettingsChange = useCallback ( ( event : any ) = > {
props . onChange ( { value : pluginService.serializePluginSettings ( event . value ) } ) ;
} , [ 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 }
2021-01-24 20:45:42 +02:00
isCompatible = { PluginService . instance ( ) . isCompatible ( item . manifest . app_min_version ) }
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' } >
{ props . renderDescription ( props . themeId , _ ( 'You do not have any installed plugin.' ) ) }
< / UserPluginsRoot >
) ;
} else {
return (
< UserPluginsRoot >
{ renderCells ( pluginItems ) }
< / UserPluginsRoot >
) ;
}
}
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 }
renderDescription = { props . renderDescription }
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 } } >
{ props . renderHeader ( props . themeId , _ ( 'Manage your plugins' ) ) }
< / 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 >
) ;
}