GNOME Photo Companion (gnome-photo-com panion) — Design Document and Prototype in C/M eson

Ole Aamot <[email protected]> Sat, 11 Jul 2026 03:48:36 +0200
Newsgroups gmane.comp.multimedia.gphoto.devel
Message-ID <CAFtywu6UDK56hCp7FcOJ5sWxJ8JOwz7zEMmLTjB+gr=nqa9onQ@mail.gmail.com>
--===============5984023267519999655==
Content-Type: multipart/alternative; boundary="00000000000069b25306564c10bc"

--00000000000069b25306564c10bc
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

GNOME Photo Companion (gnome-photo-companion) =E2=80=94 Design Document and
Prototype in C/Meson1. Purpose

GNOME Photo Companion (working ID: gnome-photo-companion) is a standalone
GTK4/Libadwaita application that complements *GNOME Photos* with two
features Photos doesn't cover well today:

   1. *Photo Location Map* =E2=80=94 plot geotagged photos on an
   OpenStreetMap-based map, using EXIF GPS tags.
   2. *EXIF Search* =E2=80=94 fast, indexed search over EXIF/IPTC metadata =
(camera,
   lens, date, GPS presence, focal length, ISO, etc.) across a photo librar=
y.

Storage is *local-only* for this iteration: the library root is
~/Pictures (XDG_PICTURES_DIR),
with no cloud backend. The architecture reserves a clean seam for a cloud
provider later (GNOME Online Accounts / WebDAV), but nothing cloud-related
ships in v0.

It is designed to sit next to the existing GNU Photo work (the gphoto C
dispatcher bridging libgphoto2 and gnome-photos): Companion doesn't talk to
cameras directly, it indexes whatever lands in ~/Pictures (including files
gphotohas already downloaded there).
2. Non-goals (v0)

   - No image editing.
   - No cloud sync/backend (Nextcloud, GOA, etc.) =E2=80=94 reserved for a =
later
   phase.
   - No camera/device access =E2=80=94 that's gphoto's job.
   - Not a GNOME Photos plugin/extension; it's a companion app that reads
   the same photo library and can hand off "open in Photos" via
   GAppLaunchContext.

3. High-level architecture

=E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90
=E2=94=82                      GpcApplication (AdwApplication)          =E2=
=94=82
=E2=94=82                                                                 =
=E2=94=82
=E2=94=82   =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90        =E2=94=
=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=90 =E2=94=82
=E2=94=82   =E2=94=82     GpcWindow       =E2=94=82        =E2=94=82      G=
pcPhotoStore        =E2=94=82 =E2=94=82
=E2=94=82   =E2=94=82  (AdwApplicationWin) =E2=94=82=E2=97=84=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=96=BA=E2=94=82  scans ~/Pictu=
res via     =E2=94=82 =E2=94=82
=E2=94=82   =E2=94=82  - search entry      =E2=94=82        =E2=94=82  GFil=
eEnumerator +        =E2=94=82 =E2=94=82
=E2=94=82   =E2=94=82  - results grid       =E2=94=82        =E2=94=82  GFi=
leMonitor (live)      =E2=94=82 =E2=94=82
=E2=94=82   =E2=94=82  - map / grid toggle  =E2=94=82        =E2=94=94=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=AC=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=98 =E2=94=82
=E2=94=82   =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=AC=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=98                    =E2=94=82                =E2=94=82
=E2=94=82             =E2=94=82                                  =E2=96=BC =
               =E2=94=82
=E2=94=82             =E2=94=82                       =E2=94=8C=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90   =E2=94=82
=E2=94=82             =E2=94=82                       =E2=94=82   GpcExifIn=
dex         =E2=94=82   =E2=94=82
=E2=94=82             =E2=94=82                       =E2=94=82  gexiv2 ext=
raction +   =E2=94=82   =E2=94=82
=E2=94=82             =E2=94=82                       =E2=94=82  SQLite cac=
he/index     =E2=94=82   =E2=94=82
=E2=94=82             =E2=94=82                       =E2=94=94=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=AC=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98   =E2=94=82
=E2=94=82             =E2=96=BC                                   =E2=94=82=
                =E2=94=82
=E2=94=82   =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90               =
         =E2=94=82                =E2=94=82
=E2=94=82   =E2=94=82    GpcMapView        =E2=94=82=E2=97=84=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98                =E2=
=94=82
=E2=94=82   =E2=94=82  libshumate widget    =E2=94=82  (queries index for G=
PS-tagged photos)  =E2=94=82
=E2=94=82   =E2=94=82  OSM tile source       =E2=94=82                     =
                   =E2=94=82
=E2=94=82   =E2=94=82  marker layer per photo=E2=94=82                     =
                   =E2=94=82
=E2=94=82   =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98               =
                           =E2=94=82
=E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98

Components
Component Responsibility
GpcApplication AdwApplication subclass; owns the single GpcPhotoStore +
GpcExifIndex.
GpcWindow AdwApplicationWindow; hosts AdwViewStack with two views: *Grid*
 and *Map*. Search bar filters both.
GpcPhotoStore Walks ~/Pictures recursively (async, GFile/GFileEnumerator),
watches for changes with GFileMonitor, emits photo-added/photo-removed.
GpcExifIndex On photo-added, extracts EXIF/IPTC/XMP via *gexiv2* and
upserts a row into a local *SQLite* cache (~/.cache/photo-companion/exif.db=
).
Exposes async search queries (GpcExifQuery).
GpcMapView Wraps *libshumate* (ShumateMap + ShumateMarkerLayer), OSM raster
tile source (shumate_map_source_registry default OSM entry). Subscribes to
index query results and drops one marker per GPS-tagged photo; clicking a
marker selects it in the grid.
GpcSearchQuery Small struct/GObject describing a parsed search (free text +
structured filters like camera:, lens:, has-gps:true, date>=3D). Parsed fro=
m
the search entry text.
Data flow

   1. GpcPhotoStore enumerates ~/Pictures, filtered by MIME type (image/jpe=
g
   , image/tiff, image/png, common raw types).
   2. Each discovered file is queued to GpcExifIndex, which runs
gexiv2 extraction
   on a worker thread (GTask), then writes to SQLite.
   3. GpcWindow's search entry produces a GpcSearchQuery; both the grid
   view and GpcMapView re-run the query against GpcExifIndex and re-render.
   4. GFileMonitor events keep the index (and therefore both views) live as
   photos are added/removed/edited on disk =E2=80=94 e.g. right after gphot=
o downloads
   new images.

4. EXIF search =E2=80=94 supported filters (v0)

Free text matches filename + camera make/model. Structured tokens:

   - camera:"Canon EOS R5"
   - lens:"RF 24-70mm"
   - iso:>=3D800
   - date:2025-06-01..2025-06-30
   - has-gps:true / has-gps:false
   - orientation:portrait|landscape

These map to a WHERE clause built against indexed columns (camera_make,
camera_model, lens, iso, taken_at, gps_lat, gps_lon, width, height).
5. Map view

   - Library: *libshumate* (the GTK4 map widget used by GNOME Maps), giving
   GTK4-native OSM rendering, clustering, and marker layers for free.
   - Tile source: default OpenStreetMap raster source from
   shumate_map_source_registry_new_with_defaults()=E2=80=94 respects OSM ti=
le usage
   policy, no API key required for v0.
   - Marker clustering (ShumateMapMarker grouping) recommended once library
   size grows, deferred to v1.
   - Clicking a marker calls gpc_window_select_photo(), syncing the grid's
   selection and opening a detail popover with a "Open in Photos" button (
   g_app_info_launch_default_for_uri scoped to org.gnome.Photos).

6. Storage layout

~/Pictures/                        # library root (XDG_PICTURES_DIR)
~/.cache/photo-companion/exif.db   # SQLite EXIF index (rebuildable cache)
~/.config/photo-companion/         # GSettings-backed prefs (schema below)

The index is a *cache*, not a source of truth =E2=80=94 it can always be
regenerated by re-scanning ~/Pictures. This matters for the future cloud
phase: a cloud backend would just become a second GpcPhotoSource implementa=
tion
feeding the same GpcExifIndex.
7. Reserved seam for cloud storage (not built now)

To avoid a rewrite later, GpcPhotoStore should be defined against a small
GpcPhotoSource interface (GListModel-based) with GpcLocalPhotoSource as the
only implementation in v0. A future GpcCloudPhotoSource (GOA- or
WebDAV-backed) would implement the same interface. *Not implemented in this
pass* =E2=80=94 noted here only so the local-only code doesn't need to be
restructured later.
8. Dependencies
Library Purpose
gtk4 (>=3D 4.14) UI toolkit
libadwaita (>=3D 1.5) GNOME HIG widgets
libshumate (>=3D 1.2) OSM map widget
gexiv2 EXIF/IPTC/XMP extraction
sqlite3 EXIF index cache
glib/gio Core, async I/O, file monitoring

Build system: *Meson*, matching standard GNOME project conventions (and the
gphoto dispatcher's own build).
9. Directory layout (this repo)

gnome-photo-companion/
=E2=94=9C=E2=94=80=E2=94=80 meson.build
=E2=94=9C=E2=94=80=E2=94=80 DESIGN.md
=E2=94=9C=E2=94=80=E2=94=80 src/
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 meson.build
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 main.c
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 gpc-application.[ch]
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 gpc-window.[ch]
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 gpc-photo-store.[ch]
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 gpc-exif-index.[ch]
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 gpc-map-view.[ch]
=E2=94=82   =E2=94=94=E2=94=80=E2=94=80 gpc-search-query.[ch]
=E2=94=94=E2=94=80=E2=94=80 data/
    =E2=94=9C=E2=94=80=E2=94=80 meson.build
    =E2=94=9C=E2=94=80=E2=94=80 org.gnome.PhotoCompanion.desktop.in
<http://org.gnome.photocompanion.desktop.in/>
    =E2=94=9C=E2=94=80=E2=94=80 org.gnome.PhotoCompanion.gschema.xml
    =E2=94=94=E2=94=80=E2=94=80 resources/
        =E2=94=9C=E2=94=80=E2=94=80 gpc-window.ui
        =E2=94=94=E2=94=80=E2=94=80 photo-companion.gresource.xml

10. Roadmap

   - *v0 (this pass)*: local scan of ~/Pictures, EXIF index + search, OSM
   map view, grid view, live file monitoring.
   - *v1*: marker clustering, thumbnail caching (via gnome-desktop-thumbnai=
l),
   saved searches, GNOME Photos hand-off polish.
   - *v2*: GpcCloudPhotoSource (GOA-backed), background sync, conflict
   handling for duplicate GPS/EXIF edits.

10. Prototype

   - *v0: *
   https://www.aamot.org/ole/software/gphoto/gnome-photo-companion.tar


Best,
Ole Aamot
[email protected]
www.aamot.org

--00000000000069b25306564c10bc
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

<div dir=3D"ltr"><div style=3D"color:rgb(0,0,0);font-family:Helvetica;font-=
size:12px"><h1 class=3D"gmail-text-text-100 gmail-mt-3 gmail--mb-1 gmail-te=
xt-[1.375rem] gmail-font-bold">GNOME Photo Companion (gnome-photo-companion=
) =E2=80=94 Design Document and Prototype in C/Meson</h1><h2 class=3D"gmail=
-text-text-100 gmail-mt-3 gmail--mb-1 gmail-text-[1.125rem] gmail-font-bold=
">1. Purpose</h2><p class=3D"gmail-font-claude-response-body gmail-break-wo=
rds gmail-whitespace-normal" style=3D"font-size:medium">GNOME Photo Compani=
on (working ID:=C2=A0<font face=3D"monospace">gnome-photo-companion</font>)=
 is a standalone GTK4/Libadwaita application that complements=C2=A0<strong>=
GNOME Photos</strong>=C2=A0with two features Photos doesn&#39;t cover well =
today:</p><ol class=3D"gmail-[li_&amp;]:mb-0 gmail-[li_&amp;]:mt-1 gmail-[l=
i_&amp;]:gap-1 gmail-[&amp;:not(:last-child)_ul]:pb-1 gmail-[&amp;:not(:las=
t-child)_ol]:pb-1 gmail-list-decimal gmail-flex gmail-flex-col gmail-gap-1 =
gmail-pl-8 gmail-mb-3" style=3D"font-size:medium"><li class=3D"gmail-font-c=
laude-response-body gmail-whitespace-normal gmail-break-words gmail-pl-2"><=
strong>Photo Location Map</strong>=C2=A0=E2=80=94 plot geotagged photos on =
an OpenStreetMap-based map, using EXIF GPS tags.</li><li class=3D"gmail-fon=
t-claude-response-body gmail-whitespace-normal gmail-break-words gmail-pl-2=
"><strong>EXIF Search</strong>=C2=A0=E2=80=94 fast, indexed search over EXI=
F/IPTC metadata (camera, lens, date, GPS presence, focal length, ISO, etc.)=
 across a photo library.</li></ol><p class=3D"gmail-font-claude-response-bo=
dy gmail-break-words gmail-whitespace-normal" style=3D"font-size:medium">St=
orage is=C2=A0<strong>local-only</strong>=C2=A0for this iteration: the libr=
ary root is=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-bord=
er-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-w=
rap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">~/Pi=
ctures</code>=C2=A0(XDG_PICTURES_DIR), with no cloud backend. The architect=
ure reserves a clean seam for a cloud provider later (GNOME Online Accounts=
 / WebDAV), but nothing cloud-related ships in v0.</p><p class=3D"gmail-fon=
t-claude-response-body gmail-break-words gmail-whitespace-normal" style=3D"=
font-size:medium">It is designed to sit next to the existing GNU Photo work=
 (the=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5=
 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gm=
ail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">gphoto</co=
de>=C2=A0C dispatcher bridging=C2=A0<code class=3D"gmail-bg-text-200/5 gmai=
l-border gmail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gma=
il-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-=
text-[0.9rem]">libgphoto2</code>=C2=A0and=C2=A0<code class=3D"gmail-bg-text=
-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-dan=
ger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-p=
y-px gmail-text-[0.9rem]">gnome-photos</code>): Companion doesn&#39;t talk =
to cameras directly, it indexes whatever lands in=C2=A0<code class=3D"gmail=
-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-=
text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1=
 gmail-py-px gmail-text-[0.9rem]">~/Pictures</code>=C2=A0(including files=
=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmai=
l-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-r=
ounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">gphoto</code>ha=
s already downloaded there).</p><h2 class=3D"gmail-text-text-100 gmail-mt-3=
 gmail--mb-1 gmail-text-[1.125rem] gmail-font-bold">2. Non-goals (v0)</h2><=
ul class=3D"gmail-[li_&amp;]:mb-0 gmail-[li_&amp;]:mt-1 gmail-[li_&amp;]:ga=
p-1 gmail-[&amp;:not(:last-child)_ul]:pb-1 gmail-[&amp;:not(:last-child)_ol=
]:pb-1 gmail-list-disc gmail-flex gmail-flex-col gmail-gap-1 gmail-pl-8 gma=
il-mb-3" style=3D"font-size:medium"><li class=3D"gmail-font-claude-response=
-body gmail-whitespace-normal gmail-break-words gmail-pl-2">No image editin=
g.</li><li class=3D"gmail-font-claude-response-body gmail-whitespace-normal=
 gmail-break-words gmail-pl-2">No cloud sync/backend (Nextcloud, GOA, etc.)=
 =E2=80=94 reserved for a later phase.</li><li class=3D"gmail-font-claude-r=
esponse-body gmail-whitespace-normal gmail-break-words gmail-pl-2">No camer=
a/device access =E2=80=94 that&#39;s=C2=A0<code class=3D"gmail-bg-text-200/=
5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-danger-0=
00 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px =
gmail-text-[0.9rem]">gphoto</code>&#39;s job.</li><li class=3D"gmail-font-c=
laude-response-body gmail-whitespace-normal gmail-break-words gmail-pl-2">N=
ot a GNOME Photos plugin/extension; it&#39;s a companion app that reads the=
 same photo library and can hand off &quot;open in Photos&quot; via=C2=A0<c=
ode class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border=
-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[=
0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GAppLaunchContext</code=
>.</li></ul><h2 class=3D"gmail-text-text-100 gmail-mt-3 gmail--mb-1 gmail-t=
ext-[1.125rem] gmail-font-bold">3. High-level architecture</h2><div role=3D=
"group" aria-label=3D"Code" tabindex=3D"0" class=3D"gmail-relative gmail-gr=
oup/copy gmail-bg-bg-000/50 gmail-border-0.5 gmail-border-border-400 gmail-=
rounded-lg gmail-focus:outline-none gmail-focus-visible:ring-2 gmail-focus-=
visible:ring-accent-100" style=3D"font-size:medium"><div class=3D"gmail-sti=
cky gmail-opacity-0 gmail-group-hover/copy:opacity-100 gmail-group-focus-wi=
thin/copy:opacity-100 gmail-top-2 gmail-py-2 gmail-h-12 gmail-w-0 gmail-flo=
at-right"><div class=3D"gmail-absolute gmail-right-0 gmail-h-8 gmail-px-2 g=
mail-items-center gmail-inline-flex gmail-z-10"></div></div><div class=3D"g=
mail-overflow-x-auto"><pre class=3D"gmail-code-block__code gmail-!my-0 gmai=
l-!rounded-lg gmail-!text-sm gmail-!leading-relaxed gmail-p-3.5" style=3D"c=
olor:rgb(20,24,31);background-image:none;background-position:0% 0%;backgrou=
nd-size:auto;background-repeat:repeat;background-origin:padding-box;backgro=
und-clip:border-box;font-family:var(--font-mono)"><code style=3D"background=
-image:none;background-position:0% 0%;background-size:auto;background-repea=
t:repeat;background-origin:padding-box;background-clip:border-box;font-fami=
ly:var(--font-mono);white-space:pre-wrap">=E2=94=8C=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=90
=E2=94=82                      GpcApplication (AdwApplication)          =E2=
=94=82
=E2=94=82                                                                 =
=E2=94=82
=E2=94=82   =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90        =E2=94=
=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=90 =E2=94=82
=E2=94=82   =E2=94=82     GpcWindow       =E2=94=82        =E2=94=82      G=
pcPhotoStore        =E2=94=82 =E2=94=82
=E2=94=82   =E2=94=82  (AdwApplicationWin) =E2=94=82=E2=97=84=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=96=BA=E2=94=82  scans ~/Pictu=
res via     =E2=94=82 =E2=94=82
=E2=94=82   =E2=94=82  - search entry      =E2=94=82        =E2=94=82  GFil=
eEnumerator +        =E2=94=82 =E2=94=82
=E2=94=82   =E2=94=82  - results grid       =E2=94=82        =E2=94=82  GFi=
leMonitor (live)      =E2=94=82 =E2=94=82
=E2=94=82   =E2=94=82  - map / grid toggle  =E2=94=82        =E2=94=94=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=AC=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=98 =E2=94=82
=E2=94=82   =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=AC=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=98                    =E2=94=82                =E2=94=82
=E2=94=82             =E2=94=82                                  =E2=96=BC =
               =E2=94=82
=E2=94=82             =E2=94=82                       =E2=94=8C=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90   =E2=94=82
=E2=94=82             =E2=94=82                       =E2=94=82   GpcExifIn=
dex         =E2=94=82   =E2=94=82
=E2=94=82             =E2=94=82                       =E2=94=82  gexiv2 ext=
raction +   =E2=94=82   =E2=94=82
=E2=94=82             =E2=94=82                       =E2=94=82  SQLite cac=
he/index     =E2=94=82   =E2=94=82
=E2=94=82             =E2=94=82                       =E2=94=94=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=AC=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98   =E2=94=82
=E2=94=82             =E2=96=BC                                   =E2=94=82=
                =E2=94=82
=E2=94=82   =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90               =
         =E2=94=82                =E2=94=82
=E2=94=82   =E2=94=82    GpcMapView        =E2=94=82=E2=97=84=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98                =E2=
=94=82
=E2=94=82   =E2=94=82  libshumate widget    =E2=94=82  (queries index for G=
PS-tagged photos)  =E2=94=82
=E2=94=82   =E2=94=82  OSM tile source       =E2=94=82                     =
                   =E2=94=82
=E2=94=82   =E2=94=82  marker layer per photo=E2=94=82                     =
                   =E2=94=82
=E2=94=82   =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98               =
                           =E2=94=82
=E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98</code></pre></div></div><h3 clas=
s=3D"gmail-text-text-100 gmail-mt-2 gmail--mb-1 gmail-text-base gmail-font-=
bold">Components</h3><div class=3D"gmail-overflow-x-auto gmail-w-full gmail=
-px-2 gmail-mb-6" style=3D"font-size:medium"><table class=3D"gmail-min-w-fu=
ll gmail-border-collapse gmail-text-sm gmail-leading-[1.7] gmail-whitespace=
-normal"><thead class=3D"gmail-text-left"><tr><th scope=3D"col" class=3D"gm=
ail-text-text-100 gmail-border-b-0.5 gmail-border-[hsl(var(--border-300)/0.=
6)] gmail-py-2 gmail-pr-4 gmail-align-top gmail-font-bold">Component</th><t=
h scope=3D"col" class=3D"gmail-text-text-100 gmail-border-b-0.5 gmail-borde=
r-[hsl(var(--border-300)/0.6)] gmail-py-2 gmail-pr-4 gmail-align-top gmail-=
font-bold">Responsibility</th></tr></thead><tbody><tr><td class=3D"gmail-bo=
rder-b-0.5 gmail-border-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 =
gmail-align-top"><code class=3D"gmail-bg-text-200/5 gmail-border gmail-bord=
er-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-w=
rap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GpcA=
pplication</code></td><td class=3D"gmail-border-b-0.5 gmail-border-[hsl(var=
(--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top"><code class=3D"=
gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 g=
mail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail=
-px-1 gmail-py-px gmail-text-[0.9rem]">AdwApplication</code>=C2=A0subclass;=
 owns the single=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail=
-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-=
pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]"=
>GpcPhotoStore</code>=C2=A0+=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-=
border gmail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail=
-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-te=
xt-[0.9rem]">GpcExifIndex</code>.</td></tr><tr><td class=3D"gmail-border-b-=
0.5 gmail-border-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-a=
lign-top"><code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 =
gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gma=
il-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GpcWindow</=
code></td><td class=3D"gmail-border-b-0.5 gmail-border-[hsl(var(--border-30=
0)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top"><code class=3D"gmail-bg-tex=
t-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-da=
nger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-=
py-px gmail-text-[0.9rem]">AdwApplicationWindow</code>; hosts=C2=A0<code cl=
ass=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-borde=
r-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem=
] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">AdwViewStack</code>=C2=A0with=
 two views:=C2=A0<strong>Grid</strong>=C2=A0and=C2=A0<strong>Map</strong>. =
Search bar filters both.</td></tr><tr><td class=3D"gmail-border-b-0.5 gmail=
-border-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top"=
><code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-bor=
der-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounde=
d-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GpcPhotoStore</code>=
</td><td class=3D"gmail-border-b-0.5 gmail-border-[hsl(var(--border-300)/0.=
3)] gmail-py-2 gmail-pr-4 gmail-align-top">Walks=C2=A0<code class=3D"gmail-=
bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-t=
ext-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 =
gmail-py-px gmail-text-[0.9rem]">~/Pictures</code>=C2=A0recursively (async,=
=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmai=
l-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-r=
ounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GFile</code>/<c=
ode class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border=
-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[=
0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GFileEnumerator</code>)=
, watches for changes with=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-bo=
rder gmail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-w=
hitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text=
-[0.9rem]">GFileMonitor</code>, emits=C2=A0<code class=3D"gmail-bg-text-200=
/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-danger-=
000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px=
 gmail-text-[0.9rem]">photo-added</code>/<code class=3D"gmail-bg-text-200/5=
 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-danger-00=
0 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px g=
mail-text-[0.9rem]">photo-removed</code>.</td></tr><tr><td class=3D"gmail-b=
order-b-0.5 gmail-border-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4=
 gmail-align-top"><code class=3D"gmail-bg-text-200/5 gmail-border gmail-bor=
