feat(app): beatconnect client revamp (#188)
This commit is contained in:
Vendored
+4
-1
@@ -5,5 +5,8 @@
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"files.eol": "\n"
|
||||
"files.eol": "\n",
|
||||
"extensions.ignoreRecommendations": true,
|
||||
"files.autoSave": "off",
|
||||
"debug.allowBreakpointsEverywhere": true
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ The official client for [Beatconnect](https://beatconnect.io) which is a mirror
|
||||
|
||||
## Quick Tour
|
||||
- This App gives you access to all the beatmaps mirrored on [Beatconnect](https://beatconnect.io). You can downloads multiple beatmaps that will be automaticaly imported into osu! </br>
|
||||
<img src="https://cdn.discordapp.com/attachments/414474227710820352/663024989984915467/unknown_2.jpg">
|
||||
<img src="https://user-images.githubusercontent.com/46972108/90391453-75d31600-e08d-11ea-9ad0-bdb881219fdd.png">
|
||||
|
||||
- You can launch an <b>IRC bot</b> from the app that will make all [available commands](./docs/commands.md) usable to peoples pming you and from all the matches chats that the bot is connected to. (how to connect docs soon..)</br>
|
||||
Comming with the <b>autobeat</b> feature that send the Beatconnect download link in the #multuplayer channel each time host change the beatmap
|
||||
|
||||
@@ -60,12 +60,14 @@ let make = (~children) => {
|
||||
},
|
||||
);
|
||||
|
||||
Audio.onplay(
|
||||
audio,
|
||||
_e => {
|
||||
Js.log("PAUSEEEE");
|
||||
setPlayingState(oldState => {...oldState, isPlaying: true});
|
||||
},
|
||||
Audio.onplay(audio, e => {
|
||||
setPlayingState(oldState =>
|
||||
{...oldState, isPlaying: e.target.readyState === 4}
|
||||
)
|
||||
});
|
||||
|
||||
Audio.oncanplay(audio, _e =>
|
||||
setPlayingState(oldState => {...oldState, isPlaying: true})
|
||||
);
|
||||
|
||||
let setPreviewAudio = (beatmapSetId: int) => {
|
||||
@@ -82,7 +84,7 @@ let make = (~children) => {
|
||||
);
|
||||
setPreviewAudio(beatmapSetId);
|
||||
Audio.play(audio);
|
||||
setPlayingState(_oldState => {isPlaying: true, beatmapSetId, songTitle});
|
||||
setPlayingState(_oldState => {isPlaying: false, beatmapSetId, songTitle});
|
||||
};
|
||||
|
||||
let pause = () => {
|
||||
@@ -96,4 +98,4 @@ let make = (~children) => {
|
||||
|
||||
let value = {playingState, pause, setAudio, setVolume};
|
||||
<Provider value> children </Provider>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
export const getFadeIn = () => ({
|
||||
'@keyframes fadeIn': {
|
||||
from: {
|
||||
opacity: 0,
|
||||
},
|
||||
to: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getAppear = () => ({
|
||||
'@keyframes appear': {
|
||||
from: {
|
||||
filter: 'blur(25px)',
|
||||
opacity: 0,
|
||||
},
|
||||
to: {
|
||||
filter: 'blur(0px)',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const sectionSwitchAnimation = () => ({
|
||||
opacity: 0,
|
||||
animation: '140ms ease-in forwards $fadeIn',
|
||||
});
|
||||
|
||||
export const getDragRegion = (override, header) => ({
|
||||
WebkitAppRegion: `drag ${override ? '!important' : ''}`,
|
||||
position: 'relative',
|
||||
...(header
|
||||
? {
|
||||
'&::after': {
|
||||
content: "''",
|
||||
height: '24px',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
width: '80px',
|
||||
right: 0,
|
||||
top: 0,
|
||||
WebkitAppRegion: `no-drag !important`,
|
||||
},
|
||||
}
|
||||
: null),
|
||||
'& > *': {
|
||||
WebkitAppRegion: `no-drag ${override ? '!important' : ''}`,
|
||||
},
|
||||
});
|
||||
+4
-1
@@ -4,8 +4,10 @@ import { createUseStyles } from 'react-jss';
|
||||
import Nav from './modules/Nav';
|
||||
import TitleBar from './modules/common/TitleBar.bs';
|
||||
import config from '../shared/config';
|
||||
import { getAppear } from './helpers/css.utils';
|
||||
|
||||
const useStyles = createUseStyles({
|
||||
...getAppear(),
|
||||
App: {
|
||||
'-webkit-font-smoothing': 'antialiased',
|
||||
userSelect: 'none',
|
||||
@@ -15,7 +17,8 @@ const useStyles = createUseStyles({
|
||||
sans-serif`,
|
||||
},
|
||||
appContentWrapper: {
|
||||
paddingTop: config.display.titleBarHeight,
|
||||
animation: '500ms ease-in forwards $appear',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ import DropDown from '../../common/DropDown';
|
||||
import renderIcons from '../../../helpers/renderIcons';
|
||||
import config from '../../../../shared/config';
|
||||
import Button from '../../common/Button';
|
||||
import { getDragRegion } from '../../../helpers/css.utils';
|
||||
|
||||
const { trackEvent } = remote.getGlobal('tracking');
|
||||
|
||||
const useStyle = createUseStyles({
|
||||
Search: {
|
||||
...getDragRegion(true),
|
||||
width: '100%',
|
||||
display: 'inline-flex',
|
||||
'& div, select, input, label': {
|
||||
@@ -27,6 +29,7 @@ const useStyle = createUseStyles({
|
||||
},
|
||||
hideDownloaded: {
|
||||
cursor: 'pointer',
|
||||
margin: 'auto 5px',
|
||||
},
|
||||
searchButtonWrapper: {
|
||||
margin: 8,
|
||||
@@ -98,14 +101,13 @@ const Search = ({ lastSearch, isBusy, beatmapCount, skeletonBeatmaps }) => {
|
||||
onKeyDown={searchOnEnter}
|
||||
onBlur={() => execSearch()}
|
||||
/>
|
||||
<div className={classes.right} />
|
||||
<div
|
||||
className={classes.hideDownloaded}
|
||||
onClick={togleHideDownloaded}
|
||||
title="Hide downloaded beatmaps"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className={classes.right} />
|
||||
{renderIcons({ name: 'Verified', color: search.hideDownloaded && theme.palette.primary.accent })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable camelcase */
|
||||
import React, { useEffect, useContext, useState, useRef } from 'react';
|
||||
import React, { useEffect, useContext, useState, useRef, useCallback } from 'react';
|
||||
import _ from 'underscore';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'redux';
|
||||
import { FixedSizeGrid } from 'react-window';
|
||||
import { VariableSizeGrid } from 'react-window';
|
||||
import injectSheet, { useTheme } from 'react-jss';
|
||||
import Beatmap from '../common/Beatmap';
|
||||
import Search from './components/Search';
|
||||
@@ -13,14 +13,17 @@ import { HistoryContext } from '../../Providers/HistoryProvider';
|
||||
import BeatmapSkeleton from '../common/Beatmap/beatmap.skeleton';
|
||||
import config from '../../../shared/config';
|
||||
import { saveLastScrollPosition } from './reducer/actions';
|
||||
import { getFadeIn, sectionSwitchAnimation } from '../../helpers/css.utils';
|
||||
|
||||
const styles = {
|
||||
...getFadeIn(),
|
||||
Beatmaps: {
|
||||
outline: 'none',
|
||||
...sectionSwitchAnimation(),
|
||||
},
|
||||
};
|
||||
|
||||
const Beatmaps = ({ searchResults, classes, setHeaderContent, window, panelExpended }) => {
|
||||
const Beatmaps = ({ searchResults, classes, setHeaderContent, window }) => {
|
||||
const theme = useTheme();
|
||||
const gridContainer = useRef(null);
|
||||
const [autoDl, setAutoDl] = useState(false);
|
||||
@@ -35,11 +38,10 @@ const Beatmaps = ({ searchResults, classes, setHeaderContent, window, panelExpen
|
||||
lastSearch.current = search;
|
||||
if (gridContainer.current) gridContainer.current.childNodes[0].scrollTop = lastScrollPosition.current;
|
||||
}
|
||||
const gridWidth =
|
||||
window.width - (panelExpended ? config.display.sidePanelExpandedLength : config.display.sidePanelCompactedLength);
|
||||
const gridHeight = window.height - (config.display.titleBarHeight + config.display.topBarHeight);
|
||||
const displayGrid = gridWidth >= 1200;
|
||||
const rowCount = displayGrid ? Math.ceil(beatmaps.length / 2) : beatmaps.length;
|
||||
const gridWidth = window.width - config.display.sidePanelCompactedLength - 1;
|
||||
const gridHeight = window.height - 1;
|
||||
const displayGrid = gridWidth >= 1000;
|
||||
const rowCount = (displayGrid ? Math.ceil(beatmaps.length / 2) : beatmaps.length) + 1; // Add one for the invisible top placeholder
|
||||
const canLoadMore = hideDownloaded ? !lastPage : beatmaps.length % 50 === 0;
|
||||
const onScroll = ({ scrollTop }) => {
|
||||
lastScrollPosition.current = scrollTop;
|
||||
@@ -65,10 +67,15 @@ const Beatmaps = ({ searchResults, classes, setHeaderContent, window, panelExpen
|
||||
return () => saveLastScrollPosition(lastScrollPosition.current);
|
||||
}, []);
|
||||
|
||||
const getColumnWidth = useCallback(() => (displayGrid ? gridWidth / 2 - 9 : gridWidth - 18), [
|
||||
displayGrid,
|
||||
gridWidth,
|
||||
]);
|
||||
|
||||
const renderBeatmaps = ({ columnIndex, rowIndex, style }) => {
|
||||
const beatmap =
|
||||
(displayGrid && beatmaps[(rowIndex === 0 ? 0 : rowIndex + rowIndex) + columnIndex]) || beatmaps[rowIndex];
|
||||
if (beatmap === 0) return <BeatmapSkeleton style={style} />;
|
||||
if (rowIndex === 0) return <div style={{ height: `${config.display.topBarHeight}px` }} />;
|
||||
const beatmap = displayGrid ? beatmaps[rowIndex * 2 + columnIndex - 2] : beatmaps[rowIndex - 1];
|
||||
if (beatmap === 0) return <BeatmapSkeleton style={style} rowIndex={rowIndex} />;
|
||||
if (!beatmap) return <div style={style} className="NoBeatmap" />;
|
||||
return (
|
||||
<div style={style}>
|
||||
@@ -91,28 +98,30 @@ const Beatmaps = ({ searchResults, classes, setHeaderContent, window, panelExpen
|
||||
role="button"
|
||||
ref={gridContainer}
|
||||
>
|
||||
<FixedSizeGrid
|
||||
<VariableSizeGrid
|
||||
columnCount={displayGrid ? 2 : 1}
|
||||
columnWidth={displayGrid ? gridWidth / 2 - 9 : gridWidth - 18}
|
||||
columnWidth={getColumnWidth}
|
||||
estimatedColumnWidth={getColumnWidth()}
|
||||
estimatedRowHeight={250}
|
||||
rowCount={rowCount}
|
||||
rowHeight={250}
|
||||
rowHeight={index => (index === 0 ? config.display.topBarHeight : 250)}
|
||||
overscanRowCount={7}
|
||||
height={gridHeight}
|
||||
width={gridWidth}
|
||||
onItemsRendered={newItemsRendered}
|
||||
onScroll={onScroll}
|
||||
initialScrollTop={lastScrollPosition.current}
|
||||
key={getColumnWidth()}
|
||||
>
|
||||
{renderBeatmaps}
|
||||
</FixedSizeGrid>
|
||||
</VariableSizeGrid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ app, settings, beatmaps }) => ({
|
||||
const mapStateToProps = ({ app, beatmaps }) => ({
|
||||
searchResults: beatmaps.searchResults,
|
||||
window: app.window,
|
||||
panelExpended: settings.userPreferences.sidePanelExpended,
|
||||
});
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { createUseStyles } from 'react-jss';
|
||||
import Start from './components/Start';
|
||||
import Matchs from './Matchs';
|
||||
import config from '../../../shared/config';
|
||||
import { getFadeIn, sectionSwitchAnimation } from '../../helpers/css.utils';
|
||||
|
||||
const useStyles = createUseStyles({
|
||||
...getFadeIn(),
|
||||
Bot: {
|
||||
paddingTop: `${config.display.topBarHeight}px`,
|
||||
...sectionSwitchAnimation(),
|
||||
},
|
||||
});
|
||||
|
||||
const Bot = ({ connected, bot, theme, setHeaderContent }) => {
|
||||
const classes = useStyles();
|
||||
useEffect(() => {
|
||||
setHeaderContent(<Start connected={connected} />);
|
||||
return () => setHeaderContent(null);
|
||||
}, [setHeaderContent, connected, theme]);
|
||||
|
||||
return (
|
||||
<div className="menuContainer">
|
||||
<div className={`menuContainer ${classes.Bot}`}>
|
||||
<Matchs bot={bot} connected={connected} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { VariableSizeList } from 'react-window';
|
||||
import { connect } from 'react-redux';
|
||||
import { HistoryContext } from '../../../Providers/HistoryProvider';
|
||||
import DownloadsItem from './Item';
|
||||
@@ -7,26 +7,35 @@ import config from '../../../../shared/config';
|
||||
|
||||
const DownloadedItems = ({ window }) => {
|
||||
const { history } = useContext(HistoryContext);
|
||||
const items = [];
|
||||
Object.values(history).forEach(item => {
|
||||
const sortedHistory = Object.values(history).sort((itemA, itemB) => itemB.date - itemA.date);
|
||||
const items = sortedHistory.map(item => {
|
||||
const { id, date, name } = item;
|
||||
items.push(<DownloadsItem id={id} date={date} name={name} status="downloaded" key={id} />);
|
||||
return isScrolling => (
|
||||
<DownloadsItem id={id} date={date} name={name} status="downloaded" key={id} isScrolling={isScrolling} />
|
||||
);
|
||||
});
|
||||
items.sort((a, b) => b.props.date - a.props.date);
|
||||
|
||||
const renderItems = ({ index, style }) => <div style={style}>{items[index]}</div>;
|
||||
const renderItems = ({ index, style, isScrolling }) =>
|
||||
index === 0 ? <div /> : <div style={style}>{items[index - 1](isScrolling)}</div>;
|
||||
return (
|
||||
<div className="downloadMenu DownloadsItem" style={{ width: '100%' }}>
|
||||
<FixedSizeList
|
||||
height={window.height - (config.display.titleBarHeight + config.display.topBarHeight)}
|
||||
itemCount={items.length}
|
||||
itemSize={130}
|
||||
overscanCount={2}
|
||||
width="100%"
|
||||
className="downloadMenu customScroll"
|
||||
>
|
||||
{items.length > 0 ? renderItems : 'The beatmaps you download will go here'}
|
||||
</FixedSizeList>
|
||||
{items.length ? (
|
||||
<VariableSizeList
|
||||
height={window.height}
|
||||
itemCount={items.length + 1}
|
||||
itemSize={index => (index === 0 ? config.display.topBarHeight : 130)}
|
||||
overscanCount={5}
|
||||
width="100%"
|
||||
className="downloadMenu customScroll"
|
||||
useIsScrolling
|
||||
>
|
||||
{renderItems}
|
||||
</VariableSizeList>
|
||||
) : (
|
||||
<span style={{ marginTop: `calc(${config.display.topBarHeight}px + 1rem)`, display: 'block' }}>
|
||||
The beatmaps you download will go here
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, memo } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import injectSheet, { useTheme } from 'react-jss';
|
||||
import { Text } from 'react-desktop';
|
||||
import { shell } from 'electron';
|
||||
@@ -10,19 +10,20 @@ import PreviewBeatmapBtn from '../../../common/Beatmap/PreviewBeatmapBtn';
|
||||
import styles from './ItemStyles';
|
||||
import Button from '../../../common/Button';
|
||||
|
||||
const DownloadsItem = ({ id, name, item, date, status, progress, speed, classes, cancel }) => {
|
||||
const DownloadsItem = ({ id, name, item, date, status, progress, speed, classes, cancel, isScrolling }) => {
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const toggleDownload = () => {
|
||||
item.isPaused() ? item.resume() : item.pause();
|
||||
if (item.isPaused()) item.resume();
|
||||
else item.pause();
|
||||
setIsPaused(item.isPaused());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.DownloadsItem}>
|
||||
<div className={classes.fade}>
|
||||
<Cover url={`https://assets.ppy.sh/beatmaps/${id}/covers/cover.jpg`} height={130} />
|
||||
<Cover url={`https://assets.ppy.sh/beatmaps/${id}/covers/cover.jpg`} height={130} canLoad={!isScrolling} />
|
||||
</div>
|
||||
{progress ? (
|
||||
<div className={classes.downloadInfos}>
|
||||
@@ -62,4 +63,4 @@ const areEqual = (prevProps, nextProps) => {
|
||||
}
|
||||
return prevProps.status === nextProps.status;
|
||||
};
|
||||
export default memo(injectSheet(styles)(DownloadsItem), areEqual);
|
||||
export default injectSheet(styles)(DownloadsItem);
|
||||
|
||||
@@ -33,7 +33,12 @@ const Downloads = ({ setHeaderContent }) => {
|
||||
|
||||
return (
|
||||
<div className="menuContainer Downloads" style={{ transition: 'background 0ms' }}>
|
||||
<NavPanel paneExpandedLength={150} defaultIsPanelExpanded sidePanelBackground={theme.palette.secondary.dark}>
|
||||
<NavPanel
|
||||
paneExpandedLength={150}
|
||||
defaultIsPanelExpanded
|
||||
sidePanelBackground={theme.palette.secondary.dark}
|
||||
subPanel
|
||||
>
|
||||
{renderItem(`Queued`, <DownloadsInQueue queue={queue} removeItemfromQueue={removeItemfromQueue} />)}
|
||||
{renderItem('Downloaded', <DownloadedItems theme={theme} />)}
|
||||
</NavPanel>
|
||||
|
||||
@@ -105,14 +105,20 @@ const BeatmapPackDetail = ({ classes, windowSize, panelExpended, pack, select })
|
||||
)
|
||||
: pack.beatmapsets;
|
||||
|
||||
const listWidth =
|
||||
windowSize.width -
|
||||
(panelExpended ? config.display.sidePanelExpandedLength : config.display.sidePanelCompactedLength);
|
||||
const listHeight = windowSize.height - (config.display.titleBarHeight + config.display.topBarHeight);
|
||||
const itemCount = filteredBeatmapsets.length + 1; // +1 for top spacer
|
||||
|
||||
const listWidth = windowSize.width - config.display.sidePanelCompactedLength;
|
||||
const listHeight = windowSize.height;
|
||||
|
||||
// optimization needed (useCallback or memo ?) k
|
||||
const renderRow = ({ index, style }) => {
|
||||
const isPlaying = audioPlayer.playingState.beatmapSetId === filteredBeatmapsets[index].id;
|
||||
if (index === 0) {
|
||||
return <div style={style} key="PackListSpacer" />;
|
||||
}
|
||||
|
||||
const offsetedIndex = index - 1;
|
||||
const item = filteredBeatmapsets[offsetedIndex];
|
||||
const { id, title, artist } = item;
|
||||
const isPlaying = audioPlayer.playingState.beatmapSetId === id;
|
||||
const wrapperStyle = {
|
||||
backgroundColor: isPlaying && 'rgba(255,255,255,.05)',
|
||||
};
|
||||
@@ -120,36 +126,28 @@ const BeatmapPackDetail = ({ classes, windowSize, panelExpended, pack, select })
|
||||
opacity: isPlaying && 0.9,
|
||||
backgroundImage: `url(${reqImgAssets(isPlaying ? './pause-button.png' : './play-button.png')})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={style} key={filteredBeatmapsets[index].id}>
|
||||
<div style={style} key={id}>
|
||||
<div className={classes.listItem} style={wrapperStyle}>
|
||||
<div
|
||||
className={`${classes.thumbnail} thumbnail`}
|
||||
style={{ backgroundImage: `url(${getThumbUrl(filteredBeatmapsets[index].id)})` }}
|
||||
>
|
||||
<div className={`${classes.thumbnail} thumbnail`} style={{ backgroundImage: `url(${getThumbUrl(id)})` }}>
|
||||
<div
|
||||
className="playIco clickable"
|
||||
style={playIcoStyle}
|
||||
role="button"
|
||||
onClick={() =>
|
||||
playPreview(
|
||||
filteredBeatmapsets[index].id,
|
||||
isPlaying,
|
||||
`${filteredBeatmapsets[index].title} - ${filteredBeatmapsets[index].artist}`,
|
||||
)
|
||||
}
|
||||
onClick={() => playPreview(id, isPlaying, `${title} - ${artist}`)}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.title}>{filteredBeatmapsets[index].title}</div>
|
||||
<div className={classes.artist}>{filteredBeatmapsets[index].artist}</div>
|
||||
<div className={classes.title}>{title}</div>
|
||||
<div className={classes.artist}>{artist}</div>
|
||||
<DownloadBeatmapBtn
|
||||
beatmapSet={filteredBeatmapsets[index]}
|
||||
beatmapSet={item}
|
||||
title="Download"
|
||||
noStyle
|
||||
className={`${classes.downloadButton} clickable`}
|
||||
/>
|
||||
<div
|
||||
onClick={() => shell.openExternal(getBeatmapInfosUrl(filteredBeatmapsets[index]))}
|
||||
onClick={() => shell.openExternal(getBeatmapInfosUrl(item))}
|
||||
role="button"
|
||||
title="See beatmap page"
|
||||
className={`${classes.beatmapPageButton} clickable`}
|
||||
@@ -163,7 +161,7 @@ const BeatmapPackDetail = ({ classes, windowSize, panelExpended, pack, select })
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<List height={listHeight} itemCount={filteredBeatmapsets.length} itemSize={50} width={listWidth}>
|
||||
<List height={listHeight} itemCount={itemCount} itemSize={50} width={listWidth}>
|
||||
{renderRow}
|
||||
</List>
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,13 @@ import BeatmapPackDetail from './BeatmapPackDetail';
|
||||
import Header from './Header';
|
||||
import config from '../../../shared/config';
|
||||
import Disabled from './Disabled';
|
||||
import { getFadeIn, sectionSwitchAnimation } from '../../helpers/css.utils';
|
||||
|
||||
const styles = {
|
||||
...getFadeIn(),
|
||||
Packs: {
|
||||
...sectionSwitchAnimation(),
|
||||
paddingTop: `${config.display.topBarHeight}px`,
|
||||
textAlign: 'auto',
|
||||
margin: 'auto 5rem',
|
||||
display: 'grid',
|
||||
|
||||
@@ -9,7 +9,7 @@ import { make as CheckBox } from '../common/Checkbox.bs';
|
||||
const useStyles = createUseStyles({
|
||||
settingWrapper: {
|
||||
margin: '0 4vmin',
|
||||
paddingTop: '20px',
|
||||
paddingTop: '70px',
|
||||
justifyContent: 'space-evenly',
|
||||
'& p': {
|
||||
fontSize: '1.05rem',
|
||||
@@ -65,6 +65,11 @@ const useStyles = createUseStyles({
|
||||
clickable: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
description: {
|
||||
fontWeight: 100,
|
||||
fontSize: '0.8rem',
|
||||
color: 'rgba(255, 255, 255, 0.75)',
|
||||
},
|
||||
});
|
||||
|
||||
const Setting = ({ settingCategory }) => {
|
||||
|
||||
@@ -204,7 +204,12 @@ const Settings = ({ userPreferences }) => {
|
||||
|
||||
return (
|
||||
<div className="menuContainer Settings" style={{ transition: 'background 0ms', textAlign: 'center' }}>
|
||||
<NavPanel paneExpandedLength={150} defaultIsPanelExpanded sidePanelBackground={theme.palette.secondary.dark}>
|
||||
<NavPanel
|
||||
paneExpandedLength={150}
|
||||
defaultIsPanelExpanded
|
||||
sidePanelBackground={theme.palette.secondary.dark}
|
||||
subPanel
|
||||
>
|
||||
{renderItems()}
|
||||
</NavPanel>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import config from '../../../../shared/config';
|
||||
|
||||
const Cover = ({ url, width, height, paddingBottom, noFade }) => {
|
||||
const Cover = ({ url, width, height, paddingBottom, noFade, canLoad = true }) => {
|
||||
const [loaded, isLoaded] = useState(false);
|
||||
const cover = new Image();
|
||||
cover.onload = () => isLoaded(url);
|
||||
|
||||
const coverRef = useRef();
|
||||
useEffect(() => {
|
||||
if (loaded !== url) cover.setAttribute('src', url);
|
||||
}, [url, loaded]);
|
||||
if (!coverRef.current && canLoad) {
|
||||
coverRef.current = new Image();
|
||||
coverRef.current.onload = () => isLoaded(true);
|
||||
}
|
||||
if (canLoad && !loaded) {
|
||||
coverRef.current.setAttribute('src', url);
|
||||
}
|
||||
}, [canLoad, coverRef.current]);
|
||||
|
||||
const style = {
|
||||
opacity: noFade ? 1 : loaded ? 1 : 0,
|
||||
@@ -20,13 +24,10 @@ const Cover = ({ url, width, height, paddingBottom, noFade }) => {
|
||||
backgroundPosition: 'center center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'cover',
|
||||
backgroundImage: `url('${url}')`,
|
||||
backgroundImage: loaded && `url('${url}')`,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
};
|
||||
return <div className="cover" style={style} />;
|
||||
};
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return JSON.stringify(prevProps) === JSON.stringify(nextProps);
|
||||
};
|
||||
export default memo(Cover, areEqual);
|
||||
export default Cover;
|
||||
|
||||
@@ -24,16 +24,16 @@ const useStyle = createUseStyles({
|
||||
},
|
||||
});
|
||||
|
||||
const BeatmapSkeleton = ({ style }) => {
|
||||
const BeatmapSkeleton = ({ style, rowIndex }) => {
|
||||
const theme = useTheme();
|
||||
const classes = useStyle({ theme });
|
||||
return (
|
||||
<div style={style}>
|
||||
<div className={`${classes.beatmap} Beatmap`}>
|
||||
<Skeleton className={classes.cover} />
|
||||
<Skeleton className={classes.text} style={{ width: '9%' }} />
|
||||
<Skeleton className={classes.text} style={{ width: '16%' }} />
|
||||
<Skeleton className={classes.buttons} />
|
||||
<Skeleton className={classes.cover} delay={rowIndex} />
|
||||
<Skeleton className={classes.text} style={{ width: '9%' }} delay={rowIndex} />
|
||||
<Skeleton className={classes.text} style={{ width: '16%' }} delay={rowIndex} />
|
||||
<Skeleton className={classes.buttons} delay={rowIndex} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ const useStyles = createUseStyles({
|
||||
background: ({ theme }) => theme.palette.primary.main,
|
||||
margin: ({ margin }) => margin || '1.3vh auto',
|
||||
paddingBottom: '10px',
|
||||
borderRadius: '5px',
|
||||
transition: ({ bpm }) => `opacity ${bpmToBps(bpm)}s`,
|
||||
'&:hover': {
|
||||
opacity: 1.1,
|
||||
@@ -55,14 +56,14 @@ const useStyles = createUseStyles({
|
||||
'@keyframes pulse': {
|
||||
'0%': {
|
||||
opacity: 0.1,
|
||||
transform: 'scale(.995)',
|
||||
transform: 'scale(.990)',
|
||||
},
|
||||
'60%': {
|
||||
transform: 'scale(1.000)',
|
||||
transform: 'scale(0.995)',
|
||||
opacity: 0.65,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'scale(1.015)',
|
||||
transform: 'scale(1.010)',
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
|
||||
+10
-1
@@ -1,18 +1,27 @@
|
||||
import React from 'react';
|
||||
import { createUseStyles, useTheme } from 'react-jss';
|
||||
import config from '../../../../shared/config';
|
||||
import { getDragRegion } from '../../../helpers/css.utils';
|
||||
|
||||
const useStyle = createUseStyles({
|
||||
header: {
|
||||
position: 'relative',
|
||||
...getDragRegion(false, true),
|
||||
position: 'absolute',
|
||||
height: '48px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: '15px',
|
||||
textTransform: 'uppercase',
|
||||
padding: '0px 24px',
|
||||
marginLeft: '48px',
|
||||
overflow: 'hidden',
|
||||
cursor: 'default',
|
||||
userSelect: 'none',
|
||||
backdropFilter: 'saturate(180%) blur(5px)',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
borderBottom: 'inset 1px hsla(0,0%,100%,0.1)',
|
||||
zIndex: 300,
|
||||
width: `calc(100% - ${config.display.sidePanelCompactedLength * 2}px)`,
|
||||
color: ({ theme }) => (theme.dark ? '#fff' : '#000'),
|
||||
},
|
||||
divider: {
|
||||
@@ -5,7 +5,7 @@ import config from '../../../../shared/config';
|
||||
|
||||
const useStyle = createUseStyles({
|
||||
contentContainer: {
|
||||
height: `calc(100vh - ${config.display.titleBarHeight}px)`,
|
||||
height: `100vh`,
|
||||
position: 'relative',
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
@@ -14,10 +14,12 @@ const useStyle = createUseStyles({
|
||||
flexDirection: 'column',
|
||||
},
|
||||
contentWrapper: {
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flex: '1 1 0%',
|
||||
flexDirection: 'column',
|
||||
padding: '0px',
|
||||
paddingLeft: `${config.display.sidePanelCompactedLength}px`,
|
||||
background: ({ theme }) => theme.palette.primary.dark,
|
||||
textAlign: 'center',
|
||||
fontSize: 'calc(10px + 2vmin)',
|
||||
@@ -25,15 +27,14 @@ const useStyle = createUseStyles({
|
||||
backgroundColor: ({ theme }) => theme.palette.primary.dark,
|
||||
textRendering: 'optimizelegibility',
|
||||
fontFamily: 'Open Sans, sans - serif',
|
||||
height: `calc(100vh - ${config.display.titleBarHeight + config.display.topBarHeight}px)`,
|
||||
width: `calc(100vw - ${config.display.sidePanelCompactedLength}px)`, // TODO Ugly when sidePanel expends but will be fixed in next revamp
|
||||
width: `calc(100vw - ${config.display.sidePanelCompactedLength}px)`,
|
||||
overflow: 'auto',
|
||||
'&, & *': {
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: ({ theme }) => theme.palette.primary.main,
|
||||
background: 'transparent',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: ({ theme }) => theme.palette.primary.accent,
|
||||
@@ -45,7 +46,7 @@ const useStyle = createUseStyles({
|
||||
const Item = ({ title, children, header }) => {
|
||||
const [headerContent, setHeaderContent] = useState(null);
|
||||
const theme = useTheme();
|
||||
const classes = useStyle({ theme });
|
||||
const classes = useStyle({ theme, hasHeader: !!header });
|
||||
|
||||
return header ? (
|
||||
<div className={classes.contentContainer}>
|
||||
@@ -99,7 +99,7 @@ const PlayOsu = ({ onSelect, osuGamePath, ...otherProps }) => {
|
||||
|
||||
return (
|
||||
<a data-radium="true" className={classes.a} onClick={launchOsu} role="tab">
|
||||
<span className={`${classes.tooltiptext} tooltiptext`}>{osuIsRunning ? 'Playing Osu!' : 'Play Osu!'}</span>
|
||||
<span className={`${classes.tooltiptext} tooltiptext`}>{osuIsRunning ? 'Playing !' : 'Play!'}</span>
|
||||
<span data-radium="true" className={classes.span}>
|
||||
<div className={`${classes.indicator} indicator`} />
|
||||
<div data-radium="true" className={classes.i}>
|
||||
@@ -110,7 +110,7 @@ const PlayOsu = ({ onSelect, osuGamePath, ...otherProps }) => {
|
||||
})}
|
||||
</div>
|
||||
<span data-radium="true" className={classes.title}>
|
||||
{osuIsRunning ? 'Playing Osu!' : 'Play Osu!'}
|
||||
{osuIsRunning ? 'Playing !' : 'Play !'}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -9,6 +9,10 @@ const useStyle = createUseStyles({
|
||||
height: '44px',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
color: 'rgb(255, 255, 255)',
|
||||
letterSpacing: '0.4pt',
|
||||
fontSize: '15px',
|
||||
padding: '0px 16px',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
},
|
||||
@@ -19,16 +23,6 @@ const useStyle = createUseStyles({
|
||||
visibility: props => (props.expended ? 'hidden' : 'visible'),
|
||||
},
|
||||
},
|
||||
span: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'rgb(255, 255, 255)',
|
||||
fontSize: '15px',
|
||||
letterSpacing: '0.4pt',
|
||||
padding: '0px 16px',
|
||||
transition: 'transform 0.1s ease-in 0s',
|
||||
userSelect: 'none',
|
||||
},
|
||||
i: {
|
||||
marginRight: '8px',
|
||||
height: '44px',
|
||||
@@ -47,18 +41,6 @@ const useStyle = createUseStyles({
|
||||
title: {
|
||||
visibility: props => (props.expended ? 'visible' : 'hidden'),
|
||||
},
|
||||
tooltiptext: {
|
||||
visibility: 'hidden',
|
||||
width: '120px',
|
||||
backgroundColor: ({ theme }) => theme.palette.primary.main,
|
||||
color: '#fff',
|
||||
textAlign: 'center',
|
||||
padding: '5px 0',
|
||||
borderRadius: '6px',
|
||||
position: 'absolute',
|
||||
left: '105%',
|
||||
zIndex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const Tab = ({ icon, title, onSelect, ...otherProps }) => {
|
||||
@@ -66,15 +48,12 @@ const Tab = ({ icon, title, onSelect, ...otherProps }) => {
|
||||
const classes = useStyle({ ...otherProps, theme });
|
||||
return (
|
||||
<a data-radium="true" className={classes.a} onClick={onSelect} role="tab">
|
||||
<span className={`${classes.tooltiptext} tooltiptext`}>{title}</span>
|
||||
<span data-radium="true" className={classes.span}>
|
||||
<div className={`${classes.indicator} indicator`} />
|
||||
<i data-radium="true" className={classes.i}>
|
||||
{icon}
|
||||
</i>
|
||||
<span data-radium="true" className={classes.title}>
|
||||
{title}
|
||||
</span>
|
||||
<div className={`${classes.indicator} indicator`} />
|
||||
<i data-radium="true" className={classes.i}>
|
||||
{icon}
|
||||
</i>
|
||||
<span data-radium="true" className={classes.title}>
|
||||
{title}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -5,20 +5,29 @@ import VolumeControl from './VolumeControl';
|
||||
import TasksControl from './TasksControl';
|
||||
import PlayOsu from './PlayOsu';
|
||||
import config from '../../../../../shared/config';
|
||||
import { getDragRegion } from '../../../../helpers/css.utils';
|
||||
|
||||
const styles = {
|
||||
SidePanel: {
|
||||
...getDragRegion(),
|
||||
cursor: 'default',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
position: ({ subPanel }) => (subPanel ? 'relative' : 'absolute'),
|
||||
marginTop: ({ subPanel }) => subPanel && '48px',
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexDirection: 'column',
|
||||
overflow: 'visible',
|
||||
width: props => (props.expended ? props.panelExpandedLength : props.panelCompactedLength),
|
||||
backgroundColor: props => props.background,
|
||||
height: ({ subPanel }) => (subPanel ? `calc(100vh - ${config.display.topBarHeight}px)` : '100%'),
|
||||
transition: `width ${config.display.defaultTransitionDuration}`,
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
zIndex: ({ subPanel }) => (subPanel ? 2000 : 3000),
|
||||
backdropFilter: 'saturate(180%) blur(5px)',
|
||||
borderRight: 'inset 1px hsla(0,0%,100%,0.1)',
|
||||
},
|
||||
head: {
|
||||
height: config.display.topBarHeight,
|
||||
@@ -34,15 +43,19 @@ const styles = {
|
||||
},
|
||||
};
|
||||
|
||||
const SidePanel = ({ classes, items, expended, expendable, volume, tasks, setExpended }) => {
|
||||
const SidePanel = ({ classes, items, expended, expendable, tasks, setExpended, subPanel }) => {
|
||||
const itemTab = () =>
|
||||
items.map((item, i) => {
|
||||
if (items.length - i === 1) {
|
||||
return (
|
||||
<>
|
||||
{tasks && <TasksControl expended={expended} tasks={tasks} />}
|
||||
{volume && <PlayOsu expended={expended} />}
|
||||
{volume && <VolumeControl expended={expended} />}
|
||||
{!subPanel && (
|
||||
<>
|
||||
<TasksControl expended={expended} tasks={tasks} />
|
||||
<PlayOsu expended={expended} />
|
||||
<VolumeControl expended={expended} />
|
||||
</>
|
||||
)}
|
||||
<Tab {...item.props} expended={expended} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,11 +23,13 @@ const NavPanel = ({
|
||||
sidePanelBackground,
|
||||
children,
|
||||
onExpended,
|
||||
subPanel,
|
||||
}) => {
|
||||
const [isExpended, setIsExpended] = useState(defaultIsPanelExpanded);
|
||||
return (
|
||||
<div className={classes.NavPanel}>
|
||||
<SidePanel
|
||||
subPanel={subPanel}
|
||||
items={children}
|
||||
expended={isExpended}
|
||||
setExpended={expended => {
|
||||
@@ -3,13 +3,13 @@ import injectSheet from 'react-jss';
|
||||
|
||||
const styles = {
|
||||
'@keyframes color': {
|
||||
'0%': {
|
||||
'10%': {
|
||||
backgroundColor: '#5c5c5c',
|
||||
},
|
||||
'50%': {
|
||||
backgroundColor: '#7d7d7d',
|
||||
},
|
||||
'100': {
|
||||
'90': {
|
||||
backgroundColor: '#5c5c5c',
|
||||
},
|
||||
},
|
||||
@@ -19,8 +19,9 @@ const styles = {
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
animationName: '$color',
|
||||
animationDuration: '2s',
|
||||
animationDuration: '2.4s',
|
||||
animationIterationCount: 'infinite',
|
||||
animationDelay: props => `0.${props.delay}s`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
open Css;
|
||||
let makeWrapperStyle = (~height as h) =>
|
||||
let makeWrapperStyle = () =>
|
||||
style([
|
||||
unsafe("WebkitAppRegion", "drag"),
|
||||
backgroundColor(hex("171717")),
|
||||
zIndex(100),
|
||||
zIndex(500),
|
||||
position(fixed),
|
||||
height(px(h)),
|
||||
width(pct(100.)),
|
||||
right(zero),
|
||||
display(`flex),
|
||||
alignItems(center),
|
||||
]);
|
||||
|
||||
let makeTitleStyle = () =>
|
||||
style([
|
||||
textOverflow(`ellipsis),
|
||||
whiteSpace(`nowrap),
|
||||
overflow(`hidden),
|
||||
maxWidth(pct(40.)),
|
||||
paddingLeft(px(12)),
|
||||
fontSize(px(12)),
|
||||
color(white),
|
||||
flex3(~grow=1., ~shrink=1., ~basis=zero),
|
||||
]);
|
||||
|
||||
let makeControlStyle = (~bgColor=?, ~spacer=false, ()) =>
|
||||
style([
|
||||
unsafe("WebkitAppRegion", "no-drag"),
|
||||
@@ -32,7 +17,7 @@ let makeControlStyle = (~bgColor=?, ~spacer=false, ()) =>
|
||||
[
|
||||
backgroundColor(
|
||||
switch (bgColor) {
|
||||
| None => rgba(255, 255, 255, 0.15)
|
||||
| None => rgba(255, 255, 255, 0.25)
|
||||
| Some(c) => c
|
||||
},
|
||||
),
|
||||
@@ -59,19 +44,18 @@ let make = (~height: int) => {
|
||||
BrowserWindow.isMaximized(window)
|
||||
? BrowserWindow.unmaximize(window) : BrowserWindow.maximize(window);
|
||||
|
||||
<div className={makeWrapperStyle(~height)}>
|
||||
<div className={makeTitleStyle()}> title->React.string </div>
|
||||
<div className={makeWrapperStyle()}>
|
||||
<div
|
||||
className={makeControlStyle(~spacer=true, ())} onClick=onMinimizeClick>
|
||||
{Icon.make(~name="Dash", ~width=12, ~height=12, ())}
|
||||
{Icon.make(~name="Dash", ~width=height, ~height, ())}
|
||||
</div>
|
||||
<div className={makeControlStyle()} onClick=onMaximizeClick>
|
||||
{Icon.make(~name="Square", ~width=12, ~height=12, ())}
|
||||
{Icon.make(~name="Square", ~width=height, ~height, ())}
|
||||
</div>
|
||||
<div className={makeControlStyle(~bgColor=red, ())} onClick=onCloseClick>
|
||||
{Icon.make(~name="Cancel", ~width=12, ~height=12, ())}
|
||||
{Icon.make(~name="Cancel", ~width=height, ~height, ())}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
let default = make;
|
||||
let default = make;
|
||||
|
||||
@@ -26,7 +26,7 @@ const makeMainWindowSettings = () => {
|
||||
darkTheme: true,
|
||||
// eslint-disable-next-line no-unneeded-ternary
|
||||
frame: process.env.ELECTRON_START_URL ? true : false,
|
||||
backgroundColor: '#121212',
|
||||
backgroundColor: '#000',
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
webSecurity: false,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
type t;
|
||||
type callback('a) = 'a => unit;
|
||||
|
||||
type target = {readyState: int};
|
||||
|
||||
type partialAudioEvent = {target};
|
||||
|
||||
[@bs.new] external make: unit => t = "Audio";
|
||||
[@bs.new] external makeWithSrc: (~src: string) => t = "Audio";
|
||||
|
||||
@@ -9,7 +13,13 @@ type callback('a) = 'a => unit;
|
||||
[@bs.set] external setVolume: (t, float) => unit = "volume";
|
||||
[@bs.send] external pause: t => unit = "pause";
|
||||
[@bs.send] external play: t => unit = "play";
|
||||
[@bs.set] external onended: (t, callback(Dom.event)) => unit = "onended";
|
||||
[@bs.set] external onerror: (t, callback(Dom.event)) => unit = "onerror";
|
||||
[@bs.set] external onpause: (t, callback(Dom.event)) => unit = "onpause";
|
||||
[@bs.set] external onplay: (t, callback(Dom.event)) => unit = "onplay";
|
||||
[@bs.set]
|
||||
external onended: (t, callback(partialAudioEvent)) => unit = "onended";
|
||||
[@bs.set]
|
||||
external onerror: (t, callback(partialAudioEvent)) => unit = "onerror";
|
||||
[@bs.set]
|
||||
external onpause: (t, callback(partialAudioEvent)) => unit = "onpause";
|
||||
[@bs.set]
|
||||
external onplay: (t, callback(partialAudioEvent)) => unit = "onplay";
|
||||
[@bs.set]
|
||||
external oncanplay: (t, callback(partialAudioEvent)) => unit = "oncanplay";
|
||||
|
||||
@@ -12,7 +12,7 @@ export default {
|
||||
topBarHeight: 48,
|
||||
sidePanelCompactedLength: 48,
|
||||
sidePanelExpandedLength: 150,
|
||||
titleBarHeight: 23,
|
||||
titleBarHeight: 12,
|
||||
defaultTransitionDuration: '200ms',
|
||||
defaultAccentColor: '#17b04c',
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { createStore, combineReducers } from 'redux';
|
||||
import bot from '../Bot/reducer';
|
||||
import settings from '../App/modules/Settings/reducer/reducer';
|
||||
import settings from '../App/modules/Settings/reducer/reducer'; // TODO Dependency cycle
|
||||
import beatmaps from '../App/modules/Beatmaps/reducer/reducer';
|
||||
import navigation from '../App/modules/reducer';
|
||||
import packs from '../App/modules/Packs/reducer/reducer';
|
||||
|
||||
Reference in New Issue
Block a user