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
@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.