├── .github ├── FUNDING.yml └── workflows │ └── lint.yml ├── LICENSE ├── README.md ├── icon.png ├── index.js ├── index.scss ├── locales.js ├── metadata.json ├── package.json ├── screenshot.png └── webpack.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: andersevenrud 4 | patreon: andersevenrud 5 | open_collective: osjs 6 | liberapay: os-js 7 | custom: https://paypal.me/andersevenrud 8 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint tests 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Lint tests (node latest) 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-node@v1 10 | with: 11 | node-version: '13.x' 12 | - run: npm install 13 | - run: npm run eslint 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | OS.js - JavaScript Cloud/Web Desktop Platform 4 | 5 | Copyright (c) Anders Evenrud 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | OS.js Logo 3 |

4 | 5 | [OS.js](https://www.os-js.org/) is an [open-source](https://raw.githubusercontent.com/os-js/OS.js/master/LICENSE) web desktop platform with a window manager, application APIs, GUI toolkit, filesystem abstractions and much more. 6 | 7 | [![Support](https://img.shields.io/badge/patreon-support-orange.svg)](https://www.patreon.com/user?u=2978551&ty=h&u=2978551) 8 | [![Support](https://img.shields.io/badge/opencollective-donate-red.svg)](https://opencollective.com/osjs) 9 | [![Donate](https://img.shields.io/badge/liberapay-donate-yellowgreen.svg)](https://liberapay.com/os-js/) 10 | [![Donate](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://paypal.me/andersevenrud) 11 | [![Community](https://img.shields.io/badge/join-community-green.svg)](https://community.os-js.org/) 12 | 13 | # OS.js v3 File Manager Application 14 | 15 | This is the File Manager Application for OS.js v3 16 | 17 | ![Screenshot](https://raw.githubusercontent.com/os-js/osjs-filemanager-application/master/screenshot.png) 18 | 19 | ## Installation 20 | 21 | ```bash 22 | npm install --save --production @osjs/filemanager-application 23 | npm run package:discover 24 | ``` 25 | 26 | ## Contribution 27 | 28 | * **Sponsor on [Github](https://github.com/sponsors/andersevenrud)** 29 | * **Become a [Patreon](https://www.patreon.com/user?u=2978551&ty=h&u=2978551)** 30 | * **Support on [Open Collective](https://opencollective.com/osjs)** 31 | * [Contribution Guide](https://github.com/os-js/OS.js/blob/master/CONTRIBUTING.md) 32 | 33 | ## Documentation 34 | 35 | See the [Official Manuals](https://manual.os-js.org/) for articles, tutorials and guides. 36 | 37 | ## Links 38 | 39 | * [Official Chat](https://gitter.im/os-js/OS.js) 40 | * [Community Forums and Announcements](https://community.os-js.org/) 41 | * [Homepage](https://os-js.org/) 42 | * [Twitter](https://twitter.com/osjsorg) ([author](https://twitter.com/andersevenrud)) 43 | * [Google+](https://plus.google.com/b/113399210633478618934/113399210633478618934) 44 | * [Facebook](https://www.facebook.com/os.js.org) 45 | * [Docker Hub](https://hub.docker.com/u/osjs/) 46 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/osjs-filemanager-application/ccbfc96000ec8b0bcfd5d73654eaf1199bc926ba/icon.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | // TODO: Check if host-system:/ '..' is an issue here 32 | 33 | import osjs from 'osjs'; 34 | import {h, app} from 'hyperapp'; 35 | 36 | import './index.scss'; 37 | import * as translations from './locales.js'; 38 | import {name as applicationName} from './metadata.json'; 39 | import { 40 | Box, 41 | Button, 42 | TextField, 43 | Toolbar, 44 | Menubar, 45 | MenubarItem, 46 | Statusbar, 47 | Panes, 48 | listView 49 | } from '@osjs/gui'; 50 | 51 | /** 52 | * Creates default settings 53 | */ 54 | const createDefaultSettings = () => ({ 55 | showHiddenFiles: false, 56 | showDate: false 57 | }); 58 | 59 | /** 60 | * Creates the default window options 61 | */ 62 | const createWindowOptions = (core, proc, title) => ({ 63 | id: 'FileManager', 64 | icon: proc.resource(proc.metadata.icon), 65 | title, 66 | attributes: { 67 | mediaQueries: { 68 | small: 'screen and (max-width: 400px)' 69 | } 70 | }, 71 | dimension: Object.assign({ 72 | width: 400, 73 | height: 400 74 | }, core.config('filemanager.defaultWindowSize', {})), 75 | }); 76 | 77 | /** 78 | * Diverts callback based on drop action event 79 | */ 80 | const divertDropAction = (browser, virtual) => (ev, data, files) => { 81 | if (files.length) { 82 | browser(files); 83 | } else if (data && data.path && data.filename) { 84 | virtual(data); 85 | } 86 | }; 87 | 88 | /** 89 | * HoF for dialogs 90 | */ 91 | const usingPositiveButton = cb => (btn, value) => { 92 | if (['yes', 'ok'].indexOf(btn) !== -1) { 93 | cb(value); 94 | } 95 | }; 96 | 97 | /** 98 | * Triggers a browser upload 99 | */ 100 | const triggerBrowserUpload = (cb) => { 101 | const field = document.createElement('input'); 102 | field.type = 'file'; 103 | field.onchange = () => { 104 | if (field.files.length > 0) { 105 | cb(field.files); 106 | } 107 | }; 108 | field.click(); 109 | }; 110 | 111 | /** 112 | * Checks if given filename is a dotted 113 | */ 114 | const isSpecialFile = filename => ['..', '.'].indexOf(filename) !== -1; 115 | 116 | /** 117 | * Creates initial paths 118 | */ 119 | const createInitialPaths = (core, proc) => { 120 | const homePath = {path: core.config('vfs.defaultPath', 'home:/')}; 121 | const initialPath = proc.args.path 122 | ? Object.assign({}, homePath, proc.args.path) 123 | : homePath; 124 | 125 | return {homePath, initialPath}; 126 | }; 127 | 128 | const getDirectoryCount = files => 129 | files.filter(file => file.isDirectory).length; 130 | const getFileCount = files => 131 | files.filter(file => !file.isDirectory).length; 132 | const getTotalSize = files => 133 | files.reduce((total, file) => total + (file.size || 0), 0); 134 | 135 | /** 136 | * Formats directory status message 137 | */ 138 | const formatStatusMessage = (core, files) => { 139 | const {translatable} = core.make('osjs/locale'); 140 | const __ = translatable(translations); 141 | 142 | return (path, files) => { 143 | const directoryCount = getDirectoryCount(files); 144 | const fileCount = getFileCount(files); 145 | const totalSize = getTotalSize(files); 146 | const directoryCountMessage = `${directoryCount} ${__(directoryCount === 1 ? 'SINGLE_DIR' : 'MULTI_DIR')}`; 147 | const fileCountMessage = `${fileCount} ${__(fileCount === 1 ? 'SINGLE_FILE' : 'MULTI_FILE')}`; 148 | 149 | if (directoryCount > 0 && fileCount > 0) { 150 | return __('LBL_DIR_AND_FILE_STATUS', directoryCountMessage, fileCountMessage, totalSize); 151 | } else if (directoryCount > 0) { 152 | return __('LBL_DIR_OR_FILE_STATUS', directoryCountMessage, totalSize); 153 | } else { 154 | return __('LBL_DIR_OR_FILE_STATUS', fileCountMessage, totalSize); 155 | } 156 | }; 157 | }; 158 | 159 | /** 160 | * Mount view rows Factory 161 | */ 162 | const mountViewRowsFactory = (core) => { 163 | const fs = core.make('osjs/fs'); 164 | const getMountpoints = () => fs.mountpoints(true); 165 | 166 | return () => getMountpoints().map(m => ({ 167 | columns: [{ 168 | icon: m.icon, 169 | label: m.label 170 | }], 171 | data: m 172 | })); 173 | }; 174 | 175 | /** 176 | * File view columns Factory 177 | */ 178 | const listViewColumnFactory = (core, proc) => { 179 | const {translate: _, translatable} = core.make('osjs/locale'); 180 | const __ = translatable(translations); 181 | 182 | return () => { 183 | const columns = [{ 184 | label: _('LBL_NAME'), 185 | style: { 186 | minWidth: '20em' 187 | } 188 | }]; 189 | 190 | if (proc.settings.showDate) { 191 | columns.push({ 192 | label: __('LBL_DATE') 193 | }); 194 | } 195 | 196 | return [ 197 | ...columns, 198 | { 199 | label: _('LBL_TYPE'), 200 | style: { 201 | maxWidth: '150px' 202 | } 203 | }, { 204 | label: _('LBL_SIZE'), 205 | style: { 206 | flex: '0 0 7em', 207 | textAlign: 'right' 208 | } 209 | } 210 | ]; 211 | }; 212 | }; 213 | 214 | /** 215 | * File view rows Factory 216 | */ 217 | const listViewRowFactory = (core, proc) => { 218 | const fs = core.make('osjs/fs'); 219 | const {format: formatDate} = core.make('osjs/locale'); 220 | const getFileIcon = file => file.icon || fs.icon(file); 221 | 222 | const formattedDate = f => { 223 | if (f.stat) { 224 | const rawDate = f.stat.mtime || f.stat.ctime; 225 | if (rawDate) { 226 | try { 227 | const d = new Date(rawDate); 228 | return `${formatDate(d, 'shortDate')} ${formatDate(d, 'shortTime')}`; 229 | } catch (e) { 230 | return rawDate; 231 | } 232 | } 233 | } 234 | 235 | return ''; 236 | }; 237 | 238 | return (list) => list.map(f => { 239 | const columns = [{ 240 | label: f.filename, 241 | icon: getFileIcon(f) 242 | }]; 243 | 244 | if (proc.settings.showDate) { 245 | columns.push(formattedDate(f)); 246 | } 247 | 248 | return { 249 | key: f.path, 250 | data: f, 251 | columns: [ 252 | ...columns, 253 | f.mime, 254 | f.humanSize 255 | ] 256 | }; 257 | }); 258 | }; 259 | 260 | /** 261 | * VFS action Factory 262 | */ 263 | const vfsActionFactory = (core, proc, win, dialog, state) => { 264 | const vfs = core.make('osjs/vfs'); 265 | const {pathJoin} = core.make('osjs/fs'); 266 | const {translatable} = core.make('osjs/locale'); 267 | const __ = translatable(translations); 268 | 269 | const refresh = (fileOrWatch) => { 270 | // FIXME This should be implemented a bit better 271 | /* 272 | if (fileOrWatch === true && core.config('vfs.watch')) { 273 | return; 274 | } 275 | */ 276 | 277 | win.emit('filemanager:navigate', state.currentPath, undefined, fileOrWatch); 278 | }; 279 | 280 | const action = async (promiseCallback, refreshValue, defaultError) => { 281 | try { 282 | win.setState('loading', true); 283 | 284 | const result = await promiseCallback(); 285 | refresh(refreshValue); 286 | return result; 287 | } catch (error) { 288 | dialog('error', error, defaultError || __('MSG_ERROR')); 289 | } finally { 290 | win.setState('loading', false); 291 | } 292 | 293 | return []; 294 | }; 295 | 296 | const writeRelative = f => { 297 | const d = dialog('progress', f); 298 | 299 | return vfs.writefile({ 300 | path: pathJoin(state.currentPath.path, f.name) 301 | }, f, { 302 | pid: proc.pid, 303 | onProgress: (ev, p) => d.setProgress(p) 304 | }).then((result) => { 305 | d.destroy(); 306 | return result; 307 | }).catch((error) => { 308 | d.destroy(); 309 | throw error; 310 | }); 311 | }; 312 | 313 | const uploadBrowserFiles = (files) => { 314 | Promise.all(files.map(writeRelative)) 315 | .then(() => refresh(files[0].name)) // FIXME: Select all ? 316 | .catch(error => dialog('error', error, __('MSG_UPLOAD_ERROR'))); 317 | }; 318 | 319 | const uploadVirtualFile = (data) => { 320 | const dest = {path: pathJoin(state.currentPath.path, data.filename)}; 321 | if (dest.path !== data.path) { 322 | action(() => vfs.copy(data, dest, {pid: proc.pid}), true, __('MSG_UPLOAD_ERROR')); 323 | } 324 | }; 325 | 326 | const drop = divertDropAction(uploadBrowserFiles, uploadVirtualFile); 327 | 328 | const readdir = async (dir, history, selectFile) => { 329 | if (win.getState('loading')) { 330 | return; 331 | } else if (Array.isArray(dir)) { 332 | dir = dir[0]; 333 | } 334 | 335 | try { 336 | const message = __('LBL_LOADING', dir.path); 337 | const options = { 338 | showHiddenFiles: proc.settings.showHiddenFiles 339 | }; 340 | 341 | win.setState('loading', true); 342 | win.emit('filemanager:status', message); 343 | 344 | const list = await vfs.readdir(dir, options); 345 | 346 | // NOTE: This sets a restore argument in the application session 347 | proc.args.path = dir; 348 | 349 | state.currentPath = dir; 350 | 351 | if (typeof history === 'undefined' || history === false) { 352 | win.emit('filemanager:historyPush', dir); 353 | } else if (history === 'clear') { 354 | win.emit('filemanager:historyClear'); 355 | } 356 | 357 | win.emit('filemanager:readdir', {list, path: dir.path, selectFile}); 358 | win.emit('filemanager:title', dir.path); 359 | } catch (error) { 360 | dialog('error', error, __('MSG_READDIR_ERROR', dir.path)); 361 | } finally { 362 | state.currentFile = []; 363 | win.setState('loading', false); 364 | } 365 | }; 366 | 367 | const upload = () => triggerBrowserUpload(files => { 368 | writeRelative(files[0]) 369 | .then(() => refresh(files[0].name)) 370 | .catch(error => dialog('error', error, __('MSG_UPLOAD_ERROR'))); 371 | }); 372 | 373 | const paste = (move, currentPath) => ({items, callback}) => { 374 | const promises = items.map(item => { 375 | const dest = { 376 | path: pathJoin(currentPath.path, item.filename) 377 | }; 378 | 379 | return move 380 | ? vfs.move(item, dest, {pid: proc.pid}) 381 | : vfs.copy(item, dest, {pid: proc.pid}); 382 | }); 383 | 384 | return Promise 385 | .all(promises) 386 | .then(results => { 387 | refresh(true); 388 | 389 | if (typeof callback === 'function') { 390 | callback(); 391 | } 392 | 393 | return results; 394 | }) 395 | .catch(error => dialog('error', error, __('MSG_PASTE_ERROR'))); 396 | }; 397 | 398 | return { 399 | download: files => files.forEach(file => vfs.download(file)), 400 | upload, 401 | refresh, 402 | action, 403 | drop, 404 | readdir, 405 | paste 406 | }; 407 | }; 408 | 409 | /** 410 | * Clipboard action Factory 411 | */ 412 | const clipboardActionFactory = (core, state, vfs) => { 413 | const clipboard = core.make('osjs/clipboard'); 414 | 415 | const set = item => clipboard.set(({item}), 'filemanager:copy'); 416 | 417 | const cut = item => clipboard.set(({ 418 | item, 419 | callback: () => core.config('vfs.watch') ? undefined : vfs.refresh(true) 420 | }), 'filemanager:move'); 421 | 422 | const paste = () => { 423 | if (clipboard.has(/^filemanager:/)) { 424 | const move = clipboard.has('filemanager:move'); 425 | clipboard.get(move) 426 | .then(vfs.paste(move, state.currentPath)); 427 | } 428 | }; 429 | 430 | return {set, cut, paste}; 431 | }; 432 | 433 | /** 434 | * Dialog Factory 435 | */ 436 | const dialogFactory = (core, proc, win) => { 437 | const vfs = core.make('osjs/vfs'); 438 | const {pathJoin} = core.make('osjs/fs'); 439 | const {translatable} = core.make('osjs/locale'); 440 | const __ = translatable(translations); 441 | 442 | const dialog = (name, args, cb, modal = true) => core.make('osjs/dialog', name, args, { 443 | parent: win, 444 | attributes: {modal} 445 | }, cb); 446 | 447 | const mkdirDialog = (action, currentPath) => dialog('prompt', { 448 | message: __('DIALOG_MKDIR_MESSAGE'), 449 | value: __('DIALOG_MKDIR_PLACEHOLDER') 450 | }, usingPositiveButton(value => { 451 | const newPath = pathJoin(currentPath.path, value); 452 | action( 453 | () => vfs.mkdir({path: newPath}, {pid: proc.pid}), 454 | value, 455 | __('MSG_MKDIR_ERROR') 456 | ); 457 | })); 458 | 459 | const renameDialog = (action, files) => files.forEach(file => 460 | dialog('prompt', { 461 | message: __('DIALOG_RENAME_MESSAGE', file.filename), 462 | value: file.filename 463 | }, usingPositiveButton(value => { 464 | const idx = file.path.lastIndexOf(file.filename); 465 | const newPath = file.path.substr(0, idx) + value; 466 | 467 | action(() => vfs.rename(file, {path: newPath}), value, __('MSG_RENAME_ERROR')); 468 | }))); 469 | 470 | const deleteDialog = (action, files) => dialog('confirm', { 471 | message: __('DIALOG_DELETE_MESSAGE', files.length), 472 | }, usingPositiveButton(() => { 473 | action( 474 | () => Promise.all( 475 | files.map(file => vfs.unlink(file, {pid: proc.pid})) 476 | ), 477 | true, 478 | __('MSG_DELETE_ERROR') 479 | ); 480 | })); 481 | 482 | const progressDialog = (file) => dialog('progress', { 483 | message: __('DIALOG_PROGRESS_MESSAGE', file.name), 484 | buttons: [] 485 | }, () => {}, false); 486 | 487 | const errorDialog = (error, message) => dialog('alert', { 488 | type: 'error', 489 | error, 490 | message 491 | }, () => {}); 492 | 493 | const dialogs = { 494 | mkdir: mkdirDialog, 495 | rename: renameDialog, 496 | delete: deleteDialog, 497 | progress: progressDialog, 498 | error: errorDialog 499 | }; 500 | 501 | return (name, ...args) => { 502 | if (dialogs[name]) { 503 | return dialogs[name](...args); 504 | } else { 505 | throw new Error(`Invalid dialog: ${name}`); 506 | } 507 | }; 508 | }; 509 | 510 | /** 511 | * Creates Menus 512 | */ 513 | const menuFactory = (core, proc, win) => { 514 | const fs = core.make('osjs/fs'); 515 | const clipboard = core.make('osjs/clipboard'); 516 | const contextmenu = core.make('osjs/contextmenu'); 517 | const {translate: _, translatable} = core.make('osjs/locale'); 518 | 519 | const __ = translatable(translations); 520 | const getMountpoints = () => fs.mountpoints(true); 521 | 522 | const menuItemsFromMiddleware = async (type, middlewareArgs) => { 523 | if (!core.has('osjs/middleware')) { 524 | return []; 525 | } 526 | 527 | const items = core.make('osjs/middleware') 528 | .get(`osjs/filemanager:menu:${type}`); 529 | 530 | const promises = items.map(fn => fn(middlewareArgs)); 531 | 532 | const resolved = await Promise.all(promises); 533 | const result = resolved 534 | .filter(items => items instanceof Array); 535 | 536 | return [].concat(...result); 537 | }; 538 | 539 | const createFileMenu = () => ([ 540 | {label: _('LBL_UPLOAD'), onclick: () => win.emit('filemanager:menu:upload')}, 541 | {label: _('LBL_MKDIR'), onclick: () => win.emit('filemanager:menu:mkdir')}, 542 | {label: _('LBL_QUIT'), onclick: () => win.emit('filemanager:menu:quit')} 543 | ]); 544 | 545 | const createEditMenu = async (items, isContextMenu) => { 546 | const emitter = name => win.emit(name, items); 547 | const item = items[items.length - 1]; 548 | 549 | if (items.length === 1 && item && isSpecialFile(item.filename)) { 550 | return [{ 551 | label: _('LBL_GO'), 552 | onclick: () => emitter('filemanager:navigate'), 553 | }]; 554 | } 555 | 556 | const canDownload = items.some( 557 | item => !item.isDirectory && !isSpecialFile(item.filename) 558 | ); 559 | const hasValidFile = items.some(item => !isSpecialFile(item.filename)); 560 | const isDirectory = items.length === 1 && item.isDirectory; 561 | 562 | const openMenu = isDirectory ? [{ 563 | label: _('LBL_GO'), 564 | disabled: !items.length, 565 | onclick: () => emitter('filemanager:navigate') 566 | }] : [{ 567 | label: _('LBL_OPEN'), 568 | disabled: !items.length, 569 | onclick: () => emitter('filemanager:open') 570 | }, { 571 | label: __('LBL_OPEN_WITH'), 572 | disabled: !items.length, 573 | onclick: () => emitter('filemanager:openWith') 574 | }]; 575 | 576 | const clipboardMenu = [{ 577 | label: _('LBL_COPY'), 578 | disabled: !hasValidFile, 579 | onclick: () => emitter('filemanager:menu:copy') 580 | }, { 581 | label: _('LBL_CUT'), 582 | disabled: !hasValidFile, 583 | onclick: () => emitter('filemanager:menu:cut') 584 | }]; 585 | 586 | if (!isContextMenu) { 587 | clipboardMenu.push({ 588 | label: _('LBL_PASTE'), 589 | disabled: !clipboard.has(/^filemanager:/), 590 | onclick: () => emitter('filemanager:menu:paste') 591 | }); 592 | } 593 | 594 | const appendItems = await menuItemsFromMiddleware('edit', {file: item, isContextMenu}); 595 | const configuredItems = []; 596 | 597 | if (core.config('filemanager.disableDownload', false) !== true) { 598 | configuredItems.push({ 599 | label: _('LBL_DOWNLOAD'), 600 | disabled: !canDownload, 601 | onclick: () => emitter('filemanager:menu:download') 602 | }); 603 | } 604 | 605 | return [ 606 | ...openMenu, 607 | { 608 | label: _('LBL_RENAME'), 609 | disabled: !hasValidFile, 610 | onclick: () => emitter('filemanager:menu:rename') 611 | }, 612 | { 613 | label: _('LBL_DELETE'), 614 | disabled: !hasValidFile, 615 | onclick: () => emitter('filemanager:menu:delete') 616 | }, 617 | ...clipboardMenu, 618 | ...configuredItems, 619 | ...appendItems 620 | ]; 621 | }; 622 | 623 | const createViewMenu = (state) => ([ 624 | {label: _('LBL_REFRESH'), onclick: () => win.emit('filemanager:menu:refresh')}, 625 | {label: __('LBL_MINIMALISTIC'), checked: state.minimalistic, onclick: () => win.emit('filemanager:menu:toggleMinimalistic')}, 626 | {label: __('LBL_SHOW_DATE'), checked: proc.settings.showDate, onclick: () => win.emit('filemanager:menu:showDate')}, 627 | {label: __('LBL_SHOW_HIDDEN_FILES'), checked: proc.settings.showHiddenFiles, onclick: () => win.emit('filemanager:menu:showHidden')} 628 | ]); 629 | 630 | const createGoMenu = () => getMountpoints().map(m => ({ 631 | label: m.label, 632 | icon: m.icon, 633 | onclick: () => win.emit('filemanager:navigate', {path: m.root}) 634 | })); 635 | 636 | const menuItems = { 637 | file: createFileMenu, 638 | edit: createEditMenu, 639 | view: createViewMenu, 640 | go: createGoMenu 641 | }; 642 | 643 | return async ({name, ev}, args, isContextMenu = false) => { 644 | if (menuItems[name]) { 645 | contextmenu.show({ 646 | menu: await menuItems[name](args, isContextMenu), 647 | position: isContextMenu ? ev : ev.target 648 | }); 649 | } else { 650 | throw new Error(`Invalid menu: ${name}`); 651 | } 652 | }; 653 | }; 654 | 655 | /** 656 | * Creates a new FileManager user interface view 657 | */ 658 | const createView = (core, proc, win) => { 659 | const {icon} = core.make('osjs/theme'); 660 | const {translate: _} = core.make('osjs/locale'); 661 | 662 | const onMenuClick = (name, args) => ev => win.emit('filemanager:menu', {ev, name}, args); 663 | const onInputEnter = (ev, value) => win.emit('filemanager:navigate', {path: value}); 664 | 665 | const canGoBack = ({list, index}) => !list.length || index <= 0; 666 | const canGoForward = ({list, index}) => !list.length || (index === list.length - 1); 667 | 668 | return (state, actions) => { 669 | const FileView = listView.component(state.fileview, actions.fileview); 670 | const MountView = listView.component(state.mountview, actions.mountview); 671 | 672 | return h(Box, { 673 | class: state.minimalistic ? 'osjs-filemanager-minimalistic' : '' 674 | }, [ 675 | h(Menubar, {}, [ 676 | h(MenubarItem, {onclick: onMenuClick('file')}, _('LBL_FILE')), 677 | h(MenubarItem, {onclick: onMenuClick('edit')}, _('LBL_EDIT')), 678 | h(MenubarItem, {onclick: onMenuClick('view', state)}, _('LBL_VIEW')), 679 | h(MenubarItem, {onclick: onMenuClick('go')}, _('LBL_GO')) 680 | ]), 681 | h(Toolbar, {}, [ 682 | h(Button, { 683 | title: _('LBL_BACK'), 684 | icon: icon('go-previous'), 685 | disabled: canGoBack(state.history), 686 | onclick: () => actions.history.back() 687 | }), 688 | h(Button, { 689 | title: _('LBL_FORWARD'), 690 | icon: icon('go-next'), 691 | disabled: canGoForward(state.history), 692 | onclick: () => actions.history.forward() 693 | }), 694 | h(Button, { 695 | title: _('LBL_HOME'), 696 | icon: icon('go-home'), 697 | onclick: () => win.emit('filemanager:home') 698 | }), 699 | h(TextField, { 700 | value: state.path, 701 | box: {grow: 1, shrink: 1}, 702 | onenter: onInputEnter 703 | }) 704 | ]), 705 | h(Panes, {style: {flex: '1 1'}}, [ 706 | h(MountView), 707 | h(FileView) 708 | ]), 709 | h(Statusbar, {}, h('span', {}, state.status)) 710 | ]); 711 | }; 712 | }; 713 | 714 | /** 715 | * Creates a new FileManager user interface 716 | */ 717 | const createApplication = (core, proc) => { 718 | const createColumns = listViewColumnFactory(core, proc); 719 | const createRows = listViewRowFactory(core, proc); 720 | const createMounts = mountViewRowsFactory(core); 721 | const {draggable} = core.make('osjs/dnd'); 722 | 723 | const initialState = { 724 | path: '', 725 | status: '', 726 | minimalistic: false, 727 | 728 | history: { 729 | index: -1, 730 | list: [] 731 | }, 732 | 733 | mountview: listView.state({ 734 | class: 'osjs-gui-fill', 735 | columns: ['Name'], 736 | hideColumns: true, 737 | rows: createMounts() 738 | }), 739 | 740 | fileview: listView.state({ 741 | columns: [], 742 | multiselect: true, 743 | previousSelectedIndex: 0 744 | }) 745 | }; 746 | 747 | const createActions = (win) => ({ 748 | history: { 749 | clear: () => ({index: -1, list: []}), 750 | 751 | push: (path) => ({index, list}) => { 752 | const newList = index === -1 ? [] : list; 753 | const lastHistory = newList[newList.length - 1]; 754 | const newIndex = lastHistory === path 755 | ? newList.length - 1 756 | : newList.push(path) - 1; 757 | 758 | return {list: newList, index: newIndex}; 759 | }, 760 | 761 | back: () => ({index, list}) => { 762 | const newIndex = Math.max(0, index - 1); 763 | win.emit('filemanager:navigate', list[newIndex], true); 764 | return {index: newIndex}; 765 | }, 766 | 767 | forward: () => ({index, list}) => { 768 | const newIndex = Math.min(list.length - 1, index + 1); 769 | win.emit('filemanager:navigate', list[newIndex], true); 770 | return {index: newIndex}; 771 | } 772 | }, 773 | 774 | toggleMinimalistic: () => ({minimalistic}) => ({minimalistic: !minimalistic}), 775 | 776 | setPath: path => ({path}), 777 | setStatus: status => ({status}), 778 | setMinimalistic: minimalistic => ({minimalistic}), 779 | setList: ({list, path, selectFile}) => ({fileview, mountview}) => { 780 | let selectedIndex = []; 781 | 782 | if (selectFile) { 783 | const foundIndex = list.findIndex(file => file.filename === selectFile); 784 | if (foundIndex !== -1) { 785 | selectedIndex = [foundIndex]; 786 | } 787 | } 788 | 789 | return { 790 | path, 791 | status: formatStatusMessage(list), 792 | mountview: Object.assign({}, mountview, { 793 | rows: createMounts() 794 | }), 795 | fileview: Object.assign({}, fileview, { 796 | selectedIndex, 797 | columns: createColumns(), 798 | rows: createRows(list) 799 | }) 800 | }; 801 | }, 802 | 803 | mountview: listView.actions({ 804 | select: ({data}) => win.emit('filemanager:navigate', {path: data.root}) 805 | }), 806 | 807 | fileview: listView.actions({ 808 | select: ({data}) => win.emit('filemanager:select', data), 809 | activate: ({data}) => 810 | data.forEach(item => 811 | win.emit(`filemanager:${item.isFile ? 'open' : 'navigate'}`, item) 812 | ), 813 | contextmenu: args => win.emit('filemanager:contextmenu', args), 814 | created: ({el, data}) => { 815 | if (data.isFile) { 816 | draggable(el, {data}); 817 | } 818 | } 819 | }) 820 | }); 821 | 822 | return ($content, win) => { 823 | const actions = createActions(win); 824 | const view = createView(core, proc, win); 825 | return app(initialState, actions, view, $content); 826 | }; 827 | }; 828 | 829 | /** 830 | * Creates a new FileManager window 831 | */ 832 | const createWindow = (core, proc) => { 833 | let wired; 834 | const state = {currentFile: [], currentPath: undefined}; 835 | const {homePath, initialPath} = createInitialPaths(core, proc); 836 | 837 | const title = core.make('osjs/locale').translatableFlat(proc.metadata.title); 838 | const win = proc.createWindow(createWindowOptions(core, proc, title)); 839 | const render = createApplication(core, proc); 840 | const dialog = dialogFactory(core, proc, win); 841 | const createMenu = menuFactory(core, proc, win); 842 | const vfs = vfsActionFactory(core, proc, win, dialog, state); 843 | const clipboard = clipboardActionFactory(core, state, vfs); 844 | 845 | const setSetting = (key, value) => proc.emit('filemanager:setting', key, value); 846 | const onTitle = append => win.setTitle(`${title} - ${append}`); 847 | const onStatus = message => wired.setStatus(message); 848 | const onRender = () => vfs.readdir(initialPath); 849 | const onDestroy = () => proc.destroy(); 850 | const onDrop = (...args) => vfs.drop(...args); 851 | const onHome = () => vfs.readdir(homePath, 'clear'); 852 | const onNavigate = (...args) => vfs.readdir(...args); 853 | const onSelectItem = files => (state.currentFile = files); 854 | const onSelectStatus = files => win.emit('filemanager:status', formatStatusMessage(files)); 855 | const onContextMenu = ({ev, data}) => createMenu({ev, name: 'edit'}, data, true); 856 | const onReaddirRender = args => wired.setList(args); 857 | const onRefresh = (...args) => vfs.refresh(...args); 858 | const onOpen = files => { 859 | if (!Array.isArray(files)) { 860 | files = [files]; 861 | } 862 | 863 | return files.forEach( 864 | file => core.open(file, {useDefault: true}) 865 | ); 866 | }; 867 | const onOpenWith = files => { 868 | if (!Array.isArray(files)) { 869 | files = [files]; 870 | } 871 | 872 | return files.forEach( 873 | file => core.open(file, {useDefault: true, forceDialog: true}) 874 | ); 875 | }; 876 | const onHistoryPush = file => wired.history.push(file); 877 | const onHistoryClear = () => wired.history.clear(); 878 | const onMenu = (props, args) => createMenu(props, args || state.currentFile); 879 | const onMenuUpload = (...args) => vfs.upload(...args); 880 | const onMenuMkdir = () => dialog('mkdir', vfs.action, state.currentPath); 881 | const onMenuQuit = () => proc.destroy(); 882 | const onMenuRefresh = () => vfs.refresh(); 883 | const onMenuToggleMinimalistic = () => wired.toggleMinimalistic(); 884 | const onMenuShowDate = () => setSetting('showDate', !proc.settings.showDate); 885 | const onMenuShowHidden = () => setSetting('showHiddenFiles', !proc.settings.showHiddenFiles); 886 | const onMenuRename = files => dialog('rename', vfs.action, files); 887 | const onMenuDelete = files => dialog('delete', vfs.action, files); 888 | const onMenuDownload = (files) => vfs.download(files); 889 | const onMenuCopy = items => clipboard.set(items); 890 | const onMenuCut = items => clipboard.cut(items); 891 | const onMenuPaste = () => clipboard.paste(); 892 | 893 | return win 894 | .once('render', () => win.focus()) 895 | .once('destroy', () => (wired = undefined)) 896 | .once('render', onRender) 897 | .once('destroy', onDestroy) 898 | .on('drop', onDrop) 899 | .on('filemanager:title', onTitle) 900 | .on('filemanager:status', onStatus) 901 | .on('filemanager:menu', onMenu) 902 | .on('filemanager:home', onHome) 903 | .on('filemanager:navigate', onNavigate) 904 | .on('filemanager:select', onSelectItem) 905 | .on('filemanager:select', onSelectStatus) 906 | .on('filemanager:contextmenu', onContextMenu) 907 | .on('filemanager:readdir', onReaddirRender) 908 | .on('filemanager:refresh', onRefresh) 909 | .on('filemanager:open', onOpen) 910 | .on('filemanager:openWith', onOpenWith) 911 | .on('filemanager:historyPush', onHistoryPush) 912 | .on('filemanager:historyClear', onHistoryClear) 913 | .on('filemanager:menu:upload', onMenuUpload) 914 | .on('filemanager:menu:mkdir', onMenuMkdir) 915 | .on('filemanager:menu:quit', onMenuQuit) 916 | .on('filemanager:menu:refresh', onMenuRefresh) 917 | .on('filemanager:menu:toggleMinimalistic', onMenuToggleMinimalistic) 918 | .on('filemanager:menu:showDate', onMenuShowDate) 919 | .on('filemanager:menu:showHidden', onMenuShowHidden) 920 | .on('filemanager:menu:copy', onMenuCopy) 921 | .on('filemanager:menu:cut', onMenuCut) 922 | .on('filemanager:menu:paste', onMenuPaste) 923 | .on('filemanager:menu:rename', onMenuRename) 924 | .on('filemanager:menu:delete', onMenuDelete) 925 | .on('filemanager:menu:download', onMenuDownload) 926 | .render(($content, win) => (wired = render($content, win))); 927 | }; 928 | 929 | /** 930 | * Launches the OS.js application process 931 | */ 932 | const createProcess = (core, args, options, metadata) => { 933 | const proc = core.make('osjs/application', { 934 | args, 935 | metadata, 936 | options: Object.assign({}, options, { 937 | settings: createDefaultSettings() 938 | }) 939 | }); 940 | 941 | const emitter = proc.emitAll(); 942 | const win = createWindow(core, proc); 943 | 944 | const onSettingsUpdate = (settings) => { 945 | proc.settings = Object.assign({}, proc.settings, settings); 946 | win.emit('filemanager:refresh'); 947 | }; 948 | 949 | const onSetting = (key, value) => { 950 | onSettingsUpdate({[key]: value}); 951 | 952 | proc.saveSettings() 953 | .then(() => emitter('osjs:filemanager:remote', proc.settings)) 954 | .catch(error => console.warn(error)); 955 | }; 956 | 957 | proc.on('osjs:filemanager:remote', onSettingsUpdate); 958 | proc.on('filemanager:setting', onSetting); 959 | 960 | const listener = (args) => { 961 | if (args.pid === proc.pid) { 962 | return; 963 | } 964 | 965 | const currentPath = String(proc.args.path.path).replace(/\/$/, ''); 966 | const watchPath = String(args.path).replace(/\/$/, ''); 967 | if (currentPath === watchPath) { 968 | win.emit('filemanager:refresh'); 969 | } 970 | }; 971 | 972 | core.on('osjs/vfs:directoryChanged', listener); 973 | proc.on('destroy', () => core.off('osjs/vfs:directoryChanged', listener)); 974 | 975 | return proc; 976 | }; 977 | 978 | osjs.register(applicationName, createProcess); 979 | -------------------------------------------------------------------------------- /index.scss: -------------------------------------------------------------------------------- 1 | .osjs-filemanager-minimalistic { 2 | .osjs-gui-panes-inner { 3 | & > div:nth-child(1), 4 | & > div:nth-child(2) { 5 | display: none; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /locales.js: -------------------------------------------------------------------------------- 1 | export const en_EN = { 2 | LBL_SHOW_HIDDEN_FILES: 'Show hidden files', 3 | LBL_MINIMALISTIC: 'Minimalistic', 4 | LBL_OPEN_WITH: 'Open with...', 5 | LBL_SHOW_DATE: 'Show date column', 6 | LBL_DIR_AND_FILE_STATUS: '{0}, {1}, {2} bytes total', 7 | LBL_DIR_OR_FILE_STATUS: '{0}, {1} bytes total', 8 | SINGLE_DIR: 'directory', 9 | MULTI_DIR: 'directories', 10 | SINGLE_FILE: 'file', 11 | MULTI_FILE: 'files', 12 | LBL_DATE: 'Date', // FIXME: Move to client 13 | LBL_LOADING: 'Loading {0}', 14 | DIALOG_MKDIR_MESSAGE: 'Create new directory', 15 | DIALOG_MKDIR_PLACEHOLDER: 'New directory', 16 | DIALOG_RENAME_MESSAGE: 'Rename {0}?', 17 | DIALOG_DELETE_MESSAGE: 'Delete {0} file(s)?', 18 | DIALOG_PROGRESS_MESSAGE: 'Uploading {0}...', 19 | MSG_ERROR: 'An error occurred', 20 | MSG_UPLOAD_ERROR: 'Failed to upload file(s)', 21 | MSG_READDIR_ERROR: 'An error occurred while reading directory: {0}', 22 | MSG_PASTE_ERROR: 'Failed to paste file(s)', 23 | MSG_MKDIR_ERROR: 'Failed to create directory', 24 | MSG_RENAME_ERROR: 'Failed to rename', 25 | MSG_DELETE_ERROR: 'Failed to delete' 26 | }; 27 | 28 | export const sv_SE = { 29 | LBL_SHOW_HIDDEN_FILES: 'Visa dölda filer', 30 | LBL_MINIMALISTIC: 'Minimalistic', 31 | LBL_OPEN_WITH: 'Öppna med...', 32 | LBL_SHOW_DATE: 'Visa datumkolumn', 33 | LBL_DIR_AND_FILE_STATUS: '{0}, {1}, {2} byte totalt', 34 | LBL_DIR_OR_FILE_STATUS: '{0}, {1} byte totalt', 35 | SINGLE_DIR: 'katalog', 36 | MULTI_DIR: 'kataloger', 37 | SINGLE_FILE: 'fil', 38 | MULTI_FILE: 'filer', 39 | LBL_DATE: 'Datum', // FIXME: Move to client 40 | LBL_LOADING: 'Laddar {0}', 41 | DIALOG_MKDIR_MESSAGE: 'Skapa ny katalog', 42 | DIALOG_MKDIR_PLACEHOLDER: 'Ny katalog', 43 | DIALOG_RENAME_MESSAGE: 'Döp om {0}?', 44 | DIALOG_DELETE_MESSAGE: 'Radera {0} fil(er)?', 45 | MSG_ERROR: 'Ett fel uppstod', 46 | MSG_UPLOAD_ERROR: 'Det gick inte att ladda upp filer(na)', 47 | MSG_READDIR_ERROR: 'Ett fel uppstod när katalogen lästes: {0}', 48 | MSG_PASTE_ERROR: 'Det gick inte att klistra in fil(erna)', 49 | MSG_MKDIR_ERROR: 'Det gick inte att skapa katalogen', 50 | MSG_RENAME_ERROR: 'Det gick inte att byta namn', 51 | MSG_DELETE_ERROR: 'Det gick inte att ta bort' 52 | }; 53 | 54 | export const nb_NO = { 55 | LBL_SHOW_HIDDEN_FILES: 'Vis skjulte filer', 56 | LBL_MINIMALISTIC: 'Minimalistisk', 57 | LBL_OPEN_WITH: 'Åpne med...', 58 | LBL_SHOW_DATE: 'Vis dato kolonne', 59 | LBL_DIR_AND_FILE_STATUS: '{0}, {1}, {2} bytes totalt', 60 | LBL_DIR_OR_FILE_STATUS: '{0}, {1} bytes totalt', 61 | SINGLE_DIR: 'mappe', 62 | MULTI_DIR: 'mapper', 63 | SINGLE_FILE: 'fil', 64 | MULTI_FILE: 'filer', 65 | LBL_DATE: 'Dato', 66 | LBL_LOADING: 'Laster {0}', 67 | DIALOG_MKDIR_MESSAGE: 'Lag ny mappe', 68 | DIALOG_MKDIR_PLACEHOLDER: 'Ny mappe', 69 | DIALOG_RENAME_MESSAGE: 'Omdøpe {0}?', 70 | DIALOG_DELETE_MESSAGE: 'Slette {0} fil(er)?', 71 | MSG_ERROR: 'En feil oppstod', 72 | MSG_UPLOAD_ERROR: 'Feil under opplasting av fil(er)', 73 | MSG_READDIR_ERROR: 'En feil oppstod under lesing av mappe: {0}', 74 | MSG_PASTE_ERROR: 'Feil oppstod under liming av fil(er)', 75 | MSG_MKDIR_ERROR: 'Feil oppstod under opprettelse av mappe', 76 | MSG_RENAME_ERROR: 'Feil oppstod under omdøping', 77 | MSG_DELETE_ERROR: 'Feil oppstod under sletting' 78 | }; 79 | 80 | export const vi_VN = { 81 | LBL_SHOW_HIDDEN_FILES: 'Hiển thị tập tin ẩn', 82 | LBL_MINIMALISTIC: 'Tối giản', 83 | LBL_OPEN_WITH: 'Mở bằng...', 84 | LBL_SHOW_DATE: 'Hiện cột thời gian', 85 | LBL_DIR_AND_FILE_STATUS: '{0}, {1}, tổng dung lượng {2} bytes', 86 | LBL_DIR_OR_FILE_STATUS: '{0}, tổng dung lượng {1} bytes', 87 | SINGLE_DIR: 'thư mục', 88 | MULTI_DIR: 'thư mục', 89 | SINGLE_FILE: 'tập tin', 90 | MULTI_FILE: 'các tập tin', 91 | LBL_DATE: 'Thời gian', 92 | LBL_LOADING: 'Đang tải {0}', 93 | DIALOG_MKDIR_MESSAGE: 'Tạo thư mục mới', 94 | DIALOG_MKDIR_PLACEHOLDER: 'Thư mục mới', 95 | DIALOG_RENAME_MESSAGE: 'Đổi tên {0}?', 96 | DIALOG_DELETE_MESSAGE: 'Xoá {0} (các) tập tin?', 97 | MSG_ERROR: 'Đã xảy ra lỗi', 98 | MSG_UPLOAD_ERROR: 'Không thể tải lên tập tin', 99 | MSG_READDIR_ERROR: 'Đã xảy ra lỗi trong khi đọc thư mục: {0}', 100 | MSG_PASTE_ERROR: 'Không thể dán tập tin', 101 | MSG_MKDIR_ERROR: 'Không thể tạo thư mục', 102 | MSG_RENAME_ERROR: 'Không thể đổi tên', 103 | MSG_DELETE_ERROR: 'Không thể xóa' 104 | }; 105 | 106 | export const pt_BR = { 107 | LBL_SHOW_HIDDEN_FILES: 'Mostrar arquivos ocultos', 108 | LBL_MINIMALISTIC: 'Minimalista', 109 | LBL_OPEN_WITH: 'Abrir com...', 110 | LBL_SHOW_DATE: 'Mostrar coluna Data', 111 | LBL_DIR_AND_FILE_STATUS: '{0}, {1}, {2} bytes no total', 112 | LBL_DIR_OR_FILE_STATUS: '{0}, {1} bytes no total', 113 | SINGLE_DIR: 'diretório', 114 | MULTI_DIR: 'diretórios', 115 | SINGLE_FILE: 'arquivo', 116 | MULTI_FILE: 'arquivos', 117 | LBL_DATE: 'Data', // FIXME: Move to client 118 | LBL_LOADING: 'Carregando {0}', 119 | DIALOG_MKDIR_MESSAGE: 'Criar novo diretório', 120 | DIALOG_MKDIR_PLACEHOLDER: 'Novo diretório', 121 | DIALOG_RENAME_MESSAGE: 'Renomear {0}?', 122 | DIALOG_DELETE_MESSAGE: 'Deletar {0} arquivo(s)?', 123 | MSG_ERROR: 'Um erro ocorreu', 124 | MSG_UPLOAD_ERROR: 'Falha ao fazer upload do(s) arquivo(s)', 125 | MSG_READDIR_ERROR: 'Um erro ocorreu ao ler o diretório: {0}', 126 | MSG_PASTE_ERROR: 'Falha ao colar arquivo(s)', 127 | MSG_MKDIR_ERROR: 'Falha ao criar diretório', 128 | MSG_RENAME_ERROR: 'Falha ao renomear', 129 | MSG_DELETE_ERROR: 'Falha ao deletar' 130 | }; 131 | 132 | export const fr_FR = { 133 | LBL_SHOW_HIDDEN_FILES: 'Afficher les fichiers cachés', 134 | LBL_MINIMALISTIC: 'Minimaliste', 135 | LBL_OPEN_WITH: 'Ouvrir avec...', 136 | LBL_SHOW_DATE: 'Affichier la colonne date', 137 | LBL_DIR_AND_FILE_STATUS: '{0}, {1}, {2} bytes au total', 138 | LBL_DIR_OR_FILE_STATUS: '{0}, {1} bytes au total', 139 | SINGLE_DIR: 'dossier', 140 | MULTI_DIR: 'dossiers', 141 | SINGLE_FILE: 'dossier', 142 | MULTI_FILE: 'des dossiers', 143 | LBL_DATE: 'Date', // FIXME: Move to client 144 | LBL_LOADING: 'Chargement en cours {0}', 145 | DIALOG_MKDIR_MESSAGE: 'Créer nouveau dossier', 146 | DIALOG_MKDIR_PLACEHOLDER: 'Nouveau dossier', 147 | DIALOG_RENAME_MESSAGE: 'Renommer {0}?', 148 | DIALOG_DELETE_MESSAGE: 'Supprimer {0} (des) dossiers?', 149 | MSG_ERROR: 'Une erreur est survenue', 150 | MSG_UPLOAD_ERROR: 'Echec du chargement du(des) fichier(s)', 151 | MSG_READDIR_ERROR: 'Une erreur est survenue lors de la lecture du répertoire : {0}', 152 | MSG_PASTE_ERROR: 'Impossible de coller le(s) file(s)', 153 | MSG_MKDIR_ERROR: 'Impossible de créer le répertoire', 154 | MSG_RENAME_ERROR: 'Echec du renommage', 155 | MSG_DELETE_ERROR: 'Echec de la suppression' 156 | }; 157 | 158 | export const tr_TR = { 159 | LBL_SHOW_HIDDEN_FILES: 'Gizli dosyaları göster', 160 | LBL_MINIMALISTIC: 'Minimalist', 161 | LBL_OPEN_WITH: 'Şununla aç:', 162 | LBL_SHOW_DATE: 'Tarih sütununu göster', 163 | LBL_DIR_AND_FILE_STATUS: 'toplamda {0}, {1}, {2} byte var', 164 | LBL_DIR_OR_FILE_STATUS: 'toplamda {0}, {1} byte var', 165 | SINGLE_DIR: 'dizin', 166 | MULTI_DIR: 'dizinler', 167 | SINGLE_FILE: 'dosya', 168 | MULTI_FILE: 'dosyalar', 169 | LBL_DATE: 'Tarih', // FIXME: Move to client 170 | LBL_LOADING: 'Yükleniyor {0}', 171 | DIALOG_MKDIR_MESSAGE: 'Yeni dizin oluştur', 172 | DIALOG_MKDIR_PLACEHOLDER: 'Yeni dizin', 173 | DIALOG_RENAME_MESSAGE: '{0} ismi değişsin mi?', 174 | DIALOG_DELETE_MESSAGE: '{0} dosya(lar) silinsin mi?', 175 | MSG_ERROR: 'Bir hata oldu', 176 | MSG_UPLOAD_ERROR: 'Dosya(lar)ın yüklenmesi işlemi başarısız', 177 | MSG_READDIR_ERROR: 'Belirtilen dizin okunurken bir hata oluştu: {0}', 178 | MSG_PASTE_ERROR: 'Dosya(lar)ın yapıştırılması işlemi başarısız', 179 | MSG_MKDIR_ERROR: 'Dizin oluşturma işlemi başarısız', 180 | MSG_RENAME_ERROR: 'Yeniden adlandırma işlemi başarısız', 181 | MSG_DELETE_ERROR: 'Silme işlemi başarısız' 182 | }; 183 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FileManager", 3 | "category": "utilities", 4 | "icon": "icon.png", 5 | "title": { 6 | "en_EN": "File Manager", 7 | "nb_NO": "Filbehandler", 8 | "fr_FR": "Gestionnaire de fichiers", 9 | "de_DE": "Dateimanager", 10 | "vi_VN": "Quản lý tập tin", 11 | "pt_BR": "Gerenciador de arquivos" 12 | }, 13 | "description": { 14 | "en_EN": "File Manager", 15 | "nb_NO": "Behandle filer", 16 | "fr_FR": "Gestionnaire de fichiers", 17 | "de_DE": "Dateimanager", 18 | "vi_VN": "Ứng dụng quản lý tập tin", 19 | "pt_BR": "Gerenciador de arquivos" 20 | }, 21 | "files": [ 22 | "main.js", 23 | "main.css" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@osjs/filemanager-application", 3 | "version": "1.6.3", 4 | "description": "OS.js v3 Filemanager Application", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "eslint": "eslint *.js", 8 | "build": "webpack", 9 | "watch": "webpack --watch", 10 | "prepublishOnly": "npm run eslint && rm ./dist/* && NODE_ENV=production npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/os-js/osjs-filemanager-application.git" 15 | }, 16 | "files": [ 17 | "dist/", 18 | "metadata.json" 19 | ], 20 | "keywords": [ 21 | "osjs" 22 | ], 23 | "author": "Anders Evenrud ", 24 | "license": "BSD-2-Clause", 25 | "bugs": { 26 | "url": "https://github.com/os-js/osjs-filemanager-application/issues" 27 | }, 28 | "homepage": "https://github.com/os-js/osjs-filemanager-application#readme", 29 | "osjs": { 30 | "type": "package" 31 | }, 32 | "dependencies": { 33 | "@osjs/gui": "^4.0.30", 34 | "hyperapp": "^1.2.10" 35 | }, 36 | "devDependencies": { 37 | "@osjs/dev-meta": "^2.1.0" 38 | }, 39 | "eslintConfig": { 40 | "env": { 41 | "browser": true, 42 | "node": true 43 | }, 44 | "parserOptions": { 45 | "sourceType": "module" 46 | }, 47 | "extends": "@osjs/eslint-config" 48 | }, 49 | "babel": { 50 | "presets": [ 51 | [ 52 | "@babel/preset-env", 53 | {} 54 | ] 55 | ], 56 | "plugins": [ 57 | "@babel/plugin-transform-runtime" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/osjs-filemanager-application/ccbfc96000ec8b0bcfd5d73654eaf1199bc926ba/screenshot.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const mode = process.env.NODE_ENV || 'development'; 3 | const minimize = mode === 'production'; 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 7 | 8 | const plugins = []; 9 | 10 | if (mode === 'production') { 11 | plugins.push(new OptimizeCSSAssetsPlugin({ 12 | cssProcessorOptions: { 13 | discardComments: true, 14 | map: { 15 | inline: false 16 | } 17 | }, 18 | })); 19 | } 20 | 21 | module.exports = { 22 | mode, 23 | devtool: 'source-map', 24 | entry: [ 25 | path.resolve(__dirname, 'index.js'), 26 | ], 27 | externals: { 28 | osjs: 'OSjs' 29 | }, 30 | optimization: { 31 | minimize, 32 | }, 33 | plugins: [ 34 | new CopyWebpackPlugin({ 35 | patterns: [ 36 | 'icon.png' 37 | ] 38 | }), 39 | new MiniCssExtractPlugin({ 40 | filename: '[name].css', 41 | chunkFilename: '[id].css' 42 | }), 43 | ...plugins 44 | ], 45 | module: { 46 | rules: [ 47 | { 48 | test: /\.(sa|sc|c)ss$/, 49 | exclude: /(node_modules|bower_components)/, 50 | use: [ 51 | MiniCssExtractPlugin.loader, 52 | { 53 | loader: 'css-loader', 54 | options: { 55 | sourceMap: true 56 | } 57 | }, 58 | { 59 | loader: 'sass-loader', 60 | options: { 61 | sourceMap: true 62 | } 63 | } 64 | ] 65 | }, 66 | { 67 | test: /\.js$/, 68 | exclude: /node_modules\/(?!@osjs)/, 69 | use: { 70 | loader: 'babel-loader' 71 | } 72 | } 73 | ] 74 | } 75 | }; 76 | --------------------------------------------------------------------------------