feat: support media files upload via paste and drop

This commit is contained in:
codytseng
2025-05-09 23:23:17 +08:00
parent 533e00d4ee
commit 4bfdd4f334
20 changed files with 162 additions and 85 deletions

View File

@@ -1,6 +1,7 @@
import Note from '@/components/Note' import Note from '@/components/Note'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useToast } from '@/hooks/use-toast' import { useToast } from '@/hooks/use-toast'
import { createCommentDraftEvent, createShortTextNoteDraftEvent } from '@/lib/draft-event' import { createCommentDraftEvent, createShortTextNoteDraftEvent } from '@/lib/draft-event'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
@@ -9,7 +10,7 @@ import { ChevronDown, ImageUp, LoaderCircle } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import TextareaWithMentions from '../TextareaWithMentions' import PostTextarea from '../PostTextarea'
import Mentions from './Mentions' import Mentions from './Mentions'
import PostOptions from './PostOptions' import PostOptions from './PostOptions'
import Preview from './Preview' import Preview from './Preview'
@@ -123,14 +124,29 @@ export default function NormalPostContent({
</div> </div>
</ScrollArea> </ScrollArea>
)} )}
<TextareaWithMentions <Tabs defaultValue="edit" className="space-y-4">
className="h-32" <TabsList>
<TabsTrigger value="edit">{t('Edit')}</TabsTrigger>
<TabsTrigger value="preview">{t('Preview')}</TabsTrigger>
</TabsList>
<TabsContent value="edit">
<PostTextarea
className="h-52"
setTextValue={setContent} setTextValue={setContent}
textValue={content} textValue={content}
placeholder={t('Write something...')} placeholder={
t('Write something...') + ' (' + t('Paste or drop media files to upload') + ')'
}
cursorOffset={cursorOffset} cursorOffset={cursorOffset}
onUploadImage={({ url, tags }) => {
setPictureInfos((prev) => [...prev, { url, tags }])
}}
/> />
{processedContent && <Preview content={processedContent} />} </TabsContent>
<TabsContent value="preview">
<Preview content={processedContent} />
</TabsContent>
</Tabs>
<SendOnlyToSwitch <SendOnlyToSwitch
parentEvent={parentEvent} parentEvent={parentEvent}
specifiedRelayUrls={specifiedRelayUrls} specifiedRelayUrls={specifiedRelayUrls}
@@ -141,7 +157,7 @@ export default function NormalPostContent({
<Uploader <Uploader
onUploadSuccess={({ url, tags }) => { onUploadSuccess={({ url, tags }) => {
setPictureInfos((prev) => [...prev, { url, tags }]) setPictureInfos((prev) => [...prev, { url, tags }])
setContent((prev) => `${prev}\n${url}`) setContent((prev) => (prev === '' ? url : `${prev}\n${url}`))
}} }}
onUploadingChange={setUploadingPicture} onUploadingChange={setUploadingPicture}
accept="image/*,video/*,audio/*" accept="image/*,video/*,audio/*"

View File

@@ -8,7 +8,7 @@ import { ChevronDown, Loader, LoaderCircle, Plus, X } from 'lucide-react'
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Image from '../Image' import Image from '../Image'
import TextareaWithMentions from '../TextareaWithMentions' import PostTextarea from '../PostTextarea'
import Mentions from './Mentions' import Mentions from './Mentions'
import PostOptions from './PostOptions' import PostOptions from './PostOptions'
import SendOnlyToSwitch from './SendOnlyToSwitch' import SendOnlyToSwitch from './SendOnlyToSwitch'
@@ -105,7 +105,7 @@ export default function PicturePostContent({ close }: { close: () => void }) {
{t('A special note for picture-first clients like Olas')} {t('A special note for picture-first clients like Olas')}
</div> </div>
<PictureUploader pictureInfos={pictureInfos} setPictureInfos={setPictureInfos} /> <PictureUploader pictureInfos={pictureInfos} setPictureInfos={setPictureInfos} />
<TextareaWithMentions <PostTextarea
className="h-32" className="h-32"
setTextValue={setContent} setTextValue={setContent}
textValue={content} textValue={content}

View File

@@ -4,7 +4,7 @@ import Content from '../Content'
export default function Preview({ content }: { content: string }) { export default function Preview({ content }: { content: string }) {
return ( return (
<Card className="p-3"> <Card className="p-3 min-h-52">
<Content <Content
event={{ event={{
content, content,
@@ -15,7 +15,7 @@ export default function Preview({ content }: { content: string }) {
pubkey: '', pubkey: '',
sig: '' sig: ''
}} }}
className="pointer-events-none" className="pointer-events-none h-full"
/> />
</Card> </Card>
) )

View File

@@ -20,14 +20,15 @@ export default function Uploader({
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0] if (!event.target.files) return
if (!file) return
try {
onUploadingChange?.(true) onUploadingChange?.(true)
try {
for (const file of event.target.files) {
const result = await upload(file) const result = await upload(file)
console.log('File uploaded successfully', result) console.log('File uploaded successfully', result)
onUploadSuccess(result) onUploadSuccess(result)
}
} catch (error) { } catch (error) {
console.error('Error uploading file', error) console.error('Error uploading file', error)
toast({ toast({
@@ -59,6 +60,7 @@ export default function Uploader({
style={{ display: 'none' }} style={{ display: 'none' }}
onChange={handleFileChange} onChange={handleFileChange}
accept={accept} accept={accept}
multiple
/> />
</div> </div>
) )

View File

@@ -6,14 +6,17 @@ import {
DialogTitle DialogTitle
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle
} from '@/components/ui/sheet'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { Dispatch, useMemo } from 'react' import { Dispatch, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '../ui/sheet'
import NormalPostContent from './NormalPostContent' import NormalPostContent from './NormalPostContent'
import PicturePostContent from './PicturePostContent'
import Title from './Title' import Title from './Title'
export default function PostEditor({ export default function PostEditor({
@@ -27,35 +30,17 @@ export default function PostEditor({
open: boolean open: boolean
setOpen: Dispatch<boolean> setOpen: Dispatch<boolean>
}) { }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const content = useMemo(() => { const content = useMemo(() => {
return parentEvent || defaultContent ? ( return (
<NormalPostContent <NormalPostContent
defaultContent={defaultContent} defaultContent={defaultContent}
parentEvent={parentEvent} parentEvent={parentEvent}
close={() => setOpen(false)} close={() => setOpen(false)}
/> />
) : (
<Tabs defaultValue="normal" className="space-y-4">
<TabsList>
<TabsTrigger value="normal">{t('Normal Note')}</TabsTrigger>
<TabsTrigger value="picture">{t('Picture Note')}</TabsTrigger>
</TabsList>
<TabsContent value="normal">
<NormalPostContent
defaultContent={defaultContent}
parentEvent={parentEvent}
close={() => setOpen(false)}
/>
</TabsContent>
<TabsContent value="picture">
<PicturePostContent close={() => setOpen(false)} />
</TabsContent>
</Tabs>
) )
}, [parentEvent, defaultContent]) }, [])
if (isSmallScreen) { if (isSmallScreen) {
return ( return (

View File

@@ -1,7 +1,9 @@
import { Command, CommandInput, CommandItem, CommandList } from '@/components/ui/command' import { Command, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { useToast } from '@/hooks'
import { pubkeyToNpub } from '@/lib/pubkey' import { pubkeyToNpub } from '@/lib/pubkey'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useMediaUploadService } from '@/providers/MediaUploadServiceProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import { TProfile } from '@/types' import { TProfile } from '@/types'
import React, { import React, {
@@ -18,22 +20,27 @@ import { SimpleUserAvatar } from '../UserAvatar'
import { SimpleUsername } from '../Username' import { SimpleUsername } from '../Username'
import { getCurrentWord, replaceWord } from './utils' import { getCurrentWord, replaceWord } from './utils'
export default function TextareaWithMentions({ export default function PostTextarea({
textValue, textValue,
setTextValue, setTextValue,
cursorOffset = 0, cursorOffset = 0,
onUploadImage,
...props ...props
}: ComponentProps<'textarea'> & { }: ComponentProps<'textarea'> & {
textValue: string textValue: string
setTextValue: Dispatch<SetStateAction<string>> setTextValue: Dispatch<SetStateAction<string>>
cursorOffset?: number cursorOffset?: number
onUploadImage?: ({ url, tags }: { url: string; tags: string[][] }) => void
}) { }) {
const { toast } = useToast()
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null) const dropdownRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const { upload } = useMediaUploadService()
const [commandValue, setCommandValue] = useState('') const [commandValue, setCommandValue] = useState('')
const [debouncedCommandValue, setDebouncedCommandValue] = useState(commandValue) const [debouncedCommandValue, setDebouncedCommandValue] = useState(commandValue)
const [profiles, setProfiles] = useState<TProfile[]>([]) const [profiles, setProfiles] = useState<TProfile[]>([])
const [dragover, setDragover] = useState(false)
useEffect(() => { useEffect(() => {
if (textareaRef.current && cursorOffset !== 0) { if (textareaRef.current && cursorOffset !== 0) {
@@ -170,9 +177,73 @@ export default function TextareaWithMentions({
} }
}, [handleBlur, handleKeyDown, handleMouseDown, handleSectionChange]) }, [handleBlur, handleKeyDown, handleMouseDown, handleSectionChange])
const uploadImages = async (files: File[]) => {
for (const file of files) {
if (file.type.startsWith('image/') || file.type.startsWith('video/')) {
const placeholder = `[Uploading "${file.name}"...]`
if (textValue.includes(placeholder)) {
continue
}
setTextValue((prev) => (prev === '' ? placeholder : `${prev}\n${placeholder}`))
try {
const result = await upload(file)
setTextValue((prev) => {
if (prev.includes(placeholder)) {
return prev.replace(placeholder, result.url)
} else {
return prev + `\n${result.url}`
}
})
onUploadImage?.(result)
} catch (error) {
console.error('Error uploading file', error)
toast({
variant: 'destructive',
title: 'Failed to upload file',
description: (error as Error).message
})
setTextValue((prev) => prev.replace(placeholder, ''))
}
}
}
}
const handlePast = async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
await uploadImages(
Array.from(e.clipboardData.items)
.map((item) => item.getAsFile())
.filter(Boolean) as File[]
)
}
const handleDrop = async (e: React.DragEvent<HTMLTextAreaElement>) => {
e.preventDefault()
setDragover(false)
await uploadImages(Array.from(e.dataTransfer.files))
}
return ( return (
<div className="relative w-full"> <div
<Textarea {...props} ref={textareaRef} value={textValue} onChange={onTextValueChange} /> className={cn(
'relative w-full',
dragover && 'outline-2 outline-offset-4 outline-dashed outline-border rounded-md'
)}
>
<Textarea
{...props}
ref={textareaRef}
value={textValue}
onChange={onTextValueChange}
onPaste={handlePast}
onDrop={handleDrop}
onDragOver={(e) => {
e.preventDefault()
setDragover(true)
}}
onDragLeave={() => {
setDragover(false)
}}
/>
<Command <Command
ref={dropdownRef} ref={dropdownRef}
className={cn( className={cn(

View File

@@ -110,8 +110,6 @@ export default {
'Picture note requires images': 'ملاحظة الصورة تتطلب صور', 'Picture note requires images': 'ملاحظة الصورة تتطلب صور',
Relays: 'الريلايات', Relays: 'الريلايات',
image: 'صورة', image: 'صورة',
'Normal Note': 'ملاحظة عادية',
'Picture Note': 'ملاحظة الصورة',
'R & W': 'قراءة وكتابة', 'R & W': 'قراءة وكتابة',
Read: 'قراءة', Read: 'قراءة',
Write: 'كتابة', Write: 'كتابة',
@@ -227,6 +225,8 @@ export default {
'Show more': 'عرض المزيد', 'Show more': 'عرض المزيد',
General: 'عام', General: 'عام',
Autoplay: 'التشغيل التلقائي', Autoplay: 'التشغيل التلقائي',
'Enable video autoplay on this device': 'تمكين التشغيل التلقائي للفيديو على هذا الجهاز' 'Enable video autoplay on this device': 'تمكين التشغيل التلقائي للفيديو على هذا الجهاز',
'Paste or drop media files to upload': 'الصق أو اسحب ملفات الوسائط لتحميلها',
Preview: 'معاينة'
} }
} }

View File

@@ -111,8 +111,6 @@ export default {
'Picture note requires images': 'Bildnotiz erfordert Bilder', 'Picture note requires images': 'Bildnotiz erfordert Bilder',
Relays: 'Relays', Relays: 'Relays',
image: 'Bild', image: 'Bild',
'Normal Note': 'Normale Notiz',
'Picture Note': 'Bildnotiz',
'R & W': 'R & W', 'R & W': 'R & W',
Read: 'Lesen', Read: 'Lesen',
Write: 'Schreiben', Write: 'Schreiben',
@@ -232,6 +230,9 @@ export default {
General: 'Allgemein', General: 'Allgemein',
Autoplay: 'Automatische Wiedergabe', Autoplay: 'Automatische Wiedergabe',
'Enable video autoplay on this device': 'Enable video autoplay on this device':
'Aktiviere die automatische Video-Wiedergabe auf diesem Gerät' 'Aktiviere die automatische Video-Wiedergabe auf diesem Gerät',
'Paste or drop media files to upload':
'Füge Medien-Dateien ein oder ziehe sie hierher, um sie hochzuladen',
Preview: 'Vorschau'
} }
} }

View File

@@ -110,8 +110,6 @@ export default {
'Picture note requires images': 'Picture note requires images', 'Picture note requires images': 'Picture note requires images',
Relays: 'Relays', Relays: 'Relays',
image: 'image', image: 'image',
'Normal Note': 'Normal Note',
'Picture Note': 'Picture Note',
'R & W': 'R & W', 'R & W': 'R & W',
Read: 'Read', Read: 'Read',
Write: 'Write', Write: 'Write',
@@ -227,6 +225,8 @@ export default {
'Show more': 'Show more', 'Show more': 'Show more',
General: 'General', General: 'General',
Autoplay: 'Autoplay', Autoplay: 'Autoplay',
'Enable video autoplay on this device': 'Enable video autoplay on this device' 'Enable video autoplay on this device': 'Enable video autoplay on this device',
'Paste or drop media files to upload': 'Paste or drop media files to upload',
Preview: 'Preview'
} }
} }

View File

@@ -111,8 +111,6 @@ export default {
'Picture note requires images': 'La nota con imagen requiere imágenes', 'Picture note requires images': 'La nota con imagen requiere imágenes',
Relays: 'Relés', Relays: 'Relés',
image: 'imagen', image: 'imagen',
'Normal Note': 'Nota normal',
'Picture Note': 'Nota con imagen',
'R & W': 'L y E', 'R & W': 'L y E',
Read: 'Leer', Read: 'Leer',
Write: 'Escribir', Write: 'Escribir',
@@ -232,6 +230,8 @@ export default {
General: 'General', General: 'General',
Autoplay: 'Reproducción automática', Autoplay: 'Reproducción automática',
'Enable video autoplay on this device': 'Enable video autoplay on this device':
'Habilitar reproducción automática de video en este dispositivo' 'Habilitar reproducción automática de video en este dispositivo',
'Paste or drop media files to upload': 'Pegar o soltar archivos multimedia para cargar',
Preview: 'Vista previa'
} }
} }

View File

@@ -111,8 +111,6 @@ export default {
'Picture note requires images': 'La note image nécessite des images', 'Picture note requires images': 'La note image nécessite des images',
Relays: 'Relais', Relays: 'Relais',
image: 'image', image: 'image',
'Normal Note': 'Note normale',
'Picture Note': 'Note image',
'R & W': 'R & W', 'R & W': 'R & W',
Read: 'Lire', Read: 'Lire',
Write: 'Écrire', Write: 'Écrire',
@@ -231,6 +229,9 @@ export default {
General: 'Général', General: 'Général',
Autoplay: 'Lecture automatique', Autoplay: 'Lecture automatique',
'Enable video autoplay on this device': 'Enable video autoplay on this device':
'Activer la lecture automatique des vidéos sur cet appareil' 'Activer la lecture automatique des vidéos sur cet appareil',
'Paste or drop media files to upload':
'Coller ou déposer des fichiers multimédias à télécharger',
Preview: 'Aperçu'
} }
} }

View File

@@ -111,8 +111,6 @@ export default {
'Picture note requires images': 'La nota illustrativa richiede immagini', 'Picture note requires images': 'La nota illustrativa richiede immagini',
Relays: 'Relays', Relays: 'Relays',
image: 'immagine', image: 'immagine',
'Normal Note': 'Nota Normale',
'Picture Note': 'Nota Immagine',
'R & W': 'L & S', 'R & W': 'L & S',
Read: 'Leggi', Read: 'Leggi',
Write: 'Scrivi', Write: 'Scrivi',
@@ -231,6 +229,8 @@ export default {
General: 'Generale', General: 'Generale',
Autoplay: 'Riproduzione automatica', Autoplay: 'Riproduzione automatica',
'Enable video autoplay on this device': 'Enable video autoplay on this device':
'Abilita riproduzione automatica video su questo dispositivo' 'Abilita riproduzione automatica video su questo dispositivo',
'Paste or drop media files to upload': 'Incolla o trascina i file multimediali per caricarli',
Preview: 'Anteprima'
} }
} }

View File

@@ -111,8 +111,6 @@ export default {
'Picture note requires images': '画像ノートには画像が必要です', 'Picture note requires images': '画像ノートには画像が必要です',
Relays: 'リレイ', Relays: 'リレイ',
image: '画像', image: '画像',
'Normal Note': '通常ノート',
'Picture Note': '画像ノート',
'R & W': '読&書', 'R & W': '読&書',
Read: '読む', Read: '読む',
Write: '書く', Write: '書く',
@@ -228,6 +226,8 @@ export default {
'Show more': 'もっと見る', 'Show more': 'もっと見る',
General: '一般', General: '一般',
Autoplay: '自動再生', Autoplay: '自動再生',
'Enable video autoplay on this device': 'このデバイスでのビデオ自動再生を有効にする' 'Enable video autoplay on this device': 'このデバイスでのビデオ自動再生を有効にする',
'Paste or drop media files to upload': 'メディアファイルを貼り付けるかドロップしてアップロード',
Preview: 'プレビュー'
} }
} }

View File

@@ -110,8 +110,6 @@ export default {
'Picture note requires images': 'Wpis graficzny wymaga obrazów', 'Picture note requires images': 'Wpis graficzny wymaga obrazów',
Relays: 'Transmitery', Relays: 'Transmitery',
image: 'grafika', image: 'grafika',
'Normal Note': 'Zwykły wpis',
'Picture Note': 'Wpis graficzny',
'R & W': 'O & Z', 'R & W': 'O & Z',
Read: 'Odczyt', Read: 'Odczyt',
Write: 'Zapis', Write: 'Zapis',
@@ -229,6 +227,9 @@ export default {
'Show more': 'Pokaż więcej', 'Show more': 'Pokaż więcej',
General: 'Ogólne', General: 'Ogólne',
Autoplay: 'Autoodtwarzanie', Autoplay: 'Autoodtwarzanie',
'Enable video autoplay on this device': 'Włącz automatyczne odtwarzanie wideo na tym urządzeniu' 'Enable video autoplay on this device':
'Włącz automatyczne odtwarzanie wideo na tym urządzeniu',
'Paste or drop media files to upload': 'Wklej lub upuść pliki multimedialne, aby przesłać',
Preview: 'Podgląd'
} }
} }

View File

@@ -110,8 +110,6 @@ export default {
'Picture note requires images': 'Nota de imagem requer imagens', 'Picture note requires images': 'Nota de imagem requer imagens',
Relays: 'Relés', Relays: 'Relés',
image: 'imagem', image: 'imagem',
'Normal Note': 'Nota de texto',
'Picture Note': 'Nota de imagem',
'R & W': 'Leitura & Escrita', 'R & W': 'Leitura & Escrita',
Read: 'Ler', Read: 'Ler',
Write: 'Escrever', Write: 'Escrever',
@@ -230,6 +228,8 @@ export default {
General: 'Geral', General: 'Geral',
Autoplay: 'Reprodução automática', Autoplay: 'Reprodução automática',
'Enable video autoplay on this device': 'Enable video autoplay on this device':
'Habilitar reprodução automática de vídeo neste dispositivo' 'Habilitar reprodução automática de vídeo neste dispositivo',
'Paste or drop media files to upload': 'Cole ou solte arquivos de mídia para fazer upload',
Preview: 'Pré-visualização'
} }
} }

View File

@@ -111,8 +111,6 @@ export default {
'Picture note requires images': 'Nota de imagem requer imagens', 'Picture note requires images': 'Nota de imagem requer imagens',
Relays: 'Relés', Relays: 'Relés',
image: 'imagem', image: 'imagem',
'Normal Note': 'Nota Normal',
'Picture Note': 'Nota de Imagem',
'R & W': 'Leitura & Escrita', 'R & W': 'Leitura & Escrita',
Read: 'Ler', Read: 'Ler',
Write: 'Escrever', Write: 'Escrever',
@@ -231,6 +229,8 @@ export default {
General: 'Geral', General: 'Geral',
Autoplay: 'Reprodução Automática', Autoplay: 'Reprodução Automática',
'Enable video autoplay on this device': 'Enable video autoplay on this device':
'Habilitar reprodução automática de vídeo neste dispositivo' 'Habilitar reprodução automática de vídeo neste dispositivo',
'Paste or drop media files to upload': 'Cole ou solte arquivos de mídia para fazer upload',
Preview: 'Pré-visualização'
} }
} }

View File

@@ -112,8 +112,6 @@ export default {
'Picture note requires images': 'Заметка с изображением требует наличия изображений', 'Picture note requires images': 'Заметка с изображением требует наличия изображений',
Relays: 'Ретрансляторы', Relays: 'Ретрансляторы',
image: 'изображение', image: 'изображение',
'Normal Note': 'Обычная заметка',
'Picture Note': 'Заметка с изображением',
'R & W': 'Чтение & Запись', 'R & W': 'Чтение & Запись',
Read: 'Читать', Read: 'Читать',
Write: 'Писать', Write: 'Писать',
@@ -231,6 +229,8 @@ export default {
'Show more': 'Показать больше', 'Show more': 'Показать больше',
General: 'Общие', General: 'Общие',
Autoplay: 'Автовоспроизведение', Autoplay: 'Автовоспроизведение',
'Enable video autoplay on this device': 'Включить автовоспроизведение видео на этом устройстве' 'Enable video autoplay on this device': 'Включить автовоспроизведение видео на этом устройстве',
'Paste or drop media files to upload': 'Вставьте или перетащите медиафайлы для загрузки',
Preview: 'Предварительный просмотр'
} }
} }

View File

@@ -110,8 +110,6 @@ export default {
Relays: '服务器', Relays: '服务器',
image: '图片', image: '图片',
Normal: '普通', Normal: '普通',
'Normal Note': '普通笔记',
'Picture Note': '图片笔记',
'R & W': '读写', 'R & W': '读写',
Read: '只读', Read: '只读',
Write: '只写', Write: '只写',
@@ -228,6 +226,8 @@ export default {
'Show more': '显示更多', 'Show more': '显示更多',
General: '常规', General: '常规',
Autoplay: '自动播放', Autoplay: '自动播放',
'Enable video autoplay on this device': '在此设备上启用视频自动播放' 'Enable video autoplay on this device': '在此设备上启用视频自动播放',
'Paste or drop media files to upload': '支持粘贴或拖放媒体文件进行上传',
Preview: '预览'
} }
} }

View File

@@ -63,7 +63,7 @@ export function MediaUploadServiceProvider({ children }: { children: React.React
}) })
if (!response.ok) { if (!response.ok) {
throw new Error(response.status.toString()) throw new Error(response.status.toString() + ' ' + response.statusText)
} }
const data = await response.json() const data = await response.json()