// Must stay first: it applies the saved language before the modules below build
// their label tables with `t()`.
import '../platform/locale-bootstrap.mjs';
import { configureShaderWorkspace, closeShaderWorkspace, createShaderProblemReporter } from './shader-tool.mjs';
import { chartShaderFiles } from '../core/shader-assets.mjs';
import { EditorSession } from '../application/session.mjs';
import { FrameLimiter, validFpsLimit } from '../core/frame-timing.mjs';
import { PerformanceMonitor } from './performance-monitor.mjs';
import { assertChart, createChart, EVENT_TYPES, previewLimitations } from '../core/chart.mjs';
import { beatValue, parseBeat, formatBeat, fromNumber, upperBound } from '../core/beat.mjs';
import { TempoMap } from '../core/tempo.mjs';
import { AudioTransport } from '../platform/audio.mjs';
import { openFiles, assetBytes, resourceReferences, attachExternalEffects, assetPath } from '../platform/files.mjs';
import { showExportDialog } from './export-dialog.mjs';
import { showImportDialog } from './import-dialog.mjs';
import { showLargeChartPreview } from './large-chart-preview.mjs';
import { LargeChartWorkspace } from './large-chart-workspace.mjs';
import { readChartImport } from '../platform/chart-import.mjs';
import { listDrafts, readDraft, saveSnapshot } from '../platform/recovery.mjs';
import { Timeline } from './timeline.mjs';
import { selectionTimeRange } from '../application/selection-time-range.mjs';
import { TimelineStrips } from './timeline-strips.mjs';
import { Preview } from './preview.mjs';
import { PreviewLinePicker } from './preview-line-picker.mjs';
import { DirectPlacement } from './direct-placement.mjs';
import { QuickCurve } from './quick-curve.mjs';
import { renderProperties, disposeNoteProperties } from './inspector.mjs';
import { showDialog, editJson, choose, confirmAction, dialogOpen } from './dialog.mjs';
import { migratePreferences, shortcutAction, shortcutReleased, shortcutMatches, DEFAULT_HOTKEYS } from '../core/preferences.mjs';
import { directoryEntries, uploadedEntries, scanMigration } from '../platform/migration.mjs';
import { readProject, readPreferences, storePreferences, storeProject, readClipboardHistory, storeClipboardHistory, subscribeLibrary } from '../platform/library.mjs';
import { storageFailure, requestPersistence } from '../platform/storage.mjs';
import { StoragePanel, noteLibrarySave, noteExternalBackup } from './storage-panel.mjs';
import { VersionControlPanel } from './version-control.mjs';
import { migrationDialog } from './migration-dialog.mjs';
import { LinePanel } from './line-panel.mjs';
import { AssetLibraryPanel } from './asset-library.mjs';
import { remapAssetReferences, assetReference } from '../platform/asset-library.mjs';
import { HELP_TEXT } from './help.mjs';
import { download } from '../platform/files.mjs';
import { RpeSkin } from './skin.mjs';
import { ProjectImages } from '../platform/images.mjs';
import { HitSounds } from '../platform/hitsounds.mjs';
import { renderEventInspector } from './event-inspector.mjs';
import { cancelValueDrag, handleValueDragKey } from './draggable-value.mjs';
import { eventKey, eventList, selectedEventEntries, selectEvent, selectAllEvents, deleteEvents, transformEvents, toggleSingleEventLock } from '../application/event-commands.mjs';
import { stickSelectedEventToNext } from '../application/event-tools.mjs';
import { BATCH_ACTIONS, applyBatchAction, nudgeSelection, captureSelection } from '../application/batch-edit.mjs';
import { copyObjects, cutObjects, pasteObjects, deleteObjects } from '../application/clipboard.mjs';
import { ClipboardHistory } from '../application/clipboard-history.mjs';
import { renderClipboardHistory } from './clipboard-history.mjs';
import { PasteGesture } from './paste-gesture.mjs';
import { SelectionOverlay } from './selection-overlay.mjs';
import { SelectionIntervalPanel } from './selection-interval.mjs';
import { PlacementDefaultsPanel } from './placement-defaults.mjs';
import { ChartDiagnostics } from '../platform/chart-diagnostics.mjs';
import { DiagnosticsPanel } from './diagnostics-panel.mjs';
import { DiagnosticHighlight } from './diagnostic-highlight.mjs';
import { diagnosticKey } from '../core/chart-diagnostics.mjs';
import { diagnosticShaderTarget } from '../application/diagnostic-target.mjs';
import { createPlacedLine, normalizeNoteDefaults, normalizeLineDefaults } from '../core/placement-defaults.mjs';
import { LineSwitcher } from './line-switcher.mjs';
import { stepLine } from '../core/line-overview.mjs';
import { applyNumberShortcut } from '../application/number-shortcuts.mjs';
import { BatchControls } from './batch-controls.mjs';
import { MultiEditPanel } from './multi-edit.mjs';
import { MultiLinePanel } from './multi-line.mjs';
import { clipboardBeat } from './clipboard-preview.mjs';
import { shaderEvents, replaceShaderEvents, shaderIdentity } from '../core/shader-events.mjs';
import { shaderAssetPath } from '../core/shader-assets.mjs';
import { shaderAssetLabel } from '../core/shader-source-info.mjs';
import { renderMetadataPanel, renderBpmPanel } from './forms.mjs';
import { EditorPlayback } from '../application/playback.mjs';
import { readEditorPreferences, writeEditorPreferences } from '../platform/editor-preferences.mjs';
import { ProjectHome } from './home.mjs';
import { createSettingsPanel } from './settings.mjs';
import { DictationInput } from './dictation.mjs';
import { currentLocale, setLocale, t } from '../i18n.mjs';
import { writeLocalePreference } from '../platform/locale.mjs';
import { installDomTranslation } from './dom-i18n.mjs';
import { chartDisplayName } from '../core/labels.mjs';
import { showHotkeySettings } from './hotkey-settings.mjs';
import { AutoSaveClock } from '../application/autosave.mjs';
import { ManualSaveQueue } from '../application/manual-save.mjs';
import { SPECIAL_TRACKS, MAX_BASE_LAYERS } from '../core/editor-display.mjs';
import { lineGroupName, isDefaultLineGroup, lineDisplayLabel, normalizeLineGroups } from '../core/line-groups.mjs';
import { isTextEntry, usesNativeTextShortcut, releaseShortcutFocus } from './keyboard.mjs';
import { setRatioOptions, applyViewControls } from './view-controls.mjs';
import { generateCurveNotes } from '../core/curve-notes.mjs';
import { createEasingPicker } from './easing-picker.mjs';
import { numericWheel } from './numeric-wheel.mjs';
import { SceneRuntime } from '../core/scene.mjs';
import { TimelineActivity } from '../core/timeline-activity.mjs';
import { assetUrl } from '../core/asset-url.mjs';
import { AudioAnalysis } from './audio-analysis.mjs';
import { TrajectoryPanel } from './trajectory-panel.mjs';
import { TextLinesPanel } from './text-lines.mjs';
import { NoiseFitPanel } from './noise-fit.mjs';
import { NoiseTrackingPanel } from './noise-tracking.mjs';
import { NoteTrackingPanel } from './note-tracking.mjs';
import { CollaborationPanel } from './collaboration.mjs';
import { NoiseAreaEditor } from './noise-area.mjs';
import { EventSampler } from './event-sampler.mjs';
import { SpeedCurvePanel, singleSpeedCurveNote } from './speed-curve.mjs';
import { closeAnimationCurveEditor, animationCurvePlaybackActive, animationCurvePreviewState } from './custom-easing-controls.mjs';
import { EditorSkins } from './editor-skins.mjs';
import { NoteSkins } from './note-skins.mjs';
import { editorSkinState, editorSkinSignal } from './editor-skin-state.mjs';
import { pcmWave } from '../platform/pcm-wave.mjs';
import { ModManager } from './mod-manager.mjs';
import { emitModHook } from '../core/mod-hooks.mjs';
import { GlobalEventRegistry, registerPlaceholderGlobalEvent } from '../core/global-events.mjs';
import { GlobalEventsEditor } from './global-events.mjs';
import { initializeLocalPreferences, flushLocalPreferences, localPreferences } from '../platform/local-preferences.mjs';
import { ProjectEditClock, chartEditSeconds } from '../platform/project-edit-clock.mjs';
import { MultiLineStateStore } from '../platform/multi-line-state.mjs';
import { SpatialLineStateStore, spatialLineState } from '../platform/spatial-line-state.mjs';
import { multiLineState } from '../application/multi-line-edit.mjs';
import { selectionCounts } from '../application/selection-count.mjs';
import { nativeStorage } from '../platform/native.mjs';
import { DEFAULT_EDITOR_PREFERENCES, DEFAULT_SETTINGS } from '../core/default-settings.mjs';
import { appendLineShortcutDigit } from '../core/line-switch.mjs';
import { installNumericExpressionInputs } from './numeric-expression-input.mjs';
import { TimeReadouts } from './time-readouts.mjs';
import { AgentPanel } from './agent-panel.mjs';
import { AudioSources } from '../application/audio-sources.mjs';
import { AudioOnsets } from '../application/audio-onsets.mjs';
import { mapperTiming } from '../core/audio-mapper.mjs';
import { AboutUpdates } from './about-updates.mjs';
import { GraphicsSettings } from './graphics-settings.mjs';
import { SpatialTool } from './spatial-tool.mjs';
import { EditorRecording } from './editor-recording.mjs';
import { KeyDisplayTool } from './key-display-tool.mjs';

document.documentElement.lang = currentLocale().locale;

try { await initializeLocalPreferences(); }
catch (error) {
  const host = document.createElement('div'); host.className = 'native-storage-migration';
  const message = document.createElement('p'); message.textContent = t('app.storageInitFailed', { error: error.message });
  const retry = document.createElement('button'); retry.textContent = t('app.retry'); retry.onclick = () => location.reload();
  host.append(message, retry); document.body.append(host); throw error;
}
if (globalThis.rpenDesktopStorage) {
  globalThis.rpenFlushPreferences = async () => { checkpointEditTime(); speedCurve?.saveView(true); await agentPanel?.flush(); await flushLocalPreferences(); };
  globalThis.rpenPrepareClose = async () => { checkpointEditTime(); await editorRecording?.finish(); multiLineStateStore.capture(); spatialLineStateStore.capture(); speedCurve?.saveView(true); await session.largeDocument?.flush(); await agentPanel?.stopAndFlush(); await flushLocalPreferences(); };
}

const element = selector => document.querySelector(selector);
let mods;
let agentPanel;
let dictationInput;
let speedCurve;
let editorRecording;
const keyDisplay = new KeyDisplayTool(error => notify(error.message, 'error'));
let keyDisplayReady = false;
function toggleKeyDisplay() {
  editorPreferences.keyDisplay = !(editorPreferences.keyDisplay ?? DEFAULT_EDITOR_PREFERENCES.keyDisplay);
  applyDisplaySettings(); persistEditor();
}
globalThis.__rpenModInputGate=true;
for(const type of ['keydown','keyup','wheel','pointerdown','pointerup','mousedown','mouseup','click','dblclick','contextmenu','drop','dragstart','input','change','submit'])window.addEventListener(type,event=>{
  if (type === 'keydown' && keyDisplayReady && !usesNativeTextShortcut(event) && shortcutMatches(event, preferences.hotkeys.KeyDisplay) && !document.querySelector('dialog.hotkey-dialog[open]')) {
    event.preventDefault(); event.stopImmediatePropagation(); if (!event.repeat) toggleKeyDisplay(); return;
  }
  keyDisplay.input(event);
  const shaderWindow = ['keydown', 'keyup'].includes(type) && event.target?.closest?.('#shader-code-window');
  if (shaderWindow) { shaderWindow.handleShaderKey?.(event); return; }
  if (type === 'keydown' && editorRecording?.active && !usesNativeTextShortcut(event) && shortcutMatches(event, preferences.hotkeys.EditorRecording)) {
    event.preventDefault(); event.stopImmediatePropagation(); if (!event.repeat) void editorRecording.finish(); return;
  }
  if (type === 'keydown' && speedCurve?.active && speedCurve.handleKeydown(event)) return;
  const runtime=mods?.runtime;
  if(type==='keydown'&&event.ctrlKey&&event.altKey&&event.shiftKey&&event.key==='F11'){cancelValueDrag();event.preventDefault();event.stopImmediatePropagation();runtime?.emergency?.();return;}
  if(type==='keydown')dictationInput?.keydown(event);
  else if(type==='keyup')dictationInput?.keyup(event);
  if(event.defaultPrevented)return;
  runtime?.handleInput(event);
  if(!event.defaultPrevented&&['keydown','keyup'].includes(type))handleValueDragKey(event);
},{capture:true,passive:false});
const displayFields = [
  ['editor-render-fps', 'editorRenderFps', DEFAULT_EDITOR_PREFERENCES.editorRenderFps],
  ['editor-render-auto-play', 'editorRenderAutoPlay', DEFAULT_EDITOR_PREFERENCES.editorRenderAutoPlay],
  ['key-display', 'keyDisplay', DEFAULT_EDITOR_PREFERENCES.keyDisplay],
  ['clear-hits-on-resume', 'clearHitsOnResume', DEFAULT_EDITOR_PREFERENCES.clearHitsOnResume],
  ['selection-interval', 'selectionInterval', DEFAULT_EDITOR_PREFERENCES.selectionInterval],
  ['delete-with-extra-keys', 'deleteWithExtraKeys', DEFAULT_EDITOR_PREFERENCES.deleteWithExtraKeys],
  ['density-strip-enabled', 'densityStrip', DEFAULT_EDITOR_PREFERENCES.densityStrip], ['history-strip-enabled', 'historyStrip', DEFAULT_EDITOR_PREFERENCES.historyStrip],
  ['density-strip-brightness', 'densityStripBrightness', DEFAULT_EDITOR_PREFERENCES.densityStripBrightness], ['density-strip-window', 'densityStripWindow', DEFAULT_EDITOR_PREFERENCES.densityStripWindow],
  ['include-backup-charts', 'includeBackupCharts', false],
  ['text-selection-drag', 'textSelectionDrag', false],
  ['event-cut-density', 'cutDensity', 4],
  ['event-drag-sensitivity', 'dragSensitivity', 1],
  ['sample-auto-fit', 'sampleAutoFit', true],
  ['paired-movement-sampling', 'pairedMovementSampling', true],
  ['note-size', 'noteSize', 200],
  ['wheel-limit-audio-start', 'wheelStopAtAudioStart', true],
  ['wheel-input-mode', 'wheelInputMode', DEFAULT_SETTINGS.wheelInputMode],
  ['hit-tap-delay', 'hitTapDelay', DEFAULT_EDITOR_PREFERENCES.hitTapDelay], ['hit-drag-delay', 'hitDragDelay', DEFAULT_EDITOR_PREFERENCES.hitDragDelay], ['hit-flick-delay', 'hitFlickDelay', DEFAULT_EDITOR_PREFERENCES.hitFlickDelay],
  ['draw-event-curves', 'drawEventCurves', true],
  ['pick-preview-noise-areas', 'pickPreviewNoiseAreas', true],
  ['seamless-noise-areas', 'seamlessNoiseAreas', true],
  ['force-simple-noise', 'forceSimpleNoise', false],
  ['judgement-offset', 'judgementOffset', 92],
  ['judgement-color', 'judgementColor', '#ffffff'],
  ['judgement-width', 'judgementWidth', DEFAULT_EDITOR_PREFERENCES.judgementWidth], ['judgement-alpha', 'judgementAlpha', DEFAULT_EDITOR_PREFERENCES.judgementAlpha],
  ['rpe-event-drag', 'rpeEventDrag', DEFAULT_EDITOR_PREFERENCES.rpeEventDrag],
  ['xy-binding', 'xyBinding', DEFAULT_EDITOR_PREFERENCES.xyBinding],
  ['movement-vector', 'movementVector', DEFAULT_EDITOR_PREFERENCES.movementVector],
  ['event-value-scale', 'eventValueScale', DEFAULT_EDITOR_PREFERENCES.eventValueScale],
  ['event-value-scale-snap', 'eventValueScaleSnap', DEFAULT_EDITOR_PREFERENCES.eventValueScaleSnap],
  ['event-value-scale-step', 'eventValueScaleStep', DEFAULT_EDITOR_PREFERENCES.eventValueScaleStep],
  ['default-line-thickness', 'lineScale', 1.5],
  ['line-switcher-enabled', 'lineSwitcher', true],
  ['overlapping-line-picker', 'overlappingLinePicker', DEFAULT_EDITOR_PREFERENCES.overlappingLinePicker],
  ['placement-opacity', 'placementOpacity', DEFAULT_EDITOR_PREFERENCES.placementOpacity],
  ['note-spacing', 'noteSpacing', DEFAULT_EDITOR_PREFERENCES.noteSpacing],
  ['notes-only-note-scale', 'notesOnlyNoteScale', DEFAULT_EDITOR_PREFERENCES.notesOnlyNoteScale],
  ['clipboard-history-enabled', 'clipboardHistory', true],
  ['bar-width', 'barWidth', DEFAULT_SETTINGS.barWidth], ['bar-alpha', 'barAlpha', DEFAULT_SETTINGS.barAlpha], ['event-value-size', 'eventValueSize', 13], ['event-value-threshold', 'eventValueThreshold', 30], ['event-curve-threshold', 'eventCurveThreshold', 24], ['event-opacity', 'eventOpacity', 0.25], ['event-bar-width', 'eventBarWidth', DEFAULT_EDITOR_PREFERENCES.eventBarWidth], ['seamless-events', 'seamlessEvents', true],
  ['background-blur', 'backgroundBlur', 10.5], ['scroll-speed', 'scrollSpeed', 5], ['tips-enabled', 'tipsEnabled', true], ['success-notifications', 'successNotifications', true],
  ['viewport-overflow', 'viewportOverflow', DEFAULT_EDITOR_PREFERENCES.viewportOverflow], ['overflow-blur', 'overflowBlur', DEFAULT_EDITOR_PREFERENCES.overflowBlur], ['overflow-darken', 'overflowDarken', DEFAULT_EDITOR_PREFERENCES.overflowDarken],
  ['line-numbers', 'lineNumbers', true], ['line-arrows', 'lineArrows', true], ['line-tint', 'lineTint', true], ['merge-line-numbers', 'mergeLineNumbers', true], ['pick-preview-lines', 'pickPreviewLines', true],
  ['note-source-hover', 'noteSourceHover', true],
  ['preserve-pitch', 'preservePitch', true], ['autoplay-view', 'autoplayView', true], ['highlight-notes', 'highlight', true],
  ['autosave-enabled', 'autoSave', true], ['autosave-seconds', 'autoSaveSeconds', 60], ['autosave-limit', 'autoSaveLimit', 10],
];
const settingsDialog = createSettingsPanel({
  onLocaleChange: async (locale, previousLocale) => {
    // Skins, mods and toolbar buttons build their DOM once at startup, so a live
    // switch would leave them in the old language. Reload and reopen the chart.
    if (hasDocument && (session.history.dirty || assetDirty || !libraryProject?.id)) {
      if (previousLocale) setLocale(previousLocale);
      notify(t('notify.localeNeedsSave'), 'warning');
      return false;
    }
    writeLocalePreference(locale.locale);
    speedCurve?.saveView(true);
    try { await flushLocalPreferences(); }
    catch (error) { writeLocalePreference(previousLocale); setLocale(previousLocale); reportError(error); return false; }
    try { if (hasDocument && libraryProject?.id) sessionStorage.setItem(LOCALE_REOPEN_KEY, libraryProject.id); } catch { /* storage unavailable */ }
    location.reload();
    return true;
  },
});
// Static markup is translated once here; switching reloads the page.
installDomTranslation(() => { document.documentElement.lang = currentLocale().locale; renderSession(); });
installNumericExpressionInputs();
let session = new EditorSession();
const multiLineStateStore = new MultiLineStateStore(localPreferences, error => notify(storageFailure(error), 'warning'));
const spatialLineStateStore = new SpatialLineStateStore(localPreferences, error => notify(storageFailure(error), 'warning'));
let largeChartWorkspace = null;
let closingLargeChart = Promise.resolve();
let assets = new Map();
let assetFolders = new Set();
let assetDirty = false;
let chartName = 'chart.json';
let recoveryId = crypto.randomUUID();
let tempo = new TempoMap(session.chart.BPMList);
let tempoEntries = session.chart.BPMList;
let dirtyFrame = true;
let lastDraftDocument;
let lastDraftAssets = [], lastDraftFolders = [];
let preferences = migratePreferences();
let libraryProject = null;
let versionRepoId = crypto.randomUUID();
let versionControl = null;
let persistenceRequested = false;
const manualSaves = new ManualSaveQueue(storeProject);
let editorPreferences = { ...DEFAULT_EDITOR_PREFERENCES, ...readEditorPreferences() };
const fileOptions = () => ({ includeBackupCharts: editorPreferences.includeBackupCharts === true, editTime: checkpointEditTime });
let atHome = true;
let hasDocument = false;
let renderStudio = null;
// Set before a language switch reloads the page so the same chart reopens.
const LOCALE_REOPEN_KEY = 'rpe-next-reopen-project';
let previewReturnTime = 0;
let placementContext;
let heldPreview;
let curveStart;
let curveEnd;
let curveAnchorMode = null;
let curveEditorOpen = false;
let curveEasingPicker;
let curveValues = { startTime: [0, 0, 1], endTime: [4, 0, 1], startX: -405, endX: 405, density: 1, type: 4, easingType: 1 };
let loop = null;
let activePaneName = 'chart';
let lastSelectionSignature = '';
let editTimeSeconds = 0;
const projectEditClock = new ProjectEditClock();
let editClockTick = performance.now();
let lastDiagnosticSignature = null;
let diagnosticAssetRevision = 0;
let diagnosticNavigation = false;
const audio = new AudioTransport();
const hitSounds = new HitSounds(audio, { onWarning: message => notify(message, 'warning', 7000) });
const preview = new Preview(element('#preview'));
const realtimePreview = new Preview(element('#realtime-preview'));
const lineInfoScene = new SceneRuntime();
const timelineActivity = new TimelineActivity();
const invalidate = () => { dirtyFrame = true; };
const timeReadouts = new TimeReadouts(element('.time-readouts'), invalidate);
preview.invalidate = realtimePreview.invalidate = invalidate;
realtimePreview.applyShaders = false;
realtimePreview.showHitEffects = false;
const skin = new RpeSkin(invalidate);
const images = new ProjectImages(invalidate, message => status(message));
const timeline = new Timeline(element('#notes'), element('#events'), () => session, editEvent, invalidate, error => reportError(error), openTimelineContextMenu);
let shaderLabelAssets, shaderLabelRevision, shaderLabelChartName, shaderLabelPaths = new Map();
function shaderDisplayName(event) {
  if (shaderLabelAssets !== assets || shaderLabelRevision !== diagnosticAssetRevision || shaderLabelChartName !== chartName) { shaderLabelAssets = assets; shaderLabelRevision = diagnosticAssetRevision; shaderLabelChartName = chartName; shaderLabelPaths.clear(); }
  if (!shaderLabelPaths.has(event.shader)) shaderLabelPaths.set(event.shader, shaderAssetPath(assets, event.shader, chartName));
  const path = shaderLabelPaths.get(event.shader); return path ? shaderAssetLabel(path, assets, session.chart, chartName) : shaderIdentity(event);
}
timeline.shaderLabel = shaderDisplayName;
const noiseEditor = new NoiseAreaEditor(element('#noise-panel'), element('#noise-editor-canvas'), () => ({ session, tempo, timeline, preview, realtimePreview, previewVisible: preview.visible, seconds: chartSeconds, sampleAutoFit: element('#sample-auto-fit').checked }), {
  batchVisible: () => activePaneName === 'noise-properties',
  commit: (label, chart) => session.commit(label, chart), invalidate, notify, activate: activatePane,
  onContextMenu: event => openTimelineContextMenu(event),
  pause: () => playback.pause(), seek: beat => seekBeat(beat), wheel: event => previewWheel(event),
  getAssets: () => assets,
  onAssets: entries => { for (const [name, bytes] of entries) assets.set(name, bytes); assetDirty = true; diagnosticAssetRevision++; images.load(session.chart, assets, chartName); invalidate(); },
});
const globalEventRegistry = new GlobalEventRegistry();
registerPlaceholderGlobalEvent(globalEventRegistry);
const globalEvents = new GlobalEventsEditor(element('#global-events-panel'), element('#global-events-canvas'), globalEventRegistry,
  () => ({ session, tempo, timeline, previewVisible: preview.visible, seconds: () => chartSeconds(), duration: () => audio.duration }),
  { activate: activatePane, invalidate, notify, seek: beat => seekBeat(beat), wheel: event => previewWheel(event), onContextMenu: event => openTimelineContextMenu(event) });
