Skip to content

Commit 6f15805

Browse files
committed
feat: bi-directional favorites sync between WebUI and Meteor
Implement real-time favorites synchronization with Meteor Client as source of truth Backend changes: - Add FAVORITES_STATE_CHANGED and FAVORITES_UPDATE message types - Add getFavorites/setFavorites/createFavoritesStateMessage to ModuleMapper - Add handleFavoritesUpdate handler in MeteorWebSocket - Add broadcastFavoritesChanged to MeteorWebServer - Track favorite state changes in EventMonitor alongside module state - Include favorites in initial WebSocket state Frontend changes: - Remove localStorage persistence (Meteor is now source of truth) - Add setFavorites() to modules store for backend updates - Modify toggleFavorite() to return new favorites array - Send favorites updates to backend via WebSocket - Handle favorites.state.changed messages - Update all favorite toggle components (ModuleCard, ModuleCardCompact, ModuleSettingsDialog) Favorites now sync bidirectionally: - WebUI → Meteor: Toggling favorites in browser updates Meteor Client - Meteor → WebUI: Toggling favorites in-game broadcasts to all connected clients - Initial state: Favorites loaded from Meteor on WebSocket connect
1 parent 1e5fa18 commit 6f15805

11 files changed

Lines changed: 154 additions & 35 deletions

File tree

.claude/settings.local.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
"Bash(dir:*)",
2727
"Skill(minecraft-fabric-dev)",
2828
"WebFetch(domain:maven.meteordev.org)",
29-
"Bash(mcp__desktop-notifications__send-notification)"
29+
"Bash(mcp__desktop-notifications__send-notification)",
30+
"Skill(meteor-addon)",
31+
"Bash(python:*)",
32+
"Bash(git clone:*)",
33+
"Bash(npx vue-tsc:*)"
3034
],
3135
"deny": [],
3236
"ask": []

src/main/java/com/cope/meteorwebgui/events/EventMonitor.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class EventMonitor {
2727
private final MeteorWebServer server;
2828
private final Map<Setting<?>, Consumer<?>> originalCallbacks = new HashMap<>();
2929
private final Map<String, Boolean> moduleStates = new HashMap<>();
30+
private final Map<String, Boolean> favoriteStates = new HashMap<>();
3031
private final Map<String, Boolean> hudStates = new HashMap<>();
3132

3233
public EventMonitor(MeteorWebServer server) {
@@ -42,6 +43,7 @@ public void startMonitoring() {
4243
// Monitor all existing modules and initialize state tracking
4344
for (Module module : Modules.get().getAll()) {
4445
moduleStates.put(module.name, module.isActive());
46+
favoriteStates.put(module.name, module.favorite);
4547
monitorModuleSettings(module);
4648
}
4749

@@ -54,11 +56,15 @@ public void startMonitoring() {
5456
* Handle module toggle events
5557
* ActiveModulesChangedEvent is a singleton event that fires whenever any module toggles,
5658
* but doesn't tell us which one. So we compare current state to our tracked state.
59+
* We also check for favorite changes here since there's no dedicated event for them.
5760
*/
5861
@EventHandler
5962
private void onModuleToggle(ActiveModulesChangedEvent event) {
63+
boolean favoritesChanged = false;
64+
6065
// Find which module(s) changed state
6166
for (Module module : Modules.get().getAll()) {
67+
// Check active state
6268
Boolean previousState = moduleStates.get(module.name);
6369
boolean currentState = module.isActive();
6470

@@ -71,6 +77,21 @@ private void onModuleToggle(ActiveModulesChangedEvent event) {
7177
LOG.debug("Module state changed: {} -> {}", module.name, currentState);
7278
}
7379
}
80+
81+
// Check favorite state
82+
Boolean previousFavorite = favoriteStates.get(module.name);
83+
boolean currentFavorite = module.favorite;
84+
85+
if (previousFavorite == null || previousFavorite != currentFavorite) {
86+
favoriteStates.put(module.name, currentFavorite);
87+
favoritesChanged = true;
88+
LOG.debug("Module favorite changed: {} -> {}", module.name, currentFavorite);
89+
}
90+
}
91+
92+
// Broadcast favorites change if any module's favorite status changed
93+
if (favoritesChanged && server.isRunning()) {
94+
server.broadcastFavoritesChanged();
7495
}
7596
}
7697

@@ -202,6 +223,8 @@ public void stopMonitoring() {
202223
LOG.info("Stopping event monitoring");
203224
// Could restore original callbacks here if needed
204225
originalCallbacks.clear();
226+
moduleStates.clear();
227+
favoriteStates.clear();
205228
hudStates.clear();
206229
}
207230
}

src/main/java/com/cope/meteorwebgui/mapping/ModuleMapper.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,46 @@ public static JsonObject createSettingChangeMessage(Module module, Setting<?> se
141141
data.add("value", SettingsReflector.getSettingValue(setting, SettingsReflector.detectSettingType(setting)));
142142
return data;
143143
}
144+
145+
/**
146+
* Get list of all favorite module names
147+
*/
148+
public static JsonArray getFavorites() {
149+
JsonArray favorites = new JsonArray();
150+
try {
151+
for (Module module : Modules.get().getAll()) {
152+
if (module.favorite) {
153+
favorites.add(module.name);
154+
}
155+
}
156+
} catch (Exception e) {
157+
LOG.error("Failed to get favorites: {}", e.getMessage(), e);
158+
}
159+
return favorites;
160+
}
161+
162+
/**
163+
* Set favorites from a list of module names
164+
* @param favoriteNames List of module names to mark as favorites
165+
*/
166+
public static void setFavorites(List<String> favoriteNames) {
167+
try {
168+
Set<String> favoriteSet = new HashSet<>(favoriteNames);
169+
for (Module module : Modules.get().getAll()) {
170+
module.favorite = favoriteSet.contains(module.name);
171+
}
172+
LOG.info("Updated favorites: {} modules marked as favorite", favoriteNames.size());
173+
} catch (Exception e) {
174+
LOG.error("Failed to set favorites: {}", e.getMessage(), e);
175+
}
176+
}
177+
178+
/**
179+
* Create favorites state changed message
180+
*/
181+
public static JsonObject createFavoritesStateMessage() {
182+
JsonObject data = new JsonObject();
183+
data.add("favorites", getFavorites());
184+
return data;
185+
}
144186
}

src/main/java/com/cope/meteorwebgui/protocol/MessageType.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public enum MessageType {
55
INITIAL_STATE("initial.state"),
66
MODULE_STATE_CHANGED("module.state.changed"),
77
SETTING_VALUE_CHANGED("setting.value.changed"),
8+
FAVORITES_STATE_CHANGED("favorites.state.changed"),
89
REGISTRY_DATA("registry.data"),
910
HUD_PREVIEW_UPDATE("hud.preview.update"),
1011
HUD_STATE_CHANGED("hud.state.changed"),
@@ -16,6 +17,7 @@ public enum MessageType {
1617
MODULE_LIST("module.list"),
1718
SETTING_UPDATE("setting.update"),
1819
SETTING_GET("setting.get"),
20+
FAVORITES_UPDATE("favorites.update"),
1921
REGISTRY_REQUEST("registry.request"),
2022
HUD_TOGGLE("hud.toggle"),
2123
PING("ping"),

src/main/java/com/cope/meteorwebgui/server/MeteorWebServer.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,21 @@ public void broadcastHudSettingChange(HudElement element, Setting<?> setting) {
133133
}
134134
}
135135

136+
/**
137+
* Broadcast favorites state change to all connected clients.
138+
*/
139+
public void broadcastFavoritesChanged() {
140+
if (!running) return;
141+
try {
142+
JsonObject data = ModuleMapper.createFavoritesStateMessage();
143+
WSMessage message = new WSMessage(MessageType.FAVORITES_STATE_CHANGED, data);
144+
webSocketHandler.broadcast(GSON.toJson(message));
145+
LOG.debug("Broadcast favorites change: {} favorites", data.getAsJsonArray("favorites").size());
146+
} catch (Exception e) {
147+
LOG.error("Failed to broadcast favorites change: {}", e.getMessage(), e);
148+
}
149+
}
150+
136151
public void broadcast(String jsonPayload) {
137152
if (!running || webSocketHandler == null) return;
138153
try {

src/main/java/com/cope/meteorwebgui/server/MeteorWebSocket.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ protected void onOpen() {
4141
JsonObject initialData = new JsonObject();
4242
initialData.add("modules", ModuleMapper.mapAllModulesByCategory());
4343
initialData.add("hud", HudMapper.mapHudState());
44+
initialData.add("favorites", ModuleMapper.getFavorites());
4445

4546
// Registry data is now loaded on-demand via REGISTRY_REQUEST
4647
WSMessage message = new WSMessage(MessageType.INITIAL_STATE, initialData);
@@ -76,6 +77,7 @@ protected void onMessage(NanoWSD.WebSocketFrame frame) {
7677
case MODULE_LIST -> handleModuleList(wsMessage);
7778
case SETTING_UPDATE -> handleSettingUpdate(wsMessage);
7879
case SETTING_GET -> handleSettingGet(wsMessage);
80+
case FAVORITES_UPDATE -> handleFavoritesUpdate(wsMessage);
7981
case REGISTRY_REQUEST -> handleRegistryRequest(wsMessage);
8082
case HUD_TOGGLE -> handleHudToggle(wsMessage);
8183
case PING -> handlePing(wsMessage);
@@ -274,6 +276,32 @@ private void handleHudToggle(WSMessage message) {
274276
}
275277
}
276278

279+
private void handleFavoritesUpdate(WSMessage message) {
280+
try {
281+
JsonObject data = message.getData().getAsJsonObject();
282+
java.util.List<String> favorites = new java.util.ArrayList<>();
283+
284+
if (data.has("favorites") && data.get("favorites").isJsonArray()) {
285+
for (var element : data.getAsJsonArray("favorites")) {
286+
favorites.add(element.getAsString());
287+
}
288+
}
289+
290+
ModuleMapper.setFavorites(favorites);
291+
292+
JsonObject response = new JsonObject();
293+
response.addProperty("success", true);
294+
response.add("favorites", ModuleMapper.getFavorites());
295+
296+
send(GSON.toJson(new WSMessage("response", response, message.getId())));
297+
298+
LOG.info("Updated favorites from WebUI: {} modules", favorites.size());
299+
} catch (Exception e) {
300+
LOG.error("Failed to update favorites: {}", e.getMessage(), e);
301+
sendError("Failed to update favorites: " + e.getMessage(), message.getId());
302+
}
303+
}
304+
277305
private void handlePing(WSMessage message) {
278306
try {
279307
send(GSON.toJson(new WSMessage(MessageType.PONG, new JsonObject(), message.getId())));

webui/src/components/ModuleCard.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ function handleCardToggle() {
8383
}
8484
8585
function toggleFavorite() {
86-
modulesStore.toggleFavorite(props.module.name)
86+
const newFavorites = modulesStore.toggleFavorite(props.module.name)
87+
wsStore.sendFavoritesUpdate(newFavorites)
8788
}
8889
</script>
8990

webui/src/components/ModuleCardCompact.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ const toggling = ref(false)
4747
const isFavorite = computed(() => modulesStore.isFavorite(props.module.name))
4848
4949
function toggleFavorite() {
50-
modulesStore.toggleFavorite(props.module.name)
50+
const newFavorites = modulesStore.toggleFavorite(props.module.name)
51+
wsStore.sendFavoritesUpdate(newFavorites)
5152
}
5253
5354
function toggleModule() {

webui/src/components/ModuleSettingsDialog.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ function toggleModule() {
100100
101101
function toggleFavorite() {
102102
if (!props.module) return
103-
modulesStore.toggleFavorite(props.module.name)
103+
const newFavorites = modulesStore.toggleFavorite(props.module.name)
104+
wsStore.sendFavoritesUpdate(newFavorites)
104105
}
105106
106107
function onKeyDown(event: KeyboardEvent) {

webui/src/stores/modules.ts

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineStore } from 'pinia'
2-
import { ref, computed, watch } from 'vue'
2+
import { ref, computed } from 'vue'
33

44
export interface ModuleInfo {
55
name: string
@@ -32,34 +32,6 @@ export const useModulesStore = defineStore('modules', () => {
3232
const loading = ref(true)
3333
const error = ref<string | null>(null)
3434
const favorites = ref<string[]>([])
35-
const FAVORITES_KEY = 'meteor-client:favorites'
36-
37-
function hydrateFavorites() {
38-
if (typeof window === 'undefined') return
39-
try {
40-
const stored = window.localStorage.getItem(FAVORITES_KEY)
41-
if (!stored) return
42-
const parsed = JSON.parse(stored)
43-
if (Array.isArray(parsed)) {
44-
favorites.value = Array.from(
45-
new Set(parsed.filter((value): value is string => typeof value === 'string'))
46-
)
47-
}
48-
} catch (err) {
49-
console.warn('Failed to parse favorites from storage', err)
50-
}
51-
}
52-
53-
hydrateFavorites()
54-
55-
watch(
56-
favorites,
57-
(value) => {
58-
if (typeof window === 'undefined') return
59-
window.localStorage.setItem(FAVORITES_KEY, JSON.stringify(value))
60-
},
61-
{ deep: true }
62-
)
6335

6436
const activeModules = computed(() => {
6537
const active: string[] = []
@@ -120,14 +92,26 @@ export const useModulesStore = defineStore('modules', () => {
12092
}
12193
}
12294

123-
function toggleFavorite(moduleName: string) {
95+
/**
96+
* Set favorites from backend (Meteor is source of truth)
97+
*/
98+
function setFavorites(favoriteNames: string[]) {
99+
favorites.value = Array.from(new Set(favoriteNames))
100+
}
101+
102+
/**
103+
* Toggle a module's favorite status (optimistic update)
104+
* Returns the new favorites array for sending to backend
105+
*/
106+
function toggleFavorite(moduleName: string): string[] {
124107
const next = new Set(favorites.value)
125108
if (next.has(moduleName)) {
126109
next.delete(moduleName)
127110
} else {
128111
next.add(moduleName)
129112
}
130113
favorites.value = Array.from(next)
114+
return favorites.value
131115
}
132116

133117
function isFavorite(moduleName: string) {
@@ -153,6 +137,7 @@ export const useModulesStore = defineStore('modules', () => {
153137
setInitialState,
154138
updateModuleState,
155139
updateSettingValue,
140+
setFavorites,
156141
getModule,
157142
toggleFavorite,
158143
isFavorite

0 commit comments

Comments
 (0)