diff --git a/.eslintignore b/.eslintignore
index 22f33b046..f6f4ba55d 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -235,6 +235,9 @@ packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
packages/app-desktop/gui/ErrorBoundary.d.ts
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ErrorBoundary.js.map
+packages/app-desktop/gui/FolderIconBox.d.ts
+packages/app-desktop/gui/FolderIconBox.js
+packages/app-desktop/gui/FolderIconBox.js.map
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.d.ts
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js.map
diff --git a/.gitignore b/.gitignore
index 3f8358219..e63cef53b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -225,6 +225,9 @@ packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
packages/app-desktop/gui/ErrorBoundary.d.ts
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ErrorBoundary.js.map
+packages/app-desktop/gui/FolderIconBox.d.ts
+packages/app-desktop/gui/FolderIconBox.js
+packages/app-desktop/gui/FolderIconBox.js.map
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.d.ts
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js.map
diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts
index 48634e0f7..ddd972007 100644
--- a/packages/app-desktop/bridge.ts
+++ b/packages/app-desktop/bridge.ts
@@ -9,6 +9,13 @@ interface LastSelectedPath {
directory: string;
}
+interface OpenDialogOptions {
+ properties?: string[];
+ defaultPath?: string;
+ createDirectory?: boolean;
+ filters?: any[];
+}
+
export class Bridge {
private electronWrapper_: ElectronAppWrapper;
@@ -155,14 +162,14 @@ export class Bridge {
return filePath;
}
- async showOpenDialog(options: any = null) {
+ async showOpenDialog(options: OpenDialogOptions = null) {
const { dialog } = require('electron');
if (!options) options = {};
let fileType = 'file';
if (options.properties && options.properties.includes('openDirectory')) fileType = 'directory';
if (!('defaultPath' in options) && (this.lastSelectedPaths_ as any)[fileType]) options.defaultPath = (this.lastSelectedPaths_ as any)[fileType];
if (!('createDirectory' in options)) options.createDirectory = true;
- const { filePaths } = await dialog.showOpenDialog(this.window(), options);
+ const { filePaths } = await dialog.showOpenDialog(this.window(), options as any);
if (filePaths && filePaths.length) {
(this.lastSelectedPaths_ as any)[fileType] = dirname(filePaths[0]);
}
diff --git a/packages/app-desktop/gui/EditFolderDialog/Dialog.tsx b/packages/app-desktop/gui/EditFolderDialog/Dialog.tsx
index cb73c1921..9852be10f 100644
--- a/packages/app-desktop/gui/EditFolderDialog/Dialog.tsx
+++ b/packages/app-desktop/gui/EditFolderDialog/Dialog.tsx
@@ -8,9 +8,11 @@ import StyledInput from '../style/StyledInput';
import { IconSelector, ChangeEvent } from './IconSelector';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import Folder from '@joplin/lib/models/Folder';
-import { FolderEntity, FolderIcon } from '@joplin/lib/services/database/types';
+import { FolderEntity, FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
import Button from '../Button/Button';
import bridge from '../../services/bridge';
+import shim from '@joplin/lib/shim';
+import FolderIconBox from '../FolderIconBox';
interface Props {
themeId: number;
@@ -93,6 +95,27 @@ export default function(props: Props) {
setFolderIcon(null);
}, []);
+ const onBrowseClick = useCallback(async () => {
+ const filePaths = await bridge().showOpenDialog();
+ if (filePaths.length !== 1) return;
+ const filePath = filePaths[0];
+
+ try {
+ const dataUrl = await shim.imageToDataUrl(filePath, 256);
+ setFolderIcon(icon => {
+ return {
+ ...icon,
+ emoji: '',
+ name: '',
+ type: FolderIconType.DataUrl,
+ dataUrl,
+ };
+ });
+ } catch (error) {
+ await bridge().showErrorMessageBox(error.message);
+ }
+ }, []);
+
function renderForm() {
return (
@@ -105,11 +128,14 @@ export default function(props: Props) {
+ { folderIcon &&
}
-
diff --git a/packages/app-desktop/gui/EditFolderDialog/IconSelector.tsx b/packages/app-desktop/gui/EditFolderDialog/IconSelector.tsx
index 8c2979a20..756df1677 100644
--- a/packages/app-desktop/gui/EditFolderDialog/IconSelector.tsx
+++ b/packages/app-desktop/gui/EditFolderDialog/IconSelector.tsx
@@ -3,7 +3,7 @@ import { useEffect, useState, useCallback, useRef } from 'react';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import { loadScript } from '../utils/loadScript';
import Button from '../Button/Button';
-import { FolderIcon } from '@joplin/lib/services/database/types';
+import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
import bridge from '../../services/bridge';
export interface ChangeEvent {
@@ -15,6 +15,7 @@ type ChangeHandler = (event: ChangeEvent)=> void;
interface Props {
onChange: ChangeHandler;
icon: FolderIcon | null;
+ title: string;
}
export const IconSelector = (props: Props) => {
@@ -62,7 +63,7 @@ export const IconSelector = (props: Props) => {
});
const onEmoji = (selection: FolderIcon) => {
- props.onChange({ value: selection });
+ props.onChange({ value: { ...selection, type: FolderIconType.Emoji } });
};
p.on('emoji', onEmoji);
@@ -78,16 +79,25 @@ export const IconSelector = (props: Props) => {
picker.togglePicker(buttonRef.current);
}, [picker]);
- const buttonText = props.icon ? props.icon.emoji : '...';
+ // const buttonText = props.icon ? props.icon.emoji : '...';
return (
);
+
+ // return (
+ //
+ // );
};
diff --git a/packages/app-desktop/gui/EditFolderDialog/style.scss b/packages/app-desktop/gui/EditFolderDialog/style.scss
index 9af2574a3..538ad3673 100644
--- a/packages/app-desktop/gui/EditFolderDialog/style.scss
+++ b/packages/app-desktop/gui/EditFolderDialog/style.scss
@@ -1,4 +1,9 @@
.icon-selector-row {
display: flex;
flex-direction: row;
+ align-items: center;
+}
+
+.icon-selector-row > .foldericon {
+ margin-right: 5px;
}
\ No newline at end of file
diff --git a/packages/app-desktop/gui/FolderIconBox.tsx b/packages/app-desktop/gui/FolderIconBox.tsx
new file mode 100644
index 000000000..f1cc2ee0d
--- /dev/null
+++ b/packages/app-desktop/gui/FolderIconBox.tsx
@@ -0,0 +1,17 @@
+import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
+
+interface Props {
+ folderIcon: FolderIcon;
+}
+
+export default function(props: Props) {
+ const folderIcon = props.folderIcon;
+
+ if (folderIcon.type === FolderIconType.Emoji) {
+ return {folderIcon.emoji};
+ } else if (folderIcon.type === FolderIconType.DataUrl) {
+ return ;
+ } else {
+ throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
+ }
+}
diff --git a/packages/app-desktop/gui/Sidebar/Sidebar.tsx b/packages/app-desktop/gui/Sidebar/Sidebar.tsx
index 0233be4b4..3c9df3202 100644
--- a/packages/app-desktop/gui/Sidebar/Sidebar.tsx
+++ b/packages/app-desktop/gui/Sidebar/Sidebar.tsx
@@ -17,11 +17,12 @@ import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
import Tag from '@joplin/lib/models/Tag';
import Logger from '@joplin/lib/Logger';
-import { FolderEntity } from '@joplin/lib/services/database/types';
+import { FolderEntity, FolderIcon } from '@joplin/lib/services/database/types';
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
import { store } from '@joplin/lib/reducer';
import PerFolderSortOrderService from '../../services/sortOrder/PerFolderSortOrderService';
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
+import FolderIconBox from '../FolderIconBox';
const { connect } = require('react-redux');
const shared = require('@joplin/lib/components/shared/side-menu-shared.js');
const { themeStyle } = require('@joplin/lib/theme');
@@ -77,6 +78,12 @@ function ExpandLink(props: any) {
);
}
+const renderFolderIcon = (folderIcon: FolderIcon) => {
+ if (!folderIcon) return null;
+
+ return
;
+};
+
function FolderItem(props: any) {
const { hasChildren, isExpanded, parentId, depth, selected, folderId, folderTitle, folderIcon, anchorRef, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
@@ -84,8 +91,6 @@ function FolderItem(props: any) {
const shareIcon = shareId && !parentId ? : null;
- const icon = folderIcon ? {folderIcon.emoji} : null;
-
return (
@@ -105,7 +110,7 @@ function FolderItem(props: any) {
}}
onDoubleClick={onFolderToggleClick_}
>
- {icon}{folderTitle}
+ {renderFolderIcon(folderIcon)}{folderTitle}
{shareIcon} {noteCountComp}
diff --git a/packages/app-mobile/components/side-menu-content.js b/packages/app-mobile/components/side-menu-content.js
index 155a71073..120a21cee 100644
--- a/packages/app-mobile/components/side-menu-content.js
+++ b/packages/app-mobile/components/side-menu-content.js
@@ -1,6 +1,6 @@
const React = require('react');
const Component = React.Component;
-const { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Alert } = require('react-native');
+const { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Alert, Image } = require('react-native');
const { connect } = require('react-redux');
const Icon = require('react-native-vector-icons/Ionicons').default;
const Folder = require('@joplin/lib/models/Folder').default;
@@ -74,7 +74,7 @@ class SideMenuContentComponent extends Component {
styles.folderButton = Object.assign({}, styles.button);
styles.folderButton.paddingLeft = 0;
- styles.folderButtonText = Object.assign({}, styles.buttonText);
+ styles.folderButtonText = Object.assign({}, styles.buttonText, { paddingLeft: 0 });
styles.folderButtonSelected = Object.assign({}, styles.folderButton);
styles.folderButtonSelected.backgroundColor = theme.selectedColor;
styles.folderIcon = Object.assign({}, theme.icon);
@@ -219,6 +219,18 @@ class SideMenuContentComponent extends Component {
if (actionDone === 'auth') this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
}
+ renderFolderIcon(theme, folderIcon) {
+ if (!folderIcon) return null;
+
+ if (folderIcon.type === 1) { // FolderIconType.Emoji
+ return {folderIcon.emoji};
+ } else if (folderIcon.type === 2) { // FolderIconType.DataUrl
+ return ;
+ } else {
+ throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
+ }
+ }
+
renderFolderItem(folder, selected, hasChildren, depth) {
const theme = themeStyle(this.props.themeId);
@@ -228,6 +240,7 @@ class SideMenuContentComponent extends Component {
height: 36,
alignItems: 'center',
paddingRight: theme.marginRight,
+ paddingLeft: 10,
};
if (selected) folderButtonStyle.backgroundColor = theme.selectedColor;
folderButtonStyle.paddingLeft = depth * 10 + theme.marginLeft;
@@ -253,7 +266,6 @@ class SideMenuContentComponent extends Component {
);
const folderIcon = Folder.unserializeIcon(folder.icon);
- const icon = folderIcon ? `${folderIcon.emoji} ` : '';
return (
@@ -267,8 +279,9 @@ class SideMenuContentComponent extends Component {
}}
>
+ {this.renderFolderIcon(theme, folderIcon)}
- {icon + Folder.displayTitle(folder)}
+ {Folder.displayTitle(folder)}
diff --git a/packages/lib/models/Folder.ts b/packages/lib/models/Folder.ts
index ab0744d8f..2a84316d7 100644
--- a/packages/lib/models/Folder.ts
+++ b/packages/lib/models/Folder.ts
@@ -1,4 +1,4 @@
-import { FolderEntity, FolderIcon, NoteEntity } from '../services/database/types';
+import { defaultFolderIcon, FolderEntity, FolderIcon, NoteEntity } from '../services/database/types';
import BaseModel, { DeleteOptions } from '../BaseModel';
import time from '../time';
import { _ } from '../locale';
@@ -767,7 +767,11 @@ export default class Folder extends BaseItem {
}
public static unserializeIcon(icon: string): FolderIcon {
- return icon ? JSON.parse(icon) : null;
+ if (!icon) return null;
+ return {
+ ...defaultFolderIcon(),
+ ...JSON.parse(icon),
+ };
}
}
diff --git a/packages/lib/services/database/types.ts b/packages/lib/services/database/types.ts
index 2e0ea7f93..fd46f0145 100644
--- a/packages/lib/services/database/types.ts
+++ b/packages/lib/services/database/types.ts
@@ -15,9 +15,26 @@ export interface BaseItemEntity {
created_time?: number;
}
+export enum FolderIconType {
+ Emoji = 1,
+ DataUrl = 2,
+}
+
export interface FolderIcon {
+ type: FolderIconType;
emoji: string;
name: string;
+ dataUrl: string;
+}
+
+export const defaultFolderIcon = () => {
+ const icon:FolderIcon = {
+ type: FolderIconType.Emoji,
+ emoji: '',
+ name: '',
+ dataUrl: '',
+ };
+ return icon;
}
diff --git a/packages/lib/shim-init-node.js b/packages/lib/shim-init-node.js
index 960982016..4cc1e255f 100644
--- a/packages/lib/shim-init-node.js
+++ b/packages/lib/shim-init-node.js
@@ -330,6 +330,23 @@ function shimInit(options = null) {
return Note.save(newNote);
};
+ shim.imageToDataUrl = async (filePath, maxSize) => {
+ if (shim.isElectron()) {
+ const nativeImage = require('electron').nativeImage;
+ const image = nativeImage.createFromPath(filePath);
+ if (!image) throw new Error(`Could not load image: ${filePath}`);
+
+ if (maxSize) {
+ const size = image.getSize();
+ if (size.width > maxSize || size.height > maxSize) throw new Error(`Image cannot be larger than ${maxSize}x${maxSize} pixels`);
+ }
+
+ return image.toDataURL();
+ } else {
+ throw new Error('Unsupported method');
+ }
+ },
+
shim.imageFromDataUrl = async function(imageDataUrl, filePath, options = null) {
if (options === null) options = {};
diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts
index e65d732a7..a9495ad29 100644
--- a/packages/lib/shim.ts
+++ b/packages/lib/shim.ts
@@ -228,6 +228,10 @@ const shim = {
throw new Error('Not implemented');
},
+ imageToDataUrl: async (_filePath: string, _maxSize: number = 0): Promise => {
+ throw new Error('Not implemented');
+ },
+
imageFromDataUrl: async (_imageDataUrl: string, _filePath: string, _options: any = null) => {
throw new Error('Not implemented');
},