129 lines
4.0 KiB
TypeScript
129 lines
4.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
|
|
import { TRASH_RETENTION_MS } from '../../constants';
|
|
import { daysUntilPermanentDelete } from '../../lib/format';
|
|
import { Icon } from '../../icons';
|
|
import { useViewMedia } from '../../hooks/useViewMedia';
|
|
import { useGallery, useGalleryStoreApi } from '../../store/context';
|
|
import { MediaGrid } from '../MediaGrid';
|
|
import { openSecuritySettings } from '../modals';
|
|
|
|
export function RecentlyDeletedView() {
|
|
const api = useGalleryStoreApi();
|
|
const items = useViewMedia();
|
|
const lockConfigured = useGallery((s) => s.lockConfigured);
|
|
const lockUnlocked = useGallery((s) => s.lockUnlocked);
|
|
const lockError = useGallery((s) => s.lockError);
|
|
const [pw, setPw] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
// Locked: a password is set and the user hasn't unlocked this session.
|
|
if (lockConfigured && !lockUnlocked) {
|
|
const submit = async () => {
|
|
setBusy(true);
|
|
await api.getState().unlockLock(pw);
|
|
setBusy(false);
|
|
setPw('');
|
|
};
|
|
return (
|
|
<div className="apg-empty">
|
|
<span style={{ color: 'var(--apg-text-tertiary)' }}>
|
|
<Icon name="lock" size={56} />
|
|
</span>
|
|
<div className="apg-empty__title" style={{ fontSize: 21 }}>
|
|
Enter Your Password to View Recently Deleted
|
|
</div>
|
|
<input
|
|
className="apg-modal__input"
|
|
type="password"
|
|
autoFocus
|
|
value={pw}
|
|
placeholder="Password"
|
|
style={{ maxWidth: 280 }}
|
|
onChange={(e) => {
|
|
setPw(e.target.value);
|
|
api.getState().clearLockError();
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') void submit();
|
|
}}
|
|
/>
|
|
{lockError ? (
|
|
<div style={{ color: 'var(--apg-danger)', fontSize: 13 }} role="alert">
|
|
{lockError === 'wrong-password'
|
|
? 'Incorrect password.'
|
|
: "Couldn't check the password. Please try again."}
|
|
</div>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
className="apg-btn apg-btn--primary"
|
|
disabled={busy}
|
|
onClick={() => void submit()}
|
|
>
|
|
{busy ? 'Checking…' : 'Unlock'}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (items.length === 0) {
|
|
return (
|
|
<div className="apg-empty">
|
|
<div className="apg-empty__card">
|
|
<span style={{ color: 'var(--apg-text-tertiary)' }}>
|
|
<Icon name="trash" size={40} />
|
|
</span>
|
|
<div className="apg-empty__title" style={{ fontSize: 22 }}>
|
|
No Recently Deleted Items
|
|
</div>
|
|
<div className="apg-empty__subtitle">
|
|
Deleted items are kept here for {Math.round(TRASH_RETENTION_MS / 86400000)} days.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Soonest expiry = the smallest remaining days across all trashed items
|
|
// (independent of display order, which is sorted by capture date).
|
|
const nextExpiry = items.reduce((min, m) => {
|
|
if (!m.deletedAt) return min;
|
|
return Math.min(min, daysUntilPermanentDelete(m.deletedAt, TRASH_RETENTION_MS));
|
|
}, Infinity);
|
|
const expiryDays = Number.isFinite(nextExpiry) ? nextExpiry : 0;
|
|
|
|
return (
|
|
<div className="apg-scroll">
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: 12,
|
|
padding: '14px 14px 0',
|
|
color: 'var(--apg-text-secondary)',
|
|
fontSize: 13,
|
|
}}
|
|
>
|
|
<span>
|
|
Items are deleted permanently after 30 days. Oldest expires in ~{expiryDays} day
|
|
{expiryDays === 1 ? '' : 's'}.
|
|
</span>
|
|
<button
|
|
type="button"
|
|
className="apg-btn"
|
|
style={{ flexShrink: 0, padding: '4px 10px' }}
|
|
onClick={openSecuritySettings}
|
|
>
|
|
<Icon name={lockConfigured ? 'lock' : 'unlock'} size={13} />
|
|
{lockConfigured ? 'Locked' : 'Lock…'}
|
|
</button>
|
|
</div>
|
|
<MediaGrid items={items} />
|
|
</div>
|
|
);
|
|
}
|