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',