feat: add export function to message
This commit is contained in:
parent
55317b5608
commit
acb2ea30fb
@ -257,7 +257,6 @@ body,
|
||||
}
|
||||
}
|
||||
.group-menu-bar {
|
||||
margin-left: 0;
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
code {
|
||||
|
||||
@ -337,7 +337,7 @@ export const PROVIDER_CONFIG = {
|
||||
},
|
||||
websites: {
|
||||
official: 'https://console.volcengine.com/ark/',
|
||||
apiKey: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey',
|
||||
apiKey: 'https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=DB4II4FC',
|
||||
docs: 'https://www.volcengine.com/docs/82379/1182403',
|
||||
models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
|
||||
}
|
||||
|
||||
@ -129,9 +129,9 @@
|
||||
"thinking": "Thinking",
|
||||
"topics.auto_rename": "Auto Rename",
|
||||
"topics.clear.title": "Clear Messages",
|
||||
"topics.copy.image": "Image",
|
||||
"topics.copy.md": "Markdown",
|
||||
"topics.copy.title": "Copy as",
|
||||
"topics.copy.image": "Copy as image",
|
||||
"topics.copy.md": "Copy as markdown",
|
||||
"topics.copy.title": "Copy",
|
||||
"topics.delete.shortcut": "Hold {{key}} to delete directly",
|
||||
"topics.edit.placeholder": "Enter new name",
|
||||
"topics.edit.title": "Edit Name",
|
||||
|
||||
@ -129,9 +129,9 @@
|
||||
"thinking": "思考中...",
|
||||
"topics.auto_rename": "自動リネーム",
|
||||
"topics.clear.title": "メッセージをクリア",
|
||||
"topics.copy.image": "画像",
|
||||
"topics.copy.md": "Markdown",
|
||||
"topics.copy.title": "複製",
|
||||
"topics.copy.image": "画像としてコピー",
|
||||
"topics.copy.md": "Markdownとしてコピー",
|
||||
"topics.copy.title": "コピー",
|
||||
"topics.delete.shortcut": "{{key}}キーを押しながらで直接削除",
|
||||
"topics.edit.placeholder": "新しい名前を入力",
|
||||
"topics.edit.title": "名前を編集",
|
||||
|
||||
@ -129,9 +129,9 @@
|
||||
"thinking": "Мыслим",
|
||||
"topics.auto_rename": "Автопереименование",
|
||||
"topics.clear.title": "Очистить сообщения",
|
||||
"topics.copy.image": "Изображение",
|
||||
"topics.copy.md": "Markdown",
|
||||
"topics.copy.title": "Скопировать как",
|
||||
"topics.copy.image": "Скопировать как изображение",
|
||||
"topics.copy.md": "Скопировать как Markdown",
|
||||
"topics.copy.title": "Скопировать",
|
||||
"topics.delete.shortcut": "Удерживайте {{key}} для мгновенного удаления",
|
||||
"topics.edit.placeholder": "Введите новый заголовок",
|
||||
"topics.edit.title": "Редактировать заголовок",
|
||||
|
||||
@ -129,9 +129,9 @@
|
||||
"thinking": "思考中",
|
||||
"topics.auto_rename": "生成话题名",
|
||||
"topics.clear.title": "清空消息",
|
||||
"topics.copy.image": "图片",
|
||||
"topics.copy.md": "Markdown",
|
||||
"topics.copy.title": "复制为",
|
||||
"topics.copy.image": "复制为图片",
|
||||
"topics.copy.md": "复制为 Markdown",
|
||||
"topics.copy.title": "复制",
|
||||
"topics.delete.shortcut": "按住 {{key}} 可直接删除",
|
||||
"topics.edit.placeholder": "输入新名称",
|
||||
"topics.edit.title": "编辑话题名",
|
||||
|
||||
@ -129,9 +129,9 @@
|
||||
"thinking": "思考中",
|
||||
"topics.auto_rename": "自動重新命名",
|
||||
"topics.clear.title": "清空消息",
|
||||
"topics.copy.image": "圖片",
|
||||
"topics.copy.md": "Markdown",
|
||||
"topics.copy.title": "複製為",
|
||||
"topics.copy.image": "複製為圖片",
|
||||
"topics.copy.md": "複製為 Markdown",
|
||||
"topics.copy.title": "複製",
|
||||
"topics.delete.shortcut": "按住 {{key}} 可直接刪除",
|
||||
"topics.edit.placeholder": "輸入新名稱",
|
||||
"topics.edit.title": "編輯名稱",
|
||||
|
||||
@ -208,6 +208,7 @@ const MessageItem: FC<Props> = ({
|
||||
isLastMessage={isLastMessage}
|
||||
isAssistantMessage={isAssistantMessage}
|
||||
isGrouped={isGrouped}
|
||||
messageContainerRef={messageContainerRef}
|
||||
setModel={setModel}
|
||||
onEditMessage={onEditMessage}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
@ -225,6 +226,7 @@ const MessageContainer = styled.div`
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease;
|
||||
padding: 0 20px;
|
||||
&.message-highlight {
|
||||
background-color: var(--color-primary-mute);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||
import { Message, Topic } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Popover } from 'antd'
|
||||
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -40,6 +41,7 @@ const MessageGroup: FC<Props> = ({
|
||||
|
||||
const isGrouped = messageLength > 1
|
||||
const isHorizontal = multiModelMessageStyle === 'horizontal'
|
||||
const isGrid = multiModelMessageStyle === 'grid'
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
window.modal.confirm({
|
||||
@ -62,10 +64,17 @@ const MessageGroup: FC<Props> = ({
|
||||
}, [messageLength])
|
||||
|
||||
return (
|
||||
<GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
|
||||
<GridContainer $count={messageLength} $layout={multiModelMessageStyle} $gridColumns={gridColumns}>
|
||||
<GroupContainer
|
||||
$isGrouped={isGrouped}
|
||||
$layout={multiModelMessageStyle}
|
||||
className={classNames([isGrouped && 'group-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
|
||||
<GridContainer
|
||||
$count={messageLength}
|
||||
$layout={multiModelMessageStyle}
|
||||
$gridColumns={gridColumns}
|
||||
className={classNames([isGrouped && 'group-grid-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
|
||||
{messages.map((message, index) => {
|
||||
const isGridGroupMessage = multiModelMessageStyle === 'grid' && message.role === 'assistant' && isGrouped
|
||||
const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped
|
||||
if (isGridGroupMessage) {
|
||||
return (
|
||||
<Popover
|
||||
@ -161,30 +170,46 @@ const MessageGroup: FC<Props> = ({
|
||||
|
||||
const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>`
|
||||
padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && 'horizontal' === $layout ? '15px' : '0')};
|
||||
&.group-container.horizontal,
|
||||
&.group-container.grid {
|
||||
padding: 0 20px;
|
||||
.message {
|
||||
padding: 0;
|
||||
}
|
||||
.group-menu-bar {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle; $gridColumns: number }>`
|
||||
width: 100%;
|
||||
display: grid;
|
||||
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
|
||||
overflow-y: auto;
|
||||
grid-template-columns: repeat(
|
||||
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
|
||||
minmax(550px, 1fr)
|
||||
);
|
||||
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
|
||||
@media (max-width: 800px) {
|
||||
grid-template-columns: repeat(
|
||||
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
|
||||
minmax(400px, 1fr)
|
||||
);
|
||||
}
|
||||
overflow-y: auto;
|
||||
${({ $layout }) =>
|
||||
$layout === 'horizontal' &&
|
||||
css`
|
||||
margin-top: 15px;
|
||||
`}
|
||||
${({ $gridColumns, $layout, $count }) =>
|
||||
$layout === 'grid' &&
|
||||
css`
|
||||
margin-top: 15px;
|
||||
grid-template-columns: repeat(${$count > 1 ? $gridColumns || 2 : 1}, minmax(0, 1fr));
|
||||
grid-template-rows: auto;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
`}
|
||||
`
|
||||
|
||||
@ -220,11 +245,11 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||
return ''
|
||||
}}
|
||||
|
||||
${({ $layout, $isInPopover, $isGrouped }) =>
|
||||
$layout === 'grid' && $isGrouped
|
||||
${({ $layout, $isInPopover, $isGrouped }) => {
|
||||
return $layout === 'grid' && $isGrouped
|
||||
? css`
|
||||
max-height: ${$isInPopover ? '50vh' : '300px'};
|
||||
overflow-y: auto;
|
||||
overflow-y: ${$isInPopover ? 'auto' : 'hidden'};
|
||||
border: 0.5px solid ${$isInPopover ? 'transparent' : 'var(--color-border)'};
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
@ -233,7 +258,8 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||
: css`
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
`}
|
||||
`
|
||||
}}
|
||||
`
|
||||
|
||||
export default memo(MessageGroup)
|
||||
|
||||
@ -35,7 +35,7 @@ const MessageGroupMenuBar: FC<Props> = ({
|
||||
onDelete
|
||||
}) => {
|
||||
return (
|
||||
<GroupMenuBar $layout={multiModelMessageStyle}>
|
||||
<GroupMenuBar $layout={multiModelMessageStyle} className="group-menu-bar">
|
||||
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
|
||||
<LayoutContainer>
|
||||
{['fold', 'vertical', 'horizontal', 'grid'].map((layout) => (
|
||||
@ -93,6 +93,7 @@ const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0 20px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
margin-top: 10px;
|
||||
@ -100,7 +101,6 @@ const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
|
||||
overflow: hidden;
|
||||
border: 0.5px solid var(--color-border);
|
||||
height: 40px;
|
||||
transition: all 0.3s ease;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
|
||||
@ -11,15 +11,22 @@ import {
|
||||
SyncOutlined,
|
||||
TranslationOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { UploadOutlined } from '@ant-design/icons'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { resetAssistantMessage } from '@renderer/services/MessagesService'
|
||||
import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { removeTrailingDoubleSpaces, uuid } from '@renderer/utils'
|
||||
import {
|
||||
captureScrollableDivAsBlob,
|
||||
captureScrollableDivAsDataURL,
|
||||
removeTrailingDoubleSpaces,
|
||||
uuid
|
||||
} from '@renderer/utils'
|
||||
import { exportMarkdownToNotion, exportMessageAsMarkdown, messageToMarkdown } from '@renderer/utils/export'
|
||||
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { isEmpty } from 'lodash'
|
||||
@ -35,6 +42,7 @@ interface Props {
|
||||
isGrouped?: boolean
|
||||
isLastMessage: boolean
|
||||
isAssistantMessage: boolean
|
||||
messageContainerRef: React.RefObject<HTMLDivElement>
|
||||
setModel: (model: Model) => void
|
||||
onEditMessage?: (message: Message) => void
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
@ -50,6 +58,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
isLastMessage,
|
||||
isAssistantMessage,
|
||||
assistantModel,
|
||||
messageContainerRef,
|
||||
onEditMessage,
|
||||
onDeleteMessage,
|
||||
onGetMessages
|
||||
@ -194,9 +203,61 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
key: 'new-branch',
|
||||
icon: <ForkOutlined />,
|
||||
onClick: onNewBranch
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.export.title'),
|
||||
key: 'export',
|
||||
icon: <UploadOutlined />,
|
||||
children: [
|
||||
{
|
||||
label: t('chat.topics.copy.image'),
|
||||
key: 'img',
|
||||
onClick: async () => {
|
||||
await captureScrollableDivAsBlob(messageContainerRef, async (blob) => {
|
||||
if (blob) {
|
||||
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.export.image'),
|
||||
key: 'image',
|
||||
onClick: async () => {
|
||||
const imageData = await captureScrollableDivAsDataURL(messageContainerRef)
|
||||
const title = getMessageTitle(message)
|
||||
if (title && imageData) {
|
||||
window.api.file.saveImage(title, imageData)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.export.md'),
|
||||
key: 'markdown',
|
||||
onClick: () => exportMessageAsMarkdown(message)
|
||||
},
|
||||
|
||||
{
|
||||
label: t('chat.topics.export.word'),
|
||||
key: 'word',
|
||||
onClick: async () => {
|
||||
const markdown = messageToMarkdown(message)
|
||||
window.api.export.toWord(markdown, getMessageTitle(message))
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.export.notion'),
|
||||
key: 'notion',
|
||||
onClick: async () => {
|
||||
const title = getMessageTitle(message)
|
||||
const markdown = messageToMarkdown(message)
|
||||
exportMarkdownToNotion(title, markdown)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[message, onEdit, onNewBranch, t]
|
||||
[message, messageContainerRef, onEdit, onNewBranch, t]
|
||||
)
|
||||
|
||||
const onRegenerate = async (e: React.MouseEvent | undefined) => {
|
||||
|
||||
@ -347,7 +347,6 @@ const LoaderContainer = styled.div<LoaderProps>`
|
||||
const ScrollContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
interface ContainerProps {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import {
|
||||
ClearOutlined,
|
||||
CloseOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
FolderOutlined,
|
||||
@ -10,6 +9,7 @@ import {
|
||||
UploadOutlined
|
||||
} from '@ant-design/icons'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
@ -23,7 +23,7 @@ import store from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { copyTopicAsMarkdown } from '@renderer/utils/copy'
|
||||
import { exportTopicAsMarkdown, exportTopicToNotion, topicToMarkdown } from '@renderer/utils/export'
|
||||
import { exportMarkdownToNotion, exportTopicAsMarkdown, topicToMarkdown } from '@renderer/utils/export'
|
||||
import { Dropdown, MenuProps, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { findIndex } from 'lodash'
|
||||
@ -194,7 +194,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
{
|
||||
label: t('chat.topics.copy.title'),
|
||||
key: 'copy',
|
||||
icon: <CopyOutlined />,
|
||||
icon: <CopyIcon />,
|
||||
children: [
|
||||
{
|
||||
label: t('chat.topics.copy.image'),
|
||||
@ -235,7 +235,10 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
{
|
||||
label: t('chat.topics.export.notion'),
|
||||
key: 'notion',
|
||||
onClick: () => exportTopicToNotion(topic)
|
||||
onClick: async () => {
|
||||
const markdown = await topicToMarkdown(topic)
|
||||
exportMarkdownToNotion(topic.name, markdown)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { Assistant, Message, Model, Topic } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { isEmpty, remove, takeRight } from 'lodash'
|
||||
import { NavigateFunction } from 'react-router'
|
||||
|
||||
@ -168,3 +169,27 @@ export function resetAssistantMessage(message: Message, model?: Model): Message
|
||||
useful: undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function getMessageTitle(message: Message, length = 30) {
|
||||
let title = message.content.split('\n')[0]
|
||||
|
||||
if (title.includes('.')) {
|
||||
title = title.split('.')[0]
|
||||
} else if (title.includes(',')) {
|
||||
title = title.split(',')[0]
|
||||
} else if (title.includes(',')) {
|
||||
title = title.split(',')[0]
|
||||
} else if (title.includes('。')) {
|
||||
title = title.split('。')[0]
|
||||
}
|
||||
|
||||
if (title.length > length) {
|
||||
title = title.slice(0, length)
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
title = dayjs(message.createdAt).format('YYYYMMDDHHmm')
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Client } from '@notionhq/client'
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { getMessageTitle } from '@renderer/services/MessagesService'
|
||||
import store from '@renderer/store'
|
||||
import { setExportState } from '@renderer/store/runtime'
|
||||
import { Message, Topic } from '@renderer/types'
|
||||
@ -34,24 +35,32 @@ export const exportTopicAsMarkdown = async (topic: Topic) => {
|
||||
window.api.file.save(fileName, markdown)
|
||||
}
|
||||
|
||||
export const exportTopicToNotion = async (topic: Topic) => {
|
||||
export const exportMessageAsMarkdown = async (message: Message) => {
|
||||
const fileName = getMessageTitle(message) + '.md'
|
||||
const markdown = messageToMarkdown(message)
|
||||
window.api.file.save(fileName, markdown)
|
||||
}
|
||||
|
||||
export const exportMarkdownToNotion = async (title: string, content: string) => {
|
||||
const { isExporting } = store.getState().runtime.export
|
||||
|
||||
if (isExporting) {
|
||||
window.message.warning({ content: i18n.t('message.warn.notion.exporting'), key: 'notion-exporting' })
|
||||
return
|
||||
}
|
||||
setExportState({
|
||||
isExporting: true
|
||||
})
|
||||
|
||||
setExportState({ isExporting: true })
|
||||
|
||||
const { notionDatabaseID, notionApiKey } = store.getState().settings
|
||||
|
||||
if (!notionApiKey || !notionDatabaseID) {
|
||||
window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const notion = new Client({ auth: notionApiKey })
|
||||
const markdown = await topicToMarkdown(topic)
|
||||
const requestBody = JSON.stringify({ md: markdown })
|
||||
const requestBody = JSON.stringify({ md: content })
|
||||
|
||||
const res = await fetch('https://md2notion.hilars.dev', {
|
||||
method: 'POST',
|
||||
@ -68,10 +77,10 @@ export const exportTopicToNotion = async (topic: Topic) => {
|
||||
parent: { database_id: notionDatabaseID },
|
||||
properties: {
|
||||
[store.getState().settings.notionPageNameKey || 'Name']: {
|
||||
title: [{ text: { content: topic.name } }]
|
||||
title: [{ text: { content: title } }]
|
||||
}
|
||||
},
|
||||
children: notionBlocks // 使用转换后的块
|
||||
children: notionBlocks
|
||||
})
|
||||
|
||||
window.message.success({ content: i18n.t('message.success.notion.export'), key: 'notion-success' })
|
||||
|
||||
@ -344,6 +344,7 @@ export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElemen
|
||||
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
|
||||
export const captureScrollableDivAsDataURL = async (divRef: React.RefObject<HTMLDivElement>) => {
|
||||
return captureScrollableDiv(divRef).then((canvas) => {
|
||||
if (canvas) {
|
||||
@ -352,11 +353,13 @@ export const captureScrollableDivAsDataURL = async (divRef: React.RefObject<HTML
|
||||
return Promise.resolve(undefined)
|
||||
})
|
||||
}
|
||||
|
||||
export const captureScrollableDivAsBlob = async (divRef: React.RefObject<HTMLDivElement>, func: BlobCallback) => {
|
||||
await captureScrollableDiv(divRef).then((canvas) => {
|
||||
canvas?.toBlob(func, 'image/png')
|
||||
})
|
||||
}
|
||||
|
||||
export function hasPath(url: string): boolean {
|
||||
try {
|
||||
const parsedUrl = new URL(url)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user