# `PhoenixKitPhotos.Components.PhotoTimeline`
[🔗](https://github.com/BeamLabEU/phoenix_kit_photos/blob/v0.3.0/lib/phoenix_kit_photos/components/photo_timeline.ex#L1)

The timeline LiveComponent: index and current window on the server, geometry
and recycling in the `PhotoTimeline` JS hook.

## Usage

    <.live_component
      module={PhoenixKitPhotos.Components.PhotoTimeline}
      id="library"
      scope={{:library, library_uuid}}
      auth={scope}
      layout={:square}
      columns={5}
      on_open="open_asset"
    />

`scope` is `{:library, uuid}` — one storage library. `auth` is the current
`PhoenixKit.Users.Auth.Scope`. The server checks that scope before it
returns a file, and again when it mints a thumbnail URL.

## Filling the screen

`fill` makes the timeline cover the whole window (`fixed inset-0`), the way a photo app
does, with the grid scrolling under the host's header. The host says how tall that header
is with the CSS length `--photo-timeline-inset-top` (any length: `3.5rem`,
`calc(3.5rem + env(safe-area-inset-top))`) set on the timeline or an ancestor; the first
row rests below it and a jump to a month lands under it. In this mode the "jump to month"
select becomes a small floating pill that always shows the month at the top of the screen
and opens the same list, and an empty library shows the `:empty` slot (or a plain line)
in the middle of the screen instead of an alert.

`cell` is a tile size in px to aim for: the number of columns then follows the width of
the screen, as many tiles of about that size as fit (at least three), and `columns` is only
the fallback. `gap` is the space between tiles in px (default 4).

    <.live_component module={PhotoTimeline} id="library" scope={...} auth={scope}
      fill cell={170} gap={2}>
      <:empty>Drop photos here</:empty>
    </.live_component>

The person can zoom: pinch on a phone, pinch on a trackpad (Chrome, Edge, Firefox send it as
ctrl + wheel; Safari as gesture events), or press `+`, `-` and `0` (back to the size the host
asks for). While the fingers are down the grid is only scaled; when they lift it is laid out
again with a whole number of columns, around the photo under the fingers. The size they chose
is a tile size, kept in the browser (`localStorage`, `phoenix_kit_photos:cell`), so it carries
over to another screen width and wins over `cell`. Tiles go down to 56 px and up to one column.

## Styles

`layout` is the style a person sees first: `:square` (the plain grid), `:rows`, `:columns`,
`:mosaic`, `:whole`, `:feed`, `:prints`, `:film`, `:honeycomb`, `:details` or `:calendar`
(each month a wall calendar, a day's photos sharing its square). `styles` puts
a menu by the month pill that lets them change it: `true` for all of them, or a list of the
ones to offer. Their pick is kept in the browser (`localStorage`,
`phoenix_kit_photos:style`) and wins over `layout`. `v` steps through the styles.

A change of style is an animation in the browser and asks nothing of the server, except
the first time a style needs the aspect ratio of every file (`Timeline.aspects/2`). Every
style's heights are exact, so the scrollbar, a jump to a month, the bookmark and the swipe
viewer's way back work the same in all of them.

`pager` makes a click on a tile open the swipe viewer (in the `PhotoTimeline`
hook) instead of sending `on_open`. `false` (the default) is no swipe viewer;
`true` is the default engine; a string or atom (`"photoswipe"`, `"snap"`) names
the engine. The engines are interchangeable behind one contract (see
`assets/js/viewer/engine.js`); a name the page does not know falls back to the
default one, and a host can offer a choice by passing the person's. The viewer
asks this component for full-size slides (`timeline:slides`) as the person
swipes, and its Info button sends `on_open` for the file on screen, so the
host's own viewer (details, comments) is still one tap away.

## Choosing many

`selectable` lets a person choose photos, and act on them from a bar along the bottom. How
they choose is in `assets/js/selection.js`: the circle on a tile, Shift + click, a box
dragged with the mouse, hold and slide on a touch screen, the circle on a month, Ctrl/Cmd + A,
and the "Select" menu of the bar (the day, the moment, every video). The ids stay in the
browser until an action is asked for; then every file is checked again on the server
(`PhoenixKitPhotos.Bulk`), the person sees what would happen, and it runs in the background
with a progress bar. Trash can be undone from the notice that follows it.

`download_path` is where `PhoenixKitPhotos.Web.Download` is mounted; without it there is no
Download. Copy links needs `photo_path`.

## Addresses

With `photo_path` and `library_path` the swipe viewer keeps the page's address in step with
the photo on screen, so a photo can be shared, bookmarked, opened in a new tab and left with
the Back button. `photo_path` is the address of a photo with `:code` where its short code goes
(`"/anna/private/p/:code"`), `library_path` the address of the library
(`"/anna/private"`). Both are paths on the host's own site. The viewer opening adds a history
entry, turning to another photo replaces it, and closing the viewer goes back (or, when the
page was opened on a photo, to `library_path`). The changes go through LiveView
(`JS.patch`), so the parent's `handle_params/3` sees every address.

The other way round, the parent tells the browser which photo its address names, on load and
whenever the address changes (a Back to the library closes the viewer; a link to a photo opens
it): `push_view/5`. The browser leaves a viewer that already shows that photo alone.

`on_open`, when set, is sent to the parent LiveView as
`{on_open, file_uuid}` when a tile is opened. The parent must authorize
that client-supplied UUID through `Timeline.viewer_file/3` before opening it.

Hosts subscribe using `on_mount {PhoenixKitPhotos.LiveUpdates, "library"}`;
use the actual component ID in place of "library".

## When files arrive

Storage announces file events to everyone, so the component first asks whether the files are in
its library (`Timeline.touches?/2`) and ignores the event if not. An upload is several events a
file, so it gathers a burst (`config :phoenix_kit_photos, refresh_delay_ms: 350`) into one new
index. The browser then keeps every tile where it is, fetches the windows it holds again, and
swaps the answers in: a photo it has never seen pops in, the tiles after it glide aside, and
when the person is looking at the end of the library the view follows it down. Nothing is
cleared, so nothing flickers; an open swipe viewer is left alone until it is closed.

# `push_view`

```elixir
@spec push_view(
  Phoenix.LiveView.Socket.t(),
  String.t(),
  PhoenixKitPhotos.Timeline.scope(),
  PhoenixKit.Users.Auth.Scope.t() | nil,
  String.t() | nil
) :: Phoenix.LiveView.Socket.t()
```

Tells the timeline's browser hook which photo the page's address names, from the parent
LiveView (usually in `handle_params/3`): `file_uuid` is that photo, or nil when the address
names none, which closes the swipe viewer if it is open. `id` is the component's id, `scope`
and `auth` are what it was given. A photo the viewer may not read is treated as none.

The hook opens the viewer on the photo (or leaves one that already shows it alone), and does
not change the address: it is already right.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