const selectionIntervalPanel = new SelectionIntervalPanel(element('#selection-interval'), () => editorPreferences.selectionInterval ?? 1, value => {
  editorPreferences.selectionInterval = value; timeline.selectionInterval = globalEvents.timeline.selectionInterval = value;
  persistEditor(); invalidate();
});
const placementDefaultsPanel = new PlacementDefaultsPanel(() => ({ session, assets, chartName, volume: hitSounds.volume }), {
  writeLineDefaults: values => { session.lineEventDefaults = values; editorPreferences.lineEventDefaults = values; persistEditor(); },
  invalidate, reportError: error => reportError(error),
});
for (const renderer of [preview, realtimePreview]) {
  renderer.noisePreview.hiddenLayers = noiseEditor.hiddenLayers;
  renderer.noisePreview.simpleLayers = noiseEditor.simpleLayers;
}
document.addEventListener('rpen-animation-curve-preview-changed', () => invalidate());
const eventSampler = new EventSampler(element('.stage'), () => ({ session, tempo, preview, realtimePreview, previewVisible: preview.visible, timeline, seconds: chartSeconds, playing: () => audio.playing, sampleAutoFit: element('#sample-auto-fit').checked, pairedMovementSampling: element('#paired-movement-sampling').checked }), { invalidate, pause: () => playback.pause(), notify });
const spatialTool = new SpatialTool(element('#spatial-editor'), () => ({ session, tempo, timeline, persistSpatialLines: () => spatialLineStateStore.capture(), previewVisible: preview.visible, showPreview: () => togglePreview(true, true, false, { autoplay: false }), hotkeys: () => ({ tap: preferences.hotkeys.AddTap, event: preferences.hotkeys.AddEvent, remove: preferences.hotkeys.Delete }), locked: () => Boolean(mods?.runtime.locks.size), sampling: () => eventSampler.active || Boolean(noiseEditor.sampleMode) }), { activate: activatePane, invalidate, pause: () => playback.pause(), notify, seek: beat => seekBeat(beat) });
element('#spatial-tool').onclick = () => spatialTool.open();
speedCurve = new SpeedCurvePanel(() => ({ session, timeline, preferences, editorPreferences }), { invalidate, pause: () => playback.pause() });
const audioAnalysis = new AudioAnalysis(element('#audio-analysis-overlay'), () => timeline, () => audio, () => { persistEditor(); invalidate(); });
const audioSources = new AudioSources(() => audio), audioOnsets = new AudioOnsets({ getTiming: sourceOffset => mapperTiming(session.chart, sourceOffset) }); audioAnalysis.attachSources(audioSources, notify, audioOnsets);
audioAnalysis.requestFrame = () => invalidate();
let noteHoverTimer = null;
let noteHoverKey = null;
function clearNoteSourceToast() {
  if (noteHoverTimer) { clearTimeout(noteHoverTimer); noteHoverTimer = null; }
  noteHoverKey = null;
  const toast = element('#note-source-toast');
  if (toast) { toast.hidden = true; toast.onclick = null; }
}
timeline.onNoteHover = entry => {
  clearTimeout(noteHoverTimer); noteHoverTimer = null;
  const toast = element('#note-source-toast');
  const hoverEnabled = editorPreferences.noteSourceHover ?? preferences.settings.noteSourceHover ?? true;
  if (!hoverEnabled) { clearNoteSourceToast(); return; }
  if (!entry || atHome || !preview.visible || session.multiLineActive || !toast) { clearNoteSourceToast(); return; }
  const key = `${entry.lineIndex}:${entry.index}`;
  if (key === noteHoverKey && !toast.hidden) return;
  noteHoverKey = key; toast.hidden = true;
  noteHoverTimer = setTimeout(() => {
    if (noteHoverKey !== key || !entry?.lineIndex && entry?.lineIndex !== 0) return;
    const line = session.chart.judgeLineList?.[entry.lineIndex];
    if (!line) return;
    toast.textContent = t('app.sb6e1d2c', { p0: entry.lineIndex });
    toast.hidden = false;
    toast.onclick = () => { session.selectLine(entry.lineIndex); clearNoteSourceToast(); invalidate(); };
  }, 1000);
};
timeline.multiLineLabels = element('#multi-line-labels');
timeline.multiLineScrollElement = element('#multi-line-scroll');
timeline.multiLineScrollElement.addEventListener('input', event => {
  const area = session.multiLineMode === 'events' ? 'events' : 'notes';
  if (typeof timeline.multiLineScroll === 'number') timeline.multiLineScroll = { notes: timeline.multiLineScroll, events: timeline.multiLineScroll };
  timeline.multiLineScroll[area] = Number(event.target.value) || 0;
  invalidate();
});
let draggingMultiLineScroll = false;
function updateMultiLineScrollFromPointer(event) {
  const range = timeline.multiLineScrollElement;
  const rectangle = range.getBoundingClientRect();
  if (!rectangle.width) return;
  const ratio = Math.max(0, Math.min(1, (event.clientX - rectangle.left) / rectangle.width));
  range.value = String(Number(range.max || 0) * ratio);
  range.dispatchEvent(new Event('input', { bubbles: true }));
}
timeline.multiLineScrollElement.addEventListener('pointerdown', event => {
  event.preventDefault(); event.stopPropagation(); draggingMultiLineScroll = true;
  timeline.multiLineScrollElement.setPointerCapture?.(event.pointerId);
  updateMultiLineScrollFromPointer(event);
});
timeline.multiLineScrollElement.addEventListener('pointermove', event => {
  if (!draggingMultiLineScroll) return;
  event.preventDefault(); event.stopPropagation(); updateMultiLineScrollFromPointer(event);
});
const stopMultiLineScrollDrag = event => {
  if (!draggingMultiLineScroll) return;
  draggingMultiLineScroll = false; event?.stopPropagation?.();
};
timeline.multiLineScrollElement.addEventListener('pointerup', stopMultiLineScrollDrag);
timeline.multiLineScrollElement.addEventListener('pointercancel', stopMultiLineScrollDrag);
timeline.multiLineScrollElement.addEventListener('lostpointercapture', stopMultiLineScrollDrag);
const valueScaleOptions = {
  get enabled() { return editorPreferences.eventValueScale ?? DEFAULT_EDITOR_PREFERENCES.eventValueScale; },
  get snap() { return editorPreferences.eventValueScaleSnap ?? DEFAULT_EDITOR_PREFERENCES.eventValueScaleSnap; },
  get step() { return editorPreferences.eventValueScaleStep ?? DEFAULT_EDITOR_PREFERENCES.eventValueScaleStep; },
  get available() { return !atHome && !noiseEditor.active && !globalEvents.active; },
};
const batchControls = new BatchControls(element('.stage'), timeline, () => session, () => !atHome && (!preview.visible || directPlacement.active) && !noiseEditor.active && !globalEvents.active, error => reportError(error), () => valueScaleOptions);
const multiEdit = new MultiEditPanel(element('#multi-editor'), () => session, timeline, {
  close: () => activatePane('chart'), invalidate, notify: (message, severity) => notify(message, severity),
});
const trajectoryPanel = new TrajectoryPanel(element('#trajectory-editor'), () => ({ session, timeline, tempo, previewVisible: preview.visible }), { invalidate, notify, activate: activatePane });
const textLinesPanel = new TextLinesPanel(element('#text-lines-editor'), () => ({ session, tempo, seconds: chartSeconds(), skin, noteSize: preview.noteSize }), { invalidate, notify, activate: activatePane });
const noiseFitPanel = new NoiseFitPanel(element('#noise-fit-editor'), element('.stage'), () => ({ session, tempo, timeline, previewVisible: preview.visible, seconds: () => chartSeconds() }), { invalidate, notify, activate: activatePane, pause: () => playback.pause() });
const noiseTrackingPanel = new NoiseTrackingPanel(element('#noise-tracking-editor'), element('.stage'), () => ({ session, tempo, timeline, preview, realtimePreview, previewVisible: preview.visible, seconds: () => chartSeconds(), wheel: event => previewWheel(event) }), { invalidate, notify, activate: activatePane, pause: () => playback.pause() });
const noteTrackingPanel = new NoteTrackingPanel(element('#note-tracking-editor'), element('.stage'), () => ({ session, tempo, timeline, preview, realtimePreview, previewVisible: preview.visible, seconds: () => chartSeconds(), wheel: event => previewWheel(event) }), { invalidate, notify, activate: activatePane, pause: () => playback.pause() });
const multiLinePanel = new MultiLinePanel(element('#multi-line-editor'), () => session, { timeline, render: renderSession, notify, persist: persistEditor });
timeline.onMultiLineChange = () => {
  const count = session.multiLineIndices.length;
  element('#multi-line-count').textContent = count;
  for (const selector of ['#multi-line-prev-remove', '#multi-line-next-remove']) element(selector).disabled = !count;
  for (const selector of ['#multi-line-prev-add', '#multi-line-next-add']) element(selector).disabled = !count || count >= session.chart.judgeLineList.length;
  if (activePaneName === 'multi-line') multiLinePanel.syncSelection();
};
const linePanel = new LinePanel(element('#line-panel'), () => session, { render: renderSession, notify, getAssets: () => assets, getChartName: () => chartName, afterTexture: () => images.load(session.chart, assets, chartName) });
const diagnostics = new ChartDiagnostics(() => ({ chart: session.chart, assets, assetRevision: diagnosticAssetRevision, chartName,
  duration: audio.buffer || audio.media?.src ? audio.duration : undefined, registry: globalEventRegistry, ratio: preview.aspectRatio ?? 1.5,
  activeMods: [...(globalThis.rpenMods?.runtime.loaded ?? [])].filter(([, state]) => state.status === 'loaded').map(([id]) => id), modRevision: [...(globalThis.rpenMods?.runtime.loaded ?? [])].map(([id, state]) => id + ':' + state.status).join('|') }));
const diagnosticPanel = new DiagnosticsPanel(diagnostics, { visible: () => activePaneName === 'diagnose', remove: deleteDiagnosticIssue, locate: locateDiagnosticIssue });
const diagnosticHighlight = new DiagnosticHighlight(element('.stage'), () => ({ session, home: atHome }));
diagnostics.addEventListener('change', () => {
  if (diagnostics.pending) return;
  const errors = diagnostics.issues.filter(issue => issue.severity === 'error'), signature = new Set(errors.map(diagnosticKey));
  if (lastDiagnosticSignature && activePaneName !== 'diagnose') { const added = errors.filter(issue => !lastDiagnosticSignature.has(diagnosticKey(issue))); if (added.length) notify(t('app.sd10d488', { p0: added.length }), 'error', 4200); }
  lastDiagnosticSignature = signature;
});
globalEventRegistry.addEventListener('change', () => { diagnostics.update(); });
const editorSkins = new EditorSkins(element('#editor-skin-settings'), { invalidate, notify });
const noteSkins = new NoteSkins(element('#note-skin-settings'), { skin, hitSounds, audio, invalidate, notify, settings: settingsDialog, pause: () => playback.pause() });
document.addEventListener('rpen-editor-skin-changed', () => {
  lineSwitcher.lastFrame = -Infinity; lineSwitcher.cache.clear(); renderSession();
  if (activePaneName === 'clipboard') renderClipboardPanel();
});
document.addEventListener('rpen-note-skin-changed', () => { if (textLinesPanel.active) textLinesPanel.refresh(); });
const assetLibrary = new AssetLibraryPanel(element('#asset-library'), () => ({ assets, folders: assetFolders, chart: session.chart, chartName, previewVolume: hitSounds.volume }), {
  notify,
  onChange: (nextAssets, nextFolders, paths) => {
    const sound = session.noteDefaults.hitsound, oldSoundPath = sound ? assetPath(assets, sound, chartName) : null;
    if (sound) {
      const path = paths?.get(oldSoundPath) ?? oldSoundPath;
      session.noteDefaults = { ...session.noteDefaults, hitsound: path && nextAssets.has(path) ? assetReference(nextAssets, path, chartName) : '' };
    }
    const nextChart = remapAssetReferences(session.chart, assets, nextAssets, paths, chartName);
    assets = nextAssets; assetFolders = new Set(nextFolders ?? []); assetDirty = true; diagnosticAssetRevision++;
    if (nextChart !== session.chart) session.commit(t('app.s66f5500'), nextChart);
    hitSounds.chart = session.chart; hitSounds.setAssets(assets, chartName); if (audio.playing) hitSounds.prepare().catch(reportError);
    images.load(session.chart, assets, chartName); status(t('app.s61fc662')); renderSession();
  },
  openFolder: nativeStorage ? async (path, folder) => {
    if (!libraryProject || assetDirty) throw Error(t('app.s7d40547'));
    await nativeStorage.call('openAssetFolder', libraryProject.id, path, folder);
  } : undefined,
});
const clipboardHistory = new ClipboardHistory();
const selectionOverlay = new SelectionOverlay(element('.stage'), timeline);
const linePicker = new PreviewLinePicker();
const directPlacement = new DirectPlacement(element('.preview-wrap'), timeline, batchControls,
  () => ({ session, preview, tempo, seconds: chartSeconds() }), { pause: () => playback.pause(), changed: invalidate,
    closed: () => { element('#close-direct-placement').hidden = true; }, reportError: error => reportError(error) });
