Merge branch 'main' into fdroid
@ -2,7 +2,7 @@
|
||||
[](https://contribute.design/balzack/databag)
|
||||
|
||||
<div align="center">
|
||||
<a href="#"><img src="/doc/icon.png" width="8%" style="border-radius:50%"></a>
|
||||
<a href="#"><img src="/doc/icon_v2.png" width="8%" style="border-radius:50%"></a>
|
||||
<h3 align="center">Databag</h3>
|
||||
<p align="center">Communication for the Decentralized Web</p>
|
||||
</div>
|
||||
@ -89,6 +89,8 @@ Instructions for installing without a container on a Raspberry Pi Zero are [here
|
||||
|
||||
Instructions for installing without a container in AWS are [here](/doc/aws.md).
|
||||
|
||||
1-click installs in [CapRover](https://caprover.com/), [CasaOS](https://casaos.io), [Unraid](https://unraid.net/), [Runtipi](https://www.runtipi.io/)
|
||||
|
||||
## Audio and Video Calls
|
||||
|
||||
Databag provides audio and video calling and relies on a STUN/TURN relay server for NAT traversal. Testing was done with both [cuturn](https://github.com/coturn/coturn) and [pion](https://github.com/pion/turn) and should work with any implementation. Instructions for installing a coturn server are provided [here](https://gabrieltanner.org/blog/turn-server/).
|
||||
|
@ -622,7 +622,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.17;
|
||||
MARKETING_VERSION = 1.19;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@ -656,7 +656,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.17;
|
||||
MARKETING_VERSION = 1.19;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
Before Width: | Height: | Size: 239 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.7 KiB |
@ -1,103 +1,103 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"filename" : "_40 2.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"filename" : "_60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"filename" : "_58 1.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"filename" : "_87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"filename" : "_80 1.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"filename" : "_120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120 1.png",
|
||||
"filename" : "_120 1.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"filename" : "_180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "40 1.png",
|
||||
"filename" : "_40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29 1.png",
|
||||
"filename" : "_29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58 1.png",
|
||||
"filename" : "_58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "40 2.png",
|
||||
"filename" : "_40 1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "80 1.png",
|
||||
"filename" : "_80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "76 1.png",
|
||||
"filename" : "_76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "152 1.png",
|
||||
"filename" : "_152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "167 1.png",
|
||||
"filename" : "_167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"filename" : "_1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
|
After Width: | Height: | Size: 214 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 18 KiB |
@ -163,7 +163,7 @@ async function upload(entry, update, complete) {
|
||||
entry.active = {};
|
||||
try {
|
||||
if (file.encrypted) {
|
||||
const { data, type, mime, size, getEncryptedBlock, position } = file;
|
||||
const { data, type, mime, size, getEncryptedBlock, position, label, extension } = file;
|
||||
const thumb = await getThumb(data, type, mime, size, position);
|
||||
const parts = [];
|
||||
for (let pos = 0; pos < size; pos += ENCRYPTED_BLOCK_SIZE) {
|
||||
@ -182,17 +182,13 @@ async function upload(entry, update, complete) {
|
||||
parts.push({ blockIv, partId: part.data.assetId });
|
||||
}
|
||||
entry.assets.push({
|
||||
encrypted: { type, thumb, parts }
|
||||
encrypted: { type, label, extension, thumb, parts }
|
||||
});
|
||||
}
|
||||
else if (file.type === 'image') {
|
||||
const formData = new FormData();
|
||||
if (file.data.startsWith('file:')) {
|
||||
formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'});
|
||||
}
|
||||
else {
|
||||
formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'});
|
||||
}
|
||||
const uri = file.data.startsWith('file:') ? file.data : `file://${file.data}`;
|
||||
formData.append("asset", {uri: uri, name: 'asset', type: 'application/octent-stream'});
|
||||
let transform = encodeURIComponent(JSON.stringify(["ithumb;photo", "ilg;photo"]));
|
||||
let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
@ -212,12 +208,8 @@ async function upload(entry, update, complete) {
|
||||
}
|
||||
else if (file.type === 'video') {
|
||||
const formData = new FormData();
|
||||
if (file.data.startsWith('file:')) {
|
||||
formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'});
|
||||
}
|
||||
else {
|
||||
formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'});
|
||||
}
|
||||
const uri = file.data.startsWith('file:') ? file.data : `file://${file.data}`;
|
||||
formData.append("asset", {uri: uri, name: 'asset', type: 'application/octent-stream'});
|
||||
let thumb = 'vthumb;video;' + file.position;
|
||||
let transform = encodeURIComponent(JSON.stringify(["vlq;video", "vhd;video", thumb]));
|
||||
let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, {
|
||||
@ -239,12 +231,8 @@ async function upload(entry, update, complete) {
|
||||
}
|
||||
else if (file.type === 'audio') {
|
||||
const formData = new FormData();
|
||||
if (file.data.startsWith('file:')) {
|
||||
formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'});
|
||||
}
|
||||
else {
|
||||
formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'});
|
||||
}
|
||||
const uri = file.data.startsWith('file:') ? file.data : `file://${file.data}`;
|
||||
formData.append("asset", {uri: uri, name: 'asset', type: 'application/octent-stream'});
|
||||
let transform = encodeURIComponent(JSON.stringify(["acopy;audio"]));
|
||||
let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
@ -262,6 +250,27 @@ async function upload(entry, update, complete) {
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (file.type === 'binary') {
|
||||
const formData = new FormData();
|
||||
const uri = file.data.startsWith('file:') ? file.data : `file://${file.data}`;
|
||||
formData.append("asset", {uri: uri, name: 'asset', type: 'application/octent-stream'});
|
||||
let asset = await axios.post(`${entry.baseUrl}blocks${entry.urlParams}&body=multipart`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
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);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import ColorPicker from 'react-native-wheel-color-picker'
|
||||
import { VideoFile } from './videoFile/VideoFile';
|
||||
import { AudioFile } from './audioFile/AudioFile';
|
||||
import { ImageFile } from './imageFile/ImageFile';
|
||||
import { BinaryFile } from './binaryFile/BinaryFile';
|
||||
|
||||
export function AddTopic({ contentKey, shareIntent, setShareIntent }) {
|
||||
|
||||
@ -80,15 +81,28 @@ export function AddTopic({ contentKey, shareIntent, setShareIntent }) {
|
||||
|
||||
const addAudio = async () => {
|
||||
try {
|
||||
const audio = await DocumentPicker.pickSingle({
|
||||
presentationStyle: 'fullScreen',
|
||||
copyTo: 'cachesDirectory',
|
||||
type: DocumentPicker.types.audio,
|
||||
})
|
||||
actions.addAudio(audio.fileCopyUri, audio.name.replace(/\.[^/.]+$/, ""));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
const audio = await DocumentPicker.pickSingle({
|
||||
presentationStyle: 'fullScreen',
|
||||
copyTo: 'cachesDirectory',
|
||||
type: DocumentPicker.types.audio,
|
||||
})
|
||||
actions.addAudio(audio.fileCopyUri, audio.name.replace(/\.[^/.]+$/, ""));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
const addBinary = async () => {
|
||||
try {
|
||||
const binary = await DocumentPicker.pickSingle({
|
||||
presentationStyle: 'fullScreen',
|
||||
copyTo: 'cachesDirectory',
|
||||
type: DocumentPicker.types.allFiles,
|
||||
})
|
||||
actions.addBinary(binary.fileCopyUri, binary.name);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
const remove = (item) => {
|
||||
@ -123,7 +137,13 @@ export function AddTopic({ contentKey, shareIntent, setShareIntent }) {
|
||||
if (item.type === 'audio') {
|
||||
return (
|
||||
<AudioFile path={item.data} label={item.label} remove={() => remove(item)}
|
||||
setLabel={(label) => actions.setAudioLabel(item.key, label)} />
|
||||
setLabel={(label) => actions.setLabel(item.key, label)} />
|
||||
)
|
||||
}
|
||||
if (item.type === 'binary') {
|
||||
return (
|
||||
<BinaryFile path={item.data} label={item.label} extension={item.extension} remove={() => remove(item)}
|
||||
setLabel={(label) => actions.setLabel(item.key, label)} />
|
||||
)
|
||||
}
|
||||
else {
|
||||
@ -171,9 +191,10 @@ export function AddTopic({ contentKey, shareIntent, setShareIntent }) {
|
||||
<MatIcons name="music-box-outline" size={20} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ (state.enableImage || state.enableVideo || state.enableAudio) && (
|
||||
<View style={styles.divider} />
|
||||
)}
|
||||
<TouchableOpacity style={styles.addButton} onPress={addBinary}>
|
||||
<MatIcons name="all-inclusive-box-outline" size={20} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.divider} />
|
||||
<TouchableOpacity style={styles.addButton} onPress={actions.showFontSize}>
|
||||
<MatIcons name="format-size" size={20} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { Image, Text, View, TextInput, TouchableOpacity } from 'react-native';
|
||||
import { styles } from './BinaryFile.styled';
|
||||
|
||||
export function BinaryFile({ path, remove, extension, label, setLabel }) {
|
||||
return (
|
||||
<TouchableOpacity style={styles.binary} onLongPress={remove}>
|
||||
<TextInput style={ styles.input } value={ label } onChangeText={setLabel}
|
||||
multiline={true} autoCapitalize={'none'} placeholder="Binary Label" />
|
||||
<View style={styles.extension}>
|
||||
<Text style={styles.label}>{ extension }</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from 'constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
binary: {
|
||||
width: 92,
|
||||
height: 92,
|
||||
backgroundColor: '#888888',
|
||||
borderRadius: 4,
|
||||
marginRight: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
maxHeight: 50,
|
||||
textAlign: 'center',
|
||||
padding: 4,
|
||||
color: 'white',
|
||||
},
|
||||
extension: {
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
label: {
|
||||
color: 'white',
|
||||
fontSize: 24,
|
||||
},
|
||||
})
|
@ -162,7 +162,19 @@ export function useAddTopic(contentKey) {
|
||||
asset.type = 'audio';
|
||||
asset.label = label;
|
||||
updateState({ assets: [ ...state.assets, asset ] });
|
||||
},
|
||||
},
|
||||
addBinary: async (data, name) => {
|
||||
assetId.current++;
|
||||
const asset = await setAsset(data);
|
||||
asset.key = assetId.current;
|
||||
asset.type = 'binary';
|
||||
asset.extension = name.split('.').pop().toUpperCase();
|
||||
asset.label = name.slice(0, -1 * (asset.extension.length + 1));
|
||||
|
||||
console.log(asset);
|
||||
|
||||
updateState({ assets: [ ...state.assets, asset ] });
|
||||
},
|
||||
setVideoPosition: (key, position) => {
|
||||
updateState({ assets: state.assets.map((item) => {
|
||||
if(item.key === key) {
|
||||
@ -172,7 +184,7 @@ export function useAddTopic(contentKey) {
|
||||
})
|
||||
});
|
||||
},
|
||||
setAudioLabel: (key, label) => {
|
||||
setLabel: (key, label) => {
|
||||
updateState({ assets: state.assets.map((item) => {
|
||||
if(item.key === key) {
|
||||
return { ...item, label };
|
||||
|
@ -9,9 +9,11 @@ import avatar from 'images/avatar.png';
|
||||
import { VideoThumb } from './videoThumb/VideoThumb';
|
||||
import { AudioThumb } from './audioThumb/AudioThumb';
|
||||
import { ImageThumb } from './imageThumb/ImageThumb';
|
||||
import { BinaryThumb } from './binaryThumb/BinaryThumb';
|
||||
import { ImageAsset } from './imageAsset/ImageAsset';
|
||||
import { AudioAsset } from './audioAsset/AudioAsset';
|
||||
import { VideoAsset } from './videoAsset/VideoAsset';
|
||||
import { BinaryAsset } from './binaryAsset/BinaryAsset';
|
||||
import Carousel from 'react-native-reanimated-carousel';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { SafeAreaView, SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
@ -120,7 +122,10 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block
|
||||
<VideoThumb url={thumb.item.thumb} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||
)}
|
||||
{ thumb.item.type === 'audio' && (
|
||||
<AudioThumb labe={thumb.item.label} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||
<AudioThumb label={thumb.item.label} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||
)}
|
||||
{ thumb.item.type === 'binary' && (
|
||||
<BinaryThumb label={thumb.item.label} extension={thumb.item.extension} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
@ -234,6 +239,9 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block
|
||||
{ state.assets[index].type === 'audio' && (
|
||||
<AudioAsset asset={state.assets[index]} dismiss={actions.hideCarousel} />
|
||||
)}
|
||||
{ state.assets[index].type === 'binary' && (
|
||||
<BinaryAsset asset={state.assets[index]} dismiss={actions.hideCarousel} />
|
||||
)}
|
||||
</View>
|
||||
)} />
|
||||
</GestureHandlerRootView>
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { ActivityIndicator, Alert, View, Text, TouchableOpacity } from 'react-native';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import Colors from 'constants/Colors';
|
||||
import { useBinaryAsset } from './useBinaryAsset.hook';
|
||||
import { styles } from './BinaryAsset.styled';
|
||||
import MatIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import AntIcons from 'react-native-vector-icons/AntDesign';
|
||||
|
||||
export function BinaryAsset({ asset, dismiss }) {
|
||||
|
||||
const { state, actions } = useBinaryAsset();
|
||||
|
||||
|
||||
const download = async () => {
|
||||
try {
|
||||
const ext = asset.extension.toLowerCase();
|
||||
const url = asset.encrypted ? asset.decrypted : asset.data;
|
||||
await actions.download(asset.label, ext, asset.encrypted, url);
|
||||
}
|
||||
catch (err) {
|
||||
Alert.alert(
|
||||
'Download Failed',
|
||||
'Please try again.'
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.container, width: state.width, height: state.height }}>
|
||||
<Text style={styles.label} ellipsizeMode='tail' numberOfLines={1}>{ asset.label }</Text>
|
||||
<TouchableOpacity style={styles.close} onPress={dismiss}>
|
||||
<MatIcons name="window-close" size={32} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.action}>
|
||||
{ asset.encrypted && !asset.decrypted && (
|
||||
<TouchableOpacity style={styles.loading} onPress={dismiss}>
|
||||
<ActivityIndicator color={Colors.white} size="large" />
|
||||
{ asset.total > 1 && (
|
||||
<Text style={styles.decrypting}>{ asset.block } / { asset.total }</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ !state.downloading && (!asset.encrypted || asset.decrypted) && (
|
||||
<TouchableOpacity onPress={download}>
|
||||
<AntIcons name="download" size={64} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ state.downloading && (
|
||||
<ActivityIndicator color={Colors.white} size="large" />
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.extension}>{ asset.extension }</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,61 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from 'constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'relative',
|
||||
borderRadius: 8,
|
||||
backgroundColor: Colors.grey,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
label: {
|
||||
textAlign: 'center',
|
||||
fontSize: 20,
|
||||
paddingTop: 8,
|
||||
paddingLeft: 48,
|
||||
paddingRight: 48,
|
||||
color: Colors.white,
|
||||
},
|
||||
action: {
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
copied: {
|
||||
height: 32,
|
||||
paddingTop: 8,
|
||||
},
|
||||
copiedText: {
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
},
|
||||
extension: {
|
||||
textAlign: 'center',
|
||||
fontSize: 48,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 48,
|
||||
paddingRight: 48,
|
||||
color: Colors.white,
|
||||
},
|
||||
close: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
},
|
||||
decrypting: {
|
||||
fontVariant: ["tabular-nums"],
|
||||
paddingTop: 16,
|
||||
fontSize: 12,
|
||||
color: '#888888',
|
||||
},
|
||||
})
|
||||
|
@ -0,0 +1,75 @@
|
||||
import { useState, useRef, useEffect, useContext } from 'react';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
import { Image } from 'react-native';
|
||||
import { useWindowDimensions, Platform } from 'react-native';
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import Share from 'react-native-share';
|
||||
import RNFS from 'react-native-fs';
|
||||
|
||||
export function useBinaryAsset() {
|
||||
|
||||
const [state, setState] = useState({
|
||||
width: 1,
|
||||
height: 1,
|
||||
downloading: false,
|
||||
});
|
||||
|
||||
const dimensions = useWindowDimensions();
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { width, height } = dimensions;
|
||||
if (width < height) {
|
||||
updateState({ width, height: width });
|
||||
}
|
||||
else {
|
||||
updateState({ widht: height, height });
|
||||
}
|
||||
}, [dimensions]);
|
||||
|
||||
const actions = {
|
||||
download: async (label, extension, cached, url) => {
|
||||
if (!state.downloading) {
|
||||
try {
|
||||
updateState({ downloading: true });
|
||||
|
||||
let src;
|
||||
if (cached) {
|
||||
src = url
|
||||
}
|
||||
else {
|
||||
const blob = await RNFetchBlob.config({ fileCache: true }).fetch("GET", url);
|
||||
src = blob.path();
|
||||
}
|
||||
|
||||
const path = `${RNFetchBlob.fs.dirs.DocumentDir}`
|
||||
const dst = `${path}/${label}.${extension.toLowerCase()}`
|
||||
if (RNFetchBlob.fs.exists(dst)) {
|
||||
RNFetchBlob.fs.unlink(dst);
|
||||
}
|
||||
await RNFetchBlob.fs.mv(src, dst);
|
||||
try {
|
||||
await Share.open({ url: `file://${dst}` })
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
RNFetchBlob.fs.unlink(dst);
|
||||
|
||||
updateState({ downloading: false });
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
updateState({ downloading: false });
|
||||
throw new Error('download failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { View, Text, Image } from 'react-native';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import { styles } from './BinaryThumb.styled';
|
||||
import Colors from 'constants/Colors';
|
||||
import AntIcons from 'react-native-vector-icons/AntDesign';
|
||||
|
||||
export function BinaryThumb({ label, extension, onAssetView }) {
|
||||
|
||||
return (
|
||||
<TouchableOpacity activeOpacity={1} style={styles.canvas} onPress={onAssetView}>
|
||||
<Text style={styles.label} ellipsizeMode='tail' numberOfLines={1}>{ label }</Text>
|
||||
<View style={styles.action}>
|
||||
<AntIcons name="download" size={28} color={Colors.white} />
|
||||
</View>
|
||||
<Text style={styles.extension}>{ extension }</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from 'constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
canvas: {
|
||||
borderRadius: 4,
|
||||
width: 92,
|
||||
height: 92,
|
||||
marginRight: 16,
|
||||
backgroundColor: Colors.grey,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
action: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
label: {
|
||||
textAlign: 'center',
|
||||
color: Colors.white,
|
||||
padding: 4,
|
||||
fontSize: 14,
|
||||
},
|
||||
extension: {
|
||||
textAlign: 'center',
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
}
|
||||
})
|
||||
|
@ -61,8 +61,8 @@ export function useTopicItem(item, hosting, remove, contentKey) {
|
||||
const asset = parsed[i];
|
||||
if (asset.encrypted) {
|
||||
const encrypted = true;
|
||||
const { type, thumb, label, parts } = asset.encrypted;
|
||||
assets.push({ type, thumb, label, encrypted, decrypted: null, parts });
|
||||
const { type, thumb, label, extension, parts } = asset.encrypted;
|
||||
assets.push({ type, thumb, label, extension, encrypted, decrypted: null, parts });
|
||||
}
|
||||
else {
|
||||
const encrypted = false
|
||||
@ -85,6 +85,12 @@ export function useTopicItem(item, hosting, remove, contentKey) {
|
||||
const full = conversation.actions.getTopicAssetUrl(item.topicId, asset.audio.full);
|
||||
assets.push({ type, label, encrypted, full });
|
||||
}
|
||||
else if (asset.binary) {
|
||||
const type = 'binary';
|
||||
const { label, extension } = asset.binary;
|
||||
const data = conversation.actions.getTopicAssetUrl(item.topicId, asset.binary.data);
|
||||
assets.push({ type, label, extension, data });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
BIN
doc/icon_v2.png
Normal file
After Width: | Height: | Size: 88 KiB |
@ -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 {
|
||||
@ -57,14 +58,35 @@ func AddChannelTopicBlock(w http.ResponseWriter, r *http.Request) {
|
||||
garbageSync.Lock()
|
||||
defer garbageSync.Unlock()
|
||||
|
||||
// save new file
|
||||
id := uuid.New().String()
|
||||
path := getStrConfigValue(CNFAssetPath, APPDefaultPath) + "/" + channelSlot.Account.GUID + "/" + id
|
||||
crc, size, err := saveAsset(r.Body, path)
|
||||
if err != nil {
|
||||
ErrResponse(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
// save new file
|
||||
var crc uint32
|
||||
var size int64
|
||||
id := uuid.New().String()
|
||||
path := getStrConfigValue(CNFAssetPath, APPDefaultPath) + "/" + channelSlot.Account.GUID + "/" + id
|
||||
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{}
|
||||
asset.AssetID = id
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { AlertIcon, DashboardWrapper, SettingsButton, AddButton, SettingsLayout, CreateLayout } from './Dashboard.styled';
|
||||
import { Tooltip, Switch, Select, Button, Space, Modal, Input, InputNumber, List } from 'antd';
|
||||
import { ExclamationCircleOutlined, SettingOutlined, CopyOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { ExclamationCircleOutlined, SettingOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { useDashboard } from './useDashboard.hook';
|
||||
import { AccountItem } from './accountItem/AccountItem';
|
||||
import { CopyButton } from './copyButton/CopyButton';
|
||||
|
||||
export function Dashboard() {
|
||||
|
||||
const { state, actions } = useDashboard();
|
||||
|
||||
const onClipboard = (value) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
const onClipboard = async (value) => {
|
||||
await navigator.clipboard.writeText(value);
|
||||
};
|
||||
|
||||
const createLink = () => {
|
||||
@ -193,14 +194,12 @@ export function Dashboard() {
|
||||
<div className="url">
|
||||
<div className="label">Browser Link:</div>
|
||||
<div className="link">{createLink()}</div>
|
||||
<Button icon={<CopyOutlined />} size="small"
|
||||
onClick={() => onClipboard(createLink())} />
|
||||
<CopyButton onCopy={async () => await onClipboard(createLink())} />
|
||||
</div>
|
||||
<div className="url">
|
||||
<div className="label">App Token:</div>
|
||||
<div className="token">{state.createToken}</div>
|
||||
<Button icon={<CopyOutlined />} size="small"
|
||||
onClick={() => onClipboard(state.createToken)} />
|
||||
<CopyButton onCopy={async () => await onClipboard(state.createToken)} />
|
||||
</div>
|
||||
</CreateLayout>
|
||||
</Modal>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Logo } from 'logo/Logo';
|
||||
import { AccountItemWrapper, AccessLayout, DeleteButton, EnableButton, DisableButton, ResetButton } from './AccountItem.styled';
|
||||
import { useAccountItem } from './useAccountItem.hook';
|
||||
import { ExclamationCircleOutlined, CopyOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { ExclamationCircleOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { Modal, Tooltip, Button } from 'antd';
|
||||
import { CopyButton } from '../copyButton/CopyButton';
|
||||
|
||||
export function AccountItem({ item, remove }) {
|
||||
|
||||
@ -132,14 +133,12 @@ export function AccountItem({ item, remove }) {
|
||||
<div className="url">
|
||||
<div className="label">Browser Link:</div>
|
||||
<div className="link">{accessLink()}</div>
|
||||
<Button icon={<CopyOutlined />} size="small"
|
||||
onClick={() => onClipboard(accessLink())}/>
|
||||
<CopyButton onCopy={async () => await onClipboard(accessLink())} />
|
||||
</div>
|
||||
<div className="url">
|
||||
<div className="label">App Token:</div>
|
||||
<div className="token">{state.accessToken}</div>
|
||||
<Button icon={<CopyOutlined />} size="small"
|
||||
onClick={() => onClipboard(state.accessToken)} />
|
||||
<CopyButton onCopy={async () => await onClipboard(state.accessToken)} />
|
||||
</div>
|
||||
</AccessLayout>
|
||||
</Modal>
|
||||
|
15
net/web/src/dashboard/copyButton/CopyButton.jsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { useCopyButton } from './useCopyButton.hook';
|
||||
import { Tooltip, Button } from 'antd';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
|
||||
export function CopyButton({ onCopy }) {
|
||||
|
||||
const { state, actions } = useCopyButton();
|
||||
|
||||
return (
|
||||
<Tooltip color={state.color} title={state.message} trigger={[]} open={state.show} placement="topRight">
|
||||
<Button icon={<CopyOutlined />} size="small" onClick={() => actions.copy(onCopy)} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
38
net/web/src/dashboard/copyButton/useCopyButton.hook.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import Colors from 'constants/Colors';
|
||||
|
||||
export function useCopyButton() {
|
||||
|
||||
const [state, setState] = useState({
|
||||
color: Colors.background,
|
||||
message: 'copeid',
|
||||
show: false,
|
||||
});
|
||||
|
||||
const timeout = useRef();
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
const actions = {
|
||||
copy: async (onCopy) => {
|
||||
try {
|
||||
await onCopy();
|
||||
updateState({ show: true, message: 'copied', color: Colors.background });
|
||||
}
|
||||
catch {
|
||||
updateState({ show: true, message: 'failed to copy', color: Colors.alert });
|
||||
}
|
||||
|
||||
clearTimeout(timeout.current);
|
||||
timeout.current = setTimeout(() => {
|
||||
updateState({ show: false });
|
||||
}, 1500);
|
||||
updateState({ show: true });
|
||||
}
|
||||
}
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -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="bar space" />
|
||||
)}
|
||||
<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,40 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Progress } 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';
|
||||
|
||||
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,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
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 });
|
||||
|