Files
pl_mail/templates/_partials/_avatar_picker.html.twig
T
19266223e8 Choose a profile picture through the compose file picker (#65)
Settings → Profile opens the same file picker as attaching to a mail, in an avatar mode: albums, people, search and paging, one picture at a time, files that cannot be a picture greyed out. The choice is posted straight back to the profile form, and the connection is matched against the user's own. From Paperless-ngx the original is taken rather than the archived PDF. The setup wizard keeps its small grid for now.

Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:38:44 +02:00

108 lines
5.8 KiB
Twig

{#
_avatar_picker.html.twig — the profile picture: the one in use, and choosing a
new one from a connected service.
Shared by the setup wizard and Settings → Profile, because they are two views of
the same thing. Only the URLs differ, so the caller passes them:
sources list<Integration> connections that can supply a picture
picking Integration|null the one whose pictures are on screen
entries list<Entry> images from it
pickUrls map id => url where "browse this one" goes
closeUrl string where "stop browsing" goes
fileField string full_name of the form field the choice binds to
current string|null URL of the picture in use, if there is one
modalPicker bool open the file picker in a dialog (see below)
Two ways to choose, because the two callers differ in where they live.
Settings passes `modalPicker`: each connection opens the same dialog the compose
window attaches files with, in its avatar mode, and the choice comes back through
settings/avatar_from_service_controller.js on the form. `picking`, `entries`,
`pickUrls`, `closeUrl` and `fileField` are not used.
The setup wizard does not, and still gets the grid below. Its profile step is
itself rendered in the modal frame, which the file picker would replace — the
wizard would vanish behind it. A thumbnail *is* the submit there: choosing and
saving are one action, and the button carries the field name the form binds it
to. Browsing is a navigation rather than a client-side toggle because these
fields belong to the surrounding form, and HTML has no nested forms.
#}
{% if current is defined and current is not null %}
{# Shown wherever the profile is edited. A file input reports a filename at
best, so without this there is no way to tell what the picture currently
is — only that there is one. #}
<div class="mt-5 flex items-center gap-3">
<img src="{{ current }}"
alt=""
class="size-12 rounded-full object-cover border border-line">
<span class="text-xs text-ink-faint">{{ 'profile.avatar_current'|trans }}</span>
</div>
{% endif %}
{% if sources|length %}
<div class="mt-5 pt-5 border-t border-line">
<p class="text-[13px] font-medium text-ink-soft">{{ 'profile.avatar_from.heading'|trans }}</p>
<div class="mt-2 flex flex-wrap items-center gap-2">
{% for source in sources %}
{% if modalPicker|default(false) %}
{# type=button: this sits inside the profile form, where the
default would submit it. #}
<button type="button"
id="avatar-source-{{ source.id }}"
data-controller="ui--modal"
data-ui--modal-title-value="{{ source.name }}"
data-ui--modal-size-value="{{ source.provider.isMediaLibrary ? 'full' : 'wide' }}"
data-ui--modal-src-value="{{ path('app_integration_browse', { id: source.id, mode: 'avatar' }) }}"
data-action="click->ui--modal#open"
class="inline-flex items-center gap-2 h-8 px-3 rounded-lg text-sm font-medium border transition-colors cursor-pointer
border-field bg-field text-ink-soft hover:bg-hover">
<i class="{{ source.provider.icon }} text-xs" aria-hidden="true"></i>
{{ source.name }}
</button>
{% else %}
{% set isOpen = picking is not null and picking.id == source.id %}
<a href="{{ isOpen ? closeUrl : pickUrls[source.id] }}"
data-turbo-frame="{{ frame|default('_self') }}"
id="avatar-source-{{ source.id }}"
class="inline-flex items-center gap-2 h-8 px-3 rounded-lg text-sm font-medium border transition-colors
{% if isOpen %}
border-accent bg-accent/10 text-accent
{% else %}
border-field bg-field text-ink-soft hover:bg-hover
{% endif %}">
<i class="{{ source.provider.icon }} text-xs" aria-hidden="true"></i>
{{ source.name }}
</a>
{% endif %}
{% endfor %}
</div>
{% if not modalPicker|default(false) and picking is not null %}
{% if entries|length %}
<ul class="mt-4 grid grid-cols-4 sm:grid-cols-6 gap-2">
{% for entry in entries %}
<li>
<button type="submit"
name="{{ fileField }}"
value="{{ entry.id }}"
title="{{ entry.name }}"
class="block w-full aspect-square overflow-hidden rounded-lg border border-line
hover:border-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-accent
transition-colors cursor-pointer">
<img src="{{ path('app_integration_thumbnail', { id: picking.id, file: entry.id }) }}"
alt="{{ entry.name }}"
loading="lazy"
class="w-full h-full object-cover">
</button>
</li>
{% endfor %}
</ul>
{% else %}
<p class="mt-4 text-xs text-ink-faint">{{ 'profile.avatar_from.empty'|trans }}</p>
{% endif %}
{% endif %}
</div>
{% endif %}