const quickCurve = new QuickCurve(timeline, () => ({ session, tempo, enabled: !atHome && (!preview.visible || directPlacement.active) && !noiseEditor.active && !globalEvents.active && !curveEditorOpen && !mods?.runtime.locks.size }), { pause: () => playback.pause(), invalidate, notify: (...args) => notify(...args) });
const lineSwitcher = new LineSwitcher(element('.stage'), () => ({
  chart: session.chart, tempo, seconds: chartSeconds(), selected: session.lineIndex,
  layer: timeline.layer, extended: timeline.extended,
  start: timeline.timeAt(timeline.viewHeight()), end: timeline.timeAt(0),
  visible: hasDocument && !atHome && !preview.visible && !noiseEditor.active && !globalEvents.active && !dialogOpen(),
}), { select: index => selectOverviewLine(index) });
function clipboardTargetLine() {
  return timeline.hoverArea === 'events'
    ? timeline.lineIndexAt(timeline.eventCursor?.x ?? 0, timeline.eventsCanvas.clientWidth, 'events')
    : timeline.lineIndexAt(timeline.cursor?.x ?? 0, timeline.notesCanvas.clientWidth, 'notes');
}
const pasteGesture = new PasteGesture({
  paste: context => { try { pasteObjects(session, context.beat, { ...timeline.clipboardMode, targetLineIndex: clipboardTargetLine() }); } catch (error) { reportError(error); } },
  open: () => { activatePane('clipboard'); renderClipboardPanel(); },
  reportError: error => notify(t('app.s70852e2', { p0: error.message }), 'error'),
  valid: context => context.session === session && context.chart === session.chart && context.layer === timeline.layer && !atHome && (!preview.visible || directPlacement.active) && !dialogOpen() && !isTextEntry(document.activeElement),
});
window.addEventListener('blur', () => pasteGesture.cancel());
document.addEventListener('focusin', event => { if (isTextEntry(event.target)) pasteGesture.cancel(); });
const timelineStrips = new TimelineStrips(element('#note-density'), element('#history-strip'));
function drawTimelineStrips() {
  if (!timelineStrips.options.densityStrip && !timelineStrips.options.historyStrip) return;
  if (!(audio.duration > 0)) timelineActivity.compile(session.chart, tempo);
  const duration = Math.max(0.001, audio.duration > 0 ? audio.duration : timelineActivity.duration);
  timelineStrips.draw(session, tempo, timelineActivity, duration, chartSeconds(), timeline.selectionTimeRange, timeline.layer, timeline.extended);
}
function seekFromStrip(event) {
  const rect = event.currentTarget.getBoundingClientRect(); const ratio = Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height));
  timelineActivity.compile(session.chart, tempo);
  const duration = audio.duration > 0 ? audio.duration : timelineActivity.duration; playback.seek((1 - ratio) * duration + offsetSeconds());
}
element('#note-density').addEventListener('click', seekFromStrip);
element('#history-strip').addEventListener('click', seekFromStrip);
timeline.curvePick = note => {
  if (!curveAnchorMode) return false;
  const anchor = { startTime: [...note.startTime], positionX: note.positionX, type: note.type };
  if (curveAnchorMode === 'start') {
    curveStart = anchor;
    curveAnchorMode = null;
    element('#curve-start')?.classList.remove('active');
    curveValues.startTime = anchor.startTime; curveValues.startX = anchor.positionX; updateCurvePanel();
    status(t('app.s55fff36'));
    invalidate();
    return true;
  }
  if (!curveStart) { curveAnchorMode = null; status(t('app.s0b8378a')); return true; }
  curveEnd = anchor;
  curveAnchorMode = null;
  element('#curve-end')?.classList.remove('active');
  curveValues.endTime = anchor.startTime; curveValues.endX = anchor.positionX; updateCurvePanel();
  status(t('app.s3739e90'));
  return true;
};
timeline.curveGhost = () => {
  if (!curveEditorOpen || !curveStart) return [];
  const exists = anchor => session.notes.some(note => note.positionX === anchor.positionX && beatValue(note.startTime) === beatValue(anchor.startTime));
  const anchors = [{ startTime: [...curveValues.startTime], positionX: curveValues.startX, type: curveValues.type, anchor: exists(curveStart) && curveValues.startX === curveStart.positionX && beatValue(curveValues.startTime) === beatValue(curveStart.startTime) }];
  if (!curveEnd) return anchors;
  const end = { startTime: [...curveValues.endTime], positionX: curveValues.endX, type: curveValues.type, anchor: exists(curveEnd) && curveValues.endX === curveEnd.positionX && beatValue(curveValues.endTime) === beatValue(curveEnd.startTime) };
  try {
    return [...anchors, ...generateCurveNotes({ ...curveValues, division: timeline.division }), end];
  } catch { return [...anchors, end]; }
};
timeline.skin = skin; preview.skin = skin; preview.images = images;
realtimePreview.skin = skin; realtimePreview.images = images;
skin.load();
document.fonts.load('35px RPEGame').then(invalidate);
const status = message => { element('#status').textContent = message; };
const notificationTimers = new Set();
function notify(message, level = 'success', duration = 2800) {
  if (['error', 'warning'].includes(level)) editorSkinSignal(level);
  if (level === 'success' && editorPreferences.successNotifications === false) return;
  const host = element('#notifications'); if (!host) return;
  const item = document.createElement('div'); item.className = `editor-notification ${level}`; item.textContent = message; host.append(item);
  requestAnimationFrame(() => item.classList.add('visible'));
  const timer = setTimeout(() => { item.classList.add('leaving'); setTimeout(() => item.remove(), 260); notificationTimers.delete(timer); }, duration); notificationTimers.add(timer);
}
timeline.notify = (message, level = 'warning') => notify(message, level);
const reportError = error => {
  const message = storageFailure(error); status(message); notify(message, 'error', 7000);
  const host = showDialog(error.name === 'ProjectConflictError' ? t('app.s61fa498') : t('app.s223fb3c'), message);
  if (error.name === 'ProjectConflictError' && libraryProject?.id === error.projectId && hasDocument) {
    for (const [label, run] of [[t('app.s15e8f96'), () => showExportDialog(session.chart, assets, chartName, (_name, format) => { if (format === 'pez') noteExternalBackup(); }, libraryProject?.identifier, fileOptions())],
      [t('app.s53213ef'), async () => { libraryProject = null; session.history.savedDocument = null; await save(); }]]) {
      const button = document.createElement('button'); button.type = 'button'; button.textContent = label;
      button.onclick = async () => { element('#modal').close(); try { await run(); } catch (failure) { reportError(failure); } }; host.append(button);
    }
  }
};
document.addEventListener('rpen-storage-blocked', () => notify(t('app.sc0d447d'), 'warning', 7000));
const tipKeys = [
  t('app.s4bf04e3'),
  t('app.s6861f58'),
  t('app.s7b81457'),
  t('app.s02fa461'),
  t('app.saeff07b'),
  t('app.s2b801b6'),
  t('app.s7795e1c'),
  t('app.sc5574a8'),
  t('app.s99a95fb'),
  t('app.sc9bc457'),
  t('app.sd67584b'),
  t('app.sad8e978'),
  t('app.s18f7e80'),
  t('app.sf35446f'),
  t('app.sf50c3c5'),
  t('app.s1ed117b'),
  t('app.s860780f'),
  t('app.saf9dadc'),
  t('app.s7a89e84'),
  t('app.s3f8c4d7'),
  t('app.s5ed173f'),
  t('app.s801fd0c'),
  t('app.s61024c2'),
  t('app.s2f3f064'),
  t('app.s2fa0c57'),
  t('app.s46e26f6'),
  t('app.s58b1d6f'),
  t('app.s26acb9c'),
  t('app.s7bce64d'),
  t('app.s9e43446'),
  t('app.s02c2119'),
  t('app.sc21eb7c'),
  t('app.s4963c44'),
  t('app.s9320781'),
  t('app.sffe6b8a'),
  t('app.sb329966'),
  t('app.s1332d89'),
  t('app.s4648271'),
  t('app.s760f48e'),
  t('app.s074da51'),
  t('app.sce9623e'),
  t('app.sd7c1b2e'),
  t('app.s7dc5fa4'),
  t('app.s15c86b5'),
  t('app.s4a39c08'),
  t('app.s6f9d89a'),
  t('app.s669449c'),
  t('app.sfd56c24'),
  t('app.sa302181'),
  t('app.s094100c'),
  t('app.sfb6f94a'),
  t('app.s3816d85'),
  t('app.s53040c5'),
  t('app.sa5551d9'),
  t('app.sbc9e4e2'),
  t('app.sb1d120a'),
  t('app.sf47134d'),
  t('app.s88df27a'),
  t('app.s5d11416'),
  t('app.s7ba89fe'),
  t('app.sa668593'),
  t('app.sa4488e7'),
  t('app.s9c16d38'),
  t('app.sd64fc07'),
  t('app.sbff4832'),
];
let tipIndex = 0;
function rotateTip() { const target = element('#tips'); if (!target || editorPreferences.tipsEnabled === false || atHome) { if (target) target.textContent = ''; return; } target.textContent = t(tipKeys[tipIndex++ % tipKeys.length]); }
setInterval(rotateTip, 10000);
const offsetSeconds = () => Number(session.chart.META.offset ?? 0) / 1000;
const chartSeconds = () => audio.time - offsetSeconds();
const resetEditClock = (chart, sourceTime = 0) => {
  editTimeSeconds = projectEditClock.attach(versionRepoId, chart, { EditTime: sourceTime }, performance.now());
  editClockTick = performance.now();
};
const advanceEditClock = timestamp => {
  editTimeSeconds = projectEditClock.tick(timestamp, hasDocument && !atHome && !document.hidden);
  editClockTick = timestamp;
};
function checkpointEditTime() { advanceEditClock(performance.now()); projectEditClock.checkpoint(); return editTimeSeconds; }
const formatEditTime = seconds => `${String(Math.floor(seconds / 3600)).padStart(2, '0')} h, ${String(Math.floor(seconds / 60) % 60).padStart(2, '0')} m, ${String(Math.floor(seconds) % 60).padStart(2, '0')} s`;
const currentBeat = () => tempo.beat(chartSeconds(), noiseEditor.active || globalEvents.active ? 1 : session.line?.bpmfactor ?? 1);
let collaborationJoining = false;
const collaborationTool = document.createElement('button'); collaborationTool.id = 'collaboration-tool'; collaborationTool.textContent = t('app.se55c4de'); element('[data-panel="chart"] .action-grid').append(collaborationTool);
const collaboration = new CollaborationPanel(element('#collaboration-panel'), () => ({
  session, timeline, interactionBusy: batchControls.active, seconds: chartSeconds(), offset: offsetSeconds(), duration: Number(element('#scrubber').max) || 600,
  seek: seconds => playback.seek(seconds + offsetSeconds()),
  sharedAssets: () => {
    const references = resourceReferences(session.chart, assets, chartName);
    const names = new Set([references.song, references.background, session.chart.META.background, ...session.chart.judgeLineList.map(line => line.Texture)].filter(Boolean));
    const skins = (session.chart.noiseAreaLayers ?? []).map(layer => layer.skin).filter(Boolean);
    return [...names].flatMap(name => { const bytes = assetBytes(assets, name, chartName); return bytes ? [[name, bytes]] : []; }).concat([...assets].filter(([name]) => skins.some(root => name.startsWith(root + '/'))));
  }
}), {
  notify, activate: activatePane,
  confirmJoin: run => guardReplace(run),
  receiveChart: (chart, owner) => {
    if (owner) { session.history.document = chart; return; }
    collaborationJoining = true;
    try { replaceChart(chart, '联机谱面.json'); } finally { collaborationJoining = false; }
    activatePane('collaboration');
  },
  receiveAsset: async (name, bytes) => {
    assets.set(name, bytes); assetDirty = true; diagnosticAssetRevision++; images.load(session.chart, assets, chartName);
    const references = resourceReferences(session.chart, assets, chartName);
    if (name === references.song || name === session.chart.META.song) await loadMusic(bytes, name, false);
    hitSounds.setProject(session.chart, assets, chartName); renderSession();
  }
});
element('#collaboration-tool').onclick = () => activatePane('collaboration');
const playback = new EditorPlayback(audio, hitSounds, () => {
  timeline.origin = currentBeat();
  for (const renderer of [preview, realtimePreview]) { renderer.noisePreview.gpu?.reset(); renderer.overflowNoisePreview?.gpu?.reset(); renderer.overflowRevision++; }
  preview.effectsSince = realtimePreview.effectsSince = chartSeconds() - .001;
  invalidate();
});
let performanceMonitor = null, monitorLoop = null;
function togglePerformanceMonitor() {
  performanceMonitor ??= new PerformanceMonitor({
    state: () => ({ canRun: hasDocument && !atHome && !dialogOpen() && !mods?.runtime.locks.size && Boolean(audio.buffer || audio.media), project: versionRepoId,
      name: session.chart.META.name || chartName, duration: audio.duration, rate: audio.rate, time: audio.time, playing: audio.playing, preview: preview.visible }),
    startPlayback: async begin => {
      directPlacement.close();
      if (eventSampler.active) eventSampler.finish(false);
      if (noiseEditor.sampleMode) noiseEditor.endSample(false);
      if (noteTrackingPanel.sampling) noteTrackingPanel.endSample();
      if (noiseTrackingPanel.sampling) noiseTrackingPanel.endSample();
      monitorLoop = loop; loop = null; element('#loop-enabled').checked = false;
      playback.pause(); togglePreview(true, true, false, { autoplay: false }); playback.seek(0);
      await hitSounds.prepare();
      if (!begin()) return;
      await togglePlayback();
    },
    stopPlayback: () => { playback.pause(); loop = monitorLoop; monitorLoop = null; element('#loop-enabled').checked = Boolean(loop); invalidate(); },
    notify: message => notify(message, 'info', 3500),
  });
  performanceMonitor.toggle();
}
window.addEventListener('keydown', event => {
  if (!shortcutMatches(event, preferences.hotkeys.PerformanceMonitor ?? DEFAULT_HOTKEYS.PerformanceMonitor) || event.isComposing || isTextEntry(event.target)) return;
  if (document.querySelector('dialog.hotkey-dialog[open]')) return;
  event.preventDefault(); event.stopImmediatePropagation();
  if (!event.repeat) togglePerformanceMonitor();
}, true);
timeline.onDragScroll = seconds => playback.seek(audio.time + seconds);
const autoSave = new AutoSaveClock(async () => {
  if (session.largeDocument) { await session.largeDocument.flush(); return; }
  const current = session; const document = current.chart; checkpointEditTime(); const snapshot = projectEditClock.snapshot(document); delete snapshot.chartTime;
  const media = [...assets], folders = [...assetFolders];
  await saveSnapshot(libraryProject?.id ?? recoveryId, chartName, snapshot, media, editorPreferences.autoSaveLimit ?? preferences.settings.autoSaveLimit, { lineIndex: current.lineIndex, vcsId: versionRepoId, multiLine: multiLineState(current) });
  if (current === session) { lastDraftDocument = document; lastDraftAssets = media; lastDraftFolders = folders; status(t('app.s43755f3')); notify(t('app.s0d49e0d'), 'success'); }
}, error => { const message = t('app.sf83f150', { p0: storageFailure(error) }); status(message); notify(message, 'error', 7000); });
timeline.onWheel = event => {
  if (directPlacement.active) { event.stopPropagation(); previewWheel(event); return; }
  if (event.ctrlKey) switchLineWithWheel(event.deltaY);
  else previewWheel(event);
};
function switchLineWithWheel(direction) {
  if (!direction) return;
  if (lineSwitcher.enabled) { lineSwitcher.step(direction); if (!preview.visible) lineSwitcher.show(); }
  else switchLine(direction);
}
function switchLine(direction) {
  const index = stepLine(session.lineIndex, direction, session.chart.judgeLineList?.length ?? 0);
  return selectOverviewLine(index);
}
function selectOverviewLine(index) {
  if (!Number.isInteger(index) || !session.chart.judgeLineList?.[index]) return false;
  batchControls.cancel(); pasteGesture.cancel(); timeline.cancelPlacement(); session.selectLine(index); return true;
}
const previewWheel = event => {
  event.preventDefault();
  linePicker.hide();
  if (preview.visible && event.ctrlKey) { switchLineWithWheel(event.deltaY); return; }
  playback.wheel(event, { ...preferences.settings, scrollSpeed: editorPreferences.scrollSpeed ?? preferences.settings.scrollSpeed, wheelInputMode: editorPreferences.wheelInputMode ?? preferences.settings.wheelInputMode, wheelStopAtAudioStart: editorPreferences.wheelStopAtAudioStart ?? preferences.settings.wheelStopAtAudioStart ?? true }, performance.now() / 1000, offsetSeconds());
};
element('.preview-wrap').addEventListener('wheel', previewWheel, { passive: false });
element('.canvases').addEventListener('wheel', event => {
  if (event.defaultPrevented || event.target !== event.currentTarget || atHome || preview.visible) return;
  event.preventDefault(); timeline.onWheel(event); invalidate();
}, { passive: false });
const pickPreviewLine = (renderer, event) => {
  if (directPlacement.active) return false;
  const matches = renderer.pickLines(event.clientX, event.clientY);
  const select = index => {
    if (!session.chart.judgeLineList[index] || !renderer.visible) return;
    timeline.cancelPlacement();
    if (editorPreferences.directPlacement && renderer === preview) {
      if (noiseEditor.active) noiseEditor.close(); if (globalEvents.active) globalEvents.close();
      eventSampler.finish(false); trajectoryPanel.hide(); textLinesPanel.hide(); noiseFitPanel.hide(); noiseTrackingPanel.hide(); noteTrackingPanel.hide();
      if (curveEditorOpen) closeCurvePanel();
      session.selectLine(index); directPlacement.open(index); element('#close-direct-placement').hidden = !directPlacement.active;
    } else session.selectLine(index);
    clearNoteSourceToast(); invalidate();
  };
  if ((editorPreferences.overlappingLinePicker ?? true) && matches.length > 1) {
    const pickedSession = session, chart = session.chart;
    linePicker.show(matches, event, index => { if (session === pickedSession && session.chart === chart) select(index); }); return true;
  }
  const index = renderer.pick(event.clientX, event.clientY);
  if (index === null) return false;
  select(index); return true;
};
element('#preview').addEventListener('click', event => {
  if (!preview.visible || directPlacement.active) return;
  if (noiseTrackingPanel.sampling) { noiseTrackingPanel.pick(event); return; }
  if (noteTrackingPanel.sampling) { noteTrackingPanel.pick(event); return; }
  const noiseIndex = preview.pickNoiseArea(event.clientX, event.clientY);
  if (Number.isInteger(noiseIndex)) {
    clearNoteSourceToast(); togglePreview(false, true); openNoisePanel(); noiseEditor.select(noiseIndex); noiseEditor.areaList.reveal(noiseIndex); return;
  }
  if ((editorPreferences.directPlacement || (editorPreferences.overlappingLinePicker ?? true) && preview.pickLines(event.clientX, event.clientY).length > 1) && pickPreviewLine(preview, event)) return;
  const noteLine = preview.pickNote(event.clientX, event.clientY);
  if (Number.isInteger(noteLine)) { timeline.cancelPlacement(); session.selectLine(noteLine); clearNoteSourceToast(); invalidate(); return; }
  pickPreviewLine(preview, event);
});
element('#preview').addEventListener('pointermove', event => {
  if (directPlacement.active || linePicker.active) { clearNoteSourceToast(); return; }
  if (noiseTrackingPanel.sampling) { clearNoteSourceToast(); return; }
  if (noteTrackingPanel.sampling) { clearNoteSourceToast(); return; }
  if (!preview.visible || (editorPreferences.noteSourceHover ?? preferences.settings.noteSourceHover ?? true) === false) { clearNoteSourceToast(); return; }
  const lineIndex = preview.pickNote(event.clientX, event.clientY);
  if (!Number.isInteger(lineIndex)) { clearNoteSourceToast(); return; }
  const key = `preview:${lineIndex}`;
  clearTimeout(noteHoverTimer); noteHoverTimer = null;
  if (key === noteHoverKey && !element('#note-source-toast')?.hidden) return;
  noteHoverKey = key;
  const toast = element('#note-source-toast'); if (!toast) return;
  toast.hidden = true;
  noteHoverTimer = setTimeout(() => {
    if (noteHoverKey !== key || !preview.visible) return;
    toast.textContent = t('app.s33058f1', { p0: lineIndex });
    toast.hidden = false;
    toast.onclick = () => { session.selectLine(lineIndex); clearNoteSourceToast(); invalidate(); };
  }, 1000);
});
element('#preview').addEventListener('pointerleave', event => { if (!element('#note-source-toast')?.contains(event.relatedTarget)) clearNoteSourceToast(); });
timeline.previewPick = () => false;
const home = new ProjectHome(async id => {
  const project = await readProject(id);
  if (!project) throw new Error(t('app.s4572dcf'));
  guardReplace(() => loadCandidate({ chart: project.chart, name: project.chartName, info: project.info, project }, new Map(project.assets)).catch(reportError));
}, reportError, project => {
  if (libraryProject?.id === project.id) {
    if (project.deleted) {
      libraryProject = null;
      if (atHome) {
        hasDocument = false; session.history.markSaved(); assetDirty = false;
        setHome(true);
      } else {
        session.history.savedDocument = null; renderSession();
      }
      return;
    }
    const renamed = libraryProject.chart.META.name !== project.chart.META.name;
    libraryProject = project;
    if (!session.history.dirty) {
      session.commit(t('app.sc456347'), project.chart); session.history.markSaved(); renderSession();
    } else if (renamed) session.commit(t('app.sf1249c2'), { ...session.chart, META: { ...session.chart.META, name: project.chart.META.name } });
  }
});
// After a language switch the page reloads; reopen the chart that was open.
try {
  const reopen = sessionStorage.getItem(LOCALE_REOPEN_KEY);
  if (reopen) { sessionStorage.removeItem(LOCALE_REOPEN_KEY); home.openProject(reopen).catch(reportError); }
} catch { /* storage unavailable */ }
const storagePanel = new StoragePanel(settingsDialog, { reportError, notify, fileOptions, canSwitchLibrary: () => {
  if (hasDocument && (session.history.dirty || assetDirty)) { notify(t('app.sa139201'), 'warning'); return false; }
  return true;
} });
dictationInput = new DictationInput(settingsDialog, { notify });
async function persistVersionRepoId() {
  if (!libraryProject?.id || libraryProject.vcsId) return;
  const current = session, repoId = versionRepoId, project = libraryProject;
  const saved = await storeProject({ ...project, vcsId: repoId });
  if (session === current && libraryProject?.id === project.id && versionRepoId === repoId) libraryProject = saved;
  storagePanel.refresh().catch(() => {});
}
versionControl = new VersionControlPanel(element('#version-control-panel'), {
  getChart: () => session.chart,
  getAssets: () => assets,
  getFolders: () => assetFolders,
  getChartName: () => chartName,
  getRepoId: () => versionRepoId,
  isDirty: () => session.history.dirty || assetDirty,
  isPersisted: () => Boolean(libraryProject?.id),
  hasDocument: () => hasDocument && !atHome,
}, {
  notify, reportError,
  loadSnapshot: (snapshot, options) => loadVersionSnapshot(snapshot, options),
  persistRepoId: persistVersionRepoId,
});
document.addEventListener('rpen-preference-error', event => reportError(event.detail));
document.addEventListener('rpen-library-refresh', () => home.refresh().catch(reportError));
subscribeLibrary(change => {
  if (atHome) home.refresh().catch(reportError);
  if (libraryProject?.id === change.id && change.revision !== libraryProject.storageRevision) notify(change.deleted ? t('app.s6516ed3') : t('app.s69c0878'), 'warning', 7000);
});

function setHome(visible) {
  checkpointEditTime();
  if (visible) closeShaderWorkspace();
  if (visible) speedCurve.close();
  if (visible) performanceMonitor?.finish('interrupted');
  if (visible) diagnosticHighlight.clear();
  if (visible) { placementDefaultsPanel.hide(); session.noteDefaults = normalizeNoteDefaults(); diagnostics.reset(); lastDiagnosticSignature = null; }
  if (visible) quickCurve.cancel();
  if (visible) { linePicker.hide(); directPlacement.close(); }
  if (visible) closeAnimationCurveEditor();
  agentPanel?.reset();
  if (visible) { audioSources.reset(audio.buffer); spatialTool.reset(); }
  if (visible && noiseTrackingPanel.sampling) noiseTrackingPanel.endSample();
  if (visible && noteTrackingPanel.sampling) noteTrackingPanel.endSample();
  if (visible && collaboration.client.active) collaboration.client.leave();
  atHome = visible;
  if (!visible && hasDocument) agentPanel?.attach(versionRepoId);
  if (visible) clearNoteSourceToast();
  if (visible) { lineSwitcher.reset(); hitSounds.onlyCurrentLine = false; element('#mute-current-line')?.setAttribute('aria-pressed', 'false'); element('#mute-current-line')?.classList.remove('active'); }
  if (visible) { lineInfoVisible = false; element('#line-info-overlay')?.setAttribute('hidden', ''); }
  element('#document-name').textContent = visible ? t('doc.home') : `${session.history.dirty ? '● ' : ''}${chartDisplayName(session.chart.META.name || chartName)}`;
  element('#home').hidden = !visible;
  element('.workspace').hidden = visible;
  element('.transport').hidden = visible;
  element('#resume-editor').hidden = !hasDocument;
  element('#render-video').disabled = visible || !hasDocument;
  for (const selector of ['#save', '#export']) element(selector).disabled = !hasDocument;
  if (visible) { playback.pause(); timeline.cancelPlacement(); home.refresh().catch(reportError); }
  invalidate();
}

let pendingTimelineMenu = null;
function timelineMarkers() {
  return Array.isArray(session.chart?.markers)
    ? session.chart.markers.filter(marker => Number.isFinite(Number(marker?.time)) && String(marker?.name ?? '').trim())
    : [];
}
function renderTimelineMarkers() {
  const root = element('#timeline-markers'); if (!root) return;
  root.replaceChildren();
  const width = root.clientWidth || element('#scrubber')?.clientWidth || 1;
  const duration = Math.max(0.001, audio.duration > 0 ? audio.duration : timelineActivity.duration || 1);
  for (const [index, marker] of timelineMarkers().entries()) {
    const button = document.createElement('button'); button.type = 'button'; button.className = 'timeline-marker'; button.textContent = String(marker.name).trim();
    const ratio = Math.max(0, Math.min(1, Number(marker.time) / duration));
    button.style.left = `${ratio * width}px`; button.style.width = 'max-content'; button.style.maxWidth = '160px'; button.style.whiteSpace = 'nowrap'; button.title = `${marker.name} · ${Number(marker.time).toFixed(3)} s`;
    button.onclick = () => playback.seek(Number(marker.time) + offsetSeconds());
    button.oncontextmenu = event => { event.preventDefault(); event.stopPropagation(); openTimelineContextMenu(event, null, index); };
    root.append(button);
  }
}
function openTimelineContextMenu(event, canvas = null, markerIndex = null) {
  const menu = element('#timeline-context-menu'); if (!menu) return;
  selectionIntervalPanel.hide();
  placementDefaultsPanel.hide();
  const point = canvas ? timeline.point(event, canvas) : null;
  pendingTimelineMenu = { time: point ? timeline.timeAt(point.y) : null, markerIndex };
  element('#timeline-add-marker').hidden = !canvas;
  element('#timeline-delete-marker').hidden = markerIndex === null;
  element('#timeline-selection-interval').hidden = markerIndex !== null;
  element('#timeline-placement-defaults').hidden = markerIndex !== null;
  const speedCurveItem = element('#timeline-speed-curve');
  speedCurveItem.hidden = markerIndex !== null;
  speedCurveItem.disabled = !singleSpeedCurveNote(session) || noiseEditor.active || globalEvents.active;
  speedCurveItem.onclick = () => {
    const entry = singleSpeedCurveNote(session), source = timeline.notesCanvas.getBoundingClientRect();
    const point = entry && !preview.visible ? { x: source.left + timeline.noteHorizontal(entry.note.positionX, entry.lineIndex), y: source.top + timeline.noteVertical(entry.note, 'startTime', entry.lineIndex) } : { x: event.clientX, y: event.clientY };
    closeTimelineContextMenu(); eventSampler.finish(false); speedCurve.open(point);
  };
  const scaleItem = element('#timeline-event-value-scale');
  scaleItem.hidden = markerIndex !== null || canvas !== timeline.eventsCanvas || !valueScaleOptions.enabled || noiseEditor.active || globalEvents.active;
  scaleItem.disabled = session.focus !== 'events' || !(session.eventSelection.size || [...session.multiEventSelection.values()].some(selection => selection.size));
  scaleItem.onclick = () => { closeTimelineContextMenu(); batchControls.enterValueScale(); };
  element('#timeline-selection-interval').textContent = t('app.s3f3831b', { p0: timeline.selectionInterval ?? 1 });
  const selections = session.multiLineActive && session.multiLineMode === 'events'
    ? [...session.multiEventSelection].flatMap(([lineIndex, keys]) => [...keys].map(key => ({ lineIndex, key })))
    : [...session.eventSelection].map(key => ({ lineIndex: session.lineIndex, key }));
  const selection = selections.length === 1 ? selections[0] : null;
  const [type, index] = selection?.key.split(':') ?? [];
  const selected = type === 'moveXEvents' ? session.chart.judgeLineList[selection.lineIndex]?.eventLayers?.[timeline.layer]?.moveXEvents?.[Number(index)] : null;
  const split = element('#trajectory-context-split'); split.hidden = !selected?.trajectory || canvas !== timeline.eventsCanvas;
  split.onclick = () => { closeTimelineContextMenu(); trajectoryPanel.open(selected, selection.lineIndex, timeline.layer); trajectoryPanel.split(); };
  menu.hidden = false;
  const bounds = menu.getBoundingClientRect();
  menu.style.left = `${Math.max(4, Math.min(window.innerWidth - bounds.width - 4, event.clientX))}px`;
  menu.style.top = `${Math.max(4, Math.min(window.innerHeight - bounds.height - 4, event.clientY))}px`;
}
function closeTimelineContextMenu() { const menu = element('#timeline-context-menu'); if (menu) menu.hidden = true; pendingTimelineMenu = null; }
function addTimelineMarker() {
  const pending = pendingTimelineMenu; closeTimelineContextMenu(); if (!pending || !Number.isFinite(pending.time)) return;
  const content = showDialog(t('app.s0898b72'), t('app.sc340606', { p0: pending.time.toFixed(3) }));
  const input = document.createElement('input'); input.type = 'text'; input.placeholder = t('app.sd762586'); input.setAttribute('aria-label', t('app.sd410ed1')); content.append(input);
  const apply = element('#modal-apply'); apply.hidden = false; apply.onclick = () => {
    const name = String(input.value ?? '').trim(); if (!name) { element('#modal-error').textContent = t('app.s7d9692d'); return; }
    const markers = [...timelineMarkers(), { time: pending.time, name }].sort((left, right) => left.time - right.time);
    session.commit(t('app.s0898b72'), { ...session.chart, markers }); element('#modal').close();
  }; input.focus();
}
function deleteTimelineMarker() {
  const pending = pendingTimelineMenu; closeTimelineContextMenu(); if (!pending || pending.markerIndex === null) return;
  const markers = timelineMarkers().filter((unused, index) => index !== pending.markerIndex);
  session.commit(t('app.sa5ceb33'), { ...session.chart, markers });
}
window.addEventListener('pointerdown', event => { const menu = element('#timeline-context-menu'); if (menu && !menu.contains(event.target)) closeTimelineContextMenu(); }, true);
element('#timeline-add-marker').addEventListener('click', addTimelineMarker);
element('#timeline-delete-marker').addEventListener('click', deleteTimelineMarker);
element('#timeline-selection-interval').addEventListener('click', () => {
  const bounds = element('#timeline-context-menu').getBoundingClientRect(); closeTimelineContextMenu(); selectionIntervalPanel.open(bounds.left, bounds.top);
});
element('#timeline-placement-defaults').addEventListener('click', () => {
  const bounds = element('#timeline-context-menu').getBoundingClientRect(); closeTimelineContextMenu(); placementDefaultsPanel.open(bounds.left, bounds.top);
});

function persistEditor() {
  editorPreferences = { ...editorPreferences, scale: timeline.scale, division: timeline.division, gridCount: timeline.gridCount, snapX: timeline.snapX, judgementOffset: timeline.judgementOffset, multiLineWidth: timeline.multiLineWidth || undefined, multiLineEventWidth: timeline.multiLineEventWidth || undefined,
    multiLineEventVisibility: timeline.multiLineEventVisibility,
    realtime: realtimePreview.visible, realtimeAlpha: Number(element('#realtime-alpha').value), volume: audio.volume, hitVolume: hitSounds.volume,
    hitEnabled: hitSounds.enabled, allLines: preview.allLines, toolbarMode: editorPreferences.toolbarMode ?? 'icons',
    analysisEnabled: audioAnalysis.enabled, analysisMode: audioAnalysis.mode, analysisAlpha: audioAnalysis.alpha, analysisWidth: audioAnalysis.width };
  try { writeEditorPreferences(editorPreferences); } catch (error) { status(t('app.s0971abd', { p0: error.message })); }
}

