User management: signing in is required before anything is loaded, the sign-in screen is drawn in the same canvas (user name, password as bullets, Enter moves on and submits) and the editor is built only after it; the API gained auth/session, auth/login and auth/logout while every other route answers 401 without a session, projects carry an owner and a foreign one comes back as 404, accounts live in data/users.json with a password hash and sessions as files in data/sessions with a sliding 30 days, so a build under the open tab does not sign anybody out; Logout sits at the far right of the toolbar and, as asked in the issue, does not reload the page: Editor.dispose() takes the editor down while the canvas stays, which is why the autosave, the focus handlers, the window listeners of the tilemap panel, the shortcuts and the plugins all got a way back; the tools sign in as e2e (tools/lib/auth.mjs, password from GAME_MAKER_E2E_PASSWORD or the git-ignored data/e2e-password.txt) and the browser gets the same session by cookie, new e2e module 45 covers the screen, the wrong pair, the bullets, logout without a reload and signing in again, new unit tests for HttpAuthStore and for what a session leaves behind (#177)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60ea56a2ce
commit
8e4bb6e1f9
38 files changed
+1530
-139
No files matched your search
+54
-12
@@ -1,4 +1,4 @@
|
||||
import type { Application } from 'pixi.js';
|
||||
import type { Application, Ticker } from 'pixi.js';
|
||||
import type { Asset } from '../core/model/Asset';
|
||||
import { findAsset, findObjectHolder } from '../core/model/Project';
|
||||
import type { ProjectStore } from '../core/store/ProjectStore';
|
||||
@@ -73,6 +73,11 @@ export type PluginTabId = `plugin:${string}`;
|
||||
export type TabId = CentreTab | PluginTabId;
|
||||
/** An open document (#171): an asset by its id, or the list the List tab edits – there is one. */
|
||||
export type DocumentId = `asset:${string}` | 'list';
|
||||
/** How to end the session; the button for it sits in the toolbar. */
|
||||
export interface EditorAccount {
|
||||
logOut: () => void;
|
||||
}
|
||||
|
||||
/** A button in the tab row. */
|
||||
export type StripTab = FixedTab | PluginTabId | DocumentId;
|
||||
|
||||
@@ -188,6 +193,10 @@ export class Editor implements PlayHost {
|
||||
/** Drawn-frame count at the last measurement, not a counter of its own. */
|
||||
private fpsFrames = 0;
|
||||
private fpsElapsed = 0;
|
||||
/** What this editor registered outside itself, as the calls that take it away again (#177). */
|
||||
private readonly cleanups: (() => void)[] = [];
|
||||
private readonly autosave: { flush: () => Promise<void>; stop: () => void };
|
||||
private disposed = false;
|
||||
private leftWidth = 250;
|
||||
private rightWidth = 350;
|
||||
/** Starts out as wide as the inspector, then goes its own way. */
|
||||
@@ -198,12 +207,13 @@ export class Editor implements PlayHost {
|
||||
|
||||
constructor(
|
||||
app: Application,
|
||||
root: VStack,
|
||||
private readonly root: VStack,
|
||||
private readonly state: EditorState,
|
||||
store: ProjectStore,
|
||||
textures: TextureStore,
|
||||
fonts: FontStore,
|
||||
sounds: SoundStore,
|
||||
account: EditorAccount,
|
||||
) {
|
||||
this.viewport = new SceneViewport(state);
|
||||
this.gameView = new GameViewport(state);
|
||||
@@ -249,6 +259,7 @@ export class Editor implements PlayHost {
|
||||
openSceneMenu: (anchor) => this.projects.openSceneMenu(anchor),
|
||||
togglePlay: () => (state.mode === 'edit' ? this.play.play() : this.play.stop()),
|
||||
togglePause: () => this.play.togglePause(),
|
||||
logOut: () => account.logOut(),
|
||||
});
|
||||
root.add(this.toolbar);
|
||||
|
||||
@@ -256,7 +267,7 @@ export class Editor implements PlayHost {
|
||||
this.leftColumn = new VStack();
|
||||
this.assets = new AssetPanel(state, textures, fonts, sounds);
|
||||
this.leftColumn.add(this.assets, this.assetsShare);
|
||||
installFileImport(app.canvas, { state, textures, fonts, sounds });
|
||||
this.cleanups.push(installFileImport(app.canvas, { state, textures, fonts, sounds }));
|
||||
this.leftColumn.add(this.columnSplitter);
|
||||
this.leftColumn.add(this.hierarchy, 1 - this.assetsShare);
|
||||
// A share rather than a height, so a taller or shorter window keeps the split the user chose.
|
||||
@@ -357,15 +368,19 @@ export class Editor implements PlayHost {
|
||||
root.add(this.workspace, 1);
|
||||
root.resize(app.screen.width, app.screen.height);
|
||||
|
||||
attachAutosave(state, store);
|
||||
installShortcuts(state, {
|
||||
play: () => this.play.play(),
|
||||
stop: () => this.play.stop(),
|
||||
togglePause: () => this.play.togglePause(),
|
||||
copyKeys: () => this.tab === 'animation' && this.animationEditor.canvas.copyKeys(),
|
||||
pasteKeys: () => this.tab === 'animation' && this.animationEditor.canvas.pasteKeys(),
|
||||
});
|
||||
app.ticker.add((ticker) => this.tick(ticker.deltaMS / 1000));
|
||||
this.autosave = attachAutosave(state, store);
|
||||
this.cleanups.push(
|
||||
installShortcuts(state, {
|
||||
play: () => this.play.play(),
|
||||
stop: () => this.play.stop(),
|
||||
togglePause: () => this.play.togglePause(),
|
||||
copyKeys: () => this.tab === 'animation' && this.animationEditor.canvas.copyKeys(),
|
||||
pasteKeys: () => this.tab === 'animation' && this.animationEditor.canvas.pasteKeys(),
|
||||
}),
|
||||
);
|
||||
const onTick = (ticker: Ticker) => this.tick(ticker.deltaMS / 1000);
|
||||
app.ticker.add(onTick);
|
||||
this.cleanups.push(() => app.ticker.remove(onTick));
|
||||
// A node, a frame and a template object exist only in their own document, so selecting one
|
||||
// brings it up. Selecting an asset never does: that is what opening it (double-click, Enter,
|
||||
// creating it) is for – see the `open` event below.
|
||||
@@ -590,6 +605,33 @@ export class Editor implements PlayHost {
|
||||
return this.projects.boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the editor off the screen and lets go of everything it registered elsewhere – ticker,
|
||||
* shortcuts, file drops, plugins, the autosave. Signing out does this instead of reloading the
|
||||
* page (#177): the canvas, the renderer and the UI context stay, only what is drawn changes.
|
||||
*
|
||||
* Unsaved work is written first, while the session that owns it is still good.
|
||||
*/
|
||||
async dispose(): Promise<void> {
|
||||
if (this.disposed) return;
|
||||
this.disposed = true;
|
||||
await this.autosave.flush();
|
||||
this.autosave.stop();
|
||||
if (this.play.isPlaying) this.play.stop();
|
||||
this.editorRun.shutDown();
|
||||
await this.plugins?.stopAll();
|
||||
for (const cleanup of this.cleanups.splice(0).reverse()) cleanup();
|
||||
// A menu or a picker left open belongs to widgets that are about to be destroyed.
|
||||
for (const popup of ui().overlay.removeChildren()) popup.destroy({ children: true });
|
||||
ui().focus.blur();
|
||||
this.root.removeChild(this.toolbar, this.workspace);
|
||||
this.toolbar.destroy({ children: true });
|
||||
this.workspace.destroy({ children: true });
|
||||
// The sign-in screen sets `root` to the size of the window as well, and `resize` lays out only
|
||||
// when the size changed – the same reason `App.ts` forgets the size after signing in.
|
||||
this.root.resize(0, 0);
|
||||
}
|
||||
|
||||
showGame(): void {
|
||||
this.showTab('game');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user