Skip to content

Show album cover art while password-protected - #4704

Merged
ildyria merged 16 commits into
LycheeOrg:masterfrom
matthewbolding:album-cover-while-locked
Sep 10, 2026
Merged

Show album cover art while password-protected#4704
ildyria merged 16 commits into
LycheeOrg:masterfrom
matthewbolding:album-cover-while-locked

Conversation

@matthewbolding

@matthewbolding matthewbolding commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Adds a "Show cover when locked" toggle to the Visibility tab (v7 and
v8) for password-protected albums. When enabled, the album's cover
thumbnail is shown to anonymous visitors on gallery listings even
before they enter the password — the photos themselves stay hidden
until unlock. This replaces the need for a custom-JS workaround some
users (myself included) had resorted to.

  • Off by default for every album, existing and new — no behavior
    change unless an admin explicitly opts an album in.
  • Scoped to gallery listings (root albums, search, tags) via
    ThumbAlbumResource. The embed endpoint is intentionally out of
    scope for this PR: EmbededRequest::authorize() already rejects any
    password-protected album outright before the resource layer is
    ever reached, and the embed DTO doesn't carry a cover/thumbnail
    concept in the first place. Wiring cover-while-locked into that
    flow is a separate design decision, not a small addition.
  • Not exposed in the bulk-edit admin action; per-album only for now.
  • Fixed a latent bug this change surfaced: AlbumThumb.vue and
    AlbumListItem.vue (v7 + v8) inferred "is this album locked" from
    thumb === null, which was only ever true by coincidence — the
    old ThumbAlbumResource had exactly one code path that nulled out
    thumb, and it was the same path that ran when the album was
    locked, so the two facts always lined up. This PR breaks that
    coincidence on purpose: a locked album with the new toggle on now
    has is_password_required = true and a non-null thumb at the
    same time. Run that state through the old badge logic and it shows
    the green "unlocked" badge on an album that still requires a
    password to view any photos — a real, user-visible bug that simply
    had no way to occur before now. Fixed by adding an explicit
    is_locked field on ThumbAlbumResource and pointing all four
    components at it instead of inferring from thumb.

Note for anyone driving the API directly: grants_cover_access is
now a required field on Album::updateProtectionPolicy, matching
the existing validation on grants_full_photo_access.

Tests added: AlbumsTest::testLockedAlbumHidesThumbByDefault and
::testLockedAlbumWithGrantsCoverAccessShowsThumb, plus updates to
existing protection-policy tests for the new required field.

Summary by CodeRabbit

  • New Features

    • Added album cover selection and removal through album properties and context menus.
    • Added settings to control cover visibility for locked albums, including manually selected covers.
    • Added localized labels and descriptions for cover actions and locked-album settings.
  • Bug Fixes

    • Album lock indicators now accurately reflect locked or unlocked status, even when thumbnails are hidden.
    • Cover selections remain synchronized across album views, search, and editing forms.
    • Prevented cover selections from being unintentionally cleared when updating other album details.
    • Improved album listings and thumbnails after album organization changes.

@matthewbolding
matthewbolding requested a review from a team as a code owner September 1, 2026 16:43
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds manual album cover selection, validates cover ownership, and persists cover_id. Locked-album thumbnails now use configuration-based visibility. Frontend badges, menus, forms, translations, migrations, cache invalidation, and tests reflect the new behavior.

Changes

Album cover selection and visibility

