Compare commits
No commits in common. "e86f4148f1cbd3718f6e381d38ac5ab4122a74f4" and "4c51d45708a34da6f24b19aa81dd62b817480374" have entirely different histories.
e86f4148f1
...
4c51d45708
|
|
@ -80,7 +80,6 @@ class CreateUploadRequest(BaseModel):
|
||||||
original_filename: str = Field(max_length=512)
|
original_filename: str = Field(max_length=512)
|
||||||
content_type: str = Field(max_length=100)
|
content_type: str = Field(max_length=100)
|
||||||
size_bytes: int = Field(gt=0, le=21474836480) # Max 20GB
|
size_bytes: int = Field(gt=0, le=21474836480) # Max 20GB
|
||||||
folder_id: Optional[str] = Field(None, max_length=36)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateUploadResponse(BaseModel):
|
class CreateUploadResponse(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ async def create_upload(
|
||||||
original_filename=data.original_filename,
|
original_filename=data.original_filename,
|
||||||
content_type=data.content_type,
|
content_type=data.content_type,
|
||||||
size_bytes=data.size_bytes,
|
size_bytes=data.size_bytes,
|
||||||
folder_id=data.folder_id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return CreateUploadResponse(
|
return CreateUploadResponse(
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ class AssetService:
|
||||||
original_filename: str,
|
original_filename: str,
|
||||||
content_type: str,
|
content_type: str,
|
||||||
size_bytes: int,
|
size_bytes: int,
|
||||||
folder_id: Optional[str] = None,
|
|
||||||
) -> tuple[Asset, dict]:
|
) -> tuple[Asset, dict]:
|
||||||
"""
|
"""
|
||||||
Create an asset and generate pre-signed upload URL.
|
Create an asset and generate pre-signed upload URL.
|
||||||
|
|
@ -60,7 +59,6 @@ class AssetService:
|
||||||
original_filename: Original filename
|
original_filename: Original filename
|
||||||
content_type: MIME type
|
content_type: MIME type
|
||||||
size_bytes: File size in bytes
|
size_bytes: File size in bytes
|
||||||
folder_id: Optional folder ID to upload to
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (asset, presigned_post_data)
|
Tuple of (asset, presigned_post_data)
|
||||||
|
|
@ -76,7 +74,6 @@ class AssetService:
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
size_bytes=size_bytes,
|
size_bytes=size_bytes,
|
||||||
storage_key_original="", # Will be set after upload
|
storage_key_original="", # Will be set after upload
|
||||||
folder_id=folder_id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate storage key
|
# Generate storage key
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="Облачное хранилище фото и видео" />
|
<meta name="description" content="Облачное хранилище фото и видео" />
|
||||||
<title>ITCloud - Облачное хранилище</title>
|
<title>ITCloud - Облачное хранилище</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -11,112 +11,32 @@ import {
|
||||||
ListItemText,
|
ListItemText,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Typography,
|
Typography,
|
||||||
Collapse,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import { Folder as FolderIcon, Home as HomeIcon } from '@mui/icons-material';
|
||||||
Folder as FolderIcon,
|
|
||||||
Home as HomeIcon,
|
|
||||||
ExpandMore as ExpandMoreIcon,
|
|
||||||
ChevronRight as ChevronRightIcon,
|
|
||||||
FolderOpen as FolderOpenIcon,
|
|
||||||
} from '@mui/icons-material';
|
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
interface Folder {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
parent_folder_id: string | null;
|
|
||||||
children?: Folder[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MoveFolderDialogProps {
|
interface MoveFolderDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onMove: (folderId: string | null) => void;
|
onMove: (folderId: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FolderTreeItemProps {
|
|
||||||
folder: Folder;
|
|
||||||
level: number;
|
|
||||||
selectedFolder: string | null;
|
|
||||||
expanded: Set<string>;
|
|
||||||
onSelect: (folderId: string) => void;
|
|
||||||
onToggle: (folderId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FolderTreeItem({ folder, level, selectedFolder, expanded, onSelect, onToggle }: FolderTreeItemProps) {
|
|
||||||
const hasChildren = folder.children && folder.children.length > 0;
|
|
||||||
const isExpanded = expanded.has(folder.id);
|
|
||||||
const isSelected = selectedFolder === folder.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ListItemButton
|
|
||||||
selected={isSelected}
|
|
||||||
onClick={() => onSelect(folder.id)}
|
|
||||||
sx={{ pl: 2 + level * 3 }}
|
|
||||||
>
|
|
||||||
<ListItemIcon onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (hasChildren) {
|
|
||||||
onToggle(folder.id);
|
|
||||||
}
|
|
||||||
}} sx={{ minWidth: 36, cursor: hasChildren ? 'pointer' : 'default' }}>
|
|
||||||
{hasChildren ? (
|
|
||||||
isExpanded ? <ExpandMoreIcon /> : <ChevronRightIcon />
|
|
||||||
) : (
|
|
||||||
<span style={{ width: 24, display: 'inline-block' }} />
|
|
||||||
)}
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemIcon sx={{ minWidth: 40 }}>
|
|
||||||
{isExpanded && hasChildren ? <FolderOpenIcon /> : <FolderIcon />}
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={folder.name} />
|
|
||||||
</ListItemButton>
|
|
||||||
|
|
||||||
{hasChildren && (
|
|
||||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
|
||||||
<List component="div" disablePadding>
|
|
||||||
{folder.children!.map((child) => (
|
|
||||||
<FolderTreeItem
|
|
||||||
key={child.id}
|
|
||||||
folder={child}
|
|
||||||
level={level + 1}
|
|
||||||
selectedFolder={selectedFolder}
|
|
||||||
expanded={expanded}
|
|
||||||
onSelect={onSelect}
|
|
||||||
onToggle={onToggle}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Collapse>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MoveFolderDialog({ open, onClose, onMove }: MoveFolderDialogProps) {
|
export default function MoveFolderDialog({ open, onClose, onMove }: MoveFolderDialogProps) {
|
||||||
const [folders, setFolders] = useState<Folder[]>([]);
|
const [folders, setFolders] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
|
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
|
||||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
loadAllFolders();
|
loadFolders();
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const loadAllFolders = async () => {
|
const loadFolders = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// Load all folders (no parent_id filter to get all)
|
|
||||||
const response = await api.listFolders();
|
const response = await api.listFolders();
|
||||||
const allFolders: Folder[] = response.items || [];
|
setFolders(response.items || []);
|
||||||
|
|
||||||
// Build tree structure
|
|
||||||
const tree = buildTree(allFolders);
|
|
||||||
setFolders(tree);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load folders:', error);
|
console.error('Failed to load folders:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -124,56 +44,14 @@ export default function MoveFolderDialog({ open, onClose, onMove }: MoveFolderDi
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildTree = (flatFolders: Folder[]): Folder[] => {
|
|
||||||
const folderMap = new Map<string, Folder>();
|
|
||||||
const roots: Folder[] = [];
|
|
||||||
|
|
||||||
// Create map and initialize children array
|
|
||||||
flatFolders.forEach((folder) => {
|
|
||||||
folderMap.set(folder.id, { ...folder, children: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build tree
|
|
||||||
flatFolders.forEach((folder) => {
|
|
||||||
const node = folderMap.get(folder.id)!;
|
|
||||||
if (folder.parent_folder_id === null) {
|
|
||||||
roots.push(node);
|
|
||||||
} else {
|
|
||||||
const parent = folderMap.get(folder.parent_folder_id);
|
|
||||||
if (parent) {
|
|
||||||
parent.children!.push(node);
|
|
||||||
} else {
|
|
||||||
// Parent not found, treat as root
|
|
||||||
roots.push(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return roots;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggle = (folderId: string) => {
|
|
||||||
setExpanded((prev) => {
|
|
||||||
const newExpanded = new Set(prev);
|
|
||||||
if (newExpanded.has(folderId)) {
|
|
||||||
newExpanded.delete(folderId);
|
|
||||||
} else {
|
|
||||||
newExpanded.add(folderId);
|
|
||||||
}
|
|
||||||
return newExpanded;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMove = () => {
|
const handleMove = () => {
|
||||||
onMove(selectedFolder);
|
onMove(selectedFolder);
|
||||||
setSelectedFolder(null);
|
setSelectedFolder(null);
|
||||||
setExpanded(new Set());
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setSelectedFolder(null);
|
setSelectedFolder(null);
|
||||||
setExpanded(new Set());
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -189,7 +67,7 @@ export default function MoveFolderDialog({ open, onClose, onMove }: MoveFolderDi
|
||||||
selected={selectedFolder === null}
|
selected={selectedFolder === null}
|
||||||
onClick={() => setSelectedFolder(null)}
|
onClick={() => setSelectedFolder(null)}
|
||||||
>
|
>
|
||||||
<ListItemIcon sx={{ minWidth: 40 }}>
|
<ListItemIcon>
|
||||||
<HomeIcon />
|
<HomeIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Библиотека (корень)" />
|
<ListItemText primary="Библиотека (корень)" />
|
||||||
|
|
@ -201,15 +79,16 @@ export default function MoveFolderDialog({ open, onClose, onMove }: MoveFolderDi
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
folders.map((folder) => (
|
folders.map((folder) => (
|
||||||
<FolderTreeItem
|
<ListItemButton
|
||||||
key={folder.id}
|
key={folder.id}
|
||||||
folder={folder}
|
selected={selectedFolder === folder.id}
|
||||||
level={0}
|
onClick={() => setSelectedFolder(folder.id)}
|
||||||
selectedFolder={selectedFolder}
|
>
|
||||||
expanded={expanded}
|
<ListItemIcon>
|
||||||
onSelect={setSelectedFolder}
|
<FolderIcon />
|
||||||
onToggle={handleToggle}
|
</ListItemIcon>
|
||||||
/>
|
<ListItemText primary={folder.name} />
|
||||||
|
</ListItemButton>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,9 @@ interface UploadDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
currentFolderId?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UploadDialog({ open, onClose, onComplete, currentFolderId }: UploadDialogProps) {
|
export default function UploadDialog({ open, onClose, onComplete }: UploadDialogProps) {
|
||||||
const [files, setFiles] = useState<UploadFile[]>([]);
|
const [files, setFiles] = useState<UploadFile[]>([]);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
|
|
@ -76,7 +75,6 @@ export default function UploadDialog({ open, onClose, onComplete, currentFolderI
|
||||||
original_filename: file.name,
|
original_filename: file.name,
|
||||||
content_type: file.type,
|
content_type: file.type,
|
||||||
size_bytes: file.size,
|
size_bytes: file.size,
|
||||||
folder_id: currentFolderId || undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
updateFileProgress(index, 33, 'uploading', undefined, uploadData.asset_id);
|
updateFileProgress(index, 33, 'uploading', undefined, uploadData.asset_id);
|
||||||
|
|
|
||||||
|
|
@ -167,9 +167,7 @@ export default function ViewerModal({
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
pt: 'max(env(safe-area-inset-top), 8px)',
|
p: 2,
|
||||||
px: 2,
|
|
||||||
pb: 2,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
@ -270,9 +268,7 @@ export default function ViewerModal({
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
px: 2,
|
p: 2,
|
||||||
pt: 2,
|
|
||||||
pb: 'max(env(safe-area-inset-bottom), 8px)',
|
|
||||||
bgcolor: 'rgba(0,0,0,0.5)',
|
bgcolor: 'rgba(0,0,0,0.5)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
|
||||||
|
|
@ -400,7 +400,6 @@ function LibraryPageContent() {
|
||||||
open={uploadOpen}
|
open={uploadOpen}
|
||||||
onClose={() => setUploadOpen(false)}
|
onClose={() => setUploadOpen(false)}
|
||||||
onComplete={handleUploadComplete}
|
onComplete={handleUploadComplete}
|
||||||
currentFolderId={currentFolderId}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ViewerModal
|
<ViewerModal
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,7 @@ class ApiClient {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
localStorage.removeItem('access_token');
|
localStorage.removeItem('access_token');
|
||||||
localStorage.removeItem('refresh_token');
|
localStorage.removeItem('refresh_token');
|
||||||
// Redirect only if not already on login/register page
|
window.location.href = '/login';
|
||||||
if (!['/login', '/register'].includes(window.location.pathname)) {
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ export type AssetStatus = 'uploading' | 'ready' | 'failed';
|
||||||
export interface Asset {
|
export interface Asset {
|
||||||
id: string;
|
id: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
folder_id?: string;
|
|
||||||
type: AssetType;
|
type: AssetType;
|
||||||
status: AssetStatus;
|
status: AssetStatus;
|
||||||
original_filename: string;
|
original_filename: string;
|
||||||
|
|
@ -43,7 +42,6 @@ export interface CreateUploadRequest {
|
||||||
original_filename: string;
|
original_filename: string;
|
||||||
content_type: string;
|
content_type: string;
|
||||||
size_bytes: number;
|
size_bytes: number;
|
||||||
folder_id?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateUploadResponse {
|
export interface CreateUploadResponse {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue