diff --git a/app/src/App.ts b/app/src/App.ts index cba3bcc..677072b 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -52,6 +52,7 @@ declare global { } interface HTMLInputElement { nwsaveas?: string + nwworkingdir?: string } } @@ -68,7 +69,7 @@ interface AppVersion { changelog: string[] } -const VERSION = "1.1.0" +const VERSION = "1.1.1" export class App { options = Options diff --git a/app/src/chart/ChartManager.ts b/app/src/chart/ChartManager.ts index 43b2f6e..a535582 100644 --- a/app/src/chart/ChartManager.ts +++ b/app/src/chart/ChartManager.ts @@ -267,23 +267,19 @@ export class ChartManager { if (this.mode == EditMode.Play || this.mode == EditMode.Record) return let newbeat = this.beat const snap = Options.chart.snap - const speed = - Options.chart.speed * - (Options.chart.reverse && Options.chart.scroll.invertReverseScroll - ? -1 - : 1) - const delta = - (event.deltaY / speed) * Options.chart.scroll.scrollSensitivity + let delta = + (event.deltaY / Options.chart.speed) * + Options.chart.scroll.scrollSensitivity + if (Options.chart.reverse && Options.chart.scroll.invertReverseScroll) + delta *= -1 + if (Options.chart.scroll.invertScroll) delta *= -1 + if (snap == 0) { this.partialScroll = 0 newbeat = this.beat + delta } else { if (Options.chart.scroll.scrollSnapEveryScroll) { - if ( - event.deltaY < 0 != - (Options.chart.scroll.invertReverseScroll && - Options.chart.reverse) - ) { + if (delta < 0) { newbeat = Math.round((this.beat - snap) / snap) * snap } else { newbeat = Math.round((this.beat + snap) / snap) * snap diff --git a/app/src/chart/sm/TimingData.ts b/app/src/chart/sm/TimingData.ts index bbd43f0..e7288b1 100644 --- a/app/src/chart/sm/TimingData.ts +++ b/app/src/chart/sm/TimingData.ts @@ -4,16 +4,25 @@ import { clamp, roundDigit } from "../../util/Math" import { Options } from "../../util/Options" import { bsearch } from "../../util/Util" import { + AttackTimingEvent, BGChangeTimingEvent, + BPMTimingEvent, BeatTimingCache, BeatTimingEvent, Cached, ColumnType, + ComboTimingEvent, + DelayTimingEvent, DeletableEvent, FGChangeTimingEvent, + FakeTimingEvent, + LabelTimingEvent, ScrollCacheTimingEvent, + ScrollTimingEvent, + SpeedTimingEvent, StopTimingEvent, TIMING_EVENT_NAMES, + TickCountTimingEvent, TimeSignatureTimingEvent, TimingCache, TimingColumn, @@ -719,27 +728,36 @@ export abstract class TimingData { case "FAKES": this.insertEvents( type, - Array.from(data.matchAll(/(-?[\d.]+)=(-?[\d.]+)/g)).map( - match => { + Array.from(data.matchAll(/(-?[\d.]+)=(-?[\d.]+)/g)) + .map< + | BPMTimingEvent + | StopTimingEvent + | WarpTimingEvent + | DelayTimingEvent + | ScrollTimingEvent + | FakeTimingEvent + >(match => { return { type, beat: parseFloat(match[1]), value: parseFloat(match[2]), } - } - ) + }) + .sort((a, b) => a.beat - b.beat) ) return case "TICKCOUNTS": this.insertEvents( type, - Array.from(data.matchAll(/(-?[\d.]+)=(-?\d+)/g)).map(match => { - return { - type: "TICKCOUNTS", - beat: parseFloat(match[1]), - value: parseInt(match[2]), - } - }) + Array.from(data.matchAll(/(-?[\d.]+)=(-?\d+)/g)) + .map(match => { + return { + type: "TICKCOUNTS", + beat: parseFloat(match[1]), + value: parseInt(match[2]), + } + }) + .sort((a, b) => a.beat - b.beat) ) return case "LABELS": @@ -747,62 +765,68 @@ export abstract class TimingData { type, Array.from( data.matchAll(/((-?[\d.]+)=([^\n]+)=\d)|((-?[\d.]+)=([^\n,]+))/g) - ).map(match => { - if (match[1] === undefined) { + ) + .map(match => { + if (match[1] === undefined) { + return { + type: "LABELS", + beat: parseFloat(match[5]), + value: match[6].trim(), + } + } return { type: "LABELS", - beat: parseFloat(match[5]), - value: match[6].trim(), + beat: parseFloat(match[2]), + value: match[3].trim(), } - } - return { - type: "LABELS", - beat: parseFloat(match[2]), - value: match[3].trim(), - } - }) + }) + .sort((a, b) => a.beat - b.beat) ) return case "SPEEDS": this.insertEvents( type, - Array.from( - data.matchAll(/(-?[\d.]+)=(-?[\d.]+)=([\d.]+)=([01])/g) - ).map(match => { - return { - type: "SPEEDS", - beat: parseFloat(match[1]), - value: parseFloat(match[2]), - delay: parseFloat(match[3]), - unit: match[4].trim() == "0" ? "B" : "T", - } - }) + Array.from(data.matchAll(/(-?[\d.]+)=(-?[\d.]+)=([\d.]+)=([01])/g)) + .map(match => { + return { + type: "SPEEDS", + beat: parseFloat(match[1]), + value: parseFloat(match[2]), + delay: parseFloat(match[3]), + unit: match[4].trim() == "0" ? "B" : "T", + } + }) + .sort((a, b) => a.beat - b.beat) ) return case "TIMESIGNATURES": this.insertEvents( type, - Array.from(data.matchAll(/(-?[\d.]+)=(\d+)=(\d+)/g)).map(match => { - return { - type: "TIMESIGNATURES", - beat: parseFloat(match[1]), - upper: parseInt(match[2]), - lower: parseInt(match[3]), - } - }) + Array.from(data.matchAll(/(-?[\d.]+)=(\d+)=(\d+)/g)) + .map(match => { + return { + type: "TIMESIGNATURES", + beat: parseFloat(match[1]), + upper: parseInt(match[2]), + lower: parseInt(match[3]), + } + }) + .sort((a, b) => a.beat - b.beat) ) return case "COMBOS": this.insertEvents( type, - Array.from(data.matchAll(/(-?[\d.]+)=(\d+)=*(\d+)*/g)).map(match => { - return { - type: "COMBOS", - beat: parseFloat(match[1]), - hitMult: parseInt(match[2]), - missMult: parseInt(match[3] ?? match[2]), - } - }) + Array.from(data.matchAll(/(-?[\d.]+)=(\d+)=*(\d+)*/g)) + .map(match => { + return { + type: "COMBOS", + beat: parseFloat(match[1]), + hitMult: parseInt(match[2]), + missMult: parseInt(match[3] ?? match[2]), + } + }) + .sort((a, b) => a.beat - b.beat) ) return case "ATTACKS": @@ -810,15 +834,17 @@ export abstract class TimingData { type, Array.from( data.matchAll(/TIME=(-?[\d.]+):(END|LEN)=(-?[\d.]+):MODS=([^:]+)/g) - ).map(match => { - return { - type: "ATTACKS", - second: parseFloat(match[1]), - endType: match[2].trim() as "END" | "LEN", - value: parseFloat(match[3]), - mods: match[4].trim(), - } - }) + ) + .map(match => { + return { + type: "ATTACKS", + second: parseFloat(match[1]), + endType: match[2].trim() as "END" | "LEN", + value: parseFloat(match[3]), + mods: match[4].trim(), + } + }) + .sort((a, b) => a.second - b.second) ) return case "BGCHANGES": @@ -829,22 +855,24 @@ export abstract class TimingData { data.matchAll( /(-?[\d.]+)=([^\n]+?)=(-?[\d.]+)=([01])=([01])=([01])=?([^\n=,]*)=?([^\n=,]*)=?([^\n=,]*)=?([^\n=,]*)=?([^\n=,]*)/g ) - ).map(match => { - return { - type, - beat: parseFloat(match[1]), - file: match[2].trim(), - updateRate: parseFloat(match[3]), - crossFade: match[4].trim() == "1", - stretchRewind: match[5].trim() == "1", - stretchNoLoop: match[6].trim() == "1", - effect: match[7].trim() ?? "", - file2: match[8].trim() ?? "", - transition: match[9].trim() ?? "", - color1: match[10].trim() ?? "", - color2: match[11].trim() ?? "", - } - }) + ) + .map(match => { + return { + type, + beat: parseFloat(match[1]), + file: match[2].trim(), + updateRate: parseFloat(match[3]), + crossFade: match[4].trim() == "1", + stretchRewind: match[5].trim() == "1", + stretchNoLoop: match[6].trim() == "1", + effect: match[7].trim() ?? "", + file2: match[8].trim() ?? "", + transition: match[9].trim() ?? "", + color1: match[10].trim() ?? "", + color2: match[11].trim() ?? "", + } + }) + .sort((a, b) => a.beat - b.beat) ) } } diff --git a/app/src/data/SMPropertiesData.ts b/app/src/data/SMPropertiesData.ts index 4c2a987..8af4d1c 100644 --- a/app/src/data/SMPropertiesData.ts +++ b/app/src/data/SMPropertiesData.ts @@ -13,10 +13,10 @@ type SMPropertyGroupData = { title: string items: SMPropertyData[] } - +// we have an extra history since we don't want new song prompt to interfere with the current one type SMPropertyCustomInput = { type: "custom" - create: (app: App, sm?: Simfile, actionHist?: ActionHistory) => HTMLElement + create: (app: App, sm: Simfile, history: ActionHistory) => HTMLElement } type SMPropertyStringInput = { type: "string" @@ -148,35 +148,27 @@ export const SM_PROPERTIES_DATA: SMPropertyGroupData[] = [ propName: "SAMPLESTART", input: { type: "custom", - create: (app, sm, actionHist) => { - const history = actionHist ?? ActionHistory.instance + create: (_, sm, history) => { const updateValues = () => { if (toSpinner.value < fromSpinner.value) { toSpinner.setValue(fromSpinner.value) } - const lastStart = - (sm ?? app.chartManager.loadedSM!).properties.SAMPLESTART ?? "0" - const lastLength = - (sm ?? app.chartManager.loadedSM!).properties.SAMPLELENGTH ?? - "10" + const lastStart = sm.properties.SAMPLESTART ?? "0" + const lastLength = sm.properties.SAMPLELENGTH ?? "10" const newStart = fromSpinner.value.toString() const newLength = (toSpinner.value - fromSpinner.value).toString() history.run({ - action: app => { - ;(sm ?? app.chartManager.loadedSM!).properties.SAMPLESTART = - newStart - ;(sm ?? app.chartManager.loadedSM!).properties.SAMPLELENGTH = - newLength + action: () => { + sm.properties.SAMPLESTART = newStart + sm.properties.SAMPLELENGTH = newLength fromSpinner.setValue(parseFloat(newStart)) toSpinner.setValue( parseFloat(newStart) + parseFloat(newLength) ) }, undo: () => { - ;(sm ?? app.chartManager.loadedSM!).properties.SAMPLESTART = - lastStart - ;(sm ?? app.chartManager.loadedSM!).properties.SAMPLELENGTH = - lastLength + sm.properties.SAMPLESTART = lastStart + sm.properties.SAMPLELENGTH = lastLength fromSpinner.setValue(parseFloat(lastStart)) toSpinner.setValue( parseFloat(lastStart) + parseFloat(lastLength) @@ -185,9 +177,7 @@ export const SM_PROPERTIES_DATA: SMPropertyGroupData[] = [ }) } const fromSpinner = NumberSpinner.create( - parseFloat( - (sm ?? app.chartManager.loadedSM!).properties.SAMPLESTART ?? "0" - ), + parseFloat(sm.properties.SAMPLESTART ?? "0"), undefined, 3, 0 @@ -195,23 +185,15 @@ export const SM_PROPERTIES_DATA: SMPropertyGroupData[] = [ fromSpinner.onChange = value => { if (value === undefined) { fromSpinner.setValue( - parseFloat( - (sm ?? app.chartManager.loadedSM!).properties.SAMPLESTART ?? - "0" - ) + parseFloat(sm.properties.SAMPLESTART ?? "0") ) return } updateValues() } const toSpinner = NumberSpinner.create( - parseFloat( - (sm ?? app.chartManager.loadedSM!).properties.SAMPLESTART ?? "0" - ) + - parseFloat( - (sm ?? app.chartManager.loadedSM!).properties.SAMPLELENGTH ?? - "10" - ), + parseFloat(sm.properties.SAMPLESTART ?? "0") + + parseFloat(sm.properties.SAMPLELENGTH ?? "10"), undefined, 3, 0 @@ -219,14 +201,8 @@ export const SM_PROPERTIES_DATA: SMPropertyGroupData[] = [ toSpinner.onChange = value => { if (value === undefined) { toSpinner.setValue( - parseFloat( - (sm ?? app.chartManager.loadedSM!).properties.SAMPLESTART ?? - "0" - ) + - parseFloat( - (sm ?? app.chartManager.loadedSM!).properties - .SAMPLELENGTH ?? "10" - ) + parseFloat(sm.properties.SAMPLESTART ?? "0") + + parseFloat(sm.properties.SAMPLELENGTH ?? "10") ) return } @@ -248,14 +224,13 @@ export const SM_PROPERTIES_DATA: SMPropertyGroupData[] = [ export function createInputElement( app: App, - data: SMPropertyData, - sm?: Simfile, - actionHist?: ActionHistory + sm: Simfile, + history: ActionHistory, + data: SMPropertyData ) { - const history = actionHist ?? ActionHistory.instance switch (data.input.type) { case "custom": - return data.input.create(app, sm, actionHist) + return data.input.create(app, sm, history) case "string": { const input = document.createElement("input") input.type = "text" @@ -265,33 +240,26 @@ export function createInputElement( if (ev.key == "Enter") input.blur() } input.onblur = () => { - const lastValue = (sm ?? app.chartManager.loadedSM!).properties[ - data.propName - ] + const lastValue = sm.properties[data.propName] const newValue = input.value history.run({ - action: app => { - ;(sm ?? app.chartManager.loadedSM!).properties[data.propName] = - newValue + action: () => { + sm.properties[data.propName] = newValue input.value = newValue }, undo: () => { - ;(sm ?? app.chartManager.loadedSM!).properties[data.propName] = - lastValue + sm.properties[data.propName] = lastValue input.value = lastValue ?? "" }, }) } - input.value = - (sm ?? app.chartManager.loadedSM!).properties[data.propName] ?? "" + input.value = sm.properties[data.propName] ?? "" return input } case "number": { const inputData = data.input const spinner = NumberSpinner.create( - parseFloat( - (sm ?? app.chartManager.loadedSM!).properties[data.propName]! - ) ?? 15, + parseFloat(sm.properties[data.propName] ?? "15"), inputData.step, inputData.precision, inputData.min, @@ -299,27 +267,18 @@ export function createInputElement( ) spinner.onChange = value => { if (value === undefined) { - spinner.setValue( - parseFloat( - (sm ?? app.chartManager.loadedSM!).properties[data.propName] ?? - "0" - ) - ) + spinner.setValue(parseFloat(sm.properties[data.propName] ?? "0")) return } - const lastValue = (sm ?? app.chartManager.loadedSM!).properties[ - data.propName - ] + const lastValue = sm.properties[data.propName] const newValue = value.toString() history.run({ - action: app => { - ;(sm ?? app.chartManager.loadedSM!).properties[data.propName] = - newValue + action: () => { + sm.properties[data.propName] = newValue spinner.setValue(parseFloat(newValue)) }, undo: () => { - ;(sm ?? app.chartManager.loadedSM!).properties[data.propName] = - lastValue + sm.properties[data.propName] = lastValue spinner.setValue(parseFloat(lastValue ?? "0")) }, }) @@ -344,26 +303,13 @@ export function createInputElement( const fileSelector = document.createElement("input") fileSelector.type = "file" fileSelector.accept = inputData.accept.join(",") + fileSelector.nwworkingdir = dir fileSelector.onchange = () => { const newValue = FileHandler.getRelativePath( dir, fileSelector.value ) - const lastValue = - (sm ?? app.chartManager.loadedSM!).properties[data.propName] ?? "" - history.run({ - action: app => { - ;(sm ?? app.chartManager.loadedSM!).properties[data.propName] = - newValue - input.value = newValue - }, - undo: () => { - ;(sm ?? app.chartManager.loadedSM!).properties[data.propName] = - lastValue - input.value = lastValue - }, - }) - callback?.(app) + setValue(newValue) } fileSelector.click() } else { @@ -378,52 +324,46 @@ export function createInputElement( disableClose: true, callback: (path: string) => { const newValue = FileHandler.getRelativePath(dir, path) - const lastValue = - (sm ?? app.chartManager.loadedSM!).properties[ - data.propName - ] ?? "" - history.run({ - action: app => { - ;(sm ?? app.chartManager.loadedSM!).properties[ - data.propName - ] = newValue - input.value = newValue - }, - undo: () => { - ;(sm ?? app.chartManager.loadedSM!).properties[ - data.propName - ] = lastValue - input.value = lastValue - }, - }) - callback?.(app) + setValue(newValue) }, }, - (sm ?? app.chartManager.loadedSM!).properties[data.propName] - ? dir + - "/" + - (sm ?? app.chartManager.loadedSM!).properties[data.propName] + sm.properties[data.propName] + ? dir + "/" + sm.properties[data.propName] : app.chartManager.smPath ) ) } } - input.value = - (sm ?? app.chartManager.loadedSM!).properties[data.propName] ?? "" + input.value = sm.properties[data.propName] ?? "" container.appendChild(input) const deleteButton = document.createElement("button") deleteButton.style.height = "100%" deleteButton.classList.add("delete") - deleteButton.disabled = !(sm ?? app.chartManager.loadedSM!).properties[ - data.propName - ] + deleteButton.disabled = !sm.properties[data.propName] deleteButton.onclick = () => { - input.value = "" + setValue(undefined) deleteButton.disabled = true } const icon = Icons.getIcon("TRASH", 12) deleteButton.appendChild(icon) container.appendChild(deleteButton) + + const setValue = (value: string | undefined) => { + const lastValue = sm.properties[data.propName] ?? "" + history.run({ + action: () => { + sm.properties[data.propName] = value + input.value = value ?? "" + deleteButton.disabled = input.value == "" + }, + undo: () => { + sm.properties[data.propName] = lastValue + input.value = lastValue + deleteButton.disabled = input.value == "" + }, + }) + callback?.(app) + } return container } } diff --git a/app/src/data/UserOptionsWindowData.ts b/app/src/data/UserOptionsWindowData.ts index b45a04f..0c05590 100644 --- a/app/src/data/UserOptionsWindowData.ts +++ b/app/src/data/UserOptionsWindowData.ts @@ -426,6 +426,14 @@ export const USER_OPTIONS_WINDOW_DATA: UserOption[] = [ tooltip: "Whether each scroll movement corresponds to moving one snap unit when scrolling. Turning this on will have the same behavior as ArrowVortex. Recommended on for those using a mouse, off for those using trackpad.", }, + { + type: "item", + label: "Invert scroll direction", + id: "chart.scroll.invertScroll", + input: { + type: "checkbox", + }, + }, { type: "item", label: "Invert zoom in/out", diff --git a/app/src/gui/window/NewSongWindow.ts b/app/src/gui/window/NewSongWindow.ts index 3452e93..32055e0 100644 --- a/app/src/gui/window/NewSongWindow.ts +++ b/app/src/gui/window/NewSongWindow.ts @@ -16,7 +16,7 @@ export class NewSongWindow extends Window { app: App private readonly sm: Simfile - private readonly actionHistory: ActionHistory + private readonly history private fileTable: { [key: string]: File } = {} constructor(app: App) { @@ -33,8 +33,8 @@ export class NewSongWindow extends Window { const blob = new Blob([DEFAULT_SM], { type: "text/plain" }) const file = new File([blob], "song.sm", { type: "text/plain" }) this.sm = new Simfile(file) + this.history = new ActionHistory(app) this.app = app - this.actionHistory = new ActionHistory(this.app) this.initView() } @@ -71,7 +71,7 @@ export class NewSongWindow extends Window { ) else grid.appendChild( - createInputElement(this.app, item, this.sm, this.actionHistory) + createInputElement(this.app, this.sm, this.history, item) ) }) groupContainer.appendChild(title) diff --git a/app/src/gui/window/SMPropertiesWindow.ts b/app/src/gui/window/SMPropertiesWindow.ts index 17b0f84..8cc64a8 100644 --- a/app/src/gui/window/SMPropertiesWindow.ts +++ b/app/src/gui/window/SMPropertiesWindow.ts @@ -3,6 +3,7 @@ import { SM_PROPERTIES_DATA, createInputElement, } from "../../data/SMPropertiesData" +import { ActionHistory } from "../../util/ActionHistory" import { EventHandler } from "../../util/EventHandler" import { Window } from "./Window" @@ -60,7 +61,14 @@ export class SMPropertiesWindow extends Window { label.innerText = item.title grid.appendChild(label) - grid.appendChild(createInputElement(this.app, item)) + grid.appendChild( + createInputElement( + this.app, + this.app.chartManager.loadedSM!, + ActionHistory.instance, + item + ) + ) }) groupContainer.appendChild(title) groupContainer.appendChild(grid) diff --git a/app/src/util/Options.ts b/app/src/util/Options.ts index bb9e888..cd49072 100644 --- a/app/src/util/Options.ts +++ b/app/src/util/Options.ts @@ -43,6 +43,7 @@ export class DefaultOptions { scroll: { scrollSensitivity: 1, scrollSnapEveryScroll: !navigator.userAgent.includes("Mac"), + invertScroll: false, invertZoomScroll: false, invertReverseScroll: true, }, diff --git a/app/src/util/file-handler/NodeFileHandler.ts b/app/src/util/file-handler/NodeFileHandler.ts index 5356086..82a5f06 100644 --- a/app/src/util/file-handler/NodeFileHandler.ts +++ b/app/src/util/file-handler/NodeFileHandler.ts @@ -3,15 +3,15 @@ import { BaseFileHandler } from "./FileHandler" import { FileHandle, FolderHandle } from "./NodeAdapter" const { - join, dirname, extname, basename, + relative, }: { - join: (...paths: string[]) => string dirname: (path: string) => string extname: (path: string) => string basename: (path: string) => string + relative: (path1: string, path2: string) => string } = window.nw.require("path") const fs = window.nw.require("fs").promises @@ -175,6 +175,6 @@ export class NodeFileHandler implements BaseFileHandler { } getRelativePath(from: string, to: string) { - return join(from, to) + return relative(from, to) } } diff --git a/app/src/util/file-handler/SafariFileWriter.ts b/app/src/util/file-handler/SafariFileWriter.ts index 36db83b..309d2ce 100644 --- a/app/src/util/file-handler/SafariFileWriter.ts +++ b/app/src/util/file-handler/SafariFileWriter.ts @@ -29,7 +29,9 @@ export class SafariFileWriter { ) const encode = new TextEncoder() const buffer = - typeof data == "string" ? encode.encode(data) : await data.arrayBuffer() + typeof data == "string" + ? encode.encode(data).buffer + : await data.arrayBuffer() this.worker.postMessage([id, path, buffer], [buffer]) return promise } diff --git a/public/assets/app/changelog.json b/public/assets/app/changelog.json index 8adb830..05d2462 100644 --- a/public/assets/app/changelog.json +++ b/public/assets/app/changelog.json @@ -1,4 +1,9 @@ [ + { + "version": "1.1.1", + "date": 1734389306256, + "changelog": "## Fixes\n- Fixed files not saving on Safari (thanks safari)\n- Fixed timing data parsing when events appear out of order in the sm file\n- Fixed Song Properties file pickers not working correctly on desktop app" + }, { "version": "1.1.0", "date": 1733884335684,