diff --git a/.eslintignore b/.eslintignore index f81c2caa68..e05f10e48f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -69,8 +69,12 @@ ElectronClient/commands/focusElement.js ElectronClient/commands/startExternalEditing.js ElectronClient/commands/stopExternalEditing.js ElectronClient/global.d.js +ElectronClient/gui/Button/Button.js +ElectronClient/gui/ConfigScreen/ButtonBar.js +ElectronClient/gui/ConfigScreen/ConfigScreen.js +ElectronClient/gui/ConfigScreen/SideBar.js +ElectronClient/gui/DropboxLoginScreen.js ElectronClient/gui/ErrorBoundary.js -ElectronClient/gui/Header/commands/focusSearch.js ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js ElectronClient/gui/KeymapConfig/ShortcutRecorder.js ElectronClient/gui/KeymapConfig/styles/index.js @@ -81,8 +85,8 @@ ElectronClient/gui/MainScreen/commands/editAlarm.js ElectronClient/gui/MainScreen/commands/exportPdf.js ElectronClient/gui/MainScreen/commands/hideModalMessage.js ElectronClient/gui/MainScreen/commands/moveToFolder.js +ElectronClient/gui/MainScreen/commands/newFolder.js ElectronClient/gui/MainScreen/commands/newNote.js -ElectronClient/gui/MainScreen/commands/newNotebook.js ElectronClient/gui/MainScreen/commands/newTodo.js ElectronClient/gui/MainScreen/commands/print.js ElectronClient/gui/MainScreen/commands/renameFolder.js @@ -94,9 +98,11 @@ ElectronClient/gui/MainScreen/commands/showModalMessage.js ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js ElectronClient/gui/MainScreen/commands/showNoteProperties.js ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js +ElectronClient/gui/MainScreen/commands/toggleEditors.js ElectronClient/gui/MainScreen/commands/toggleNoteList.js ElectronClient/gui/MainScreen/commands/toggleSidebar.js ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js +ElectronClient/gui/MainScreen/MainScreen.js ElectronClient/gui/MultiNoteActions.js ElectronClient/gui/NoteContentPropertiesDialog.js ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js @@ -116,6 +122,7 @@ ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js +ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js ElectronClient/gui/NoteEditor/NoteEditor.js @@ -125,19 +132,40 @@ ElectronClient/gui/NoteEditor/utils/index.js ElectronClient/gui/NoteEditor/utils/resourceHandling.js ElectronClient/gui/NoteEditor/utils/types.js ElectronClient/gui/NoteEditor/utils/useDropHandler.js +ElectronClient/gui/NoteEditor/utils/useFolder.js ElectronClient/gui/NoteEditor/utils/useFormNote.js ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.js ElectronClient/gui/NoteEditor/utils/useMessageHandler.js ElectronClient/gui/NoteEditor/utils/useNoteSearchBar.js +ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.js ElectronClient/gui/NoteEditor/utils/useSearchMarkers.js ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.js ElectronClient/gui/NoteList/commands/focusElementNoteList.js +ElectronClient/gui/NoteList/NoteList.js +ElectronClient/gui/NoteListControls/commands/focusSearch.js +ElectronClient/gui/NoteListControls/NoteListControls.js ElectronClient/gui/NoteListItem.js ElectronClient/gui/NoteToolbar/NoteToolbar.js +ElectronClient/gui/OneDriveLoginScreen.js +ElectronClient/gui/ResizableLayout/hooks/useLayoutItemSizes.js +ElectronClient/gui/ResizableLayout/hooks/useWindowResizeEvent.js +ElectronClient/gui/ResizableLayout/ResizableLayout.js ElectronClient/gui/ResourceScreen.js ElectronClient/gui/Root_UpgradeSyncTarget.js +ElectronClient/gui/SearchBar/hooks/useSearch.js +ElectronClient/gui/SearchBar/SearchBar.js +ElectronClient/gui/SearchBar/styles/index.js ElectronClient/gui/ShareNoteDialog.js ElectronClient/gui/SideBar/commands/focusElementSideBar.js +ElectronClient/gui/SideBar/SideBar.js +ElectronClient/gui/SideBar/styles/index.js +ElectronClient/gui/StatusScreen/StatusScreen.js +ElectronClient/gui/style/StyledInput.js +ElectronClient/gui/style/StyledTextInput.js +ElectronClient/gui/ToggleEditorsButton/styles/index.js +ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.js +ElectronClient/gui/ToolbarButton/styles/index.js +ElectronClient/gui/ToolbarButton/ToolbarButton.js ReactNativeClient/lib/AsyncActionQueue.js ReactNativeClient/lib/checkPermissions.js ReactNativeClient/lib/commands/historyBackward.js @@ -176,6 +204,16 @@ ReactNativeClient/lib/services/synchronizer/utils/types.js ReactNativeClient/lib/services/UndoRedoService.js ReactNativeClient/lib/ShareExtension.js ReactNativeClient/lib/shareHandler.js +ReactNativeClient/lib/theme.js +ReactNativeClient/lib/themes/aritimDark.js +ReactNativeClient/lib/themes/dark.js +ReactNativeClient/lib/themes/dracula.js +ReactNativeClient/lib/themes/light.js +ReactNativeClient/lib/themes/nord.js +ReactNativeClient/lib/themes/oledDark.js +ReactNativeClient/lib/themes/solarizedDark.js +ReactNativeClient/lib/themes/solarizedLight.js +ReactNativeClient/lib/themes/type.js ReactNativeClient/lib/versionInfo.js ReactNativeClient/PluginAssetsLoader.js ReactNativeClient/setUpQuickActions.js diff --git a/.gitignore b/.gitignore index ecdc4c3417..c73d67936d 100644 --- a/.gitignore +++ b/.gitignore @@ -62,8 +62,12 @@ ElectronClient/commands/focusElement.js ElectronClient/commands/startExternalEditing.js ElectronClient/commands/stopExternalEditing.js ElectronClient/global.d.js +ElectronClient/gui/Button/Button.js +ElectronClient/gui/ConfigScreen/ButtonBar.js +ElectronClient/gui/ConfigScreen/ConfigScreen.js +ElectronClient/gui/ConfigScreen/SideBar.js +ElectronClient/gui/DropboxLoginScreen.js ElectronClient/gui/ErrorBoundary.js -ElectronClient/gui/Header/commands/focusSearch.js ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js ElectronClient/gui/KeymapConfig/ShortcutRecorder.js ElectronClient/gui/KeymapConfig/styles/index.js @@ -74,8 +78,8 @@ ElectronClient/gui/MainScreen/commands/editAlarm.js ElectronClient/gui/MainScreen/commands/exportPdf.js ElectronClient/gui/MainScreen/commands/hideModalMessage.js ElectronClient/gui/MainScreen/commands/moveToFolder.js +ElectronClient/gui/MainScreen/commands/newFolder.js ElectronClient/gui/MainScreen/commands/newNote.js -ElectronClient/gui/MainScreen/commands/newNotebook.js ElectronClient/gui/MainScreen/commands/newTodo.js ElectronClient/gui/MainScreen/commands/print.js ElectronClient/gui/MainScreen/commands/renameFolder.js @@ -87,9 +91,11 @@ ElectronClient/gui/MainScreen/commands/showModalMessage.js ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js ElectronClient/gui/MainScreen/commands/showNoteProperties.js ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js +ElectronClient/gui/MainScreen/commands/toggleEditors.js ElectronClient/gui/MainScreen/commands/toggleNoteList.js ElectronClient/gui/MainScreen/commands/toggleSidebar.js ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js +ElectronClient/gui/MainScreen/MainScreen.js ElectronClient/gui/MultiNoteActions.js ElectronClient/gui/NoteContentPropertiesDialog.js ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js @@ -109,6 +115,7 @@ ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js +ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js ElectronClient/gui/NoteEditor/NoteEditor.js @@ -118,19 +125,40 @@ ElectronClient/gui/NoteEditor/utils/index.js ElectronClient/gui/NoteEditor/utils/resourceHandling.js ElectronClient/gui/NoteEditor/utils/types.js ElectronClient/gui/NoteEditor/utils/useDropHandler.js +ElectronClient/gui/NoteEditor/utils/useFolder.js ElectronClient/gui/NoteEditor/utils/useFormNote.js ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.js ElectronClient/gui/NoteEditor/utils/useMessageHandler.js ElectronClient/gui/NoteEditor/utils/useNoteSearchBar.js +ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.js ElectronClient/gui/NoteEditor/utils/useSearchMarkers.js ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.js ElectronClient/gui/NoteList/commands/focusElementNoteList.js +ElectronClient/gui/NoteList/NoteList.js +ElectronClient/gui/NoteListControls/commands/focusSearch.js +ElectronClient/gui/NoteListControls/NoteListControls.js ElectronClient/gui/NoteListItem.js ElectronClient/gui/NoteToolbar/NoteToolbar.js +ElectronClient/gui/OneDriveLoginScreen.js +ElectronClient/gui/ResizableLayout/hooks/useLayoutItemSizes.js +ElectronClient/gui/ResizableLayout/hooks/useWindowResizeEvent.js +ElectronClient/gui/ResizableLayout/ResizableLayout.js ElectronClient/gui/ResourceScreen.js ElectronClient/gui/Root_UpgradeSyncTarget.js +ElectronClient/gui/SearchBar/hooks/useSearch.js +ElectronClient/gui/SearchBar/SearchBar.js +ElectronClient/gui/SearchBar/styles/index.js ElectronClient/gui/ShareNoteDialog.js ElectronClient/gui/SideBar/commands/focusElementSideBar.js +ElectronClient/gui/SideBar/SideBar.js +ElectronClient/gui/SideBar/styles/index.js +ElectronClient/gui/StatusScreen/StatusScreen.js +ElectronClient/gui/style/StyledInput.js +ElectronClient/gui/style/StyledTextInput.js +ElectronClient/gui/ToggleEditorsButton/styles/index.js +ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.js +ElectronClient/gui/ToolbarButton/styles/index.js +ElectronClient/gui/ToolbarButton/ToolbarButton.js ReactNativeClient/lib/AsyncActionQueue.js ReactNativeClient/lib/checkPermissions.js ReactNativeClient/lib/commands/historyBackward.js @@ -169,6 +197,16 @@ ReactNativeClient/lib/services/synchronizer/utils/types.js ReactNativeClient/lib/services/UndoRedoService.js ReactNativeClient/lib/ShareExtension.js ReactNativeClient/lib/shareHandler.js +ReactNativeClient/lib/theme.js +ReactNativeClient/lib/themes/aritimDark.js +ReactNativeClient/lib/themes/dark.js +ReactNativeClient/lib/themes/dracula.js +ReactNativeClient/lib/themes/light.js +ReactNativeClient/lib/themes/nord.js +ReactNativeClient/lib/themes/oledDark.js +ReactNativeClient/lib/themes/solarizedDark.js +ReactNativeClient/lib/themes/solarizedLight.js +ReactNativeClient/lib/themes/type.js ReactNativeClient/lib/versionInfo.js ReactNativeClient/PluginAssetsLoader.js ReactNativeClient/setUpQuickActions.js diff --git a/BUILD.md b/BUILD.md index 9d0ce597fd..b03f238c3c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -11,6 +11,7 @@ Note that all the applications share the same library, which, for historical rea - macOS, Linux: Install rsync - https://nodejs.org/en/ - macOS: Install Cocoapods - `brew install cocoapods` - Windows: Install Windows Build Tools - `npm install -g windows-build-tools` +- Linux: Install dependencies - `sudo apt install libnss3 libsecret-1-dev` ## Building @@ -25,6 +26,8 @@ Then you can test the various applications: cd ElectronClient npm start +You can also run it under WSL 2. To do so, [follow these instructions](https://www.beekeeperstudio.io/blog/building-electron-windows-ubuntu-wsl2) to setup your environment. + ## Testing the Terminal application cd CliClient diff --git a/CliClient/tests/timeUtils.js b/CliClient/tests/timeUtils.js index de7987bb9e..b25df820dc 100644 --- a/CliClient/tests/timeUtils.js +++ b/CliClient/tests/timeUtils.js @@ -26,10 +26,11 @@ describe('timeUtils', function() { startDate = new Date('3 Aug 2020 07:30:20'); expect(time.goBackInTime(startDate, 1, 'day')).toBe(endDate.getTime().toString()); + // Note: this test randomly fails - https://github.com/laurent22/joplin/issues/3722 - startDate = new Date('11 Aug 2020'); - endDate = new Date('9 Aug 2020'); // week start; - expect(time.goBackInTime(startDate, 0, 'week')).toBe(endDate.getTime().toString()); + // startDate = new Date('11 Aug 2020'); + // endDate = new Date('9 Aug 2020'); // week start; + // expect(time.goBackInTime(startDate, 0, 'week')).toBe(endDate.getTime().toString()); startDate = new Date('02 Feb 2020'); endDate = new Date('01 Jan 2020'); diff --git a/ElectronClient/app.js b/ElectronClient/app.js index a23b6e8a28..e47e62f680 100644 --- a/ElectronClient/app.js +++ b/ElectronClient/app.js @@ -37,13 +37,13 @@ const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/red const versionInfo = require('lib/versionInfo').default; const commands = [ - require('./gui/Header/commands/focusSearch'), + require('./gui/NoteListControls/commands/focusSearch'), require('./gui/MainScreen/commands/editAlarm'), require('./gui/MainScreen/commands/exportPdf'), require('./gui/MainScreen/commands/hideModalMessage'), require('./gui/MainScreen/commands/moveToFolder'), require('./gui/MainScreen/commands/newNote'), - require('./gui/MainScreen/commands/newNotebook'), + require('./gui/MainScreen/commands/newFolder'), require('./gui/MainScreen/commands/newTodo'), require('./gui/MainScreen/commands/print'), require('./gui/MainScreen/commands/renameFolder'), @@ -58,6 +58,7 @@ const commands = [ require('./gui/MainScreen/commands/toggleNoteList'), require('./gui/MainScreen/commands/toggleSidebar'), require('./gui/MainScreen/commands/toggleVisiblePanes'), + require('./gui/MainScreen/commands/toggleEditors'), require('./gui/NoteEditor/commands/focusElementNoteBody'), require('./gui/NoteEditor/commands/focusElementNoteTitle'), require('./gui/NoteEditor/commands/showLocalSearch'), @@ -286,10 +287,11 @@ class Application extends BaseApplication { } newState = resourceEditWatcherReducer(newState, action); + newState = super.reducer(newState, action); CommandService.instance().scheduleMapStateToProps(newState); - return super.reducer(newState, action); + return newState; } toggleDevTools(visible) { @@ -519,7 +521,7 @@ class Application extends BaseApplication { const newNoteItem = cmdService.commandToMenuItem('newNote'); const newTodoItem = cmdService.commandToMenuItem('newTodo'); - const newNotebookItem = cmdService.commandToMenuItem('newNotebook'); + const newFolderItem = cmdService.commandToMenuItem('newFolder'); const printItem = cmdService.commandToMenuItem('print'); toolsItemsFirst.push(syncStatusItem, { @@ -650,7 +652,7 @@ class Application extends BaseApplication { }, shim.isMac() ? noItem : newNoteItem, shim.isMac() ? noItem : newTodoItem, - shim.isMac() ? noItem : newNotebookItem, { + shim.isMac() ? noItem : newFolderItem, { type: 'separator', visible: shim.isMac() ? false : true, }, { @@ -699,7 +701,7 @@ class Application extends BaseApplication { submenu: [ newNoteItem, newTodoItem, - newNotebookItem, { + newFolderItem, { label: _('Close Window'), platforms: ['darwin'], accelerator: shim.isMac() && keymapService.getAccelerator('closeWindow'), diff --git a/ElectronClient/commands/startExternalEditing.ts b/ElectronClient/commands/startExternalEditing.ts index 8666fdd48c..141e72e1ae 100644 --- a/ElectronClient/commands/startExternalEditing.ts +++ b/ElectronClient/commands/startExternalEditing.ts @@ -11,7 +11,7 @@ interface Props { export const declaration:CommandDeclaration = { name: 'startExternalEditing', label: () => _('Edit in external editor'), - iconName: 'fa-share-square', + iconName: 'icon-share', }; export const runtime = ():CommandRuntime => { diff --git a/ElectronClient/gui/Button/Button.tsx b/ElectronClient/gui/Button/Button.tsx new file mode 100644 index 0000000000..81e73b6ecc --- /dev/null +++ b/ElectronClient/gui/Button/Button.tsx @@ -0,0 +1,195 @@ +import * as React from 'react'; +const styled = require('styled-components').default; +const { space } = require('styled-system'); + +export enum ButtonLevel { + Primary = 'primary', + Secondary = 'secondary', + Tertiary = 'tertiary', + SideBarSecondary = 'sideBarSecondary', +} + +interface Props { + title?: string, + iconName?: string, + level?: ButtonLevel, + className?:string, + onClick:Function, + color?: string, + iconAnimation?: string, + tooltip?: string, + disabled?: boolean, + style?:any, +} + +const StyledTitle = styled.span` + +`; + +const StyledButtonBase = styled.button` + display: flex; + align-items: center; + flex-direction: row; + height: ${(props:any) => `${props.theme.toolbarHeight}px`}; + min-height: ${(props:any) => `${props.theme.toolbarHeight}px`}; + max-height: ${(props:any) => `${props.theme.toolbarHeight}px`}; + width: ${(props:any) => props.iconOnly ? `${props.theme.toolbarHeight}px` : 'auto'}; + ${(props:any) => props.iconOnly ? `min-width: ${props.theme.toolbarHeight}px;` : ''} + ${(props:any) => !props.iconOnly ? 'min-width: 100px;' : ''} + ${(props:any) => props.iconOnly ? `max-width: ${props.theme.toolbarHeight}px;` : ''} + box-sizing: border-box; + border-radius: 3px; + border-style: solid; + border-width: 1px; + font-size: ${(props:any) => props.theme.fontSize}px; + padding: 0 ${(props:any) => props.iconOnly ? 4 : 8}px; + justify-content: center; + opacity: ${(props:any) => props.disabled ? 0.5 : 1}; + user-select: none; +`; + +const StyledIcon = styled(styled.span(space))` + font-size: ${(props:any) => props.theme.toolbarIconSize}px; + ${(props:any) => props.animation ? `animation: ${props.animation}` : ''}; +`; + +const StyledButtonPrimary = styled(StyledButtonBase)` + border: none; + background-color: ${(props:any) => props.theme.backgroundColor5}; + + &:hover { + background-color: ${(props:any) => props.theme.backgroundColorHover5}; + } + + &:active { + background-color: ${(props:any) => props.theme.backgroundColorActive5}; + } + + ${StyledIcon} { + color: ${(props:any) => props.theme.color5}; + } + + ${StyledTitle} { + color: ${(props:any) => props.theme.color5}; + } +`; + +const StyledButtonSecondary = styled(StyledButtonBase)` + border: 1px solid ${(props:any) => props.theme.borderColor4}; + background-color: ${(props:any) => props.theme.backgroundColor4}; + + &:hover { + background-color: ${(props:any) => props.theme.backgroundColorHover4}; + } + + &:active { + background-color: ${(props:any) => props.theme.backgroundColorActive4}; + } + + ${StyledIcon} { + color: ${(props:any) => props.theme.color4}; + } + + ${StyledTitle} { + color: ${(props:any) => props.theme.color4}; + } +`; + +const StyledButtonTertiary = styled(StyledButtonBase)` + border: 1px solid ${(props:any) => props.theme.color3}; + background-color: ${(props:any) => props.theme.backgroundColor3}; + + &:hover { + background-color: ${(props:any) => props.theme.backgroundColorHoverDim3}; + } + + &:active { + background-color: ${(props:any) => props.theme.backgroundColorActive3}; + } + + ${StyledIcon} { + color: ${(props:any) => props.theme.color}; + } + + ${StyledTitle} { + color: ${(props:any) => props.theme.color}; + opacity: 0.9; + } +`; + +const StyledButtonSideBarSecondary = styled(StyledButtonBase)` + background: none; + border-color: ${(props:any) => props.theme.color2}; + color: ${(props:any) => props.theme.color2}; + + &:hover { + color: ${(props:any) => props.theme.colorHover2}; + border-color: ${(props:any) => props.theme.colorHover2}; + background: none; + + ${StyledTitle} { + color: ${(props:any) => props.theme.colorHover2}; + } + + ${StyledIcon} { + color: ${(props:any) => props.theme.colorHover2}; + } + } + + &:active { + color: ${(props:any) => props.theme.colorActive2}; + border-color: ${(props:any) => props.theme.colorActive2}; + background: none; + + ${StyledTitle} { + color: ${(props:any) => props.theme.colorActive2}; + } + + ${StyledIcon} { + color: ${(props:any) => props.theme.colorActive2}; + } + } + + ${StyledTitle} { + color: ${(props:any) => props.theme.color2}; + } + + ${StyledIcon} { + color: ${(props:any) => props.theme.color2}; + } +`; + +function buttonClass(level:ButtonLevel) { + if (level === ButtonLevel.Primary) return StyledButtonPrimary; + if (level === ButtonLevel.Tertiary) return StyledButtonTertiary; + if (level === ButtonLevel.SideBarSecondary) return StyledButtonSideBarSecondary; + return StyledButtonSecondary; +} + +export default function Button(props:Props) { + const iconOnly = props.iconName && !props.title; + + const StyledButton = buttonClass(props.level); + + function renderIcon() { + if (!props.iconName) return null; + return ; + } + + function renderTitle() { + if (!props.title) return null; + return {props.title}; + } + + function onClick() { + if (props.disabled) return; + props.onClick(); + } + + return ( + + {renderIcon()} + {renderTitle()} + + ); +} diff --git a/ElectronClient/gui/ClipperConfigScreen.jsx b/ElectronClient/gui/ClipperConfigScreen.jsx index 15d3f47d15..beea36392f 100644 --- a/ElectronClient/gui/ClipperConfigScreen.jsx +++ b/ElectronClient/gui/ClipperConfigScreen.jsx @@ -40,10 +40,12 @@ class ClipperConfigScreenComponent extends React.Component { } render() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const containerStyle = Object.assign({}, theme.containerStyle, { overflowY: 'scroll', + padding: theme.configScreenPadding, + backgroundColor: theme.backgroundColor3, }); const buttonStyle = Object.assign({}, theme.buttonStyle, { marginRight: 10 }); @@ -106,8 +108,8 @@ class ClipperConfigScreenComponent extends React.Component { return (
-
-

{_('Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.')}

+
+

{_('Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.')}

{_('In order to use the web clipper, you need to do the following:')}

@@ -120,8 +122,8 @@ class ClipperConfigScreenComponent extends React.Component {

{_('Step 2: Install the extension')}

{_('Download and install the relevant extension for your browser:')}

- - + +
@@ -145,7 +147,7 @@ class ClipperConfigScreenComponent extends React.Component { const mapStateToProps = state => { return { - theme: state.settings.theme, + themeId: state.settings.theme, clipperServer: state.clipperServer, clipperServerAutoStart: state.settings['clipperServer.autoStart'], apiToken: state.settings['api.token'], diff --git a/ElectronClient/gui/ConfigMenuBar.jsx b/ElectronClient/gui/ConfigMenuBar.jsx deleted file mode 100644 index bc618fbc16..0000000000 --- a/ElectronClient/gui/ConfigMenuBar.jsx +++ /dev/null @@ -1,44 +0,0 @@ -const React = require('react'); -const styleSelector = require('./style/ConfigMenuBar'); -const Setting = require('lib/models/Setting'); - -function ConfigMenuBarButton(props) { - const style = styleSelector(null, props); - - const iconStyle = props.selected ? style.buttonIconSelected : style.buttonIcon; - const labelStyle = props.selected ? style.buttonLabelSelected : style.buttonLabel; - - return ( - - ); -} - -function ConfigMenuBar(props) { - const buttons = []; - - const style = styleSelector(null, props); - - for (const section of props.sections) { - buttons.push( { props.onSelectionChange({ section: section }); }} - />); - } - - return ( -
-
- {buttons} -
-
- ); -} - -module.exports = ConfigMenuBar; diff --git a/ElectronClient/gui/ConfigScreen/ButtonBar.tsx b/ElectronClient/gui/ConfigScreen/ButtonBar.tsx new file mode 100644 index 0000000000..c5b34baaf4 --- /dev/null +++ b/ElectronClient/gui/ConfigScreen/ButtonBar.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import Button, { ButtonLevel } from '../Button/Button'; +const styled = require('styled-components').default; +const { _ } = require('lib/locale.js'); + +interface Props { + backButtonTitle?: string, + hasChanges?: boolean, + onCancelClick: Function, + onSaveClick?: Function, + onApplyClick?: Function, +} + +export const StyledRoot = styled.div` + display: flex; + align-items: center; + padding: 10px; + background-color: ${(props:any) => props.theme.backgroundColor3}; + padding-left: ${(props:any) => props.theme.configScreenPadding}px; + border-top-width: 1px; + border-top-style: solid; + border-top-color: ${(props:any) => props.theme.dividerColor}; +`; + +export default function ButtonBar(props:Props) { + function renderOkButton() { + if (!props.onSaveClick) return null; + return +
); @@ -204,9 +223,7 @@ class ConfigScreenComponent extends React.Component {    {showLogButton}    - + ; + // const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 }); + advancedSettingsButton = ( +
+
+ ); advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none'; } @@ -235,35 +261,39 @@ class ConfigScreenComponent extends React.Component { ); } - settingToComponent(key, value) { - const theme = themeStyle(this.props.theme); + settingToComponent(key:string, value:any) { + const theme = themeStyle(this.props.themeId); - const output = null; + const output:any = null; - const rowStyle = this.rowStyle_; + const rowStyle = { + marginBottom: theme.mainPadding, + }; const labelStyle = Object.assign({}, theme.textStyle, { - display: 'inline-block', - marginRight: 10, + display: 'block', color: theme.color, + fontSize: theme.fontSize * 1.083333, + fontWeight: 500, + marginBottom: theme.mainPadding / 4, }); const subLabel = Object.assign({}, labelStyle, { + display: 'block', opacity: 0.7, - marginBottom: Math.round(rowStyle.marginBottom * 0.7), - }); - - const invisibleLabel = Object.assign({}, labelStyle, { - opacity: 0, + marginBottom: labelStyle.marginBottom, }); const checkboxLabelStyle = Object.assign({}, labelStyle, { marginLeft: 8, + display: 'inline', + backgroundColor: 'transparent', }); const controlStyle = { display: 'inline-block', color: theme.color, + fontFamily: theme.fontFamily, backgroundColor: theme.backgroundColor, }; @@ -275,13 +305,19 @@ class ConfigScreenComponent extends React.Component { }); const textInputBaseStyle = Object.assign({}, controlStyle, { + fontFamily: theme.fontFamily, border: '1px solid', padding: '4px 6px', - borderColor: theme.dividerColor, - borderRadius: 4, + boxSizing: 'border-box', + borderColor: theme.borderColor4, + borderRadius: 3, + paddingLeft: 6, + paddingRight: 6, + paddingTop: 4, + paddingBottom: 4, }); - const updateSettingValue = (key, value) => { + const updateSettingValue = (key:string, value:any) => { // console.info(key + ' = ' + value); return shared.updateSettingValue(this, key, value); }; @@ -306,7 +342,14 @@ class ConfigScreenComponent extends React.Component { ); } - const selectStyle = Object.assign({}, controlStyle, { height: 22, borderColor: theme.dividerColor }); + const selectStyle = Object.assign({}, controlStyle, { + paddingLeft: 6, + paddingRight: 6, + paddingTop: 4, + paddingBottom: 4, + borderColor: theme.borderColor4, + borderRadius: 3, + }); return (
@@ -316,7 +359,7 @@ class ConfigScreenComponent extends React.Component { { - onCheckboxClick(event); + onChange={() => { + onCheckboxClick(); }} + style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }} /> - {descriptionComp}
+ {descriptionComp}
); } else if (md.type === Setting.TYPE_STRING) { - const inputStyle = Object.assign({}, textInputBaseStyle, { + const inputStyle:any = Object.assign({}, textInputBaseStyle, { width: '50%', minWidth: '20em', }); @@ -367,13 +413,13 @@ class ConfigScreenComponent extends React.Component { if (md.subType === 'file_path_and_args') { inputStyle.marginBottom = subLabel.marginBottom; - const splitCmd = cmdString => { + const splitCmd = (cmdString:string) => { const path = pathUtils.extractExecutablePath(cmdString); const args = cmdString.substr(path.length + 1); return [pathUtils.unquotePath(path), args]; }; - const joinCmd = cmdArray => { + const joinCmd = (cmdArray:string[]) => { if (!cmdArray[0] && !cmdArray[1]) return ''; let cmdString = pathUtils.quotePath(cmdArray[0]); if (!cmdString) cmdString = '""'; @@ -381,13 +427,13 @@ class ConfigScreenComponent extends React.Component { return cmdString; }; - const onPathChange = event => { + const onPathChange = (event:any) => { const cmd = splitCmd(this.state.settings[key]); cmd[0] = event.target.value; updateSettingValue(key, joinCmd(cmd)); }; - const onArgsChange = event => { + const onArgsChange = (event:any) => { const cmd = splitCmd(this.state.settings[key]); cmd[1] = event.target.value; updateSettingValue(key, joinCmd(cmd)); @@ -405,53 +451,51 @@ class ConfigScreenComponent extends React.Component { return (
+
+ +
-
-
- -
-
-
-
Path:
-
Arguments:
-
-
+
+
Path:
+
+ { + onPathChange(event); + }} + value={cmd[0]} + /> +
+
+
+
Arguments:
{ - onPathChange(event); + style={inputStyle} + onChange={(event:any) => { + onArgsChange(event); }} - value={cmd[0]} + value={cmd[1]} /> - +
+ {descriptionComp} +
- { - onArgsChange(event); - }} - value={cmd[1]} - />
-
-
-
- -
-
-
{descriptionComp}
-
+
); } else { - const onTextChange = event => { + const onTextChange = (event:any) => { updateSettingValue(key, event.target.value); }; @@ -464,16 +508,18 @@ class ConfigScreenComponent extends React.Component { type={inputType} style={inputStyle} value={this.state.settings[key]} - onChange={event => { + onChange={(event:any) => { onTextChange(event); }} /> - {descriptionComp} +
+ {descriptionComp} +
); } } else if (md.type === Setting.TYPE_INT) { - const onNumChange = event => { + const onNumChange = (event:any) => { updateSettingValue(key, event.target.value); }; @@ -491,7 +537,7 @@ class ConfigScreenComponent extends React.Component { type="number" style={inputStyle} value={this.state.settings[key]} - onChange={event => { + onChange={(event:any) => { onNumChange(event); }} min={md.minimum} @@ -502,20 +548,12 @@ class ConfigScreenComponent extends React.Component {
); } else if (md.type === Setting.TYPE_BUTTON) { - const theme = themeStyle(this.props.theme); - const buttonStyle = Object.assign({}, theme.buttonStyle, { - display: 'inline-block', - marginRight: 10, - }); - return (
- +
); @@ -544,46 +582,35 @@ class ConfigScreenComponent extends React.Component { } render() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); - const style = Object.assign( - { - backgroundColor: theme.backgroundColor, - }, + const style = Object.assign({}, this.props.style, { overflow: 'hidden', display: 'flex', flexDirection: 'column', + backgroundColor: theme.backgroundColor3, } ); const settings = this.state.settings; - const containerStyle = Object.assign({}, theme.containerStyle, { padding: 10, paddingTop: 0, display: 'flex', flex: 1 }); + const containerStyle = { + overflow: 'auto', + padding: theme.configScreenPadding, + paddingTop: 0, + display: 'flex', + flex: 1, + }; const hasChanges = this.hasChanges(); - const buttonStyle = Object.assign({}, theme.buttonStyle, { - display: 'inline-block', - marginRight: 10, - }); - - const buttonStyleApprove = Object.assign({}, buttonStyle, { - opacity: hasChanges ? 1 : theme.disabledOpacity, - }); - const settingComps = shared.settingsToComponents2(this, 'desktop', settings, this.state.selectedSectionName); - const buttonBarStyle = { - display: 'flex', - alignItems: 'center', - padding: 10, - borderTopWidth: 1, - borderTopStyle: 'solid', - borderTopColor: theme.dividerColor, - }; - + // screenComp is a custom config screen, such as the encryption config screen or keymap config screen. + // These screens handle their own loading/saving of settings and have bespoke rendering. + // When screenComp is null, it means we are viewing the regular settings. const screenComp = this.state.screenName ?
{this.screenFromName(this.state.screenName)}
: null; if (screenComp) containerStyle.display = 'none'; @@ -591,45 +618,35 @@ class ConfigScreenComponent extends React.Component { const sections = shared.settingsSections({ device: 'desktop', settings }); return ( -
- + - {screenComp} -
{settingComps}
-
- - { !screenComp && ( -
- - -
- )} +
+ {screenComp} +
{settingComps}
+
); } } -const mapStateToProps = state => { +const mapStateToProps = (state:any) => { return { - theme: state.settings.theme, + themeId: state.settings.theme, settings: state.settings, locale: state.settings.locale, }; }; -const ConfigScreen = connect(mapStateToProps)(ConfigScreenComponent); +export default connect(mapStateToProps)(ConfigScreenComponent); -module.exports = { ConfigScreen }; diff --git a/ElectronClient/gui/ConfigScreen/SideBar.tsx b/ElectronClient/gui/ConfigScreen/SideBar.tsx new file mode 100644 index 0000000000..afe507ca98 --- /dev/null +++ b/ElectronClient/gui/ConfigScreen/SideBar.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +const styled = require('styled-components').default; +const Setting = require('lib/models/Setting'); + +interface Props { + selection: string, + onSelectionChange: Function, + sections: any[], +} + +export const StyledRoot = styled.div` + display: flex; + background-color: ${(props:any) => props.theme.backgroundColor2}; + flex-direction: column; +`; + +export const StyledListItem = styled.a` + box-sizing: border-box; + display: flex; + flex-direction: row; + padding: ${(props:any) => props.theme.mainPadding}px; + background: ${(props:any) => props.selected ? props.theme.selectedColor2 : 'none'}; + transition: 0.1s; + text-decoration: none; + cursor: default; + opacity: ${(props:any) => props.selected ? 1 : 0.8}; + + &:hover { + background-color: ${(props:any) => props.theme.backgroundColorHover2}; + } +`; + +export const StyledListItemLabel = styled.span` + font-size: ${(props:any) => Math.round(props.theme.fontSize * 1.2)}px; + font-weight: 500; + color: ${(props:any) => props.theme.color2}; + white-space: nowrap; + display: flex; + flex: 1; + align-items: center; + user-select: none; +`; + +export const StyledListItemIcon = styled.i` + font-size: ${(props:any) => Math.round(props.theme.fontSize * 1.4)}px; + color: ${(props:any) => props.theme.color2}; + margin-right: ${(props:any) => props.theme.mainPadding / 1.5}px; +`; + +export default function SideBar(props:Props) { + const buttons:any[] = []; + + function renderButton(section:any) { + const selected = props.selection === section.name; + return ( + { props.onSelectionChange({ section: section }); }}> + + + {Setting.sectionNameToLabel(section.name)} + + + ); + } + + for (const section of props.sections) { + buttons.push(renderButton(section)); + } + + return ( + + {buttons} + + ); +} diff --git a/ElectronClient/gui/DialogButtonRow.jsx b/ElectronClient/gui/DialogButtonRow.jsx index ca5294a9f7..c2b3f29434 100644 --- a/ElectronClient/gui/DialogButtonRow.jsx +++ b/ElectronClient/gui/DialogButtonRow.jsx @@ -3,7 +3,7 @@ const { _ } = require('lib/locale.js'); const { themeStyle } = require('lib/theme'); function DialogButtonRow(props) { - const theme = themeStyle(props.theme); + const theme = themeStyle(props.themeId); const okButton_click = () => { if (props.onClick) props.onClick({ buttonName: 'ok' }); diff --git a/ElectronClient/gui/DropboxLoginScreen.jsx b/ElectronClient/gui/DropboxLoginScreen.tsx similarity index 61% rename from ElectronClient/gui/DropboxLoginScreen.jsx rename to ElectronClient/gui/DropboxLoginScreen.tsx index c46906d884..2c316a6f15 100644 --- a/ElectronClient/gui/DropboxLoginScreen.jsx +++ b/ElectronClient/gui/DropboxLoginScreen.tsx @@ -1,16 +1,24 @@ -const React = require('react'); +import * as React from 'react'; +import ButtonBar from './ConfigScreen/ButtonBar'; + const { connect } = require('react-redux'); const { bridge } = require('electron').remote.require('./bridge'); -const { Header } = require('./Header/Header.min.js'); const { themeStyle } = require('lib/theme'); const { _ } = require('lib/locale.js'); const Shared = require('lib/components/shared/dropbox-login-shared'); -class DropboxLoginScreenComponent extends React.Component { - constructor() { - super(); +interface Props { + themeId: string, +} - this.shared_ = new Shared(this, msg => bridge().showInfoMessageBox(msg), msg => bridge().showErrorMessageBox(msg)); +class DropboxLoginScreenComponent extends React.Component { + + shared_:any; + + constructor(props:Props) { + super(props); + + this.shared_ = new Shared(this, (msg:string) => bridge().showInfoMessageBox(msg), (msg:string) => bridge().showErrorMessageBox(msg)); } UNSAFE_componentWillMount() { @@ -19,19 +27,18 @@ class DropboxLoginScreenComponent extends React.Component { render() { const style = this.props.style; - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); - const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width }); const containerStyle = Object.assign({}, theme.containerStyle, { - padding: theme.margin, - height: style.height - theme.headerHeight - theme.margin * 2, + padding: theme.configScreenPadding, + height: style.height - theme.margin * 2, + flex: 1, }); const inputStyle = Object.assign({}, theme.inputStyle, { width: 500 }); return ( -
-
+

{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}

{_('Step 1: Open this URL in your browser to authorise the application:')}

@@ -46,17 +53,18 @@ class DropboxLoginScreenComponent extends React.Component { {_('Submit')}
+ this.props.dispatch({ type: 'NAV_BACK' })} + />
); } } -const mapStateToProps = state => { +const mapStateToProps = (state:any) => { return { - theme: state.settings.theme, + themeId: state.settings.theme, }; }; -const DropboxLoginScreen = connect(mapStateToProps)(DropboxLoginScreenComponent); - -module.exports = { DropboxLoginScreen }; +export default connect(mapStateToProps)(DropboxLoginScreenComponent); diff --git a/ElectronClient/gui/EncryptionConfigScreen.jsx b/ElectronClient/gui/EncryptionConfigScreen.jsx index a6a81ced6b..30fc9c3ee0 100644 --- a/ElectronClient/gui/EncryptionConfigScreen.jsx +++ b/ElectronClient/gui/EncryptionConfigScreen.jsx @@ -35,7 +35,7 @@ class EncryptionConfigScreenComponent extends React.Component { } renderMasterKey(mk) { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const passwordStyle = { color: theme.color, @@ -80,7 +80,7 @@ class EncryptionConfigScreenComponent extends React.Component { const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(this.props.masterKeys); if (!needUpgradeMasterKeys.length) return null; - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const rows = []; const comp = this; @@ -114,7 +114,7 @@ class EncryptionConfigScreenComponent extends React.Component { renderReencryptData() { if (!shim.isElectron()) return null; - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const buttonLabel = _('Re-encrypt data'); const intro = this.props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.'); @@ -139,13 +139,13 @@ class EncryptionConfigScreenComponent extends React.Component { } render() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const masterKeys = this.props.masterKeys; - const containerPadding = 10; const containerStyle = Object.assign({}, theme.containerStyle, { - padding: containerPadding, + padding: theme.configScreenPadding, overflow: 'auto', + backgroundColor: theme.backgroundColor3, }); const mkComps = []; @@ -289,7 +289,7 @@ class EncryptionConfigScreenComponent extends React.Component { const mapStateToProps = state => { return { - theme: state.settings.theme, + themeId: state.settings.theme, masterKeys: state.masterKeys, passwords: state.settings['encryption.passwordCache'], encryptionEnabled: state.settings['encryption.enabled'], diff --git a/ElectronClient/gui/Header/Header.jsx b/ElectronClient/gui/Header/Header.jsx deleted file mode 100644 index 5c3d68c55f..0000000000 --- a/ElectronClient/gui/Header/Header.jsx +++ /dev/null @@ -1,329 +0,0 @@ -const React = require('react'); -const { connect } = require('react-redux'); -const { themeStyle } = require('lib/theme'); -const { _ } = require('lib/locale.js'); -const { bridge } = require('electron').remote.require('./bridge'); -const CommandService = require('lib/services/CommandService').default; -const Setting = require('lib/models/Setting.js'); - -const commands = [ - require('./commands/focusSearch'), -]; - -class HeaderComponent extends React.Component { - constructor() { - super(); - this.state = { - searchQuery: '', - showSearchUsageLink: false, - showButtonLabels: true, - }; - - for (const command of commands) { - CommandService.instance().registerRuntime(command.declaration.name, command.runtime(this)); - } - - this.scheduleSearchChangeEventIid_ = null; - this.searchOnQuery_ = null; - this.searchElement_ = null; - - const triggerOnQuery = query => { - clearTimeout(this.scheduleSearchChangeEventIid_); - if (this.searchOnQuery_) this.searchOnQuery_(query, Setting.value('db.fuzzySearchEnabled')); - this.scheduleSearchChangeEventIid_ = null; - }; - - this.search_onChange = event => { - this.setState({ searchQuery: event.target.value }); - - if (this.scheduleSearchChangeEventIid_) clearTimeout(this.scheduleSearchChangeEventIid_); - - this.scheduleSearchChangeEventIid_ = setTimeout(() => { - triggerOnQuery(this.state.searchQuery); - }, 500); - }; - - this.search_onClear = () => { - this.resetSearch(); - if (this.searchElement_) this.searchElement_.focus(); - }; - - this.search_onFocus = () => { - if (this.hideSearchUsageLinkIID_) { - clearTimeout(this.hideSearchUsageLinkIID_); - this.hideSearchUsageLinkIID_ = null; - } - - this.setState({ showSearchUsageLink: true }); - }; - - this.search_onBlur = () => { - if (this.hideSearchUsageLinkIID_) return; - - this.hideSearchUsageLinkIID_ = setTimeout(() => { - this.setState({ showSearchUsageLink: false }); - }, 5000); - }; - - this.search_keyDown = event => { - if (event.keyCode === 27) { - // ESCAPE - this.resetSearch(); - } - }; - - this.resetSearch = () => { - this.setState({ searchQuery: '' }); - triggerOnQuery(''); - }; - - this.searchUsageLink_click = () => { - bridge().openExternal('https://joplinapp.org/#searching'); - }; - } - - componentDidUpdate(prevProps) { - if (prevProps.notesParentType !== this.props.notesParentType && this.props.notesParentType !== 'Search' && this.state.searchQuery) { - this.resetSearch(); - } - - if (this.props.zoomFactor !== prevProps.zoomFactor || this.props.size !== prevProps.size) { - this.determineButtonLabelState(); - } - } - - componentDidMount() { - this.determineButtonLabelState(); - } - - componentWillUnmount() { - if (this.hideSearchUsageLinkIID_) { - clearTimeout(this.hideSearchUsageLinkIID_); - this.hideSearchUsageLinkIID_ = null; - } - - for (const command of commands) { - CommandService.instance().unregisterRuntime(command.declaration.name); - } - } - - determineButtonLabelState() { - const mediaQuery = window.matchMedia(`(max-width: ${780 * this.props.zoomFactor}px)`); - const showButtonLabels = !mediaQuery.matches; - - if (this.state.showButtonLabels !== showButtonLabels) { - this.setState({ - showButtonLabels: !mediaQuery.matches, - }); - } - } - - back_click() { - this.props.dispatch({ type: 'NAV_BACK' }); - } - - makeButton(key, style, options) { - // TODO: "tab" type is not finished - if (options.type === 'tab') { - const buttons = []; - for (let i = 0; i < options.items.length; i++) { - const item = options.items[i]; - buttons.push(this.makeButton(key + item.title, style, Object.assign({}, options, { - title: item.title, - type: 'button', - }))); - } - - return {buttons}; - } - - const theme = themeStyle(this.props.theme); - - let icon = null; - if (options.iconName) { - const iconStyle = { - fontSize: Math.round(style.fontSize * 1.1), - color: theme.iconColor, - }; - if (options.title) iconStyle.marginRight = 5; - if ('undefined' != typeof options.iconRotation) { - iconStyle.transition = 'transform 0.15s ease-in-out'; - iconStyle.transform = `rotate(${options.iconRotation}deg)`; - } - icon = ; - } - - const isEnabled = !('enabled' in options) || options.enabled; - const classes = ['button']; - if (!isEnabled) classes.push('disabled'); - - const finalStyle = Object.assign({}, style, { - opacity: isEnabled ? 1 : 0.4, - }); - - const title = options.title ? options.title : ''; - - if (options.type === 'checkbox' && options.checked) { - finalStyle.backgroundColor = theme.selectedColor; - finalStyle.borderWidth = 1; - finalStyle.borderTopColor = theme.selectedDividerColor; - finalStyle.borderLeftColor = theme.selectedDividerColor; - finalStyle.borderTopStyle = 'solid'; - finalStyle.borderLeftStyle = 'solid'; - finalStyle.paddingLeft++; - finalStyle.paddingTop++; - finalStyle.paddingBottom--; - finalStyle.paddingRight--; - finalStyle.boxSizing = 'border-box'; - } - - return ( - { - if (isEnabled) options.onClick(); - }} - > - {icon} - {title} - - ); - } - - makeSearch(key, style, options, state) { - const theme = themeStyle(this.props.theme); - - const inputStyle = { - display: 'flex', - flex: 1, - marginLeft: 10, - paddingLeft: 6, - paddingRight: 6, - paddingTop: 1, // vertical alignment with buttons - paddingBottom: 0, // vertical alignment with buttons - height: style.fontSize * 2, - maxWidth: 300, - color: style.color, - fontSize: style.fontSize, - fontFamily: style.fontFamily, - backgroundColor: style.searchColor, - border: '1px solid', - borderColor: style.dividerColor, - }; - - const searchButton = { - paddingLeft: 4, - paddingRight: 4, - paddingTop: 2, - paddingBottom: 2, - textDecoration: 'none', - }; - - const iconStyle = { - display: 'flex', - fontSize: Math.round(style.fontSize) * 1.2, - color: style.color, - }; - - const containerStyle = { - display: 'flex', - flexDirection: 'row', - flexGrow: 1, - alignItems: 'center', - }; - - const iconName = state.searchQuery ? 'fa-times' : 'fa-search'; - const icon = ; - if (options.onQuery) this.searchOnQuery_ = options.onQuery; - - const usageLink = !this.state.showSearchUsageLink ? null : ( - - {_('Usage')} - - ); - - return ( -
- (this.searchElement_ = elem)} onFocus={this.search_onFocus} onBlur={this.search_onBlur} onKeyDown={this.search_keyDown} /> - - {icon} - - {usageLink} -
- ); - } - - render() { - const style = Object.assign({}, this.props.style); - const theme = themeStyle(this.props.theme); - const showBackButton = this.props.showBackButton === undefined || this.props.showBackButton === true; - style.height = theme.headerHeight; - style.display = 'flex'; - style.flexDirection = 'row'; - style.borderBottom = `1px solid ${theme.dividerColor}`; - style.boxSizing = 'border-box'; - - const items = []; - - const itemStyle = { - height: theme.headerHeight, - display: 'flex', - alignItems: 'center', - paddingTop: 1, - paddingBottom: 1, - paddingLeft: theme.headerButtonHPadding, - paddingRight: theme.headerButtonHPadding, - color: theme.color, - searchColor: theme.backgroundColor, - dividerColor: theme.dividerColor, - textDecoration: 'none', - fontFamily: theme.fontFamily, - fontSize: theme.fontSize, - boxSizing: 'border-box', - cursor: 'default', - whiteSpace: 'nowrap', - userSelect: 'none', - }; - - if (showBackButton) { - items.push(this.makeButton('back', itemStyle, { title: _('Back'), onClick: () => this.back_click(), iconName: 'fa-chevron-left ' })); - } - - if (this.props.items) { - for (let i = 0; i < this.props.items.length; i++) { - const item = this.props.items[i]; - - if (item.type === 'search') { - items.push(this.makeSearch(`item_${i}_search`, itemStyle, item, this.state)); - } else { - items.push(this.makeButton(`item_${i}_${item.title}`, itemStyle, item)); - } - } - } - - return ( -
- {items} -
- ); - } -} - -const mapStateToProps = state => { - return { - theme: state.settings.theme, - notesParentType: state.notesParentType, - size: state.windowContentSize, - zoomFactor: state.settings.windowContentZoomFactor / 100, - }; -}; - -const Header = connect(mapStateToProps)(HeaderComponent); - -module.exports = { Header }; diff --git a/ElectronClient/gui/HelpButton.jsx b/ElectronClient/gui/HelpButton.jsx index 9601e202d5..be5a8f1449 100644 --- a/ElectronClient/gui/HelpButton.jsx +++ b/ElectronClient/gui/HelpButton.jsx @@ -14,7 +14,7 @@ class HelpButtonComponent extends React.Component { } render() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const style = Object.assign({}, this.props.style, { color: theme.color, textDecoration: 'none' }); const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 }; const extraProps = {}; @@ -29,7 +29,7 @@ class HelpButtonComponent extends React.Component { const mapStateToProps = state => { return { - theme: state.settings.theme, + themeId: state.settings.theme, }; }; diff --git a/ElectronClient/gui/IconButton.jsx b/ElectronClient/gui/IconButton.jsx index f26a506c39..99daa5aed9 100644 --- a/ElectronClient/gui/IconButton.jsx +++ b/ElectronClient/gui/IconButton.jsx @@ -4,7 +4,7 @@ const { themeStyle } = require('lib/theme'); class IconButton extends React.Component { render() { const style = this.props.style; - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const iconStyle = { color: theme.color, fontSize: theme.fontSize * 1.4, diff --git a/ElectronClient/gui/ImportScreen.jsx b/ElectronClient/gui/ImportScreen.jsx index e037106f88..e14f6db8d0 100644 --- a/ElectronClient/gui/ImportScreen.jsx +++ b/ElectronClient/gui/ImportScreen.jsx @@ -1,7 +1,6 @@ const React = require('react'); const { connect } = require('react-redux'); const Folder = require('lib/models/Folder.js'); -const { Header } = require('./Header/Header.min.js'); const { themeStyle } = require('lib/theme'); const { _ } = require('lib/locale.js'); const { filename, basename } = require('lib/path-utils.js'); @@ -94,8 +93,7 @@ class ImportScreenComponent extends React.Component { } render() { - const theme = themeStyle(this.props.theme); - const style = this.props.style; + const theme = themeStyle(this.props.themeId); const messages = this.uniqueMessages(); const messagesStyle = { @@ -105,10 +103,6 @@ class ImportScreenComponent extends React.Component { backgroundColor: theme.backgroundColor, }; - const headerStyle = { - width: style.width, - }; - const messageComps = []; for (let i = 0; i < messages.length; i++) { messageComps.push(
{messages[i].text}
); @@ -116,7 +110,6 @@ class ImportScreenComponent extends React.Component { return (
-
{messageComps}
); @@ -125,7 +118,7 @@ class ImportScreenComponent extends React.Component { const mapStateToProps = state => { return { - theme: state.settings.theme, + themeId: state.settings.theme, }; }; diff --git a/ElectronClient/gui/ItemList.jsx b/ElectronClient/gui/ItemList.jsx index cd2d48dcb2..08eb723a39 100644 --- a/ElectronClient/gui/ItemList.jsx +++ b/ElectronClient/gui/ItemList.jsx @@ -36,6 +36,10 @@ class ItemList extends React.Component { return this.listRef.current ? this.listRef.current.offsetTop : 0; } + offsetScroll() { + return this.scrollTop_; + } + UNSAFE_componentWillMount() { this.updateStateItemIndexes(); } diff --git a/ElectronClient/gui/KeymapConfig/styles/index.ts b/ElectronClient/gui/KeymapConfig/styles/index.ts index facd626977..eeb101adb6 100644 --- a/ElectronClient/gui/KeymapConfig/styles/index.ts +++ b/ElectronClient/gui/KeymapConfig/styles/index.ts @@ -5,7 +5,8 @@ export default function styles(themeId: number) { return { container: { ...theme.containerStyle, - padding: 16, + padding: theme.configScreenPadding, + backgroundColor: theme.backgroundColor3, }, actionsContainer: { display: 'flex', diff --git a/ElectronClient/gui/MainScreen/MainScreen.jsx b/ElectronClient/gui/MainScreen/MainScreen.tsx similarity index 65% rename from ElectronClient/gui/MainScreen/MainScreen.jsx rename to ElectronClient/gui/MainScreen/MainScreen.tsx index e1f9fa4d87..695472a7ee 100644 --- a/ElectronClient/gui/MainScreen/MainScreen.jsx +++ b/ElectronClient/gui/MainScreen/MainScreen.tsx @@ -1,24 +1,26 @@ -const React = require('react'); +import * as React from 'react'; +import ResizableLayout, { findItemByKey, LayoutItem, LayoutItemDirection } from '../ResizableLayout/ResizableLayout'; +import NoteList from '../NoteList/NoteList.js'; +import NoteEditor from '../NoteEditor/NoteEditor.js'; +import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog.js'; +import ShareNoteDialog from '../ShareNoteDialog.js'; +import NoteListControls from '../NoteListControls/NoteListControls.js'; +import CommandService from 'lib/services/CommandService'; + +const produce = require('immer').default; const { connect } = require('react-redux'); -const { Header } = require('../Header/Header.min.js'); -const { SideBar } = require('../SideBar/SideBar.min.js'); -const { NoteList } = require('../NoteList/NoteList.min.js'); -const NoteEditor = require('../NoteEditor/NoteEditor.js').default; +const { SideBar } = require('../SideBar/SideBar.js'); const { stateUtils } = require('lib/reducer.js'); const { PromptDialog } = require('../PromptDialog.min.js'); -const NoteContentPropertiesDialog = require('../NoteContentPropertiesDialog.js').default; const NotePropertiesDialog = require('../NotePropertiesDialog.min.js'); -const ShareNoteDialog = require('../ShareNoteDialog.js').default; const InteropServiceHelper = require('../../InteropServiceHelper.js'); const Setting = require('lib/models/Setting.js'); const { shim } = require('lib/shim'); const { themeStyle } = require('lib/theme.js'); const { _ } = require('lib/locale.js'); const { bridge } = require('electron').remote.require('./bridge'); -const VerticalResizer = require('../VerticalResizer.min'); const PluginManager = require('lib/services/PluginManager'); const EncryptionService = require('lib/services/EncryptionService'); -const CommandService = require('lib/services/CommandService').default; const ipcRenderer = require('electron').ipcRenderer; const { time } = require('lib/time-utils.js'); @@ -28,7 +30,7 @@ const commands = [ require('./commands/hideModalMessage'), require('./commands/moveToFolder'), require('./commands/newNote'), - require('./commands/newNotebook'), + require('./commands/newFolder'), require('./commands/newTodo'), require('./commands/print'), require('./commands/renameFolder'), @@ -40,14 +42,76 @@ const commands = [ require('./commands/showNoteContentProperties'), require('./commands/showNoteProperties'), require('./commands/showShareNoteDialog'), + require('./commands/toggleEditors'), require('./commands/toggleNoteList'), require('./commands/toggleSidebar'), require('./commands/toggleVisiblePanes'), ]; -class MainScreenComponent extends React.Component { - constructor() { - super(); +class MainScreenComponent extends React.Component { + + waitForNotesSavedIID_:any; + isPrinting_:boolean; + styleKey_:string; + styles_:any; + promptOnClose_:Function; + + constructor(props:any) { + super(props); + + const rootLayoutSize = this.rootLayoutSize(); + const theme = themeStyle(props.themeId); + const sideBarMinWidth = 200; + + const layout:LayoutItem = { + key: 'root', + direction: LayoutItemDirection.Row, + resizable: false, + width: rootLayoutSize.width, + height: rootLayoutSize.height, + children: [ + { + key: 'sidebarColumn', + direction: LayoutItemDirection.Column, + resizable: true, + width: Setting.value('style.sidebar.width') < sideBarMinWidth ? sideBarMinWidth : Setting.value('style.sidebar.width'), + visible: Setting.value('sidebarVisibility'), + minWidth: sideBarMinWidth, + children: [ + { + key: 'sideBar', + }, + ], + }, + { + key: 'noteListColumn', + direction: LayoutItemDirection.Column, + resizable: true, + width: Setting.value('style.noteList.width') < sideBarMinWidth ? sideBarMinWidth : Setting.value('style.noteList.width'), + visible: Setting.value('noteListVisibility'), + minWidth: sideBarMinWidth, + children: [ + { + height: theme.topRowHeight, + key: 'noteListControls', + }, + { + key: 'noteList', + }, + ], + }, + { + key: 'editorColumn', + direction: LayoutItemDirection.Column, + resizable: false, + children: [ + { + key: 'editor', + }, + ], + }, + ], + }; this.state = { promptOptions: null, @@ -58,6 +122,7 @@ class MainScreenComponent extends React.Component { notePropertiesDialogOptions: {}, noteContentPropertiesDialogOptions: {}, shareNoteDialogOptions: {}, + layout: layout, }; this.registerCommands(); @@ -70,6 +135,16 @@ class MainScreenComponent extends React.Component { this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this); this.sidebar_onDrag = this.sidebar_onDrag.bind(this); this.noteList_onDrag = this.noteList_onDrag.bind(this); + this.resizableLayout_resize = this.resizableLayout_resize.bind(this); + this.resizableLayout_renderItem = this.resizableLayout_renderItem.bind(this); + this.window_resize = this.window_resize.bind(this); + this.rowHeight = this.rowHeight.bind(this); + + window.addEventListener('resize', this.window_resize); + } + + window_resize() { + this.updateRootLayoutSize(); } setupAppCloseHandling() { @@ -103,11 +178,11 @@ class MainScreenComponent extends React.Component { }); } - sidebar_onDrag(event) { + sidebar_onDrag(event:any) { Setting.setValue('style.sidebar.width', this.props.sidebarWidth + event.deltaX); } - noteList_onDrag(event) { + noteList_onDrag(event:any) { Setting.setValue('style.noteList.width', Setting.value('style.noteList.width') + event.deltaX); } @@ -123,13 +198,13 @@ class MainScreenComponent extends React.Component { this.setState({ shareNoteDialogOptions: {} }); } - commandService_commandsEnabledStateChange(event) { + commandService_commandsEnabledStateChange(event:any) { const buttonCommandNames = [ 'toggleSidebar', 'toggleNoteList', 'newNote', 'newTodo', - 'newNotebook', + 'newFolder', 'toggleVisiblePanes', ]; @@ -141,13 +216,40 @@ class MainScreenComponent extends React.Component { } } + updateRootLayoutSize() { + this.setState({ layout: produce(this.state.layout, (draftState:any) => { + const s = this.rootLayoutSize(); + draftState.width = s.width; + draftState.height = s.height; + }) }); + } + + componentDidUpdate(prevProps:any) { + if (this.props.noteListVisibility !== prevProps.noteListVisibility || this.props.sidebarVisibility !== prevProps.sidebarVisibility) { + this.setState({ layout: produce(this.state.layout, (draftState:any) => { + const noteListColumn = findItemByKey(draftState, 'noteListColumn'); + noteListColumn.visible = this.props.noteListVisibility; + + const sidebarColumn = findItemByKey(draftState, 'sidebarColumn'); + sidebarColumn.visible = this.props.sidebarVisibility; + }) }); + } + + if (prevProps.style.width !== this.props.style.width || prevProps.style.height !== this.props.style.height) { + this.updateRootLayoutSize(); + } + } + componentDidMount() { CommandService.instance().on('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange); + this.updateRootLayoutSize(); } componentWillUnmount() { CommandService.instance().off('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange); this.unregisterCommands(); + + window.removeEventListener('resize', this.window_resize); } toggleSidebar() { @@ -162,14 +264,14 @@ class MainScreenComponent extends React.Component { }); } - async waitForNoteToSaved(noteId) { + async waitForNoteToSaved(noteId:string) { while (noteId && this.props.editorNoteStatuses[noteId] === 'saving') { console.info('Waiting for note to be saved...', this.props.editorNoteStatuses); await time.msleep(100); } } - async printTo_(target, options) { + async printTo_(target:string, options:any) { // Concurrent print calls are disallowed to avoid incorrect settings being restored upon completion if (this.isPrinting_) { console.info(`Printing ${options.path} to ${target} disallowed, already printing.`); @@ -208,7 +310,23 @@ class MainScreenComponent extends React.Component { this.isPrinting_ = false; } - styles(themeId, width, height, messageBoxVisible, isSidebarVisible, isNoteListVisible, sidebarWidth, noteListWidth) { + rootLayoutSize() { + return { + width: window.innerWidth, + height: this.rowHeight(), + }; + } + + rowHeight() { + if (!this.props) return 0; + return this.props.style.height - (this.messageBoxVisible() ? this.messageBoxHeight() : 0); + } + + messageBoxHeight() { + return 50; + } + + styles(themeId:number, width:number, height:number, messageBoxVisible:boolean, isSidebarVisible:any, isNoteListVisible:any, sidebarWidth:number, noteListWidth:number) { const styleKey = [themeId, width, height, messageBoxVisible, +isSidebarVisible, +isNoteListVisible, sidebarWidth, noteListWidth].join('_'); if (styleKey === this.styleKey_) return this.styles_; @@ -224,14 +342,16 @@ class MainScreenComponent extends React.Component { this.styles_.messageBox = { width: width, - height: 50, + height: this.messageBoxHeight(), display: 'flex', alignItems: 'center', paddingLeft: 10, backgroundColor: theme.warningBackgroundColor, }; - const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0); + const rowHeight = height - (messageBoxVisible ? this.styles_.messageBox.height : 0); + + this.styles_.rowHeight = rowHeight; this.styles_.verticalResizerSidebar = { width: 5, @@ -241,6 +361,10 @@ class MainScreenComponent extends React.Component { display: 'inline-block', }; + this.styles_.resizableLayout = { + height: rowHeight, + }; + this.styles_.verticalResizerNotelist = Object.assign({}, this.styles_.verticalResizerSidebar); this.styles_.sideBar = { @@ -295,7 +419,7 @@ class MainScreenComponent extends React.Component { return this.styles_; } - renderNotification(theme, styles) { + renderNotification(theme:any, styles:any) { if (!this.messageBoxVisible()) return null; const onViewStatusScreen = () => { @@ -401,8 +525,34 @@ class MainScreenComponent extends React.Component { } } + resizableLayout_resize(event:any) { + this.setState({ layout: event.layout }); + + const col1 = findItemByKey(event.layout, 'sidebarColumn'); + const col2 = findItemByKey(event.layout, 'noteListColumn'); + Setting.setValue('style.sidebar.width', col1.width); + Setting.setValue('style.noteList.width', col2.width); + } + + resizableLayout_renderItem(key:string, event:any) { + const eventEmitter = event.eventEmitter; + + if (key === 'sideBar') { + return ; + } else if (key === 'noteList') { + return ; + } else if (key === 'editor') { + const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE'; + return ; + } else if (key === 'noteListControls') { + return ; + } + + throw new Error(`Invalid layout component: ${key}`); + } + render() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const style = Object.assign( { color: theme.color, @@ -411,48 +561,12 @@ class MainScreenComponent extends React.Component { this.props.style ); const promptOptions = this.state.promptOptions; - const notes = this.props.notes; const sidebarVisibility = this.props.sidebarVisibility; const noteListVisibility = this.props.noteListVisibility; - const styles = this.styles(this.props.theme, style.width, style.height, this.messageBoxVisible(), sidebarVisibility, noteListVisibility, this.props.sidebarWidth, this.props.noteListWidth); - - const headerItems = []; - - headerItems.push(CommandService.instance().commandToToolbarButton('toggleSidebar', { iconRotation: sidebarVisibility ? 0 : 90 })); - headerItems.push(CommandService.instance().commandToToolbarButton('toggleNoteList', { iconRotation: noteListVisibility ? 0 : 90 })); - headerItems.push(CommandService.instance().commandToToolbarButton('newNote')); - headerItems.push(CommandService.instance().commandToToolbarButton('newTodo')); - headerItems.push(CommandService.instance().commandToToolbarButton('newNotebook')); - - headerItems.push({ - title: _('Code View'), - iconName: 'fa-file-code ', - enabled: !!notes.length, - type: 'checkbox', - checked: this.props.settingEditorCodeView, - onClick: () => { - // A bit of a hack, but for now don't allow changing code view - // while a note is being saved as it will cause a problem with - // TinyMCE because it won't have time to send its content before - // being switch to the Code Editor. - if (this.props.hasNotesBeingSaved) return; - Setting.toggle('editor.codeView'); - }, - }); - - headerItems.push(CommandService.instance().commandToToolbarButton('toggleVisiblePanes')); - - headerItems.push({ - title: _('Search...'), - iconName: 'fa-search', - onQuery: (query, fuzzy = false) => { - CommandService.instance().execute('search', { query, fuzzy }); - }, - type: 'search', - }); + const styles = this.styles(this.props.themeId, style.width, style.height, this.messageBoxVisible(), sidebarVisibility, noteListVisibility, this.props.sidebarWidth, this.props.noteListWidth); if (!this.promptOnClose_) { - this.promptOnClose_ = (answer, buttonType) => { + this.promptOnClose_ = (answer:any, buttonType:any) => { return this.state.promptOptions.onClose(answer, buttonType); }; } @@ -468,34 +582,33 @@ class MainScreenComponent extends React.Component { const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions; const shareNoteDialogOptions = this.state.shareNoteDialogOptions; - const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE'; - return (
{this.state.modalLayer.message}
- {noteContentPropertiesDialogOptions.visible && } - {notePropertiesDialogOptions.visible && } - {shareNoteDialogOptions.visible && } + {noteContentPropertiesDialogOptions.visible && } + {notePropertiesDialogOptions.visible && } + {shareNoteDialogOptions.visible && } - + -
{messageComp} - - - - - + {pluginDialog}
); } } -const mapStateToProps = state => { +const mapStateToProps = (state:any) => { return { - theme: state.settings.theme, + themeId: state.settings.theme, settingEditorCodeView: state.settings['editor.codeView'], sidebarVisibility: state.sidebarVisibility, noteListVisibility: state.noteListVisibility, @@ -519,6 +632,4 @@ const mapStateToProps = state => { }; }; -const MainScreen = connect(mapStateToProps)(MainScreenComponent); - -module.exports = { MainScreen }; +export default connect(mapStateToProps)(MainScreenComponent); diff --git a/ElectronClient/gui/MainScreen/commands/editAlarm.ts b/ElectronClient/gui/MainScreen/commands/editAlarm.ts index 3d5c9e9aa3..7880d46c36 100644 --- a/ElectronClient/gui/MainScreen/commands/editAlarm.ts +++ b/ElectronClient/gui/MainScreen/commands/editAlarm.ts @@ -8,7 +8,7 @@ const { time } = require('lib/time-utils'); export const declaration:CommandDeclaration = { name: 'editAlarm', label: () => _('Set alarm'), - iconName: 'fa-clock', + iconName: 'icon-alarm', }; export const runtime = (comp:any):CommandRuntime => { diff --git a/ElectronClient/gui/MainScreen/commands/newNotebook.ts b/ElectronClient/gui/MainScreen/commands/newFolder.ts similarity index 98% rename from ElectronClient/gui/MainScreen/commands/newNotebook.ts rename to ElectronClient/gui/MainScreen/commands/newFolder.ts index 1a35a3e708..6a9cae5562 100644 --- a/ElectronClient/gui/MainScreen/commands/newNotebook.ts +++ b/ElectronClient/gui/MainScreen/commands/newFolder.ts @@ -4,7 +4,7 @@ const Folder = require('lib/models/Folder'); const { bridge } = require('electron').remote.require('./bridge'); export const declaration:CommandDeclaration = { - name: 'newNotebook', + name: 'newFolder', label: () => _('New notebook'), iconName: 'fa-book', }; diff --git a/ElectronClient/gui/MainScreen/commands/search.ts b/ElectronClient/gui/MainScreen/commands/search.ts index bdacb9497e..4e46443875 100644 --- a/ElectronClient/gui/MainScreen/commands/search.ts +++ b/ElectronClient/gui/MainScreen/commands/search.ts @@ -6,13 +6,12 @@ const { uuid } = require('lib/uuid.js'); export const declaration:CommandDeclaration = { name: 'search', + iconName: 'icon-search', }; export const runtime = (comp:any):CommandRuntime => { return { - execute: async ({ query, fuzzy }:any) => { - console.info('RUNTIME', query); - + execute: async ({ query }:any) => { if (!comp.searchId_) comp.searchId_ = uuid.create(); comp.props.dispatch({ @@ -23,7 +22,6 @@ export const runtime = (comp:any):CommandRuntime => { query_pattern: query, query_folder_id: null, type_: BaseModel.TYPE_SEARCH, - fuzzy: fuzzy, }, }); diff --git a/ElectronClient/gui/MainScreen/commands/setTags.ts b/ElectronClient/gui/MainScreen/commands/setTags.ts index d2d5cbda6d..114dafc7c9 100644 --- a/ElectronClient/gui/MainScreen/commands/setTags.ts +++ b/ElectronClient/gui/MainScreen/commands/setTags.ts @@ -5,7 +5,7 @@ const { _ } = require('lib/locale'); export const declaration:CommandDeclaration = { name: 'setTags', label: () => _('Tags'), - iconName: 'fa-tags', + iconName: 'icon-tags', }; export const runtime = (comp:any):CommandRuntime => { diff --git a/ElectronClient/gui/MainScreen/commands/showNoteProperties.ts b/ElectronClient/gui/MainScreen/commands/showNoteProperties.ts index 4ad44e42f8..1f932cf95a 100644 --- a/ElectronClient/gui/MainScreen/commands/showNoteProperties.ts +++ b/ElectronClient/gui/MainScreen/commands/showNoteProperties.ts @@ -4,7 +4,7 @@ const { _ } = require('lib/locale'); export const declaration:CommandDeclaration = { name: 'showNoteProperties', label: () => _('Note properties'), - iconName: 'fa-info-circle', + iconName: 'icon-info', }; export const runtime = (comp:any):CommandRuntime => { diff --git a/ElectronClient/gui/MainScreen/commands/toggleEditors.ts b/ElectronClient/gui/MainScreen/commands/toggleEditors.ts new file mode 100644 index 0000000000..101183b531 --- /dev/null +++ b/ElectronClient/gui/MainScreen/commands/toggleEditors.ts @@ -0,0 +1,32 @@ +import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; +const { _ } = require('lib/locale'); +const { stateUtils } = require('lib/reducer.js'); +const Setting = require('lib/models/Setting'); + +export const declaration:CommandDeclaration = { + name: 'toggleEditors', + label: () => _('Toggle editors'), + iconName: 'fa-columns', +}; + +export const runtime = ():CommandRuntime => { + return { + execute: async (props:any) => { + // A bit of a hack, but for now don't allow changing code view + // while a note is being saved as it will cause a problem with + // TinyMCE because it won't have time to send its content before + // being switch to Ace Editor. + if (props.hasNotesBeingSaved) return; + Setting.toggle('editor.codeView'); + }, + isEnabled: (props:any):boolean => { + return !props.hasNotesBeingSaved && props.selectedNoteIds.length === 1; + }, + mapStateToProps: (state:any):any => { + return { + hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state), + selectedNoteIds: state.selectedNoteIds, + }; + }, + }; +}; diff --git a/ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.ts b/ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.ts index 24f2d34462..58b4ddb784 100644 --- a/ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.ts +++ b/ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.ts @@ -4,7 +4,7 @@ const { _ } = require('lib/locale'); export const declaration:CommandDeclaration = { name: 'toggleVisiblePanes', label: () => _('Toggle editor layout'), - iconName: 'fa-columns', + iconName: 'icon-layout ', }; export const runtime = (comp:any):CommandRuntime => { diff --git a/ElectronClient/gui/MultiNoteActions.tsx b/ElectronClient/gui/MultiNoteActions.tsx index 765361a123..fe595407d7 100644 --- a/ElectronClient/gui/MultiNoteActions.tsx +++ b/ElectronClient/gui/MultiNoteActions.tsx @@ -5,22 +5,21 @@ const { bridge } = require('electron').remote.require('./bridge'); const NoteListUtils = require('./utils/NoteListUtils'); interface MultiNoteActionsProps { - theme: number, + themeId: number, selectedNoteIds: string[], notes: any[], dispatch: Function, watchedNoteFiles: string[], - style: any, } function styles_(props:MultiNoteActionsProps) { - return buildStyle('MultiNoteActions', props.theme, (theme:any) => { + return buildStyle('MultiNoteActions', props.themeId, (theme:any) => { return { root: { - ...props.style, display: 'inline-flex', justifyContent: 'center', paddingTop: theme.marginTop, + width: '100%', }, itemList: { display: 'flex', diff --git a/ElectronClient/gui/NoteContentPropertiesDialog.tsx b/ElectronClient/gui/NoteContentPropertiesDialog.tsx index 6c4608f709..2a48216c6c 100644 --- a/ElectronClient/gui/NoteContentPropertiesDialog.tsx +++ b/ElectronClient/gui/NoteContentPropertiesDialog.tsx @@ -7,7 +7,7 @@ const Countable = require('countable'); const markupLanguageUtils = require('lib/markupLanguageUtils'); interface NoteContentPropertiesDialogProps { - theme: number, + themeId: number, text: string, markupLanguage: number, onClose: Function, @@ -46,7 +46,9 @@ function formatReadTime(readTimeMinutes: number) { } export default function NoteContentPropertiesDialog(props:NoteContentPropertiesDialogProps) { - const theme = themeStyle(props.theme); + + console.info('MMMMMMMMMMMM', props.markupLanguage); + const theme = themeStyle(props.themeId); const tableBodyComps: JSX.Element[] = []; // For the source Markdown const [lines, setLines] = useState(0); @@ -165,7 +167,7 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
{_('Read time: %s min', formatReadTime(strippedReadTime))}
- +
); diff --git a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx index e649f49f88..f2f5b77d7e 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx +++ b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx @@ -48,7 +48,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { props_onChangeRef.current = props.onChange; const contentKeyHasChangedRef = useRef(false); contentKeyHasChangedRef.current = previousContentKey !== props.contentKey; - const theme = themeStyle(props.theme); const rootSize = useRootSize({ rootRef }); @@ -351,6 +350,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { }, [styles.editor.codeMirrorTheme]); useEffect(() => { + const theme = themeStyle(props.themeId); + const element = document.createElement('style'); element.setAttribute('id', 'codemirrorStyle'); document.head.appendChild(element); @@ -420,7 +421,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { return () => { document.head.removeChild(element); }; - }, [props.theme]); + }, [props.themeId]); const webview_domReady = useCallback(() => { setWebviewReady(true); @@ -549,7 +550,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { value={props.content} ref={editorRef} mode={props.contentMarkupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'xml' : 'joplin-markdown'} - theme={styles.editor.codeMirrorTheme} + codeMirrorTheme={styles.editor.codeMirrorTheme} style={styles.editor} readOnly={props.visiblePanes.indexOf('editor') < 0} autoMatchBraces={Setting.value('editor.autoMatchingBraces')} @@ -580,7 +581,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
diff --git a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx index 468b90924b..88bad8d4c7 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx +++ b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx @@ -69,7 +69,7 @@ export interface EditorProps { value: string, mode: string, style: any, - theme: any, + codeMirrorTheme: any, readOnly: boolean, autoMatchBraces: boolean, keyMap: string, @@ -216,7 +216,7 @@ function Editor(props: EditorProps, ref: any) { const cmOptions = { value: props.value, screenReaderLabel: props.value, - theme: props.theme, + theme: props.codeMirrorTheme, mode: props.mode, readOnly: props.readOnly, autoCloseBrackets: props.autoMatchBraces, @@ -265,9 +265,9 @@ function Editor(props: EditorProps, ref: any) { useEffect(() => { if (editor) { - editor.setOption('theme', props.theme); + editor.setOption('theme', props.codeMirrorTheme); } - }, [props.theme]); + }, [props.codeMirrorTheme]); useEffect(() => { if (editor) { diff --git a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx index bffcc38759..d82b844c63 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx +++ b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx @@ -2,22 +2,20 @@ import * as React from 'react'; import CommandService from '../../../../lib/services/CommandService'; const ToolbarBase = require('../../../Toolbar.min.js'); -const { buildStyle, themeStyle } = require('lib/theme'); +const { buildStyle } = require('lib/theme'); interface ToolbarProps { - theme: number, + themeId: number, dispatch: Function, disabled: boolean, } function styles_(props:ToolbarProps) { - return buildStyle('CodeMirrorToolbar', props.theme, (/* theme:any*/) => { - const theme = themeStyle(props.theme); + return buildStyle('CodeMirrorToolbar', props.themeId, () => { return { root: { flex: 1, marginBottom: 0, - borderTop: `1px solid ${theme.dividerColor}`, }, }; }); @@ -29,6 +27,11 @@ export default function Toolbar(props:ToolbarProps) { const cmdService = CommandService.instance(); const toolbarItems = [ + cmdService.commandToToolbarButton('historyBackward'), + cmdService.commandToToolbarButton('historyForward'), + cmdService.commandToToolbarButton('startExternalEditing'), + + { type: 'separator' }, cmdService.commandToToolbarButton('textBold'), cmdService.commandToToolbarButton('textItalic'), { type: 'separator' }, @@ -42,6 +45,8 @@ export default function Toolbar(props:ToolbarProps) { cmdService.commandToToolbarButton('textHeading'), cmdService.commandToToolbarButton('textHorizontalRule'), cmdService.commandToToolbarButton('insertDateTime'), + + cmdService.commandToToolbarButton('toggleEditors'), ]; return ; diff --git a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/styles/index.ts b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/styles/index.ts index 4f63906745..c77736e4e2 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/styles/index.ts +++ b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/styles/index.ts @@ -2,7 +2,7 @@ import { NoteBodyEditorProps } from '../../../utils/types'; const { buildStyle } = require('lib/theme'); export default function styles(props: NoteBodyEditorProps) { - return buildStyle('CodeMirror', props.theme, (theme: any) => { + return buildStyle('CodeMirror', props.themeId, (theme: any) => { return { root: { position: 'relative', diff --git a/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx index a48155ecf2..1a6b88d5e0 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx +++ b/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx @@ -3,15 +3,18 @@ import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHand import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps } from '../../utils/types'; import { resourcesStatus, commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling'; import useScroll from './utils/useScroll'; +import styles_ from './styles'; import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../utils/contextMenu'; -import CommandService from '../../../../lib/services/CommandService'; +import CommandService, { ToolbarButtonInfo } from 'lib/services/CommandService'; +import ToggleEditorsButton, { Value as ToggleEditorsButtonValue } from '../../../ToggleEditorsButton/ToggleEditorsButton'; +import ToolbarButton from '../../../../gui/ToolbarButton/ToolbarButton'; const { MarkupToHtml } = require('lib/joplin-renderer'); const taboverride = require('taboverride'); const { reg } = require('lib/registry.js'); const { _, closestSupportedLocale } = require('lib/locale'); const BaseItem = require('lib/models/BaseItem'); const Resource = require('lib/models/Resource'); -const { themeStyle, buildStyle } = require('lib/theme'); +const { themeStyle } = require('lib/theme'); const { clipboard } = require('electron'); const supportedLocales = require('./supportedLocales'); @@ -112,31 +115,6 @@ const joplinCommandToTinyMceCommands:JoplinCommandToTinyMceCommands = { 'search': { name: 'SearchReplace' }, }; -function styles_(props:NoteBodyEditorProps) { - return buildStyle('TinyMCE', props.theme, (/* theme:any */) => { - return { - disabledOverlay: { - zIndex: 10, - position: 'absolute', - backgroundColor: 'white', - opacity: 0.7, - height: '100%', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: 20, - paddingTop: 50, - textAlign: 'center', - width: '100%', - }, - rootStyle: { - position: 'relative', - ...props.style, - }, - }; - }); -} - let loadedCssFiles_:string[] = []; let loadedJsFiles_:string[] = []; let dispatchDidUpdateIID_:any = null; @@ -170,7 +148,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => { editorRef.current = editor; const styles = styles_(props); - const theme = themeStyle(props.theme); + // const theme = themeStyle(props.themeId); const { scrollToPercent } = useScroll({ editor, onScroll: props.onScroll }); @@ -368,10 +346,17 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => { useEffect(() => { if (!editorReady) return () => {}; + const theme = themeStyle(props.themeId); + const element = document.createElement('style'); element.setAttribute('id', 'tinyMceStyle'); document.head.appendChild(element); element.appendChild(document.createTextNode(` + .joplin-tinymce .tox-editor-header { + padding-left: ${styles.leftExtraToolbarContainer.width + styles.leftExtraToolbarContainer.padding * 2}px; + padding-right: ${styles.rightExtraToolbarContainer.width + styles.rightExtraToolbarContainer.padding * 2}px; + } + .tox .tox-toolbar, .tox .tox-toolbar__overflow, .tox .tox-toolbar__primary, @@ -388,8 +373,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => { } .tox .tox-editor-header { - border-top: 1px solid ${theme.dividerColor}; - border-bottom: 1px solid ${theme.dividerColor}; + border: none; } .tox .tox-tbtn, @@ -401,8 +385,8 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => { .tox input, .tox .tox-label, .tox .tox-toolbar-label { - color: ${theme.iconColor} !important; - fill: ${theme.iconColor} !important; + color: ${theme.color3} !important; + fill: ${theme.color3} !important; } .tox .tox-statusbar a, @@ -424,32 +408,59 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => { } .tox .tox-tbtn:hover { - background-color: ${theme.backgroundHover}; - color: ${theme.colorHover}; - fill: ${theme.colorHover}; + color: ${theme.colorHover3} !important; + fill: ${theme.colorHover3} !important; + background-color: ${theme.backgroundColorHover3} + } + + .tox .tox-tbtn { + width: ${theme.toolbarHeight}px; + height: ${theme.toolbarHeight}px; + min-width: ${theme.toolbarHeight}px; + min-height: ${theme.toolbarHeight}px; + margin: 0; + } + + + .tox .tox-tbtn[aria-haspopup=true] { + width: ${theme.toolbarHeight + 15}px; + min-width: ${theme.toolbarHeight + 15}px; + } + + .tox .tox-tbtn > span, + .tox .tox-tbtn:active > span, + .tox .tox-tbtn:hover > span { + transform: scale(0.8); } .tox .tox-toolbar__primary, .tox .tox-toolbar__overflow { background: none; + background-color: ${theme.backgroundColor3} !important; } .tox-tinymce, .tox .tox-toolbar__group, .tox.tox-tinymce-aux .tox-toolbar__overflow, .tox .tox-dialog__footer { - border-color: ${theme.dividerColor} !important; + border: none !important; } .tox-tinymce { border-top: none !important; } + + .joplin-tinymce .tox-toolbar__group { + background-color: ${theme.backgroundColor3}; + padding-top: ${theme.toolbarPadding}px; + padding-bottom: ${theme.toolbarPadding}px; + } `)); return () => { document.head.removeChild(element); }; - }, [editorReady, props.theme]); + }, [editorReady, props.themeId]); // ----------------------------------------------------------------------------------------- // Enable or disable the editor @@ -499,6 +510,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => { menubar: false, relative_urls: false, branding: false, + statusbar: false, target_list: false, table_resize_bars: false, language: ['en_US', 'en_GB'].includes(language) ? undefined : language, @@ -706,6 +718,8 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => { // The fix would be to make allAssets() return a name and a version for each asset. Then the loading // code would check this and either append the CSS or replace. + const theme = themeStyle(props.themeId); + let docHead_:any = null; function docHead() { @@ -1039,12 +1053,64 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => { }; }, []); + function renderExtraToolbarButton(key:string, info:ToolbarButtonInfo) { + return ; + } + + const leftButtonCommandNames = ['historyBackward', 'historyForward', 'startExternalEditing']; + + function renderLeftExtraToolbarButtons() { + const buttons = []; + for (const buttonName in props.noteToolbarButtonInfos) { + if (!leftButtonCommandNames.includes(buttonName)) continue; + const info = props.noteToolbarButtonInfos[buttonName]; + buttons.push(renderExtraToolbarButton(buttonName, info)); + } + + return ( +
+ {buttons} +
+ ); + } + + function renderRightExtraToolbarButtons() { + const buttons = []; + for (const buttonName in props.noteToolbarButtonInfos) { + if (leftButtonCommandNames.includes(buttonName)) continue; + const info = props.noteToolbarButtonInfos[buttonName]; + + if (buttonName === 'toggleEditors') { + buttons.push(); + } else { + buttons.push(renderExtraToolbarButton(buttonName, info)); + } + } + + return ( +
+ {buttons} +
+ ); + } + // Currently we don't handle resource "auto" and "manual" mode with TinyMCE // as it is quite complex and probably rarely used. function renderDisabledOverlay() { const status = resourcesStatus(props.resourceInfos); if (status === 'ready' && !draggingStarted) return null; + const theme = themeStyle(props.themeId); + const message = draggingStarted ? _('Drop notes or files here') : _('Please wait for all attachments to be downloaded and decrypted. You may also switch to %s to edit the note.', _('Code View')); const statusComp = draggingStarted ? null :

{`Status: ${status}`}

; return ( @@ -1056,8 +1122,10 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => { } return ( -
+
{renderDisabledOverlay()} + {renderLeftExtraToolbarButtons()} + {renderRightExtraToolbarButtons()}
); diff --git a/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/styles/index.ts b/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/styles/index.ts new file mode 100644 index 0000000000..7cf69cb093 --- /dev/null +++ b/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/styles/index.ts @@ -0,0 +1,61 @@ +import { NoteBodyEditorProps } from '../../../utils/types'; +const { buildStyle } = require('lib/theme'); + +export default function styles(props:NoteBodyEditorProps) { + return buildStyle(['TinyMCE', props.style.width, props.style.height], props.themeId, (theme:any) => { + const extraToolbarContainer = { + backgroundColor: theme.backgroundColor3, + display: 'flex', + flexDirection: 'row', + position: 'absolute', + height: theme.toolbarHeight, + zIndex: 2, + top: 0, + padding: theme.toolbarPadding, + }; + + return { + disabledOverlay: { + zIndex: 10, + position: 'absolute', + backgroundColor: 'white', + opacity: 0.7, + height: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: 20, + paddingTop: 50, + textAlign: 'center', + width: '100%', + }, + rootStyle: { + position: 'relative', + width: props.style.width, + height: props.style.height, + }, + leftExtraToolbarContainer: { + ...extraToolbarContainer, + width: 80, + left: 0, + }, + rightExtraToolbarContainer: { + ...extraToolbarContainer, + alignItems: 'center', + justifyContent: 'flex-end', + width: 70, + right: 0, + paddingRight: theme.mainPadding, + }, + extraToolbarButton: { + display: 'flex', + border: 'none', + background: 'none', + }, + extraToolbarButtonIcon: { + fontSize: theme.toolbarIconSize, + color: theme.color3, + }, + }; + }); +} diff --git a/ElectronClient/gui/NoteEditor/NoteEditor.tsx b/ElectronClient/gui/NoteEditor/NoteEditor.tsx index daef549e6f..4882ac09b7 100644 --- a/ElectronClient/gui/NoteEditor/NoteEditor.tsx +++ b/ElectronClient/gui/NoteEditor/NoteEditor.tsx @@ -13,13 +13,18 @@ import useMessageHandler from './utils/useMessageHandler'; import useWindowCommandHandler from './utils/useWindowCommandHandler'; import useDropHandler from './utils/useDropHandler'; import useMarkupToHtml from './utils/useMarkupToHtml'; +import useNoteToolbarButtons from './utils/useNoteToolbarButtons'; import useFormNote, { OnLoadEvent } from './utils/useFormNote'; +import useFolder from './utils/useFolder'; import styles_ from './styles'; import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types'; import ResourceEditWatcher from '../../lib/services/ResourceEditWatcher/index'; -import CommandService from '../../lib/services/CommandService'; +import CommandService from 'lib/services/CommandService'; +import ToolbarButton from '../ToolbarButton/ToolbarButton'; +import Button, { ButtonLevel } from '../Button/Button'; const { themeStyle } = require('lib/theme'); +const { substrWithEllipsis } = require('lib/string-utils'); const NoteSearchBar = require('../NoteSearchBar.min.js'); const { reg } = require('lib/registry.js'); const { time } = require('lib/time-utils.js'); @@ -70,6 +75,8 @@ function NoteEditor(props: NoteEditorProps) { const formNoteRef = useRef(); formNoteRef.current = { ...formNote }; + const formNoteFolder = useFolder({ folderId: formNote.parent_id }); + const { localSearch, onChange: localSearch_change, @@ -133,17 +140,17 @@ function NoteEditor(props: NoteEditorProps) { return formNote.saveActionQueue.waitForAllDone(); } - const markupToHtml = useMarkupToHtml({ themeId: props.theme, customCss: props.customCss }); + const markupToHtml = useMarkupToHtml({ themeId: props.themeId, customCss: props.customCss }); const allAssets = useCallback(async (markupLanguage: number): Promise => { - const theme = themeStyle(props.theme); + const theme = themeStyle(props.themeId); const markupToHtml = markupLanguageUtils.newMarkupToHtml({ resourceBaseUrl: `file://${Setting.value('resourceDir')}/`, }); return markupToHtml.allAssets(markupLanguage, theme); - }, [props.theme]); + }, [props.themeId]); const handleProvisionalFlag = useCallback(() => { if (props.isProvisional) { @@ -331,25 +338,49 @@ function NoteEditor(props: NoteEditorProps) { } function renderNoteToolbar() { + // const theme = themeStyle(props.themeId); + const toolbarStyle = { marginBottom: 0, + // paddingTop: theme.mainPadding, + // paddingBottom: theme.mainPadding, }; return ; } + function renderTagButton() { + const info = CommandService.instance().commandToToolbarButton('setTags'); + return ; + } + function renderTagBar() { - return props.selectedNoteTags.length ? : null; + const theme = themeStyle(props.themeId); + let control = null; + if (!props.selectedNoteTags.length) { + const noteIds = [formNote.id]; + control = { CommandService.instance().execute('setTags', { noteIds }); }} style={theme.clickableTextStyle}>Click to add some tags...; + } else { + control = ; + } + + return ( +
{control}
+ ); } function renderTitleBar() { + const theme = themeStyle(props.themeId); const titleBarDate = {time.formatMsToLocal(formNote.user_updated_time)}; return ( -
+
{titleBarDate} + {renderNoteToolbar()}
); } @@ -381,7 +413,7 @@ function NoteEditor(props: NoteEditorProps) { markupToHtml: markupToHtml, allAssets: allAssets, disabled: false, - theme: props.theme, + themeId: props.themeId, dispatch: props.dispatch, noteToolbar: null,// renderNoteToolbar(), onScroll: onScroll, @@ -391,6 +423,7 @@ function NoteEditor(props: NoteEditorProps) { keyboardMode: Setting.value('editor.keyboardMode'), locale: Setting.value('locale'), onDrop: onDrop, + noteToolbarButtonInfos: useNoteToolbarButtons(), }; let editor = null; @@ -414,10 +447,10 @@ function NoteEditor(props: NoteEditorProps) { }, []); if (showRevisions) { - const theme = themeStyle(props.theme); + const theme = themeStyle(props.themeId); - const revStyle = { - ...props.style, + const revStyle:any = { + // ...props.style, display: 'inline-flex', padding: theme.margin, verticalAlign: 'top', @@ -433,19 +466,18 @@ function NoteEditor(props: NoteEditorProps) { if (props.selectedNoteIds.length > 1) { return ; } function renderSearchBar() { if (!showLocalSearch) return false; - const theme = themeStyle(props.theme); + const theme = themeStyle(props.themeId); return ( +
+ ); + } else { + return null; + } + } + if (formNote.encryption_applied || !formNote.id || !props.noteId) { return renderNoNotes(styles.root); } @@ -488,15 +544,17 @@ function NoteEditor(props: NoteEditorProps) {
{renderResourceWatchingNotification()} {renderTitleBar()} -
- {renderNoteToolbar()}{renderTagBar()} -
+ {renderSearchInfo()}
{editor}
{renderSearchBar()}
+
+ {renderTagButton()} + {renderTagBar()} +
{wysiwygBanner}
@@ -518,7 +576,7 @@ const mapStateToProps = (state: any) => { isProvisional: state.provisionalNoteIds.includes(noteId), editorNoteStatuses: state.editorNoteStatuses, syncStarted: state.syncStarted, - theme: state.settings.theme, + themeId: state.settings.theme, watchedNoteFiles: state.watchedNoteFiles, notesParentType: state.notesParentType, selectedNoteTags: state.selectedNoteTags, diff --git a/ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.ts b/ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.ts index 5d2e3b9196..31b0d3b1da 100644 --- a/ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.ts +++ b/ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.ts @@ -28,57 +28,57 @@ const declarations:CommandDeclaration[] = [ { name: 'textBold', label: () => _('Bold'), - iconName: 'fa-bold', + iconName: 'icon-bold', }, { name: 'textItalic', label: () => _('Italic'), - iconName: 'fa-italic', + iconName: 'icon-italic', }, { name: 'textLink', label: () => _('Hyperlink'), - iconName: 'fa-link', + iconName: 'icon-link', }, { name: 'textCode', label: () => _('Code'), - iconName: 'fa-code', + iconName: 'icon-code', }, { name: 'attachFile', label: () => _('Attach file'), - iconName: 'fa-paperclip', + iconName: 'icon-attachment', }, { name: 'textNumberedList', label: () => _('Numbered List'), - iconName: 'fa-list-ol', + iconName: 'icon-numbered-list', }, { name: 'textBulletedList', label: () => _('Bulleted List'), - iconName: 'fa-list-ul', + iconName: 'icon-bulleted-list', }, { name: 'textCheckbox', label: () => _('Checkbox'), - iconName: 'fa-check-square', + iconName: 'icon-to-do-list', }, { name: 'textHeading', label: () => _('Heading'), - iconName: 'fa-heading', + iconName: 'icon-heading', }, { name: 'textHorizontalRule', label: () => _('Horizontal Rule'), - iconName: 'fa-ellipsis-h', + iconName: 'fas fa-ellipsis-h', }, { name: 'insertDateTime', label: () => _('Insert Date Time'), - iconName: 'fa-calendar-plus', + iconName: 'icon-add-date', }, ]; diff --git a/ElectronClient/gui/NoteEditor/styles/index.ts b/ElectronClient/gui/NoteEditor/styles/index.ts index f015154c51..8a40fbecdf 100644 --- a/ElectronClient/gui/NoteEditor/styles/index.ts +++ b/ElectronClient/gui/NoteEditor/styles/index.ts @@ -3,16 +3,18 @@ import { NoteEditorProps } from '../utils/types'; const { buildStyle } = require('lib/theme'); export default function styles(props: NoteEditorProps) { - return buildStyle(['NoteEditor', props.style.width, props.style.height], props.theme, (theme: any) => { + return buildStyle(['NoteEditor'], props.themeId, (theme: any) => { return { root: { - ...props.style, + // ...props.style, boxSizing: 'border-box', - paddingLeft: 10, - paddingTop: 5, + paddingLeft: theme.mainPadding, + paddingTop: 0, borderLeftWidth: 1, borderLeftColor: theme.dividerColor, borderLeftStyle: 'solid', + width: '100%', + height: '100%', }, titleInput: { flex: 1, @@ -20,16 +22,15 @@ export default function styles(props: NoteEditorProps) { paddingTop: 5, minHeight: 35, boxSizing: 'border-box', + fontWeight: 'bold', paddingBottom: 5, - paddingLeft: 8, + paddingLeft: 0, paddingRight: 8, marginLeft: 5, - // marginRight: theme.paddingLeft, color: theme.textStyle.color, - fontSize: theme.textStyle.fontSize * 1.25, + fontSize: Math.round(theme.textStyle.fontSize * 1.5), backgroundColor: theme.backgroundColor, - border: '1px solid', - borderColor: theme.dividerColor, + border: 'none', }, warningBanner: { background: theme.warningBackgroundColor, diff --git a/ElectronClient/gui/NoteEditor/utils/types.ts b/ElectronClient/gui/NoteEditor/utils/types.ts index d30aadc5ce..05cd087e09 100644 --- a/ElectronClient/gui/NoteEditor/utils/types.ts +++ b/ElectronClient/gui/NoteEditor/utils/types.ts @@ -1,10 +1,15 @@ // eslint-disable-next-line no-unused-vars import AsyncActionQueue from '../../../lib/AsyncActionQueue'; +import { ToolbarButtonInfo } from 'lib/services/CommandService'; + +export interface ToolbarButtonInfos { + [key:string]: ToolbarButtonInfo; +} export interface NoteEditorProps { - style: any; + // style: any; noteId: string; - theme: number; + themeId: number; dispatch: Function; selectedNoteIds: string[]; notes: any[]; @@ -29,7 +34,7 @@ export interface NoteEditorProps { export interface NoteBodyEditorProps { style: any; ref: any, - theme: number; + themeId: number; content: string, contentKey: string, contentMarkupLanguage: number, @@ -51,6 +56,7 @@ export interface NoteBodyEditorProps { resourceInfos: ResourceInfos, locale: string, onDrop: Function, + noteToolbarButtonInfos: ToolbarButtonInfos, } export interface FormNote { diff --git a/ElectronClient/gui/NoteEditor/utils/useFolder.ts b/ElectronClient/gui/NoteEditor/utils/useFolder.ts new file mode 100644 index 0000000000..98baadd97e --- /dev/null +++ b/ElectronClient/gui/NoteEditor/utils/useFolder.ts @@ -0,0 +1,29 @@ +import { useState, useEffect } from 'react'; +const Folder = require('lib/models/Folder'); + +interface HookDependencies { + folderId: string, +} + +export default function(dependencies:HookDependencies) { + const { folderId } = dependencies; + const [folder, setFolder] = useState(null); + + useEffect(function() { + let cancelled = false; + + async function loadFolder() { + const f = await Folder.load(folderId); + if (cancelled) return; + setFolder(f); + } + + loadFolder(); + + return function() { + cancelled = true; + }; + }, [folderId]); + + return folder; +} diff --git a/ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.ts b/ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.ts new file mode 100644 index 0000000000..3130422eb9 --- /dev/null +++ b/ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.ts @@ -0,0 +1,33 @@ +import { useState, useEffect } from 'react'; +import CommandService, { ToolbarButtonInfo } from 'lib/services/CommandService'; + +interface ToolbarButtonInfos { + [key:string]: ToolbarButtonInfo; +} + +export default function useNoteToolbarButtons():ToolbarButtonInfos { + const [noteToolbarButtons, setNoteToolbarButtons] = useState({}); + + function update() { + const buttonNames = ['historyBackward', 'historyForward', 'toggleEditors', 'startExternalEditing']; + const output:ToolbarButtonInfos = {}; + + for (const buttonName of buttonNames) { + output[buttonName] = CommandService.instance().commandToToolbarButton(buttonName); + } + + setNoteToolbarButtons(output); + } + + useEffect(() => { + update(); + + CommandService.instance().on('commandsEnabledStateChange', update); + + return () => { + CommandService.instance().off('commandsEnabledStateChange', update); + }; + }, []); + + return noteToolbarButtons; +} diff --git a/ElectronClient/gui/NoteList/NoteList.jsx b/ElectronClient/gui/NoteList/NoteList.tsx similarity index 78% rename from ElectronClient/gui/NoteList/NoteList.jsx rename to ElectronClient/gui/NoteList/NoteList.tsx index 4b94dcd997..20323e9fa0 100644 --- a/ElectronClient/gui/NoteList/NoteList.jsx +++ b/ElectronClient/gui/NoteList/NoteList.tsx @@ -12,11 +12,18 @@ const Setting = require('lib/models/Setting'); const NoteListUtils = require('../utils/NoteListUtils'); const NoteListItem = require('../NoteListItem').default; const CommandService = require('lib/services/CommandService.js').default; +const styled = require('styled-components').default; const commands = [ require('./commands/focusElementNoteList'), ]; +const StyledRoot = styled.div` + width: 100%; + height: 100%; + background-color: ${(props:any) => props.theme.backgroundColor3}; +`; + class NoteListComponent extends React.Component { constructor() { super(); @@ -27,12 +34,15 @@ class NoteListComponent extends React.Component { this.state = { dragOverTargetNoteIndex: null, + width: 0, + height: 0, }; + this.noteListRef = React.createRef(); this.itemListRef = React.createRef(); this.itemAnchorRefs_ = {}; - this.itemRenderer = this.itemRenderer.bind(this); + this.renderItem = this.renderItem.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.noteItem_titleClick = this.noteItem_titleClick.bind(this); this.noteItem_noteDragOver = this.noteItem_noteDragOver.bind(this); @@ -43,12 +53,13 @@ class NoteListComponent extends React.Component { this.registerGlobalDragEndEvent_ = this.registerGlobalDragEndEvent_.bind(this); this.unregisterGlobalDragEndEvent_ = this.unregisterGlobalDragEndEvent_.bind(this); this.itemContextMenu = this.itemContextMenu.bind(this); + this.resizableLayout_resize = this.resizableLayout_resize.bind(this); } style() { - if (this.styleCache_ && this.styleCache_[this.props.theme]) return this.styleCache_[this.props.theme]; + if (this.styleCache_ && this.styleCache_[this.props.themeId]) return this.styleCache_[this.props.themeId]; - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const style = { root: { @@ -85,12 +96,12 @@ class NoteListComponent extends React.Component { }; this.styleCache_ = {}; - this.styleCache_[this.props.theme] = style; + this.styleCache_[this.props.themeId] = style; return style; } - itemContextMenu(event) { + itemContextMenu(event:any) { const currentItemId = event.currentTarget.getAttribute('data-id'); if (!currentItemId) return; @@ -128,11 +139,11 @@ class NoteListComponent extends React.Component { document.removeEventListener('dragend', this.onGlobalDrop_); } - dragTargetNoteIndex_(event) { - return Math.abs(Math.round((event.clientY - this.itemListRef.current.offsetTop()) / this.itemHeight)); + dragTargetNoteIndex_(event:any) { + return Math.abs(Math.round((event.clientY - this.itemListRef.current.offsetTop() + this.itemListRef.current.offsetScroll()) / this.itemHeight)); } - noteItem_noteDragOver(event) { + noteItem_noteDragOver(event:any) { if (this.props.notesParentType !== 'Folder') return; const dt = event.dataTransfer; @@ -146,7 +157,7 @@ class NoteListComponent extends React.Component { } } - async noteItem_noteDrop(event) { + async noteItem_noteDrop(event:any) { if (this.props.notesParentType !== 'Folder') return; if (this.props.noteSortOrder !== 'order') { @@ -172,7 +183,7 @@ class NoteListComponent extends React.Component { } - async noteItem_checkboxClick(event, item) { + async noteItem_checkboxClick(event:any, item:any) { const checked = event.target.checked; const newNote = { id: item.id, @@ -182,7 +193,7 @@ class NoteListComponent extends React.Component { eventManager.emit('todoToggle', { noteId: item.id, note: newNote }); } - async noteItem_titleClick(event, item) { + async noteItem_titleClick(event:any, item:any) { if (event.ctrlKey || event.metaKey) { event.preventDefault(); this.props.dispatch({ @@ -203,7 +214,7 @@ class NoteListComponent extends React.Component { } } - noteItem_dragStart(event) { + noteItem_dragStart(event:any) { let noteIds = []; // Here there is two cases: @@ -223,7 +234,7 @@ class NoteListComponent extends React.Component { event.dataTransfer.setData('text/x-jop-note-ids', JSON.stringify(noteIds)); } - itemRenderer(item, index) { + renderItem(item:any, index:number) { const highlightedWords = () => { if (this.props.notesParentType === 'Search') { const query = BaseModel.byId(this.props.searches, this.props.selectedSearchId); @@ -240,11 +251,12 @@ class NoteListComponent extends React.Component { return ; } - itemAnchorRef(itemId) { + itemAnchorRef(itemId:string) { if (this.itemAnchorRefs_[itemId] && this.itemAnchorRefs_[itemId].current) return this.itemAnchorRefs_[itemId].current; return null; } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps:any) { if (prevProps.selectedNoteIds !== this.props.selectedNoteIds && this.props.selectedNoteIds.length === 1) { const id = this.props.selectedNoteIds[0]; const doRefocus = this.props.notes.length < prevProps.notes.length; @@ -281,9 +293,13 @@ class NoteListComponent extends React.Component { } } } + + if (prevProps.visible !== this.props.visible) { + this.updateSizeState(); + } } - scrollNoteIndex_(keyCode, ctrlKey, metaKey, noteIndex) { + scrollNoteIndex_(keyCode:any, ctrlKey:any, metaKey:any, noteIndex:any) { if (keyCode === 33) { // Page Up @@ -314,7 +330,7 @@ class NoteListComponent extends React.Component { return noteIndex; } - async onKeyDown(event) { + async onKeyDown(event:any) { const keyCode = event.keyCode; const noteIds = this.props.selectedNoteIds; @@ -350,7 +366,7 @@ class NoteListComponent extends React.Component { event.preventDefault(); const notes = BaseModel.modelsByIds(this.props.notes, noteIds); - const todos = notes.filter(n => !!n.is_todo); + const todos = notes.filter((n:any) => !!n.is_todo); if (!todos.length) return; for (let i = 0; i < todos.length; i++) { @@ -382,7 +398,7 @@ class NoteListComponent extends React.Component { } } - focusNoteId_(noteId) { + focusNoteId_(noteId:string) { // - We need to focus the item manually otherwise focus might be lost when the // list is scrolled and items within it are being rebuilt. // - We need to use an interval because when leaving the arrow pressed, the rendering @@ -401,56 +417,86 @@ class NoteListComponent extends React.Component { } } + updateSizeState() { + this.setState({ + width: this.noteListRef.current.clientWidth, + height: this.noteListRef.current.clientHeight, + }); + } + + resizableLayout_resize() { + this.updateSizeState(); + } + + componentDidMount() { + this.props.resizableLayoutEventEmitter.on('resize', this.resizableLayout_resize); + this.updateSizeState(); + } + componentWillUnmount() { if (this.focusItemIID_) { clearInterval(this.focusItemIID_); this.focusItemIID_ = null; } + this.props.resizableLayoutEventEmitter.off('resize', this.resizableLayout_resize); + CommandService.instance().componentUnregisterCommands(commands); } + renderEmptyList() { + if (this.props.notes.length) return null; + + const theme = themeStyle(this.props.themeId); + const padding = 10; + const emptyDivStyle = { + padding: `${padding}px`, + fontSize: theme.fontSize, + color: theme.color, + backgroundColor: theme.backgroundColor, + fontFamily: theme.fontFamily, + }; + // emptyDivStyle.width = emptyDivStyle.width - padding * 2; + // emptyDivStyle.height = emptyDivStyle.height - padding * 2; + return
{this.props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}
; + } + + renderItemList(style:any) { + if (!this.props.notes.length) return null; + + return ( + + ); + } + render() { - const theme = themeStyle(this.props.theme); - const style = this.props.style; + if (!this.props.size) throw new Error('props.size is required'); - if (!this.props.notes.length) { - const padding = 10; - const emptyDivStyle = Object.assign( - { - padding: `${padding}px`, - fontSize: theme.fontSize, - color: theme.color, - backgroundColor: theme.backgroundColor, - fontFamily: theme.fontFamily, - }, - style - ); - emptyDivStyle.width = emptyDivStyle.width - padding * 2; - emptyDivStyle.height = emptyDivStyle.height - padding * 2; - return
{this.props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}
; - } - - return ; + return ( + + {this.renderEmptyList()} + {this.renderItemList(this.props.size)} + + ); } } -const mapStateToProps = state => { +const mapStateToProps = (state:any) => { return { notes: state.notes, folders: state.folders, selectedNoteIds: state.selectedNoteIds, selectedFolderId: state.selectedFolderId, - theme: state.settings.theme, + themeId: state.settings.theme, notesParentType: state.notesParentType, searches: state.searches, selectedSearchId: state.selectedSearchId, @@ -462,6 +508,4 @@ const mapStateToProps = state => { }; }; -const NoteList = connect(mapStateToProps)(NoteListComponent); - -module.exports = { NoteList }; +export default connect(mapStateToProps)(NoteListComponent); diff --git a/ElectronClient/gui/NoteListControls/NoteListControls.tsx b/ElectronClient/gui/NoteListControls/NoteListControls.tsx new file mode 100644 index 0000000000..aaec380134 --- /dev/null +++ b/ElectronClient/gui/NoteListControls/NoteListControls.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { useEffect, useRef } from 'react'; +import SearchBar from '../SearchBar/SearchBar'; +import Button, { ButtonLevel } from '../Button/Button'; +import CommandService from 'lib/services/CommandService'; +import { runtime as focusSearchRuntime } from './commands/focusSearch'; +const styled = require('styled-components').default; + +const StyledRoot = styled.div` + width: 100%; + /*height: 100%;*/ + display: flex; + flex-direction: row; + padding: ${(props:any) => props.theme.mainPadding}px; + background-color: ${(props:any) => props.theme.backgroundColor3}; +`; + +const StyledButton = styled(Button)` + margin-left: 8px; +`; + +export default function NoteListControls() { + const searchBarRef = useRef(null); + + useEffect(function() { + CommandService.instance().registerRuntime('focusSearch', focusSearchRuntime(searchBarRef)); + + return function() { + CommandService.instance().unregisterRuntime('focusSearch'); + }; + }, []); + + function onNewTodoButtonClick() { + CommandService.instance().execute('newTodo'); + } + + function onNewNoteButtonClick() { + CommandService.instance().execute('newNote'); + } + + return ( + + + + + + ); +} diff --git a/ElectronClient/gui/Header/commands/focusSearch.ts b/ElectronClient/gui/NoteListControls/commands/focusSearch.ts similarity index 70% rename from ElectronClient/gui/Header/commands/focusSearch.ts rename to ElectronClient/gui/NoteListControls/commands/focusSearch.ts index 9abe4c8006..c403a22870 100644 --- a/ElectronClient/gui/Header/commands/focusSearch.ts +++ b/ElectronClient/gui/NoteListControls/commands/focusSearch.ts @@ -6,10 +6,10 @@ export const declaration:CommandDeclaration = { label: () => _('Search in all the notes'), }; -export const runtime = (comp:any):CommandRuntime => { +export const runtime = (searchBarRef:any):CommandRuntime => { return { execute: async () => { - if (comp.searchElement_) comp.searchElement_.focus(); + if (searchBarRef.current) searchBarRef.current.focus(); }, }; }; diff --git a/ElectronClient/gui/NoteListItem.tsx b/ElectronClient/gui/NoteListItem.tsx index f99709fe72..d9efc0e2b3 100644 --- a/ElectronClient/gui/NoteListItem.tsx +++ b/ElectronClient/gui/NoteListItem.tsx @@ -5,10 +5,45 @@ const Mark = require('mark.js/dist/mark.min.js'); const markJsUtils = require('lib/markJsUtils'); const Note = require('lib/models/Note'); const { replaceRegexDiacritics, pregQuote } = require('lib/string-utils'); +const styled = require('styled-components').default; + +const StyledRoot = styled.div` + width: ${(props:any) => props.width}px; + height: ${(props:any) => props.height}px; + opacity: ${(props:any) => props.isProvisional ? '0.5' : '1'}; + max-width: 100%; + box-sizing: border-box; + display: flex; + align-items: stretch; + position: relative; + background-color: ${(props:any) => props.selected ? props.theme.selectedColor : 'none'}; + + border-style: solid; + border-color: ${(props:any) => props.theme.color}; + border-top-width: ${(props:any) => props.dragItemPosition === 'top' ? 2 : 0}px; + border-bottom-width: ${(props:any) => props.dragItemPosition === 'bottom' ? 2 : 0}px; + border-right: none; + border-left: none; + + // https://stackoverflow.com/questions/50174448/css-how-to-add-white-space-before-elements-border + &::before { + content: ''; + border-bottom: 1px solid ${(props:any) => props.theme.dividerColor}; + width: ${(props:any) => props.width - 32}px; + position: absolute; + bottom: 0; + left: 16px; + } + + &:hover { + background-color: ${(props:any) => props.theme.backgroundColorHover3}; + } +`; interface NoteListItemProps { - theme: number, + themeId: number, width: number, + height: number, style: any, dragItemIndex: number, highlightedWords: string[], @@ -28,8 +63,8 @@ interface NoteListItemProps { function NoteListItem(props:NoteListItemProps, ref:any) { const item = props.item; - const theme = themeStyle(props.theme); - const hPadding = 10; + const theme = themeStyle(props.themeId); + const hPadding = 16; const anchorRef = useRef(null); @@ -41,14 +76,11 @@ function NoteListItem(props:NoteListItemProps, ref:any) { }; }); - let rootStyle = Object.assign({ width: props.width, opacity: props.isProvisional ? 0.5 : 1 }, props.style.listItem); - - if (props.isSelected) rootStyle = Object.assign(rootStyle, props.style.listItemSelected); - + let dragItemPosition = ''; if (props.dragItemIndex === props.index) { - rootStyle.borderTop = `2px solid ${theme.color}`; + dragItemPosition = 'top'; } else if (props.index === props.itemCount - 1 && props.dragItemIndex >= props.itemCount) { - rootStyle.borderBottom = `2px solid ${theme.color}`; + dragItemPosition = 'bottom'; } const onTitleClick = useCallback((event) => { @@ -65,7 +97,7 @@ function NoteListItem(props:NoteListItemProps, ref:any) { if (!item.is_todo) return null; return ( -
+
; - // key={`${item.id}_${item.todo_completed}`} - // Need to include "todo_completed" in key so that checkbox is updated when // item is changed via sync. return ( -
+ {renderCheckbox()} -
+ ); } diff --git a/ElectronClient/gui/NotePropertiesDialog.jsx b/ElectronClient/gui/NotePropertiesDialog.jsx index 3a05a5dcbe..5b894a64e8 100644 --- a/ElectronClient/gui/NotePropertiesDialog.jsx +++ b/ElectronClient/gui/NotePropertiesDialog.jsx @@ -224,8 +224,8 @@ class NotePropertiesDialog extends React.Component { } createNoteField(key, value) { - const styles = this.styles(this.props.theme); - const theme = themeStyle(this.props.theme); + const styles = this.styles(this.props.themeId); + const theme = themeStyle(this.props.themeId); const labelComp = ; let controlComp = null; let editComp = null; @@ -356,7 +356,7 @@ class NotePropertiesDialog extends React.Component { } render() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const formNote = this.state.formNote; const noteComps = []; @@ -374,7 +374,7 @@ class NotePropertiesDialog extends React.Component {
{_('Note properties')}
{noteComps}
- +
); diff --git a/ElectronClient/gui/NoteRevisionViewer.jsx b/ElectronClient/gui/NoteRevisionViewer.jsx index b129f66f85..e29267f570 100644 --- a/ElectronClient/gui/NoteRevisionViewer.jsx +++ b/ElectronClient/gui/NoteRevisionViewer.jsx @@ -38,7 +38,7 @@ class NoteRevisionViewerComponent extends React.PureComponent { } style() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const style = { root: { @@ -114,7 +114,7 @@ class NoteRevisionViewerComponent extends React.PureComponent { this.setState({ note: note }); } - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const markupToHtml = markupLanguageUtils.newMarkupToHtml({ resourceBaseUrl: `file://${Setting.value('resourceDir')}/`, @@ -164,7 +164,7 @@ class NoteRevisionViewerComponent extends React.PureComponent { } render() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const style = this.style(); const revisionListItems = []; @@ -213,7 +213,7 @@ class NoteRevisionViewerComponent extends React.PureComponent { const mapStateToProps = state => { return { - theme: state.settings.theme, + themeId: state.settings.theme, }; }; diff --git a/ElectronClient/gui/NoteSearchBar.jsx b/ElectronClient/gui/NoteSearchBar.jsx index 16e340e594..ec60454366 100644 --- a/ElectronClient/gui/NoteSearchBar.jsx +++ b/ElectronClient/gui/NoteSearchBar.jsx @@ -17,7 +17,7 @@ class NoteSearchBarComponent extends React.Component { } style() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const style = { root: Object.assign({}, theme.textStyle, { @@ -34,7 +34,7 @@ class NoteSearchBarComponent extends React.Component { } buttonIconComponent(iconName, clickHandler, isEnabled) { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const searchButton = { paddingLeft: 4, @@ -119,7 +119,7 @@ class NoteSearchBarComponent extends React.Component { // backgroundColor needs to cached to a local variable to prevent the // colour from blinking. // For more info: https://github.com/laurent22/joplin/pull/2329#issuecomment-578376835 - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); if (!this.props.searching) { if (this.props.resultCount === 0 && query.length > 0) { this.backgroundColor = theme.warningBackgroundColor; @@ -181,7 +181,7 @@ class NoteSearchBarComponent extends React.Component { const mapStateToProps = state => { return { - theme: state.settings.theme, + themeId: state.settings.theme, }; }; diff --git a/ElectronClient/gui/NoteStatusBar.jsx b/ElectronClient/gui/NoteStatusBar.jsx index d0701730d3..031c36d999 100644 --- a/ElectronClient/gui/NoteStatusBar.jsx +++ b/ElectronClient/gui/NoteStatusBar.jsx @@ -5,7 +5,7 @@ const { themeStyle } = require('lib/theme'); class NoteStatusBarComponent extends React.Component { style() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const style = { root: Object.assign({}, theme.textStyle, { @@ -28,7 +28,7 @@ const mapStateToProps = state => { // notes: state.notes, // folders: state.folders, // selectedNoteIds: state.selectedNoteIds, - theme: state.settings.theme, + themeId: state.settings.theme, }; }; diff --git a/ElectronClient/gui/NoteTextViewer.jsx b/ElectronClient/gui/NoteTextViewer.jsx index f5bb54fe1c..fa896a11fa 100644 --- a/ElectronClient/gui/NoteTextViewer.jsx +++ b/ElectronClient/gui/NoteTextViewer.jsx @@ -169,7 +169,7 @@ class NoteTextViewerComponent extends React.Component { const mapStateToProps = state => { return { - theme: state.settings.theme, + themeId: state.settings.theme, }; }; diff --git a/ElectronClient/gui/NoteToolbar/NoteToolbar.tsx b/ElectronClient/gui/NoteToolbar/NoteToolbar.tsx index 08923a9d97..c4340692ed 100644 --- a/ElectronClient/gui/NoteToolbar/NoteToolbar.tsx +++ b/ElectronClient/gui/NoteToolbar/NoteToolbar.tsx @@ -1,19 +1,19 @@ import * as React from 'react'; -import { useEffect, useCallback, useState } from 'react'; +import { useEffect, useState } from 'react'; import CommandService from '../../lib/services/CommandService'; const { connect } = require('react-redux'); const { buildStyle } = require('lib/theme'); const Toolbar = require('../Toolbar.min.js'); -const Folder = require('lib/models/Folder'); -const { _ } = require('lib/locale'); -const { substrWithEllipsis } = require('lib/string-utils'); +// const Folder = require('lib/models/Folder'); +// const { _ } = require('lib/locale'); +// const { substrWithEllipsis } = require('lib/string-utils'); interface ButtonClickEvent { name: string, } interface NoteToolbarProps { - theme: number, + themeId: number, style: any, folders: any[], watchedNoteFiles: string[], @@ -26,11 +26,12 @@ interface NoteToolbarProps { } function styles_(props:NoteToolbarProps) { - return buildStyle('NoteToolbar', props.theme, (/* theme:any*/) => { + return buildStyle('NoteToolbar', props.themeId, (theme:any) => { return { root: { ...props.style, borderBottom: 'none', + backgroundColor: theme.backgroundColor, }, }; }); @@ -39,52 +40,27 @@ function styles_(props:NoteToolbarProps) { function NoteToolbar(props:NoteToolbarProps) { const styles = styles_(props); const [toolbarItems, setToolbarItems] = useState([]); - const selectedNoteFolder = Folder.byId(props.folders, props.note.parent_id); - const folderId = selectedNoteFolder ? selectedNoteFolder.id : ''; - const folderTitle = selectedNoteFolder && selectedNoteFolder.title ? selectedNoteFolder.title : ''; + // const selectedNoteFolder = Folder.byId(props.folders, props.note.parent_id); + // const folderId = selectedNoteFolder ? selectedNoteFolder.id : ''; + // const folderTitle = selectedNoteFolder && selectedNoteFolder.title ? selectedNoteFolder.title : ''; const cmdService = CommandService.instance(); - const updateToolbarItems = useCallback(() => { + function updateToolbarItems() { const output = []; - output.push( - cmdService.commandToToolbarButton('historyBackward') - ); - - output.push( - cmdService.commandToToolbarButton('historyForward') - ); - - if (folderId && ['Search', 'Tag', 'SmartFilter'].includes(props.notesParentType)) { - output.push({ - title: _('In: %s', substrWithEllipsis(folderTitle, 0, 16)), - tooltip: folderTitle, - iconName: 'fa-book', - onClick: () => { - props.dispatch({ - type: 'FOLDER_AND_NOTE_SELECT', - folderId: folderId, - noteId: props.note.id, - }); - }, - }); - } - - output.push(cmdService.commandToToolbarButton('showNoteProperties')); - - if (props.watchedNoteFiles.indexOf(props.note.id) >= 0) { - output.push(cmdService.commandToToolbarButton('stopExternalEditing')); - } else { - output.push(cmdService.commandToToolbarButton('startExternalEditing')); - } + // if (props.watchedNoteFiles.indexOf(props.note.id) >= 0) { + // output.push(cmdService.commandToToolbarButton('stopExternalEditing')); + // } else { + // output.push(cmdService.commandToToolbarButton('startExternalEditing')); + // } output.push(cmdService.commandToToolbarButton('editAlarm')); - - output.push(cmdService.commandToToolbarButton('setTags')); + output.push(cmdService.commandToToolbarButton('toggleVisiblePanes')); + output.push(cmdService.commandToToolbarButton('showNoteProperties')); setToolbarItems(output); - }, [props.note.id, folderId, folderTitle, props.watchedNoteFiles, props.notesParentType]); + } useEffect(() => { updateToolbarItems(); @@ -92,7 +68,7 @@ function NoteToolbar(props:NoteToolbarProps) { return () => { cmdService.off('commandsEnabledStateChange', updateToolbarItems); }; - }, [updateToolbarItems]); + }, []); return ; } diff --git a/ElectronClient/gui/OneDriveLoginScreen.jsx b/ElectronClient/gui/OneDriveLoginScreen.tsx similarity index 69% rename from ElectronClient/gui/OneDriveLoginScreen.jsx rename to ElectronClient/gui/OneDriveLoginScreen.tsx index ef9f058faf..db0f281271 100644 --- a/ElectronClient/gui/OneDriveLoginScreen.jsx +++ b/ElectronClient/gui/OneDriveLoginScreen.tsx @@ -1,16 +1,21 @@ -const React = require('react'); +import * as React from 'react'; +import ButtonBar from './ConfigScreen/ButtonBar'; + const { connect } = require('react-redux'); const { reg } = require('lib/registry.js'); const Setting = require('lib/models/Setting'); const { bridge } = require('electron').remote.require('./bridge'); -const { Header } = require('./Header/Header.min.js'); const { themeStyle } = require('lib/theme'); const { _ } = require('lib/locale.js'); const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js'); -class OneDriveLoginScreenComponent extends React.Component { - constructor() { - super(); +interface Props { + themeId: string, +} + +class OneDriveLoginScreenComponent extends React.Component { + constructor(props:Props) { + super(props); this.state = { authLog: [], @@ -18,8 +23,8 @@ class OneDriveLoginScreenComponent extends React.Component { } async componentDidMount() { - const log = (s) => { - this.setState(state => { + const log = (s:any) => { + this.setState((state:any) => { const authLog = state.authLog.slice(); authLog.push({ key: (Date.now() + Math.random()).toString(), text: s }); return { authLog: authLog }; @@ -30,7 +35,7 @@ class OneDriveLoginScreenComponent extends React.Component { const syncTarget = reg.syncTarget(syncTargetId); const oneDriveApiUtils = new OneDriveApiNodeUtils(syncTarget.api()); const auth = await oneDriveApiUtils.oauthDance({ - log: (s) => log(s), + log: (s:any) => log(s), }); Setting.setValue(`sync.${syncTargetId}.auth`, auth ? JSON.stringify(auth) : null); @@ -52,9 +57,7 @@ class OneDriveLoginScreenComponent extends React.Component { } render() { - const style = this.props.style; - const theme = themeStyle(this.props.theme); - const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width }); + const theme = themeStyle(this.props.themeId); const logComps = []; for (const l of this.state.authLog) { @@ -66,22 +69,23 @@ class OneDriveLoginScreenComponent extends React.Component { } return ( -
-
-
+
+
{logComps}
+ this.props.dispatch({ type: 'NAV_BACK' })} + />
); } } -const mapStateToProps = state => { +const mapStateToProps = (state:any) => { return { - theme: state.settings.theme, + themeId: state.settings.theme, }; }; -const OneDriveLoginScreen = connect(mapStateToProps)(OneDriveLoginScreenComponent); +export default connect(mapStateToProps)(OneDriveLoginScreenComponent); -module.exports = { OneDriveLoginScreen }; diff --git a/ElectronClient/gui/PromptDialog.jsx b/ElectronClient/gui/PromptDialog.jsx index 9819047297..d21ace17bc 100644 --- a/ElectronClient/gui/PromptDialog.jsx +++ b/ElectronClient/gui/PromptDialog.jsx @@ -164,10 +164,10 @@ class PromptDialog extends React.Component { render() { const style = this.props.style; - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel']; - const styles = this.styles(this.props.theme, style.width, style.height, this.state.visible); + const styles = this.styles(this.props.themeId, style.width, style.height, this.state.visible); const onClose = (accept, buttonType) => { if (this.props.onClose) { diff --git a/ElectronClient/gui/ResizableLayout/ResizableLayout.tsx b/ElectronClient/gui/ResizableLayout/ResizableLayout.tsx new file mode 100644 index 0000000000..45b84eb4c3 --- /dev/null +++ b/ElectronClient/gui/ResizableLayout/ResizableLayout.tsx @@ -0,0 +1,173 @@ +import * as React from 'react'; +import { useRef, useState } from 'react'; +import produce from 'immer'; +import useWindowResizeEvent from './hooks/useWindowResizeEvent'; +import useLayoutItemSizes, { LayoutItemSizes, itemSize } from './hooks/useLayoutItemSizes'; +const { Resizable } = require('re-resizable'); +const EventEmitter = require('events'); + +export enum LayoutItemDirection { + Row = 'row', + Column = 'column', +} + +export interface Size { + width: number, + height: number, +} + +export interface LayoutItem { + key: string, + width?: number, + height?: number, + minWidth?: number, + minHeight?: number, + children?: LayoutItem[] + direction?: LayoutItemDirection, + resizable?: boolean, + visible?: boolean, +} + +interface onResizeEvent { + layout: LayoutItem +} + +interface Props { + layout: LayoutItem, + renderItem(key:string, event:any):JSX.Element; + onResize(event:onResizeEvent):void; + width?: number, + height?: number, +} + +export function findItemByKey(layout:LayoutItem, key:string):LayoutItem { + function recurseFind(item:LayoutItem):LayoutItem { + if (item.key === key) return item; + + if (item.children) { + for (const child of item.children) { + const found = recurseFind(child); + if (found) return found; + } + } + return null; + } + + const output = recurseFind(layout); + if (!output) throw new Error(`Invalid item key: ${key}`); + return output; +} + +function updateLayoutItem(layout:LayoutItem, key:string, props:any) { + return produce(layout, (draftState:LayoutItem) => { + function recurseFind(item:LayoutItem) { + if (item.key === key) { + for (const n in props) { + (item as any)[n] = props[n]; + } + } else { + if (item.children) { + for (const child of item.children) { + recurseFind(child); + } + } + } + } + + recurseFind(draftState); + }); +} + +function renderContainer(item:LayoutItem, sizes:LayoutItemSizes, onResizeStart:Function, onResize:Function, onResizeStop:Function, children:JSX.Element[]):JSX.Element { + const style:any = { + display: item.visible !== false ? 'flex' : 'none', + flexDirection: item.direction, + }; + + const size:Size = itemSize(item, sizes); + + const className = `resizableLayoutItem rli-${item.key}`; + if (item.resizable) { + const enable = { top: false, right: true, bottom: false, left: false, topRight: false, bottomRight: false, bottomLeft: false, topLeft: false }; + + return ( + + {children} + + ); + } else { + return ( +
+ {children} +
+ ); + } +} + +function ResizableLayout(props:Props) { + const eventEmitter = useRef(new EventEmitter()); + + const [resizedItem, setResizedItem] = useState(null); + + function renderLayoutItem(item:LayoutItem, sizes:LayoutItemSizes, isVisible:boolean):JSX.Element { + + function onResizeStart() { + setResizedItem({ + key: item.key, + initialWidth: sizes[item.key].width, + initialHeight: sizes[item.key].height, + }); + } + + function onResize(_event:any, _direction:any, _refToElement: HTMLDivElement, delta:any) { + const newLayout = updateLayoutItem(props.layout, item.key, { + width: resizedItem.initialWidth + delta.width, + height: resizedItem.initialHeight + delta.height, + }); + + props.onResize({ layout: newLayout }); + eventEmitter.current.emit('resize'); + } + + function onResizeStop(_event:any, _direction:any, _refToElement: HTMLDivElement, delta:any) { + onResize(_event, _direction, _refToElement, delta); + setResizedItem(null); + } + + if (!item.children) { + const comp = props.renderItem(item.key, { + item: item, + eventEmitter: eventEmitter.current, + size: sizes[item.key], + visible: isVisible, + }); + + return renderContainer(item, sizes, onResizeStart, onResize, onResizeStop, [comp]); + } else { + const childrenComponents = []; + for (const child of item.children) { + childrenComponents.push(renderLayoutItem(child, sizes, isVisible && child.visible !== false)); + } + + return renderContainer(item, sizes, onResizeStart, onResize, onResizeStop, childrenComponents); + } + } + + useWindowResizeEvent(eventEmitter); + const sizes = useLayoutItemSizes(props.layout); + + return renderLayoutItem(props.layout, sizes, props.layout.visible !== false); +} + +export default ResizableLayout; diff --git a/ElectronClient/gui/ResizableLayout/hooks/useLayoutItemSizes.ts b/ElectronClient/gui/ResizableLayout/hooks/useLayoutItemSizes.ts new file mode 100644 index 0000000000..5e77925fa9 --- /dev/null +++ b/ElectronClient/gui/ResizableLayout/hooks/useLayoutItemSizes.ts @@ -0,0 +1,79 @@ +import { useMemo } from 'react'; +import { LayoutItem, Size } from '../ResizableLayout'; + +export interface LayoutItemSizes { + [key:string]: Size, +} + +export function itemSize(item:LayoutItem, sizes:LayoutItemSizes):Size { + return { + width: 'width' in item ? item.width : sizes[item.key].width, + height: 'height' in item ? item.height : sizes[item.key].height, + }; +} + +function calculateChildrenSizes(item:LayoutItem, sizes:LayoutItemSizes):LayoutItemSizes { + if (!item.children) return sizes; + + const parentSize = itemSize(item, sizes); + + const remainingSize:Size = { + width: parentSize.width, + height: parentSize.height, + }; + + const noWidthChildren:LayoutItem[] = []; + const noHeightChildren:LayoutItem[] = []; + + for (const child of item.children) { + let w = 'width' in child ? child.width : null; + let h = 'height' in child ? child.height : null; + if (child.visible === false) { + w = 0; + h = 0; + } + sizes[child.key] = { width: w, height: h }; + if (w !== null) remainingSize.width -= w; + if (h !== null) remainingSize.height -= h; + if (w === null) noWidthChildren.push(child); + if (h === null) noHeightChildren.push(child); + } + + if (noWidthChildren.length) { + const w = item.direction === 'row' ? remainingSize.width / noWidthChildren.length : parentSize.width; + for (const child of noWidthChildren) { + sizes[child.key].width = w; + } + } + + if (noHeightChildren.length) { + const h = item.direction === 'column' ? remainingSize.height / noHeightChildren.length : parentSize.height; + for (const child of noHeightChildren) { + sizes[child.key].height = h; + } + } + + for (const child of item.children) { + const childrenSizes = calculateChildrenSizes(child, sizes); + sizes = { ...sizes, ...childrenSizes }; + } + + return sizes; +} + +export default function useLayoutItemSizes(layout:LayoutItem) { + return useMemo(() => { + let sizes:LayoutItemSizes = {}; + + if (!('width' in layout) || !('height' in layout)) throw new Error('width and height are required on layout root'); + + sizes[layout.key] = { + width: layout.width, + height: layout.height, + }; + + sizes = calculateChildrenSizes(layout, sizes); + + return sizes; + }, [layout]); +} diff --git a/ElectronClient/gui/ResizableLayout/hooks/useWindowResizeEvent.ts b/ElectronClient/gui/ResizableLayout/hooks/useWindowResizeEvent.ts new file mode 100644 index 0000000000..8037751906 --- /dev/null +++ b/ElectronClient/gui/ResizableLayout/hooks/useWindowResizeEvent.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; +const debounce = require('debounce'); + +export default function useWindowResizeEvent(eventEmitter:any) { + useEffect(() => { + const window_resize = debounce(() => { + eventEmitter.current.emit('resize'); + }, 500); + + window.addEventListener('resize', window_resize); + + return () => { + window_resize.clear(); + window.removeEventListener('resize', window_resize); + }; + }, []); +} diff --git a/ElectronClient/gui/ResourceScreen.tsx b/ElectronClient/gui/ResourceScreen.tsx index 5244e04d22..653ce9673f 100644 --- a/ElectronClient/gui/ResourceScreen.tsx +++ b/ElectronClient/gui/ResourceScreen.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; +import ButtonBar from './ConfigScreen/ButtonBar'; const { connect } = require('react-redux'); const { _ } = require('lib/locale.js'); const { themeStyle } = require('lib/theme'); const { bridge } = require('electron').remote.require('./bridge'); -const { Header } = require('./Header/Header.min.js'); const prettyBytes = require('pretty-bytes'); const Resource = require('lib/models/Resource.js'); @@ -14,8 +14,9 @@ interface Style { } interface Props { - theme: any; - style: Style + themeId: number; + style: Style, + dispatch: Function, } interface Resource { @@ -37,7 +38,7 @@ interface ResourceTable { onResourceClick: (resource: Resource) => any onResourceDelete: (resource: Resource) => any onToggleSorting: (order: SortingOrder) => any - theme: any + themeId: number style: Style } @@ -50,17 +51,19 @@ interface ActiveSorting { } const ResourceTable: React.FC = (props: ResourceTable) => { + const theme = themeStyle(props.themeId); + const sortOrderEngagedMarker = (s: SortingOrder) => { return (
props.onToggleSorting(s)}>{ (props.sorting.order === s && props.sorting.type === 'desc') ? 'â–¾' : 'â–´'} ); }; const titleCellStyle = { - ...props.theme.textStyle, + ...theme.textStyle, textOverflow: 'ellipsis', overflowX: 'hidden', maxWidth: 1, @@ -69,14 +72,14 @@ const ResourceTable: React.FC = (props: ResourceTable) => { }; const cellStyle = { - ...props.theme.textStyle, + ...theme.textStyle, whiteSpace: 'nowrap', - color: props.theme.colorFaded, + color: theme.colorFaded, width: 1, }; const headerStyle = { - ...props.theme.textStyle, + ...theme.textStyle, whiteSpace: 'nowrap', width: 1, fontWeight: 'bold', @@ -97,7 +100,7 @@ const ResourceTable: React.FC = (props: ResourceTable) => { props.onResourceClick(resource)}>{resource.title || `(${_('Untitled')})`} @@ -105,7 +108,7 @@ const ResourceTable: React.FC = (props: ResourceTable) => { {prettyBytes(resource.size)} {resource.id} - + )} @@ -202,8 +205,7 @@ class ResourceScreenComponent extends React.Component { render() { const style = this.props.style; - const theme = themeStyle(this.props.theme); - const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width }); + const theme = themeStyle(this.props.themeId); const rootStyle:any = { ...style, @@ -211,13 +213,16 @@ class ResourceScreenComponent extends React.Component { color: theme.color, padding: 20, boxSizing: 'border-box', + flex: 1, }; - rootStyle.height = style.height - 35; // Minus the header height + // rootStyle.height = style.height - 35; // Minus the header height + delete rootStyle.height; delete rootStyle.width; + const containerHeight = style.height; + return ( -
-
+
{ _('This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.') @@ -232,7 +237,7 @@ class ResourceScreenComponent extends React.Component {
{_('Warning: not all resources shown for performance reasons (limit: %s).', MAX_RESOURCES)}
} {this.state.resources && {
}
+ this.props.dispatch({ type: 'NAV_BACK' })} + />
); } } const mapStateToProps = (state: any) => ({ - theme: state.settings.theme, + themeId: state.settings.theme, }); const ResourceScreen = connect(mapStateToProps)(ResourceScreenComponent); diff --git a/ElectronClient/gui/Root.jsx b/ElectronClient/gui/Root.jsx index 030a175408..958d865bc7 100644 --- a/ElectronClient/gui/Root.jsx +++ b/ElectronClient/gui/Root.jsx @@ -5,20 +5,30 @@ const { connect, Provider } = require('react-redux'); const { _ } = require('lib/locale.js'); const Setting = require('lib/models/Setting.js'); -const { MainScreen } = require('.//MainScreen/MainScreen.min.js'); +const MainScreen = require('./MainScreen/MainScreen').default; +const ConfigScreen = require('./ConfigScreen/ConfigScreen').default; +const StatusScreen = require('./StatusScreen/StatusScreen').default; +const OneDriveLoginScreen = require('./OneDriveLoginScreen').default; +const DropboxLoginScreen = require('./DropboxLoginScreen').default; const ErrorBoundary = require('./ErrorBoundary').default; -const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js'); -const { DropboxLoginScreen } = require('./DropboxLoginScreen.min.js'); -const { StatusScreen } = require('./StatusScreen.min.js'); const { ImportScreen } = require('./ImportScreen.min.js'); -const { ConfigScreen } = require('./ConfigScreen.min.js'); const { ResourceScreen } = require('./ResourceScreen.js'); const { Navigator } = require('./Navigator.min.js'); const WelcomeUtils = require('lib/WelcomeUtils'); const { app } = require('../app'); +const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components'); +const { themeStyle } = require('lib/theme'); const { bridge } = require('electron').remote.require('./bridge'); +const GlobalStyle = createGlobalStyle` + div, span, a { + color: ${(props) => props.theme.color}; + font-size: ${(props) => props.theme.fontSize}px; + font-family: ${(props) => props.theme.fontFamily}; + } +`; + async function initialize() { this.wcsTimeoutId_ = null; @@ -84,6 +94,8 @@ class RootComponent extends React.Component { height: this.props.size.height / this.props.zoomFactor, }; + const theme = themeStyle(this.props.themeId); + const screens = { Main: { screen: MainScreen }, OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') }, @@ -94,7 +106,14 @@ class RootComponent extends React.Component { Status: { screen: StatusScreen, title: () => _('Synchronisation Status') }, }; - return ; + return ( + + + + + + + ); } } @@ -103,6 +122,7 @@ const mapStateToProps = state => { size: state.windowContentSize, zoomFactor: state.settings.windowContentZoomFactor / 100, appState: state.appState, + themeId: state.settings.theme, }; }; diff --git a/ElectronClient/gui/SearchBar/SearchBar.tsx b/ElectronClient/gui/SearchBar/SearchBar.tsx new file mode 100644 index 0000000000..dc8e437768 --- /dev/null +++ b/ElectronClient/gui/SearchBar/SearchBar.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { useState, useCallback, useEffect } from 'react'; +import CommandService from 'lib/services/CommandService'; +import useSearch from './hooks/useSearch'; +import { Root, SearchInput, SearchButton, SearchButtonIcon } from './styles'; +const { connect } = require('react-redux'); + +const { _ } = require('lib/locale.js'); + +interface Props { + inputRef?: any, + notesParentType: string, +} + +function SearchBar(props:Props) { + const [query, setQuery] = useState(''); + const iconName = !query ? CommandService.instance().iconName('search') : 'fa fa-times'; + + const onChange = (event:any) => { + setQuery(event.currentTarget.value); + }; + + const onSearchButtonClick = useCallback(() => { + setQuery(''); + }, []); + + useSearch(query); + + useEffect(() => { + if (props.notesParentType !== 'Search') { + setQuery(''); + } + }, [props.notesParentType]); + + return ( + + + + + + + ); +} + +const mapStateToProps = (state:any) => { + return { + notesParentType: state.notesParentType, + }; +}; + +export default connect(mapStateToProps)(SearchBar); diff --git a/ElectronClient/gui/SearchBar/hooks/useSearch.ts b/ElectronClient/gui/SearchBar/hooks/useSearch.ts new file mode 100644 index 0000000000..59985a4d39 --- /dev/null +++ b/ElectronClient/gui/SearchBar/hooks/useSearch.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; +import CommandService from 'lib/services/CommandService'; +const debounce = require('debounce'); + +export default function useSearch(query:string) { + useEffect(() => { + const search = debounce((query:string) => { + CommandService.instance().execute('search', { query }); + }, 500); + + search(query); + + return () => { + search.clear(); + }; + }, [query]); +} diff --git a/ElectronClient/gui/SearchBar/styles/index.ts b/ElectronClient/gui/SearchBar/styles/index.ts new file mode 100644 index 0000000000..7b37d2c7ac --- /dev/null +++ b/ElectronClient/gui/SearchBar/styles/index.ts @@ -0,0 +1,28 @@ +import StyledInput from '../../style/StyledInput'; +const styled = require('styled-components').default; + +export const Root = styled.div` + position: relative; + display: flex; + width: 100%; +`; + +export const SearchButton = styled.button` + position: absolute; + right: 0; + background: none; + border: none; + height: 100%; + opacity: ${(props:any) => props.disabled ? 0.5 : 1}; +`; + +export const SearchButtonIcon = styled.span` + font-size: ${(props:any) => props.theme.toolbarIconSize}px; + color: ${(props:any) => props.theme.color4}; +`; + +export const SearchInput = styled(StyledInput)` + padding-right: 20px; + flex: 1; + width: 10px; +`; diff --git a/ElectronClient/gui/ShareNoteDialog.tsx b/ElectronClient/gui/ShareNoteDialog.tsx index fb2dde5bed..15589e2ea8 100644 --- a/ElectronClient/gui/ShareNoteDialog.tsx +++ b/ElectronClient/gui/ShareNoteDialog.tsx @@ -12,7 +12,7 @@ const { reg } = require('lib/registry.js'); const { clipboard } = require('electron'); interface ShareNoteDialogProps { - theme: number, + themeId: number, noteIds: Array, onClose: Function, } @@ -22,7 +22,7 @@ interface SharesMap { } function styles_(props:ShareNoteDialogProps) { - return buildStyle('ShareNoteDialog', props.theme, (theme:any) => { + return buildStyle('ShareNoteDialog', props.themeId, (theme:any) => { return { noteList: { marginBottom: 10, @@ -67,7 +67,7 @@ export default function ShareNoteDialog(props:ShareNoteDialogProps) { const [shares, setShares] = useState({}); const noteCount = notes.length; - const theme = themeStyle(props.theme); + const theme = themeStyle(props.themeId); const styles = styles_(props); useEffect(() => { @@ -206,7 +206,7 @@ export default function ShareNoteDialog(props:ShareNoteDialogProps) {
{statusMessage(sharesState)}
{encryptionWarningMessage} - +
); diff --git a/ElectronClient/gui/SideBar/SideBar.jsx b/ElectronClient/gui/SideBar/SideBar.jsx deleted file mode 100644 index 2f1e898f3e..0000000000 --- a/ElectronClient/gui/SideBar/SideBar.jsx +++ /dev/null @@ -1,766 +0,0 @@ -const React = require('react'); -const { connect } = require('react-redux'); -const shared = require('lib/components/shared/side-menu-shared.js'); -const { Synchronizer } = require('lib/synchronizer.js'); -const CommandService = require('lib/services/CommandService.js').default; -const BaseModel = require('lib/BaseModel.js'); -const Setting = require('lib/models/Setting.js'); -const Folder = require('lib/models/Folder.js'); -const Note = require('lib/models/Note.js'); -const Tag = require('lib/models/Tag.js'); -const { _ } = require('lib/locale.js'); -const { themeStyle } = require('lib/theme'); -const { bridge } = require('electron').remote.require('./bridge'); -const Menu = bridge().Menu; -const MenuItem = bridge().MenuItem; -const InteropServiceHelper = require('../../InteropServiceHelper.js'); -const { substrWithEllipsis } = require('lib/string-utils'); -const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids'); - -const commands = [ - require('./commands/focusElementSideBar'), -]; - -class SideBarComponent extends React.Component { - constructor() { - super(); - - CommandService.instance().componentRegisterCommands(this, commands); - - this.onFolderDragStart_ = event => { - const folderId = event.currentTarget.getAttribute('folderid'); - if (!folderId) return; - - event.dataTransfer.setDragImage(new Image(), 1, 1); - event.dataTransfer.clearData(); - event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId])); - }; - - this.onFolderDragOver_ = event => { - if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault(); - if (event.dataTransfer.types.indexOf('text/x-jop-folder-ids') >= 0) event.preventDefault(); - }; - - this.onFolderDrop_ = async event => { - const folderId = event.currentTarget.getAttribute('folderid'); - const dt = event.dataTransfer; - if (!dt) return; - - // folderId can be NULL when dropping on the sidebar Notebook header. In that case, it's used - // to put the dropped folder at the root. But for notes, folderId needs to always be defined - // since there's no such thing as a root note. - - if (dt.types.indexOf('text/x-jop-note-ids') >= 0) { - event.preventDefault(); - - if (!folderId) return; - - const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids')); - for (let i = 0; i < noteIds.length; i++) { - await Note.moveToFolder(noteIds[i], folderId); - } - } else if (dt.types.indexOf('text/x-jop-folder-ids') >= 0) { - event.preventDefault(); - - const folderIds = JSON.parse(dt.getData('text/x-jop-folder-ids')); - for (let i = 0; i < folderIds.length; i++) { - await Folder.moveToFolder(folderIds[i], folderId); - } - } - }; - - this.onTagDrop_ = async event => { - const tagId = event.currentTarget.getAttribute('tagid'); - const dt = event.dataTransfer; - if (!dt) return; - - if (dt.types.indexOf('text/x-jop-note-ids') >= 0) { - event.preventDefault(); - - const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids')); - for (let i = 0; i < noteIds.length; i++) { - await Tag.addNote(tagId, noteIds[i]); - } - } - }; - - this.onFolderToggleClick_ = async event => { - const folderId = event.currentTarget.getAttribute('folderid'); - - this.props.dispatch({ - type: 'FOLDER_TOGGLE', - id: folderId, - }); - }; - - this.folderItemsOrder_ = []; - this.tagItemsOrder_ = []; - - this.onKeyDown = this.onKeyDown.bind(this); - this.onAllNotesClick_ = this.onAllNotesClick_.bind(this); - - this.rootRef = React.createRef(); - - this.anchorItemRefs = {}; - - this.state = { - tagHeaderIsExpanded: Setting.value('tagHeaderIsExpanded'), - folderHeaderIsExpanded: Setting.value('folderHeaderIsExpanded'), - }; - } - - style() { - const theme = themeStyle(this.props.theme); - - const itemHeight = 25; - - const style = { - root: { - backgroundColor: theme.backgroundColor2, - }, - listItemContainer: { - boxSizing: 'border-box', - height: itemHeight, - display: 'flex', - flexDirection: 'row', - }, - listItem: { - fontFamily: theme.fontFamily, - fontSize: theme.fontSize, - textDecoration: 'none', - color: theme.color2, - cursor: 'default', - opacity: 0.8, - whiteSpace: 'nowrap', - display: 'flex', - flex: 1, - alignItems: 'center', - userSelect: 'none', - }, - listItemSelected: { - backgroundColor: theme.selectedColor2, - }, - listItemExpandIcon: { - color: theme.color2, - cursor: 'default', - opacity: 0.8, - fontSize: theme.fontSize, - textDecoration: 'none', - paddingRight: 5, - display: 'flex', - alignItems: 'center', - width: 12, - }, - conflictFolder: { - color: theme.colorError2, - fontWeight: 'bold', - }, - header: { - height: itemHeight * 1.8, - fontFamily: theme.fontFamily, - fontSize: theme.fontSize * 1.16, - textDecoration: 'none', - boxSizing: 'border-box', - color: theme.color2, - paddingLeft: 8, - display: 'flex', - alignItems: 'center', - userSelect: 'none', - }, - button: { - padding: 6, - fontFamily: theme.fontFamily, - fontSize: theme.fontSize, - textDecoration: 'none', - boxSizing: 'border-box', - color: theme.color2, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - border: '1px solid rgba(255,255,255,0.2)', - marginTop: 10, - marginLeft: 5, - marginRight: 5, - cursor: 'default', - userSelect: 'none', - }, - syncReport: { - fontFamily: theme.fontFamily, - fontSize: Math.round(theme.fontSize * 0.9), - color: theme.color2, - opacity: 0.5, - display: 'flex', - alignItems: 'left', - justifyContent: 'top', - flexDirection: 'column', - marginTop: 10, - marginLeft: 5, - marginRight: 5, - marginBottom: 10, - wordWrap: 'break-word', - }, - noteCount: { - paddingLeft: 5, - opacity: 0.5, - userSelect: 'none', - }, - }; - - style.tagItem = Object.assign({}, style.listItem); - style.tagItem.paddingLeft = 23; - style.tagItem.height = itemHeight; - - return style; - } - - clearForceUpdateDuringSync() { - if (this.forceUpdateDuringSyncIID_) { - clearInterval(this.forceUpdateDuringSyncIID_); - this.forceUpdateDuringSyncIID_ = null; - } - } - - componentWillUnmount() { - this.clearForceUpdateDuringSync(); - - CommandService.instance().componentUnregisterCommands(commands); - } - - async itemContextMenu(event) { - const itemId = event.currentTarget.getAttribute('data-id'); - if (itemId === Folder.conflictFolderId()) return; - - const itemType = Number(event.currentTarget.getAttribute('data-type')); - if (!itemId || !itemType) throw new Error('No data on element'); - - let deleteMessage = ''; - let buttonLabel = _('Remove'); - if (itemType === BaseModel.TYPE_FOLDER) { - const folder = await Folder.load(itemId); - deleteMessage = _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32)); - buttonLabel = _('Delete'); - } else if (itemType === BaseModel.TYPE_TAG) { - const tag = await Tag.load(itemId); - deleteMessage = _('Remove tag "%s" from all notes?', substrWithEllipsis(tag.title, 0, 32)); - } else if (itemType === BaseModel.TYPE_SEARCH) { - deleteMessage = _('Remove this search from the sidebar?'); - } - - const menu = new Menu(); - - let item = null; - if (itemType === BaseModel.TYPE_FOLDER) { - item = BaseModel.byId(this.props.folders, itemId); - } - - if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { - menu.append( - new MenuItem(CommandService.instance().commandToMenuItem('newNotebook', { parentId: itemId })) - ); - } - - menu.append( - new MenuItem({ - label: buttonLabel, - click: async () => { - const ok = bridge().showConfirmMessageBox(deleteMessage, { - buttons: [buttonLabel, _('Cancel')], - defaultId: 1, - }); - if (!ok) return; - - if (itemType === BaseModel.TYPE_FOLDER) { - await Folder.delete(itemId); - } else if (itemType === BaseModel.TYPE_TAG) { - await Tag.untagAll(itemId); - } else if (itemType === BaseModel.TYPE_SEARCH) { - this.props.dispatch({ - type: 'SEARCH_DELETE', - id: itemId, - }); - } - }, - }) - ); - - if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { - menu.append(new MenuItem(CommandService.instance().commandToMenuItem('renameFolder', { folderId: itemId }))); - - menu.append(new MenuItem({ type: 'separator' })); - - const InteropService = require('lib/services/InteropService.js'); - - const exportMenu = new Menu(); - const ioService = new InteropService(); - const ioModules = ioService.modules(); - for (let i = 0; i < ioModules.length; i++) { - const module = ioModules[i]; - if (module.type !== 'exporter') continue; - - exportMenu.append( - new MenuItem({ - label: module.fullLabel(), - click: async () => { - await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId] }); - }, - }) - ); - } - - menu.append( - new MenuItem({ - label: _('Export'), - submenu: exportMenu, - }) - ); - } - - if (itemType === BaseModel.TYPE_TAG) { - menu.append(new MenuItem( - CommandService.instance().commandToMenuItem('renameTag', { tagId: itemId }) - )); - } - - menu.popup(bridge().window()); - } - - folderItem_click(folder) { - this.props.dispatch({ - type: 'FOLDER_SELECT', - id: folder ? folder.id : null, - }); - } - - tagItem_click(tag) { - this.props.dispatch({ - type: 'TAG_SELECT', - id: tag ? tag.id : null, - }); - } - - // async sync_click() { - // await shared.synchronize_press(this); - // } - - anchorItemRef(type, id) { - if (!this.anchorItemRefs[type]) this.anchorItemRefs[type] = {}; - if (this.anchorItemRefs[type][id]) return this.anchorItemRefs[type][id]; - this.anchorItemRefs[type][id] = React.createRef(); - return this.anchorItemRefs[type][id]; - } - - firstAnchorItemRef(type) { - const refs = this.anchorItemRefs[type]; - if (!refs) return null; - - const n = `${type}s`; - const item = this.props[n] && this.props[n].length ? this.props[n][0] : null; - console.info('props', this.props[n], item); - if (!item) return null; - - return refs[item.id]; - } - - noteCountElement(count) { - return
({count})
; - } - - folderItem(folder, selected, hasChildren, depth) { - let style = Object.assign({}, this.style().listItem); - if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder); - - const itemTitle = Folder.displayTitle(folder); - - let containerStyle = Object.assign({}, this.style().listItemContainer); - if (selected) containerStyle = Object.assign(containerStyle, this.style().listItemSelected); - - containerStyle.paddingLeft = 8 + depth * 15; - - const expandLinkStyle = Object.assign({}, this.style().listItemExpandIcon); - const expandIconStyle = { - visibility: hasChildren ? 'visible' : 'hidden', - }; - - const iconName = this.props.collapsedFolderIds.indexOf(folder.id) >= 0 ? 'fa-chevron-right' : 'fa-chevron-down'; - const expandIcon = ; - const expandLink = hasChildren ? ( - - {expandIcon} - - ) : ( - {expandIcon} - ); - - const anchorRef = this.anchorItemRef('folder', folder.id); - const noteCount = folder.note_count ? this.noteCountElement(folder.note_count) : ''; - - return ( - - ); - } - - tagItem(tag, selected) { - let style = Object.assign({}, this.style().tagItem); - if (selected) style = Object.assign(style, this.style().listItemSelected); - - const anchorRef = this.anchorItemRef('tag', tag.id); - const noteCount = Setting.value('showNoteCounts') ? this.noteCountElement(tag.note_count) : ''; - - return ( - this.itemContextMenu(event)} - tagid={tag.id} - key={tag.id} - style={style} - onDrop={this.onTagDrop_} - onClick={() => { - this.tagItem_click(tag); - }} - > - {Tag.displayTitle(tag)} {noteCount} - - ); - } - - // searchItem(search, selected) { - // let style = Object.assign({}, this.style().listItem); - // if (selected) style = Object.assign(style, this.style().listItemSelected); - // return ( - // this.itemContextMenu(event)} - // key={search.id} - // style={style} - // onClick={() => { - // this.searchItem_click(search); - // }} - // > - // {search.title} - // - // ); - // } - - makeDivider(key) { - return
; - } - - makeHeader(key, label, iconName, extraProps = {}) { - const style = this.style().header; - const icon = ; - - if (extraProps.toggleblock || extraProps.onClick) { - style.cursor = 'pointer'; - } - - const headerClick = extraProps.onClick || null; - delete extraProps.onClick; - - // check if toggling option is set. - let toggleIcon = null; - const toggleKey = `${key}IsExpanded`; - if (extraProps.toggleblock) { - const isExpanded = this.state[toggleKey]; - toggleIcon = ; - } - if (extraProps.selected) { - style.backgroundColor = this.style().listItemSelected.backgroundColor; - } - - const ref = this.anchorItemRef('headers', key); - - return ( -
{ - // if a custom click event is attached, trigger that. - if (headerClick) { - headerClick(key, event); - } - this.onHeaderClick_(key, event); - }} - > - {icon} - {label} - {toggleIcon} -
- ); - } - - selectedItem() { - if (this.props.notesParentType === 'Folder' && this.props.selectedFolderId) { - return { type: 'folder', id: this.props.selectedFolderId }; - } else if (this.props.notesParentType === 'Tag' && this.props.selectedTagId) { - return { type: 'tag', id: this.props.selectedTagId }; - } - - return null; - } - - onKeyDown(event) { - const keyCode = event.keyCode; - const selectedItem = this.selectedItem(); - - if (keyCode === 40 || keyCode === 38) { - // DOWN / UP - event.preventDefault(); - - const focusItems = []; - - for (let i = 0; i < this.folderItemsOrder_.length; i++) { - const id = this.folderItemsOrder_[i]; - focusItems.push({ id: id, ref: this.anchorItemRefs['folder'][id], type: 'folder' }); - } - - for (let i = 0; i < this.tagItemsOrder_.length; i++) { - const id = this.tagItemsOrder_[i]; - focusItems.push({ id: id, ref: this.anchorItemRefs['tag'][id], type: 'tag' }); - } - - let currentIndex = 0; - for (let i = 0; i < focusItems.length; i++) { - if (!selectedItem || focusItems[i].id === selectedItem.id) { - currentIndex = i; - break; - } - } - - const inc = keyCode === 38 ? -1 : +1; - let newIndex = currentIndex + inc; - - if (newIndex < 0) newIndex = 0; - if (newIndex > focusItems.length - 1) newIndex = focusItems.length - 1; - - const focusItem = focusItems[newIndex]; - - const actionName = `${focusItem.type.toUpperCase()}_SELECT`; - - this.props.dispatch({ - type: actionName, - id: focusItem.id, - }); - - focusItem.ref.current.focus(); - } - - if (keyCode === 9) { - // TAB - event.preventDefault(); - - if (event.shiftKey) { - CommandService.instance().execute('focusElement', { target: 'noteBody' }); - } else { - CommandService.instance().execute('focusElement', { target: 'noteList' }); - } - } - - if (selectedItem && selectedItem.type === 'folder' && keyCode === 32) { - // SPACE - event.preventDefault(); - - this.props.dispatch({ - type: 'FOLDER_TOGGLE', - id: selectedItem.id, - }); - } - - if (keyCode === 65 && (event.ctrlKey || event.metaKey)) { - // Ctrl+A key - event.preventDefault(); - } - } - - onHeaderClick_(key, event) { - const currentHeader = event.currentTarget; - const toggleBlock = +currentHeader.getAttribute('toggleblock'); - if (toggleBlock) { - const toggleKey = `${key}IsExpanded`; - const isExpanded = this.state[toggleKey]; - this.setState({ [toggleKey]: !isExpanded }); - Setting.setValue(toggleKey, !isExpanded); - } - } - - onAllNotesClick_() { - this.props.dispatch({ - type: 'SMART_FILTER_SELECT', - id: ALL_NOTES_FILTER_ID, - }); - } - - synchronizeButton(type) { - const style = Object.assign({}, this.style().button, { marginBottom: 5 }); - const iconName = 'fa-sync-alt'; - const label = type === 'sync' ? _('Synchronise') : _('Cancel'); - const iconStyle = { fontSize: style.fontSize, marginRight: 5 }; - - if (type !== 'sync') { - iconStyle.animation = 'icon-infinite-rotation 1s linear infinite'; - } - - const icon = ; - return ( - { - CommandService.instance().execute('synchronize'); - // this.sync_click(); - }} - > - {icon} - {label} - - ); - } - - render() { - const style = Object.assign({}, this.style().root, this.props.style, { - overflowX: 'hidden', - overflowY: 'hidden', - display: 'inline-flex', - flexDirection: 'column', - }); - - const items = []; - items.push( - this.makeHeader('allNotesHeader', _('All notes'), 'fa-clone', { - onClick: this.onAllNotesClick_, - selected: this.props.notesParentType === 'SmartFilter' && this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID, - }) - ); - - items.push( - this.makeHeader('folderHeader', _('Notebooks'), 'fa-book', { - onDrop: this.onFolderDrop_, - folderid: '', - toggleblock: 1, - }) - ); - - if (this.props.folders.length) { - const result = shared.renderFolders(this.props, this.folderItem.bind(this)); - const folderItems = result.items; - this.folderItemsOrder_ = result.order; - items.push( -
- {folderItems} -
- ); - } - - items.push( - this.makeHeader('tagHeader', _('Tags'), 'fa-tags', { - toggleblock: 1, - }) - ); - - if (this.props.tags.length) { - const result = shared.renderTags(this.props, this.tagItem.bind(this)); - const tagItems = result.items; - this.tagItemsOrder_ = result.order; - - items.push( -
- {tagItems} -
- ); - } - - let decryptionReportText = ''; - if (this.props.decryptionWorker && this.props.decryptionWorker.state !== 'idle' && this.props.decryptionWorker.itemCount) { - decryptionReportText = _('Decrypting items: %d/%d', this.props.decryptionWorker.itemIndex + 1, this.props.decryptionWorker.itemCount); - } - - let resourceFetcherText = ''; - if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) { - resourceFetcherText = _('Fetching resources: %d/%d', this.props.resourceFetcher.fetchingCount, this.props.resourceFetcher.toFetchCount); - } - - const lines = Synchronizer.reportToLines(this.props.syncReport); - if (resourceFetcherText) lines.push(resourceFetcherText); - if (decryptionReportText) lines.push(decryptionReportText); - const syncReportText = []; - for (let i = 0; i < lines.length; i++) { - syncReportText.push( -
- {lines[i]} -
- ); - } - - const syncButton = this.synchronizeButton(this.props.syncStarted ? 'cancel' : 'sync'); - - const syncReportComp = !syncReportText.length ? null : ( -
- {syncReportText} -
- ); - - return ( -
-
{items}
-
- {syncReportComp} - {syncButton} -
-
- ); - } -} - -const mapStateToProps = state => { - return { - folders: state.folders, - tags: state.tags, - searches: state.searches, - syncStarted: state.syncStarted, - syncReport: state.syncReport, - selectedFolderId: state.selectedFolderId, - selectedTagId: state.selectedTagId, - selectedSearchId: state.selectedSearchId, - selectedSmartFilterId: state.selectedSmartFilterId, - notesParentType: state.notesParentType, - locale: state.settings.locale, - theme: state.settings.theme, - collapsedFolderIds: state.collapsedFolderIds, - decryptionWorker: state.decryptionWorker, - resourceFetcher: state.resourceFetcher, - sidebarVisibility: state.sidebarVisibility, - noteListVisibility: state.noteListVisibility, - }; -}; - -const SideBar = connect(mapStateToProps)(SideBarComponent); - -module.exports = { SideBar }; diff --git a/ElectronClient/gui/SideBar/SideBar.tsx b/ElectronClient/gui/SideBar/SideBar.tsx new file mode 100644 index 0000000000..d36ac59e69 --- /dev/null +++ b/ElectronClient/gui/SideBar/SideBar.tsx @@ -0,0 +1,646 @@ +import * as React from 'react'; +import { StyledRoot, StyledAddButton, StyledHeader, StyledHeaderIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles'; +import { ButtonLevel } from '../Button/Button'; +import CommandService from 'lib/services/CommandService'; + +const { connect } = require('react-redux'); +const shared = require('lib/components/shared/side-menu-shared.js'); +const { Synchronizer } = require('lib/synchronizer.js'); +const BaseModel = require('lib/BaseModel.js'); +const Setting = require('lib/models/Setting.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const Tag = require('lib/models/Tag.js'); +const { _ } = require('lib/locale.js'); +const { themeStyle } = require('lib/theme'); +const { bridge } = require('electron').remote.require('./bridge'); +const Menu = bridge().Menu; +const MenuItem = bridge().MenuItem; +const InteropServiceHelper = require('../../InteropServiceHelper.js'); +const { substrWithEllipsis } = require('lib/string-utils'); +const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids'); + +interface Props { + themeId: number, + dispatch: Function, + folders: any[], + collapsedFolderIds: string[], + notesParentType: string, + selectedFolderId: string, + selectedTagId: string, + selectedSmartFilterId:string, + decryptionWorker: any, + resourceFetcher: any, + syncReport: any, + tags: any[], + syncStarted: boolean, +} + +interface State { + tagHeaderIsExpanded: boolean, + folderHeaderIsExpanded: boolean, +} + +const commands = [ + require('./commands/focusElementSideBar'), +]; + +class SideBarComponent extends React.Component { + + private folderItemsOrder_:any[] = []; + private tagItemsOrder_:any[] = []; + private rootRef:any = null; + private anchorItemRefs:any = {}; + private forceUpdateDuringSyncIID_:any = null; + + constructor(props:any) { + super(props); + + CommandService.instance().componentRegisterCommands(this, commands); + + this.state = { + tagHeaderIsExpanded: Setting.value('tagHeaderIsExpanded'), + folderHeaderIsExpanded: Setting.value('folderHeaderIsExpanded'), + }; + + this.onFolderToggleClick_ = this.onFolderToggleClick_.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onAllNotesClick_ = this.onAllNotesClick_.bind(this); + this.header_contextMenu = this.header_contextMenu.bind(this); + this.onAddFolderButtonClick = this.onAddFolderButtonClick.bind(this); + } + + onFolderDragStart_(event:any) { + const folderId = event.currentTarget.getAttribute('data-folder-id'); + if (!folderId) return; + + event.dataTransfer.setDragImage(new Image(), 1, 1); + event.dataTransfer.clearData(); + event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId])); + } + + onFolderDragOver_(event:any) { + if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault(); + if (event.dataTransfer.types.indexOf('text/x-jop-folder-ids') >= 0) event.preventDefault(); + } + + async onFolderDrop_(event:any) { + const folderId = event.currentTarget.getAttribute('data-folder-id'); + const dt = event.dataTransfer; + if (!dt) return; + + // folderId can be NULL when dropping on the sidebar Notebook header. In that case, it's used + // to put the dropped folder at the root. But for notes, folderId needs to always be defined + // since there's no such thing as a root note. + + if (dt.types.indexOf('text/x-jop-note-ids') >= 0) { + event.preventDefault(); + + if (!folderId) return; + + const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids')); + for (let i = 0; i < noteIds.length; i++) { + await Note.moveToFolder(noteIds[i], folderId); + } + } else if (dt.types.indexOf('text/x-jop-folder-ids') >= 0) { + event.preventDefault(); + + const folderIds = JSON.parse(dt.getData('text/x-jop-folder-ids')); + for (let i = 0; i < folderIds.length; i++) { + await Folder.moveToFolder(folderIds[i], folderId); + } + } + } + + async onTagDrop_(event:any) { + const tagId = event.currentTarget.getAttribute('data-tag-id'); + const dt = event.dataTransfer; + if (!dt) return; + + if (dt.types.indexOf('text/x-jop-note-ids') >= 0) { + event.preventDefault(); + + const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids')); + for (let i = 0; i < noteIds.length; i++) { + await Tag.addNote(tagId, noteIds[i]); + } + } + } + + async onFolderToggleClick_(event:any) { + const folderId = event.currentTarget.getAttribute('data-folder-id'); + + this.props.dispatch({ + type: 'FOLDER_TOGGLE', + id: folderId, + }); + } + + clearForceUpdateDuringSync() { + if (this.forceUpdateDuringSyncIID_) { + clearInterval(this.forceUpdateDuringSyncIID_); + this.forceUpdateDuringSyncIID_ = null; + } + } + + componentWillUnmount() { + this.clearForceUpdateDuringSync(); + + CommandService.instance().componentUnregisterCommands(commands); + } + + async header_contextMenu() { + const menu = new Menu(); + + menu.append( + new MenuItem(CommandService.instance().commandToMenuItem('newFolder')) + ); + + menu.popup(bridge().window()); + } + + async itemContextMenu(event:any) { + const itemId = event.currentTarget.getAttribute('data-id'); + if (itemId === Folder.conflictFolderId()) return; + + const itemType = Number(event.currentTarget.getAttribute('data-type')); + if (!itemId || !itemType) throw new Error('No data on element'); + + let deleteMessage = ''; + let buttonLabel = _('Remove'); + if (itemType === BaseModel.TYPE_FOLDER) { + const folder = await Folder.load(itemId); + deleteMessage = _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32)); + buttonLabel = _('Delete'); + } else if (itemType === BaseModel.TYPE_TAG) { + const tag = await Tag.load(itemId); + deleteMessage = _('Remove tag "%s" from all notes?', substrWithEllipsis(tag.title, 0, 32)); + } else if (itemType === BaseModel.TYPE_SEARCH) { + deleteMessage = _('Remove this search from the sidebar?'); + } + + const menu = new Menu(); + + let item = null; + if (itemType === BaseModel.TYPE_FOLDER) { + item = BaseModel.byId(this.props.folders, itemId); + } + + if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { + menu.append( + new MenuItem(CommandService.instance().commandToMenuItem('newFolder', { parentId: itemId })) + ); + } + + menu.append( + new MenuItem({ + label: buttonLabel, + click: async () => { + const ok = bridge().showConfirmMessageBox(deleteMessage, { + buttons: [buttonLabel, _('Cancel')], + defaultId: 1, + }); + if (!ok) return; + + if (itemType === BaseModel.TYPE_FOLDER) { + await Folder.delete(itemId); + } else if (itemType === BaseModel.TYPE_TAG) { + await Tag.untagAll(itemId); + } else if (itemType === BaseModel.TYPE_SEARCH) { + this.props.dispatch({ + type: 'SEARCH_DELETE', + id: itemId, + }); + } + }, + }) + ); + + if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { + menu.append(new MenuItem(CommandService.instance().commandToMenuItem('renameFolder', { folderId: itemId }))); + + menu.append(new MenuItem({ type: 'separator' })); + + const InteropService = require('lib/services/InteropService.js'); + + const exportMenu = new Menu(); + const ioService = new InteropService(); + const ioModules = ioService.modules(); + for (let i = 0; i < ioModules.length; i++) { + const module = ioModules[i]; + if (module.type !== 'exporter') continue; + + exportMenu.append( + new MenuItem({ + label: module.fullLabel(), + click: async () => { + await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId] }); + }, + }) + ); + } + + menu.append( + new MenuItem({ + label: _('Export'), + submenu: exportMenu, + }) + ); + } + + if (itemType === BaseModel.TYPE_TAG) { + menu.append(new MenuItem( + CommandService.instance().commandToMenuItem('renameTag', { tagId: itemId }) + )); + } + + menu.popup(bridge().window()); + } + + folderItem_click(folder:any) { + this.props.dispatch({ + type: 'FOLDER_SELECT', + id: folder ? folder.id : null, + }); + } + + tagItem_click(tag:any) { + this.props.dispatch({ + type: 'TAG_SELECT', + id: tag ? tag.id : null, + }); + } + + anchorItemRef(type:string, id:string) { + if (!this.anchorItemRefs[type]) this.anchorItemRefs[type] = {}; + if (this.anchorItemRefs[type][id]) return this.anchorItemRefs[type][id]; + this.anchorItemRefs[type][id] = React.createRef(); + return this.anchorItemRefs[type][id]; + } + + firstAnchorItemRef(type:string) { + const refs = this.anchorItemRefs[type]; + if (!refs) return null; + + const n = `${type}s`; + const p = this.props as any; + const item = p[n] && p[n].length ? p[n][0] : null; + if (!item) return null; + + return refs[item.id]; + } + + renderNoteCount(count:number) { + return {count}; + } + + renderExpandIcon(isExpanded:boolean, isVisible:boolean = true) { + const theme = themeStyle(this.props.themeId); + const style:any = { width: 16, maxWidth: 16, opacity: 0.5, fontSize: Math.round(theme.toolbarIconSize * 0.8), display: 'flex', justifyContent: 'center' }; + if (!isVisible) style.visibility = 'hidden'; + return ; + } + + renderAllNotesItem(selected:boolean) { + return ( + + {this.renderExpandIcon(false, false)} + { + this.onAllNotesClick_(); + }} + > + ({_('All notes')}) + + + ); + } + + renderFolderItem(folder:any, selected:boolean, hasChildren:boolean, depth:number) { + const isExpanded = this.props.collapsedFolderIds.indexOf(folder.id) < 0; + const expandIcon = this.renderExpandIcon(isExpanded, hasChildren); + const expandLink = hasChildren ? ( + + {expandIcon} + + ) : ( + {expandIcon} + ); + + const anchorRef = this.anchorItemRef('folder', folder.id); + const noteCount = folder.note_count ? this.renderNoteCount(folder.note_count) : ''; + + return ( + + {expandLink} + this.itemContextMenu(event)} + data-folder-id={folder.id} + onClick={() => { + this.folderItem_click(folder); + }} + onDoubleClick={this.onFolderToggleClick_} + > + {Folder.displayTitle(folder)} {noteCount} + + + ); + } + + renderTag(tag:any, selected:boolean) { + const anchorRef = this.anchorItemRef('tag', tag.id); + const noteCount = Setting.value('showNoteCounts') ? this.renderNoteCount(tag.note_count) : ''; + + return ( + + {this.renderExpandIcon(false, false)} + this.itemContextMenu(event)} + onClick={() => { + this.tagItem_click(tag); + }} + > + {Tag.displayTitle(tag)} {noteCount} + + + ); + } + + makeDivider(key:string) { + return
; + } + + renderHeader(key:string, label:string, iconName:string, contextMenuHandler:Function = null, onPlusButtonClick:Function = null, extraProps:any = {}) { + const headerClick = extraProps.onClick || null; + delete extraProps.onClick; + const ref = this.anchorItemRef('headers', key); + + return ( +
+ { + // if a custom click event is attached, trigger that. + if (headerClick) { + headerClick(key, event); + } + this.onHeaderClick_(key); + }} + > + + {label} + + { onPlusButtonClick && } +
+ ); + } + + selectedItem() { + if (this.props.notesParentType === 'Folder' && this.props.selectedFolderId) { + return { type: 'folder', id: this.props.selectedFolderId }; + } else if (this.props.notesParentType === 'Tag' && this.props.selectedTagId) { + return { type: 'tag', id: this.props.selectedTagId }; + } + + return null; + } + + onKeyDown(event:any) { + const keyCode = event.keyCode; + const selectedItem = this.selectedItem(); + + if (keyCode === 40 || keyCode === 38) { + // DOWN / UP + event.preventDefault(); + + const focusItems = []; + + for (let i = 0; i < this.folderItemsOrder_.length; i++) { + const id = this.folderItemsOrder_[i]; + focusItems.push({ id: id, ref: this.anchorItemRefs['folder'][id], type: 'folder' }); + } + + for (let i = 0; i < this.tagItemsOrder_.length; i++) { + const id = this.tagItemsOrder_[i]; + focusItems.push({ id: id, ref: this.anchorItemRefs['tag'][id], type: 'tag' }); + } + + let currentIndex = 0; + for (let i = 0; i < focusItems.length; i++) { + if (!selectedItem || focusItems[i].id === selectedItem.id) { + currentIndex = i; + break; + } + } + + const inc = keyCode === 38 ? -1 : +1; + let newIndex = currentIndex + inc; + + if (newIndex < 0) newIndex = 0; + if (newIndex > focusItems.length - 1) newIndex = focusItems.length - 1; + + const focusItem = focusItems[newIndex]; + + const actionName = `${focusItem.type.toUpperCase()}_SELECT`; + + this.props.dispatch({ + type: actionName, + id: focusItem.id, + }); + + focusItem.ref.current.focus(); + } + + if (keyCode === 9) { + // TAB + event.preventDefault(); + + if (event.shiftKey) { + CommandService.instance().execute('focusElement', { target: 'noteBody' }); + } else { + CommandService.instance().execute('focusElement', { target: 'noteList' }); + } + } + + if (selectedItem && selectedItem.type === 'folder' && keyCode === 32) { + // SPACE + event.preventDefault(); + + this.props.dispatch({ + type: 'FOLDER_TOGGLE', + id: selectedItem.id, + }); + } + + if (keyCode === 65 && (event.ctrlKey || event.metaKey)) { + // Ctrl+A key + event.preventDefault(); + } + } + + onHeaderClick_(key:string) { + const toggleKey = `${key}IsExpanded`; + const isExpanded = (this.state as any)[toggleKey]; + const newState:any = { [toggleKey]: !isExpanded }; + this.setState(newState); + Setting.setValue(toggleKey, !isExpanded); + } + + onAllNotesClick_() { + this.props.dispatch({ + type: 'SMART_FILTER_SELECT', + id: ALL_NOTES_FILTER_ID, + }); + } + + renderSynchronizeButton(type:string) { + const label = type === 'sync' ? _('Synchronise') : _('Cancel'); + const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : ''; + + return ( + { + CommandService.instance().execute('synchronize', { syncStarted: type !== 'sync' }); + }} + /> + ); + } + + onAddFolderButtonClick() { + CommandService.instance().execute('newFolder'); + } + + render() { + const theme = themeStyle(this.props.themeId); + + const items = []; + + items.push( + this.renderHeader('folderHeader', _('Notebooks'), 'icon-notebooks', this.header_contextMenu, this.onAddFolderButtonClick, { + onDrop: this.onFolderDrop_, + ['data-folder-id']: '', + toggleblock: 1, + }) + ); + + if (this.props.folders.length) { + const allNotesSelected = this.props.notesParentType === 'SmartFilter' && this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID; + const result = shared.renderFolders(this.props, this.renderFolderItem.bind(this)); + const folderItems = [this.renderAllNotesItem(allNotesSelected)].concat(result.items); + this.folderItemsOrder_ = result.order; + items.push( +
+ {folderItems} +
+ ); + } + + items.push( + this.renderHeader('tagHeader', _('Tags'), 'icon-tags', null, null, { + toggleblock: 1, + }) + ); + + if (this.props.tags.length) { + const result = shared.renderTags(this.props, this.renderTag.bind(this)); + const tagItems = result.items; + this.tagItemsOrder_ = result.order; + + items.push( +
+ {tagItems} +
+ ); + } + + let decryptionReportText = ''; + if (this.props.decryptionWorker && this.props.decryptionWorker.state !== 'idle' && this.props.decryptionWorker.itemCount) { + decryptionReportText = _('Decrypting items: %d/%d', this.props.decryptionWorker.itemIndex + 1, this.props.decryptionWorker.itemCount); + } + + let resourceFetcherText = ''; + if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) { + resourceFetcherText = _('Fetching resources: %d/%d', this.props.resourceFetcher.fetchingCount, this.props.resourceFetcher.toFetchCount); + } + + const lines = Synchronizer.reportToLines(this.props.syncReport); + if (resourceFetcherText) lines.push(resourceFetcherText); + if (decryptionReportText) lines.push(decryptionReportText); + const syncReportText = []; + for (let i = 0; i < lines.length; i++) { + syncReportText.push( + + {lines[i]} + + ); + } + + const syncButton = this.renderSynchronizeButton(this.props.syncStarted ? 'cancel' : 'sync'); + + const syncReportComp = !syncReportText.length ? null : ( + + {syncReportText} + + ); + + return ( + +
{items}
+
+ {syncReportComp} + {syncButton} +
+
+ ); + } +} + +const mapStateToProps = (state:any) => { + return { + folders: state.folders, + tags: state.tags, + searches: state.searches, + syncStarted: state.syncStarted, + syncReport: state.syncReport, + selectedFolderId: state.selectedFolderId, + selectedTagId: state.selectedTagId, + selectedSearchId: state.selectedSearchId, + selectedSmartFilterId: state.selectedSmartFilterId, + notesParentType: state.notesParentType, + locale: state.settings.locale, + themeId: state.settings.theme, + collapsedFolderIds: state.collapsedFolderIds, + decryptionWorker: state.decryptionWorker, + resourceFetcher: state.resourceFetcher, + sidebarVisibility: state.sidebarVisibility, + noteListVisibility: state.noteListVisibility, + }; +}; + +const SideBar = connect(mapStateToProps)(SideBarComponent); + +module.exports = { SideBar }; diff --git a/ElectronClient/gui/SideBar/styles/index.ts b/ElectronClient/gui/SideBar/styles/index.ts new file mode 100644 index 0000000000..59395121df --- /dev/null +++ b/ElectronClient/gui/SideBar/styles/index.ts @@ -0,0 +1,122 @@ +import Button from '../../Button/Button'; +const styled = require('styled-components').default; + +export const StyledRoot = styled.div` + background-color: ${(props:any) => props.theme.backgroundColor2}; + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + display: inline-flex; + flex-direction: column; +`; + +export const StyledHeader = styled.div` + //height: ${(props:any) => props.theme.topRowHeight}px; + //text-decoration: none; + flex: 1; + box-sizing: border-box; + padding: ${(props:any) => props.theme.mainPadding}px; + padding-bottom: ${(props:any) => props.theme.mainPadding / 2}px; + display: flex; + align-items: center; + user-select: none; + text-transform: uppercase; + //cursor: pointer; +`; + +export const StyledHeaderIcon = styled.i` + font-size: ${(props:any) => props.theme.toolbarIconSize}px; + color: ${(props:any) => props.theme.color2}; + margin-right: 8px; +`; + +export const StyledHeaderLabel = styled.span` + flex: 1; + color: ${(props:any) => props.theme.color2}; + font-size: ${(props:any) => Math.round(props.theme.fontSize * 1.1)}px; + font-weight: bold; +`; + +export const StyledListItem = styled.div` + box-sizing: border-box; + height: 25px; + display: flex; + flex-direction: row; + padding-left: ${(props:any) => props.theme.mainPadding + ('depth' in props ? props.depth : 0) * 16}px; + background: ${(props:any) => props.selected ? props.theme.selectedColor2 : 'none'}; + text-transform: ${(props:any) => props.isSpecialItem ? 'uppercase' : 'none'}; + transition: 0.1s; + + &:hover { + background-color: ${(props:any) => props.theme.backgroundColorHover2}; + } +`; + +function listItemTextColor(props:any) { + if (props.isConflictFolder) return props.theme.colorError2; + if (props.isSpecialItem) return props.theme.colorFaded2; + return props.theme.color2; +} + +export const StyledListItemAnchor = styled.a` + font-size: ${(props:any) => Math.round(props.theme.fontSize * 1.0833333)}px; + font-weight: 500; + text-decoration: none; + color: ${(props:any) => listItemTextColor(props)}; + cursor: default; + opacity: ${(props:any) => props.selected ? 1 : 0.8}; + white-space: nowrap; + display: flex; + flex: 1; + align-items: center; + user-select: none; +`; + +export const StyledExpandLink = styled.a` + color: ${(props:any) => props.theme.color2}; + cursor: default; + opacity: 0.8; + text-decoration: none; + padding-right: 8px; + display: flex; + align-items: center; + width: 16px; + max-width: 16px; + min-width: 16px; +`; + +export const StyledNoteCount = styled.div` + color: ${(props:any) => props.theme.color2}; + padding-left: 8px; + opacity: 0.5; + user-select: none; +`; + +export const StyledSynchronizeButton = styled(Button)` + width: 100%; +`; + +export const StyledAddButton = styled(Button)` + border: none; + padding-right: 15px; + padding-top: 4px; +`; + +export const StyledSyncReport = styled.div` + font-size: ${(props:any) => Math.round(props.theme.fontSize * 0.9)}px; + color: ${(props:any) => props.theme.color2}; + opacity: 0.5; + display: flex; + flex-direction: column; + margin-left: 5px; + margin-right: 5px; + margin-bottom: 10px; + word-wrap: break-word; +`; + +export const StyledSyncReportText = styled.div` + color: ${(props:any) => props.theme.color2}; + word-wrap: break-word; + width: 100%; +`; diff --git a/ElectronClient/gui/StatusScreen.jsx b/ElectronClient/gui/StatusScreen.jsx deleted file mode 100644 index 8f568b3c88..0000000000 --- a/ElectronClient/gui/StatusScreen.jsx +++ /dev/null @@ -1,159 +0,0 @@ -const React = require('react'); -const { connect } = require('react-redux'); -const Setting = require('lib/models/Setting.js'); -const { bridge } = require('electron').remote.require('./bridge'); -const { Header } = require('./Header/Header.min.js'); -const { themeStyle } = require('lib/theme'); -const { _ } = require('lib/locale.js'); -const { ReportService } = require('lib/services/report.js'); -const fs = require('fs-extra'); - -class StatusScreenComponent extends React.Component { - constructor() { - super(); - this.state = { - report: [], - }; - } - - UNSAFE_componentWillMount() { - this.resfreshScreen(); - } - - async resfreshScreen() { - const service = new ReportService(); - const report = await service.status(Setting.value('sync.target')); - this.setState({ report: report }); - } - - async exportDebugReportClick() { - const filename = `syncReport-${new Date().getTime()}.csv`; - - const filePath = bridge().showSaveDialog({ - title: _('Please select where the sync status should be exported to'), - defaultPath: filename, - }); - - if (!filePath) return; - - const service = new ReportService(); - const csv = await service.basicItemList({ format: 'csv' }); - await fs.writeFileSync(filePath, csv); - } - - render() { - const theme = themeStyle(this.props.theme); - const style = this.props.style; - - const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width }); - const retryStyle = Object.assign({}, theme.urlStyle, { marginLeft: 5 }); - const retryAllStyle = Object.assign({}, theme.urlStyle, { marginTop: 5, display: 'inline-block' }); - - const containerPadding = 10; - - const containerStyle = Object.assign({}, theme.containerStyle, { - padding: containerPadding, - height: style.height - theme.headerHeight - containerPadding * 2, - }); - - function renderSectionTitleHtml(key, title) { - return ( -

- {title} -

- ); - } - - function renderSectionRetryAllHtml(key, retryAllHandler) { - return ( - - {_('Retry All')} - - ); - } - - const renderSectionHtml = (key, section) => { - const itemsHtml = []; - - itemsHtml.push(renderSectionTitleHtml(section.title, section.title)); - - for (const n in section.body) { - if (!section.body.hasOwnProperty(n)) continue; - const item = section.body[n]; - let text = ''; - - let retryLink = null; - if (typeof item === 'object') { - if (item.canRetry) { - const onClick = async () => { - await item.retryHandler(); - this.resfreshScreen(); - }; - - retryLink = ( - - {_('Retry')} - - ); - } - text = item.text; - } else { - text = item; - } - - if (!text) text = '\xa0'; - - itemsHtml.push( -
- {text} - {retryLink} -
- ); - } - - if (section.canRetryAll) { - itemsHtml.push(renderSectionRetryAllHtml(section.title, section.retryAllHandler)); - } - - return
{itemsHtml}
; - }; - - function renderBodyHtml(report) { - const sectionsHtml = []; - - for (let i = 0; i < report.length; i++) { - const section = report[i]; - if (!section.body.length) continue; - sectionsHtml.push(renderSectionHtml(i, section)); - } - - return
{sectionsHtml}
; - } - - const body = renderBodyHtml(this.state.report); - - return ( - - ); - } -} - -const mapStateToProps = state => { - return { - theme: state.settings.theme, - settings: state.settings, - locale: state.settings.locale, - }; -}; - -const StatusScreen = connect(mapStateToProps)(StatusScreenComponent); - -module.exports = { StatusScreen }; diff --git a/ElectronClient/gui/StatusScreen/StatusScreen.tsx b/ElectronClient/gui/StatusScreen/StatusScreen.tsx new file mode 100644 index 0000000000..d892aee779 --- /dev/null +++ b/ElectronClient/gui/StatusScreen/StatusScreen.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import ButtonBar from '../ConfigScreen/ButtonBar'; + +const { connect } = require('react-redux'); +const Setting = require('lib/models/Setting.js'); +const { bridge } = require('electron').remote.require('./bridge'); +const { themeStyle } = require('lib/theme'); +const { _ } = require('lib/locale.js'); +const { ReportService } = require('lib/services/report.js'); +const fs = require('fs-extra'); + +interface Props { + themeId: string, + style: any, + dispatch: Function, +} + +async function exportDebugReportClick() { + const filename = `syncReport-${new Date().getTime()}.csv`; + + const filePath = bridge().showSaveDialog({ + title: _('Please select where the sync status should be exported to'), + defaultPath: filename, + }); + + if (!filePath) return; + + const service = new ReportService(); + const csv = await service.basicItemList({ format: 'csv' }); + await fs.writeFileSync(filePath, csv); +} + +function StatusScreen(props:Props) { + const [report, setReport] = useState([]); + + async function resfreshScreen() { + const service = new ReportService(); + const r = await service.status(Setting.value('sync.target')); + setReport(r); + } + + useEffect(() => { + resfreshScreen(); + }, []); + + const theme = themeStyle(props.themeId); + const style = { ...props.style, + display: 'flex', + flexDirection: 'column', + }; + + const retryStyle = Object.assign({}, theme.urlStyle, { marginLeft: 5 }); + const retryAllStyle = Object.assign({}, theme.urlStyle, { marginTop: 5, display: 'inline-block' }); + + const containerPadding = theme.configScreenPadding; + + const containerStyle = Object.assign({}, theme.containerStyle, { + padding: containerPadding, + flex: 1, + }); + + function renderSectionTitleHtml(key:string, title:string) { + return ( +

+ {title} +

+ ); + } + + function renderSectionRetryAllHtml(key:string, retryAllHandler:any) { + return ( + + {_('Retry All')} + + ); + } + + const renderSectionHtml = (key:string, section:any) => { + const itemsHtml = []; + + itemsHtml.push(renderSectionTitleHtml(section.title, section.title)); + + for (const n in section.body) { + if (!section.body.hasOwnProperty(n)) continue; + const item = section.body[n]; + let text = ''; + + let retryLink = null; + if (typeof item === 'object') { + if (item.canRetry) { + const onClick = async () => { + await item.retryHandler(); + resfreshScreen(); + }; + + retryLink = ( + + {_('Retry')} + + ); + } + text = item.text; + } else { + text = item; + } + + if (!text) text = '\xa0'; + + itemsHtml.push( +
+ {text} + {retryLink} +
+ ); + } + + if (section.canRetryAll) { + itemsHtml.push(renderSectionRetryAllHtml(section.title, section.retryAllHandler)); + } + + return
{itemsHtml}
; + }; + + function renderBodyHtml(report:any) { + const sectionsHtml = []; + + for (let i = 0; i < report.length; i++) { + const section = report[i]; + if (!section.body.length) continue; + sectionsHtml.push(renderSectionHtml(`${i}`, section)); + } + + return
{sectionsHtml}
; + } + + const body = renderBodyHtml(report); + + return ( +
+ + props.dispatch({ type: 'NAV_BACK' })} + /> +
+ ); +} + +const mapStateToProps = (state:any) => { + return { + themeId: state.settings.theme, + settings: state.settings, + locale: state.settings.locale, + }; +}; + +export default connect(mapStateToProps)(StatusScreen); + diff --git a/ElectronClient/gui/TagItem.jsx b/ElectronClient/gui/TagItem.jsx index 726ea6376a..ce3ee196e2 100644 --- a/ElectronClient/gui/TagItem.jsx +++ b/ElectronClient/gui/TagItem.jsx @@ -4,7 +4,7 @@ const { themeStyle } = require('lib/theme'); class TagItemComponent extends React.Component { render() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const style = Object.assign({}, theme.tagStyle); const title = this.props.title; @@ -13,7 +13,7 @@ class TagItemComponent extends React.Component { } const mapStateToProps = state => { - return { theme: state.settings.theme }; + return { themeId: state.settings.theme }; }; const TagItem = connect(mapStateToProps)(TagItemComponent); diff --git a/ElectronClient/gui/TagList.jsx b/ElectronClient/gui/TagList.jsx index 2997ada860..ff87b9a38a 100644 --- a/ElectronClient/gui/TagList.jsx +++ b/ElectronClient/gui/TagList.jsx @@ -6,7 +6,7 @@ const TagItem = require('./TagItem.min.js'); class TagListComponent extends React.Component { render() { const style = Object.assign({}, this.props.style); - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const tags = this.props.items; style.display = 'flex'; @@ -15,11 +15,13 @@ class TagListComponent extends React.Component { style.boxSizing = 'border-box'; style.fontSize = theme.fontSize; style.whiteSpace = 'nowrap'; - style.height = 25; + // style.height = 40; + style.paddingTop = 8; + style.paddingBottom = 8; const tagItems = []; if (tags && tags.length > 0) { - // Sort by id for now, but probably needs to be changed in the future. + tags.sort((a, b) => { return a.title < b.title ? -1 : +1; }); @@ -42,7 +44,7 @@ class TagListComponent extends React.Component { } const mapStateToProps = state => { - return { theme: state.settings.theme }; + return { themeId: state.settings.theme }; }; const TagList = connect(mapStateToProps)(TagListComponent); diff --git a/ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.tsx b/ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.tsx new file mode 100644 index 0000000000..5b6368a718 --- /dev/null +++ b/ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import styles_ from './styles'; +import { ToolbarButtonInfo } from 'lib/services/CommandService'; + +export enum Value { + Markdown = 'markdown', + RichText = 'richText', +} + +export interface Props { + themeId: number, + value: Value, + toolbarButtonInfo: ToolbarButtonInfo, +} + +export default function ToggleEditorsButton(props:Props) { + const style = styles_(props); + + return ( + + ); +} diff --git a/ElectronClient/gui/ToggleEditorsButton/styles/index.ts b/ElectronClient/gui/ToggleEditorsButton/styles/index.ts new file mode 100644 index 0000000000..d8c12d0c0f --- /dev/null +++ b/ElectronClient/gui/ToggleEditorsButton/styles/index.ts @@ -0,0 +1,68 @@ +import { Props, Value } from '../ToggleEditorsButton'; +const { buildStyle } = require('lib/theme'); + +export default function styles(props:Props) { + return buildStyle(['ToggleEditorsButton', props.value], props.themeId, (theme: any) => { + const iconSize = 15; + const mdIconWidth = iconSize * 1.25; + const buttonHeight = theme.toolbarHeight - 8; + const mdIconPadding = Math.round((buttonHeight - iconSize) / 2) + 3; + + const innerButton:any = { + borderStyle: 'solid', + borderColor: theme.color3, + borderWidth: 1, + borderRadius: 0, + width: mdIconWidth + mdIconPadding * 2, + height: buttonHeight, + display: 'flex', + justifyContent: 'center', + }; + + const output:any = { + button: { + border: 'none', + padding: 0, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + background: 'none', + }, + leftInnerButton: { + ...innerButton, + borderTopLeftRadius: 4, + borderBottomLeftRadius: 4, + }, + rightInnerButton: { + ...innerButton, + borderTopRightRadius: 4, + borderBottomRightRadius: 4, + }, + leftIcon: { + fontSize: iconSize, + position: 'relative', + top: 1, + color: theme.color3, + }, + rightIcon: { + fontSize: iconSize - 1, + borderLeft: 'none', + position: 'relative', + top: 1, + color: theme.color3, + }, + }; + + if (props.value === Value.Markdown) { + output.leftInnerButton.backgroundColor = theme.color3; + output.leftIcon.color = theme.backgroundColor3; + output.rightInnerButton.opacity = 0.5; + } else if (props.value === Value.RichText) { + output.rightInnerButton.backgroundColor = theme.color3; + output.rightIcon.color = theme.backgroundColor3; + output.leftInnerButton.opacity = 0.5; + } + + return output; + }); +} diff --git a/ElectronClient/gui/Toolbar.jsx b/ElectronClient/gui/Toolbar.jsx index e593104dc6..6e77778e9b 100644 --- a/ElectronClient/gui/Toolbar.jsx +++ b/ElectronClient/gui/Toolbar.jsx @@ -1,22 +1,32 @@ const React = require('react'); const { connect } = require('react-redux'); const { themeStyle } = require('lib/theme'); -const ToolbarButton = require('./ToolbarButton.min.js'); +const ToolbarButton = require('./ToolbarButton/ToolbarButton.js').default; const ToolbarSpace = require('./ToolbarSpace.min.js'); +const ToggleEditorsButton = require('./ToggleEditorsButton/ToggleEditorsButton.js').default; class ToolbarComponent extends React.Component { render() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const style = Object.assign({ - // height: theme.toolbarHeight, display: 'flex', flexDirection: 'row', - borderBottom: `1px solid ${theme.dividerColor}`, boxSizing: 'border-box', + backgroundColor: theme.backgroundColor3, + padding: theme.toolbarPadding, + paddingRight: theme.mainPadding, }, this.props.style); - const itemComps = []; + const groupStyle = { + display: 'flex', + flexDirection: 'row', + boxSizing: 'border-box', + }; + + const leftItemComps = []; + const centerItemComps = []; + const rightItemComps = []; if (this.props.items) { for (let i = 0; i < this.props.items.length; i++) { @@ -30,31 +40,47 @@ class ToolbarComponent extends React.Component { const props = Object.assign( { key: key, - theme: this.props.theme, + themeId: this.props.themeId, }, o ); if (this.props.disabled) props.disabled = true; - if (itemType === 'button') { - itemComps.push(); + if (o.name === 'toggleEditors') { + rightItemComps.push(); + } else if (itemType === 'button') { + const target = ['historyForward', 'historyBackward', 'startExternalEditing'].includes(o.name) ? leftItemComps : centerItemComps; + target.push(); } else if (itemType === 'separator') { - itemComps.push(); + centerItemComps.push(); } } } return (
- {itemComps} +
+ {leftItemComps} +
+
+ {centerItemComps} +
+
+ {rightItemComps} +
); } } const mapStateToProps = state => { - return { theme: state.settings.theme }; + return { themeId: state.settings.theme }; }; const Toolbar = connect(mapStateToProps)(ToolbarComponent); diff --git a/ElectronClient/gui/ToolbarButton.jsx b/ElectronClient/gui/ToolbarButton.jsx deleted file mode 100644 index eebd21b3d0..0000000000 --- a/ElectronClient/gui/ToolbarButton.jsx +++ /dev/null @@ -1,51 +0,0 @@ -const React = require('react'); -const { themeStyle } = require('lib/theme'); - -class ToolbarButton extends React.Component { - render() { - const theme = themeStyle(this.props.theme); - - const style = Object.assign({}, theme.toolbarStyle); - - const title = this.props.title ? this.props.title : ''; - const tooltip = this.props.tooltip ? this.props.tooltip : title; - - let icon = null; - if (this.props.iconName) { - const iconStyle = { - fontSize: Math.round(theme.fontSize * 1.5), - color: theme.iconColor, - }; - if (title) iconStyle.marginRight = 5; - icon = ; - } - - // Keep this for legacy compatibility but for consistency we should use "disabled" prop - let isEnabled = !('enabled' in this.props) || this.props.enabled === true; - if (this.props.disabled) isEnabled = false; - - const classes = ['button']; - if (!isEnabled) classes.push('disabled'); - - const finalStyle = Object.assign({}, style, { - opacity: isEnabled ? 1 : 0.4, - }); - - return ( - { - if (isEnabled && this.props.onClick) this.props.onClick(); - }} - > - {icon} - {title} - - ); - } -} - -module.exports = ToolbarButton; diff --git a/ElectronClient/gui/ToolbarButton/ToolbarButton.tsx b/ElectronClient/gui/ToolbarButton/ToolbarButton.tsx new file mode 100644 index 0000000000..bc5a0cbfee --- /dev/null +++ b/ElectronClient/gui/ToolbarButton/ToolbarButton.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { ToolbarButtonInfo } from 'lib/services/CommandService'; +import { StyledRoot, StyledIconSpan, StyledIconI } from './styles'; + +interface Props { + readonly themeId: number, + readonly toolbarButtonInfo?: ToolbarButtonInfo, + readonly title?: string, + readonly tooltip?: string, + readonly iconName?: string, + readonly disabled?: boolean, + readonly backgroundHover?: boolean, +} + +function isFontAwesomeIcon(iconName:string) { + const s = iconName.split(' '); + return s.length === 2 && ['fa', 'fas'].includes(s[0]); +} + +function getProp(props:Props, name:string, defaultValue:any = null) { + if (props.toolbarButtonInfo && (name in props.toolbarButtonInfo)) return (props.toolbarButtonInfo as any)[name]; + if (!(name in props)) return defaultValue; + return (props as any)[name]; +} + +export default function ToolbarButton(props:Props) { + const title = getProp(props, 'title', ''); + const tooltip = getProp(props, 'tooltip', title); + + let icon = null; + const iconName = getProp(props, 'iconName'); + if (iconName) { + const IconClass = isFontAwesomeIcon(iconName) ? StyledIconI : StyledIconSpan; + icon = ; + } + + // Keep this for legacy compatibility but for consistency we should use "disabled" prop + let isEnabled = getProp(props, 'enabled', null); + if (isEnabled === null) isEnabled = true; + if (props.disabled) isEnabled = false; + + const classes = ['button']; + if (!isEnabled) classes.push('disabled'); + + const onClick = getProp(props, 'onClick'); + + return ( + { + if (isEnabled && onClick) onClick(); + }} + > + {icon} + {title} + + ); +} + diff --git a/ElectronClient/gui/ToolbarButton/styles/index.ts b/ElectronClient/gui/ToolbarButton/styles/index.ts new file mode 100644 index 0000000000..5cfa85d4f9 --- /dev/null +++ b/ElectronClient/gui/ToolbarButton/styles/index.ts @@ -0,0 +1,40 @@ +const styled = require('styled-components').default; +const { css } = require('styled-components'); + +interface RootProps { + readonly theme: any; + readonly disabled: boolean; + readonly hasTitle: boolean; +} + +export const StyledRoot = styled.a` + opacity: ${(props:RootProps) => props.disabled ? 0.3 : 1}; + height: ${(props:RootProps) => props.theme.toolbarHeight}px; + min-height: ${(props:RootProps) => props.theme.toolbarHeight}px; + width: ${(props:RootProps) => props.hasTitle ? 'auto' : `${props.theme.toolbarHeight}px`}; + max-width: ${(props:RootProps) => props.hasTitle ? 'auto' : `${props.theme.toolbarHeight}px`}; + display: flex; + align-items: center; + justify-content: center; + cursor: default; + border-radius: 3px; + box-sizing: border-box; + + &:hover { + background-color: ${(props:RootProps) => props.disabled ? 'none' : props.theme.backgroundColorHover3}; + } +`; + +interface IconProps { + readonly theme: any; + readonly title: string; +} + +const iconStyle = css` + font-size: ${(props:IconProps) => props.theme.toolbarIconSize}px; + color: ${(props:IconProps) => props.theme.color3}; + margin-right: ${(props:IconProps) => props.title ? 5 : 0}px; +`; + +export const StyledIconI = styled.i`${iconStyle}`; +export const StyledIconSpan = styled.span`${iconStyle}`; diff --git a/ElectronClient/gui/ToolbarSpace.jsx b/ElectronClient/gui/ToolbarSpace.jsx index f6e515f773..8829e45b25 100644 --- a/ElectronClient/gui/ToolbarSpace.jsx +++ b/ElectronClient/gui/ToolbarSpace.jsx @@ -3,7 +3,7 @@ const { themeStyle } = require('lib/theme'); class ToolbarSpace extends React.Component { render() { - const theme = themeStyle(this.props.theme); + const theme = themeStyle(this.props.themeId); const style = Object.assign({}, theme.toolbarStyle); style.minWidth = style.height / 2; diff --git a/ElectronClient/gui/style/ConfigMenuBar.js b/ElectronClient/gui/style/ConfigMenuBar.js index 12a7889320..950db97ba0 100644 --- a/ElectronClient/gui/style/ConfigMenuBar.js +++ b/ElectronClient/gui/style/ConfigMenuBar.js @@ -1,7 +1,7 @@ const { createSelector } = require('reselect'); const { themeStyle } = require('lib/theme'); -const themeSelector = (state, props) => themeStyle(props.theme); +const themeSelector = (state, props) => themeStyle(props.themeId); const style = createSelector( themeSelector, diff --git a/ElectronClient/gui/style/ExtensionBadge.js b/ElectronClient/gui/style/ExtensionBadge.js index 854373c1e1..409e00b317 100644 --- a/ElectronClient/gui/style/ExtensionBadge.js +++ b/ElectronClient/gui/style/ExtensionBadge.js @@ -1,7 +1,7 @@ const { createSelector } = require('reselect'); const { themeStyle } = require('lib/theme'); -const themeSelector = (state, props) => themeStyle(props.theme); +const themeSelector = (state, props) => themeStyle(props.themeId); const style = createSelector( themeSelector, diff --git a/ElectronClient/gui/style/StyledInput.tsx b/ElectronClient/gui/style/StyledInput.tsx new file mode 100644 index 0000000000..1a7bec487e --- /dev/null +++ b/ElectronClient/gui/style/StyledInput.tsx @@ -0,0 +1,25 @@ +const styled = require('styled-components').default; +const Color = require('color'); + +const StyledInput = styled.input` + border: 1px solid ${(props:any) => Color(props.theme.color3).alpha(0.6)}; + border-radius: 3px; + font-size: ${(props:any) => props.theme.fontSize}px; + color: ${(props:any) => props.theme.color}; + padding: 0 8px; + height: ${(props:any) => `${props.theme.toolbarHeight}px`}; + max-height: ${(props:any) => `${props.theme.toolbarHeight}px`}; + box-sizing: border-box; + background-color: ${(props:any) => Color(props.theme.backgroundColor4).alpha(0.5)}; + + &::placeholder { + color: ${(props:any) => props.theme.colorFaded}; + } + + &:focus { + background-color: ${(props:any) => props.theme.backgroundColor4}; + border: 1px solid ${(props:any) => props.theme.color3}; + } +`; + +export default StyledInput; diff --git a/ElectronClient/gui/style/StyledTextInput.tsx b/ElectronClient/gui/style/StyledTextInput.tsx new file mode 100644 index 0000000000..d2cb7fcc99 --- /dev/null +++ b/ElectronClient/gui/style/StyledTextInput.tsx @@ -0,0 +1,7 @@ +const styled = require('styled-components').default; + +const StyledInput = styled.input` + +`; + +export default StyledInput; diff --git a/ElectronClient/index.html b/ElectronClient/index.html index cae72fd08f..60ebaa6350 100644 --- a/ElectronClient/index.html +++ b/ElectronClient/index.html @@ -9,9 +9,12 @@ --> Joplin + + +