function activatePane(name) {
  if (session.largeDocument && ['version-control', 'collaboration', 'assets', 'ai', 'diagnose'].includes(name)) { notify('超大谱工程请使用顶部保存或工程另存为；此工具尚未支持完整的磁盘谱面。', 'warning'); return; }
  if (diagnosticNavigation && name !== 'diagnose') return;
  if (activePaneName === 'assets' && name !== 'assets') assetLibrary.disposeUrls();
  if (name !== 'notes') disposeNoteProperties();
  if (name === 'global-event-properties' && (!globalEvents.active || globalEvents.selection.size !== 1)) name = 'global-events';
  if (name === 'global-events' && !globalEvents.active) { openGlobalEvents(); return; }
  if (name !== 'events' && eventSampler.active) eventSampler.finish(false);
  if (name !== 'multi') multiEdit.hide();
  if (name !== 'trajectory') trajectoryPanel.hide();
  if (name !== 'text-lines') textLinesPanel.hide();
  if (name !== 'noise-fit') noiseFitPanel.hide();
  if (name !== 'noise-tracking') noiseTrackingPanel.hide();
  if (name !== 'note-tracking') noteTrackingPanel.hide();
  activePaneName = name;
  editorSkinState({ pane: name });
  noiseEditor.syncChrome();
  globalEvents.syncChrome();
  for (const panel of document.querySelectorAll('[data-panel]')) panel.hidden = panel.dataset.panel !== name;
  for (const button of document.querySelectorAll('[data-pane]')) button.classList.toggle('active', button.dataset.pane === name);
  if (globalEvents.active && ['global-events', 'global-event-properties'].includes(name)) globalEvents.render();
  if (name === 'version-control') versionControl?.open().catch(reportError);
  invalidate();
}
for (const button of document.querySelectorAll('[data-pane]')) button.onclick = () => {
  const pane = button.dataset.pane;
  if (['notes', 'events'].includes(pane)) session.focus = pane;
  activatePane(pane);
  if (pane === 'lines') linePanel.render();
};
function openMultiLinePanel() { directPlacement.close(); if (globalEvents.active) globalEvents.close(); activatePane('multi-line'); multiLinePanel.render(); }
function openNoisePanel() { directPlacement.close(); if (globalEvents.active) globalEvents.close(); timeline.cancelPlacement(); batchControls.cancel(); pasteGesture.cancel(); multiEdit.hide(); trajectoryPanel.hide(); curveEditorOpen = false; noiseEditor.open(); }
function openGlobalEvents() {
  directPlacement.close();
  if (noiseEditor.active) noiseEditor.close();
  eventSampler.finish?.(false); timeline.cancelPlacement(); batchControls.cancel(); pasteGesture.cancel(); multiEdit.hide(); trajectoryPanel.hide(); curveEditorOpen = false;
  globalEvents.open();
  renderSession();
}
const globalEventsTool = document.createElement('button'); globalEventsTool.id = 'global-events-tool'; globalEventsTool.textContent = t('app.s895a20e'); element('#noise-areas-tool').after(globalEventsTool);
const aiTool = document.createElement('button'); aiTool.id = 'ai-tool'; aiTool.textContent = t('app.s9efe3ea'); element('#history-panel').before(aiTool);
const noteTrackingTool = document.createElement('button'); noteTrackingTool.id = 'note-tracking-tool'; noteTrackingTool.textContent = t('note-track.title'); element('#noise-tracking-tool').after(noteTrackingTool);
noteTrackingTool.onclick = () => { directPlacement.close(); quickCurve.cancel(); if (noiseEditor.active) noiseEditor.close(); if (globalEvents.active) globalEvents.close(); eventSampler.finish(false); timeline.cancelPlacement(); batchControls.cancel(); pasteGesture.cancel(); curveEditorOpen = false; noteTrackingPanel.open(); };
globalEventsTool.onclick = openGlobalEvents;
element('#close-global-events').onclick = () => { globalEvents.close(); renderSession(); };
element('#noise-areas-tool').addEventListener('click', openNoisePanel);
element('#close-noise-editor').addEventListener('click', () => noiseEditor.close());
element('#noise-tab').addEventListener('click', () => { activatePane('noise'); noiseEditor.render(); });
element('#multi-line-tool').addEventListener('click', openMultiLinePanel);
element('#multi-line-open').addEventListener('click', openMultiLinePanel);
element('#multi-line-toggle').addEventListener('click', () => {
  directPlacement.close();
  const enabling = !session.multiLineEnabled;
  session.setMultiLineEnabled(enabling, session.focus === 'events' ? 'events' : 'notes');
  if (enabling) openMultiLinePanel();
});
element('#multi-line-prev-add').addEventListener('click', () => session.addPreviousMultiLine());
element('#multi-line-prev-remove').addEventListener('click', () => session.removeMinimumMultiLine());
element('#multi-line-next-add').addEventListener('click', () => session.addNextMultiLine());
element('#multi-line-next-remove').addEventListener('click', () => session.removeMaximumMultiLine());
element('#multi-line-merge').addEventListener('click', () => session.setMultiLineMerge(!session.multiLineMerge));
for (const button of document.querySelectorAll('[data-return-chart]')) button.onclick = () => activatePane('chart');
for (const form of document.querySelectorAll('#properties, #event-properties')) form.addEventListener('submit', event => { event.preventDefault(); document.activeElement?.blur(); });
for (const button of document.querySelectorAll('[data-icon]')) button.style.setProperty('--icon', `url('${assetUrl(`rpe/Texture/icon/${button.dataset.icon}.png`)}')`);
function togglePreview(force, stay = false, replay = false, { autoplay = true } = {}) {
  const visible = force ?? !preview.visible;
  if (!visible) performanceMonitor?.finish('interrupted');
  linePicker.hide();
  if (!visible) directPlacement.close();
  if (visible) { lineSwitcher.hide(); clearNoteSourceToast(); }
  if (visible && (!preview.visible || replay)) {
    previewReturnTime = audio.time; timeline.cancelPlacement();
    if (replay) playback.seek(0);
    if (autoplay && (editorPreferences.autoplayView ?? preferences.settings.autoplayView) && !audio.playing && !playback.pending) togglePlayback().catch(reportError);
  }
  if (!visible && preview.visible) playback.seek(stay ? audio.time : previewReturnTime);
  preview.visible = visible;
  if (!visible && spatialTool.enabled) spatialTool.panel.hideAll();
  element('.preview-wrap').hidden = !preview.visible;
  element('#view-toggle').classList.toggle('active', preview.visible);
  globalEvents.syncChrome();
  element('#multi-line-labels').hidden = preview.visible;
  element('#multi-line-scroll').hidden = preview.visible;
  element('#preview-title').textContent = chartDisplayName(session.chart.META.name);
  timeline.origin = currentBeat();
  invalidate();
}

function listen(selector, callback) {
  element(selector).addEventListener('click', async () => {
    try { await callback(); } catch (error) { reportError(error); }
  });
}

let lineInfoVisible = false;
const lineInfoNumber = (value, digits = 2) => Number.isFinite(value) ? value.toFixed(digits) : '0.00';
function selectedEventSpeed(type, event) {
  if (!event) return null;
  const duration = tempo.seconds(event.endTime, session.line?.bpmfactor ?? 1) - tempo.seconds(event.startTime, session.line?.bpmfactor ?? 1);
  if (!Number.isFinite(duration) || Math.abs(duration) < 0.000001 || !Number.isFinite(event.start) || !Number.isFinite(event.end)) return null;
  const rate = (event.end - event.start) / duration;
  if (type === 'moveXEvents') return `X ${lineInfoNumber(rate / 120)}`;
  if (type === 'moveYEvents') return `Y ${lineInfoNumber(rate / 120)}`;
  if (type === 'rotateEvents') return `R ${lineInfoNumber(rate)}`;
  if (type === 'speedEvents') return `Speed ${lineInfoNumber(rate / 120)}`;
  return null;
}
function updateLineInfo() {
  const overlay = element('#line-info-overlay');
  if (!overlay) return;
  if (!lineInfoVisible || !session.line || atHome) { overlay.hidden = true; return; }
  lineInfoScene.compile(session.largeDocument?.header ?? session.chart, tempo);
  const seconds = chartSeconds();
  const state = lineInfoScene.sampler(seconds)(session.lineIndex) ?? { x: 0, y: 0, rotation: 0, alpha: 255 };
  const runtime = lineInfoScene.lines[session.lineIndex];
  const scrollSpeed = runtime?.speeds?.reduce((sum, track) => sum + (track.value(seconds) || 0), 0) ?? 0;
  const line = session.line;
  const lineText = `Pos: (${lineInfoNumber(state.x)},${lineInfoNumber(state.y)})  Dir: ${lineInfoNumber(state.rotation)}  Alpha: ${lineInfoNumber(state.alpha, 0)}  Speed: ${lineInfoNumber(scrollSpeed)}`;
  const dt = 1 / 120;
  const before = lineInfoScene.sampler(seconds - dt)(session.lineIndex) ?? state;
  const after = lineInfoScene.sampler(seconds + dt)(session.lineIndex) ?? state;
  const xSpeed = (after.x - before.x) / (2 * dt) / 120;
  const ySpeed = (after.y - before.y) / (2 * dt) / 120;
  const rotateSpeed = (after.rotation - before.rotation) / (2 * dt);
  const activeSpeeds = [];
  for (const [type, label] of [['moveXEvents', 'X'], ['moveYEvents', 'Y'], ['rotateEvents', 'R'], ['speedEvents', 'Speed']]) {
    for (const track of runtime?.tracks?.[type] ?? []) {
      for (const entry of track.events ?? []) {
        if (seconds < entry.start || seconds > entry.end) continue;
        const rate = selectedEventSpeed(type, entry.event);
        if (rate) activeSpeeds.push(rate);
        else if (type === 'speedEvents') activeSpeeds.push(`${label} ${lineInfoNumber(track.value(seconds))}`);
      }
    }
  }
  const selectedSpeeds = [];
  for (const key of session.eventSelection ?? []) {
    const separator = key.lastIndexOf(':');
    const type = separator < 0 ? key : key.slice(0, separator);
    const index = Number(key.slice(separator + 1));
    const event = eventList(session, type)[index];
    const speed = selectedEventSpeed(type, event);
    if (speed) selectedSpeeds.push(speed);
  }
  const eventSpeed = selectedSpeeds.length ? selectedSpeeds : activeSpeeds;
  const eventText = `X Y R Speed: ${lineInfoNumber(xSpeed)}, ${lineInfoNumber(ySpeed)}, ${lineInfoNumber(rotateSpeed)}${eventSpeed.length ? `\nEvent Speed: ${eventSpeed.slice(0, 6).join(', ')}` : ''}`;
  const globalShaders = [];
  for (const index of session.chart.judgeLineList?.keys?.() ?? []) {
    const eventBeat = tempo.beat(seconds, session.chart.judgeLineList[index]?.bpmfactor ?? 1);
    for (const event of shaderEvents(session.chart, index)) {
      if (!event.global) continue;
      const start = beatValue(event.startTime); const end = beatValue(event.endTime ?? event.startTime);
      if (eventBeat >= start && eventBeat < end) globalShaders.push(shaderDisplayName(event));
    }
  }
  const shaderText = globalShaders.length ? `\nShader: ${globalShaders.join(', ')}` : '';
  overlay.textContent = `${lineText}\n${eventText}${shaderText}`;
  overlay.hidden = false;
}

function renderSession() {
  if (speedCurve.active) speedCurve.sync();
  editorSkinState({ dirty: session.history.dirty || assetDirty, location: atHome ? 'home' : 'editor', mode: globalEvents.active ? 'global-events' : noiseEditor.active ? 'noise' : session.multiLineActive ? 'multi-' + session.multiLineMode : timeline.notesOnly ? 'notes' : 'notes-events' });
  const editor = element('.editor');
  noiseEditor.syncChrome();
  editor.classList.toggle('multi-notes', session.multiLineActive && session.multiLineMode === 'notes');
  editor.classList.toggle('multi-events', session.multiLineActive && session.multiLineMode === 'events');
  const context = `${session.lineIndex}:${timeline.layer}`;
  if (placementContext !== context) { timeline.cancelPlacement(); placementContext = context; }
  if (tempoEntries !== session.chart.BPMList) { tempoEntries = session.chart.BPMList; tempo = new TempoMap(tempoEntries); }
  timeline.tempo = tempo;
  session.tempo = tempo; session.division = timeline.division;
  session.cutDensity = Number(element('#event-cut-density').value);
  session.dragSensitivity = Number(element('#event-drag-sensitivity').value);
  session.rpeEventDrag = timeline.rpeEventDrag;
  session.xyBinding = element('#xy-binding').checked;
  session.movementVector = !noiseEditor.active && !globalEvents.active && !preview.visible && element('#movement-vector').checked;
  updateLineInfo();
  session.eventWheelSteps = Object.fromEntries(['moveXEvents', 'moveYEvents', 'rotateEvents', 'alphaEvents', 'speedEvents', 'scaleXEvents', 'scaleYEvents'].flatMap((key, index) => Number.isFinite(preferences.originalSettings.scrollValueIncrement?.[index]) ? [[key, preferences.originalSettings.scrollValueIncrement[index]]] : []));
  timeline.origin = currentBeat();
  element('#document-name').textContent = atHome ? t('doc.home') : `${session.history.dirty ? '● ' : ''}${chartDisplayName(session.chart.META.name || chartName)}`;
  timeline.refreshMultiLineExpression();
  renderTimelineMarkers();
  element('#note-count').textContent = `${session.largeDocument?.summary.lines[session.lineIndex] ?? session.notes.length} notes`;
  const countEvents = line => [...(line.eventLayers ?? []), line.extended ?? {}].reduce((total, layer) => total + Object.entries(layer ?? {}).filter(([type, events]) => type !== 'paintEvents' && Array.isArray(events)).reduce((count, [, events]) => count + events.length, 0), 0) + shaderEvents(session.chart, session.chart.judgeLineList?.indexOf(line) ?? -1).length;
  const totalLines = session.chart.judgeLineList?.length ?? 0;
  const totalNotes = session.largeDocument?.summary.total ?? (session.chart.judgeLineList ?? []).reduce((count, line) => count + (line.notes?.length ?? 0), 0);
  const totalEvents = (session.chart.judgeLineList ?? []).reduce((count, line) => count + countEvents(line), 0);
  const chartInfo = [[t('app.sd3892f9'), `Notes: ${session.notes.length}  Events: ${countEvents(session.line ?? {})}  TotalLines: ${totalLines}  TotalNotes: ${totalNotes}  TotalEvents: ${totalEvents}  Time: ${formatEditTime(editTimeSeconds)}`]];
  element('#chart-info').replaceChildren(...chartInfo.flatMap(([label, value]) => {
    const term = document.createElement('dt');
    term.textContent = label;
    const description = document.createElement('dd');
    description.textContent = value;
    return [term, description];
  }));
  const songName = session.chart.META?.song;
  const backgroundName = session.chart.META?.background;
  const largeMedia = session.largeDocument?.summary.media;
  const missingMedia = [!largeMedia?.music && (!songName || !assets.has(songName)) ? t('app.safb3c40') : '', !largeMedia?.cover && (!backgroundName || !assets.has(backgroundName)) ? t('app.sb8bb31e') : ''].filter(Boolean);
  const materialHint = element('#chart-material-hint');
  if (materialHint) materialHint.textContent = missingMedia.length ? t('app.s1674bb2', { p0: missingMedia.join('、') }) : '';
  const mediaStatus = element('#metadata-media-status');
  if (mediaStatus) mediaStatus.textContent = t('app.s40fd2fc', { p0: songName && assets.has(songName) ? songName : t('app.sab8115f'), p1: backgroundName && assets.has(backgroundName) ? backgroundName : t('app.sab8115f') });
  syncProjectFolderButton();
  if (!session.liveBeatEdit) element('#offset').value = session.chart.META.offset ?? 0;
  const { notes: selectedNoteCount, events: selectedEventCount } = selectionCounts(session);
  element('#selection-info').textContent = session.focus === 'events' ? t('app.sa922b31', { p0: selectedEventCount }) : t('app.s4112683', { p0: selectedNoteCount });
  element('#undo').disabled = session.largeDocument?.busy || !session.history.undoStack.length;
  element('#redo').disabled = session.largeDocument?.busy || !session.history.redoStack.length;
  for (const [id, label] of [['save', t('large-chart.save')], ['export', t('large-chart.saveAs')]]) {
    const button = element('#' + id); button.dataset.standardLabel ??= button.textContent;
    button.textContent = session.largeDocument ? label : button.dataset.standardLabel;
  }
  for (const id of ['music', 'background', 'assets', 'audio-leading-silence', 'version-control-tool', 'collaboration-tool', 'ai-tool', 'diagnose']) { const button = element('#' + id); if (button) button.disabled = Boolean(session.largeDocument); }
  element('#batch-run').disabled = !session.selection.size;
  element('#line-select').replaceChildren();
  (session.chart.judgeLineList ?? []).forEach((line, index) => {
    const option = document.createElement('option'); option.value = index; option.textContent = lineDisplayLabel(session.chart, index); element('#line-select').append(option);
  });
  element('#line-select').value = session.lineIndex;
  element('#multi-line-toggle').classList.toggle('active', session.multiLineActive);
  element('#multi-line-toggle').setAttribute('aria-pressed', String(session.multiLineActive));
  element('#notes-only').disabled = session.multiLineActive || noiseEditor.active || globalEvents.active;
  element('#multi-line-count').textContent = session.multiLineActive ? session.multiLineIndices.length : 0;
  element('#multi-line-merge').classList.toggle('active', session.multiLineMerge && session.multiLineMode === 'notes');
  element('#multi-line-merge').disabled = !session.multiLineActive || session.multiLineMode !== 'notes';
  const hasLines = session.multiLineActive && session.multiLineIndices.length;
  element('#multi-line-prev-add').disabled = !hasLines || session.multiLineIndices.length >= session.chart.judgeLineList.length;
  element('#multi-line-next-add').disabled = !hasLines || session.multiLineIndices.length >= session.chart.judgeLineList.length;
  element('#multi-line-prev-remove').disabled = !hasLines;
  element('#multi-line-next-remove').disabled = !hasLines;
  element('#line-next').disabled = element('#line-previous').disabled = session.chart.judgeLineList.length < 2;
  const lineGroup = element('#line-group-label');
  if (lineGroup) { const line = session.line; lineGroup.textContent = line && !isDefaultLineGroup(session.chart, line) ? lineGroupName(session.chart, line) : ''; lineGroup.title = lineGroup.textContent ? t('app.s595b038', { p0: lineGroup.textContent }) : ''; }
  if (activePaneName === 'lines') linePanel.render();
  updateLayerButtons();
  diagnostics.update();
  if (activePaneName === 'diagnose') renderDiagnostics();
  if (activePaneName === 'history') renderHistoryPanel();
  if (activePaneName === 'multi') multiEdit.sync();
  if (noiseTrackingPanel.active) noiseTrackingPanel.sync();
  if (noteTrackingPanel.active) noteTrackingPanel.sync();
  if (activePaneName === 'multi-line') multiLinePanel.render();
  if (noiseEditor.active) noiseEditor.render();
  if (globalEvents.active) globalEvents.render();
  if (activePaneName === 'metadata') renderMetadataPanel(session, element('#metadata-editor'), () => { activatePane('chart'); renderSession(); });
  if (activePaneName === 'bpm') renderBpmPanel(session, element('#bpm-editor'));
  session.eventLayer = timeline.layer;
  renderProperties(session, reportError, { getAssets: () => assets, getChartName: () => chartName, volume: () => hitSounds.volume, onHitSoundChange: () => { hitSounds.chart = session.chart; if (audio.playing) hitSounds.prepare().catch(reportError); } });
  if (!session.liveEventEdit) renderEventInspector(session, tempo, currentBeat, reportError, notify, eventSampler);
  if (session.eventSelection.size) {
    timeline.eventPlacementType = session.eventSelection.values().next().value.split(':')[0];
  }
  const multiEventSignature = [...(session.multiEventSelection ?? new Map())].map(([line, values]) => `${line}:${[...values].sort().join(',')}`).sort().join('|');
  const multiNoteSignature = [...(session.multiLineSelection ?? new Map())].map(([line, values]) => `${line}:${[...values].sort((left, right) => left - right).join(',')}`).sort().join('|');
  const selectionSignature = `${session.lineIndex}:${session.eventLayer}|${session.focus}|${[...session.selection].sort((left, right) => left - right).join(',')}|${[...session.eventSelection].sort().join(',')}|${multiNoteSignature}|${multiEventSignature}|${session.largeDocument?.logicalSelection?.token??''}`;
  const selectionRangeMode = session.multiLineActive ? session.multiLineMode : 'single';
  if (!timelineStrips.options.historyStrip) { timeline.selectionTimeRange = null; timeline.selectionRangeChart = null; }
  else if (timeline.selectionRangeChart !== session.chart || timeline.selectionRangeSignature !== selectionSignature || timeline.selectionRangeTempo !== tempo || timeline.selectionRangeMode !== selectionRangeMode) {
    timeline.selectionRangeChart = session.chart; timeline.selectionRangeSignature = selectionSignature; timeline.selectionRangeTempo = tempo;
    timeline.selectionRangeMode = selectionRangeMode;
    timeline.selectionTimeRange = selectionTimeRange(session, tempo);
  }
  if (selectionSignature !== lastSelectionSignature) {
    lastSelectionSignature = selectionSignature;
    const selectionCount = session.focus === 'events' ? selectedEventCount : selectedNoteCount;
    if (activePaneName === 'diagnose') {}
    else if (globalEvents.active) globalEvents.render();
    else if (noiseEditor.active) noiseEditor.render();
    else if (multiEdit.committing) multiEdit.sync();
    else if (activePaneName === 'collaboration') collaboration.renderState();
    else if (activePaneName === 'ai' || activePaneName === 'spatial') {}
    else if (textLinesPanel.active) textLinesPanel.sync();
    else if (noiseFitPanel.active) noiseFitPanel.sync();
    else if (noiseTrackingPanel.active) noiseTrackingPanel.sync();
    else if (noteTrackingPanel.active) noteTrackingPanel.sync();
    else if (trajectoryPanel.active && selectionCount !== 1) activatePane('trajectory');
    else if (curveEditorOpen) activatePane('curve');
    else if (activePaneName === 'clipboard') renderClipboardPanel();
    else if (selectionCount > 1) { activatePane('multi'); multiEdit.open(session.focus === 'events' ? 'events' : 'notes'); }
    else if (selectionCount === 1) {
      const entries = session.multiLineActive && session.multiLineMode === 'events'
        ? [...session.multiEventSelection].flatMap(([lineIndex, keys]) => [...keys].map(key => ({ lineIndex, key })))
        : [...session.eventSelection].map(key => ({ lineIndex: session.lineIndex, key }));
      const selected = entries.length === 1 ? entries[0] : null;
      const [type, index] = selected?.key.split(':') ?? [];
      const event = session.chart.judgeLineList[selected?.lineIndex]?.eventLayers?.[timeline.layer]?.[type]?.[Number(index)];
      if (session.focus === 'events' && event?.trajectory) { curveEditorOpen = false; trajectoryPanel.open(event, selected.lineIndex, timeline.layer); }
      else activatePane(session.focus === 'events' ? 'events' : 'notes');
    }
    else if (!['multi-line', 'lines', 'assets', 'collaboration', 'version-control'].includes(activePaneName)) activatePane('chart');
  }
  const limits = previewLimitations(session.chart);
  element('#compatibility').textContent = t('app.s22feffd') + (limits.length ? t('app.s7f51215', { p0: limits.join('、') }) : '');
  invalidate();
  globalEvents.syncChrome();
  versionControl?.tick?.();
}

session.addEventListener('change', renderSession);

function replaceChart(chart, name, nextAssets = new Map(), nextFolders = [], options = {}) {
  checkpointEditTime();
  speedCurve.close();
  if (largeChartWorkspace) {
    const previous = largeChartWorkspace; largeChartWorkspace = null;
    previous.dispose(); closingLargeChart = previous.close().catch(reportError);
  }
  performanceMonitor?.finish('interrupted');
  diagnosticHighlight.clear();
  diagnostics.reset(); lastDiagnosticSignature = null;
  placementDefaultsPanel.hide();
  quickCurve.cancel();
  linePicker.hide(); directPlacement.close();
  chart = normalizeLineGroups(chart);
  closeAnimationCurveEditor();
  agentPanel?.reset();
  assertChart(chart);
  if (!options.preserveRepoId) { versionRepoId = options.repoId ?? crypto.randomUUID(); versionControl?.detach(); }
  if (eventSampler.active) eventSampler.finish(false);
  noiseEditor.reset(); spatialTool.reset();
  globalEvents.reset();
  noiseFitPanel.reset();
  noiseTrackingPanel.reset();
  noteTrackingPanel.reset();
  if (!collaborationJoining && collaboration.client.active) collaboration.client.leave();
  lineSwitcher.reset();
  disposeNoteProperties(); assetLibrary.disposeUrls();
  playback.pause(); audio.clear();
  audioAnalysis.setBuffer(null);
  hitSounds.stop(); hitSounds.onlyCurrentLine = false; element('#mute-current-line').setAttribute('aria-pressed', 'false'); element('#mute-current-line').classList.remove('active'); images.clear(); libraryProject = null;
  session.removeEventListener('change', renderSession);
  session = new EditorSession(chart, { lineEventDefaults: editorPreferences.lineEventDefaults });
  session.getProjectEditTime = checkpointEditTime;
  multiLineStateStore.attach(session, options.viewStateKey ?? versionRepoId, options.multiLineState);
  spatialLineStateStore.attach(session, options.viewStateKey ?? versionRepoId, options.spatialLineState);
  if (options.dirty) session.history.markUnsaved();
  clipboardHistory.attach(session);
  lastSelectionSignature = '';
  activatePane('chart');
  session.history.limit = preferences.settings.historyLimit;
  session.addEventListener('change', renderSession);
  recoveryId = crypto.randomUUID();
  lastDraftDocument = undefined; lastDraftAssets = []; lastDraftFolders = [];
  autoSave.reset(performance.now());
  heldPreview = null; curveStart = null; curveEnd = null; curveAnchorMode = null; curveEditorOpen = false; loop = null; element('#loop-enabled').checked = false;
  assets = nextAssets;
  for (const renderer of [preview, realtimePreview]) { renderer.noisePreview.gpu?.clearSkins(); renderer.overflowNoisePreview?.gpu?.clearSkins(); renderer.overflowRevision++; }
  assetFolders = new Set(nextFolders ?? []);
  assetDirty = Boolean(options.dirty);
  chartName = name;
  closeShaderWorkspace();
  const customShaderFiles = chartShaderFiles(chart, assets, name);
  if (customShaderFiles.length) notify(t('shader-editor.importScoped', { count: customShaderFiles.length }), 'warning', 8000);
  resetEditClock(chart, options.editTime);
  session.recordRecentEdits = timelineStrips.options.historyStrip;
  hitSounds.setProject(chart, assets, name);
  images.load(chart, assets, name);
  timeline.origin = 0;
  timeline.layer = 0;
  timeline.cancelPlacement();
  preview.visible = false; element('.preview-wrap').hidden = true; element('#view-toggle').classList.remove('active');
  element('#preview-title').textContent = chartDisplayName(chart.META.name);
  element('#music-name').textContent = t('app.sbefb963');
  element('#scrubber').max = 600;
  renderSession();
  hasDocument = true; setHome(false);
  agentPanel?.attach(versionRepoId);
  status(t('app.s5e07c0a', { p0: name }));
}

