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 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("../theme.js"); const { bridge } = require("electron").remote.require("./bridge"); const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; const InteropServiceHelper = require("../InteropServiceHelper.js"); const { shim } = require('lib/shim'); class SideBarComponent extends React.Component { constructor() { super(); 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; 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 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.state = { tagHeaderIsExpanded: Setting.value('tagHeaderIsExpanded'), folderHeaderIsExpanded: Setting.value('folderHeaderIsExpanded') }; } style() { const theme = themeStyle(this.props.theme); const itemHeight = 25; let style = { root: { backgroundColor: theme.backgroundColor2, }, listItemContainer: { boxSizing: "border-box", height: itemHeight, // paddingLeft: 14, display: "flex", alignItems: "stretch", }, 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', }, listItemSelected: { backgroundColor: theme.selectedColor2, }, listItemExpandIcon: { color: theme.color2, cursor: "default", opacity: 0.8, // fontFamily: theme.fontFamily, fontSize: theme.fontSize, textDecoration: "none", paddingRight: 5, display: "flex", alignItems: 'center', }, conflictFolder: { color: theme.colorError2, fontWeight: "bold", }, header: { height: itemHeight * 1.8, fontFamily: theme.fontFamily, fontSize: theme.fontSize * 1.3, textDecoration: "none", boxSizing: "border-box", color: theme.color2, paddingLeft: 8, display: "flex", alignItems: "center", }, 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", }, 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, minHeight: 70, wordWrap: "break-word", //width: "100%", }, }; 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; } } componentDidUpdate(prevProps) { if (shim.isLinux()) { // For some reason, the UI seems to sleep in some Linux distro during // sync. Cannot find the reason for it and cannot replicate, so here // as a test force the update at regular intervals. // https://github.com/laurent22/joplin/issues/312#issuecomment-429472193 if (!prevProps.syncStarted && this.props.syncStarted) { this.clearForceUpdateDuringSync(); this.forceUpdateDuringSyncIID_ = setInterval(() => { this.forceUpdate(); }, 2000); } if (prevProps.syncStarted && !this.props.syncStarted) this.clearForceUpdateDuringSync(); } } componentWillUnmount() { this.clearForceUpdateDuringSync(); } itemContextMenu(event) { const itemId = event.target.getAttribute("data-id"); if (itemId === Folder.conflictFolderId()) return; const itemType = Number(event.target.getAttribute("data-type")); if (!itemId || !itemType) throw new Error("No data on element"); let deleteMessage = ""; if (itemType === BaseModel.TYPE_FOLDER) { deleteMessage = _("Delete notebook? All notes and sub-notebooks within this notebook will also be deleted."); } else if (itemType === BaseModel.TYPE_TAG) { deleteMessage = _("Remove this tag from all the notes?"); } 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); } menu.append( new MenuItem({ label: _("Delete"), click: async () => { const ok = bridge().showConfirmMessageBox(deleteMessage); 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({ label: _("Rename"), click: async () => { this.props.dispatch({ type: "WINDOW_COMMAND", name: "renameFolder", id: itemId, }); }, }) ); // menu.append( // new MenuItem({ // label: _("Move"), // click: async () => { // this.props.dispatch({ // type: "WINDOW_COMMAND", // name: "renameFolder", // id: 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({ label: _('Rename'), click: async () => { this.props.dispatch({ type: "WINDOW_COMMAND", name: "renameTag", id: 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, }); } searchItem_click(search) { this.props.dispatch({ type: "SEARCH_SELECT", id: search ? search.id : null, }); } async sync_click() { await shared.synchronize_press(this); } 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); // containerStyle.paddingLeft = containerStyle.paddingLeft + depth * 10; if (selected) containerStyle = Object.assign(containerStyle, this.style().listItemSelected); let expandLinkStyle = Object.assign({}, this.style().listItemExpandIcon); let expandIconStyle = { visibility: hasChildren ? 'visible' : 'hidden', paddingLeft: 8 + depth * 10, } const iconName = this.props.collapsedFolderIds.indexOf(folder.id) >= 0 ? 'fa-plus-square' : 'fa-minus-square'; const expandIcon = const expandLink = hasChildren ? {expandIcon} : {expandIcon} return (
{ expandLink } this.itemContextMenu(event)} style={style} folderid={folder.id} onClick={() => { this.folderItem_click(folder); }} onDoubleClick={this.onFolderToggleClick_} > {itemTitle}
); } tagItem(tag, selected) { let style = Object.assign({}, this.style().tagItem); if (selected) style = Object.assign(style, this.style().listItemSelected); return ( this.itemContextMenu(event)} tagid={tag.id} key={tag.id} style={style} onDrop={this.onTagDrop_} onClick={() => { this.tagItem_click(tag); }} > {Tag.displayTitle(tag)} ); } searchItem(search, selected) { let style = Object.assign({}, this.style().listItem); if (selected) style = Object.assign(style, this.style().listItemSelected); return ( this.itemContextMenu(event)} key={search.id} style={style} onClick={() => { this.searchItem_click(search); }} > {search.title} ); } makeDivider(key) { return
; } makeHeader(key, label, iconName, extraProps = {}) { const style = this.style().header; const icon = ; if (extraProps.toggleblock || extraProps.onClick) { style.cursor = "pointer"; } let headerClick = extraProps.onClick || null; delete extraProps.onClick; // check if toggling option is set. let toggleIcon = null; const toggleKey = `${key}IsExpanded`; if (extraProps.toggleblock) { let isExpanded = this.state[toggleKey]; toggleIcon = ; } return (
{ // if a custom click event is attached, trigger that. if (headerClick) { headerClick(key, event); } this.onHeaderClick_(key, event); }}> {icon} {label} {toggleIcon}
); } 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); } } synchronizeButton(type) { const style = this.style().button; const iconName = type === "sync" ? "fa-refresh" : "fa-times"; const label = type === "sync" ? _("Synchronise") : _("Cancel"); const icon = ; return ( { this.sync_click(); }} > {icon} {label} ); } render() { const theme = themeStyle(this.props.theme); const style = Object.assign({}, this.style().root, this.props.style, { overflowX: "hidden", overflowY: "auto", }); let items = []; items.push(this.makeHeader("folderHeader", _("Notebooks"), "fa-folder-o", { onDrop: this.onFolderDrop_, folderid: '', toggleblock: 1 })); if (this.props.folders.length) { const folderItems = shared.renderFolders(this.props, this.folderItem.bind(this)); items.push(
{folderItems}
); } items.push(this.makeHeader("tagHeader", _("Tags"), "fa-tags", { toggleblock: 1 })); if (this.props.tags.length) { const tagItems = shared.renderTags(this.props, this.tagItem.bind(this)); items.push(
{tagItems}
); } let decryptionReportText = ''; if (this.props.decryptionWorker && this.props.decryptionWorker.state !== 'idle' && this.props.decryptionWorker.itemCount) { decryptionReportText = _('Decrypting items: %d/%d', this.props.decryptionWorker.itemIndex + 1, this.props.decryptionWorker.itemCount); } let resourceFetcherText = ''; if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) { resourceFetcherText = _('Fetching resources: %d', this.props.resourceFetcher.toFetchCount); } let lines = Synchronizer.reportToLines(this.props.syncReport); if (resourceFetcherText) lines.push(resourceFetcherText); if (decryptionReportText) lines.push(decryptionReportText); const syncReportText = []; for (let i = 0; i < lines.length; i++) { syncReportText.push(
{lines[i]}
); } items.push(this.synchronizeButton(this.props.syncStarted ? "cancel" : "sync")); items.push(
{syncReportText}
); return (
{items}
); } } 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, notesParentType: state.notesParentType, locale: state.settings.locale, theme: state.settings.theme, collapsedFolderIds: state.collapsedFolderIds, decryptionWorker: state.decryptionWorker, resourceFetcher: state.resourceFetcher, }; }; const SideBar = connect(mapStateToProps)(SideBarComponent); module.exports = { SideBar };