der-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-=
wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">Gpc=
ExifIndex</code></td><td class=3D"gmail-border-b-0.5 gmail-border-[hsl(var(=
--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top">On=C2=A0<code cl=
ass=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-borde=
r-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem=
] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">photo-added</code>, extracts =
EXIF/IPTC/XMP via=C2=A0<strong>gexiv2</strong>=C2=A0and upserts a row into =
a local=C2=A0<strong>SQLite</strong>=C2=A0cache (<code class=3D"gmail-bg-te=
xt-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-d=
anger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail=
-py-px gmail-text-[0.9rem]">~/.cache/photo-companion/exif.db</code>). Expos=
es async search queries (<code class=3D"gmail-bg-text-200/5 gmail-border gm=
ail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespa=
ce-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9re=
m]">GpcExifQuery</code>).</td></tr><tr><td class=3D"gmail-border-b-0.5 gmai=
l-border-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top=
"><code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-bo=
rder-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-round=
ed-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GpcMapView</code></=
td><td class=3D"gmail-border-b-0.5 gmail-border-[hsl(var(--border-300)/0.3)=
] gmail-py-2 gmail-pr-4 gmail-align-top">Wraps=C2=A0<strong>libshumate</str=
ong>=C2=A0(<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5=
 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gm=
ail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">ShumateMap=
</code>=C2=A0+=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-b=
order-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pr=
e-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">S=
humateMarkerLayer</code>), OSM raster tile source (<code class=3D"gmail-bg-=
text-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text=
-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gma=
il-py-px gmail-text-[0.9rem]">shumate_map_source_registry</code>=C2=A0defau=
lt OSM entry). Subscribes to index query results and drops one marker per G=
PS-tagged photo; clicking a marker selects it in the grid.</td></tr><tr><td=
 class=3D"gmail-border-b-0.5 gmail-border-[hsl(var(--border-300)/0.3)] gmai=
l-py-2 gmail-pr-4 gmail-align-top"><code class=3D"gmail-bg-text-200/5 gmail=
-border gmail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmai=
l-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-t=
ext-[0.9rem]">GpcSearchQuery</code></td><td class=3D"gmail-border-b-0.5 gma=
il-border-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-to=
p">Small struct/GObject describing a parsed search (free text + structured =
filters like=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-bor=
der-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-=
wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">cam=
era:</code>,=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-bor=
der-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-=
wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">len=
s:</code>,=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-borde=
r-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wr=
ap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">has-g=
ps:true</code>,=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-=
border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-p=
re-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">=
date&gt;=3D</code>). Parsed from the search entry text.</td></tr></tbody></=
table></div><h3 class=3D"gmail-text-text-100 gmail-mt-2 gmail--mb-1 gmail-t=
ext-base gmail-font-bold">Data flow</h3><ol class=3D"gmail-[li_&amp;]:mb-0 =
gmail-[li_&amp;]:mt-1 gmail-[li_&amp;]:gap-1 gmail-[&amp;:not(:last-child)_=
ul]:pb-1 gmail-[&amp;:not(:last-child)_ol]:pb-1 gmail-list-decimal gmail-fl=
ex gmail-flex-col gmail-gap-1 gmail-pl-8 gmail-mb-3" style=3D"font-size:med=
ium"><li class=3D"gmail-font-claude-response-body gmail-whitespace-normal g=
mail-break-words gmail-pl-2"><code class=3D"gmail-bg-text-200/5 gmail-borde=
r gmail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whit=
espace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0=
.9rem]">GpcPhotoStore</code>=C2=A0enumerates=C2=A0<code class=3D"gmail-bg-t=
ext-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-=
danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmai=
l-py-px gmail-text-[0.9rem]">~/Pictures</code>, filtered by MIME type (<cod=
e class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-b=
order-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.=
4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">image/jpeg</code>,=C2=A0<=
code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-borde=
r-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-=
[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">image/tiff</code>,=C2=
=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-b=
order-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-roun=
ded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">image/png</code>, =
common raw types).</li><li class=3D"gmail-font-claude-response-body gmail-w=
hitespace-normal gmail-break-words gmail-pl-2">Each discovered file is queu=
ed to=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5=
 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gm=
ail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GpcExifInd=
ex</code>, which runs=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border =
gmail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whites=
pace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9=
rem]">gexiv2</code>=C2=A0extraction on a worker thread (<code class=3D"gmai=
l-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail=
-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-=
1 gmail-py-px gmail-text-[0.9rem]">GTask</code>), then writes to SQLite.</l=
i><li class=3D"gmail-font-claude-response-body gmail-whitespace-normal gmai=
l-break-words gmail-pl-2"><code class=3D"gmail-bg-text-200/5 gmail-border g=
mail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitesp=
ace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9r=
em]">GpcWindow</code>&#39;s search entry produces a=C2=A0<code class=3D"gma=
il-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmai=
l-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px=
-1 gmail-py-px gmail-text-[0.9rem]">GpcSearchQuery</code>; both the grid vi=
ew and=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.=
5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap g=
mail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GpcMapVie=
w</code>=C2=A0re-run the query against=C2=A0<code class=3D"gmail-bg-text-20=
0/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-danger=
-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-p=
x gmail-text-[0.9rem]">GpcExifIndex</code>=C2=A0and re-render.</li><li clas=
s=3D"gmail-font-claude-response-body gmail-whitespace-normal gmail-break-wo=
rds gmail-pl-2"><code class=3D"gmail-bg-text-200/5 gmail-border gmail-borde=
r-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wr=
ap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GFile=
Monitor</code>=C2=A0events keep the index (and therefore both views) live a=
s photos are added/removed/edited on disk =E2=80=94 e.g. right after=C2=A0<=
code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-borde=
r-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-=
[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">gphoto</code>=C2=A0dow=
nloads new images.</li></ol><h2 class=3D"gmail-text-text-100 gmail-mt-3 gma=
il--mb-1 gmail-text-[1.125rem] gmail-font-bold">4. EXIF search =E2=80=94 su=
pported filters (v0)</h2><p class=3D"gmail-font-claude-response-body gmail-=
break-words gmail-whitespace-normal" style=3D"font-size:medium">Free text m=
atches filename + camera make/model. Structured tokens:</p><ul class=3D"gma=
il-[li_&amp;]:mb-0 gmail-[li_&amp;]:mt-1 gmail-[li_&amp;]:gap-1 gmail-[&amp=
;:not(:last-child)_ul]:pb-1 gmail-[&amp;:not(:last-child)_ol]:pb-1 gmail-li=
st-disc gmail-flex gmail-flex-col gmail-gap-1 gmail-pl-8 gmail-mb-3" style=
=3D"font-size:medium"><li class=3D"gmail-font-claude-response-body gmail-wh=
itespace-normal gmail-break-words gmail-pl-2"><code class=3D"gmail-bg-text-=
200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-dang=
er-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py=
-px gmail-text-[0.9rem]">camera:&quot;Canon EOS R5&quot;</code></li><li cla=
ss=3D"gmail-font-claude-response-body gmail-whitespace-normal gmail-break-w=
ords gmail-pl-2"><code class=3D"gmail-bg-text-200/5 gmail-border gmail-bord=
er-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-w=
rap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">lens=
:&quot;RF 24-70mm&quot;</code></li><li class=3D"gmail-font-claude-response-=
body gmail-whitespace-normal gmail-break-words gmail-pl-2"><code class=3D"g=
mail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gm=
ail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-=
px-1 gmail-py-px gmail-text-[0.9rem]">iso:&gt;=3D800</code></li><li class=
=3D"gmail-font-claude-response-body gmail-whitespace-normal gmail-break-wor=
ds gmail-pl-2"><code class=3D"gmail-bg-text-200/5 gmail-border gmail-border=
-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wra=
p gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">date:2=
025-06-01..2025-06-30</code></li><li class=3D"gmail-font-claude-response-bo=
dy gmail-whitespace-normal gmail-break-words gmail-pl-2"><code class=3D"gma=
il-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmai=
l-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px=
-1 gmail-py-px gmail-text-[0.9rem]">has-gps:true</code>=C2=A0/=C2=A0<code c=
lass=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-bord=
er-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4re=
m] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">has-gps:false</code></li><li=
 class=3D"gmail-font-claude-response-body gmail-whitespace-normal gmail-bre=
ak-words gmail-pl-2"><code class=3D"gmail-bg-text-200/5 gmail-border gmail-=
border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-p=
re-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">=
orientation:portrait|landscape</code></li></ul><p class=3D"gmail-font-claud=
e-response-body gmail-break-words gmail-whitespace-normal" style=3D"font-si=
ze:medium">These map to a=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-bor=
der gmail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-wh=
itespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-=
[0.9rem]">WHERE</code>=C2=A0clause built against indexed columns (<code cla=
ss=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border=
-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem]=
 gmail-px-1 gmail-py-px gmail-text-[0.9rem]">camera_make</code>,=C2=A0<code=
 class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-bo=
rder-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4=
rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">camera_model</code>,=C2=A0=
<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-bord=
er-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded=
-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">lens</code>,=C2=A0<co=
de class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-=
border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0=
.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">iso</code>,=C2=A0<code c=
lass=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-bord=
er-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4re=
m] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">taken_at</code>,=C2=A0<code =
class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-bor=
der-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4r=
em] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">gps_lat</code>,=C2=A0<code =
class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-bor=
der-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4r=
em] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">gps_lon</code>,=C2=A0<code =
class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-bor=
der-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4r=
em] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">width</code>,=C2=A0<code cl=
ass=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-borde=
r-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem=
] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">height</code>).</p><h2 class=
=3D"gmail-text-text-100 gmail-mt-3 gmail--mb-1 gmail-text-[1.125rem] gmail-=
font-bold">5. Map view</h2><ul class=3D"gmail-[li_&amp;]:mb-0 gmail-[li_&am=
p;]:mt-1 gmail-[li_&amp;]:gap-1 gmail-[&amp;:not(:last-child)_ul]:pb-1 gmai=
l-[&amp;:not(:last-child)_ol]:pb-1 gmail-list-disc gmail-flex gmail-flex-co=
l gmail-gap-1 gmail-pl-8 gmail-mb-3" style=3D"font-size:medium"><li class=
=3D"gmail-font-claude-response-body gmail-whitespace-normal gmail-break-wor=
ds gmail-pl-2">Library:=C2=A0<strong>libshumate</strong>=C2=A0(the GTK4 map=
 widget used by GNOME Maps), giving GTK4-native OSM rendering, clustering, =
and marker layers for free.</li><li class=3D"gmail-font-claude-response-bod=
y gmail-whitespace-normal gmail-break-words gmail-pl-2">Tile source: defaul=
t OpenStreetMap raster source from=C2=A0<code class=3D"gmail-bg-text-200/5 =
gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-danger-000=
 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gm=
ail-text-[0.9rem]">shumate_map_source_registry_new_with_defaults()</code>=
=E2=80=94 respects OSM tile usage policy, no API key required for v0.</li><=
li class=3D"gmail-font-claude-response-body gmail-whitespace-normal gmail-b=
reak-words gmail-pl-2">Marker clustering (<code class=3D"gmail-bg-text-200/=
5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-danger-0=
00 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px =
gmail-text-[0.9rem]">ShumateMapMarker</code>=C2=A0grouping) recommended onc=
e library size grows, deferred to v1.</li><li class=3D"gmail-font-claude-re=
sponse-body gmail-whitespace-normal gmail-break-words gmail-pl-2">Clicking =
a marker calls=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-b=
order-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pr=
e-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">g=
pc_window_select_photo()</code>, syncing the grid&#39;s selection and openi=
ng a detail popover with a &quot;Open in Photos&quot; button (<code class=
=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border-3=
00 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] g=
mail-px-1 gmail-py-px gmail-text-[0.9rem]">g_app_info_launch_default_for_ur=
i</code>=C2=A0scoped to=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-borde=
r gmail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whit=
espace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0=
.9rem]">org.gnome.Photos</code>).</li></ul><h2 class=3D"gmail-text-text-100=
 gmail-mt-3 gmail--mb-1 gmail-text-[1.125rem] gmail-font-bold">6. Storage l=
ayout</h2><div role=3D"group" aria-label=3D"Code" tabindex=3D"0" class=3D"g=
mail-relative gmail-group/copy gmail-bg-bg-000/50 gmail-border-0.5 gmail-bo=
rder-border-400 gmail-rounded-lg gmail-focus:outline-none gmail-focus-visib=
le:ring-2 gmail-focus-visible:ring-accent-100" style=3D"font-size:medium"><=
div class=3D"gmail-sticky gmail-opacity-0 gmail-group-hover/copy:opacity-10=
0 gmail-group-focus-within/copy:opacity-100 gmail-top-2 gmail-py-2 gmail-h-=
12 gmail-w-0 gmail-float-right"><div class=3D"gmail-absolute gmail-right-0 =
gmail-h-8 gmail-px-2 gmail-items-center gmail-inline-flex gmail-z-10"></div=
></div><div class=3D"gmail-overflow-x-auto"><pre class=3D"gmail-code-block_=
_code gmail-!my-0 gmail-!rounded-lg gmail-!text-sm gmail-!leading-relaxed g=
mail-p-3.5" style=3D"color:rgb(20,24,31);background-image:none;background-p=
osition:0% 0%;background-size:auto;background-repeat:repeat;background-orig=
in:padding-box;background-clip:border-box;font-family:var(--font-mono)"><co=
de style=3D"background-image:none;background-position:0% 0%;background-size=
:auto;background-repeat:repeat;background-origin:padding-box;background-cli=
p:border-box;font-family:var(--font-mono);white-space:pre-wrap">~/Pictures/=
                        # library root (XDG_PICTURES_DIR)
