mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2025-04-12 06:54:41 -04:00
Merge branch 'rewrite/master' into rewrite/ci-verify-submodule-commits
This commit is contained in:
commit
99e3bc81ec
28 changed files with 1599 additions and 1031 deletions
Project.xmlassets
docs
source
funkin
play
save
ui/debug
DebugMenuSubState.hx
charting
ChartEditorState.hx
commands
components
dialogs
ChartEditorAboutDialog.hxChartEditorBaseDialog.hxChartEditorUploadChartDialog.hxChartEditorWelcomeDialog.hx
handlers
ChartEditorDialogHandler.hxChartEditorImportExportHandler.hxChartEditorNotificationHandler.hxChartEditorShortcutHandler.hxChartEditorToolboxHandler.hx
import.hxutil
haxe/ui/backend/flixel
|
@ -162,10 +162,13 @@
|
|||
<icon path="art/iconOG.png" />
|
||||
<haxedef name="CAN_OPEN_LINKS" unless="switch" />
|
||||
<haxedef name="CAN_CHEAT" if="switch debug" />
|
||||
<!-- I don't -->
|
||||
<!-- I don't remember what this is for. -->
|
||||
<haxedef name="haxeui_no_mouse_reset" />
|
||||
<!-- Clicking outside a dialog should deselect the current focused component. -->
|
||||
<haxedef name="haxeui_focus_out_on_click" />
|
||||
<!-- Required to use haxe.ui.backend.flixel.UIState with build macros. -->
|
||||
<haxedef name="haxeui_dont_impose_base_class" />
|
||||
|
||||
<!-- Skip the Intro -->
|
||||
<section if="debug">
|
||||
<!-- Starts the game at the specified week, at the first song -->
|
||||
|
|
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit 4ed2b3084d54899e10d10a97eaafe210158768be
|
||||
Subproject commit 2dd4ab0eb9979422c1c4cb849ebe899b7bf1758a
|
|
@ -32,6 +32,10 @@ Example:
|
|||
public function checkSyncError(?targetTime:Float):Float
|
||||
```
|
||||
|
||||
## Commenting Unused Code
|
||||
|
||||
Do not comment out sections of code that are unused. Keep these snippets elsewhere or remove them. Older chunks of code can be retrieved by referring to the older Git commits, and having large chunks of commented code makes files longer and more confusing to navigate.
|
||||
|
||||
## License Headers
|
||||
|
||||
Do not include headers specifying code license on individual files in the repo, since the main `LICENSE.md` file covers all of them.
|
||||
|
|
|
@ -233,6 +233,7 @@ class PauseSubState extends MusicBeatSubState
|
|||
|
||||
if (PlayStatePlaylist.isStoryMode)
|
||||
{
|
||||
PlayStatePlaylist.reset();
|
||||
openSubState(new funkin.ui.transition.StickerSubState(null, STORY));
|
||||
}
|
||||
else
|
||||
|
|
|
@ -1354,8 +1354,7 @@ class PlayState extends MusicBeatSubState
|
|||
else
|
||||
{
|
||||
// lolol
|
||||
lime.app.Application.current.window.alert('Nice job, you ignoramus. $id isn\'t a real stage.\nI\'m falling back to the default so the game doesn\'t shit itself.',
|
||||
'Stage Error');
|
||||
lime.app.Application.current.window.alert('Unable to load stage ${id}, is its data corrupted?.', 'Stage Error');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -177,6 +177,23 @@ abstract Save(RawSaveData)
|
|||
return this.optionsChartEditor.previousFiles;
|
||||
}
|
||||
|
||||
public var chartEditorHasBackup(get, set):Bool;
|
||||
|
||||
function get_chartEditorHasBackup():Bool
|
||||
{
|
||||
if (this.optionsChartEditor.hasBackup == null) this.optionsChartEditor.hasBackup = false;
|
||||
|
||||
return this.optionsChartEditor.hasBackup;
|
||||
}
|
||||
|
||||
function set_chartEditorHasBackup(value:Bool):Bool
|
||||
{
|
||||
// Set and apply.
|
||||
this.optionsChartEditor.hasBackup = value;
|
||||
flush();
|
||||
return this.optionsChartEditor.hasBackup;
|
||||
}
|
||||
|
||||
public var chartEditorNoteQuant(get, set):Int;
|
||||
|
||||
function get_chartEditorNoteQuant():Int
|
||||
|
@ -926,6 +943,13 @@ typedef SaveControlsData =
|
|||
*/
|
||||
typedef SaveDataChartEditorOptions =
|
||||
{
|
||||
/**
|
||||
* Whether the Chart Editor created a backup the last time it closed.
|
||||
* Prompt the user to load it, then set this back to `false`.
|
||||
* @default `false`
|
||||
*/
|
||||
var ?hasBackup:Bool;
|
||||
|
||||
/**
|
||||
* Previous files opened in the Chart Editor.
|
||||
* @default `[]`
|
||||
|
|
|
@ -7,6 +7,8 @@ import funkin.ui.MusicBeatSubState;
|
|||
import funkin.ui.TextMenuList;
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
import funkin.ui.MusicBeatSubState;
|
||||
import funkin.util.logging.CrashHandler;
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
|
||||
class DebugMenuSubState extends MusicBeatSubState
|
||||
{
|
||||
|
@ -50,7 +52,9 @@ class DebugMenuSubState extends MusicBeatSubState
|
|||
createItem("ANIMATION EDITOR", openAnimationEditor);
|
||||
createItem("STAGE EDITOR", openStageEditor);
|
||||
createItem("TEST STICKERS", testStickers);
|
||||
|
||||
#if sys
|
||||
createItem("OPEN CRASH LOG FOLDER", openLogFolder);
|
||||
#end
|
||||
FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y));
|
||||
FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y + 500));
|
||||
}
|
||||
|
@ -81,6 +85,8 @@ class DebugMenuSubState extends MusicBeatSubState
|
|||
|
||||
function openChartEditor()
|
||||
{
|
||||
FlxTransitionableState.skipNextTransIn = true;
|
||||
|
||||
FlxG.switchState(new ChartEditorState());
|
||||
}
|
||||
|
||||
|
@ -101,6 +107,22 @@ class DebugMenuSubState extends MusicBeatSubState
|
|||
trace('Stage Editor');
|
||||
}
|
||||
|
||||
#if sys
|
||||
function openLogFolder()
|
||||
{
|
||||
#if windows
|
||||
Sys.command('explorer', [CrashHandler.LOG_FOLDER]);
|
||||
#elseif mac
|
||||
// mac could be fuckie with where the log folder is relative to the game file...
|
||||
// if this comment is still here... it means it has NOT been verified on mac yet!
|
||||
Sys.command('open', [CrashHandler.LOG_FOLDER]);
|
||||
#end
|
||||
|
||||
// TODO: implement linux
|
||||
// some shit with xdg-open :thinking: emoji...
|
||||
}
|
||||
#end
|
||||
|
||||
function exitDebugMenu()
|
||||
{
|
||||
// TODO: Add a transition?
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,8 +4,6 @@ import funkin.data.song.SongData.SongEventData;
|
|||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.data.song.SongDataUtils.SongClipboardItems;
|
||||
import haxe.ui.notifications.NotificationManager;
|
||||
import haxe.ui.notifications.NotificationType;
|
||||
|
||||
/**
|
||||
* A command which inserts the contents of the clipboard into the chart editor.
|
||||
|
@ -30,15 +28,7 @@ class PasteItemsCommand implements ChartEditorCommand
|
|||
|
||||
if (currentClipboard.valid != true)
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failed to Paste',
|
||||
body: 'Could not parse clipboard contents.',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
state.error('Failed to Paste', 'Could not parse clipboard contents.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -58,15 +48,7 @@ class PasteItemsCommand implements ChartEditorCommand
|
|||
|
||||
state.sortChartData();
|
||||
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Paste Successful',
|
||||
body: 'Successfully pasted clipboard contents.',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
state.success('Paste Successful', 'Successfully pasted clipboard contents.');
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
|
|
|
@ -9,6 +9,7 @@ import flixel.graphics.frames.FlxTileFrames;
|
|||
import flixel.math.FlxPoint;
|
||||
import funkin.play.notes.SustainTrail;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import flixel.math.FlxMath;
|
||||
|
||||
/**
|
||||
* A sprite that can be used to display the trail of a hold note in a chart.
|
||||
|
@ -42,9 +43,12 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
* Set the height directly, to a value in pixels.
|
||||
* @param h The desired height in pixels.
|
||||
*/
|
||||
public function setHeightDirectly(h:Float)
|
||||
public function setHeightDirectly(h:Float, ?lerp:Bool = false)
|
||||
{
|
||||
sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS);
|
||||
if (lerp != null && lerp) sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25);
|
||||
else
|
||||
sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS);
|
||||
|
||||
fullSustainLength = sustainLength;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package funkin.ui.debug.charting.components;
|
||||
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
|
||||
/**
|
||||
* The component which contains the playhead for the chart editor.
|
||||
* This is in a separate component so it can be positioned independently.
|
||||
*/
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/components/playbar-head.xml"))
|
||||
class ChartEditorPlaybarHead extends Box
|
||||
{
|
||||
// Auto-populated
|
||||
// public var playbarHead:HorizontalSlider;
|
||||
// Auto-populated.
|
||||
// public function new() { }
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package funkin.ui.debug.charting.dialogs;
|
||||
|
||||
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams;
|
||||
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/about.xml"))
|
||||
class ChartEditorAboutDialog extends ChartEditorBaseDialog
|
||||
{
|
||||
public function new(state2:ChartEditorState, params2:DialogParams)
|
||||
{
|
||||
super(state2, params2);
|
||||
}
|
||||
|
||||
public static function build(state:ChartEditorState, ?closable:Bool, ?modal:Bool):ChartEditorAboutDialog
|
||||
{
|
||||
var dialog = new ChartEditorAboutDialog(state,
|
||||
{
|
||||
closable: closable ?? true,
|
||||
modal: modal ?? true
|
||||
});
|
||||
|
||||
dialog.showDialog(modal ?? true);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package funkin.ui.debug.charting.dialogs;
|
||||
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
|
||||
import haxe.ui.core.Component;
|
||||
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class ChartEditorBaseDialog extends Dialog
|
||||
{
|
||||
var state:ChartEditorState;
|
||||
var params:DialogParams;
|
||||
|
||||
var locked:Bool = false;
|
||||
|
||||
public function new(state:ChartEditorState, params:DialogParams)
|
||||
{
|
||||
super();
|
||||
|
||||
this.state = state;
|
||||
this.params = params;
|
||||
|
||||
this.destroyOnClose = true;
|
||||
this.closable = params.closable ?? false;
|
||||
|
||||
this.onDialogClosed = event -> onClose(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the dialog is closed.
|
||||
* Override this to add custom behavior.
|
||||
*/
|
||||
public function onClose(event:DialogEvent):Void
|
||||
{
|
||||
state.isHaxeUIDialogOpen = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks this dialog from interaction.
|
||||
* Use this when you want to prevent dialog interaction while another dialog is open.
|
||||
*/
|
||||
public function lock():Void
|
||||
{
|
||||
this.locked = true;
|
||||
|
||||
this.closable = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks the dialog for interaction.
|
||||
*/
|
||||
public function unlock():Void
|
||||
{
|
||||
this.locked = false;
|
||||
|
||||
this.closable = params.closable ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
typedef DialogParams =
|
||||
{
|
||||
?closable:Bool,
|
||||
?modal:Bool
|
||||
};
|
||||
|
||||
typedef DialogDropTarget =
|
||||
{
|
||||
component:Component,
|
||||
handler:String->Void
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
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 haxe.io.Path;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import haxe.ui.notifications.NotificationManager;
|
||||
import haxe.ui.notifications.NotificationType;
|
||||
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-chart.xml"))
|
||||
class ChartEditorUploadChartDialog extends ChartEditorBaseDialog
|
||||
{
|
||||
var dropHandlers:Array<DialogDropTarget> = [];
|
||||
|
||||
public function new(state2:ChartEditorState, params2:DialogParams)
|
||||
{
|
||||
super(state2, params2);
|
||||
|
||||
this.dialogCancel.onClick = (_) -> this.hideDialog(DialogButton.CANCEL);
|
||||
|
||||
this.chartBox.onClick = (_) -> this.onClickChartBox();
|
||||
|
||||
this.chartBox.onMouseOver = function(_event) {
|
||||
if (this.locked) return;
|
||||
this.chartBox.swapClass('upload-bg', 'upload-bg-hover');
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
this.chartBox.onMouseOut = function(_event) {
|
||||
this.chartBox.swapClass('upload-bg-hover', 'upload-bg');
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
|
||||
dropHandlers.push({component: this.chartBox, handler: this.onDropFileChartBox});
|
||||
}
|
||||
|
||||
public static function build(state:ChartEditorState, ?closable:Bool, ?modal:Bool):ChartEditorUploadChartDialog
|
||||
{
|
||||
var dialog = new ChartEditorUploadChartDialog(state,
|
||||
{
|
||||
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.
|
||||
state.openWelcomeDialog(this.closable);
|
||||
}
|
||||
|
||||
for (dropTarget in dropHandlers)
|
||||
{
|
||||
state.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();
|
||||
FileUtil.browseForBinaryFile('Open Chart', [FileUtil.FILE_EXTENSION_INFO_FNFC], onSelectFile, onCancelBrowse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString());
|
||||
if (result != null)
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}',
|
||||
type: result.length == 0 ? NotificationType.Success : NotificationType.Warning,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
this.hideDialog(DialogButton.APPLY);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Failed to load chart (${path.toString()})',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Failed to load chart (${path.toString()}): ${err}',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Array<String>> = ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes);
|
||||
if (result != null)
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded chart (${selectedFile.name})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
|
||||
if (selectedFile.fullPath != null) state.currentWorkingFilePath = selectedFile.fullPath;
|
||||
this.hideDialog(DialogButton.APPLY);
|
||||
}
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Failed to load chart (${selectedFile.name}): ${err}',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onCancelBrowse():Void
|
||||
{
|
||||
this.unlock();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
package funkin.ui.debug.charting.dialogs;
|
||||
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog;
|
||||
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.util.SortUtil;
|
||||
import haxe.ui.components.Label;
|
||||
import haxe.ui.components.Link;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
|
||||
import haxe.ui.core.Component;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.events.UIEvent;
|
||||
import haxe.ui.notifications.NotificationManager;
|
||||
import haxe.ui.notifications.NotificationType;
|
||||
|
||||
/**
|
||||
* Builds and opens a dialog letting the user create a new chart, open a recent chart, or load from a template.
|
||||
* Opens when the chart editor first opens.
|
||||
*/
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/welcome.xml"))
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class ChartEditorWelcomeDialog extends ChartEditorBaseDialog
|
||||
{
|
||||
/**
|
||||
* @param closable Whether the dialog can be closed by the user.
|
||||
* @param modal Whether the dialog is locked to the center of the screen (with a dark overlay behind it).
|
||||
*/
|
||||
public function new(state2:ChartEditorState, params2:DialogParams)
|
||||
{
|
||||
super(state2, params2);
|
||||
|
||||
this.splashBrowse.onClick = _ -> onClickButtonBrowse();
|
||||
this.splashCreateFromSongBasicOnly.onClick = _ -> onClickLinkCreateBasicOnly();
|
||||
this.splashCreateFromSongErectOnly.onClick = _ -> onClickLinkCreateErectOnly();
|
||||
this.splashCreateFromSongBasicErect.onClick = _ -> onClickLinkCreateBasicErect();
|
||||
this.splashImportChartLegacy.onClick = _ -> onClickLinkImportChartLegacy();
|
||||
|
||||
// Add items to the Recent Charts list
|
||||
#if sys
|
||||
for (chartPath in state.previousWorkingFilePaths)
|
||||
{
|
||||
if (chartPath == null) continue;
|
||||
this.addRecentFilePath(state, chartPath);
|
||||
}
|
||||
#else
|
||||
this.addHTML5RecentFileMessage();
|
||||
#end
|
||||
|
||||
// Add items to the Load From Template list
|
||||
this.buildTemplateSongList(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param state The current state of the chart editor.
|
||||
* @return A newly created `ChartEditorWelcomeDialog`.
|
||||
*/
|
||||
public static function build(state:ChartEditorState, ?closable:Bool, ?modal:Bool):ChartEditorWelcomeDialog
|
||||
{
|
||||
var dialog = new ChartEditorWelcomeDialog(state,
|
||||
{
|
||||
closable: closable ?? false,
|
||||
modal: modal ?? true
|
||||
});
|
||||
|
||||
dialog.showDialog(modal ?? true);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public override function onClose(event:DialogEvent):Void
|
||||
{
|
||||
super.onClose(event);
|
||||
state.stopWelcomeMusic();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a file path to the "Open Recent" scroll box on the left.
|
||||
* @param path
|
||||
*/
|
||||
public function addRecentFilePath(state:ChartEditorState, chartPath:String):Void
|
||||
{
|
||||
var linkRecentChart:Link = new Link();
|
||||
linkRecentChart.text = chartPath;
|
||||
linkRecentChart.onClick = function(_event) {
|
||||
this.hideDialog(DialogButton.CANCEL);
|
||||
state.stopWelcomeMusic();
|
||||
|
||||
// Load chart from file
|
||||
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, chartPath);
|
||||
if (result != null)
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: result.length == 0 ? 'Loaded chart (${chartPath.toString()})' : 'Loaded chart (${chartPath.toString()})\n${result.join("\n")}',
|
||||
type: result.length == 0 ? NotificationType.Success : NotificationType.Warning,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
else
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Failed to load chart (${chartPath.toString()})',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
if (!FileUtil.doesFileExist(chartPath))
|
||||
{
|
||||
trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...');
|
||||
linkRecentChart.disabled = true;
|
||||
}
|
||||
|
||||
splashRecentContainer.addComponent(linkRecentChart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a string message to the "Open Recent" scroll box on the left.
|
||||
* Only displays on platforms which don't support direct file system access.
|
||||
*/
|
||||
public function addHTML5RecentFileMessage():Void
|
||||
{
|
||||
var webLoadLabel:Label = new Label();
|
||||
webLoadLabel.text = 'Click the button below to load a chart file (.fnfc) from your computer.';
|
||||
|
||||
splashRecentContainer.addComponent(webLoadLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all the links to the "Create From Template" scroll box on the right.
|
||||
*/
|
||||
public function buildTemplateSongList(state:ChartEditorState):Void
|
||||
{
|
||||
var songList:Array<String> = SongRegistry.instance.listEntryIds();
|
||||
songList.sort(SortUtil.alphabetically);
|
||||
|
||||
for (targetSongId in songList)
|
||||
{
|
||||
var songData:Null<Song> = SongRegistry.instance.fetchEntry(targetSongId);
|
||||
if (songData == null) continue;
|
||||
|
||||
var songName:Null<String> = songData.getDifficulty('normal')?.songName;
|
||||
if (songName == null) songName = songData.getDifficulty()?.songName;
|
||||
if (songName == null) // Still null?
|
||||
{
|
||||
trace('[WARN] Could not fetch song name for ${targetSongId}');
|
||||
continue;
|
||||
}
|
||||
|
||||
this.addTemplateSong(songName, targetSongId, (_) -> {
|
||||
this.hideDialog(DialogButton.CANCEL);
|
||||
state.stopWelcomeMusic();
|
||||
|
||||
// Load song from template
|
||||
state.loadSongAsTemplate(targetSongId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loadTemplateCb The callback to call when the user clicks the link. The callback should load the song ID from the template.
|
||||
*/
|
||||
public function addTemplateSong(songName:String, songId:String, onClickCb:(MouseEvent) -> Void):Void
|
||||
{
|
||||
var linkTemplateSong:Link = new Link();
|
||||
linkTemplateSong.text = songName;
|
||||
linkTemplateSong.onClick = onClickCb;
|
||||
|
||||
this.splashTemplateContainer.addComponent(linkTemplateSong);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks the "Browse Chart" button in the dialog.
|
||||
* Reassign this function to change the behavior.
|
||||
*/
|
||||
public function onClickButtonBrowse():Void
|
||||
{
|
||||
// Hide the welcome dialog
|
||||
this.hideDialog(DialogButton.CANCEL);
|
||||
state.stopWelcomeMusic();
|
||||
|
||||
// Open the "Open Chart" dialog
|
||||
state.openBrowseFNFC(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks the "Create From Template: Easy/Normal/Hard Only" link in the dialog.
|
||||
* Reassign this function to change the behavior.
|
||||
*/
|
||||
public function onClickLinkCreateBasicOnly():Void
|
||||
{
|
||||
// Hide the welcome dialog
|
||||
this.hideDialog(DialogButton.CANCEL);
|
||||
state.stopWelcomeMusic();
|
||||
|
||||
//
|
||||
// Create Song Wizard
|
||||
//
|
||||
state.openCreateSongWizardBasicOnly(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks the "Create From Template: Erect/Nightmare Only" link in the dialog.
|
||||
* Reassign this function to change the behavior.
|
||||
*/
|
||||
public function onClickLinkCreateErectOnly():Void
|
||||
{
|
||||
// Hide the welcome dialog
|
||||
this.hideDialog(DialogButton.CANCEL);
|
||||
state.stopWelcomeMusic();
|
||||
|
||||
//
|
||||
// Create Song Wizard
|
||||
//
|
||||
state.openCreateSongWizardErectOnly(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks the "Create From Template: Easy/Normal/Hard/Erect/Nightmare" link in the dialog.
|
||||
* Reassign this function to change the behavior.
|
||||
*/
|
||||
public function onClickLinkCreateBasicErect():Void
|
||||
{
|
||||
// Hide the welcome dialog
|
||||
this.hideDialog(DialogButton.CANCEL);
|
||||
state.stopWelcomeMusic();
|
||||
|
||||
//
|
||||
// Create Song Wizard
|
||||
//
|
||||
state.openCreateSongWizardBasicErect(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks the "Import Chart: FNF Legacy" link in the dialog.
|
||||
* Reassign this function to change the behavior.
|
||||
*/
|
||||
public function onClickLinkImportChartLegacy():Void
|
||||
{
|
||||
// Hide the welcome dialog
|
||||
this.hideDialog(DialogButton.CANCEL);
|
||||
state.stopWelcomeMusic();
|
||||
|
||||
// Open the "Import Chart" dialog
|
||||
state.openImportChartWizard('legacy', false);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,11 +1,10 @@
|
|||
package funkin.ui.debug.charting.handlers;
|
||||
|
||||
import funkin.util.VersionUtil;
|
||||
import haxe.ui.notifications.NotificationType;
|
||||
import funkin.util.DateUtil;
|
||||
import haxe.io.Path;
|
||||
import funkin.util.SerializerUtil;
|
||||
import haxe.ui.notifications.NotificationManager;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.util.FileUtil.FileWriteMode;
|
||||
import haxe.io.Bytes;
|
||||
|
@ -22,6 +21,8 @@ import funkin.data.song.importer.ChartManifestData;
|
|||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class ChartEditorImportExportHandler
|
||||
{
|
||||
public static final BACKUPS_PATH:String = './backups/';
|
||||
|
||||
/**
|
||||
* Fetch's a song's existing chart and audio and loads it, replacing the current song.
|
||||
*/
|
||||
|
@ -100,15 +101,7 @@ class ChartEditorImportExportHandler
|
|||
|
||||
state.refreshMetadataToolbox();
|
||||
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded song (${rawSongMetadata[0].songName})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
state.success('Success', 'Loaded song (${rawSongMetadata[0].songName})');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -318,11 +311,56 @@ class ChartEditorImportExportHandler
|
|||
return warnings;
|
||||
}
|
||||
|
||||
public static function getLatestBackupPath():Null<String>
|
||||
{
|
||||
#if sys
|
||||
var entries:Array<String> = sys.FileSystem.readDirectory(BACKUPS_PATH);
|
||||
entries.sort(SortUtil.alphabetically);
|
||||
|
||||
var latestBackupPath:Null<String> = entries[(entries.length - 1)];
|
||||
|
||||
if (latestBackupPath == null) return null;
|
||||
return haxe.io.Path.join([BACKUPS_PATH, latestBackupPath]);
|
||||
#else
|
||||
return null;
|
||||
#end
|
||||
}
|
||||
|
||||
public static function getLatestBackupDate():Null<Date>
|
||||
{
|
||||
#if sys
|
||||
var latestBackupPath:Null<String> = getLatestBackupPath();
|
||||
if (latestBackupPath == null) return null;
|
||||
|
||||
var latestBackupName:String = haxe.io.Path.withoutDirectory(latestBackupPath);
|
||||
latestBackupName = haxe.io.Path.withoutExtension(latestBackupName);
|
||||
|
||||
var parts = latestBackupName.split('-');
|
||||
|
||||
// var chart:String = parts[0];
|
||||
// var editor:String = parts[1];
|
||||
var year:Int = Std.parseInt(parts[2] ?? '0') ?? 0;
|
||||
var month:Int = Std.parseInt(parts[3] ?? '1') ?? 1;
|
||||
var day:Int = Std.parseInt(parts[4] ?? '0') ?? 0;
|
||||
var hour:Int = Std.parseInt(parts[5] ?? '0') ?? 0;
|
||||
var minute:Int = Std.parseInt(parts[6] ?? '0') ?? 0;
|
||||
var second:Int = Std.parseInt(parts[7] ?? '0') ?? 0;
|
||||
|
||||
var date:Date = new Date(year, month - 1, day, hour, minute, second);
|
||||
return date;
|
||||
#else
|
||||
return null;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* @param force Whether to export without prompting. `false` will prompt the user for a location.
|
||||
* @param targetPath where to export if `force` is `true`. If `null`, will export to the `backups` folder.
|
||||
* @param onSaveCb Callback for when the file is saved.
|
||||
* @param onCancelCb Callback for when saving is cancelled.
|
||||
*/
|
||||
public static function exportAllSongData(state:ChartEditorState, force:Bool = false, ?targetPath:String):Void
|
||||
public static function exportAllSongData(state:ChartEditorState, force:Bool = false, targetPath:Null<String>, ?onSaveCb:String->Void,
|
||||
?onCancelCb:Void->Void):Void
|
||||
{
|
||||
var zipEntries:Array<haxe.zip.Entry> = [];
|
||||
|
||||
|
@ -369,13 +407,13 @@ class ChartEditorImportExportHandler
|
|||
// Force writing to a generic path (autosave or crash recovery)
|
||||
targetMode = Skip;
|
||||
targetPath = Path.join([
|
||||
'./backups/',
|
||||
BACKUPS_PATH,
|
||||
'chart-editor-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}'
|
||||
]);
|
||||
// We have to force write because the program will die before the save dialog is closed.
|
||||
trace('Force exporting to $targetPath...');
|
||||
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
|
||||
|
||||
if (onSaveCb != null) onSaveCb(targetPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -383,7 +421,7 @@ class ChartEditorImportExportHandler
|
|||
trace('Force exporting to $targetPath...');
|
||||
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
|
||||
state.saveDataDirty = false;
|
||||
|
||||
if (onSaveCb != null) onSaveCb(targetPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -400,11 +438,13 @@ class ChartEditorImportExportHandler
|
|||
trace('Saved to "${paths[0]}"');
|
||||
state.currentWorkingFilePath = paths[0];
|
||||
state.applyWindowTitle();
|
||||
if (onSaveCb != null) onSaveCb(paths[0]);
|
||||
}
|
||||
};
|
||||
|
||||
var onCancel:Void->Void = function() {
|
||||
trace('Export cancelled.');
|
||||
if (onCancelCb != null) onCancelCb();
|
||||
};
|
||||
|
||||
trace('Exporting to user-defined location...');
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
package funkin.ui.debug.charting.handlers;
|
||||
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.containers.HBox;
|
||||
import haxe.ui.notifications.Notification;
|
||||
import haxe.ui.notifications.NotificationManager;
|
||||
import haxe.ui.notifications.NotificationType;
|
||||
|
||||
class ChartEditorNotificationHandler
|
||||
{
|
||||
public static function setupNotifications(state:ChartEditorState):Void
|
||||
{
|
||||
// Setup notifications.
|
||||
@:privateAccess
|
||||
NotificationManager.GUTTER_SIZE = 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification with a checkmark indicating success.
|
||||
* @param state The current state of the chart editor.
|
||||
*/
|
||||
public static function success(state:ChartEditorState, title:String, body:String):Notification
|
||||
{
|
||||
return sendNotification(title, body, NotificationType.Success);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification with a warning icon.
|
||||
* @param state The current state of the chart editor.
|
||||
*/
|
||||
public static function warning(state:ChartEditorState, title:String, body:String):Notification
|
||||
{
|
||||
return sendNotification(title, body, NotificationType.Warning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification with a warning icon.
|
||||
* @param state The current state of the chart editor.
|
||||
*/
|
||||
public static inline function warn(state:ChartEditorState, title:String, body:String):Notification
|
||||
{
|
||||
return warning(state, title, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification with a cross indicating an error.
|
||||
* @param state The current state of the chart editor.
|
||||
*/
|
||||
public static function error(state:ChartEditorState, title:String, body:String):Notification
|
||||
{
|
||||
return sendNotification(title, body, NotificationType.Error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification with a cross indicating failure.
|
||||
* @param state The current state of the chart editor.
|
||||
*/
|
||||
public static inline function failure(state:ChartEditorState, title:String, body:String):Notification
|
||||
{
|
||||
return error(state, title, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification with an info icon.
|
||||
* @param state The current state of the chart editor.
|
||||
*/
|
||||
public static function info(state:ChartEditorState, title:String, body:String):Notification
|
||||
{
|
||||
return sendNotification(title, body, NotificationType.Info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification with an info icon and one or more actions.
|
||||
* @param state The current state of the chart editor.
|
||||
* @param title The title of the notification.
|
||||
* @param body The body of the notification.
|
||||
* @param actions The actions to add to the notification.
|
||||
* @return The notification that was sent.
|
||||
*/
|
||||
public static function infoWithActions(state:ChartEditorState, title:String, body:String, actions:Array<NotificationAction>):Notification
|
||||
{
|
||||
return sendNotification(title, body, NotificationType.Info, actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all active notifications.
|
||||
* @param state The current state of the chart editor.
|
||||
*/
|
||||
public static function clearNotifications(state:ChartEditorState):Void
|
||||
{
|
||||
NotificationManager.instance.clearNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a specific notification.
|
||||
* @param state The current state of the chart editor.
|
||||
* @param notif The notification to clear.
|
||||
*/
|
||||
public static function clearNotification(state:ChartEditorState, notif:Notification):Void
|
||||
{
|
||||
NotificationManager.instance.removeNotification(notif);
|
||||
}
|
||||
|
||||
static function sendNotification(title:String, body:String, ?type:NotificationType, ?actions:Array<NotificationAction>):Notification
|
||||
{
|
||||
#if !mac
|
||||
var actionNames:Array<String> = actions == null ? [] : actions.map(action -> action.text);
|
||||
|
||||
var notif = NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: title,
|
||||
body: body,
|
||||
type: type ?? NotificationType.Default,
|
||||
expiryMs: Constants.NOTIFICATION_DISMISS_TIME,
|
||||
actions: actionNames
|
||||
});
|
||||
|
||||
if (actionNames.length > 0)
|
||||
{
|
||||
// TODO: Tell Ian that this is REALLY dumb.
|
||||
var actionsContainer:HBox = notif.findComponent('actionsContainer', HBox);
|
||||
actionsContainer.walkComponents(function(component) {
|
||||
if (Std.isOfType(component, Button))
|
||||
{
|
||||
var button:Button = cast component;
|
||||
var action:Null<NotificationAction> = actions.find(action -> action.text == button.text);
|
||||
if (action != null && action.callback != null)
|
||||
{
|
||||
button.onClick = function(_) {
|
||||
action.callback();
|
||||
};
|
||||
}
|
||||
}
|
||||
return true; // Continue walking.
|
||||
});
|
||||
}
|
||||
|
||||
return notif;
|
||||
#else
|
||||
trace('WARNING: Notifications are not supported on Mac OS.');
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
typedef NotificationAction =
|
||||
{
|
||||
text:String,
|
||||
callback:Void->Void
|
||||
}
|
|
@ -7,28 +7,28 @@ class ChartEditorShortcutHandler
|
|||
{
|
||||
public static function applyPlatformShortcutText(state:ChartEditorState):Void
|
||||
{
|
||||
state.setComponentShortcutText('menubarItemNewChart', ctrlOrCmd('N'));
|
||||
state.setComponentShortcutText('menubarItemOpenChart', ctrlOrCmd('O'));
|
||||
state.setComponentShortcutText('menubarItemSaveChartAs', ctrlOrCmd(shift('S')));
|
||||
state.setComponentShortcutText('menubarItemExit', ctrlOrCmd('Q'));
|
||||
state.menubarItemNewChart.shortcutText = ctrlOrCmd('N');
|
||||
state.menubarItemOpenChart.shortcutText = ctrlOrCmd('O');
|
||||
state.menubarItemSaveChartAs.shortcutText = ctrlOrCmd(shift('S'));
|
||||
state.menubarItemExit.shortcutText = ctrlOrCmd('Q');
|
||||
|
||||
state.setComponentShortcutText('menubarItemUndo', ctrlOrCmd('Z'));
|
||||
state.setComponentShortcutText('menubarItemRedo', ctrlOrCmd('Y'));
|
||||
state.setComponentShortcutText('menubarItemCut', ctrlOrCmd('X'));
|
||||
state.setComponentShortcutText('menubarItemCopy', ctrlOrCmd('C'));
|
||||
state.setComponentShortcutText('menubarItemPaste', ctrlOrCmd('V'));
|
||||
state.menubarItemUndo.shortcutText = ctrlOrCmd('Z');
|
||||
state.menubarItemRedo.shortcutText = ctrlOrCmd('Y');
|
||||
state.menubarItemCut.shortcutText = ctrlOrCmd('X');
|
||||
state.menubarItemCopy.shortcutText = ctrlOrCmd('C');
|
||||
state.menubarItemPaste.shortcutText = ctrlOrCmd('V');
|
||||
|
||||
state.setComponentShortcutText('menubarItemSelectAll', ctrlOrCmd('A'));
|
||||
state.setComponentShortcutText('menubarItemSelectInverse', ctrlOrCmd('I'));
|
||||
state.setComponentShortcutText('menubarItemSelectNone', ctrlOrCmd('D'));
|
||||
state.setComponentShortcutText('menubarItemSelectBeforeCursor', shift('Home'));
|
||||
state.setComponentShortcutText('menubarItemSelectAfterCursor', shift('End'));
|
||||
state.menubarItemSelectAll.shortcutText = ctrlOrCmd('A');
|
||||
state.menubarItemSelectInverse.shortcutText = ctrlOrCmd('I');
|
||||
state.menubarItemSelectNone.shortcutText = ctrlOrCmd('D');
|
||||
state.menubarItemSelectBeforeCursor.shortcutText = shift('Home');
|
||||
state.menubarItemSelectAfterCursor.shortcutText = shift('End');
|
||||
|
||||
state.setComponentShortcutText('menubarItemDifficultyDown', ctrlOrCmd('←'));
|
||||
state.setComponentShortcutText('menubarItemDifficultyUp', ctrlOrCmd('→'));
|
||||
state.menubarItemDifficultyDown.shortcutText = ctrlOrCmd('←');
|
||||
state.menubarItemDifficultyUp.shortcutText = ctrlOrCmd('→');
|
||||
|
||||
state.setComponentShortcutText('menubarItemPlaytestFull', 'Enter');
|
||||
state.setComponentShortcutText('menubarItemPlaytestMinimal', shift('Enter'));
|
||||
state.menubarItemPlaytestFull.shortcutText = 'Enter';
|
||||
state.menubarItemPlaytestMinimal.shortcutText = shift('Enter');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,6 +18,7 @@ import funkin.play.event.SongEvent;
|
|||
import funkin.play.song.SongSerializer;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import haxe.ui.RuntimeComponentBuilder;
|
||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||
import funkin.ui.haxeui.components.CharacterPlayer;
|
||||
import funkin.util.FileUtil;
|
||||
|
@ -217,7 +218,7 @@ class ChartEditorToolboxHandler
|
|||
|
||||
static function buildToolboxNoteDataLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
|
||||
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
|
@ -226,7 +227,7 @@ class ChartEditorToolboxHandler
|
|||
toolbox.y = 100;
|
||||
|
||||
toolbox.onDialogClosed = function(event:DialogEvent) {
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false);
|
||||
state.menubarItemToggleToolboxNotes.selected = false;
|
||||
}
|
||||
|
||||
var toolboxNotesNoteKind:Null<DropDown> = toolbox.findComponent('toolboxNotesNoteKind', DropDown);
|
||||
|
@ -269,7 +270,7 @@ class ChartEditorToolboxHandler
|
|||
|
||||
static function buildToolboxEventDataLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
|
||||
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
|
@ -278,7 +279,7 @@ class ChartEditorToolboxHandler
|
|||
toolbox.y = 150;
|
||||
|
||||
toolbox.onDialogClosed = function(event:DialogEvent) {
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false);
|
||||
state.menubarItemToggleToolboxEvents.selected = false;
|
||||
}
|
||||
|
||||
var toolboxEventsEventKind:Null<DropDown> = toolbox.findComponent('toolboxEventsEventKind', DropDown);
|
||||
|
@ -418,7 +419,7 @@ class ChartEditorToolboxHandler
|
|||
|
||||
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
||||
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
|
@ -427,7 +428,7 @@ class ChartEditorToolboxHandler
|
|||
toolbox.y = 200;
|
||||
|
||||
toolbox.onDialogClosed = function(event:UIEvent) {
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false);
|
||||
state.menubarItemToggleToolboxDifficulty.selected = false;
|
||||
}
|
||||
|
||||
var difficultyToolboxAddVariation:Null<Button> = toolbox.findComponent('difficultyToolboxAddVariation', Button);
|
||||
|
@ -505,7 +506,7 @@ class ChartEditorToolboxHandler
|
|||
|
||||
static function buildToolboxMetadataLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
|
@ -514,7 +515,7 @@ class ChartEditorToolboxHandler
|
|||
toolbox.y = 250;
|
||||
|
||||
toolbox.onDialogClosed = function(event:UIEvent) {
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false);
|
||||
state.menubarItemToggleToolboxMetadata.selected = false;
|
||||
}
|
||||
|
||||
var inputSongName:Null<TextField> = toolbox.findComponent('inputSongName', TextField);
|
||||
|
@ -667,7 +668,7 @@ class ChartEditorToolboxHandler
|
|||
|
||||
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
|
||||
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
|
@ -676,7 +677,7 @@ class ChartEditorToolboxHandler
|
|||
toolbox.y = 350;
|
||||
|
||||
toolbox.onDialogClosed = function(event:DialogEvent) {
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false);
|
||||
state.menubarItemToggleToolboxPlayerPreview.selected = false;
|
||||
}
|
||||
|
||||
var charPlayer:Null<CharacterPlayer> = toolbox.findComponent('charPlayer');
|
||||
|
@ -696,7 +697,7 @@ class ChartEditorToolboxHandler
|
|||
|
||||
static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
|
||||
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
|
@ -705,7 +706,7 @@ class ChartEditorToolboxHandler
|
|||
toolbox.y = 350;
|
||||
|
||||
toolbox.onDialogClosed = (event:DialogEvent) -> {
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxOpponentPreview', false);
|
||||
state.menubarItemToggleToolboxOpponentPreview.selected = false;
|
||||
}
|
||||
|
||||
var charPlayer:Null<CharacterPlayer> = toolbox.findComponent('charPlayer');
|
||||
|
|
|
@ -5,6 +5,8 @@ package funkin.ui.debug.charting;
|
|||
using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler;
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler;
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler;
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler;
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorThemeHandler;
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorToolboxHandler;
|
||||
#end
|
||||
|
|
|
@ -96,7 +96,7 @@ class CLIUtil
|
|||
|
||||
static function printUsage():Void
|
||||
{
|
||||
trace('Usage: Funkin.exe [--chart <chart>]');
|
||||
trace('Usage: Funkin.exe [--chart <chart>] [--help] [--version]');
|
||||
}
|
||||
|
||||
static function buildDefaultParams():CLIParams
|
||||
|
|
|
@ -12,4 +12,11 @@ class DateUtil
|
|||
return
|
||||
'${date.getFullYear()}-${Std.string(date.getMonth() + 1).lpad('0', 2)}-${Std.string(date.getDate()).lpad('0', 2)}-${Std.string(date.getHours()).lpad('0', 2)}-${Std.string(date.getMinutes()).lpad('0', 2)}-${Std.string(date.getSeconds()).lpad('0', 2)}';
|
||||
}
|
||||
|
||||
public static function generateCleanTimestamp(?date:Date = null):String
|
||||
{
|
||||
if (date == null) date = Date.now();
|
||||
|
||||
return '${DateTools.format(date, '%B %d, %Y')} at ${DateTools.format(date, '%I:%M %p')}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,10 @@ import haxe.io.Path;
|
|||
import openfl.net.FileReference;
|
||||
import openfl.events.Event;
|
||||
import openfl.events.IOErrorEvent;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import haxe.ui.containers.dialogs.Dialogs.SelectedFileInfo;
|
||||
import haxe.ui.containers.dialogs.Dialogs.FileDialogExtensionInfo;
|
||||
|
||||
/**
|
||||
* Utilities for reading and writing files on various platforms.
|
||||
|
@ -17,32 +21,81 @@ class FileUtil
|
|||
public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc");
|
||||
public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip");
|
||||
|
||||
public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo =
|
||||
{
|
||||
extension: 'fnfc',
|
||||
label: 'Friday Night Funkin\' Chart',
|
||||
};
|
||||
public static final FILE_EXTENSION_INFO_ZIP:FileDialogExtensionInfo =
|
||||
{
|
||||
extension: 'zip',
|
||||
label: 'ZIP Archive',
|
||||
};
|
||||
|
||||
/**
|
||||
* Browses for a single file, then calls `onSelect(path)` when a path chosen.
|
||||
* Note that on HTML5 this will immediately fail, you should call `openFile(onOpen:Resource->Void)` instead.
|
||||
* Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected.
|
||||
* Powered by HaxeUI, so it works on all platforms.
|
||||
* File contents will be binary, not String.
|
||||
*
|
||||
* @param typeFilter Filters what kinds of files can be selected.
|
||||
* @return Whether the file dialog was opened successfully.
|
||||
* @param typeFilter
|
||||
* @param onSelect A callback that provides a `SelectedFileInfo` object when a file is selected.
|
||||
* @param onCancel A callback that is called when the user closes the dialog without selecting a file.
|
||||
*/
|
||||
public static function browseForFile(?typeFilter:Array<FileFilter>, ?onSelect:String->Void, ?onCancel:Void->Void, ?defaultPath:String,
|
||||
?dialogTitle:String):Bool
|
||||
public static function browseForBinaryFile(dialogTitle:String, ?typeFilter:Array<FileDialogExtensionInfo>, ?onSelect:SelectedFileInfo->Void,
|
||||
?onCancel:Void->Void)
|
||||
{
|
||||
#if desktop
|
||||
var filter:String = convertTypeFilter(typeFilter);
|
||||
var onComplete = function(button, selectedFiles) {
|
||||
if (button == DialogButton.OK && selectedFiles.length > 0)
|
||||
{
|
||||
onSelect(selectedFiles[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
var fileDialog:FileDialog = new FileDialog();
|
||||
if (onSelect != null) fileDialog.onSelect.add(onSelect);
|
||||
if (onCancel != null) fileDialog.onCancel.add(onCancel);
|
||||
Dialogs.openFile(onComplete,
|
||||
{
|
||||
readContents: true,
|
||||
readAsBinary: true, // Binary
|
||||
multiple: false,
|
||||
extensions: typeFilter ?? [],
|
||||
title: dialogTitle,
|
||||
});
|
||||
}
|
||||
|
||||
fileDialog.browse(OPEN, filter, defaultPath, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
onCancel();
|
||||
return false;
|
||||
#else
|
||||
onCancel();
|
||||
return false;
|
||||
#end
|
||||
/**
|
||||
* Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected.
|
||||
* Powered by HaxeUI, so it works on all platforms.
|
||||
* File contents will be a String, not binary.
|
||||
*
|
||||
* @param typeFilter
|
||||
* @param onSelect A callback that provides a `SelectedFileInfo` object when a file is selected.
|
||||
* @param onCancel A callback that is called when the user closes the dialog without selecting a file.
|
||||
*/
|
||||
public static function browseForTextFile(dialogTitle:String, ?typeFilter:Array<FileDialogExtensionInfo>, ?onSelect:SelectedFileInfo->Void,
|
||||
?onCancel:Void->Void)
|
||||
{
|
||||
var onComplete = function(button, selectedFiles) {
|
||||
if (button == DialogButton.OK && selectedFiles.length > 0)
|
||||
{
|
||||
onSelect(selectedFiles[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
Dialogs.openFile(onComplete,
|
||||
{
|
||||
readContents: true,
|
||||
readAsBinary: false, // Text
|
||||
multiple: false,
|
||||
extensions: typeFilter ?? [],
|
||||
title: dialogTitle,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,11 +110,9 @@ class FileUtil
|
|||
{
|
||||
#if desktop
|
||||
var filter:String = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog:FileDialog = new FileDialog();
|
||||
if (onSelect != null) fileDialog.onSelect.add(onSelect);
|
||||
if (onCancel != null) fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.browse(OPEN_DIRECTORY, filter, defaultPath, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
|
@ -84,11 +135,9 @@ class FileUtil
|
|||
{
|
||||
#if desktop
|
||||
var filter:String = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog:FileDialog = new FileDialog();
|
||||
if (onSelect != null) fileDialog.onSelectMultiple.add(onSelect);
|
||||
if (onCancel != null) fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.browse(OPEN_MULTIPLE, filter, defaultPath, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
|
@ -112,11 +161,9 @@ class FileUtil
|
|||
{
|
||||
#if desktop
|
||||
var filter:String = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog:FileDialog = new FileDialog();
|
||||
if (onSelect != null) fileDialog.onSelect.add(onSelect);
|
||||
if (onCancel != null) fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.browse(SAVE, filter, defaultPath, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
|
@ -128,48 +175,6 @@ class FileUtil
|
|||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Browses for a single file location, then reads it and passes it to `onOpen(resource:haxe.io.Bytes)`.
|
||||
* Works great on desktop and HTML5.
|
||||
*
|
||||
* @param typeFilter TODO What does this do?
|
||||
* @return Whether the file dialog was opened successfully.
|
||||
*/
|
||||
public static function openFile(?typeFilter:Array<FileFilter>, ?onOpen:Bytes->Void, ?onCancel:Void->Void, ?defaultPath:String, ?dialogTitle:String):Bool
|
||||
{
|
||||
#if desktop
|
||||
var filter:String = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog:FileDialog = new FileDialog();
|
||||
if (onOpen != null) fileDialog.onOpen.add(onOpen);
|
||||
if (onCancel != null) fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.open(filter, defaultPath, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
var onFileLoaded:Event->Void = function(event) {
|
||||
var loadedFileRef:FileReference = event.target;
|
||||
trace('Loaded file: ' + loadedFileRef.name);
|
||||
onOpen(loadedFileRef.data);
|
||||
}
|
||||
|
||||
var onFileSelected:Event->Void = function(event) {
|
||||
var selectedFileRef:FileReference = event.target;
|
||||
trace('Selected file: ' + selectedFileRef.name);
|
||||
selectedFileRef.addEventListener(Event.COMPLETE, onFileLoaded);
|
||||
selectedFileRef.load();
|
||||
}
|
||||
|
||||
var fileRef:FileReference = new FileReference();
|
||||
fileRef.addEventListener(Event.SELECT, onFileSelected);
|
||||
fileRef.browse(typeFilter);
|
||||
return true;
|
||||
#else
|
||||
onCancel();
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Browses for a single file location, then writes the provided `haxe.io.Bytes` data and calls `onSave(path)` when done.
|
||||
* Works great on desktop and HTML5.
|
||||
|
@ -181,20 +186,16 @@ class FileUtil
|
|||
{
|
||||
#if desktop
|
||||
var filter:String = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog:FileDialog = new FileDialog();
|
||||
if (onSave != null) fileDialog.onSave.add(onSave);
|
||||
if (onCancel != null) fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.save(data, filter, defaultFileName, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null;
|
||||
|
||||
var fileDialog:FileDialog = new FileDialog();
|
||||
if (onSave != null) fileDialog.onSave.add(onSave);
|
||||
if (onCancel != null) fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.save(data, filter, defaultFileName, dialogTitle);
|
||||
return true;
|
||||
#else
|
||||
|
@ -241,17 +242,14 @@ class FileUtil
|
|||
}
|
||||
onSaveAll(paths);
|
||||
}
|
||||
|
||||
trace('Browsing for directory to save individual files to...');
|
||||
#if mac
|
||||
defaultPath = null;
|
||||
#end
|
||||
browseForDirectory(null, onSelectDir, onCancel, defaultPath, 'Choose directory to save all files to...');
|
||||
|
||||
return true;
|
||||
#elseif html5
|
||||
saveFilesAsZIP(resources, onSaveAll, onCancel, defaultPath, force);
|
||||
|
||||
return true;
|
||||
#else
|
||||
onCancel();
|
||||
|
@ -266,15 +264,12 @@ class FileUtil
|
|||
{
|
||||
// Create a ZIP file.
|
||||
var zipBytes:Bytes = createZIPFromEntries(resources);
|
||||
|
||||
var onSave:String->Void = function(path:String) {
|
||||
trace('Saved ${resources.length} files to ZIP at "$path".');
|
||||
if (onSave != null) onSave([path]);
|
||||
};
|
||||
|
||||
// Prompt the user to save the ZIP file.
|
||||
saveFile(zipBytes, [FILE_FILTER_ZIP], onSave, onCancel, defaultPath, 'Save files as ZIP...');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -286,15 +281,12 @@ class FileUtil
|
|||
{
|
||||
// Create a ZIP file.
|
||||
var zipBytes:Bytes = createZIPFromEntries(resources);
|
||||
|
||||
var onSave:String->Void = function(path:String) {
|
||||
trace('Saved FNF file to "$path"');
|
||||
if (onSave != null) onSave([path]);
|
||||
};
|
||||
|
||||
// Prompt the user to save the ZIP file.
|
||||
saveFile(zipBytes, [FILE_FILTER_FNFC], onSave, onCancel, defaultPath, 'Save chart as FNFC...');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -309,10 +301,8 @@ class FileUtil
|
|||
#if desktop
|
||||
// Create a ZIP file.
|
||||
var zipBytes:Bytes = createZIPFromEntries(resources);
|
||||
|
||||
// Write the ZIP.
|
||||
writeBytesToPath(path, zipBytes, mode);
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
|
@ -371,7 +361,6 @@ class FileUtil
|
|||
public static function browseFileReference(callback:FileReference->Void)
|
||||
{
|
||||
var file = new FileReference();
|
||||
|
||||
file.addEventListener(Event.SELECT, function(e) {
|
||||
var selectedFileRef:FileReference = e.target;
|
||||
trace('Selected file: ' + selectedFileRef.name);
|
||||
|
@ -382,7 +371,6 @@ class FileUtil
|
|||
});
|
||||
selectedFileRef.load();
|
||||
});
|
||||
|
||||
file.browse();
|
||||
}
|
||||
|
||||
|
@ -439,7 +427,6 @@ class FileUtil
|
|||
{
|
||||
#if sys
|
||||
createDirIfNotExists(Path.directory(path));
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case Force:
|
||||
|
@ -482,7 +469,6 @@ class FileUtil
|
|||
{
|
||||
#if sys
|
||||
createDirIfNotExists(Path.directory(path));
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case Force:
|
||||
|
@ -557,19 +543,15 @@ class FileUtil
|
|||
public static function getTempDir():String
|
||||
{
|
||||
if (tempDir != null) return tempDir;
|
||||
|
||||
#if sys
|
||||
#if windows
|
||||
var path:String = null;
|
||||
|
||||
for (envName in TEMP_ENV_VARS)
|
||||
{
|
||||
path = Sys.getEnv(envName);
|
||||
|
||||
if (path == '') path = null;
|
||||
if (path != null) break;
|
||||
}
|
||||
|
||||
tempDir = Path.join([path, 'funkin/']);
|
||||
return tempDir;
|
||||
#else
|
||||
|
@ -590,10 +572,8 @@ class FileUtil
|
|||
public static function createZIPFromEntries(entries:Array<Entry>):Bytes
|
||||
{
|
||||
var o:haxe.io.BytesOutput = new haxe.io.BytesOutput();
|
||||
|
||||
var zipWriter:haxe.zip.Writer = new haxe.zip.Writer(o);
|
||||
zipWriter.write(entries.list());
|
||||
|
||||
return o.getBytes();
|
||||
}
|
||||
|
||||
|
@ -601,10 +581,8 @@ class FileUtil
|
|||
{
|
||||
trace('TEST: ' + input.length);
|
||||
trace(input.sub(0, 30).toHex());
|
||||
|
||||
var bytesInput = new haxe.io.BytesInput(input);
|
||||
var zippedEntries = haxe.zip.Reader.readZip(bytesInput);
|
||||
|
||||
var results:Array<Entry> = [];
|
||||
for (entry in zippedEntries)
|
||||
{
|
||||
|
@ -637,7 +615,6 @@ class FileUtil
|
|||
public static function makeZIPEntry(name:String, content:String):Entry
|
||||
{
|
||||
var data:Bytes = haxe.io.Bytes.ofString(content, UTF8);
|
||||
|
||||
return makeZIPEntryFromBytes(name, data);
|
||||
}
|
||||
|
||||
|
@ -653,12 +630,9 @@ class FileUtil
|
|||
return {
|
||||
fileName: name,
|
||||
fileSize: data.length,
|
||||
|
||||
data: data,
|
||||
dataSize: data.length,
|
||||
|
||||
compressed: false,
|
||||
|
||||
fileTime: Date.now(),
|
||||
crc32: null,
|
||||
extraFields: null,
|
||||
|
@ -677,7 +651,6 @@ class FileUtil
|
|||
}
|
||||
filter = filters.join(';');
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package funkin.util;
|
|||
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* Utilities for operating on the current window, such as changing the title.
|
||||
*/
|
||||
|
@ -18,7 +20,7 @@ class WindowUtil
|
|||
* Runs platform-specific code to open a URL in a web browser.
|
||||
* @param targetUrl The URL to open.
|
||||
*/
|
||||
public static function openURL(targetUrl:String)
|
||||
public static function openURL(targetUrl:String):Void
|
||||
{
|
||||
#if CAN_OPEN_LINKS
|
||||
#if linux
|
||||
|
@ -32,6 +34,45 @@ class WindowUtil
|
|||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs platform-specific code to open a path in the file explorer.
|
||||
* @param targetPath The path to open.
|
||||
*/
|
||||
public static function openFolder(targetPath:String):Void
|
||||
{
|
||||
#if CAN_OPEN_LINKS
|
||||
#if windows
|
||||
Sys.command('explorer', [targetPath.replace("/", "\\")]);
|
||||
#elseif mac
|
||||
Sys.command('open', [targetPath]);
|
||||
#elseif linux
|
||||
Sys.command('open', [targetPath]);
|
||||
#end
|
||||
#else
|
||||
throw 'Cannot open URLs on this platform.';
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs platform-specific code to open a file explorer and select a specific file.
|
||||
* @param targetPath The path of the file to select.
|
||||
*/
|
||||
public static function openSelectFile(targetPath:String):Void
|
||||
{
|
||||
#if CAN_OPEN_LINKS
|
||||
#if windows
|
||||
Sys.command('explorer', ["/select," + targetPath.replace("/", "\\")]);
|
||||
#elseif mac
|
||||
Sys.command('open', ["-R", targetPath]);
|
||||
#elseif linux
|
||||
// TODO: unsure of the linux equivalent to opening a folder and then "selecting" a file.
|
||||
Sys.command('open', [targetPath]);
|
||||
#end
|
||||
#else
|
||||
throw 'Cannot open URLs on this platform.';
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatched when the game window is closed.
|
||||
*/
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.util.logging;
|
|||
|
||||
import openfl.Lib;
|
||||
import openfl.events.UncaughtErrorEvent;
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
|
||||
/**
|
||||
* A custom crash handler that writes to a log file and displays a message box.
|
||||
|
@ -9,7 +10,20 @@ import openfl.events.UncaughtErrorEvent;
|
|||
@:nullSafety
|
||||
class CrashHandler
|
||||
{
|
||||
static final LOG_FOLDER = 'logs';
|
||||
public static final LOG_FOLDER = 'logs';
|
||||
|
||||
/**
|
||||
* Called before exiting the game when a standard error occurs, like a thrown exception.
|
||||
* @param message The error message.
|
||||
*/
|
||||
public static var errorSignal(default, null):FlxTypedSignal<String->Void> = new FlxTypedSignal<String->Void>();
|
||||
|
||||
/**
|
||||
* Called before exiting the game when a critical error occurs, like a stack overflow or null object reference.
|
||||
* CAREFUL: The game may be in an unstable state when this is called.
|
||||
* @param message The error message.
|
||||
*/
|
||||
public static var criticalErrorSignal(default, null):FlxTypedSignal<String->Void> = new FlxTypedSignal<String->Void>();
|
||||
|
||||
/**
|
||||
* Initializes
|
||||
|
@ -34,6 +48,8 @@ class CrashHandler
|
|||
{
|
||||
try
|
||||
{
|
||||
errorSignal.dispatch(generateErrorMessage(error));
|
||||
|
||||
#if sys
|
||||
logError(error);
|
||||
#end
|
||||
|
@ -50,6 +66,8 @@ class CrashHandler
|
|||
{
|
||||
try
|
||||
{
|
||||
criticalErrorSignal.dispatch(message);
|
||||
|
||||
#if sys
|
||||
logErrorMessage(message, true);
|
||||
#end
|
||||
|
|
3
source/haxe/ui/backend/flixel/UIStateBase.hx
Normal file
3
source/haxe/ui/backend/flixel/UIStateBase.hx
Normal file
|
@ -0,0 +1,3 @@
|
|||
package haxe.ui.backend.flixel;
|
||||
|
||||
typedef UIStateBase = funkin.ui.MusicBeatState;
|
3
source/haxe/ui/backend/flixel/UISubStateBase.hx
Normal file
3
source/haxe/ui/backend/flixel/UISubStateBase.hx
Normal file
|
@ -0,0 +1,3 @@
|
|||
package haxe.ui.backend.flixel;
|
||||
|
||||
typedef UISubStateBase = funkin.ui.MusicBeatSubState;
|
Loading…
Add table
Reference in a new issue