Files
Arkie-Library-Frontend/src/components/messageStream/AttachmentDownloadPill.tsx

134 lines
4.2 KiB
TypeScript
Raw Normal View History

import { LoaderCircle } from "lucide-react";
import { DownloadCloudIcon } from "../icons/DownloadCloudIcon";
2026-05-27 12:33:26 +08:00
import { useState, type MouseEvent } from "react";
import { useI18n } from "../../i18n";
import type { Attachment } from "../../types/post";
2026-06-01 23:00:28 +08:00
import { downloadAttachment, pauseActiveVideos } from "./utils/downloadFile";
2026-05-27 12:33:26 +08:00
import { formatBytes } from "./utils/formatBytes";
2026-06-01 23:00:28 +08:00
import {
mediaSaveKindFromAttachment,
useSaveToAlbumGuide,
} from "../SaveToAlbumGuide";
import { useToast } from "../Toast";
2026-05-27 12:33:26 +08:00
type AttachmentDownloadPillProps = {
postId: string;
attachment: Attachment;
leadingLabel?: string;
className?: string;
/**
* When true, the pill scales with its host container's width via container-
* query units (clamped 22-30px). The host element must establish a query
* container with `style={{ containerType: "inline-size" }}`. Use this on
* album tiles so the pill shrinks on small thumbnails in mixed layouts.
* Defaults to false (fixed 30px) for standalone images/videos.
*/
adaptive?: boolean;
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.
2026-05-30 02:25:01 +08:00
/**
* 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";
2026-05-27 12:33:26 +08:00
};
export function AttachmentDownloadPill({
postId,
attachment,
leadingLabel,
className = "absolute left-2 top-2",
adaptive = false,
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.
2026-05-30 02:25:01 +08:00
size = "sm",
2026-05-27 12:33:26 +08:00
}: AttachmentDownloadPillProps) {
const { t } = useI18n();
const { showToast } = useToast();
2026-06-01 23:00:28 +08:00
const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
2026-05-27 12:33:26 +08:00
const [isDownloading, setIsDownloading] = useState(false);
const handleDownload = async (e: MouseEvent<HTMLButtonElement>) => {
2026-05-27 12:33:26 +08:00
e.stopPropagation();
if (isDownloading) return;
2026-06-01 23:00:28 +08:00
pauseActiveVideos();
2026-05-27 12:33:26 +08:00
setIsDownloading(true);
try {
await downloadAttachment(postId, attachment.id, attachment.filename);
2026-06-01 23:00:28 +08:00
const mediaKind = mediaSaveKindFromAttachment(attachment);
if (mediaKind) showSaveToAlbumGuide(mediaKind);
} catch {
showToast(t("downloadFail"), "error");
} finally {
setIsDownloading(false);
}
2026-05-27 12:33:26 +08:00
};
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.
2026-05-30 02:25:01 +08:00
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";
2026-05-27 12:33:26 +08:00
return (
<button
type="button"
2026-06-01 23:00:28 +08:00
onPointerDown={(e) => e.stopPropagation()}
2026-05-27 12:33:26 +08:00
onClick={handleDownload}
disabled={isDownloading}
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.
2026-05-30 02:25:01 +08:00
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}`}
2026-05-27 12:33:26 +08:00
aria-label={
isDownloading ? t("downloading") : `Download ${attachment.filename}`
}
aria-busy={isDownloading}
>
<span
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.
2026-05-30 02:25:01 +08:00
className={`flex items-center justify-center bg-[#545454]/50 transition group-hover:bg-[#545454]/70 ${squareCls}`}
>
2026-05-27 12:33:26 +08:00
{isDownloading ? (
<LoaderCircle
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.
2026-05-30 02:25:01 +08:00
className={`${iconCls} animate-spin`}
strokeWidth={2.3}
/>
2026-05-27 12:33:26 +08:00
) : (
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.
2026-05-30 02:25:01 +08:00
<DownloadCloudIcon className={iconCls} />
2026-05-27 12:33:26 +08:00
)}
</span>
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.
2026-05-30 02:25:01 +08:00
<span className={`flex items-center gap-1 ${textBoxCls}`}>
2026-05-27 12:33:26 +08:00
{isDownloading ? (
t("downloading")
) : (
<>
{leadingLabel ? (
<>
<span>{leadingLabel}</span>
<span className="opacity-60">·</span>
</>
) : null}
<span>{formatBytes(attachment.sizeBytes)}</span>
</>
)}
</span>
</button>
);
}