Files
photos/web/SelectionBar.tsx
pastilhasandClaude Opus 5 70fc2b08c6 a shared link is a URL, not a key
Create, edit and revoke shared links from the album header, the selection
bar and the Sharing panel. Password, expiry, and the three flags that
decide what the person opening it can do.

The panel showed a bare key because this plugin never learned the
instance's public address. It does not need to store one: Immich has
`externalDomain` in its own admin settings, `/server/config` already
returns it, and it is the same field Immich's own copy-link button reads,
so the two cannot disagree. Empty is a real state and the UI says so
rather than inventing a host.

Payload shapes came from the live instance's spec.json, not from memory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 18:16:02 +01:00

141 lines
5.2 KiB
TypeScript

import type { Selection } from './useSelection';
import { useState } from 'react';
import { Archive, ArchiveRestore, Heart, Plus, Share2, Trash2, Undo2, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { ShareLinkDialog } from './ShareLinkDialog';
import { useAlbums } from './usePhotosData';
import { useAssetActions } from './useAssetActions';
// The bar that replaces the header once anything is selected — Immich does the same, and it keeps the
// destructive buttons off screen entirely until they have something to act on.
type SelectionBarProps = {
selection: Selection;
/** Trash swaps the whole action set: restore and delete-forever, not favorite and archive. */
inTrash?: boolean;
/** When viewing one album, offer "remove from album" alongside the library-wide actions. */
albumId?: string;
/** Currently in the archive view, so the archive button should read "unarchive". */
archived?: boolean;
};
export const SelectionBar = ({ selection, inTrash = false, albumId, archived = false }: SelectionBarProps) => {
const { ids, count, clear, selectAll } = selection;
const { setFavorite, setArchived, trash, restore, deleteForever, addToAlbum, removeFromAlbum, createAlbum } =
useAssetActions();
const { data: albums } = useAlbums();
/*
The ids are captured when Share is pressed rather than read live, so the dialog keeps acting on what was
selected even if the selection changes behind it. Selection is deliberately NOT cleared: a link is usually
made to send, and losing the selection makes a second link over the same photos a re-selection job.
*/
const [sharing, setSharing] = useState<string[] | null>(null);
const run = (action: () => void) => {
action();
clear();
};
return (
<div className="flex items-center gap-1 border-b px-2 py-1.5">
<Button variant="ghost" size="icon" onClick={clear} title="Clear selection">
<X className="h-4 w-4" />
</Button>
<span className="mr-2 text-sm font-medium">{count} selected</span>
<Button variant="ghost" size="sm" onClick={selectAll}>
Select all
</Button>
<div className="flex-1" />
{inTrash ? (
<>
<Button variant="ghost" size="sm" onClick={() => run(() => restore.mutate(ids))}>
<Undo2 className="mr-1.5 h-4 w-4" />
Restore
</Button>
<Button variant="ghost" size="sm" onClick={() => run(() => deleteForever.mutate(ids))}>
<Trash2 className="mr-1.5 h-4 w-4" />
Delete permanently
</Button>
</>
) : (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<Plus className="mr-1.5 h-4 w-4" />
Add to album
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="max-h-80 overflow-y-auto">
<DropdownMenuItem
onClick={() => {
const albumName = window.prompt('New album name');
if (albumName) run(() => createAlbum.mutate({ albumName, assetIds: ids }));
}}
>
<Plus className="mr-2 h-4 w-4" />
New album
</DropdownMenuItem>
{albums && albums.length > 0 && <DropdownMenuSeparator />}
{albums?.map((album) => (
<DropdownMenuItem
key={album.id}
onClick={() => run(() => addToAlbum.mutate({ albumId: album.id, ids }))}
>
{album.albumName}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{albumId && (
<Button variant="ghost" size="sm" onClick={() => run(() => removeFromAlbum.mutate({ albumId, ids }))}>
Remove from album
</Button>
)}
<Button variant="ghost" size="sm" onClick={() => setSharing(ids)}>
<Share2 className="mr-1.5 h-4 w-4" />
Share
</Button>
<Button
variant="ghost"
size="icon"
title="Favorite"
onClick={() => run(() => setFavorite.mutate({ ids, isFavorite: true }))}
>
<Heart className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
title={archived ? 'Unarchive' : 'Archive'}
onClick={() => run(() => setArchived.mutate({ ids, archived: !archived }))}
>
{archived ? <ArchiveRestore className="h-4 w-4" /> : <Archive className="h-4 w-4" />}
</Button>
<Button variant="ghost" size="icon" title="Move to trash" onClick={() => run(() => trash.mutate(ids))}>
<Trash2 className="h-4 w-4" />
</Button>
</>
)}
<ShareLinkDialog
open={!!sharing}
onOpenChange={(next) => !next && setSharing(null)}
target={sharing ? { kind: 'assets', assetIds: sharing } : null}
/>
</div>
);
};