Layer / File(s) Summary
Backend cover selection flow
app/Contracts/..., app/Http/Requests/Album/..., app/Http/Controllers/..., tests/Feature_v2/Album/...
Album updates validate an optional in-album cover photo and persist or clear cover_id. Sorting changes recompute child buckets and invalidate child listings.
Locked-album visibility
app/Http/Resources/Models/ThumbAlbumResource.php, database/migrations/2026_09_06_000000_add_locked_album_cover_configs.php, tests/Feature_v2/Album/AlbumsTest.php
Locked thumbnails use show_cover_of_locked_albums and show_selected_cover_on_locked_albums.
Frontend cover editing and menus
resources/js/v7/components/forms/..., resources/js/v8/components/forms/..., resources/js/v*/components/gallery/..., resources/js/v*/views/..., resources/js/composables/contextMenus/...
Album forms select covers, submit cover_id, synchronize editable album state, and show set-or-remove cover actions.
Lock state and cache integration
resources/js/lychee.d.ts, resources/js/services/album-service.ts, resources/js/v8/utils/..., resources/js/v*/components/gallery/albumModule/...
Frontend lock indicators use is_locked. Album cache clearing removes the affected v3 listing keys.
Permission transition and localization
database/migrations/*grants_cover_access*, lang/*/all_settings.php, lang/*/gallery.php, tests/Unit/Http/Requests/Album/*
The former cover-access permission is removed. Locales and request tests cover the new settings and cover attribute.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🟠 High · up to 97a42

Album updates may accept a cover from another album, while cover and sorting changes can leave gallery state stale or inconsistent. These paths should be corrected before merge.

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 39 functions across 91 files. (5 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Full details: Docstring Coverage

Explanation

Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 39 functions across 91 files. (5 skipped: 5 unsupported.)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the cover,
A photo hops into place,
Locked thumbs follow settings,
Forms remember each choice,
Menus set and clear with care,
Tests watch every path.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/Feature_v2/Album/AlbumsTest.php (1)

229-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use snake_case for the locked album variable.

Rename $lockedAlbum to $locked_album in both tests. Update all references in each test.

  • tests/Feature_v2/Album/AlbumsTest.php#L229-L250: Rename $lockedAlbum and its references to $locked_album.
  • tests/Feature_v2/Album/AlbumsTest.php#L254-L276: Rename $lockedAlbum and its references to $locked_album.

As per coding guidelines: “Variable names should be in snake_case.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 8d7ad47d-68b4-448e-afe3-12c2b33ae587

📥 Commits

Reviewing files that changed from the base of the PR and between 2ffb790 and b49f8a3.

📒 Files selected for processing (47)
  • app/Actions/Admin/BulkEditAlbumsAction.php
  • app/Actions/Album/SetProtectionPolicy.php
  • app/Constants/AccessPermissionConstants.php
  • app/Contracts/Http/Requests/RequestAttribute.php
  • app/Http/Requests/Album/SetAlbumProtectionPolicyRequest.php
  • app/Http/Resources/Models/ThumbAlbumResource.php
  • app/Http/Resources/Models/Utils/AlbumProtectionPolicy.php
  • app/Models/AccessPermission.php
  • database/factories/AccessPermissionFactory.php
  • database/migrations/2026_08_31_000000_add_grants_cover_access_to_access_permissions_table.php
  • lang/ar/dialogs.php
  • lang/bg/dialogs.php
  • lang/cz/dialogs.php
  • lang/de/dialogs.php
  • lang/el/dialogs.php
  • lang/en/dialogs.php
  • lang/es/dialogs.php
  • lang/fa/dialogs.php
  • lang/fr/dialogs.php
  • lang/hu/dialogs.php
  • lang/it/dialogs.php
  • lang/ja/dialogs.php
  • lang/nl/dialogs.php
  • lang/no/dialogs.php
  • lang/pl/dialogs.php
  • lang/pt/dialogs.php
  • lang/ru/dialogs.php
  • lang/sk/dialogs.php
  • lang/sv/dialogs.php
  • lang/tr/dialogs.php
  • lang/vi/dialogs.php
  • lang/zh_CN/dialogs.php
  • lang/zh_TW/dialogs.php
  • resources/js/lychee.d.ts
  • resources/js/services/album-service.ts
  • resources/js/v7/components/forms/album/AlbumVisibility.vue
  • resources/js/v7/components/gallery/albumModule/AlbumListItem.vue
  • resources/js/v7/components/gallery/albumModule/thumbs/AlbumThumb.vue
  • resources/js/v8/components/forms/album/AlbumVisibility.vue
  • resources/js/v8/components/gallery/albumModule/AlbumListItem.vue
  • resources/js/v8/components/gallery/albumModule/thumbs/AlbumThumb.vue
  • tests/Feature_v2/Album/AlbumUpdateTest.php
  • tests/Feature_v2/Album/AlbumsTest.php
  • tests/Feature_v2/SmartAlbums/OverridePermissionsTest.php
  • tests/Precomputing/CoverSelection/EventPropagationIntegrationTest.php
  • tests/Unit/Actions/Album/SetProtectionPolicyTest.php
  • tests/Unit/Http/Requests/Album/SetAlbumProtectionPolicyRequestTest.php

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

@ildyria

ildyria commented Sep 1, 2026

Copy link
Copy Markdown
Member

Hi @matthewbolding I am currently in the middle of a huge refactoring of the way albums are actually being displayed in the front-end.

Do you mind we wait for it to be merged as it will have a LOT of changes with regard to your proposition?

PS: I really like the idea BTW, it is just that it arrives at the worse moment and I don't want you to waste your time on something which is going to be removed soon.

See here for what is coming: #4705
If you join discord, feel free to ping me so we can see how to synchronize. :)

And yes, I am planning the same change for the photo display...

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.93%. Comparing base (17bcae8) to head (8b83f5d).

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@matthewbolding

Copy link
Copy Markdown
Contributor Author

Hi @matthewbolding I am currently in the middle of a huge refactoring of the way albums are actually being displayed in the front-end.

Do you mind we wait for it to be merged as it will have a LOT of changes with regard to your proposition?

PS: I really like the idea BTW, it is just that it arrives at the worse moment and I don't want you to waste your time on something which is going to be removed soon.

See here for what is coming: #4705 If you join discord, feel free to ping me so we can see how to synchronize. :)

And yes, I am planning the same change for the photo display...

Hi @ildyria. Thanks for the reply. Yes, I have no issue waiting on other changes to the codebase to be applied before this feature gets incorporated. I'll likely reach out to you on Discord late on. Glad you like the idea!

Another idea I had was to permit fully custom album covers, i.e., the system shall accept a new image file to be used for the sole purpose of an album cover.

@ildyria

ildyria commented Sep 2, 2026

Copy link
Copy Markdown
Member

Another idea I had was to permit fully custom album covers, i.e., the system shall accept a new image file to be used for the sole purpose of an album cover.

Isn't cover_id already enough for that ?

@matthewbolding

Copy link
Copy Markdown
Contributor Author

Another idea I had was to permit fully custom album covers, i.e., the system shall accept a new image file to be used for the sole purpose of an album cover.

Isn't cover_id already enough for that ?

Not necessarily. To my understanding, cover_id can only be a photo that already exists in the album (or sub-album, I suppose), but the cover_id cannot be, for instance, some standalone photo that's not meant to be part of the album.

In my own deployments of Lychee, I've inserted custom JS to force the covers of certain top-level password-protected albums to be 1) visible to anonymous users and 2) be a custom image that's not part of the gallery.

@ildyria ildyria added parked - will revisit later This PR needs more work but some things need to be fixed before. and removed parked - will revisit later This PR needs more work but some things need to be fixed before. labels Sep 3, 2026
Comment thread app/Http/Resources/Models/ThumbAlbumResource.php Outdated
@ildyria

ildyria commented Sep 5, 2026

Copy link
Copy Markdown
Member

I had a look, pretty damn, good!
Though I am wondering, should this be a per album setting or a global setting ?

On one end, a global setting would make it easier and avoid the Access rights propagation.
On the other end, a per album setting allows for more configurable setup.

In that specific case, given the complexity added by the flexibility I would suggest to go for the global config instead.
This will also limit the refactoring needed with the Struct-of-Array migration.

Other questions are:

  • Should we consider that all albums with a cover_id set and protected by password should display the thumb?
  • Still on that vibe, should we make that a separate setting actually?
    two separate settings:
    • show cover of locked albums => on = always display the cover on locked albums, off = do not display the cover
    • show selected cover on locked albums => on = display the cover even if the above is set to off, off = do not display the cover.

@matthewbolding

Copy link
Copy Markdown
Contributor Author

Though I am wondering, should this be a per album setting or a global setting ?

I think that having the option global would likely be the best good idea, at least initially, although I could certainly see a user make a compelling case for the setting to be album-by-album.

Should we consider that all albums with a cover_id set and protected by password should display the thumb?

Yes, that's what I was thinking; if the option is enabled for the album cover to show while locked, then display it. I'm not particularly sure if differentiating between a specifically set cover_id and the auto-selected thumbnail is necessary, but that's just me. Did I understand your question properly?

And another thought crossed my mind: replacing the lock icon with the cover will give the user no indication that a particular album is locked. Perhaps overlaying a transparent, small lock icon that's positioned, say, in the bottom right might be prudent.

@ildyria

ildyria commented Sep 6, 2026

Copy link
Copy Markdown
Member

I think then the following is the best approach:

Two separate settings:

  • show cover of locked albums => on = always display the cover on locked albums, off = do not display the cover
  • show selected cover on locked albums => on = display the cover even if the above is set to off, off = do not display the cover.

The reason why the selected cover is interesting is because it allows the granularity you have from the per-album setting and it gives control on which thumb is selected, while if you just have the per album setting, you are exposed to a bit of randomness (the thumb selected depends of the ordering inside the album).

With regard to locked, you can probably just put a span 50% with pi pi-lock and on v8 similar approach but with lucide:lock. With the change of license of PrimeVue from v4 => v5, I no longer want this anywhere close to Lychee, so I migrated to NuxtUI, hence also the change of icons for which we use mostly lucid from https://iconify.design/

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/Feature_v2/Album/AlbumsTest.php (1)

261-261: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use snake_case test variables. The project coding convention applies to this test file. Rename $lockedAlbum to $locked_album and $selectedCover to $selected_cover, including all references.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: dfa9a6d8-3479-42aa-8639-bb7e2e112b9a

📥 Commits

Reviewing files that changed from the base of the PR and between ca6db96 and bc47266.

📒 Files selected for processing (68)
  • app/Contracts/Http/Requests/RequestAttribute.php
  • app/Http/Controllers/Gallery/AlbumController.php
  • app/Http/Requests/Album/UpdateAlbumRequest.php
  • app/Http/Resources/Models/ThumbAlbumResource.php
  • database/migrations/2026_09_06_000000_add_locked_album_cover_configs.php
  • database/migrations/2026_09_06_000001_drop_grants_cover_access_from_access_permissions_table.php
  • lang/ar/all_settings.php
  • lang/ar/gallery.php
  • lang/bg/all_settings.php
  • lang/bg/gallery.php
  • lang/cz/all_settings.php
  • lang/cz/gallery.php
  • lang/de/all_settings.php
  • lang/de/gallery.php
  • lang/el/all_settings.php
  • lang/el/gallery.php
  • lang/en/all_settings.php
  • lang/en/gallery.php
  • lang/es/all_settings.php
  • lang/es/gallery.php
  • lang/fa/all_settings.php
  • lang/fa/gallery.php
  • lang/fr/all_settings.php
  • lang/fr/gallery.php
  • lang/hu/all_settings.php
  • lang/hu/gallery.php
  • lang/it/all_settings.php
  • lang/it/gallery.php
  • lang/ja/all_settings.php
  • lang/ja/gallery.php
  • lang/nl/all_settings.php
  • lang/nl/gallery.php
  • lang/no/all_settings.php
  • lang/no/gallery.php
  • lang/pl/all_settings.php
  • lang/pl/gallery.php
  • lang/pt/all_settings.php
  • lang/pt/gallery.php
  • lang/ru/all_settings.php
  • lang/ru/gallery.php
  • lang/sk/all_settings.php
  • lang/sk/gallery.php
  • lang/sv/all_settings.php
  • lang/sv/gallery.php
  • lang/tr/all_settings.php
  • lang/tr/gallery.php
  • lang/vi/all_settings.php
  • lang/vi/gallery.php
  • lang/zh_CN/all_settings.php
  • lang/zh_CN/gallery.php
  • lang/zh_TW/all_settings.php
  • lang/zh_TW/gallery.php
  • resources/js/composables/contextMenus/contextMenu.ts
  • resources/js/lychee.d.ts
  • resources/js/services/album-service.ts
  • resources/js/v7/components/forms/album/AlbumProperties.vue
  • resources/js/v7/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/v7/views/gallery-panels/Search.vue
  • resources/js/v8/components/forms/album/AlbumProperties.vue
  • resources/js/v8/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/v8/composables/contextMenus/contextMenu.ts
  • resources/js/v8/views/gallery-panels/Search.vue
  • tests/Feature_v2/Album/AlbumMatchingAlbumsTest.php
  • tests/Feature_v2/Album/AlbumTitleSyncTest.php
  • tests/Feature_v2/Album/AlbumUpdateTest.php
  • tests/Feature_v2/Album/AlbumsTest.php
  • tests/Feature_v2/TitleSplitIntegrityTest.php
  • tests/Unit/Http/Requests/Album/UpdateAlbumRequestTest.php
💤 Files with no reviewable changes (1)
  • resources/js/lychee.d.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread resources/js/composables/contextMenus/contextMenu.ts
Comment thread resources/js/v7/components/forms/album/AlbumProperties.vue
Comment thread resources/js/v8/components/forms/album/AlbumProperties.vue Outdated
Comment thread resources/js/v8/components/forms/album/AlbumProperties.vue
Comment thread resources/js/v8/composables/contextMenus/contextMenu.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
app/Http/Controllers/Gallery/AlbumController.php (2)

135-135: 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win

IDOR (CWE-639): Authorization Bypass Through User-Controlled Key (IDOR)

Reachability: External · Exploitability: Moderate

Enforce album membership before persisting cover_id.

UpdateAlbumRequest::processValidatedValues() resolves the submitted ID with Photo::query()->findOrFail($cover_id), but it does not constrain the photo to the album being updated. This allows an existing photo from another album to become the target album’s cover. Resolve the photo through the target album’s photos() relation before assigning cover_id.


158-164: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Dispatch AlbumChildrenChanged after the queued recomputation.

When QUEUE_CONNECTION uses an asynchronous driver, RecomputeChildAlbumBucketsJob can execute its upsert() after the controller synchronously invalidates the cache. A request can then repopulate the children cache with old bucket_id values. Dispatch AlbumChildrenChanged from the job after a successful upsert(), as RecomputeRootAlbumBucketsJob does, and add a regression test.

resources/js/v7/views/gallery-panels/Search.vue (1)

366-366: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Update editable.cover_id only after PhotoService.setAsCover() succeeds.

Both Search panels update modelAlbum.cover_id and editable.cover_id before the Album::cover request settles. A failed request leaves the local value changed. The album editor later sends this value as cover_id, so a save can persist a cover that the server rejected. Use the successful response to refresh or synchronize the authoritative album state, and leave local state unchanged on failure. Apply this correction in both panels.

resources/js/v8/components/gallery/albumModule/AlbumPanel.vue (1)

364-364: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Invalidate parent and root listing state after setting a cover.

AlbumService.clearCache(albumStore.album.id) does not remove the parent key album_albums_<parent-id>_page<page> or the root albums entry. Both cover callbacks can therefore leave parent or root navigation showing the previous cover. Use a shared cover-invalidation helper that clears the changed album, clears its parent listing or root listing as applicable, and invalidates useAlbumListStore.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: fcde9932-90d5-41b7-a96d-58190229b7dd

📥 Commits

Reviewing files that changed from the base of the PR and between deb2c84 and 97a4253.

📒 Files selected for processing (11)
  • app/Contracts/Http/Requests/RequestAttribute.php
  • app/Http/Controllers/Gallery/AlbumController.php
  • resources/js/lychee.d.ts
  • resources/js/services/album-service.ts
  • resources/js/v7/views/gallery-panels/Search.vue
  • resources/js/v8/components/gallery/albumModule/AlbumListItem.vue
  • resources/js/v8/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/v8/components/gallery/albumModule/thumbs/AlbumThumb.vue
  • resources/js/v8/utils/adaptAlbumChildTile.ts
  • resources/js/v8/utils/adaptCategoryTile.ts
  • resources/js/v8/views/gallery-panels/Search.vue

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

@ildyria

ildyria commented Sep 9, 2026

Copy link
Copy Markdown
Member

The two migrations which add and drop the grants_cover_access column should be obviously removed. ;)
And you still have some tests to fix which are returning 422 due to the missing argument.

Comment thread app/Http/Requests/Album/UpdateAlbumRequest.php Outdated

@ildyria ildyria left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one fix on the validation that the photo belongs to albums (and descendants)

matthewbolding and others added 8 commits September 9, 2026 21:57
Adds a per-album 'grants_cover_access' toggle so a locked album can
still display its cover thumbnail to anonymous visitors, without
revealing photo contents. Defaults to off for every existing album.
It was never read by the front-end; is_locked already carries the
state listing components actually branch on. Updated the two
protection-policy tests to assert on is_locked/thumb instead.
Per maintainer feedback on the per-album grants_cover_access toggle
(complexity of access-rights propagation, and friction with the
upcoming Struct-of-Array album refactor), replace it with two global
configs: show_cover_of_locked_albums (always show) and
show_selected_cover_on_locked_albums (show only when the cover was
manually chosen via "Set as Cover", not auto-selected).

ThumbAlbumResource now decides locked-album thumb visibility from
these configs instead of the per-album AccessPermission flag, which
is dropped from the schema along with all its plumbing (model,
factory, request validation, SetProtectionPolicy, BulkEditAlbumsAction,
and the now-removed per-album toggle in both v7 and v8 AlbumVisibility
forms).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVGbZsGYEK8nCGLfoXVmBN
"Set Album Cover" always showed even when right-clicking the photo (or
sub-album) that was already the cover, so the only way to clear it was
to click what looked like a no-op action. Mirror the existing
set/remove pattern already used for the album header: show "Remove
Album Cover" when the target is the current cover_id, "Set Album
Cover" otherwise. Applied to both the shared (v7) and v8-specific
contextMenu.ts composables.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVGbZsGYEK8nCGLfoXVmBN
Extend PATCH /Album (UpdateAlbumRequest/AlbumController::updateAlbum)
to accept cover_id, mirroring the existing header_id handling exactly:
validated as a nullable RandomID, membership-checked against the
album's photos in authorize(), and persisted alongside the other
fields in one save. EditableBaseAlbumResource already exposed cover_id
for reading; this adds the write path.

Add a "Set album cover" picker to the About Album tab in both v7 and
v8, next to the existing header picker (v8 gates it behind Expert Mode
like header already is).

Also fix a real bug this exposed: the photo/sub-album context menu's
"Set/Remove Album Cover" action only updated the top-level
albumStore.modelAlbum/tagAlbum.cover_id, not the separate nested
editable.cover_id copy that the About Album Select reads from, so a
context-menu change never showed up there without a full reload.
setAsHeader already had this sync for header_id; setAsCover was
missing it entirely. Applied the same fix to both the photo-level and
sub-album-level setAsCover callbacks, in both v7 and v8's AlbumPanel.vue
and Search.vue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVGbZsGYEK8nCGLfoXVmBN
CodeRabbit flagged that photoMenu() checked selectors.album (the ref
itself, always truthy) instead of selectors.album.value, so a photo
refresh that resolves before albumStore.album is reassigned could
crash the context menu when it dereferences cover_id/header_id/rights
on undefined.
CodeRabbit found that when editable.cover_id points to a photo not in
the currently loaded page of photosStore.photos, buildCoverId (v7 and
v8) returned undefined for it. Since saveAlbum() maps a missing cover
selection to cover_id: null, saving any unrelated field on the album
silently cleared the real persisted cover. Return an id-only option
instead so the id round-trips even when the photo record isn't loaded
yet.
CodeRabbit noted the v8 cover USelectMenu had no way to remove an
existing cover: Nuxt UI only renders its clear button when the clear
prop is set. Add clear and widen cover_id's type to allow null, which
Nuxt UI resets a cleared selection to; saveAlbum() already maps that
to cover_id: null.
The multi-line condition and cast added in 45bacef didn't match
Prettier's line-length rules; npm run check-formatting was failing
CI.
The cover Select added in bc47266 exceeded Prettier's line length;
this was already on origin and failing CI's formatting check.
Merging upstream/master brought in adaptAlbumChildTile.ts and
adaptCategoryTile.ts (Feature 062's struct-of-arrays listing adapters),
which predate is_locked on ThumbAlbumResource and didn't set it,
failing typecheck once combined with this branch's changes.

adaptAlbumChildTile has real is_password_required data but no
per-viewer unlock state, so is_locked mirrors is_password_required
(assume still locked rather than claim unlocked without evidence).
adaptCategoryTile already hardcodes is_password_required to false for
these four listings, so is_locked follows the same false default.
@matthewbolding
matthewbolding force-pushed the album-cover-while-locked branch from 5820dfd to 8b83f5d Compare September 10, 2026 03:00
@ildyria
ildyria merged commit b652ec2 into LycheeOrg:master Sep 10, 2026
54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants