diff --git a/website/models/artifact.py b/website/models/artifact.py index 1a2f9181..3eacd138 100644 --- a/website/models/artifact.py +++ b/website/models/artifact.py @@ -106,6 +106,39 @@ def raw_file_label(self): return None return self.RAW_FILE_LABELS.get(ext, ext.lstrip('.').upper()) + @staticmethod + def _safe_file_size(file_field): + """ + Size of a FileField's file in bytes, or None if there is no file or it + can't be read. + + Reading ``FieldFile.size`` hits storage (a stat) and raises + ``FileNotFoundError`` when the file has gone missing on disk — which + happens in practice on the servers (the fuzzy ``serve_pdf`` view exists + precisely because publication files get renamed/removed). The public + preview card (#840) reads these sizes at render time for every card on + a listing, so a single missing file must degrade to "no size shown" + rather than 500 the whole page. + """ + if not file_field: + return None + try: + return file_field.size + except (OSError, ValueError): + return None + + @property + def pdf_file_size(self): + """Size of ``pdf_file`` in bytes, or None if empty/missing. See + :meth:`_safe_file_size`.""" + return self._safe_file_size(self.pdf_file) + + @property + def raw_file_size(self): + """Size of ``raw_file`` in bytes, or None if empty/missing. See + :meth:`_safe_file_size`.""" + return self._safe_file_size(self.raw_file) + def __str__(self): if self.id and self.authors.exists(): return "{}, '{}', {} {}".format(self.get_first_author_last_name(), self.title, self.forum_name, self.date) diff --git a/website/static/website/css/publications.css b/website/static/website/css/publications.css index 2d3efc41..a65b651c 100644 --- a/website/static/website/css/publications.css +++ b/website/static/website/css/publications.css @@ -684,4 +684,75 @@ border-width: 2px; border-color: var(--color-primary); font-weight: var(--font-weight-medium); -} \ No newline at end of file +} +/* ============================================================================ + ARTIFACT PREVIEW CARD (#840) + ============================================================================ + Poster / Talk preview popover opened by thumbnailPreview.js: a thumbnail + plus download actions (PDF, raw file, Source). It reuses Bootstrap's + `.popover` base class (border, shadow, arrow); the rules below size the card + and style its contents. */ + +.artifact-preview-popover { + /* Compact card. Talk slides are landscape, so width drives the size; keep it + modest. Posters (often portrait) are bounded by the image max-height. */ + max-width: 272px; + /* Bootstrap sets .popover { display: none }; the JS reveals it. Keep it + above page chrome but below modals. */ + z-index: 1060; +} + +.artifact-preview-popover .popover-content { + padding: 6px; +} + +.artifact-preview-image-link { + display: block; + cursor: pointer; +} + +.artifact-preview-image { + display: block; + width: 100%; + height: auto; + /* Never let a tall poster run past the viewport. */ + max-height: 45vh; + object-fit: contain; + border-radius: var(--border-radius-sm, 3px); +} + +.artifact-preview-actions { + display: flex; + flex-direction: column; + gap: 2px; + margin-top: 6px; +} + +.artifact-preview-action { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 8px; + border-radius: var(--border-radius-sm, 3px); + color: var(--color-text-primary); + text-decoration: none; + white-space: nowrap; +} + +.artifact-preview-action:hover, +.artifact-preview-action:focus { + background-color: var(--color-bg-muted, #f0f0f0); + text-decoration: none; +} + +.artifact-preview-action:focus-visible { + outline: var(--focus-ring-width, 2px) solid var(--focus-ring-color, #1b6ec2); + outline-offset: var(--focus-ring-offset, 2px); +} + +/* File size, pushed to the trailing edge of each action row. */ +.artifact-preview-size { + margin-left: auto; + color: var(--color-text-secondary, #666); + font-size: 0.85em; +} diff --git a/website/static/website/js/thumbnailPreview.js b/website/static/website/js/thumbnailPreview.js new file mode 100644 index 00000000..0352e047 --- /dev/null +++ b/website/static/website/js/thumbnailPreview.js @@ -0,0 +1,380 @@ +/** + * ============================================================================ + * ARTIFACT PREVIEW POPOVER MODULE + * ============================================================================ + * + * Opens a small card previewing a publication's poster / talk-slides thumbnail + * plus download actions (PDF, the raw file e.g. PPTX, and the editable Source + * link). Implements issue #840. + * + * The trigger is the "Poster" / "Talk" link rendered by + * snippets/artifact_preview_link.html. Behavior mirrors the Cite popover + * (citationPopoverSimple.js): + * + * - MOUSE: hovering the trigger opens the card (a passive glance that + * auto-closes when the pointer leaves). Clicking PINS it open so the user + * can interact without holding hover. + * - KEYBOARD: Enter/Space activates the trigger (a click), which opens and + * pins the card and moves focus to its first action. Focus is kept within + * the card while pinned; Escape closes it and restores focus to the trigger. + * - TOUCH: there is no hover, so a tap just opens (pins) the card — the same + * accessible path as keyboard. + * + * The trigger keeps `href` pointing at the PDF, so with JS disabled it still + * works as a plain download link (progressive enhancement). + * + * The card's HTML lives in a per-trigger