Spaces:
				
			
			
	
			
			
		Runtime error
		
	
	
	
			
			
	
	
	
	
		
		
		Runtime error
		
	use hooks
Browse files- Makefile +0 -2
- frontend/package.json +2 -1
- frontend/src/lib/App.svelte +49 -20
- frontend/src/lib/Canvas.svelte +38 -19
- frontend/src/lib/Frame.svelte +2 -1
- frontend/src/lib/liveblocks/LiveblocksProvider.svelte +19 -0
- frontend/src/lib/liveblocks/RoomProvider.svelte +30 -0
- frontend/src/lib/liveblocks/index.ts +18 -0
- frontend/src/lib/liveblocks/symbols.ts +3 -0
- frontend/src/lib/liveblocks/useBatch.ts +14 -0
- frontend/src/lib/liveblocks/useHistory.ts +12 -0
- frontend/src/lib/liveblocks/useList.ts +46 -0
- frontend/src/lib/liveblocks/useMyPresence.ts +42 -0
- frontend/src/lib/liveblocks/useObject.ts +43 -0
- frontend/src/lib/liveblocks/useOthers.ts +28 -0
- frontend/src/lib/liveblocks/useRedo.ts +12 -0
- frontend/src/lib/liveblocks/useRoom.ts +21 -0
- frontend/src/lib/liveblocks/useSelf.ts +32 -0
- frontend/src/lib/liveblocks/useStorage.ts +21 -0
- frontend/src/lib/liveblocks/useUndo.ts +12 -0
- frontend/src/lib/liveblocks/useUpdateMyPresence.ts +20 -0
- frontend/src/lib/store.ts +2 -42
- frontend/src/lib/types.ts +18 -2
- frontend/src/routes/+page.svelte +31 -32
    	
        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 | 
            -
            		 | 
| 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 | 
            -
            	 | 
|  | |
| 30 |  | 
| 31 | 
            -
            	 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 32 |  | 
| 33 | 
            -
            	 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 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  | 
| 228 | 
            -
            			{#each  | 
| 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 | 
|  | |
| 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 | 
| 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 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
            	 | 
| 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 | 
            -
            	 | 
| 25 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 26 | 
             
            	}
         | 
| 27 |  | 
| 28 | 
             
            	onMount(() => {
         | 
| @@ -58,23 +71,29 @@ | |
| 58 | 
             
            		canvasCtx.strokeRect(0, 0, width, height);
         | 
| 59 | 
             
            	});
         | 
| 60 |  | 
| 61 | 
            -
            	function renderImages( | 
| 62 | 
            -
            		const images = [...imagesList.toImmutable()].sort((a, b) => a.date - b.date);
         | 
| 63 | 
             
            		Promise.all(
         | 
| 64 | 
            -
            			 | 
| 65 | 
            -
            				({ imgURL, position }) =>
         | 
| 66 | 
             
            					new Promise((resolve) => {
         | 
| 67 | 
             
            						const img = new Image();
         | 
| 68 | 
             
            						img.crossOrigin = 'anonymous';
         | 
| 69 | 
             
            						img.onload = () => {
         | 
| 70 | 
            -
            							 | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 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 | 
            -
            		 | 
| 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 | 
            -
            		 | 
| 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"> | 
| 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 | 
            -
             | 
|  | |
|  | |
|  | |
| 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 {  | 
| 4 | 
            -
            	import type { Client | 
| 5 | 
            -
            	import  | 
| 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 | 
            -
            		 | 
| 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 | 
            -
             | 
| 41 | 
            -
            	 | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
|  | |
|  | |
| 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}
         | 

