PhoenixKitPhotos.Components.PhotoTimeline (PhoenixKit Photos v0.3.0)

Copy Markdown View Source

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.

Summary

Functions

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.

Functions

push_view(socket, id, scope, auth, file_uuid)

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.