async function openLargeChartDocument(summary) {
  replaceChart(summary.chart, summary.savedPath?.split(/[\\/]/).at(-1) ?? `${summary.chart.META.name || '谱面'}.rpenlarge`);
  largeChartWorkspace = new LargeChartWorkspace(session, summary, { timeline, previews: [preview, realtimePreview], audio, playback, invalidate, reportError });
  largeChartWorkspace.document.range = largeChartWorkspace.range();
  await largeChartWorkspace.document.refresh();
  await audio.loadStream(summary.media.music, Math.max(1, summary.endTime + offsetSeconds() + 2)).catch(reportError);
  element('#music-name').textContent = summary.media.music ? '超大谱音乐（流式播放）' : t('app.sbefb963');
  element('#scrubber').max = String(audio.duration);
  if (summary.media.cover) {
    const image = new Image(); image.src = summary.media.cover;
    try { await image.decode(); if (image.naturalWidth * image.naturalHeight <= 16 * 1024 ** 2) images.background = image; } catch { /* Cover is optional. */ }
  }
  await largeChartWorkspace.readPreview(chartSeconds());
  renderSession();
}

function guardReplace(action) {
  noiseEditor.flush();
  if (session.largeDocument?.busy) { session.largeDocument.flush().then(() => guardReplace(action)).catch(reportError); return; }
  const proceed = () => {
    if (!largeChartWorkspace) return action();
    const previous = largeChartWorkspace; largeChartWorkspace = null;
    hasDocument = false; setHome(true);
    closingLargeChart = previous.close({ discard: true });
    closingLargeChart.then(action).catch(reportError);
  };
  if (hasDocument && (session.history.dirty || assetDirty)) confirmAction(t('app.s13472be'), proceed);
  else proceed();
}

async function loadCandidate(candidate, nextAssets, options = {}) {
  attachExternalEffects([candidate], nextAssets);
  replaceChart(candidate.chart, candidate.name, nextAssets, options.folders ?? candidate.project?.assetFolders ?? [], { ...options, repoId: candidate.project?.vcsId ?? candidate.project?.id,
    editTime: chartEditSeconds(candidate.chart, candidate.info ?? candidate.project?.info),
    viewStateKey: candidate.project?.id ?? candidate.project?.vcsId, multiLineState: candidate.project?.viewState?.multiLine, spatialLineState: candidate.project?.viewState?.spatialLines });
  libraryProject = candidate.project ?? null;
  if (candidate.project?.vcsId) versionRepoId = candidate.project.vcsId;
  syncProjectFolderButton();
  images.load(candidate.chart, assets, chartName, candidate.info);
  const references = resourceReferences(candidate.chart, assets, chartName, candidate.info);
  const bytes = assetBytes(assets, references.song, chartName);
  if (bytes) await loadMusic(bytes, references.song, false);
  else if (references.song) status(t('app.s54ac1d5', { p0: references.song }));
  if (session.chart === candidate.chart && candidate.project?.viewState) {
    const view = candidate.project.viewState;
    if (Number.isInteger(view.lineIndex) && session.chart.judgeLineList?.[view.lineIndex]) session.selectLine(view.lineIndex);
  }
  if (session.chart === candidate.chart && candidate.chart.rpeNextLegacySource?.format === 'phigros-v3') notify(t('app.s2882e27', { p0: candidate.chart.judgeLineList.length, p1: candidate.chart.blockAreaList?.length ?? 0 }), 'success');
}

function loadVersionSnapshot(snapshot, { force = false } = {}) {
  const project = libraryProject;
  const run = () => loadCandidate(
    { chart: snapshot.chart, name: snapshot.chartName ?? chartName, project },
    snapshot.assets,
    { preserveRepoId: true, folders: snapshot.folders, dirty: true }
  );
  if (force || !hasDocument || (!session.history.dirty && !assetDirty)) return run();
  return new Promise((resolve, reject) => {
    showDialog(t('app.s13472be'), t('app.s47a52ce'));
    const modal = element('#modal');
    const apply = element('#modal-apply');
    let done = false;
    apply.hidden = false;
    apply.textContent = t('app.s1fc1afc');
    modal.addEventListener('close', () => { if (!done) { done = true; resolve(); } }, { once: true });
    apply.onclick = () => { done = true; modal.close(); run().then(resolve, reject); };
  });
}

async function loadMusic(bytes, name, updateMetadata = true, recordHistory = true) {
  const chartPosition = chartSeconds();
  playback.pause();
  const loadingSession = session;
  const loaded = await audio.load(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), name);
  if (!loaded || session !== loadingSession) return false;
  audioAnalysis.setBuffer(audio.buffer);
  assets.set(name, bytes); diagnosticAssetRevision++;
  element('#music-name').textContent = name;
  element('#scrubber').max = audio.duration;
  renderTimelineMarkers();
  if (updateMetadata) {
    assetDirty = true; diagnosticAssetRevision++;
    if (recordHistory) session.commit(t('app.s47261a4'), { ...session.chart, META: { ...session.chart.META, song: name } });
    else session.replaceMusicResource(name);
  }
  playback.seek(chartPosition + offsetSeconds());
  return true;
}

function prependAudioSilence() {
  if (!audio.buffer) { notify(t('app.s510310b'), 'error'); return; }
  const targetSession = session; const buffer = audio.buffer; const revision = audio.loadRevision;
  const first = [...session.chart.BPMList].sort((left, right) => beatValue(left.startTime) - beatValue(right.startTime))[0];
  const seconds = 240 / Number(first?.bpm);
  if (!(seconds > 0 && Number.isFinite(seconds))) { notify(t('app.s3820b91'), 'error'); return; }
  showDialog(t('app.s9329c06'), t('app.sa860bb7', { p0: first.bpm, p1: Number(seconds.toFixed(3)) }));
  const apply = element('#modal-apply'); apply.hidden = false; apply.textContent = t('app.s95f5c3a');
  apply.onclick = async () => {
    element('#modal').close();
    try { await applyAudioLeadingSilence({ targetSession, buffer, revision, seconds }); } catch (error) { reportError(error); }
  };
}

async function applyAudioLeadingSilence({ targetSession, buffer, revision, seconds, signal }) {
    const button = element('#audio-leading-silence'); button.disabled = true; button.textContent = t('app.se86312b');
    const cancelled = () => signal?.aborted || session !== targetSession || audio.loadRevision !== revision;
    const check = () => { if (cancelled()) throw new DOMException(t('app.sd5286e8'), 'AbortError'); };
    try {
      check();
      playback.pause();
      const wave = await pcmWave(buffer, cancelled, seconds);
      check(); if (!wave) throw Error(t('app.sbb19d4e'));
      const bytes = new Uint8Array(await wave.arrayBuffer());
      check();
      const sourceName = resourceReferences(session.chart, assets, chartName).song || 'music';
      const stem = sourceName.replaceAll('\\', '/').split('/').at(-1).replace(/\.[^.]+$/, '') + '-leading';
      let name = stem + '.wav'; let suffix = 2;
      while (assetBytes(assets, name, chartName)) name = `${stem}-${suffix++}.wav`;
      if (!await loadMusic(bytes, name, true, false)) throw Error(t('app.s5402e57'));
      notify(t('app.sad632ef'), 'success');
      return { applied: true, name, seconds, undoable: false, offsetUnchanged: true, needsSave: true };
    }
    finally { button.disabled = false; button.textContent = t('app.s763d8cd'); invalidate(); }
}

function seekBeat(value, pause = true) {
  playback.seek(tempo.seconds(value, noiseEditor.active || globalEvents.active ? 1 : session.line?.bpmfactor ?? 1) + (session.chart.META.offset ?? 0) / 1000, pause);
  invalidate();
}

async function togglePlayback() {
  if (!audio.playing && !playback.pending && (editorPreferences.clearHitsOnResume ?? DEFAULT_EDITOR_PREFERENCES.clearHitsOnResume)) preview.effectsSince = realtimePreview.effectsSince = chartSeconds() - 0.001;
  await playback.toggle(session.chart);
  invalidate();
}

function save() {
  noiseEditor.flush();
  checkpointEditTime();
  if (session.largeDocument) return session.largeDocument.save().then(summary => { renderSession(); if (summary && !summary.dirty) { status('超大谱工程已保存'); notify('超大谱工程已保存', 'success'); } });
  agentPanel?.flush().catch(reportError);
  if (!persistenceRequested) { persistenceRequested = true; requestPersistence().then(() => storagePanel.refresh()).catch(() => {}); }
  editorSkinSignal('save-started');
  mods?.runtime.emit('editor:save-started',{});
  const savingSession = session;
  return manualSaves.save(savingSession, () => {
    const document = session.chart; const snapshot = projectEditClock.snapshot(document); delete snapshot.chartTime;
    const project = { ...(libraryProject ?? { id: crypto.randomUUID(), source: t('app.s70d7944'), imported: Date.now() }),
      chart: snapshot, chartName, vcsId: versionRepoId, assets: [...assets], assetFolders: [...assetFolders], bytes: [...assets.values()].reduce((sum, bytes) => sum + bytes.length, 0), updated: Date.now(), viewState: { lineIndex: session.lineIndex, multiLine: multiLineState(session), spatialLines: spatialLineState(session) } };
    status(t('app.se64da0d'));
    return { document, project };
  }, ({ document, project }) => {
    savingSession.history.markSaved(document);
    if (session !== savingSession) return;
    libraryProject = project;
    multiLineStateStore.rekey(project.id);
    spatialLineStateStore.rekey(project.id);
    noteLibrarySave(); storagePanel.refresh().catch(() => {});
    assetDirty = assets.size !== project.assets.length || project.assets.some(([name, bytes]) => assets.get(name) !== bytes)
      || assetFolders.size !== project.assetFolders.length || project.assetFolders.some(folder => !assetFolders.has(folder));
    renderSession();
    const message = session.history.dirty || assetDirty ? t('app.s46644e0') : t('app.s09bce60');
    status(message); notify(message, 'success'); editorSkinSignal('save-succeeded');mods?.runtime.emit('editor:save-succeeded',{message});
  }).catch(error => { editorSkinSignal('save-failed');mods?.runtime.emit('editor:save-failed',{error}); throw error; });
}

function validateCommit(label, next) { assertChart(next); session.commit(label, next); }

function editEvent(type, index, beat = currentBeat()) {
  if (!session.line) { status(t('app.sb9277d1')); return; }
  timeline.eventPlacementType = type;
  timeline.extended = !EVENT_TYPES.includes(type);
  timeline.indexedLayer = null;
  session.focus = 'events'; session.eventLayer = timeline.layer; session.selection.clear();
  activatePane('events');
  if (index !== null) { session.eventSelection = new Set([eventKey(type, index)]); session.notify(); return; }
  timeline.eventInteraction.place(type, beat);
  status(timeline.eventInteraction.pending ? t('app.s5d00864') : t('app.sbf92ebb'));
}

