Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#12 Add View for song sections
Browse files Browse the repository at this point in the history
tscz committed Dec 28, 2019
1 parent 54ea631 commit 5aabebe
Showing 15 changed files with 333 additions and 135 deletions.
4 changes: 2 additions & 2 deletions src/components/contentLayout/contentLayout.tsx
Original file line number Diff line number Diff line change
@@ -11,10 +11,10 @@ class ContentLayout extends React.Component<Props> {
render() {
return (
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={10} style={{ minHeight: "400px" }}>
<Grid item xs={9} style={{ minHeight: "400px" }}>
{this.props.topLeft}
</Grid>
<Grid item xs={2}>
<Grid item xs={3}>
{this.props.topRight}
</Grid>
<Grid item xs={12}>
4 changes: 0 additions & 4 deletions src/components/view/view.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import CardHeader from "@material-ui/core/CardHeader";
import React, { Component, ReactElement } from "react";

interface Props {
header: ReactElement | String;
body: ReactElement;
actions?: ReactElement;
}

class View extends Component<Props> {
render() {
return (
<Card style={{ height: "100%" }}>
<CardHeader action={this.props.actions} title={this.props.header} />
<CardContent>{this.props.body}</CardContent>
</Card>
);
8 changes: 6 additions & 2 deletions src/pages/default/defaultPage.tsx
Original file line number Diff line number Diff line change
@@ -6,8 +6,12 @@ class DefaultPage extends React.Component {
render() {
return (
<View
header="No Transcription started"
body={<p>Please create a new Transcription or open an existing one.</p>}
body={
<>
<p>No Transcription started!!</p>
<p>Please create a new Transcription or open an existing one.</p>
</>
}
></View>
);
}
1 change: 0 additions & 1 deletion src/pages/drum/drumPage.tsx
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@ class DrumPage extends React.Component {
render() {
return (
<View
header="Drums"
body={
<p>
In this section you'll be able to transcribe the drum part. Please
1 change: 0 additions & 1 deletion src/pages/guitar/guitarPage.tsx
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@ class GuitarPage extends React.Component {
render() {
return (
<View
header="Strumming"
body={
<p>
In this section you'll be able to transcribe the guitar part. Please
1 change: 0 additions & 1 deletion src/pages/harmony/harmonyPage.tsx
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@ class HarmonyPage extends React.Component {
render() {
return (
<View
header="Harmony"
body={
<p>
In this section you'll be able to transcribe the harmony. Please
1 change: 0 additions & 1 deletion src/pages/print/printPage.tsx
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@ class PrintPage extends React.Component {
render() {
return (
<View
header="Print"
body={
<p>
In this section you'll be able to print the leadsheet. Please come
116 changes: 8 additions & 108 deletions src/pages/structure/structurePage.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,18 @@
import { MenuItem, Select, Tooltip } from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import LoopIcon from "@material-ui/icons/Loop";
import PauseIcon from "@material-ui/icons/Pause";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import TimerIcon from "@material-ui/icons/Timer";
import ZoomInIcon from "@material-ui/icons/ZoomIn";
import ZoomOutIcon from "@material-ui/icons/ZoomOut";
import React, { ReactElement } from "react";
import React from "react";
import { connect } from "react-redux";

import ContentLayout from "../../components/contentLayout/contentLayout";
import View from "../../components/view/view";
import {
addedSection,
Section,
updatedRhythm
} from "../../states/analysisSlice";
import { triggeredPause, triggeredPlay } from "../../states/audioSlice";
import { ApplicationState } from "../../states/store";
import { zoomedIn, zoomedOut } from "../../states/waveSlice";
import StructureView from "../../views/structure/structureView";
import WaveContainer from "../../views/wave/waveContainer";
import WaveControlView from "../../views/waveControl/waveControlView";

interface PropsFromState {
sections: Section[];
zoom: number;
isPlaying: boolean;
loaded: boolean;
}

interface PropsFromDispatch {
addedSection: typeof addedSection;
zoomedIn: typeof zoomedIn;
zoomedOut: typeof zoomedOut;
updatedRhythm: typeof updatedRhythm;
triggeredPlay: typeof triggeredPlay;
triggeredPause: typeof triggeredPause;
}
interface PropsFromDispatch {}

interface Props {}

@@ -48,98 +24,22 @@ class StructurePage extends React.Component<AllProps> {
return (
<ContentLayout
topLeft={
<View
header="Waveform"
actions={
<>
{this.props.isPlaying ? (
<WaveformControlButton
title="Pause"
icon={<PauseIcon />}
onClick={this.props.triggeredPause}
/>
) : (
<WaveformControlButton
title="Play"
icon={<PlayArrowIcon />}
onClick={this.props.triggeredPlay}
/>
)}
<MeasureSwitch id="startMeasure" />
<MeasureSwitch id="endMeasure" />

<WaveformControlButton
title="Zoom in"
icon={<ZoomInIcon />}
onClick={this.props.zoomedIn}
/>
<WaveformControlButton
title="Zoom out"
icon={<ZoomOutIcon />}
onClick={this.props.zoomedOut}
/>
<WaveformControlButton title="Loop" icon={<LoopIcon />} />
<WaveformControlButton title="Metronome" icon={<TimerIcon />} />
</>
}
body={this.props.loaded ? <WaveContainer /> : <></>}
></View>
<View body={this.props.loaded ? <WaveContainer /> : <></>}></View>
}
topRight={
<View
header={<>Properties</>}
body={this.props.loaded ? <WaveControlView /> : <></>}
></View>
<View body={this.props.loaded ? <WaveControlView /> : <></>}></View>
}
bottom={<View body={<StructureView />}></View>}
></ContentLayout>
);
}
}

const WaveformControlButton = (props: {
title: string;
icon: ReactElement;
onClick?: () => void;
}) => {
return (
<Tooltip title={props.title}>
<IconButton onClick={props.onClick}>{props.icon}</IconButton>
</Tooltip>
);
};

const MeasureSwitch = (props: { id: string }) => {
return (
<Select id={props.id} value={0}>
<MenuItem value={0}>0</MenuItem>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
<MenuItem value={3}>3</MenuItem>
</Select>
);
};

const mapStateToProps = ({
analysis,
audio,
project,
wave
}: ApplicationState) => {
const mapStateToProps = ({ project }: ApplicationState) => {
return {
sections: analysis.sections,
zoom: wave.zoom,
status: audio.status,
isPlaying: audio.isPlaying,
loaded: project.loaded
};
};

const mapDispatchToProps = {
addedSection,
zoomedIn,
zoomedOut,
updatedRhythm,
triggeredPlay,
triggeredPause
};
const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(StructurePage);
38 changes: 38 additions & 0 deletions src/states/analysisSlice.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import reducer, {
addedSection,
AnalysisState,
initialAnalysisState,
removedSection,
resettedAnalysis,
SectionType,
TimeSignatureType,
updatedRhythm,
updatedSource
@@ -116,3 +119,38 @@ it("can update the audio source", () => {
expect(state.audioLength).toEqual(42);
expect(state.audioSampleRate).toEqual(44400);
});

it("can add a section", () => {
let state: AnalysisState = reducer(
undefined,
addedSection({
id: "section",
startTime: 0,
endTime: 10,
type: SectionType.BRIDGE
})
);
expect(state.sections).toContainEqual({
id: "section",
startTime: 0,
endTime: 10,
type: SectionType.BRIDGE
});
});

it("can remove a section", () => {
let state: AnalysisState = reducer(
undefined,
addedSection({
id: "section",
startTime: 0,
endTime: 10,
type: SectionType.BRIDGE
})
);
expect(state.sections.length).toBe(1);

let state2 = reducer(state, removedSection("section"));

expect(state2.sections.length).toBe(0);
});
11 changes: 7 additions & 4 deletions src/states/analysisSlice.ts
Original file line number Diff line number Diff line change
@@ -56,13 +56,16 @@ const analysisSlice = createSlice({
initialState: initialAnalysisState,
reducers: {
addedSection(state, action: PayloadAction<Section>) {
//TODO
state.sections.push(action.payload);
},
updatedSection(state, action: PayloadAction<Section>) {
//TODO
state.sections.filter(section => section.id !== action.payload.id);
state.sections.push(action.payload);
},
removedSection(state, action: PayloadAction<Section>) {
//TODO
removedSection(state, action: PayloadAction<string>) {
state.sections = state.sections.filter(
section => section.id !== action.payload
);
},
resettedAnalysis(state, action: PayloadAction<{ state?: PersistedState }>) {
if (action.payload.state?.analysis) {
5 changes: 3 additions & 2 deletions src/states/audioSlice.ts
Original file line number Diff line number Diff line change
@@ -43,8 +43,9 @@ const audioSlice = createSlice({
state,
action: PayloadAction<{ playbackRate?: number; detune?: number }>
) {
if (action.payload.detune) state.detune = action.payload.detune;
if (action.payload.playbackRate)
if (action.payload.detune !== undefined)
state.detune = action.payload.detune;
if (action.payload.playbackRate !== undefined)
state.playbackRate = action.payload.playbackRate;
}
}
50 changes: 50 additions & 0 deletions src/views/structure/structureView.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { CssBaseline, ThemeProvider } from "@material-ui/core";
import React from "react";
import { Provider } from "react-redux";

import { addedSection, SectionType } from "../../states/analysisSlice";
import store from "../../states/store";
import theme from "../../styles/theme";
import StructureView from "./structureView";

export default {
title: "Views|StructureView",
component: StructureView
};

export const Default = () => {
store.dispatch(
addedSection({
type: SectionType.INTRO,
startTime: 0,
endTime: 1,
id: "intro"
})
);
store.dispatch(
addedSection({
type: SectionType.VERSE,
startTime: 1,
endTime: 2,
id: "verse"
})
);
store.dispatch(
addedSection({
type: SectionType.CHORUS,
startTime: 2,
endTime: 3,
id: "chorus"
})
);

return (
<Provider store={store}>
<ThemeProvider theme={theme}>
<CssBaseline />

<StructureView></StructureView>
</ThemeProvider>
</Provider>
);
};
8 changes: 8 additions & 0 deletions src/views/structure/structureView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from "react";

import TestEnvironment from "../../tests/TestEnvironment";
import StructureView from "./structureView";

it("renders without crashing", () => {
TestEnvironment.smokeTest(<StructureView />);
});
112 changes: 112 additions & 0 deletions src/views/structure/structureView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
IconButton,
Table,
TableBody,
TableCell,
TableHead,
TableRow
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import RemoveIcon from "@material-ui/icons/Remove";
import React, { Component } from "react";
import { connect } from "react-redux";

import {
addedSection,
removedSection,
Section,
SectionType,
updatedSection
} from "../../states/analysisSlice";
import { ApplicationState } from "../../states/store";

interface PropsFromState {
readonly sections: Section[];
}

interface PropsFromDispatch {
addedSection: typeof addedSection;
updatedSection: typeof updatedSection;
removedSection: typeof removedSection;
}

type AllProps = PropsFromState & PropsFromDispatch;

class StructureView extends Component<AllProps> {
handleAddSection = (section: Section) => {
this.props.addedSection(section);
};

handleRemoveSection = (id: string) => {
this.props.removedSection(id);
};

render() {
return (
<Table size="small">
<TableHead>
<TableRow>
<TableCell style={{ width: "10%" }}></TableCell>
<TableCell style={{ width: "30%" }}>Section</TableCell>
<TableCell style={{ width: "30%" }}>First Measure</TableCell>
<TableCell style={{ width: "30%" }}>Last Measure</TableCell>
</TableRow>
</TableHead>
<TableBody>
{this.props.sections.map(section => (
<TableRow key={section.id}>
<TableCell component="th" scope="row">
<IconButton
onClick={() => this.handleRemoveSection(section.id!)}
>
<RemoveIcon></RemoveIcon>
</IconButton>
</TableCell>
<TableCell>{section.type}</TableCell>
<TableCell>{section.startTime}</TableCell>
<TableCell>{section.endTime}</TableCell>
</TableRow>
))}
<TableRow key="last">
<TableCell component="th" scope="row">
<IconButton
onClick={() => {
this.handleAddSection({
endTime: Math.random(),
startTime: 34,
type: SectionType.OUTRO,
id:
Math.random()
.toString(36)
.substring(2, 15) +
Math.random()
.toString(36)
.substring(2, 15)
});
}}
>
<AddIcon></AddIcon>
</IconButton>
</TableCell>
<TableCell />
<TableCell />
<TableCell />
</TableRow>
</TableBody>
</Table>
);
}
}

const mapStateToProps = ({ analysis }: ApplicationState) => {
return {
sections: analysis.sections
};
};

const mapDispatchToProps = {
addedSection,
updatedSection,
removedSection
};
export default connect(mapStateToProps, mapDispatchToProps)(StructureView);
108 changes: 99 additions & 9 deletions src/views/waveControl/waveControlView.tsx
Original file line number Diff line number Diff line change
@@ -3,21 +3,35 @@ import {
FormControl,
Input,
InputLabel,
MenuItem,
NativeSelect,
Select,
Tooltip
} from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import IconButton from "@material-ui/core/IconButton";
import InputAdornment from "@material-ui/core/InputAdornment";
import Slider from "@material-ui/core/Slider";
import LoopIcon from "@material-ui/icons/Loop";
import PauseIcon from "@material-ui/icons/Pause";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import SyncAltIcon from "@material-ui/icons/SyncAlt";
import TimerIcon from "@material-ui/icons/Timer";
import ZoomInIcon from "@material-ui/icons/ZoomIn";
import ZoomOutIcon from "@material-ui/icons/ZoomOut";
import ToggleButton from "@material-ui/lab/ToggleButton";
import React, { Component } from "react";
import React, { Component, ReactElement } from "react";
import { connect } from "react-redux";

import { TimeSignatureType, updatedRhythm } from "../../states/analysisSlice";
import { updatedPlaybackSettings } from "../../states/audioSlice";
import {
triggeredPause,
triggeredPlay,
updatedPlaybackSettings
} from "../../states/audioSlice";
import { enabledSyncFirstMeasureStart } from "../../states/projectSlice";
import { ApplicationState } from "../../states/store";
import { zoomedIn, zoomedOut } from "../../states/waveSlice";

interface PropsFromState {
readonly firstMeasureStart: number;
@@ -26,12 +40,18 @@ interface PropsFromState {
readonly syncFirstMeasureStart: boolean;
readonly detune: number;
readonly playbackRate: number;
readonly zoom: number;
readonly isPlaying: boolean;
}

interface PropsFromDispatch {
updatedRhythm: typeof updatedRhythm;
enabledSyncFirstMeasureStart: typeof enabledSyncFirstMeasureStart;
updatedPlaybackSettings: typeof updatedPlaybackSettings;
zoomedIn: typeof zoomedIn;
zoomedOut: typeof zoomedOut;
triggeredPlay: typeof triggeredPlay;
triggeredPause: typeof triggeredPause;
}

type AllProps = PropsFromState & PropsFromDispatch;
@@ -48,13 +68,16 @@ class WaveControlView extends Component<AllProps> {
};

handleDetuneChange = (e: any, detune: number | number[]) => {
if (!Array.isArray(detune)) {
if (!Array.isArray(detune) && detune !== this.props.detune) {
this.props.updatedPlaybackSettings({ detune: detune });
}
};

handlePlaybackRateChange = (e: any, playbackRate: number | number[]) => {
if (!Array.isArray(playbackRate)) {
if (
!Array.isArray(playbackRate) &&
playbackRate !== this.props.playbackRate
) {
this.props.updatedPlaybackSettings({ playbackRate: playbackRate });
}
};
@@ -64,7 +87,38 @@ class WaveControlView extends Component<AllProps> {
<>
<Grid container direction="column" spacing={3}>
<Grid item>
<FormControl>
<InputLabel shrink>Actions</InputLabel>
{this.props.isPlaying ? (
<WaveformControlButton
title="Pause"
icon={<PauseIcon />}
onClick={this.props.triggeredPause}
/>
) : (
<WaveformControlButton
title="Play"
icon={<PlayArrowIcon />}
onClick={this.props.triggeredPlay}
/>
)}
<MeasureSwitch id="startMeasure" />
<MeasureSwitch id="endMeasure" />

<WaveformControlButton
title="Zoom in"
icon={<ZoomInIcon />}
onClick={this.props.zoomedIn}
/>
<WaveformControlButton
title="Zoom out"
icon={<ZoomOutIcon />}
onClick={this.props.zoomedOut}
/>
<WaveformControlButton title="Loop" icon={<LoopIcon />} />
<WaveformControlButton title="Metronome" icon={<TimerIcon />} />
</Grid>
<Grid item>
<FormControl fullWidth={true}>
<InputLabel>Start Measure 1</InputLabel>
<Input
type="text"
@@ -102,7 +156,7 @@ class WaveControlView extends Component<AllProps> {
></SliderInput>
</Grid>
<Grid item>
<FormControl style={{ width: "100%" }}>
<FormControl fullWidth={true}>
<InputLabel>Time Signature</InputLabel>
<NativeSelect
value={this.props.timeSignature}
@@ -139,6 +193,29 @@ class WaveControlView extends Component<AllProps> {
}
}

const WaveformControlButton = (props: {
title: string;
icon: ReactElement;
onClick?: () => void;
}) => {
return (
<Tooltip title={props.title}>
<IconButton onClick={props.onClick}>{props.icon}</IconButton>
</Tooltip>
);
};

const MeasureSwitch = (props: { id: string }) => {
return (
<Select id={props.id} value={0}>
<MenuItem value={0}>0</MenuItem>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
<MenuItem value={3}>3</MenuItem>
</Select>
);
};

const SliderInput = (props: {
title: string;
min: number;
@@ -164,20 +241,33 @@ const SliderInput = (props: {
);
};

const mapStateToProps = ({ project, analysis, audio }: ApplicationState) => {
const mapStateToProps = ({
project,
analysis,
audio,
wave
}: ApplicationState) => {
return {
firstMeasureStart: analysis.firstMeasureStart,
bpm: analysis.bpm,
timeSignature: analysis.timeSignature,
syncFirstMeasureStart: project.syncFirstMeasureStart,
detune: audio.detune,
playbackRate: audio.playbackRate
playbackRate: audio.playbackRate,
zoom: wave.zoom,
status: audio.status,
isPlaying: audio.isPlaying,
loaded: project.loaded
};
};

const mapDispatchToProps = {
updatedRhythm,
enabledSyncFirstMeasureStart,
updatedPlaybackSettings
updatedPlaybackSettings,
zoomedIn,
zoomedOut,
triggeredPlay,
triggeredPause
};
export default connect(mapStateToProps, mapDispatchToProps)(WaveControlView);

0 comments on commit 5aabebe

Please sign in to comment.