Skip to content

Commit

Permalink
Implement import/export of patterns, add pattern insert actions to st…
Browse files Browse the repository at this point in the history
…ate history
  • Loading branch information
Igor Zinken committed Apr 3, 2022
1 parent aaf7a49 commit 1126ac0
Show file tree
Hide file tree
Showing 18 changed files with 395 additions and 165 deletions.
169 changes: 96 additions & 73 deletions src/components/advanced-pattern-editor/advanced-pattern-editor.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2016-2021 - https://www.igorski.nl
* Igor Zinken 2016-2022 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
Expand All @@ -21,12 +21,17 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
<template>
<div id="advancedPatternEditor">
<div
class="advanced-pattern-editor"
@keyup.esc="handleClose()"
>
<div class="header">
<h2 v-t="'title'"></h2>
<button type="button"
class="close-button"
@click="handleClose">x</button>
<button
type="button"
class="close-button"
@click="handleClose"
>x</button>
</div>
<hr class="divider" />
<fieldset>
Expand All @@ -43,27 +48,35 @@
<input type="number" min="1" max="8" v-model.number="lastChannel">
</div>
</fieldset>
<button
v-t="'exportContent'"
type="button"
class="export-button"
@click="handleExportClick()"
></button>
<fieldset>
<div class="wrapper input">
<label v-t="'insertAfterLabel'"></label>
<input type="number" min="1" :max="maxPattern" v-model.number="pastePattern">
</div>
</fieldset>
<button v-t="'copyContent'"
type="button"
class="confirm-button"
@keyup.enter="handleConfirm"
@click="handleConfirm"
<button
v-t="'insertClonedContent'"
type="button"
class="confirm-button"
@keyup.enter="handleDuplicateClick()"
@click="handleDuplicateClick()"
></button>
</div>
</template>

<script>
import { mapState, mapMutations } from 'vuex';
import { mapState, mapMutations, mapActions } from "vuex";
import PatternFactory from '@/model/factories/pattern-factory';
import { clone } from '@/utils/object-util';
import messages from './messages.json';
import { PATTERN_FILE_EXTENSION } from "@/definitions/file-types";
import { saveAsFile } from "@/utils/file-util";
import { serializePatternFile } from "@/utils/pattern-util";
import messages from "./messages.json";
export default {
i18n: { messages },
Expand Down Expand Up @@ -91,77 +104,88 @@ export default {
this.lastChannel = this.activeSong.instruments.length;
this.pastePattern = this.maxPattern;
this.suspendKeyboardService(true);
this.suspendKeyboardService( true );
this.$nextTick(() => {
this.$refs.firstPatternInput.focus();
});
},
beforeDestroy() {
this.suspendKeyboardService(false);
this.suspendKeyboardService( false );
},
methods: {
...mapMutations([
'createLinkedList',
'replacePatterns',
'suspendKeyboardService',
"setLoading",
"showError",
"showNotification",
"suspendKeyboardService",
"unsetLoading",
]),
...mapActions([
"pastePatternsIntoSong",
]),
async handleExportClick() {
this.setLoading( "pexp" );
try {
const {
firstChannelValue, lastChannelValue,
firstPatternValue, lastPatternValue,
patternsToClone,
} = this.clonePatternRange();
// encode pattern range
const data = serializePatternFile( patternsToClone, firstChannelValue, lastChannelValue );
const name = `${this.activeSong.meta.title}_${firstPatternValue}-${lastPatternValue}_${firstChannelValue}-${lastChannelValue}`;
// download file to disk
saveAsFile(
`data:application/json;charset=utf-8, ${encodeURIComponent(data)}`, `${name}${PATTERN_FILE_EXTENSION}`
);
this.showNotification({ message: this.$t( "patternRangeExported" ) });
} catch {
this.showError( this.$t( "errorPatternRangeExport" ));
}
this.unsetLoading( "pexp" );
},
handleClose() {
this.$emit('close');
this.$emit( "close" );
},
async handleDuplicateClick() {
const patterns = this.activeSong.patterns;
const pastePatternValue = Math.min( patterns.length, this.pastePattern );
const {
firstChannelValue,
lastChannelValue,
patternsToClone,
} = this.clonePatternRange();
await this.pastePatternsIntoSong({
patterns : patternsToClone,
channelRange : [ firstChannelValue, lastChannelValue ],
insertIndex : pastePatternValue
});
this.handleClose();
},
handleConfirm() {
clonePatternRange() {
const patterns = this.activeSong.patterns;
const maxPatternValue = patterns.length;
const maxChannelValue = this.activeSong.instruments.length - 1;
const firstPatternValue = Math.min( maxPatternValue, this.firstPattern - 1 );
const lastPatternValue = Math.min( maxPatternValue, this.lastPattern - 1 );
const firstChannelValue = Math.min( maxChannelValue, this.firstChannel - 1 );
const lastChannelValue = Math.min( maxChannelValue, this.lastChannel - 1 );
const pastePatternValue = Math.min( maxPatternValue, this.pastePattern );
const patternsToClone = patterns.slice( firstPatternValue, lastPatternValue + 1 );
// splice the pattern list at the insertion point, head will contain
// the front of the list, tail the end of the list, and inserted will contain the cloned content
const patternsHead = clone(patterns);
const patternsTail = patternsHead.splice( pastePatternValue );
const patternsInserted = [];
// clone the patterns into the insertion list
patternsToClone.forEach(p => {
const clonedPattern = PatternFactory.create(p.steps);
const firstPatternValue = Math.min( patterns.length, this.firstPattern - 1 );
const lastPatternValue = Math.min( patterns.length, this.lastPattern - 1 );
for ( let i = firstChannelValue; i <= lastChannelValue; ++i )
clonedPattern.channels[ i ] = clone( p.channels[ i ]);
patternsInserted.push(clonedPattern);
});
// commit the changes
this.replacePatterns(patternsHead.concat(patternsInserted, patternsTail));
// update event offsets
for ( let patternIndex = pastePatternValue; patternIndex < this.activeSong.patterns.length; ++patternIndex ) {
this.activeSong.patterns[ patternIndex ].channels.forEach(channel => {
channel.forEach(event => {
if ( event && event.seq ) {
const eventStart = event.seq.startMeasure;
const eventEnd = event.seq.endMeasure;
const eventLength = isNaN( eventEnd ) ? 1 : eventEnd - eventStart;
const patternsToClone = patterns.slice( firstPatternValue, lastPatternValue + 1 );
event.seq.startMeasure = patternIndex;
event.seq.endMeasure = event.seq.startMeasure + eventLength;
}
});
});
}
this.createLinkedList(this.activeSong);
this.handleClose();
return {
firstPatternValue,
lastPatternValue,
firstChannelValue,
lastChannelValue,
patternsToClone,
};
},
},
};
Expand All @@ -172,9 +196,9 @@ export default {
@import "@/styles/forms";
$width: 450px;
$height: 355px;
$height: 410px;
#advancedPatternEditor {
.advanced-pattern-editor {
@include editorComponent();
@include overlay();
@include noSelect();
Expand Down Expand Up @@ -207,12 +231,6 @@ $height: 355px;
margin: 0;
}
fieldset {
h2 {
padding-left: 0;
}
}
.wrapper.input {
label {
width: 50%;
Expand All @@ -223,9 +241,14 @@ $height: 355px;
}
}
.export-button,
.confirm-button {
width: 100%;
padding: $spacing-medium $spacing-large;
}
.export-button {
margin-bottom: $spacing-medium;
}
}
</style>
5 changes: 4 additions & 1 deletion src/components/advanced-pattern-editor/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"copyPatternRangeLabel": "Copy pattern range:",
"copyChannelRangeLabel": "Copy channel range:",
"insertAfterLabel": "Insert after pattern:",
"copyContent": "Copy content"
"exportContent": "Export content",
"insertClonedContent": "Insert cloned content",
"patternRangeExported": "Pattern range exported",
"errorPatternRangeExport": "An error has occurred while exporting the pattern range"
}
}
41 changes: 23 additions & 18 deletions src/components/pattern-editor/pattern-editor.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2016-2021 - https://www.igorski.nl
* Igor Zinken 2016-2022 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
Expand Down Expand Up @@ -106,7 +106,7 @@ export default {
mobileMode: state => state.mobileMode
}),
...mapGetters([
'amountOfSteps',
"amountOfSteps",
]),
patternStep: {
get() {
Expand All @@ -125,47 +125,52 @@ export default {
},
methods: {
...mapMutations([
'setHelpTopic',
'saveState',
'clearSelection',
'setPatternSteps',
'gotoNextPattern',
'openModal',
'showError',
"setHelpTopic",
"saveState",
"clearSelection",
"setPatternSteps",
"gotoNextPattern",
"openModal",
"showError",
]),
handlePatternClear() {
this.clearSelection();
this.saveState(createAction(Actions.CLEAR_PATTERN, { store: this.$store }));
this.saveState( createAction( Actions.CLEAR_PATTERN, { store: this.$store }));
},
handlePatternCopy() {
this.patternCopy = clone(this.activeSong.patterns[this.activePattern]);
this.patternCopy = clone( this.activeSong.patterns[ this.activePattern ]);
},
handlePatternPaste() {
if (this.patternCopy) {
if ( this.patternCopy ) {
this.clearSelection();
this.saveState(createAction(Actions.PASTE_PATTERN, { store: this.$store, patternCopy: this.patternCopy }));
this.saveState(
createAction(
Actions.PASTE_PATTERN,
{ store: this.$store, patternCopy: this.patternCopy }
)
);
}
},
handlePatternAdd() {
const patterns = this.activeSong.patterns;
if ( patterns.length === Config.MAX_PATTERN_AMOUNT ) {
this.showError(this.$t('errorMaxExceeded', { amount: Config.MAX_PATTERN_AMOUNT }));
this.showError( this.$t( "errorMaxExceeded", { amount: Config.MAX_PATTERN_AMOUNT }));
return;
}
this.saveState(createAction(Actions.ADD_PATTERN, { store: this.$store }));
this.gotoNextPattern(this.activeSong);
this.saveState( createAction( Actions.ADD_PATTERN, { store: this.$store }));
this.gotoNextPattern( this.activeSong );
},
handlePatternDelete() {
const patterns = this.activeSong.patterns;
if ( patterns.length === 1 ) {
this.handlePatternClear();
}
else {
this.saveState(createAction(Actions.DELETE_PATTERN, { store: this.$store }));
this.saveState( createAction( Actions.DELETE_PATTERN, { store: this.$store }));
}
},
handlePatternAdvanced() {
this.openModal(ModalWindows.ADVANCED_PATTERN_EDITOR);
this.openModal( ModalWindows.ADVANCED_PATTERN_EDITOR );
}
}
};
Expand Down
13 changes: 7 additions & 6 deletions src/definitions/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ export default
DELETE_MODULE_AUTOMATION : "S:4",
CLEAR_PATTERN : "S:5",
PASTE_PATTERN : "S:6",
ADD_PATTERN : "S:7",
DELETE_PATTERN : "S:8",
CUT_SELECTION : "S:9",
PASTE_SELECTION : "S:10",
TEMPO_CHANGE : "S:11",
REPLACE_INSTRUMENT : "S:12",
PASTE_PATTERN_MULTIPLE : "S:7",
ADD_PATTERN : "S:8",
DELETE_PATTERN : "S:9",
CUT_SELECTION : "S:10",
PASTE_SELECTION : "S:11",
TEMPO_CHANGE : "S:12",
REPLACE_INSTRUMENT : "S:13",
};
5 changes: 3 additions & 2 deletions src/definitions/file-types.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020-2021 - https://www.igorski.nl
* Igor Zinken 2020-2022 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
Expand Down Expand Up @@ -30,5 +30,6 @@ export const WEBM = "audio/webm"; // not for Safari, see https://en.wikipedia.or

export const ACCEPTED_FILE_TYPES = [ FLAC, MPG, MP3, MP4, OGG, WAV, WEBM ];
export const ACCEPTED_FILE_EXTENSIONS = [ ".flac", ".mp3", ".mp4", ".ogg", ".webm", ".wav" ];
export const PATTERN_FILE_EXTENSION = ".xpt";
export const PROJECT_FILE_EXTENSION = ".xtk";
export const INSTRUMENT_FILE_EXTENSION = ".xit"
export const INSTRUMENT_FILE_EXTENSION = ".xit";
Loading

0 comments on commit 1126ac0

Please sign in to comment.