mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
audio editor
This commit is contained in:
parent
a9d4be064e
commit
645ea8e29e
15 changed files with 273 additions and 9 deletions
|
@ -3,8 +3,10 @@ import { INode } from '~/redux/types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { ImageGrid } from '../ImageGrid';
|
import { ImageGrid } from '../ImageGrid';
|
||||||
|
import { AudioGrid } from '../AudioGrid';
|
||||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||||
import { selectUploads } from '~/redux/uploads/selectors';
|
import { selectUploads } from '~/redux/uploads/selectors';
|
||||||
|
import * as styles from './styles.scss';
|
||||||
|
|
||||||
const mapStateToProps = selectUploads;
|
const mapStateToProps = selectUploads;
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
@ -25,23 +27,45 @@ const AudioEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) =
|
||||||
[data.files]
|
[data.files]
|
||||||
);
|
);
|
||||||
|
|
||||||
const pending_images = useMemo(() => temp.filter(id => !!statuses[id]).map(id => statuses[id]), [
|
const pending_images = useMemo(
|
||||||
temp,
|
() =>
|
||||||
statuses,
|
temp
|
||||||
]);
|
.filter(id => !!statuses[id] && statuses[id].type === UPLOAD_TYPES.IMAGE)
|
||||||
|
.map(id => statuses[id]),
|
||||||
|
[temp, statuses]
|
||||||
|
);
|
||||||
|
|
||||||
const audios = useMemo(
|
const audios = useMemo(
|
||||||
() => data.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO),
|
() => data.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO),
|
||||||
[data.files]
|
[data.files]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pending_audios = useMemo(
|
||||||
|
() =>
|
||||||
|
temp
|
||||||
|
.filter(id => !!statuses[id] && statuses[id].type === UPLOAD_TYPES.AUDIO)
|
||||||
|
.map(id => statuses[id]),
|
||||||
|
[temp, statuses]
|
||||||
|
);
|
||||||
|
|
||||||
const setImages = useCallback(files => setData({ ...data, files: [...files, ...audios] }), [
|
const setImages = useCallback(files => setData({ ...data, files: [...files, ...audios] }), [
|
||||||
setData,
|
setData,
|
||||||
data,
|
data,
|
||||||
audios,
|
audios,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <ImageGrid files={images} setFiles={setImages} locked={pending_images} />;
|
const setAudios = useCallback(files => setData({ ...data, files: [...files, ...images] }), [
|
||||||
|
setData,
|
||||||
|
data,
|
||||||
|
images,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
<ImageGrid files={images} setFiles={setImages} locked={pending_images} />
|
||||||
|
<AudioGrid files={audios} setFiles={setAudios} locked={pending_audios} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AudioEditor = connect(
|
const AudioEditor = connect(
|
||||||
|
|
4
src/components/editors/AudioEditor/styles.scss
Normal file
4
src/components/editors/AudioEditor/styles.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.wrap {
|
||||||
|
padding-bottom: 64px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
43
src/components/editors/AudioGrid/index.tsx
Normal file
43
src/components/editors/AudioGrid/index.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import { SortEnd } from 'react-sortable-hoc';
|
||||||
|
import * as styles from './styles.scss';
|
||||||
|
import { IFile } from '~/redux/types';
|
||||||
|
import { IUploadStatus } from '~/redux/uploads/reducer';
|
||||||
|
import { moveArrItem } from '~/utils/fn';
|
||||||
|
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
files: IFile[];
|
||||||
|
setFiles: (val: IFile[]) => void;
|
||||||
|
locked: IUploadStatus[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
|
const onMove = useCallback(
|
||||||
|
({ oldIndex, newIndex }: SortEnd) => {
|
||||||
|
setFiles(moveArrItem(oldIndex, newIndex, files.filter(file => !!file)) as IFile[]);
|
||||||
|
},
|
||||||
|
[setFiles, files]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDrop = useCallback(
|
||||||
|
(remove_id: IFile['id']) => {
|
||||||
|
setFiles(files.filter(file => file && file.id !== remove_id));
|
||||||
|
},
|
||||||
|
[setFiles, files]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableAudioGrid
|
||||||
|
onDrop={onDrop}
|
||||||
|
onSortEnd={onMove}
|
||||||
|
axis="xy"
|
||||||
|
items={files}
|
||||||
|
locked={locked}
|
||||||
|
pressDelay={window.innerWidth < 768 ? 200 : 0}
|
||||||
|
helperClass={styles.helper}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { AudioGrid };
|
4
src/components/editors/AudioGrid/styles.scss
Normal file
4
src/components/editors/AudioGrid/styles.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.helper {
|
||||||
|
opacity: 0.5;
|
||||||
|
z-index: 10 !important;
|
||||||
|
}
|
38
src/components/editors/SortableAudioGrid/index.tsx
Normal file
38
src/components/editors/SortableAudioGrid/index.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { SortableContainer } from 'react-sortable-hoc';
|
||||||
|
import { AudioUpload } from '~/components/upload/AudioUpload';
|
||||||
|
import * as styles from './styles.scss';
|
||||||
|
import { SortableImageGridItem } from '~/components/editors/SortableImageGridItem';
|
||||||
|
import { IFile } from '~/redux/types';
|
||||||
|
import { IUploadStatus } from '~/redux/uploads/reducer';
|
||||||
|
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
||||||
|
|
||||||
|
const SortableAudioGrid = SortableContainer(
|
||||||
|
({
|
||||||
|
items,
|
||||||
|
locked,
|
||||||
|
onDrop,
|
||||||
|
}: {
|
||||||
|
items: IFile[];
|
||||||
|
locked: IUploadStatus[];
|
||||||
|
onDrop: (file_id: IFile['id']) => void;
|
||||||
|
}) => (
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{items
|
||||||
|
.filter(file => file && file.id)
|
||||||
|
.map((file, index) => (
|
||||||
|
<SortableImageGridItem key={file.id} index={index} collection={0}>
|
||||||
|
<AudioPlayer file={file} onDrop={onDrop} />
|
||||||
|
</SortableImageGridItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{locked.map((item, index) => (
|
||||||
|
<SortableImageGridItem key={item.temp_id} index={index} collection={1} disabled>
|
||||||
|
<AudioUpload title={item.name} progress={item.progress} is_uploading />
|
||||||
|
</SortableImageGridItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export { SortableAudioGrid };
|
13
src/components/editors/SortableAudioGrid/styles.scss
Normal file
13
src/components/editors/SortableAudioGrid/styles.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.grid {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: $gap;
|
||||||
|
grid-row-gap: $gap;
|
||||||
|
grid-template-columns: auto;
|
||||||
|
grid-template-rows: $comment_height;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(30vw, 1fr));
|
||||||
|
}
|
||||||
|
}
|
10
src/components/editors/SortableAudioGridItem/index.tsx
Normal file
10
src/components/editors/SortableAudioGridItem/index.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { SortableElement } from 'react-sortable-hoc';
|
||||||
|
|
||||||
|
import * as styles from './styles.scss';
|
||||||
|
|
||||||
|
const SortableAudioGridItem = SortableElement(({ children }) => (
|
||||||
|
<div className={styles.item}>{children}</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
export { SortableAudioGridItem };
|
4
src/components/editors/SortableAudioGridItem/styles.scss
Normal file
4
src/components/editors/SortableAudioGridItem/styles.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.item {
|
||||||
|
z-index: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
|
@ -1,6 +1,4 @@
|
||||||
.grid {
|
.grid {
|
||||||
min-height: 200px;
|
|
||||||
padding-bottom: 62px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
43
src/components/upload/AudioUpload/index.tsx
Normal file
43
src/components/upload/AudioUpload/index.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import * as styles from './styles.scss';
|
||||||
|
import { ArcProgress } from '~/components/input/ArcProgress';
|
||||||
|
import { IFile } from '~/redux/types';
|
||||||
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
id?: IFile['id'];
|
||||||
|
title?: string;
|
||||||
|
progress?: number;
|
||||||
|
onDrop?: (file_id: IFile['id']) => void;
|
||||||
|
|
||||||
|
is_uploading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AudioUpload: FC<IProps> = ({ title, progress, is_uploading, id, onDrop }) => {
|
||||||
|
const onDropFile = useCallback(() => {
|
||||||
|
if (!id || !onDrop) return;
|
||||||
|
onDrop(id);
|
||||||
|
}, [id, onDrop]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
{id && onDrop && (
|
||||||
|
<div className={styles.drop} onMouseDown={onDropFile}>
|
||||||
|
<Icon icon="close" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={classNames(styles.thumb_wrap, { is_uploading })}>
|
||||||
|
{is_uploading && (
|
||||||
|
<div className={styles.progress}>
|
||||||
|
<ArcProgress size={40} progress={progress} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{title && <div className={styles.title}>{title}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { AudioUpload };
|
75
src/components/upload/AudioUpload/styles.scss
Normal file
75
src/components/upload/AudioUpload/styles.scss
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
.wrap {
|
||||||
|
background: lighten($content_bg, 4%);
|
||||||
|
// padding-bottom: 100%;
|
||||||
|
border-radius: $radius;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
height: $comment_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb_wrap {
|
||||||
|
// position: absolute;
|
||||||
|
// width: 100%;
|
||||||
|
// height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: $radius;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: $radius;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
flex: 0 0 $comment_height;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
fill: none;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.helper {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: #222222;
|
||||||
|
position: absolute;
|
||||||
|
right: $gap;
|
||||||
|
top: $gap;
|
||||||
|
border-radius: 12px;
|
||||||
|
z-index: 2;
|
||||||
|
transition: background-color 250ms, opacity 0.25s;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: $red;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ export const ERRORS = {
|
||||||
TEXT_REQUIRED: 'Text_Required',
|
TEXT_REQUIRED: 'Text_Required',
|
||||||
UNKNOWN_NODE_TYPE: 'Unknown_Node_Type',
|
UNKNOWN_NODE_TYPE: 'Unknown_Node_Type',
|
||||||
URL_INVALID: 'Url_Invalid',
|
URL_INVALID: 'Url_Invalid',
|
||||||
|
FILES_AUDIO_REQUIRED: 'Files_Audio_Required',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ERROR_LITERAL = {
|
export const ERROR_LITERAL = {
|
||||||
|
@ -18,4 +19,5 @@ export const ERROR_LITERAL = {
|
||||||
[ERRORS.TEXT_REQUIRED]: 'Нужно немного текста',
|
[ERRORS.TEXT_REQUIRED]: 'Нужно немного текста',
|
||||||
[ERRORS.UNKNOWN_NODE_TYPE]: 'Неизвестный тип поста',
|
[ERRORS.UNKNOWN_NODE_TYPE]: 'Неизвестный тип поста',
|
||||||
[ERRORS.URL_INVALID]: 'Неизвестный адрес',
|
[ERRORS.URL_INVALID]: 'Неизвестный адрес',
|
||||||
|
[ERRORS.FILES_AUDIO_REQUIRED]: 'Нужна хотя бы одна песня',
|
||||||
};
|
};
|
||||||
|
|
|
@ -38,6 +38,7 @@ export const EMPTY_UPLOAD_STATUS: IUploadStatus = {
|
||||||
thumbnail_url: null,
|
thumbnail_url: null,
|
||||||
type: null,
|
type: null,
|
||||||
temp_id: null,
|
temp_id: null,
|
||||||
|
name: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// for targeted cancellation
|
// for targeted cancellation
|
||||||
|
|
|
@ -12,6 +12,7 @@ export interface IUploadStatus {
|
||||||
thumbnail_url: string;
|
thumbnail_url: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
temp_id: UUID;
|
temp_id: UUID;
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUploadState {
|
export interface IUploadState {
|
||||||
|
|
|
@ -78,8 +78,10 @@ function* uploadFile({ file, temp_id, type, target }: IFileWithUUID) {
|
||||||
{
|
{
|
||||||
preview,
|
preview,
|
||||||
is_uploading: true,
|
is_uploading: true,
|
||||||
type: file.type,
|
// type: file.type,
|
||||||
temp_id,
|
temp_id,
|
||||||
|
type,
|
||||||
|
name: file.name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -106,7 +108,7 @@ function* uploadFile({ file, temp_id, type, target }: IFileWithUUID) {
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return yield put(
|
return yield put(
|
||||||
uploadSetStatus(temp_id, { is_uploading: false, error: data.detail || error })
|
uploadSetStatus(temp_id, { is_uploading: false, error: data.detail || error, type })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,8 +118,10 @@ function* uploadFile({ file, temp_id, type, target }: IFileWithUUID) {
|
||||||
error: null,
|
error: null,
|
||||||
uuid: data.id,
|
uuid: data.id,
|
||||||
url: data.full_path,
|
url: data.full_path,
|
||||||
|
type,
|
||||||
thumbnail_url: data.full_path,
|
thumbnail_url: data.full_path,
|
||||||
progress: 1,
|
progress: 1,
|
||||||
|
name: file.name,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue