1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-11-09 00:04:42 +00:00

Merge pull request #230 from FunkinCrew/feature/chart-editor-backup-popup

Chart Editor: Backup Popup
This commit is contained in:
Cameron Taylor 2023-11-23 20:11:56 -05:00 committed by GitHub
commit 1315f7e182
12 changed files with 598 additions and 407 deletions

2
assets

@ -1 +1 @@
Subproject commit f715fd22cd51e71b017fae796cfe564b76dec20e
Subproject commit c6aef3e3e6abc573c75dc5dbc90059ccdb6da83c

View file

@ -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 `[]`

View file

@ -1,5 +1,6 @@
package funkin.ui.debug.charting;
import funkin.util.logging.CrashHandler;
import haxe.ui.containers.menus.MenuBar;
import flixel.addons.display.FlxSliceSprite;
import flixel.addons.display.FlxTiledSprite;
@ -47,7 +48,6 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongDataUtils;
import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;
import funkin.play.stage.StageData;
import funkin.save.Save;
import funkin.ui.debug.charting.commands.AddEventsCommand;
@ -98,8 +98,6 @@ import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.focus.FocusManager;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
import openfl.display.BitmapData;
import funkin.util.FileUtil;
@ -779,6 +777,18 @@ class ChartEditorState extends HaxeUIState
return saveDataDirty;
}
var shouldShowBackupAvailableDialog(get, set):Bool;
function get_shouldShowBackupAvailableDialog():Bool
{
return Save.get().chartEditorHasBackup;
}
function set_shouldShowBackupAvailableDialog(value:Bool):Bool
{
return Save.get().chartEditorHasBackup = value;
}
/**
* Whether the difficulty tree view in the toolbox has been modified and needs to be updated.
* This happens when we add/remove difficulties.
@ -1498,7 +1508,7 @@ class ChartEditorState extends HaxeUIState
buildAdditionalUI();
populateOpenRecentMenu();
ChartEditorShortcutHandler.applyPlatformShortcutText(this);
this.applyPlatformShortcutText();
// Setup the onClick listeners for the UI after it's been created.
setupUIListeners();
@ -1511,33 +1521,28 @@ class ChartEditorState extends HaxeUIState
if (params != null && params.fnfcTargetPath != null)
{
// Chart editor was opened from the command line. Open the FNFC file now!
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(this, params.fnfcTargetPath);
var result:Null<Array<String>> = this.loadFromFNFCPath(params.fnfcTargetPath);
if (result != null)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: result.length == 0 ? 'Loaded chart (${params.fnfcTargetPath})' : 'Loaded chart (${params.fnfcTargetPath})\n${result.join("\n")}',
type: result.length == 0 ? NotificationType.Success : NotificationType.Warning,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
if (result.length == 0)
{
this.success('Loaded Chart', 'Loaded chart (${params.fnfcTargetPath})');
}
else
{
this.warning('Loaded Chart', 'Loaded chart with issues (${params.fnfcTargetPath})\n${result.join("\n")}');
}
}
else
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart (${params.fnfcTargetPath})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
this.error('Failure', 'Failed to load chart (${params.fnfcTargetPath})');
// Song failed to load, open the Welcome dialog so we aren't in a broken state.
ChartEditorDialogHandler.openWelcomeDialog(this, false);
var welcomeDialog = this.openWelcomeDialog(false);
if (shouldShowBackupAvailableDialog)
{
this.openBackupAvailableDialog(welcomeDialog);
}
}
}
else if (params != null && params.targetSongId != null)
@ -1546,7 +1551,11 @@ class ChartEditorState extends HaxeUIState
}
else
{
ChartEditorDialogHandler.openWelcomeDialog(this, false);
var welcomeDialog = this.openWelcomeDialog(false);
if (shouldShowBackupAvailableDialog)
{
this.openBackupAvailableDialog(welcomeDialog);
}
}
}
@ -1583,7 +1592,7 @@ class ChartEditorState extends HaxeUIState
// audioVocalTrackGroup.pitch = save.chartEditorPlaybackSpeed;
}
public function writePreferences():Void
public function writePreferences(hasBackup:Bool):Void
{
var save:Save = Save.get();
@ -1591,8 +1600,11 @@ class ChartEditorState extends HaxeUIState
var filteredWorkingFilePaths:Array<String> = [];
for (chartPath in previousWorkingFilePaths)
if (chartPath != null) filteredWorkingFilePaths.push(chartPath);
save.chartEditorPreviousFiles = filteredWorkingFilePaths;
if (hasBackup) trace('Queuing backup prompt for next time!');
save.chartEditorHasBackup = hasBackup;
save.chartEditorNoteQuant = noteSnapQuantIndex;
save.chartEditorLiveInputStyle = currentLiveInputStyle;
save.chartEditorDownscroll = isViewDownscroll;
@ -1624,36 +1636,27 @@ class ChartEditorState extends HaxeUIState
stopWelcomeMusic();
// Load chart from file
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(this, chartPath);
var result:Null<Array<String>> = this.loadFromFNFCPath(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
if (result.length == 0)
{
this.success('Loaded Chart', 'Loaded chart (${chartPath.toString()})');
}
else
{
this.warning('Loaded Chart', 'Loaded chart with issues (${chartPath.toString()})\n${result.join("\n")}');
}
}
else
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart (${chartPath.toString()})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
this.error('Failure', 'Failed to load chart (${chartPath.toString()})');
}
}
if (!FileUtil.doesFileExist(chartPath))
{
trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...');
trace('Previously loaded chart file (${chartPath.toString()}) does not exist, disabling link...');
menuItemRecentChart.disabled = true;
}
else
@ -1992,9 +1995,7 @@ class ChartEditorState extends HaxeUIState
if (menubar == null) throw "Could not find menubar!";
if (!Preferences.debugDisplay) menubar.paddingLeft = null;
// Setup notifications.
@:privateAccess
NotificationManager.GUTTER_SIZE = 20;
this.setupNotifications();
}
/**
@ -2022,21 +2023,21 @@ class ChartEditorState extends HaxeUIState
// Add functionality to the menu items.
addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseFNFC(this, true));
addUIClickListener('menubarItemNewChart', _ -> this.openWelcomeDialog(true));
addUIClickListener('menubarItemOpenChart', _ -> this.openBrowseFNFC(true));
addUIClickListener('menubarItemSaveChart', _ -> {
if (currentWorkingFilePath != null)
{
ChartEditorImportExportHandler.exportAllSongData(this, true, currentWorkingFilePath);
this.exportAllSongData(true, currentWorkingFilePath);
}
else
{
ChartEditorImportExportHandler.exportAllSongData(this, false);
this.exportAllSongData(false, null);
}
});
addUIClickListener('menubarItemSaveChartAs', _ -> ChartEditorImportExportHandler.exportAllSongData(this));
addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true));
addUIClickListener('menubarItemSaveChartAs', _ -> this.exportAllSongData(false, null));
addUIClickListener('menubarItemLoadInst', _ -> this.openUploadInstDialog(true));
addUIClickListener('menubarItemImportChart', _ -> this.openImportChartDialog('legacy', true));
addUIClickListener('menubarItemExit', _ -> quitChartEditor());
addUIClickListener('menubarItemUndo', _ -> undoLastCommand());
@ -2130,6 +2131,16 @@ class ChartEditorState extends HaxeUIState
addUIClickListener('menubarItemAbout', _ -> this.openAboutDialog());
addUIClickListener('menubarItemWelcomeDialog', _ -> this.openWelcomeDialog(true));
#if sys
addUIClickListener('menubarItemGoToBackupsFolder', _ -> this.openBackupsFolder());
#else
// Disable the menu item if we're not on a desktop platform.
var menubarItemGoToBackupsFolder = findComponent('menubarItemGoToBackupsFolder', MenuItem);
if (menubarItemGoToBackupsFolder != null) menubarItemGoToBackupsFolder.disabled = true;
menubarItemGoToBackupsFolder.disabled = true;
#end
addUIClickListener('menubarItemUserGuide', _ -> this.openUserGuideDialog());
addUIChangeListener('menubarItemDownscroll', event -> isViewDownscroll = event.value);
@ -2232,7 +2243,13 @@ class ChartEditorState extends HaxeUIState
*/
function setupAutoSave():Void
{
// Called when clicking the X button on the window.
WindowUtil.windowExit.add(onWindowClose);
// Called when the game crashes.
CrashHandler.errorSignal.add(onWindowCrash);
CrashHandler.criticalErrorSignal.add(onWindowCrash);
saveDataDirty = false;
}
@ -2241,10 +2258,12 @@ class ChartEditorState extends HaxeUIState
*/
function autoSave():Void
{
var needsAutoSave:Bool = saveDataDirty;
saveDataDirty = false;
// Auto-save preferences.
writePreferences();
writePreferences(needsAutoSave);
// Auto-save the chart.
#if html5
@ -2252,24 +2271,72 @@ class ChartEditorState extends HaxeUIState
// TODO: Implement this.
#else
// Auto-save to temp file.
ChartEditorImportExportHandler.exportAllSongData(this, true);
if (needsAutoSave)
{
this.exportAllSongData(true, null);
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [
{
text: "Take Me There",
callback: openBackupsFolder,
}
]);
}
#end
}
/**
* Open the backups folder in the file explorer.
* Don't call this on HTML5.
*/
function openBackupsFolder():Void
{
// TODO: Is there a way to open a folder and highlight a file in it?
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
WindowUtil.openFolder(absoluteBackupsPath);
}
/**
* Called when the window was closed, to save a backup of the chart.
* @param exitCode The exit code of the window. We use `-1` when calling the function due to a game crash.
*/
function onWindowClose(exitCode:Int):Void
{
trace('Window exited with exit code: $exitCode');
trace('Should save chart? $saveDataDirty');
if (saveDataDirty)
var needsAutoSave:Bool = saveDataDirty;
writePreferences(needsAutoSave);
if (needsAutoSave)
{
ChartEditorImportExportHandler.exportAllSongData(this, true);
this.exportAllSongData(true, null);
}
}
function onWindowCrash(message:String):Void
{
trace('Chart editor intercepted crash:');
trace('${message}');
trace('Should save chart? $saveDataDirty');
var needsAutoSave:Bool = saveDataDirty;
writePreferences(needsAutoSave);
if (needsAutoSave)
{
this.exportAllSongData(true, null);
}
}
function cleanupAutoSave():Void
{
WindowUtil.windowExit.remove(onWindowClose);
CrashHandler.errorSignal.remove(onWindowCrash);
CrashHandler.criticalErrorSignal.remove(onWindowCrash);
}
public override function update(elapsed:Float):Void
@ -3428,6 +3495,11 @@ class ChartEditorState extends HaxeUIState
// Finished dragging. Release the note.
currentPlaceNoteData = null;
}
else
{
// Cursor should be a grabby hand.
if (targetCursorMode == null) targetCursorMode = Grabbing;
}
}
else
{
@ -3972,19 +4044,21 @@ class ChartEditorState extends HaxeUIState
if (currentWorkingFilePath == null || FlxG.keys.pressed.SHIFT)
{
// CTRL + SHIFT + S = Save As
ChartEditorImportExportHandler.exportAllSongData(this, false);
this.exportAllSongData(false, null, function(path:String) {
// CTRL + SHIFT + S Successful
this.success('Saved Chart', 'Chart saved successfully to ${path}.');
}, function() {
// CTRL + SHIFT + S Cancelled
});
}
else
{
// CTRL + S = Save Chart
ChartEditorImportExportHandler.exportAllSongData(this, true, currentWorkingFilePath);
this.exportAllSongData(true, currentWorkingFilePath);
this.success('Saved Chart', 'Chart saved successfully to ${currentWorkingFilePath}.');
}
}
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.S)
{
this.exportAllSongData(false);
}
// CTRL + Q = Quit to Menu
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Q)
{
@ -4145,6 +4219,14 @@ class ChartEditorState extends HaxeUIState
{
// F1 = Open Help
if (FlxG.keys.justPressed.F1) this.openUserGuideDialog();
// DEBUG KEYBIND: Ctrl + Alt + Shift + L = Crash the game.
#if debug
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.L)
{
throw "DEBUG: Crashing the chart editor!";
}
#end
}
override function handleQuickWatch():Void
@ -4481,15 +4563,7 @@ class ChartEditorState extends HaxeUIState
}
}
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Switch Difficulty',
body: 'Switched difficulty to ${selectedDifficulty.toTitleCase()}',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
this.success('Switch Difficulty', 'Switched difficulty to ${selectedDifficulty.toTitleCase()}');
}
/**
@ -4754,9 +4828,6 @@ class ChartEditorState extends HaxeUIState
*/
// ====================
/**
* Dismiss any existing HaxeUI notifications, if there are any.
*/
function handleNotePreview():Void
{
if (notePreviewDirty && notePreview != null)
@ -4868,9 +4939,9 @@ class ChartEditorState extends HaxeUIState
switch (noteData.getStrumlineIndex())
{
case 0: // Player
if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(this, Paths.sound('chartingSounds/hitNotePlayer'));
if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'));
case 1: // Opponent
if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(this, Paths.sound('chartingSounds/hitNoteOpponent'));
if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'));
}
}
}
@ -4957,14 +5028,6 @@ class ChartEditorState extends HaxeUIState
ChartEditorNoteSprite.noteFrameCollection = null;
}
/**
* Dismiss any existing notifications, if there are any.
*/
function dismissNotifications():Void
{
NotificationManager.instance.clearNotifications();
}
function applyCanQuickSave():Void
{
if (menubarItemSaveChart == null) return;

View file

@ -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

View file

@ -20,6 +20,8 @@ import funkin.util.FileUtil;
import funkin.util.SerializerUtil;
import funkin.util.SortUtil;
import funkin.util.VersionUtil;
import funkin.util.DateUtil;
import funkin.util.WindowUtil;
import haxe.io.Path;
import haxe.ui.components.Button;
import haxe.ui.components.DropDown;
@ -36,8 +38,6 @@ import haxe.ui.containers.Form;
import haxe.ui.containers.VBox;
import haxe.ui.core.Component;
import haxe.ui.events.UIEvent;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
import thx.semver.Version;
using Lambda;
@ -63,6 +63,7 @@ class ChartEditorDialogHandler
static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide');
static final CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-variation');
static final CHART_EDITOR_DIALOG_ADD_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-difficulty');
static final CHART_EDITOR_DIALOG_BACKUP_AVAILABLE_LAYOUT:String = Paths.ui('chart-editor/dialogs/backup-available');
/**
* Builds and opens a dialog giving brief credits for the chart editor.
@ -116,27 +117,20 @@ class ChartEditorDialogHandler
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
if (result.length == 0)
{
// No warnings.
state.success('Loaded Chart', 'Loaded chart (${chartPath.toString()})');
}
else
{
// One or more warnings.
state.warning('Loaded Chart', 'Loaded chart (${chartPath.toString()})\n${result.join("\n")}');
}
}
else
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart (${chartPath.toString()})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failed to Load Chart', 'Failed to load chart (${chartPath.toString()})');
}
}
@ -256,6 +250,89 @@ class ChartEditorDialogHandler
return dialog;
}
/**
* [Description]
* @param state
* @return Null<Dialog>
*/
public static function openBackupAvailableDialog(state:ChartEditorState, welcomeDialog:Null<Dialog>):Null<Dialog>
{
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_BACKUP_AVAILABLE_LAYOUT, true, true);
if (dialog == null) throw 'Could not locate Backup Available dialog';
dialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// User loaded the backup! Close the welcome dialog behind this.
if (welcomeDialog != null) welcomeDialog.hideDialog(DialogButton.CANCEL);
}
else
{
// User cancelled the dialog, don't close the welcome dialog so we aren't in a broken state.
}
};
state.isHaxeUIDialogOpen = true;
var backupTimeLabel:Null<Label> = dialog.findComponent('backupTimeLabel', Label);
if (backupTimeLabel == null) throw 'Could not locate backupTimeLabel button in Backup Available dialog';
var latestBackupDate:Null<Date> = ChartEditorImportExportHandler.getLatestBackupDate();
if (latestBackupDate != null)
{
var latestBackupDateStr:String = DateUtil.generateCleanTimestamp(latestBackupDate);
backupTimeLabel.text = latestBackupDateStr;
}
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Backup Available dialog';
buttonCancel.onClick = function(_event) {
// Don't hide the welcome dialog behind this.
dialog.hideDialog(DialogButton.CANCEL);
}
var buttonGoToFolder:Null<Button> = dialog.findComponent('buttonGoToFolder', Button);
if (buttonGoToFolder == null) throw 'Could not locate buttonGoToFolder button in Backup Available dialog';
buttonGoToFolder.onClick = function(_event) {
state.openBackupsFolder();
// Don't hide the welcome dialog behind this.
// dialog.hideDialog(DialogButton.CANCEL);
}
var buttonOpenBackup:Null<Button> = dialog.findComponent('buttonOpenBackup', Button);
if (buttonOpenBackup == null) throw 'Could not locate buttonOpenBackup button in Backup Available dialog';
buttonOpenBackup.onClick = function(_event) {
var latestBackupPath:Null<String> = ChartEditorImportExportHandler.getLatestBackupPath();
var result:Null<Array<String>> = (latestBackupPath != null) ? state.loadFromFNFCPath(latestBackupPath) : null;
if (result != null)
{
if (result.length == 0)
{
// No warnings.
state.success('Loaded Chart', 'Loaded chart (${latestBackupPath})');
}
else
{
// One or more warnings.
state.warning('Loaded Chart', 'Loaded chart (${latestBackupPath})\n${result.join("\n")}');
}
// Close the welcome dialog behind this.
dialog.hideDialog(DialogButton.APPLY);
}
else
{
state.error('Failed to Load Chart', 'Failed to load chart (${latestBackupPath})');
// Song failed to load, don't close the Welcome dialog so we aren't in a broken state.
dialog.hideDialog(DialogButton.CANCEL);
}
}
return dialog;
}
public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog>
{
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT, true, closable);
@ -307,15 +384,14 @@ class ChartEditorDialogHandler
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 (result.length == 0)
{
state.success('Loaded Chart', 'Loaded chart (${selectedFile.name})');
}
else
{
state.warning('Loaded Chart', 'Loaded chart (${selectedFile.name})\n${result.join("\n")}');
}
if (selectedFile.fullPath != null) state.currentWorkingFilePath = selectedFile.fullPath;
dialog.hideDialog(DialogButton.APPLY);
@ -324,15 +400,7 @@ class ChartEditorDialogHandler
}
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
state.error('Failed to Load Chart', 'Failed to load chart (${selectedFile.name}): ${err}');
}
}
});
@ -347,42 +415,26 @@ class ChartEditorDialogHandler
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
if (result.length == 0)
{
state.success('Loaded Chart', 'Loaded chart (${path.file}.${path.ext})');
}
else
{
state.warning('Loaded Chart', 'Loaded chart (${path.file}.${path.ext})\n${result.join("\n")}');
}
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
}
else
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart (${path.toString()})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failed to Load Chart', 'Failed to load chart (${path.file}.${path.ext})');
}
}
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
state.error('Failed to Load Chart', 'Failed to load chart (${path.file}.${path.ext})');
}
};
@ -672,15 +724,7 @@ class ChartEditorDialogHandler
{
if (state.loadInstFromBytes(selectedFile.bytes, instId))
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.success('Loaded Instrumental', 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})');
state.switchToCurrentInstrumental();
dialog.hideDialog(DialogButton.APPLY);
@ -688,15 +732,7 @@ class ChartEditorDialogHandler
}
else
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failed to Load Instrumental', 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})');
}
}
});
@ -708,15 +744,7 @@ class ChartEditorDialogHandler
if (state.loadInstFromPath(path, instId))
{
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.success('Loaded Instrumental', 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})');
state.switchToCurrentInstrumental();
dialog.hideDialog(DialogButton.APPLY);
@ -734,15 +762,7 @@ class ChartEditorDialogHandler
}
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: message,
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failed to Load Instrumental', message);
}
};
@ -964,15 +984,7 @@ class ChartEditorDialogHandler
if (state.loadVocalsFromPath(path, charKey, instId))
{
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
#else
@ -986,16 +998,7 @@ class ChartEditorDialogHandler
{
trace('Failed to load vocal track (${path.file}.${path.ext})');
// Vocals failed to load.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failed to Load Vocals', 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
@ -1019,15 +1022,8 @@ class ChartEditorDialogHandler
if (state.loadVocalsFromBytes(selectedFile.bytes, charKey, instId))
{
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.success('Loaded Vocals', 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
@ -1040,15 +1036,7 @@ class ChartEditorDialogHandler
{
trace('Failed to load vocal track (${selectedFile.fullPath})');
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failed to Load Vocals', 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
@ -1199,15 +1187,7 @@ class ChartEditorDialogHandler
if (songMetadataVersion == null)
{
// Tell the user the load was not successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Could not parse metadata file version (${path.file}.${path.ext})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failure', 'Could not parse metadata file version (${path.file}.${path.ext})');
return;
}
@ -1217,30 +1197,14 @@ class ChartEditorDialogHandler
if (songMetadataVariation == null)
{
// Tell the user the load was not successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Could not load metadata file (${path.file}.${path.ext})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failure', 'Could not load metadata file (${path.file}.${path.ext})');
return;
}
songMetadata.set(variation, songMetadataVariation);
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded metadata file (${path.file}.${path.ext})',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.success('Loaded Metadata', 'Loaded metadata file (${path.file}.${path.ext})');
#if FILE_DROP_SUPPORTED
label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
@ -1264,15 +1228,7 @@ class ChartEditorDialogHandler
if (songMetadataVersion == null)
{
// Tell the user the load was not successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Could not parse metadata file version (${selectedFile.name})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failure', 'Could not parse metadata file version (${selectedFile.name})');
return;
}
@ -1284,15 +1240,7 @@ class ChartEditorDialogHandler
songMetadata.set(variation, songMetadataVariation);
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded metadata file (${selectedFile.name})',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.success('Loaded Metadata', 'Loaded metadata file (${selectedFile.name})');
#if FILE_DROP_SUPPORTED
label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
@ -1305,15 +1253,7 @@ class ChartEditorDialogHandler
else
{
// Tell the user the load was unsuccessful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load metadata file (${selectedFile.name})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failure', 'Failed to load metadata file (${selectedFile.name})');
}
}
});
@ -1329,15 +1269,7 @@ class ChartEditorDialogHandler
if (songChartDataVersion == null)
{
// Tell the user the load was not successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Could not parse chart data file version (${path.file}.${path.ext})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failure', 'Could not parse chart data file version (${path.file}.${path.ext})');
return;
}
@ -1352,15 +1284,7 @@ class ChartEditorDialogHandler
state.noteDisplayDirty = true;
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded chart data file (${path.file}.${path.ext})',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.success('Loaded Chart Data', 'Loaded chart data file (${path.file}.${path.ext})');
#if FILE_DROP_SUPPORTED
label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
@ -1371,15 +1295,7 @@ class ChartEditorDialogHandler
else
{
// Tell the user the load was unsuccessful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart data file (${path.file}.${path.ext})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failure', 'Failed to load chart data file (${path.file}.${path.ext})');
}
};
@ -1396,15 +1312,7 @@ class ChartEditorDialogHandler
if (songChartDataVersion == null)
{
// Tell the user the load was not successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Could not parse chart data file version (${selectedFile.name})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failure', 'Could not parse chart data file version (${selectedFile.name})');
return;
}
@ -1419,15 +1327,7 @@ class ChartEditorDialogHandler
state.noteDisplayDirty = true;
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded chart data file (${selectedFile.name})',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.success('Loaded Chart Data', 'Loaded chart data file (${selectedFile.name})');
#if FILE_DROP_SUPPORTED
label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
@ -1524,15 +1424,7 @@ class ChartEditorDialogHandler
if (fnfLegacyData == null)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to parse FNF chart file (${selectedFile.name})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.error('Failure', 'Failed to parse FNF chart file (${selectedFile.name})');
return;
}
@ -1542,15 +1434,7 @@ class ChartEditorDialogHandler
state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
dialog.hideDialog(DialogButton.APPLY);
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded chart file (${selectedFile.name})',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.success('Success', 'Loaded chart file (${selectedFile.name})');
}
});
}
@ -1565,15 +1449,7 @@ class ChartEditorDialogHandler
state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
dialog.hideDialog(DialogButton.APPLY);
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded chart file (${path.file}.${path.ext})',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
state.success('Success', 'Loaded chart file (${path.file}.${path.ext})');
};
addDropHandler(importBox, onDropFile);
@ -1673,14 +1549,9 @@ class ChartEditorDialogHandler
state.songMetadata.set(pendingVariation.variation, pendingVariation);
state.difficultySelectDirty = true; // Force the Difficulty toolbox to update.
#if !mac
NotificationManager.instance.addNotification(
{
title: "Add Variation",
body: 'Added new variation "${pendingVariation.variation}"',
type: NotificationType.Success
});
#end
state.success('Add Variation', 'Added new variation "${pendingVariation.variation}"');
dialog.hideDialog(DialogButton.APPLY);
}
@ -1737,14 +1608,8 @@ class ChartEditorDialogHandler
state.createDifficulty(dialogVariation.value.id, dialogDifficultyName.text.toLowerCase(), inputScrollSpeed.value ?? 1.0);
#if !mac
NotificationManager.instance.addNotification(
{
title: "Add Difficulty",
body: 'Added new difficulty "${dialogDifficultyName.text.toLowerCase()}"',
type: NotificationType.Success
});
#end
state.success('Add Difficulty', 'Added new difficulty "${dialogDifficultyName.text.toLowerCase()}"');
dialog.hideDialog(DialogButton.APPLY);
}

View file

@ -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...');

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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')}';
}
}

View file

@ -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.
*/

View file

@ -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.
@ -11,6 +12,19 @@ class CrashHandler
{
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