Feat: FileManager (#98) (#703)

* feat: add file manager module

- Complete file manager implementation with UI/UX
- Add drive management functionality
- Add file upload/download with progress tracking
- Add stamp integration and handling
- Add bulk operations and context menus

Co-authored-by: Roland Seres <roland.seres90@gmail.com>
Co-authored-by: nidishk <nidishkrishnan45@gmail.com>
This commit is contained in:
Bálint Ujvári
2025-11-12 11:26:00 +01:00
committed by GitHub
parent 1249c0df71
commit 5bfe2a0331
107 changed files with 21529 additions and 5578 deletions
@@ -0,0 +1,97 @@
.fm-rename-modal {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(10, 10, 10, 0.55);
backdrop-filter: blur(2px);
}
.fm-rename-modal .fm-modal-window {
.fm-modal-window-header {
display: flex;
align-items: center;
gap: 8px;
svg {
opacity: 0.9;
}
}
.fm-modal-window-body {
.fm-modal-white-section {
display: flex;
flex-direction: column;
gap: 10px;
}
}
.fm-input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--fm-border-color, rgba(255, 255, 255, 0.08));
background: var(--fm-input-bg, rgba(255, 255, 255, 0.04));
color: var(--fm-text-color, #fff);
border-radius: 8px;
outline: none;
transition: border-color 120ms ease, background-color 120ms ease;
&:focus {
border-color: var(--fm-accent, #6aa7ff);
background: var(--fm-input-bg-focus, rgba(255, 255, 255, 0.06));
}
&::placeholder {
color: var(--fm-placeholder, rgba(255, 255, 255, 0.5));
}
}
.fm-rename-input {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.16);
color: #fff;
&:focus {
border-color: var(--fm-accent, #6aa7ff);
background: rgba(255, 255, 255, 0.12);
}
&::placeholder {
color: rgba(255, 255, 255, 0.6);
}
}
.fm-soft-text {
line-height: 1.3;
}
.fm-error-text {
color: var(--fm-error, #ff6b6b);
font-size: 12px;
line-height: 1.25;
}
.fm-modal-window-footer {
display: flex;
justify-content: space-between;
align-items: center;
.fm-button + .fm-button {
margin-left: 8px;
}
}
}
.fm-rename-modal .fm-modal-white-section input#fm-rename-input.fm-input {
background: #fff !important;
border: 1px solid #e5e7eb !important;
color: #111 !important;
-webkit-text-fill-color: #111 !important;
caret-color: #111 !important;
}
.fm-rename-modal .fm-modal-white-section input#fm-rename-input.fm-input::placeholder {
color: rgba(0, 0, 0, 0.5) !important;
}
@@ -0,0 +1,143 @@
import { ReactElement, useEffect, useMemo, useRef, useState } from 'react'
import '../../styles/global.scss'
import './RenameFileModal.scss'
import { Button } from '../Button/Button'
import EditIcon from 'remixicon-react/EditLineIcon'
import { createPortal } from 'react-dom'
import { safeSetState } from '../../utils/common'
interface RenameFileModalProps {
currentName: string
takenNames?: Set<string> | string[]
onCancelClick: () => void
onProceed: (newName: string) => void | Promise<void>
}
export function RenameFileModal({
currentName,
takenNames,
onCancelClick,
onProceed,
}: RenameFileModalProps): ReactElement {
const [value, setValue] = useState(currentName)
const [touched, setTouched] = useState(false)
const [submitting, setSubmitting] = useState(false)
const inputRef = useRef<HTMLInputElement | null>(null)
const isMountedRef = useRef(true)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
useEffect(() => {
const t = setTimeout(() => inputRef.current?.focus(), 0)
return () => clearTimeout(t)
}, [])
const taken = useMemo(() => {
if (!takenNames) return new Set<string>()
return Array.isArray(takenNames) ? new Set(takenNames) : takenNames
}, [takenNames])
const trimmed = useMemo(() => value.trim(), [value])
const error = useMemo(() => {
if (!touched) return ''
if (!trimmed) return 'Name is required.'
if (trimmed === currentName) return 'Enter a different name.'
if (/[\\/:*?"<>|]+/.test(trimmed)) return 'Name contains invalid characters.'
if (taken.has(trimmed)) return 'A different file already uses this name. Please choose another.'
return ''
}, [touched, trimmed, currentName, taken])
const canSubmit =
trimmed.length > 0 && trimmed !== currentName && !/[\\/:*?"<>|]+/.test(trimmed) && !taken.has(trimmed)
const handleSubmit = async () => {
if (!canSubmit || submitting) {
setTouched(true)
return
}
try {
setSubmitting(true)
await onProceed(trimmed)
} finally {
safeSetState(isMountedRef, setSubmitting)(false)
}
}
const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = e => {
if (e.key === 'Enter') {
e.preventDefault()
void handleSubmit()
} else if (e.key === 'Escape') {
e.preventDefault()
onCancelClick()
}
}
const modalRoot = (document.querySelector('.fm-main') as HTMLElement) || document.body
return createPortal(
<div className="fm-modal-container fm-rename-modal">
<div className="fm-modal-window">
<div className="fm-modal-window-header">
<EditIcon size="21px" />
<span className="fm-main-font-color">Rename file</span>
</div>
<div className="fm-modal-window-body">
<div className="fm-modal-white-section">
<label htmlFor="fm-rename-input" className="fm-soft-text" style={{ display: 'block', marginBottom: 8 }}>
New name
</label>
<input
id="fm-rename-input"
ref={inputRef}
type="text"
className="fm-input fm-rename-input"
value={value}
onChange={e => setValue(e.target.value)}
onBlur={() => setTouched(true)}
onKeyDown={onKeyDown}
placeholder="Enter a new file name"
/>
{error && (
<div className="fm-error-text" style={{ marginTop: 8 }}>
{error}
</div>
)}
<div className="fm-soft-text" style={{ marginTop: 10, fontSize: 12 }}>
This creates a new version that only changes metadata (no re-upload).
</div>
</div>
</div>
<div className="fm-modal-window-footer fm-space-between">
<Button label="Cancel" variant="secondary" onClick={onCancelClick} />
<Button
label="Rename"
variant="primary"
onClick={() => void handleSubmit()}
disabled={!canSubmit || submitting}
/>
</div>
</div>
</div>,
modalRoot,
)
}