adding generic file attachement to browser app

This commit is contained in:
Roland Osborne 2023-08-11 14:33:43 -07:00
parent f45750627e
commit 221b36895c
11 changed files with 327 additions and 17 deletions

View File

@ -15,6 +15,7 @@ func AddChannelTopicBlock(w http.ResponseWriter, r *http.Request) {
// scan parameters
params := mux.Vars(r)
topicID := params["topicID"]
body := r.FormValue("body")
channelSlot, guid, code, err := getChannelSlot(r, true)
if err != nil {
@ -58,12 +59,33 @@ func AddChannelTopicBlock(w http.ResponseWriter, r *http.Request) {
defer garbageSync.Unlock()
// save new file
var crc uint32
var size int64
id := uuid.New().String()
path := getStrConfigValue(CNFAssetPath, APPDefaultPath) + "/" + channelSlot.Account.GUID + "/" + id
crc, size, err := saveAsset(r.Body, path)
if body == "multipart" {
if err := r.ParseMultipartForm(32 << 20); err != nil {
ErrResponse(w, http.StatusBadRequest, err)
return
}
file, _, err := r.FormFile("asset")
if err != nil {
ErrResponse(w, http.StatusBadRequest, err)
return
}
defer file.Close()
crc, size, err = saveAsset(file, path)
if err != nil {
ErrResponse(w, http.StatusInternalServerError, err)
return
}
} else {
crc, size, err = saveAsset(r.Body, path)
if err != nil {
ErrResponse(w, http.StatusInternalServerError, err)
return
}
}
asset := &store.Asset{}

View File

@ -232,8 +232,8 @@ async function upload(entry, update, complete) {
entry.active = {};
try {
if (file.encrypted) {
const { size, getEncryptedBlock, position, label, image, video, audio } = file;
const { data, type } = image ? { data: image, type: 'image' } : video ? { data: video, type: 'video' } : audio ? { data: audio, type: 'audio' } : {}
const { size, getEncryptedBlock, position, label, extension, image, video, audio, binary } = file;
const { data, type } = image ? { data: image, type: 'image' } : video ? { data: video, type: 'video' } : audio ? { data: audio, type: 'audio' } : { data: binary, type: 'binary' }
const thumb = await getThumb(data, type, position);
const parts = [];
for (let pos = 0; pos < size; pos += ENCRYPTED_BLOCK_SIZE) {
@ -252,7 +252,7 @@ async function upload(entry, update, complete) {
parts.push({ blockIv, partId: part.data.assetId });
}
entry.assets.push({
encrypted: { type, thumb, label, parts }
encrypted: { type, thumb, label, extension, parts }
});
}
else if (file.image) {
@ -314,6 +314,25 @@ async function upload(entry, update, complete) {
}
});
}
else if (file.binary) {
const formData = new FormData();
formData.append('asset', file.binary);
let asset = await axios.post(`${entry.baseUrl}blocks${entry.urlParams}&body=multipart`, formData, {
signal: entry.cancel.signal,
onUploadProgress: (ev) => {
const { loaded, total } = ev;
entry.active = { loaded, total }
update();
},
});
entry.assets.push({
binary: {
label: file.label,
extension: file.extension,
data: asset.data.assetId,
}
});
}
entry.active = null;
upload(entry, update, complete);
}

View File

@ -2,10 +2,11 @@ import { AddTopicWrapper } from './AddTopic.styled';
import { useAddTopic } from './useAddTopic.hook';
import { Modal, Input, Menu, Dropdown, Spin } from 'antd';
import { useRef } from 'react';
import { SoundOutlined, VideoCameraOutlined, PictureOutlined, FontColorsOutlined, FontSizeOutlined, SendOutlined } from '@ant-design/icons';
import { FieldBinaryOutlined, SoundOutlined, VideoCameraOutlined, PictureOutlined, FontColorsOutlined, FontSizeOutlined, SendOutlined } from '@ant-design/icons';
import { SketchPicker } from "react-color";
import { AudioFile } from './audioFile/AudioFile';
import { VideoFile } from './videoFile/VideoFile';
import { BinaryFile } from './binaryFile/BinaryFile';
import { Carousel } from 'carousel/Carousel';
import { Gluejar } from '@charliewilco/gluejar'
@ -17,6 +18,7 @@ export function AddTopic({ contentKey }) {
const attachImage = useRef(null);
const attachAudio = useRef(null);
const attachVideo = useRef(null);
const attachBinary = useRef(null);
const msg = useRef();
const keyDown = (e) => {
@ -66,6 +68,11 @@ export function AddTopic({ contentKey }) {
attachVideo.current.value = '';
};
const onSelectBinary = (e) => {
actions.addBinary(e.target.files[0]);
attachBinary.current.value = '';
};
const renderItem = (item, index) => {
if (item.image) {
return <img style={{ height: 128, objectFit: 'contain' }} src={item.url} alt="" />
@ -76,6 +83,9 @@ export function AddTopic({ contentKey }) {
if (item.video) {
return <VideoFile onPosition={(pos) => actions.setPosition(index, pos)} url={item.url} />
}
if (item.binary) {
return <BinaryFile onLabel={(label) => actions.setLabel(index, label)} label={item.label} extension={item.extension} url={item.url} />
}
return <></>
};
@ -110,6 +120,7 @@ export function AddTopic({ contentKey }) {
<input type='file' name="asset" accept="image/*" ref={attachImage} onChange={e => onSelectImage(e)} style={{display: 'none'}}/>
<input type='file' name="asset" accept="audio/*" ref={attachAudio} onChange={e => onSelectAudio(e)} style={{display: 'none'}}/>
<input type='file' name="asset" accept="video/*" ref={attachVideo} onChange={e => onSelectVideo(e)} style={{display: 'none'}}/>
<input type='file' name="asset" accept="*/*" ref={attachBinary} onChange={e => onSelectBinary(e)} style={{display: 'none'}}/>
{ state.assets.length > 0 && (
<div class="assets">
<Carousel pad={32} items={state.assets} itemRenderer={renderItem} itemRemove={removeItem} />
@ -136,9 +147,10 @@ export function AddTopic({ contentKey }) {
<SoundOutlined />
</div>
)}
{ (state.enableImage || state.enableVideo || state.enableAudio) && (
<div class="button space" onClick={() => attachBinary.current.click()}>
<FieldBinaryOutlined />
</div>
<div class="bar space" />
)}
<div class="button space">
<Dropdown overlay={picker} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="top">
<FontColorsOutlined />

View File

@ -0,0 +1,29 @@
import { useState } from 'react';
import { Input } from 'antd';
import ReactResizeDetector from 'react-resize-detector';
import { BinaryFileWrapper } from './BinaryFile.styled';
export function BinaryFile({ url, extension, label, onLabel }) {
const [width, setWidth] = useState(0);
return (
<BinaryFileWrapper>
<ReactResizeDetector handleWidth={false} handleHeight={true}>
{({ height }) => {
if (height !== width) {
setWidth(height);
}
return <div style={{ height: '100%', width: width }} />
}}
</ReactResizeDetector>
<div class="player" style={{ width: width, height: width }}>
<div class="extension">{ extension }</div>
<div class="label">
<Input bordered={false} size="small" defaultValue={label} onChange={(e) => onLabel(e.target.value)} />
</div>
</div>
</BinaryFileWrapper>
)
}

View File

@ -0,0 +1,50 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const BinaryFileWrapper = styled.div`
position: relative;
height: 100%;
.player {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: #aaaaaa;
}
.background {
height: 100%;
object-fit: contain;
}
.extension {
font-size: 32px;
color: ${Colors.white};
}
.label {
bottom: 0;
position: absolute;
width: 100%;
overflow: hidden;
text-align: center;
color: white;
background-color: #cccccc;
}
.control {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
`;

View File

@ -128,6 +128,13 @@ export function useAddTopic(contentKey) {
asset.label = '';
addAsset(asset);
},
addBinary: async (binary) => {
const asset = await setUrl(binary);
asset.binary = binary;
asset.extension = binary.name.split('.').pop().toUpperCase();
asset.label = binary.name.slice(0, -1 * (asset.extension.length + 1));
addAsset(asset);
},
setLabel: (index, label) => {
updateAsset(index, { label });
},

View File

@ -2,6 +2,7 @@ import { TopicItemWrapper } from './TopicItem.styled';
import { VideoAsset } from './videoAsset/VideoAsset';
import { AudioAsset } from './audioAsset/AudioAsset';
import { ImageAsset } from './imageAsset/ImageAsset';
import { BinaryAsset } from './binaryAsset/BinaryAsset';
import { Logo } from 'logo/Logo';
import { Space, Skeleton, Button, Modal, Input } from 'antd';
import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined, PictureOutlined } from '@ant-design/icons';
@ -61,6 +62,9 @@ export function TopicItem({ host, contentKey, sealed, topic, update, remove }) {
if (asset.type === 'audio') {
return <AudioAsset asset={asset} />
}
if (asset.type === 'binary') {
return <BinaryAsset asset={asset} />
}
return <></>
}

View File

@ -0,0 +1,42 @@
import React, { useState, useRef } from 'react';
import { Progress, Modal, Spin } from 'antd';
import ReactResizeDetector from 'react-resize-detector';
import { DownloadOutlined } from '@ant-design/icons';
import { BinaryAssetWrapper } from './BinaryAsset.styled';
import { useBinaryAsset } from './useBinaryAsset.hook';
import Colors from 'constants/Colors';
import background from 'images/audio.png';
export function BinaryAsset({ asset }) {
const [width, setWidth] = useState(0);
const { actions, state } = useBinaryAsset(asset);
return (
<BinaryAssetWrapper>
<ReactResizeDetector handleWidth={false} handleHeight={true}>
{({ height }) => {
if (height !== width) {
setWidth(height);
}
return <div style={{ height: '100%', width: width }} />
}}
</ReactResizeDetector>
<div class="player" style={{ backgroundColor: '#888888', borderRadius: 4, width: width, height: width }}>
<div class="label">{ asset.label }</div>
<div class="control" onClick={actions.download}>
<DownloadOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
</div>
<div class="unsealing">
{ state.unsealing && (
<Progress percent={Math.floor(100 * state.block / state.total)} size="small" showInfo={false} trailColor={Colors.white} strokeColor={Colors.background} />
)}
</div>
<div class="extension">{ asset.extension }</div>
</div>
</BinaryAssetWrapper>
)
}

View File

@ -0,0 +1,57 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const BinaryAssetWrapper = styled.div`
position: relative;
height: 100%;
.player {
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #888888;
}
.background {
height: 100%;
object-fit: contain;
}
.unsealing {
padding-left: 8px;
padding-right: 8px;
width: 64px;
height: 16px;
}
.label {
width: 100%;
overflow: hidden;
text-align: center;
color: white;
font-size: 14px;
text-overflow: ellipsis;
padding: 4px;
}
.control {
width: 100%;
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
}
.extension {
width: 100%;
overflow: hidden;
text-align: center;
color: white;
font-size: 24px;
}
`;

View File

@ -0,0 +1,61 @@
import { useState, useRef } from 'react';
export function useBinaryAsset(asset) {
const index = useRef(0);
const updated = useRef(false);
const [state, setState] = useState({
error: false,
unsealing: false,
block: 0,
total: 0,
});
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const actions = {
download: async () => {
if (asset.encrypted) {
if (!state.unsealing) {
try {
updateState({ error: false, unsealing: true });
const view = index.current;
updateState({ active: true, ready: false, error: false, loading: true, url: null });
const blob = await asset.getDecryptedBlob(() => view !== index.current, (block, total) => {
if (!updated.current || block == total) {
updated.current = true;
setTimeout(() => {
updated.current = false;
}, 1000);
updateState({ block, total });
}
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = `${asset.label}.${asset.extension.toLowerCase()}`
link.href = url;
link.click();
URL.revokeObjectURL(url);
}
catch (err) {
console.log(err);
updateState({ error: true });
}
updateState({ unsealing: false });
}
}
else {
const link = document.createElement("a");
link.download = `${asset.label}.${asset.extension.toLowerCase()}`
link.href = asset.data;
link.click();
}
},
};
return { state, actions };
}

View File

@ -29,7 +29,7 @@ export function useTopicItem(topic, contentKey) {
topic.assets.forEach(asset => {
if (asset.encrypted) {
const encrypted = true;
const { type, thumb, label, parts } = asset.encrypted;
const { type, thumb, label, extension, parts } = asset.encrypted;
const getDecryptedBlob = async (abort, progress) => {
let pos = 0;
let len = 0;
@ -59,7 +59,7 @@ export function useTopicItem(topic, contentKey) {
}
return new Blob([data]);
}
assets.push({ type, thumb, label, encrypted, getDecryptedBlob });
assets.push({ type, thumb, label, extension, encrypted, getDecryptedBlob });
}
else {
const encrypted = false
@ -82,6 +82,13 @@ export function useTopicItem(topic, contentKey) {
const full = topic.assetUrl(asset.audio.full, topic.id);
assets.push({ type, label, encrypted, full });
}
else if (asset.binary) {
const type = 'binary';
const label = asset.binary.label;
const extension = asset.binary.extension;
const data = topic.assetUrl(asset.binary.data, topic.id);
assets.push({ type, label, extension, encrypted, data });
}
}
});
updateState({ assets });