import * as React from 'react'; import { Dispatch } from 'redux'; import { useRef, useCallback, useId, useState } from 'react'; import { _ } from '@joplin/lib/locale'; import DialogButtonRow from '../DialogButtonRow'; import Dialog from '@joplin/lib/components/Dialog'; import styled from 'styled-components'; import DialogTitle from '../DialogTitle'; import SyncTargetRegistry, { SyncTargetInfo } from '@joplin/lib/SyncTargetRegistry'; import useElementSize from '@joplin/lib/hooks/useElementSize'; import Button, { ButtonLevel } from '../Button/Button'; import bridge from '../../services/bridge'; import Setting from '@joplin/lib/models/Setting'; import JoplinCloudSignUpCallToAction from '../JoplinCloudSignUpCallToAction'; interface Props { themeId: number; dispatch: Dispatch; } const StyledRoot = styled.div<{ expanded: boolean }>` width: 100%; max-width: 100%; `; const SyncTargetDescription = styled.div<{ height: number }>` ${props => props.height ? `height: ${props.height}px` : ''}; margin-bottom: 1.3em; line-height: ${props => props.theme.lineHeight}; font-size: 16px; `; const ContentRoot = styled.div` background-color: ${props => props.theme.backgroundColor3}; padding: 16px; border-radius: 8px; `; const SelfHostingMessage = styled.div` color: ${props => props.theme.color}; font-style: italic; margin-top: 1em; opacity: 0.6; `; const SyncTargetBoxes = styled.div` display: flex; flex-direction: row; justify-content: center; `; const OtherOptions = styled.div` margin-top: 1.5em; `; const OtherOptionsToggle = styled.button` display: flex; align-items: center; gap: 0.5em; width: 100%; background: none; border: none; cursor: pointer; font-family: ${props => props.theme.fontFamily}; font-size: 16px; font-weight: bold; color: ${props => props.theme.color}; padding: 0.5em 0; text-align: left; &:hover { opacity: 0.8; } `; const OtherOptionsBoxes = styled.div` display: flex; flex-direction: row; justify-content: center; gap: 1em; margin-top: 1em; > * { margin-right: 0; } `; const SyncTargetTitle = styled.h2` display: flex; flex-direction: row; font-weight: bold; font-size: 1.7em; align-items: center; white-space: nowrap; `; const SyncTargetLogo = styled.img` height: 1.3em; margin-right: 0.4em; `; const SyncTargetBox = styled.div<{ hero?: boolean }>` display: flex; flex-direction: column; box-sizing: border-box; font-family: ${props => props.theme.fontFamily}; color: ${props => props.theme.color}; background-color: ${props => props.theme.backgroundColor}; border: ${props => props.hero ? `2px solid ${props.theme.color4}` : `1px solid ${props.theme.dividerColor}`}; border-radius: 8px; padding: 2em 2.2em 2em 2.2em; flex: 1 1 0; min-width: 0; max-width: 400px; opacity: 1; `; const FeatureList = styled.ul` margin-bottom: 1em; list-style-type: none; padding: 0; `; const FeatureIcon = styled.i` display: inline-flex; width: 16px; justify-content: center; color: ${props => props.theme.color4}; position: absolute; `; const FeatureLine = styled.li<{ enabled: boolean }>` margin-bottom: .5em; opacity: ${props => props.enabled ? 1 : 0.5}; position: relative; font-size: 16px; `; const FeatureLabel = styled.div` margin-left: 24px; line-height: ${props => props.theme.lineHeight}; `; const SelectButton = styled(Button)` padding: 10px 10px; height: auto; min-height: auto; max-height: fit-content; font-size: 1em; `; const LoginLink = styled.a` margin-top: 0.8em; font-size: 14px; align-self: center; color: ${props => props.theme.urlColor}; `; const SlowSyncWarning = styled.div` margin-top: 1em; opacity: 0.8; font-family: ${props => props.theme.fontFamily}; color: ${props => props.theme.color}; font-size: 14px; `; const syncTargetNames: string[] = [ 'joplinCloud', 'dropbox', 'onedrive', 'nextcloud', 'webdav', 'amazon_s3', 'joplinServer', 'joplinServerSaml', ]; const logosImageNames: Record = { 'dropbox': 'SyncTarget_Dropbox.svg', 'joplinCloud': 'SyncTarget_JoplinCloud.svg', 'onedrive': 'SyncTarget_OneDrive.svg', }; type SyncTargetInfoName = 'dropbox' | 'onedrive' | 'joplinCloud'; export default function(props: Props) { const joplinCloudDescriptionRef = useRef(null); const closeDialog = useCallback(() => { props.dispatch({ type: 'DIALOG_CLOSE', name: 'syncWizard', }); }, [props.dispatch]); const onButtonRowClick = useCallback(() => { closeDialog(); }, [closeDialog]); const { height: descriptionHeight } = useElementSize(joplinCloudDescriptionRef); function renderFeature(enabled: boolean, label: string) { const className = enabled ? 'fas fa-check' : 'fas fa-times'; return ( {label} ); } function renderFeatures(name: string) { return ( {[ renderFeature(true, _('Sync your notes')), renderFeature(name === 'joplinCloud', _('Publish notes to the internet')), renderFeature(name === 'joplinCloud', _('Collaborate on notebooks with others')), ]} ); } const onSelectButtonClick = useCallback(async (name: SyncTargetInfoName) => { const routes = { 'dropbox': { name: 'DropboxLogin', target: 7 }, 'onedrive': { name: 'OneDriveLogin', target: 3 }, 'joplinCloud': { name: 'JoplinCloudLogin', target: 10 }, }; const route = routes[name]; if (!route) return; // throw error?? Setting.setValue('sync.target', route.target); await Setting.saveAll(); closeDialog(); props.dispatch({ type: 'NAV_GO', routeName: route.name, }); }, [props.dispatch, closeDialog]); const baseId = useId(); const [otherOptionsExpanded, setOtherOptionsExpanded] = useState(false); function renderSelectArea(info: SyncTargetInfo, describedById: string) { return ( onSelectButtonClick(info.name as SyncTargetInfoName)} disabled={false} aria-describedby={describedById} /> ); } function renderActionArea(info: SyncTargetInfo, describedById: string) { if (info.name === 'joplinCloud') { return ( <> { event.preventDefault(); void onSelectButtonClick(info.name as SyncTargetInfoName); }} aria-describedby={describedById} >{_('Already have an account? Log in')} ); } return renderSelectArea(info, describedById); } function renderSyncTarget(info: SyncTargetInfo, hero: boolean) { const key = `syncTarget_${info.name}`; const height = info.name !== 'joplinCloud' ? descriptionHeight : null; const logoImageName = logosImageNames[info.name]; const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/${logoImageName}` : ''; const logo = logoImageSrc ? : null; const descriptionComp = ( {info.description} ); const featuresComp = renderFeatures(info.name); const renderSlowSyncWarning = () => { if (info.name === 'joplinCloud') return null; return {_('%s is not optimised for synchronising many small files so your initial synchronisation will be slow.', info.label)}; }; const headerId = `${baseId}-${info.id}`; return ( {logo}{info.label} {descriptionComp} {featuresComp} {renderActionArea(info, headerId)} {renderSlowSyncWarning()} ); } const onSelfHostingClick = useCallback(() => { closeDialog(); props.dispatch({ type: 'NAV_GO', routeName: 'Config', props: { defaultSection: 'sync', }, }); }, [props.dispatch, closeDialog]); function renderContent() { const heroBoxes: React.ReactNode[] = []; const otherBoxes: React.ReactNode[] = []; for (const name of syncTargetNames) { const info = SyncTargetRegistry.infoByName(name); if (info.supportsSelfHosted) continue; if (info.name === 'joplinCloud') { heroBoxes.push(renderSyncTarget(info, true)); } else { otherBoxes.push(renderSyncTarget(info, false)); } } const selfHostingLabelId = `${baseId}-selfHosting`; const selfHostingLinkId = `${baseId}-selfHostingLink`; const selfHostingMessage = Self-hosting? Joplin also supports various self-hosting options such as Nextcloud, WebDAV, AWS S3 and Joplin Server. {' '} Click here to select one . ; const otherOptionsRegionId = `${baseId}-otherOptions`; const otherOptions = setOtherOptionsExpanded(prev => !prev)} > {_('Other sync options')} ; return ( {heroBoxes} {otherOptions} ); } function renderDialogWrapper() { return ( {renderContent()} ); } return ( {renderDialogWrapper()} ); }