mirror of https://github.com/usememos/memos.git
Merge 43e8099efc into b0558824c4
This commit is contained in:
commit
3d1a4cd89c
|
|
@ -4,6 +4,7 @@ import {
|
|||
BookmarkMinusIcon,
|
||||
BookmarkPlusIcon,
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
Edit3Icon,
|
||||
FileTextIcon,
|
||||
LinkIcon,
|
||||
|
|
@ -49,6 +50,7 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
|
|||
handleToggleMemoStatusClick,
|
||||
handleCopyLink,
|
||||
handleCopyContent,
|
||||
handleDownloadContent,
|
||||
handleDeleteMemoClick,
|
||||
confirmDeleteMemo,
|
||||
handleRemoveCompletedTaskListItemsClick,
|
||||
|
|
@ -73,7 +75,11 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
|
|||
<>
|
||||
{!isComment && (
|
||||
<DropdownMenuItem onClick={handleTogglePinMemoBtnClick}>
|
||||
{memo.pinned ? <BookmarkMinusIcon className="w-4 h-auto" /> : <BookmarkPlusIcon className="w-4 h-auto" />}
|
||||
{memo.pinned ? (
|
||||
<BookmarkMinusIcon className="w-4 h-auto" />
|
||||
) : (
|
||||
<BookmarkPlusIcon className="w-4 h-auto" />
|
||||
)}
|
||||
{memo.pinned ? t("common.unpin") : t("common.pin")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
|
@ -91,6 +97,10 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
|
|||
<CopyIcon className="w-4 h-auto" />
|
||||
{t("common.copy")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuItem onClick={handleDownloadContent}>
|
||||
<DownloadIcon className="w-4 h-auto" />
|
||||
{t("memo.download")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem onClick={handleCopyLink}>
|
||||
<LinkIcon className="w-4 h-auto" />
|
||||
|
|
@ -109,7 +119,9 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
|
|||
<>
|
||||
{/* Remove completed tasks (non-archived, non-comment, has completed tasks) */}
|
||||
{!isArchived && !isComment && hasCompletedTaskList && (
|
||||
<DropdownMenuItem onClick={handleRemoveCompletedTaskListItemsClick}>
|
||||
<DropdownMenuItem
|
||||
onClick={handleRemoveCompletedTaskListItemsClick}
|
||||
>
|
||||
<SquareCheckIcon className="w-4 h-auto" />
|
||||
{t("memo.remove-completed-task-list-items")}
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -118,7 +130,11 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
|
|||
{/* Archive/Restore (non-comment) */}
|
||||
{!isComment && (
|
||||
<DropdownMenuItem onClick={handleToggleMemoStatusClick}>
|
||||
{isArchived ? <ArchiveRestoreIcon className="w-4 h-auto" /> : <ArchiveIcon className="w-4 h-auto" />}
|
||||
{isArchived ? (
|
||||
<ArchiveRestoreIcon className="w-4 h-auto" />
|
||||
) : (
|
||||
<ArchiveIcon className="w-4 h-auto" />
|
||||
)}
|
||||
{isArchived ? t("common.restore") : t("common.archive")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ import { userKeys } from "@/hooks/useUserQueries";
|
|||
import { handleError } from "@/lib/error";
|
||||
import { State } from "@/types/proto/api/v1/common_pb";
|
||||
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { toAttachmentItems } from "@/components/memo-metadata";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { removeCompletedTasks } from "@/utils/markdown-manipulation";
|
||||
import { downloadMemoContentAndAttachments } from "@/utils/content";
|
||||
|
||||
interface UseMemoActionHandlersOptions {
|
||||
memo: Memo;
|
||||
|
|
@ -20,7 +22,12 @@ interface UseMemoActionHandlersOptions {
|
|||
setRemoveTasksDialogOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRemoveTasksDialogOpen }: UseMemoActionHandlersOptions) => {
|
||||
export const useMemoActionHandlers = ({
|
||||
memo,
|
||||
onEdit,
|
||||
setDeleteDialogOpen,
|
||||
setRemoveTasksDialogOpen,
|
||||
}: UseMemoActionHandlersOptions) => {
|
||||
const t = useTranslate();
|
||||
const location = useLocation();
|
||||
const navigateTo = useNavigateTo();
|
||||
|
|
@ -56,7 +63,10 @@ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRe
|
|||
const handleToggleMemoStatusClick = useCallback(async () => {
|
||||
const isArchiving = memo.state !== State.ARCHIVED;
|
||||
const state = memo.state === State.ARCHIVED ? State.NORMAL : State.ARCHIVED;
|
||||
const message = memo.state === State.ARCHIVED ? t("message.restored-successfully") : t("message.archived-successfully");
|
||||
const message =
|
||||
memo.state === State.ARCHIVED
|
||||
? t("message.restored-successfully")
|
||||
: t("message.archived-successfully");
|
||||
|
||||
try {
|
||||
await updateMemo({
|
||||
|
|
@ -99,6 +109,11 @@ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRe
|
|||
setDeleteDialogOpen(true);
|
||||
}, [setDeleteDialogOpen]);
|
||||
|
||||
const handleDownloadContent = useCallback(() => {
|
||||
const attachmentItems = toAttachmentItems(memo.attachments, []);
|
||||
downloadMemoContentAndAttachments(memo, attachmentItems);
|
||||
}, [memo, memo.attachments]);
|
||||
|
||||
const confirmDeleteMemo = useCallback(async () => {
|
||||
await deleteMemo(memo.name);
|
||||
toast.success(t("message.deleted-successfully"));
|
||||
|
|
@ -132,6 +147,7 @@ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRe
|
|||
handleCopyLink,
|
||||
handleCopyContent,
|
||||
handleDeleteMemoClick,
|
||||
handleDownloadContent,
|
||||
confirmDeleteMemo,
|
||||
handleRemoveCompletedTaskListItemsClick,
|
||||
confirmRemoveCompletedTaskListItems,
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
},
|
||||
"copy-content": "Copy Content",
|
||||
"copy-link": "Copy Link",
|
||||
"download": "Download",
|
||||
"count-memos-in-date": "{{count}} {{memos}} in {{date}}",
|
||||
"delete-confirm": "Are you sure you want to delete this memo?",
|
||||
"delete-confirm-description": "This action is irreversible. Attachments, links, and references will also be removed.",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import type { AttachmentItem } from "@/components/memo-metadata";
|
||||
|
||||
/**
|
||||
* Downloads a file using a temporary anchor element
|
||||
* @param url - The URL to download from
|
||||
* @param filename - The filename to save as
|
||||
* @param isLocal - Whether this is a local file that needs special handling
|
||||
*/
|
||||
const downloadFile = (
|
||||
url: string,
|
||||
filename: string,
|
||||
isLocal: boolean = false,
|
||||
): void => {
|
||||
const downloadElement = document.createElement("a");
|
||||
downloadElement.href = url;
|
||||
downloadElement.download = filename;
|
||||
|
||||
// For local files, ensure proper download behavior
|
||||
if (isLocal) {
|
||||
downloadElement.setAttribute("download", filename);
|
||||
downloadElement.target = "_blank";
|
||||
}
|
||||
|
||||
document.body.appendChild(downloadElement);
|
||||
downloadElement.click();
|
||||
document.body.removeChild(downloadElement);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a date prefix in YYYY/MM/DD format
|
||||
* @param date - Date object to format (defaults to current date)
|
||||
* @returns Formatted date string
|
||||
*/
|
||||
const createDatePrefix = (date: Date = new Date()): string => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}/${month}/${day}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts memo short name from full name path
|
||||
* @param memoName - Full memo name (format: memos/{id})
|
||||
* @returns Short memo name
|
||||
*/
|
||||
const extractMemoName = (memoName: string): string => {
|
||||
return memoName.split("/").pop() || "memo";
|
||||
};
|
||||
|
||||
/**
|
||||
* Downloads memo content as a markdown file if content exists
|
||||
* @param memo - The memo object
|
||||
* @param namePrefix - The filename prefix to use
|
||||
*/
|
||||
const downloadMemoContent = (memo: Memo, namePrefix: string): void => {
|
||||
if (!memo.content || !memo.content.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentBlob = new Blob([memo.content], {
|
||||
type: "text/markdown;charset=utf-8",
|
||||
});
|
||||
const contentUrl = URL.createObjectURL(contentBlob);
|
||||
|
||||
downloadFile(contentUrl, `${namePrefix}.md`);
|
||||
|
||||
URL.revokeObjectURL(contentUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Downloads all attachments from a memo
|
||||
* @param attachmentItems - Array of attachment items
|
||||
* @param namePrefix - The filename prefix to use
|
||||
*/
|
||||
const downloadAttachments = (
|
||||
attachmentItems: AttachmentItem[],
|
||||
namePrefix: string,
|
||||
): void => {
|
||||
attachmentItems.forEach((item) => {
|
||||
downloadFile(
|
||||
item.sourceUrl,
|
||||
`${namePrefix} - ${item.filename}`,
|
||||
item.isLocal,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Downloads all content and attachments from a memo
|
||||
* @param memo - The memo object
|
||||
* @param attachmentItems - Array of attachment items
|
||||
*/
|
||||
export const downloadMemoContentAndAttachments = (
|
||||
memo: Memo,
|
||||
attachmentItems: AttachmentItem[],
|
||||
): void => {
|
||||
const date = new Date();
|
||||
const datePrefix = createDatePrefix(date);
|
||||
const memoShortName = extractMemoName(memo.name);
|
||||
const namePrefix = `${datePrefix} ${memoShortName}`;
|
||||
|
||||
// Download memo content first
|
||||
downloadMemoContent(memo, namePrefix);
|
||||
|
||||
// Then download all attachments
|
||||
downloadAttachments(attachmentItems, namePrefix);
|
||||
};
|
||||
|
||||
// Legacy export for backward compatibility
|
||||
export const download = downloadMemoContentAndAttachments;
|
||||
Loading…
Reference in New Issue