import * as React from 'react'; import { useCallback, useId, useMemo } from 'react'; import { _ } from '@joplin/lib/locale'; import styled from 'styled-components'; import ToggleButton from '../../../lib/ToggleButton/ToggleButton'; import Button, { ButtonLevel } from '../../../Button/Button'; import { PluginManifest } from '@joplin/lib/services/plugins/utils/types'; import bridge from '../../../../services/bridge'; import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import PluginService from '@joplin/lib/services/plugins/PluginService'; import getPluginHelpUrl from '@joplin/lib/services/plugins/utils/getPluginHelpUrl'; export enum InstallState { NotInstalled = 1, Installing = 2, Installed = 3, } export enum UpdateState { Idle = 1, CanUpdate = 2, Updating = 3, HasBeenUpdated = 4, } interface Props { item?: PluginItem; manifest?: PluginManifest; installState?: InstallState; updateState?: UpdateState; themeId: number; isCompatible: boolean; onToggle?: (event: ItemEvent)=> void; onDelete?: (event: ItemEvent)=> void; onInstall?: (event: ItemEvent)=> void; onUpdate?: (event: ItemEvent)=> void; } function manifestToItem(manifest: PluginManifest): PluginItem { return { manifest: manifest, installed: true, enabled: true, deleted: false, devMode: false, builtIn: false, hasBeenUpdated: false, }; } const CellRoot = styled.div<{ isCompatible: boolean }>` display: flex; box-sizing: border-box; background-color: ${props => props.theme.backgroundColor}; flex-direction: column; align-items: stretch; min-height: 164px; padding: 20px; border: 1px solid ${props => props.theme.dividerColor}; border-radius: 8px; width: 100%; box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 8px 20px rgba(0,0,0,0.04); transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease; opacity: ${props => props.isCompatible ? '1' : '0.6'}; &:hover { border-color: ${props => props.theme.color}; box-shadow: 0 3px 6px rgba(0,0,0,0.07), 0 12px 28px rgba(0,0,0,0.08); transform: translateY(-1px); } &:focus-within { border-color: ${props => props.theme.color}; box-shadow: 0 0 0 2px ${props => props.theme.color}33, 0 3px 6px rgba(0,0,0,0.07); } @media (prefers-reduced-motion: reduce) { transition: none; &:hover { transform: none; } } `; const CellTop = styled.div` display: flex; flex-direction: row; width: 100%; margin-bottom: 14px; align-items: flex-start; `; const CellContent = styled.div` display: flex; margin-bottom: 10px; flex: 1; `; const CellFooter = styled.div` display: flex; flex-direction: row; align-items: center; gap: 8px; min-height: 32px; `; const NeedUpgradeMessage = styled.span` font-family: ${props => props.theme.fontFamily}; color: ${props => props.theme.colorWarn}; font-size: ${props => props.theme.fontSize}px; `; const BoxedLabel = styled.div` border: 1px solid ${props => props.theme.color}; border-radius: 6px; padding: 5px 8px; font-size: ${props => props.theme.fontSize * 0.78}px; font-weight: 600; color: ${props => props.theme.color}; flex-grow: 0; height: min-content; margin-top: auto; `; const StyledNameAndVersion = styled.div<{ mb: string | number }>` font-family: ${props => props.theme.fontFamily}; color: ${props => props.theme.color}; font-size: ${props => props.theme.fontSize * 1.08}px; font-weight: 650; padding-right: 12px; flex: 1; `; const StyledName = styled.a` color: ${props => props.theme.color}; &:hover { text-decoration: underline; } `; const StyledVersion = styled.span` color: ${props => props.theme.colorFaded}; font-size: ${props => props.theme.fontSize * 0.82}px; font-weight: 500; white-space: nowrap; `; const StyledDescription = styled.div` font-family: ${props => props.theme.fontFamily}; color: ${props => props.theme.colorFaded}; font-size: ${props => props.theme.fontSize}px; line-height: 1.55; `; const RecommendedBadge = styled.a` font-family: ${props => props.theme.fontFamily}; color: ${props => props.theme.colorWarn}; font-size: ${props => props.theme.fontSize}px; border: 1px solid ${props => props.theme.colorWarn}; padding: 5px 8px; border-radius: 6px; opacity: 0.8; &:hover { opacity: 1; } `; export default function(props: Props) { const item = useMemo(() => { return props.item ? props.item : manifestToItem(props.manifest); }, [props.item, props.manifest]); const onNameClick = useCallback(() => { const manifest = item.manifest; void bridge().openExternal(getPluginHelpUrl(manifest.id)); }, [item]); const onRecommendedClick = useCallback(() => { void bridge().openExternal('https://github.com/joplin/plugins/blob/master/readme/recommended.md#recommended-plugins'); }, []); // For plugins in dev mode things like enabling/disabling or // uninstalling them doesn't make sense, as that should be done by // adding/removing them from wherever they were loaded from. function renderToggleButton() { if (!props.onToggle) return null; if (item.devMode) { return DEV; } return props.onToggle({ item })} aria-label={item.enabled ? _('Disable %s', item.manifest.name) : _('Enable %s', item.manifest.name)} />; } function renderDeleteButton() { // Built-in plugins can only be disabled if (item.builtIn) return null; if (!props.onDelete) return null; return