Files
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

134 lines
7.5 KiB
Twig

{#
integration/_row.html.twig — one file in the picker list.
The whole row selects, the way a whole tile does in the photo grid. Before, the
only target was a small "Copy" chip on the right, which meant the obvious
gesture — click the file you want — did nothing at all.
Copy and link stay mutually exclusive without any JavaScript by being two radios
in one group. The trick that avoids nesting a label inside a label: the row's
label is absolutely positioned over the whole row, the content above it is
pointer-events-none so clicks fall through, and the link chip sits above both
with its own label. So one tap picks copy, and the chip still picks link.
Link only appears where it is useful — the service can mint one, and the file is
either oversized or being hovered. Showing it permanently made every row look
like it needed a decision.
#}
{% if entry.isFolder %}
{% include 'integration/_folder_row.html.twig' with {
integration: integration,
entry: entry,
draftId: draftId,
mode: mode|default('attach'),
isMediaLibrary: isMediaLibrary|default(false),
} only %}
{% else %}
{# Only a *known* oversize blocks a copy — see the picker's docblock. #}
{% set knownTooBig = entry.size is not null and entry.size > maxBytes %}
{# Profile-picture mode only: a file that cannot become a picture. Shown, so
the page does not look empty, but not choosable. #}
{% set notAnImage = entry.id in refusedIds|default([]) %}
{% set canCopy = not knownTooBig and not notAnImage %}
{% set unattachable = not canCopy and not canLink %}
<li class="group relative flex items-center gap-3 px-3 py-2.5 -mx-2 rounded-xl
{{ unattachable ? 'opacity-50' : '' }}">
{% if canCopy %}
{# Covers the row. Everything else is layered above it. #}
<label class="absolute inset-0 cursor-pointer rounded-xl select-none touch-manipulation"
data-action="pointerdown->integration--integration-picker#tileDown click->integration--integration-picker#tileClick"
aria-label="{{ entry.name }}">
<input type="radio" class="peer sr-only"
name="mode[{{ entry.id }}]" value="copy"
data-integration--integration-picker-target="mode">
<span class="absolute inset-0 rounded-xl transition-colors
group-hover:bg-hover
peer-checked:bg-accent-soft
peer-checked:ring-2 peer-checked:ring-inset peer-checked:ring-accent"></span>
{# No tick here, unlike the grid: the ring plus the tinted fill
is unambiguous across a full-width row, and the only free
corner is where the link chip lives. #}
</label>
{% endif %}
{# pointer-events-none so a click on the name reaches the row label. #}
<div class="relative pointer-events-none flex items-center gap-3 flex-1 min-w-0">
{# A preview only where it tells you something. Asking for one per row
meant a 200-hit search fired 200 proxied requests, each making the
service render a document preview — 4.5 MB and half a minute for a
list of invoices, where the answer was always a red PDF square.
Pictures are worth previewing; a filename identifies the rest. #}
{% if canThumb and entry.mime is not null and (entry.mime starts with 'image/' or entry.mime starts with 'video/') %}
<img src="{{ path('app_integration_thumbnail', { id: integration.id, file: entry.id }) }}"
alt="" loading="lazy"
class="w-10 h-10 rounded-lg object-cover bg-sunken shrink-0">
{% else %}
{% set icon = 'fa-file' %}
{% set tint = 'text-ink-faint' %}
{% if entry.mime is not null %}
{% if entry.mime == 'application/pdf' %}
{% set icon = 'fa-file-pdf' %}{% set tint = 'text-red-400' %}
{% elseif entry.mime starts with 'image/' %}
{% set icon = 'fa-file-image' %}{% set tint = 'text-violet-400' %}
{% elseif entry.mime starts with 'video/' %}
{% set icon = 'fa-file-video' %}{% set tint = 'text-violet-400' %}
{% elseif entry.mime starts with 'audio/' %}
{% set icon = 'fa-file-audio' %}{% set tint = 'text-violet-400' %}
{% elseif entry.mime starts with 'text/' %}
{% set icon = 'fa-file-lines' %}
{% elseif 'zip' in entry.mime or 'compressed' in entry.mime or 'tar' in entry.mime %}
{% set icon = 'fa-file-zipper' %}{% set tint = 'text-amber-400' %}
{% elseif 'sheet' in entry.mime or 'excel' in entry.mime or 'csv' in entry.mime %}
{% set icon = 'fa-file-excel' %}{% set tint = 'text-emerald-400' %}
{% elseif 'word' in entry.mime or 'document' in entry.mime %}
{% set icon = 'fa-file-word' %}{% set tint = 'text-sky-400' %}
{% endif %}
{% endif %}
<span class="w-10 h-10 shrink-0 flex items-center justify-center rounded-lg bg-raised">
<i class="fa-solid {{ icon }} text-base {{ tint }}" aria-hidden="true"></i>
</span>
{% endif %}
<span class="flex-1 min-w-0">
<span class="block truncate text-sm text-ink">{{ entry.name }}</span>
<span class="block text-[11px] text-ink-faint tabular-nums">
{%- if entry.size is not null -%}
{{ entry.size > 1048576
? (entry.size / 1048576)|round(1) ~ ' MB'
: (entry.size / 1024)|round ~ ' KB' }}
{%- endif -%}
{%- if entry.size is not null and entry.modifiedAt %} · {% endif -%}
{%- if entry.modifiedAt %}{{ entry.modifiedAt|date('Y-m-d') }}{% endif -%}
</span>
</span>
</div>
{% if unattachable %}
<span class="relative shrink-0 text-[11px] text-ink-faint">
{{ (notAnImage ? 'integration.picker.not_an_image' : 'integration.picker.too_big')|trans }}
</span>
{% elseif canLink %}
{# Above the row label, so the chip wins the click it is under. #}
<label class="relative shrink-0 cursor-pointer
{{ knownTooBig ? '' : 'opacity-0 group-hover:opacity-100 focus-within:opacity-100' }}
transition-opacity">
<input type="radio" class="peer sr-only"
name="mode[{{ entry.id }}]" value="link"
data-integration--integration-picker-target="mode">
<span class="inline-flex items-center gap-1.5 h-7 px-2.5 rounded-lg text-[11px] font-medium
text-ink-faint bg-field border border-field
peer-checked:bg-accent peer-checked:text-accent-ink peer-checked:border-accent
peer-checked:opacity-100
transition-colors">
<i class="fa-solid fa-link text-[9px]" aria-hidden="true"></i>
{{ 'integration.picker.link'|trans }}
</span>
</label>
{% endif %}
</li>
{% endif %}