~/.cache/photo-companion/exif.db   # SQLite EXIF index (rebuildable cache)
~/.config/photo-companion/         # GSettings-backed prefs (schema below)<=
/code></pre></div></div><p class=3D"gmail-font-claude-response-body gmail-b=
reak-words gmail-whitespace-normal" style=3D"font-size:medium">The index is=
 a=C2=A0<strong>cache</strong>, not a source of truth =E2=80=94 it can alwa=
ys be regenerated by re-scanning=C2=A0<code class=3D"gmail-bg-text-200/5 gm=
ail-border gmail-border-0.5 gmail-border-border-300 gmail-text-danger-000 g=
mail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmai=
l-text-[0.9rem]">~/Pictures</code>. This matters for the future cloud phase=
: a cloud backend would just become a second=C2=A0<code class=3D"gmail-bg-t=
ext-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 gmail-text-=
danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmai=
l-py-px gmail-text-[0.9rem]">GpcPhotoSource</code>=C2=A0implementation feed=
ing the same=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-bor=
der-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-=
wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">Gpc=
ExifIndex</code>.</p><h2 class=3D"gmail-text-text-100 gmail-mt-3 gmail--mb-=
1 gmail-text-[1.125rem] gmail-font-bold">7. Reserved seam for cloud storage=
 (not built now)</h2><p class=3D"gmail-font-claude-response-body gmail-brea=
k-words gmail-whitespace-normal" style=3D"font-size:medium">To avoid a rewr=
ite later,=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-borde=
r-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wr=
ap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GpcPh=
otoStore</code>=C2=A0should be defined against a small=C2=A0<code class=3D"=
gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border-300 g=
mail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] gmail=
-px-1 gmail-py-px gmail-text-[0.9rem]">GpcPhotoSource</code>=C2=A0interface=
 (<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-bo=
rder-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-round=
ed-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GListModel</code>-b=
ased) with=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-borde=
r-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wr=
ap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GpcLo=
calPhotoSource</code>=C2=A0as the only implementation in v0. A future=C2=A0=
<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-bord=
er-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded=
-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">GpcCloudPhotoSource</=
code>=C2=A0(GOA- or WebDAV-backed) would implement the same interface.=C2=
=A0<strong>Not implemented in this pass</strong>=C2=A0=E2=80=94 noted here =
only so the local-only code doesn&#39;t need to be restructured later.</p><=
h2 class=3D"gmail-text-text-100 gmail-mt-3 gmail--mb-1 gmail-text-[1.125rem=
] gmail-font-bold">8. Dependencies</h2><div class=3D"gmail-overflow-x-auto =
gmail-w-full gmail-px-2 gmail-mb-6" style=3D"font-size:medium"><table class=
=3D"gmail-min-w-full gmail-border-collapse gmail-text-sm gmail-leading-[1.7=
] gmail-whitespace-normal"><thead class=3D"gmail-text-left"><tr><th scope=
=3D"col" class=3D"gmail-text-text-100 gmail-border-b-0.5 gmail-border-[hsl(=
var(--border-300)/0.6)] gmail-py-2 gmail-pr-4 gmail-align-top gmail-font-bo=
ld">Library</th><th scope=3D"col" class=3D"gmail-text-text-100 gmail-border=
-b-0.5 gmail-border-[hsl(var(--border-300)/0.6)] gmail-py-2 gmail-pr-4 gmai=
l-align-top gmail-font-bold">Purpose</th></tr></thead><tbody><tr><td class=
=3D"gmail-border-b-0.5 gmail-border-[hsl(var(--border-300)/0.3)] gmail-py-2=
 gmail-pr-4 gmail-align-top">gtk4 (&gt;=3D 4.14)</td><td class=3D"gmail-bor=
der-b-0.5 gmail-border-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 g=
mail-align-top">UI toolkit</td></tr><tr><td class=3D"gmail-border-b-0.5 gma=
il-border-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-to=
p">libadwaita (&gt;=3D 1.5)</td><td class=3D"gmail-border-b-0.5 gmail-borde=
r-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top">GNOME=
 HIG widgets</td></tr><tr><td class=3D"gmail-border-b-0.5 gmail-border-[hsl=
