1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Desktop: UI update (#3586)

This commit is contained in:
Laurent 2020-09-15 14:01:07 +01:00 committed by GitHub
parent bdedf69439
commit 056285deda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
138 changed files with 4620 additions and 2308 deletions

View File

@ -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

42
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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');

View File

@ -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'),

View File

@ -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 => {

View File

@ -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 <StyledIcon animation={props.iconAnimation} mr={iconOnly ? '0' : '6px'} color={props.color} className={props.iconName}/>;
}
function renderTitle() {
if (!props.title) return null;
return <StyledTitle color={props.color}>{props.title}</StyledTitle>;
}
function onClick() {
if (props.disabled) return;
props.onClick();
}
return (
<StyledButton style={props.style} disabled={props.disabled} title={props.tooltip} className={props.className} iconOnly={iconOnly} onClick={onClick}>
{renderIcon()}
{renderTitle()}
</StyledButton>
);
}

View File

@ -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 (
<div>
<div style={containerStyle}>
<div style={{ padding: theme.margin }}>
<p style={theme.textStyle}>{_('Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.')}</p>
<div>
<p style={Object.assign({}, theme.textStyle, { marginTop: 0 })}>{_('Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.')}</p>
<p style={theme.textStyle}>{_('In order to use the web clipper, you need to do the following:')}</p>
<div style={stepBoxStyle}>
@ -120,8 +122,8 @@ class ClipperConfigScreenComponent extends React.Component {
<p style={theme.h1Style}>{_('Step 2: Install the extension')}</p>
<p style={theme.textStyle}>{_('Download and install the relevant extension for your browser:')}</p>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<ExtensionBadge theme={this.props.theme} type="firefox" url="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/"/>
<ExtensionBadge style={{ marginLeft: 10 }} theme={this.props.theme} type="chrome" url="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek"/>
<ExtensionBadge themeId={this.props.themeId} type="firefox" url="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/"/>
<ExtensionBadge style={{ marginLeft: 10 }} themeId={this.props.themeId} type="chrome" url="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek"/>
</div>
</div>
@ -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'],

View File

@ -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 (
<button style={style.button} onClick={props.onClick}>
<i style={iconStyle} className={props.iconName}></i>
<span style={labelStyle}>{props.label}</span>
</button>
);
}
function ConfigMenuBar(props) {
const buttons = [];
const style = styleSelector(null, props);
for (const section of props.sections) {
buttons.push(<ConfigMenuBarButton
selected={props.selection === section.name}
theme={props.theme}
key={section.name}
iconName={Setting.sectionNameToIcon(section.name)}
label={Setting.sectionNameToLabel(section.name)}
onClick={() => { props.onSelectionChange({ section: section }); }}
/>);
}
return (
<div style={style.root} className="config-menu-bar">
<div style={style.barButtons}>
{buttons}
</div>
</div>
);
}
module.exports = ConfigMenuBar;

View File

@ -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 <Button style={{ marginRight: 10 }} level={ButtonLevel.Primary} disabled={!props.hasChanges} onClick={props.onSaveClick} title={_('OK')}/>;
}
function renderApplyButton() {
if (!props.onApplyClick) return null;
return <Button level={ButtonLevel.Primary} disabled={!props.hasChanges} onClick={props.onApplyClick} title={_('Apply')}/>;
}
return (
<StyledRoot>
<Button
onClick={props.onCancelClick}
level={ButtonLevel.Secondary}
iconName="fa fa-chevron-left"
title={props.backButtonTitle ? props.backButtonTitle : _('Back')}
/>
{ (props.onApplyClick || props.onSaveClick) && (
<div style={{ display: 'flex', flexDirection: 'row', marginLeft: 30 }}>
{renderOkButton()}
{renderApplyButton()}
</div>
)}
</StyledRoot>
);
}

View File

