diff --git a/img/link.png b/img/link.png new file mode 100644 index 00000000..9976c83e Binary files /dev/null and b/img/link.png differ diff --git a/package-lock.json b/package-lock.json index 5d321686..30b219c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,15 +101,6 @@ "integrity": "sha512-rzOhiQ55WzAiFgXRtitP/ZUT8iVNyllEpylJ5zHzR4vArUvMB39GTk+Zon/uAM0JxEFAWnwsxC2gH8s+tZ3Myg==", "dev": true }, - "@types/del": { - "version": "3.0.1", - "resolved": "http://registry.npmjs.org/@types/del/-/del-3.0.1.tgz", - "integrity": "sha512-y6qRq6raBuu965clKgx6FHuiPu3oHdtmzMPXi8Uahsjdq1L6DL5fS/aY5/s71YwM7k6K1QIWvem5vNwlnNGIkQ==", - "dev": true, - "requires": { - "@types/glob": "*" - } - }, "@types/dom4": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.1.tgz", @@ -118,14 +109,12 @@ "@types/events": { "version": "1.2.0", "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", - "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", - "dev": true + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" }, "@types/glob": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, "requires": { "@types/events": "*", "@types/minimatch": "*", @@ -141,8 +130,7 @@ "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" }, "@types/mkdirp": { "version": "0.5.2", @@ -2234,16 +2222,31 @@ } }, "del": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "version": "github:warpdesign/del#d19552a209261d118901d1da2bfa5c2bbe6f006d", + "from": "github:warpdesign/del", "requires": { + "@types/glob": "^7.1.1", "globby": "^6.1.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "p-map": "^1.1.1", - "pify": "^3.0.0", - "rimraf": "^2.2.8" + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "requires": { + "glob": "^7.1.3" + } + } } }, "delayed-stream": { @@ -3453,14 +3456,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3480,8 +3481,7 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", @@ -3629,7 +3629,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4187,7 +4186,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" } } @@ -4893,22 +4892,33 @@ "dev": true }, "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.1.0.tgz", + "integrity": "sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw==" }, "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", "requires": { - "is-path-inside": "^1.0.0" + "is-path-inside": "^2.1.0" + }, + "dependencies": { + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "requires": { + "path-is-inside": "^1.0.2" + } + } } }, "is-path-inside": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, "requires": { "path-is-inside": "^1.0.1" } @@ -5913,9 +5923,9 @@ } }, "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" }, "p-try": { "version": "1.0.0", @@ -6094,7 +6104,8 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true }, "pinkie": { "version": "2.0.4", @@ -6994,6 +7005,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, "requires": { "glob": "^7.0.5" } @@ -8101,6 +8113,30 @@ } } }, + "url-loader": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", + "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "mime": "^2.0.3", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", diff --git a/package.json b/package.json index ea16076e..85680533 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "license": "MIT", "devDependencies": { "@types/classnames": "^2.2.7", - "@types/del": "^3.0.1", "@types/i18next": "^12.1.0", "@types/react-i18next": "^8.1.0", "@types/react-virtualized": "^9.18.12", @@ -63,6 +62,7 @@ "source-map-loader": "^0.2.4", "style-loader": "^0.23.0", "typescript": "^3.1.1", + "url-loader": "^1.1.2", "webpack": "^4.30.0", "webpack-cli": "^3.1.2" }, @@ -74,7 +74,7 @@ "@types/react-dom": "^16.0.8", "basic-ftp": "^3.2.2", "classnames": "^2.2.6", - "del": "^3.0.0", + "del": "github:warpdesign/del", "electron-window-state": "^5.0.3", "get-folder-size": "^2.0.0", "i18next": "^13.0.0", diff --git a/src/components/App.tsx b/src/components/App.tsx index ec17eb7e..a1922337 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -23,6 +23,7 @@ import { remote } from 'electron'; import classnames from 'classnames'; import { isPackage, isWin } from '../utils/platform'; import { TabDescriptor } from "./TabList"; +import { getLocalizedError } from "../locale/error"; require("@blueprintjs/core/lib/css/blueprint.css"); require("@blueprintjs/icons/lib/css/blueprint-icons.css"); @@ -438,7 +439,7 @@ class App extends React.Component { onDebugCache = () => { let i = 0; for (let cache of this.appState.views[0].caches) { - console.log('cache', cache.selected.length, cache.selected, cache.selectedId, cache.editingId); + console.log('cache', cache.selected.length, cache.selected, cache.selectedId, cache.editingId, cache); } } @@ -539,9 +540,40 @@ class App extends React.Component { private onPaste = (): void => { const fileCache: FileState = this.getActiveFileCache(); - - if (fileCache) { - this.appState.prepareClipboardTransferTo(fileCache); + const appState = this.appState; + + if (fileCache && !fileCache.error && appState.clipboard.files.length) { + this.appState.prepareClipboardTransferTo(fileCache) + .then((noErrors: any) => { + const { t } = this.injected; + if (noErrors) { + AppToaster.show({ + message: t('COMMON.COPY_FINISHED'), + icon: "tick", + intent: Intent.SUCCESS, + timeout: 3000 + }); + } else { + AppToaster.show({ + message: t('COMMON.COPY_WARNING'), + icon: "warning-sign", + intent: Intent.WARNING, + timeout: 5000 + }); + } + }) + .catch((err: any) => { + const { t } = this.injected; + const localizedError = getLocalizedError(err); + const message = err.code ? t('ERRORS.COPY_ERROR', { message: localizedError.message }) : t('ERRORS.COPY_UNKNOWN_ERROR'); + + AppToaster.show({ + message: message, + icon: "error", + intent: Intent.DANGER, + timeout: 5000 + }); + }); } } @@ -651,7 +683,7 @@ class App extends React.Component {
- + {}
diff --git a/src/components/Downloads.tsx b/src/components/Downloads.tsx index a1dac53c..39dedd74 100644 --- a/src/components/Downloads.tsx +++ b/src/components/Downloads.tsx @@ -158,6 +158,7 @@ class DownloadsClass extends React.Component { } getIntent(transfer: Batch) { + console.log(transfer.status, transfer); const status = transfer.status; let intent: Intent = Intent.NONE; if (!status.match(/queued|calculating/)) { @@ -190,9 +191,8 @@ class DownloadsClass extends React.Component { const currentSize = formatBytes(transfer.progress); const percent = transfer.status === 'calculating' ? 0 : transfer.progress / transfer.size; const ended = transfer.hasEnded; - const error = transfer.status === 'error'; - const rightLabel = ended ? (error ? t('DOWNLOADS.ERROR') : t('DOWNLOADS.FINISHED', { size: sizeFormatted })) : t('DOWNLOADS.PROGRESS', { current: currentSize, size: transferSize }); - // const classes = classnames({[Classes.INTENT_DANGER]: error }); + const errors = transfer.numErrors; + const rightLabel = ended ? (errors ? t('DOWNLOADS.FINISHED_ERRORS') : t('DOWNLOADS.FINISHED')) : t('DOWNLOADS.PROGRESS', { current: currentSize, size: transferSize }); return ( @@ -254,13 +254,13 @@ class DownloadsClass extends React.Component { let i = 0; for (let file of transfer.files) { - if (!file.file.isDir) { + if (!file.file.isDir || file.status === 'error') { const id = transfer.id + '_' + i; const filetype = file.file.type; node.childNodes.push({ id: id, - icon: this.getFileIcon(filetype), + icon: file.file.isDir ? 'folder-close' : this.getFileIcon(filetype), label: file.subDirectory ? (file.subDirectory + '/' + file.file.fullname) : file.file.fullname, secondaryLabel: this.createFileRightLabel(file), nodeData: { diff --git a/src/components/FileTable.tsx b/src/components/FileTable.tsx index bad4062f..fe8eea42 100644 --- a/src/components/FileTable.tsx +++ b/src/components/FileTable.tsx @@ -19,12 +19,11 @@ import { SettingsState } from '../state/settingsState'; import { ViewState } from '../state/viewState'; import { debounce } from '../utils/debounce'; import { TSORT_METHOD_NAME, TSORT_ORDER, getSortMethod } from '../services/FsSort'; +import { getSelectionRange } from '../utils/fileUtils'; require('react-virtualized/styles.css'); require('../css/filetable.css'); -const REGEX_EXTENSION = /\.(?=[^0-9])/; - const CLICK_DELAY = 300; const SCROLL_DEBOUNCE = 50; const ROW_HEIGHT = 28; @@ -97,7 +96,7 @@ interface InjectedProps extends IProps { @HotkeysTarget export class FileTableClass extends React.Component { private viewState: ViewState; - private disposer: IReactionDisposer; + private disposers: Array = new Array(); private editingElement: HTMLElement = null; private editingFile: File; private clickTimeout: any; @@ -117,7 +116,7 @@ export class FileTableClass extends React.Component { path: cache.path }; - this.installReaction(); + this.installReactions(); // since the nodes are only generated after the files are updated // we re-render them after language has changed otherwise FileList // gets re-rendered with the wrong language after language has been changed @@ -144,7 +143,9 @@ export class FileTableClass extends React.Component { } public componentWillUnmount() { - this.disposer(); + for (let disposer of this.disposers) { + disposer(); + } document.removeEventListener('keydown', this.onDocKeyDown); this.unbindLanguageChange(); } @@ -206,20 +207,25 @@ export class FileTableClass extends React.Component { return this.props as InjectedProps; } - private installReaction() { - this.disposer = reaction( - () => { return toJS(this.cache.files) }, + private installReactions() { + this.disposers.push(reaction( + () => toJS(this.cache.files), (files: File[]) => { const cache = this.cache; // when cache is being (re)loaded, cache.files is empty: - // we don't want to show "empty folder" placeholder in that + // we don't want to show "empty folder" placeholder // that case, only when cache is loaded and there are no files const { viewState } = this.injected; if (cache.cmd === 'cwd' || cache.history.length) { this.updateNodes(files); } - }); + }), + reaction( + () => this.cache.error, + () => this.updateNodes(this.cache.files) + ) + ); } private getSelectedState(name: string) { @@ -231,12 +237,16 @@ export class FileTableClass extends React.Component { buildNodeFromFile(file: File, keepSelection: boolean) { const filetype = file.type; let isSelected = keepSelection && this.getSelectedState(file.fullname) || false; + const classes = classnames({ + isHidden: file.fullname.startsWith('.'), + isSymlink: file.isSym + }); const res: ITableRow = { icon: file.isDir && "folder-close" || (filetype && TYPE_ICONS[filetype] || TYPE_ICONS['any']), name: file.fullname, nodeData: file, - className: file.fullname.startsWith('.') && 'isHidden' || '', + className: classes, isSelected: isSelected, size: !file.isDir && formatBytes(file.length) || '--' }; @@ -265,11 +275,12 @@ export class FileTableClass extends React.Component { _noRowsRenderer = () => { const { t } = this.injected; const status = this.cache.status; + const error = this.cache.error; // we don't want to show empty + loader at the same time if (status !== 'busy') { - const placeholder = status === 'blank' && t('COMMON.NO_SUCH_FOLDER') || t('COMMON.EMPTY_FOLDER'); - const icon = status === 'blank' ? 'warning-sign' : 'tick-circle'; + const placeholder = error && t('COMMON.NO_SUCH_FOLDER') || t('COMMON.EMPTY_FOLDER'); + const icon = error ? 'warning-sign' : 'tick-circle'; return (
{placeholder}
); } else { return (
); @@ -293,6 +304,8 @@ export class FileTableClass extends React.Component { this.setState({ nodes, selected: keepSelection ? this.state.selected : 0, position, path: newPath }, () => { if (keepSelection && cache.editingId && position > -1) { this.getElementAndToggleRename(undefined, false); + } else if (this.editingElement) { + this.editingElement = null; } }); } @@ -375,7 +388,6 @@ export class FileTableClass extends React.Component { { columnData: any, dataKey: string, event: Event } */ onHeaderClick = ({ columnData, dataKey }: HeaderMouseEventHandlerParams) => { - console.log('column click', columnData, dataKey); const { sortMethod, sortOrder } = this.cache; const newMethod = columnData.sortMethod as TSORT_METHOD_NAME; const newOrder = sortMethod !== newMethod ? 'asc' : (sortOrder === 'asc' && 'desc' || 'asc') as TSORT_ORDER; @@ -390,7 +402,8 @@ export class FileTableClass extends React.Component { const file = rowData.nodeData as File; // keep a reference to the target before set setTimeout is called - // because React set the event to null + // because React appaers to recycle the event object after event handler + // has returned const element = event.target as HTMLElement; // do not select parent dir pseudo file @@ -443,11 +456,9 @@ export class FileTableClass extends React.Component { const editingElement = this.editingElement; if (cancel) { - console.log('restoring value'); // restore previous value editingElement.innerText = this.editingFile.fullname; } else { - console.log('renaming value', this.cache.path, this.editingFile); // since the File element is modified by the rename FileState.rename method there is // no need to refresh the file cache: // 1. innerText has been updated and is valid @@ -479,6 +490,8 @@ export class FileTableClass extends React.Component { const fileCache = this.cache; const { nodes, position } = this.state; + console.log('selected', nodes.filter((node, i) => i !== position && node.isSelected)); + const selection = nodes.filter((node, i) => i !== position && node.isSelected).map((node) => node.nodeData) as File[]; if (position > -1) { @@ -491,13 +504,14 @@ export class FileTableClass extends React.Component { selectLeftPart() { const filename = this.editingFile.fullname; - const selectionLength = filename.split(REGEX_EXTENSION)[0].length; + const selectionRange = getSelectionRange(filename); + // const selectionLength = filename.split(REGEX_EXTENSION)[0].length; const selection = window.getSelection(); const range = document.createRange(); const textNode = this.editingElement.childNodes[0]; - range.setStart(textNode, 0); - range.setEnd(textNode, selectionLength); + range.setStart(textNode, selectionRange.start); + range.setEnd(textNode, selectionRange.length); selection.empty(); selection.addRange(range); } @@ -510,16 +524,13 @@ export class FileTableClass extends React.Component { } toggleInlineRename(element: HTMLElement, originallySelected: boolean, file: File, selectText = true) { - console.log('toggle inlinerename'); if (!file.readonly) { if (originallySelected) { - console.log('activate inline rename!'); element.contentEditable = "true"; element.focus(); this.setEditElement(element, file); selectText && this.selectLeftPart(); element.onblur = () => { - console.log('onblur!!'); if (this.editingElement) { this.onInlineEdit(true); } @@ -550,7 +561,6 @@ export class FileTableClass extends React.Component { // await this.cache.openFile(file); // await appState.getFile(file); await this.cache.openFile(appState, this.cache, file); - console.log('** done'); } else { const isShiftDown = event.shiftKey; const cache = isShiftDown ? appState.getInactiveViewVisibleCache() : this.cache; @@ -569,27 +579,21 @@ export class FileTableClass extends React.Component { selectAll(invert = false) { let { position, selected } = this.state; let { nodes } = this.state; - const fileCache = this.cache; if (nodes.length && this.isViewActive()) { - console.log('onSelectAll', document.activeElement); - const isRoot = fileCache.isRoot((nodes[0].nodeData as File).dir); selected = 0; let i = 0; for (let node of nodes) { // do not select parent dir - if (i || isRoot) { - node.isSelected = invert ? !node.isSelected : true; - if (node.isSelected) { - position = i; - selected++; - } + node.isSelected = invert ? !node.isSelected : true; + if (node.isSelected) { + position = i; + selected++; } i++; } - console.log('setState 3', position); this.setState({ nodes, selected, position }, () => { this.updateSelection(); }); @@ -617,7 +621,6 @@ export class FileTableClass extends React.Component { this.selectAll(); } else { // need to select all text: send message - console.log('isEditable'); ipcRenderer.send('selectAll'); } } @@ -633,7 +636,6 @@ export class FileTableClass extends React.Component { if (e.keyCode === KEYS.Enter) { e.preventDefault(); } - // console.log('end inline edit'); this.onInlineEdit(e.keyCode === KEYS.Escape); } } @@ -664,8 +666,8 @@ export class FileTableClass extends React.Component { const node = nodes[position]; const file = nodes[position].nodeData as File; const element = this.getNodeContentElement(position + 1); - console.log('got element', position + 1, element, nodes.length); const span: HTMLElement = element.querySelector(`.${LABEL_CLASSNAME}`); + if (e && typeof e !== 'string') { e.preventDefault(); } @@ -713,9 +715,7 @@ export class FileTableClass extends React.Component { let { position, selected } = this.state; let { nodes } = this.state; - console.log('moveSelection', position); position += step; - console.log('moveSelection apres', position); if (position > -1 && position <= this.state.nodes.length - 1) { if (isShiftDown) { @@ -728,7 +728,6 @@ export class FileTableClass extends React.Component { nodes[position].isSelected = true; - console.log('setState 4', position); // move in method to reuse this.setState({ nodes, selected, position }, () => { this.updateSelection(); @@ -763,7 +762,6 @@ export class FileTableClass extends React.Component { const { t } = this.injected; const { position } = this.state; const rowCount = this.state.nodes.length; - const scrollTop = position === -1 && this.cache.scrollTop || undefined; return (
diff --git a/src/components/SideView.tsx b/src/components/SideView.tsx index 95327d38..167d3073 100644 --- a/src/components/SideView.tsx +++ b/src/components/SideView.tsx @@ -12,8 +12,12 @@ import { DropTargetSpec, DropTargetConnector, DropTargetMonitor, DropTargetColle import { DraggedObject } from './RowRenderer'; import { TabList } from "./TabList"; import { ViewState } from "../state/viewState"; +import { AppToaster } from "./AppToaster"; +import { Intent } from "@blueprintjs/core"; +import { getLocalizedError } from "../locale/error"; +import { withNamespaces, WithNamespaces } from "react-i18next"; -interface SideViewProps { +interface SideViewProps extends WithNamespaces { hide: boolean; viewState: ViewState; onPaste: () => void; @@ -32,7 +36,7 @@ const fileTarget: DropTargetSpec = { const sourceViewId = monitor.getItem().fileState.viewId; const viewState = props.viewState; const fileCache = viewState.getVisibleCache(); - return props.viewState.viewId !== sourceViewId && fileCache.status !== 'busy'; + return props.viewState.viewId !== sourceViewId && fileCache.status !== 'busy' && !fileCache.error; }, drop(props, monitor, component) { const item = monitor.getItem(); @@ -119,9 +123,37 @@ export class SideViewClass extends React.Component{ const files = item.selectedCount > 0 ? item.fileState.selected.slice(0) : [item.dragFile]; // TODO: check both cache are active? - appState.prepareTransferTo(item.fileState, fileCache, files).catch(err => { - debugger; - }); + appState.prepareTransferTo(item.fileState, fileCache, files) + .then((noErrors: any) => { + const { t } = this.injected; + if (noErrors) { + AppToaster.show({ + message: t('COMMON.COPY_FINISHED'), + icon: "tick", + intent: Intent.SUCCESS, + timeout: 3000 + }); + } else { + AppToaster.show({ + message: t('COMMON.COPY_WARNING'), + icon: "warning-sign", + intent: Intent.WARNING, + timeout: 5000 + }); + } + }) + .catch((err: any) => { + const { t } = this.injected; + const localizedError = getLocalizedError(err); + const message = err.code ? t('ERRORS.COPY_ERROR', { message: localizedError.message }) : t('ERRORS.COPY_UNKNOWN_ERROR'); + + AppToaster.show({ + message: message, + icon: "error", + intent: Intent.DANGER, + timeout: 5000 + }); + }); } shouldComponentUpdate() { @@ -147,4 +179,8 @@ export class SideViewClass extends React.Component{ } } -export const SideView = DropTarget('file', fileTarget, collect)(SideViewClass); +const SideViewDD = DropTarget('file', fileTarget, collect)(SideViewClass); + +const SideView = withNamespaces()(SideViewDD); + +export { SideView }; diff --git a/src/components/TabList.tsx b/src/components/TabList.tsx index 2d9830c3..3fde8924 100644 --- a/src/components/TabList.tsx +++ b/src/components/TabList.tsx @@ -8,6 +8,7 @@ import { ContextMenu } from './ContextMenu'; import { MenuItemConstructorOptions, MenuItem } from "electron"; import { SettingsState } from "../state/settingsState"; import { DOWNLOADS_DIR, HOME_DIR, DOCS_DIR, DESKTOP_DIR, MUSIC_DIR, PICTURES_DIR, VIDEOS_DIR } from '../utils/platform'; +import { AppAlert } from "./AppAlert"; export interface TabDescriptor { viewId: number; @@ -146,7 +147,11 @@ class TabListClass extends React.Component { cache.openDirectory({ dir: cache.path, fullname: menuItem.id - }); + }).catch((err) => { + AppAlert.show(`${err.message} (${err.code})`, { + intent: 'danger' + }); + }) } }; @@ -232,7 +237,7 @@ class TabListClass extends React.Component { caches.map((cache, index) => { const closeIcon = caches.length > 1 && ; const path = cache.path; - const tabIcon = this.getTabIcon(path); + const tabIcon = cache.error ? 'issue' : this.getTabIcon(path); const tabInfo = cache.getFS().displaypath(path); return ( diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 26ab6e12..e381fab6 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -16,6 +16,7 @@ import { ViewState } from "../state/viewState"; const TOOLTIP_DELAY = 1200; const MOVE_EVENT_THROTTLE = 300; +const ERROR_MESSAGE_TIMEOUT = 3500; interface IProps extends WithNamespaces { onPaste: () => void; @@ -88,12 +89,10 @@ export class ToolbarClass extends React.Component { } private onBackward = (event: React.FormEvent) => { - console.log(this.cache.history); this.cache.navHistory(-1); } private onForward = (event: React.FormEvent) => { - console.log(this.cache.history); this.cache.navHistory(1); } @@ -140,8 +139,6 @@ export class ToolbarClass extends React.Component { } private onBlur = () => { - // console.log('blur'); - // this.pathHasFocus = false; this.setState({ path: this.cache.path, status: 0, isTooltipOpen: false }); } @@ -175,7 +172,7 @@ export class ToolbarClass extends React.Component { message: t('ERRORS.CREATE_FOLDER', { message: err.message }), icon: 'error', intent: Intent.DANGER, - timeout: 3000 + timeout: ERROR_MESSAGE_TIMEOUT }); } } @@ -184,50 +181,51 @@ export class ToolbarClass extends React.Component { this.setState({ isDeleteOpen: false }); } + private onDeleteError = (err?: Error) => { + const { t } = this.props; + + if (err) { + AppToaster.show({ + message: t('ERRORS.DELETE', { message: err.message }), + icon: 'error', + intent: Intent.DANGER, + timeout: ERROR_MESSAGE_TIMEOUT + }); + } else { + AppToaster.show({ + message: t('ERRORS.DELETE_WARN'), + icon: 'warning-sign', + intent: Intent.WARNING, + timeout: ERROR_MESSAGE_TIMEOUT + }); + } + } + private delete = async () => { Logger.log('delete selected files'); try { const { viewState } = this.injected; const fileCache = viewState.getVisibleCache(); - await this.cache.delete(this.state.path, fileCache.selected); - console.log('need to refresh cache'); - this.cache.reload(); - // appState.refreshCache(this.cache); - } catch (err) { - const { t } = this.props; + const deleted = await this.cache.delete(this.state.path, fileCache.selected); - AppToaster.show({ - message: t('ERRORS.DELETE', { message: err }), - icon: 'error', - intent: Intent.DANGER, - timeout: 4000 - }); + // TODO: reset selection ? + if (!deleted) { + this.onDeleteError(); + } else { + if (deleted !== fileCache.selected.length) { + // show warning + this.onDeleteError(); + } + this.cache.reload(); + } + } catch (err) { + this.onDeleteError(err); } this.setState({ isDeleteOpen: false }); } - // private renderCopyProgress(percent: number): IToasterOpts { - // return { - // icon: "cloud-upload", - // message: ( - // = 100 && Classes.PROGRESS_NO_STRIPES} - // intent={percent < 100 ? Intent.PRIMARY : Intent.SUCCESS} - // value={percent / 100} - // /> - // ), - // timeout: percent < 100 ? 0 : 2000 - // } - // } - - private copy = async () => { - // TODO: attempt to copy - const { appState, viewState } = this.injected; - appState.prepareClipboardTransferTo(viewState.getVisibleCache()); - } - private onMakedir = () => { const { appState, viewState } = this.injected; @@ -356,16 +354,11 @@ export class ToolbarClass extends React.Component { clearTimeout(this.tooltipTimeout); this.setTooltipTimeout(); } - // else { - // console.log('cannot show'); - // } }, MOVE_EVENT_THROTTLE); setTooltipTimeout() { this.tooltipTimeout = window.setTimeout(() => { - // console.log('timeout reached', this.tooltipReady, this.canShowTooltip); if (this.tooltipReady && this.canShowTooltip) { - // console.log('yeah ! opening'); this.setState({ isTooltipOpen: true }); } }, TOOLTIP_DELAY); diff --git a/src/css/filetable.css b/src/css/filetable.css index 3894c1e3..ded11556 100644 --- a/src/css/filetable.css +++ b/src/css/filetable.css @@ -74,6 +74,19 @@ color:#5c7080; } +.tableRow.isSymlink:after{ + content:''; + position:absolute; + bottom:4px; + left:6px; + background:transparent url(../../img/link.png) no-repeat; + background-color:white; + background-size:100% 100%; + width:10px; + height:10px; + border:1px solid rgba(0,0,0,.4); +} + .tableRow.error, .tableRow.error .bp3-icon, body.bp3-dark .tableRow.error .bp3-icon{ color: rgb(160, 43, 43); } diff --git a/src/locale/error.ts b/src/locale/error.ts index c4837536..c46ee3e4 100644 --- a/src/locale/error.ts +++ b/src/locale/error.ts @@ -40,6 +40,14 @@ export function getLocalizedError(error: any) { niceError.message = i18next.t('ERRORS.EACCES'); break; + case 'EIO': + niceError.message = i18next.t('ERRORS.EIO'); + break; + + case 'ENOSPC': + niceError.message = i18next.t('ERRORS.ENOSPC'); + break; + case 'EEXIST': niceError.message = i18next.t('ERRORS.EEXIST'); break; @@ -58,6 +66,10 @@ export function getLocalizedError(error: any) { niceError.message = i18next.t('ERRORS.NOT_A_DIR'); break; + case 'NODEST': + niceError.message = i18next.t('ERRORS.NODEST'); + break; + case 530: niceError.message = i18next.t('ERRORS.530'); break; diff --git a/src/locale/lang/en.json b/src/locale/lang/en.json index d67cdfcd..95d66f7c 100644 --- a/src/locale/lang/en.json +++ b/src/locale/lang/en.json @@ -149,7 +149,9 @@ "CLOSE": "Close", "EMPTY_FOLDER": "Folder is empty.", "NO_SUCH_FOLDER": "No such folder or permission denied.", - "PATH_PLACEHOLDER": "Enter path to load" + "PATH_PLACEHOLDER": "Enter path to load", + "COPY_FINISHED": "The files have been copied successfully.", + "COPY_WARNING": "Copy finished: some items could not be copied." }, "FILETABLE": { "COL_NAME": "Name", @@ -163,7 +165,8 @@ }, "DOWNLOADS": { "EMPTY_TITLE": "No download.", - "FINISHED": "Finished ({{size}})", + "FINISHED": "Finished", + "FINISHED_ERRORS": "Finished, with errors", "PROGRESS": "{{current}} / {{size}}", "ERROR": "Error", "CANCELLED": "Cancelled", @@ -175,7 +178,9 @@ "ECONNREFUSED": "Connection refused by the server", "ENOENT": "No such file or directory", "EPERM": "You don't have the permission to do this action", + "EIO": "Transfer was interrupted", "EACCES": "Permission denied", + "ENOSPC": "no space left on device", "EHOSTDOWN": "Your internet connection appears to be down :(", "530": "Login authentication failed", "550": "File unavailable, not found, not accessible", @@ -186,8 +191,12 @@ "WIN_VALID_FILENAME": "All characters except *:<>\\?| can be used", "UNIX_VALID_FILENAME": "All characters except / can be used", "DELETE": "Error deleting files: {{message}}", + "DELETE_WARN": "Some files could not be deleted", "CREATE_FOLDER": "Error creating folder: {{message}}", - "EEXIST": "a file with that name already exists" + "EEXIST": "A file with that name already exists", + "NODEST": "copy target could not be accessed", + "COPY_ERROR": "Couldn't copy files: {{message}}", + "COPY_UNKNOWN_ERROR": "Error while copying files." }, "MAIN_PROCESS": { "PRESS_TO_EXIT": "Keep pressing ⌘Q to exit" diff --git a/src/locale/lang/fr.json b/src/locale/lang/fr.json index 941890e0..08e8ca76 100644 --- a/src/locale/lang/fr.json +++ b/src/locale/lang/fr.json @@ -72,7 +72,7 @@ "TEST_TERMINAL": "Cliquez ici pour lancer le terminal specifié" }, "DELETE": { - "CONFIRM": "Etes-vous sûr de vouloir supprimer <1>{{count}} fichier(s)/dossier(s)?<3 /><4 />Cette action supprimera de manière <6>permanente les éléments sélectionnés." + "CONFIRM": "Etes-vous sûr de vouloir supprimer <1>{{count}} fichier(s)/dossier(s) ?<3 /><4 />Cette action supprimera de manière <6>permanente les éléments sélectionnés." } }, "SHORTCUT": { @@ -149,7 +149,9 @@ "CLOSE": "Fermer", "EMPTY_FOLDER": "Dossier vide.", "NO_SUCH_FOLDER": "Dossier inexistant ou inaccessible.", - "PATH_PLACEHOLDER": "Entrer le chemin à visiter" + "PATH_PLACEHOLDER": "Entrer le chemin à visiter", + "COPY_FINISHED": "La copie a été réalisée avec succès", + "COPY_WARNING": "Copie terminée: certains éléments n'ont pu être copiés." }, "FILETABLE": { "COL_NAME": "Nom", @@ -163,7 +165,8 @@ }, "DOWNLOADS": { "EMPTY_TITLE": "Aucun téléchargement.", - "FINISHED": "Terminé ({{size}})", + "FINISHED": "Terminé", + "FINISHED_ERRORS": "Terminé, mais avec erreurs", "PROGRESS": "{{current}} / {{size}}", "ERROR": "Erreur", "CANCELLED": "Annulé", @@ -175,7 +178,9 @@ "ECONNREFUSED": "Connexion refusée par le serveur", "ENOENT": "Dossier ou fichier introuvable", "EPERM": "Vous n'avez pas la permission d'effectuer cette action", + "EIO": "Le transfert a été interrompu", "EACCES": "Accès refusé", + "ENOSPC": "le disque est plein", "EHOSTDOWN": "Houston, on a un problème: il n'y a plus d'internet :(", "530": "Echec de l'authentification", "550": "Fichier non disponible, introuvable ou non accessible.", @@ -186,8 +191,12 @@ "WIN_VALID_FILENAME": "Tous les caractères sauf *:<>\\?| sont acceptés", "UNIX_VALID_FILENAME": "Tous les caractères sauf / sont acceptés", "DELETE": "Erreur lors de la suppression des fichiers: {{message}}", + "DELETE_WARN": "Certains fichiers n'ont pu être supprimés", "CREATE_FOLDER": "Erreur lors de la création du dossier: {{message}}", - "EEXIST": "un fichier avec ce nom existe déjà" + "EEXIST": "Un fichier avec ce nom existe déjà", + "NODEST": "la destination de la copie n'est pas accessible", + "COPY_ERROR": "La copie n'a pu s'effectuer: {{message}}", + "COPY_UNKNOWN_ERROR": "Impossible de copier les fichiers demandés." }, "MAIN_PROCESS": { "PRESS_TO_EXIT": "Maintenez la touche ⌘Q enfoncée pour quitter" diff --git a/src/services/Fs.ts b/src/services/Fs.ts index 1f03c6bc..a4fd4384 100644 --- a/src/services/Fs.ts +++ b/src/services/Fs.ts @@ -2,6 +2,7 @@ import { FsLocal } from './plugins/FsLocal'; import { FsGeneric } from './plugins/FsGeneric'; import { FsSimpleFtp } from './plugins/FsSimpleFtp'; import { Readable, Writable } from 'stream'; +import { isWin } from '../utils/platform'; export interface FileID { ino: number; @@ -38,14 +39,14 @@ export interface Fs { icon: string; } -const Extensions = { - 'exe': /\.(exe|bat|com|msi|mui|cmd)$/, - 'img': /\.(png|jpeg|jpg|gif|pcx|tiff|raw|webp|svg|heif|bmp|ilbm|iff|lbm|ppm|pgw|pbm|pnm|psd)$/, - 'arc': /\.(zip|tar|rar|7zip|7z|dmg|shar|ar|bz2|lz|gz|tgz|lha|lzh|lzx|sz|xz|z|s7z|ace|apk|arp|arj|cab|car|cfs|cso|dar|iso|ice|jar|pak|sea|sfx|sit|sitx|lzma|war|xar|zoo|zipx|img|adf|dms|dmz)$/, - 'snd': /\.(mp3|wav|mp2|ogg|aac|aiff|mod|flac|m4a|mpc|oga|opus|ra|rm|vox|wma|8svx)$/, - 'vid': /\.(webm|avi|mpeg|mpg|mp4|mov|mkv|qt|wmv|vob|ogb|m4v|m4p|asf|mts|m2ts|3gp|flv|anim)$/, - 'cod': /\.(json|js|cpp|c|cxx|java|rb|s|tsx|ts|jsx|lua|as|coffee|ps1|py|r|rexx|spt|sptd|go|rs|sh|bash|vbs|cljs)$/, - 'doc': /\.(log|last|css|htm|html|rtf|doc|pdf|docx|txt|md|1st|asc|epub|xhtml|xml|amigaguide|info)$/ +export const Extensions: { [index: string]: RegExp } = { + 'exe': /\.([\.]*(exe|bat|com|msi|mui|cmd))+$/, + 'img': /\.([\.]*(png|jpeg|jpg|gif|pcx|tiff|raw|webp|svg|heif|bmp|ilbm|iff|lbm|ppm|pgw|pbm|pnm|psd))+$/, + 'arc': /\.([\.]*(zip|tar|rar|7zip|7z|dmg|shar|ar|bz2|lz|gz|tgz|lha|lzh|lzx|sz|xz|z|s7z|ace|apk|arp|arj|cab|car|cfs|cso|dar|iso|ice|jar|pak|sea|sfx|sit|sitx|lzma|war|xar|zoo|zipx|img|adf|dms|dmz))+$/, + 'snd': /\.([\.]*(mp3|wav|mp2|ogg|aac|aiff|mod|flac|m4a|mpc|oga|opus|ra|rm|vox|wma|8svx))+$/, + 'vid': /\.([\.]*(webm|avi|mpeg|mpg|mp4|mov|mkv|qt|wmv|vob|ogb|m4v|m4p|asf|mts|m2ts|3gp|flv|anim))+$/, + 'cod': /\.([\.]*(json|js|cpp|c|cxx|java|rb|s|tsx|ts|jsx|lua|as|coffee|ps1|py|r|rexx|spt|sptd|go|rs|sh|bash|vbs|cljs))+$/, + 'doc': /\.([\.]*(log|last|css|htm|html|rtf|doc|pdf|docx|txt|md|1st|asc|epub|xhtml|xml|amigaguide|info))+$/ }; const ExeMaskAll = 0o0001; const ExeMaskGroup = 0o0010; @@ -60,12 +61,19 @@ export function MakeId(stats: any): FileID { } } -function isModeExe(mode: number): Boolean { - return !!((mode & ExeMaskAll) || (mode & ExeMaskUser) || (mode & ExeMaskGroup)); +function isModeExe(mode: number, gid: number, uid: number): Boolean { + if (isWin) { + return false; + } + + const isGroup = gid ? process.getgid && gid === process.getgid() : false; + const isUser = uid ? process.getuid && uid === process.getuid() : false; + + return !!((mode & ExeMaskAll) || ((mode & ExeMaskUser) && isGroup) || ((mode & ExeMaskGroup) && isUser)); } -export function filetype(mode: number, extension: string): FileType { - if (isModeExe(mode) || extension.match(Extensions.exe)) { +export function filetype(mode: number, gid: number, uid: number, extension: string): FileType { + if (isModeExe(mode, gid, uid) || extension.match(Extensions.exe)) { return 'exe'; } else if (extension.match(Extensions.img)) { return 'img'; @@ -87,7 +95,7 @@ export function filetype(mode: number, extension: string): FileType { export interface FsApi { // public API // async methods that may require server access - list(dir: string, appendParent?: boolean, transferId?: number): Promise; + list(dir: string, transferId?: number): Promise; cd(path: string, transferId?: number): Promise; delete(parent: string, files: File[], transferId?: number): Promise; makedir(parent: string, name: string, transferId?: number): Promise; diff --git a/src/services/plugins/FsFtp.ts b/src/services/plugins/FsFtp.ts index 9596a184..e9a9aa27 100644 --- a/src/services/plugins/FsFtp.ts +++ b/src/services/plugins/FsFtp.ts @@ -197,7 +197,7 @@ class Client { } } - public list(path: string, appendParent = true): Promise { + public list(path: string): Promise { this.status = 'busy'; this.log('list', path); @@ -236,7 +236,7 @@ class Client { extension: '', mode: 0, readonly: false, - type: ftpFile.type !== 'd' && filetype(0, ext) || '', + type: ftpFile.type !== 'd' && filetype(0, 0, 0, ext) || '', isSym: false, id: { ino: mDate.getTime(), @@ -245,27 +245,8 @@ class Client { }; return file; }); - // TODO: build list of files - /* - dir: string; - name: string; - fullname: string; - extension: string; - cDate: Date; - mDate: Date; - length: number; - mode: number; - isDir: boolean; - readonly: boolean; - */ - resolve(files); - // if (appendParent && !this.api.isRoot(newpath)) { - // const parent = { ...Parent, dir: path }; - // resolve([parent].concat(files)); - // } else { - // resolve(files); - // } + resolve(files); } }); }); @@ -565,7 +546,7 @@ class FtpAPI implements FsApi { const fullPath = this.join(dir.dir, dir.fullname) await this.master.cd(fullPath); debugger; - const files = await this.master.list(fullPath, false); + const files = await this.master.list(fullPath); debugger; // first delete files found inside the folder await this.delete(fullPath, files); @@ -600,9 +581,9 @@ class FtpAPI implements FsApi { // return Promise.resolve(false); } - list(dir: string, appendParent = true): Promise { + list(dir: string): Promise { console.log('FsFtp.readDirectory', dir); - return this.master.list(dir, appendParent); + return this.master.list(dir); }; async stat(fullPath: string): Promise { diff --git a/src/services/plugins/FsGeneric.ts b/src/services/plugins/FsGeneric.ts index 2c70a6e2..35190852 100644 --- a/src/services/plugins/FsGeneric.ts +++ b/src/services/plugins/FsGeneric.ts @@ -92,7 +92,7 @@ class GenericApi implements FsApi { } as File); } - async list(dir: string, appendParent = true, transferId = -1): Promise { + async list(dir: string, transferId = -1): Promise { console.log('FsGeneric.readDirectory'); const pathExists = await this.isDir(dir); diff --git a/src/services/plugins/FsLocal.ts b/src/services/plugins/FsLocal.ts index 8d0ec994..237dcb22 100644 --- a/src/services/plugins/FsLocal.ts +++ b/src/services/plugins/FsLocal.ts @@ -87,6 +87,7 @@ class LocalApi implements FsApi { const unixPath = path.join(source, dirName).replace(/\\/g, '/'); try { console.log('mkdir', unixPath); + mkdir(unixPath, (err) => { if (err) { reject(err); @@ -96,6 +97,7 @@ class LocalApi implements FsApi { }); } catch (err) { + debugger; reject(err); } }); @@ -106,9 +108,8 @@ class LocalApi implements FsApi { return new Promise(async (resolve, reject) => { try { - console.log('delete', toDelete); - await del(toDelete, { force: true }); - resolve(files.length); + const deleted = await del(toDelete, { force: true, noGlob: true }); + resolve(deleted.length); } catch (err) { reject(err); } @@ -122,16 +123,27 @@ class LocalApi implements FsApi { if (!newName.match(invalidFileChars)) { console.log('valid !', oldPath, newPath); return new Promise((resolve, reject) => { - fs.rename(oldPath, newPath, (err) => { - if (err) { + // since node's fs.rename will overwrite the destination + // path if it exists, first check that file dones't exists + this.exists(newPath).then((exists) => { + if (exists) { reject({ - code: err.code, - message: err.message, - newName: newName, + code: 'EEXIST', oldName: file.fullname }); } else { - resolve(newName); + fs.rename(oldPath, newPath, (err) => { + if (err) { + reject({ + code: err.code, + message: err.message, + newName: newName, + oldName: file.fullname + }); + } else { + resolve(newName); + } + }); } }); }); @@ -190,7 +202,7 @@ class LocalApi implements FsApi { mode: stats.mode, isDir: stats.isDirectory(), readonly: false, - type: !stats.isDirectory() && filetype(stats.mode, format.ext.toLowerCase()) || '', + type: !stats.isDirectory() && filetype(stats.mode, stats.gid, stats.uid, format.ext.toLowerCase()) || '', isSym: stats.isSymbolicLink(), id: MakeId(stats) }; @@ -216,7 +228,7 @@ class LocalApi implements FsApi { } } - async list(dir: string, appendParent = true, transferId = -1): Promise { + async list(dir: string, transferId = -1): Promise { const pathExists = await this.isDir(dir); if (pathExists) { @@ -232,10 +244,16 @@ class LocalApi implements FsApi { for (var i = 0; i < items.length; i++) { const fullPath = path.join(dirPath, items[i]); const format = path.parse(fullPath); - let stats; + let name = fullPath; + let stats = null; + let target_stats = null; try { - stats = fs.statSync(path.join(dirPath, items[i])); + stats = fs.lstatSync(fullPath); + if (stats.isSymbolicLink()) { + target_stats = fs.statSync(fullPath); + name = fs.readlinkSync(fullPath); + } } catch (err) { console.warn('error getting stats for', path.join(dirPath, items[i]), err); stats = { @@ -251,20 +269,23 @@ class LocalApi implements FsApi { } } + const extension = path.parse(name).ext.toLowerCase(); + const mode = target_stats ? target_stats.mode : stats.mode; + const file: File = { dir: format.dir, fullname: items[i], name: format.name, - extension: format.ext.toLowerCase(), + extension: extension, cDate: stats.ctime, mDate: stats.mtime, bDate: stats.birthtime, length: stats.size, - mode: stats.mode, - isDir: stats.isDirectory(), + mode: mode, + isDir: target_stats ? target_stats.isDirectory() : stats.isDirectory(), readonly: false, - type: !stats.isDirectory() && filetype(stats.mode, format.ext.toLowerCase()) || '', + type: !(target_stats ? target_stats.isDirectory() : stats.isDirectory()) && filetype(mode, 0, 0, extension) || '', isSym: stats.isSymbolicLink(), id: MakeId(stats) }; @@ -275,15 +296,6 @@ class LocalApi implements FsApi { this.onList(dirPath); resolve(files); - // add parent - // if (appendParent && !this.isRoot(dir)) { - // debugger; - // const parent = { ...Parent, dir: dirPath }; - - // resolve([parent].concat(files)); - // } else { - // resolve(files); - // } } }); }); @@ -314,13 +326,10 @@ class LocalApi implements FsApi { }; } - static counter = 0; - - // TODO: handle stream error async putStream(readStream: fs.ReadStream, dstPath: string, progress: (pourcent: number) => void, transferId = -1): Promise { return new Promise((resolve: (val?: any) => void, reject: (val?: any) => void) => { - let count = LocalApi.counter++; let finished = false; + let readError = false; let bytesRead = 0; const throttledProgress = throttle(() => { progress(bytesRead) }, 800); @@ -328,7 +337,6 @@ class LocalApi implements FsApi { const reportProgress = new Transform({ transform(chunk: any, encoding: any, callback: any) { bytesRead += chunk.length; - // console.log('dataChunk', bytesRead / 1024, 'Ko'); throttledProgress(); callback(null, chunk); }, @@ -337,41 +345,49 @@ class LocalApi implements FsApi { readStream.once('error', (err) => { console.log('error on read stream'); + readError = true; readStream.destroy(); writeStream.destroy(err); }); - // console.log('open', count); const writeStream = fs.createWriteStream(dstPath); readStream.pipe(reportProgress) .pipe(writeStream); writeStream.once('finish', () => { - // console.log('finish', count); finished = true; - // resolve(); }); writeStream.once('error', err => { + // remove created file if it's empty and there was a problem + // accessing the source file: we will report an error to the + // user so there's no need to leave an empty file + if (readError && !bytesRead && !writeStream.bytesWritten) { + console.log('cleaning up fs'); + fs.unlink(dstPath, (err) => { + if (!err) { + console.log('cleaned-up fs'); + } else { + console.log('error cleaning-up fs', err); + } + }); + } reject(err); }); writeStream.once('close', () => { - // console.log('close', count); if (finished) { resolve(); } else { reject(); } }); - // writeStream.once('end', () => console.log('end', count)); + writeStream.once('error', err => { reject(err); - // console.log('error', count) }); - // writeStream.once('destroy', () => console.log('destroy', count)); - }) + }); } getParentTree(dir: string): Array<{ dir: string, fullname: string, name: string }> { diff --git a/src/services/plugins/FsSimpleFtp.ts b/src/services/plugins/FsSimpleFtp.ts index 77e30691..6943e8a8 100644 --- a/src/services/plugins/FsSimpleFtp.ts +++ b/src/services/plugins/FsSimpleFtp.ts @@ -340,7 +340,7 @@ class SimpleFtpApi implements FsApi { } as File); } - list(path: string, appendParent = true, transferId = -1): Promise { + list(path: string, transferId = -1): Promise { return new Promise(async (resolve, reject) => { const newpath = this.pathpart(path); @@ -364,7 +364,7 @@ class SimpleFtpApi implements FsApi { extension: '', mode: 0, readonly: false, - type: !ftpFile.isDirectory && filetype(0, ext) || '', + type: !ftpFile.isDirectory && filetype(0, 0, 0, ext) || '', isSym: false, id: { ino: mDate.getTime(), diff --git a/src/state/appState.ts b/src/state/appState.ts index 7cd7b56d..85f4bb61 100644 --- a/src/state/appState.ts +++ b/src/state/appState.ts @@ -11,7 +11,7 @@ declare var ENV: any; // wait 1 sec before showing badge: this avoids // flashing (1) badge when the transfer is very fast -const SHOW_BADGE_DELAY = 1000; +const SHOW_BADGE_DELAY = 600; /** * Interface for a clipboard entry @@ -95,10 +95,6 @@ export class AppState { * @returns {Promise} */ prepareClipboardTransferTo(cache: FileState) { - if (!this.clipboard.files.length) { - return; - } - const options = { files: this.clipboard.files, srcFs: this.clipboard.srcFs, @@ -109,12 +105,15 @@ export class AppState { }; return this.addTransfer(options) - .then(() => { + .then((res) => { + debugger; if (options.dstPath === cache.path && options.dstFsName === cache.getFS().name) { // FIX ME: since watcher has been implemented, there's no need to reload cache // if destination is local fs cache.reload(); } + + return res; }); } @@ -142,11 +141,14 @@ export class AppState { }; return this.addTransfer(options) - .then(() => { + .then((res) => { + debugger; if (options.dstPath === dstCache.path && options.dstFsName === dstCache.getFS().name) { dstCache.reload(); } - }).catch(() => { + + return res; + }).catch((err) => { debugger; }); } @@ -278,7 +280,21 @@ export class AppState { @action // addTransfer(srcFs: FsApi, dstFs: FsApi, files: File[], srcPath: string, dstPath: string) { - addTransfer(options: TransferOptions) { + async addTransfer(options: TransferOptions) { + let isDir = false; + + try { + isDir = await options.dstFs.isDir(options.dstPath); + } catch (err) { + isDir = false; + } + + if (!isDir) { + return Promise.reject({ + code: 'NODEST' + }); + } + console.log('addTransfer', options.files, options.srcFs, options.dstFs, options.dstPath); const batch = new Batch(options.srcFs, options.dstFs, options.srcPath, options.dstPath); this.transfers.unshift(batch); @@ -299,12 +315,7 @@ export class AppState { } this.activeTransfers.push(batch); - return batch.start() - .catch(err => { - debugger; - }) - }).catch((err) => { - debugger; + return batch.start(); }); } @@ -354,9 +365,16 @@ export class AppState { @action updateSelection(cache: FileState, newSelection: File[]) { - // console.log('updateSelection', newSelection.length); + console.log('updateSelection', newSelection.length); cache.selected.replace(newSelection); - cache.updateSelection(); + for (let selected of cache.selected) { + console.log(selected.fullname, selected.id.dev, selected.id.ino); + } + // cache.updateSelection(); + // console.log('== apres =='); + // for (let selected of cache.selected) { + // console.log(selected.fullname, selected.id.dev, selected.id.ino); + // } } @observable @@ -372,6 +390,8 @@ export class AppState { this.clipboard = { srcFs: fileState.getAPI(), srcPath: fileState.path, files }; + console.log('clipboard', files); + return files.length; } diff --git a/src/state/fileState.ts b/src/state/fileState.ts index a0f3bbe5..9ccd70ea 100644 --- a/src/state/fileState.ts +++ b/src/state/fileState.ts @@ -8,9 +8,7 @@ import * as process from 'process'; import { AppState } from "./appState"; import { TSORT_METHOD_NAME, TSORT_ORDER } from "../services/FsSort"; -const isWin = process.platform === "win32"; - -export type TStatus = 'blank' | 'busy' | 'ok' | 'login' | 'offline'; +export type TStatus = 'busy' | 'ok' | 'login' | 'offline'; export class FileState { /* observable properties start here */ @@ -46,6 +44,9 @@ export class FileState { @observable status: TStatus; + @observable + error = false; + cmd: string = ''; // @observable @@ -63,8 +64,9 @@ export class FileState { current: number = -1; @action - setStatus(status: TStatus) { + setStatus(status: TStatus, error = false) { this.status = status; + this.error = error; } @action @@ -77,11 +79,15 @@ export class FileState { @action navHistory(dir = -1, force = false) { if (!this.history.length) { + debugger; console.warn('attempting to nav in empty history'); - this.setStatus('blank'); return; } + if (force) { + debugger; + } + const history = this.history; const current = this.current; const length = history.length; @@ -96,12 +102,19 @@ export class FileState { this.current = newCurrent; const path = history[current + dir]; - if (path !== this.path || force) { - // console.log('opening path from history', path); - this.cd(path, '', true, true); - } else { - console.warn('preventing endless loop'); - } + + return this.cd(path, '', true, true) + .catch(() => { + // whatever happens, we want switch to that folder + this.updatePath(path, true); + this.emptyCache(); + }); + // if (path !== this.path || force) { + // // console.log('opening path from history', path); + // this.cd(path, '', true, true); + // } else { + // console.warn('preventing endless loop'); + // } } // /history @@ -194,6 +207,7 @@ export class FileState { // only reload directory if connection hasn't been lost otherwise we enter // into an infinite loop if (this.api.isConnected()) { + debugger; this.navHistory(0); this.setStatus('ok'); } @@ -303,11 +317,20 @@ export class FileState { } reload() { - if (this.status === 'ok') { - this.navHistory(0, true); + if (this.status !== 'busy') { + this.cd(this.path, "", true, true) + .catch(this.emptyCache); } } + @action + emptyCache = () => { + this.files.clear(); + this.clearSelection(); + this.setStatus('ok', true); + console.log('emptycache'); + } + handleError = (error: any) => { console.log('handleError', error); this.setStatus('ok'); @@ -320,13 +343,13 @@ export class FileState { async cd(path: string, path2: string = '', skipHistory = false, skipContext = false): Promise { // first updates fs (eg. was local fs, is now ftp) // console.log('cd', path, this.path); - if (this.path !== path) { if (this.getNewFS(path, skipContext)) { this.server = this.fs.serverpart(path); this.credentials = this.fs.credentials(path); } else { - this.navHistory(0); + debugger; + // this.navHistory(0); return Promise.reject({ message: i18next.t('ERRORS.CANNOT_READ_FOLDER', { folder: path }), code: 'NO_FS' @@ -334,18 +357,13 @@ export class FileState { } } - return this.cwd(path, path2, skipHistory, skipContext); + return this.cwd(path, path2, skipHistory); } @action @needsConnection // changes current path and retrieves file list - async cwd(path: string, path2: string = '', skipHistory = false, skipContext = false): Promise { - // try { - // await this.waitForConnection(); - // } catch (err) { - // return this.cd(path, path2, false, true); - // } + async cwd(path: string, path2: string = '', skipHistory = false): Promise { const joint = path2 ? this.api.join(path, path2) : this.api.sanityze(path); this.cmd = 'cwd'; @@ -359,26 +377,24 @@ export class FileState { }) .catch((error) => { this.cmd = ''; - console.log('path not valid ?', joint, 'restoring previous path'); + console.log('error cd/list for path', joint, 'error was', error); this.setStatus('ok'); - this.navHistory(0); const localizedError = getLocalizedError(error); - return Promise.reject(localizedError); + //return Promise.reject(localizedError); + throw localizedError; }); } @action @needsConnection - async list(path: string, appendParent?: boolean): Promise { - return this.api.list(path, appendParent) + async list(path: string): Promise { + return this.api.list(path) .then((files: File[]) => { runInAction(() => { this.files.replace(files); // update the cache's selection, keeping files that were previously selected this.updateSelection(); - // TODO: sync caches ? - this.setStatus('ok'); }); @@ -490,7 +506,6 @@ export class FileState { } openDirectory(file: { dir: string, fullname: string }) { - // console.log('need to read dir', file.dir, file.fullname); return this.cd(file.dir, file.fullname).catch(this.handleError); } @@ -502,7 +517,10 @@ export class FileState { openParentDirectory() { const parent = { dir: this.path, fullname: '..' }; - this.openDirectory(parent); + this.openDirectory(parent).catch(() => { + this.updatePath(this.api.join(this.path, '..'), true); + this.emptyCache(); + }); } isRoot(path: string): boolean { diff --git a/src/state/viewState.ts b/src/state/viewState.ts index e6589367..69d50f79 100644 --- a/src/state/viewState.ts +++ b/src/state/viewState.ts @@ -41,7 +41,7 @@ export class ViewState { } @action - setVisibleCache(index: number, activateInput = false) { + setVisibleCache(index: number) { const previous = this.getVisibleCache(); const next = this.caches[index]; // do nothing if previous === next @@ -58,8 +58,6 @@ export class ViewState { @action removeCache(index: number) { - // const toDelete = this.caches.splice(index, 1)[0]; - return this.caches.splice(index, 1)[0]; } diff --git a/src/transfers/batch.ts b/src/transfers/batch.ts index 06ed5a47..f81c8f78 100644 --- a/src/transfers/batch.ts +++ b/src/transfers/batch.ts @@ -4,11 +4,11 @@ import { FileTransfer } from "./fileTransfer"; import { Deferred } from "../utils/deferred"; import { getLocalizedError } from "../locale/error"; import { Readable } from "stream"; +import { getSelectionRange } from "../utils/fileUtils"; const MAX_TRANSFERS = 2; const MAX_ERRORS = 5; const RENAME_SUFFIX = '_'; -const REGEX_EXTENSION = /\.(?=[^0-9])/; type Status = 'started' | 'queued' | 'error' | 'done' | 'cancelled' | 'calculating'; @@ -46,7 +46,7 @@ export class Batch { } get numErrors(): number { - return this.files.reduce((acc, val) => acc + (val.error && 1 || 0), 0); + return this.files.reduce((acc, val) => acc + ((val.error || val.status === 'cancelled') && 1 || 0), 0); } public isExpanded: boolean = false; @@ -70,57 +70,66 @@ export class Batch { } @action - onEndTransfer = (status: Status = 'done') => { + onEndTransfer = () => { // console.log('transfer ended ! duration=', Math.round((new Date().getTime() - this.startDate.getTime()) / 1000), 'sec(s)'); // console.log('destroy batch, new maxId', Batch.maxId); - this.status = status; + this.status = 'done'; + return Promise.resolve(this.numErrors === 0); } @action - start(): Promise { + start(): Promise { console.log('starting batch'); if (this.status === 'queued') { this.slotsAvailable = MAX_TRANSFERS; this.status = 'started'; this.transferDef = new Deferred(); - this.transferDef.promise.then( - this.onEndTransfer - ).catch((err: Error) => { - console.log('error transfer', err); - this.status = 'error'; - return Promise.reject(err); - }); - this.transfersDone = 0; this.startDate = new Date(); this.queueNextTransfers(); } - return this.transferDef.promise; + // return this.transferDef.promise; + return this.transferDef.promise.then( + this.onEndTransfer + ).catch((err: Error) => { + console.log('error transfer', err); + this.status = 'error'; + return Promise.reject(err); + }); } @action - updatePendingTransfers(subDir: string, newFilename: string) { + updatePendingTransfers(subDir: string, newFilename: string, cancel = false) { // TODO: escape '(' & ')' in subDir if needed const regExp = new RegExp('^(' + subDir + ')'); const files = this.files.filter((file) => file.subDirectory.match(regExp) !== null); - let newPrefix = ''; - // need to rename - if (!subDir.match(new RegExp(newFilename + '$'))) { - const parts = subDir.split('/'); - parts[parts.length - 1] = newFilename; - newPrefix = parts.join('/'); - } - for (let transfer of files) { - // enable files inside this directory - if (transfer.subDirectory === subDir) { - transfer.ready = true; + // destination directory for these files could not be created: we cancel these transfers + if (cancel) { + for (let file of files) { + file.status = 'cancelled'; + this.transfersDone++; } - // for all files (ie. this directory & subdirectories) - // rename this part if needed - if (newPrefix) { - transfer.newSub = transfer.subDirectory.replace(regExp, newPrefix); + } else { + let newPrefix = ''; + // need to rename + if (!subDir.match(new RegExp(newFilename + '$'))) { + const parts = subDir.split('/'); + parts[parts.length - 1] = newFilename; + newPrefix = parts.join('/'); + } + + for (let transfer of files) { + // enable files inside this directory + if (transfer.subDirectory === subDir) { + transfer.ready = true; + } + // for all files (ie. this directory & subdirectories) + // rename this part if needed + if (newPrefix) { + transfer.newSub = transfer.subDirectory.replace(regExp, newPrefix); + } } } } @@ -146,7 +155,8 @@ export class Batch { } @action - onTransferError = (transfer: FileTransfer, err: Error) => { + onTransferError = (transfer: FileTransfer, err: any) => { + // console.log('transfer error', transfer.file.fullname, err); transfer.status = 'error'; transfer.error = getLocalizedError(err); this.errors++; @@ -179,13 +189,14 @@ export class Batch { try { newFilename = await this.renameOrCreateDir(transfer, fullDstPath); + transfer.status = 'done'; } catch (err) { console.log('error creating directory', err); - transfer.status = 'error'; + this.onTransferError(transfer, err); } if (this.status === 'cancelled') { - debugger; + console.warn('startTransfer while cancelled (1)'); } if (!transfer.file.isDir) { @@ -206,7 +217,6 @@ export class Batch { // }); await dstFs.putStream(stream, dstFs.join(fullDstPath, newFilename), (bytesRead: number) => { - // console.log('read', bytesRead); this.onData(transfer, bytesRead); }, this.id); @@ -226,19 +236,19 @@ export class Batch { //} this.onTransferError(transfer, err); if (this.errors > MAX_ERRORS) { + console.warn('maximum errors occurred: cancelling upcoming file transfers'); this.status = 'error'; this.cancelFiles(); } } } else { - transfer.status = 'done'; // make transfers with this directory ready - this.updatePendingTransfers(srcFs.join(transfer.subDirectory, wantedName), newFilename); + this.updatePendingTransfers(srcFs.join(transfer.subDirectory, wantedName), newFilename, (transfer as FileTransfer).status !== 'done'); } if (this.status === 'cancelled') { - debugger; + console.warn('startTransfer while cancelled (2)'); } this.transfersDone++; @@ -247,11 +257,31 @@ export class Batch { if (this.status !== 'error' && this.transfersDone < this.files.length) { this.queueNextTransfers(); } else { + for (let transfer of this.files) { + console.log(transfer.status, transfer); + } // console.log('no more transfers !!'); - this.transferDef.resolve(); + if (this.numErrors === this.files.length) { + for (let transfer of this.files) { + console.log(transfer.status, transfer.file.fullname, transfer); + } + + this.transferDef.reject({ + code: '' + }); + } else { + for (let transfer of this.files) { + console.log(transfer.status, transfer.file.fullname, transfer); + } + + this.transferDef.resolve(); + } } } + // TODO: do not infinite loop if directory cannot be created + // if not, reject and then we should set each file found inside + // that directory to error async renameOrCreateDir(transfer: FileTransfer, dstPath: string): Promise { const isDir = transfer.file.isDir; const dstFs = this.dstFs; @@ -276,8 +306,11 @@ export class Batch { if (isDir) { // directory already exists: for now, simply use it if (!exists) { - // TODO: handle error - const newDir = await dstFs.makedir(dstPath, newName, this.id); + try { + const newDir = await dstFs.makedir(dstPath, newName, this.id); + } catch (err) { + return Promise.reject(err); + } } else if (!stats.isDir) { // exists but is a file: attempt to create a directory with newName let success = false; @@ -287,7 +320,7 @@ export class Batch { await dstFs.makedir(dstPath, newName, this.id); success = true; } catch (err) { - debugger; + return Promise.reject(err); } } } @@ -306,11 +339,14 @@ export class Batch { let newName = wantedName; // put suffix before the extension, so foo.txt will be renamed foo_1.txt to preserve the extension - // TODO: avoid endless loop, give up after enough tries + // TODO: avoid endless loop, give up after max tries while (exists) { - const split = wantedName.split(REGEX_EXTENSION); - split[0] += RENAME_SUFFIX + i++; - newName = split.join('.'); + const range = getSelectionRange(wantedName); + const prefix = wantedName.startsWith('.') ? wantedName : wantedName.substring(range.start, range.length); + const suffix = wantedName.startsWith('.') ? '' : wantedName.substring(range.length); + + newName = prefix + RENAME_SUFFIX + i++ + suffix; + const tmpPath = this.dstFs.join(this.dstPath, newName); try { exists = await this.dstFs.exists(tmpPath, this.id); @@ -388,24 +424,36 @@ export class Batch { // dir: need to call list for each directry to get files for (let dir of dirs) { - transfers.push({ + const transfer: FileTransfer = { file: dir, status: 'queued', progress: 0, subDirectory, newSub: subDirectory, ready: subDirectory === '' - }); + }; + + transfers.push(transfer); // get directory listing const currentPath = this.srcFs.join(dir.dir, dir.fullname); // note: this is needed for FTP only: since Ftp.list(path) has to ignore the path // and lists the contents of the CWD (which is changed by calling Ftp.cd()) - await this.srcFs.cd(currentPath); + let subFiles: File[] = null; // /note - const subFiles = await this.srcFs.list(currentPath, false); - const subDir = this.srcFs.join(subDirectory, dir.fullname); - transfers = transfers.concat(await this.getFileList(subFiles, subDir)); + try { + await this.srcFs.cd(currentPath); + subFiles = await this.srcFs.list(currentPath); + const subDir = this.srcFs.join(subDirectory, dir.fullname); + transfers = transfers.concat(await this.getFileList(subFiles, subDir)); + } catch (err) { + // TODO: set the transfer to error/skip + // then, simply skip it when doing the transfer + this.onTransferError(transfer, { code: 'ENOENT' }); + this.transfersDone++; + console.log('could not get directory content for', currentPath); + console.log('directory was still added to transfer list for consistency'); + } } return Promise.resolve(transfers); @@ -414,7 +462,7 @@ export class Batch { @action async setFileList(files: File[]) { return this.getFileList(files).then((transfers) => { - // console.log('got files', transfers); + console.log('got files', transfers); this.files.replace(transfers); }).catch((err) => { return Promise.reject(err); diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts new file mode 100644 index 00000000..6e90ff05 --- /dev/null +++ b/src/utils/fileUtils.ts @@ -0,0 +1,44 @@ +import { Extensions } from '../services/Fs'; + +export const REGEX_EXTENSION = /\.(?=[^0-9])/; + +export interface SelectionRange { + start: number; + length: number; +} + +function getExtensionIndex(filename: string): number { + let index = -1; + for (let ext of Object.keys(Extensions)) { + const matches = filename.match(Extensions[ext]); + if (matches && (index === -1 || matches.index < index)) { + index = matches.index; + } + } + + return index; +} + +export function getSelectionRange(filename: string): SelectionRange { + const length = filename.length; + + if (filename.startsWith('.')) { + return { + start: 1, + length: length + }; + } else { + const index = getExtensionIndex(filename); + if (index > -1) { + return { + start: 0, + length: index + }; + } else { + return { + start: 0, + length: length + }; + } + } +} \ No newline at end of file diff --git a/webpack.config.e2e.js b/webpack.config.e2e.js index 3121b794..e00d5620 100644 --- a/webpack.config.e2e.js +++ b/webpack.config.e2e.js @@ -52,6 +52,18 @@ const baseConfig = { outputPath: 'fonts/' } }] + }, + // images embbeded into css + { + test: /\.(png|jpg|gif)$/i, + use: [ + { + loader: 'url-loader', + options: { + limit: 8192 + } + } + ] } ] }, diff --git a/webpack.config.js b/webpack.config.js index b22f0543..b424351f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -49,6 +49,18 @@ const baseConfig = { outputPath: 'fonts/' } }] + }, + // images embbeded into css + { + test: /\.(png|jpg|gif)$/i, + use: [ + { + loader: 'url-loader', + options: { + limit: 8192 + } + } + ] } ] }, diff --git a/webpack.config.production.js b/webpack.config.production.js index 3f4ec03f..6117387b 100644 --- a/webpack.config.production.js +++ b/webpack.config.production.js @@ -47,6 +47,18 @@ const baseConfig = { outputPath: 'fonts/' } }] + }, + // images embbeded into css + { + test: /\.(png|jpg|gif)$/i, + use: [ + { + loader: 'url-loader', + options: { + limit: 8192 + } + } + ] } ] },