(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top">libshumate =
(&gt;=3D 1.2)</td><td class=3D"gmail-border-b-0.5 gmail-border-[hsl(var(--b=
order-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top">OSM map widget</td>=
</tr><tr><td class=3D"gmail-border-b-0.5 gmail-border-[hsl(var(--border-300=
)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top">gexiv2</td><td class=3D"gmai=
l-border-b-0.5 gmail-border-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-p=
r-4 gmail-align-top">EXIF/IPTC/XMP extraction</td></tr><tr><td class=3D"gma=
il-border-b-0.5 gmail-border-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-=
pr-4 gmail-align-top">sqlite3</td><td class=3D"gmail-border-b-0.5 gmail-bor=
der-[hsl(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top">EXI=
F index cache</td></tr><tr><td class=3D"gmail-border-b-0.5 gmail-border-[hs=
l(var(--border-300)/0.3)] gmail-py-2 gmail-pr-4 gmail-align-top">glib/gio</=
td><td class=3D"gmail-border-b-0.5 gmail-border-[hsl(var(--border-300)/0.3)=
] gmail-py-2 gmail-pr-4 gmail-align-top">Core, async I/O, file monitoring</=
td></tr></tbody></table></div><p class=3D"gmail-font-claude-response-body g=
mail-break-words gmail-whitespace-normal" style=3D"font-size:medium">Build =
system:=C2=A0<strong>Meson</strong>, matching standard GNOME project conven=
tions (and the=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-b=
order-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pr=
e-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">g=
photo</code>=C2=A0dispatcher&#39;s own build).</p><h2 class=3D"gmail-text-t=
ext-100 gmail-mt-3 gmail--mb-1 gmail-text-[1.125rem] gmail-font-bold">9. Di=
rectory layout (this repo)</h2><div role=3D"group" aria-label=3D"Code" tabi=
ndex=3D"0" class=3D"gmail-relative gmail-group/copy gmail-bg-bg-000/50 gmai=
l-border-0.5 gmail-border-border-400 gmail-rounded-lg gmail-focus:outline-n=
one gmail-focus-visible:ring-2 gmail-focus-visible:ring-accent-100" style=
=3D"font-size:medium"><div class=3D"gmail-sticky gmail-opacity-0 gmail-grou=
p-hover/copy:opacity-100 gmail-group-focus-within/copy:opacity-100 gmail-to=
p-2 gmail-py-2 gmail-h-12 gmail-w-0 gmail-float-right"><div class=3D"gmail-=
absolute gmail-right-0 gmail-h-8 gmail-px-2 gmail-items-center gmail-inline=
-flex gmail-z-10"></div></div><div class=3D"gmail-overflow-x-auto"><pre cla=
ss=3D"gmail-code-block__code gmail-!my-0 gmail-!rounded-lg gmail-!text-sm g=
mail-!leading-relaxed gmail-p-3.5" style=3D"color:rgb(20,24,31);background-=
image:none;background-position:0% 0%;background-size:auto;background-repeat=
:repeat;background-origin:padding-box;background-clip:border-box;font-famil=
y:var(--font-mono)"><code style=3D"background-image:none;background-positio=
n:0% 0%;background-size:auto;background-repeat:repeat;background-origin:pad=
ding-box;background-clip:border-box;font-family:var(--font-mono);white-spac=
e:pre-wrap">gnome-photo-companion/
=E2=94=9C=E2=94=80=E2=94=80 meson.build
=E2=94=9C=E2=94=80=E2=94=80 DESIGN.md
=E2=94=9C=E2=94=80=E2=94=80 src/
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 meson.build
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 main.c
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 gpc-application.[ch]
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 gpc-window.[ch]
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 gpc-photo-store.[ch]
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 gpc-exif-index.[ch]
=E2=94=82   =E2=94=9C=E2=94=80=E2=94=80 gpc-map-view.[ch]
=E2=94=82   =E2=94=94=E2=94=80=E2=94=80 gpc-search-query.[ch]
=E2=94=94=E2=94=80=E2=94=80 data/
    =E2=94=9C=E2=94=80=E2=94=80 meson.build
    =E2=94=9C=E2=94=80=E2=94=80 <a href=3D"http://org.gnome.photocompanion.=
desktop.in/">org.gnome.PhotoCompanion.desktop.in</a>
    =E2=94=9C=E2=94=80=E2=94=80 org.gnome.PhotoCompanion.gschema.xml
    =E2=94=94=E2=94=80=E2=94=80 resources/
        =E2=94=9C=E2=94=80=E2=94=80 gpc-window.ui
        =E2=94=94=E2=94=80=E2=94=80 photo-companion.gresource.xml</code></p=
re></div></div><h2 class=3D"gmail-text-text-100 gmail-mt-3 gmail--mb-1 gmai=
l-text-[1.125rem] gmail-font-bold">10. Roadmap</h2><ul class=3D"gmail-[li_&=
amp;]:mb-0 gmail-[li_&amp;]:mt-1 gmail-[li_&amp;]:gap-1 gmail-[&amp;:not(:l=
ast-child)_ul]:pb-1 gmail-[&amp;:not(:last-child)_ol]:pb-1 gmail-list-disc =
gmail-flex gmail-flex-col gmail-gap-1 gmail-pl-8 gmail-mb-3" style=3D"font-=
size:medium"><li class=3D"gmail-font-claude-response-body gmail-whitespace-=
normal gmail-break-words gmail-pl-2"><strong>v0 (this pass)</strong>: local=
 scan of=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border gmail-border-=
0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitespace-pre-wrap=
 gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9rem]">~/Pictu=
res</code>, EXIF index + search, OSM map view, grid view, live file monitor=
ing.</li><li class=3D"gmail-font-claude-response-body gmail-whitespace-norm=
al gmail-break-words gmail-pl-2"><strong>v1</strong>: marker clustering, th=
umbnail caching (via=C2=A0<code class=3D"gmail-bg-text-200/5 gmail-border g=
mail-border-0.5 gmail-border-border-300 gmail-text-danger-000 gmail-whitesp=
ace-pre-wrap gmail-rounded-[0.4rem] gmail-px-1 gmail-py-px gmail-text-[0.9r=
em]">gnome-desktop-thumbnail</code>), saved searches, GNOME Photos hand-off=
 polish.</li><li class=3D"gmail-font-claude-response-body gmail-whitespace-=
normal gmail-break-words gmail-pl-2"><strong>v2</strong>:=C2=A0<code class=
=3D"gmail-bg-text-200/5 gmail-border gmail-border-0.5 gmail-border-border-3=
00 gmail-text-danger-000 gmail-whitespace-pre-wrap gmail-rounded-[0.4rem] g=
mail-px-1 gmail-py-px gmail-text-[0.9rem]">GpcCloudPhotoSource</code>=C2=A0=
(GOA-backed), background sync, conflict handling for duplicate GPS/EXIF edi=
ts.</li></ul></div><div style=3D"color:rgb(0,0,0);font-family:Helvetica;fon=
t-size:12px"><h2 class=3D"gmail-text-text-100 gmail-mt-3 gmail--mb-1 gmail-=
text-[1.125rem] gmail-font-bold">10. Prototype</h2><div><ul class=3D"gmail-=
MailOutline"><li><font size=3D"4"><span style=3D"font-size:18px"><b>v0:=C2=
=A0</b><a href=3D"https://www.aamot.org/ole/software/gphoto/gnome-photo-com=
panion.tar">https://www.aamot.org/ole/software/gphoto/gnome-photo-companion=
.tar</a></span></font></li></ul></div><div><br></div></div><div style=3D"co=
lor:rgb(0,0,0);font-family:Helvetica;font-size:12px">Best,<br>Ole Aamot<br>=
<a href=3D"mailto:[email protected]">[email protected]</a><br><a href=3D"http://www=
.aamot.org/">www.aamot.org</a></div></div>

--00000000000069b25306564c10bc--


--===============5984023267519999655==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline


--===============5984023267519999655==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline

_______________________________________________
Gphoto-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/gphoto-devel

--===============5984023267519999655==--