Compare commits
6 Commits
13e38737fb
...
61387e9fd8
| Author | SHA1 | Date | |
|---|---|---|---|
| 61387e9fd8 | |||
| 1354383860 | |||
| 6f25be49ca | |||
| 5f4da5d13f | |||
| 24e0bcbf92 | |||
| 5e7169079c |
4
src/assets/spinner.svg
Normal file
4
src/assets/spinner.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg class="animate-spin h-6 w-6 dark:text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 400 B |
@@ -4,7 +4,6 @@ import genDownloadLink from "../functions/genDownloadLink"
|
||||
import { FileOpenerProps } from "../types/FileOpenerProps"
|
||||
|
||||
const AudioOpener: React.FC<FileOpenerProps> = (props) => {
|
||||
|
||||
const audio = React.createRef<HTMLAudioElement>()
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { ReactComponent as BreadcrumImage } from "./../assets/breadcrum.svg"
|
||||
|
||||
interface Props{
|
||||
@@ -19,6 +18,7 @@ const Breadcrum: React.FC<Props> = (props) => {
|
||||
Root
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{parts.map((e,i,arr)=>{
|
||||
const last = i == arr.length - 1
|
||||
return <div key={e} className="inline-flex items-center cursor-pointer">
|
||||
@@ -39,9 +39,4 @@ const Breadcrum: React.FC<Props> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
Breadcrum.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
onDirClick: PropTypes.func
|
||||
}
|
||||
|
||||
export default Breadcrum
|
||||
|
||||
@@ -8,7 +8,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const CreateDirButton: React.FC<Props> = (props) => {
|
||||
|
||||
const [name,setName] = useState("")
|
||||
const [show,setShow] = useState(false)
|
||||
const input = useRef<HTMLInputElement>(null)
|
||||
@@ -37,6 +36,7 @@ const CreateDirButton: React.FC<Props> = (props) => {
|
||||
setName("")
|
||||
setShow(false)
|
||||
}}>
|
||||
|
||||
<input
|
||||
className="bg-transparent dark:text-gray-300 outline-none mx-1 border-b"
|
||||
type="text"
|
||||
@@ -48,15 +48,6 @@ const CreateDirButton: React.FC<Props> = (props) => {
|
||||
setName("")
|
||||
}}
|
||||
/>
|
||||
{/* <button
|
||||
onClick={()=>{
|
||||
props.onPressed?.(name)
|
||||
setName("")
|
||||
setShow(false)
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</button> */}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Directory } from "../generated/graphql"
|
||||
import { MdFolderOpen } from "react-icons/md"
|
||||
|
||||
@@ -21,8 +20,4 @@ const DirectoryElement: React.FC<Props> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
DirectoryElement.propTypes = {
|
||||
dir: PropTypes.any.isRequired // TODO: maybe you can use the interface
|
||||
}
|
||||
|
||||
export default DirectoryElement
|
||||
@@ -45,7 +45,6 @@ const DragAndDrop: React.FC<Props> = (props) => {
|
||||
props.onDrop?.()
|
||||
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
|
||||
props.handleDrop?.(event.dataTransfer.files)
|
||||
// event.dataTransfer.clearData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ import Breadcrum from "./Breadcrum"
|
||||
import CreateDirButton from "./CreateDirButton"
|
||||
import DragAndDrop from "./DragAndDrop"
|
||||
import FileBrowserContextMenu, { Action, CONTEXT_MENU_DIR, CONTEXT_MENU_FILE } from "./FileBrowserContextMenu"
|
||||
import FileBrowserElement from "./FileBrowserElement"
|
||||
import FileOpen from "./FileOpen"
|
||||
import FileUploadButton from "./FileUploadButton"
|
||||
import { ReactComponent as Spinner } from "./../assets/spinner.svg"
|
||||
import FileBrowserList from "./FileBrowserList"
|
||||
|
||||
function uriToPath(pathname:string) {
|
||||
// strip the "/f" from e.g. "/f/dir1/dir2"
|
||||
@@ -32,6 +33,8 @@ const FileBrowser: React.FC<RouteComponentProps> = (props) => {
|
||||
|
||||
const [srcID,setSrcID] = useState("")
|
||||
const [pasteAction,setPasteAction] = useState<Action>()
|
||||
const [editID,setEditID] = useState("")
|
||||
const [editEnable,setEditEnable] = useState(false)
|
||||
|
||||
const [deleteMutation] = useDeleteFileMutation()
|
||||
const [copyMutation] = useCopyMutation()
|
||||
@@ -64,24 +67,6 @@ const FileBrowser: React.FC<RouteComponentProps> = (props) => {
|
||||
refetchDir()
|
||||
}
|
||||
|
||||
function openFileContextMenu(e: React.MouseEvent, id: string) {
|
||||
e.preventDefault()
|
||||
showFileContext(e,{
|
||||
props:{
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openDirContextMenu(e: React.MouseEvent, id: string) {
|
||||
e.preventDefault()
|
||||
showDirContext(e,{
|
||||
props:{
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onContextSelect(action:Action, id: string) {
|
||||
switch (action) {
|
||||
case Action.FileDelete:
|
||||
@@ -111,6 +96,10 @@ const FileBrowser: React.FC<RouteComponentProps> = (props) => {
|
||||
case Action.FileDownload:
|
||||
downloadFile(genDownloadLink(id))
|
||||
break
|
||||
case Action.FileRename:
|
||||
setEditID(id)
|
||||
setEditEnable(true)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -147,49 +136,51 @@ const FileBrowser: React.FC<RouteComponentProps> = (props) => {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FileBrowserList
|
||||
directorys={data?.directorys || []}
|
||||
files={data?.files || []}
|
||||
|
||||
onDirClick={(e,path)=>{
|
||||
props.history.push(pathToUri(path))
|
||||
}}
|
||||
|
||||
onDirContext={(e,path)=>{
|
||||
e.preventDefault()
|
||||
showDirContext(e,{props:{id:path}})
|
||||
}}
|
||||
|
||||
onFileClick={(e,id)=>{
|
||||
setOpenFileId(id)
|
||||
setShowFile(true)
|
||||
}}
|
||||
|
||||
onFileContext={(e,id)=>{
|
||||
e.preventDefault()
|
||||
showFileContext(e,{props:{id}})
|
||||
}}
|
||||
|
||||
editId={editID}
|
||||
editEnable={editEnable}
|
||||
|
||||
onRenameDone={(id,changed,newName)=>{
|
||||
setEditEnable(false)
|
||||
if (changed){
|
||||
console.debug("Changed: ",newName)
|
||||
}else{
|
||||
console.debug("Not changed")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{loading &&
|
||||
<div>Loading...</div> // TODO: center
|
||||
<div className="flex justify-center mt-4">
|
||||
<Spinner />
|
||||
</div>
|
||||
}
|
||||
<table className="w-full">
|
||||
<thead className="border-b-2 dark:border-gray-900">
|
||||
<tr>
|
||||
<th className="text-left">Name</th>
|
||||
<th className="text-left">Last Modified</th>
|
||||
<th className="text-left">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y dark:divide-gray-900">
|
||||
|
||||
{ data?.directorys.map(v => (<FileBrowserElement
|
||||
key={v?.id}
|
||||
dir={v}
|
||||
onClick={(dir)=>{
|
||||
props.history.push(pathToUri(dir.id))
|
||||
}}
|
||||
onContextMenu={(e)=>{
|
||||
openDirContextMenu(e,v.id)
|
||||
}}
|
||||
/>))}
|
||||
|
||||
{ data?.files.map(v => (<FileBrowserElement
|
||||
key={v?.id}
|
||||
file={v}
|
||||
onClick={(file)=>{
|
||||
setOpenFileId(file.id)
|
||||
setShowFile(true)
|
||||
}}
|
||||
onContextMenu={(e)=>{
|
||||
openFileContextMenu(e,v.id)
|
||||
}}
|
||||
/>))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{<FileOpen id={openFileId} show={showFile} onCloseClick={()=>{
|
||||
<FileOpen id={openFileId} show={showFile} onCloseClick={()=>{
|
||||
setShowFile(false)
|
||||
}} />}
|
||||
}} />
|
||||
</DragAndDrop>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ export enum Action {
|
||||
FilePaste,
|
||||
FileMove,
|
||||
FileDownload,
|
||||
FileRename,
|
||||
DirDelete
|
||||
}
|
||||
|
||||
@@ -20,7 +21,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const FileBrowserContextMenu: React.FC<Props> = (props) => {
|
||||
|
||||
function onClick({ props: itemProps, data }: ItemParams<{id:string}, Action>) {
|
||||
if (itemProps?.id && data != null){
|
||||
props.onSelect?.(data,itemProps.id)
|
||||
@@ -33,6 +33,7 @@ const FileBrowserContextMenu: React.FC<Props> = (props) => {
|
||||
<Item onClick={onClick} data={Action.FileDelete} >Delete</Item>
|
||||
<Item onClick={onClick} data={Action.FileCopy} >Copy</Item>
|
||||
<Item onClick={onClick} data={Action.FileMove} >Move</Item>
|
||||
<Item onClick={onClick} data={Action.FileRename} >Rename</Item>
|
||||
<Item onClick={onClick} data={Action.FileDownload} >Download</Item>
|
||||
<Separator />
|
||||
<Item onClick={onClick} data={Action.FilePaste} disabled={!props.pasteActive}>Paste</Item>
|
||||
|
||||
@@ -1,40 +1,43 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Directory, File } from "../generated/graphql"
|
||||
import DirectoryComponent from "./DirectoryElement"
|
||||
import FileElement from "./FileElement"
|
||||
|
||||
interface Props {
|
||||
file?: File | null
|
||||
dir?: Directory | null
|
||||
onClick?: (data: File | Directory) => void
|
||||
file?: File
|
||||
dir?: Directory
|
||||
onClick?: (event: React.MouseEvent ,data: File | Directory) => void
|
||||
onContextMenu?: (e:React.MouseEvent) => void
|
||||
edit: boolean
|
||||
onRename?: (newName: string)=>void
|
||||
onCancleRename?: ()=>void
|
||||
}
|
||||
|
||||
const FileBrowserElement: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<tr
|
||||
className="hover:bg-gray-100 dark:hover:bg-gray-900 text-lg"
|
||||
onClick={()=>{
|
||||
onClick={(e)=>{
|
||||
if(props.file){
|
||||
props.onClick?.(props.file)
|
||||
props.onClick?.(e,props.file)
|
||||
}else if(props.dir){
|
||||
props.onClick?.(props.dir)
|
||||
props.onClick?.(e,props.dir)
|
||||
}
|
||||
}}
|
||||
|
||||
onContextMenu={(e)=>props.onContextMenu?.(e)}
|
||||
|
||||
>
|
||||
{(props.file) ? <FileElement file={props.file}/>:(props.dir)?<DirectoryComponent dir={props.dir} />:<></>}
|
||||
{(props.file) ? <FileElement
|
||||
edit={props.edit}
|
||||
file={props.file}
|
||||
onCancleRename={props.onCancleRename}
|
||||
onRename={props.onRename}
|
||||
/>:(props.dir)?<DirectoryComponent
|
||||
dir={props.dir}
|
||||
/>:<></>}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
FileBrowserElement.propTypes = {
|
||||
dir: PropTypes.any,
|
||||
file: PropTypes.any,
|
||||
onClick: PropTypes.func
|
||||
}
|
||||
|
||||
export default FileBrowserElement
|
||||
80
src/components/FileBrowserList.tsx
Normal file
80
src/components/FileBrowserList.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from "react"
|
||||
import { Directory, File } from "../generated/graphql"
|
||||
import FileBrowserElement from "./FileBrowserElement"
|
||||
|
||||
interface Props{
|
||||
directorys: Directory[]
|
||||
files: File[]
|
||||
|
||||
onFileContext?: (event: React.MouseEvent, id: string)=>void
|
||||
onDirContext?: (event: React.MouseEvent, path: string)=>void
|
||||
onFileClick?: (event: React.MouseEvent,id: string)=>void
|
||||
onDirClick?: (event: React.MouseEvent,path: string)=>void
|
||||
|
||||
editId: string
|
||||
editEnable: boolean
|
||||
onRenameDone?: (id: string, changed: boolean, newName: string)=>void
|
||||
}
|
||||
|
||||
const FileBrowserList: React.FC<Props> = (props) => {
|
||||
return <>
|
||||
<table className="w-full">
|
||||
<thead className="border-b-2 dark:border-gray-900">
|
||||
<tr>
|
||||
<th className="text-left">Name</th>
|
||||
<th className="text-left">Last Modified</th>
|
||||
<th className="text-left">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y dark:divide-gray-900">
|
||||
{ props.directorys.map(v => (<FileBrowserElement
|
||||
key={v.id}
|
||||
dir={v}
|
||||
onClick={(e,dir)=>{
|
||||
props.onDirClick?.(e,dir.id)
|
||||
}}
|
||||
onContextMenu={(e)=>{
|
||||
props.onDirContext?.(e,v.id)
|
||||
}}
|
||||
|
||||
edit={props.editEnable && (v.id === props.editId)}
|
||||
|
||||
onRename={(newName)=>{
|
||||
if (v.name != newName){
|
||||
props.onRenameDone?.(v.id,true,newName)
|
||||
}else{
|
||||
props.onRenameDone?.(v.id,false,newName)
|
||||
}
|
||||
}}
|
||||
|
||||
onCancleRename={()=>props.onRenameDone?.(v.id,false,"")}
|
||||
/>))}
|
||||
|
||||
{ props.files.map(v => (<FileBrowserElement
|
||||
key={v.id}
|
||||
file={v}
|
||||
onClick={(e,file)=>{
|
||||
props.onFileClick?.(e,file.id)
|
||||
}}
|
||||
onContextMenu={(e)=>{
|
||||
props.onFileContext?.(e,v.id)
|
||||
}}
|
||||
|
||||
edit={props.editEnable && (v.id === props.editId)}
|
||||
|
||||
onRename={(newName)=>{
|
||||
if (v.name != newName){
|
||||
props.onRenameDone?.(v.id,true,newName)
|
||||
}else{
|
||||
props.onRenameDone?.(v.id,false,newName)
|
||||
}
|
||||
}}
|
||||
|
||||
onCancleRename={()=>props.onRenameDone?.(v.id,false,"")}
|
||||
/>))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
}
|
||||
|
||||
export default FileBrowserList
|
||||
@@ -1,19 +1,28 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { File } from "../generated/graphql"
|
||||
import sizeToReadable from "../functions/sizeToReadable"
|
||||
import dateFormat from "../functions/dateFomat"
|
||||
import { FaRegFileAlt } from "react-icons/fa"
|
||||
import Renameable from "./Renameable"
|
||||
|
||||
interface Props {
|
||||
file: File
|
||||
file: File,
|
||||
edit: boolean
|
||||
onRename?: (newName: string)=>void
|
||||
onCancleRename?: ()=>void
|
||||
}
|
||||
|
||||
const FileElement: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<td>
|
||||
<FaRegFileAlt className="inline" /> {props.file.name}
|
||||
<FaRegFileAlt className="inline" />
|
||||
<Renameable
|
||||
text={props.file.name || ""}
|
||||
edit={props.edit}
|
||||
onCancleRename={props.onCancleRename}
|
||||
onRename={props.onRename}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{dateFormat(props.file.lastModified)}
|
||||
@@ -26,8 +35,4 @@ const FileElement: React.FC<Props> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
FileElement.propTypes = {
|
||||
file: PropTypes.any.isRequired
|
||||
}
|
||||
|
||||
export default FileElement
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { useGetFileQuery } from "../generated/graphql"
|
||||
import ImageOpener from "./ImageOpener"
|
||||
import TextOpener from "./TextOpener"
|
||||
@@ -13,7 +12,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const FileOpen: React.FC<Props> = (props) => {
|
||||
|
||||
if (!props.id) {
|
||||
return <></>
|
||||
}
|
||||
@@ -66,10 +64,4 @@ const FileOpen: React.FC<Props> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
FileOpen.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
onCloseClick: PropTypes.func
|
||||
}
|
||||
|
||||
export default FileOpen
|
||||
|
||||
@@ -7,7 +7,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const FileUploadButton: React.FC<Props> = (props) => {
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import genDownloadLink from "../functions/genDownloadLink"
|
||||
import { FileOpenerProps } from "../types/FileOpenerProps"
|
||||
|
||||
@@ -9,8 +8,4 @@ const ImageOpener: React.FC<FileOpenerProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
ImageOpener.propTypes = {
|
||||
file: PropTypes.any.isRequired
|
||||
}
|
||||
|
||||
export default ImageOpener
|
||||
|
||||
51
src/components/Renameable.tsx
Normal file
51
src/components/Renameable.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from "react"
|
||||
import { useEffect } from "react"
|
||||
import { useRef } from "react"
|
||||
import { useState } from "react"
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
edit: boolean
|
||||
onRename?: (newName: string)=>void
|
||||
onCancleRename?: ()=>void
|
||||
}
|
||||
|
||||
const Renameable: React.FC<Props> = (props) => {
|
||||
const [inputValue,setinputValue] = useState(props.text)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(()=>{
|
||||
if (props.edit){
|
||||
const i = inputValue.lastIndexOf(".")
|
||||
inputRef.current?.select()
|
||||
inputRef.current?.setSelectionRange(0,i == -1?inputValue.length:i)
|
||||
}else{
|
||||
setinputValue(props.text)
|
||||
}
|
||||
},[props.edit])
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.edit && <form className="inline" onSubmit={(e)=>{
|
||||
e.preventDefault()
|
||||
props.onRename?.(inputValue)
|
||||
}}>
|
||||
<input
|
||||
className="bg-transparent dark:text-gray-300 outline-none inline"
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onClick={(e)=>e.stopPropagation()}
|
||||
onChange={(e)=>{setinputValue(e.target.value)}}
|
||||
onBlur={props.onCancleRename}
|
||||
/></form>
|
||||
|
||||
}
|
||||
{!props.edit &&
|
||||
<div className="inline">{props.text}</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Renameable
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { FileOpenerProps } from "../types/FileOpenerProps"
|
||||
import { useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
@@ -23,8 +22,4 @@ const TextOpener: React.FC<FileOpenerProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
TextOpener.propTypes = {
|
||||
file: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
export default TextOpener
|
||||
|
||||
Reference in New Issue
Block a user