Skip to content

Commit 2c1a2b9

Browse files
committed
observer db value changes through event fix useStateProducer to call dispose function
1 parent d748f85 commit 2c1a2b9

File tree

6 files changed

+109
-67
lines changed

6 files changed

+109
-67
lines changed
Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
import { useEffect, useMemo, useRef } from "react"
1+
import { useLayoutEffect, useRef } from "react"
22
import { ReadImageFile } from "wailsjs/go/main/App"
33

44
type AsyncImageProps = React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>
55

66
export default function AsyncImage(props: AsyncImageProps) {
77

88
const ref = useRef<HTMLImageElement>(null)
9-
const url = useMemo(() => props.src?.trim() ?? "", [props.src])
109

11-
useEffect(() => {
12-
if (url.startsWith("file://") && ref.current) {
13-
ReadImageFile(url).then((base64) => {
14-
const dotIdx = url.lastIndexOf(".")
10+
useLayoutEffect(() => {
11+
const uri = ref?.current?.src
12+
if (!uri) return
13+
if (uri.startsWith("file://")) {
14+
ReadImageFile(uri).then((base64) => {
15+
const dotIdx = uri.lastIndexOf(".")
1516
if (ref.current) {
16-
ref.current.src = `data:image/${url.slice(dotIdx, url.length)};base64,${base64}`
17+
ref.current.src = `data:image/${uri.slice(dotIdx, uri.length)};base64,${base64}`
1718
}
1819
})
1920
}
20-
}, [url, ref])
21+
}, [props.src, ref])
2122

2223
return <img {...props} />
2324
}

app/frontend/src/data/database.ts

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,72 @@
1+
import { CancelFn } from "@/lib/tsutils";
2+
import { useEffect } from "react";
13
import { CreateCustomCharacter, CreatePlaylist, DeleteCharacter, DeleteModById, DeletePlaylistById, DeleteTextureById, InsertTag, InsertTagForAllModsByCharacterIds, RenameMod, RenameTexture, SelectCharactersByGame, SelectCharacterWithModsTagsAndTextures, SelectClosestCharacter, SelectModById, SelectModsByCharacterName, SelectModsByGbId, SelectPlaylistWithModsAndTags, UpdateDisableAllModsByGame, UpdateModEnabledById, UpdateModGbId, UpdateModImages, UpdateModsEnabledFromSlice, UpdatePlaylistName, UpdateTextureEnabledById } from "wailsjs/go/dbh/DbHelper";
24
import { SplitTexture } from "wailsjs/go/main/App";
5+
import { EventsEmit, EventsOn, LogDebug } from "wailsjs/runtime/runtime";
6+
7+
8+
type DBKey = "characters" | "mods" | "tags" | "playlist" | "all"
9+
10+
const DBEvent = "DB_EVENT"
11+
type DBEventData = DBKey[]
12+
13+
const subscribeToDbUpdates = (key: DBKey[] | DBKey, callback: () => void, runOnStart: boolean = false): CancelFn => {
14+
15+
if (runOnStart) {
16+
callback()
17+
}
18+
19+
return EventsOn(DBEvent, (keys: DBEventData) => {
20+
LogDebug("received DBEVENT event keys=" + keys)
21+
if (typeof key === 'string') {
22+
if (key === "all" || keys.includes(key)) {
23+
callback()
24+
}
25+
} else {
26+
if (key.any((k) => k === "all" || keys.includes(k))) {
27+
callback()
28+
}
29+
}
30+
})
31+
}
32+
33+
export const useDbUpdateListener = (key: DBKey[] | DBKey, callback: () => void) => {
34+
useEffect(() => {
35+
const cancel = subscribeToDbUpdates(key, callback)
36+
37+
return () => cancel()
38+
}, [key])
39+
40+
}
41+
42+
const broadcastCharacterUpdate = () => EventsEmit(DBEvent, ["characters"])
43+
const broadcastModsUpdate = () => EventsEmit(DBEvent, ["mods"])
44+
const broadcastTagsUpdate = () => EventsEmit(DBEvent, ["tags"])
45+
const broadcastPlaylistUpdate = () => EventsEmit(DBEvent, ["playlist"])
46+
//const broadcastMultiUpdate = (keys: DBKey[]) => EventsEmit(DBEvent, keys)
347

448
const DB = {
49+
onValueChangedListener: subscribeToDbUpdates,
550
deleteMod: async (id: number) => {
6-
return DeleteModById(id)
51+
return DeleteModById(id).then(broadcastCharacterUpdate)
752
},
853
enableMod: async (id: number, enabled: boolean) => {
9-
return UpdateModEnabledById(enabled, id)
54+
return UpdateModEnabledById(enabled, id).then(broadcastModsUpdate)
1055
},
1156
splitTexture: async (id: number) => {
12-
return SplitTexture(id)
57+
return SplitTexture(id).then(broadcastModsUpdate)
1358
},
1459
deleteTexture: async (id: number) => {
15-
return DeleteTextureById(id)
60+
return DeleteTextureById(id).then(broadcastModsUpdate)
1661
},
1762
enableTexture: async (id: number, enabled: boolean) => {
18-
return UpdateTextureEnabledById(id, enabled)
63+
return UpdateTextureEnabledById(id, enabled).then(broadcastModsUpdate)
1964
},
2065
disableAllMods: async (game: number) => {
21-
return UpdateDisableAllModsByGame(game)
66+
return UpdateDisableAllModsByGame(game).then(broadcastModsUpdate)
2267
},
2368
deleteCharacter: async (name: string, id: number, game: number) => {
24-
return DeleteCharacter(name, id, game)
69+
return DeleteCharacter(name, id, game).then(broadcastCharacterUpdate)
2570
},
2671
selectModById: async (id: number) => {
2772
return SelectModById(id)
@@ -39,25 +84,25 @@ const DB = {
3984
return SelectCharactersByGame(game)
4085
},
4186
updateModImages: async (id: number, images: string[]) => {
42-
return UpdateModImages(id, images)
87+
return UpdateModImages(id, images).then(broadcastModsUpdate)
4388
},
4489
updateModGbId: async (modId: number, gbId: number) => {
45-
return UpdateModGbId(modId, gbId)
90+
return UpdateModGbId(modId, gbId).then(broadcastModsUpdate)
4691
},
4792
renameMod: async (id: number, name: string) => {
48-
return RenameMod(id, name)
93+
return RenameMod(id, name).then(broadcastModsUpdate)
4994
},
5095
renameTexture: async (id: number, name: string) => {
51-
return RenameTexture(id, name)
96+
return RenameTexture(id, name).then(broadcastModsUpdate)
5297
},
5398
createCustomCharacter: async (name: string, image: string, element: string | undefined, game: number) => {
54-
return CreateCustomCharacter(name, image, element ?? "", game)
99+
return CreateCustomCharacter(name, image, element ?? "", game).then(broadcastCharacterUpdate)
55100
},
56101
insertTag: async (modId: number, name: string) => {
57-
return InsertTag(name, modId)
102+
return InsertTag(name, modId).then(broadcastTagsUpdate)
58103
},
59104
insertTagForAllModsByCharacterIds: async (characterIds: number[], tagName: string, game: number) => {
60-
return InsertTagForAllModsByCharacterIds(characterIds, tagName, game)
105+
return InsertTagForAllModsByCharacterIds(characterIds, tagName, game).then(broadcastTagsUpdate)
61106
},
62107
selectModsByGbId: async (gbId: number) => {
63108
return SelectModsByGbId(gbId)
@@ -66,16 +111,16 @@ const DB = {
66111
return SelectPlaylistWithModsAndTags(game)
67112
},
68113
createPlaylist: async (game: number, name: string) => {
69-
return CreatePlaylist(game, name)
114+
return CreatePlaylist(game, name).then(broadcastPlaylistUpdate)
70115
},
71116
enableMods: (ids: number[], game: number) => {
72-
return UpdateModsEnabledFromSlice(ids, game)
117+
return UpdateModsEnabledFromSlice(ids, game).then(broadcastModsUpdate)
73118
},
74119
deletePlaylistById: (id: number) => {
75-
return DeletePlaylistById(id)
120+
return DeletePlaylistById(id).then(broadcastPlaylistUpdate)
76121
},
77122
updatePlaylistName: (id: number, name: string) => {
78-
return UpdatePlaylistName(id, name)
123+
return UpdatePlaylistName(id, name).then(broadcastPlaylistUpdate)
79124
}
80125
}
81126

app/frontend/src/data/sync.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@ export async function syncCharacters(dataApi: DataApi, type: SyncType) {
1212
SyncHelper.Sync(await dataApi.game(), type)
1313
}
1414

15-
export const useSync = (dataApi: DataApi, onComplete: () => void) => {
15+
export const useSync = (dataApi: DataApi) => {
1616
const [syncing, setSyncing] = useState(false);
1717

1818

1919
const sync = (type: SyncType) => {
2020
setSyncing(true);
2121
syncCharacters(dataApi, type)
22-
.then(onComplete)
2322
.finally(() => setSyncing(false));
2423
};
2524

app/frontend/src/lib/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export function useStateProducer<T extends any>(
113113
LogError(e);
114114
}
115115

116-
() => {
116+
return () => {
117117
aborted = true
118118
disposeIfAborted()
119119
}

app/frontend/src/screens/GameScreen.tsx

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from "@/components/CharacterInfoCard";
2525
import { usePlaylistStore } from "@/state/playlistStore";
2626
import { SearchIcon, XIcon } from "lucide-react";
27-
import { EventsOn } from "wailsjs/runtime/runtime";
27+
import { EventsOn, LogDebug } from "wailsjs/runtime/runtime";
2828
import useCrossfadeNavigate from "@/hooks/useCrossfadeNavigate";
2929
import { useDialogStore } from "@/components/appdialog";
3030
import DB from "@/data/database";
@@ -45,7 +45,7 @@ const getElementPref = (game: number): GoPref<string[]> => {
4545
}
4646
};
4747

48-
const useMultiSelectState = (cwmt: types.CharacterWithModsAndTags[], refreshCharacters: () => void) => {
48+
const useMultiSelectState = (cwmt: types.CharacterWithModsAndTags[]) => {
4949

5050
const [multiSelect, setMultiSelect] = useState(false)
5151
const [selectedCardsUnfiltered, setSelectedCards] = useState<number[] | undefined>(undefined)
@@ -64,7 +64,7 @@ const useMultiSelectState = (cwmt: types.CharacterWithModsAndTags[], refreshChar
6464
c.characters.game
6565
)
6666
)
67-
).finally(refreshCharacters)
67+
)
6868
}
6969

7070
const multiSelectedCharacters = useMemo(() => {
@@ -175,14 +175,13 @@ interface FilterState {
175175
}
176176

177177
function GameScreen(props: { dataApi: DataApi; game: number }) {
178+
178179
const navigate = useCrossfadeNavigate();
179-
const [refreshTrigger, setRefreshTrigger] = useState(0);
180180
const updates = usePlaylistStore(useShallow((state) => state.updates));
181181

182182
const setDialog = useDialogStore(useShallow(s => s.setDialog))
183183

184184
const running = useDownloadStore(useShallow((state) => state.running));
185-
const refreshCharacters = () => setRefreshTrigger((prev) => prev + 1);
186185

187186
const elements = useStateProducer<string[]>(
188187
[],
@@ -195,18 +194,23 @@ function GameScreen(props: { dataApi: DataApi; game: number }) {
195194
const characters = useStateProducer<types.CharacterWithModsAndTags[]>(
196195
[],
197196
async (update, onDispose) => {
198-
199-
update(await props.dataApi.charactersWithModsAndTags());
197+
const unsubscribe = DB.onValueChangedListener(['characters', 'mods', 'tags'], () => {
198+
props.dataApi.charactersWithModsAndTags().then(update)
199+
}, true)
200200

201201
const cancel = EventsOn("sync", ({ game }) => {
202202
if (game === props.game) {
203203
props.dataApi.charactersWithModsAndTags().then(update)
204204
}
205205
})
206206

207-
onDispose(() => cancel())
207+
onDispose(() => {
208+
LogDebug("disposing character state producer")
209+
cancel()
210+
unsubscribe()
211+
})
208212
},
209-
[props.dataApi, running, updates, refreshTrigger]
213+
[props.dataApi, running, updates]
210214
);
211215

212216
const filterState = useFilterState(characters, props.game)
@@ -219,28 +223,28 @@ function GameScreen(props: { dataApi: DataApi; game: number }) {
219223
mutliSelectedIds,
220224
deleteCharacters,
221225
clearMultiSelected
222-
} = useMultiSelectState(characters, refreshCharacters)
226+
} = useMultiSelectState(characters)
223227

224228
const handleUnselectAll = () => {
225-
DB.disableAllMods(props.game).then(refreshCharacters)
229+
DB.disableAllMods(props.game)
226230
}
227231

228232
const handleEnableMod = (id: number, enabled: boolean) => {
229-
DB.enableMod(id, enabled).then(refreshCharacters)
233+
DB.enableMod(id, enabled)
230234
}
231235

232236
const handleDeleteMod = (id: number) => {
233-
DB.deleteMod(id).then(refreshCharacters)
237+
DB.deleteMod(id)
234238
}
235239

236240
const handleEnableTexture = (id: number, enabled: boolean) => {
237-
DB.enableTexture(id, enabled).then(refreshCharacters)
241+
DB.enableTexture(id, enabled)
238242
}
239243
const handleDeleteTexture = (id: number) => {
240-
DB.deleteTexture(id).then(refreshCharacters)
244+
DB.deleteTexture(id)
241245
}
242246
const handleSplitTexture = (id: number) => {
243-
DB.splitTexture(id).then(refreshCharacters)
247+
DB.splitTexture(id)
244248
}
245249

246250
return (
@@ -251,7 +255,7 @@ function GameScreen(props: { dataApi: DataApi; game: number }) {
251255
addTag={() => setDialog({
252256
type: "add_tag_multi",
253257
selectedChars: multiSelectedCharacters.map((it) => it.characters),
254-
refresh: refreshCharacters,
258+
refresh: () => { },
255259
game: props.game
256260
})}
257261
clearMultiSelected={clearMultiSelected}
@@ -263,7 +267,7 @@ function GameScreen(props: { dataApi: DataApi; game: number }) {
263267
<GameActionsTopBar
264268
unselectAll={handleUnselectAll}
265269
elements={elements}
266-
addCharacter={() => setDialog({ type: "add_character", game: props.game, elements: elements, refresh: refreshCharacters })}
270+
addCharacter={() => setDialog({ type: "add_character", game: props.game, elements: elements, refresh: () => { } })}
267271
importMod={() => navigate("/import", {
268272
state: { game: props.game }
269273
})}
@@ -273,7 +277,6 @@ function GameScreen(props: { dataApi: DataApi; game: number }) {
273277
</div>
274278
<FloatingActionButtons
275279
dataApi={props.dataApi}
276-
refreshCharacters={refreshCharacters}
277280
/>
278281
<div className="columns-1 sm:columns-2 lg:columns-3 gap-4 space-y-4 mb-16 mx-2">
279282
{filterState.filteredCharacters.map((c) => (
@@ -288,10 +291,10 @@ function GameScreen(props: { dataApi: DataApi; game: number }) {
288291
cmt={c}
289292
modDropdownMenu={(mwt) => (
290293
<ModActionsDropDown
291-
addTag={() => setDialog({ type: "add_tag", mod: mwt.mod, refresh: refreshCharacters })}
294+
addTag={() => setDialog({ type: "add_tag", mod: mwt.mod, refresh: () => { } })}
292295
onEnable={() => handleEnableMod(mwt.mod.id, !mwt.mod.enabled)}
293296
onDelete={() => handleDeleteMod(mwt.mod.id)}
294-
onRename={() => setDialog({ type: "rename_mod", id: mwt.mod.id, refresh: refreshCharacters })}
297+
onRename={() => setDialog({ type: "rename_mod", id: mwt.mod.id, refresh: () => { } })}
295298
onView={() => {
296299
if (mwt.mod.gbId !== 0) {
297300
navigate(`/mods/${mwt.mod.gbId}`);
@@ -305,7 +308,7 @@ function GameScreen(props: { dataApi: DataApi; game: number }) {
305308
onEnable={() => handleEnableTexture(t.id, !t.enabled)}
306309
onDelete={() => handleDeleteTexture(t.id)}
307310
onSplit={() => handleSplitTexture(t.id)}
308-
onRename={() => setDialog({ type: "rename_texture", id: t.id, refresh: refreshCharacters })}
311+
onRename={() => setDialog({ type: "rename_texture", id: t.id, refresh: () => { } })}
309312
onView={() => {
310313
if (t.gbId !== 0) {
311314
navigate(`/mods/${t.gbId}`);
@@ -395,11 +398,9 @@ function MultiSelectTopBar(
395398
}
396399

397400
function FloatingActionButtons({
398-
dataApi,
399-
refreshCharacters,
401+
dataApi
400402
}: {
401-
dataApi: DataApi;
402-
refreshCharacters: () => void;
403+
dataApi: DataApi
403404
}) {
404405

405406
const {
@@ -410,7 +411,7 @@ function FloatingActionButtons({
410411
const {
411412
syncing,
412413
sync,
413-
} = useSync(dataApi, refreshCharacters)
414+
} = useSync(dataApi)
414415

415416
return (
416417
<div className="fixed bottom-4 -translate-y-1 end-6 flex flex-row z-10">

0 commit comments

Comments
 (0)