mirror of
https://github.com/balzack/databag.git
synced 2025-02-12 03:29:16 +00:00
adding generic file attachement to browser app
This commit is contained in:
parent
f45750627e
commit
221b36895c
@ -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{}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -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 });
|
||||
},
|
||||
|
@ -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 <></>
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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 });
|
||||
|
Loading…
Reference in New Issue
Block a user