VaKeR CYBER ARMY
Logo of a company Server : Apache/2.4.41 (Ubuntu)
System : Linux absol.cf 5.4.0-198-generic #218-Ubuntu SMP Fri Sep 27 20:18:53 UTC 2024 x86_64
User : www-data ( 33)
PHP Version : 7.4.33
Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Directory :  /var/www/html/libs/absol-acomp/js/finder/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /var/www/html/libs/absol-acomp/js/finder/Finder.js
import '../../css/finder.css';
import '../../css/mobileapp.css';
import ACore, { _, $, $$ } from "../../ACore";
import ResizeSystem from "absol/src/HTML5/ResizeSystem";
import FlexiconButton from "../FlexiconButton";
import OOP from "absol/src/HTML5/OOP";
import { ExpGroup, ExpTree } from "../ExpTree";
import MessageInput from "../messageinput/MessageInput";
import ext2MineType from "absol/src/Converter/ext2MineType";
import { fileInfoOf, measureText, openFileDialog, openYesNoQuestionDialog, vScrollIntoView } from "../utils";
import { randomArbitrary } from "absol/src/Math/random";
import TaskManager from "absol/src/AppPattern/TaskManager";
import Modal from "../Modal";
import MessageDialog from "../MessageDialog";
import Hanger from "../Hanger";
import Vec2 from "absol/src/Math/Vec2";
import Rectangle from "absol/src/Math/Rectangle";
import WindowBox from "../WindowBox";
import { saveAs } from "absol/src/Network/FileSaver";
import DropZone from "../DropZone";
import BrowserDetector from "absol/src/Detector/BrowserDetector";
import EventEmitter, { isMouseRight } from "absol/src/HTML5/EventEmitter";
import ContextCaptor from "../ContextMenu";
import TextArea2 from "../TextArea2";
import FileThumbnail from "./FileThumbnail";
import noop from "absol/src/Code/noop";
import SearchTextInput from "../Searcher";
import Context from "absol/src/AppPattern/Context";
import { nonAccentVietnamese } from "absol/src/String/stringFormat";
import { randomIdent } from "absol/src/String/stringGenerate";
import DomSignal from "absol/src/HTML5/DomSignal";
import RibbonButton from "../RibbonButton";

var isMobile = BrowserDetector.isMobile;

/***
 * @extends AElement
 * @constructor
 */
function Finder() {
    if (isMobile) this.addClass('as-mobile');
    // if (BrowserDetector.isMobile) alert("Chưa hỗ trợ điện thoại!");

    this.$attachhook = _('attachhook').addTo(this);
    this.$attachhook.requestUpdateSize = () => {
        this.layoutCtn.update();
        this.navCtrl.notifyVisibleContentItems();
    };
    this.$attachhook.once('attached', () => {
        ResizeSystem.add(this.$attachhook);
        this.layoutCtn.update();
        this.navCtrl.onStart();
        this.navCtrl.notifyVisibleContentItems();
        ContextCaptor.auto();
    });

    this.domSignal = new DomSignal(_('attachhook').addTo(this));
    this.$header = $('.as-finder-header', this);
    this.$nomalActionCtn = $('.as-finder-normal-action-button-ctn', this);
    this.$tinyActionCtn = $('.as-finder-tiny-action-button-ctn', this);
    this.$contentHeader = $('.as-finder-content-header', this);

    this.$navCtn = $('.as-finder-nav-ctn', this);
    this.$nav = $(ExpGroup.tag, this.$navCtn);

    this.$searchCtn = $('.as-finder-search-ctn', this);


    this.$contentCtn = $('.as-finder-content-ctn', this);
    this.$content = $('.as-finder-content', this);
    this.$body = $('.as-finder-body', this);
    this.$commandButtons = $$('.as-finder-nav-header button', this)
        .concat($$('.as-finder-content-header button', this))
        .concat($$('.as-finder-search-footer button', this))
        .concat($$('.as-finder-search-header button', this))
        .reduce((ac, cr) => {
            var name = cr.attr('name');
            ac[name] = cr;
            if (cr.items) {//ribbon button
                cr.on('select', (event) => {
                    this.execCommand(name, event.item.value, event.item);
                });
            }
            else {
                cr.on('click', () => {
                    this.execCommand(name);
                });
            }
            return ac;
        }, {});

    this.$searchTypeSelect = $('.as-finder-search-type-select', this);
    this.$searchText = $('.as-finder-search-text', this);

    this.fileSystem = new AbsolFileSystem();
    this.layoutCtn = new LayoutController(this);
    this.navCtrl = new NavigatorController(this);
    this.selectCtrl = isMobile ? new MobileSelectController(this) : new SelectController(this);
    this.uploadCtrl = new UploadController(this);
    this.commandCtrl = new CommandController(this);
    this.folderDialog = new FolderDialog(this);
    this.searchCtrl = new SearchController(this);

    /***
     * @type {string}
     * @name displayPath
     * @memberOf Finder#
     */
    /***
     * @type {string}
     * @name path
     * @memberOf Finder#
     */
    /***
     * @type {string}
     * @name rootPath
     * @memberOf Finder#
     */

    /***
     * @type {string}
     * @name accept
     * @memberOf Finder#
     */
}

Finder.tag = 'Finder'.toLowerCase();

Finder.render = function () {
    return _({
        class: 'as-finder',
        extendEvent: ['selectedchange', 'dblclickfile'],
        attr: {
            'data-selected-file-count': '0',
            'data-selected-folder-count': '0'
        },
        child: [
            {
                class: 'as-finder-header',
                child: [
                    {
                        class: 'as-finder-normal-action-button-ctn',
                        /* child: Finder.prototype.actions.map(act => ({
                             tag: FlexiconButton.tag,
                             attr: { name: act.name },
                             props: {
                                 text: act.text,
                                 // icon: act.icon
                             }
                         }))*/
                    },
                    {
                        class: 'as-finder-tiny-action-button-ctn',
                        /*child: Finder.prototype.actions.map(act => ({
                            tag: 'button',
                            attr: { name: act.name, title: act.text },
                            child: act.icon
                        }))*/
                    }
                ]
            },
            {
                class: ['as-finder-nav-ctn'],
                child: [
                    {
                        class: 'as-finder-nav-header',
                        child: [
                            {
                                class: 'as-finder-nav-header-left',
                                child: [{
                                    tag: 'button',
                                    class: 'as-transparent-button',
                                    attr: { title: "Close Navigator", name: 'nav_toggle' },
                                    child: 'span.mdi.mdi-menu-open'
                                }]
                            },
                            {
                                class: 'as-finder-nav-header-right',
                                child: [
                                    {
                                        tag: 'button',
                                        class: 'as-transparent-button',
                                        attr: { title: "Search", name: 'switch_to_search' },
                                        child: 'span.mdi.mdi-magnify',
                                    },
                                    {
                                        tag: 'button',
                                        class: 'as-transparent-button',
                                        attr: { title: "Expand All", name: 'nav_expand_all' },

                                        child: 'span.mdi.mdi-arrow-expand-vertical',
                                    },
                                    {
                                        tag: 'button',
                                        class: 'as-transparent-button',
                                        attr: { title: "Collapse All", name: 'nav_collapse_all' },
                                        child: 'span.mdi.mdi-arrow-collapse-vertical'
                                    },
                                    {
                                        tag: 'button',
                                        class: 'as-transparent-button',
                                        attr: { title: "Reload", name: 'reload' },

                                        child: 'span.mdi.mdi-reload'
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        tag: ExpGroup.tag,
                        class: 'as-bscroller'
                    }
                ]
            },
            {
                class: 'as-finder-search-ctn',
                child: [
                    {
                        class: 'as-finder-search-header',
                        child: [{
                            tag: 'button',
                            class: 'as-transparent-button',
                            attr: { title: "Close Navigator", name: 'nav_toggle' },
                            child: 'span.mdi.mdi-menu-open'
                        }]
                    },
                    {
                        class: 'as-finder-search-body',
                        child: [
                            {
                                tag: SearchTextInput.tag,
                                class: 'as-finder-search-text',
                            },
                            {
                                class: 'as-finder-search-field',
                                child: [
                                    { child: { text: 'Kiểu' } },
                                    {
                                        child: {
                                            tag: 'selectmenu',
                                            class: 'as-finder-search-type-select',
                                            props: {
                                                items: [
                                                    { text: 'Tất cả', value: 'all', icon: 'span.mdi.mdi-asterisk' },
                                                    {
                                                        text: 'Hình ảnh',
                                                        value: 'image',
                                                        icon: 'span.mdi.mdi-image-outline'
                                                    },
                                                    {
                                                        text: 'Tài liệu',
                                                        value: 'document',
                                                        icon: 'span.mdi.mdi-file-document'
                                                    }
                                                ]
                                            }
                                        }
                                    }
                                ]
                            },
                            {
                                class: 'as-finder-search-footer',
                                child: [
                                    {
                                        tag: FlexiconButton.tag,
                                        attr: { name: 'start_search' },
                                        props: {
                                            text: 'OK'
                                        }
                                    },
                                    {
                                        tag: FlexiconButton.tag,
                                        attr: { name: 'cancel_search' },
                                        props: {
                                            text: 'Hủy'
                                        }
                                    }
                                ]
                            }
                        ]
                    }
                ]
            },
            {
                tag: DropZone.tag,
                attr: { 'data-view-as': 'list' },
                class: ['as-finder-body'],
                child: [
                    {
                        class: 'as-finder-content-header',
                        child: [
                            {
                                class: 'as-finder-content-header-left',
                                child: [
                                    {
                                        tag: 'button',
                                        attr: { title: 'Open Navigator', name: 'nav_toggle' },
                                        class: 'as-transparent-button',
                                        child: ['span.mdi.mdi-menu']
                                    },
                                    {
                                        tag: RibbonButton,
                                        attr: { title: 'View As', name: 'content_view_as' },
                                        class: 'as-transparent-button',
                                        props: {
                                            text: 'List',
                                            icon: 'span.mdi.mdi-format-list-bulleted-square',
                                            items: [
                                                {
                                                    icon: 'span.mdi.mdi-format-list-bulleted-square',
                                                    text: 'List',
                                                    value: 'list',
                                                }, {
                                                    text: 'Medium Icons',
                                                    icon: 'span.mdi.mdi-grid',
                                                    value: 'content'
                                                },
                                                {
                                                    text: 'Lage Icons',
                                                    icon: 'span.mdi.mdi-image-outline',
                                                    value: 'lage_icons'
                                                }
                                            ]
                                        },
                                        // child: ['span.mdi.mdi-format-list-bulleted-square']
                                    }
                                ]
                            },
                            {
                                class: 'as-finder-content-header-right',
                            }
                        ]
                    },
                    {
                        tag: Hanger.tag,
                        class: 'as-finder-content-ctn',
                        props: {
                            hangOn: 5
                        },
                        child: {
                            class: ['as-finder-content',]
                        }
                    },
                    {
                        class: 'as-finder-upload-overlay',
                        child: [
                            {
                                class: 'as-finder-upload-overlay-icon-ctn',
                                child: 'span.mdi.mdi-cloud-upload-outline'
                            },
                            {
                                child: { text: 'Thả file vào đây để tải lên' }
                            }
                        ]

                    }
                ]
            }

        ]
    });
};


Finder.property = {};

Finder.property.fileSystem = {
    set: function (fs) {
        this._fileSystem = fs;
    },
    get: function () {
        return this._fileSystem;
    }
};

Finder.property.path = {
    set: function (path) {
        this.navCtrl.path = path;
    },
    get: function () {
        return this.navCtrl.path;
    }
};

Finder.property.displayPath = {
    // set: function (path) {
    //     this.navCtrl.path = path;
    // },
    get: function () {
        return this.navCtrl.displayPath;
    }
};

Finder.property.rootPath = {
    set: function (path) {
        this.navCtrl.rootPath = path;
    },
    get: function () {
        return this.navCtrl.rootPath;
    }
};


Finder.property.selectedFiles = {
    get: function () {
        return this.selectCtrl.$selectedItems.filter(elt => elt.stat && !elt.stat.isDirectory).map(elt => elt.stat);
    }
}

Finder.prototype.execCommand = function (name) {
    return this.commandCtrl.execCommand.apply(this.commandCtrl, arguments);
};

Finder.prototype.addCommand = function (name, descriptor) {
    this.commandCtrl.addCommand(name, descriptor);
};


Finder.prototype.addButton = function (name, bf) {
    this.commandCtrl.addButton(name, bf);
};


/****
 *
 * @param {string} name
 * @param {string=} bf
 */
Finder.prototype.addFolderMenuItem = function (name, bf) {
    this.commandCtrl.addFolderMenuItem(name, bf);
};


Finder.prototype.findZIndex = function () {
    var c = this;
    var res = 0;
    var zIndex;
    while (c) {
        zIndex = parseInt(getComputedStyle(c).getPropertyValue('z-index'));
        if (!isNaN(zIndex)) res = Math.max(zIndex, res);
        c = c.parentElement;
    }
    return res;
};

ACore.install(Finder);

export default Finder;

export var FinderCommands = {};
Finder.prototype.commands = FinderCommands;


FinderCommands.upload = {
    text: 'Tải lên',
    icon: 'span.mdi.mdi-upload-outline',
    match: function (fileElt) {
        return !fileElt && this.searchCtrl.state !== 'RUNNING' && this.dirStat && this.dirStat.writable;
    },
    /***
     * @this Finder
     */
    exec: function () {
        openFileDialog({ multiple: true }).then(files => {
            if (files && files.length > 0) {
                this.uploadCtrl.upload(files);
            }
        })
    }
};

FinderCommands.upload_to_folder = {
    text: 'Tải lên',
    icon: 'span.mdi.mdi-upload-outline',
    match: function (treElt) {
        return treElt && treElt.stat && treElt.stat.writable;
    },
    /***
     * @this Finder
     */
    exec: function () {
        //todo: selected folder
        openFileDialog({ multiple: true }).then(files => {
            if (files && files.length > 0) {
                this.uploadCtrl.upload(files);
            }
        })
    }
};

FinderCommands.delete = {
    icon: 'span.mdi.mdi-delete-outline',
    text: 'Xóa',
    /***
     * @this Finder
     */
    match: function (fileElt) {
        return fileElt && this.selectCtrl.$selectedItems.length > 0 && this.selectCtrl.$selectedItems.every(elt => elt.stat && !elt.stat.isDirectory && elt.stat.writable);
    },
    /***
     * @this Finder
     */
    exec: function () {
        var paths = this.selectCtrl.$selectedItems.map(elt => elt.stat.path);
        var names = this.selectCtrl.$selectedItems.map(elt => elt.fileName);
        if (names.length === 0) return;
        var contentElt = _({
            style: { maxHeight: '50vh', overflow: 'auto' },
            child: {
                style: { display: 'table' },
                child: names.map(name => ({
                    style: { display: 'table-row' },
                    child: [
                        {
                            style: { display: 'table-cell', padding: '5px 20px 5px 10px' },
                            child: {
                                style: {
                                    'min-width': '30em',
                                },
                                child: { text: name }
                            }
                        },
                        {
                            style: { display: 'table-cell', padding: '5px 10px' },
                            child: {
                                class: 'as-finder-task-check',
                                style: {
                                    'min-width': '3em',
                                    textAlign: 'right',
                                },
                                child: 'span'
                            }
                        }
                    ]
                }))
            }
        });
        var modal = _({
            tag: Modal.tag,
            style: { zIndex: this.findZIndex() + 9000 },
            child: {
                tag: MessageDialog.tag,
                props: {
                    dialogTitle: 'Xóa file',
                    dialogActions: [
                        { name: "ok", text: "OK" },
                        { name: 'cancel', text: 'Hủy' }
                    ]
                },
                child: [{ child: { tag: 'span', child: { text: "Xác nhận xóa những file dưới đây: " } } }, contentElt],
                on: {
                    action: (event, sender) => {
                        var promises;
                        var errors = [];
                        if (event.action.name === 'ok') {
                            sender.$actionBtns[0].disabled = true;
                            sender.$actionBtns[0].text = "Đang tiến hành xóa..";
                            sender.$actionBtns[1].disabled = true;
                            promises = paths.map((path, i) => {
                                return this.fileSystem.unlink(path).then(() => {
                                    $('.as-finder-task-check', contentElt.firstChild.childNodes[i]).addChild(_('span.mdi.mdi-check'))
                                }).catch(err => {
                                    errors.push(err);
                                    $('.as-finder-task-check', contentElt.firstChild.childNodes[i])
                                        .addChild(_('span.mdi.mdi-alert-decagram-outline'))
                                        .addChild(_({
                                            tag: 'span',
                                            class: '.as-finder-task-error-message',
                                            child: { text: err.message }
                                        }))
                                });
                            });
                            Promise.all(promises).then(() => {
                                var commands = {};
                                if (errors.length > 0) {
                                    errors.forEach(err => {
                                        if (err.command) {
                                            commands[err.command] = true;
                                        }
                                    });
                                    if (commands.reload) this.execCommand('reload');
                                    sender.$actionBtns[1].disabled = false;
                                    sender.$actionBtns[0].text = "Hoàn thành";
                                }
                                else {
                                    this.navCtrl.reload(this.path, true).then(() => modal.remove());
                                }
                            });
                        }
                        else {
                            modal.remove();

                        }
                    }
                }
            }
        }).addTo(document.body);
    }
};


FinderCommands.view = {
    icon: 'span.mdi.mdi-eye-outline',
    text: 'Xem',
    match: function (fileElt) {
        return !!fileElt;
    },
    /***
     * @this Finder
     */
    exec: function () {
        var elt = this.selectCtrl.$selectedItems[0];
        if (!elt) return;
        if (elt.stat.isDirectory) {
            this.navCtrl.viewDir(elt.stat.path);
            return;
        }
        var url = elt.stat.url;
        if (!url) return;
        var type = elt.fileType;
        if (type === 'xlsx' || type === 'docx' || type === 'xls' || type === 'doc' || type === 'ppt' || type === 'pptx') {
            url = 'https://view.officeapps.live.com/op/embed.aspx?src=' + encodeURIComponent(url);
        }
        else {
            url = encodeURI(url);
        }

        var mineType = ext2MineType[type] || 'none';
        var content;
        if (mineType.startsWith('video')) {
            content = _({
                tag: 'video',
                style: {
                    maxWidth: 'calc(90vw - 20px)',
                    maxHeight: 'calc(90vh - 80px)',
                    width: '900px',
                    height: 'auto'
                },
                attr: {
                    autoplay: 'true',
                    controls: 'true'
                },
                props: {
                    src: url
                }
            });
        }
        else if (mineType.startsWith('audio')) {
            content = _({
                tag: 'audio',
                style: {
                    margin: '5px'
                },
                attr: {
                    autoplay: 'true',
                    controls: 'true'
                },
                props: {
                    src: url
                }

            });
        }
        else if (mineType.startsWith('image')) {
            content = _({
                tag: 'img',
                style: {
                    maxWidth: 'calc(90vw - 20px)',
                    maxHeight: 'calc(90vh - 80px)',
                    width: 'auto',
                    height: 'auto'
                },
                attr: {},
                props: {
                    src: url
                }
            });
        }
        else {
            content = _({
                tag: 'iframe',
                style: {
                    maxWidth: 'calc(90vw - 20px)',
                    maxHeight: 'calc(90vh - 80px)',
                    width: '900px',
                    height: '600px'
                },
                props: {
                    src: url,
                    onload: function () {
                        // console.log(this.contentWindow.document.body.offsetHeight, this.contentWindow.document.body.offsetWidth)
                    }
                }

            });
        }

        var modal = _({
            tag: Modal.tag,
            style: { zIndex: this.findZIndex() + 9000 },
            child: {
                tag: WindowBox.tag,
                child: content,
                props: {
                    windowTitle: elt.stat.displayName || elt.stat.name,
                    windowActions: [
                        { name: 'close', icon: 'span.mdi.mdi-close' }
                    ]
                },
                on: {
                    action: () => {
                        modal.remove();
                    }
                }
            }
        }).addTo(document.body);
    }
};


FinderCommands.download = {
    icon: 'span.mdi.mdi-download-outline',
    text: 'Tải về',
    match: function (elt) {
        return elt && this.selectCtrl.$selectedItems.length > 0
            && this.selectCtrl.$selectedItems.every(elt => elt.stat && !elt.stat.isDirectory);
    },
    /***
     * @this Finder
     */
    exec: function () {
        var taskMng = new TaskManager({ limit: 4 });
        this.selectCtrl.$selectedItems.forEach(elt => {
            if (elt.isDirectory) return;
            var url = elt.stat.url;
            if (!url) return;
            taskMng.requestTask(function (onFinish, bundle) {
                saveAs(bundle.url, bundle.name)
                setTimeout(onFinish, 100);
            }, { url: url, name: elt.fileName });
        });
    }
};


FinderCommands.rename = {
    icon: 'span.mdi.mdi-rename',
    text: 'Đổi tên',
    /***
     * @this Finder
     */
    match: function (elt) {
        return elt && this.selectCtrl.$selectedItems.length === 1 && elt.stat && !elt.stat.isDirectory && elt.stat.writable;//todo: rename folder
    },
    /***
     * @this Finder
     */
    exec: function () {
        var elt = this.selectCtrl.$selectedItems[0];
        if (!elt) return;
        var path = elt.stat.path;
        var value = elt.fileName;
        var input = _({
            tag: TextArea2.tag,
            style: {
                outline: 'none',
                position: 'absolute',
                zIndex: 100,
                left: 0,
                bottom: 0,
                width: '100%'
            },
            props: {
                value: elt.fileName
            },
            on: {
                blur: () => {
                    var newValue = input.value.replace(/<>:\\\/\|\?\*\^/g, '').trim();
                    input.remove();
                    if (!value) return;
                    if (value === newValue) return;
                    this.fileSystem.rename(path, newValue).then(newStat => {
                        elt.stat = newStat;
                        elt.value = newStat.url;
                        elt.fileName = newStat.displayName || newStat.name;
                    });
                }
            }
        });
        elt.addChild(input);
        input.on('keydown', function (event) {
            if (event.key.match(/<>:\\\/\|\?\*\^/)) {
                event.preventDefault();
                setTimeout(() => input.updateSize(), 30)
            }
            else if (event.key === 'Enter') {
                input.blur();
            }
            else if (event.key === 'Escape') {
                input.value = value;
                input.updateSize();
                input.blur();
            }
        }, true);
        input.updateSize();
        input.focus();
        var ext = value.match(/\.[a-zA-Z0-9]+$/);
        if (ext) {
            ext = ext[0];
        }
        else {
            ext = '';
        }
        input.setSelectionRange(0, value.length - ext.length);

    }
};

FinderCommands.copy = {
    icon: 'span.mdi.mdi-content-copy',
    text: 'Sao chép',
    exec: function () {

    }
};

FinderCommands.move = {
    text: 'Di chuyển',
    icon: 'span.mdi.mdi-file-move-outline',
    match: function (fileElt) {
        if (arguments.length === 0) {
            return this.selectCtrl.$selectedItems.every(elt => elt.stat.writable);
        }

        return fileElt && fileElt.stat && fileElt.stat.writable;
    },
    /***
     * @this Finder
     */
    exec: function () {
        var itemElements = this.selectCtrl.$selectedItems.slice();
        var paths = itemElements.map(elt => elt.stat.path);
        var names = itemElements.map(elt => elt.fileName);
        if (names.length === 0) return;
        var currentFolderPath = this.path;
        this.folderDialog.open(currentFolderPath, false, (newFolderPath, stat) => newFolderPath !== currentFolderPath && stat.writable, 'Di chuyển file').then(newFolderPath => {
            if (!newFolderPath) return;
            if (newFolderPath === currentFolderPath) return;
            var contentElt = _({
                style: { maxHeight: '50vh', overflow: 'auto' },
                child: {
                    style: { display: 'table' },
                    child: names.map(name => ({
                        style: { display: 'table-row' },
                        child: [
                            {
                                style: { display: 'table-cell', padding: '5px 20px 5px 10px' },
                                child: {
                                    style: {
                                        'min-width': '30em',
                                    },
                                    child: { text: name }
                                }
                            },
                            {
                                style: { display: 'table-cell', padding: '5px 10px' },
                                child: {
                                    class: 'as-finder-task-check',
                                    style: {
                                        'min-width': '3em',
                                        textAlign: 'right',
                                    },
                                    child: 'span'
                                }
                            }
                        ]
                    }))
                }
            });
            var modal = _({
                tag: Modal.tag,
                style: { zIndex: this.findZIndex() + 9000 },
                child: {
                    tag: MessageDialog.tag,
                    props: {
                        dialogTitle: 'Di chuyển file',
                        dialogActions: [{
                            name: 'close',
                            text: 'Đóng'
                        }]
                    },
                    child: [{
                        child: {
                            tag: 'span',
                            child: { text: "Danh sách đang di chuyển: " }
                        }
                    }, contentElt],
                    on: {
                        action: (event, sender) => {
                            modal.remove();
                        }
                    }
                }
            }).addTo(document.body);
            var errors = [];
            var promises = paths.map((path, i) => {
                var newPath = newFolderPath + '/' + path.split('/').pop();
                return this.fileSystem.move(path, newPath).then(() => {
                    $('.as-finder-task-check', contentElt.firstChild.childNodes[i]).addChild(_('span.mdi.mdi-check'))
                }).catch(err => {
                    errors.push(err);
                    $('.as-finder-task-check', contentElt.firstChild.childNodes[i])
                        .addChild(_('span.mdi.mdi-alert-decagram-outline')).addChild(_({
                        tag: 'span',
                        class: '.as-finder-task-error-message',
                        child: { text: err.message }
                    }));
                });
            });
            Promise.all(promises).then(() => {
                var commands = {};
                if (errors.length > 0) {
                    errors.forEach(err => {
                        if (err.command) {
                            commands[err.command] = true;
                        }
                    });
                    if (commands.reload) this.execCommand('reload');
                }
                else {
                    this.navCtrl.reload(this.path, true).then(() => modal.remove());
                }
            });

        });
    }
};


FinderCommands.move_dir = {
    text: 'Di chuyển',
    icon: 'span.mdi.mdi-folder-arrow-right-outline',
    match: function (expElt) {
        return expElt && expElt.stat && expElt.stat.writable && !expElt.stat.isVirtual;
    },
    /***
     * @this Finder
     */
    exec: function (expElt) {
        var path = expElt.stat.path;
        var currentFolderPath = path.split('/');
        var name = currentFolderPath.pop();
        currentFolderPath = currentFolderPath.join('/');
        this.folderDialog.open(currentFolderPath, true, newPath => !newPath.startsWith(path), 'Di chuyển thư mục').then(newFolderPath => {
            if (!newFolderPath) return;
            return this.fileSystem.move(path, newFolderPath + '/' + name).then(() => {
                this.path = newFolderPath + '/' + name;
                this.navCtrl.reload(this.rootPath).then(() => {
                    this.navCtrl.viewDir(this.path)
                });
            });
        })
    }
};


FinderCommands.rmdir = {
    text: 'Xóa',
    icon: 'span.mdi.mdi-delete-outline',
    /***
     * @this Finder
     * @param elt
     */
    match: function (elt) {
        if (elt.stat && elt.stat.isVirtual) return false;
        if (elt) return elt.stat && elt.stat.writable;
        return false;
    },
    exec: function (elt) {

    }
};


FinderCommands.select_all = {
    text: 'Chọn tất cả',
    icon: 'span.mdi.mdi-select-all',
    match: function (fileElt) {
        return !fileElt;
    },
    /***
     * @this Finder
     */
    exec: function () {
        this.selectCtrl.selectAll();
    }
};

FinderCommands.nav_expand_all = {
    /***
     * @this Finder
     */
    exec: function () {
        this.navCtrl.expandAll();
    }
};

FinderCommands.nav_collapse_all = {
    /***
     * @this Finder
     */
    exec: function () {
        this.navCtrl.collapseAll();
    }
};


FinderCommands.reload = {
    /***
     * @this Finder
     */
    exec: function () {
        this.fileSystem.clearCache();
        this.navCtrl.reload().then(() => {
            this.navCtrl.viewDir(this.path);
        });
    }
};

FinderCommands.content_view_as = {
    /***
     * @this Finder
     */
    exec: function (value, item) {
        this.$commandButtons['content_view_as'].text = item.text;
        this.$commandButtons['content_view_as'].icon = item.icon;
        this.$body.attr('data-view-as', value);

    }
};

FinderCommands.switch_to_search = {
    /***
     * @this Finder
     */
    exec: function () {
        this.searchCtrl.start();
    }
};

FinderCommands.cancel_search = {
    /***
     * @this Finder
     */
    exec: function () {
        this.searchCtrl.stop();
        this.navCtrl.viewDir(this.path);
    }
};


FinderCommands.start_search = {
    /***
     * @this Finder
     */
    exec: function () {
        this.searchCtrl.search();
    }
};

FinderCommands.nav_toggle = {
    icon: 'span.mdi.mdi-menu',
    exec: function () {
        if (this.hasClass('as-nav-open')) {
            this.removeClass('as-nav-open');
        }
        else {
            this.addClass('as-nav-open');
        }
    }
};


/***
 *
 * @param {Finder} elt
 * @constructor
 */
function LayoutController(elt) {
    this.elt = elt;
    this.actionButtonWidth = 0;
    this.elt.domSignal.on('requestUpdateActionButtonSize', this.updateActionButtonSize.bind(this));
    this.elt.on('click', this.ev_click.bind(this))

}

LayoutController.prototype.requestUpdateActionButtonSize = function () {
    this.elt.domSignal.emit('requestUpdateActionButtonSize');
};

LayoutController.prototype.updateActionButtonSize = function () {
    var font = this.elt.$nomalActionCtn.getComputedStyleValue('font');
    var fontSize = this.elt.$nomalActionCtn.getFontSize();
    this.actionButtonWidth = Array.prototype.reduce.call(this.elt.$nomalActionCtn.childNodes, (ac, cr) => {
        return ac + Math.max(110, 0.715 * fontSize * 2 + measureText(cr.text, font).width) + 10;
    }, 60);
    this.update();
};

LayoutController.prototype.update = function () {
    var bound = this.elt.getBoundingClientRect();
    if (bound.width < 500) {
        if (!this.elt.hasClass('as-mini-layout'))
            this.elt.addClass('as-mini-layout');
    }
    else {
        if (this.elt.hasClass('as-mini-layout')) {
            this.elt.removeClass('as-mini-layout');
            this.elt.removeClass('as-nav-open');
        }
    }
    if (this.elt.hasClass('as-action-button-minimized')) {
        if (this.actionButtonWidth <= bound.width) {
            this.elt.removeClass('as-action-button-minimized');
        }
    }
    else {
        if (this.actionButtonWidth > bound.width) {
            this.elt.addClass('as-action-button-minimized');
        }
    }
    var bodyBound = this.elt.$body.getBoundingClientRect();
    var col = Math.floor(bodyBound.width / 300) || 1;
    this.elt.$body.addStyle('--col', col + '');
};

LayoutController.prototype.ev_click = function (event) {
    if (event.target === this.elt) {
        this.elt.removeClass('as-nav-open');
    }
};

/***
 *
 * @param {Finder} elt
 * @constructor
 */

function CommandController(elt) {
    Object.keys(this.constructor.prototype).forEach(key => {
        if (key.startsWith('ev_')) {
            this[key] = this[key].bind(this);
        }
    });

    this.elt = elt;
    this.$normalActionCtn = this.elt.$nomalActionCtn;
    this.$tinyActionCtn = this.elt.$tinyActionCtn;
    this.commands = Object.assign({}, this.elt.commands);
    this.buttonNames = ['upload', 'view', 'download', 'move', 'rename', 'delete'];
    this.folderMenuItemNames = ['upload_to_folder', 'move_dir'];
    this.contentMenuItemNames = ['view', 'download', 'upload', 'select_all', 'move', 'delete', 'rename'];

    this.$navCtn = this.elt.$navCtn;
    this.$navCtn.defineEvent('contextmenu').on('contextmenu', this.ev_navContextMenu);
    this.$contentCtn = this.elt.$contentCtn;
    this.$contentCtn.defineEvent('contextmenu').on('contextmenu', this.ev_contentContextMenu);

    this.updateButtons();
}

CommandController.prototype.updateButtons = function () {
    this.$normalActionCtn.clearChild();
    var buttons = this.buttonNames.map(name => {
        var desc = this.commands[name] || {};
        return _({
            tag: FlexiconButton.tag,
            attr: { name: name },
            props: {
                text: desc.text || name
            },
            on: {
                click: () => {
                    this.execCommand(name);
                }
            }
        });
    });
    this.$normalActionCtn.addChild(buttons);

    buttons = this.buttonNames.map(name => {
        var desc = this.commands[name] || {};
        return _({
            tag: 'button',
            class: 'as-transparent-button',
            attr: { name: name },
            child: desc.icon,
            on: {
                click: () => {
                    this.execCommand(name);
                }
            }
        });
    });
    this.$tinyActionCtn.addChild(buttons);


    this.elt.layoutCtn.requestUpdateActionButtonSize();
};


CommandController.prototype.execCommand = function (name) {
    var args = Array.prototype.slice.call(arguments, 1);
    var desc = this.commands[name];
    if (desc && typeof desc.exec === 'function') {
        return desc.exec.apply(this.elt, args);
    }
    return null;
};


CommandController.prototype.addCommand = function (name, desc) {
    this.commands[name] = Object.assign({}, this.commands[name], desc);
};

/****
 *
 * @param {string} name
 * @param {string=} bf
 */
CommandController.prototype.addButton = function (name, bf) {
    var idx = this.buttonNames.indexOf(bf);
    var bfElt, smallBfElt;
    if (idx >= 0) {
        this.buttonNames.splice(idx, 0, name);
        bfElt = $(`button[name="${name}"]`, this.$normalActionCtn);
        smallBfElt = $(`button[name="${name}"]`, this.$tinyActionCtn);
    }
    else {
        this.buttonNames.push(name);
    }
    var desc = this.commands[name] || {};
    this.$normalActionCtn.addChildBefore(_({
        tag: FlexiconButton.tag,
        attr: { name: name },
        props: {
            text: desc.text || name
        },
        on: {
            click: () => {
                this.execCommand(name);
            }
        }
    }), bfElt);

    this.$tinyActionCtn.addChild(_({
        tag: 'button',
        class: 'as-transparent-button',
        attr: { name: name },
        child: desc.icon,
        on: {
            click: () => {
                this.execCommand(name);
            }
        }
    }), smallBfElt);


    this.elt.layoutCtn.requestUpdateActionButtonSize();
};

/****
 *
 * @param {string} name
 * @param {string=} bf
 */
CommandController.prototype.addFolderMenuItem = function (name, bf) {
    idx = this.folderMenuItemNames.indexOf(name);
    if (idx >= 0) return;
    var idx = this.folderMenuItemNames.indexOf(bf);
    if (idx >= 0)
        this.folderMenuItemNames.splice(idx, 0, name);
    else this.folderMenuItemNames.push(name);
};

/****
 *
 * @param {string} name
 * @param {string=} bf
 */
CommandController.prototype.addContentMenuItem = function (name, bf) {
    idx = this.folderMenuItemNames.indexOf(name);
    if (idx >= 0) return;//todo
    var idx = this.folderMenuItemNames.indexOf(bf);
    if (idx >= 0)
        this.folderMenuItemNames.splice(idx, 0, name);
    else this.folderMenuItemNames.push(name);
};


CommandController.prototype.ev_navContextMenu = function (event) {
    var expTree;
    var c = event.target;
    while (c && !expTree) {
        if (c.stat) expTree = c;
        c = c.parentElement;
    }
    if (expTree) this.elt.navCtrl.viewDir(expTree.stat.path, [this.elt.rootPath].concat(expTree.getPath()).join('/'));
    var items = this.folderMenuItemNames.map(name => {
        var desc = this.commands[name];
        if (!desc) return null;
        if (typeof desc.match === "function") {
            if (!desc.match.call(this.elt, expTree)) return null;
        }
        return {
            text: desc.text,
            icon: desc.icon,
            cmd: name
        }
    }).filter(x => !!x);
    if (items.length > 0) {
        event.showContextMenu({ items: items }, (event) => {
            var cmd = event.menuItem.cmd;
            this.execCommand(cmd, expTree);
        });
    }
};


CommandController.prototype.ev_contentContextMenu = function (event) {
    var fileElt;
    var c = event.target;
    while (c && !fileElt) {
        if (c.stat) fileElt = c;
        c = c.parentElement;
    }

    var selectedElements = this.elt.selectCtrl.$selectedItems;
    if (fileElt && selectedElements.indexOf(fileElt) < 0) {
        this.elt.selectCtrl.deselectAll();
        this.elt.selectCtrl.select(fileElt);

    }
    // if (expTree) this.elt.navCtrl.viewDir(expTree.stat.path, [this.elt.rootPath].concat(expTree.getPath()).join('/'));
    var items = this.contentMenuItemNames.map(name => {
        var desc = this.commands[name];
        if (!desc) return null;
        if (typeof desc.match === "function") {
            if (!desc.match.call(this.elt, fileElt)) return null;
        }
        return {
            text: desc.text,
            icon: desc.icon,
            cmd: name
        }
    }).filter(x => !!x);
    if (items.length > 0) {
        event.showContextMenu({ items: items }, (event) => {
            var cmd = event.menuItem.cmd;
            this.execCommand(cmd, fileElt);
        });
    }
};


/***
 * for desktop
 * @param {Finder} elt
 * @constructor
 */
function SelectController(elt) {
    this.elt = elt;
    this.$selectedItems = [];// first element is focus
    this.$content = this.elt.$content;
    this.$contentCtn = this.elt.$contentCtn;
    Object.keys(this.constructor.prototype).forEach(key => {
        if (key.startsWith('ev_')) {
            this[key] = this[key].bind(this);
        }
    });
    this._setupSelectTool();

}

/***
 *
 * @protected
 */
SelectController.prototype._setupSelectTool = function () {
    this._draged = false;
    this._dragOffset = new Vec2(0, 0);
    this.$selectArea = _('.as-finder-select-area');
    this.$contentCtn.on('draginit', this.ev_dragInit)
        .on('dragdeinit', this.ev_dragDeinit)
        .on('dragstart', this.ev_dragStart)
        .on('drag', this.ev_drag)
        .on('dragend', this.ev_dragEnd);
};

SelectController.prototype.deselectAll = function () {
    while (this.$selectedItems.length > 0) {
        this.$selectedItems.pop().checked = false;
    }
    this._updateCount();
    this.elt.emit('selectedchange');
};

SelectController.prototype.select = function (elt) {//todo: more option
    this.deselectAll();
    this.$selectedItems.push(elt);
    elt.checked = true;
    this._updateCount();
    this.elt.emit('selectedchange');
};

SelectController.prototype.selectAll = function () {//todo: more option
    this.deselectAll();
    var fileElements = Array.prototype.slice.call(this.$content.childNodes);
    this.$selectedItems.push.apply(this.$selectedItems, fileElements);
    fileElements.forEach(elt => {
        elt.checked = true;
    });
    this._updateCount();
    this.elt.emit('selectedchange');
};


SelectController.prototype.ev_dragInit = function (event) {
    if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
        event.cancel();
        return;
    }
    // event.preventDefault();
    this._draged = false;
};


SelectController.prototype.ev_dragDeinit = function (event) {
    if (!this._draged) this.ev_click(event);
};

SelectController.prototype.ev_dragStart = function (event) {
    this._draged = true;
    this.elt.addClass('as-dragging');
    this.$selectArea.addStyle('z-index', this.elt.findZIndex() + 100 + '').addTo(document.body);
    var bound = Rectangle.fromClientRect(this.$content.getBoundingClientRect());
    this._dragOffset = event.currentPoint.sub(bound.A());
    var pos = bound.A().add(this._dragOffset);
    this.$selectArea.addStyle({
        left: pos.x + 'px',
        top: pos.y + 'px',
    });
};

SelectController.prototype.ev_drag = function (event) {
    this._draged = true;
    var bound = Rectangle.fromClientRect(this.$content.getBoundingClientRect());
    var A = bound.A().add(this._dragOffset);
    var C = event.currentPoint;
    var sRect = Rectangle.boundingPoints([A, C]);

    this.$selectArea.addStyle({
        left: sRect.x + 'px',
        top: sRect.y + 'px',
        width: sRect.width + 'px',
        height: sRect.height + 'px',
    });
};

SelectController.prototype.ev_dragEnd = function () {
    while (this.$selectedItems.length > 0) {
        this.$selectedItems.pop().checked = false;
    }
    var selectBound = Rectangle.fromClientRect(this.$selectArea.getBoundingClientRect());
    Array.prototype.forEach.call(this.$content.childNodes, elt => {
        var bound = Rectangle.fromClientRect(elt.getBoundingClientRect());
        if (selectBound.isCollapse(bound, 0)) {
            this.$selectedItems.push(elt);
            elt.checked = true;
        }
    });
    this._draged = true;
    this.$selectArea.remove();
    this.elt.removeClass('as-dragging');
    this._updateCount();
    this.elt.layoutCtn.update();
    this.elt.emit('selectedchange');
};


SelectController.prototype._updateCount = function () {
    var folderCount = this.$selectedItems.filter(elt => elt.stat.isDirectory).length;
    this.elt.attr('data-selected-file-count', this.$selectedItems.length - folderCount + '');
    this.elt.attr('data-selected-folder-count', folderCount + '');
}

SelectController.prototype.ev_click = function (event) {
    event = event.originalEvent || event.originEvent || event;

    var c = event.target;
    var itemElt;
    while (c && !itemElt) {
        if (c.hasClass && c.hasClass('as-file-thumbnail')) {
            itemElt = c;
            break;
        }

        c = c.parentElement;
    }

    var focusIdx;
    var currentIdx;
    if (this.$selectedItems.length === 0 && itemElt) {
        this.$selectedItems.push(itemElt);
        itemElt.checked = true;
    }
    else if (isMouseRight(event)) {

    }
    else if (!event.ctrlKey && !event.shiftKey) {
        while (this.$selectedItems.length > 0) {
            this.$selectedItems.pop().checked = false;
        }
        if (itemElt) {
            this.$selectedItems.push(itemElt);
            itemElt.checked = true;
        }
    }
    else if (event.shiftKey) {
        if (itemElt) {
            focusIdx = Array.prototype.indexOf.call(this.$content.childNodes, this.$selectedItems[0]);
            currentIdx = Array.prototype.indexOf.call(this.$content.childNodes, itemElt);
            while (this.$selectedItems.length > 1) {
                this.$selectedItems.pop().checked = false;
            }
            while (currentIdx !== focusIdx) {
                itemElt = this.$content.childNodes[currentIdx];
                this.$selectedItems.push(itemElt);
                itemElt.checked = true;
                if (currentIdx < focusIdx) currentIdx++;
                else currentIdx--;
            }
        }
    }
    else if (event.ctrlKey) {
        if (itemElt) {
            currentIdx = this.$selectedItems.indexOf(itemElt);
            if (currentIdx >= 0) {
                this.$selectedItems.splice(currentIdx, 1);
                itemElt.checked = false;
            }
            else {
                this.$selectedItems.unshift(itemElt);
                itemElt.checked = true;
            }
        }
    }

    this._updateCount();
    this.elt.layoutCtn.update();
    this.elt.emit('selectedchange');
};

/***
 * @extends SelectController
 * @param elt
 * @constructor
 */
function MobileSelectController(elt) {
    SelectController.apply(this, arguments);
}


OOP.mixClass(MobileSelectController, SelectController);

MobileSelectController.prototype._setupSelectTool = function () {
    this.$content.on('click', this.ev_click);
};


/***
 *
 * @param {Finder} elt
 * @constructor
 */
function UploadController(elt) {
    /***
     *
     * @type {Finder}
     */
    this.elt = elt;
    this.$body = this.elt.$body;
    Object.keys(this.constructor.prototype).forEach(key => {
        if (key.startsWith('ev_')) {
            this[key] = this[key].bind(this);
        }
    });

    this.$body.on({
        fileenter: this.ev_fileEnter,
        filedrop: this.ev_fileDrop
    })
}

UploadController.prototype.upload = function (files) {
    var contentElt = _({
        style: { maxHeight: '50vh', overflow: 'auto' },
        child: {
            style: { display: 'table' },
            child: files.map(file => ({
                style: { display: 'table-row' },
                child: [
                    {
                        style: { display: 'table-cell', padding: '5px 20px 5px 10px' },
                        child: {
                            style: {
                                'min-width': '30em',
                            },
                            child: { text: file.name }
                        }
                    },
                    {
                        style: { display: 'table-cell', padding: '5px 10px' },
                        child: {
                            class: 'as-upload-percent',
                            style: {
                                'min-width': '3em',
                                textAlign: 'right',
                                color: 'rgb(30,237,219)'
                            },
                            child: { text: '' }
                        }
                    }
                ]
            }))
        }
    });
    var modal = _({
        tag: Modal.tag,
        style: { zIndex: this.elt.findZIndex() + 9000 },
        child: {
            tag: MessageDialog.tag,
            props: {
                dialogTitle: 'Tải lên',
            },
            child: contentElt
        }
    }).addTo(document.body);

    var syncs = files.map((file, i) => {
        var percentText = $('.as-upload-percent', contentElt.firstChild.childNodes[i]);
        return this.elt.fileSystem.writeFile(this.elt.path + '/' + file.name, file, done => {
            var textBound = percentText.getBoundingClientRect();
            var ctnBound = contentElt.getBoundingClientRect();
            if (textBound.bottom > ctnBound.bottom) {
                contentElt.scrollTop += textBound.bottom - ctnBound.bottom;
            }
            percentText.firstChild.data = Math.round(done * 100) + '%';
        });
    });
    Promise.all(syncs).then(() => {
        this.elt.navCtrl.reload(this.elt.path, true).then(() => modal.remove());
    });
}


UploadController.prototype.ev_fileEnter = function (event) {
    var files = Array.prototype.slice.call(event.dataTransfer.files);
    if (files.length === 0) return;
};


UploadController.prototype.ev_fileDrop = function (event) {
    if (this.elt.searchCtrl.state === "RUNNING") return;
    var files = event.files;
    if (files.length > 0)
        this.upload(files);
};

/***
 * @extends EventEmitter
 * @param {Finder} elt
 * @constructor
 */
function FolderDialog(elt) {
    EventEmitter.call(this);
    this.elt = elt;
}

OOP.mixClass(FolderDialog, EventEmitter);

FolderDialog.prototype._init = function () {
    if (this.$modal) return;
    this.$modal = _({
        tag: 'modal',
        class: 'as-finder-folder-dialog-modal',
        child: {
            tag: MessageDialog.tag,
            props: {
                dialogTitle: 'Duyệt thư mục',
                dialogActions: [
                    { name: 'ok', text: 'OK' },
                    { name: 'cancel', text: 'Hủy' }
                ]
            },
            child: [
                {
                    class: 'as-finder-folder-dialog-content',
                    child: [
                        {
                            class: 'as-finder-folder-dialog-selected-ctn',
                            child: [
                                { tag: 'span', child: { text: 'Đã chọn thư mục: ' } },
                                { tag: 'span', child: { text: ' ... ' } }
                            ]
                        },
                        {
                            class: 'as-finder-folder-dialog-tree-ctn',
                            child: [
                                {
                                    tag: ExpGroup.tag
                                },
                                {
                                    tag: ExpTree.tag,
                                    class: 'as-finder-folder-dialog-tree-root',
                                    props: {
                                        name: 'root',
                                        icon: 'span.mdi.mdi-harddisk'
                                    }
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    });
    this.$dialog = $(MessageDialog.tag, this.$modal);
    this.$treeCtn = $('.as-finder-folder-dialog-tree-ctn', this.$modal);
    this.$rootTree = $('.as-finder-folder-dialog-tree-root', this.$modal);
    this.$content = $('.as-finder-folder-dialog-content', this.$modal);
    this.$expGroup = $(ExpGroup.tag, this.$content);
    this.$selectedCtn = $('.as-finder-folder-dialog-selected-ctn', this.$content);
    this.$selectedPath = this.$selectedCtn.childNodes[1];
    this.$activeNode = null;
}

FolderDialog.prototype.open = function (initPath, showRoot, checkFunc, title) {
    var cPath = initPath;
    this._init();
    var fileSystem = this.elt.fileSystem;
    var zIndex = this.elt.findZIndex() + 9000;
    this.$modal.addStyle('z-index', zIndex + '');
    this.$modal.addTo(document.body);
    if (this.$activeNode) {
        this.$activeNode.active = false;
        this.$activeNode = null;
    }

    this.$dialog.$actionBtns[0].disabled = true;
    this.$dialog.dialogTitle = title || 'Duyệt';


    var makeTree = (path, ctnElt, level) => {
        level = level || 0;
        return fileSystem.readDir(path).then(dirs => Promise.all(dirs.map(dir => fileSystem.stat(path + '/' + dir))))
            .then(stats => stats.filter(stat => {
                return stat.isDirectory
            }))
            .then(stats => {
                var syncs = [];
                ctnElt.clearChild();
                var children = stats.map(stat => {
                    var nodePath = path + '/' + stat.name;
                    var node = _({
                        tag: ExpTree.tag,
                        props: {
                            stat: stat,
                            name: stat.displayName || stat.name,
                            icon: { tag: 'img', props: { src: MessageInput.iconAssetRoot + '/folder.svg' } },
                            path: nodePath
                        },
                        on: {
                            statuschage: () => {
                            },
                            press: () => {
                                if (this.$activeNode) this.$activeNode.active = false;
                                this.$activeNode = node;
                                this.$activeNode.active = true;
                                this.$selectedPath.firstChild.data = node.getPath().join('/');
                                cPath = nodePath;
                                if (checkFunc && !checkFunc(cPath, stat)) {
                                    this.$dialog.$actionBtns[0].disabled = true;
                                }
                                else {
                                    this.$dialog.$actionBtns[0].disabled = false;
                                }
                            }
                        }

                    });
                    if (nodePath === cPath) {
                        node.active = true;
                        this.$activeNode = node;
                    }
                    if (checkFunc && !checkFunc(nodePath, stat)) {
                        node.getNode().addStyle('opacity', 0.3 + '');
                    }
                    node.getNode().on({
                        dblclick: () => {
                            if (node.status === 'close') {
                                node.status = 'open';
                            }
                            else if (node.status === 'open') {
                                node.status = 'close';
                            }
                        }
                    });
                    if (stat.name !== 'node_modules')
                        syncs.push(makeTree(nodePath, node, level + 1));

                    return node;
                });
                children.forEach(c => {
                    ctnElt.addChild(c);
                });
                if (children.length) {
                    ctnElt.status = level > 1 ? 'close' : 'open';
                }
                else {
                    ctnElt.status = 'none';
                }
                return Promise.all(syncs);
            });
    }

    var onRootPress;

    if (showRoot) {
        this.$expGroup.addStyle('display', 'none');
        this.$rootTree.removeStyle('display');
        if (cPath === this.elt.rootPath) {
            this.$activeNode = this.$rootTree;
            this.$activeNode.active = true;
        }

        onRootPress = () => {
            var node = this.$rootTree;
            var nodePath = this.elt.rootPath;
            if (this.$activeNode) this.$activeNode.active = false;
            this.$activeNode = node;
            this.$activeNode.active = true;
            this.$selectedPath.firstChild.data = node.getPath().join('/');
            cPath = nodePath;
            if (cPath === initPath || (checkFunc && !checkFunc(cPath, { writable: true }))) {
                this.$dialog.$actionBtns[0].disabled = true;
            }
            else {
                this.$dialog.$actionBtns[0].disabled = false;
            }
        };
        this.$rootTree.on('press', onRootPress);
    }
    else {
        this.$expGroup.removeStyle('display');
        this.$rootTree.addStyle('display', 'none');
    }

    makeTree(this.elt.rootPath, showRoot ? this.$rootTree : this.$expGroup).then(() => {
        var p;
        if (this.$activeNode) {
            this.$selectedPath.firstChild.data = this.$activeNode.getPath().join('/');
            p = this.$activeNode.getParent();
            while (p) {
                if (p.status === 'close') {
                    p.status = 'open';
                }
                p = p.getParent && p.getParent();
            }
            setTimeout(() => {
                vScrollIntoView(this.$activeNode.firstChild);
            }, 10)
        }
    });


    return new Promise((resolve) => {
        var finish = (event) => {
            this.$dialog.off('action', finish);
            this.$modal.remove();
            if (event.action.name === 'cancel') resolve(null);
            else resolve(cPath);
            if (onRootPress) {
                this.$rootTree.off('press', onRootPress);
            }
        }

        this.$dialog.on('action', finish);
    });
};

var isMatchAccept = (accept, statInfo) => {
    if (accept && (typeof accept === "object") && accept.accept) accept = accept.accept;
    if (typeof accept !== "string") return true;
    if (!accept) return true;
    if (statInfo.isDirectory) return true;
    var fileInfo = fileInfoOf(statInfo);
    if (accept.startsWith('image')) {
        return fileInfo.mimeType && fileInfo.mimeType.startsWith('image');
    }
    return true;// not handle other case
}

/***
 *
 * @param {Finder} elt
 * @constructor
 */
function NavigatorController(elt) {
    this.elt = elt;
    this.path = '';
    this.rootPath = '';

    this._states = {};
    this._notifiedVisibleIdx = 0;

    this.$navCtn = this.elt.$navCtn;
    this.$nav = this.elt.$nav;
    this.$contentCtn = this.elt.$contentCtn;
    this.$content = this.elt.$content;

    Object.keys(this.constructor.prototype).forEach(key => {
        if (key.startsWith('ev_')) {
            this[key] = this[key].bind(this);
        }
    });

    this.$contentCtn.on('scroll', this.ev_contentScroll);
}

NavigatorController.prototype.onStart = function () {
    /**
     *
     * @type {AbsolFileSystem}
     */
    this.fileSystem = this.elt.fileSystem;

    this.$treeByPath = {};
    this.$treeByPath[this.rootPath || '..'] = this.$nav;
    this.reload(this.rootPath, true);
};

NavigatorController.prototype.reload = function (fromPath, autoOpen) {
    var opened = !autoOpen;

    var makeTree = (path, ctnElt) => {
        if (!opened && ctnElt.path) {
            this.viewDir(ctnElt.path, [this.elt.rootPath].concat(ctnElt.getPath()).join('/'));
            opened = true;
        }
        return this.fileSystem.readDir(path).then(dirs => Promise.all(dirs.map(dir => this.fileSystem.stat(path + '/' + dir))))
            .then(stats => stats.filter(stat => {
                return stat.isDirectory
            }))
            .then(stats => {
                var syncs = [];
                ctnElt.clearChild();
                var children = stats.map(stat => {
                    var nodePath = path + '/' + stat.name;
                    var node = _({
                        tag: ExpTree.tag,

                        props: {
                            stat: stat,
                            name: stat.displayName || stat.name,
                            icon: { tag: 'img', props: { src: MessageInput.iconAssetRoot + '/folder.svg' } },
                            path: nodePath,
                        },
                        on: {
                            statuschage: () => {
                                this._states[path + '/' + stat.name] = node.status;
                            },
                            press: () => {
                                if (this.path !== nodePath)
                                    this.viewDir(nodePath, [this.elt.rootPath].concat(node.getPath()).join('/'));
                            }
                        }

                    });
                    node.getNode().on({
                        dblclick: () => {
                            if (node.status === 'close') {
                                node.status = 'open';
                            }
                            else if (node.status === 'open') {
                                node.status = 'close';
                            }
                            this._states[nodePath] = node.status;
                        }
                    });
                    this.$treeByPath[nodePath] = node;
                    if (stat.name !== 'node_modules')
                        syncs.push(makeTree(nodePath, node));
                    if (!opened) {
                        this.viewDir(nodePath, [this.elt.rootPath].concat(node.getPath()).join('/'));
                        opened = true;
                    }
                    return node;
                });
                children.forEach(c => {
                    ctnElt.addChild(c);
                });
                if (children.length) {
                    if (this._states[ctnElt.path] === 'close' || this._states[ctnElt.path] === 'open') {
                        ctnElt.status = this._states[ctnElt.path];
                    }
                    else {
                        ctnElt.status = 'close';
                    }
                }
                else {
                    ctnElt.status = 'none';
                }
                return Promise.all(syncs);
            });

    }


    if (this.$treeByPath[fromPath || '..'])
        return makeTree(fromPath, this.$treeByPath[fromPath || '..']);
    return Promise.resolve();
};

NavigatorController.prototype.viewDir = function (path) {
    this.elt.selectCtrl.deselectAll();
    if (this.$treeByPath[this.path]) {
        this.$treeByPath[this.path].active = false;
        this.$treeByPath[this.path].active = false;
    }
    this.path = path;
    this.$treeByPath[this.path].active = true;
    var c = this.$treeByPath[this.path].getParent();
    while (c) {
        if (c.status === 'close') c.status = 'open';
        c = c.getParent && c.getParent();
    }

    vScrollIntoView(this.$treeByPath[this.path].firstChild);


    this.fileSystem.stat(path).then(stat => {
        this.elt.dirStat = stat;
        if (this.path !== path) return;
        if (stat.writable) this.elt.addClass('as-writable-folder');
        else this.elt.removeClass('as-writable-folder');
        if (stat.isVirtual) {
            this.elt.addClass('as-virtual-folder');
        }
        else {
            this.elt.removeClass('as-virtual-folder');
        }
    })
    this.fileSystem.readDir(path).then(dirs => Promise.all(dirs.map(dir => this.fileSystem.stat(path + '/' + dir))))
        .then(stats => {
            if (this.path !== path) return;
            stats.sort((a, b) => {
                var aName, bName;
                if (a.isDirectory === b.isDirectory) {
                    aName = a.displayName || a.name;
                    bName = b.displayName || b.name;
                    if (aName < bName) return -1;
                    return 1;
                }
                else {
                    if (a.isDirectory) return -1;
                    return 1;
                }
            });
            stats = stats.filter(x => isMatchAccept(this.elt.accept, x));
            this.viewContent(stats);
        });
};

NavigatorController.prototype.viewContent = function (stats) {
    this.clearContent();
    stats.forEach(stat => {
        this.pushContentItem(stat);

    });
    this.notifyVisibleContentItems();
};

NavigatorController.prototype.clearContent = function () {
    this._notifiedVisibleIdx = 0;
    this.$content.clearChild();
};

NavigatorController.prototype.pushContentItem = function (stat) {
    var elt = _({
        tag: FileThumbnail.tag,
        extendEvent: ['visible'],
        attr: {
            title: stat.displayName || stat.name
        },
        props: {
            isDirectory: stat.isDirectory,
            value: stat.url,
            fileName: stat.displayName || stat.name,
            stat: stat
        },
        on: {
            visible: () => {
                var mineType = ext2MineType[elt.fileType];
                if (mineType && mineType.startsWith('image/')) {
                    elt.thumbnail = stat.url;
                }
            },
            dblclick: () => {
                var prevented = false;
                var event;
                if (!stat.isDirectory) {
                    event = {
                        fileElt: elt,
                        stat: stat,
                        preventDefault: () => {
                            prevented = true;
                        }
                    };
                    this.elt.emit('dblclickfile', event);
                }
                if (!prevented)
                    this.elt.execCommand('view');

            }
        }
    });

    this.$content.addChild(elt);
};


NavigatorController.prototype.notifyVisibleContentItems = function () {
    var elt;
    var bound = this.$contentCtn.getBoundingClientRect();
    var eBound;
    while (this._notifiedVisibleIdx < this.$content.childNodes.length) {
        elt = this.$content.childNodes[this._notifiedVisibleIdx];
        eBound = elt.getBoundingClientRect();
        if (eBound.top < bound.bottom) {
            elt.emit('visible');
        }
        else {
            break;
        }
        this._notifiedVisibleIdx++;
    }
};

NavigatorController.prototype.expandAll = function () {
    var visit = nodeElt => {
        if (nodeElt.status === 'close') {
            nodeElt.status = 'open';
            this._states[nodeElt.path] = 'open';
        }
        if (nodeElt.status === 'open') {
            nodeElt.getChildren().forEach(visit);
        }
    };

    Array.prototype.forEach.call(this.$nav.childNodes, visit);
};


NavigatorController.prototype.collapseAll = function () {
    var visit = nodeElt => {
        if (nodeElt.status === 'open') {
            nodeElt.status = 'close';
            this._states[nodeElt.path] = 'close';
        }
        if (nodeElt.status === 'close') {
            nodeElt.getChildren().forEach(visit);
        }
    };

    Array.prototype.forEach.call(this.$nav.childNodes, visit);
};

NavigatorController.prototype.ev_contentScroll = function (event) {
    this.notifyVisibleContentItems();
};

var fileTextQuery2Regex = text => {
    var code = nonAccentVietnamese(text.toLowerCase())
        .replace(/[.,+^$()\[\]{}|\\]/g, x => '\\' + x)
        .replace(/\*+/g, '(.*)')
        .replace(/\?/g, '.');

    return new RegExp(code, 'i');
}


/***
 * @extends Context
 * @param {Finder} elt
 * @constructor
 */
function SearchController(elt) {
    Context.apply(this);
    this.elt = elt;
    this.$searchText = this.elt.$searchText.on('keydown', event => {
        if (event.key === 'Enter') {
            this.$searchText.blur();
            this.search();
        }
    });
}


OOP.mixClass(SearchController, Context);


SearchController.prototype.onStart = function () {
    // console.log('start')
    this.elt.addClass('as-searching');
    this.$searchText.value = '';
    this.$searchText.focus();
};


SearchController.prototype.onStop = function () {
    this.elt.removeClass('as-searching');
    this.session = randomIdent(10);
    this.$searchText.waiting = false;

};

SearchController.prototype.search = function () {
    var session = randomIdent(10);
    this.session = session;
    this.elt.navCtrl.clearContent();
    this.$searchText.waiting = true;
    var fileSystem = this.elt.fileSystem;
    var rootPath = this.elt.rootPath;
    var result = [];
    var type = this.elt.$searchTypeSelect.value;
    var text = this.$searchText.value.trim();
    var regex = fileTextQuery2Regex(text);


    var isMatched = stat => {
        var fileInfo = fileInfoOf(stat.displayName || stat.name);
        var mineType = fileInfo.mimeType || '';
        var fileType = fileInfo.type || '';
        fileType = fileType.toLowerCase();

        if (mineType && type !== 'all') {
            if (type === 'image' && !mineType.startsWith('image') && type !== 'svg') {
                return false;
            }
            else if (type === 'document' && ['doc', 'docx', 'pdf', 'xlsx'].indexOf(fileType) < 0 && !mineType.startsWith('text')) {
                return false;
            }
        }
        if (text.length === 0) return true;
        if (stat.displayName && stat.displayName.match(regex)) return true;
        if (stat.name && nonAccentVietnamese(stat.name.toLowerCase()).match(regex)) return true;

        return false;
    }


    var handleStat = stat => {
        if (session !== this.session) return;
        if (stat.isDirectory) return visitDir(stat.path);
        if (isMatched(stat)) {
            this.elt.navCtrl.pushContentItem(stat);
            this.elt.navCtrl.notifyVisibleContentItems();
        }
    };

    var handleDirResult = (dir, names) => {
        var syncs = names.map(name => {
            return fileSystem.stat(dir + '/' + name).then(stat => {
                if (session !== this.session) return;
                if (stat)
                    return handleStat(stat);
            });
        });
        return Promise.all(syncs);
    }

    var visitDir = path => {
        return fileSystem.readDir(path).then((names => handleDirResult(path, names)));
    }

    visitDir(rootPath).then(() => {
        if (session !== this.session) return;
        this.$searchText.waiting = false;
    });

};


/***
 *
 * @constructor
 */
export function FinderFileSystem() {

}

FinderFileSystem.prototype.supporteDisplayName = false;

FinderFileSystem.prototype.readDir = function (path) {

};


FinderFileSystem.prototype.unlink = function (path) {
    console.log(path);
};

FinderFileSystem.prototype.stat = function (path) {

};

FinderFileSystem.prototype.rmdir = function (path) {

};

FinderFileSystem.prototype.mkdir = function (path) {

};


FinderFileSystem.prototype.writeFile = function (file, data) {

};

FinderFileSystem.prototype.copy = function () {

};


FinderFileSystem.prototype.rename = function (path, name) {

};

FinderFileSystem.prototype.move = function (oldPath, newPath) {

};

FinderFileSystem.prototype.clearCache = function () {

};

/***
 * @extends FinderFileSystem
 * @constructor
 */
function AbsolFileSystem() {
    FinderFileSystem.apply(this, arguments);
    this.sync = Promise.resolve();
    this.cache = { readDir: {}, stats: {} };
    this.taskMng = new TaskManager({ limit: 4 });
}

OOP.mixClass(AbsolFileSystem, FinderFileSystem);

AbsolFileSystem.prototype.API_PREFIX = 'https://c9.absol.cf';

AbsolFileSystem.prototype.clearCache = function () {
    this.cache = { readDir: {}, stats: {} };
};

AbsolFileSystem.prototype.readDir = function (path) {
    this.sync = this.sync.then(() => {
        if (this.cache.readDir[path || '..']) return this.cache.readDir[path || '..'];
        return fetch(this.API_PREFIX + '/filesystem/ls.php', {
            method: 'POST',
            cache: "no-cache",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                path: path
            })
        }).then(res => res.json()).then(res => {
            res = res.filter(c => c.path.startsWith('/html'));
            res.forEach(c => {
                c.name = c.path.split('/').pop();
                c.url = c.path.replace('/html', location.origin)
            });
            this.cache.readDir[path || '..'] = res.map(c => c.name);
            res.forEach(c => {
                this.cache.stats[c.path] = c;
            });

        }).then(() => {
            return this.cache.readDir[path || '..'];
        });
    });
    return this.sync;
};

AbsolFileSystem.prototype.stat = function (path) {
    return this.sync.then(() => {
        path = path || '';
        if (this.cache.stats[path || '..']) return this.cache.stats[path || '..'];
        var dir = path.split('/');
        dir.pop();
        dir = dir.join('/');
        return this.readDir(dir).then(() => {
            return this.cache.stats[path || '..'];
        })
    });
}

/***
 *
 * @param file
 * @param {File|Blob}data
 * @param {function(done: number):void=}onProcess
 */
AbsolFileSystem.prototype.writeFile = function (file, data, onProcess) {
    if (file.toLowerCase().endsWith('.php')) file += '.txt';
    var folderPath = file.split('/');
    folderPath.pop();
    folderPath = folderPath.join('/');
    delete this.cache.readDir[folderPath];
    var prefix = ['file', new Date().getTime(), randomArbitrary(0, 1000000) >> 0].join('_');
    var parts = [];
    var chuck_limit = 2 << 20;
    var partName;
    var fileSize = data.size;
    var fileStartOffset = 0;
    var fileEndOffset = 0;

    var idx = 0;
    var syncs = [];
    var syncDone = 0;

    var bundle;
    var began = false;
    var handle = bundle => {
        return new Promise(rs => {
            this.taskMng.requestTask((finishTask, bundle) => {
                if (typeof onProcess === "function" && !began) {
                    began = true;
                    onProcess(syncDone / (syncs.length || 1));
                }
                var form = new FormData();
                form.append('action', 'upload_part');
                form.append('fileUpload', bundle.file, bundle.name);
                fetch(this.API_PREFIX + '/filesystem/writefile.php', {
                    method: 'POST',
                    body: form
                }).then(res => res.text()).then(text => {
                    if (text !== 'OK') throw new Error(text);
                    syncDone++;
                    if (typeof onProcess === "function") {
                        onProcess(syncDone / (syncs.length || 1));
                    }
                    rs(location.origin);
                    finishTask();
                });
            }, bundle);
        })
    }
    while (fileStartOffset < fileSize) {
        fileEndOffset = Math.min(fileStartOffset + chuck_limit, fileSize);
        partName = prefix + '.p' + idx;
        parts.push(partName);
        bundle = {
            file: data.slice(fileStartOffset, fileEndOffset),
            idx: idx,
            name: partName
        };
        idx++;
        fileStartOffset = fileEndOffset;
        syncs.push(handle(bundle));
    }

    return Promise.all(syncs).then(() => {
        var form = new FormData();
        form.append('action', 'join_parts');
        form.append('parts', parts.join(';'));
        form.append('path', file);
        fetch(this.API_PREFIX + '/filesystem/writefile.php', {
            method: 'POST',
            body: form
        }).then(res => res.text()).then(text => {
            if (text !== 'OK') throw new Error(text);
        });
    });
};

AbsolFileSystem.prototype.unlink = function (path) {
    var folderPath = path.split('/');
    folderPath.pop();
    folderPath = folderPath.join('/');
    delete this.cache.readDir[folderPath];
    var form = new FormData();
    form.append('action', 'delete_files');
    form.append('paths', path);
    return fetch(this.API_PREFIX + '/filesystem/writefile.php', {
        method: 'POST',
        body: form
    }).then(res => res.text()).then(text => {
        if (text !== 'OK') throw new Error(text);
    });
};


AbsolFileSystem.prototype.rename = function (path, name) {
    var folderPath = path.split('/');
    folderPath.pop();
    folderPath = folderPath.join('/');
    var form = new FormData();
    form.append('action', 'rename');
    form.append('path', path);
    form.append('new_name', name);
    return fetch(this.API_PREFIX + '/filesystem/writefile.php', {
        method: 'POST',
        body: form
    }).then(res => res.text()).then(text => {
        if (text !== 'OK') throw new Error(text);
        var newPath = folderPath + '/' + name;
        delete this.cache.readDir[folderPath];
        delete this.cache.stats[path];
        return {
            url: newPath.replace('/html', location.origin),
            path: newPath,
            name: name
        }
    });

};


AbsolFileSystem.prototype.move = function (oldPath, newPath) {
    var oldFolderPath = oldPath.split('/');
    oldFolderPath.pop();
    oldFolderPath = oldFolderPath.join('/');
    var newFolderPath = newPath.split('/');
    newFolderPath.pop();
    newFolderPath = newFolderPath.join('/');
    var form = new FormData();
    form.append('action', 'move');
    form.append('old_path', oldPath);
    form.append('new_path', newPath);
    return fetch(this.API_PREFIX + '/filesystem/writefile.php', {
        method: 'POST',
        body: form
    }).then(res => res.text()).then(text => {
        if (text !== 'OK') throw new Error(text);
        delete this.cache.readDir[oldFolderPath];
        delete this.cache.readDir[newFolderPath];
        delete this.cache.stats[oldPath];
    });
};

VaKeR 2022