diff --git a/frontend/src/common/command/CommandManager.ts b/frontend/src/common/command/CommandManager.ts index 29a0282b..09b6713d 100644 --- a/frontend/src/common/command/CommandManager.ts +++ b/frontend/src/common/command/CommandManager.ts @@ -15,7 +15,7 @@ class CommandManager { command.execute(); this.undoList.push(command); this.redoList = []; - storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, ''); + storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, null); } undo() { @@ -24,7 +24,7 @@ class CommandManager { if (command) { this.redoList.push(command); command.undo(); - storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, ''); + storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, null); } } } @@ -35,7 +35,7 @@ class CommandManager { if (command) { this.undoList.push(command); command.execute(); - storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, ''); + storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, null); } } } diff --git a/frontend/src/common/command/DeleteCommand.ts b/frontend/src/common/command/DeleteCommand.ts index c52ebbfc..bcd3bfd2 100644 --- a/frontend/src/common/command/DeleteCommand.ts +++ b/frontend/src/common/command/DeleteCommand.ts @@ -1,8 +1,9 @@ import ICommand from './ICommand' import { Controller } from '@controllers' import { TrackSection } from '@model' +import { CopyUtil } from '@util' -export default class DeleteCommand extends ICommand { +export class DeleteCommand extends ICommand { private deleteList: TrackSection[]; constructor() { @@ -17,17 +18,7 @@ export default class DeleteCommand extends ICommand { if (focusList.length === 0) return; this.deleteList = focusList.map(focus => { const trackSection = focus.trackSection; - return new TrackSection({ - id: trackSection.id, - sourceId: trackSection.sourceId, - trackId: trackSection.trackId, - channelStartTime: trackSection.channelStartTime, - channelEndTime: trackSection.channelEndTime, - parsedChannelStartTime: trackSection.parsedChannelStartTime, - parsedChannelEndTime: trackSection.parsedChannelEndTime, - trackStartTime: trackSection.trackStartTime, - audioStartTime: trackSection.audioStartTime - }) + return CopyUtil.copySection(trackSection); }); } diff --git a/frontend/src/common/command/PasteCommand.ts b/frontend/src/common/command/PasteCommand.ts new file mode 100644 index 00000000..8f6cec2c --- /dev/null +++ b/frontend/src/common/command/PasteCommand.ts @@ -0,0 +1,55 @@ +import ICommand from './ICommand' +import { Controller } from '@controllers' +import { StoreChannelType } from '@types' +import { storeChannel } from '@store' +import { TrackSection, Track } from '@model' +import { CopyUtil } from '@util' + +export class PasteCommand extends ICommand { + private beforeTrack: Track; + private addTrackSection: TrackSection; + + constructor(track: Track, trackSection: TrackSection) { + super(); + this.beforeTrack = track; + this.addTrackSection = trackSection; + } + + execute() { + const newTrack: Track = CopyUtil.copyTrack(this.beforeTrack); + const newSection: TrackSection = CopyUtil.copySection(this.addTrackSection); + + const endTime: number = newSection.trackStartTime + newSection.length; + const firstDelayIndex: number = newTrack.trackSectionList.findIndex(section => section.trackStartTime >= newSection.trackStartTime && section.trackStartTime < endTime); + + if (firstDelayIndex !== -1) { + const deleyTime = endTime - newTrack.trackSectionList[firstDelayIndex].audioStartTime; + + newTrack.trackSectionList = newTrack.trackSectionList.map((cur, idx) => { + if (idx >= firstDelayIndex) { + cur.trackStartTime += deleyTime; + } + return cur; + }) + Controller.setTrack(newTrack); + Controller.addTrackSection(newTrack.id, newSection); + } else { + Controller.addTrackSection(this.beforeTrack.id, newSection); + } + + }; + + undo() { + const newTrack = CopyUtil.copyTrack(this.beforeTrack); + Controller.setTrack(newTrack); + + storeChannel.publish(StoreChannelType.TRACK_SECTION_LIST_CHANNEL, { + trackId: newTrack.id, + trackSectionList: newTrack.trackSectionList + }); + + storeChannel.publish(StoreChannelType.TRACK_CHANNEL, newTrack.trackSectionList); + }; + +} + diff --git a/frontend/src/common/command/SplitCommand.ts b/frontend/src/common/command/SplitCommand.ts new file mode 100644 index 00000000..3478a143 --- /dev/null +++ b/frontend/src/common/command/SplitCommand.ts @@ -0,0 +1,64 @@ +import ICommand from './ICommand' +import { Controller } from '@controllers' +import { StoreChannelType } from '@types' +import { storeChannel } from '@store' +import { Track, TrackSection } from '@model' +import { CopyUtil, PlayBarUtil } from '@util' + +export class SplitCommand extends ICommand { + private beforeTrack: Track; + private cursorPosition: number; + private trackContainerElement: HTMLElement | null; + private targetSection: TrackSection; + + constructor(cursorPosition: number, currentTrack: Track, targetSection: TrackSection) { + super(); + this.cursorPosition = cursorPosition; + this.beforeTrack = currentTrack; + this.targetSection = targetSection; + this.trackContainerElement = document.querySelector('.audi-main-audio-track-container'); + } + + execute() { + if (!this.trackContainerElement) return; + const startX = this.trackContainerElement.getBoundingClientRect().left; + const endX = this.trackContainerElement.getBoundingClientRect().right; + const [minute, second, milsecond, location, totalCursorTime] = PlayBarUtil.getCursorPosition(startX, this.cursorPosition, endX - startX); + + const splitTime = totalCursorTime - this.targetSection.trackStartTime; + const leftSection = CopyUtil.copySection(this.targetSection); + leftSection.id = 0; + leftSection.channelEndTime = splitTime; + leftSection.parsedChannelEndTime = splitTime; + leftSection.length = splitTime; + + const rightSection = CopyUtil.copySection(this.targetSection); + rightSection.id = 0; + rightSection.channelStartTime = splitTime; + rightSection.parsedChannelStartTime = splitTime; + rightSection.trackStartTime += splitTime; + rightSection.length -= splitTime; + + const sectionIndex = this.beforeTrack.trackSectionList.findIndex(section => section.id === this.targetSection.id); + + if (sectionIndex === -1) return; + const trackId = this.beforeTrack.id; + Controller.removeSection(trackId, sectionIndex); + Controller.addTrackSection(trackId, leftSection); + Controller.addTrackSection(trackId, rightSection); + }; + + undo() { + const newTrack = CopyUtil.copyTrack(this.beforeTrack); + Controller.setTrack(newTrack); + + storeChannel.publish(StoreChannelType.TRACK_SECTION_LIST_CHANNEL, { + trackId: newTrack.id, + trackSectionList: newTrack.trackSectionList + }); + + storeChannel.publish(StoreChannelType.TRACK_CHANNEL, newTrack.trackSectionList); + }; +} + + diff --git a/frontend/src/common/command/index.js b/frontend/src/common/command/index.js new file mode 100644 index 00000000..f2166cba --- /dev/null +++ b/frontend/src/common/command/index.js @@ -0,0 +1,3 @@ +export { DeleteCommand } from './DeleteCommand'; +export { PasteCommand } from './PasteCommand'; +export { SplitCommand } from './SplitCommand'; \ No newline at end of file diff --git a/frontend/src/common/types/eventTypes.ts b/frontend/src/common/types/eventTypes.ts index 2ee34e0b..957a9eda 100644 --- a/frontend/src/common/types/eventTypes.ts +++ b/frontend/src/common/types/eventTypes.ts @@ -22,9 +22,9 @@ enum EventKeyType { AUDIO_SKIP_NEXT = 'AUDIO_SKIP_NEXT', AUDIO_TRACK_CONTAINER_MULTIPLE = 'AUDIO_TRACK_CONTAINER_MULTIPLE', SOURCE_LIST_MULTIPLE = 'SOURCE_LIST_MULTIPLE', - AUDIO_TRACK_SECTION_CLICK = 'AUDIO_TRACK_SECTION_CLICK', + AUDIO_TRACK_SECTION_MULTIPLE = 'AUDIO_TRACK_SECTION_MULTIPLE', FOCUS_RESET_CLICK = 'FOCUS_RESET_CLICK', - EDIT_TOOLS_CLICK = 'EDIT_TOOLS_CLICK' + EDIT_TOOLS_CLICK = 'EDIT_TOOLS_CLICK', } enum EventType { @@ -39,10 +39,11 @@ enum EventType { drop = 'drop', change = 'change', input = 'input', - mousemove = 'mousemove' + mousemove = 'mousemove', + mouseout = 'mouseout' } -const eventTypes = ['click', 'dblclick', 'keyup', 'dragstart', 'dragover', 'dragenter', 'dragleave', 'drop', 'change', 'input', 'mousemove', 'dragend']; +const eventTypes = ['click', 'dblclick', 'keyup', 'dragstart', 'dragover', 'dragenter', 'dragleave', 'drop', 'change', 'input', 'mousemove', 'dragend', 'mouseout']; interface EventTargetDataType { listener: EventListener; diff --git a/frontend/src/common/types/keyType.ts b/frontend/src/common/types/keyType.ts index 33f728e0..6d07e33d 100644 --- a/frontend/src/common/types/keyType.ts +++ b/frontend/src/common/types/keyType.ts @@ -8,7 +8,9 @@ enum KeyBoard { CTRL = 17, LEFT = 37, RIGHT = 39, - SPACE = 32 + SPACE = 32, + LEFT_BRACKET = 219, + RIGHT_BRACKET = 221, } export { KeyBoard } \ No newline at end of file diff --git a/frontend/src/common/util/CopyUtil.ts b/frontend/src/common/util/CopyUtil.ts index 960f066f..552ee179 100644 --- a/frontend/src/common/util/CopyUtil.ts +++ b/frontend/src/common/util/CopyUtil.ts @@ -2,17 +2,28 @@ import { Source, Track, TrackSection } from '@model'; const copySection = (trackSection: TrackSection): TrackSection => { const newTrackSection = new TrackSection({ + id: trackSection.id, sourceId: trackSection.sourceId, trackId: trackSection.trackId, channelStartTime: trackSection.channelStartTime, channelEndTime: trackSection.channelEndTime, parsedChannelStartTime: trackSection.parsedChannelStartTime, parsedChannelEndTime: trackSection.parsedChannelEndTime, - trackStartTime: 0, - audioStartTime: 0 + trackStartTime: trackSection.trackStartTime, + // effectList: trackSection.effectList, // Effect 기능 구현시 추가 + audioStartTime: trackSection.audioStartTime }); return newTrackSection; } -export { copySection } \ No newline at end of file +const copyTrack = (track: Track): Track => { + const newTrack = new Track({ + id: track.id, + trackSectionList: track.trackSectionList.map(section => copySection(section)) + }); + + return newTrack; +} + +export { copySection, copyTrack } \ No newline at end of file diff --git a/frontend/src/common/util/PlayBarUtil.ts b/frontend/src/common/util/PlayBarUtil.ts index 6ca297bb..a0b12c38 100644 --- a/frontend/src/common/util/PlayBarUtil.ts +++ b/frontend/src/common/util/PlayBarUtil.ts @@ -39,7 +39,7 @@ const getCursorPosition = (defaultStartX: number, currentX: number, mainWidth: n const [newMinute, newSecond, newMilsecond] = setTime(cursorTime); - return [newMinute, newSecond, newMilsecond, differenceWidth, cursorTime]; + return [newMinute, newSecond, newMilsecond, differenceWidth, cursorTime]; // {} 으로 retrun해도 좋을 것 같아요 }; const getStringTime = (time: number): string[] => { diff --git a/frontend/src/components/App/App.ts b/frontend/src/components/App/App.ts index b4a9ff8d..909c5fac 100644 --- a/frontend/src/components/App/App.ts +++ b/frontend/src/components/App/App.ts @@ -38,7 +38,6 @@ import { Controller } from "@controllers"; } KeyDownListener(e): void { - const { } = Controller const isCtrl = Controller.getCtrlIsPressed(); if (e.which === KeyBoard.CTRL) { @@ -60,6 +59,12 @@ import { Controller } from "@controllers"; else if (e.which === KeyBoard.RIGHT && !isCtrl) { // console.log('오른쪽'); } + else if (e.which === KeyBoard.LEFT_BRACKET && !isCtrl) { + // console.log('시작지점'); + } + else if (e.which === KeyBoard.RIGHT_BRACKET && !isCtrl) { + // console.log('마지막지점'); + } else if (e.which === KeyBoard.SPACE && !isCtrl) { // console.log('스페이스바'); } @@ -67,10 +72,10 @@ import { Controller } from "@controllers"; Controller.setClipBoard(); } else if (e.which === KeyBoard.X && isCtrl) { - // console.log('잘라내기'); + Controller.cutCommand(); } else if (e.which === KeyBoard.V && isCtrl) { - // console.log('붙여넣기'); + Controller.pasteCommand(); } else if (e.which === KeyBoard.Z && isCtrl) { Controller.undoCommand(); diff --git a/frontend/src/components/AudioTrack/AudioTrack.scss b/frontend/src/components/AudioTrack/AudioTrack.scss index 6a195f97..cadb4027 100644 --- a/frontend/src/components/AudioTrack/AudioTrack.scss +++ b/frontend/src/components/AudioTrack/AudioTrack.scss @@ -8,7 +8,7 @@ audi-audio-track:not(:defined) { position: relative; width: 100%; min-height: 150px; - padding: 10px; + padding: 10px 0px; background-color: #000000; box-sizing: border-box; @@ -38,6 +38,18 @@ audi-audio-track:not(:defined) { box-shadow: rgb(43, 130, 211) 0 0 15px 5px; } } + .cut-line { + position: absolute; + width: 1px; + height: 100%; + + top:0; + left: 0px; + border-left: 1px dashed white; + z-index: 999; + + pointer-events: none; + } } &:last-of-type{ diff --git a/frontend/src/components/AudioTrack/AudioTrack.ts b/frontend/src/components/AudioTrack/AudioTrack.ts index 72f54822..37be5ee8 100644 --- a/frontend/src/components/AudioTrack/AudioTrack.ts +++ b/frontend/src/components/AudioTrack/AudioTrack.ts @@ -53,11 +53,12 @@ import "./AudioTrack.scss"; ${this.getTrackSectionList()}
Drag & Drop
+
`; } - + getTrackSectionList(): string { return this.trackSectionList.reduce((acc, trackSection, idx) => acc += `` @@ -83,8 +84,9 @@ import "./AudioTrack.scss"; listeners: [this.focusResetListener], bindObj: this }); + } - + focusResetListener(e): void { const ctrlIsPressed = Controller.getCtrlIsPressed(); if (!ctrlIsPressed) { @@ -102,25 +104,26 @@ import "./AudioTrack.scss"; e.stopPropagation(); const sourceId = e.dataTransfer.getData("text/plain"); const source = Controller.getSourceBySourceId(Number(sourceId)); - if(!source) return; + if (!source) return; const { duration } = source; - const trackSection = new TrackSection({ - sourceId : source.id, + const trackSection = new TrackSection({ + id: 0, + sourceId: source.id, trackId: this.trackId, - channelStartTime : 0, - channelEndTime : duration, - parsedChannelStartTime : 0, + channelStartTime: 0, + channelEndTime: duration, + parsedChannelStartTime: 0, parsedChannelEndTime: duration, - trackStartTime : 0, - audioStartTime : 0 - }); - + trackStartTime: 0, + audioStartTime: 0 + }); + Controller.addTrackSection(this.trackId, trackSection); this.hideMessage(); } - + trackDragenterListener(e): void { e.preventDefault() this.trackDropzoneElement?.classList.add('focus'); @@ -132,7 +135,7 @@ import "./AudioTrack.scss"; } hideMessage(): void { - this.trackMessage?.classList.add('hide'); + this.trackMessage?.classList.add('hide'); } subscribe(): void { @@ -141,7 +144,7 @@ import "./AudioTrack.scss"; } trackDragStateObserverCallback(isTrackDraggable): void { - if(isTrackDraggable){ + if (isTrackDraggable) { this.activeTrackDropzone(); return; } @@ -156,8 +159,8 @@ import "./AudioTrack.scss"; this.trackDropzoneElement?.classList.add('hide'); } - trackSectionListObserverCallback({trackId, trackSectionList}): void { - if(trackId !== this.trackId) return; + trackSectionListObserverCallback({ trackId, trackSectionList }): void { + if (trackId !== this.trackId) return; this.trackSectionList = trackSectionList; this.render(); diff --git a/frontend/src/components/AudioTrack/AudioTrackSection/AudioTrackSection.ts b/frontend/src/components/AudioTrack/AudioTrackSection/AudioTrackSection.ts index fb4025e4..3106e879 100644 --- a/frontend/src/components/AudioTrack/AudioTrackSection/AudioTrackSection.ts +++ b/frontend/src/components/AudioTrack/AudioTrackSection/AudioTrackSection.ts @@ -1,6 +1,6 @@ import { Controller } from '@controllers'; -import { EventKeyType, EventType } from '@types'; -import { EventUtil } from '@util'; +import { CursorType, EventKeyType, EventType } from '@types'; +import { EventUtil, PlayBarUtil } from '@util'; import './AudioTrackSection.scss'; @@ -15,6 +15,9 @@ interface SectionData { private sectionId: number; private sectionData: SectionData | undefined; private trackCanvasElement: HTMLCanvasElement | undefined | null; + private cutLineElement: HTMLElement | undefined | null; + private trackContainerElement: HTMLElement | null; + constructor() { super(); @@ -22,6 +25,8 @@ interface SectionData { this.sectionId = 0; this.sectionData; this.trackCanvasElement; + this.cutLineElement; + this.trackContainerElement = null; } static get observedAttributes(): string[] { @@ -55,13 +60,15 @@ interface SectionData { render(): void { this.innerHTML = ` - + `; } init(): void { this.trackCanvasElement = this.querySelector('.audio-track-section'); this.sectionData = Controller.getSectionChannelData(this.trackId, this.sectionId); + this.cutLineElement = document.getElementById(`section-cut-line-${this.trackId}`); + this.trackContainerElement = document.querySelector('.audi-main-audio-track-container'); } draw(): void { @@ -101,15 +108,49 @@ interface SectionData { initEvent(): void { EventUtil.registerEventToRoot({ - eventTypes: [EventType.click], - eventKey: EventKeyType.AUDIO_TRACK_SECTION_CLICK + this.sectionId, - listeners: [this.clickListener], + eventTypes: [EventType.click, EventType.mousemove, EventType.mouseout], + eventKey: EventKeyType.AUDIO_TRACK_SECTION_MULTIPLE + this.sectionId, + listeners: [this.clickListener, this.cutLineMouseMoveListener, this.mouseoutListener], bindObj: this }); + } clickListener(e): void { - Controller.toggleFocus(this.trackId, this.sectionId, e.target); + const cursorMode = Controller.getCursorMode(); + if (cursorMode === CursorType.SELECT_MODE) { + Controller.toggleFocus(this.trackId, this.sectionId, e.target); + + } else if (cursorMode === CursorType.CUT_MODE) { + const cursorPosition = e.pageX; + Controller.splitCommand(cursorPosition, this.trackId, this.sectionId); + } + } + + cutLineMouseMoveListener(e): void { + const cursorMode = Controller.getCursorMode(); + + if (!this.cutLineElement || !this.trackContainerElement) return; + + + if (cursorMode !== CursorType.CUT_MODE) { + this.cutLineElement.classList.add('hide'); + return; + } + + const cursorPosition = e.pageX; + const startX = this.trackContainerElement.getBoundingClientRect().left; + const endX = this.trackContainerElement.getBoundingClientRect().right; + const [minute, second, milsecond, location, totalCursorTime] = PlayBarUtil.getCursorPosition(startX, cursorPosition, endX - startX); + + this.cutLineElement.classList.remove('hide'); + this.cutLineElement.style.left = `${location}px`; + } + + mouseoutListener(e): void { + if (!this.cutLineElement) return; + + this.cutLineElement.classList.add('hide'); } }; diff --git a/frontend/src/components/EditTools/EditTools.ts b/frontend/src/components/EditTools/EditTools.ts index 2e7d72ad..a8b9570a 100644 --- a/frontend/src/components/EditTools/EditTools.ts +++ b/frontend/src/components/EditTools/EditTools.ts @@ -85,11 +85,11 @@ import './EditTools.scss' listeners: [this.undoListener], bindObj: this }); - + EventUtil.registerEventToRoot({ eventTypes: [EventType.click], eventKey: EventKeyType.EDIT_TOOLS_CLICK + IconType.blade, - listeners: [this.cutCursorListener], + listeners: [this.cuttingCursorListener], bindObj: this }); @@ -99,15 +99,29 @@ import './EditTools.scss' listeners: [this.redoListener], bindObj: this }); - + EventUtil.registerEventToRoot({ eventTypes: [EventType.click], eventKey: EventKeyType.EDIT_TOOLS_CLICK + IconType.copy, listeners: [this.copyListener], bindObj: this }); + + EventUtil.registerEventToRoot({ + eventTypes: [EventType.click], + eventKey: EventKeyType.EDIT_TOOLS_CLICK + IconType.cut, + listeners: [this.cutListener], + bindObj: this + }); + + EventUtil.registerEventToRoot({ + eventTypes: [EventType.click], + eventKey: EventKeyType.EDIT_TOOLS_CLICK + IconType.paste, + listeners: [this.pasteListener], + bindObj: this + }); } - + cursorState() { const cursorMode = Controller.getCursorMode(); if (cursorMode === CursorType.SELECT_MODE) { @@ -152,7 +166,7 @@ import './EditTools.scss' Controller.setCursorMode(CursorType.SELECT_MODE); } - cutCursorListener(e) { + cuttingCursorListener(e) { Controller.setCursorMode(CursorType.CUT_MODE); } @@ -160,6 +174,14 @@ import './EditTools.scss' Controller.setClipBoard(); } + cutListener(e) { + Controller.cutCommand(); + } + + pasteListener(e) { + Controller.pasteCommand(); + } + subscribe(): void { storeChannel.subscribe(StoreChannelType.EDIT_TOOLS_CHANNEL, this.updateEditTools, this); } @@ -183,7 +205,7 @@ import './EditTools.scss' Controller.redoCommand(); } }; - + customElements.define('audi-edit-tools', EditTools); })() diff --git a/frontend/src/components/IconButton/IconButton.scss b/frontend/src/components/IconButton/IconButton.scss index b56bbcce..5d171808 100644 --- a/frontend/src/components/IconButton/IconButton.scss +++ b/frontend/src/components/IconButton/IconButton.scss @@ -18,4 +18,31 @@ path { fill: variables.$green-color } +} + +.icon-buttion-wrap { + position: relative; + &:hover { + .icon-info{ + transition-property: visibility; + transition-delay: 0.3s; + visibility: visible; + } + } +} +.icon-info { + visibility: hidden; + position:absolute; + left: -20%; + width: fit-content; + padding: 3px 5px; + z-index: 1000; + + color:white; + border: 0.5px solid gray; + border-radius: 3px; + background-color:rgb(80,80,80); + + white-space:nowrap; + font-size: 15px; } \ No newline at end of file diff --git a/frontend/src/components/IconButton/IconButton.ts b/frontend/src/components/IconButton/IconButton.ts index a0fa1f2d..3a702d01 100644 --- a/frontend/src/components/IconButton/IconButton.ts +++ b/frontend/src/components/IconButton/IconButton.ts @@ -1,5 +1,5 @@ import { IconType } from '@types'; -import icons from './icons'; +import { icons, iconInfo } from './icons'; import './IconButton.scss'; (() => { @@ -24,9 +24,9 @@ import './IconButton.scss'; } attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void { - if(oldVal === newVal) return; + if (oldVal === newVal) return; - switch(attrName){ + switch (attrName) { case 'icontype': this.icontype = IconType[newVal]; break; @@ -41,7 +41,7 @@ import './IconButton.scss'; break; } this[attrName] = newVal; - if(this.isDoneInit) + if (this.isDoneInit) this.render(); } @@ -52,7 +52,7 @@ import './IconButton.scss'; render(): void { this.innerHTML = ` -
+
${this.getIcon()} +
${this.icontype ? iconInfo[this.icontype] : ''}
`; } - getIcon(){ - if(!this.color || !this.icontype) return; + getIcon() { + if (!this.color || !this.icontype) return; - switch(this.icontype){ + switch (this.icontype) { case IconType.record_on: - return ``; + return ``; case IconType.zoomIn: return ` `; diff --git a/frontend/src/components/IconButton/icons.ts b/frontend/src/components/IconButton/icons.ts index a4e64487..79c9868e 100644 --- a/frontend/src/components/IconButton/icons.ts +++ b/frontend/src/components/IconButton/icons.ts @@ -8,11 +8,11 @@ const icons = { fastRewind: "M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z", repeat: "M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z", record: "M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z", - user: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z", - menu: "M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z", - listAdd: "M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z", - zoomOut: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z", - headset: "M12 1c-4.97 0-9 4.03-9 9v7c0 1.66 1.34 3 3 3h3v-8H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-4v8h3c1.66 0 3-1.34 3-3v-7c0-4.97-4.03-9-9-9z", + user: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z", + menu: "M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z", + listAdd: "M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z", + zoomOut: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z", + headset: "M12 1c-4.97 0-9 4.03-9 9v7c0 1.66 1.34 3 3 3h3v-8H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-4v8h3c1.66 0 3-1.34 3-3v-7c0-4.97-4.03-9-9-9z", colorLens: "M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z", save: "M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z", upload: "M20 6h-8l-2-2H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-1 8h-3v3h-2v-3h-3v-2h3V9h2v3h3v2z", @@ -26,4 +26,36 @@ const icons = { link: "M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z", } -export default icons; +const iconInfo = { + play: '재생(Space)', + pause: '일시정지(Space)', + stop: '중지', + skipNext: '종료 지점( ] )', + skipPrev: '시작 지점( [ )', + fastForward: '건너뛰기(→)', + fastRewind: '건너뛰기(←)', + repeat: '반복 재생', + record: '녹음하기', + record_on: '녹음 중', + user: '회원메뉴', + menu: '회원메뉴', + listAdd: '이펙트 추가', + zoomIn: '확대하기', + zoomOut: '축소하기', + headset: '정보', + colorLens: '정보', + save: '저장하기', + upload: '불러오기', + cursor: '선택 도구(V)', + copy: '복사하기(Ctrl + C)', + paste: '붙여넣기(Ctrl + V)', + cut: '잘라내기(Ctrl + X)', + delete: '삭제하기(Delete)', + redo: 'Redo(Ctrl + Z)', + undo: 'Undo(Ctrl + Y)', + blade: '자르기 도구(C)', + link: '이어붙이기', + none: '' +} + +export { icons, iconInfo }; diff --git a/frontend/src/components/Main/Main.scss b/frontend/src/components/Main/Main.scss index 63b162f0..5f7bc502 100644 --- a/frontend/src/components/Main/Main.scss +++ b/frontend/src/components/Main/Main.scss @@ -37,9 +37,10 @@ } } .cursor-change { - cursor: url("https://user-images.githubusercontent.com/7006837/100991542-73da8780-3596-11eb-84fc-de0de7494be8.png") 6 12, auto !important; - & * { - cursor: url("https://user-images.githubusercontent.com/7006837/100991542-73da8780-3596-11eb-84fc-de0de7494be8.png") 6 12, auto !important; - } - } + cursor: url("https://user-images.githubusercontent.com/7006837/100991542-73da8780-3596-11eb-84fc-de0de7494be8.png") 7 12, auto !important; + + & * { + cursor: url("https://user-images.githubusercontent.com/7006837/100991542-73da8780-3596-11eb-84fc-de0de7494be8.png") 7 12, auto !important; + } + } } diff --git a/frontend/src/controllers/controller.ts b/frontend/src/controllers/controller.ts index bd232cc2..6f3983e1 100644 --- a/frontend/src/controllers/controller.ts +++ b/frontend/src/controllers/controller.ts @@ -2,9 +2,8 @@ import { Source, Track, TrackSection } from '@model'; import { store } from "@store"; import { ModalType, FocusInfo, CursorType } from "@types"; import CommandManager from '@command/CommandManager'; -import DeleteCommand from '@command/DeleteCommand' -import { CopyUtil } from '@util' -import { PlayBarUtil } from '@util'; +import { DeleteCommand, PasteCommand, SplitCommand } from '@command' +import { CopyUtil, PlayBarUtil } from '@util' interface SectionData { sectionChannelData: number[]; @@ -22,7 +21,7 @@ const getSectionChannelData = (trackId: number, trackSectionId: number): Section if (!trackSection) return; const source = sourceList.find((source) => source.id === trackSection.sourceId); - + if (!source) return; const { parsedChannelData, duration } = source; @@ -68,7 +67,17 @@ const getTrackList = (): Track[] => { return trackList; }; -const addTrack = (track: Track): void => { +const getTrack = (trackId: number): Track | null => { + const { trackList } = store.getState(); + const track = trackList.find(track => track.id === trackId); + + if (!track) + return null; + + return track; +} + +const setTrack = (track: Track): void => { store.setTrack(track); }; @@ -279,15 +288,55 @@ const redoCommand = () => { if (CommandManager.redoList.length === 0) return; CommandManager.redo(); }; - -const setClipBoard = () => { + +const setClipBoard = (): boolean => { const { focusList } = store.getState(); - if (focusList.length !== 1) return; + if (focusList.length !== 1) return false; const newSection: TrackSection = CopyUtil.copySection(focusList[0].trackSection); - + newSection.id = 0; store.setClipBoard(newSection); + + return true; +}; + +const cutCommand = () => { + if (!setClipBoard()) return; + + const command = new DeleteCommand(); + CommandManager.execute(command); +}; + +const pasteCommand = () => { + const { focusList, trackList, clipBoard } = store.getState(); + + if (focusList.length !== 1) return false; + + const track = trackList.find(track => track.id === focusList[0].trackSection.trackId); + if (!track || !clipBoard) return; + + const copyTrack = CopyUtil.copyTrack(track); + const copySection = CopyUtil.copySection(clipBoard); + const focusSection = focusList[0].trackSection; + + copySection.trackStartTime = focusSection.trackStartTime + focusSection.length; + copySection.trackId = focusSection.trackId; + + const command = new PasteCommand(copyTrack, copySection); + + CommandManager.execute(command) +}; + +const splitCommand = (cursorPosition: number, trackId: number, sectionId: number): void => { + const track = getTrack(trackId); + const trackSection = track?.trackSectionList.find(section => section.id === sectionId); + + if (!trackSection || !track) return; + + const command = new SplitCommand(cursorPosition, CopyUtil.copyTrack(track), CopyUtil.copySection(trackSection)) + CommandManager.execute(command); + }; export default { @@ -297,7 +346,8 @@ export default { changeModalState, changeTrackDragState, getTrackList, - addTrack, + getTrack, + setTrack, addTrackSection, changeCursorTime, changeCurrentPosition, @@ -326,5 +376,8 @@ export default { removeSection, deleteCommand, undoCommand, - redoCommand + redoCommand, + cutCommand, + pasteCommand, + splitCommand }; diff --git a/frontend/src/model/TrackSection.ts b/frontend/src/model/TrackSection.ts index bfeca2bf..8393b6b0 100644 --- a/frontend/src/model/TrackSection.ts +++ b/frontend/src/model/TrackSection.ts @@ -1,27 +1,28 @@ -class TrackSection{ +class TrackSection { public id: number; public trackId: number; public sourceId: number; - public channelStartTime : number; - public channelEndTime : number; - public parsedChannelStartTime : number; - public parsedChannelEndTime : number; - public trackStartTime : number; + public channelStartTime: number; + public channelEndTime: number; + public parsedChannelStartTime: number; + public parsedChannelEndTime: number; + public trackStartTime: number; public length: number; public effectList: object[]; public audioStartTime: number; constructor({ - id=0, + id, trackId, - sourceId, - channelStartTime, - channelEndTime, + sourceId, + channelStartTime, + channelEndTime, parsedChannelStartTime, parsedChannelEndTime, trackStartTime, - audioStartTime - }){ + effectList = [], + audioStartTime = 0 + }) { this.id = id; this.trackId = trackId; this.sourceId = sourceId; @@ -29,10 +30,10 @@ class TrackSection{ this.channelEndTime = channelEndTime; this.parsedChannelStartTime = parsedChannelStartTime; this.parsedChannelEndTime = parsedChannelEndTime; - this.trackStartTime = trackStartTime; + this.trackStartTime = trackStartTime; this.length = this.channelEndTime - this.channelStartTime; - this.effectList = []; - this.audioStartTime = 0; + this.effectList = effectList; + this.audioStartTime = audioStartTime; } } diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index e0442ee4..bff51aec 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -20,8 +20,8 @@ const store = new (class Store { focusList: [], ctrlIsPressed: false, cursorMode: CursorType.SELECT_MODE, - trackIndex: 3, - sectionIndex: 0, + trackIndex: 4, + sectionIndex: 1, clipBoard: null, audioSourceInfoInTrackList: [], currentPosition: 0, @@ -35,7 +35,7 @@ const store = new (class Store { return Array(numOfTracks) .fill(0) .reduce((acc, cur, idx) => { - const track = new Track({ id: idx, trackSectionList: [] }); + const track = new Track({ id: idx + 1, trackSectionList: [] }); return acc.concat(track); }, []); } @@ -86,10 +86,15 @@ const store = new (class Store { setTrack(newTrack: Track): void { const { trackList } = this.state; - newTrack.id = trackList.length; - const newAudioTrackList = trackList.concat(newTrack); + const track = trackList.find(track => track.id === newTrack.id); + if (track) { + track.trackSectionList = [...newTrack.trackSectionList]; + } else { + newTrack.id = this.state.trackIndex++; + const newAudioTrackList = trackList.concat(newTrack); - this.state = { ...this.state, trackList: newAudioTrackList }; + this.state = { ...this.state, trackList: newAudioTrackList }; + } } setTrackSection(trackId: number, newTrackSection: TrackSection): void { @@ -98,7 +103,9 @@ const store = new (class Store { if (!track) return; const { trackSectionList } = track; - newTrackSection.id = this.state.sectionIndex++; + if (newTrackSection.id === 0) { + newTrackSection.id = this.state.sectionIndex++; + } const newTrackSectionList = trackSectionList.concat(newTrackSection).sort((a, b) => a.trackStartTime - b.trackStartTime); @@ -143,17 +150,19 @@ const store = new (class Store { const newfocusList = [...focusList]; newfocusList.splice(removeIndex, 1); this.state = { ...this.state, focusList: newfocusList }; - storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, ''); + storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, null); } resetFocus(): void { this.state = { ...this.state, focusList: [] }; - storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, ''); + storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, null); } setClipBoard(newSection: TrackSection): void { this.state.clipBoard = newSection; - storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, ''); + storeChannel.publish(StoreChannelType.EDIT_TOOLS_CHANNEL, null); + console.log(newSection); + } setMarkerTime(newMarkerTime: number): void { @@ -199,14 +208,14 @@ const store = new (class Store { const newTrack = new Track({ ...track, trackSectionList: newTrackSectionList }); const newTrackList: Array = trackList.reduce((acc, track) => - (track.id === trackId) ? acc.concat(newTrack) : acc.concat(track), - []); + (track.id === trackId) ? acc.concat(newTrack) : acc.concat(track), + []); this.state = { ...this.state, trackList: newTrackList }; storeChannel.publish(StoreChannelType.TRACK_SECTION_LIST_CHANNEL, { - trackId: trackId, - trackSectionList: newTrackSectionList + trackId: trackId, + trackSectionList: newTrackSectionList }); storeChannel.publish(StoreChannelType.TRACK_CHANNEL, newTrackList);