diff --git a/wwwroot/modules/main.js b/wwwroot/modules/main.js
index ddc8370..07e9740 100644
--- a/wwwroot/modules/main.js
+++ b/wwwroot/modules/main.js
@@ -888,6 +888,17 @@ function handleStateEvent(args) {
// Update instance details
instanceState.previousProjectId = args.previousProjectId;
resetViewportToCentre();
+ // Push the new project to window history to make the back and forward buttons work
+ // (as long as this project change wasn't due to 'history' ie. the user clicking back or forward buttons)
+ if (args.context !== State.Contexts.history) {
+ const newUrl = new URL(window.location);
+ newUrl.searchParams.set('project', args.projectId);
+ if (args.context === State.Contexts.init) {
+ window.history.replaceState({ projectId: args.projectId }, '', newUrl);
+ } else {
+ window.history.pushState({ projectId: args.projectId }, '', newUrl);
+ }
+ }
}
displaySelectedProject();
break;
@@ -996,6 +1007,16 @@ async function handleProjectDropdownOnCommand(args) {
exportProjectToJson();
break;
+ case ProjectDropdown.Commands.projectLink:
+ const url = new URL(window.location);
+ url.hash = '';
+ url.search = '';
+ url.searchParams.set('project', getProject().id);
+ navigator.clipboard.writeText(url.toString()).then(() => {
+ toast.show('Project link copied to clipboard.');
+ });
+ break;
+
case ProjectDropdown.Commands.projectLoadById:
state.setProjectById(args.projectId);
projectDropdown.setState({ visible: false });
@@ -4009,7 +4030,7 @@ function changePaletteSystem(paletteIndex, system) {
}
function changePaletteEditorDisplayNativeColours(displayNative) {
- currentProject.nativePalettes = null;
+ currentProject.nativePalettes = null;
state.persistentUIState.displayNativeColour = displayNative;
state.saveToLocalStorage();
@@ -5414,13 +5435,35 @@ window.addEventListener('load', async () => {
projectEntryList = state.getProjectEntries();
}
+ // Load project from URL?
+ const params = new URLSearchParams(window.location.search);
+ if (params.has('project')) {
+ const projectId = params.get('project');
+ const project = state.getProjectEntries().filter((p) => p.id === projectId)[0];
+ if (project) {
+ getUIState().lastProjectId = projectId;
+ } else {
+ toast.show('Project ID from URL not found.');
+ }
+ }
+
+ // Load project
try {
- state.setProjectById(getUIState().lastProjectId);
+ state.setProjectById(getUIState().lastProjectId, State.Contexts.init);
} catch {
const firstProjectId = state.getProjectEntries()[0].id;
- state.setProjectById(firstProjectId);
+ state.setProjectById(firstProjectId, State.Contexts.init);
}
+ // Add event listener for when the user clicks back or forward, so that we load their project
+ window.addEventListener('popstate', (e) => {
+ if (e.state?.projectId) {
+ if (e.state?.projectId !== getProject().id) {
+ state.setProjectById(e.state?.projectId, State.Contexts.history);
+ }
+ }
+ });
+
projectToolbar.setState({
projects: projectEntryList
});
diff --git a/wwwroot/modules/state.js b/wwwroot/modules/state.js
index 17efbfa..a016ef0 100644
--- a/wwwroot/modules/state.js
+++ b/wwwroot/modules/state.js
@@ -27,7 +27,9 @@ const events = {
};
const contexts = {
- deleted: 'deleted'
+ deleted: 'deleted',
+ init: 'init',
+ history: 'history'
};
@@ -54,6 +56,13 @@ export default class State {
return contexts;
}
+ /**
+ * Gets a list of sources that may trigger an event.
+ */
+ static get EventSources() {
+ return eventSources;
+ }
+
/**
* Gets the presistent UI state (must call the 'loadPersistentUIStateFromLocalStorage()' method before accessing).
@@ -138,8 +147,9 @@ export default class State {
/**
* Set the current project.
* @param {Project?} project - Project to set, or null if no project.
+ * @param {string?} [context=null] - Context on what triggered the project change.
*/
- setProject(project) {
+ setProject(project, context) {
let lastProjectId = this.project?.id ?? null;
if (project instanceof Project) {
this.#project = project;
@@ -149,9 +159,15 @@ export default class State {
let thisProjectId = this.project?.id ?? null;
if (lastProjectId !== thisProjectId) {
- this.#dispatcher.dispatch(EVENT_OnEvent, createArgs(events.projectChanged, { projectId: thisProjectId, lastProjectId: lastProjectId }));
+ this.#dispatcher.dispatch(EVENT_OnEvent, createArgs(events.projectChanged, {
+ projectId: thisProjectId,
+ lastProjectId: lastProjectId,
+ context: context ?? null
+ }));
} else {
- this.#dispatcher.dispatch(EVENT_OnEvent, createArgs(events.projectUpdated, { projectId: thisProjectId }));
+ this.#dispatcher.dispatch(EVENT_OnEvent, createArgs(events.projectUpdated, {
+ projectId: thisProjectId
+ }));
}
}
@@ -169,20 +185,22 @@ export default class State {
/**
* Loads a project from local storage and sets it as the current project.
* @param {string?} projectId - Unique ID of the project to load from local storage.
+ * @param {string?} [context=null] - Context on what triggered the project change.
*/
- setProjectFromLocalStorage(projectId) {
+ setProjectFromLocalStorage(projectId, context) {
const project = this.getProjectFromLocalStorage(projectId);
ensureProjectHasId(project);
- this.setProject(project);
+ this.setProject(project, context);
}
/**
* Loads a project, favouring any cached project, otherwise from local storage, sets it as the current project.
* @param {string?} projectId - Unique ID of the project to load.
+ * @param {string?} [context=null] - Context on what triggered the project change.
*/
- setProjectById(projectId) {
+ setProjectById(projectId, context) {
const project = this.getProjectById(projectId);
- this.setProject(project);
+ this.setProject(project, context);
}
/**
@@ -305,7 +323,7 @@ export default class State {
this.#dispatcher.dispatch(EVENT_OnEvent, createArgs(events.projectListChanged, { context: contexts.deleted, projectId: projectId }));
if (this.project?.id === projectId) {
- this.setProject(null);
+ this.setProject(null, eventSources.none);
}
}
addOrRemoveProjectEntriesBasedOnLocalStorage(this.#projectEntries, this);
@@ -427,6 +445,7 @@ function updateProjectEntriesFromProjects(projectEntryList, projects) {
/**
* @typedef {Object} StateEventArgs
* @property {string} event - The event that occurred.
+ * @property {string} eventSource - Context on what triggered the event.
* @property {string} context - Context about the event that occurred.
* @property {string} projectId - Project ID from the current project.
* @property {string} previousProjectId - Project ID from the last loaded project.
diff --git a/wwwroot/modules/ui/dialogues/projectDropdown.html b/wwwroot/modules/ui/dialogues/projectDropdown.html
index edeb1ca..f707240 100644
--- a/wwwroot/modules/ui/dialogues/projectDropdown.html
+++ b/wwwroot/modules/ui/dialogues/projectDropdown.html
@@ -11,9 +11,13 @@
+
diff --git a/wwwroot/modules/ui/dialogues/projectDropdown.js b/wwwroot/modules/ui/dialogues/projectDropdown.js
index 30a5ca9..8911c3c 100644
--- a/wwwroot/modules/ui/dialogues/projectDropdown.js
+++ b/wwwroot/modules/ui/dialogues/projectDropdown.js
@@ -16,6 +16,7 @@ const commands = {
projectLoadFromFile: 'projectLoadFromFile',
projectLoadById: 'projectLoadById',
projectSaveToFile: 'projectSaveToFile',
+ projectLink: 'projectLink',
projectDelete: 'projectDelete',
projectSort: 'projectSort',
sampleProjectSelect: 'sampleProjectSelect',