- Fix MUI v4/React 18 StrictMode warnings (justify→justifyContent, Button migration) - Responsive UploadDiary: footer visible without scroll, mobile layout centered, overview text hidden on mobile - Fix Redux selector memoization with createSelector - RSS sync: auto-refresh PosterSelector after sync, Snackbar feedback with film count - Add .gitea/workflows/deploy.yml for tag-based CI/CD - Fix nginx config (default.conf), increase Node heap for Docker build - Update README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
174 lines
5.7 KiB
JavaScript
174 lines
5.7 KiB
JavaScript
import React, { useState } from "react";
|
|
import { useSelector } from "react-redux";
|
|
import { useNavigate } from "react-router-dom";
|
|
import {
|
|
Container,
|
|
Typography,
|
|
Box,
|
|
Button,
|
|
IconButton,
|
|
LinearProgress,
|
|
} from "@material-ui/core";
|
|
import GetAppIcon from "@material-ui/icons/GetApp";
|
|
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
|
|
import CheckIcon from "@material-ui/icons/Check";
|
|
import JSZip from "jszip";
|
|
|
|
const MONTH_NAMES = [
|
|
"January", "February", "March", "April", "May", "June",
|
|
"July", "August", "September", "October", "November", "December",
|
|
];
|
|
|
|
const SelectionRecap = () => {
|
|
const navigate = useNavigate();
|
|
const posterSelections = useSelector((state) => state.posterSelections);
|
|
const [downloadState, setDownloadState] = useState(null);
|
|
|
|
const entries = Object.entries(posterSelections)
|
|
.flatMap(([movieId, posters]) => {
|
|
if (!posters?.length) return [];
|
|
const match = movieId.match(/^(.+)-(\d{4})$/);
|
|
if (!match) return [];
|
|
return posters.map((p) => ({
|
|
movieId,
|
|
movieName: match[1],
|
|
movieYear: match[2],
|
|
posterId: p.posterId,
|
|
watchedDate: p.watchedDate || "",
|
|
}));
|
|
})
|
|
.sort((a, b) => a.watchedDate.localeCompare(b.watchedDate));
|
|
|
|
const groups = entries.reduce((acc, entry) => {
|
|
const key = entry.watchedDate.slice(0, 7);
|
|
if (!acc[key]) acc[key] = [];
|
|
acc[key].push(entry);
|
|
return acc;
|
|
}, {});
|
|
|
|
const sortedMonths = Object.keys(groups).sort().reverse();
|
|
|
|
const handleDownload = async () => {
|
|
if (downloadState) return;
|
|
setDownloadState({ current: 0, total: entries.length });
|
|
try {
|
|
const zip = new JSZip();
|
|
for (let i = 0; i < entries.length; i++) {
|
|
const { movieId, posterId, watchedDate } = entries[i];
|
|
const res = await fetch(`https://image.tmdb.org/t/p/original${posterId}`);
|
|
const blob = await res.blob();
|
|
zip.file(`${watchedDate}_${movieId}.jpg`, blob);
|
|
setDownloadState({ current: i + 1, total: entries.length });
|
|
}
|
|
const content = await zip.generateAsync({ type: "blob" });
|
|
const url = URL.createObjectURL(content);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = "posters.zip";
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
URL.revokeObjectURL(url);
|
|
} catch (err) {
|
|
console.error("Download error:", err);
|
|
} finally {
|
|
setDownloadState(null);
|
|
}
|
|
};
|
|
|
|
if (entries.length === 0) {
|
|
return (
|
|
<Container className="selection-recap">
|
|
<Box className="recap-empty">
|
|
<Typography variant="h6">No posters selected yet.</Typography>
|
|
<Button
|
|
variant="contained"
|
|
className="recap-go-btn"
|
|
onClick={() => navigate("/PosterSelector")}
|
|
>
|
|
Go to calendar
|
|
</Button>
|
|
</Box>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
const isDownloading = !!downloadState;
|
|
const progress = isDownloading
|
|
? Math.round((downloadState.current / downloadState.total) * 100)
|
|
: 0;
|
|
|
|
return (
|
|
<Container className="selection-recap">
|
|
<Box className="recap-header">
|
|
<IconButton className="recap-back-btn" onClick={() => navigate(-1)}>
|
|
<ArrowBackIcon />
|
|
</IconButton>
|
|
<Typography variant="h5" className="recap-title">
|
|
Selected Posters ({entries.length})
|
|
</Typography>
|
|
<Button
|
|
className={`recap-download-btn${isDownloading ? " recap-download-btn--loading" : ""}`}
|
|
startIcon={!isDownloading && <GetAppIcon />}
|
|
onClick={handleDownload}
|
|
disabled={isDownloading}
|
|
>
|
|
{isDownloading ? (
|
|
<Box className="recap-download-progress">
|
|
<span>{downloadState.current} / {downloadState.total}</span>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={progress}
|
|
className="recap-progress-bar"
|
|
/>
|
|
</Box>
|
|
) : (
|
|
"Download all"
|
|
)}
|
|
</Button>
|
|
</Box>
|
|
|
|
{sortedMonths.map((monthKey) => {
|
|
const [year, month] = monthKey.split("-").map(Number);
|
|
const monthLabel = `${MONTH_NAMES[month - 1]} ${year}`;
|
|
return (
|
|
<Box key={monthKey} className="recap-month-section">
|
|
<Typography className="recap-month-label">{monthLabel}</Typography>
|
|
<div className="recap-grid">
|
|
{groups[monthKey].map((entry) => (
|
|
<div
|
|
key={entry.movieId}
|
|
className="recap-card"
|
|
onClick={() =>
|
|
navigate(
|
|
`/posters/${encodeURIComponent(entry.movieName)}/${encodeURIComponent(entry.movieYear)}`,
|
|
{ state: { watchedDate: entry.watchedDate } }
|
|
)
|
|
}
|
|
>
|
|
<div className="recap-poster-wrapper">
|
|
<img
|
|
src={`https://image.tmdb.org/t/p/w300${entry.posterId}`}
|
|
alt={entry.movieName}
|
|
className="recap-poster"
|
|
/>
|
|
<div className="recap-overlay">
|
|
<span className="recap-movie-title">{entry.movieName}</span>
|
|
<span className="recap-movie-year">{entry.movieYear}</span>
|
|
</div>
|
|
<span className="recap-check-badge">
|
|
<CheckIcon style={{ fontSize: 12 }} />
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Box>
|
|
);
|
|
})}
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export default SelectionRecap;
|