@ -1,48 +1,63 @@
const React = require('react');
import * as React from 'react';
import SideBar from './SideBar';
import ButtonBar from './ButtonBar';
import Button, { ButtonLevel } from '../Button/Button';
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 pathUtils = require('lib/path-utils.js');
const { _ } = require('lib/locale.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
const shared = require('lib/components/shared/config-shared.js');
const ConfigMenuBar = require('./ConfigMenuBar.min.js');
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min');
const { ClipperConfigScreen } = require('./ClipperConfigScreen.min');
const { KeymapConfigScreen } = require('./KeymapConfig/KeymapConfigScreen');
const { bridge } = require('electron').remote.require('./bridge');
const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min');
const { ClipperConfigScreen } = require('../ClipperConfigScreen.min');
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
class ConfigScreenComponent extends React.Component {
constructor() {
super();
class ConfigScreenComponent extends React.Component<any, any> {
rowStyle_:any = null;
constructor(props:any) {
super(props);
shared.init(this);
this.state.selectedSectionName = 'general';
this.state.screenName = '';
this.checkSyncConfig_ = async () => {
await shared.checkSyncConfig(this, this.state.settings);
};
this.checkNextcloudAppButton_click = async () => {
this.setState({ showNextcloudAppLog: true });
await shared.checkNextcloudApp(this, this.state.settings);
};
this.showLogButton_click = () => {
this.setState({ showNextcloudAppLog: true });
};
this.nextcloudAppHelpLink_click = () => {
bridge().openExternal('https://joplinapp.org/nextcloud_app');
this.state = {
selectedSectionName: 'general',
screenName: '',
changedSettingKeys: [],
};
this.rowStyle_ = {
marginBottom: 10,
};
this.configMenuBar_selectionChange = this.configMenuBar_selectionChange.bind(this);
this.sideBar_selectionChange = this.sideBar_selectionChange.bind(this);
this.checkSyncConfig_ = this.checkSyncConfig_.bind(this);
this.checkNextcloudAppButton_click = this.checkNextcloudAppButton_click.bind(this);
this.showLogButton_click = this.showLogButton_click.bind(this);
this.nextcloudAppHelpLink_click = this.nextcloudAppHelpLink_click.bind(this);
this.onCancelClick = this.onCancelClick.bind(this);
this.onSaveClick = this.onSaveClick.bind(this);
this.onApplyClick = this.onApplyClick.bind(this);
}
async checkSyncConfig_() {
await shared.checkSyncConfig(this, this.state.settings);
}
async checkNextcloudAppButton_click() {
this.setState({ showNextcloudAppLog: true });
await shared.checkNextcloudApp(this, this.state.settings);
}
showLogButton_click() {
this.setState({ showNextcloudAppLog: true });
}
nextcloudAppHelpLink_click() {
bridge().openExternal('https://joplinapp.org/nextcloud_app');
}
UNSAFE_componentWillMount() {
@ -57,7 +72,7 @@ class ConfigScreenComponent extends React.Component {
}
}
sectionByName(name) {
sectionByName(name:string) {
const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings });
for (const section of sections) {
if (section.name === name) return section;
@ -66,15 +81,15 @@ class ConfigScreenComponent extends React.Component {
throw new Error(`Invalid section name: ${name}`);
}
screenFromName(screenName) {
if (screenName === 'encryption') return <EncryptionConfigScreen theme={this.props.theme}/>;
if (screenName === 'server') return <ClipperConfigScreen theme={this.props.theme}/>;
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.theme}/>;
screenFromName(screenName:string) {
if (screenName === 'encryption') return <EncryptionConfigScreen themeId={this.props.themeId}/>;
if (screenName === 'server') return <ClipperConfigScreen themeId={this.props.themeId}/>;
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.themeId}/>;
throw new Error(`Invalid screen name: ${screenName}`);
}
switchSection(name) {
switchSection(name:string) {
const section = this.sectionByName(name);
let screenName = '';
if (section.isScreen) {
@ -89,11 +104,11 @@ class ConfigScreenComponent extends React.Component {
this.setState({ selectedSectionName: section.name, screenName: screenName });
}
configMenuBar_selectionChange(event) {
sideBar_selectionChange(event:any) {
this.switchSection(event.section.name);
}
keyValueToArray(kv) {
keyValueToArray(kv:any) {
const output = [];
for (const k in kv) {
if (!kv.hasOwnProperty(k)) continue;
@ -106,11 +121,11 @@ class ConfigScreenComponent extends React.Component {
return output;
}
renderSectionDescription(section) {
renderSectionDescription(section:any) {
const description = Setting.sectionDescription(section.name);
if (!description) return null;
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
return (
<div style={Object.assign({}, theme.textStyle, { marginBottom: 15 })}>
{description}
@ -118,10 +133,10 @@ class ConfigScreenComponent extends React.Component {
);
}
sectionToComponent(key, section, settings, selected) {
const theme = themeStyle(this.props.theme);
sectionToComponent(key:string, section:any, settings:any, selected:boolean) {
const theme = themeStyle(this.props.themeId);
const createSettingComponents = (advanced) => {
const createSettingComponents = (advanced:boolean) => {
const output = [];
for (let i = 0; i < section.metadatas.length; i++) {
const md = section.metadatas[i];
@ -135,9 +150,10 @@ class ConfigScreenComponent extends React.Component {
const settingComps = createSettingComponents(false);
const advancedSettingComps = createSettingComponents(true);
const sectionStyle = {
const sectionStyle:any = {
marginTop: 20,
marginBottom: 20,
maxWidth: 640,
};
if (!selected) sectionStyle.display = 'none';
@ -161,9 +177,12 @@ class ConfigScreenComponent extends React.Component {
settingComps.push(
<div key="check_sync_config_button" style={this.rowStyle_}>
<button disabled={this.state.checkSyncConfigResult === 'checking'} style={theme.buttonStyle} onClick={this.checkSyncConfig_}>
{_('Check synchronisation configuration')}
</button>
<Button
title={_('Check synchronisation configuration')}
level={ButtonLevel.Secondary}
disabled={this.state.checkSyncConfigResult === 'checking'}
onClick={this.checkSyncConfig_}
/>
{statusComp}
</div>
);
@ -204,9 +223,7 @@ class ConfigScreenComponent extends React.Component {
&nbsp;&nbsp;
{showLogButton}
&nbsp;&nbsp;
<button disabled={this.state.checkNextcloudAppResult === 'checking'} style={theme.buttonStyle} onClick={this.checkNextcloudAppButton_click}>
{_('Check Status')}
</button>
<Button level={ButtonLevel.Secondary} style={{ display: 'inline-block' }} title={_('Check Status')} disabled={this.state.checkNextcloudAppResult === 'checking'} onClick={this.checkNextcloudAppButton_click}/>
&nbsp;&nbsp;
<a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a>
{statusComp}
@ -220,8 +237,17 @@ class ConfigScreenComponent extends React.Component {
if (advancedSettingComps.length) {
const iconName = this.state.showAdvancedSettings ? 'fa fa-angle-down' : 'fa fa-angle-right';
const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 });
advancedSettingsButton = <button onClick={() => shared.advancedSettingsButton_click(this)} style={advancedSettingsButtonStyle}><i style={{ fontSize: 14 }} className={iconName}></i> {_('Show Advanced Settings')}</button>;
// const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 });
advancedSettingsButton = (
<div style={{ marginBottom: 10 }}>
<Button
level={ButtonLevel.Secondary}
onClick={() => shared.advancedSettingsButton_click(this)}
iconName={iconName}
title={_('Show Advanced Settings')}
/>
</div>
);
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 (
<div key={key} style={rowStyle}>
@ -316,7 +359,7 @@ class ConfigScreenComponent extends React.Component {
<select
value={value}
style={selectStyle}
onChange={event => {
onChange={(event:any) => {
updateSettingValue(key, event.target.value);
}}
>
@ -330,35 +373,38 @@ class ConfigScreenComponent extends React.Component {
updateSettingValue(key, !value);
};
const checkboxSize = theme.fontSize * 1.1666666666666;
// Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes.
// There's probably a better way to do this but can't figure it out.
return (
<div key={key + value.toString()} style={rowStyle}>
<div style={controlStyle}>
<div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
<input
id={`setting_checkbox_${key}`}
type="checkbox"
checked={!!value}
onChange={event => {
onCheckboxClick(event);
onChange={() => {
onCheckboxClick();
}}
style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }}
/>
<label
onClick={event => {
onCheckboxClick(event);
onClick={() => {
onCheckboxClick();
}}
style={checkboxLabelStyle}
style={{ ...checkboxLabelStyle, marginLeft: 5, marginBottom: 0 }}
htmlFor={`setting_checkbox_${key}`}
>
{md.label()}
</label>
{descriptionComp}
</div>
{descriptionComp}
</div>
);
} 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 (
<div key={key} style={rowStyle}>
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
<div style={{ display: 'flex' }}>
<div style={{ flex: 0, whiteSpace: 'nowrap' }}>
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
</div>
<div style={{ flex: 0 }}>
<div style={subLabel}>Path:</div>
<div style={subLabel}>Arguments:</div>
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom }}>
<div style={{ ...rowStyle, marginBottom: 5 }}>
<div style={subLabel}>Path:</div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom }}>
<input
type={inputType}
style={Object.assign({}, inputStyle, { marginBottom: 0, marginRight: 5 })}
onChange={(event:any) => {
onPathChange(event);
}}
value={cmd[0]}
/>
<Button
level={ButtonLevel.Secondary}
title={_('Browse...')}
onClick={browseButtonClick}
/>
</div>
</div>
<div style={{ ...rowStyle, marginBottom: 5 }}>
<div style={subLabel}>Arguments:</div>
<input
type={inputType}
style={Object.assign({}, inputStyle, { marginBottom: 0 })}
onChange={event => {
onPathChange(event);
style={inputStyle}
onChange={(event:any) => {
onArgsChange(event);
}}
value={cmd[0]}
value={cmd[1]}
/>
<button onClick={browseButtonClick} style={Object.assign({}, theme.buttonStyle, { marginLeft: 5 })}>
{_('Browse...')}
</button>
<div style={{ width: inputStyle.width }}>
{descriptionComp}
</div>
</div>
<input
type={inputType}
style={inputStyle}
onChange={event => {
onArgsChange(event);
}}
value={cmd[1]}
/>
</div>
</div>
<div style={{ display: 'flex' }}>
<div style={{ flex: 0, whiteSpace: 'nowrap' }}>
<div style={invisibleLabel}>
<label>{md.label()}</label>
</div>
</div>
<div style={{ flex: 1 }}>{descriptionComp}</div>
</div>
</div>
);
} 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}
<div style={{ width: inputStyle.width }}>
{descriptionComp}
</div>
</div>
);
}
} 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 {
</div>
);
} 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 (
<div key={key} style={rowStyle}>
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
<button style={buttonStyle} onClick={md.onClick}>
{_('Edit')}
</button>
<Button level={ButtonLevel.Secondary} title={_('Edit')} onClick={md.onClick}/>
{descriptionComp}
</div>
);
@ -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 ? <div style={{ overflow: 'scroll', flex: 1 }}>{this.screenFromName(this.state.screenName)}</div> : null;
if (screenComp) containerStyle.display = 'none';
@ -591,45 +618,35 @@ class ConfigScreenComponent extends React.Component {
const sections = shared.settingsSections({ device: 'desktop', settings });
return (
<div style={style}>
<ConfigMenuBar
<div style={{ display: 'flex', flexDirection: 'row' }}>
<SideBar
selection={this.state.selectedSectionName}
onSelectionChange={this.configMenuBar_selectionChange}
onSelectionChange={this.sideBar_selectionChange}
sections={sections}
theme={this.props.theme}
/>
{screenComp}
<div style={containerStyle}>{settingComps}</div>
<div style={buttonBarStyle}>
<button
onClick={() => {
this.onCancelClick();
}}
style={buttonStyle}
>
<i style={theme.buttonIconStyle} className={'fa fa-chevron-left'}></i>
{hasChanges && !screenComp ? _('Cancel') : _('Back')}
</button>
{ !screenComp && (
<div>
<button disabled={!hasChanges} onClick={() => { this.onSaveClick(); }} style={buttonStyleApprove}>{_('OK')}</button>
<button disabled={!hasChanges} onClick={() => { this.onApplyClick(); }} style={buttonStyleApprove}>{_('Apply')}</button>
</div>
)}
<div style={style}>
{screenComp}
<div style={containerStyle}>{settingComps}</div>
<ButtonBar
hasChanges={hasChanges}
backButtonTitle={hasChanges && !screenComp ? _('Cancel') : _('Back')}
onCancelClick={this.onCancelClick}
onSaveClick={screenComp ? null : this.onSaveClick}
onApplyClick={screenComp ? null : this.onApplyClick}
/>
</div>
</div>
);
}
}
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 };

View File

@ -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 (
<StyledListItem key={section.name} selected={selected} onClick={() => { props.onSelectionChange({ section: section }); }}>
<StyledListItemIcon className={Setting.sectionNameToIcon(section.name)} />
<StyledListItemLabel>
{Setting.sectionNameToLabel(section.name)}
</StyledListItemLabel>
</StyledListItem>
);
}
for (const section of props.sections) {
buttons.push(renderButton(section));
}
return (
<StyledRoot>
{buttons}
</StyledRoot>
);
}

View File

@ -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' });

View File

@ -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<any, any> {
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 (
<div>
<Header style={headerStyle} />
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={containerStyle}>
<p style={theme.textStyle}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</p>
<p style={theme.textStyle}>{_('Step 1: Open this URL in your browser to authorise the application:')}</p>
@ -46,17 +53,18 @@ class DropboxLoginScreenComponent extends React.Component {
{_('Submit')}
</button>
</div>
<ButtonBar
onCancelClick={() => this.props.dispatch({ type: 'NAV_BACK' })}
/>
</div>
);
}
}
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);

View File

@ -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'],

View File

@ -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 <span style={{ display: 'flex', flexDirection: 'row' }}>{buttons}</span>;
}
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 = <i style={iconStyle} className={`fas ${options.iconName}`}></i>;
}
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 (
<a
className={classes.join(' ')}
style={finalStyle}
key={key}
href="#"
title={title}
onClick={() => {
if (isEnabled) options.onClick();
}}
>
{icon}
<span className="title" style={{
display: this.state.showButtonLabels ? 'inline-block' : 'none',
}}>{title}</span>
</a>
);
}
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 = <i style={iconStyle} className={`fas ${iconName}`}></i>;
if (options.onQuery) this.searchOnQuery_ = options.onQuery;
const usageLink = !this.state.showSearchUsageLink ? null : (
<a onClick={this.searchUsageLink_click} style={theme.urlStyle} href="#">
{_('Usage')}
</a>
);
return (
<div key={key} style={containerStyle}>
<input type="text" style={inputStyle} placeholder={options.title} value={state.searchQuery} onChange={this.search_onChange} ref={elem => (this.searchElement_ = elem)} onFocus={this.search_onFocus} onBlur={this.search_onBlur} onKeyDown={this.search_keyDown} />
<a href="#" style={searchButton} onClick={this.search_onClear}>
{icon}
</a>
{usageLink}
</div>
);
}
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 (
<div className="header" style={style}>
{items}
</div>
);
}
}
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 };

View File

@ -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,
};
};

View File

@ -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,

View File

@ -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(<div key={messages[i].key}>{messages[i].text}</div>);
@ -116,7 +110,6 @@ class ImportScreenComponent extends React.Component {
return (
<div style={{}}>
<Header style={headerStyle} />
<div style={messagesStyle}>{messageComps}</div>
</div>
);
@ -125,7 +118,7 @@ class ImportScreenComponent extends React.Component {
const mapStateToProps = state => {
return {
theme: state.settings.theme,
themeId: state.settings.theme,
};
};

View File

@ -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();
}

View File

@ -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',

View File

@ -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<any, any> {
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 <SideBar key={key} />;
} else if (key === 'noteList') {
return <NoteList key={key} resizableLayoutEventEmitter={eventEmitter} size={event.size} visible={event.visible}/>;
} else if (key === 'editor') {
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
return <NoteEditor key={key} bodyEditor={bodyEditor} />;
} else if (key === 'noteListControls') {
return <NoteListControls key={key} />;
}
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 (
<div style={style}>
<div style={modalLayerStyle}>{this.state.modalLayer.message}</div>
{noteContentPropertiesDialogOptions.visible && <NoteContentPropertiesDialog theme={this.props.theme} onClose={this.noteContentPropertiesDialog_close} text={noteContentPropertiesDialogOptions.text}/>}
{notePropertiesDialogOptions.visible && <NotePropertiesDialog theme={this.props.theme} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
{shareNoteDialogOptions.visible && <ShareNoteDialog theme={this.props.theme} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />}
{noteContentPropertiesDialogOptions.visible && <NoteContentPropertiesDialog markupLanguage={noteContentPropertiesDialogOptions.markupLanguage} themeId={this.props.themeId} onClose={this.noteContentPropertiesDialog_close} text={noteContentPropertiesDialogOptions.text}/>}
{notePropertiesDialogOptions.visible && <NotePropertiesDialog themeId={this.props.themeId} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
{shareNoteDialogOptions.visible && <ShareNoteDialog themeId={this.props.themeId} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />}
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} theme={this.props.theme} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} themeId={this.props.themeId} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
<Header style={styles.header} showBackButton={false} items={headerItems} />
{messageComp}
<SideBar style={styles.sideBar} />
<VerticalResizer style={styles.verticalResizerSidebar} onDrag={this.sidebar_onDrag} />
<NoteList style={styles.noteList} />
<VerticalResizer style={styles.verticalResizerNotelist} onDrag={this.noteList_onDrag} />
<NoteEditor bodyEditor={bodyEditor} style={styles.noteText} />
<ResizableLayout
width={this.state.width}
height={styles.rowHeight}
layout={this.state.layout}
onResize={this.resizableLayout_resize}
renderItem={this.resizableLayout_renderItem}
/>
{pluginDialog}
</div>
);
}
}
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);

View File

@ -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 => {

View File

@ -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',
};

View File

@ -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,
},
});

View File

@ -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 => {

View File

@ -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 => {

View File

@ -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,
};
},
};
};

View File

@ -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 => {

View File

@ -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',

View File

@ -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<number>(0);
@ -165,7 +167,7 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
<div style={labelCompStyle}>
{_('Read time: %s min', formatReadTime(strippedReadTime))}
</div>
<DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</div>
</div>
);

View File

@ -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) {
<div style={styles.root} ref={rootRef}>
<div style={styles.rowToolbar}>
<Toolbar
theme={props.theme}
themeId={props.themeId}
dispatch={props.dispatch}
disabled={editorReadOnly}
/>

View File

@ -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) {

View File

@ -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 <ToolbarBase disabled={props.disabled} style={styles.root} items={toolbarItems} />;

View File

@ -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',

View File

@ -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 <ToolbarButton
key={key}
themeId={props.themeId}
toolbarButtonInfo={info}
/>;
}
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 (
<div style={styles.leftExtraToolbarContainer}>
{buttons}
</div>
);
}
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(<ToggleEditorsButton
key={buttonName}
value={ToggleEditorsButtonValue.RichText}
themeId={props.themeId}
toolbarButtonInfo={info}
/>);
} else {
buttons.push(renderExtraToolbarButton(buttonName, info));
}
}
return (
<div style={styles.rightExtraToolbarContainer}>
{buttons}
</div>
);
}
// 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 : <p style={theme.textStyleMinor}>{`Status: ${status}`}</p>;
return (
@ -1056,8 +1122,10 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
}
return (
<div style={styles.rootStyle}>
<div style={styles.rootStyle} className="joplin-tinymce">
{renderDisabledOverlay()}
{renderLeftExtraToolbarButtons()}
{renderRightExtraToolbarButtons()}
<div style={{ width: '100%', height: '100%' }} id={rootIdRef.current}/>
</div>
);

View File

@ -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,
},
};
});
}

View File

@ -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<FormNote>();
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<any[]> => {
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 <NoteToolbar
theme={props.theme}
themeId={props.themeId}
note={formNote}
style={toolbarStyle}
/>;
}
function renderTagButton() {
const info = CommandService.instance().commandToToolbarButton('setTags');
return <ToolbarButton
themeId={props.themeId}
toolbarButtonInfo={info}
/>;
}
function renderTagBar() {
return props.selectedNoteTags.length ? <TagList items={props.selectedNoteTags} /> : null;
const theme = themeStyle(props.themeId);
let control = null;
if (!props.selectedNoteTags.length) {
const noteIds = [formNote.id];
control = <span onClick={() => { CommandService.instance().execute('setTags', { noteIds }); }} style={theme.clickableTextStyle}>Click to add some tags...</span>;
} else {
control = <TagList items={props.selectedNoteTags} />;
}
return (
<div style={{ paddingLeft: 8 }}>{control}</div>
);
}
function renderTitleBar() {
const theme = themeStyle(props.themeId);
const titleBarDate = <span style={styles.titleDate}>{time.formatMsToLocal(formNote.user_updated_time)}</span>;
return (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', height: theme.topRowHeight }}>
<input
type="text"
ref={titleInputRef}
@ -360,6 +391,7 @@ function NoteEditor(props: NoteEditorProps) {
value={formNote.title}
/>
{titleBarDate}
{renderNoteToolbar()}
</div>
);
}
@ -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 <MultiNoteActions
theme={props.theme}
themeId={props.themeId}
selectedNoteIds={props.selectedNoteIds}
notes={props.notes}
dispatch={props.dispatch}
watchedNoteFiles={props.watchedNoteFiles}
style={props.style}
/>;
}
function renderSearchBar() {
if (!showLocalSearch) return false;
const theme = themeStyle(props.theme);
const theme = themeStyle(props.themeId);
return (
<NoteSearchBar
@ -479,6 +511,30 @@ function NoteEditor(props: NoteEditorProps) {
);
}
function renderSearchInfo() {
if (formNoteFolder && ['Search', 'Tag', 'SmartFilter'].includes(props.notesParentType)) {
return (
<div style={{ paddingTop: 10, paddingBottom: 10 }}>
<Button
iconName="icon-notebooks"
level={ButtonLevel.Primary}
title={_('In: %s', substrWithEllipsis(formNoteFolder.title, 0, 100))}
onClick={() => {
props.dispatch({
type: 'FOLDER_AND_NOTE_SELECT',
folderId: formNoteFolder.id,
noteId: formNote.id,
});
}}
/>
<div style={{ flex: 1 }}></div>
</div>
);
} else {
return null;
}
}
if (formNote.encryption_applied || !formNote.id || !props.noteId) {
return renderNoNotes(styles.root);
}
@ -488,15 +544,17 @@ function NoteEditor(props: NoteEditorProps) {
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{renderResourceWatchingNotification()}
{renderTitleBar()}
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
{renderNoteToolbar()}{renderTagBar()}
</div>
{renderSearchInfo()}
<div style={{ display: 'flex', flex: 1 }}>
{editor}
</div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
{renderSearchBar()}
</div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', height: 40 }}>
{renderTagButton()}
{renderTagBar()}
</div>
{wysiwygBanner}
</div>
</div>
@ -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,

View File

@ -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',
},
];

View File

@ -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,

View File

@ -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 {

View File

@ -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;
}

View File

@ -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<ToolbarButtonInfos>({});
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;
}

View File

@ -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 <NoteListItem
ref={ref}
key={item.id}
style={this.style(this.props.theme)}
style={this.style()}
item={item}
index={index}
theme={this.props.theme}
width={this.props.style.width}
themeId={this.props.themeId}
width={this.state.width}
height={this.itemHeight}
dragItemIndex={this.state.dragOverTargetNoteIndex}
highlightedWords={highlightedWords()}
isProvisional={this.props.provisionalNoteIds.includes(item.id)}
@ -260,12 +272,12 @@ class NoteListComponent extends React.Component {
/>;
}
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 <div style={emptyDivStyle}>{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".')}</div>;
}
renderItemList(style:any) {
if (!this.props.notes.length) return null;
return (
<ItemList
ref={this.itemListRef}
disabled={this.props.isInsertingNotes}
itemHeight={this.style().listItem.height}
className={'note-list'}
items={this.props.notes}
style={style}
itemRenderer={this.renderItem}
onKeyDown={this.onKeyDown}
/>
);
}
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 <div style={emptyDivStyle}>{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".')}</div>;
}
return <ItemList
ref={this.itemListRef}
disabled={this.props.isInsertingNotes}
itemHeight={this.style(this.props.theme).listItem.height}
className={'note-list'}
items={this.props.notes}
style={style}
itemRenderer={this.itemRenderer}
onKeyDown={this.onKeyDown}
/>;
return (
<StyledRoot ref={this.noteListRef}>
{this.renderEmptyList()}
{this.renderItemList(this.props.size)}
</StyledRoot>
);
}
}
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);

View File

@ -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 (
<StyledRoot>
<SearchBar inputRef={searchBarRef}/>
<StyledButton
tooltip={CommandService.instance().title('newTodo')}
iconName="far fa-check-square"
level={ButtonLevel.Primary}
onClick={onNewTodoButtonClick}
/>
<StyledButton
tooltip={CommandService.instance().title('newNote')}
iconName="icon-note"
level={ButtonLevel.Primary}
onClick={onNewNoteButtonClick}
/>
</StyledRoot>
);
}

View File

@ -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();
},
};
};

View File

@ -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 (
<div style={{ display: 'flex', height: rootStyle.height, alignItems: 'center', paddingLeft: hPadding }}>
<div style={{ display: 'flex', height: props.height, alignItems: 'center', paddingLeft: hPadding }}>
<input
style={{ margin: 0, marginBottom: 1, marginRight: 5 }}
type="checkbox"
@ -118,12 +150,19 @@ function NoteListItem(props:NoteListItemProps, ref:any) {
};
const watchedIcon = props.isWatched ? null : <i style={watchedIconStyle} className={'fa fa-share-square'}></i>;
// 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 (
<div className="list-item-container" style={rootStyle} onDragOver={props.onNoteDragOver} onDrop={props.onNoteDrop}>
<StyledRoot
className="list-item-container"
onDragOver={props.onNoteDragOver}
onDrop={props.onNoteDrop}
width={props.width}
height={props.height}
isProvisional={props.isProvisional}
selected={props.isSelected}
dragItemPosition={dragItemPosition}
>
{renderCheckbox()}
<a
ref={anchorRef}
@ -138,7 +177,7 @@ function NoteListItem(props:NoteListItemProps, ref:any) {
{watchedIcon}
{titleComp}
</a>
</div>
</StyledRoot>
);
}

View File

@ -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 = <label style={Object.assign({}, theme.textStyle, theme.controlBoxLabel)}>{this.formatLabel(key)}</label>;
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 {
<div style={theme.dialogBox}>
<div style={theme.dialogTitle}>{_('Note properties')}</div>
<div>{noteComps}</div>
<DialogButtonRow theme={this.props.theme} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
<DialogButtonRow themeId={this.props.themeId} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
</div>
</div>
);

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -169,7 +169,7 @@ class NoteTextViewerComponent extends React.Component {
const mapStateToProps = state => {
return {
theme: state.settings.theme,
themeId: state.settings.theme,
};
};

View File

@ -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 <Toolbar style={styles.root} items={toolbarItems} />;
}

View File

@ -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<any, any> {
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 (
<div>
<Header style={headerStyle}/>
<div style={{ padding: 10 }}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ padding: theme.configScreenPadding, flex: 1 }}>
{logComps}
</div>
<ButtonBar
onCancelClick={() => this.props.dispatch({ type: 'NAV_BACK' })}
/>
</div>
);
}
}
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 };

View File

@ -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) {

View File

@ -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 (
<Resizable
key={item.key}
className={className}
style={style}
size={size}
onResizeStart={onResizeStart}
onResize={onResize}
onResizeStop={onResizeStop}
enable={enable}
minWidth={item.minWidth}
minHeight={item.minHeight}
>
{children}
</Resizable>
);
} else {
return (
<div key={item.key} className={className} style={{ ...style, ...size }}>
{children}
</div>
);
}
}
function ResizableLayout(props:Props) {
const eventEmitter = useRef(new EventEmitter());
const [resizedItem, setResizedItem] = useState<any>(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;

View File

@ -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]);
}

View File

@ -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);
};
}, []);
}

View File

@ -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<ResourceTable> = (props: ResourceTable) => {
const theme = themeStyle(props.themeId);
const sortOrderEngagedMarker = (s: SortingOrder) => {
return (
<a href="#"
style={{ color: props.theme.urlColor }}
style={{ color: theme.urlColor }}
onClick={() => props.onToggleSorting(s)}>{
(props.sorting.order === s && props.sorting.type === 'desc') ? '▾' : '▴'}</a>
);
};
const titleCellStyle = {
...props.theme.textStyle,
...theme.textStyle,
textOverflow: 'ellipsis',
overflowX: 'hidden',
maxWidth: 1,
@ -69,14 +72,14 @@ const ResourceTable: React.FC<ResourceTable> = (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<ResourceTable> = (props: ResourceTable) => {
<tr key={index}>
<td style={titleCellStyle} className="titleCell">
<a
style={{ color: props.theme.urlColor }}
style={{ color: theme.urlColor }}
href="#"
onClick={() => props.onResourceClick(resource)}>{resource.title || `(${_('Untitled')})`}
</a>
@ -105,7 +108,7 @@ const ResourceTable: React.FC<ResourceTable> = (props: ResourceTable) => {
<td style={cellStyle} className="dataCell">{prettyBytes(resource.size)}</td>
<td style={cellStyle} className="dataCell">{resource.id}</td>
<td style={cellStyle} className="dataCell">
<button style={props.theme.buttonStyle} onClick={() => props.onResourceDelete(resource)}>{_('Delete')}</button>
<button style={theme.buttonStyle} onClick={() => props.onResourceDelete(resource)}>{_('Delete')}</button>
</td>
</tr>
)}
@ -202,8 +205,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
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<Props, State> {
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 (
<div style={{ ...theme.containerStyle, fontFamily: theme.fontFamily }}>
<Header style={headerStyle} />
<div style={{ ...theme.containerStyle, fontFamily: theme.fontFamily, height: containerHeight, display: 'flex', flexDirection: 'column' }}>
<div style={rootStyle}>
<div style={{ ...theme.notificationBox, marginBottom: 10 }}>{
_('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<Props, State> {
<div>{_('Warning: not all resources shown for performance reasons (limit: %s).', MAX_RESOURCES)}</div>
}
{this.state.resources && <ResourceTable
theme={theme}
themeId={this.props.themeId}
style={style}
resources={this.state.resources}
sorting={this.state.sorting}
@ -243,13 +248,16 @@ class ResourceScreenComponent extends React.Component<Props, State> {
</div>
}
</div>
<ButtonBar
onCancelClick={() => this.props.dispatch({ type: 'NAV_BACK' })}
/>
</div>
);
}
}
const mapStateToProps = (state: any) => ({
theme: state.settings.theme,
themeId: state.settings.theme,
});
const ResourceScreen = connect(mapStateToProps)(ResourceScreenComponent);

View File

@ -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 <Navigator style={navigatorStyle} screens={screens} />;
return (
<StyleSheetManager disableVendorPrefixes>
<ThemeProvider theme={theme}>
<GlobalStyle/>
<Navigator style={navigatorStyle} screens={screens} />
</ThemeProvider>
</StyleSheetManager>
);
}
}
@ -103,6 +122,7 @@ const mapStateToProps = state => {
size: state.windowContentSize,
zoomFactor: state.settings.windowContentZoomFactor / 100,
appState: state.appState,
themeId: state.settings.theme,
};
};

View File

@ -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 (
<Root>
<SearchInput ref={props.inputRef} value={query} type="text" placeholder={_('Search...')} onChange={onChange}/>
<SearchButton onClick={onSearchButtonClick}>
<SearchButtonIcon className={iconName}/>
</SearchButton>
</Root>
);
}
const mapStateToProps = (state:any) => {
return {
notesParentType: state.notesParentType,
};
};
export default connect(mapStateToProps)(SearchBar);

View File

@ -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]);
}

View File

