mirror of https://github.com/usememos/memos.git
chore: move memo-metadata components to MemoView and MemoEditor
- Remove shared memo-metadata folder - Move metadata display components (AttachmentList, LocationDisplay, RelationList) to MemoView/components/metadata - Move attachment types and utilities (LocalFile, AttachmentItem, toAttachmentItems) to MemoEditor/types/attachment - Simplify AttachmentList and AttachmentCard to work directly with Attachment proto - Update all imports across MemoEditor and MemoView components - Better separation of concerns: MemoView handles display, MemoEditor handles local files + attachments
This commit is contained in:
parent
a6e8ba7fb2
commit
e761ef8684
|
|
@ -4,7 +4,6 @@ import { FileIcon, LinkIcon, LoaderIcon, MapPinIcon, Maximize2Icon, MoreHorizont
|
|||
import { useEffect, useState } from "react";
|
||||
import { useDebounce } from "react-use";
|
||||
import { useReverseGeocoding } from "@/components/map";
|
||||
import type { LocalFile } from "@/components/memo-metadata";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -22,6 +21,7 @@ import { LinkMemoDialog, LocationDialog } from "../components";
|
|||
import { useFileUpload, useLinkMemo, useLocation } from "../hooks";
|
||||
import { useEditorContext } from "../state";
|
||||
import type { InsertMenuProps } from "../types";
|
||||
import type { LocalFile } from "../types/attachment";
|
||||
|
||||
const InsertMenu = (props: InsertMenuProps) => {
|
||||
const t = useTranslate();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { ChevronDownIcon, ChevronUpIcon, FileIcon, Loader2Icon, PaperclipIcon, XIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import type { LocalFile } from "@/components/memo-metadata/types";
|
||||
import { toAttachmentItems } from "@/components/memo-metadata/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
|
||||
import type { LocalFile } from "../types/attachment";
|
||||
import { toAttachmentItems } from "../types/attachment";
|
||||
|
||||
interface AttachmentListProps {
|
||||
attachments: Attachment[];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { forwardRef } from "react";
|
||||
import type { LocalFile } from "@/components/memo-metadata";
|
||||
import Editor, { type EditorRefActions } from "../Editor";
|
||||
import { useBlobUrls, useDragAndDrop } from "../hooks";
|
||||
import { useEditorContext } from "../state";
|
||||
import type { EditorContentProps } from "../types";
|
||||
import type { LocalFile } from "../types/attachment";
|
||||
|
||||
export const EditorContent = forwardRef<EditorRefActions, EditorContentProps>(({ placeholder }, ref) => {
|
||||
const { state, actions, dispatch } = useEditorContext();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useRef } from "react";
|
||||
import type { LocalFile } from "@/components/memo-metadata";
|
||||
import type { LocalFile } from "../types/attachment";
|
||||
|
||||
export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { create } from "@bufbuild/protobuf";
|
||||
import type { LocalFile } from "@/components/memo-metadata";
|
||||
import { attachmentServiceClient } from "@/connect";
|
||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import { AttachmentSchema } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import type { LocalFile } from "../types/attachment";
|
||||
|
||||
export const uploadService = {
|
||||
async uploadFiles(localFiles: LocalFile[]): Promise<Attachment[]> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { LocalFile } from "@/components/memo-metadata";
|
||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import type { LocalFile } from "../types/attachment";
|
||||
import type { EditorAction, EditorState, LoadingKey } from "./types";
|
||||
|
||||
export const editorActions = {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { LocalFile } from "@/components/memo-metadata";
|
||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import type { Location, MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { Visibility } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import type { LocalFile } from "../types/attachment";
|
||||
|
||||
export type LoadingKey = "saving" | "uploading" | "loading";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,9 @@
|
|||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
||||
|
||||
export type DisplayMode = "edit" | "view";
|
||||
|
||||
export interface BaseMetadataProps {
|
||||
mode: DisplayMode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type FileCategory = "image" | "video" | "document";
|
||||
|
||||
// Pure view model for rendering attachments and local files
|
||||
// Unified view model for rendering attachments and local files
|
||||
export interface AttachmentItem {
|
||||
readonly id: string;
|
||||
readonly filename: string;
|
||||
|
|
@ -22,6 +15,12 @@ export interface AttachmentItem {
|
|||
readonly isLocal: boolean;
|
||||
}
|
||||
|
||||
// For MemoEditor: local files being uploaded
|
||||
export interface LocalFile {
|
||||
readonly file: File;
|
||||
readonly previewUrl: string;
|
||||
}
|
||||
|
||||
function categorizeFile(mimeType: string): FileCategory {
|
||||
if (mimeType.startsWith("image/")) return "image";
|
||||
if (mimeType.startsWith("video/")) return "video";
|
||||
|
|
@ -46,7 +45,7 @@ export function attachmentToItem(attachment: Attachment): AttachmentItem {
|
|||
|
||||
export function fileToItem(file: File, blobUrl: string): AttachmentItem {
|
||||
return {
|
||||
id: blobUrl, // Use blob URL as unique ID since we don't have a server ID yet
|
||||
id: blobUrl,
|
||||
filename: file.name,
|
||||
category: categorizeFile(file.type),
|
||||
mimeType: file.type,
|
||||
|
|
@ -57,11 +56,6 @@ export function fileToItem(file: File, blobUrl: string): AttachmentItem {
|
|||
};
|
||||
}
|
||||
|
||||
export interface LocalFile {
|
||||
readonly file: File;
|
||||
readonly previewUrl: string;
|
||||
}
|
||||
|
||||
export function toAttachmentItems(attachments: Attachment[], localFiles: LocalFile[] = []): AttachmentItem[] {
|
||||
return [...attachments.map(attachmentToItem), ...localFiles.map(({ file, previewUrl }) => fileToItem(file, previewUrl))];
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { createContext } from "react";
|
||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import type { LocalFile } from "../../memo-metadata";
|
||||
import type { LocalFile } from "./attachment";
|
||||
|
||||
export interface MemoEditorContextValue {
|
||||
attachmentList: Attachment[];
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
|
|||
import { useTranslate } from "@/utils/i18n";
|
||||
import MemoContent from "../../MemoContent";
|
||||
import { MemoReactionListView } from "../../MemoReactionListView";
|
||||
import { AttachmentList, LocationDisplay, RelationList } from "../../memo-metadata";
|
||||
import { useMemoViewContext } from "../MemoViewContext";
|
||||
import type { MemoBodyProps } from "../types";
|
||||
import { AttachmentList, LocationDisplay, RelationList } from "./metadata";
|
||||
|
||||
const MemoBody: React.FC<MemoBodyProps> = ({ compact, onContentClick, onContentDoubleClick, onToggleNsfwVisibility }) => {
|
||||
const t = useTranslate();
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import type { AttachmentItem } from "./types";
|
||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
||||
|
||||
interface AttachmentCardProps {
|
||||
item: AttachmentItem;
|
||||
mode: "view";
|
||||
attachment: Attachment;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const AttachmentCard = ({ item, onClick, className }: AttachmentCardProps) => {
|
||||
const { category, filename, sourceUrl } = item;
|
||||
const AttachmentCard = ({ attachment, onClick, className }: AttachmentCardProps) => {
|
||||
const attachmentType = getAttachmentType(attachment);
|
||||
const sourceUrl = getAttachmentUrl(attachment);
|
||||
|
||||
if (category === "image") {
|
||||
if (attachmentType === "image/*") {
|
||||
return (
|
||||
<img
|
||||
src={sourceUrl}
|
||||
alt={filename}
|
||||
alt={attachment.filename}
|
||||
className={cn("w-full h-full object-cover rounded-lg cursor-pointer", className)}
|
||||
onClick={onClick}
|
||||
loading="lazy"
|
||||
|
|
@ -23,7 +24,7 @@ const AttachmentCard = ({ item, onClick, className }: AttachmentCardProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (category === "video") {
|
||||
if (attachmentType === "video/*") {
|
||||
return <video src={sourceUrl} className={cn("w-full h-full object-cover rounded-lg", className)} controls preload="metadata" />;
|
||||
}
|
||||
|
||||
|
|
@ -1,15 +1,30 @@
|
|||
import { useState } from "react";
|
||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
||||
import MemoAttachment from "../MemoAttachment";
|
||||
import PreviewImageDialog from "../PreviewImageDialog";
|
||||
import MemoAttachment from "../../../MemoAttachment";
|
||||
import PreviewImageDialog from "../../../PreviewImageDialog";
|
||||
import AttachmentCard from "./AttachmentCard";
|
||||
import { separateMediaAndDocs, toAttachmentItems } from "./types";
|
||||
|
||||
interface AttachmentListProps {
|
||||
attachments: Attachment[];
|
||||
}
|
||||
|
||||
function separateMediaAndDocs(attachments: Attachment[]): { media: Attachment[]; docs: Attachment[] } {
|
||||
const media: Attachment[] = [];
|
||||
const docs: Attachment[] = [];
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const attachmentType = getAttachmentType(attachment);
|
||||
if (attachmentType === "image/*" || attachmentType === "video/*") {
|
||||
media.push(attachment);
|
||||
} else {
|
||||
docs.push(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
return { media, docs };
|
||||
}
|
||||
|
||||
const AttachmentList = ({ attachments }: AttachmentListProps) => {
|
||||
const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number }>({
|
||||
open: false,
|
||||
|
|
@ -25,8 +40,7 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
|
|||
setPreviewImage({ open: true, urls: imgUrls, index });
|
||||
};
|
||||
|
||||
const items = toAttachmentItems(attachments, []);
|
||||
const { media: mediaItems, docs: docItems } = separateMediaAndDocs(items);
|
||||
const { media: mediaItems, docs: docItems } = separateMediaAndDocs(attachments);
|
||||
|
||||
if (attachments.length === 0) {
|
||||
return null;
|
||||
|
|
@ -36,13 +50,12 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
|
|||
<>
|
||||
{mediaItems.length > 0 && (
|
||||
<div className="w-full flex flex-row justify-start overflow-auto gap-2">
|
||||
{mediaItems.map((item) => (
|
||||
<div key={item.id} className="max-w-[60%] w-fit flex flex-col justify-start items-start shrink-0">
|
||||
{mediaItems.map((attachment) => (
|
||||
<div key={attachment.name} className="max-w-[60%] w-fit flex flex-col justify-start items-start shrink-0">
|
||||
<AttachmentCard
|
||||
item={item}
|
||||
mode="view"
|
||||
attachment={attachment}
|
||||
onClick={() => {
|
||||
handleImageClick(item.sourceUrl, attachments);
|
||||
handleImageClick(getAttachmentUrl(attachment), mediaItems);
|
||||
}}
|
||||
className="max-h-64 grow"
|
||||
/>
|
||||
|
|
@ -53,16 +66,15 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
|
|||
|
||||
{docItems.length > 0 && (
|
||||
<div className="w-full flex flex-row justify-start overflow-auto gap-2">
|
||||
{docItems.map((item) => {
|
||||
const attachment = attachments.find((a) => a.name === item.id);
|
||||
return attachment ? <MemoAttachment key={item.id} attachment={attachment} /> : null;
|
||||
})}
|
||||
{docItems.map((attachment) => (
|
||||
<MemoAttachment key={attachment.name} attachment={attachment} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PreviewImageDialog
|
||||
open={previewImage.open}
|
||||
onOpenChange={(open) => setPreviewImage((prev) => ({ ...prev, open }))}
|
||||
onOpenChange={(open: boolean) => setPreviewImage((prev) => ({ ...prev, open }))}
|
||||
imgUrls={previewImage.urls}
|
||||
initialIndex={previewImage.index}
|
||||
/>
|
||||
|
|
@ -4,7 +4,7 @@ import { useState } from "react";
|
|||
import { LocationPicker } from "@/components/map";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../../../ui/popover";
|
||||
|
||||
interface LocationDisplayProps {
|
||||
location?: Location;
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
export { default as AttachmentCard } from "./AttachmentCard";
|
||||
export { default as AttachmentList } from "./AttachmentList";
|
||||
export { default as LocationDisplay } from "./LocationDisplay";
|
||||
|
||||
// Base components (can be used for other metadata types)
|
||||
export { default as MetadataCard } from "./MetadataCard";
|
||||
export { default as RelationCard } from "./RelationCard";
|
||||
export { default as RelationList } from "./RelationList";
|
||||
|
||||
// Types
|
||||
export type { AttachmentItem, FileCategory, LocalFile } from "./types";
|
||||
export { attachmentToItem, fileToItem, filterByCategory, separateMediaAndDocs, toAttachmentItems } from "./types";
|
||||
Loading…
Reference in New Issue