mirror of
https://git.v0l.io/florian/bouquet.git
synced 2025-03-17 21:13:02 +01:00
feat: Added support for thumb re upload
This commit is contained in:
parent
694cb4cc49
commit
27e9eda2b0
13
package-lock.json
generated
13
package-lock.json
generated
@ -17,6 +17,7 @@
|
||||
"add": "^2.0.6",
|
||||
"axios": "^1.7.2",
|
||||
"blossom-client-sdk": "^0.9.0",
|
||||
"compress.js": "^2.1.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"id3js": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
@ -29,6 +30,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "^5.35.6",
|
||||
"@types/compress.js": "^1.1.3",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
@ -1215,6 +1217,12 @@
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/compress.js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/compress.js/-/compress.js-1.1.3.tgz",
|
||||
"integrity": "sha512-3uflodXzp7qcfxCwbkZ0KrqwgLIXzR/0IHoJvLfWmFXCDrB85aVSZFElsVkAm8ng227oFpNLFT4yWnm0Ozfhfw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
@ -1998,6 +2006,11 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/compress.js": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/compress.js/-/compress.js-2.1.2.tgz",
|
||||
"integrity": "sha512-DBb6M4wwe0rRAPeiKQ8HJrWuocVppUw9Qte4rEXiDrc5X3TrzeRKLzpvSE9oZ0Nd4HTXSSFphj3/XWwuptkQqw=="
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
33
package.json
33
package.json
@ -12,43 +12,44 @@
|
||||
"analyze": "vite-bundle-visualizer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@heroicons/react": "^2.1.4",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@nostr-dev-kit/ndk": "^2.8.2",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "^2.4.2",
|
||||
"@tanstack/react-query": "^5.39.0",
|
||||
"@tanstack/react-query-devtools": "^5.39.0",
|
||||
"@tanstack/react-query": "^5.50.1",
|
||||
"@tanstack/react-query-devtools": "^5.50.1",
|
||||
"add": "^2.0.6",
|
||||
"axios": "^1.7.2",
|
||||
"blossom-client-sdk": "^0.9.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"dayjs": "^1.11.11",
|
||||
"id3js": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nostr-tools": "^2.5.2",
|
||||
"p-limit": "^5.0.0",
|
||||
"nostr-tools": "^2.7.0",
|
||||
"p-limit": "^6.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-pdf": "^8.0.2",
|
||||
"react-router-dom": "^6.23.1"
|
||||
"react-pdf": "^9.1.0",
|
||||
"react-router-dom": "^6.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "^5.35.6",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@tanstack/eslint-plugin-query": "^5.50.1",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.10.0",
|
||||
"@typescript-eslint/parser": "^7.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "latest",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.2",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.3",
|
||||
"vite-bundle-visualizer": "^1.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
29
src/components/BlurImage.tsx
Normal file
29
src/components/BlurImage.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { decode } from 'blurhash';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
type BlurhashImageProps = {
|
||||
blurhash: string;
|
||||
width: number;
|
||||
height: number;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
export function BlurhashImage({ blurhash, width, height, ...props }: BlurhashImageProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const pixels = decode(blurhash, width, height);
|
||||
if (ctx) {
|
||||
const imageData = ctx.createImageData(width, height);
|
||||
imageData.data.set(pixels);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
}
|
||||
}, [blurhash, width, height]);
|
||||
|
||||
return <canvas ref={canvasRef} width={width} height={height} {...props} />;
|
||||
}
|
@ -3,19 +3,23 @@ import { formatFileSize } from '../../utils/utils';
|
||||
import { fetchId3Tag } from '../../utils/id3';
|
||||
import useVideoThumbnailDvm from './dvm';
|
||||
import { usePublishing } from './usePublishing';
|
||||
import { BlobDescriptor } from 'blossom-client-sdk';
|
||||
import { transferBlob } from '../../utils/transfer';
|
||||
import { useNDK } from '../../utils/ndk';
|
||||
|
||||
export type FileEventData = {
|
||||
originalFile: File;
|
||||
content: string;
|
||||
url: string[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
dim?: string;
|
||||
x: string;
|
||||
m?: string;
|
||||
size: number;
|
||||
thumbnails?: string[];
|
||||
thumbnail?: string;
|
||||
//summary: string;
|
||||
//alt: string;
|
||||
blurHash?: string;
|
||||
|
||||
artist?: string;
|
||||
title?: string;
|
||||
@ -24,13 +28,19 @@ export type FileEventData = {
|
||||
};
|
||||
|
||||
const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
const { signEventTemplate } = useNDK();
|
||||
const [fileEventData, setFileEventData] = useState(data);
|
||||
const [selectedThumbnail, setSelectedThumbnail] = useState<string | undefined>();
|
||||
|
||||
const { createDvmThumbnailRequest, thumbnailRequestEventId } = useVideoThumbnailDvm(setFileEventData);
|
||||
const { publishAudioEvent, publishFileEvent, publishVideoEvent } = usePublishing();
|
||||
const [jsonOutput, setJsonOutput] = useState('');
|
||||
|
||||
const isAudio = fileEventData.m?.startsWith('audio/');
|
||||
const isVideo = fileEventData.m?.startsWith('video/');
|
||||
|
||||
useEffect(() => {
|
||||
if (fileEventData.m?.startsWith('video/') && fileEventData.thumbnails == undefined) {
|
||||
if (isVideo && fileEventData.thumbnails == undefined) {
|
||||
createDvmThumbnailRequest(fileEventData);
|
||||
}
|
||||
if (
|
||||
@ -62,9 +72,37 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
}
|
||||
}, [fileEventData]);
|
||||
|
||||
function extractProtocolAndDomain(url: string): string | null {
|
||||
const regex = /^(https?:\/\/[^/]+)/;
|
||||
const match = url.match(regex);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
const publishSelectedThumbnailToOwnServer = async () => {
|
||||
const servers = data.url.map(extractProtocolAndDomain);
|
||||
|
||||
// upload selected thumbnail to the same blossom servers as the video
|
||||
let uploadedThumbnails: BlobDescriptor[] = [];
|
||||
if (selectedThumbnail) {
|
||||
uploadedThumbnails = (
|
||||
await Promise.all(
|
||||
servers.map(s => {
|
||||
if (s && selectedThumbnail) return transferBlob(selectedThumbnail, s, signEventTemplate);
|
||||
})
|
||||
)
|
||||
).filter(t => t !== undefined) as BlobDescriptor[];
|
||||
}
|
||||
|
||||
if (uploadedThumbnails.length > 0) {
|
||||
data.thumbnail = uploadedThumbnails[0].url; // TODO do we need multiple thumbsnails?? or server URLs?
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// TODO add tags editor
|
||||
return (
|
||||
<>
|
||||
<div className=" bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-row">
|
||||
<div className="bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-row">
|
||||
{fileEventData.m?.startsWith('video/') && (
|
||||
<>
|
||||
{thumbnailRequestEventId &&
|
||||
@ -82,7 +120,7 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
<a
|
||||
key={`link${i + 1}`}
|
||||
href={`#item${i + 1}`}
|
||||
onClick={() => setFileEventData(ed => ({ ...ed, thumbnail: t }))}
|
||||
onClick={() => setSelectedThumbnail(t)}
|
||||
className={'btn btn-xs ' + (t == fileEventData.thumbnail ? 'btn-primary' : '')}
|
||||
>{`${i + 1}`}</a>
|
||||
))}
|
||||
@ -95,9 +133,9 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{fileEventData.m?.startsWith('audio/') && fileEventData.thumbnail && (
|
||||
{isAudio && fileEventData.thumbnail && (
|
||||
<div className="w-2/6">
|
||||
<img src={fileEventData.thumbnail} className="w-full" />
|
||||
<img src={fileEventData.thumbnail || selectedThumbnail} className="w-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -111,31 +149,44 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-4 w-4/6" style={{ gridTemplateColumns: '1fr 30em' }}>
|
||||
{fileEventData.title && (
|
||||
{(isAudio || isVideo) && (
|
||||
<>
|
||||
<span className="font-bold">Title</span>
|
||||
<span>{fileEventData.title}</span>
|
||||
<input
|
||||
type="text"
|
||||
className="input input-primary"
|
||||
value={fileEventData.title}
|
||||
onChange={e => setFileEventData(ed => ({ ...ed, title: e.target.value }))}
|
||||
></input>
|
||||
</>
|
||||
)}
|
||||
{fileEventData.artist && (
|
||||
{isAudio && (
|
||||
<>
|
||||
<span className="font-bold">Artist</span>
|
||||
<span>{fileEventData.artist}</span>
|
||||
</>
|
||||
)}
|
||||
{fileEventData.album && (
|
||||
{isAudio && (
|
||||
<>
|
||||
<span className="font-bold">Album</span>
|
||||
<span>{fileEventData.album}</span>
|
||||
</>
|
||||
)}
|
||||
{fileEventData.year && (
|
||||
{isAudio && (
|
||||
<>
|
||||
<span className="font-bold">Year</span>
|
||||
<span>{fileEventData.year}</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
<span className="font-bold">Summary / Description</span>
|
||||
<textarea
|
||||
value={fileEventData.content}
|
||||
onChange={e => setFileEventData(ed => ({ ...ed, content: e.target.value }))}
|
||||
className="textarea textarea-primary"
|
||||
placeholder="Caption"
|
||||
></textarea>
|
||||
|
||||
<span className="font-bold">Type</span>
|
||||
<span>{fileEventData.m}</span>
|
||||
|
||||
@ -148,13 +199,6 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
|
||||
<span className="font-bold">File size</span>
|
||||
<span>{fileEventData.size ? formatFileSize(fileEventData.size) : 'unknown'}</span>
|
||||
<span className="font-bold">Content / Description</span>
|
||||
<textarea
|
||||
value={fileEventData.content}
|
||||
onChange={e => setFileEventData(ed => ({ ...ed, content: e.target.value }))}
|
||||
className="textarea"
|
||||
placeholder="Caption"
|
||||
></textarea>
|
||||
<span className="font-bold">URL</span>
|
||||
<div className="">
|
||||
{fileEventData.url.map((text, i) => (
|
||||
@ -172,7 +216,12 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={async () => setJsonOutput(await publishFileEvent(fileEventData))}
|
||||
onClick={async () => {
|
||||
if (!data.thumbnail) {
|
||||
await publishSelectedThumbnailToOwnServer();
|
||||
}
|
||||
setJsonOutput(await publishFileEvent(fileEventData));
|
||||
}}
|
||||
>
|
||||
Create File Event
|
||||
</button>
|
||||
@ -184,7 +233,12 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={async () => setJsonOutput(await publishVideoEvent(fileEventData))}
|
||||
onClick={async () => {
|
||||
if (!data.thumbnail) {
|
||||
await publishSelectedThumbnailToOwnServer();
|
||||
}
|
||||
setJsonOutput(await publishVideoEvent(fileEventData));
|
||||
}}
|
||||
>
|
||||
Create Video Event
|
||||
</button>
|
||||
|
@ -3,26 +3,24 @@ import dayjs from 'dayjs';
|
||||
import { FileEventData } from './FileEventEditor';
|
||||
import { uniq } from 'lodash';
|
||||
import { useNDK } from '../../utils/ndk';
|
||||
import { KIND_AUDIO, KIND_FILE_META, KIND_VIDEO_HORIZONTAL, KIND_VIDEO_VERTICAL } from '../../utils/useFileMetaEvents';
|
||||
|
||||
export const usePublishing = () => {
|
||||
const { ndk, user } = useNDK();
|
||||
|
||||
const publishFileEvent = async (data: FileEventData): Promise<string> => {
|
||||
// TODO REupload selected video thumbnail from DVM
|
||||
|
||||
// TODO where to put video title?
|
||||
const e: NostrEvent = {
|
||||
created_at: dayjs().unix(),
|
||||
content: data.content,
|
||||
tags: [
|
||||
...uniq(data.url).map(du => ['url', du]),
|
||||
['x', data.x],
|
||||
//['summary', data.summary],
|
||||
//['alt', data.alt],
|
||||
],
|
||||
kind: 1063,
|
||||
tags: [...uniq(data.url).map(du => ['url', du]), ['x', data.x], ['summary', data.content]],
|
||||
kind: KIND_FILE_META,
|
||||
pubkey: user?.pubkey || '',
|
||||
};
|
||||
|
||||
if (data.title) {
|
||||
e.tags.push(['alt', `${data.title}`]);
|
||||
}
|
||||
if (data.size) {
|
||||
e.tags.push(['size', `${data.size}`]);
|
||||
}
|
||||
@ -32,8 +30,10 @@ export const usePublishing = () => {
|
||||
if (data.m) {
|
||||
e.tags.push(['m', data.m]);
|
||||
}
|
||||
if (data.blurHash) {
|
||||
e.tags.push(['blurhash', data.blurHash]);
|
||||
}
|
||||
if (data.thumbnail) {
|
||||
// TODO upload thumbnail to own storage
|
||||
e.tags.push(['thumb', data.thumbnail]);
|
||||
e.tags.push(['image', data.thumbnail]);
|
||||
}
|
||||
@ -55,7 +55,7 @@ export const usePublishing = () => {
|
||||
['x', data.x],
|
||||
...uniq(data.url).map(du => ['imeta', `url ${du}`, `m ${data.m}`]),
|
||||
],
|
||||
kind: 31337, // TODO vertical video event based on dim?!
|
||||
kind: KIND_AUDIO,
|
||||
pubkey: user?.pubkey || '',
|
||||
};
|
||||
|
||||
@ -81,6 +81,8 @@ export const usePublishing = () => {
|
||||
};
|
||||
|
||||
const publishVideoEvent = async (data: FileEventData): Promise<string> => {
|
||||
const videoIsHorizontal = data.width == undefined || data.height == undefined || data.width > data.height;
|
||||
|
||||
const e: NostrEvent = {
|
||||
created_at: dayjs().unix(),
|
||||
content: data.content,
|
||||
@ -88,14 +90,16 @@ export const usePublishing = () => {
|
||||
['d', data.x],
|
||||
['x', data.x],
|
||||
['url', data.url[0]],
|
||||
['title', data.content],
|
||||
// ['summary', data.], TODO add summary
|
||||
['summary', data.content],
|
||||
['published_at', `${dayjs().unix()}`],
|
||||
['client', 'bouquet'],
|
||||
],
|
||||
kind: 31337,
|
||||
kind: videoIsHorizontal ? KIND_VIDEO_HORIZONTAL : KIND_VIDEO_VERTICAL,
|
||||
pubkey: user?.pubkey || '',
|
||||
};
|
||||
if (data.title) {
|
||||
e.tags.push(['title', data.title]);
|
||||
}
|
||||
if (data.size) {
|
||||
e.tags.push(['size', `${data.size}`]);
|
||||
}
|
||||
@ -106,7 +110,6 @@ export const usePublishing = () => {
|
||||
e.tags.push(['m', data.m]);
|
||||
}
|
||||
if (data.thumbnail) {
|
||||
// TODO upload to own blossom instance
|
||||
e.tags.push(['thumb', data.thumbnail]);
|
||||
e.tags.push(['preview', data.thumbnail]);
|
||||
}
|
||||
|
@ -8,15 +8,15 @@ import {
|
||||
import { ServerList } from '../components/ServerList/ServerList';
|
||||
import { useServerInfo } from '../utils/useServerInfo';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { BlobDescriptor, BlossomClient, SignedEvent } from 'blossom-client-sdk';
|
||||
import { BlobDescriptor } from 'blossom-client-sdk';
|
||||
import { useNDK } from '../utils/ndk';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { formatFileSize } from '../utils/utils';
|
||||
import BlobList from '../components/BlobList/BlobList';
|
||||
import './Transfer.css';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import axios, { AxiosProgressEvent } from 'axios';
|
||||
import ProgressBar from '../components/ProgressBar/ProgressBar';
|
||||
import { downloadBlob, uploadBlob } from '../utils/transfer';
|
||||
|
||||
type TransferStatus = {
|
||||
[key: string]: {
|
||||
@ -31,6 +31,9 @@ type TransferStatus = {
|
||||
};
|
||||
|
||||
export const Transfer = () => {
|
||||
// TODO add transfer for single files
|
||||
// TODO add support for mirror command (fallback to upload)
|
||||
|
||||
const { source } = useParams();
|
||||
const [transferSource, setTransferSource] = useState(source);
|
||||
const navigate = useNavigate();
|
||||
@ -60,38 +63,6 @@ export const Transfer = () => {
|
||||
return [];
|
||||
}, [serverInfo, transferSource, transferTarget]);
|
||||
|
||||
const uploadBlob = async (
|
||||
server: string,
|
||||
file: File,
|
||||
auth?: SignedEvent,
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||
) => {
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': file.type,
|
||||
};
|
||||
|
||||
const res = await axios.put<BlobDescriptor>(`${server}/upload`, file, {
|
||||
headers: auth ? { ...headers, authorization: BlossomClient.encodeAuthorizationHeader(auth) } : headers,
|
||||
onUploadProgress,
|
||||
});
|
||||
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const downloadBlob = async (
|
||||
server: string,
|
||||
sha256: string,
|
||||
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||
) => {
|
||||
const response = await axios.get(`${server}/${sha256}`, {
|
||||
responseType: 'blob',
|
||||
onDownloadProgress,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const performTransfer = async (sourceServer: string, targetServer: string, blobs: BlobDescriptor[]) => {
|
||||
setTransferLog({});
|
||||
setTransferCancelled(false);
|
||||
@ -111,7 +82,7 @@ export const Transfer = () => {
|
||||
},
|
||||
}));
|
||||
|
||||
const data = await downloadBlob(serverInfo[sourceServer].url, b.sha256, progressEvent => {
|
||||
const result = await downloadBlob(`${serverInfo[sourceServer].url}/${b.sha256}`, progressEvent => {
|
||||
setTransferLog(ts => ({
|
||||
...ts,
|
||||
[b.sha256]: {
|
||||
@ -136,12 +107,11 @@ export const Transfer = () => {
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!data) continue;
|
||||
if (!result) continue;
|
||||
|
||||
const file = new File([data], b.sha256, { type: b.type, lastModified: b.created });
|
||||
const uploadAuth = await BlossomClient.getUploadAuth(file, signEventTemplate, 'Upload Blob');
|
||||
const file = new File([result.data], b.sha256, { type: b.type, lastModified: b.created });
|
||||
|
||||
await uploadBlob(serverInfo[targetServer].url, file, uploadAuth, progressEvent => {
|
||||
await uploadBlob(serverInfo[targetServer].url, file, signEventTemplate, progressEvent => {
|
||||
setTransferLog(ts => ({
|
||||
...ts,
|
||||
[b.sha256]: {
|
||||
|
@ -13,6 +13,8 @@ import FileEventEditor, { FileEventData } from '../components/FileEventEditor/Fi
|
||||
import pLimit from 'p-limit';
|
||||
import { Server, useUserServers } from '../utils/useUserServers';
|
||||
import { resizeImage } from '../utils/resize';
|
||||
import { getImageSize } from '../utils/image';
|
||||
import { getBlurhashFromFile } from '../utils/blur';
|
||||
|
||||
type TransferStats = {
|
||||
enabled: boolean;
|
||||
@ -79,25 +81,6 @@ function Upload() {
|
||||
// const [resizeImages, setResizeImages] = useState(false);
|
||||
// const [publishToNostr, setPublishToNostr] = useState(false);
|
||||
|
||||
type ImageSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
const getImageSize = async (imageFile: File): Promise<ImageSize> => {
|
||||
const img = new Image();
|
||||
const objectUrl = URL.createObjectURL(imageFile);
|
||||
const promise = new Promise<ImageSize>((resolve, reject) => {
|
||||
img.onload = () => {
|
||||
resolve({ width: img.width, height: img.height });
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
img.onerror = () => reject();
|
||||
});
|
||||
img.src = objectUrl;
|
||||
return promise;
|
||||
};
|
||||
|
||||
async function uploadBlob(
|
||||
server: string,
|
||||
file: File,
|
||||
@ -147,11 +130,28 @@ function Upload() {
|
||||
} as FileEventData;
|
||||
if (file.type.startsWith('image/')) {
|
||||
const dimensions = await getImageSize(file);
|
||||
data = { ...data, dim: `${dimensions.width}x${dimensions.height}` };
|
||||
data = {
|
||||
...data,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
dim: `${dimensions.width}x${dimensions.height}`,
|
||||
};
|
||||
|
||||
// TODO maybe combine fileSize and Hash!
|
||||
const blur = await getBlurhashFromFile(file);
|
||||
if (blur) {
|
||||
data = {
|
||||
...data,
|
||||
blurHash: blur,
|
||||
};
|
||||
}
|
||||
}
|
||||
fileDimensions[file.name] = data;
|
||||
}
|
||||
|
||||
// TODO icon to cancel upload
|
||||
// TODO detect if the file already exists? if we have the hash??
|
||||
|
||||
const startTransfer = async (server: Server, primary: boolean) => {
|
||||
const serverUrl = serverInfo[server.name].url;
|
||||
let serverTransferred = 0;
|
||||
|
50
src/utils/blur.ts
Normal file
50
src/utils/blur.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { encode } from 'blurhash';
|
||||
|
||||
const loadImage = async (src: string): Promise<HTMLImageElement> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = (...args) => reject(args);
|
||||
img.src = src;
|
||||
});
|
||||
|
||||
const getImageData = (image: HTMLImageElement) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const context = canvas.getContext('2d');
|
||||
if (context) {
|
||||
context.drawImage(image, 0, 0);
|
||||
return context.getImageData(0, 0, image.width, image.height);
|
||||
}
|
||||
};
|
||||
|
||||
function getFileDataURL(file: File): Promise<string | ArrayBuffer | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
reader.onload = function (event) {
|
||||
if (event.target) {
|
||||
const dataURL = event.target.result;
|
||||
resolve(dataURL);
|
||||
}
|
||||
reject();
|
||||
};
|
||||
|
||||
reader.onerror = function (error) {
|
||||
reject(error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getBlurhashFromFile(file: File) {
|
||||
const imageUrl = await getFileDataURL(file);
|
||||
if (imageUrl) {
|
||||
const image = await loadImage(imageUrl?.toString());
|
||||
const imageData = getImageData(image);
|
||||
if (imageData) {
|
||||
return encode(imageData.data, imageData.width, imageData.height, 4, 3);
|
||||
}
|
||||
}
|
||||
}
|
18
src/utils/image.ts
Normal file
18
src/utils/image.ts
Normal file
@ -0,0 +1,18 @@
|
||||
type ImageSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export const getImageSize = async (imageFile: File): Promise<ImageSize> => {
|
||||
const img = new Image();
|
||||
const objectUrl = URL.createObjectURL(imageFile);
|
||||
const promise = new Promise<ImageSize>((resolve, reject) => {
|
||||
img.onload = () => {
|
||||
resolve({ width: img.width, height: img.height });
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
img.onerror = () => reject();
|
||||
});
|
||||
img.src = objectUrl;
|
||||
return promise;
|
||||
};
|
48
src/utils/transfer.ts
Normal file
48
src/utils/transfer.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import axios, { AxiosProgressEvent } from 'axios';
|
||||
import { BlobDescriptor, BlossomClient, EventTemplate, SignedEvent } from 'blossom-client-sdk';
|
||||
|
||||
export const uploadBlob = async (
|
||||
server: string,
|
||||
file: File,
|
||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>,
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||
) => {
|
||||
const uploadAuth = await BlossomClient.getUploadAuth(file, signEventTemplate, 'Upload Blob');
|
||||
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': file.type,
|
||||
};
|
||||
|
||||
const res = await axios.put<BlobDescriptor>(`${server}/upload`, file, {
|
||||
headers: uploadAuth ? { ...headers, authorization: BlossomClient.encodeAuthorizationHeader(uploadAuth) } : headers,
|
||||
onUploadProgress,
|
||||
});
|
||||
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const downloadBlob = async (url: string, onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void) => {
|
||||
const response = await axios.get(url, {
|
||||
responseType: 'blob',
|
||||
onDownloadProgress,
|
||||
});
|
||||
|
||||
return { data: response.data, type: response.headers['Content-Type']?.toString() };
|
||||
};
|
||||
|
||||
export const transferBlob = async (
|
||||
sourceUrl: string,
|
||||
targetServer: string,
|
||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>,
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||
): Promise<BlobDescriptor> => {
|
||||
console.log({ sourceUrl, targetServer });
|
||||
const result = await downloadBlob(sourceUrl, onUploadProgress);
|
||||
|
||||
const fileName = sourceUrl.replace(/.*\//, '');
|
||||
|
||||
const file = new File([result.data], fileName, { type: result.type, lastModified: new Date().getTime() });
|
||||
|
||||
return await uploadBlob(targetServer, file, signEventTemplate, onUploadProgress);
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user