listen('#new', () => guardReplace(() => {
  const chart = createChart(); chart.judgeLineList = [createPlacedLine('Line 1', editorPreferences.lineEventDefaults)]; replaceChart(chart, 'chart.json');
}));
listen('#home-new', () => element('#new').click());
listen('#home-open', () => element('#open').click());
listen('#home-migrate', () => element('#migrate').click());
listen('#resume-editor', () => setHome(false));
function chartImportOptions() {
  return { ...fileOptions(), confirmLargeArchive: info => new Promise(resolve => {
    const host = showDialog(t('app.sfa57b17'), t('app.s3e6d0c4', { p0: (info.declaredBytes / 1024 ** 2).toFixed(1), p1: info.count }));
    const modal = host.closest('dialog'), apply = element('#modal-apply'); let decided = false;
    modal.addEventListener('close', () => { if (!decided) resolve(false); }, { once: true });
    apply.hidden = false; apply.textContent = t('app.s797d0a5');
    apply.onclick = () => { decided = true; modal.close(); resolve(true); };
  }) };
}
function openImportedCharts(loaded) {
  if (!loaded) return;
  guardReplace(() => {
    if (loaded.candidates.length === 1) loadCandidate(loaded.candidates[0], loaded.assets).catch(reportError);
    else choose(t('app.s47008a9'), t('app.sb2a3d63'), loaded.candidates, candidate => `${candidate.name} · ${candidate.chart.META.name ?? ''}`, candidate => loadCandidate(candidate, loaded.assets));
  });
}
listen('#open', () => showImportDialog({
  onLargePreview: () => {
    guardReplace(async () => {
      await closingLargeChart; playback.pause();
      showLargeChartPreview({ skin, settings: { noteSize: preview.noteSize, aspectRatio: preview.aspectRatio, volume: audio.volume }, onEdit: openLargeChartDocument });
    });
  },
  readFiles: (mode, selected) => readChartImport(mode, selected, chartImportOptions()),
  onOpen: openImportedCharts,
  onError: reportError,
}));
element('#file-input').addEventListener('change', async event => {
  try {
    openImportedCharts(await openFiles(event.target.files, chartImportOptions()));
  } catch (error) { reportError(error); }
  finally { event.target.value = ''; }
});
listen('#save', () => save());
listen('#export', () => { noiseEditor.flush(); if (session.largeDocument) return session.largeDocument.save(true); showExportDialog(session.chart, assets, chartName, (name, format) => { status(t('app.s94b8e7e', { p0: name })); if (format === 'pez') { noteExternalBackup(); storagePanel.refresh().catch(() => {}); } }, libraryProject?.identifier ?? libraryProject?.info?.Path, fileOptions()); });
listen('#render-video', async () => {
  if (atHome || !hasDocument || mods?.runtime.locks.size || renderStudio?.opening) return;
  try {
    noiseEditor.flush(); eventSampler.finish(false); speedCurve.close();
    const { closeAnimationCurveEditor } = await import('./custom-easing-controls.mjs'); closeAnimationCurveEditor();
    renderStudio ??= (await import('./render-studio/index.mjs')).createRenderStudio(modEditor, { notify, drawOverlays: (renderer, frame) => mods?.runtime.draw(renderer, frame) });
    await renderStudio.open();
  } catch (error) { reportError(error); }
});
listen('#view-toggle', () => togglePreview());
listen('#close-preview', () => togglePreview(false, true));
listen('#background', () => element('#background-input').click());
element('#background-input').addEventListener('change', async event => {
  try {
    const file = event.target.files[0]; if (!file) return;
    const loadingSession = session;
    const bytes = new Uint8Array(await file.arrayBuffer());
    if (session !== loadingSession) return;
    assets.set(file.name, bytes);
    session.commit(t('app.s3ace90d'), { ...session.chart, META: { ...session.chart.META, background: file.name } });
    await images.load(session.chart, assets, chartName);
  } catch (error) { reportError(error); }
  finally { event.target.value = ''; }
});
listen('#music', () => element('#music-input').click());
element('#music-input').addEventListener('change', async event => {
  try {
    const loadingSession = session;
    const file = event.target.files[0];
    if (file) {
      const bytes = new Uint8Array(await file.arrayBuffer());
      if (session === loadingSession) await loadMusic(bytes, file.name);
    }
  }
  catch (error) { reportError(error); }
  finally { event.target.value = ''; }
});
listen('#recover', async () => {
  const drafts = await listDrafts(atHome ? null : (libraryProject?.id ?? recoveryId));
  choose(t('app.s4d3e2db'), drafts.length ? t('app.s413ad27') : t('app.s96b0de9'), drafts,
    draft => `${draft.title} · ${new Date(draft.updated).toLocaleString()}`,
    entry => { guardReplace(async () => {
      try {
        const draft = await readDraft(entry.id);
        if (!draft) throw new Error(t('app.s2dee158'));
        await loadCandidate({ chart: draft.chart, name: draft.name, project: { viewState: draft.viewState, vcsId: draft.viewState?.vcsId } }, new Map(draft.assets ?? []));
        libraryProject = null; session.history.savedDocument = null; renderSession();
      } catch (error) { reportError(error); }
    }); });
});
listen('#play', togglePlayback);
listen('#rewind', () => seekBeat(0));
listen('#seek', () => seekBeat(beatValue(parseBeat(element('#seek-beat').value))));
element('#scrubber').addEventListener('input', event => playback.seek(Number(event.target.value)));
element('#rate').addEventListener('change', event => audio.setRate(Number(event.target.value)));
element('#offset').addEventListener('change', event => {
  const value = Number(event.target.value);
  if (!Number.isFinite(value)) { event.target.value = session.chart.META.offset ?? 0; return; }
  const chartPosition = chartSeconds();
  session.commit(t('app.s2f6297b'), { ...session.chart, META: { ...session.chart.META, offset: value } });
  playback.seek(chartPosition + value / 1000);
});
element('#chart-info-toggle').addEventListener('click', event => {
  const info = element('#chart-info'); info.hidden = !info.hidden;
  event.currentTarget.setAttribute('aria-expanded', String(!info.hidden));
});
element('#volume').addEventListener('input', event => { audio.setVolume(Number(event.target.value)); persistEditor(); });
element('#preview-mode').addEventListener('change', event => { preview.allLines = realtimePreview.allLines = event.target.value === 'all'; persistEditor(); invalidate(); });
for (const [selector, property, minimum, maximum, integer] of [['#division', 'division', 1, 100, true], ['#grid-count', 'gridCount', 2, 100, false], ['#y-scale', 'scale', 20, 2000, false]]) {
  element(selector).addEventListener('change', event => {
    const value = Number(event.target.value);
    if (Number.isFinite(value)) timeline[property] = Math.max(minimum, Math.min(maximum, integer ? Math.round(value) : value));
    event.target.value = timeline[property]; session.division = timeline.division; updateCurvePanel(); persistEditor(); invalidate();
    if (property === 'scale') element('#y-scale-slider').value = timeline.scale;
  });
}
element('#y-scale-slider').addEventListener('input', event => {
  timeline.scale = Number(event.target.value); element('#y-scale').value = timeline.scale; persistEditor(); invalidate();
});
element('#snap-x').addEventListener('change', event => { timeline.snapX = event.target.checked; persistEditor(); invalidate(); });
function toggleDirectPlacement() {
  editorPreferences.directPlacement = !editorPreferences.directPlacement;
  element('#direct-placement-enabled').checked = editorPreferences.directPlacement;
  if (!editorPreferences.directPlacement) directPlacement.close();
  linePicker.hide(); persistEditor(); invalidate();
}
element('#direct-placement-enabled').addEventListener('change', toggleDirectPlacement);
listen('#close-direct-placement', () => directPlacement.close());
function updateLayerButtons() {
  const container = element('#layer');
  if (!container.children.length) for (let index = 0; index <= MAX_BASE_LAYERS; index++) {
    const button = document.createElement('button'); button.type = 'button'; button.textContent = index === MAX_BASE_LAYERS ? t('app.s20d7a24') : String(index);
    button.onclick = () => {
      timeline.cancelPlacement(); timeline.extended = index === MAX_BASE_LAYERS;
      if (!timeline.extended) timeline.layer = index;
      timeline.indexedLayer = null; session.eventLayer = timeline.layer; session.eventSelection.clear();
      timeline.eventPlacementType = timeline.eventTypes[0]; session.notify();
    };
    container.append(button);
  }
  const bottom = timeline.beatAt(timeline.notesCanvas.clientHeight); const top = timeline.beatAt(0);
  timelineActivity.compile(session.chart, tempo);
  [...container.children].forEach((button, index) => {
    const state = timelineActivity.layerState(session.lineIndex, index, index === MAX_BASE_LAYERS, bottom, top, timeline.eventBeatAt(timeline.notesCanvas.clientHeight, 'paintEvents'), timeline.eventBeatAt(0, 'paintEvents'));
    button.dataset.state = state;
    button.classList.toggle('active', timeline.extended ? index === MAX_BASE_LAYERS : index === timeline.layer);
    button.setAttribute('aria-pressed', String(button.classList.contains('active')));
    button.title = t('app.s1e8776f', { p0: index === MAX_BASE_LAYERS ? t('app.s0804a42') : t('app.sfb282c2', { p0: index }), p1: state === 'empty' ? t('app.s1525642') : state === 'visible' ? t('app.s9728cde') : t('app.sc6758d7') });
  });
}
element('#line-select').addEventListener('change', event => { timeline.cancelPlacement(); session.selectLine(Number(event.target.value)); });
listen('#line-next', () => switchLine(1));
listen('#line-previous', () => switchLine(-1));
element('#hit-volume').addEventListener('input', event => { hitSounds.setVolume(Number(event.target.value)); persistEditor(); });
element('#hit-enabled').addEventListener('change', event => { hitSounds.enabled = event.target.checked; hitSounds.stop(); persistEditor(); });
element('#mute-current-line').addEventListener('click', () => {
  hitSounds.onlyCurrentLine = !hitSounds.onlyCurrentLine;
  const button = element('#mute-current-line');
  button.setAttribute('aria-pressed', String(hitSounds.onlyCurrentLine));
  button.classList.toggle('active', hitSounds.onlyCurrentLine);
  hitSounds.stop(); invalidate();
});
element('#realtime-enabled').addEventListener('change', event => { realtimePreview.visible = event.target.checked; element('#realtime-preview').hidden = !event.target.checked; persistEditor(); invalidate(); });
element('#realtime-alpha').addEventListener('input', event => { realtimePreview.opacity = Number(event.target.value); persistEditor(); invalidate(); });
for (const selector of ['#loop-start', '#loop-end', '#loop-enabled']) element(selector).addEventListener('change', () => {
  try {
    const start = beatValue(parseBeat(element('#loop-start').value));
    const end = beatValue(parseBeat(element('#loop-end').value));
    if (start < 0 || end <= start) throw new Error(t('app.s982f6d2'));
    loop = element('#loop-enabled').checked ? { start, end } : null;
  } catch (error) { loop = null; element('#loop-enabled').checked = false; reportError(error); }
});
for (const button of document.querySelectorAll('[data-tool]')) button.onclick = () => {
  timeline.cancelPlacement();
  timeline.tool = Number(button.dataset.tool);
  for (const item of document.querySelectorAll('[data-tool]')) item.classList.toggle('active', item === button);
  invalidate();
};
function travel(direction, silent = false) {
  globalEvents.timeline.cancelPlacement();
  noiseEditor.cancelGesture(); noiseEditor.endSample(false);
  timeline.cancelPlacement();
  const stack = direction === 'undo' ? session.history.undoStack : session.history.redoStack;
  const command = stack.at(-1);
  if (!command) { notify(direction === 'undo' ? t('app.s5f3dcc0') : t('app.s89594eb'), 'warning'); return; }
  const result = session.travel(direction);
  if (session.largeDocument && !result) return;
  if (!silent) notify(t('app.sf1164c8', { p0: direction === 'undo' ? t('app.s9fcefd8') : t('app.s1238f0d'), p1: command.label }), 'success');
}
listen('#undo', () => travel('undo'));
listen('#redo', () => travel('redo'));
listen('#curve-notes', openCurvePanel);
listen('#curve-trajectory', () => { curveEditorOpen = false; trajectoryPanel.open(); });
listen('#text-lines-tool', () => {
  if (globalEvents.active) globalEvents.close();
  if (noiseEditor.active) noiseEditor.close();
  timeline.cancelPlacement(); batchControls.cancel(); pasteGesture.cancel(); curveEditorOpen = false; textLinesPanel.open();
});
listen('#noise-fit-tool', () => {
  if (globalEvents.active) globalEvents.close();
  if (noiseEditor.active) noiseEditor.close();
  timeline.cancelPlacement(); batchControls.cancel(); pasteGesture.cancel(); curveEditorOpen = false; noiseFitPanel.open();
});
listen('#noise-tracking-tool', () => {
  if (globalEvents.active) globalEvents.close();
  if (noiseEditor.active) noiseEditor.close();
  timeline.cancelPlacement(); batchControls.cancel(); pasteGesture.cancel(); clearNoteSourceToast(); curveEditorOpen = false; noiseTrackingPanel.open();
});
listen('#curve-start', () => captureCurve(false));
listen('#curve-end', () => captureCurve(true));
function switchNoteView() {
  if (session.multiLineActive || noiseEditor.active || globalEvents.active) return;
  timeline.cancelPlacement(); timeline.hoverArea = 'notes'; session.focus = 'notes';
  activatePane('notes');
  editorPreferences.notesOnly = !timeline.notesOnly;
  applyDisplaySettings(); persistEditor(); session.notify();
}
function resetCamera(resetPreview = false) {
  editorPreferences.cameraX = 0;
  if (resetPreview) editorPreferences.viewDivisor = 1;
  applyDisplaySettings(); persistEditor();
}
function captureCurve(end) {
  if (preview.visible) return;
  if (!curveEditorOpen) openCurvePanel();
  curveAnchorMode = end ? 'end' : 'start';
  if (end && !curveStart) { curveAnchorMode = null; throw new Error(t('app.se232390')); }
  element('#curve-start')?.classList.toggle('active', !end);
  element('#curve-end')?.classList.toggle('active', end);
  status(end ? t('app.s6737539') : t('app.s96cf478'));
}
listen('#notes-only', switchNoteView);
listen('#reset-camera', () => resetCamera(true));
listen('#game-ui', () => { editorPreferences.showGameUI = !preview.showGameUI; applyDisplaySettings(); persistEditor(); });
element('#preview-ratio').onchange = event => {
  [editorPreferences.ratioWidth, editorPreferences.ratioHeight] = event.target.value.split(':').map(Number);
  applyDisplaySettings(); persistEditor();
};
for (const [id, key] of [['camera-x', 'cameraX'], ['view-divisor', 'viewDivisor']]) element(`#${id}`).onchange = event => {
  if (!event.target.value.trim() || !event.target.validity.valid) { applyDisplaySettings(); return; }
  editorPreferences[key] = Number(event.target.value); applyDisplaySettings(); persistEditor();
};
element('#toolbar-mode').onclick = () => {
  const modes = ['compact', 'icons', 'wide'];
  const current = editorPreferences.toolbarMode ?? 'compact';
  editorPreferences.toolbarMode = modes[(modes.indexOf(current) + 1) % modes.length];
  applyDisplaySettings(); persistEditor();
};
async function copySelection() {
  if (globalEvents.active) { globalEvents.copy(); return; }
  if (noiseEditor.active) { noiseEditor.copy(); return; }
  const count = await copyObjects(session);
  if (count) { clipboardHistory.remember(session); status(t('app.s44cef9c', { p0: count })); }
  invalidate();
}
function deleteSelection() { if (globalEvents.active) { globalEvents.safely(() => globalEvents.remove()); return; } if (noiseEditor.active) { noiseEditor.remove(); return; } deleteObjects(session); }
async function cutSelection() { if (globalEvents.active) { globalEvents.safely(() => globalEvents.copy(true)); return; } if (noiseEditor.active) { noiseEditor.copy(true); return; } if (await cutObjects(session)) clipboardHistory.remember(session); invalidate(); }
function pasteSelection(mirror = false, keepTime = false) {
  if (globalEvents.active) { globalEvents.safely(() => globalEvents.paste(keepTime)); return; }
  if (noiseEditor.active) { noiseEditor.action(keepTime ? 'KeepTimePaste' : 'Paste'); return; }
  timeline.clipboardMode = { mirror, keepTime };
  pasteObjects(session, clipboardBeat(timeline), { ...timeline.clipboardMode, targetLineIndex: clipboardTargetLine() });
}
listen('#copy', copySelection);
listen('#cut', cutSelection);
listen('#paste', () => pasteSelection());
for (const [name, description] of BATCH_ACTIONS) {
  const option = new Option(name, name); option.title = description; element('#batch-action').append(option);
}
element('#batch-action').onchange = () => { element('#batch-run').title = BATCH_ACTIONS.find(([name]) => name === element('#batch-action').value)[1]; };
element('#batch-action').onchange();
listen('#batch-run', () => applyBatchAction(session, element('#batch-action').value, timeline.gridCount));
listen('#delete', deleteSelection);
listen('#mirror', () => {
  if (session.focus === 'events') transformEvents(session, t('app.sd39cfc6'), (event, type) => ['moveXEvents', 'rotateEvents'].includes(type) ? { ...event, start: -event.start, end: -event.end } : event);
  else session.transformSelection(t('app.sd8373bc'), note => ({ ...note, positionX: -note.positionX }));
});
function syncProjectFolderButton() {
  const button = element('#open-project-folder');
  button.hidden = !nativeStorage;
  button.disabled = !nativeStorage || !libraryProject?.id;
  button.title = libraryProject?.id ? t('app.s5cf10e1') : t('app.s265de32');
}
listen('#metadata', () => { activatePane('metadata'); renderMetadataPanel(session, element('#metadata-editor'), () => { activatePane('chart'); renderSession(); }); });
listen('#open-project-folder', async () => {
  if (!nativeStorage || !libraryProject?.id) return;
  await nativeStorage.call('openProjectFolder', libraryProject.id);
});
listen('#bpm', () => { activatePane('bpm'); renderBpmPanel(session, element('#bpm-editor')); });
listen('#audio-leading-silence', prependAudioSilence);
listen('#assets', () => { activatePane('assets'); assetLibrary.render(); });
listen('#audio-analysis-tool', () => {
  activatePane('audio-analysis'); audioAnalysis.render(element('#audio-analysis-panel')); audioAnalysis.draw(offsetSeconds());
});
function renderHistoryPanel() {
  const host = element('#history-results'); if (!host) return;
  host.replaceChildren();
  const history = session.history; const current = history.undoStack.length;
  const entries = [{ label: t('app.sc43a712'), index: 0 }, ...history.undoStack.map((command, index) => ({ label: command.label, index: index + 1 })), ...history.redoStack.toReversed().map((command, index) => ({ label: command.label, index: current + index + 1 }))];
  for (const entry of entries) {
    const button = document.createElement('button'); button.type = 'button'; button.className = entry.index === current ? 'active' : ''; button.textContent = `${entry.index === current ? '● ' : ''}${entry.index} · ${entry.label}`;
    button.onclick = async () => { try { const direction = entry.index < current ? 'undo' : 'redo'; for (let count = 0; count < Math.abs(entry.index - current); count++) { travel(direction, true); await session.largeDocument?.flush(); } renderHistoryPanel(); notify(t('app.sf2d283b', { p0: entry.index }), 'success', 1800); } catch (error) { reportError(error); } };
    host.append(button);
  }
}
listen('#history-panel', () => { activatePane('history'); renderHistoryPanel(); });
listen('#version-control-tool', () => activatePane('version-control'));
listen('#clear-clipboard', () => { if (globalEvents.active) { globalEvents.document.editor.eventClipboard = []; globalEvents.document.editor.clipboardVisible = false; invalidate(); return; } noiseEditor.clipboard = null; clipboardHistory.clearCurrent(session); timeline.clipboardMode = {}; invalidate(); });
const clipboardButton = document.createElement('button'); clipboardButton.id = 'clipboard-history'; clipboardButton.textContent = t('app.sb377bb5'); clipboardButton.title = t('app.s4c6bff4');
element('[data-panel="chart"] .action-grid').append(clipboardButton);
function renderClipboardPanel() {
  renderClipboardHistory(element('#clipboard-results'), clipboardHistory, {
    use: id => { clipboardHistory.use(session, id); invalidate(); },
  });
}
function toggleClipboardPanel() {
  if (!clipboardHistory.enabled) return;
  activatePane(activePaneName === 'clipboard' ? 'chart' : 'clipboard'); renderClipboardPanel();
}
listen('#clipboard-history', toggleClipboardPanel);
listen('#clear-clipboard-history', () => clipboardHistory.clearUnpinned());
let clipboardSave = Promise.resolve();
clipboardHistory.addEventListener('change', event => {
  if (activePaneName === 'clipboard' && event.reason !== 'rename') renderClipboardPanel();
  invalidate();
  if (!event.persist) return;
  const entries = structuredClone(clipboardHistory.entries);
  clipboardSave = clipboardSave.then(() => storeClipboardHistory(entries)).catch(error => status(t('app.s205b4b6', { p0: error.message })));
});
readClipboardHistory().then(entries => clipboardHistory.restore(entries)).catch(error => status(t('app.sbabe9e5', { p0: error.message })));
function deleteDiagnosticIssue(issue) {
  if (issue.path.startsWith('paintEvents[')) {
    session.commit(t('app.s35d1128'), replaceShaderEvents(session.chart, issue.line, shaderEvents(session.chart, issue.line).filter((event, index) => index !== issue.index)));
    notify(t('app.s1788c09'), 'success'); return;
  }
  const chart = structuredClone(session.chart);
  if (issue.noiseArea !== undefined) {
    if (issue.noiseTrack) chart.blockAreaList?.[issue.noiseArea]?.[issue.noiseTrack]?.splice(issue.index, 1);
    else chart.blockAreaList?.splice(issue.noiseArea, 1);
  }
  else if (issue.path.startsWith('notes[') && chart.judgeLineList?.[issue.line]) chart.judgeLineList[issue.line].notes.splice(issue.index, 1);
  else if (issue.path === 'father' && chart.judgeLineList?.[issue.line]) chart.judgeLineList[issue.line].father = -1;
  else if (issue.path.startsWith('BPMList[')) chart.BPMList.splice(issue.index, 1);
  else if (issue.line != null && issue.path.match(/^\w+Events\[/)) {
    const type = issue.path.slice(0, issue.path.indexOf('[')); const line = chart.judgeLineList?.[issue.line];
    const layer = issue.extended ? line?.extended : line?.eventLayers?.[issue.layer];
    if (!layer?.[type]) return;
    layer[type].splice(issue.index, 1);
  }
  else return;
  session.commit(t('app.s634fa27'), chart); notify(t('app.s1788c09'), 'success');
}
function renderDiagnostics() { diagnostics.update(); diagnosticPanel.render(); }
function locateDiagnosticIssue(issue) {
  if (issue.asset && !issue.shaderParameter) { activatePane('assets'); assetLibrary.selected = assetPath(assets, issue.asset, chartName); assetLibrary.render(); return; }
  const shaderTarget = diagnosticShaderTarget(session.chart, issue);
  if (shaderTarget) issue = { ...issue, line: shaderTarget.lineIndex, extended: true };
  diagnosticNavigation = true;
  let geometry;
  try {
    if (preview.visible) togglePreview(false, true);
    directPlacement.close();
    if (issue.globalType) {
      if (noiseEditor.active) noiseEditor.close(); openGlobalEvents();
      const column = globalEvents.document.columns.find(entry => entry.type.id === issue.globalType && (!issue.globalColumn || entry.column.id === issue.globalColumn));
      if (column && issue.index !== undefined) {
        globalEvents.document.editor.eventSelection = new Set([column.key + ':' + issue.index]); globalEvents.document.notify();
        const columnIndex = globalEvents.document.columns.indexOf(column); globalEvents.sync();
        const bounds = globalEvents.geometry[columnIndex];
        if (bounds) globalEvents.canvas.parentElement.scrollLeft = Math.max(0, bounds.left - (globalEvents.canvas.parentElement.clientWidth - bounds.width) / 2);
        const event = column.events[issue.index];
        if (event) geometry = () => [globalEvents.timeline.diagnosticEventBounds(column.key, event)].filter(Boolean);
      }
    } else if (issue.noiseArea !== undefined) {
      openNoisePanel(); noiseEditor.select(issue.noiseArea, issue.noiseTrack ? { type: issue.noiseTrack, index: issue.index } : null);
      geometry = () => [noiseEditor.diagnosticBounds(issue.noiseArea, issue.noiseTrack, issue.index)].filter(Boolean);
    } else if (session.chart.judgeLineList[issue.line]) {
      if (noiseEditor.active) noiseEditor.close(); if (globalEvents.active) globalEvents.close();
      const shader = shaderTarget ? [null, 'paintEvents', String(shaderTarget.index)] : null;
      const match = shader ?? issue.path.match(/^(\w+Events)\[(\d+)\]/) ?? (issue.eventType ? [null, issue.eventType, null] : null), noteIndex = issue.path.match(/^notes\[(\d+)\]/)?.[1], note = noteIndex !== undefined ? session.chart.judgeLineList[issue.line].notes[Number(noteIndex)] : null;
      const area = match ? 'events' : 'notes';
      if (session.multiLineActive && (session.multiLineMode !== area || !session.isTargetLine(issue.line))) session.setMultiLineEnabled(false);
      if (match) { timeline.extended = Boolean(issue.extended || shader); if (issue.layer !== undefined && !issue.extended && !shader) timeline.layer = issue.layer; if (timeline.notesOnly) element('#notes-only').click(); }
      session.selectLine(issue.line);
      if (session.multiLineActive && timeline.panelCount(area) > 1) { const canvas = area === 'events' ? timeline.eventsCanvas : timeline.notesCanvas; timeline.multiLineScroll[area] = timeline.panelIndex(issue.line, area) * timeline.panelStride(canvas.clientWidth, area); }
      if (note) {
        const x = timeline.noteHorizontal(note.positionX, issue.line), width = timeline.notesCanvas.clientWidth;
        if (x < 16 || x > width - 16) { element('#camera-x').value = note.positionX; element('#camera-x').dispatchEvent(new Event('change', { bubbles: true })); }
        geometry = () => !noiseEditor.active && !globalEvents.active && (session.lineIndex === issue.line || session.isTargetLine(issue.line)) ? [timeline.diagnosticNoteBounds(note, issue.line)] : [];
      } else if (match) {
        const type = match[1], event = match[2] === null ? { startTime: [0, 0, 1], endTime: [0, 0, 1] } : eventList(session, type)[Number(match[2])];
        if (event) geometry = () => !noiseEditor.active && !globalEvents.active && (session.lineIndex === issue.line || session.isTargetLine(issue.line)) ? [timeline.diagnosticEventBounds(type, event, issue.line)].filter(Boolean) : [];
      }
      geometry ??= () => [{ canvas: timeline.notesCanvas, x: timeline.panelHorizontal(0, issue.line, timeline.notesCanvas.clientWidth, 'notes'), y: timeline.baseline() - 4, width: timeline.panelWidth(timeline.notesCanvas.clientWidth, 'notes'), height: 8 }];
    }
    const factor = issue.globalType || issue.noiseArea !== undefined ? 1 : session.chart.judgeLineList[issue.line]?.bpmfactor ?? 1;
    playback.seek(tempo.seconds(Number.isFinite(issue.beat) ? issue.beat : 0, factor) + offsetSeconds());
    // Seek updates the edit clock synchronously; refresh the origin and layer
    // chrome before drawing the focused object's outline.
    renderSession();
  } finally { diagnosticNavigation = false; }
  activatePane('diagnose');
  if (geometry) diagnosticHighlight.show(issue, geometry);
  invalidate();
}
listen('#diagnose', () => { activatePane('diagnose'); renderDiagnostics(); });
element('#diagnose-show-low').addEventListener('change', renderDiagnostics);
element('#diagnose-close').addEventListener('click', () => activatePane('chart'));
element('#history-close').addEventListener('click', () => activatePane('chart'));

function curveField(id, label, value, type = 'text', options = []) {
  const row = document.createElement('label'); row.className = 'field'; row.append(label);
  const input = type === 'select' ? document.createElement('select') : document.createElement('input');
  input.id = id;
  if (type === 'select') input.replaceChildren(...options.map(([optionValue, optionLabel]) => new Option(optionLabel, optionValue)));
  else { input.type = type; input.step = 'any'; }
  input.value = value; row.append(input);
  if (type === 'number') numericWheel(input, id === 'curve-density' ? 0.25 : 1, direction => { input.value = Number(input.value) + direction * (id === 'curve-density' ? 0.25 : 1); input.dispatchEvent(new Event('input', { bubbles: true })); });
  return row;
}
function generatedCurveCount() {
  if (!curveStart || !curveEnd) return 0;
  try { return generateCurveNotes({ ...curveValues, division: timeline.division }).length; } catch { return 0; }
}
function updateCurvePanel() {
  if (!curveEditorOpen) return;
  const root = element('#curve-editor');
  if (!root.children.length) {
    root.append(curveField('curve-start-time', t('app.sfb9f6db'), formatBeat(curveValues.startTime)), curveField('curve-end-time', t('app.s1e77861'), formatBeat(curveValues.endTime)),
      curveField('curve-start-x', t('app.sf9dc1c1'), curveValues.startX, 'number'), curveField('curve-end-x', t('app.s908b423'), curveValues.endX, 'number'),
      curveField('curve-density', t('app.sf90554a'), curveValues.density, 'number'), curveField('curve-type', t('app.se4e46c7'), curveValues.type, 'select', [['1', 'Tap'], ['3', 'Flick'], ['4', 'Drag']]), curveField('curve-easing', t('app.s6537c87'), curveValues.easingType, 'number'));
    root.addEventListener('input', () => {
      try {
        curveValues = { startTime: parseBeat(element('#curve-start-time').value), endTime: parseBeat(element('#curve-end-time').value), startX: Number(element('#curve-start-x').value), endX: Number(element('#curve-end-x').value), density: Number(element('#curve-density').value), type: Number(element('#curve-type').value), easingType: Number(element('#curve-easing').value) };
        element('#curve-summary').textContent = t('app.s32e6942', { p0: generatedCurveCount() });
        curveEasingPicker.select(curveValues.easingType);
        invalidate();
      } catch (error) { element('#curve-summary').textContent = error.message; }
    });
    const summary = document.createElement('p'); summary.id = 'curve-summary'; summary.className = 'hint'; root.append(summary);
    curveEasingPicker = createEasingPicker(curveValues.easingType, value => { curveValues.easingType = value; updateCurvePanel(); });
    root.append(curveEasingPicker.element);
  }
  const values = [['#curve-start-time', formatBeat(curveValues.startTime)], ['#curve-end-time', formatBeat(curveValues.endTime)], ['#curve-start-x', curveValues.startX], ['#curve-end-x', curveValues.endX], ['#curve-density', curveValues.density], ['#curve-type', curveValues.type], ['#curve-easing', curveValues.easingType]];
  for (const [selector, value] of values) if (document.activeElement !== element(selector)) element(selector).value = value;
  element('#curve-summary').textContent = t('app.s32e6942', { p0: generatedCurveCount() });
  curveEasingPicker.select(curveValues.easingType);
  invalidate();
}
function openCurvePanel() {
  curveEditorOpen = true; curveStart = null; curveEnd = null; curveAnchorMode = null;
  element('#curve-start')?.classList.remove('active'); element('#curve-end')?.classList.remove('active');
  activatePane('curve'); updateCurvePanel(); status(t('app.sd847adc'));
}
function closeCurvePanel() {
  curveEditorOpen = false; curveStart = null; curveEnd = null; curveAnchorMode = null;
  element('#curve-start')?.classList.remove('active'); element('#curve-end')?.classList.remove('active');
  activatePane('chart'); invalidate();
}
listen('#curve-generate', () => {
  try {
    const notes = generateCurveNotes({ ...curveValues, division: timeline.division });
    if (!notes.length) throw new Error(t('app.s2124d24'));
    session.focus = 'notes'; session.eventSelection.clear(); session.insertNotes(notes, t('app.s7f30a53')); closeCurvePanel(); notify(t('app.sac98de3', { p0: notes.length }), 'success');
  } catch (error) { reportError(error); }
});
listen('#curve-cancel', closeCurvePanel);
listen('#help', () => showDialog(t('app.s066b024'), HELP_TEXT));

let playbackSpaceHeld = false;
let lineSwitchDigits = 0;
window.addEventListener('keydown', event => {
  if (mods?.runtime.locks.size) return;
  if (atHome || !hasDocument || !shortcutMatches(event, preferences.hotkeys.ShowLineInfo) || dialogOpen() || usesNativeTextShortcut(event)) return;
  event.preventDefault();
  if (event.repeat) return;
  lineInfoVisible = !lineInfoVisible;
  updateLineInfo();
}, true);
window.addEventListener('blur', () => { lineInfoVisible = false; element('#line-info-overlay')?.setAttribute('hidden', ''); });
window.addEventListener('keydown', event => {
  if (mods?.runtime.locks.size) return;
  if (atHome || !hasDocument || dialogOpen() || usesNativeTextShortcut(event) || isTextEntry(event.target) || !shortcutMatches(event, preferences.hotkeys.Pause)) return;
  event.preventDefault(); event.stopImmediatePropagation();
  playbackSpaceHeld = true;
  if (!event.repeat) togglePlayback().catch(reportError);
}, true);
window.addEventListener('keyup', event => {
  if (mods?.runtime.locks.size) return;
  if (!shortcutReleased(event, preferences.hotkeys.Pause) || !playbackSpaceHeld) return;
  event.preventDefault(); event.stopImmediatePropagation(); playbackSpaceHeld = false;
}, true);
window.addEventListener('blur', () => { playbackSpaceHeld = false; });
function lineSwitchModifierMatches(event) {
  const binding = String(preferences.hotkeys.SwitchLine ?? 'LEFTCTRL').toUpperCase();
  const parts = binding.split(/[&+]/).filter(Boolean).map(part => part.replace(/^(LEFT|RIGHT)/, ''));
  if (!parts.length || parts.some(part => !['CTRL', 'SHIFT', 'ALT'].includes(part))) return false;
  const required = new Set(parts);
  return Boolean(event.ctrlKey) === required.has('CTRL') && Boolean(event.shiftKey) === required.has('SHIFT') && Boolean(event.altKey) === required.has('ALT') && !event.metaKey;
}
window.addEventListener('keydown', event => {
  if (mods?.runtime.locks.size || atHome || !hasDocument || dialogOpen() || usesNativeTextShortcut(event) || event.isComposing || !/^\d$/.test(event.key) || !lineSwitchModifierMatches(event)) return;
  if (shortcutMatches(event, preferences.hotkeys.Paste) || shortcutMatches(event, preferences.hotkeys.PasteMirror)) return;
  lineSwitchDigits = appendLineShortcutDigit(lineSwitchDigits, session.chart.judgeLineList?.length ?? 0, Number(event.key));
  event.preventDefault(); event.stopImmediatePropagation();
  selectOverviewLine(lineSwitchDigits);
}, true);
window.addEventListener('keyup', event => { if (!lineSwitchModifierMatches(event)) lineSwitchDigits = 0; }, true);
window.addEventListener('blur', () => { lineSwitchDigits = 0; });
function setEventEndpointDragHeld(held) {
  if (!held) timeline.freezeHoldEndpoint();
  if (!held) for (const interaction of [timeline.eventInteraction, globalEvents.timeline.eventInteraction]) if (interaction.drag?.endpoint && !interaction.drag.frozen) interaction.drag.frozen = interaction.previewEvent(interaction.drag.event);
  timeline.eventEndpointDragHeld = globalEvents.timeline.eventEndpointDragHeld = held;
  timeline.eventInteraction.updateCursor(); globalEvents.timeline.eventInteraction.updateCursor(); noiseEditor.syncHover(); invalidate();
}
window.addEventListener('blur', () => {
  setEventEndpointDragHeld(false);
  if (timeline.eventInteraction.drag?.endpoint) timeline.cancelPlacement();
  if (timeline.drag?.endpoint) timeline.cancelPlacement();
  if (globalEvents.timeline.eventInteraction.drag?.endpoint) globalEvents.timeline.cancelPlacement();
  if (noiseEditor.drag?.endpoint) noiseEditor.cancelGesture();
});
window.addEventListener('keydown', async event => {
  if (mods?.runtime.locks.size) return;
  const target = event.target;
  if (atHome || !hasDocument || dialogOpen() || usesNativeTextShortcut(event)) return;
  if (batchControls.active) { event.preventDefault(); return; }
  const area = spatialTool.enabled && spatialTool.panel.interactive || globalEvents.active ? 'events' : timeline.hoverArea ?? session.focus;
  const hasMultiSelection = session.multiLineActive && ([...(session.multiLineSelection?.values() ?? [])].some(values => values.size) || [...(session.multiEventSelection?.values() ?? [])].some(values => values.size));
  const hasSelection = spatialTool.enabled && spatialTool.panel?.source ? true : globalEvents.active ? globalEvents.selection.size > 0 : noiseEditor.active ? noiseEditor.selection.size > 0 : Boolean(session.selection.size || session.eventSelection.size || hasMultiSelection);
  const action = shortcutAction(event, preferences, area, { hasSelection: hasSelection && (!preview.visible || directPlacement.active), deleteWithExtraKeys: editorPreferences.deleteWithExtraKeys ?? DEFAULT_EDITOR_PREFERENCES.deleteWithExtraKeys });
  if (action === 'EditorRecording') { event.preventDefault(); event.stopImmediatePropagation(); if (!event.repeat) void editorRecording.toggle(); return; }
  if (action === 'Esc' && (linePicker.active || directPlacement.active)) {
    if (linePicker.active) linePicker.hide(); else directPlacement.close();
    event.preventDefault(); event.stopImmediatePropagation(); return;
  }
  if (linePicker.active) return;
  if (action === 'DragLong') {
    if (timeline.rpeEventDrag === true && !preview.visible && !isTextEntry(event.target)) { setEventEndpointDragHeld(true); event.preventDefault(); event.stopImmediatePropagation(); }
    return;
  }
  if (globalEvents.active) {
    try { if (globalEvents.action(action, event)) { event.preventDefault(); event.stopImmediatePropagation(); return; } }
    catch (error) { event.preventDefault(); reportError(error); return; }
  }
  if (spatialTool.action(action,event)) { event.preventDefault(); event.stopImmediatePropagation(); return; }
  if (noiseFitPanel.action(action)) { event.preventDefault(); event.stopImmediatePropagation(); return; }
  if (noiseTrackingPanel.action(action)) { event.preventDefault(); event.stopImmediatePropagation(); return; }
  if (noteTrackingPanel.action(action)) { event.preventDefault(); event.stopImmediatePropagation(); return; }
  if (noiseEditor.active && (!preview.visible || noiseEditor.sampleMode)) {
    try { if (noiseEditor.action(action, event)) { event.preventDefault(); event.stopImmediatePropagation(); return; } }
    catch (error) { event.preventDefault(); reportError(error); return; }
  }
    if (action || ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) releaseShortcutFocus(target);
  if (pasteGesture.pending && action !== 'Paste') pasteGesture.cancel();
    if (action === 'Paste' && clipboardHistory.enabled && (!preview.visible || directPlacement.active) && shortcutMatches(event, preferences.hotkeys.ClipboardHistory ?? DEFAULT_HOTKEYS.ClipboardHistory)) {
    pasteGesture.down(event, { session, chart: session.chart, lineIndex: session.lineIndex, targetLineIndex: clipboardTargetLine(), layer: timeline.layer, beat: clipboardBeat(timeline) }); return;
  }
  let handled = true;
  try {
    if (event.repeat && ['NumberMirror', 'NumberFill', 'Pause', 'AddHold', 'AddEvent', 'AddTap', 'StartView', 'EndView', 'JumpView', 'ReplayView', 'StartView_HOLD', 'JumpView_HOLD', 'ToggleMultiLine', 'SwitchMultiLineMode', 'SwitchLock', 'StickNext', 'TimeAttach'].includes(action)) { event.preventDefault(); return; }
    if (action === 'DirectPlacement') { if (!event.repeat) toggleDirectPlacement(); }
    else if (action === 'TimeAttach') seekBeat(Math.round(currentBeat() * timeline.division) / timeline.division);
    else if ((!preview.visible || directPlacement.active) && action?.startsWith('Page')) nudgeSelection(session, { PageLeft: 'ArrowLeft', PageRight: 'ArrowRight', PageUp: 'ArrowUp', PageDown: 'ArrowDown' }[action], timeline.division, timeline.gridCount);
    else if (timeline.eventInteraction.pending && /^[0-9]$/.test(event.key) && !event.ctrlKey && !event.altKey) timeline.eventInteraction.place(undefined, undefined, Number(event.key) || 10);
    else if (action === 'Save') { event.preventDefault(); await save(); }
    else if (action === 'Undo') travel('undo');
    else if (action === 'Redo') travel('redo');
    else if (action === 'SelectAll') {
      if (session.largeDocument && session.focus === 'notes') { session.largeDocument.selectAll(); await session.largeDocument.flush(); }
      else if (session.focus === 'events') selectAllEvents(session, timeline.eventTypes);
      else if (session.multiLineActive && session.multiLineMode === 'notes') {
        session.multiLineSelection = new Map(session.targetLineIndices.map(lineIndex => [lineIndex, new Set((session.chart.judgeLineList?.[lineIndex]?.notes ?? []).map((note, index) => index))]));
        session.selection = new Set(session.multiLineSelection.get(session.lineIndex) ?? []);
      } else session.selection = new Set(session.notes.map((note, index) => index));
      timeline.completeSelection(session.focus);
      session.notify();
    }
    else if (action === 'Copy') await copySelection();
    else if (action === 'Shear') await cutSelection();
    else if (action === 'ClipboardHistory') toggleClipboardPanel();
    else if ((!preview.visible || directPlacement.active) && ['NumberMirror', 'NumberFill'].includes(action)) applyNumberShortcut(session, action);
    else if (['Paste', 'PasteMirror', 'KeepTimePaste', 'KeepTimePasteMirror'].includes(action)) pasteSelection(action.endsWith('Mirror'), action.startsWith('KeepTime'));
    else if (action === 'Pause') { event.preventDefault(); await togglePlayback(); }
    else if (['StartView', 'ReplayView', 'StartView_HOLD', 'JumpView_HOLD'].includes(action)) {
      if (action.endsWith('_HOLD')) heldPreview = { action, code: event.code };
      togglePreview(true, false, action === 'ReplayView');
    }
    else if (action === 'EndView') togglePreview(false);
    else if (action === 'JumpView') togglePreview(false, true);
    else if (action === 'SwitchUI') switchNoteView();
    else if (action === 'ResetCamera') resetCamera();
    else if (action === 'ToggleMultiLine') {
      directPlacement.close();
      const enabling = !session.multiLineEnabled;
      session.setMultiLineEnabled(enabling, session.focus === 'events' ? 'events' : 'notes');
      if (enabling) openMultiLinePanel();
    }
    else if (action === 'SwitchMultiLineMode' && session.multiLineActive) session.setMultiLineMode(session.multiLineMode === 'events' ? 'notes' : 'events');
    else if (action === 'CurveBegin' || action === 'CurveEnd') captureCurve(action === 'CurveEnd');
    else if (action === 'SwitchLock') {
      if (!noiseEditor.active && !globalEvents.active && session.focus === 'events') toggleSingleEventLock(session);
    }
    else if (action === 'StickNext') {
      if (!noiseEditor.active && !globalEvents.active && session.focus === 'events' && selectedEventEntries(session).length === 1) stickSelectedEventToNext(session);
    }
    else if (preview.visible && !directPlacement.active && ['AddTap', 'AddDrag', 'AddFlick', 'AddHold', 'AddEvent'].includes(action)) handled = false;
    else if (area === 'events' && ['AddTap', 'AddEvent'].includes(action)) handled = timeline.eventInteraction.place(undefined, undefined, undefined, action === 'AddTap');
    else if (['AddTap', 'AddDrag', 'AddFlick', 'AddHold'].includes(action)) handled = timeline.addAtCursor({ AddTap: 1, AddDrag: 4, AddFlick: 3, AddHold: 2 }[action]);
    else if (action === 'Delete') deleteSelection();
    else if (action === 'QuickDelete') {
      const selected = captureSelection(session);
      if (selected.notes.length + selected.events.length > 1) deleteSelection();
      else if (area === 'events' && timeline.eventCursor) {
        const hit = timeline.eventInteraction.hit(timeline.eventCursor);
        if (hit) { selectEvent(session, hit.lineIndex ?? session.lineIndex, hit.type, hit.index, { replace: true }); deleteEvents(session); }
      } else if (area === 'notes' && timeline.cursor) {
        const hit = timeline.hit(timeline.cursor);
        if (hit) { if (session.multiLineActive && session.multiLineMode === 'notes') session.multiLineSelection = new Map([[hit.lineIndex, new Set([hit.index])]]); session.selection = new Set([hit.index]); deleteSelection(); }
      }
    } else if (['LastBeat', 'NextBeat'].includes(action)) seekBeat(Math.max(0, currentBeat() + (action === 'LastBeat' ? -1 : 1) / timeline.division));
    else if (action === 'Esc') { session.clipboardVisible = false; session.selection.clear(); session.eventSelection.clear(); session.multiLineSelection?.clear(); session.multiEventSelection?.clear(); timeline.cancelPlacement(); curveAnchorMode = null; curveStart = null; curveEnd = null; curveEditorOpen = false; togglePreview(false); activatePane('chart'); session.notify(); }
    else handled = false;
    if (handled) { event.preventDefault(); event.stopPropagation(); }
  } catch (error) { event.preventDefault(); reportError(error); }
}, true);
window.addEventListener('keyup', event => {
  if (timeline.eventEndpointDragHeld && shortcutReleased(event, preferences.hotkeys.DragLong ?? DEFAULT_HOTKEYS.DragLong)) setEventEndpointDragHeld(false);
  if (mods?.runtime.locks.size) return;
  pasteGesture.up(event);
  if (timeline.clipboardMode?.mirror || timeline.clipboardMode?.keepTime) { timeline.clipboardMode = {}; invalidate(); }
  if (heldPreview && (event.code === heldPreview.code || shortcutReleased(event, preferences.hotkeys[heldPreview.action]))) {
    togglePreview(false, heldPreview.action === 'JumpView_HOLD'); heldPreview = null;
  }
}, true);
window.addEventListener('blur', () => { if (heldPreview) { togglePreview(false); heldPreview = null; } });

window.addEventListener('beforeunload', event => { checkpointEditTime(); if (hasDocument && (session.history.dirty || assetDirty)) { event.preventDefault(); event.returnValue = ''; } });
document.addEventListener('visibilitychange', () => { checkpointEditTime(); });
window.addEventListener('resize', invalidate);
element('#modal').addEventListener('close', () => { if (atHome) home.refresh().catch(reportError); });
const markerResizeObserver = new ResizeObserver(() => { renderTimelineMarkers(); invalidate(); });
markerResizeObserver.observe(element('.stage'));
markerResizeObserver.observe(element('.scrubber-wrap'));
const placementToolbar = element('.editor-toolbar'), placementControl = element('.direct-placement-control');
let placementAlignmentFrame = 0;
const schedulePlacementAlignment = () => {
  if (placementAlignmentFrame) return;
  placementAlignmentFrame = requestAnimationFrame(() => {
    placementAlignmentFrame = 0;
    const rect = placementControl.getBoundingClientRect(), target = element('.beat-seek').getBoundingClientRect();
    if (!rect.width || !target.width) return;
    const margin = parseFloat(getComputedStyle(placementControl).marginLeft) || 0;
    const gap = Math.max(0, Math.round((target.left - rect.left + margin) * 100) / 100) + 'px';
    if (placementToolbar.style.getPropertyValue('--rpen-placement-align-gap') !== gap) placementToolbar.style.setProperty('--rpen-placement-align-gap', gap);
  });
};
const placementAlignmentObserver = new ResizeObserver(schedulePlacementAlignment);
for (const control of [placementToolbar, ...placementToolbar.children, ...element('.transport').children]) placementAlignmentObserver.observe(control);
document.addEventListener('rpen-editor-skin-changed', schedulePlacementAlignment);
let lastPaint = 0;
let lastInfoTick = 0;
const frameLimiter = new FrameLimiter();
let frameSampleStart = 0;
let frameSampleCount = 0;
let measuredFps = 0;
let lastTick = 0;
function frame(timestamp, recording = false) {
  if (!recording && editorRecording?.capturing) { requestAnimationFrame(frame); return; }
  const elapsed = lastTick ? (timestamp - lastTick) / 1000 : 0; lastTick = timestamp;
  audio.update();
  if (!recording && largeChartWorkspace && hasDocument && !atHome) largeChartWorkspace.tick(chartSeconds());
  mods?.runtime.tick({timestamp,elapsed,chartSeconds:chartSeconds(),musicSeconds:audio.time,playing:audio.playing});
  batchControls.sync();
  lineSwitcher.draw(timestamp);
  if (!atHome) { if (globalEvents.active) globalEvents.autoScroll(elapsed); else if (noiseEditor.active) noiseEditor.autoScroll(elapsed); else timeline.autoScroll(elapsed); }
  if (timestamp - frameSampleStart >= 500) { measuredFps = frameSampleCount * 1000 / (timestamp - frameSampleStart); frameSampleStart = timestamp; frameSampleCount = 0; }
  if (!recording) advanceEditClock(timestamp);
  if (hasDocument && !atHome && timestamp - lastInfoTick > 500) {
    diagnostics.update();
    const info = element('#chart-info dd');
    if (info && !element('#chart-info').hidden) info.textContent = info.textContent.replace(/Time: .*$/, `Time: ${formatEditTime(editTimeSeconds)}  FPS: ${measuredFps.toFixed(1)}`);
    lastInfoTick = timestamp;
  }
  if (!recording && hasDocument && !atHome && !document.hidden) {
    const interval = editorPreferences.autoSaveSeconds ?? preferences.settings.autoSaveSeconds;
    // Compare media only when a backup is due, not on every animation frame.
    const mediaDirty = assetDirty && !autoSave.pending && timestamp - autoSave.last >= interval * 1000
      && (assets.size !== lastDraftAssets.length || lastDraftAssets.some(([name, bytes]) => assets.get(name) !== bytes)
        || assetFolders.size !== lastDraftFolders.length || lastDraftFolders.some(folder => !assetFolders.has(folder)));
    autoSave.tick(timestamp, editorPreferences.autoSave ?? preferences.settings.autoSave, interval, session.history.dirty && session.chart !== lastDraftDocument || mediaDirty);
  }
  if (!recording && audio.playing && !mods?.runtime.locks.size && loop && currentBeat() >= loop.end) seekBeat(loop.start, false);
  if (!recording && audio.playing && audio.time >= audio.duration) { audio.pause(); if (performanceMonitor?.run) performanceMonitor.finish('complete'); invalidate(); }
  if (!recording && !session.largeDocument) hitSounds.tick(session.chart, tempo, session.lineIndex);
  if (recording || !atHome && (audio.playing || dirtyFrame || spatialTool.animating || eventSampler.looping || animationCurvePlaybackActive() || trajectoryPanel.active || noteTrackingPanel.active && noteTrackingPanel.animating || noiseTrackingPanel.active && noiseTrackingPanel.animating || noiseFitPanel.active && noiseFitPanel.options.animate && noiseFitPanel.options.show && noiseFitPanel.previewChart && !noiseFitPanel.fullPreview) && frameLimiter.shouldRender(timestamp, editorPreferences.fpsLimit ?? preferences.settings.fpsLimit)) {
    frameSampleCount++;
    const start = performance.now();
    const beat = currentBeat();
    session.editSeconds = Math.max(0, chartSeconds());
    updateLineInfo();
    timeline.origin = beat;
    if (audioAnalysis.pitchView?.active) audioAnalysis.pitchView.draw(offsetSeconds());
    if (!preview.visible && !noiseEditor.active && !globalEvents.active && !noiseFitPanel.active && !noiseTrackingPanel.previewing && !noteTrackingPanel.previewing) {
      timeline.draw(beat);
      multiEdit.drawTimeline();
      selectionOverlay.draw();
      drawTimelineStrips();
      updateLayerButtons();
      audioAnalysis.draw(offsetSeconds());
    }
    if (noiseEditor.active) noiseEditor.draw();
    if (globalEvents.active) globalEvents.draw(beat);
    preview.duration = realtimePreview.duration = audio.duration;
    preview.selectedNoiseArea = realtimePreview.selectedNoiseArea = noiseEditor.active ? noiseEditor.selected : -1;
    if (directPlacement.active) directPlacement.layout();
    const viewSession = timeline.getSession();
    const noteOverrides = timeline.previewNotes();
    const trajectoryView = trajectoryPanel.tick(timestamp, chartSeconds(), audio.playing, realtimePreview);
    const noiseView = noiseEditor.active ? noiseEditor.previewState() : null;
    const eventSampleView = eventSampler.previewState();
    const animationCurveView = animationCurvePreviewState(timestamp);
    const speedCurveView = speedCurve.previewState();
    const textView = textLinesPanel.view();
    const noiseFitView = noiseFitPanel.tick(timestamp, chartSeconds(), audio.playing, realtimePreview);
    const trackingView = noteTrackingPanel.tick(timestamp, chartSeconds(), audio.playing) ?? noiseTrackingPanel.tick(timestamp, chartSeconds(), audio.playing);
    const quickCurveView = quickCurve.view();
    const modView=emitModHook('preview:before',{chart:animationCurveView?.chart ?? trackingView?.chart ?? noiseView?.chart ?? eventSampleView?.chart ?? noiseFitView?.chart ?? textView?.chart ?? trajectoryView?.chart ?? (multiEdit.active && multiEdit.previewHovered && multiEdit.previewEnabled.checked && multiEdit.result ? multiEdit.result.chart : viewSession.chart),seconds:animationCurveView?.seconds ?? trackingView?.seconds ?? noiseView?.seconds ?? eventSampleView?.seconds ?? noiseFitView?.seconds ?? textView?.seconds ?? trajectoryView?.seconds ?? chartSeconds(),lineIndex:animationCurveView?.lineIndex ?? viewSession.lineIndex});
    const viewChart = performanceMonitor?.run ? session.chart : speedCurveView?.chart ?? quickCurveView ?? modView.chart;
    preview.noteOverrides = realtimePreview.noteOverrides = viewChart === session.chart ? noteOverrides : null;
    const viewSeconds = performanceMonitor?.run ? chartSeconds() : modView.seconds;
    const previewAllLines = preview.allLines; const previewEffects = preview.showHitEffects;
    const previewNumbers = preview.lineNumbers; const previewArrows = preview.lineArrows;
    const forcedLineOpacity = preview.forceLineOpacity;
    preview.forceLineOpacity = Boolean(trackingView?.forceLineOpacity);
    if (trackingView) { preview.allLines = true; preview.showHitEffects = false; }
    if (textView || noiseFitView) { preview.allLines = true; preview.showHitEffects = false; preview.lineNumbers = false; preview.lineArrows = false; }
    if(spatialTool.enabled)spatialTool.before(preview);
    preview.draw(spatialTool.panel?.draft ?? viewChart, tempo, viewSeconds, modView.lineIndex);
    if(preview.visible)mods?.runtime.draw(preview,{target:'preview',chart:viewChart,tempo,seconds:viewSeconds,timestamp});
    if(spatialTool.enabled)spatialTool.draw(preview,viewChart,tempo,viewSeconds);
    if (directPlacement.active) { directPlacement.draw(true); batchControls.sync(); }
    preview.allLines = previewAllLines; preview.showHitEffects = previewEffects;
    preview.lineNumbers = previewNumbers; preview.lineArrows = previewArrows;
    preview.forceLineOpacity = forcedLineOpacity;
    if (!preview.visible) {
      const visible = realtimePreview.visible;
      if (animationCurveView || trackingView || trajectoryView || textView || noiseFitView || noiseEditor.sampleMode || eventSampler.active) realtimePreview.visible = true;
      element('#realtime-preview').hidden = !realtimePreview.visible;
      const opacity = realtimePreview.opacity;
      const allLines = realtimePreview.allLines;
      const lineNumbers = realtimePreview.lineNumbers; const lineArrows = realtimePreview.lineArrows;
      const forcedLineOpacity = realtimePreview.forceLineOpacity;
      realtimePreview.forceLineOpacity = Boolean(trackingView?.forceLineOpacity);
      if (trackingView) realtimePreview.allLines = true;
      if (textView || noiseFitView) { realtimePreview.allLines = true; realtimePreview.lineNumbers = false; realtimePreview.lineArrows = false; }
      if (animationCurveView || trackingView || noiseEditor.sampleMode || eventSampler.active || noiseFitView) realtimePreview.opacity = 1;
      if(spatialTool.enabled)spatialTool.before(realtimePreview);
      realtimePreview.draw(spatialTool.panel?.draft ?? viewChart, tempo, viewSeconds, modView.lineIndex);
      if(realtimePreview.visible)mods?.runtime.draw(realtimePreview,{target:'realtime',chart:viewChart,tempo,seconds:viewSeconds,timestamp});
      if(spatialTool.enabled)spatialTool.draw(realtimePreview,viewChart,tempo,viewSeconds);
      realtimePreview.opacity = opacity;
      realtimePreview.allLines = allLines;
      realtimePreview.lineNumbers = lineNumbers; realtimePreview.lineArrows = lineArrows;
      realtimePreview.forceLineOpacity = forcedLineOpacity;
      realtimePreview.visible = visible;
    }
    noiseEditor.drawSample();
    eventSampler.draw();
    element('#play').textContent = audio.playing || playback.buffering ? t('app.s87aecc9') : t('app.sb410f77');
    element('#play').dataset.playing = String(audio.playing || Boolean(playback.buffering));
    const skinBeat = Math.floor(beat);
    if (audio.playing && document.documentElement.dataset.rpenBeat !== String(skinBeat)) editorSkinSignal('beat');
    editorSkinState({ playing: audio.playing, beat: skinBeat, dirty: session.history.dirty || assetDirty, location: atHome ? 'home' : preview.visible ? 'preview' : 'editor' });
    element('#play').title = audio.playing ? t('app.s130448b') : t('app.s2192535');
    timeReadouts.update(chartSeconds(), audio.duration, beat);
    element('#scrubber').value = audio.time;
    const tempoPoint = tempo.points[Math.max(0, upperBound(tempo.points, beat, point => point.beat) - 1)];
    const lineTimeFactor = noiseEditor.active || globalEvents.active ? 1 : session.line?.bpmfactor ?? 1;
    element('#bpm-display').textContent = `${(60 / (tempoPoint.secondsPerBeat * lineTimeFactor)).toFixed(2)} BPM`;
    const draggedLineIndex = Number.isInteger(timeline.drag?.lineIndex) ? timeline.drag.lineIndex : session.lineIndex;
    const draggedSource = session.chart.judgeLineList?.[draggedLineIndex]?.notes?.[timeline.drag?.anchor];
    const draggedNote = ['move', 'startTime', 'endTime'].includes(timeline.drag?.kind) ? timeline.movedNote(draggedSource) : null;
    const positionInput = element('#properties input[aria-label="X 坐标"]');
    if (draggedNote && positionInput && document.activeElement !== positionInput) positionInput.value = Number(draggedNote.positionX).toFixed(2);
    element('#cursor-position').textContent = draggedNote ? t('app.sd67248b', { p0: draggedNote.positionX.toFixed(2), p1: beatValue(draggedNote.startTime).toFixed(3) }) : timeline.cursor ? t('app.sa1e6c17', { p0: timeline.positionAt(timeline.cursor.x).toFixed(2), p1: timeline.snappedBeat(timeline.cursor.y).toFixed(3) }) : '';
    if (timestamp - lastPaint > 400) {
      element('#performance').textContent = t('app.s274ea37', { p0: (performance.now() - start).toFixed(1) });
      lastPaint = timestamp;
    }
    dirtyFrame = false;
    if (performanceMonitor?.enabled) { const end = performance.now(); performanceMonitor.frame(end, end - start); }
  }
  if (!recording) requestAnimationFrame(frame);
}
applyPreferences(preferences);
keyDisplayReady = true;
setHome(true);
requestAnimationFrame(frame);

function applyPreferences(next) {
  next = { ...next, hotkeys: { ...DEFAULT_HOTKEYS, ...next.hotkeys } };
  preferences = next;
  audio.setVolume(editorPreferences.volume ?? next.settings.volume);
  element('#volume').value = audio.volume;
  hitSounds.setVolume(editorPreferences.hitVolume ?? next.settings.hitVolume);
  element('#hit-volume').value = hitSounds.volume;
  preview.noteSize = realtimePreview.noteSize = editorPreferences.noteSize ?? next.settings.noteSize;
  preview.lineScale = realtimePreview.lineScale = editorPreferences.lineScale ?? next.settings.lineScale;
  preview.backgroundAlpha = realtimePreview.backgroundAlpha = next.settings.backgroundAlpha;
  timeline.noteScale = (editorPreferences.noteSize ?? next.settings.noteSize) / 175;
  timeline.gridCount = editorPreferences.gridCount ?? next.settings.gridCount;
  timeline.scale = editorPreferences.scale ?? 500;
  timeline.division = editorPreferences.division ?? 4;
  timeline.snapX = editorPreferences.snapX ?? true;
  timeline.multiLineWidth = editorPreferences.multiLineWidth ?? 0;
  timeline.multiLineEventWidth = editorPreferences.multiLineEventWidth ?? 0;
  timeline.multiLineEventVisibility = editorPreferences.multiLineEventVisibility ?? {};
  timeline.multiLineWidthExplicit = Number.isFinite(editorPreferences.multiLineWidth);
  timeline.multiLineEventWidthExplicit = Number.isFinite(editorPreferences.multiLineEventWidth);
  element('#grid-count').value = timeline.gridCount; element('#division').value = timeline.division; element('#y-scale').value = timeline.scale; element('#snap-x').checked = timeline.snapX;
  element('#y-scale-slider').value = timeline.scale;
  realtimePreview.visible = editorPreferences.realtime ?? true;
  element('#realtime-enabled').checked = realtimePreview.visible; element('#realtime-preview').hidden = !realtimePreview.visible;
  element('#realtime-alpha').value = editorPreferences.realtimeAlpha ?? next.settings.realtimeAlpha;
  realtimePreview.opacity = Number(element('#realtime-alpha').value);
  audioAnalysis.enabled = editorPreferences.analysisEnabled ?? false;
  audioAnalysis.mode = editorPreferences.analysisMode === 'spectrum' ? 'spectrum' : 'waveform';
  audioAnalysis.alpha = Number.isFinite(Number(editorPreferences.analysisAlpha)) ? Number(editorPreferences.analysisAlpha) : 0.2;
  audioAnalysis.width = Number.isFinite(Number(editorPreferences.analysisWidth)) ? Number(editorPreferences.analysisWidth) : 0.62;
  hitSounds.enabled = editorPreferences.hitEnabled ?? true; element('#hit-enabled').checked = hitSounds.enabled;
  preview.allLines = realtimePreview.allLines = editorPreferences.allLines ?? true; element('#preview-mode').value = preview.allLines ? 'all' : 'current';
  timeline.scrollSpeed = next.settings.scrollSpeed / 5;
  session.history.limit = next.settings.historyLimit;
  element('#hotkey-help').hidden = !next.settings.showHotkey;
  element('#hotkey-help').textContent = t('app.s3789316', { p0: next.hotkeys.AddTap, p1: next.hotkeys.AddDrag, p2: next.hotkeys.AddFlick, p3: next.hotkeys.AddHold, p4: next.hotkeys.AddEvent, p5: next.hotkeys.Pause, p6: next.hotkeys.StartView, p7: next.hotkeys.EndView, p8: next.hotkeys.JumpView, p9: next.hotkeys.StartView_HOLD, p10: next.hotkeys.JumpView_HOLD, p11: next.hotkeys.SwitchUI, p12: next.hotkeys.ResetCamera, p13: next.hotkeys.CurveBegin, p14: next.hotkeys.CurveEnd });
  applyDisplaySettings();
  renderSession();
}

function applyMigratedPreferences(next) {
  editorPreferences = { ...editorPreferences, volume: next.settings.volume, hitVolume: next.settings.hitVolume, gridCount: next.settings.gridCount, realtimeAlpha: next.settings.realtimeAlpha,
    fpsLimit: next.settings.fpsLimit,
    noteSize: next.settings.noteSize,
    lineScale: next.settings.lineScale,
    ratioWidth: next.settings.ratioWidth, ratioHeight: next.settings.ratioHeight, barWidth: next.settings.barWidth, barAlpha: next.settings.barAlpha,
    autoSave: next.settings.autoSave, autoSaveSeconds: next.settings.autoSaveSeconds, autoSaveLimit: next.settings.autoSaveLimit, autoplayView: next.settings.autoplayView, highlight: next.settings.highlight, showGameUI: next.settings.showGameUI };
  applyPreferences(next); persistEditor();
}

listen('#migrate', async () => {
  const content = showDialog(t('app.sa14179a'), t('app.s0d0658f'));
  if (window.showDirectoryPicker) {
    const select = document.createElement('button'); select.type = 'button'; select.className = 'primary'; select.textContent = t('app.s1da6c74');
    select.onclick = async () => {
      try {
        const directory = await window.showDirectoryPicker({ mode: 'read' });
        status(t('app.s6f10570'));
        const entries = await directoryEntries(directory);
        migrationDialog(await scanMigration(entries, directory.name, fileOptions()), applyMigratedPreferences);
      } catch (error) { if (error.name !== 'AbortError') reportError(error); }
    };
    content.append(select);
  }
  const fallback = document.createElement('button'); fallback.type = 'button'; fallback.textContent = t('app.s92f35bf');
  fallback.onclick = () => element('#directory-input').click(); content.append(fallback);
});
element('#directory-input').addEventListener('change', async event => {
  try {
    if (!event.target.files.length) return;
    status(t('app.s6f10570'));
    const entries = uploadedEntries(event.target.files);
    const name = event.target.files[0].webkitRelativePath.split('/')[0];
    migrationDialog(await scanMigration(entries, name, fileOptions()), applyMigratedPreferences);
  } catch (error) { reportError(error); }
  finally { event.target.value = ''; }
});
listen('#library', () => {
  if (atHome) return;
  noiseEditor.flush();
  playback.pause();
  if (!session.history.dirty && !assetDirty) { setHome(true); return; }
  choose(t('app.s6ae650b'), t('app.s873b360'), [t('app.s78b733b'), t('app.s09bc228')], value => value, async value => {
    if (value === t('app.s78b733b')) {
      await save();
      if (session.history.dirty || assetDirty) throw new Error(t('app.sec16507'));
    }
    else {
      if (largeChartWorkspace) { await largeChartWorkspace.close({ discard: true }); largeChartWorkspace = null; }
      hasDocument = false; session.history.markSaved(); assetDirty = false;
    }
    setHome(true);
  });
});
listen('#preferences', () => settingsDialog.showModal());
listen('#advanced-preferences', () => {
  settingsDialog.close();
  pasteGesture.cancel();
  showHotkeySettings(preferences, async next => { await storePreferences(next); applyPreferences(next); });
});
readPreferences().then(saved => {
  if (saved) applyPreferences(migratePreferences(JSON.stringify(saved.originalSettings), Object.entries(saved.originalHotkeys).map(([key, value]) => `${key} ${value}`).join('\n'), saved.originalUI));
}).catch(error => status(t('app.s01610aa', { p0: error.message })));

function applyDisplaySettings() {
  timelineStrips.options = { densityStrip: editorPreferences.densityStrip ?? true, historyStrip: editorPreferences.historyStrip ?? true, densityStripBrightness: editorPreferences.densityStripBrightness ?? DEFAULT_EDITOR_PREFERENCES.densityStripBrightness, densityStripWindow: editorPreferences.densityStripWindow ?? DEFAULT_EDITOR_PREFERENCES.densityStripWindow };
  const strips = timelineStrips.options;
  const editor = element('.editor'); editor.classList.toggle('density-strip-off', !strips.densityStrip); editor.classList.toggle('history-strip-off', !strips.historyStrip);
  element('#note-density').hidden = !strips.densityStrip; element('#history-strip').hidden = !strips.historyStrip;
  session.recordRecentEdits = strips.historyStrip;
  if (!strips.densityStrip) timelineStrips.note = null;
  if (!strips.historyStrip) { timelineStrips.edits = null; timeline.selectionTimeRange = null; timeline.selectionRangeChart = null; }
  if (!strips.densityStrip || !strips.historyStrip) timelineStrips.key = null;
  session.lineEventDefaults = normalizeLineDefaults(editorPreferences.lineEventDefaults);
  timeline.noteSpacing = editorPreferences.noteSpacing ?? DEFAULT_EDITOR_PREFERENCES.noteSpacing;
  timeline.notesOnlyNoteScale = editorPreferences.notesOnlyNoteScale ?? DEFAULT_EDITOR_PREFERENCES.notesOnlyNoteScale;
  directPlacement.opacity = editorPreferences.placementOpacity ?? DEFAULT_EDITOR_PREFERENCES.placementOpacity;
  element('#direct-placement-enabled').checked = editorPreferences.directPlacement ?? false;
  const fpsLimit = editorPreferences.fpsLimit ?? preferences.settings.fpsLimit;
  element('#fps-unlimited').checked = !fpsLimit;
  element('#fps-limit').disabled = !fpsLimit;
  if (fpsLimit) element('#fps-limit').value = fpsLimit;
  element('#fps-limit').setCustomValidity('');
  element('#fps-limit-error').hidden = true;
  for (const [id, key, fallback] of displayFields) {
    const control = element(`#${id}`); const value = editorPreferences[key] ?? preferences.settings[key] ?? fallback;
    if (control.type === 'checkbox') control.checked = value; else control.value = value;
  }
  keyDisplay.set(editorPreferences.keyDisplay ?? DEFAULT_EDITOR_PREFERENCES.keyDisplay);
  const ratioWidth = editorPreferences.ratioWidth ?? preferences.settings.ratioWidth;
  const ratioHeight = editorPreferences.ratioHeight ?? preferences.settings.ratioHeight;
  setRatioOptions(element('#preview-ratio'), ratioWidth, ratioHeight);
  preview.aspectRatio = realtimePreview.aspectRatio = ratioWidth / ratioHeight;
  preview.noteSize = realtimePreview.noteSize = Number(element('#note-size').value);
  timeline.noteScale = preview.noteSize / 175;
  applyViewControls({ ...editorPreferences, showGameUI: editorPreferences.showGameUI ?? preferences.settings.showGameUI }, timeline, [preview, realtimePreview]);
  for (const renderer of [preview, realtimePreview]) for (const key of ['lineNumbers', 'lineArrows', 'lineTint', 'mergeLineNumbers', 'pickPreviewLines', 'pickPreviewNoiseAreas']) renderer[key] = editorPreferences[key] ?? true;
  for (const renderer of [preview, realtimePreview]) {
    renderer.noisePreview.seamless = editorPreferences.seamlessNoiseAreas ?? true;
    if (renderer.noisePreview.forceSimple !== (editorPreferences.forceSimpleNoise ?? false)) { renderer.noisePreview.gpu?.reset(); renderer.overflowNoisePreview?.gpu?.reset(); }
    renderer.noisePreview.forceSimple = editorPreferences.forceSimpleNoise ?? false;
    renderer.noisePreview.getAssets = () => assets;
    renderer.noisePreview.invalidate = invalidate;
    renderer.noisePreview.report = message => notify(message + t('app.s9c427ae'), 'error');
  }
  const toolbarMode = editorPreferences.toolbarMode ?? 'icons';
  element('.editor-toolbar').classList.remove('mode-compact', 'mode-icons', 'mode-wide');
  element('.editor-toolbar').classList.add(`mode-${toolbarMode}`);
  element('#toolbar-mode').title = t('app.sdb02ef4', { p0: toolbarMode === 'icons' ? t('app.s1f24c1e') : toolbarMode === 'wide' ? t('app.s6e6e581') : t('app.sc837edc') });
  timeline.barWidth = Number(element('#bar-width').value); timeline.barAlpha = Number(element('#bar-alpha').value); timeline.judgementOffset = Number(element('#judgement-offset').value);
  timeline.judgementColor = element('#judgement-color').value;
  timeline.judgementWidth = Number(element('#judgement-width').value); timeline.judgementAlpha = Number(element('#judgement-alpha').value);
  timeline.eventValueFontSize = Number(element('#event-value-size').value); timeline.eventValueThreshold = Number(element('#event-value-threshold').value); timeline.eventCurveThreshold = Number(element('#event-curve-threshold').value); timeline.eventOpacity = Number(element('#event-opacity').value); timeline.eventBarWidth = Number(element('#event-bar-width').value);
  timeline.seamlessEvents = element('#seamless-events').checked;
  timeline.rpeEventDrag = globalEvents.timeline.rpeEventDrag = element('#rpe-event-drag').checked;
  session.xyBinding = element('#xy-binding').checked;
  session.movementVector = !noiseEditor.active && !globalEvents.active && !preview.visible && element('#movement-vector').checked;
  if (!timeline.rpeEventDrag) setEventEndpointDragHeld(false);
  timeline.drawEventCurves = element('#draw-event-curves').checked;
  timeline.selectionInterval = globalEvents.timeline.selectionInterval = Number(element('#selection-interval').value);
  selectionIntervalPanel.sync();
  session.cutDensity = Number(element('#event-cut-density').value);
  session.dragSensitivity = Number(element('#event-drag-sensitivity').value);
  clipboardHistory.enabled = element('#clipboard-history-enabled').checked;
  lineSwitcher.enabled = element('#line-switcher-enabled').checked;
  if (!lineSwitcher.enabled) lineSwitcher.hide();
  if (!clipboardHistory.enabled) pasteGesture.cancel();
  element('#clipboard-history').hidden = !clipboardHistory.enabled;
  if (!clipboardHistory.enabled && activePaneName === 'clipboard') activatePane('chart');
  preview.backgroundBlur = realtimePreview.backgroundBlur = Number(element('#background-blur').value);
  preview.viewportOverflow = realtimePreview.viewportOverflow = Boolean(element('#viewport-overflow').checked);
  preview.overflowBlur = realtimePreview.overflowBlur = Number(element('#overflow-blur').value);
  preview.overflowDarken = realtimePreview.overflowDarken = Number(element('#overflow-darken').value);
  preview.lineScale = realtimePreview.lineScale = Number(element('#default-line-thickness').value);
  timeline.highlight = preview.highlight = realtimePreview.highlight = element('#highlight-notes').checked;
  audio.setPreservePitch(element('#preserve-pitch').checked);
  hitSounds.setDelays({ tap: Number(element('#hit-tap-delay').value), drag: Number(element('#hit-drag-delay').value), flick: Number(element('#hit-flick-delay').value) });
  rotateTip();
  invalidate();
}
function applyFpsLimit() {
  const input = element('#fps-limit');
  const unlimited = element('#fps-unlimited').checked;
  input.disabled = unlimited;
  const value = unlimited ? 0 : Number(input.value);
  const valid = unlimited || input.value.trim() && value > 10 && validFpsLimit(value);
  input.setCustomValidity(valid ? '' : t('app.sbb9273d'));
  element('#fps-limit-error').hidden = Boolean(valid);
  if (!valid) return;
  editorPreferences.fpsLimit = value;
  persistEditor(); invalidate();
}
document.addEventListener('dragstart', event => {
  if (!editorPreferences.textSelectionDrag && isTextEntry(event.target)) event.preventDefault();
}, true);
element('#fps-limit').addEventListener('input', applyFpsLimit);
element('#fps-unlimited').addEventListener('change', applyFpsLimit);
for (const [id, key] of displayFields.filter(([, key]) => key !== 'selectionInterval')) element(`#${id}`).addEventListener(['number', 'select-one'].includes(element(`#${id}`).type) ? 'change' : 'input', event => {
  const control = event.target;
  if (control.type === 'checkbox') editorPreferences[key] = control.checked;
  else if (control.tagName === 'SELECT' && [...control.options].some(option => option.value === control.value)) editorPreferences[key] = control.value;
  else if (control.type === 'color') editorPreferences[key] = control.value;
  else if (control.value.trim() && control.validity.valid && Number.isFinite(Number(control.value))) editorPreferences[key] = Number(control.value);
  else return;
  applyDisplaySettings(); persistEditor();
  if (key === 'movementVector') session.notify();
  if (key === 'historyStrip') session.notify();
});
timeline.eventPlacementType = timeline.eventTypes[0];

const modEditor={
  get session(){return session;},get chart(){return session.chart;},get tempo(){return tempo;},get assets(){return assets;},get preferences(){return preferences;},get editorPreferences(){return editorPreferences;},get chartName(){return chartName;},get agentWorkspaceId(){return versionRepoId;},
  get hasDocument(){return hasDocument&&!atHome;},get home(){return atHome;},get activePane(){return activePaneName;},
  get dirty(){return session.history.dirty||assetDirty;},get libraryProject(){return libraryProject;},get loop(){return loop;},set loop(value){loop=value;},
  audio,audioSources,audioOnsets,audioAnalysis,playback,preview,realtimePreview,timeline,skin,images,hitSounds,editorSkins,noteSkins,noiseEditor,eventSampler,spatialTool,speedCurve,trajectoryPanel,textLinesPanel,noiseFitPanel,noiseTrackingPanel,noteTrackingPanel,quickCurve,multiEdit,batchControls,linePanel,collaboration,globalEvents,globalEventRegistry,openGlobalEvents,versionControl,directPlacement,placementDefaultsPanel,diagnostics,diagnosticHighlight,
  chartSeconds,offsetSeconds,currentBeat,invalidate,render:renderSession,notify,save,activatePane,
  commit:(label,chart)=>session.commit(label,chart),replace:(chart,name=chartName)=>replaceChart(chart,name),
  seek:(seconds,{music=false,pause=true}={})=>{playback.seek(seconds+(music?0:offsetSeconds()),pause);invalidate();},
  seekBeat,selectLine:index=>{session.selectLine(index);renderSession();},setPreview:(visible,options={})=>togglePreview(visible,options.stay??true,false,{autoplay:options.autoplay??false}),
  play:async()=>{if(!audio.playing&&!playback.pending)await togglePlayback();},pause:()=>playback.pause(),
  addAsset:(path,bytes)=>{assets.set(path,bytes);assetDirty=true;diagnosticAssetRevision++;images.load(session.chart,assets,chartName);invalidate();},
  removeAsset:path=>{assets.delete(path);assetDirty=true;diagnosticAssetRevision++;images.load(session.chart,assets,chartName);invalidate();},
  shaderAssetsChanged:()=>{assetDirty=true;diagnosticAssetRevision++;images.shaderRevision=(images.shaderRevision??0)+1;invalidate();},
  raw:{get session(){return session;},get chart(){return session.chart;},set chart(value){session.history.document=value;session.notify();},get assets(){return assets;},get tempo(){return tempo;},set tempo(value){tempo=value;timeline.tempo=value;invalidate();},renderSession,replaceChart,activatePane,togglePlayback,togglePreview,save,applyDisplaySettings,persistEditor,setHome},
  beginPreviewSession(){
    if(atHome||!hasDocument)throw Error(t('app.s21c0425'));
    directPlacement.close();linePicker.hide();
    const snapshot={visible:preview.visible,pane:activePaneName,allLines:preview.allLines,lineNumbers:preview.lineNumbers,lineArrows:preview.lineArrows,lineTint:preview.lineTint,cameraX:preview.cameraX,viewDivisor:preview.viewDivisor,collisionGeometry:preview.collisionGeometry};
    playback.pause();timeline.cancelPlacement();batchControls.cancel();pasteGesture.cancel();heldPreview=null;playbackSpaceHeld=false;clearNoteSourceToast();
    eventSampler.finish?.(false);noiseEditor.endSample(false);noiseEditor.flush();noiseTrackingPanel.endSample();noteTrackingPanel.endSample();quickCurve.cancel();activatePane('chart');
    preview.allLines=true;preview.lineNumbers=false;preview.lineArrows=false;preview.lineTint=false;preview.collisionGeometry=true;togglePreview(true,true,false,{autoplay:false});
    let done=false;return()=>{if(done)return;done=true;playback.pause();togglePreview(snapshot.visible,true,false,{autoplay:false});preview.allLines=snapshot.allLines;preview.lineNumbers=snapshot.lineNumbers;preview.lineArrows=snapshot.lineArrows;preview.lineTint=snapshot.lineTint;preview.cameraX=snapshot.cameraX;preview.viewDivisor=snapshot.viewDivisor;preview.collisionGeometry=snapshot.collisionGeometry;activatePane(snapshot.pane);invalidate();};
  },
};
configureShaderWorkspace(() => modEditor);
document.addEventListener('rpen-shader-library-changed', () => { if (hasDocument && !atHome && !session.liveEventEdit) renderSession(); });
const reportShaderProblem = createShaderProblemReporter(() => session, message => notify(message, 'error', 7000));
for (const renderer of [preview, realtimePreview]) renderer.shaderRuntime.report = reportShaderProblem;
editorRecording = new EditorRecording(modEditor, {
  begin(start, timestamp) { lastTick = 0; preview.effectsSince = realtimePreview.effectsSince = start - offsetSeconds() - .001; keyDisplay.beginRender(timestamp); },
  async prepareFrame() {
    timeline.origin = currentBeat();
    if (largeChartWorkspace) await largeChartWorkspace.prepareRecordingFrame(chartSeconds());
  },
  drawFrame(timestamp) { frame(timestamp, true); keyDisplay.draw(timestamp); },
  end() { lastTick = 0; editClockTick = performance.now(); projectEditClock.last = editClockTick; frameLimiter.nextFrame = null; keyDisplay.endRender(); }
});
agentPanel = new AgentPanel(modEditor, { activate: activatePane, notify, prependSilence: applyAudioLeadingSilence,
  writeAsset: async (path, bytes, binding) => {
    const references = resourceReferences(session.chart, assets, chartName);
    if (!bytes) {
      if (Object.values(references).includes(path) || session.chart.judgeLineList.some(line => line.Texture === path)) throw Error(t('app.s31f8305'));
      assets.delete(path); assetDirty = true; diagnosticAssetRevision++; images.load(session.chart, assets, chartName); invalidate(); return { deleted: path, needsSave: true, undoable: false };
    }
    if (binding === 'song' || references.song === path) { if (!await loadMusic(bytes, path, true, false)) throw Error(t('app.s5402e57')); }
    else { assets.set(path, bytes); assetDirty = true; diagnosticAssetRevision++; if (binding === 'background') session.commit(t('app.sf699ea9'), { ...session.chart, META: { ...session.chart.META, background: path } }); images.load(session.chart, assets, chartName); }
    hitSounds.setProject(session.chart, assets, chartName); invalidate(); return { written: path, bytes: bytes.length, needsSave: true, undoable: false };
  },
  capture: async () => {
    const renderer = preview.visible ? preview : realtimePreview;
    const visible = renderer.visible; renderer.visible = true;
    try { renderer.draw(session.chart, tempo, chartSeconds(), session.lineIndex); } finally { renderer.visible = visible; }
    const canvas = document.createElement('canvas'); canvas.width = Math.min(1600, renderer.canvas.width); canvas.height = Math.max(1, Math.round(renderer.canvas.height * canvas.width / Math.max(1, renderer.canvas.width)));
    const context = canvas.getContext('2d'); context.drawImage(renderer.canvas, 0, 0, canvas.width, canvas.height);
    for (const layer of [renderer.shaderCanvas, renderer.overlayCanvas]) if (layer?.style.visibility === 'visible') context.drawImage(layer, 0, 0, canvas.width, canvas.height);
    return canvas.toDataURL('image/png');
  },
});
mods=new ModManager(modEditor,{notify,invalidate});
new GraphicsSettings(settingsDialog, { notify, startupReady: () => Promise.all([editorSkins.ready, noteSkins.ready, mods.ready]) });
const aboutUpdates = new AboutUpdates(settingsDialog, {
  startupReady: () => Promise.all([editorSkins.ready, noteSkins.ready, mods.ready]),
  dirty: () => hasDocument && (session.history.dirty || assetDirty),
  beforeRestart: async () => {
    playback.pause(); noiseEditor.flush(); await agentPanel?.stopAndFlush();
    if (hasDocument && (session.history.dirty || assetDirty)) {
      try { await save(); } catch { throw Object.assign(Error('Save incomplete'), { code: 'UPDATE_SAVE' }); }
    }
    if (hasDocument && (session.history.dirty || assetDirty)) throw Object.assign(Error('Save incomplete'), { code: 'UPDATE_SAVE' });
    speedCurve.saveView(true);
    await flushLocalPreferences();
    if (!nativeStorage && hasDocument && libraryProject?.id) sessionStorage.setItem(LOCALE_REOPEN_KEY, libraryProject.id);
  },
});
