1
0
Fork 0
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:
Fedor Katurov 2022-01-07 18:32:22 +07:00
parent 88f8fe21f7
commit 558e8f8a4f
211 changed files with 7131 additions and 10318 deletions

View file

@ -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;

View 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;
};

View 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;
};

View file

@ -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;
}

View file

@ -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

View file

@ -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);

View file

@ -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 };

View 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);

View file

@ -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 {}

View 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);

View file

@ -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'];

View file

@ -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;

View file

@ -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 => {