+
+
+ {files?.filter(i => i.previewType === 'audio').map((file, index) => {
+ const metadata = audioMetadata[file.name];
+ const isCurrentTrack = musicManager.currentTrack?.file.name === file.name;
+ const currentDuration = isCurrentTrack ? duration[file.name] : '0:00';
+ const currentPlayTime = isCurrentTrack ? currentTime[file.name] : '0:00';
+
+ return (
+
+ {metadata?.coverUrl && (
+ <>
+
+
+ >
+ )}
+
+
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('[AudioRender:PlayButton] 点击播放按钮');
+ togglePlay(file.name);
+ }}>
+ {metadata?.coverUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {metadata?.trackName || file.name}
+
+
+ {isCurrentTrack && (
+
+ {currentPlayTime} / {currentDuration}
+
+ )}
+
+
+ {metadata?.artists && metadata.artists.length > 0 && (
+
+ {metadata.artists.join(', ')}
+
+ )}
+
+
+ {isCurrentTrack && (
+
+ handleProgressBarClick(e, file.name)}
+ onMouseDown={(e) => handleProgressBarDrag(e, file.name)}
+ >
+
el && (progressRefs.current[file.name] = el)}
+ className={`absolute h-full rounded-full transition-all duration-100 ${metadata?.coverUrl ? 'bg-white' : 'bg-primary'
+ }`}
+ />
+
+
+ )}
+
+
+
+ {!file.uploadPromise?.loading?.value && !preview && (
+
+ )}
+ {preview && (
+
+ )}
+
+
+ );
+ })}
+
+ );
+})
\ No newline at end of file
diff --git a/src/components/Common/AttachmentRender/icons.tsx b/src/components/Common/AttachmentRender/icons.tsx
index ba4248f2..e742970e 100644
--- a/src/components/Common/AttachmentRender/icons.tsx
+++ b/src/components/Common/AttachmentRender/icons.tsx
@@ -10,6 +10,7 @@ import { PromiseState } from '@/store/standard/PromiseState';
import { BlinkoStore } from '@/store/blinkoStore';
import { helper } from '@/lib/helper';
import { FileType } from '../Editor/type';
+import { DialogStandaloneStore } from '@/store/module/DialogStandalone';
export const DeleteIcon = observer(({ className, file, files, size = 20 }: { className: string, file: FileType, files: FileType[], size?: number }) => {
const store = RootStore.Local(() => ({
@@ -21,7 +22,7 @@ export const DeleteIcon = observer(({ className, file, files, size = 20 }: { cla
});
const index = files.findIndex(i => i.name == file.name)
files.splice(index, 1)
- RootStore.Get(DialogStore).close()
+ RootStore.Get(DialogStandaloneStore).close()
RootStore.Get(ToastPlugin).success(t('delete-success'))
RootStore.Get(BlinkoStore).updateTicker++
}
diff --git a/src/components/Common/AttachmentRender/imageRender.tsx b/src/components/Common/AttachmentRender/imageRender.tsx
index 3bdf9cdb..ab068065 100644
--- a/src/components/Common/AttachmentRender/imageRender.tsx
+++ b/src/components/Common/AttachmentRender/imageRender.tsx
@@ -1,28 +1,29 @@
import React, { useEffect, useMemo, useState } from 'react';
import { FileType } from '../Editor/type';
-import { Image, Skeleton } from '@nextui-org/react';
+import { Image } from '@nextui-org/react';
import { PhotoProvider, PhotoView } from 'react-photo-view';
import { Icon } from '@iconify/react';
import { DeleteIcon, DownloadIcon } from './icons';
import { observer } from 'mobx-react-lite';
-import { RootStore } from '@/store';
import { useMediaQuery } from 'usehooks-ts';
-import { api } from '@/lib/trpc';
+import { DraggableFileGrid } from './DraggableFileGrid';
type IProps = {
files: FileType[]
preview?: boolean
columns?: number
+ onReorder?: (newFiles: FileType[]) => void
}
const ImageThumbnailRender = ({ file, className }: { file: FileType, className?: string }) => {
const [isOriginalError, setIsOriginalError] = useState(false);
const [currentSrc, setCurrentSrc] = useState(
+ file.preview.includes('/api/s3file/') ? file.preview :
`${file.preview}?thumbnail=true`
);
useEffect(() => {
if (isOriginalError) {
- setCurrentSrc('/image-fallback.svg')
+ setCurrentSrc('/image-fallback.svg')
}
}, [isOriginalError])
@@ -31,13 +32,13 @@ const ImageThumbnailRender = ({ file, className }: { file: FileType, className?:
classNames={{
wrapper: '!max-w-full',
}}
+ draggable={false}
onError={() => {
if (file.preview === currentSrc) {
return setIsOriginalError(true)
}
setCurrentSrc(file.preview)
}}
- crossOrigin="use-credentials"
className={`object-cover w-full ${className}`}
/>
}
@@ -48,13 +49,14 @@ const ImageRender = observer((props: IProps) => {
const images = files?.filter(i => i.previewType == 'image')
const imageRenderClassName = useMemo(() => {
- const imageLength = files?.filter(i => i.previewType == 'image')?.length
+ if (!preview) {
+ return 'flex flex-row gap-2 overflow-x-auto pb-2'
+ }
+
+ const imageLength = images?.length
if (columns) {
return `grid grid-cols-${columns} gap-2`
}
- if (!preview && !isPc) {
- return `flex items-center overflow-x-scroll gap-2`
- }
if (imageLength == 1) {
return `grid grid-cols-2 gap-2`
}
@@ -65,16 +67,17 @@ const ImageRender = observer((props: IProps) => {
return `grid grid-cols-3 gap-3`
}
return ''
- }, [images])
+ }, [images, preview, columns])
const imageHeight = useMemo(() => {
- const imageLength = files?.filter(i => i.previewType == 'image')?.length
+ if (!preview) {
+ return 'h-[160px] w-[160px]'
+ }
+
+ const imageLength = images?.length
if (columns) {
return `max-h-[100px] w-auto`
}
- if (!preview && !isPc) {
- return `h-[80px] w-[80px] min-w-[80px]`
- }
if (imageLength == 1) {
return `h-[200px] max-h-[200px] md:max-w-[200px]`
}
@@ -85,31 +88,45 @@ const ImageRender = observer((props: IProps) => {
return `lg:h-[160px] md:h-[120px] h-[100px]`
}
return ''
- }, [images])
+ }, [images, preview, columns])
- return
-
- {images.map((file, index) => (
-
- {file.uploadPromise?.loading?.value &&
-
-
}
-
- {!file.uploadPromise?.loading?.value && !preview &&
-
- }
- {preview &&
- }
+ const renderImage = (file: FileType) => (
+
+ {file.uploadPromise?.loading?.value && (
+
+
- ))}
+ )}
+
+ {!file.uploadPromise?.loading?.value && !preview &&
+
+ }
+ {preview &&
}
+
+ )
+
+ return (
+
+
-
+ )
})
export { ImageRender }
\ No newline at end of file
diff --git a/src/components/Common/AttachmentRender/index.tsx b/src/components/Common/AttachmentRender/index.tsx
index baf829cd..05abd951 100644
--- a/src/components/Common/AttachmentRender/index.tsx
+++ b/src/components/Common/AttachmentRender/index.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { FileIcons } from '../FileIcon';
+import { FileIcons } from './FileIcon';
import { observer } from 'mobx-react-lite';
import { helper } from '@/lib/helper';
import { type Attachment } from '@/server/types';
@@ -8,15 +8,13 @@ import { DeleteIcon, DownloadIcon } from './icons';
import { ImageRender } from './imageRender';
import { HandleFileType } from '../Editor/editorUtils';
import { Icon } from '@iconify/react';
-import { RootStore } from '@/store';
-import { BlinkoStore } from '@/store/blinkoStore';
import { Popover, PopoverContent, PopoverTrigger } from '@nextui-org/popover';
import { BlinkoCard } from '@/components/BlinkoCard';
import { api } from '@/lib/trpc';
-import { PromiseState } from '@/store/standard/PromiseState';
-import { cache } from '@/lib/cache';
-import { reaction } from 'mobx';
import { EditorStore } from '../Editor/editorStore';
+import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd-next';
+import { DraggableFileGrid } from './DraggableFileGrid';
+import { AudioRender } from './audioRender';
//https://www.npmjs.com/package/browser-thumbnail-generator
@@ -24,65 +22,112 @@ type IProps = {
files: FileType[]
preview?: boolean
columns?: number
+ onReorder?: (newFiles: FileType[]) => void
}
const AttachmentsRender = observer((props: IProps) => {
const { files, preview = false, columns = 3 } = props
- return <>
- {/* image render */}
-
- {/* video render todo:improve style*/}
-
- {files?.filter(i => i.previewType == 'video').map((file, index) => (
-
-
- ))}
-
- {/* audio render todo:improve style*/}
-
- {files?.filter(i => i.previewType == 'audio').map((file, index) => (
-
-
- {!file.uploadPromise?.loading?.value && !preview &&
-
- }
- {preview &&
}
-
- ))}
-
-
-
- {/* other file render */}
-
- {files?.filter(i => i.previewType == 'other').map((file, index) => (
-
{
- if (preview) {
- helper.download.downloadByLink(file.uploadPromise.value)
- }
- }} className='relative flex p-2 w-full items-center gap-2 cursor-pointer bg-sencondbackground hover:bg-hover tansition-all rounded-md group'>
-
-
{file.name}
- {!file.uploadPromise?.loading?.value && !preview &&
-
}
-
- ))}
-
- >
+ const gridClassName = preview
+ ? `grid grid-cols-${(columns - 1) < 1 ? 1 : (columns - 1)} md:grid-cols-${columns} gap-2 mt-3`
+ : 'flex flex-row gap-2 overflow-x-auto pb-2 mt-3';
+
+ return (
+
+ {/* image render */}
+
+
+ {/* video render */}
+
+ {files?.filter(i => i.previewType == 'video').map((file, index) => (
+
+
+ ))}
+
+
+ {/* audio render */}
+
+
+ {/* other file render */}
+
(
+ {
+ if (preview) {
+ helper.download.downloadByLink(file.uploadPromise.value)
+ }
+ }}
+ >
+
+
{file.name}
+ {!file.uploadPromise?.loading?.value && !preview &&
+
+ }
+
+ )}
+ />
+
+ )
})
-const FilesAttachmentRender = observer(({ files, preview, columns }: { files: Attachment[], preview?: boolean, columns?: number }) => {
- const [handledFiles, setFiles] = useState([])
+const FilesAttachmentRender = observer(({
+ files,
+ preview,
+ columns,
+ onReorder
+}: {
+ files: Attachment[],
+ preview?: boolean,
+ columns?: number,
+ onReorder?: (newFiles: Attachment[]) => void
+}) => {
+ const [handledFiles, setFiles] = useState([]);
+
useEffect(() => {
- setFiles(HandleFileType(files))
- }, [files])
- return
-})
+ setFiles(HandleFileType(files));
+ }, [files]);
+
+ const handleReorder = (newFiles: FileType[]) => {
+ const newAttachments = files.slice().sort((a, b) => {
+ const aIndex = newFiles.findIndex(f => f.name === a.name);
+ const bIndex = newFiles.findIndex(f => f.name === b.name);
+ return aIndex - bIndex;
+ });
+ onReorder?.(newAttachments);
+ };
+
+ return (
+
+ );
+});
const ReferenceRender = observer(({ store }: { store: EditorStore }) => {
diff --git a/src/components/Common/Editor/Toolbar/AIWriteButton/index.tsx b/src/components/Common/Editor/Toolbar/AIWriteButton/index.tsx
new file mode 100644
index 00000000..9d3799b5
--- /dev/null
+++ b/src/components/Common/Editor/Toolbar/AIWriteButton/index.tsx
@@ -0,0 +1,167 @@
+import { IconButton } from '../IconButton';
+import { useTranslation } from 'react-i18next';
+import { EditorStore } from '../../editorStore';
+import { useMediaQuery } from 'usehooks-ts';
+import { Input, Button } from '@nextui-org/react';
+import { Popover, PopoverContent, PopoverTrigger } from '@nextui-org/react';
+import { ScrollArea } from '@/components/Common/ScrollArea';
+import { BlinkoStore } from '@/store/blinkoStore';
+import { RootStore } from '@/store/root';
+import { AiStore } from '@/store/aiStore';
+import { observer } from 'mobx-react-lite';
+import { useEffect, useRef } from 'react';
+import { Icon } from '@iconify/react';
+import { SendIcon } from '@/components/Common/Icons';
+import { MarkdownRender } from '@/components/Common/MarkdownRender';
+import { eventBus } from '@/lib/event';
+
+interface Props {
+ store: EditorStore;
+ content: string;
+}
+
+export const AIWriteButton = observer(({ store, content }: Props) => {
+ const { t } = useTranslation();
+ const isPc = useMediaQuery('(min-width: 768px)');
+ const blinko = RootStore.Get(BlinkoStore);
+ const ai = RootStore.Get(AiStore);
+ const scrollRef = useRef(null);
+
+ const localStore = RootStore.Local(() => ({
+ show: false,
+ setShow: (show: boolean) => {
+ localStore.show = show;
+ },
+
+ async handleSubmit() {
+ if (!ai.writeQuestion.trim()) return;
+ try {
+ ai.writeStream('custom', blinko.isCreateMode ? blinko.noteContent : blinko.curSelectedNote!.content);
+ } catch (error) {
+ console.error('error:', error);
+ }
+ }
+ }));
+
+ useEffect(() => {
+ scrollRef.current?.scrollToBottom();
+ }, [ai.writingResponseText]);
+
+ return (
+
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ localStore.setShow(true);
+ }}>
+
+
+
+
+
+
+ ai.writeQuestion = e.target.value}
+ placeholder={'Prompt...'}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ localStore.handleSubmit();
+ }
+ }}
+ startContent={}
+ endContent={
+ ai.isLoading ?
+ :
+
+ }
+ />
+
+
+
+ {ai.writingResponseText && (
+
{ }}>
+ {ai.isLoading ?
+ {ai.writingResponseText}
:
+
+ }
+
+ )}
+
+ {ai.isWriting && (
+
+
+
+
+
+
+
+ )}
+
+
+ }
+ variant='flat'
+ color='warning'
+ size='sm'
+ onPress={() => {
+ ai.writeStream('expand', blinko.isCreateMode ? blinko.noteContent : blinko.curSelectedNote!.content);
+ }}
+ >
+ {t('ai-expand')}
+
+
+ }
+ variant='flat'
+ color='warning'
+ size='sm'
+ onPress={() => {
+ ai.writeStream('polish', blinko.isCreateMode ? blinko.noteContent : blinko.curSelectedNote!.content);
+ }}
+ >
+ {t('ai-polish')}
+
+
+
+
+
+
+
+
+ );
+});
\ No newline at end of file
diff --git a/src/components/Common/Editor/Toolbar/ReferenceButton/index.tsx b/src/components/Common/Editor/Toolbar/ReferenceButton/index.tsx
index d68e955e..a07c1b4d 100644
--- a/src/components/Common/Editor/Toolbar/ReferenceButton/index.tsx
+++ b/src/components/Common/Editor/Toolbar/ReferenceButton/index.tsx
@@ -45,7 +45,7 @@ export const ReferenceButton = observer(({ store }: Props) => {
size='sm'
/>
{ blinko.referenceSearchList.callNextPage({}) }}
>
{blinko.referenceSearchList && blinko.referenceSearchList?.value?.map(i => {
diff --git a/src/components/Common/Editor/Toolbar/SendButton/index.tsx b/src/components/Common/Editor/Toolbar/SendButton/index.tsx
index 80f93997..b8b1801c 100644
--- a/src/components/Common/Editor/Toolbar/SendButton/index.tsx
+++ b/src/components/Common/Editor/Toolbar/SendButton/index.tsx
@@ -2,8 +2,6 @@ import { Button } from '@nextui-org/react';
import { Icon } from '@iconify/react';
import { SendIcon } from '../../../Icons';
import { EditorStore } from '../../editorStore';
-import { useMediaQuery } from 'usehooks-ts';
-import { Div } from '@/components/Common/Div';
import { observer } from 'mobx-react-lite';
interface Props {
@@ -13,25 +11,29 @@ interface Props {
export const SendButton = observer(({ store, isSendLoading }: Props) => {
return (
-
-