feat(video): cross-platform inline player + polished overlay controls

- MessageInlineVideo (new): custom-controlled inline video that disables
  the iOS Safari / Chromium native overlays entirely and reimplements
  the essentials: tap-to-play, centered play affordance while paused,
  bottom bar with play/pause + current time + drag-to-scrub progress
  bar + remaining time + fullscreen. Pointer events with pointer
  capture cover both mouse and touch scrubbing, including dragging
  past the bar's bounds. The element listens to 'seeked' as well as
  'timeupdate' so external currentTime writes paint the bar even when
  the video is paused, and the goFullscreen callback synchronously
  syncs React state on close so the inline progress reflects the user's
  fullscreen playhead with no perceptible delay.
- VideoBubble: replace the inline <video controls> with
  MessageInlineVideo and thread postId through openVideo so the
  fullscreen overlay can attach the download pill to the right post.
- VideoPlayer overlay: replace its <video controls> with
  MessageInlineVideo size='lg', removing the iOS native arrows / PiP /
  mute / overflow controls. The overlay supplies its own large
  download pill and a beefier close button.
- AttachmentDownloadPill: new 'size' prop ('sm' default 30 px, 'lg'
  44 px with 22 px icon and text-[14px]) for overlay surfaces where
  the affordance can breathe and should feel touch-friendly.
- ImageLightbox: drop the inline LightboxDownloadButton and use the
  shared AttachmentDownloadPill size='lg' instead, with a matching
  larger close button. Unused imports cleaned up.
This commit is contained in:
TerryM
2026-05-30 02:25:01 +08:00
parent 3d5681e7de
commit 8646b51b6c
5 changed files with 500 additions and 130 deletions

View File

@@ -20,6 +20,14 @@ type AttachmentDownloadPillProps = {
* Defaults to false (fixed 30px) for standalone images/videos.
*/
adaptive?: boolean;
/**
* Visual size. `sm` (default, 30 px tall) for inline tiles where space is
* tight. `lg` (44 px tall with a 24 px icon and 14 px text) for overlay
* surfaces like the image lightbox or fullscreen video player where the
* affordance can breathe and should feel touch-friendly.
* Ignored when `adaptive` is set.
*/
size?: "sm" | "lg";
};
export function AttachmentDownloadPill({
@@ -28,6 +36,7 @@ export function AttachmentDownloadPill({
leadingLabel,
className = "absolute left-2 top-2",
adaptive = false,
size = "sm",
}: AttachmentDownloadPillProps) {
const { t } = useI18n();
const { showToast } = useToast();
@@ -46,52 +55,56 @@ export function AttachmentDownloadPill({
}
};
const isLg = !adaptive && size === "lg";
const fontCls = adaptive
? "text-[clamp(10px,7cqw,12px)]"
: isLg
? "text-[14px] font-medium"
: "text-[12px]";
const squareCls = adaptive
? "h-[clamp(22px,18cqw,30px)] w-[clamp(22px,18cqw,30px)]"
: isLg
? "h-[44px] w-[44px]"
: "h-[30px] w-[30px]";
const iconCls = adaptive
? "h-[clamp(13px,11cqw,18px)] w-[clamp(13px,11cqw,18px)]"
: isLg
? "h-[22px] w-[22px]"
: "h-[18px] w-[18px]";
const textBoxCls = adaptive
? "h-[clamp(22px,18cqw,30px)] px-[clamp(6px,6cqw,10px)]"
: isLg
? "h-[44px] px-4"
: "h-[30px] px-2.5";
return (
<button
type="button"
onClick={handleDownload}
disabled={isDownloading}
className={`group z-10 inline-flex overflow-hidden rounded-full bg-black/80 ${
adaptive ? "text-[clamp(10px,7cqw,12px)]" : "text-[12px]"
} text-white shadow-lg ring-1 ring-inset ring-white/20 backdrop-blur-md transition hover:bg-black/90 disabled:cursor-wait ${className}`}
className={`group z-10 inline-flex overflow-hidden rounded-full bg-black/80 ${fontCls} text-white shadow-lg ring-1 ring-inset ring-white/20 backdrop-blur-md transition hover:bg-black/90 disabled:cursor-wait ${className}`}
aria-label={
isDownloading ? t("downloading") : `Download ${attachment.filename}`
}
aria-busy={isDownloading}
>
<span
className={`flex items-center justify-center bg-[#545454]/50 transition group-hover:bg-[#545454]/70 ${
adaptive
? "h-[clamp(22px,18cqw,30px)] w-[clamp(22px,18cqw,30px)]"
: "h-[30px] w-[30px]"
}`}
className={`flex items-center justify-center bg-[#545454]/50 transition group-hover:bg-[#545454]/70 ${squareCls}`}
>
{isDownloading ? (
<LoaderCircle
className={`${
adaptive
? "h-[clamp(13px,11cqw,18px)] w-[clamp(13px,11cqw,18px)]"
: "h-[18px] w-[18px]"
} animate-spin`}
className={`${iconCls} animate-spin`}
strokeWidth={2.3}
/>
) : (
<DownloadCloudIcon
className={
adaptive
? "h-[clamp(13px,11cqw,18px)] w-[clamp(13px,11cqw,18px)]"
: "h-[18px] w-[18px]"
}
/>
<DownloadCloudIcon className={iconCls} />
)}
</span>
<span
className={`flex items-center gap-0.5 ${
adaptive
? "h-[clamp(22px,18cqw,30px)] px-[clamp(6px,6cqw,10px)]"
: "h-[30px] px-2.5"
}`}
>
<span className={`flex items-center gap-1 ${textBoxCls}`}>
{isDownloading ? (
t("downloading")
) : (