@ -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;
`;

View File

@ -12,7 +12,7 @@ const { reg } = require('lib/registry.js');
const { clipboard } = require('electron');
interface ShareNoteDialogProps {
theme: number,
themeId: number,
noteIds: Array<string>,
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<SharesMap>({});
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) {
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
{encryptionWarningMessage}
<DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</div>
</div>
);

View File

@ -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 <div style={this.style().noteCount}>({count})</div>;
}
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 = <i style={expandIconStyle} className={`fas ${iconName}`}></i>;
const expandLink = hasChildren ? (
<a style={expandLinkStyle} href="#" folderid={folder.id} onClick={this.onFolderToggleClick_}>
{expandIcon}
</a>
) : (
<span style={expandLinkStyle}>{expandIcon}</span>
);
const anchorRef = this.anchorItemRef('folder', folder.id);
const noteCount = folder.note_count ? this.noteCountElement(folder.note_count) : '';
return (
<div className={`list-item-container list-item-depth-${depth}`} style={containerStyle} key={folder.id} onDragStart={this.onFolderDragStart_} onDragOver={this.onFolderDragOver_} onDrop={this.onFolderDrop_} draggable={true} folderid={folder.id}>
{expandLink}
<a
ref={anchorRef}
className="list-item"
href="#"
data-id={folder.id}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={event => this.itemContextMenu(event)}
style={style}
folderid={folder.id}
onClick={() => {
this.folderItem_click(folder);
}}
onDoubleClick={this.onFolderToggleClick_}
>
{itemTitle} {noteCount}
</a>
</div>
);
}
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 (
<a
className="list-item"
href="#"
ref={anchorRef}
data-id={tag.id}
data-type={BaseModel.TYPE_TAG}
onContextMenu={event => this.itemContextMenu(event)}
tagid={tag.id}
key={tag.id}
style={style}
onDrop={this.onTagDrop_}
onClick={() => {
this.tagItem_click(tag);
}}
>
{Tag.displayTitle(tag)} {noteCount}
</a>
);
}
// searchItem(search, selected) {
// let style = Object.assign({}, this.style().listItem);
// if (selected) style = Object.assign(style, this.style().listItemSelected);
// return (
// <a
// className="list-item"
// href="#"
// data-id={search.id}
// data-type={BaseModel.TYPE_SEARCH}
// onContextMenu={event => this.itemContextMenu(event)}
// key={search.id}
// style={style}
// onClick={() => {
// this.searchItem_click(search);
// }}
// >
// {search.title}
// </a>
// );
// }
makeDivider(key) {
return <div style={{ height: 2, backgroundColor: 'blue' }} key={key} />;
}
makeHeader(key, label, iconName, extraProps = {}) {
const style = this.style().header;
const icon = <i style={{ fontSize: style.fontSize, marginRight: 5 }} className={`fas ${iconName}`} />;
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 = <i className={`fas ${isExpanded ? 'fa-chevron-down' : 'fa-chevron-right'}`} style={{ fontSize: style.fontSize * 0.75, marginRight: 12, marginLeft: 5, marginTop: style.fontSize * 0.125 }}></i>;
}
if (extraProps.selected) {
style.backgroundColor = this.style().listItemSelected.backgroundColor;
}
const ref = this.anchorItemRef('headers', key);
return (
<div
ref={ref}
style={style}
key={key}
{...extraProps}
onClick={event => {
// if a custom click event is attached, trigger that.
if (headerClick) {
headerClick(key, event);
}
this.onHeaderClick_(key, event);
}}
>
{icon}
<span style={{ flex: 1 }}>{label}</span>
{toggleIcon}
</div>
);
}
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 = <i style={iconStyle} className={`fas ${iconName}`} />;
return (
<a
className="synchronize-button"
style={style}
href="#"
key="sync_button"
onClick={() => {
CommandService.instance().execute('synchronize');
// this.sync_click();
}}
>
{icon}
{label}
</a>
);
}
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(
<div className="folders" key="folder_items" style={{ display: this.state.folderHeaderIsExpanded ? 'block' : 'none' }}>
{folderItems}
</div>
);
}
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(
<div className="tags" key="tag_items" style={{ display: this.state.tagHeaderIsExpanded ? 'block' : 'none' }}>
{tagItems}
</div>
);
}
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(
<div key={i} style={{ wordWrap: 'break-word', width: '100%' }}>
{lines[i]}
</div>
);
}
const syncButton = this.synchronizeButton(this.props.syncStarted ? 'cancel' : 'sync');
const syncReportComp = !syncReportText.length ? null : (
<div style={this.style().syncReport} key="sync_report">
{syncReportText}
</div>
);
return (
<div ref={this.rootRef} onKeyDown={this.onKeyDown} className="side-bar" style={style}>
<div style={{ flex: 1, overflowX: 'hidden', overflowY: 'auto' }}>{items}</div>
<div style={{ flex: 0 }}>
{syncReportComp}
{syncButton}
</div>
</div>
);
}
}
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 };

View File

@ -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<Props, State> {
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 <StyledNoteCount>{count}</StyledNoteCount>;
}
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 <i className={isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right'} style={style}></i>;
}
renderAllNotesItem(selected:boolean) {
return (
<StyledListItem key="allNotesHeader" selected={selected} className={'list-item-container list-item-depth-0'} isSpecialItem={true}>
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink>
<StyledListItemAnchor
className="list-item"
isSpecialItem={true}
href="#"
selected={selected}
onClick={() => {
this.onAllNotesClick_();
}}
>
({_('All notes')})
</StyledListItemAnchor>
</StyledListItem>
);
}
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 ? (
<StyledExpandLink href="#" data-folder-id={folder.id} onClick={this.onFolderToggleClick_}>
{expandIcon}
</StyledExpandLink>
) : (
<StyledExpandLink>{expandIcon}</StyledExpandLink>
);
const anchorRef = this.anchorItemRef('folder', folder.id);
const noteCount = folder.note_count ? this.renderNoteCount(folder.note_count) : '';
return (
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth}`} key={folder.id} onDragStart={this.onFolderDragStart_} onDragOver={this.onFolderDragOver_} onDrop={this.onFolderDrop_} draggable={true} data-folder-id={folder.id}>
{expandLink}
<StyledListItemAnchor
ref={anchorRef}
className="list-item"
isConflictFolder={folder.id === Folder.conflictFolderId()}
href="#"
selected={selected}
data-id={folder.id}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={(event:any) => this.itemContextMenu(event)}
data-folder-id={folder.id}
onClick={() => {
this.folderItem_click(folder);
}}
onDoubleClick={this.onFolderToggleClick_}
>
{Folder.displayTitle(folder)} {noteCount}
</StyledListItemAnchor>
</StyledListItem>
);
}
renderTag(tag:any, selected:boolean) {
const anchorRef = this.anchorItemRef('tag', tag.id);
const noteCount = Setting.value('showNoteCounts') ? this.renderNoteCount(tag.note_count) : '';
return (
<StyledListItem selected={selected} className={'list-item-container'} key={tag.id} onDrop={this.onTagDrop_} data-tag-id={tag.id}>
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink>
<StyledListItemAnchor
ref={anchorRef}
className="list-item"
href="#"
selected={selected}
data-id={tag.id}
data-type={BaseModel.TYPE_TAG}
onContextMenu={(event:any) => this.itemContextMenu(event)}
onClick={() => {
this.tagItem_click(tag);
}}
>
{Tag.displayTitle(tag)} {noteCount}
</StyledListItemAnchor>
</StyledListItem>
);
}
makeDivider(key:string) {
return <div style={{ height: 2, backgroundColor: 'blue' }} key={key} />;
}
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 (
<div key={key} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<StyledHeader
ref={ref}
{...extraProps}
onContextMenu={contextMenuHandler}
onClick={(event:any) => {
// if a custom click event is attached, trigger that.
if (headerClick) {
headerClick(key, event);
}
this.onHeaderClick_(key);
}}
>
<StyledHeaderIcon className={iconName}/>
<StyledHeaderLabel>{label}</StyledHeaderLabel>
</StyledHeader>
{ onPlusButtonClick && <StyledAddButton onClick={onPlusButtonClick} iconName="fas fa-plus" level={ButtonLevel.SideBarSecondary}/> }
</div>
);
}
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 (
<StyledSynchronizeButton
level={ButtonLevel.SideBarSecondary}
iconName="icon-sync"
key="sync_button"
iconAnimation={iconAnimation}
title={label}
onClick={() => {
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(
<div className="folders" key="folder_items" style={{ display: this.state.folderHeaderIsExpanded ? 'block' : 'none', paddingBottom: 10 }}>
{folderItems}
</div>
);
}
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(
<div className="tags" key="tag_items" style={{ display: this.state.tagHeaderIsExpanded ? 'block' : 'none' }}>
{tagItems}
</div>
);
}
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(
<StyledSyncReportText key={i}>
{lines[i]}
</StyledSyncReportText>
);
}
const syncButton = this.renderSynchronizeButton(this.props.syncStarted ? 'cancel' : 'sync');
const syncReportComp = !syncReportText.length ? null : (
<StyledSyncReport key="sync_report">
{syncReportText}
</StyledSyncReport>
);
return (
<StyledRoot ref={this.rootRef} onKeyDown={this.onKeyDown} className="side-bar">
<div style={{ flex: 1, overflowX: 'hidden', overflowY: 'auto' }}>{items}</div>
<div style={{ flex: 0, padding: theme.mainPadding }}>
{syncReportComp}
{syncButton}
</div>
</StyledRoot>
);
}
}
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 };

View File

