radames commited on
Commit
423b87b
1 Parent(s): 795c159
Makefile CHANGED
@@ -4,8 +4,6 @@ build-dev:
4
  cd frontend && npm install && npm run build-dev && rm -rf ../static && cp -r build/ ../static/
5
  run-front-dev:
6
  cd frontend && npm install && npm run dev
7
- run-dev:
8
- rm -rf .data/ && FLASK_ENV=development python app.py
9
  run-prod:
10
  python app.py
11
  build-all: run-prod
 
4
  cd frontend && npm install && npm run build-dev && rm -rf ../static && cp -r build/ ../static/
5
  run-front-dev:
6
  cd frontend && npm install && npm run dev
 
 
7
  run-prod:
8
  python app.py
9
  build-all: run-prod
frontend/package.json CHANGED
@@ -42,6 +42,7 @@
42
  "@fontsource/fira-mono": "^4.5.0",
43
  "@liveblocks/client": "^0.18.2",
44
  "d3-selection": "^3.0.0",
45
- "d3-zoom": "^3.0.0"
 
46
  }
47
  }
 
42
  "@fontsource/fira-mono": "^4.5.0",
43
  "@liveblocks/client": "^0.18.2",
44
  "d3-selection": "^3.0.0",
45
+ "d3-zoom": "^3.0.0",
46
+ "nanoid": "^4.0.0"
47
  }
48
  }
frontend/src/lib/App.svelte CHANGED
@@ -8,29 +8,54 @@
8
  import { COLORS, EMOJIS } from '$lib/constants';
9
  import { PUBLIC_WS_INPAINTING } from '$env/static/public';
10
  import { onMount } from 'svelte';
 
11
  import {
12
  isLoading,
13
  loadingState,
14
  currZoomTransform,
15
- myPresence,
16
- others,
17
  isPrompting,
18
  clickedPosition,
19
- imagesList,
20
- showFrames,
21
- text2img
22
  } from '$lib/store';
 
 
 
23
  import { base64ToBlob, uploadImage } from '$lib/utils';
 
 
 
24
  /**
25
  * The main Liveblocks code for the example.
26
  * Check in src/routes/index.svelte to see the setup code.
27
  */
28
 
29
- export let room: Room;
 
30
 
31
- let canvasEl: HTMLCanvasElement;
 
 
 
 
 
 
 
 
 
32
 
33
- onMount(() => {});
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  async function onClose(e: CustomEvent) {
36
  $isPrompting = false;
@@ -182,13 +207,15 @@
182
  }
183
  const imgBlob = await base64ToBlob(imgBase64);
184
  const imgURL = await uploadImage(imgBlob, _prompt);
185
-
186
- $imagesList.push({
187
  prompt: _prompt,
188
  imgURL: imgURL,
189
  position: $clickedPosition,
190
- date: new Date().getTime()
191
- });
 
 
 
192
  console.log(imgURL);
193
  $loadingState = data.success ? 'Complete' : 'Error';
