Merge branch 'local-storage-in-store' into 'v3'

Local storage in store

See merge request openflexure/openflexure-microscope-server!430
This commit is contained in:
Julian Stirling 2025-11-03 11:27:25 +00:00
commit 0b4c90ed9d
7 changed files with 112 additions and 120 deletions

View file

@ -23,23 +23,15 @@ export default {
data: function() { data: function() {
return { return {
newOrigin: this.$store.state.origin, newOrigin: this.$store.state.overrideOrigin,
reloadWhenOverridingOrigin: true, reloadWhenOverridingOrigin: true,
}; };
}, },
mounted() {
if (localStorage.overrideOrigin) {
this.newOrigin = localStorage.overrideOrigin;
} else {
this.newOrigin = "http://microscope.local:5000";
}
},
methods: { methods: {
overrideAPIHost: function(event) { overrideAPIHost: function(event) {
// Save the origin override, so that if we reload the web app, you can easily // Save the origin override, so that if we reload the web app, you can easily
localStorage.overrideOrigin = this.newOrigin; this.$store.commit("changeOverrideOrigin", this.newOrigin);
// If we have elected not to reload the interface, just update the origin // If we have elected not to reload the interface, just update the origin
// in the store. Otherwise, the form's default action will do the job for us. // in the store. Otherwise, the form's default action will do the job for us.

View file

@ -10,12 +10,7 @@
<div> <div>
<label class="uk-form-label" for="form-stacked-text">x</label> <label class="uk-form-label" for="form-stacked-text">x</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<input <input v-model="stepSize.x" class="uk-input uk-form-small" type="number" />
v-model="stepSize.x"
class="uk-input uk-form-small"
type="number"
name="inputStepXy"
/>
</div> </div>
<label class="uk-margin-small-right" <label class="uk-margin-small-right"
><input v-model="invert.x" class="uk-checkbox" type="checkbox" /> Invert x</label ><input v-model="invert.x" class="uk-checkbox" type="checkbox" /> Invert x</label
@ -25,12 +20,7 @@
<div> <div>
<label class="uk-form-label" for="form-stacked-text">y</label> <label class="uk-form-label" for="form-stacked-text">y</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<input <input v-model="stepSize.y" class="uk-input uk-form-small" type="number" />
v-model="stepSize.y"
class="uk-input uk-form-small"
type="number"
name="inputStepY"
/>
</div> </div>
<label class="uk-margin-small-right" <label class="uk-margin-small-right"
><input v-model="invert.y" class="uk-checkbox" type="checkbox" /> Invert y</label ><input v-model="invert.y" class="uk-checkbox" type="checkbox" /> Invert y</label
@ -40,12 +30,7 @@
<div> <div>
<label class="uk-form-label" for="form-stacked-text">z</label> <label class="uk-form-label" for="form-stacked-text">z</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<input <input v-model="stepSize.z" class="uk-input uk-form-small" type="number" />
v-model="stepSize.z"
class="uk-input uk-form-small"
type="number"
name="inputStepZz"
/>
</div> </div>
</div> </div>
</div> </div>
@ -177,18 +162,6 @@ export default {
data: function() { data: function() {
return { return {
stepXy: 200,
stepZz: 50,
stepSize: {
x: 200,
y: 200,
z: 50,
},
invert: {
x: false,
y: false,
z: false,
},
setPosition: null, setPosition: null,
isAutofocusing: 0, isAutofocusing: 0,
moveLock: false, moveLock: false,
@ -196,6 +169,15 @@ export default {
}, },
computed: { computed: {
// Note that as stepSize and invert are set based on internals we can use
// get() and set() to interact with the store. Instead use a deep watcher to
// update the store (see ``watch:`` below)
stepSize() {
return this.$store.state.navigationStepSize;
},
invert() {
return this.$store.state.navigationInvert;
},
baseUri: function() { baseUri: function() {
return this.$store.getters.baseUri; return this.$store.getters.baseUri;
}, },
@ -210,22 +192,19 @@ export default {
watch: { watch: {
stepSize: { stepSize: {
deep: true, deep: true,
handler() { handler(newVal) {
this.setLocalStorageObj("navigation_stepSize", this.stepSize); this.$store.commit("changeNavigationStepSize", newVal);
}, },
}, },
invert: { invert: {
deep: true, deep: true,
handler() { handler(newVal) {
this.setLocalStorageObj("navigation_invert", this.invert); this.$store.commit("changeNavigationInvert", newVal);
}, },
}, },
}, },
async mounted() { async mounted() {
// Reload saved settings
this.stepSize = this.getLocalStorageObj("navigation_stepSize") || this.stepSize;
this.invert = this.getLocalStorageObj("navigation_invert") || this.invert;
let self = this; let self = this;
// A global signal listener to perform a move action // A global signal listener to perform a move action
this.$root.$on("globalMoveEvent", self.move); this.$root.$on("globalMoveEvent", self.move);

View file

@ -38,17 +38,6 @@ export default {
}, },
}, },
watch: {
appTheme: function() {
this.setLocalStorageObj("appTheme", this.appTheme);
},
},
mounted() {
// Try loading settings from localStorage. If null, don't change.
this.appTheme = this.getLocalStorageObj("appTheme") || this.appTheme;
},
methods: { methods: {
async toggleFullscreen() { async toggleFullscreen() {
if (!document.fullscreenElement) { if (!document.fullscreenElement) {

View file

@ -32,14 +32,6 @@ export default {
}, },
}, },
}, },
watch: {
// Cache the stream settings to local storage for persistence
// (the next 3 functions all relate to this)
disableStream: function(newValue) {
this.setLocalStorageObj("disableStream", newValue);
},
},
}; };
</script> </script>

View file

@ -193,22 +193,6 @@ Vue.mixin({
toggleModalElement: function(element) { toggleModalElement: function(element) {
UIkit.modal(element).toggle(); UIkit.modal(element).toggle();
}, },
getLocalStorageObj: function(keyName) {
if (localStorage.getItem(keyName)) {
try {
return JSON.parse(localStorage.getItem(keyName));
} catch (e) {
localStorage.removeItem(keyName);
return null;
}
}
},
setLocalStorageObj: function(keyName, object) {
const parsed = JSON.stringify(object);
localStorage.setItem(keyName, parsed);
},
}, },
}); });

View file

@ -17,6 +17,44 @@ function getOriginFromLocation() {
} }
} }
/**
* Converts a Vuex state key (e.g. "appTheme") into a corresponding
* Vuex mutation name (e.g. "changeAppTheme") using the `change<Key>`
* convention.
*
* @param {string} key - The Vuex state key to convert.
* @returns {string} - The formatted mutation name.
*/
function keyToMutationName(key) {
return `change${key.charAt(0).toUpperCase() + key.slice(1)}`;
}
/**
* Converts a Vuex mutation name (e.g. "changeAppTheme") back into
* the corresponding state key (e.g. "appTheme") using the
* `change<Key>` convention.
*
* @param {string} mutationName - The Vuex mutation name to reverse.
* @returns {string|null} - The derived state key, or null if the
* mutation name doesn't match the `change<Key>` convention.
*/
function mutationToKey(mutationName) {
const prefix = "change";
if (!mutationName.startsWith(prefix)) {
return null; // Not a mutation we care about
}
const key = mutationName.slice(prefix.length);
return key.charAt(0).toLowerCase() + key.slice(1);
}
const LOCALSTORAGE_KEYS = [
"appTheme",
"disableStream",
"overrideOrigin",
"navigationStepSize",
"navigationInvert",
];
export default new Vuex.Store({ export default new Vuex.Store({
modules: { modules: {
wot: wotStoreModule, wot: wotStoreModule,
@ -26,13 +64,29 @@ export default new Vuex.Store({
available: false, available: false,
waiting: false, waiting: false,
error: "", error: "",
disableStream: false,
autoGpuPreview: false, autoGpuPreview: false,
trackWindow: true, trackWindow: true,
galleryEnabled: true, galleryEnabled: true,
appTheme: "system",
activeStreams: {}, activeStreams: {},
microscopeHostname: "", microscopeHostname: "",
// Persistent items:
// The app theme (e.g. light/dark)
appTheme: "system",
disableStream: false,
// The origin to use if overriding with dev tools
overrideOrigin: "http://microscope.local:5000",
// The step sizes for navigation via control pane/keys presses
navigationStepSize: {
x: 200,
y: 200,
z: 50,
},
// The axis inversion for navigation via control pane/keys presses
navigationInvert: {
x: false,
y: false,
z: false,
},
}, },
mutations: { mutations: {
@ -78,6 +132,15 @@ export default new Vuex.Store({
changeMicroscopeHostname(state, value) { changeMicroscopeHostname(state, value) {
state.microscopeHostname = value; state.microscopeHostname = value;
}, },
changeOverrideOrigin(state, value) {
state.overrideOrigin = value;
},
changeNavigationStepSize(state, value) {
state.navigationStepSize = value;
},
changeNavigationInvert(state, value) {
state.navigationInvert = value;
},
}, },
actions: {}, actions: {},
@ -86,4 +149,33 @@ export default new Vuex.Store({
baseUri: state => state.origin, baseUri: state => state.origin,
ready: state => state.available, ready: state => state.available,
}, },
plugins: [
store => {
// Load initial state from localStorage
LOCALSTORAGE_KEYS.forEach(key => {
console.log(key);
const saved = localStorage.getItem(key);
if (saved !== null) {
try {
const parsed = JSON.parse(saved);
const mutationName = keyToMutationName(key);
store.commit(mutationName, parsed);
} catch (e) {
console.warn(`Failed to parse localStorage key "${key}":`, e);
localStorage.removeItem(key);
}
}
});
// Subscribe to mutations
store.subscribe((mutation, state) => {
const key = mutationToKey(mutation.type);
// If the mutation is chacning a local storage key then update localStorage
if (key && LOCALSTORAGE_KEYS.includes(key)) {
localStorage.setItem(key, JSON.stringify(state[key]));
}
});
},
],
}); });

View file

@ -1,36 +0,0 @@
/**
* This function should be called from a Vue component's "created" hook
* It will initialise a list of properties from local storage.
* It then sets up watchers for said properties to sync them back
* so that next time we load, the values persist.
*
* Arguments:
* keyName: a string, used as the key in local storage
* syncedObject: the object whose data you want to sync (usually `this`)
* syncedData: an object, the keys of which set the properties to be synced
*/
export function syncDataWithLocalStorage(keyName, syncedObject, syncedData) {
const syncedKeys = Object.keys(syncedData);
// First, we try to retrieve the stored data
const storedString = localStorage.getItem(keyName);
if (storedString) {
const storedValues = JSON.parse(storedString);
for (const item of syncedKeys) {
if (item in storedValues) syncedObject[item] = storedValues[item];
}
}
// This function will update local storage with current values
let updateStoredValues = function() {
let newData = {};
for (const item of syncedKeys) {
newData[item] = syncedObject[item];
}
localStorage.setItem(keyName, JSON.stringify(newData));
};
// Now, set up watchers to update local storage when things change
for (const item of syncedKeys) {
syncedObject.$watch(item, updateStoredValues, { deep: true });
}
}