mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-11-04 05:27:04 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			505 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			505 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import React from 'react';
 | 
						|
import path from 'path';
 | 
						|
import PropTypes from 'prop-types';
 | 
						|
import { Link } from 'react-router-dom';
 | 
						|
import { DragSource, DropTarget } from 'react-dnd';
 | 
						|
import { createSelectable } from 'react-selectable';
 | 
						|
 | 
						|
import './thing.scss';
 | 
						|
import { Card, NgIf, Icon, EventEmitter, Button, img_placeholder } from '../../components/';
 | 
						|
import { pathBuilder, basename, filetype, prompt, alert, leftPad, getMimeType, debounce, memory } from '../../helpers/';
 | 
						|
import { Files } from '../../model/';
 | 
						|
import { ShareComponent } from './share';
 | 
						|
import { t } from '../../locales/';
 | 
						|
 | 
						|
const fileSource = {
 | 
						|
    beginDrag(props, monitor, component) {
 | 
						|
        return {
 | 
						|
            path: props.path,
 | 
						|
            name: props.file.name,
 | 
						|
            type: props.file.type
 | 
						|
        };
 | 
						|
    },
 | 
						|
    canDrag(props, monitor){
 | 
						|
        if (props.metadata.can_move === false){
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        if(props.file.icon === "loading") return false;
 | 
						|
        else if(props.selected === false && props.currentSelection.length > 0) return false;
 | 
						|
        return true;
 | 
						|
    },
 | 
						|
    endDrag(props, monitor, component){
 | 
						|
        if(monitor.didDrop() && component.state.icon !== 'loading'){
 | 
						|
            let result = monitor.getDropResult();
 | 
						|
            if(result.action === 'rename'){
 | 
						|
                props.emit.apply(component, ['file.rename'].concat(result.args));
 | 
						|
            }else if(result.action === "rename.multiple"){
 | 
						|
                props.emit.call(component, "file.rename.multiple", result.args);
 | 
						|
            }else{
 | 
						|
                throw 'unknown action';
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
const fileTarget = {
 | 
						|
    canDrop(props, monitor){
 | 
						|
        let file = monitor.getItem();
 | 
						|
        if(props.file.type !== "directory") return false;
 | 
						|
        else if(file.name === props.file.name) return false;
 | 
						|
        else if(props.file.icon === "loading") return false;
 | 
						|
        else if(props.selected === true) return false;
 | 
						|
        return true;
 | 
						|
    },
 | 
						|
    drop(props, monitor, component){
 | 
						|
        let src = monitor.getItem();
 | 
						|
        let dest = props.file;
 | 
						|
 | 
						|
        if(props.currentSelection.length === 0){
 | 
						|
            const from = pathBuilder(props.path, src.name, src.type);
 | 
						|
            const to = pathBuilder(props.path, './'+dest.name+'/'+src.name, src.type);
 | 
						|
            return {action: 'rename', args: [from, to, src.type], ctx: 'existingfile'};
 | 
						|
        } else {
 | 
						|
            return {action: 'rename.multiple', args: props.currentSelection.map((selectionPath) => {
 | 
						|
                const from = selectionPath;
 | 
						|
                const to = pathBuilder(
 | 
						|
                    props.path,
 | 
						|
                    "./"+dest.name+"/"+basename(selectionPath),
 | 
						|
                    filetype(selectionPath)
 | 
						|
                );
 | 
						|
                return [ from, to ];
 | 
						|
            })};
 | 
						|
        }
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
const nativeFileTarget = {
 | 
						|
    canDrop: fileTarget.canDrop,
 | 
						|
    drop(props, monitor){
 | 
						|
        let path = pathBuilder(props.path, props.file.name, 'directory');
 | 
						|
        props.emit('file.upload', path, monitor.getItem());
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
 | 
						|
@createSelectable
 | 
						|
@EventEmitter
 | 
						|
@DropTarget('__NATIVE_FILE__', nativeFileTarget, (connect, monitor) => ({
 | 
						|
    connectDropNativeFile: connect.dropTarget(),
 | 
						|
    nativeFileIsOver: monitor.isOver(),
 | 
						|
    canDropNativeFile: monitor.canDrop()
 | 
						|
}))
 | 
						|
@DropTarget('file', fileTarget, (connect, monitor) => ({
 | 
						|
    connectDropFile: connect.dropTarget(),
 | 
						|
    fileIsOver: monitor.isOver(),
 | 
						|
    canDropFile: monitor.canDrop()
 | 
						|
}))
 | 
						|
@DragSource('file', fileSource, (connect, monitor) => ({
 | 
						|
    connectDragSource: connect.dragSource(),
 | 
						|
    isDragging: monitor.isDragging()
 | 
						|
}))
 | 
						|
export class ExistingThing extends React.Component {
 | 
						|
    constructor(props){
 | 
						|
        super(props);
 | 
						|
        this.state = {
 | 
						|
            hover: null,
 | 
						|
            filename: props.file.name,
 | 
						|
            is_renaming: false,
 | 
						|
            preview: null
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    shouldComponentUpdate(nextProps, nextState){
 | 
						|
        if(nextState.hover !== this.state.hover ||
 | 
						|
           nextState.is_renaming !== this.state.is_renaming ||
 | 
						|
           nextProps.view !== this.props.view ||
 | 
						|
           this.state.preview !== nextState.preview ||
 | 
						|
           this.props.fileIsOver !== nextProps.fileIsOver ||
 | 
						|
           this.props.canDropFile !== nextProps.canDropFile ||
 | 
						|
           this.props.nativeFileIsOver !== nextProps.nativeFileIsOver ||
 | 
						|
           this.props.canDropNativeFile !== nextProps.canDropNativeFile ||
 | 
						|
           this.props.selected !== nextProps.selected
 | 
						|
          ){
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    componentDidMount(){
 | 
						|
        this.updateThumbnail(this.props);
 | 
						|
    }
 | 
						|
 | 
						|
    componentWillReceiveProps(props){
 | 
						|
        if(props.view !== this.props.view){
 | 
						|
            this.updateThumbnail(props);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    updateThumbnail(props){
 | 
						|
        if(props.view === "grid" && props.icon !== "loading"){
 | 
						|
            const type = getMimeType(props.file.path).split("/")[0];
 | 
						|
            if(type === "image"){
 | 
						|
                Files.url(props.file.path).then((url) => {
 | 
						|
                    this.setState({preview: url+"&thumbnail=true"});
 | 
						|
                });
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onRename(newFilename){
 | 
						|
        if(typeof newFilename === "string"){
 | 
						|
            this.props.emit(
 | 
						|
                'file.rename',
 | 
						|
                pathBuilder(this.props.path, this.props.file.name, this.props.file.type),
 | 
						|
                pathBuilder(this.props.path, newFilename, this.props.file.type),
 | 
						|
                this.props.file.type
 | 
						|
            );
 | 
						|
        }
 | 
						|
        this.setState({is_renaming: false});
 | 
						|
    }
 | 
						|
 | 
						|
    onRenameRequest(force){
 | 
						|
        let new_state = !this.state.is_renaming;
 | 
						|
        if(typeof force === "boolean"){
 | 
						|
            new_state = force;
 | 
						|
        }
 | 
						|
        this.setState({is_renaming: new_state});
 | 
						|
    }
 | 
						|
 | 
						|
    onDeleteRequest(filename){
 | 
						|
        prompt.now(
 | 
						|
            t("Confirm by typing") +" \""+this._confirm_delete_text()+"\"",
 | 
						|
            (answer) => { // click on ok
 | 
						|
                if(answer === this._confirm_delete_text()){
 | 
						|
                    this.setState({icon: 'loading'});
 | 
						|
                    this.props.emit(
 | 
						|
                        'file.delete',
 | 
						|
                        pathBuilder(this.props.path, this.props.file.name, this.props.file.type),
 | 
						|
                        this.props.file.type
 | 
						|
                    );
 | 
						|
                    return Promise.resolve();
 | 
						|
                }else{
 | 
						|
                    return Promise.reject(t("Doesn't match"));
 | 
						|
                }
 | 
						|
            },
 | 
						|
            () => { /* click on cancel */ });
 | 
						|
    }
 | 
						|
    onDeleteConfirm(answer){
 | 
						|
        if(answer === this._confirm_delete_text()){
 | 
						|
            this.setState({icon: 'loading', delete_request: false});
 | 
						|
            this.props.emit(
 | 
						|
                'file.delete',
 | 
						|
                pathBuilder(this.props.path, this.props.file.name, this.props.file.type),
 | 
						|
                this.props.file.type
 | 
						|
            );
 | 
						|
        }else{
 | 
						|
            this.setState({delete_error: t("Doesn't match")});
 | 
						|
        }
 | 
						|
    }
 | 
						|
    onDeleteCancel(){
 | 
						|
        this.setState({delete_request: false});
 | 
						|
    }
 | 
						|
 | 
						|
    onShareRequest(filename){
 | 
						|
        alert.now(
 | 
						|
            <ShareComponent path={this.props.file.path} type={this.props.file.type} />,
 | 
						|
            (ok) => {}
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    onThingClick(e){
 | 
						|
        if(e.ctrlKey === true){
 | 
						|
            e.preventDefault();
 | 
						|
            this.props.emit(
 | 
						|
                "file.select",
 | 
						|
                pathBuilder(this.props.path, this.props.file.name, this.props.file.type)
 | 
						|
            );
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    _confirm_delete_text(){
 | 
						|
        return this.props.file.name.length > 16? this.props.file.name.substring(0, 10).toLowerCase() : this.props.file.name;
 | 
						|
    }
 | 
						|
 | 
						|
    render(highlight){
 | 
						|
        const { connectDragSource, connectDropFile, connectDropNativeFile } = this.props;
 | 
						|
        let className = "";
 | 
						|
        if(this.props.isDragging) {
 | 
						|
            className += "is-dragging ";
 | 
						|
        }
 | 
						|
        if((this.props.fileIsOver && this.props.canDropFile) || (this.props.nativeFileIsOver && this.props.canDropNativeFile)) {
 | 
						|
            className += "file-is-hover ";
 | 
						|
        }
 | 
						|
        if(this.state.is_renaming){
 | 
						|
            className += "highlight ";
 | 
						|
        }
 | 
						|
        if(this.props.file.icon === 'loading'){
 | 
						|
            className += "loading ";
 | 
						|
        }
 | 
						|
        if(this.state.preview){
 | 
						|
            className += "preview ";
 | 
						|
        }
 | 
						|
        className = className.trim();
 | 
						|
 | 
						|
        let fileLink = this.props.file.link
 | 
						|
            .replace(/%2F/g, "/")
 | 
						|
            .replace(/\%/g, "%2525") // Hack to get the Link Component to work
 | 
						|
            .replace(/\?/g, "%3F")
 | 
						|
            .replace(/\#/g, "%23");
 | 
						|
 | 
						|
        return connectDragSource(connectDropNativeFile(connectDropFile(
 | 
						|
            <div className={"component_thing view-"+this.props.view+(this.props.selected === true ? " selected" : " not-selected")}>
 | 
						|
              <ToggleableLink onClick={this.onThingClick.bind(this)} to={fileLink + window.location.search} disabled={this.props.file.icon === "loading"}>
 | 
						|
                <Card ref="$card"className={this.state.hover} className={className}>
 | 
						|
                  <Image preview={this.state.preview}
 | 
						|
                         icon={this.props.file.icon || this.props.file.type}
 | 
						|
                         view={this.props.view}
 | 
						|
                         path={path.join(this.props.path, this.props.file.name)}
 | 
						|
                         hide_extension={this.props.metadata.hide_extension} />
 | 
						|
                  <Filename filename={this.props.file.name}
 | 
						|
                            filesize={this.props.file.size}
 | 
						|
                            filetype={this.props.file.type}
 | 
						|
                            hide_extension={this.props.metadata.hide_extension}
 | 
						|
                            onRename={this.onRename.bind(this)}
 | 
						|
                            is_renaming={this.state.is_renaming}
 | 
						|
                            onRenameCancel={this.onRenameRequest.bind(this, false)}/>
 | 
						|
                  <DateTime show={this.state.icon !== 'loading'} timestamp={this.props.file.time} />
 | 
						|
                  <ActionButton onClickRename={this.onRenameRequest.bind(this)} onClickDelete={this.onDeleteRequest.bind(this)} onClickShare={this.onShareRequest.bind(this)} is_renaming={this.state.is_renaming}
 | 
						|
                                can_rename={this.props.metadata.can_rename !== false} can_delete={this.props.metadata.can_delete !== false} can_share={this.props.metadata.can_share !== false && window.CONFIG.enable_share === true} />
 | 
						|
                  <div className="selectionOverlay"></div>
 | 
						|
                </Card>
 | 
						|
              </ToggleableLink>
 | 
						|
            </div>
 | 
						|
        )));
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export default function ToggleableLink(props)  {
 | 
						|
    const { disabled, ...rest } = props;
 | 
						|
    return disabled ? props.children : <Link {...rest}>{props.children}</Link>;
 | 
						|
}
 | 
						|
 | 
						|
class Filename extends React.Component {
 | 
						|
    constructor(props){
 | 
						|
        super(props);
 | 
						|
        this.state = {
 | 
						|
            filename: props.filename
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
    onInputFocus(e){
 | 
						|
        let value = e.target.value.split(".");
 | 
						|
        if(value.length > 1){
 | 
						|
            value.pop();
 | 
						|
        }
 | 
						|
        value = value.join(".");
 | 
						|
        e.target.setSelectionRange(0, value.length);
 | 
						|
    }
 | 
						|
 | 
						|
    onRename(e){
 | 
						|
        e.preventDefault();
 | 
						|
        e.stopPropagation();
 | 
						|
        this.props.onRename(this.state.filename);
 | 
						|
    }
 | 
						|
 | 
						|
    onCancel(){
 | 
						|
        this.setState({filename: this.props.filename});
 | 
						|
        this.props.onRenameCancel();
 | 
						|
    }
 | 
						|
 | 
						|
    preventSelect(e){
 | 
						|
        e.preventDefault();
 | 
						|
    }
 | 
						|
 | 
						|
    render(){
 | 
						|
        const [fileWithoutExtension, fileExtension] = function(filename){
 | 
						|
            const fname = filename.split(".");
 | 
						|
            if(fname.length < 2){
 | 
						|
                return [filename, ""];
 | 
						|
            }
 | 
						|
            const ext = fname.pop();
 | 
						|
            if(window.CONFIG.mime[ext] === undefined){
 | 
						|
                return [filename, ""];
 | 
						|
            }
 | 
						|
            return [fname.join("."), "." + ext];
 | 
						|
        }(this.state.filename);
 | 
						|
        return (
 | 
						|
            <span className="component_filename">
 | 
						|
              <span className="file-details">
 | 
						|
                <NgIf cond={this.props.is_renaming === false} type='inline'>
 | 
						|
                  { fileWithoutExtension }{ this.props.hide_extension ? null : <span className="extension">{fileExtension}</span> }
 | 
						|
                  <FileSize type={this.props.filetype} size={this.props.filesize} />
 | 
						|
                </NgIf>
 | 
						|
                <NgIf cond={this.props.is_renaming === true} type='inline'>
 | 
						|
                  <form onClick={this.preventSelect} onSubmit={this.onRename.bind(this)}>
 | 
						|
                    <input value={this.state.filename} onChange={(e) => this.setState({filename: e.target.value})} onBlur={this.onCancel.bind(this)} onFocus={this.onInputFocus.bind(this)} autoFocus />
 | 
						|
                  </form>
 | 
						|
                </NgIf>
 | 
						|
              </span>
 | 
						|
            </span>
 | 
						|
        );
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
const ActionButton = (props) => {
 | 
						|
    const onRename = (e) => {
 | 
						|
        e.preventDefault();
 | 
						|
        props.onClickRename();
 | 
						|
    };
 | 
						|
 | 
						|
    const onDelete = (e) => {
 | 
						|
        e.preventDefault();
 | 
						|
        props.onClickDelete();
 | 
						|
    };
 | 
						|
 | 
						|
    const onShare = (e) => {
 | 
						|
        e.preventDefault();
 | 
						|
        props.onClickShare();
 | 
						|
    };
 | 
						|
 | 
						|
    return (
 | 
						|
        <div className="component_action">
 | 
						|
          <NgIf cond={props.can_rename !== false && props.is_renaming === false} type="inline">
 | 
						|
            <Icon name="edit" onClick={onRename} className="component_updater--icon" />
 | 
						|
          </NgIf>
 | 
						|
          <NgIf cond={props.can_delete !== false} type="inline">
 | 
						|
            <Icon name="delete" onClick={onDelete} className="component_updater--icon"/>
 | 
						|
          </NgIf>
 | 
						|
          <NgIf cond={props.can_share !== false} type="inline">
 | 
						|
            <Icon name="share" onClick={onShare} className="component_updater--icon"/>
 | 
						|
          </NgIf>
 | 
						|
        </div>
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
const DateTime = (props) => {
 | 
						|
    function displayTime(timestamp){
 | 
						|
        if(timestamp){
 | 
						|
            let t = new Date(timestamp);
 | 
						|
            return t.getFullYear() + "-" + leftPad((t.getMonth() + 1).toString(), 2) + "-" + leftPad(t.getDate().toString(), 2);
 | 
						|
        }else{
 | 
						|
            return '';
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if(props.show === false){
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
 | 
						|
    return (
 | 
						|
        <span className="component_datetime">
 | 
						|
          <span>{displayTime(props.timestamp)}</span>
 | 
						|
        </span>
 | 
						|
    );
 | 
						|
};
 | 
						|
 | 
						|
const FileSize = (props) => {
 | 
						|
    function displaySize(bytes){
 | 
						|
        if(bytes === -1) return "";
 | 
						|
        if(Number.isNaN(bytes) || bytes === undefined){
 | 
						|
            return "";
 | 
						|
        }else if(bytes < 1024){
 | 
						|
            return "("+bytes+'B'+")";
 | 
						|
        }else if(bytes < 1048576){
 | 
						|
            return "("+Math.round(bytes/1024*10)/10+'KB'+")";
 | 
						|
        }else if(bytes < 1073741824){
 | 
						|
            return "("+Math.round(bytes/(1024*1024)*10)/10+'MB'+")";
 | 
						|
        }else if(bytes < 1099511627776){
 | 
						|
            return "("+Math.round(bytes/(1024*1024*1024)*10)/10+'GB'+")";
 | 
						|
        }else{
 | 
						|
            return "("+Math.round(bytes/(1024*1024*1024*1024))+'TB'+")";
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return (
 | 
						|
        <NgIf type="inline" className="component_filesize" cond={props.type === 'file'}>
 | 
						|
          <span> {displaySize(props.size)}</span>
 | 
						|
        </NgIf>
 | 
						|
    );
 | 
						|
};
 | 
						|
 | 
						|
const Message = (props) => {
 | 
						|
    return (
 | 
						|
        <NgIf cond={props.message !== null} className="component_message" type="inline">
 | 
						|
          - {props.message}
 | 
						|
        </NgIf>
 | 
						|
    );
 | 
						|
};
 | 
						|
 | 
						|
 | 
						|
class Image extends React.Component{
 | 
						|
    constructor(props){
 | 
						|
        super(props);
 | 
						|
    }
 | 
						|
 | 
						|
    render(){
 | 
						|
        if(this.props.preview && this.props.view === "grid"){
 | 
						|
            return (
 | 
						|
                <span>
 | 
						|
                  <div className="image_layer"></div>
 | 
						|
                  <LazyLoadImage scroller=".scroll-y" className="thumbnail" src={this.props.preview} />
 | 
						|
                </span>
 | 
						|
            );
 | 
						|
        }
 | 
						|
 | 
						|
        const ext = path.extname(this.props.path).replace(/^\./, "");
 | 
						|
        const img = this.props.icon === "file" ? "file" : "folder";
 | 
						|
        return (
 | 
						|
            <span>
 | 
						|
              <Icon name={this.props.icon} />
 | 
						|
              <NgIf cond={!!ext && this.props.view === "grid" && this.props.icon === "file" && this.props.hide_extension !== true} className="info_extension">
 | 
						|
                <span>{ext}</span>
 | 
						|
              </NgIf>
 | 
						|
            </span>
 | 
						|
        );
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
 | 
						|
class LazyLoadImage extends React.Component {
 | 
						|
    constructor(props){
 | 
						|
        super(props);
 | 
						|
        this.state = {
 | 
						|
            appear: false,
 | 
						|
            error: false
 | 
						|
        };
 | 
						|
        this.$scroll = document.querySelector(props.scroller);
 | 
						|
        this.onScroll = debounce(this.onScroll.bind(this), 250);
 | 
						|
    }
 | 
						|
 | 
						|
    componentDidMount(){
 | 
						|
        if(!this.$scroll){ throw("No scroll detected on LazyLoadImage"); }
 | 
						|
        this.$scroll.addEventListener("scroll", this.onScroll, {passive: true});
 | 
						|
        this.onScroll();
 | 
						|
    }
 | 
						|
    componentWillUnmount(){
 | 
						|
        this.$scroll.removeEventListener("scroll", this.onScroll);
 | 
						|
    }
 | 
						|
 | 
						|
    onScroll(){
 | 
						|
        if(!this.refs.$el) return this.componentWillUnmount();
 | 
						|
        const dim_el = this.refs.$el.getBoundingClientRect();
 | 
						|
        if(dim_el.top + dim_el.height > 0 && dim_el.top < window.innerHeight){
 | 
						|
            this.componentWillUnmount();
 | 
						|
            memory.set(this.props.src, true);
 | 
						|
            this.setState({appear: true});
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onError(){
 | 
						|
        this.setState({error: true});
 | 
						|
    }
 | 
						|
 | 
						|
    render(){
 | 
						|
        if((this.props.preview || memory.get(this.props.src) === null) || this.state.error === true){
 | 
						|
            return (
 | 
						|
                <img ref="$el" className={this.props.className} src={img_placeholder} />
 | 
						|
            );
 | 
						|
        }
 | 
						|
        return (
 | 
						|
            <img onError={this.onError.bind(this)} className={this.props.className} src={this.props.src} />
 | 
						|
        );
 | 
						|
    }
 | 
						|
}
 |