@ -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%;
`;

View File

@ -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 (
<h2 key={`section_${key}`} style={theme.h2Style}>
{title}
</h2>
);
}
function renderSectionRetryAllHtml(key, retryAllHandler) {
return (
<a key={`retry_all_${key}`} href="#" onClick={retryAllHandler} style={retryAllStyle}>
{_('Retry All')}
</a>
);
}
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 = (
<a href="#" onClick={onClick} style={retryStyle}>
{_('Retry')}
</a>
);
}
text = item.text;
} else {
text = item;
}
if (!text) text = '\xa0';
itemsHtml.push(
<div style={theme.textStyle} key={`item_${n}`}>
<span>{text}</span>
{retryLink}
</div>
);
}
if (section.canRetryAll) {
itemsHtml.push(renderSectionRetryAllHtml(section.title, section.retryAllHandler));
}
return <div key={key}>{itemsHtml}</div>;
};
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 <div>{sectionsHtml}</div>;
}
const body = renderBodyHtml(this.state.report);
return (
<div style={style}>
<Header style={headerStyle} />
<div style={containerStyle}>
<a style={theme.textStyle} onClick={() => this.exportDebugReportClick()} href="#">
Export debug report
</a>
{body}
</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
theme: state.settings.theme,
settings: state.settings,
locale: state.settings.locale,
};
};
const StatusScreen = connect(mapStateToProps)(StatusScreenComponent);
module.exports = { StatusScreen };

View File

@ -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<any[]>([]);
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 (
<h2 key={`section_${key}`} style={theme.h2Style}>
{title}
</h2>
);
}
function renderSectionRetryAllHtml(key:string, retryAllHandler:any) {
return (
<a key={`retry_all_${key}`} href="#" onClick={retryAllHandler} style={retryAllStyle}>
{_('Retry All')}
</a>
);
}
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 = (
<a href="#" onClick={onClick} style={retryStyle}>
{_('Retry')}
</a>
);
}
text = item.text;
} else {
text = item;
}
if (!text) text = '\xa0';
itemsHtml.push(
<div style={theme.textStyle} key={`item_${n}`}>
<span>{text}</span>
{retryLink}
</div>
);
}
if (section.canRetryAll) {
itemsHtml.push(renderSectionRetryAllHtml(section.title, section.retryAllHandler));
}
return <div key={key}>{itemsHtml}</div>;
};
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 <div>{sectionsHtml}</div>;
}
const body = renderBodyHtml(report);
return (
<div style={style}>
<div style={containerStyle}>
<a style={theme.textStyle} onClick={() => exportDebugReportClick()} href="#">
Export debug report
</a>
{body}
</div>
<ButtonBar
onCancelClick={() => props.dispatch({ type: 'NAV_BACK' })}
/>
</div>
);
}
const mapStateToProps = (state:any) => {
return {
themeId: state.settings.theme,
settings: state.settings,
locale: state.settings.locale,
};
};
export default connect(mapStateToProps)(StatusScreen);

View File

@ -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);

View File

@ -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);

View File

@ -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 (
<button style={style.button} disabled={!props.toolbarButtonInfo.enabled} aria-label={props.toolbarButtonInfo.title} title={props.toolbarButtonInfo.title} type="button" className="tox-tbtn" aria-pressed="false" onClick={props.toolbarButtonInfo.onClick}>
<div style={style.leftInnerButton}>
<i style={style.leftIcon} className="fab fa-markdown"></i>
</div>
<div style={style.rightInnerButton}>
<i style={style.rightIcon} className="fas fa-edit"></i>
</div>
</button>
);
}

View File

@ -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;
});
}

View File

@ -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(<ToolbarButton {...props} />);
if (o.name === 'toggleEditors') {
rightItemComps.push(<ToggleEditorsButton
key={o.name}
value={'markdown'}
themeId={this.props.themeId}
toolbarButtonInfo={o}
/>);
} else if (itemType === 'button') {
const target = ['historyForward', 'historyBackward', 'startExternalEditing'].includes(o.name) ? leftItemComps : centerItemComps;
target.push(<ToolbarButton {...props} />);
} else if (itemType === 'separator') {
itemComps.push(<ToolbarSpace {...props} />);
centerItemComps.push(<ToolbarSpace {...props} />);
}
}
}
return (
<div className="editor-toolbar" style={style}>
{itemComps}
<div style={groupStyle}>
{leftItemComps}
</div>
<div style={groupStyle}>
{centerItemComps}
</div>
<div style={Object.assign({}, groupStyle, { flex: 1, justifyContent: 'flex-end' })}>
{rightItemComps}
</div>
</div>
);
}
}
const mapStateToProps = state => {
return { theme: state.settings.theme };
return { themeId: state.settings.theme };
};
const Toolbar = connect(mapStateToProps)(ToolbarComponent);

View File

@ -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 = <i style={iconStyle} className={`fas ${this.props.iconName}`}></i>;
}
// 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 (
<a
className={classes.join(' ')}
style={finalStyle}
title={tooltip}
href="#"
onClick={() => {
if (isEnabled && this.props.onClick) this.props.onClick();
}}
>
{icon}
{title}
</a>
);
}
}
module.exports = ToolbarButton;

View File

@ -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 = <IconClass className={iconName} title={title}/>;
}
// 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 (
<StyledRoot
className={classes.join(' ')}
disabled={!isEnabled}
title={tooltip}
href="#"
hasTitle={!!title}
onClick={() => {
if (isEnabled && onClick) onClick();
}}
>
{icon}
{title}
</StyledRoot>
);
}

View File

@ -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<RootProps>`
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<IconProps>`
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}`;

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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;

View File

@ -0,0 +1,7 @@
const styled = require('styled-components').default;
const StyledInput = styled.input`
`;
export default StyledInput;

View File

@ -9,9 +9,12 @@
-->
<title>Joplin</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style/icons/style.css">
<!-- TODO: Remove once all icons have been swapped -->
<link rel="stylesheet" href="node_modules/@fortawesome/fontawesome-free/css/all.min.css">
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
<link rel="stylesheet" href="node_modules/smalltalk/css/smalltalk.css">
<link rel="stylesheet" href="node_modules/roboto-fontface/css/roboto/roboto-fontface.css">
<link rel="stylesheet" href="node_modules/codemirror/lib/codemirror.css">
<style>

View File

