270 lines
8.4 KiB
TypeScript
270 lines
8.4 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import {
|
||
Box,
|
||
Grid,
|
||
Fab,
|
||
Typography,
|
||
CircularProgress,
|
||
Button,
|
||
Select,
|
||
MenuItem,
|
||
FormControl,
|
||
InputLabel,
|
||
Dialog,
|
||
DialogTitle,
|
||
DialogContent,
|
||
DialogActions,
|
||
TextField,
|
||
Alert,
|
||
Snackbar,
|
||
} from '@mui/material';
|
||
import { Add as AddIcon, FilterList as FilterIcon } from '@mui/icons-material';
|
||
import Layout from '../components/Layout';
|
||
import MediaCard from '../components/MediaCard';
|
||
import UploadDialog from '../components/UploadDialog';
|
||
import ViewerModal from '../components/ViewerModal';
|
||
import type { Asset, AssetType } from '../types';
|
||
import api from '../services/api';
|
||
|
||
export default function LibraryPage() {
|
||
const [assets, setAssets] = useState<Asset[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [hasMore, setHasMore] = useState(false);
|
||
const [cursor, setCursor] = useState<string | undefined>();
|
||
const [filter, setFilter] = useState<AssetType | 'all'>('all');
|
||
|
||
const [uploadOpen, setUploadOpen] = useState(false);
|
||
const [viewerAsset, setViewerAsset] = useState<Asset | null>(null);
|
||
const [shareDialogOpen, setShareDialogOpen] = useState(false);
|
||
const [shareAssetId, setShareAssetId] = useState<string>('');
|
||
const [shareLink, setShareLink] = useState<string>('');
|
||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||
|
||
useEffect(() => {
|
||
loadAssets(true);
|
||
}, [filter]);
|
||
|
||
const loadAssets = async (reset: boolean = false) => {
|
||
try {
|
||
setLoading(true);
|
||
const response = await api.listAssets({
|
||
cursor: reset ? undefined : cursor,
|
||
limit: 50,
|
||
type: filter === 'all' ? undefined : filter,
|
||
});
|
||
|
||
setAssets(reset ? response.items : [...assets, ...response.items]);
|
||
setHasMore(response.has_more);
|
||
setCursor(response.next_cursor);
|
||
} catch (error) {
|
||
console.error('Failed to load assets:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleUploadComplete = () => {
|
||
setUploadOpen(false);
|
||
loadAssets(true);
|
||
showSnackbar('Файлы успешно загружены');
|
||
};
|
||
|
||
const handleDelete = async (assetId: string) => {
|
||
try {
|
||
await api.deleteAsset(assetId);
|
||
setAssets(assets.filter((a) => a.id !== assetId));
|
||
showSnackbar('Файл перемещен в корзину');
|
||
} catch (error) {
|
||
console.error('Failed to delete asset:', error);
|
||
showSnackbar('Ошибка при удалении файла');
|
||
}
|
||
};
|
||
|
||
const handleShare = (assetId: string) => {
|
||
setShareAssetId(assetId);
|
||
setShareDialogOpen(true);
|
||
};
|
||
|
||
const handleCreateShare = async () => {
|
||
try {
|
||
const share = await api.createShare({
|
||
asset_id: shareAssetId,
|
||
expires_in_seconds: 86400 * 7, // 7 days
|
||
});
|
||
const link = `${window.location.origin}/share/${share.token}`;
|
||
setShareLink(link);
|
||
showSnackbar('Ссылка создана');
|
||
} catch (error) {
|
||
console.error('Failed to create share:', error);
|
||
showSnackbar('Ошибка создания ссылки');
|
||
}
|
||
};
|
||
|
||
const handleCopyShareLink = () => {
|
||
// Fallback for HTTP (clipboard API requires HTTPS)
|
||
if (navigator.clipboard && window.isSecureContext) {
|
||
navigator.clipboard.writeText(shareLink);
|
||
} else {
|
||
// Fallback method for HTTP
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = shareLink;
|
||
textArea.style.position = 'fixed';
|
||
textArea.style.left = '-999999px';
|
||
document.body.appendChild(textArea);
|
||
textArea.focus();
|
||
textArea.select();
|
||
try {
|
||
document.execCommand('copy');
|
||
} catch (err) {
|
||
console.error('Failed to copy:', err);
|
||
}
|
||
document.body.removeChild(textArea);
|
||
}
|
||
showSnackbar('Ссылка скопирована');
|
||
setShareDialogOpen(false);
|
||
setShareLink('');
|
||
};
|
||
|
||
const showSnackbar = (message: string) => {
|
||
setSnackbarMessage(message);
|
||
setSnackbarOpen(true);
|
||
};
|
||
|
||
return (
|
||
<Layout>
|
||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||
{/* Filters */}
|
||
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||
<FormControl size="small" sx={{ minWidth: 200 }}>
|
||
<InputLabel>Тип файлов</InputLabel>
|
||
<Select
|
||
value={filter}
|
||
label="Тип файлов"
|
||
onChange={(e) => setFilter(e.target.value as AssetType | 'all')}
|
||
>
|
||
<MenuItem value="all">Все файлы</MenuItem>
|
||
<MenuItem value="photo">Фото</MenuItem>
|
||
<MenuItem value="video">Видео</MenuItem>
|
||
</Select>
|
||
</FormControl>
|
||
</Box>
|
||
|
||
{/* Content */}
|
||
<Box sx={{ flexGrow: 1, overflow: 'auto', p: 2 }}>
|
||
{loading && assets.length === 0 && (
|
||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
||
<CircularProgress />
|
||
</Box>
|
||
)}
|
||
|
||
{!loading && assets.length === 0 && (
|
||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||
Нет файлов
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Нажмите кнопку + чтобы загрузить файлы
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
|
||
{assets.length > 0 && (
|
||
<>
|
||
<Grid container spacing={2}>
|
||
{assets.map((asset) => (
|
||
<Grid item xs={6} sm={4} md={3} lg={2} key={asset.id}>
|
||
<MediaCard
|
||
asset={asset}
|
||
onClick={() => setViewerAsset(asset)}
|
||
/>
|
||
</Grid>
|
||
))}
|
||
</Grid>
|
||
|
||
{hasMore && (
|
||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
|
||
<Button
|
||
variant="outlined"
|
||
onClick={() => loadAssets(false)}
|
||
disabled={loading}
|
||
>
|
||
{loading ? 'Загрузка...' : 'Загрузить еще'}
|
||
</Button>
|
||
</Box>
|
||
)}
|
||
</>
|
||
)}
|
||
</Box>
|
||
|
||
{/* FAB */}
|
||
<Fab
|
||
color="primary"
|
||
sx={{ position: 'fixed', bottom: 24, right: 24 }}
|
||
onClick={() => setUploadOpen(true)}
|
||
>
|
||
<AddIcon />
|
||
</Fab>
|
||
|
||
{/* Upload Dialog */}
|
||
<UploadDialog
|
||
open={uploadOpen}
|
||
onClose={() => setUploadOpen(false)}
|
||
onComplete={handleUploadComplete}
|
||
/>
|
||
|
||
{/* Viewer Modal */}
|
||
<ViewerModal
|
||
asset={viewerAsset}
|
||
assets={assets}
|
||
onClose={() => setViewerAsset(null)}
|
||
onDelete={handleDelete}
|
||
onShare={handleShare}
|
||
/>
|
||
|
||
{/* Share Dialog */}
|
||
<Dialog open={shareDialogOpen} onClose={() => setShareDialogOpen(false)}>
|
||
<DialogTitle>Поделиться файлом</DialogTitle>
|
||
<DialogContent>
|
||
{!shareLink ? (
|
||
<Typography>
|
||
Создать публичную ссылку на файл? Ссылка будет действительна 7 дней.
|
||
</Typography>
|
||
) : (
|
||
<TextField
|
||
fullWidth
|
||
value={shareLink}
|
||
label="Ссылка для доступа"
|
||
margin="normal"
|
||
InputProps={{
|
||
readOnly: true,
|
||
}}
|
||
/>
|
||
)}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={() => setShareDialogOpen(false)}>Отмена</Button>
|
||
{!shareLink ? (
|
||
<Button onClick={handleCreateShare} variant="contained">
|
||
Создать ссылку
|
||
</Button>
|
||
) : (
|
||
<Button onClick={handleCopyShareLink} variant="contained">
|
||
Скопировать
|
||
</Button>
|
||
)}
|
||
</DialogActions>
|
||
</Dialog>
|
||
|
||
{/* Snackbar */}
|
||
<Snackbar
|
||
open={snackbarOpen}
|
||
autoHideDuration={3000}
|
||
onClose={() => setSnackbarOpen(false)}
|
||
message={snackbarMessage}
|
||
/>
|
||
</Box>
|
||
</Layout>
|
||
);
|
||
}
|