mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
removed player reducer, migrated to CRA 5
This commit is contained in:
parent
88f8fe21f7
commit
558e8f8a4f
211 changed files with 7131 additions and 10318 deletions
|
@ -1,5 +1,5 @@
|
|||
import { INodeRelated } from '~/types/node';
|
||||
import React, { createContext, FC, useContext } from 'react';
|
||||
import { INodeRelated } from "~/types/node";
|
||||
import React, { createContext, FC, useContext } from "react";
|
||||
|
||||
interface NodeRelatedProviderProps {
|
||||
related: INodeRelated;
|
||||
|
|
25
src/utils/errors/getErrorMessage.ts
Normal file
25
src/utils/errors/getErrorMessage.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { has, path } from 'ramda';
|
||||
import { ERROR_LITERAL, ERRORS } from '~/constants/errors';
|
||||
|
||||
export const getErrorMessage = (error: unknown) => {
|
||||
if (typeof error === 'string' && has(error, ERROR_LITERAL)) {
|
||||
return ERROR_LITERAL[error];
|
||||
}
|
||||
|
||||
if (!(error instanceof Error)) {
|
||||
console.warn('catched strange exception', error);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Network error
|
||||
if (error.message === 'Network Error') {
|
||||
return ERROR_LITERAL[ERRORS.NETWORK_ERROR];
|
||||
}
|
||||
|
||||
const messageFromBackend = String(path(['response', 'data', 'error'], error));
|
||||
if (messageFromBackend && has(messageFromBackend, ERROR_LITERAL)) {
|
||||
return ERROR_LITERAL[messageFromBackend];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
9
src/utils/errors/getValidationErrors.ts
Normal file
9
src/utils/errors/getValidationErrors.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { hasPath, path } from 'ramda';
|
||||
|
||||
export const getValidationErrors = (error: unknown): Record<string, string> | undefined => {
|
||||
if (hasPath(['response', 'data', 'errors'], error)) {
|
||||
return path(['response', 'data', 'errors'], error);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
import { hideToast, showToastError } from '~/utils/toast';
|
||||
import { has, path } from 'ramda';
|
||||
import { ERROR_LITERAL, ERRORS } from '~/constants/errors';
|
||||
import { getErrorMessage } from '~/utils/errors/getErrorMessage';
|
||||
|
||||
let toastId = '';
|
||||
|
||||
|
@ -10,29 +9,16 @@ const handleTranslated = (message: string) => {
|
|||
hideToast(toastId);
|
||||
}
|
||||
|
||||
toastId = showToastError(ERROR_LITERAL[message]);
|
||||
toastId = showToastError(message);
|
||||
};
|
||||
|
||||
export const showErrorToast = (error: unknown) => {
|
||||
if (typeof error === 'string' && has(error, ERROR_LITERAL)) {
|
||||
handleTranslated(error);
|
||||
return;
|
||||
const message = getErrorMessage(error);
|
||||
if (message) {
|
||||
return handleTranslated(message);
|
||||
}
|
||||
|
||||
if (!(error instanceof Error)) {
|
||||
console.warn('catched strange exception', error);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Network error
|
||||
if (error.message === 'Network Error') {
|
||||
handleTranslated(ERRORS.NETWORK_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
const messageFromBackend = String(path(['response', 'data', 'error'], error));
|
||||
if (messageFromBackend && has(messageFromBackend, ERROR_LITERAL)) {
|
||||
handleTranslated(messageFromBackend);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import marked from 'marked';
|
||||
import { stripHTMLTags } from '~/utils/stripHTMLTags';
|
||||
import marked from "marked";
|
||||
import { stripHTMLTags } from "~/utils/stripHTMLTags";
|
||||
|
||||
/**
|
||||
* Cleans youtube urls
|
||||
|
|
|
@ -5,8 +5,7 @@ import { path } from 'ramda';
|
|||
import { NODE_TYPES } from '~/constants/node';
|
||||
|
||||
export const canEditNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
|
||||
path(['role'], user) === USER_ROLES.ADMIN ||
|
||||
(path(['user', 'id'], node) && path(['user', 'id'], node) === path(['id'], user));
|
||||
path(['role'], user) === USER_ROLES.ADMIN || path(['user', 'id'], node) === path(['id'], user);
|
||||
|
||||
export const canEditComment = (comment?: Partial<ICommentGroup>, user?: Partial<IUser>): boolean =>
|
||||
path(['role'], user) === USER_ROLES.ADMIN || path(['user', 'id'], comment) === path(['id'], user);
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
import { store } from '~/redux/store';
|
||||
import { playerSetStatus, playerStopped } from '~/redux/player/actions';
|
||||
import { PlayerState } from '~/redux/player/constants';
|
||||
|
||||
type PlayerEventType = keyof HTMLMediaElementEventMap;
|
||||
|
||||
type PlayerEventListener = (
|
||||
this: HTMLAudioElement,
|
||||
event: HTMLMediaElementEventMap[keyof HTMLMediaElementEventMap]
|
||||
) => void;
|
||||
|
||||
export interface IPlayerProgress {
|
||||
current: number;
|
||||
total: number;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export class PlayerClass {
|
||||
public constructor() {
|
||||
this.element?.addEventListener('timeupdate', () => {
|
||||
const { duration: total, currentTime: current } = this.element;
|
||||
const progress = parseFloat(((current / total) * 100).toFixed(2));
|
||||
|
||||
this.current = current || 0;
|
||||
this.total = total || 0;
|
||||
|
||||
this.element.dispatchEvent(
|
||||
new CustomEvent('playprogress', {
|
||||
detail: { current, total, progress },
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public current = 0;
|
||||
public total = 0;
|
||||
public element = new Audio();
|
||||
public duration = 0;
|
||||
|
||||
public set = (src: string) => {
|
||||
this.element.src = src;
|
||||
};
|
||||
|
||||
public on = (type: string, callback) => {
|
||||
this.element?.addEventListener(type, callback);
|
||||
};
|
||||
|
||||
public off = (type: string, callback) => {
|
||||
this.element?.removeEventListener(type, callback);
|
||||
};
|
||||
|
||||
public load = () => {
|
||||
this.element.load();
|
||||
};
|
||||
|
||||
public play = () => {
|
||||
this.element.play();
|
||||
};
|
||||
|
||||
public pause = () => {
|
||||
this.element.pause();
|
||||
};
|
||||
|
||||
public stop = () => {
|
||||
this.element.src = '';
|
||||
this.element.dispatchEvent(new CustomEvent('stop'));
|
||||
};
|
||||
|
||||
public getDuration = () => {
|
||||
return this.element.currentTime;
|
||||
};
|
||||
|
||||
public jumpToTime = (time: number) => {
|
||||
this.element.currentTime = time;
|
||||
};
|
||||
|
||||
public jumpToPercent = (percent: number) => {
|
||||
this.element.currentTime = (this.total * percent) / 100;
|
||||
};
|
||||
}
|
||||
|
||||
const Player = new PlayerClass();
|
||||
|
||||
Player.on('play', () => store.dispatch(playerSetStatus(PlayerState.PLAYING)));
|
||||
Player.on('pause', () => store.dispatch(playerSetStatus(PlayerState.PAUSED)));
|
||||
Player.on('stop', () => store.dispatch(playerStopped()));
|
||||
Player.on('error', () => store.dispatch(playerStopped()));
|
||||
|
||||
export { Player };
|
115
src/utils/providers/AudioPlayerProvider.tsx
Normal file
115
src/utils/providers/AudioPlayerProvider.tsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
import React, { createContext, FC, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { IFile } from "~/redux/types";
|
||||
import { getURL } from "~/utils/dom";
|
||||
import { path } from "ramda";
|
||||
import { PlayerState } from "~/constants/player";
|
||||
import { PlayerProgress } from "~/types/player";
|
||||
|
||||
interface AudioPlayerProps {
|
||||
file?: IFile;
|
||||
title: string;
|
||||
progress: PlayerProgress;
|
||||
status: PlayerState;
|
||||
|
||||
play: () => Promise<void>;
|
||||
pause: () => void;
|
||||
stop: () => void;
|
||||
setFile: (file: IFile) => void;
|
||||
toTime: (time: number) => void;
|
||||
toPercent: (percent: number) => void;
|
||||
}
|
||||
|
||||
const PlayerContext = createContext<AudioPlayerProps>({
|
||||
file: undefined,
|
||||
title: '',
|
||||
progress: { progress: 0, current: 0, total: 0 },
|
||||
status: PlayerState.UNSET,
|
||||
|
||||
play: async () => {},
|
||||
pause: () => {},
|
||||
stop: () => {},
|
||||
setFile: () => {},
|
||||
toTime: () => {},
|
||||
toPercent: () => {},
|
||||
});
|
||||
|
||||
const audio = new Audio();
|
||||
|
||||
export const AudioPlayerProvider: FC = ({ children }) => {
|
||||
const [status, setStatus] = useState(PlayerState.UNSET);
|
||||
const [file, setFile] = useState<IFile | undefined>();
|
||||
const [progress, setProgress] = useState<PlayerProgress>({ progress: 0, current: 0, total: 0 });
|
||||
|
||||
/** controls */
|
||||
const play = audio.play.bind(audio);
|
||||
const pause = audio.pause.bind(audio);
|
||||
const stop = useCallback(() => {
|
||||
audio.pause();
|
||||
audio.dispatchEvent(new CustomEvent('stop'));
|
||||
setFile(undefined);
|
||||
setStatus(PlayerState.UNSET);
|
||||
}, [setFile]);
|
||||
|
||||
const toTime = useCallback((time: number) => {
|
||||
audio.currentTime = time;
|
||||
}, []);
|
||||
|
||||
const toPercent = useCallback(
|
||||
(percent: number) => {
|
||||
audio.currentTime = (progress.total * percent) / 100;
|
||||
},
|
||||
[progress]
|
||||
);
|
||||
|
||||
/** handles progress update */
|
||||
useEffect(() => {
|
||||
const onProgress = () => {
|
||||
setProgress({
|
||||
total: audio.duration,
|
||||
current: audio.currentTime,
|
||||
progress: (audio.currentTime / audio.duration) * 100,
|
||||
});
|
||||
};
|
||||
|
||||
const onPause = () => {
|
||||
setStatus(PlayerState.PAUSED);
|
||||
};
|
||||
|
||||
const onPlay = () => {
|
||||
setStatus(PlayerState.PLAYING);
|
||||
};
|
||||
|
||||
audio.addEventListener('playprogress', onProgress);
|
||||
audio.addEventListener('timeupdate', onProgress);
|
||||
audio.addEventListener('pause', onPause);
|
||||
audio.addEventListener('playing', onPlay);
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('playprogress', onProgress);
|
||||
audio.removeEventListener('timeupdate', onProgress);
|
||||
audio.removeEventListener('pause', onPause);
|
||||
audio.removeEventListener('playing', onPlay);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** update audio src */
|
||||
useEffect(() => {
|
||||
audio.src = file ? getURL(file) : '';
|
||||
}, [file]);
|
||||
|
||||
const metadata: IFile['metadata'] = path(['metadata'], file);
|
||||
const title =
|
||||
(metadata &&
|
||||
(metadata.title || [metadata.id3artist, metadata.id3title].filter(el => !!el).join(' - '))) ||
|
||||
'';
|
||||
|
||||
return (
|
||||
<PlayerContext.Provider
|
||||
value={{ file, setFile, title, progress, toTime, toPercent, play, pause, stop, status }}
|
||||
>
|
||||
{children}
|
||||
</PlayerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAudioPlayer = () => useContext(PlayerContext);
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
import { LabContextProvider } from '~/utils/context/LabContextProvider';
|
||||
import { useLab } from '~/hooks/lab/useLab';
|
||||
import React, { FC } from "react";
|
||||
import { LabContextProvider } from "~/utils/context/LabContextProvider";
|
||||
import { useLab } from "~/hooks/lab/useLab";
|
||||
|
||||
interface LabProviderProps {}
|
||||
|
||||
|
|
39
src/utils/providers/MetadataProvider.tsx
Normal file
39
src/utils/providers/MetadataProvider.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React, { createContext, FC, useContext, useEffect } from "react";
|
||||
import { MetadataStore } from "~/store/metadata/MetadataStore";
|
||||
import { observer, useLocalObservable } from "mobx-react-lite";
|
||||
import { apiGetEmbedYoutube } from "~/api/metadata";
|
||||
import { EmbedMetadata } from "~/types/metadata";
|
||||
|
||||
interface MetadataContextProps {
|
||||
metadata: Record<string, EmbedMetadata>;
|
||||
queue: string[];
|
||||
pending: string[];
|
||||
enqueue: (id: string) => void;
|
||||
}
|
||||
const MetadataContext = createContext<MetadataContextProps>({
|
||||
metadata: {},
|
||||
queue: [],
|
||||
pending: [],
|
||||
enqueue: () => {},
|
||||
});
|
||||
|
||||
const fetchItems = async (ids: string[]) => {
|
||||
const metadata = await apiGetEmbedYoutube(ids);
|
||||
return metadata.items;
|
||||
};
|
||||
|
||||
export const MetadataProvider: FC = observer(({ children }) => {
|
||||
const { metadata, enqueue, queue, pending, watch } = useLocalObservable(
|
||||
() => new MetadataStore(fetchItems)
|
||||
);
|
||||
|
||||
useEffect(watch, [watch]);
|
||||
|
||||
return (
|
||||
<MetadataContext.Provider value={{ metadata, enqueue, queue, pending }}>
|
||||
{children}
|
||||
</MetadataContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
export const useMetadataProvider = () => useContext(MetadataContext);
|
|
@ -1,8 +1,8 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import { INode, ITag } from '~/redux/types';
|
||||
import { NodeRelatedContextProvider } from '~/utils/context/NodeRelatedContextProvider';
|
||||
import { INodeRelated } from '~/types/node';
|
||||
import { useGetNodeRelated } from '~/hooks/node/useGetNodeRelated';
|
||||
import React, { FC, useEffect } from "react";
|
||||
import { INode, ITag } from "~/redux/types";
|
||||
import { NodeRelatedContextProvider } from "~/utils/context/NodeRelatedContextProvider";
|
||||
import { INodeRelated } from "~/types/node";
|
||||
import { useGetNodeRelated } from "~/hooks/node/useGetNodeRelated";
|
||||
|
||||
interface NodeRelatedProviderProps {
|
||||
id: INode['id'];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// create-index.tsx
|
||||
import { Action } from 'redux';
|
||||
import { Action } from "redux";
|
||||
|
||||
type Handlers<State, Types extends string, Actions extends Action<Types>> = {
|
||||
readonly [Type in Types]: (state: State, action: Actions) => State;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { VALIDATORS } from '~/utils/validators';
|
||||
import { FILE_MIMES, UploadType } from '~/constants/uploads';
|
||||
import { VALIDATORS } from "~/utils/validators";
|
||||
import { FILE_MIMES, UploadType } from "~/constants/uploads";
|
||||
|
||||
/** if file is image, returns data-uri of thumbnail */
|
||||
export const uploadGetThumb = async file => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue