mirror of
https://github.com/balzack/databag.git
synced 2025-03-13 00:50:03 +00:00
complete e2e asset support in browser app
This commit is contained in:
parent
5f20b22250
commit
d486986ebd
@ -7,6 +7,7 @@ const Colors = {
|
||||
formHover: '#efefef',
|
||||
grey: '#888888',
|
||||
white: '#ffffff',
|
||||
black: '#000000',
|
||||
divider: '#dddddd',
|
||||
mask: '#dddddd',
|
||||
encircle: '#cccccc',
|
||||
|
@ -217,7 +217,7 @@ async function upload(entry, update, complete) {
|
||||
entry.active = {};
|
||||
try {
|
||||
if (file.encrypted) {
|
||||
const { size, getEncryptedBlock, position, image, video, audio } = file;
|
||||
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 thumb = await getThumb(data, type, position);
|
||||
const parts = [];
|
||||
@ -234,12 +234,10 @@ async function upload(entry, update, complete) {
|
||||
update();
|
||||
}
|
||||
});
|
||||
console.log("PART?", part.data);
|
||||
|
||||
parts.push({ blockIv, partId: part.data.assetId });
|
||||
}
|
||||
entry.assets.push({
|
||||
encrypted: { type, thumb, parts }
|
||||
encrypted: { type, thumb, label, parts }
|
||||
});
|
||||
}
|
||||
else if (file.image) {
|
||||
|
@ -1,37 +1,21 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import { Modal, Spin } from 'antd';
|
||||
import ReactResizeDetector from 'react-resize-detector';
|
||||
import { PlayCircleOutlined, MinusCircleOutlined, SoundOutlined } from '@ant-design/icons';
|
||||
import { AudioAssetWrapper } from './AudioAsset.styled';
|
||||
import { AudioAssetWrapper, AudioModalWrapper } from './AudioAsset.styled';
|
||||
import { useAudioAsset } from './useAudioAsset.hook';
|
||||
|
||||
import background from 'images/audio.png';
|
||||
|
||||
export function AudioAsset({ label, audioUrl }) {
|
||||
export function AudioAsset({ asset }) {
|
||||
|
||||
const [active, setActive] = useState(false);
|
||||
const [width, setWidth] = useState(0);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [playing, setPlaying] = useState(true);
|
||||
const [url, setUrl] = useState(null);
|
||||
|
||||
const { actions, state } = useAudioAsset(asset);
|
||||
|
||||
const audio = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(false);
|
||||
setReady(false);
|
||||
setPlaying(true);
|
||||
setUrl(null);
|
||||
}, [label, audioUrl]);
|
||||
|
||||
const onActivate = () => {
|
||||
setUrl(audioUrl);
|
||||
setActive(true);
|
||||
}
|
||||
|
||||
const onReady = () => {
|
||||
setReady(true);
|
||||
}
|
||||
|
||||
const play = (on) => {
|
||||
setPlaying(on);
|
||||
if (on) {
|
||||
@ -54,32 +38,44 @@ export function AudioAsset({ label, audioUrl }) {
|
||||
</ReactResizeDetector>
|
||||
<div class="player" style={{ width: width, height: width }}>
|
||||
<img class="background" src={background} alt="audio background" />
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{ !active && (
|
||||
<div class="control" onClick={() => onActivate()}>
|
||||
<SoundOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)}
|
||||
{ active && !ready && (
|
||||
<div class="control">
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
{ active && ready && playing && (
|
||||
<div class="control" onClick={() => play(false)}>
|
||||
<MinusCircleOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)}
|
||||
{ active && ready && !playing && (
|
||||
<div class="control" onClick={() => play(true)}>
|
||||
<PlayCircleOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)}
|
||||
<audio style={{ position: 'absolute', top: 0, visibility: 'hidden' }} autoplay="true"
|
||||
src={url} type="audio/mpeg" ref={audio} onPlay={onReady} />
|
||||
<div class="control" onClick={actions.setActive}>
|
||||
<SoundOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
<div class="label">{ asset.label }</div>
|
||||
</div>
|
||||
<div class="label">{ label }</div>
|
||||
<Modal centered={true} visible={state.active} width={256 + 12} bodyStyle={{ width: '100%', height: 'auto', paddingBottom: 6, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd' }} footer={null} destroyOnClose={true} closable={false} onCancel={actions.clearActive}>
|
||||
<audio style={{ position: 'absolute', top: 0, visibility: 'hidden' }} autoplay="true"
|
||||
src={state.url} type="audio/mpeg" ref={audio} onPlay={actions.ready} />
|
||||
<AudioModalWrapper>
|
||||
<img class="background" src={background} alt="audio background" />
|
||||
{ state.loading && state.error && (
|
||||
<div class="failed">
|
||||
<Spin size="large" delay={250} />
|
||||
</div>
|
||||
)}
|
||||
{ state.loading && !state.error && (
|
||||
<div class="loading">
|
||||
<Spin size="large" delay={250} />
|
||||
</div>
|
||||
)}
|
||||
{ !state.ready && !state.loading && (
|
||||
<div class="loading">
|
||||
<Spin size="large" delay={250} />
|
||||
</div>
|
||||
)}
|
||||
{ state.ready && !state.loading && playing && (
|
||||
<div class="control" onClick={() => play(false)}>
|
||||
<MinusCircleOutlined style={{ fontSize: 64, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)}
|
||||
{ state.ready && !state.loading && !playing && (
|
||||
<div class="control" onClick={() => play(true)}>
|
||||
<PlayCircleOutlined style={{ fontSize: 64, color: '#eeeeee', cursor: 'pointer' }} />
|
||||
</div>
|
||||
)}
|
||||
<div class="label">{ asset.label }</div>
|
||||
</AudioModalWrapper>
|
||||
</Modal>
|
||||
</AudioAssetWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import Colors from 'constants/Colors';
|
||||
|
||||
export const AudioAssetWrapper = styled.div`
|
||||
position: relative;
|
||||
@ -41,3 +42,48 @@ export const AudioAssetWrapper = styled.div`
|
||||
`;
|
||||
|
||||
|
||||
export const AudioModalWrapper = styled.div`
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #aaaaaa;
|
||||
|
||||
.background {
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
}
|
||||
|
||||
.control {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-top: 8px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.failed {
|
||||
position: absolute;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-spin-dot-item {
|
||||
background-color: ${Colors.alert};
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-spin-dot-item {
|
||||
background-color: ${Colors.white};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
export function useAudioAsset(asset) {
|
||||
|
||||
const revoke = useRef();
|
||||
const index = useRef(0);
|
||||
|
||||
const [state, setState] = useState({
|
||||
active: false,
|
||||
loading: false,
|
||||
error: false,
|
||||
ready: false,
|
||||
url: null,
|
||||
});
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
const actions = {
|
||||
setActive: async () => {
|
||||
if (asset.encrypted) {
|
||||
try {
|
||||
const view = index.current;
|
||||
updateState({ active: true, ready: false, error: false, loading: true, url: null });
|
||||
const blob = await asset.getDecryptedBlob(() => view != index.current);
|
||||
const url = URL.createObjectURL(blob);
|
||||
revoke.current = url;
|
||||
updateState({ loading: false, url });
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
updateState({ error: true });
|
||||
}
|
||||
}
|
||||
else {
|
||||
updateState({ active: true, loading: false, url: asset.full });
|
||||
}
|
||||
},
|
||||
clearActive: () => {
|
||||
index.current += 1;
|
||||
updateState({ active: false, url: null });
|
||||
if (revoke.current) {
|
||||
URL.revokeObjectURL(revoke.current);
|
||||
revoke.current = null;
|
||||
}
|
||||
},
|
||||
ready: () => {
|
||||
updateState({ ready: true });
|
||||
}
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -37,17 +37,22 @@ export function ImageAsset({ asset }) {
|
||||
onClick={popout} />
|
||||
<Modal centered={true} visible={state.popout} width={state.width + 12} bodyStyle={{ width: '100%', height: 'auto', paddingBottom: 6, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd' }} footer={null} destroyOnClose={true} closable={false} onCancel={actions.clearPopout}>
|
||||
<ImageModalWrapper onClick={actions.clearPopout}>
|
||||
{ state.loading && (
|
||||
<div class="frame">
|
||||
<img style={{ width: '100%', objectFit: 'contain' }} src={asset.thumb} alt="topic asset" />
|
||||
<div class="spinner">
|
||||
<Spin color={'white'} size="large" delay={250} />
|
||||
<div class="frame">
|
||||
<img class="thumb" src={asset.thumb} alt="topic asset" />
|
||||
{ !state.error && (
|
||||
<div class="loading">
|
||||
<Spin size="large" delay={250} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{ !state.loading && (
|
||||
<img style={{ width: '100%', objectFit: 'contain' }} src={state.url} alt="topic asset" />
|
||||
)}
|
||||
)}
|
||||
{ state.error && (
|
||||
<div class="failed">
|
||||
<Spin size="large" delay={250} />
|
||||
</div>
|
||||
)}
|
||||
{ !state.loading && (
|
||||
<img class="full" src={state.url} alt="topic asset" />
|
||||
)}
|
||||
</div>
|
||||
</ImageModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import Colors from 'constants/Colors';
|
||||
|
||||
export const ImageAssetWrapper = styled.div`
|
||||
position: relative;
|
||||
@ -45,16 +46,39 @@ export const ImageModalWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: ${Colors.black};
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
opacity: 0.5;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item {
|
||||
background-color: white;
|
||||
.full {
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
.failed {
|
||||
position: absolute;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-spin-dot-item {
|
||||
background-color: ${Colors.alert};
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-spin-dot-item {
|
||||
background-color: ${Colors.white};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
export function useImageAsset(asset) {
|
||||
|
||||
const revoke = useRef();
|
||||
const index = useRef(0);
|
||||
|
||||
const [state, setState] = useState({
|
||||
popout: false,
|
||||
width: 0,
|
||||
@ -18,17 +21,30 @@ export function useImageAsset(asset) {
|
||||
const actions = {
|
||||
setPopout: async (width, height) => {
|
||||
if (asset.encrypted) {
|
||||
updateState({ popout: true, width, height, loading: true, url: null });
|
||||
const blob = await asset.getDecryptedBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
updateState({ loading: false, url });
|
||||
try {
|
||||
const view = index.current;
|
||||
updateState({ popout: true, width, height, error: false, loading: true, url: null });
|
||||
const blob = await asset.getDecryptedBlob(() => view != index.current);
|
||||
const url = URL.createObjectURL(blob);
|
||||
updateState({ loading: false, url });
|
||||
revoke.current = url;
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err);
|
||||
updateState({ error: true });
|
||||
}
|
||||
}
|
||||
else {
|
||||
updateState({ popout: true, width, height, loading: false, url: asset.full });
|
||||
}
|
||||
},
|
||||
clearPopout: () => {
|
||||
index.current += 1;
|
||||
updateState({ popout: false });
|
||||
if (revoke.current) {
|
||||
URL.revokeObjectURL(revoke.current);
|
||||
revoke.current = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -10,9 +10,6 @@ export function useTopicItem(topic, contentKey) {
|
||||
assets: [],
|
||||
});
|
||||
|
||||
console.log(topic);
|
||||
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
@ -32,13 +29,16 @@ export function useTopicItem(topic, contentKey) {
|
||||
topic.assets.forEach(asset => {
|
||||
if (asset.encrypted) {
|
||||
const encrypted = true;
|
||||
const { type, thumb, parts } = asset.encrypted;
|
||||
const getDecryptedBlob = async () => {
|
||||
const { type, thumb, label, parts } = asset.encrypted;
|
||||
const getDecryptedBlob = async (abort) => {
|
||||
let pos = 0;
|
||||
let len = 0;
|
||||
|
||||
const slices = []
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (abort()) {
|
||||
throw new Error("asset unseal aborted");
|
||||
}
|
||||
const part = parts[i];
|
||||
const url = topic.assetUrl(part.partId, topic.id);
|
||||
const response = await fetchWithTimeout(url, { method: 'GET' });
|
||||
@ -57,7 +57,7 @@ export function useTopicItem(topic, contentKey) {
|
||||
}
|
||||
return new Blob([data]);
|
||||
}
|
||||
assets.push({ type, thumb, encrypted, getDecryptedBlob });
|
||||
assets.push({ type, thumb, label, encrypted, getDecryptedBlob });
|
||||
}
|
||||
else {
|
||||
const encrypted = false
|
||||
|
@ -40,10 +40,19 @@ export function VideoAsset({ asset }) {
|
||||
<Modal centered={true} style={{ backgroundColor: '#aacc00', padding: 0 }} visible={state.active} width={state.width + 12} bodyStyle={{ paddingBottom: 0, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd', margin: 0 }} footer={null} destroyOnClose={true} closable={false} onCancel={actions.clearActive}>
|
||||
<VideoModalWrapper>
|
||||
{ state.loading && (
|
||||
<div class="frame">
|
||||
<img style={{ width: '100%', objectFit: 'contain' }} src={asset.thumb} alt="topic asset" />
|
||||
<div class="spinner">
|
||||
<Spin color={'white'} size="large" delay={250} />
|
||||
<div class="wrapper">
|
||||
<div class="frame">
|
||||
<img class="thumb" src={asset.thumb} alt="topic asset" />
|
||||
{ state.error && (
|
||||
<div class="failed">
|
||||
<Spin size="large" delay={250} />
|
||||
</div>
|
||||
)}
|
||||
{ !state.error && (
|
||||
<div class="loading">
|
||||
<Spin size="large" delay={250} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import Colors from 'constants/Colors';
|
||||
|
||||
export const VideoAssetWrapper = styled.div`
|
||||
position: relative;
|
||||
@ -15,20 +16,41 @@ export const VideoAssetWrapper = styled.div`
|
||||
`;
|
||||
|
||||
export const VideoModalWrapper = styled.div`
|
||||
|
||||
.wrapper {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.frame {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item {
|
||||
background-color: white;
|
||||
.thumb {
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
.failed {
|
||||
position: absolute;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-spin-dot-item {
|
||||
background-color: ${Colors.alert};
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-spin-dot-item {
|
||||
background-color: ${Colors.white};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
export function useVideoAsset(asset) {
|
||||
|
||||
const revoke = useRef();
|
||||
const index = useRef(0);
|
||||
|
||||
const [state, setState] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
@ -19,17 +22,30 @@ export function useVideoAsset(asset) {
|
||||
const actions = {
|
||||
setActive: async (width, height) => {
|
||||
if (asset.encrypted) {
|
||||
updateState({ active: true, width, height, loading: true, url: null });
|
||||
const blob = await asset.getDecryptedBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
updateState({ loading: false, url });
|
||||
try {
|
||||
const view = index.current;
|
||||
updateState({ active: true, width, height, error: false, loading: true, url: null });
|
||||
const blob = await asset.getDecryptedBlob(() => view != index.current);
|
||||
const url = URL.createObjectURL(blob);
|
||||
revoke.current = url;
|
||||
updateState({ loading: false, url });
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
updateState({ error: true });
|
||||
}
|
||||
}
|
||||
else {
|
||||
updateState({ popout: true, width, height, loading: false, url: asset.hd });
|
||||
updateState({ active: true, width, height, loading: false, url: asset.hd });
|
||||
}
|
||||
},
|
||||
clearActive: () => {
|
||||
index.current += 1;
|
||||
updateState({ active: false });
|
||||
if (revoke.current) {
|
||||
URL.revokeObjectURL(revoke.current);
|
||||
revoke.current = null;
|
||||
}
|
||||
},
|
||||
setDimension: (dimension) => {
|
||||
updateState({ dimension });
|
||||
|
Loading…
Reference in New Issue
Block a user