diff --git a/src/components/LatestUpdateCard.tsx b/src/components/LatestUpdateCard.tsx
index 16d62b8..ef30bff 100644
--- a/src/components/LatestUpdateCard.tsx
+++ b/src/components/LatestUpdateCard.tsx
@@ -1,10 +1,17 @@
-import { Play } from "lucide-react";
-import { Link, useNavigate } from "react-router-dom";
+import { LoaderCircle, Play } from "lucide-react";
+import { useState } from "react";
+import { Link } from "react-router-dom";
import { FavoriteButton } from "../favorites/FavoriteButton";
import { useI18n } from "../i18n";
import { useLocalizedPath } from "../useLocalizedPath";
import type { Attachment, Post } from "../types/post";
import { DownloadCloudIcon } from "./icons/DownloadCloudIcon";
+import {
+ mediaSaveKindFromAttachment,
+ useSaveToAlbumGuide,
+} from "./SaveToAlbumGuide";
+import { useToast } from "./Toast";
+import { downloadAttachment } from "./messageStream/utils/downloadFile";
import { fileIcon } from "./messageStream/utils/fileIcon";
import {
filenameWithExtension,
@@ -23,11 +30,24 @@ function LatestActions({
attachment?: Attachment;
}) {
const { t } = useI18n();
- const lp = useLocalizedPath();
- const navigate = useNavigate();
+ const { showToast } = useToast();
+ const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
+ const [isDownloading, setIsDownloading] = useState(false);
- const goToPost = () => {
- navigate(lp(`/browse?post=${encodeURIComponent(post.id)}`));
+ const handleDownload = async () => {
+ if (!attachment || isDownloading) return;
+ setIsDownloading(true);
+ try {
+ await downloadAttachment(post.id, attachment.id, attachment.filename, {
+ sizeBytes: attachment.sizeBytes,
+ });
+ const mediaKind = mediaSaveKindFromAttachment(attachment);
+ if (mediaKind) showSaveToAlbumGuide(mediaKind);
+ } catch {
+ showToast(t("downloadFail"), "error");
+ } finally {
+ setIsDownloading(false);
+ }
};
return (
@@ -39,13 +59,23 @@ function LatestActions({
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
- goToPost();
+ void handleDownload();
}}
- aria-label={t("download")}
- title={t("download")}
- className="relative z-20 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#191921] text-white outline-none transition hover:bg-[#22232D] focus-visible:ring-2 focus-visible:ring-ark-gold/70"
+ disabled={isDownloading}
+ aria-label={
+ isDownloading ? t("downloading") : `Download ${attachment.filename}`
+ }
+ aria-busy={isDownloading}
+ className="relative z-20 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#191921] text-white outline-none transition hover:bg-[#22232D] focus-visible:ring-2 focus-visible:ring-ark-gold/70 disabled:cursor-wait"
>
-
+ {isDownloading ? (
+
+ ) : (
+
+ )}
) : null}
diff --git a/src/components/PopularRankList.tsx b/src/components/PopularRankList.tsx
index 12123df..c939352 100644
--- a/src/components/PopularRankList.tsx
+++ b/src/components/PopularRankList.tsx
@@ -4,6 +4,7 @@ import {
FileText,
Image as ImageIcon,
Link as LinkIcon,
+ LoaderCircle,
Music,
Presentation,
Video,
@@ -20,6 +21,9 @@ import { cleanCategoryDisplayName } from "../utils/categoryDisplay";
import { formatDateYmd } from "../utils/format";
import { postToResource } from "../utils/postResourceAdapter";
import type { Post } from "../types/post";
+import { downloadAttachment } from "./messageStream/utils/downloadFile";
+import { mediaSaveKindFromType, useSaveToAlbumGuide } from "./SaveToAlbumGuide";
+import { useToast } from "./Toast";
import { FavoriteButton } from "../favorites/FavoriteButton";
const MEDALS = ["🥇", "🥈", "🥉"];
@@ -101,18 +105,31 @@ export function PopularRankRow({
const { t, lang } = useI18n();
const navigate = useNavigate();
const lp = useLocalizedPath();
+ const { showToast } = useToast();
+ const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
+ const [isDownloading, setIsDownloading] = useState(false);
const [coverFailed, setCoverFailed] = useState(false);
const r = postToResource(post, lang, categories);
const cover = r.coverImage && !coverFailed ? assetUrl(r.coverImage) : "";
const isTop3 = showRank && index < MEDALS.length;
- const goToPost = () => {
- const params = new URLSearchParams();
- if (browseSort) params.set("sort", browseSort);
- params.set("post", post.id);
- if (singlePostLink) params.set("single", "1");
- navigate(lp(`/browse?${params.toString()}`));
+ const handleDownload = async () => {
+ if (isDownloading || !r.downloadPostId || !r.downloadAttachmentId) return;
+ setIsDownloading(true);
+ try {
+ await downloadAttachment(
+ r.downloadPostId,
+ r.downloadAttachmentId,
+ r.title,
+ );
+ const mediaKind = mediaSaveKindFromType(r.type);
+ if (mediaKind) showSaveToAlbumGuide(mediaKind);
+ } catch {
+ showToast(t("downloadFail"), "error");
+ } finally {
+ setIsDownloading(false);
+ }
};
return (
@@ -125,7 +142,13 @@ export function PopularRankRow({
>
) : null}