import React, { createRef } from "react"; import path from "path"; import { Link } from "react-router-dom"; import { DragSource, DropTarget } from "react-dnd"; import "./thing.scss"; import { Card, NgIf, Icon, EventEmitter, img_placeholder, Input } from "../../components/"; import { pathBuilder, basename, filetype, prompt, alert, leftPad, getMimeType, debounce, memory } from "../../helpers/"; import { Files } from "../../model/"; import { ShareComponent } from "./share"; import { TagComponent } from "./tag"; import { t } from "../../locales/"; const canDrop = (props, monitor) => { const 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; }; const HOCDropTargetForFsFile = (Cmp) => { const nativeFileTarget = { canDrop, drop(props, monitor) { const path = pathBuilder(props.path, props.file.name, "directory"); props.emit( "file.upload", path, monitor.getItem(), ); }, }; return DropTarget( "__NATIVE_FILE__", nativeFileTarget, (connect, monitor) => ({ connectDropNativeFile: connect.dropTarget(), nativeFileIsOver: monitor.isOver(), canDropNativeFile: monitor.canDrop(), }), )(Cmp); }; const HOVDropTargetForVirtualFile = (Cmp) => { const fileTarget = { canDrop, drop(props, monitor, component) { const src = monitor.getItem(); const 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]; }), }; } }, }; return DropTarget( "file", fileTarget, (connect, monitor) => ({ connectDropFile: connect.dropTarget(), fileIsOver: monitor.isOver(), canDropFile: monitor.canDrop(), }), )(Cmp); }; const HOVDropSourceForVirtualFile = (Cmp) => { 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") { const 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 new Error("unknown action"); } } }, }; return DragSource( "file", fileSource, (connect, monitor) => ({ connectDragSource: connect.dragSource(), isDragging: monitor.isDragging(), }), )(Cmp); }; class ExistingThingComponent 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); } UNSAFE_componentWillReceiveProps(props) { if (props.view !== this.props.view) { this.updateThumbnail(props); } } updateThumbnail(props) { if (props.view === "grid" && props.icon !== "loading") { const mimetype = getMimeType(props.file.path); if (window.CONFIG.thumbnailer.indexOf(mimetype) !== -1) { 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 }); } onTagRequest() { alert.now( , () => {}, ) } onShareRequest(filename) { alert.now( , (ok) => {}, ); } onThingClick(e) { if (e.ctrlKey === true || e.target.classList.contains("component_checkbox")) { e.preventDefault(); this.props.emit( "file.select", pathBuilder(this.props.path, this.props.file.name, this.props.file.type), ); } } onThingClickCheckbox(e) { 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(); const 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"); const onClickCheckbox = (e, scaleNumber) => { const $box = e.target.parentElement.parentElement.parentElement; if ($box.classList.contains("view-grid") === false) return; $box.style.transform = `scale(${scaleNumber})`; }; return connectDragSource(connectDropNativeFile(connectDropFile(
onClickCheckbox(e, 1)} onMouseDown={(e) => onClickCheckbox(e, 0.95)}/>
, ))); } } export const ExistingThing = EventEmitter( HOCDropTargetForFsFile( HOVDropTargetForVirtualFile( HOVDropSourceForVirtualFile( ExistingThingComponent, ), ), ), ); export default function ToggleableLink(props) { const { disabled, ...rest } = props; return disabled ? props.children : {props.children}; } 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); return false; } onCancel() { this.setState({ filename: this.props.filename }); this.props.onRenameCancel(); } preventSelect(e) { e.preventDefault(); e.stopPropagation(); } 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 ( { fileWithoutExtension }{ this.props.hide_extension ? null : {fileExtension} }
{ e.preventDefault(); e.stopPropagation(); this.onRename(e) }}> this.setState({ filename: e.target.value })} onBlur={this.onCancel.bind(this)} onFocus={this.onInputFocus.bind(this)} autoFocus />
); } } const ActionButton = (props) => { const onRename = (e) => { e.preventDefault(); props.onClickRename(); }; const onDelete = (e) => { e.preventDefault(); props.onClickDelete(); }; const onShare = (e) => { e.preventDefault(); props.onClickShare(); }; const onTag = (e) => { e.preventDefault(); props.onClickTag(); } return (
{ /canary/.test(location.search) ? ( ) : ( ) }
); }; const DateTime = (props) => { function displayTime(timestamp) { if (!timestamp || timestamp < 0) { return ""; } const t = new Date(timestamp); if("DateTimeFormat" in Intl) { const str = new Intl.DateTimeFormat({ dateStyle: "short" }).format(t); if (str.length <= 10) return str; } return t.getFullYear() + "-" + leftPad((t.getMonth() + 1).toString(), 2) + "-" + leftPad(t.getDate().toString(), 2); } if (props.show === false) { return null; } return ( {displayTime(props.timestamp)} ); }; const FileSize = (props) => { function displaySize(bytes) { if (Number.isNaN(bytes) || bytes < 0 || 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 ( {displaySize(props.size)} ); }; class Image extends React.Component { constructor(props) { super(props); } render() { if (this.props.preview && this.props.view === "grid") { return (
); } const ext = path.extname(this.props.path).replace(/^\./, ""); return ( {ext} ); } }; 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); this.$el = createRef(); } componentDidMount() { if (!this.$scroll) throw new Error("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.$el.current) return this.componentWillUnmount(); const dim_el = this.$el.current.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 ( ); } return ( ); } }