mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Desktop: Add support for custom notebook icons (#6110)
This commit is contained in:
parent
db497ee0a5
commit
9f252ea673
@ -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
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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 (
|
||||
<div>
|
||||
@ -105,11 +128,14 @@ export default function(props: Props) {
|
||||
<div className="form-input-group">
|
||||
<label>{_('Icon')}</label>
|
||||
<div className="icon-selector-row">
|
||||
{ folderIcon && <div className="foldericon"><FolderIconBox folderIcon={folderIcon} /></div> }
|
||||
<IconSelector
|
||||
title={_('Select emoji...')}
|
||||
icon={folderIcon}
|
||||
onChange={onFolderIconChange}
|
||||
/>
|
||||
<Button ml={1} title={_('Clear')} onClick={onClearClick}/>
|
||||
<Button ml={1} title={_('Select file...')} onClick={onBrowseClick}/>
|
||||
{ folderIcon && <Button ml={1} title={_('Clear')} onClick={onClearClick}/> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 (
|
||||
<Button
|
||||
disabled={!picker}
|
||||
ref={buttonRef}
|
||||
onClick={onClick}
|
||||
title={buttonText}
|
||||
isSquare={true}
|
||||
fontSize={20}
|
||||
title={props.title}
|
||||
/>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <Button
|
||||
// disabled={!picker}
|
||||
// ref={buttonRef}
|
||||
// onClick={onClick}
|
||||
// title={buttonText}
|
||||
// isSquare={true}
|
||||
// fontSize={20}
|
||||
// />
|
||||
// );
|
||||
};
|
||||
|
@ -1,4 +1,9 @@
|
||||
.icon-selector-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon-selector-row > .foldericon {
|
||||
margin-right: 5px;
|
||||
}
|
17
packages/app-desktop/gui/FolderIconBox.tsx
Normal file
17
packages/app-desktop/gui/FolderIconBox.tsx
Normal file
@ -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 <span style={{ fontSize: 20 }}>{folderIcon.emoji}</span>;
|
||||
} else if (folderIcon.type === FolderIconType.DataUrl) {
|
||||
return <img style={{ width: 20, height: 20 }} src={folderIcon.dataUrl} />;
|
||||
} else {
|
||||
throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
|
||||
}
|
||||
}
|
@ -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 <div style={{ marginRight: 5, display: 'flex' }}><FolderIconBox folderIcon={folderIcon}/></div>;
|
||||
};
|
||||
|
||||
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 ? <StyledShareIcon className="fas fa-share-alt"></StyledShareIcon> : null;
|
||||
|
||||
const icon = folderIcon ? <span style={{ fontSize: 20, marginRight: 5 }}>{folderIcon.emoji}</span> : null;
|
||||
|
||||
return (
|
||||
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth} ${selected ? 'selected' : ''}`} onDragStart={onFolderDragStart_} onDragOver={onFolderDragOver_} onDrop={onFolderDrop_} draggable={true} data-folder-id={folderId}>
|
||||
<ExpandLink themeId={props.themeId} hasChildren={hasChildren} folderId={folderId} onClick={onFolderToggleClick_} isExpanded={isExpanded}/>
|
||||
@ -105,7 +110,7 @@ function FolderItem(props: any) {
|
||||
}}
|
||||
onDoubleClick={onFolderToggleClick_}
|
||||
>
|
||||
{icon}<span className="title" style={{ lineHeight: 0 }}>{folderTitle}</span>
|
||||
{renderFolderIcon(folderIcon)}<span className="title" style={{ lineHeight: 0 }}>{folderTitle}</span>
|
||||
{shareIcon} {noteCountComp}
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
|
@ -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 <Text style={{ fontSize: theme.fontSize, marginRight: 4 }}>{folderIcon.emoji}</Text>;
|
||||
} else if (folderIcon.type === 2) { // FolderIconType.DataUrl
|
||||
return <Image style={{ width: 20, height: 20, marginRight: 4, resizeMode: 'contain' }} source={{ uri: folderIcon.dataUrl }}/>;
|
||||
} 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 (
|
||||
<View key={folder.id} style={{ flex: 1, flexDirection: 'row' }}>
|
||||
@ -267,8 +279,9 @@ class SideMenuContentComponent extends Component {
|
||||
}}
|
||||
>
|
||||
<View style={folderButtonStyle}>
|
||||
{this.renderFolderIcon(theme, folderIcon)}
|
||||
<Text numberOfLines={1} style={this.styles().folderButtonText}>
|
||||
{icon + Folder.displayTitle(folder)}
|
||||
{Folder.displayTitle(folder)}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 = {};
|
||||
|
||||
|
@ -228,6 +228,10 @@ const shim = {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
|
||||
imageToDataUrl: async (_filePath: string, _maxSize: number = 0): Promise<string> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
|
||||
imageFromDataUrl: async (_imageDataUrl: string, _filePath: string, _options: any = null) => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user