Skip to content

feat: add a Both / Personal / Spaces view to Photos - #1248

Open
defts wants to merge 1 commit into
open-noodle:mainfrom
defts:feat/library-view
Open

defts wants to merge 1 commit into
open-noodle:mainfrom
defts:feat/library-view

Conversation

@defts

@defts defts commented Oct 27, 2026 •

Copy link
Copy Markdown

Summary

#1038 asks how to see only the photos that are in no space (Q2). This adds a three-way view to Photos, like the library view of Apple Photos:

  • Both (default): the timeline as it is today.
  • Personal: your own photos that are in none of your spaces, by any path (added to a space, in a linked album, in a linked library).
  • Spaces: the photos that reach you through a space on your timeline, yours included.

The control only shows once one of your spaces is on your timeline; until then the view is Both. The choice is remembered (per browser on web, per device on mobile), filters narrow within it (on mobile, browsing in search mode), and a text or similar-photo search spans every photo.

Design: specs/2026-10-26-library-view-design.md.

Web

Both Personal Spaces
web-en-both web-en-personal web-en-spaces

Mobile

On mobile the Photos app bar's grouping chip becomes a display menu, in the same slot: the photos shown (Both / Personal / Spaces) and the timeline grouping (Years / Months / All). The menu stays open while each choice applies and closes on a tap outside; the button stays neutral and shows the view that narrows the timeline. Browsing in search mode stays within the view, and its counts follow it.

Both Display menu Personal chosen Personal Spaces
mobile-en-both mobile-en-menu mobile-en-menu-personal mobile-en-personal mobile-en-spaces

How

  • Server: an optional libraryView (personal | spaces) on the timeline and the flat search fields; without it nothing changes, so stock clients are unaffected. withLibraryView (src/gallery/utils/library-view.ts) only narrows the query, reusing spaceAssetPathBranches and sharedWithViewer. checkLibraryView runs in resolveViewerScope and refuses shared links, a timeline that is not the caller's (album and space pages), and spaces without withSharedSpaces.
  • Web: PillGroup (web/src/lib/gallery/pill-group/) is the pill the grouping control already drew, and both controls use it now (arrow keys included). The toolbar wraps its chips instead of squeezing them. An empty view shows "no results", and "View in timeline" goes back to Both so the photo is there.
  • Search options: POST /search/options takes libraryView too, as /timeline/buckets does, so the counts match the results; the cache of unfiltered options keys on it.
  • Mobile: PhotosViewMenuButton (mobile/lib/gallery/photos_view_menu.widget.dart) replaces the compact grouping chip, which goes away with its short labels; it is no wider, so the logo keeps its size (Logo shrinks inconsistently in top bar on mobile depending on active icons (loading spinner / sort icon) #1030). Every choice reloads the timeline and rebuilds the app bar, so the menu is a route of its own that reads app-wide providers, not an overlay anchored to the button; it slides over the neighbouring actions on a 360-dp screen and follows the text direction. The view section only shows with a space on the timeline. The merged timeline SQL (drift and the scoped SQL) takes the view as a nullable :library_view. The Photos search scope (searchScopeProvider's default) carries the view, and browsing sends it (/timeline/buckets, the metadata search, /search/options); a query (/search/smart) does not. The view is stored under 90007, in the fork's block of store ids.

Known limits

  • On web, the filter panel's suggestions (people, places, tags, cameras) are not narrowed by the view.
  • An app updated before its server gets unnarrowed search results until the server is updated; the timeline itself is local and follows the view.
  • On mobile, search mode steps the app bar and its menu aside, so nothing there shows the view that browsing is narrowed to. The Filters sheet could show it; I kept the view out of it, since the sheet's choices are cleared and forgotten while the view is remembered.

Verification

  • Server: the timeline-library-view and search-library-view medium specs (12: each view, bucket counts, statistics, the Photos album filter, a stack split across views, the refusals); /search/options under each view against /timeline/buckets, each view cached on its own; the controller passing the view on and refusing an unknown one; the unit suite (6940) and the utils, search options and library view medium suites (287).
  • Web: the unit suite (6740); check:svelte and tsc clean.
  • Mobile: the suite (3813); one test runs every path of every view (space, linked album, hidden album, linked library, hidden space, partner, trash, archive, locked, device-only, merged) with and without a date scope, bucket counts included. Dropping any one of four SQL conditions fails it. The Photos search scope under each view on the options, timeline and smart-search requests, and the Photos page's search under a remembered view through the real providers: browsing sends the view, a text query does not.
  • On a local stack: web, and the app on the iOS simulator (screenshots above).

server/src/queries/*.sql unchanged; the OpenAPI spec and the SDK regenerated.

Seen while testing, not changed here

On mobile, switching the view in the "All" grid swaps the timeline service while the previous rows are still on screen (the segments keep their last value while the new buckets load). Those rows ask the new service for assets before it knows its count, so TimelineService.getAssets throws and the error is logged until the grid redraws. I left it for a separate fix: the row should not requireData a failed load, and AsyncMutex should not hand a failed operation's error to the next one.

Upstream files: grew

File Change
server/src/dtos/time-bucket.dto.ts +2: one .extend
server/src/repositories/asset.repository.ts +3/−1: the options type, one call
server/src/repositories/search.repository.ts +2/−1: the options type
web/src/lib/services/asset.service.ts +5/−1: one call in "View in timeline"
web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte one condition swapped
mobile/lib/presentation/pages/dev/main_timeline.page.dart +5/−5: the app-bar action and its comment
mobile/lib/presentation/widgets/timeline/timeline.widget.dart one comment
mobile/lib/domain/models/store.model.dart +2: the store key and its comment
mobile/lib/infrastructure/repositories/timeline.repository.dart +1: one import (AGENTS.md updated)
mobile/lib/domain/services/timeline.service.dart +9/−1: the view threaded to the queries

server/src/services/timeline.service.ts is untouched.

@Deeds67 Deeds67 added the changelog:feat Feature change for changelog label Oct 27, 2026
@defts
defts force-pushed the feat/library-view branch 3 times, most recently from 242c482 to 2fd77e6 Compare October 29, 2026 07:42
Photos can now show only your personal photos (yours, in no space) or
only what reaches you through your spaces, yours included, like the
library view of Apple Photos. Both stays the default, and the choice only
shows once one of your spaces is on your timeline.

The server takes an optional libraryView on the timeline, the flat search
fields and POST /search/options, so search counts match the results. Web
and mobile remember the choice, and mobile filters its local timeline the
same way. On mobile, the Photos grouping chip becomes a display menu
holding the view and the grouping, and browsing in search mode stays
within the view; a text search spans every photo.

Design: specs/2026-10-26-library-view-design.md
See open-noodle#1038.
@defts
defts force-pushed the feat/library-view branch from 2fd77e6 to 312a3f5 Compare October 31, 2026 21:31
@Deeds67

Deeds67 commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator

@defts I really like the idea. The only thing that I would like to change is the location of the filter. I find that it adds a bit too much noise to the main timeline. How about we move these buttons into the filter panel? On mobile it looks fine to me.

I'm referring to this part

image

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

2 participants