194
  } catch (err) {
@@ -224,18 +251,19 @@
224
  <Canvas bind:value={canvasEl} />
225
 
226
  <main class="z-10 relative">
227
- {#if $imagesList && $showFrames}
228
- {#each $imagesList as image, i}
229
  <Frame
230
  color={COLORS[0]}
231
- position={$imagesList.get(i).position}
232
  transform={$currZoomTransform}
 
 
233
  />
234
  {/each}
235
  {/if}
236
- {#if $clickedPosition}
237
  <Frame color={COLORS[0]} position={$clickedPosition} transform={$currZoomTransform} />
238
- {/if}
239
  {#if $myPresence?.cursor}
240
  <Frame color={COLORS[0]} position={$myPresence.cursor} transform={$currZoomTransform} />
241
  <Cursor
@@ -247,19 +275,20 @@
247
  {/if}
248
 
249
  <!-- When others connected, iterate through others and show their cursors -->
250
- {#if others}
251
  {#each [...$others] as { connectionId, presence } (connectionId)}
252
  {#if presence?.cursor}
253
  <Frame
254
  color={COLORS[1 + (connectionId % (COLORS.length - 1))]}
255
- position={presence.cursor}
 
256
  transform={$currZoomTransform}
257
  />
258
 
259
  <Cursor
260
  emoji={EMOJIS[1 + (connectionId % (EMOJIS.length - 1))]}
261
  color={COLORS[1 + (connectionId % (COLORS.length - 1))]}
262
- position={presence.cursor}
263
  transform={$currZoomTransform}
264
  />
265
  {/if}
 
8
  import { COLORS, EMOJIS } from '$lib/constants';
9
  import { PUBLIC_WS_INPAINTING } from '$env/static/public';
10
  import { onMount } from 'svelte';
11
+ import type { PromptImgObject, PromptImgKey } from '$lib/types';
12
  import {
13
  isLoading,
14
  loadingState,
15
  currZoomTransform,
 
 
16
  isPrompting,
17
  clickedPosition,
18
+ showFrames
 
 
19
  } from '$lib/store';
20
+
21
+ import { useMyPresence, useObject, useOthers } from '$lib/liveblocks';
22
+
23
  import { base64ToBlob, uploadImage } from '$lib/utils';
24
+
25
+ import { nanoid } from 'nanoid';
26
+
27
  /**
28
  * The main Liveblocks code for the example.
29
  * Check in src/routes/index.svelte to see the setup code.
30
  */
31
 
32
+ const myPresence = useMyPresence();
33
+ const others = useOthers();
34
 
35
+ // Set a default value for presence
36
+ myPresence.update({
37
+ name: '',
38
+ cursor: null,
39
+ isPrompting: false,
40
+ currentPrompt: ''
41
+ });
42
+ function getKey({ position }: PromptImgObject): PromptImgKey {
43
+ return `${position.x}_${position.y}`;
44
+ }
45
 
46
+ const promptImgStorage = useObject('promptImgStorage');
47
+
48
+ function getpromptImgList(promptImgList: PromptImgObject[]): PromptImgObject[] {
49
+ if (promptImgList) {
50
+ const list: PromptImgObject[] = Object.values(promptImgList);
51
+ return list.sort((a, b) => a.date - b.date);
52
+ }
53
+ return [];
54
+ }
55
+ let promptImgList: PromptImgObject[] = [];
56
+ $: promptImgList = getpromptImgList($promptImgStorage?.toObject());
57
+
58
+ let canvasEl: HTMLCanvasElement;
59
 
60
  async function onClose(e: CustomEvent) {
61
  $isPrompting = false;
 
207
  }
208
  const imgBlob = await base64ToBlob(imgBase64);
209
  const imgURL = await uploadImage(imgBlob, _prompt);
210
+ const promptImg = {
 
211
  prompt: _prompt,
212
  imgURL: imgURL,
213
  position: $clickedPosition,
214
+ date: new Date().getTime(),
215
+ id: nanoid()
216
+ };
217
+ const key = getKey(promptImg);
218
+ $promptImgStorage.set(key, promptImg);
219
  console.log(imgURL);
220
  $loadingState = data.success ? 'Complete' : 'Error';
221
  } catch (err) {
 
251
  <Canvas bind:value={canvasEl} />
252
 
253
  <main class="z-10 relative">
254
+ {#if promptImgList && $showFrames}
255
+ {#each promptImgList as promptImg, i}
256
  <Frame
257
  color={COLORS[0]}
 
258
  transform={$currZoomTransform}
259
+ position={promptImg?.position}
260
+ prompt={promptImg?.prompt}
261
  />
262
  {/each}
263
  {/if}
264
+ <!-- {#if $clickedPosition}
265
  <Frame color={COLORS[0]} position={$clickedPosition} transform={$currZoomTransform} />
266
+ {/if} -->
267
  {#if $myPresence?.cursor}
268
  <Frame color={COLORS[0]} position={$myPresence.cursor} transform={$currZoomTransform} />
269
  <Cursor
 
275
  {/if}
276
 
277
  <!-- When others connected, iterate through others and show their cursors -->
278
+ {#if $others}
279
  {#each [...$others] as { connectionId, presence } (connectionId)}
280
  {#if presence?.cursor}
281
  <Frame
282
  color={COLORS[1 + (connectionId % (COLORS.length - 1))]}
283
+ position={presence?.cursor}
284
+ prompt={presence?.currentPrompt}
285
  transform={$currZoomTransform}
286
  />
287
 
288
  <Cursor
289
  emoji={EMOJIS[1 + (connectionId % (EMOJIS.length - 1))]}
290
  color={COLORS[1 + (connectionId % (COLORS.length - 1))]}
291
+ position={presence?.cursor}
292
  transform={$currZoomTransform}
293
  />
294
  {/if}
frontend/src/lib/Canvas.svelte CHANGED
@@ -3,13 +3,13 @@
3
  import { select } from 'd3-selection';
4
  import { onMount } from 'svelte';
5
  import { PUBLIC_UPLOADS } from '$env/static/public';
6
- import {
7
- currZoomTransform,
8
- myPresence,
9
- isPrompting,
10
- clickedPosition,
11
- imagesList
12
- } from '$lib/store';
13
 
14
  const height = 512 * 5;
15
  const width = 512 * 5;
@@ -21,8 +21,21 @@
21
  let containerEl: HTMLDivElement;
22
  let canvasCtx: CanvasRenderingContext2D;
23
 
24
- $: if ($imagesList) {
25
- renderImages($imagesList);
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
 
28
  onMount(() => {
@@ -58,23 +71,29 @@
58
  canvasCtx.strokeRect(0, 0, width, height);
59
  });
60
 
61
- function renderImages(imagesList) {
62
- const images = [...imagesList.toImmutable()].sort((a, b) => a.date - b.date);
63
  Promise.all(
64
- images.map(
65
- ({ imgURL, position }) =>
66
  new Promise((resolve) => {
67
  const img = new Image();
68
  img.crossOrigin = 'anonymous';
69
  img.onload = () => {
70
- resolve({ img, position });
 
 
 
 
 
71
  };
72
  const url = imgURL.split('/');
73
  img.src = `${PUBLIC_UPLOADS}/${url.slice(3).join('/')}`;
74
  })
75
  )
76
  ).then((images) => {
77
- images.forEach(({ img, position }) => {
 
 
78
  canvasCtx.drawImage(img, position.x, position.y, img.width, img.height);
79
  });
80
  });
@@ -99,19 +118,19 @@
99
  // const y = Math.round(event.layerY / grid) * grid; //round(Math.max(r, Math.min(512 * 5 - r, event.clientY)), 100);
100
  // const x = round(Math.max(r, Math.min(512 * 5 - r, event.clientX)), grid);
101
  // const y = round(Math.max(r, Math.min(512 * 5 - r, event.clientY)), grid);
102
- $myPresence = {
103
  cursor: {
104
  x,
105
  y
106
  }
107
- };
108
  }
109
 
110
  // When the pointer leaves the page, set cursor presence to null
111
  function handlePointerLeave() {
112
- $myPresence = {
113
  cursor: null
114
- };
115
  }
116
  </script>
117
 
 
3
  import { select } from 'd3-selection';
4
  import { onMount } from 'svelte';
5
  import { PUBLIC_UPLOADS } from '$env/static/public';
6
+ import { currZoomTransform, isPrompting, clickedPosition } from '$lib/store';
7
+
8
+ import { useMyPresence, useObject } from '$lib/liveblocks';
9
+ import type { PromptImgObject } from '$lib/types';
10
+
11
+ const myPresence = useMyPresence();
12
+ const promptImgStorage = useObject('promptImgStorage');
13
 
14
  const height = 512 * 5;
15
  const width = 512 * 5;
 
21
  let containerEl: HTMLDivElement;
22
  let canvasCtx: CanvasRenderingContext2D;
23
 
24
+ // keep track of images already rendered
25
+ const imagesOnCanvas = new Set();
26
+
27
+ function getpromptImgList(promptImgList: PromptImgObject[]): PromptImgObject[] {
28
+ if (promptImgList) {
29
+ const list: PromptImgObject[] = Object.values(promptImgList).sort((a, b) => a.date - b.date);
30
+ return list.filter(({ id }) => !imagesOnCanvas.has(id));
31
+ }
32
+ return [];
33
+ }
34
+ let promptImgList: PromptImgObject[] = [];
35
+ $: promptImgList = getpromptImgList($promptImgStorage?.toObject());
36
+
37
+ $: if (promptImgList) {
38
+ renderImages(promptImgList);
39
  }
40
 
41
  onMount(() => {
 
71
  canvasCtx.strokeRect(0, 0, width, height);
72
  });
73
 
74
+ function renderImages(promptImgList: PromptImgObject[]) {
 
75
  Promise.all(
76
+ promptImgList.map(
77
+ ({ imgURL, position, id }) =>
78
  new Promise((resolve) => {
79
  const img = new Image();
80
  img.crossOrigin = 'anonymous';
81
  img.onload = () => {
82
+ const res = { img, position, id } as {
83
+ img: HTMLImageElement;
84
+ position: { x: number; y: number };
85
+ id: string;
86
+ };
87
+ resolve(res);
88
  };
89
  const url = imgURL.split('/');
90
  img.src = `${PUBLIC_UPLOADS}/${url.slice(3).join('/')}`;
91
  })
92
  )
93
  ).then((images) => {
94
+ images.forEach(({ img, position, id }) => {
95
+ // keep track of images already rendered
96
+ imagesOnCanvas.add(id);
97
  canvasCtx.drawImage(img, position.x, position.y, img.width, img.height);
98
  });
99
  });
 
118
  // const y = Math.round(event.layerY / grid) * grid; //round(Math.max(r, Math.min(512 * 5 - r, event.clientY)), 100);
119
  // const x = round(Math.max(r, Math.min(512 * 5 - r, event.clientX)), grid);
120
  // const y = round(Math.max(r, Math.min(512 * 5 - r, event.clientY)), grid);
121
+ myPresence.update({
122
  cursor: {
123
  x,
124
  y
125
  }
126
+ });
127
  }
128
 
129
  // When the pointer leaves the page, set cursor presence to null
130
  function handlePointerLeave() {
131
+ myPresence.update({
132
  cursor: null
133
+ });
134
  }
135
  </script>
136
 
frontend/src/lib/Frame.svelte CHANGED
@@ -6,6 +6,7 @@
6
  export let transform: ZoomTransform;
7
  export let color = '';
8
  export let position = { x: 0, y: 0 };
 
9
 
10
  $: coord = {
11
  x: transform.applyX(position.x),
@@ -24,7 +25,7 @@
24
  <LoadingIcon />
25
  <h2 class="text-lg">Click to paint</h2>
26
 
27
- <div class="absolute bottom-0 font-bold">A cat on grass</div>
28
  </div>
29
 
30
  <style lang="postcss" scoped>
 
6
  export let transform: ZoomTransform;
7
  export let color = '';
8
  export let position = { x: 0, y: 0 };
9
+ export let prompt = '';
10
 
11
  $: coord = {
12
  x: transform.applyX(position.x),
 
25
  <LoadingIcon />
26
  <h2 class="text-lg">Click to paint</h2>
27
 
28
+ <div class="absolute bottom-0 font-bold">{prompt}}</div>
29
  </div>
30
 
31
  <style lang="postcss" scoped>
frontend/src/lib/liveblocks/LiveblocksProvider.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--
2
+ Works similarly to `liveblocks-react` LiveblocksProvider
3
+ https://liveblocks.io/docs/api-reference/liveblocks-react#RoomProvider
4
+ -->
5
+ <script lang="ts">
6
+ import { clientSymbol } from "./symbols";
7
+ import type { Client } from "@liveblocks/client";
8
+ import { setContext } from "svelte";
9
+
10
+ export let client: Client;
11
+
12
+ if (!client) {
13
+ throw new Error("LiveblocksProvider requires a client");
14
+ }
15
+
16
+ setContext<Client>(clientSymbol, client);
17
+ </script>
18
+
19
+ <slot />
frontend/src/lib/liveblocks/RoomProvider.svelte ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--
2
+ Works similarly to `liveblocks-react` RoomProvider
3
+ https://liveblocks.io/docs/api-reference/liveblocks-react#RoomProvider
4
+ -->
5
+ <script lang="ts">
6
+ import { clientSymbol, roomSymbol } from "./symbols";
7
+ import type { Client, Room } from "@liveblocks/client";
8
+ import { getContext, onDestroy, setContext } from "svelte";
9
+
10
+ export let id: string;
11
+ export let defaultPresence = () => ({});
12
+
13
+ if (!id) {
14
+ throw new Error("RoomProvider requires an id");
15
+ }
16
+
17
+ const client = getContext<Client>(clientSymbol);
18
+
19
+ if (client) {
20
+ const room = client.enter(id, defaultPresence());
21
+
22
+ setContext<Room>(roomSymbol, room);
23
+
24
+ onDestroy(() => {
25
+ client.leave(id);
26
+ });
27
+ }
28
+ </script>
29
+
30
+ <slot />
frontend/src/lib/liveblocks/index.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export * from "./symbols";
2
+ export * from "./useUpdateMyPresence";
3
+ export * from "./useMyPresence";
4
+ export * from "./useOthers";
5
+ export * from "./useObject";
6
+ export * from "./useList";
7
+ export * from "./useSelf";
8
+ export * from "./useRoom";
9
+ export * from "./useUndo";
10
+ export * from "./useRedo";
11
+ export * from "./useBatch";
12
+ export * from "./useHistory";
13
+
14
+ /**
15
+ * These components were built to (mostly) match the
16
+ * liveblocks-react library
17
+ * https://liveblocks.io/docs/api-reference/liveblocks-react
18
+ */
frontend/src/lib/liveblocks/symbols.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ // Symbols are used for context to avoid polluting the global scope
2
+ export const clientSymbol = Symbol();
3
+ export const roomSymbol = Symbol();
frontend/src/lib/liveblocks/useBatch.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRoom } from "./useRoom";
2
+
3
+ /**
4
+ * Works similarly to `liveblocks-react` useBatch
5
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useBatch
6
+ *
7
+ * const batch = useBatch()
8
+ * batch(() => {
9
+ * // ...
10
+ * })
11
+ */
12
+ export function useBatch() {
13
+ return useRoom().batch;
14
+ }
frontend/src/lib/liveblocks/useHistory.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Works similarly to `liveblocks-react` useHistory
3
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useHistory
4
+ *
5
+ * const history = useHistory()
6
+ * history.pause()
7
+ */
8
+ import { useRoom } from "./useRoom";
9
+
10
+ export function useHistory() {
11
+ return useRoom().history;
12
+ }
frontend/src/lib/liveblocks/useList.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { LiveList } from "@liveblocks/client";
2
+ import { useStorage } from "./useStorage";
3
+ import { onDestroy } from "svelte";
4
+ import type { Writable } from "svelte/store";
5
+ import { writable } from "svelte/store";
6
+ import { useRoom } from "./useRoom";
7
+
8
+ /**
9
+ * Works similarly to `liveblocks-react` useList
10
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useList
11
+ *
12
+ * The main difference is that it returns a Svelte store:
13
+ * const list = useList()
14
+ * $list.push([{ item: 1 }])
15
+ * console.log([...$list])
16
+ */
17
+ export function useList<T>(
18
+ name: string,
19
+ initial?: any[]
20
+ ): Writable<LiveList<T>> {
21
+ const room = useRoom();
22
+ const rootStore = useStorage();
23
+ const list = writable<LiveList<T>>();
24
+ let unsubscribe = () => {};
25
+
26
+ const unsubscribeRoot = rootStore.subscribe((root) => {
27
+ if (!root) {
28
+ return;
29
+ }
30
+
31
+ if (!root.get(name)) {
32
+ root.set(name, new LiveList<T>(initial));
33
+ }
34
+
35
+ list.set(root.get(name));
36
+
37
+ unsubscribe();
38
+ unsubscribe = room.subscribe(root.get(name) as LiveList<T>, (newList) => {
39
+ list.set(newList);
40
+ });
41
+ });
42
+
43
+ onDestroy(unsubscribeRoot);
44
+
45
+ return list;
46
+ }
frontend/src/lib/liveblocks/useMyPresence.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Presence } from "@liveblocks/client";
2
+ import { onDestroy } from "svelte";
3
+ import { writable } from "svelte/store";
4
+ import { useRoom } from "./useRoom";
5
+
6
+ /**
7
+ * Works similarly to `liveblocks-react` useMyPresence
8
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useMyPresence
9
+ *
10
+ * The main difference is that it returns a custom Svelte store:
11
+ * const presence = useMyPresence()
12
+ * presence.update({ name: 'Chris })
13
+ * console.log($presence.name)
14
+ * <div>{$presence.count}</div>
15
+ *
16
+ * USAGE NOTE:
17
+ * This is a custom Svelte store, `set` does nothing, only `update`.
18
+ * `update` does NOT take a function like regular Svelte stores,
19
+ * it takes an object and works like `useUpdateMyPresence` in Liveblocks
20
+ */
21
+
22
+ export function useMyPresence(): any {
23
+ const room = useRoom();
24
+ const { subscribe, set } = writable<Presence>();
25
+
26
+ function update(newPresence) {
27
+ room.updatePresence(newPresence);
28
+ }
29
+
30
+ const unsubscribePresence = room.subscribe("my-presence", (presence) => {
31
+ set(presence);
32
+ });
33
+
34
+ onDestroy(() => {
35
+ unsubscribePresence();
36
+ });
37
+
38
+ return {
39
+ subscribe,
40
+ update,
41
+ };
42
+ }
frontend/src/lib/liveblocks/useObject.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { LiveObject } from "@liveblocks/client";
2
+ import { useStorage } from "./useStorage";
3
+ import { onDestroy } from "svelte";
4
+ import type { Writable } from "svelte/store";
5
+ import { writable } from "svelte/store";
6
+ import { useRoom } from "./useRoom";
7
+
8
+ /**
9
+ * Works similarly to `liveblocks-react` useObject
10
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useObject
11
+ *
12
+ * The main difference is that it returns a Svelte store:
13
+ * const obj = useObject()
14
+ * $obj.set('name', 'Chris')
15
+ * console.log($obj.get('name'))
16
+ */
17
+ export function useObject(name: string, initial?: any): Writable<LiveObject> {
18
+ const room = useRoom();
19
+ const rootStore = useStorage();
20
+ const list = writable<LiveObject>();
21
+ let unsubscribe = () => {};
22
+
23
+ const unsubscribeRoot = rootStore.subscribe((root) => {
24
+ if (!root) {
25
+ return;
26
+ }
27
+
28
+ if (!root.get(name)) {
29
+ root.set(name, new LiveObject(initial));
30
+ }
31
+
32
+ list.set(root.get(name));
33
+
34
+ unsubscribe();
35
+ unsubscribe = room.subscribe(root.get(name) as LiveObject, (newObject) => {
36
+ list.set(newObject);
37
+ });
38
+ });
39
+
40
+ onDestroy(unsubscribeRoot);
41
+
42
+ return list;
43
+ }
frontend/src/lib/liveblocks/useOthers.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Others } from "@liveblocks/client";
2
+ import { onDestroy } from "svelte";
3
+ import type { Writable } from "svelte/store";
4
+ import { writable } from "svelte/store";
5
+ import { useRoom } from "./useRoom";
6
+
7
+ /**
8
+ * Works similarly to `liveblocks-react` useOthers
9
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useOthers
10
+ *
11
+ * The main difference is that it returns a Svelte store:
12
+ * const others = useOthers()
13
+ * console.log($others.value)
14
+ * {#each [...$others] as other}
15
+ * ...
16
+ */
17
+ export function useOthers(): Writable<Others> {
18
+ const room = useRoom();
19
+ const others = writable<Others>();
20
+
21
+ const unsubscribe = room.subscribe("others", (newOthers) => {
22
+ others.set(newOthers);
23
+ });
24
+
25
+ onDestroy(unsubscribe);
26
+
27
+ return others;
28
+ }
frontend/src/lib/liveblocks/useRedo.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Works similarly to `liveblocks-react` useRedo
3
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useRedo
4
+ *
5
+ * const redo = useRedo()
6
+ * redo()
7
+ */
8
+ import { useRoom } from "./useRoom";
9
+
10
+ export function useRedo() {
11
+ return useRoom().history.redo;
12
+ }
frontend/src/lib/liveblocks/useRoom.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getContext } from "svelte";
2
+ import type { Room } from "@liveblocks/client";
3
+ import { roomSymbol } from "./symbols";
4
+
5
+ /**
6
+ * Works similarly to `liveblocks-react` useRoom
7
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useRoom
8
+ *
9
+ * This does NOT return a Svelte store, just the plain room object
10
+ * const room = useRoom()
11
+ * room.history.undo()
12
+ */
13
+ export function useRoom(): Room {
14
+ const room = getContext<Room>(roomSymbol);
15
+
16
+ if (!room) {
17
+ throw new Error("Use RoomProvider as parent with id prop");
18
+ }
19
+
20
+ return room;
21
+ }
frontend/src/lib/liveblocks/useSelf.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { onDestroy } from "svelte";
2
+ import type { Writable } from "svelte/store";
3
+ import { writable } from "svelte/store";
4
+ import { useRoom } from "./useRoom";
5
+
6
+ /**
7
+ * Works similarly to `liveblocks-react` useSelf
8
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useSelf
9
+ *
10
+ * The main difference is that it returns a Svelte store:
11
+ * const self = useSelf()
12
+ * console.log($self.info.id)
13
+ * <div>{$self.info.name}</div>
14
+ */
15
+ export function useSelf(): Writable<any> {
16
+ const room = useRoom();
17
+ const self = writable();
18
+
19
+ const unsubscribeConnection = room.subscribe("connection", () => {
20
+ self.set(room.getSelf());
21
+ });
22
+ const unsubscribe = room.subscribe("my-presence", () => {
23
+ self.set(room.getSelf());
24
+ });
25
+
26
+ onDestroy(() => {
27
+ unsubscribeConnection();
28
+ unsubscribe();
29
+ });
30
+
31
+ return self;
32
+ }
frontend/src/lib/liveblocks/useStorage.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { LiveObject } from "@liveblocks/client";
2
+ import type { Writable } from "svelte/store";
3
+ import { writable } from "svelte/store";
4
+ import { useRoom } from "./useRoom";
5
+
6
+ /**
7
+ * No `liveblocks-react` public API equivalent, but useStorage is used internally
8
+ */
9
+ export function useStorage(): Writable<LiveObject> {
10
+ const room = useRoom();
11
+ const rootStore = writable<LiveObject>();
12
+
13
+ async function fetchStorage() {
14
+ const { root }: { root: LiveObject } = await room!.getStorage();
15
+ rootStore.set(root);
16
+ }
17
+
18
+ fetchStorage();
19
+
20
+ return rootStore;
21
+ }
frontend/src/lib/liveblocks/useUndo.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Works similarly to `liveblocks-react` useUndo
3
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useUndo
4
+ *
5
+ * const undo = useUndo()
6
+ * undo()
7
+ */
8
+ import { useRoom } from "./useRoom";
9
+
10
+ export function useUndo() {
11
+ return useRoom().history.undo;
12
+ }
frontend/src/lib/liveblocks/useUpdateMyPresence.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMyPresence } from "./useMyPresence";
2
+
3
+ /**
4
+ * Works similarly to `liveblocks-react` useUpdateMyPresence
5
+ * https://liveblocks.io/docs/api-reference/liveblocks-react#useUpdateMyPresence
6
+ *
7
+ * const updateMyPresence = useUpdateMyPresence()
8
+ * updateMyPresence({ name: 'Chris' })
9
+ *
10
+ *
11
+ * Can also import useMyPresence instead and use .update() instead:
12
+ *
13
+ * const myPresence = useMyPresence()
14
+ * myPresence.update({ name: 'Chris' })
15
+ */
16
+
17
+ export function useUpdateMyPresence(): (val: any) => void {
18
+ const presence = useMyPresence();
19
+ return (updatedPresence) => presence.update(updatedPresence);
20
+ }
frontend/src/lib/store.ts CHANGED
@@ -3,6 +3,8 @@ import type { Room } from '@liveblocks/client';
3
 
4
  import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
5
 
 
 
6
  export const loadingState = writable<string>('');
7
  export const isLoading = writable<boolean>(false);
8
  export const isPrompting = writable<boolean>(false);
@@ -12,45 +14,3 @@ export const text2img = writable<boolean>(false);
12
 
13
  export const currZoomTransform = writable<ZoomTransform>(zoomIdentity);
14
 
15
- export const myPresence = writable(null);
16
- export const others = writable(null);
17
- export const imagesList = writable(null);
18
-
19
- export function createPresenceStore(room: Room) {
20
- // Get initial values for presence and others
21
- myPresence.set(room.getPresence());
22
- others.set(room.getOthers());
23
-
24
- const unsubscribeMyPresence = room.subscribe('my-presence', (presence) => {
25
- myPresence.update((_) => presence);
26
- });
27
-
28
- const unsubscribeOthers = room.subscribe('others', (otherUsers) => {
29
- others.update((_) => otherUsers);
30
- });
31
-
32
- myPresence.set = (presence) => {
33
- room.updatePresence(presence);
34
- return presence;
35
- };
36
-
37
- return () => {
38
- unsubscribeMyPresence();
39
- unsubscribeOthers();
40
- };
41
- }
42
-
43
- export async function createStorageStore(room: Room) {
44
- try {
45
- const { root } = await room.getStorage();
46
-
47
- const _imagesList = root.get('imagesList');
48
-
49
- imagesList.set(_imagesList);
50
- room.subscribe(_imagesList, () => {
51
- imagesList.update((_) => _imagesList);
52
- });
53
- } catch (e) {
54
- console.log(e);
55
- }
56
- }
 
3
 
4
  import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
5
 
6
+ import type {Person } from "$lib/types"
7
+
8
  export const loadingState = writable<string>('');
9
  export const isLoading = writable<boolean>(false);
10
  export const isPrompting = writable<boolean>(false);
 
14
 
15
  export const currZoomTransform = writable<ZoomTransform>(zoomIdentity);
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/lib/types.ts CHANGED
@@ -1,4 +1,7 @@
1
- export type Presence = {
 
 
 
2
  cursor: {
3
  x: number;
4
  y: number;
@@ -10,4 +13,17 @@ export type Storage = {
10
  // ...
11
  };
12
 
13
- export type User = string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { JsonObject } from "@liveblocks/client";
2
+
3
+
4
+ export interface Presence extends JsonObject {
5
  cursor: {
6
  x: number;
7
  y: number;
 
13
  // ...
14
  };
15
 
16
+ export type User = string;
17
+
18
+ export type PromptImgObject = {
19
+ prompt: string;
20
+ imgURL: string;
21
+ position: {
22
+ x: number;
23
+ y: number;
24
+ }
25
+ date: number;
26
+ id: string;
27
+ };
28
+
29
+ export type PromptImgKey = string;
frontend/src/routes/+page.svelte CHANGED
@@ -1,47 +1,46 @@
 
 
 
 
 
 
 
 
 
 
1
  <script lang="ts">
2
  import { onMount } from 'svelte';
3
- import { isLoading, loadingState, createPresenceStore, createStorageStore } from '$lib/store';
4
- import type { Client, Room } from '@liveblocks/client';
5
- import { createClient, LiveList } from '@liveblocks/client';
6
-
7
  import App from '$lib/App.svelte';
8
- import type { Presence, Storage } from '$lib/types';
9
 
 
 
10
  let client: Client;
11
- let room: Room;
12
- let roomId = 'multiplayer-SD';
13
 
14
  onMount(() => {
 
 
 
 
 
15
  client = createClient({
16
  publicApiKey: 'pk_test_JlUZGH3kQmhmZQiqU2l8eIi5'
17
  });
18
 
19
- room = client.enter<Presence, Storage /* UserMeta, RoomEvent */>(roomId, {
20
- initialPresence: {
21
- cursor: null
22
- },
23
- initialStorage: { imagesList: new LiveList() }
24
- });
25
- const unsubscribe = room.subscribe('error', (error) => {
26
- console.error('error', error);
27
- });
28
-
29
- const unsubscribePresence = createPresenceStore(room);
30
- createStorageStore(room);
31
- return () => {
32
- if (client && room) {
33
- client.leave(roomId);
34
- unsubscribePresence();
35
- }
36
- };
37
  });
38
  </script>
39
 
40
- <div class="max-w-screen-md mx-auto p-5 relative pointer-events-none touch-none z-10">
41
- <h1 class="text-lg md:text-3xl font-bold leading-normal">
42
- Stable Diffussion Outpainting Multiplayer
43
- </h1>
44
- </div>
45
- {#if room}
46
- <App {room} />
 
 
47
  {/if}
 
1
+ <script context="module" lang="ts">
2
+ export const prerender = true;
3
+ </script>
4
+
5
+ <!--
6
+ The main code for this component is in src/PixelArtTogether.svelte
7
+ This file contains the Liveblocks providers, based on the
8
+ liveblocks-react library
9
+ https://liveblocks.io/docs/api-reference/liveblocks-react#LiveblocksProvider
10
+ -->
11
  <script lang="ts">
12
  import { onMount } from 'svelte';
13
+ import { createClient } from '@liveblocks/client';
14
+ import type { Client } from '@liveblocks/client';
15
+ import LiveblocksProvider from '$lib/liveblocks/LiveblocksProvider.svelte';
16
+ import RoomProvider from '$lib/liveblocks/RoomProvider.svelte';
17
  import App from '$lib/App.svelte';
 
18
 
19
+ let roomId: string;
20
+ let loaded = false;
21
  let client: Client;
 
 
22
 
23
  onMount(() => {
24
+ // Add random id to room param if not set, and return the id string
25
+ // e.g. /?room=758df70b5e94c13289df6
26
+ roomId = 'multiplayer-SD';
27
+
28
+ // Connect to the authentication API for Liveblocks
29
  client = createClient({
30
  publicApiKey: 'pk_test_JlUZGH3kQmhmZQiqU2l8eIi5'
31
  });
32
 
33
+ loaded = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  });
35
  </script>
36
 
37
+ {#if loaded}
38
+ <!-- Provides Liveblocks hooks to children -->
39
+ <LiveblocksProvider {client}>
40
+ <!-- Create a room from id e.g. `sveltekit-pixel-art-758df70b5e94c13289df6` -->
41
+ <RoomProvider id={roomId}>
42
+ <!-- Main app component -->
43
+ <App />
44
+ </RoomProvider>
45
+ </LiveblocksProvider>
46
  {/if}