release: v0.19.0

Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of
[CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md)
for the full list.

Highlights: client proofing links (#683), custom content pages (#738),
asset picker search (#740), concurrent-edit detection (#742), env-locked
settings (#739), an accessibility baseline (#741), three security fixes
with advisories (#737, #755, #759), and a pre-release quality pass
(#780–#797).

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
This commit is contained in:
Ralksta
2026-09-29 22:50:30 +02:00
committed by GitHub
parent 5f251923f0
commit ad8b09700b
297 changed files with 19398 additions and 1800 deletions

View File

@@ -5,6 +5,7 @@ import { useProofing } from './ProofingContext';
import { IconCheck, IconCopy, IconLink } from './Icons';
import { useDictionary } from './I18nProvider';
import { useModalDialog } from '@/hooks/useModalDialog';
import { decodeEmail } from '@/lib/emailObfuscation';
export function ProofingModal() {
const t = useDictionary();
@@ -27,22 +28,28 @@ export function ProofingModal() {
if (!proofing || !proofing.isModalOpen) return null;
const {
favorites,
setIsModalOpen,
getProofingUrl,
getFormattedList,
getSelectedTokens,
clearFavorites,
allowMailto,
encodedMailto,
downloadArchiveUrl,
} = proofing;
// What the archive would actually receive. Gating on this rather than on
// `favorites.size` means a favourite left over from another album (they share
// the provider's storage key when no `albumName` is passed) can never light up
// a button that would post an empty selection and 404.
// What the archive would actually receive, and the only count shown here.
// `favorites.size` also counts a favourite left over from another album (they
// share a storage key when their names slug alike) or a photo since removed
// from this one, so it would light up a button that posts an empty selection
// and 404s, and put the wrong number in the title and the email subject.
const selectedCount = getSelectedTokens().length;
// Decoded in the browser only: the address never travels in the clear. The
// email button is hidden without a recipient, so a mailto is never built with
// an empty To: — which opened a compose that went nowhere (#736).
const mailtoRecipient = encodedMailto ? decodeEmail(encodedMailto) : '';
/** Copy to the clipboard, or show the text to copy by hand where it is unavailable. */
const copy = (kind: 'link' | 'list', text: string) => {
if (!navigator.clipboard) {
@@ -64,9 +71,12 @@ export function ProofingModal() {
const handleCopyList = () => copy('list', getFormattedList());
const handleMailto = () => {
const subject = encodeURIComponent(t.proofing.mailSubject(favorites.size));
const subject = encodeURIComponent(t.proofing.mailSubject(getSelectedTokens().length));
const body = encodeURIComponent(t.proofing.mailBody(getFormattedList(), getProofingUrl()));
window.location.href = `mailto:?subject=${subject}&body=${body}`;
// Percent-encode the address, keeping '@' readable, so a value from the
// settings cannot smuggle its own `?bcc=`/`&…` header fields into the draft.
const to = encodeURIComponent(mailtoRecipient).replace(/%40/g, '@');
window.location.href = `mailto:${to}?subject=${subject}&body=${body}`;
};
const handleDownloadSelection = () => {
@@ -143,7 +153,7 @@ export function ProofingModal() {
}}
>
<h3 id="proofing-modal-title" style={{ margin: 0, fontSize: '1.2rem', fontWeight: 600 }}>
{t.proofing.modalTitle(favorites.size)}
{t.proofing.modalTitle(selectedCount)}
</h3>
<button
type="button"
@@ -164,7 +174,7 @@ export function ProofingModal() {
</div>
<p style={{ fontSize: '0.9rem', opacity: 0.8, marginBottom: '1rem' }}>
{t.proofing.intro(favorites.size)}
{t.proofing.intro(selectedCount)}
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
@@ -219,13 +229,21 @@ export function ProofingModal() {
gap: '0.5rem',
padding: '0.75rem 1rem',
borderRadius: 'var(--radius-sm, 6px)',
// Secondary matches the other buttons below: a white-alpha fill
// and border vanished on the light theme's white card. Primary
// text takes --on-accent, which the layout computes from the
// accent; a fixed white was invisible on minimal's white accent.
...(downloadArchiveUrl
? {
background: 'rgba(255,255,255,0.1)',
background: 'var(--bg-card-hover)',
color: 'inherit',
border: '1px solid rgba(255,255,255,0.15)',
border: '1px solid var(--border-subtle)',
}
: { background: 'var(--accent, #e60012)', color: '#fff', border: 'none' }),
: {
background: 'var(--accent, #e60012)',
color: 'var(--on-accent, #fff)',
border: 'none',
}),
fontWeight: 500,
cursor: 'pointer',
}}
@@ -296,7 +314,7 @@ export function ProofingModal() {
</div>
)}
{allowMailto && (
{allowMailto && mailtoRecipient && (
<button
type="button"
onClick={handleMailto}
@@ -330,10 +348,12 @@ export function ProofingModal() {
marginTop: '0.5rem',
background: 'none',
border: 'none',
color: '#ff4d4f',
// Per-mode token, at full strength: the fixed red at 80% opacity it
// replaces measured 2.7:1 on the light card and under 4:1 on the
// dark ones.
color: 'var(--error)',
fontSize: '0.85rem',
cursor: 'pointer',
opacity: 0.8,
}}
>
{t.proofing.clearSelection}