Add mobile save sheet with same-origin photo downloads.

Phones cannot land downloads in the photo library, so the gallery opens a share sheet for a cacheable preview while streaming via a dedicated download endpoint and recording preview sizes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-08-06 20:14:31 +00:00
co-authored by Cursor
parent 390e1dc0ea
commit be4dd5b47f
20 changed files with 962 additions and 56 deletions
+28 -15
View File
@@ -22,6 +22,9 @@ type Photo struct {
Height int
ThumbKey string
PreviewKey string
// PreviewSizeBytes is the size of the preview object, 0 when it has not
// been measured yet (rows written before the column existed).
PreviewSizeBytes int64
TakenAt time.Time
Status string
Attempts int
@@ -35,11 +38,11 @@ type Photo struct {
}
const photoColumns = `id, gallery_id, position, original_key, original_filename, content_type,
size_bytes, width, height, thumb_key, preview_key, taken_at, status, attempts, next_attempt_at,
last_error, created_at, updated_at, checksum`
size_bytes, width, height, thumb_key, preview_key, preview_size_bytes, taken_at, status,
attempts, next_attempt_at, last_error, created_at, updated_at, checksum`
func scanPhoto(s scanner) (Photo, error) {
var v [19]any
var v [20]any
dest := make([]any, len(v))
for i := range v {
dest[i] = &v[i]
@@ -59,14 +62,15 @@ func scanPhoto(s scanner) (Photo, error) {
Height: int(asInt(v[8])),
ThumbKey: asString(v[9]),
PreviewKey: asString(v[10]),
TakenAt: asTime(v[11]),
Status: asString(v[12]),
Attempts: int(asInt(v[13])),
NextAttemptAt: asTime(v[14]),
LastError: asString(v[15]),
CreatedAt: asTime(v[16]),
UpdatedAt: asTime(v[17]),
Checksum: asString(v[18]),
PreviewSizeBytes: asInt(v[11]),
TakenAt: asTime(v[12]),
Status: asString(v[13]),
Attempts: int(asInt(v[14])),
NextAttemptAt: asTime(v[15]),
LastError: asString(v[16]),
CreatedAt: asTime(v[17]),
UpdatedAt: asTime(v[18]),
Checksum: asString(v[19]),
}, nil
}
@@ -323,17 +327,26 @@ func (db *DB) ClaimNextPhoto(ctx context.Context) (Photo, error) {
return db.GetPhoto(ctx, id)
}
func (db *DB) MarkPhotoReady(ctx context.Context, id, thumbKey, previewKey string, width, height int, takenAt time.Time) error {
func (db *DB) MarkPhotoReady(ctx context.Context, id, thumbKey, previewKey string, previewSize int64, width, height int, takenAt time.Time) error {
var takenArg any
if !takenAt.IsZero() {
takenArg = db.TimeArg(takenAt)
}
_, err := db.ExecContext(ctx, db.Rebind(`
UPDATE photos_photos
SET status = 'ready', thumb_key = ?, preview_key = ?, width = ?, height = ?, taken_at = ?,
last_error = NULL, updated_at = ?
SET status = 'ready', thumb_key = ?, preview_key = ?, preview_size_bytes = ?, width = ?,
height = ?, taken_at = ?, last_error = NULL, updated_at = ?
WHERE id = ?`),
thumbKey, previewKey, width, height, takenArg, db.TimeArg(time.Now()), id)
thumbKey, previewKey, previewSize, width, height, takenArg, db.TimeArg(time.Now()), id)
return err
}
// SetPreviewSize records the measured size of an already-generated preview.
// Used to fill in rows processed before the column existed; failure to write
// is not worth failing a read over, so callers may ignore the error.
func (db *DB) SetPreviewSize(ctx context.Context, id string, size int64) error {
_, err := db.ExecContext(ctx, db.Rebind(
"UPDATE photos_photos SET preview_size_bytes = ? WHERE id = ?"), size, id)
return err
}