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>
|
|||
|
|
);
|
|||
|
|
}
|