@ -28,6 +28,123 @@
"integrity": "sha512-uPHXapEmUtlUKTBx4asWMlxtFUWXzEY0KVEgU7QKhgO2LJzzM3kYxM6yOyUZTtYE6mhK4dDn3FDut9SCQWHzgg==",
"optional": true
},
"@babel/code-frame": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
"integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
"requires": {
"@babel/highlight": "^7.10.4"
}
},
"@babel/generator": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz",
"integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==",
"requires": {
"@babel/types": "^7.11.0",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
},
"dependencies": {
"@babel/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}
}
},
"@babel/helper-annotate-as-pure": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz",
"integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==",
"requires": {
"@babel/types": "^7.10.4"
},
"dependencies": {
"@babel/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}
}
},
"@babel/helper-function-name": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz",
"integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==",
"requires": {
"@babel/helper-get-function-arity": "^7.10.4",
"@babel/template": "^7.10.4",
"@babel/types": "^7.10.4"
},
"dependencies": {
"@babel/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}
}
},
"@babel/helper-get-function-arity": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz",
"integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==",
"requires": {
"@babel/types": "^7.10.4"
},
"dependencies": {
"@babel/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}
}
},
"@babel/helper-module-imports": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz",
@ -36,6 +153,84 @@
"@babel/types": "^7.0.0"
}
},
"@babel/helper-split-export-declaration": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz",
"integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==",
"requires": {
"@babel/types": "^7.11.0"
},
"dependencies": {
"@babel/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}
}
},
"@babel/helper-validator-identifier": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
"integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw=="
},
"@babel/highlight": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
"integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"@babel/parser": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-qvRvi4oI8xii8NllyEc4MDJjuZiNaRzyb7Y7lup1NqJV8TZHF4O27CcP+72WPn/k1zkgJ6WJfnIbk4jTsVAZHw=="
},
"@babel/runtime": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
@ -51,6 +246,84 @@
}
}
},
"@babel/template": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
"integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/parser": "^7.10.4",
"@babel/types": "^7.10.4"
},
"dependencies": {
"@babel/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}
}
},
"@babel/traverse": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz",
"integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==",
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.11.0",
"@babel/helper-function-name": "^7.10.4",
"@babel/helper-split-export-declaration": "^7.11.0",
"@babel/parser": "^7.11.0",
"@babel/types": "^7.11.0",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.19"
},
"dependencies": {
"@babel/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
}
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}
}
},
"@babel/types": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz",
@ -172,6 +445,21 @@
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz",
"integrity": "sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ=="
},
"@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"requires": {
"@emotion/memoize": "0.7.4"
},
"dependencies": {
"@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
}
}
},
"@emotion/memoize": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz",
@ -214,6 +502,108 @@
"integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
"dev": true
},
"@styled-system/background": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz",
"integrity": "sha512-jtwH2C/U6ssuGSvwTN3ri/IyjdHb8W9X/g8Y0JLcrH02G+BW3OS8kZdHphF1/YyRklnrKrBT2ngwGUK6aqqV3A==",
"requires": {
"@styled-system/core": "^5.1.2"
}
},
"@styled-system/border": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/@styled-system/border/-/border-5.1.5.tgz",
"integrity": "sha512-JvddhNrnhGigtzWRCVuAHepniyVi6hBlimxWDVAdcTuk7aRn9BYJUwfHslURtwYFsF5FoEs8Zmr1oZq2M1AP0A==",
"requires": {
"@styled-system/core": "^5.1.2"
}
},
"@styled-system/color": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/color/-/color-5.1.2.tgz",
"integrity": "sha512-1kCkeKDZkt4GYkuFNKc7vJQMcOmTl3bJY3YBUs7fCNM6mMYJeT1pViQ2LwBSBJytj3AB0o4IdLBoepgSgGl5MA==",
"requires": {
"@styled-system/core": "^5.1.2"
}
},
"@styled-system/core": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/core/-/core-5.1.2.tgz",
"integrity": "sha512-XclBDdNIy7OPOsN4HBsawG2eiWfCcuFt6gxKn1x4QfMIgeO6TOlA2pZZ5GWZtIhCUqEPTgIBta6JXsGyCkLBYw==",
"requires": {
"object-assign": "^4.1.1"
}
},
"@styled-system/css": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/@styled-system/css/-/css-5.1.5.tgz",
"integrity": "sha512-XkORZdS5kypzcBotAMPBoeckDs9aSZVkvrAlq5K3xP8IMAUek+x2O4NtwoSgkYkWWzVBu6DGdFZLR790QWGG+A=="
},
"@styled-system/flexbox": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/flexbox/-/flexbox-5.1.2.tgz",
"integrity": "sha512-6hHV52+eUk654Y1J2v77B8iLeBNtc+SA3R4necsu2VVinSD7+XY5PCCEzBFaWs42dtOEDIa2lMrgL0YBC01mDQ==",
"requires": {
"@styled-system/core": "^5.1.2"
}
},
"@styled-system/grid": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/grid/-/grid-5.1.2.tgz",
"integrity": "sha512-K3YiV1KyHHzgdNuNlaw8oW2ktMuGga99o1e/NAfTEi5Zsa7JXxzwEnVSDSBdJC+z6R8WYTCYRQC6bkVFcvdTeg==",
"requires": {
"@styled-system/core": "^5.1.2"
}
},
"@styled-system/layout": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/layout/-/layout-5.1.2.tgz",
"integrity": "sha512-wUhkMBqSeacPFhoE9S6UF3fsMEKFv91gF4AdDWp0Aym1yeMPpqz9l9qS/6vjSsDPF7zOb5cOKC3tcKKOMuDCPw==",
"requires": {
"@styled-system/core": "^5.1.2"
}
},
"@styled-system/position": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/position/-/position-5.1.2.tgz",
"integrity": "sha512-60IZfMXEOOZe3l1mCu6sj/2NAyUmES2kR9Kzp7s2D3P4qKsZWxD1Se1+wJvevb+1TP+ZMkGPEYYXRyU8M1aF5A==",
"requires": {
"@styled-system/core": "^5.1.2"
}
},
"@styled-system/shadow": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/shadow/-/shadow-5.1.2.tgz",
"integrity": "sha512-wqniqYb7XuZM7K7C0d1Euxc4eGtqEe/lvM0WjuAFsQVImiq6KGT7s7is+0bNI8O4Dwg27jyu4Lfqo/oIQXNzAg==",
"requires": {
"@styled-system/core": "^5.1.2"
}
},
"@styled-system/space": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/space/-/space-5.1.2.tgz",
"integrity": "sha512-+zzYpR8uvfhcAbaPXhH8QgDAV//flxqxSjHiS9cDFQQUSznXMQmxJegbhcdEF7/eNnJgHeIXv1jmny78kipgBA==",
"requires": {
"@styled-system/core": "^5.1.2"
}
},
"@styled-system/typography": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/typography/-/typography-5.1.2.tgz",
"integrity": "sha512-BxbVUnN8N7hJ4aaPOd7wEsudeT7CxarR+2hns8XCX1zp0DFfbWw4xYa/olA0oQaqx7F1hzDg+eRaGzAJbF+jOg==",
"requires": {
"@styled-system/core": "^5.1.2"
}
},
"@styled-system/variant": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/@styled-system/variant/-/variant-5.1.5.tgz",
"integrity": "sha512-Yn8hXAFoWIro8+Q5J8YJd/mP85Teiut3fsGVR9CAxwgNfIAiqlYxsk5iHU7VHJks/0KjL4ATSjmbtCDC/4l1qw==",
"requires": {
"@styled-system/core": "^5.1.2",
"@styled-system/css": "^5.1.5"
}
},
"@szmarczak/http-timer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
@ -1145,7 +1535,8 @@
},
"mkdirp": {
"version": "0.5.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"optional": true,
"requires": {
@ -1310,7 +1701,8 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true,
"optional": true
}
@ -1425,7 +1817,8 @@
},
"tar": {
"version": "4.4.8",
"resolved": "",
"resolved": false,
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
"dev": true,
"optional": true,
"requires": {
@ -1652,6 +2045,17 @@
"resolve": "^1.10.0"
}
},
"babel-plugin-styled-components": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.11.1.tgz",
"integrity": "sha512-YwrInHyKUk1PU3avIRdiLyCpM++18Rs1NgyMXEAQC33rIXs/vro0A+stf4sT0Gf22Got+xRWB8Cm0tw+qkRzBA==",
"requires": {
"@babel/helper-annotate-as-pure": "^7.0.0",
"@babel/helper-module-imports": "^7.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"lodash": "^4.17.11"
}
},
"babel-plugin-syntax-flow": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz",
@ -2432,6 +2836,11 @@
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
},
"camelize": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
@ -3123,6 +3532,21 @@
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
"integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI="
},
"css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
},
"css-to-react-native": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
"integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
"requires": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"cssom": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@ -3465,6 +3889,11 @@
"whatwg-url": "^7.0.0"
}
},
"debounce": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -4312,8 +4741,7 @@
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"escaper": {
"version": "2.5.3",
@ -4652,6 +5080,11 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
},
"fast-memoize": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz",
"integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="
},
"fbjs": {
"version": "0.8.16",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz",
@ -5440,7 +5873,9 @@
"minimist": {
"version": "0.0.8",
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true,
"optional": true
},
"minipass": {
"version": "2.9.0",
@ -5464,9 +5899,14 @@
}
},
"mkdirp": {
"version": "0.5.5",
"version": "0.5.1",
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"optional": true
"optional": true,
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "2.1.2",
@ -5636,7 +6076,8 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true,
"optional": true
}
@ -6194,8 +6635,7 @@
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"has-symbols": {
"version": "1.0.1",
@ -9021,6 +9461,11 @@
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
"dev": true
},
"postcss-value-parser": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
},
"prebuild-install": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz",
@ -9260,6 +9705,14 @@
}
}
},
"re-resizable": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.5.4.tgz",
"integrity": "sha512-7T3L1lexB2zkZIDmzRJbwdq+xGFuRkrEVQIf5hBPnh7JuS9kG9Yc8XgIaxTWic1kU7jVlDgqzfId/gvmpBCjpA==",
"requires": {
"fast-memoize": "^2.5.1"
}
},
"react": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.9.0.tgz",
@ -9906,6 +10359,11 @@
}
}
},
"roboto-fontface": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
},
"rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
@ -10078,6 +10536,11 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@ -10559,6 +11022,71 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
},
"styled-components": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.1.1.tgz",
"integrity": "sha512-1ps8ZAYu2Husx+Vz8D+MvXwEwvMwFv+hqqUwhNlDN5ybg6A+3xyW1ECrAgywhvXapNfXiz79jJyU0x22z0FFTg==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/traverse": "^7.4.5",
"@emotion/is-prop-valid": "^0.8.8",
"@emotion/stylis": "^0.8.4",
"@emotion/unitless": "^0.7.4",
"babel-plugin-styled-components": ">= 1",
"css-to-react-native": "^3.0.0",
"hoist-non-react-statics": "^3.0.0",
"shallowequal": "^1.1.0",
"supports-color": "^5.5.0"
},
"dependencies": {
"@emotion/stylis": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
},
"@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"styled-system": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/styled-system/-/styled-system-5.1.5.tgz",
"integrity": "sha512-7VoD0o2R3RKzOzPK0jYrVnS8iJdfkKsQJNiLRDjikOpQVqQHns/DXWaPZOH4tIKkhAT7I6wIsy9FWTWh2X3q+A==",
"requires": {
"@styled-system/background": "^5.1.2",
"@styled-system/border": "^5.1.5",
"@styled-system/color": "^5.1.2",
"@styled-system/core": "^5.1.2",
"@styled-system/flexbox": "^5.1.2",
"@styled-system/grid": "^5.1.2",
"@styled-system/layout": "^5.1.2",
"@styled-system/position": "^5.1.2",
"@styled-system/shadow": "^5.1.2",
"@styled-system/space": "^5.1.2",
"@styled-system/typography": "^5.1.2",
"@styled-system/variant": "^5.1.5",
"object-assign": "^4.1.1"
}
},
"stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",

View File

@ -125,6 +125,7 @@
"color": "^3.1.2",
"compare-versions": "^3.2.1",
"countable": "^3.0.1",
"debounce": "^1.2.0",
"diacritics": "^1.3.0",
"diff-match-patch": "^1.0.4",
"electron-context-menu": "^0.15.0",
@ -177,6 +178,7 @@
"pretty-bytes": "^5.3.0",
"promise": "^8.0.1",
"query-string": "^5.1.1",
"re-resizable": "^6.5.4",
"react": "^16.9.0",
"react-datetime": "^2.14.0",
"react-dom": "^16.9.0",
@ -188,6 +190,7 @@
"redux": "^3.7.2",
"relative": "^3.0.2",
"reselect": "^4.0.0",
"roboto-fontface": "^0.10.0",
"sax": "^1.2.4",
"server-destroy": "^1.0.1",
"smalltalk": "^2.5.1",
@ -195,6 +198,8 @@
"sqlite3": "^4.1.1",
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.1",
"styled-components": "^5.1.1",
"styled-system": "^5.1.5",
"syswide-cas": "^5.1.0",
"taboverride": "^4.0.3",
"tar": "^4.4.4",

View File

@ -33,6 +33,8 @@ class Dialog extends React.PureComponent {
constructor() {
super();
this.fuzzy_ = false;
this.state = {
query: '',
results: [],
@ -58,11 +60,11 @@ class Dialog extends React.PureComponent {
}
style() {
const styleKey = [this.props.theme, this.state.resultsInBody ? '1' : '0'].join('-');
const styleKey = [this.props.themeId, this.state.resultsInBody ? '1' : '0'].join('-');
if (this.styles_[styleKey]) return this.styles_[styleKey];
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
const itemHeight = this.state.resultsInBody ? 84 : 64;
@ -178,7 +180,7 @@ class Dialog extends React.PureComponent {
}
async keywords(searchQuery) {
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, false);
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, this.fuzzy_);
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
}
@ -215,7 +217,7 @@ class Dialog extends React.PureComponent {
} else { // Note TITLE or BODY
listType = BaseModel.TYPE_NOTE;
searchQuery = this.makeSearchQuery(this.state.query);
results = await SearchEngine.instance().search(searchQuery);
results = await SearchEngine.instance().search(searchQuery, { fuzzy: this.fuzzy_ });
resultsInBody = !!results.find(row => row.fields.includes('body'));
@ -341,7 +343,7 @@ class Dialog extends React.PureComponent {
}
listItemRenderer(item) {
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
const style = this.style();
const rowStyle = item.id === this.state.selectedItemId ? style.rowSelected : style.row;
const titleHtml = item.fragments
@ -430,7 +432,7 @@ class Dialog extends React.PureComponent {
}
render() {
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
const style = this.style();
const helpComp = !this.state.showHelp ? null : <div style={style.help}>{_('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name.')}</div>;
@ -453,7 +455,7 @@ class Dialog extends React.PureComponent {
const mapStateToProps = (state) => {
return {
folders: state.folders,
theme: state.settings.theme,
themeId: state.settings.theme,
showCompletedTodos: state.settings.showCompletedTodos,
highlightedWords: state.highlightedWords,
};

View File

@ -38,18 +38,18 @@ a {
::-webkit-scrollbar-track {
border: none;
}
::-webkit-scrollbar-thumb {
background: rgba(100, 100, 100, 0.3);
background: rgba(100, 100, 100, 0.3);
border-radius: 5px;
}
::-webkit-scrollbar-track:hover {
background: rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(100, 100, 100, 0.7);
background: rgba(100, 100, 100, 0.7);
}
.fade_out {
@ -64,20 +64,13 @@ a {
opacity: 1;
}
/*
.note-list .list-item-container:hover {
background-color: rgba(0,160,255,0.1) !important;
}
*/
/*.side-bar .list-item:hover,
.side-bar .synchronize-button:hover {
background-color: #01427B;
}
.side-bar .list-item:active,
.side-bar .synchronize-button:active {
background-color: #0465BB;
}*/
/*
.editor-toolbar .button:not(.disabled):hover,
.header .button:not(.disabled):hover {
background-color: rgba(0,160,255,0.1);
@ -91,6 +84,7 @@ a {
border: 1px solid rgba(0,160,255,0.7);
box-sizing: 'border-box';
}
*/
.editor-toolbar .button,
.header .button {
@ -163,3 +157,12 @@ a {
from {transform: rotate(0deg);}
to {transform: rotate(360deg);}
}
/* .joplin-tinymce .tox-editor-header {
padding-left: 88px;
padding-right: 150px;
} */
*:focus {
outline: none;
}

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,162 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?f410v3');
src: url('fonts/icomoon.eot?f410v3#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?f410v3') format('truetype'),
url('fonts/icomoon.woff?f410v3') format('woff'),
url('fonts/icomoon.svg?f410v3#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-error:before {
content: "\e900";
}
.icon-attention:before {
content: "\e901";
}
.icon-collapse:before {
content: "\e902";
}
.icon-cancel:before {
content: "\e903";
}
.icon-expand:before {
content: "\e904";
}
.icon-notebooks:before {
content: "\e905";
}
.icon-plus:before {
content: "\e906";
}
.icon-update:before {
content: "\e907";
}
.icon-tags:before {
content: "\e908";
}
.icon-search:before {
content: "\e909";
}
.icon-notes:before {
content: "\e90a";
}
.icon-sync:before {
content: "\e90b";
}
.icon-general:before {
content: "\e90c";
}
.icon-note:before {
content: "\e90d";
}
.icon-note-history:before {
content: "\e90e";
}
.icon-application:before {
content: "\e90f";
}
.icon-encryption:before {
content: "\e910";
}
.icon-plugins:before {
content: "\e911";
}
.icon-web-clipper:before {
content: "\e912";
}
.icon-appearance:before {
content: "\e913";
}
.icon-code:before {
content: "\e914";
}
.icon-asterisks:before {
content: "\e915";
}
.icon-bold:before {
content: "\e916";
}
.icon-bulleted-list:before {
content: "\e917";
}
.icon-embed-code:before {
content: "\e918";
}
.icon-back:before {
content: "\e919";
}
.icon-forward:before {
content: "\e91a";
}
.icon-h1:before {
content: "\e91b";
}
.icon-h2:before {
content: "\e91c";
}
.icon-h3:before {
content: "\e91d";
}
.icon-heading:before {
content: "\e91e";
}
.icon-info:before {
content: "\e91f";
}
.icon-italic:before {
content: "\e920";
}
.icon-layout:before {
content: "\e921";
}
.icon-line:before {
content: "\e922";
}
.icon-link:before {
content: "\e923";
}
.icon-more:before {
content: "\e924";
}
.icon-numbered-list:before {
content: "\e925";
}
.icon-quote:before {
content: "\e926";
}
.icon-alarm:before {
content: "\e927";
}
.icon-share:before {
content: "\e928";
}
.icon-table:before {
content: "\e929";
}
.icon-to-do-list:before {
content: "\e92a";
}
.icon-add-date:before {
content: "\e92b";
}
.icon-attachment:before {
content: "\e92c";
}

View File

@ -43,9 +43,7 @@ function convertJsx(path) {
module.exports = function() {
convertJsx(`${__dirname}/../gui`);
convertJsx(`${__dirname}/../gui/SideBar`);
convertJsx(`${__dirname}/../gui/MainScreen`);
convertJsx(`${__dirname}/../gui/Header`);
convertJsx(`${__dirname}/../gui/NoteList`);
convertJsx(`${__dirname}/../plugins`);

View File

@ -292,8 +292,8 @@ class BaseApplication {
notes = await Tag.notes(parentId, options);
} else if (parentType === BaseModel.TYPE_SEARCH) {
const search = BaseModel.byId(state.searches, parentId);
notes = await SearchEngineUtils.notesForQuery(search.query_pattern, { fuzzy: search.fuzzy });
const parsedQuery = await SearchEngine.instance().parseQuery(search.query_pattern, search.fuzzy);
notes = await SearchEngineUtils.notesForQuery(search.query_pattern);
const parsedQuery = await SearchEngine.instance().parseQuery(search.query_pattern);
highlightedWords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
} else if (parentType === BaseModel.TYPE_SMART_FILTER) {
notes = await Note.previews(parentId, options);

View File

@ -4,7 +4,8 @@ const { _ } = require('lib/locale');
export const declaration:CommandDeclaration = {
name: 'historyBackward',
label: () => _('Back'),
iconName: 'fa-arrow-left',
// iconName: 'fa-arrow-left',
iconName: 'icon-back',
};
interface Props {

View File

@ -4,7 +4,7 @@ const { _ } = require('lib/locale');
export const declaration:CommandDeclaration = {
name: 'historyForward',
label: () => _('Forward'),
iconName: 'fa-arrow-right',
iconName: 'icon-forward',
};
interface Props {

View File

@ -10,7 +10,7 @@ class ModalDialog extends React.Component {
}
styles() {
const themeId = this.props.theme;
const themeId = this.props.themeId;
const theme = themeStyle(themeId);
if (this.styles_[themeId]) return this.styles_[themeId];

View File

@ -63,7 +63,7 @@ class AppNavComponent extends Component {
this.previousRouteName_ = route.routeName;
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
const style = { flex: 1, backgroundColor: theme.backgroundColor };
@ -81,7 +81,7 @@ class AppNavComponent extends Component {
const AppNav = connect(state => {
return {
route: state.route,
theme: state.settings.theme,
themeId: state.settings.theme,
};
})(AppNavComponent);

View File

@ -44,7 +44,7 @@ class NoteBodyViewer extends Component {
this.forceUpdate_ = false;
const note = this.props.note;
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
const bodyToRender = note ? note.body : '';

View File

@ -22,9 +22,9 @@ class NoteItemComponent extends Component {
}
styles() {
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
if (this.styles_[this.props.theme]) return this.styles_[this.props.theme];
if (this.styles_[this.props.themeId]) return this.styles_[this.props.themeId];
this.styles_ = {};
const styles = {
@ -62,8 +62,8 @@ class NoteItemComponent extends Component {
styles.selectionWrapperSelected = Object.assign({}, styles.selectionWrapper);
styles.selectionWrapperSelected.backgroundColor = theme.selectedColor;
this.styles_[this.props.theme] = StyleSheet.create(styles);
return this.styles_[this.props.theme];
this.styles_[this.props.themeId] = StyleSheet.create(styles);
return this.styles_[this.props.themeId];
}
async todoCheckbox_change(checked) {
@ -107,7 +107,7 @@ class NoteItemComponent extends Component {
const note = this.props.note ? this.props.note : {};
const isTodo = !!Number(note.is_todo);
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
// IOS: display: none crashes the app
const checkboxStyle = !isTodo ? { display: 'none' } : { color: theme.color };
@ -145,7 +145,7 @@ class NoteItemComponent extends Component {
const NoteItem = connect(state => {
return {
theme: state.settings.theme,
themeId: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
};

Some files were not shown because too many files have changed in this diff Show More