You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Desktop: Add Sync Wizard dialog
This commit is contained in:
		| @@ -573,6 +573,9 @@ packages/app-desktop/gui/Sidebar/styles/index.js.map | ||||
| packages/app-desktop/gui/StatusScreen/StatusScreen.d.ts | ||||
| packages/app-desktop/gui/StatusScreen/StatusScreen.js | ||||
| packages/app-desktop/gui/StatusScreen/StatusScreen.js.map | ||||
| packages/app-desktop/gui/SyncWizard/Dialog.d.ts | ||||
| packages/app-desktop/gui/SyncWizard/Dialog.js | ||||
| packages/app-desktop/gui/SyncWizard/Dialog.js.map | ||||
| packages/app-desktop/gui/TagList.d.ts | ||||
| packages/app-desktop/gui/TagList.js | ||||
| packages/app-desktop/gui/TagList.js.map | ||||
| @@ -870,6 +873,9 @@ packages/lib/SyncTargetJoplinServer.js.map | ||||
| packages/lib/SyncTargetOneDrive.d.ts | ||||
| packages/lib/SyncTargetOneDrive.js | ||||
| packages/lib/SyncTargetOneDrive.js.map | ||||
| packages/lib/SyncTargetRegistry.d.ts | ||||
| packages/lib/SyncTargetRegistry.js | ||||
| packages/lib/SyncTargetRegistry.js.map | ||||
| packages/lib/Synchronizer.d.ts | ||||
| packages/lib/Synchronizer.js | ||||
| packages/lib/Synchronizer.js.map | ||||
| @@ -927,6 +933,12 @@ packages/lib/fs-driver-node.js.map | ||||
| packages/lib/fsDriver.test.d.ts | ||||
| packages/lib/fsDriver.test.js | ||||
| packages/lib/fsDriver.test.js.map | ||||
| packages/lib/hooks/useElementSize.d.ts | ||||
| packages/lib/hooks/useElementSize.js | ||||
| packages/lib/hooks/useElementSize.js.map | ||||
| packages/lib/hooks/useEventListener.d.ts | ||||
| packages/lib/hooks/useEventListener.js | ||||
| packages/lib/hooks/useEventListener.js.map | ||||
| packages/lib/htmlUtils.d.ts | ||||
| packages/lib/htmlUtils.js | ||||
| packages/lib/htmlUtils.js.map | ||||
|   | ||||
							
								
								
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -558,6 +558,9 @@ packages/app-desktop/gui/Sidebar/styles/index.js.map | ||||
| packages/app-desktop/gui/StatusScreen/StatusScreen.d.ts | ||||
| packages/app-desktop/gui/StatusScreen/StatusScreen.js | ||||
| packages/app-desktop/gui/StatusScreen/StatusScreen.js.map | ||||
| packages/app-desktop/gui/SyncWizard/Dialog.d.ts | ||||
| packages/app-desktop/gui/SyncWizard/Dialog.js | ||||
| packages/app-desktop/gui/SyncWizard/Dialog.js.map | ||||
| packages/app-desktop/gui/TagList.d.ts | ||||
| packages/app-desktop/gui/TagList.js | ||||
| packages/app-desktop/gui/TagList.js.map | ||||
| @@ -855,6 +858,9 @@ packages/lib/SyncTargetJoplinServer.js.map | ||||
| packages/lib/SyncTargetOneDrive.d.ts | ||||
| packages/lib/SyncTargetOneDrive.js | ||||
| packages/lib/SyncTargetOneDrive.js.map | ||||
| packages/lib/SyncTargetRegistry.d.ts | ||||
| packages/lib/SyncTargetRegistry.js | ||||
| packages/lib/SyncTargetRegistry.js.map | ||||
| packages/lib/Synchronizer.d.ts | ||||
| packages/lib/Synchronizer.js | ||||
| packages/lib/Synchronizer.js.map | ||||
| @@ -912,6 +918,12 @@ packages/lib/fs-driver-node.js.map | ||||
| packages/lib/fsDriver.test.d.ts | ||||
| packages/lib/fsDriver.test.js | ||||
| packages/lib/fsDriver.test.js.map | ||||
| packages/lib/hooks/useElementSize.d.ts | ||||
| packages/lib/hooks/useElementSize.js | ||||
| packages/lib/hooks/useElementSize.js.map | ||||
| packages/lib/hooks/useEventListener.d.ts | ||||
| packages/lib/hooks/useEventListener.js | ||||
| packages/lib/hooks/useEventListener.js.map | ||||
| packages/lib/htmlUtils.d.ts | ||||
| packages/lib/htmlUtils.js | ||||
| packages/lib/htmlUtils.js.map | ||||
|   | ||||
| @@ -10,7 +10,7 @@ const { cliUtils } = require('./cli-utils.js'); | ||||
| const md5 = require('md5'); | ||||
| const locker = require('proper-lockfile'); | ||||
| const fs = require('fs-extra'); | ||||
| const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry'); | ||||
| const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry').default; | ||||
| const MigrationHandler = require('@joplin/lib/services/synchronizer/MigrationHandler').default; | ||||
|  | ||||
| class Command extends BaseCommand { | ||||
|   | ||||
| @@ -114,6 +114,10 @@ interface AppStateRoute { | ||||
| 	props: any; | ||||
| } | ||||
|  | ||||
| export interface AppStateDialog { | ||||
| 	name: string; | ||||
| } | ||||
|  | ||||
| export interface AppState extends State { | ||||
| 	route: AppStateRoute; | ||||
| 	navHistory: any[]; | ||||
| @@ -130,6 +134,7 @@ export interface AppState extends State { | ||||
| 	// Extra reducer keys go here | ||||
| 	watchedResources: any; | ||||
| 	mainLayout: LayoutItem; | ||||
| 	dialogs: AppStateDialog[]; | ||||
| } | ||||
|  | ||||
| const appDefaultState: AppState = { | ||||
| @@ -150,6 +155,7 @@ const appDefaultState: AppState = { | ||||
| 	layoutMoveMode: false, | ||||
| 	mainLayout: null, | ||||
| 	startupPluginsLoaded: false, | ||||
| 	dialogs: [], | ||||
| 	...resourceEditWatcherDefaultState, | ||||
| }; | ||||
|  | ||||
| @@ -370,6 +376,30 @@ class Application extends BaseApplication { | ||||
| 				} | ||||
| 				break; | ||||
|  | ||||
| 			case 'DIALOG_OPEN': | ||||
|  | ||||
| 				{ | ||||
| 					newState = Object.assign({}, state); | ||||
| 					const newDialogs = newState.dialogs.slice(); | ||||
|  | ||||
| 					if (newDialogs.find(d => d.name === action.name)) throw new Error(`This dialog is already opened: ${action.name}`); | ||||
|  | ||||
| 					newDialogs.push({ | ||||
| 						name: action.name, | ||||
| 					}); | ||||
| 					newState.dialogs = newDialogs; | ||||
| 				} | ||||
| 				break; | ||||
|  | ||||
| 			case 'DIALOG_CLOSE': | ||||
|  | ||||
| 				{ | ||||
| 					newState = Object.assign({}, state); | ||||
| 					const newDialogs = newState.dialogs.slice().filter(d => d.name !== action.name); | ||||
| 					newState.dialogs = newDialogs; | ||||
| 				} | ||||
| 				break; | ||||
|  | ||||
| 			case 'LAYOUT_MOVE_MODE_SET': | ||||
|  | ||||
| 				newState = { | ||||
| @@ -834,6 +864,14 @@ class Application extends BaseApplication { | ||||
| 		// 	}); | ||||
| 		// }, 5000); | ||||
|  | ||||
|  | ||||
| 		// setTimeout(() => { | ||||
| 		// 	this.dispatch({ | ||||
| 		// 		type: 'DIALOG_OPEN', | ||||
| 		// 		name: 'syncWizard', | ||||
| 		// 	}); | ||||
| 		// }, 2000); | ||||
|  | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,4 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <svg width="43px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 43 40" version="1.1" height="40px"> | ||||
|  <path d="m12.5 0l-12.5 8.1 8.7 7 12.5-7.8-8.7-7.3zm-12.5 21.9l12.5 8.2 8.7-7.3-12.5-7.7-8.7 6.8zm21.2 0.9l8.8 7.3 12.4-8.1-8.6-6.9-12.6 7.7zm21.2-14.7l-12.4-8.1-8.8 7.3 12.6 7.8 8.6-7zm-21.1 16.3l-8.8 7.3-3.7-2.5v2.8l12.5 7.5 12.5-7.5v-2.8l-3.8 2.5-8.7-7.3z" fill="#007EE5"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 441 B | 
| @@ -0,0 +1,19 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 682.66669 682.66669" height="682.66669" width="682.66669" xml:space="preserve" id="svg2" version="1.1"> | ||||
|   <defs id="defs6"> | ||||
|     <linearGradient id="linearGradient26" spreadMethod="pad" gradientTransform="matrix(-4387.91,4387.91,4387.91,4387.91,4753.95,366.05)" gradientUnits="userSpaceOnUse" y2="0" x2="1" y1="0" x1="0"> | ||||
|       <stop id="stop22" offset="0" style="stop-opacity:1;stop-color:#004caf"/> | ||||
|       <stop id="stop24" offset="1" style="stop-opacity:1;stop-color:#1f95f8"/> | ||||
|     </linearGradient> | ||||
|   </defs> | ||||
|   <g transform="matrix(1.3333333,0,0,-1.3333333,0,682.66667)" id="g10"> | ||||
|     <g transform="scale(0.1)" id="g12"> | ||||
|       <g id="g14"> | ||||
|         <g clip-path="url(#clipPath20)" id="g16"> | ||||
|           <path id="path28" style="fill:url(#linearGradient26);fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 3873.89,0 H 1246.11 C 560.754,0 0,560.75 0,1246.11 V 3873.88 C 0,4559.25 560.754,5120 1246.11,5120 H 3873.89 C 4559.25,5120 5120,4559.25 5120,3873.88 V 1246.11 C 5120,560.75 4559.25,0 3873.89,0"/> | ||||
|         </g> | ||||
|       </g> | ||||
|       <path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 3961.59,4435.23 H 2570.18 c -13.15,0 -23.78,-10.64 -23.78,-23.77 v -441.84 c 0,-14.87 12.04,-26.92 26.92,-26.92 h 190.77 c 77.16,0 139.73,-59.35 146.43,-134.77 V 3505 3336.23 1728.75 1717.36 h -0.05 c 0.48,-16.84 -0.19,-33.4 -1.83,-49.71 -0.18,-2.38 -0.5,-4.73 -0.79,-7.09 -1.1,-9.53 -2.32,-19.01 -4.17,-28.29 -1.01,-5.29 -2.44,-10.44 -3.71,-15.65 -1.71,-6.93 -3.09,-13.97 -5.22,-20.75 -12.58,-40.27 -32.47,-77.62 -59.98,-110.5 -1.01,-1.17 -2.26,-2.25 -3.26,-3.41 -8.39,-9.72 -17.2,-19.19 -26.95,-28.06 -9.84,-8.95 -20.26,-17.27 -31.21,-25 -77.84,-55.14 -182.61,-79.4 -299.67,-68.2 -149.26,14.03 -297.34,81.72 -417.03,190.62 -119.67,108.89 -194.08,243.62 -209.48,379.41 -13.85,121.48 22.55,228.38 102.42,301.05 0.21,0.16 0.4,0.31 0.56,0.48 3.09,2.77 6.49,5.2 9.67,7.87 57.16,47.89 131.67,76.91 216.7,84.91 0.96,0.09 1.88,0.24 2.79,0.32 8.95,0.79 18.07,1.15 27.27,1.49 4.81,0.16 9.56,0.5 14.44,0.54 1.62,0.02 3.16,0.19 4.78,0.19 2.9,0 5.91,-0.38 8.81,-0.42 13.4,-0.21 26.9,-0.76 40.67,-1.94 1.74,-0.14 3.4,-0.08 5.19,-0.24 1.27,-0.13 2.53,-0.41 3.8,-0.54 78,-7.82 155.23,-31.11 228.52,-66.4 1.53,-0.07 3.3,-0.54 5.51,-1.76 22.34,-12.34 26.62,0.9 27.28,9.65 v 382.24 282.82 c 0,19.05 -13.25,35.9 -31.83,39.99 -394.76,86.88 -782.08,-3.55 -1055.38,-252.34 -238.75,-217.18 -354.24,-530.58 -316.82,-859.79 33.39,-293.23 183.91,-574.94 423.88,-793.33 233.89,-212.79 531.69,-345.86 838.88,-374.801 42.33,-3.918 84.86,-5.938 126.36,-5.938 293.38,0 565.61,100.598 766.54,283.379 190.34,173.3 304.35,411.27 321.08,670.16 l 1.55,1697.91 h 0.17 v 453.97 h 0.06 v 7.92 c 1.72,80.12 67.05,144.58 147.61,144.58 h 190.77 c 14.86,0 26.92,12.05 26.92,26.92 v 441.84 c 0,13.13 -10.63,23.77 -23.78,23.77"/> | ||||
|     </g> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 3.0 KiB | 
| @@ -0,0 +1,34 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ | ||||
| 	<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/"> | ||||
| 	<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/"> | ||||
| 	<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/"> | ||||
| 	<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/"> | ||||
| 	<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/"> | ||||
| 	<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/"> | ||||
| 	<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/"> | ||||
| 	<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/"> | ||||
| ]> | ||||
| <svg version="1.1" id="Livello_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;" | ||||
| 	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1030.04 659.922" | ||||
| 	 enable-background="new 0 0 1030.04 659.922" xml:space="preserve"> | ||||
| <metadata> | ||||
| 	<sfw  xmlns="&ns_sfw;"> | ||||
| 		<slices></slices> | ||||
| 		<sliceSourceBounds  bottomLeftOrigin="true" height="659.922" width="1030.04" x="-490" y="-344.922"></sliceSourceBounds> | ||||
| 	</sfw> | ||||
| </metadata> | ||||
| <g id="STYLE_COLOR_1_"> | ||||
| 	<path fill="#0364B8" d="M622.292,445.338l212.613-203.327C790.741,69.804,615.338-33.996,443.13,10.168 | ||||
| 		C365.58,30.056,298.224,78.13,254.209,145.005C257.5,144.922,622.292,445.338,622.292,445.338z"/> | ||||
| 	<path fill="#0078D4" d="M392.776,183.283l-0.01,0.035c-40.626-25.162-87.479-38.462-135.267-38.397 | ||||
| 		c-1.104,0-2.189,0.07-3.291,0.083C112.064,146.765-1.74,263.423,0.02,405.567c0.638,51.562,16.749,101.743,46.244,144.04 | ||||
| 		l318.528-39.894l244.209-196.915L392.776,183.283z"/> | ||||
| 	<path fill="#1490DF" d="M834.905,242.012c-4.674-0.312-9.371-0.528-14.123-0.528c-28.523-0.028-56.749,5.798-82.93,17.117 | ||||
| 		l-0.006-0.022l-128.844,54.22l142.041,175.456l253.934,61.728c54.799-101.732,16.752-228.625-84.98-283.424 | ||||
| 		c-26.287-14.16-55.301-22.529-85.091-24.546V242.012z"/> | ||||
| 	<path fill="#28A8EA" d="M46.264,549.607C94.359,618.756,173.27,659.966,257.5,659.922h563.281 | ||||
| 		c76.946,0.022,147.691-42.202,184.195-109.937L609.001,312.798L46.264,549.607z"/> | ||||
| </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 2.1 KiB | 
| @@ -11,7 +11,7 @@ import EncryptionConfigScreen from '../EncryptionConfigScreen'; | ||||
| const { connect } = require('react-redux'); | ||||
| const { themeStyle } = require('@joplin/lib/theme'); | ||||
| const pathUtils = require('@joplin/lib/path-utils'); | ||||
| const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry'); | ||||
| import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; | ||||
| const shared = require('@joplin/lib/components/shared/config-shared.js'); | ||||
| import ClipperConfigScreen from '../ClipperConfigScreen'; | ||||
| const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen'); | ||||
| @@ -94,6 +94,11 @@ class ConfigScreenComponent extends React.Component<any, any> { | ||||
| 			Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalData); | ||||
| 			await Setting.saveAll(); | ||||
| 			bridge().restart(); | ||||
| 		} else if (key === 'sync.openSyncWizard') { | ||||
| 			this.props.dispatch({ | ||||
| 				type: 'DIALOG_OPEN', | ||||
| 				name: 'syncWizard', | ||||
| 			}); | ||||
| 		} else { | ||||
| 			throw new Error(`Unhandled key: ${key}`); | ||||
| 		} | ||||
| @@ -606,11 +611,15 @@ class ConfigScreenComponent extends React.Component<any, any> { | ||||
| 				</div> | ||||
| 			); | ||||
| 		} else if (md.type === Setting.TYPE_BUTTON) { | ||||
| 			const labelComp = md.hideLabel ? null : ( | ||||
| 				<div style={labelStyle}> | ||||
| 					<label>{md.label()}</label> | ||||
| 				</div> | ||||
| 			); | ||||
|  | ||||
| 			return ( | ||||
| 				<div key={key} style={rowStyle}> | ||||
| 					<div style={labelStyle}> | ||||
| 						<label>{md.label()}</label> | ||||
| 					</div> | ||||
| 					{labelComp} | ||||
| 					<Button level={ButtonLevel.Secondary} title={md.label()} onClick={md.onClick ? md.onClick : () => this.handleSettingButton(key)}/> | ||||
| 					{descriptionComp} | ||||
| 				</div> | ||||
|   | ||||
| @@ -18,10 +18,11 @@ const DialogRoot = styled.div` | ||||
| 	background-color: ${props => props.theme.backgroundColor}; | ||||
| 	padding: 16px; | ||||
| 	box-shadow: 6px 6px 20px rgba(0,0,0,0.5); | ||||
| 	margin-top: 20px; | ||||
| 	margin: 20px; | ||||
| 	min-height: fit-content; | ||||
| 	display: flex; | ||||
| 	flex-direction: column; | ||||
| 	border-radius: 10px; | ||||
| `; | ||||
|  | ||||
| interface Props { | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import styled from 'styled-components'; | ||||
|  | ||||
| const Root = styled.div` | ||||
| 	display: flex; | ||||
| 	justify-content: ${props => props.justifyContent ? props.justifyContent : 'flex-start'}; | ||||
| 	font-family: ${props => props.theme.fontFamily}; | ||||
| 	font-size: ${props => props.theme.fontSize * 1.5}px; | ||||
| 	line-height: 1.6em; | ||||
| @@ -12,10 +14,11 @@ const Root = styled.div` | ||||
|  | ||||
| interface Props { | ||||
| 	title: string; | ||||
| 	justifyContent?: string; | ||||
| } | ||||
|  | ||||
| export default function DialogTitle(props: Props) { | ||||
| 	return ( | ||||
| 		<Root>{props.title}</Root> | ||||
| 		<Root justifyContent={props.justifyContent}>{props.title}</Root> | ||||
| 	); | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import app from '../app'; | ||||
| import app, { AppState, AppStateDialog } from '../app'; | ||||
| import MainScreen from './MainScreen/MainScreen'; | ||||
| import ConfigScreen from './ConfigScreen/ConfigScreen'; | ||||
| import StatusScreen from './StatusScreen/StatusScreen'; | ||||
| @@ -10,7 +10,6 @@ import { Size } from './ResizableLayout/utils/types'; | ||||
| import MenuBar from './MenuBar'; | ||||
| import { _ } from '@joplin/lib/locale'; | ||||
| const React = require('react'); | ||||
|  | ||||
| const { render } = require('react-dom'); | ||||
| const { connect, Provider } = require('react-redux'); | ||||
| import Setting from '@joplin/lib/models/Setting'; | ||||
| @@ -19,6 +18,7 @@ import ClipperServer from '@joplin/lib/ClipperServer'; | ||||
| import DialogTitle from './DialogTitle'; | ||||
| import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './DialogButtonRow'; | ||||
| import Dialog from './Dialog'; | ||||
| import SyncWizardDialog from './SyncWizard/Dialog'; | ||||
| const { ImportScreen } = require('./ImportScreen.min.js'); | ||||
| const { ResourceScreen } = require('./ResourceScreen.js'); | ||||
| const { Navigator } = require('./Navigator.min.js'); | ||||
| @@ -33,6 +33,7 @@ interface Props { | ||||
| 	size: Size; | ||||
| 	zoomFactor: number; | ||||
| 	needApiAuth: boolean; | ||||
| 	dialogs: AppStateDialog; | ||||
| } | ||||
|  | ||||
| interface ModalDialogProps { | ||||
| @@ -42,6 +43,24 @@ interface ModalDialogProps { | ||||
| 	onClick: ClickEventHandler; | ||||
| } | ||||
|  | ||||
| interface RegisteredDialogProps { | ||||
| 	themeId: number; | ||||
| 	key: string; | ||||
| 	dispatch: Function; | ||||
| } | ||||
|  | ||||
| interface RegisteredDialog { | ||||
| 	render: (props: RegisteredDialogProps)=> any; | ||||
| } | ||||
|  | ||||
| const registeredDialogs: Record<string, RegisteredDialog> = { | ||||
| 	syncWizard: { | ||||
| 		render: (props: RegisteredDialogProps) => { | ||||
| 			return <SyncWizardDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId}/>; | ||||
| 		}, | ||||
| 	}, | ||||
| }; | ||||
|  | ||||
| const GlobalStyle = createGlobalStyle` | ||||
| 	* { | ||||
| 		box-sizing: border-box; | ||||
| @@ -151,6 +170,22 @@ class RootComponent extends React.Component<Props, any> { | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	private renderDialogs() { | ||||
| 		if (!this.props.dialogs.length) return null; | ||||
|  | ||||
| 		const output: any[] = []; | ||||
| 		for (const dialog of this.props.dialogs) { | ||||
| 			const md = registeredDialogs[dialog.name]; | ||||
| 			if (!md) throw new Error(`Unknown dialog: ${dialog.name}`); | ||||
| 			output.push(md.render({ | ||||
| 				key: dialog.name, | ||||
| 				themeId: this.props.themeId, | ||||
| 				dispatch: this.props.dispatch, | ||||
| 			})); | ||||
| 		} | ||||
| 		return output; | ||||
| 	} | ||||
|  | ||||
| 	public render() { | ||||
| 		const navigatorStyle = { | ||||
| 			width: this.props.size.width / this.props.zoomFactor, | ||||
| @@ -176,19 +211,21 @@ class RootComponent extends React.Component<Props, any> { | ||||
| 					<GlobalStyle/> | ||||
| 					<Navigator style={navigatorStyle} screens={screens} /> | ||||
| 					{this.renderModalMessage(this.modalDialogProps())} | ||||
| 					{this.renderDialogs()} | ||||
| 				</ThemeProvider> | ||||
| 			</StyleSheetManager> | ||||
| 		); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const mapStateToProps = (state: any) => { | ||||
| const mapStateToProps = (state: AppState) => { | ||||
| 	return { | ||||
| 		size: state.windowContentSize, | ||||
| 		zoomFactor: state.settings.windowContentZoomFactor / 100, | ||||
| 		appState: state.appState, | ||||
| 		themeId: state.settings.theme, | ||||
| 		needApiAuth: state.needApiAuth, | ||||
| 		dialogs: state.dialogs, | ||||
| 	}; | ||||
| }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										343
									
								
								packages/app-desktop/gui/SyncWizard/Dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								packages/app-desktop/gui/SyncWizard/Dialog.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,343 @@ | ||||
| import * as React from 'react'; | ||||
| import { useState, useRef, useCallback } from 'react'; | ||||
| import { _ } from '@joplin/lib/locale'; | ||||
| import DialogButtonRow from '../DialogButtonRow'; | ||||
| import Dialog from '../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 StyledInput from '../style/StyledInput'; | ||||
| import Setting from '../../../lib/models/Setting'; | ||||
| import SyncTargetJoplinCloud from '../../../lib/SyncTargetJoplinCloud'; | ||||
| import StyledLink from '../style/StyledLink'; | ||||
|  | ||||
| interface Props { | ||||
| 	themeId: number; | ||||
| 	dispatch: Function; | ||||
| } | ||||
|  | ||||
| const StyledRoot = styled.div` | ||||
| 	min-width: 500px; | ||||
| 	max-width: 1200px; | ||||
| `; | ||||
|  | ||||
| const SyncTargetDescription = styled.div` | ||||
| 	${props => props.height ? `height: ${props.height}px` : ''}; | ||||
| 	margin-bottom: 1.3em; | ||||
| 	line-height: ${props => props.theme.lineHeight}; | ||||
| 	font-size: 16px; | ||||
| `; | ||||
|  | ||||
| const CreateAccountLink = styled(StyledLink)` | ||||
| 	font-size: 16px; | ||||
| `; | ||||
|  | ||||
| const ContentRoot = styled.div` | ||||
| 	background-color: ${props => props.theme.backgroundColor3}; | ||||
| 	padding: 1em; | ||||
| 	padding-right: 0; | ||||
| `; | ||||
|  | ||||
| const SelfHostingMessage = styled.div` | ||||
| 	color: ${props => props.theme.color}; | ||||
| 	padding-right: 1em; | ||||
| 	font-style: italic; | ||||
| 	margin-top: 1em; | ||||
| 	opacity: 0.6; | ||||
| `; | ||||
|  | ||||
| const SyncTargetBoxes = styled.div` | ||||
| 	display: flex; | ||||
| 	flex-direction: row; | ||||
| 	justify-content: center; | ||||
| `; | ||||
|  | ||||
| const SyncTargetTitle = styled.p` | ||||
| 	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` | ||||
| 	display: flex; | ||||
| 	flex: 1; | ||||
| 	flex-direction: column; | ||||
| 	font-family: ${props => props.theme.fontFamily}; | ||||
| 	color: ${props => props.theme.color}; | ||||
| 	background-color: ${props => props.theme.backgroundColor}; | ||||
| 	border: 1px solid ${props => props.theme.dividerColor}; | ||||
| 	border-radius: 8px; | ||||
| 	padding: 0.8em 2.2em 2em 2.2em; | ||||
| 	margin-right: 1em; | ||||
| 	max-width: 400px; | ||||
| 	opacity: ${props => props.faded ? 0.5 : 1}; | ||||
| `; | ||||
|  | ||||
| const FeatureList = styled.div` | ||||
| 	margin-bottom: 1em; | ||||
| `; | ||||
|  | ||||
| const FeatureIcon = styled.i` | ||||
| 	display: inline-flex; | ||||
| 	width: 16px; | ||||
| 	justify-content: center; | ||||
| 	color: ${props => props.theme.color4}; | ||||
| 	position: absolute; | ||||
| `; | ||||
|  | ||||
| const FeatureLine = styled.div` | ||||
| 	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 JoplinCloudLoginForm = styled.div` | ||||
| 	display: flex; | ||||
| 	flex-direction: column; | ||||
| `; | ||||
|  | ||||
| const FormLabel = styled.label` | ||||
| 	font-weight: bold; | ||||
| 	margin: 1em 0 0.6em 0; | ||||
| `; | ||||
|  | ||||
| const syncTargetNames: string[] = [ | ||||
| 	'joplinCloud', | ||||
| 	'dropbox', | ||||
| 	'onedrive', | ||||
| 	'nextcloud', | ||||
| 	'webdav', | ||||
| 	'amazon_s3', | ||||
| 	'joplinServer', | ||||
| ]; | ||||
|  | ||||
|  | ||||
| const logosImageNames: Record<string, string> = { | ||||
| 	'dropbox': 'Dropbox.svg', | ||||
| 	'joplinCloud': 'JoplinCloud.svg', | ||||
| 	'onedrive': 'OneDrive.svg', | ||||
| }; | ||||
|  | ||||
| export default function(props: Props) { | ||||
| 	const [showJoplinCloudForm, setShowJoplinCloudForm] = useState(false); | ||||
| 	const joplinCloudDescriptionRef = useRef(null); | ||||
| 	const [joplinCloudEmail, setJoplinCloudEmail] = useState(''); | ||||
| 	const [joplinCloudPassword, setJoplinCloudPassword] = useState(''); | ||||
| 	const [joplinCloudLoginInProgress, setJoplinCloudLoginInProgress] = useState(false); | ||||
|  | ||||
| 	function closeDialog(dispatch: Function) { | ||||
| 		dispatch({ | ||||
| 			type: 'DIALOG_CLOSE', | ||||
| 			name: 'syncWizard', | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	const onButtonRowClick = useCallback(() => { | ||||
| 		closeDialog(props.dispatch); | ||||
| 	}, [props.dispatch]); | ||||
|  | ||||
| 	const { height: descriptionHeight } = useElementSize(joplinCloudDescriptionRef); | ||||
|  | ||||
| 	function renderFeature(enabled: boolean, label: string) { | ||||
| 		const className = enabled ? 'fas fa-check' : 'fas fa-times'; | ||||
| 		return ( | ||||
| 			<FeatureLine enabled={enabled} key={label}><FeatureIcon className={className}></FeatureIcon> <FeatureLabel>{label}</FeatureLabel></FeatureLine> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	function renderFeatures(name: string) { | ||||
| 		return ( | ||||
| 			<FeatureList> | ||||
| 				{[ | ||||
| 					renderFeature(true, _('Sync your notes')), | ||||
| 					renderFeature(name === 'joplinCloud', _('Publish notes to the internet')), | ||||
| 					renderFeature(name === 'joplinCloud', _('Collaborate on notebooks with others')), | ||||
| 				]} | ||||
| 			</FeatureList> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	const onJoplinCloudEmailChange = useCallback((event: any) => { | ||||
| 		setJoplinCloudEmail(event.target.value); | ||||
| 	}, []); | ||||
|  | ||||
| 	const onJoplinCloudPasswordChange = useCallback((event: any) => { | ||||
| 		setJoplinCloudPassword(event.target.value); | ||||
| 	}, []); | ||||
|  | ||||
| 	const onJoplinCloudLoginClick = useCallback(async () => { | ||||
| 		setJoplinCloudLoginInProgress(true); | ||||
|  | ||||
| 		try { | ||||
| 			const result = await SyncTargetJoplinCloud.checkConfig({ | ||||
| 				password: () => joplinCloudPassword, | ||||
| 				path: () => Setting.value('sync.10.path'), | ||||
| 				userContentPath: () => Setting.value('sync.10.userContentPath'), | ||||
| 				username: () => joplinCloudEmail, | ||||
| 			}); | ||||
|  | ||||
| 			if (result.ok) { | ||||
| 				Setting.setValue('sync.target', 10); | ||||
| 				Setting.setValue('sync.10.username', joplinCloudEmail); | ||||
| 				Setting.setValue('sync.10.password', joplinCloudPassword); | ||||
| 				await Setting.saveAll(); | ||||
|  | ||||
| 				alert(_('Thank you! Your Joplin Cloud account is now setup and ready to use.')); | ||||
|  | ||||
| 				closeDialog(props.dispatch); | ||||
|  | ||||
| 				props.dispatch({ | ||||
| 					type: 'NAV_GO', | ||||
| 					routeName: 'Main', | ||||
| 				}); | ||||
| 			} else { | ||||
| 				alert(_('There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s', result.errorMessage)); | ||||
| 			} | ||||
| 		} finally { | ||||
| 			setJoplinCloudLoginInProgress(false); | ||||
| 		} | ||||
| 	}, [joplinCloudEmail, joplinCloudPassword, props.dispatch]); | ||||
|  | ||||
| 	const onJoplinCloudCreateAccountClick = useCallback(() => { | ||||
| 		bridge().openExternal('https://joplinapp.org/plans/'); | ||||
| 	}, []); | ||||
|  | ||||
| 	function renderJoplinCloudLoginForm() { | ||||
| 		return ( | ||||
| 			<JoplinCloudLoginForm> | ||||
| 				<div>{_('Login below.')} <CreateAccountLink href="#" onClick={onJoplinCloudCreateAccountClick}>{_('Or create an account.')}</CreateAccountLink></div> | ||||
| 				<FormLabel>Email</FormLabel> | ||||
| 				<StyledInput type="email" onChange={onJoplinCloudEmailChange}/> | ||||
| 				<FormLabel>Password</FormLabel> | ||||
| 				<StyledInput type="password" onChange={onJoplinCloudPasswordChange}/> | ||||
| 				<SelectButton mt="1.3em" disabled={joplinCloudLoginInProgress} level={ButtonLevel.Primary} title={_('Login')} onClick={onJoplinCloudLoginClick}/> | ||||
| 			</JoplinCloudLoginForm> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	const onSelectButtonClick = useCallback(async (name: string) => { | ||||
| 		if (name === 'joplinCloud') { | ||||
| 			setShowJoplinCloudForm(true); | ||||
| 		} else { | ||||
| 			Setting.setValue('sync.target', name === 'dropbox' ? 7 : 3); | ||||
| 			await Setting.saveAll(); | ||||
| 			closeDialog(props.dispatch); | ||||
| 			props.dispatch({ | ||||
| 				type: 'NAV_GO', | ||||
| 				routeName: name === 'dropbox' ? 'DropboxLogin' : 'OneDriveLogin', | ||||
| 			}); | ||||
| 		} | ||||
| 	}, [props.dispatch]); | ||||
|  | ||||
| 	function renderSelectArea(info: SyncTargetInfo) { | ||||
| 		if (info.name === 'joplinCloud' && showJoplinCloudForm) { | ||||
| 			return renderJoplinCloudLoginForm(); | ||||
| 		} else { | ||||
| 			return ( | ||||
| 				<SelectButton | ||||
| 					level={ButtonLevel.Primary} | ||||
| 					title={_('Select')} | ||||
| 					onClick={() => onSelectButtonClick(info.name)} | ||||
| 					disabled={joplinCloudLoginInProgress} | ||||
| 				/> | ||||
| 			); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	function renderSyncTarget(info: SyncTargetInfo) { | ||||
| 		const key = `syncTarget_${info.name}`; | ||||
| 		const height = info.name !== 'joplinCloud' ? descriptionHeight : null; | ||||
|  | ||||
| 		const logoImageName = logosImageNames[info.name]; | ||||
| 		const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/syncTargetLogos/${logoImageName}` : ''; | ||||
| 		const logo = logoImageSrc ? <SyncTargetLogo src={logoImageSrc}/> : null; | ||||
| 		const descriptionComp = <SyncTargetDescription height={height} ref={info.name === 'joplinCloud' ? joplinCloudDescriptionRef : null}>{info.description}</SyncTargetDescription>; | ||||
| 		const featuresComp = showJoplinCloudForm && info.name === 'joplinCloud' ? null : renderFeatures(info.name); | ||||
|  | ||||
| 		return ( | ||||
| 			<SyncTargetBox id={key} key={key} faded={showJoplinCloudForm && info.name !== 'joplinCloud'}> | ||||
| 				<SyncTargetTitle>{logo}{info.label}</SyncTargetTitle> | ||||
| 				{descriptionComp} | ||||
| 				{featuresComp} | ||||
| 				{renderSelectArea(info)} | ||||
| 			</SyncTargetBox> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	const onSelfHostingClick = useCallback(() => { | ||||
| 		closeDialog(props.dispatch); | ||||
|  | ||||
| 		props.dispatch({ | ||||
| 			type: 'NAV_GO', | ||||
| 			routeName: 'Config', | ||||
| 			props: { | ||||
| 				defaultSection: 'sync', | ||||
| 			}, | ||||
| 		}); | ||||
| 	}, [props.dispatch]); | ||||
|  | ||||
| 	function renderContent() { | ||||
| 		const boxes: any[] = []; | ||||
|  | ||||
| 		for (const name of syncTargetNames) { | ||||
| 			const info = SyncTargetRegistry.infoByName(name); | ||||
| 			if (info.supportsSelfHosted) continue; | ||||
| 			boxes.push(renderSyncTarget(info)); | ||||
| 		} | ||||
|  | ||||
| 		const selfHostingMessage = showJoplinCloudForm ? null : <SelfHostingMessage>Self-hosting? Joplin also supports various self-hosting options such as Nextcloud, WebDAV, AWS S3 and Joplin Server. <a href="#" onClick={onSelfHostingClick}>Click here to select one</a>.</SelfHostingMessage>; | ||||
|  | ||||
| 		return ( | ||||
| 			<ContentRoot> | ||||
| 				<SyncTargetBoxes> | ||||
| 					{boxes} | ||||
| 				</SyncTargetBoxes> | ||||
| 				{selfHostingMessage} | ||||
| 			</ContentRoot> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	function renderDialogWrapper() { | ||||
| 		return ( | ||||
| 			<StyledRoot> | ||||
| 				<DialogTitle title={_('Joplin can synchronise your notes using various providers. Select one from the list below.')} justifyContent="center"/> | ||||
| 				{renderContent()} | ||||
| 				<DialogButtonRow | ||||
| 					themeId={props.themeId} | ||||
| 					onClick={onButtonRowClick} | ||||
| 					okButtonShow={false} | ||||
| 					cancelButtonLabel={_('Close')} | ||||
| 				/> | ||||
| 			</StyledRoot> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<Dialog renderContent={renderDialogWrapper}/> | ||||
| 	); | ||||
| } | ||||
| @@ -19,7 +19,7 @@ const { BaseScreenComponent } = require('../base-screen.js'); | ||||
| const { Dropdown } = require('../Dropdown.js'); | ||||
| const { themeStyle } = require('../global-style.js'); | ||||
| const shared = require('@joplin/lib/components/shared/config-shared.js'); | ||||
| const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry'); | ||||
| import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; | ||||
| const RNFS = require('react-native-fs'); | ||||
|  | ||||
| class ConfigScreenComponent extends BaseScreenComponent { | ||||
|   | ||||
| @@ -78,7 +78,7 @@ import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine'; | ||||
| const WelcomeUtils = require('@joplin/lib/WelcomeUtils'); | ||||
| const { themeStyle } = require('./components/global-style.js'); | ||||
|  | ||||
| const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry.js'); | ||||
| import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; | ||||
| const SyncTargetFilesystem = require('@joplin/lib/SyncTargetFilesystem.js'); | ||||
| const SyncTargetNextcloud = require('@joplin/lib/SyncTargetNextcloud.js'); | ||||
| const SyncTargetWebDAV = require('@joplin/lib/SyncTargetWebDAV.js'); | ||||
|   | ||||
| @@ -30,7 +30,7 @@ const fs = require('fs-extra'); | ||||
| import JoplinError from './JoplinError'; | ||||
| const EventEmitter = require('events'); | ||||
| const syswidecas = require('./vendor/syswide-cas'); | ||||
| const SyncTargetRegistry = require('./SyncTargetRegistry.js'); | ||||
| import SyncTargetRegistry from './SyncTargetRegistry'; | ||||
| const SyncTargetFilesystem = require('./SyncTargetFilesystem.js'); | ||||
| const SyncTargetNextcloud = require('./SyncTargetNextcloud.js'); | ||||
| const SyncTargetWebDAV = require('./SyncTargetWebDAV.js'); | ||||
|   | ||||
| @@ -25,6 +25,14 @@ export default class BaseSyncTarget { | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	public static description(): string { | ||||
| 		return ''; | ||||
| 	} | ||||
|  | ||||
| 	public static supportsSelfHosted(): boolean { | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	public option(name: string, defaultValue: any = null) { | ||||
| 		return this.options_ && name in this.options_ ? this.options_[name] : defaultValue; | ||||
| 	} | ||||
|   | ||||
| @@ -28,6 +28,10 @@ class SyncTargetAmazonS3 extends BaseSyncTarget { | ||||
| 		return `${_('AWS S3')} (Beta)`; | ||||
| 	} | ||||
|  | ||||
| 	static description() { | ||||
| 		return 'A service offered by Amazon Web Services (AWS) that provides object storage through a web service interface.'; | ||||
| 	} | ||||
|  | ||||
| 	async isAuthenticated() { | ||||
| 		return true; | ||||
| 	} | ||||
|   | ||||
| @@ -25,6 +25,14 @@ class SyncTargetDropbox extends BaseSyncTarget { | ||||
| 		return _('Dropbox'); | ||||
| 	} | ||||
|  | ||||
| 	static description() { | ||||
| 		return 'A file hosting service that offers cloud storage and file synchronization'; | ||||
| 	} | ||||
|  | ||||
| 	static supportsSelfHosted() { | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	authRouteName() { | ||||
| 		return 'DropboxLogin'; | ||||
| 	} | ||||
|   | ||||
| @@ -30,6 +30,14 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget { | ||||
| 		return _('Joplin Cloud'); | ||||
| 	} | ||||
|  | ||||
| 	public static description() { | ||||
| 		return _('Joplin\'s own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.'); | ||||
| 	} | ||||
|  | ||||
| 	public static supportsSelfHosted(): boolean { | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	public async isAuthenticated() { | ||||
| 		return true; | ||||
| 	} | ||||
|   | ||||
| @@ -51,6 +51,10 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget { | ||||
| 		return 'joplinServer'; | ||||
| 	} | ||||
|  | ||||
| 	public static description() { | ||||
| 		return 'Besides synchronisation and improved performances, Joplin Server also gives access to Joplin-specific sharing features.'; | ||||
| 	} | ||||
|  | ||||
| 	public static label() { | ||||
| 		return `${_('Joplin Server')} (Beta)`; | ||||
| 	} | ||||
|   | ||||
| @@ -25,6 +25,10 @@ class SyncTargetNextcloud extends BaseSyncTarget { | ||||
| 		return _('Nextcloud'); | ||||
| 	} | ||||
|  | ||||
| 	static description() { | ||||
| 		return 'A suite of client-server software for creating and using file hosting services.'; | ||||
| 	} | ||||
|  | ||||
| 	async isAuthenticated() { | ||||
| 		return true; | ||||
| 	} | ||||
|   | ||||
| @@ -29,6 +29,14 @@ export default class SyncTargetOneDrive extends BaseSyncTarget { | ||||
| 		return _('OneDrive'); | ||||
| 	} | ||||
|  | ||||
| 	public static description() { | ||||
| 		return 'A file hosting service operated by Microsoft as part of its web version of Office.'; | ||||
| 	} | ||||
|  | ||||
| 	public static supportsSelfHosted(): boolean { | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	async isAuthenticated() { | ||||
| 		return !!this.api().auth(); | ||||
| 	} | ||||
|   | ||||
| @@ -1,25 +1,47 @@ | ||||
| class SyncTargetRegistry { | ||||
| 	static classById(syncTargetId) { | ||||
| export interface SyncTargetInfo { | ||||
| 	id: number; | ||||
| 	name: string; | ||||
| 	label: string; | ||||
| 	supportsSelfHosted: boolean; | ||||
| 	supportsConfigCheck: boolean; | ||||
| 	description: string; | ||||
| 	classRef: any; | ||||
| } | ||||
| 
 | ||||
| export default class SyncTargetRegistry { | ||||
| 
 | ||||
| 	private static reg_: Record<number, SyncTargetInfo> = {}; | ||||
| 
 | ||||
| 	public static classById(syncTargetId: number) { | ||||
| 		const info = SyncTargetRegistry.reg_[syncTargetId]; | ||||
| 		if (!info) throw new Error(`Invalid id: ${syncTargetId}`); | ||||
| 		return info.classRef; | ||||
| 	} | ||||
| 
 | ||||
| 	static addClass(SyncTargetClass) { | ||||
| 	public static infoByName(name: string): SyncTargetInfo { | ||||
| 		for (const [, info] of Object.entries(this.reg_)) { | ||||
| 			if (info.name === name) return info; | ||||
| 		} | ||||
| 		throw new Error(`Unknown name: ${name}`); | ||||
| 	} | ||||
| 
 | ||||
| 	public static addClass(SyncTargetClass: any) { | ||||
| 		this.reg_[SyncTargetClass.id()] = { | ||||
| 			id: SyncTargetClass.id(), | ||||
| 			name: SyncTargetClass.targetName(), | ||||
| 			label: SyncTargetClass.label(), | ||||
| 			classRef: SyncTargetClass, | ||||
| 			description: SyncTargetClass.description(), | ||||
| 			supportsSelfHosted: SyncTargetClass.supportsSelfHosted(), | ||||
| 			supportsConfigCheck: SyncTargetClass.supportsConfigCheck(), | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	static allIds() { | ||||
| 	public static allIds() { | ||||
| 		return Object.keys(this.reg_); | ||||
| 	} | ||||
| 
 | ||||
| 	static nameToId(name) { | ||||
| 	public static nameToId(name: string) { | ||||
| 		for (const n in this.reg_) { | ||||
| 			if (!this.reg_.hasOwnProperty(n)) continue; | ||||
| 			if (this.reg_[n].name === name) return this.reg_[n].id; | ||||
| @@ -27,7 +49,7 @@ class SyncTargetRegistry { | ||||
| 		throw new Error(`Name not found: ${name}. Was the sync target registered?`); | ||||
| 	} | ||||
| 
 | ||||
| 	static idToMetadata(id) { | ||||
| 	public static idToMetadata(id: number) { | ||||
| 		for (const n in this.reg_) { | ||||
| 			if (!this.reg_.hasOwnProperty(n)) continue; | ||||
| 			if (this.reg_[n].id === id) return this.reg_[n]; | ||||
| @@ -35,12 +57,12 @@ class SyncTargetRegistry { | ||||
| 		throw new Error(`ID not found: ${id}`); | ||||
| 	} | ||||
| 
 | ||||
| 	static idToName(id) { | ||||
| 	public static idToName(id: number) { | ||||
| 		return this.idToMetadata(id).name; | ||||
| 	} | ||||
| 
 | ||||
| 	static idAndLabelPlainObject(os) { | ||||
| 		const output = {}; | ||||
| 	public static idAndLabelPlainObject(os: string) { | ||||
| 		const output: Record<string, string> = {}; | ||||
| 		for (const n in this.reg_) { | ||||
| 			if (!this.reg_.hasOwnProperty(n)) continue; | ||||
| 			const info = this.reg_[n]; | ||||
| @@ -52,7 +74,3 @@ class SyncTargetRegistry { | ||||
| 		return output; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| SyncTargetRegistry.reg_ = {}; | ||||
| 
 | ||||
| module.exports = SyncTargetRegistry; | ||||
| @@ -23,6 +23,10 @@ class SyncTargetWebDAV extends BaseSyncTarget { | ||||
| 		return _('WebDAV'); | ||||
| 	} | ||||
|  | ||||
| 	static description() { | ||||
| 		return 'The WebDAV protocol allows users to create, change and move documents on a server. There are many WebDAV compatible servers, including SeaFile, Nginx or Apache.'; | ||||
| 	} | ||||
|  | ||||
| 	async isAuthenticated() { | ||||
| 		return true; | ||||
| 	} | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| const Setting = require('../../models/Setting').default; | ||||
| const SyncTargetRegistry = require('../../SyncTargetRegistry'); | ||||
| const SyncTargetRegistry = require('../../SyncTargetRegistry').default; | ||||
| const ObjectUtils = require('../../ObjectUtils'); | ||||
| const { _ } = require('../../locale'); | ||||
| const { createSelector } = require('reselect'); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| const shim = require('../../shim').default; | ||||
| const SyncTargetRegistry = require('../../SyncTargetRegistry'); | ||||
| const SyncTargetRegistry = require('../../SyncTargetRegistry').default; | ||||
| const { reg } = require('../../registry.js'); | ||||
| const { _ } = require('../../locale'); | ||||
| const Setting = require('../../models/Setting').default; | ||||
|   | ||||
							
								
								
									
										38
									
								
								packages/lib/hooks/useElementSize.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								packages/lib/hooks/useElementSize.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import shim from '../shim'; | ||||
| const { useCallback, useEffect, useState } = shim.react(); | ||||
| import useEventListener from './useEventListener'; | ||||
|  | ||||
| interface Size { | ||||
|   width: number; | ||||
|   height: number; | ||||
| } | ||||
|  | ||||
| function useElementSize(elementRef: any): Size { | ||||
| 	const [size, setSize] = useState({ | ||||
| 		width: 0, | ||||
| 		height: 0, | ||||
| 	}); | ||||
|  | ||||
| 	// Prevent too many rendering using useCallback | ||||
| 	const updateSize = useCallback(() => { | ||||
| 		const node = elementRef?.current; | ||||
| 		if (node) { | ||||
| 			setSize({ | ||||
| 				width: node.offsetWidth || 0, | ||||
| 				height: node.offsetHeight || 0, | ||||
| 			}); | ||||
| 		} | ||||
| 	}, [elementRef]); | ||||
|  | ||||
| 	// Initial size on mount | ||||
| 	useEffect(() => { | ||||
| 		updateSize(); | ||||
| 		// eslint-disable-next-line react-hooks/exhaustive-deps | ||||
| 	}, []); | ||||
|  | ||||
| 	useEventListener('resize', updateSize); | ||||
|  | ||||
| 	return size; | ||||
| } | ||||
|  | ||||
| export default useElementSize; | ||||
							
								
								
									
										41
									
								
								packages/lib/hooks/useEventListener.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								packages/lib/hooks/useEventListener.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import shim from '../shim'; | ||||
| const { useEffect, useRef } = shim.react(); | ||||
|  | ||||
| function useEventListener( | ||||
| 	eventName: any, | ||||
| 	handler: any, | ||||
| 	element?: any | ||||
| ) { | ||||
| 	// Create a ref that stores handler | ||||
| 	const savedHandler = useRef(); | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		// Define the listening target | ||||
| 		const targetElement = element?.current || window; | ||||
| 		if (!(targetElement && targetElement.addEventListener)) { | ||||
| 			return null; | ||||
| 		} | ||||
|  | ||||
| 		// Update saved handler if necessary | ||||
| 		if (savedHandler.current !== handler) { | ||||
| 			savedHandler.current = handler; | ||||
| 		} | ||||
|  | ||||
| 		// Create event listener that calls handler function stored in ref | ||||
| 		const eventListener = (event: Event) => { | ||||
| 			// eslint-disable-next-line no-extra-boolean-cast | ||||
| 			if (!!savedHandler?.current) { | ||||
| 				savedHandler.current(event); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		targetElement.addEventListener(eventName, eventListener); | ||||
|  | ||||
| 		// Remove event listener on cleanup | ||||
| 		return () => { | ||||
| 			targetElement.removeEventListener(eventName, eventListener); | ||||
| 		}; | ||||
| 	}, [eventName, element, handler]); | ||||
| } | ||||
|  | ||||
| export default useEventListener; | ||||
| @@ -3,7 +3,7 @@ import { _, supportedLocalesToLanguages, defaultLocale } from '../locale'; | ||||
| import eventManager from '../eventManager'; | ||||
| import BaseModel from '../BaseModel'; | ||||
| import Database from '../database'; | ||||
| const SyncTargetRegistry = require('../SyncTargetRegistry.js'); | ||||
| import SyncTargetRegistry from '../SyncTargetRegistry'; | ||||
| import time from '../time'; | ||||
| import FileHandler, { SettingValues } from './settings/FileHandler'; | ||||
| const { sprintf } = require('sprintf-js'); | ||||
| @@ -55,6 +55,7 @@ export interface SettingItem { | ||||
| 	needRestart?: boolean; | ||||
| 	autoSave?: boolean; | ||||
| 	storage?: SettingStorage; | ||||
| 	hideLabel?: boolean; | ||||
| } | ||||
|  | ||||
| interface SettingItems { | ||||
| @@ -306,6 +307,17 @@ class Setting extends BaseModel { | ||||
| 				appTypes: [AppType.Desktop], | ||||
| 				storage: SettingStorage.File, | ||||
| 			}, | ||||
|  | ||||
| 			'sync.openSyncWizard': { | ||||
| 				value: null, | ||||
| 				type: SettingItemType.Button, | ||||
| 				public: true, | ||||
| 				appTypes: [AppType.Desktop], | ||||
| 				label: () => _('Open Sync Wizard...'), | ||||
| 				hideLabel: true, | ||||
| 				section: 'sync', | ||||
| 			}, | ||||
|  | ||||
| 			'sync.target': { | ||||
| 				value: SyncTargetRegistry.nameToId('dropbox'), | ||||
| 				type: SettingItemType.Int, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import Logger from './Logger'; | ||||
| import Setting from './models/Setting'; | ||||
| import shim from './shim'; | ||||
| const SyncTargetRegistry = require('./SyncTargetRegistry.js'); | ||||
| import SyncTargetRegistry from './SyncTargetRegistry'; | ||||
|  | ||||
| class Registry { | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { SqlQuery } from '../../database'; | ||||
| import JoplinDatabase from '../../JoplinDatabase'; | ||||
| import BaseItem from '../../models/BaseItem'; | ||||
| import Setting from '../../models/Setting'; | ||||
| const SyncTargetRegistry = require('../../SyncTargetRegistry'); | ||||
| import SyncTargetRegistry from '../../SyncTargetRegistry'; | ||||
|  | ||||
| async function clearSyncContext() { | ||||
| 	const syncTargetIds = SyncTargetRegistry.allIds(); | ||||
|   | ||||
| @@ -35,7 +35,7 @@ const { FileApiDriverWebDav } = require('../file-api-driver-webdav.js'); | ||||
| const { FileApiDriverDropbox } = require('../file-api-driver-dropbox.js'); | ||||
| const { FileApiDriverOneDrive } = require('../file-api-driver-onedrive.js'); | ||||
| const { FileApiDriverAmazonS3 } = require('../file-api-driver-amazon-s3.js'); | ||||
| const SyncTargetRegistry = require('../SyncTargetRegistry.js'); | ||||
| import SyncTargetRegistry from '../SyncTargetRegistry'; | ||||
| const SyncTargetMemory = require('../SyncTargetMemory.js'); | ||||
| const SyncTargetFilesystem = require('../SyncTargetFilesystem.js'); | ||||
| const SyncTargetNextcloud = require('../SyncTargetNextcloud.js'); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user