diff --git a/assets b/assets index e0a1446ae..7e19c4cfa 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e0a1446ae956b15a20bf039de0c46a59b8e242bb +Subproject commit 7e19c4cfa7db57178f03ed4a58a9fd4d2b93dea7 diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx index 5b84148c6..17f047106 100644 --- a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx +++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx @@ -13,6 +13,7 @@ import haxe.ui.notifications.NotificationType; // @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros. @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-chart.xml")) +@:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorUploadChartDialog extends ChartEditorBaseDialog { var dropHandlers:Array = []; diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx new file mode 100644 index 000000000..537c7c36e --- /dev/null +++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx @@ -0,0 +1,311 @@ +package funkin.ui.debug.charting.dialogs; + +import funkin.input.Cursor; +import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget; +import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams; +import funkin.util.FileUtil; +import funkin.play.character.CharacterData; +import haxe.io.Path; +import haxe.ui.components.Button; +import haxe.ui.components.Label; +import haxe.ui.containers.dialogs.Dialog.DialogButton; +import haxe.ui.containers.dialogs.Dialog.DialogEvent; +import haxe.ui.containers.Box; +import haxe.ui.containers.dialogs.Dialogs; +import haxe.ui.core.Component; +import haxe.ui.notifications.NotificationManager; +import haxe.ui.notifications.NotificationType; + +// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros. + +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals.xml")) +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChartEditorUploadVocalsDialog extends ChartEditorBaseDialog +{ + var dropHandlers:Array = []; + + var vocalContainer:Component; + var dialogCancel:Button; + var dialogNoVocals:Button; + var dialogContinue:Button; + + var charIds:Array; + var instId:String; + var hasClearedVocals:Bool = false; + + public function new(state2:ChartEditorState, charIds:Array, params2:DialogParams) + { + super(state2, params2); + + this.charIds = charIds; + this.instId = chartEditorState.currentInstrumentalId; + + dialogCancel.onClick = function(_) { + hideDialog(DialogButton.CANCEL); + } + + dialogNoVocals.onClick = function(_) { + // Dismiss + chartEditorState.wipeVocalData(); + hideDialog(DialogButton.APPLY); + }; + + dialogContinue.onClick = function(_) { + // Dismiss + hideDialog(DialogButton.APPLY); + }; + + buildDropHandlers(); + } + + function buildDropHandlers():Void + { + for (charKey in charIds) + { + trace('Adding vocal upload for character ${charKey}'); + + var charMetadata:Null = CharacterDataParser.fetchCharacterData(charKey); + var charName:String = charMetadata?.name ?? charKey; + + var vocalsEntry = new ChartEditorUploadVocalsEntry(charName); + + var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null}; + + var onDropFile:String->Void = function(pathStr:String) { + trace('Selected file: $pathStr'); + var path:Path = new Path(pathStr); + + if (chartEditorState.loadVocalsFromPath(path, charKey, this.instId, !this.hasClearedVocals)) + { + this.hasClearedVocals = true; + // Tell the user the load was successful. + chartEditorState.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${chartEditorState.selectedVariation}'); + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}'; + #end + + dialogNoVocals.hidden = true; + chartEditorState.removeDropHandler(dropHandler); + } + else + { + trace('Failed to load vocal track (${path.file}.${path.ext})'); + + chartEditorState.error('Failed to Load Vocals', + 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${chartEditorState.selectedVariation})'); + + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end + } + }; + + vocalsEntry.onClick = function(_event) { + Dialogs.openBinaryFile('Open $charName Vocals', [ + {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) { + if (selectedFile != null && selectedFile.bytes != null) + { + trace('Selected file: ' + selectedFile.name); + + if (chartEditorState.loadVocalsFromBytes(selectedFile.bytes, charKey, this.instId, !this.hasClearedVocals)) + { + hasClearedVocals = true; + // Tell the user the load was successful. + chartEditorState.success('Loaded Vocals', + 'Loaded vocals for $charName (${selectedFile.name}), variation ${chartEditorState.selectedVariation}'); + + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}'; + #end + + dialogNoVocals.hidden = true; + } + else + { + trace('Failed to load vocal track (${selectedFile.fullPath})'); + + chartEditorState.error('Failed to Load Vocals', + 'Failed to load vocal track (${selectedFile.name}) for variation (${chartEditorState.selectedVariation})'); + + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end + } + } + }); + } + + dropHandler.handler = onDropFile; + + // onDropFile + #if FILE_DROP_SUPPORTED + dropHandlers.push(dropHandler); + #end + + vocalContainer.addComponent(vocalsEntry); + } + } + + public static function build(state:ChartEditorState, charIds:Array, ?closable:Bool, ?modal:Bool):ChartEditorUploadVocalsDialog + { + var dialog = new ChartEditorUploadVocalsDialog(state, charIds, + { + closable: closable ?? false, + modal: modal ?? true + }); + + for (dropTarget in dialog.dropHandlers) + { + state.addDropHandler(dropTarget); + } + + dialog.showDialog(modal ?? true); + + return dialog; + } + + public override function onClose(event:DialogEvent):Void + { + super.onClose(event); + + if (event.button != DialogButton.APPLY && !this.closable) + { + // User cancelled the wizard! Back to the welcome dialog. + chartEditorState.openWelcomeDialog(this.closable); + } + + for (dropTarget in dropHandlers) + { + chartEditorState.removeDropHandler(dropTarget); + } + } + + public override function lock():Void + { + super.lock(); + this.dialogCancel.disabled = true; + } + + public override function unlock():Void + { + super.unlock(); + this.dialogCancel.disabled = false; + } + + /** + * Called when clicking the Upload Chart box. + */ + public function onClickChartBox():Void + { + if (this.locked) return; + + this.lock(); + // TODO / BUG: File filtering not working on mac finder dialog, so we don't use it for now + #if !mac + FileUtil.browseForBinaryFile('Open Chart', [FileUtil.FILE_EXTENSION_INFO_FNFC], onSelectFile, onCancelBrowse); + #else + FileUtil.browseForBinaryFile('Open Chart', null, onSelectFile, onCancelBrowse); + #end + } + + /** + * Called when a file is selected by dropping a file onto the Upload Chart box. + */ + function onDropFileChartBox(pathStr:String):Void + { + var path:Path = new Path(pathStr); + trace('Dropped file (${path})'); + + try + { + var result:Null> = ChartEditorImportExportHandler.loadFromFNFCPath(chartEditorState, path.toString()); + if (result != null) + { + chartEditorState.success('Loaded Chart', + result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}'); + this.hideDialog(DialogButton.APPLY); + } + else + { + chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()})'); + } + } + catch (err) + { + chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()}): ${err}'); + } + } + + /** + * Called when a file is selected by the dialog displayed when clicking the Upload Chart box. + */ + function onSelectFile(selectedFile:SelectedFileInfo):Void + { + this.unlock(); + + if (selectedFile != null && selectedFile.bytes != null) + { + try + { + var result:Null> = ChartEditorImportExportHandler.loadFromFNFC(chartEditorState, selectedFile.bytes); + if (result != null) + { + chartEditorState.success('Loaded Chart', + result.length == 0 ? 'Loaded chart (${selectedFile.name})' : 'Loaded chart (${selectedFile.name})\n${result.join("\n")}'); + + if (selectedFile.fullPath != null) chartEditorState.currentWorkingFilePath = selectedFile.fullPath; + this.hideDialog(DialogButton.APPLY); + } + } + catch (err) + { + chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${selectedFile.name}): ${err}'); + } + } + } + + function onCancelBrowse():Void + { + this.unlock(); + } +} + +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals-entry.xml")) +class ChartEditorUploadVocalsEntry extends Box +{ + public var vocalsEntryLabel:Label; + + var charName:String; + + public function new(charName:String) + { + super(); + + this.charName = charName; + + #if FILE_DROP_SUPPORTED + vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end + + this.onMouseOver = function(_event) { + // if (this.locked) return; + this.swapClass('upload-bg', 'upload-bg-hover'); + Cursor.cursorMode = Pointer; + } + + this.onMouseOut = function(_event) { + this.swapClass('upload-bg-hover', 'upload-bg'); + Cursor.cursorMode = Default; + } + } +} diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 32e9fef53..970f021ac 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -19,6 +19,7 @@ import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget; import funkin.ui.debug.charting.dialogs.ChartEditorCharacterIconSelectorMenu; import funkin.ui.debug.charting.dialogs.ChartEditorUploadChartDialog; import funkin.ui.debug.charting.dialogs.ChartEditorWelcomeDialog; +import funkin.ui.debug.charting.dialogs.ChartEditorUploadVocalsDialog; import funkin.ui.debug.charting.util.ChartEditorDropdowns; import funkin.util.Constants; import funkin.util.DateUtil; @@ -59,11 +60,8 @@ using Lambda; class ChartEditorDialogHandler { // Paths to HaxeUI layout files for each dialog. - static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart'); static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst'); static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata'); - static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals'); - static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry'); static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts'); static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry'); static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart'); @@ -105,6 +103,56 @@ class ChartEditorDialogHandler return dialog; } + /** + * Builds and opens a dialog letting the user browse for a chart file to open. + * @param state The current chart editor state. + * @param closable Whether the dialog can be closed by the user. + * @return The dialog that was opened. + */ + public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null + { + var dialog = ChartEditorUploadChartDialog.build(state, closable); + + dialog.zIndex = 1000; + state.isHaxeUIDialogOpen = true; + + return dialog; + } + + /** + * Builds and opens a dialog where the user uploads vocals for the current song. + * @param state The current chart editor state. + * @param closable Whether the dialog can be closed by the user. + * @return The dialog that was opened. + */ + public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog + { + var charData:SongCharacterData = state.currentSongMetadata.playData.characters; + + var hasClearedVocals:Bool = false; + + var charIdsForVocals:Array = [charData.player, charData.opponent]; + + var dialog = ChartEditorUploadVocalsDialog.build(state, charIdsForVocals, closable); + + dialog.zIndex = 1000; + state.isHaxeUIDialogOpen = true; + + return dialog; + } + + /** + * Builds and opens the dialog for selecting a character. + */ + public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null + { + var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition); + + menu.zIndex = 1000; + + return menu; + } + /** * Builds and opens a dialog letting the user know a backup is available, and prompting them to load it. */ @@ -186,22 +234,6 @@ class ChartEditorDialogHandler return dialog; } - /** - * Builds and opens a dialog letting the user browse for a chart file to open. - * @param state The current chart editor state. - * @param closable Whether the dialog can be closed by the user. - * @return The dialog that was opened. - */ - public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null - { - var dialog = ChartEditorUploadChartDialog.build(state, closable); - - dialog.zIndex = 1000; - state.isHaxeUIDialogOpen = true; - - return dialog; - } - /** * Open the wizard for opening an existing chart from individual files. * @param state @@ -288,15 +320,6 @@ class ChartEditorDialogHandler }; } - public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null - { - var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition); - - menu.zIndex = 1000; - - return menu; - } - public static function openCreateSongWizardBasicOnly(state:ChartEditorState, closable:Bool):Void { // Step 1. Song Metadata @@ -699,150 +722,6 @@ class ChartEditorDialogHandler return dialog; } - /** - * Builds and opens a dialog where the user uploads vocals for the current song. - * @param state The current chart editor state. - * @param closable Whether the dialog can be closed by the user. - * @return The dialog that was opened. - */ - public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog - { - var instId:String = state.currentInstrumentalId; - var charIdsForVocals:Array = []; - - var charData:SongCharacterData = state.currentSongMetadata.playData.characters; - - var hasClearedVocals:Bool = false; - - charIdsForVocals.push(charData.player); - charIdsForVocals.push(charData.opponent); - - var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable); - if (dialog == null) throw 'Could not locate Upload Vocals dialog'; - - var dialogContainer:Null = dialog.findComponent('vocalContainer'); - if (dialogContainer == null) throw 'Could not locate vocalContainer in Upload Vocals dialog'; - - var buttonCancel:Null