From 239c879649bde6516b000a0dc188329da0c9549d Mon Sep 17 00:00:00 2001 From: Zutatensuppe Date: Sat, 22 May 2021 01:51:44 +0200 Subject: [PATCH] categorys and titles for images --- build/public/assets/index.1c0291a2.js | 1 + build/public/assets/index.b49dadca.js | 1 - ...{index.6c4f6859.css => index.c5b0553c.css} | 2 +- build/public/index.html | 4 +- build/server/main.js | 286 ++++++++++++++++-- package-lock.json | 232 ++++++++++++++ package.json | 5 +- rollup.server.config.js | 4 +- scripts/import_images.ts | 23 ++ scripts/server | 6 + src/common/GameCommon.ts | 12 +- src/common/Util.ts | 7 + src/dbpatches/01_initial.sqlite | 24 ++ src/frontend/components/NewGameDialog.vue | 1 + src/frontend/components/NewImageDialog.vue | 72 +++-- src/frontend/views/NewGame.vue | 34 ++- src/server/Db.ts | 212 +++++++++++++ src/server/Dirs.ts | 3 + src/server/Images.ts | 77 ++++- src/server/main.ts | 40 ++- tsconfig.server.json => tsconfig.json | 0 vite.config.js | 4 + 22 files changed, 964 insertions(+), 86 deletions(-) create mode 100644 build/public/assets/index.1c0291a2.js delete mode 100644 build/public/assets/index.b49dadca.js rename build/public/assets/{index.6c4f6859.css => index.c5b0553c.css} (80%) create mode 100644 scripts/import_images.ts create mode 100644 src/dbpatches/01_initial.sqlite create mode 100644 src/server/Db.ts rename tsconfig.server.json => tsconfig.json (100%) diff --git a/build/public/assets/index.1c0291a2.js b/build/public/assets/index.1c0291a2.js new file mode 100644 index 0000000..1f35521 --- /dev/null +++ b/build/public/assets/index.1c0291a2.js @@ -0,0 +1 @@ +import{d as e,c as t,a as n,w as o,b as l,r as i,o as a,e as s,t as r,F as d,f as c,g as u,v as p,h as g,i as h,j as y,k as m,l as f,m as w,n as v,p as b}from"./vendor.8616a479.js";var x=e({name:"app",computed:{showNav(){return!["game","replay"].includes(String(this.$route.name))}}});const C={id:"app"},A={key:0,class:"nav"},k=s("Index"),T=s("New game");x.render=function(e,s,r,d,c,u){const p=i("router-link"),g=i("router-view");return a(),t("div",C,[e.showNav?(a(),t("ul",A,[n("li",null,[n(p,{class:"btn",to:{name:"index"}},{default:o((()=>[k])),_:1})]),n("li",null,[n(p,{class:"btn",to:{name:"new-game"}},{default:o((()=>[T])),_:1})])])):l("",!0),n(g)])};const z=864e5,S=e=>{const t=Math.floor(e/z);e%=z;const n=Math.floor(e/36e5);e%=36e5;const o=Math.floor(e/6e4);e%=6e4;return`${t}d ${n}h ${o}m ${Math.floor(e/1e3)}s`};var I=1e3,P=()=>{const e=new Date;return Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),e.getUTCMilliseconds())},_=(e,t)=>S(t-e),D=S,B=e({name:"game-teaser",props:{game:{type:Object,required:!0}},computed:{style(){return{"background-image":`url("${this.game.imageUrl.replace("uploads/","uploads/r/")+"-375x210.webp"}")`}}},methods:{time(e,t){const n=t?"🏁":"⏳",o=e,l=t||P();return`${n} ${_(o,l)}`}}});const O={class:"game-info-text"},E=n("br",null,null,-1),M=n("br",null,null,-1),N=n("br",null,null,-1);B.render=function(e,d,c,u,p,g){const h=i("router-link");return a(),t("div",{class:"game-teaser",style:e.style},[n(h,{class:"game-info",to:{name:"game",params:{id:e.game.id}}},{default:o((()=>[n("span",O,[s(" 🧩 "+r(e.game.tilesFinished)+"/"+r(e.game.tilesTotal),1),E,s(" πŸ‘₯ "+r(e.game.players),1),M,s(" "+r(e.time(e.game.started,e.game.finished)),1),N])])),_:1},8,["to"]),l("",!0)],4)};var U=e({components:{GameTeaser:B},data:()=>({gamesRunning:[],gamesFinished:[]}),async created(){const e=await fetch("/api/index-data"),t=await e.json();this.gamesRunning=t.gamesRunning,this.gamesFinished=t.gamesFinished}});const G=n("h1",null,"Running games",-1),R=n("h1",null,"Finished games",-1);U.render=function(e,o,l,s,r,u){const p=i("game-teaser");return a(),t("div",null,[G,(a(!0),t(d,null,c(e.gamesRunning,((e,o)=>(a(),t("div",{class:"game-teaser-wrap",key:o},[n(p,{game:e},null,8,["game"])])))),128)),R,(a(!0),t(d,null,c(e.gamesFinished,((e,o)=>(a(),t("div",{class:"game-teaser-wrap",key:o},[n(p,{game:e},null,8,["game"])])))),128))])};var $=e({name:"image-teaser",props:{image:{type:Object,required:!0}},computed:{style(){return{backgroundImage:`url("${this.image.url.replace("uploads/","uploads/r/")+"-150x100.webp"}")`}}},methods:{onClick(){this.$emit("click")}}});$.render=function(e,n,o,l,i,s){return a(),t("div",{class:"imageteaser",style:e.style,onClick:n[1]||(n[1]=(...t)=>e.onClick&&e.onClick(...t))},null,4)};var V=e({name:"image-library",components:{ImageTeaser:$},props:{images:{type:Array,required:!0}},emits:{imageClicked:null},methods:{imageClicked(e){this.$emit("imageClicked",e)}}});V.render=function(e,n,o,l,s,r){const u=i("image-teaser");return a(),t("div",null,[(a(!0),t(d,null,c(e.images,((n,o)=>(a(),t(u,{image:n,onClick:t=>e.imageClicked(n),key:o},null,8,["image","onClick"])))),128))])};var j=e({name:"upload",props:{accept:String,label:String},methods:{async upload(e){const t=e.target;if(!t.files)return;const n=t.files[0];if(!n)return;const o=new FormData;o.append("file",n,n.name);const l=await fetch("/upload",{method:"post",body:o}),i=await l.json();this.$emit("uploaded",i)}}});const F={class:"btn"};j.render=function(e,o,l,i,s,d){return a(),t("label",null,[n("input",{type:"file",style:{display:"none"},onChange:o[1]||(o[1]=(...t)=>e.upload&&e.upload(...t)),accept:e.accept},null,40,["accept"]),n("span",F,r(e.label||"Upload File"),1)])};const W={name:"responsive-image",props:{src:String,title:{type:String,default:""},height:{type:String,default:"100%"},width:{type:String,default:"100%"}},computed:{style(){return{display:"inline-block",verticalAlign:"text-bottom",backgroundImage:`url('${this.src}')`,backgroundRepeat:"no-repeat",backgroundSize:"contain",backgroundPosition:"center",width:this.width,height:this.height}}}};W.render=function(e,n,o,l,i,s){return a(),t("div",{style:s.style,title:o.title},null,12,["title"])};var L=e({name:"new-image-dialog",components:{Upload:j,ResponsiveImage:W},emits:{bgclick:null,setupGameClick:null,postToGalleryClick:null},data:()=>({previewUrl:"",file:null,title:"",category:""}),computed:{canPostToGallery(){return!(!this.previewUrl||!this.file)},canSetupGameClick(){return!(!this.previewUrl||!this.file)}},methods:{preview(e){const t=e.target;if(!t.files)return;const n=t.files[0];if(!n)return;const o=new FileReader;o.readAsDataURL(n),o.onload=e=>{this.previewUrl=e.target.result,this.file=n}},postToGallery(){this.$emit("postToGalleryClick",{file:this.file,title:this.title,category:this.category})},setupGameClick(){this.$emit("setupGameClick",{file:this.file,title:this.title,category:this.category})}}});const H={key:0,class:"has-image"},Q={key:1},Y={class:"upload"},q={class:"btn"},Z={class:"area-settings"},K=n("td",null,[n("label",null,"Title")],-1),J=n("tr",null,[n("td",{colspan:"2"},[n("div",{class:"hint"},"Feel free to leave a credit to the artist/photographer in the title :)")])],-1),X=n("td",null,[n("label",null,"Category")],-1),ee={class:"area-buttons"},te=s("🧩 Post to gallery "),ne=n("br",null,null,-1),oe=s(" + set up game");function le(e,t){const n=e.x-t.x,o=e.y-t.y;return Math.sqrt(n*n+o*o)}function ie(e){return{x:e.x+e.w/2,y:e.y+e.h/2}}L.render=function(e,o,l,s,d,c){const h=i("responsive-image");return a(),t("div",{class:"overlay new-image-dialog",onClick:o[8]||(o[8]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:o[7]||(o[7]=g((()=>{}),["stop"]))},[n("div",{class:["area-image",{"has-image":!!e.previewUrl,"no-image":!e.previewUrl}]},[e.previewUrl?(a(),t("div",H,[n("span",{class:"remove btn",onClick:o[1]||(o[1]=t=>e.previewUrl="")},"X"),n(h,{src:e.previewUrl},null,8,["src"])])):(a(),t("div",Q,[n("label",Y,[n("input",{type:"file",style:{display:"none"},onChange:o[2]||(o[2]=(...t)=>e.preview&&e.preview(...t)),accept:"image/*"},null,32),n("span",q,r(e.label||"Upload File"),1)])]))],2),n("div",Z,[n("table",null,[n("tr",null,[K,n("td",null,[u(n("input",{type:"text","onUpdate:modelValue":o[3]||(o[3]=t=>e.title=t),placeholder:"Flower by @artist"},null,512),[[p,e.title]])])]),J,n("tr",null,[X,n("td",null,[u(n("input",{type:"text","onUpdate:modelValue":o[4]||(o[4]=t=>e.category=t),placeholder:"Plants"},null,512),[[p,e.category]])])])])]),n("div",ee,[n("button",{class:"btn",disabled:!e.canPostToGallery,onClick:o[5]||(o[5]=(...t)=>e.postToGallery&&e.postToGallery(...t))},"πŸ–ΌοΈ Post to gallery",8,["disabled"]),n("button",{class:"btn",disabled:!e.canSetupGameClick,onClick:o[6]||(o[6]=(...t)=>e.setupGameClick&&e.setupGameClick(...t))},[te,ne,oe],8,["disabled"])])])])};var ae={pointSub:function(e,t){return{x:e.x-t.x,y:e.y-t.y}},pointAdd:function(e,t){return{x:e.x+t.x,y:e.y+t.y}},pointDistance:le,pointInBounds:function(e,t){return e.x>=t.x&&e.x<=t.x+t.w&&e.y>=t.y&&e.y<=t.y+t.h},rectCenter:ie,rectMoved:function(e,t,n){return{x:e.x+t,y:e.y+n,w:e.w,h:e.h}},rectCenterDistance:function(e,t){return le(ie(e),ie(t))},rectsOverlap:function(e,t){return!(t.x>e.x+e.w||e.x>t.x+t.w||t.y>e.y+e.h||e.y>t.y+t.h)}};var se=1,re=4,de=5,ce=2,ue=3,pe=6,ge=2,he=4,ye=3,me=9,fe=1,we=2,ve=3,be=4,xe=5,Ce=6,Ae=7,ke=8,Te=10,ze=1,Se=2,Ie=3;class Pe{constructor(e){this.rand_high=e||3735929054,this.rand_low=1231121986^e}random(e,t){return this.rand_high=(this.rand_high<<16)+(this.rand_high>>16)+this.rand_low&4294967295,this.rand_low=this.rand_low+this.rand_high&4294967295,e+(this.rand_high>>>0)/4294967295*(t-e+1)|0}choice(e){return e[this.random(0,e.length-1)]}shuffle(e){const t=e.slice();for(let n=0;n<=t.length-2;n++){const e=this.random(n,t.length-1),o=t[n];t[n]=t[e],t[e]=o}return t}static serialize(e){return{rand_high:e.rand_high,rand_low:e.rand_low}}static unserialize(e){const t=new Pe(0);return t.rand_high=e.rand_high,t.rand_low=e.rand_low,t}}const _e=(e,t)=>{const n=`${e}`;return n.length>=t.length?n:t.substr(0,t.length-n.length)+n},De=(...e)=>{const t=t=>(...n)=>{const o=new Date,l=_e(o.getHours(),"00"),i=_e(o.getMinutes(),"00"),a=_e(o.getSeconds(),"00");console[t](`${l}:${i}:${a}`,...e,...n)};return{log:t("log"),error:t("error"),info:t("info")}};var Be,Oe,Ee,Me,Ne={hash:e=>{let t=0;for(let n=0;n{let t=e.toLowerCase();return t=t.replace(/[^a-z0-9]+/g,"-"),t=t.replace(/^-|-$/,""),t},uniqId:()=>Date.now().toString(36)+Math.random().toString(36).substring(2),encodeShape:function(e){return e.top+1<<0|e.right+1<<2|e.bottom+1<<4|e.left+1<<6},decodeShape:function(e){return{top:(e>>0&3)-1,right:(e>>2&3)-1,bottom:(e>>4&3)-1,left:(e>>6&3)-1}},encodeTile:function(e){return[e.idx,e.pos.x,e.pos.y,e.z,e.owner,e.group]},decodeTile:function(e){return{idx:e[0],pos:{x:e[1],y:e[2]},z:e[3],owner:e[4],group:e[5]}},encodePlayer:function(e){return[e.id,e.x,e.y,e.d,e.name,e.color,e.bgcolor,e.points,e.ts]},decodePlayer:function(e){return{id:e[0],x:e[1],y:e[2],d:e[3],name:e[4],color:e[5],bgcolor:e[6],points:e[7],ts:e[8]}},encodeGame:function(e){return Array.isArray(e)?e:[e.id,e.rng.type,Pe.serialize(e.rng.obj),e.puzzle,e.players,e.evtInfos,e.scoreMode]},decodeGame:function(e){return Array.isArray(e)?{id:e[0],rng:{type:e[1],obj:Pe.unserialize(e[2])},puzzle:e[3],players:e[4],evtInfos:e[5],scoreMode:e[6]}:e},coordByTileIdx:function(e,t){const n=e.width/e.tileSize;return{x:t%n,y:Math.floor(t/n)}},asQueryArgs:function(e){const t=[];for(let n in e){const o=[n,e[n]].map(encodeURIComponent);t.push(o.join("="))}return 0===t.length?"":`?${t.join("&")}`}};(Oe=Be||(Be={}))[Oe.Flat=0]="Flat",Oe[Oe.Out=1]="Out",Oe[Oe.In=-1]="In",(Me=Ee||(Ee={}))[Me.FINAL=0]="FINAL",Me[Me.ANY=1]="ANY";const Ue={};function Ge(e,t){return{id:e,x:0,y:0,d:0,name:null,color:null,bgcolor:null,points:0,ts:t}}function Re(e,t){let n=0;for(let o of Ue[e].players){if(Ne.decodePlayer(o).id===t)return n;n++}return-1}function $e(e,t){const n=Re(e,t);return Ne.decodePlayer(Ue[e].players[n])}function Ve(e,t,n){const o=Re(e,t);-1===o?Ue[e].players.push(Ne.encodePlayer(n)):Ue[e].players[o]=Ne.encodePlayer(n)}function je(e,t){return-1!==Re(e,t)}function Fe(e){return Ue[e]?Ue[e].players.map(Ne.decodePlayer):[]}function We(e){return Ue[e].puzzle.tiles.length}function Le(e){return Ue[e].scoreMode||0}function He(e){return Qe(e)===We(e)}function Qe(e){let t=0;for(let n of Ue[e].puzzle.tiles)-1===Ne.decodeTile(n).owner&&t++;return t}function Ye(e,t,n){const o=$e(e,t);for(let l of Object.keys(n))o[l]=n[l];Ve(e,t,o)}function qe(e,t){for(let n of Object.keys(t))Ue[e].puzzle.data[n]=t[n]}function Ze(e,t,n){for(let o of Object.keys(n)){const l=Ne.decodeTile(Ue[e].puzzle.tiles[t]);l[o]=n[o],Ue[e].puzzle.tiles[t]=Ne.encodeTile(l)}}const Ke=(e,t)=>Ne.decodeTile(Ue[e].puzzle.tiles[t]),Je=(e,t)=>Ke(e,t).group,Xe=(e,t)=>{const n=Ue[e].puzzle.info,o={x:(n.table.width-n.width)/2,y:(n.table.height-n.height)/2},l=function(e,t){const n=Ue[e].puzzle.info,o=Ne.coordByTileIdx(n,t),l=o.x*n.tileSize,i=o.y*n.tileSize;return{x:l,y:i}}(e,t);return ae.pointAdd(o,l)},et=(e,t)=>Ke(e,t).pos,tt=e=>{const t=mt(e),n=ft(e),o=Math.round(t/4),l=Math.round(n/4);return{x:0-o,y:0-l,w:t+2*o,h:n+2*l}},nt=(e,t)=>{const n=at(e),o=Ke(e,t);return{x:o.pos.x,y:o.pos.y,w:n,h:n}},ot=(e,t)=>Ke(e,t).z,lt=(e,t)=>{for(let n of Ue[e].puzzle.tiles){const e=Ne.decodeTile(n);if(e.owner===t)return e.idx}return-1},it=e=>Ue[e].puzzle.info.tileDrawSize,at=e=>Ue[e].puzzle.info.tileSize,st=e=>Ue[e].puzzle.data.maxGroup,rt=e=>Ue[e].puzzle.data.maxZ;function dt(e,t){const n=Ue[e].puzzle.info,o=Ne.coordByTileIdx(n,t);return[o.y>0?t-n.tilesX:-1,o.x0?t-1:-1]}const ct=(e,t,n)=>{for(let o of t)Ze(e,o,{z:n})},ut=(e,t,n)=>{const o=et(e,t);Ze(e,t,{pos:ae.pointAdd(o,n)})},pt=(e,t,n)=>{const o=it(e),l=tt(e),i=n;for(let a of t){const t=Ke(e,a);t.pos.x+n.xl.x+l.w&&(i.x=Math.min(l.x+l.w-t.pos.x+o,i.x)),t.pos.y+n.yl.y+l.h&&(i.y=Math.min(l.y+l.h-t.pos.y+o,i.y))}for(let a of t)ut(e,a,i)},gt=(e,t,n)=>{for(let o of t)Ze(e,o,{owner:n})};function ht(e,t){const n=Ue[e].puzzle.tiles,o=Ne.decodeTile(n[t]),l=[];if(o.group)for(let i of n){const e=Ne.decodeTile(i);e.group===o.group&&l.push(e.idx)}else l.push(o.idx);return l}const yt=(e,t)=>{const n=$e(e,t);return n?n.points:0},mt=e=>Ue[e].puzzle.info.table.width,ft=e=>Ue[e].puzzle.info.table.height;var wt={__createPlayerObject:Ge,setGame:function(e,t){Ue[e]=t},exists:function(e){return!!Ue[e]||!1},playerExists:je,getActivePlayers:function(e,t){const n=t-30*I;return Fe(e).filter((e=>e.ts>=n))},getIdlePlayers:function(e,t){const n=t-30*I;return Fe(e).filter((e=>e.ts0))},addPlayer:function(e,t,n){je(e,t)?Ye(e,t,{ts:n}):Ve(e,t,Ge(t,n))},getFinishedTileCount:Qe,getTileCount:We,getImageUrl:function(e){return Ue[e].puzzle.info.imageUrl},setImageUrl:function(e,t){Ue[e].puzzle.info.imageUrl=t},get:function(e){return Ue[e]},getAllGames:function(){return Object.values(Ue).sort(((e,t)=>He(e.id)===He(t.id)?t.puzzle.data.started-e.puzzle.data.started:He(e.id)?1:-1))},getPlayerBgColor:(e,t)=>{const n=$e(e,t);return n?n.bgcolor:null},getPlayerColor:(e,t)=>{const n=$e(e,t);return n?n.color:null},getPlayerName:(e,t)=>{const n=$e(e,t);return n?n.name:null},getPlayerIndexById:Re,getPlayerIdByIndex:function(e,t){return Ue[e].players.length>t?Ne.decodePlayer(Ue[e].players[t]).id:null},changePlayer:Ye,setPlayer:Ve,setTile:function(e,t,n){Ue[e].puzzle.tiles[t]=Ne.encodeTile(n)},setPuzzleData:function(e,t){Ue[e].puzzle.data=t},getTableWidth:mt,getTableHeight:ft,getPuzzle:e=>Ue[e].puzzle,getRng:e=>Ue[e].rng.obj,getPuzzleWidth:e=>Ue[e].puzzle.info.width,getPuzzleHeight:e=>Ue[e].puzzle.info.height,getTilesSortedByZIndex:function(e){return Ue[e].puzzle.tiles.map(Ne.decodeTile).sort(((e,t)=>e.z-t.z))},getFirstOwnedTile:(e,t)=>{const n=lt(e,t);return n<0?null:Ue[e].puzzle.tiles[n]},getTileDrawOffset:e=>Ue[e].puzzle.info.tileDrawOffset,getTileDrawSize:it,getFinalTilePos:Xe,getStartTs:e=>Ue[e].puzzle.data.started,getFinishTs:e=>Ue[e].puzzle.data.finished,handleInput:function(e,t,n,o){const l=Ue[e].puzzle,i=function(e,t){return t in Ue[e].evtInfos?Ue[e].evtInfos[t]:{_last_mouse:null,_last_mouse_down:null}}(e,t),a=[],s=()=>{a.push([ze,l.data])},r=t=>{a.push([Se,Ne.encodeTile(Ke(e,t))])},d=e=>{for(const t of e)r(t)},c=()=>{a.push([Ie,Ne.encodePlayer($e(e,t))])},u=n[0];if(u===Ce){const l=n[1];Ye(e,t,{bgcolor:l,ts:o}),c()}else if(u===Ae){const l=n[1];Ye(e,t,{color:l,ts:o}),c()}else if(u===ke){const l=`${n[1]}`.substr(0,16);Ye(e,t,{name:l,ts:o}),c()}else if(u===fe){const l={x:n[1],y:n[2]};Ye(e,t,{d:1,ts:o}),c(),i._last_mouse_down=l;const a=((e,t)=>{let n=Ue[e].puzzle.info,o=Ue[e].puzzle.tiles,l=-1,i=-1;for(let a=0;al)&&(l=e.z,i=a)}return i})(e,l);if(a>=0){let n=rt(e)+1;qe(e,{maxZ:n}),s();const o=ht(e,a);ct(e,o,rt(e)),gt(e,o,t),d(o)}i._last_mouse=l}else if(u===ve){const l=n[1],a=n[2],s={x:l,y:a};if(null===i._last_mouse_down)Ye(e,t,{x:l,y:a,ts:o}),c();else{let n=lt(e,t);if(n>=0){Ye(e,t,{x:l,y:a,ts:o}),c();const r=ht(e,n);let u=ae.pointInBounds(s,tt(e))&&ae.pointInBounds(i._last_mouse_down,tt(e));for(let t of r){const n=nt(e,t);if(ae.pointInBounds(s,n)){u=!0;break}}if(u){const t=l-i._last_mouse_down.x,n=a-i._last_mouse_down.y;pt(e,r,{x:t,y:n}),d(r)}}else Ye(e,t,{ts:o}),c();i._last_mouse_down=s}i._last_mouse=s}else if(u===we){const a={x:n[1],y:n[2]},u=0;i._last_mouse_down=null;let p=lt(e,t);if(p>=0){let n=ht(e,p);gt(e,n,0),d(n);let i=et(e,p),a=Xe(e,p);if(ae.pointDistance(a,i){for(let n of t)Ze(e,n,{owner:-1,z:1})})(e,n),d(n);let r=yt(e,t);0===Le(e)?r+=n.length:1===Le(e)&&(r+=1),Ye(e,t,{d:u,ts:o,points:r}),c(),Qe(e)===We(e)&&(qe(e,{finished:o}),s())}else{const n=(e,t,n,o)=>{let l=Ue[e].puzzle.info;if(n<0)return!1;if(((e,t,n)=>{const o=Je(e,t),l=Je(e,n);return o&&o===l})(e,t,n))return!1;const i=et(e,t),a=ae.pointAdd(et(e,n),{x:o[0]*l.tileSize,y:o[1]*l.tileSize});if(ae.pointDistance(i,a){const o=Ue[e].puzzle.tiles,l=Je(e,t),i=Je(e,n);let a;const d=[];l&&d.push(l),i&&d.push(i),l?a=l:i?a=i:(qe(e,{maxGroup:st(e)+1}),s(),a=st(e));if(Ze(e,t,{group:a}),r(t),Ze(e,n,{group:a}),r(n),d.length>0)for(const s of o){const t=Ne.decodeTile(s);d.includes(t.group)&&(Ze(e,t.idx,{group:a}),r(t.idx))}})(e,t,n),l=ht(e,t);const c=((e,t)=>{let n=0;for(let o of t){let t=ot(e,o);t>n&&(n=t)}return n})(e,l);return ct(e,l,c),d(l),!0}return!1};let l=!1;for(let t of ht(e,p)){let o=dt(e,t);if(n(e,t,o[0],[0,1])||n(e,t,o[1],[-1,0])||n(e,t,o[2],[0,-1])||n(e,t,o[3],[1,0])){l=!0;break}}if(l&&1===Le(e)){const n=yt(e,t)+1;Ye(e,t,{d:u,ts:o,points:n}),c()}else Ye(e,t,{d:u,ts:o}),c()}}else Ye(e,t,{d:u,ts:o}),c();i._last_mouse=a}else if(u===be){const l=n[1],a=n[2];Ye(e,t,{x:l,y:a,ts:o}),c(),i._last_mouse={x:l,y:a}}else if(u===xe){const l=n[1],a=n[2];Ye(e,t,{x:l,y:a,ts:o}),c(),i._last_mouse={x:l,y:a}}else Ye(e,t,{ts:o}),c();return function(e,t,n){Ue[e].evtInfos[t]=n}(e,t,i),a}},vt=e({name:"new-game-dialog",components:{ResponsiveImage:W},props:{image:{type:Object,required:!0}},emits:{newGame:null,bgclick:null},data:()=>({tiles:1e3,scoreMode:Ee.ANY}),methods:{onNewGameClick(){this.$emit("newGame",{tiles:this.tilesInt,image:this.image,scoreMode:this.scoreModeInt})}},computed:{canStartNewGame(){return!!(this.tilesInt&&this.image&&this.image.url&&[0,1].includes(this.scoreModeInt))},scoreModeInt(){return parseInt(`${this.scoreMode}`,10)},tilesInt(){return parseInt(`${this.tiles}`,10)}}});const bt={class:"area-image"},xt={class:"has-image"},Ct={class:"area-settings"},At=n("td",null,[n("label",null,"Pieces")],-1),kt=n("td",null,[n("label",null,"Scoring: ")],-1),Tt=s(" Any (Score when pieces are connected to each other or on final location)"),zt=n("br",null,null,-1),St=s(" Final (Score when pieces are put to their final location)"),It={class:"area-buttons"};vt.render=function(e,o,l,s,r,d){const c=i("responsive-image");return a(),t("div",{class:"overlay new-game-dialog",onClick:o[6]||(o[6]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:o[5]||(o[5]=g((()=>{}),["stop"]))},[n("div",bt,[n("div",xt,[n(c,{src:e.image.url,title:e.image.title},null,8,["src","title"])])]),n("div",Ct,[n("table",null,[n("tr",null,[At,n("td",null,[u(n("input",{type:"text","onUpdate:modelValue":o[1]||(o[1]=t=>e.tiles=t)},null,512),[[p,e.tiles]])])]),n("tr",null,[kt,n("td",null,[n("label",null,[u(n("input",{type:"radio","onUpdate:modelValue":o[2]||(o[2]=t=>e.scoreMode=t),value:"1"},null,512),[[h,e.scoreMode]]),Tt]),zt,n("label",null,[u(n("input",{type:"radio","onUpdate:modelValue":o[3]||(o[3]=t=>e.scoreMode=t),value:"0"},null,512),[[h,e.scoreMode]]),St])])])])]),n("div",It,[n("button",{class:"btn",disabled:!e.canStartNewGame,onClick:o[4]||(o[4]=(...t)=>e.onNewGameClick&&e.onNewGameClick(...t))}," 🧩 Generate Puzzle ",8,["disabled"])])])])};var Pt=e({components:{ImageLibrary:V,NewImageDialog:L,NewGameDialog:vt},data:()=>({filters:{sort:"date_desc",category:""},images:[],categories:[],image:{id:0,filename:"",file:"",url:"",title:"",categories:[],created:0},dialog:""}),async created(){await this.loadImages()},methods:{async loadImages(){const e=await fetch(`/api/newgame-data${Ne.asQueryArgs(this.filters)}`),t=await e.json();this.images=t.images,this.categories=t.categories},async filtersChanged(){await this.loadImages()},imageClicked(e){this.image=e,this.dialog="new-game"},async uploadImage(e){const t=new FormData;t.append("file",e.file,e.file.name),t.append("title",e.title),t.append("category",e.category);const n=await fetch("/upload",{method:"post",body:t});return await n.json()},async postToGalleryClick(e){await this.uploadImage(e),this.dialog="",await this.loadImages()},async setupGameClick(e){const t=await this.uploadImage(e);this.loadImages(),this.image=t,this.dialog="new-game"},async onNewGame(e){const t=await fetch("/newgame",{method:"post",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(e)});if(200===t.status){const e=await t.json();this.$router.push({name:"game",params:{id:e.id}})}}}});const _t={class:"upload-image-teaser"},Dt=n("div",{class:"hint"},"(The image you upload will be added to the public gallery.)",-1),Bt={key:0},Ot=s(" Category: "),Et=n("option",{value:""},"All",-1),Mt=s(" Sort by: "),Nt=n("option",{value:"date_desc"},"Newest first",-1),Ut=n("option",{value:"date_asc"},"Oldest first",-1),Gt=n("option",{value:"alpha_asc"},"A-Z",-1),Rt=n("option",{value:"alpha_desc"},"Z-A",-1);Pt.render=function(e,o,s,p,g,h){const m=i("image-library"),f=i("new-image-dialog"),w=i("new-game-dialog");return a(),t("div",null,[n("div",_t,[n("div",{class:"btn btn-big",onClick:o[1]||(o[1]=t=>e.dialog="new-image")},"Upload your image"),Dt]),n("div",null,[e.categories.length>0?(a(),t("label",Bt,[Ot,u(n("select",{"onUpdate:modelValue":o[2]||(o[2]=t=>e.filters.category=t),onChange:o[3]||(o[3]=(...t)=>e.filtersChanged&&e.filtersChanged(...t))},[Et,(a(!0),t(d,null,c(e.categories,((e,n)=>(a(),t("option",{key:n,value:e.slug},r(e.title),9,["value"])))),128))],544),[[y,e.filters.category]])])):l("",!0),n("label",null,[Mt,u(n("select",{"onUpdate:modelValue":o[4]||(o[4]=t=>e.filters.sort=t),onChange:o[5]||(o[5]=(...t)=>e.filtersChanged&&e.filtersChanged(...t))},[Nt,Ut,Gt,Rt],544),[[y,e.filters.sort]])])]),n(m,{images:e.images,onImageClicked:e.imageClicked},null,8,["images","onImageClicked"]),"new-image"===e.dialog?(a(),t(f,{key:0,onBgclick:o[6]||(o[6]=t=>e.dialog=""),onPostToGalleryClick:e.postToGalleryClick,onSetupGameClick:e.setupGameClick},null,8,["onPostToGalleryClick","onSetupGameClick"])):l("",!0),e.image&&"new-game"===e.dialog?(a(),t(w,{key:1,onBgclick:o[7]||(o[7]=t=>e.dialog=""),onNewGame:e.onNewGame,image:e.image},null,8,["onNewGame","image"])):l("",!0)])};var $t=e({name:"scores",props:{activePlayers:{type:Array,required:!0},idlePlayers:{type:Array,required:!0}},computed:{actives(){return this.activePlayers.sort(((e,t)=>t.points-e.points)),this.activePlayers},idles(){return this.idlePlayers.sort(((e,t)=>t.points-e.points)),this.idlePlayers}}});const Vt={class:"scores"},jt=n("div",null,"Scores",-1),Ft=n("td",null,"⚑",-1),Wt=n("td",null,"πŸ’€",-1);$t.render=function(e,o,l,i,s,u){return a(),t("div",Vt,[jt,n("table",null,[(a(!0),t(d,null,c(e.actives,((e,o)=>(a(),t("tr",{key:o,style:{color:e.color}},[Ft,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128)),(a(!0),t(d,null,c(e.idles,((e,o)=>(a(),t("tr",{key:o,style:{color:e.color}},[Wt,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128))])])};var Lt=e({name:"puzzle-status",props:{finished:{type:Boolean,required:!0},duration:{type:Number,required:!0},piecesDone:{type:Number,required:!0},piecesTotal:{type:Number,required:!0}},computed:{icon(){return this.finished?"🏁":"⏳"},durationStr(){return D(this.duration)}}});const Ht={class:"timer"};Lt.render=function(e,o,l,i,s,d){return a(),t("div",Ht,[n("div",null," 🧩 "+r(e.piecesDone)+"/"+r(e.piecesTotal),1),n("div",null,r(e.icon)+" "+r(e.durationStr),1),m(e.$slots,"default")])};var Qt=e({name:"settings-overlay",emits:{bgclick:null,"update:modelValue":null},props:{modelValue:Object},created(){this.$watch("modelValue",(e=>{this.$emit("update:modelValue",e)}),{deep:!0})}});const Yt=n("td",null,[n("label",null,"Background: ")],-1),qt=n("td",null,[n("label",null,"Color: ")],-1),Zt=n("td",null,[n("label",null,"Name: ")],-1);Qt.render=function(e,o,l,i,s,r){return a(),t("div",{class:"overlay transparent",onClick:o[5]||(o[5]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content settings",onClick:o[4]||(o[4]=g((()=>{}),["stop"]))},[n("tr",null,[Yt,n("td",null,[u(n("input",{type:"color","onUpdate:modelValue":o[1]||(o[1]=t=>e.modelValue.background=t)},null,512),[[p,e.modelValue.background]])])]),n("tr",null,[qt,n("td",null,[u(n("input",{type:"color","onUpdate:modelValue":o[2]||(o[2]=t=>e.modelValue.color=t)},null,512),[[p,e.modelValue.color]])])]),n("tr",null,[Zt,n("td",null,[u(n("input",{type:"text",maxLength:"16","onUpdate:modelValue":o[3]||(o[3]=t=>e.modelValue.name=t)},null,512),[[p,e.modelValue.name]])])])])])};var Kt=e({name:"preview-overlay",props:{img:String},emits:{bgclick:null},computed:{previewStyle(){return{backgroundImage:`url('${this.img}')`}}}});const Jt={class:"preview"};Kt.render=function(e,o,l,i,s,r){return a(),t("div",{class:"overlay",onClick:o[1]||(o[1]=t=>e.$emit("bgclick"))},[n("div",Jt,[n("div",{class:"img",style:e.previewStyle},null,4)])])};const Xt=De("Communication.js");let en,tn=e=>{},nn=e=>{};let on=0;const ln=e=>{on!==e&&(on=e,nn(e))};function an(e){if(2===on)try{en.send(JSON.stringify(e))}catch(t){Xt.info("unable to send message.. maybe because ws is invalid?")}}let sn,rn;var dn={connect:function(e,t,n){return sn=0,rn={},ln(3),new Promise((o=>{en=new WebSocket(e,n+"|"+t),en.onopen=e=>{ln(2),an([ue])},en.onmessage=e=>{const t=JSON.parse(e.data),l=t[0];if(l===re){const e=t[1];o(e)}else{if(l!==se)throw`[ 2021-05-09 invalid connect msgType ${l} ]`;{const e=t[1],o=t[2];if(e===n&&rn[o])return void delete rn[o];tn(t)}}},en.onerror=e=>{throw ln(1),"[ 2021-05-15 onerror ]"},en.onclose=e=>{4e3===e.code||1001===e.code?ln(4):ln(1)}}))},connectReplay:function(e,t,n){return sn=0,rn={},ln(3),new Promise((o=>{en=new WebSocket(e,n+"|"+t),en.onopen=e=>{ln(2),an([pe])},en.onmessage=e=>{const t=JSON.parse(e.data),n=t[0];if(n!==de)throw`[ 2021-05-09 invalid connectReplay msgType ${n} ]`;{const e=t[1],n=t[2];o({game:e,log:n})}},en.onerror=e=>{throw ln(1),"[ 2021-05-15 onerror ]"},en.onclose=e=>{4e3===e.code||1001===e.code?ln(4):ln(1)}}))},disconnect:function(){en&&en.close(4e3),sn=0,rn={}},sendClientEvent:function(e){sn++,rn[sn]=e,an([ce,sn,rn[sn]])},onServerChange:function(e){tn=e},onConnectionStateChange:function(e){nn=e},CODE_CUSTOM_DISCONNECT:4e3,CONN_STATE_NOT_CONNECTED:0,CONN_STATE_DISCONNECTED:1,CONN_STATE_CLOSED:4,CONN_STATE_CONNECTED:2,CONN_STATE_CONNECTING:3},cn=e({name:"connection-overlay",emits:{reconnect:null},props:{connectionState:Number},computed:{lostConnection(){return this.connectionState===dn.CONN_STATE_DISCONNECTED},connecting(){return this.connectionState===dn.CONN_STATE_CONNECTING},show(){return!(!this.lostConnection&&!this.connecting)}}});const un={key:0,class:"overlay connection-lost"},pn={key:0,class:"overlay-content"},gn=n("div",null,"⁉️ LOST CONNECTION ⁉️",-1),hn={key:1,class:"overlay-content"},yn=n("div",null,"Connecting...",-1);cn.render=function(e,o,i,s,r,d){return e.show?(a(),t("div",un,[e.lostConnection?(a(),t("div",pn,[gn,n("span",{class:"btn",onClick:o[1]||(o[1]=t=>e.$emit("reconnect"))},"Reconnect")])):l("",!0),e.connecting?(a(),t("div",hn,[yn])):l("",!0)])):l("",!0)};var mn=e({name:"help-overlay",emits:{bgclick:null}});const fn=n("tr",null,[n("td",null,"⬆️ Move up:"),n("td",null,[n("div",null,[n("kbd",null,"W"),s("/"),n("kbd",null,"↑"),s("/πŸ–±οΈ")])])],-1),wn=n("tr",null,[n("td",null,"⬇️ Move down:"),n("td",null,[n("div",null,[n("kbd",null,"S"),s("/"),n("kbd",null,"↓"),s("/πŸ–±οΈ")])])],-1),vn=n("tr",null,[n("td",null,"⬅️ Move left:"),n("td",null,[n("div",null,[n("kbd",null,"A"),s("/"),n("kbd",null,"←"),s("/πŸ–±οΈ")])])],-1),bn=n("tr",null,[n("td",null,"➑️ Move right:"),n("td",null,[n("div",null,[n("kbd",null,"D"),s("/"),n("kbd",null,"β†’"),s("/πŸ–±οΈ")])])],-1),xn=n("tr",null,[n("td"),n("td",null,[n("div",null,[s("Move faster by holding "),n("kbd",null,"Shift")])])],-1),Cn=n("tr",null,[n("td",null,"πŸ”+ Zoom in:"),n("td",null,[n("div",null,[n("kbd",null,"E"),s("/πŸ–±οΈ-Wheel")])])],-1),An=n("tr",null,[n("td",null,"πŸ”- Zoom out:"),n("td",null,[n("div",null,[n("kbd",null,"Q"),s("/πŸ–±οΈ-Wheel")])])],-1),kn=n("tr",null,[n("td",null,"πŸ–ΌοΈ Toggle preview:"),n("td",null,[n("div",null,[n("kbd",null,"Space")])])],-1),Tn=n("tr",null,[n("td",null,"πŸ§©βœ”οΈ Toggle fixed pieces:"),n("td",null,[n("div",null,[n("kbd",null,"F")])])],-1),zn=n("tr",null,[n("td",null,"πŸ§©β“ Toggle loose pieces:"),n("td",null,[n("div",null,[n("kbd",null,"G")])])],-1);mn.render=function(e,o,l,i,s,r){return a(),t("div",{class:"overlay transparent",onClick:o[2]||(o[2]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content help",onClick:o[1]||(o[1]=g((()=>{}),["stop"]))},[fn,wn,vn,bn,xn,Cn,An,kn,Tn,zn])])};var Sn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAW0lEQVQ4je1RywrAIAxLxP//5exixRWlVgZelpOKeTQFfnDypgy3eLIkSLLL8mxGPoHsU2hPAgDHBLvRX6hZZw/fwT0BtlLSONqCbWAmEIqMZOCDDlaDR3N03gOyDCn+y4DWmAAAAABJRU5ErkJggg=="}),In=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAARElEQVQ4jWNgGAU0Af+hmBCbgYGBgYERhwHEAEYGBgYGJtIdiApYyLAZBVDsAqoagC1ACQJyY4ERg0GCISh6KA4DigEAou8LC+LnIJoAAAAASUVORK5CYII="}),Pn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAcUlEQVQ4ja1TQQ7AIAgD///n7jCozA2Hbk00jbG1KIrcARszTugoBs49qioZj7r2kKACptkyAOCJsJuA+GzglwHjvMSSWFVaENWVASxh5eRLiq5fN/ASjI89VcP2K3hHpq1cEXNaMfnrL3TDZP2tDuoOA6MzCCXWr38AAAAASUVORK5CYII="}),_n=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAU0lEQVQ4jWNgoAH4D8X42HDARKlt5BoAd82AuQAOGLGIYQQUPv0wF5CiCQUge4EsQ5C9QI4BjMguwBYeBAElscCIy1ZivMKIwSDBEBQ9FCckigEAU3QOD7TGvY4AAAAASUVORK5CYII="});function Dn(){let e=0,t=0,n=1;const o=(o,l)=>{e+=o/n,t+=l/n},l=e=>{const t=n+.05*n*("in"===e?1:-1);return Math.min(Math.max(t,.1),6)},i=o=>({x:o.x/n-e,y:o.y/n-t}),a=o=>({x:(o.x+e)*n,y:(o.y+t)*n}),s=e=>({w:e.w*n,h:e.h*n});return{move:o,canZoom:e=>n!=l(e),zoom:(e,t)=>((e,t)=>{if(n==e)return!1;const l=1-n/e;return o(-t.x*l,-t.y*l),n=e,!0})(l(e),t),worldToViewport:e=>{const{x:t,y:n}=a(e);return{x:Math.round(t),y:Math.round(n)}},worldToViewportRaw:a,worldDimToViewport:e=>{const{w:t,h:n}=s(e);return{w:Math.round(t),h:Math.round(n)}},worldDimToViewportRaw:s,viewportToWorld:e=>{const{x:t,y:n}=i(e);return{x:Math.round(t),y:Math.round(n)}},viewportToWorldRaw:i}}function Bn(e=0,t=0){const n=document.createElement("canvas");return n.width=e,n.height=t,n}var On={createCanvas:Bn,loadImageToBitmap:async function(e){return new Promise((t=>{const n=new Image;n.onload=()=>{createImageBitmap(n).then(t)},n.src=e}))},resizeBitmap:async function(e,t,n){const o=Bn(t,n);return o.getContext("2d").drawImage(e,0,0,e.width,e.height,0,0,t,n),await createImageBitmap(o)},colorize:async function(e,t,n){const o=Bn(e.width,e.height),l=o.getContext("2d");return l.save(),l.drawImage(t,0,0),l.fillStyle=n,l.globalCompositeOperation="source-in",l.fillRect(0,0,t.width,t.height),l.restore(),l.save(),l.globalCompositeOperation="destination-over",l.drawImage(e,0,0),l.restore(),await createImageBitmap(o)}};const En=De("Debug.js");let Mn=0,Nn=0;var Un=e=>{Mn=performance.now(),Nn=e},Gn=e=>{const t=performance.now(),n=t-Mn;n>Nn&&En.log(e+": "+n),Mn=t};const Rn=De("PuzzleGraphics.js");function $n(e,t){const n=Ne.coordByTileIdx(e,t);return{x:n.x*e.tileSize,y:n.y*e.tileSize,w:e.tileSize,h:e.tileSize}}var Vn={loadPuzzleBitmaps:async function(e){const t=await On.loadImageToBitmap(e.info.imageUrl),n=await On.resizeBitmap(t,e.info.width,e.info.height);return await async function(e,t,n){Rn.log("start createPuzzleTileBitmaps");var o=n.tileSize,l=n.tileMarginWidth,i=n.tileDrawSize,a=o/100,s=[0,0,40,15,37,5,37,5,40,0,38,-5,38,-5,20,-20,50,-20,50,-20,80,-20,62,-5,62,-5,60,0,63,5,63,5,65,15,100,0];const r=new Array(t.length),d={};function c(e){const t=`${e.top}${e.right}${e.left}${e.bottom}`;if(d[t])return d[t];const n=new Path2D,i={x:l,y:l},r=ae.pointAdd(i,{x:o,y:0}),c=ae.pointAdd(r,{x:0,y:o}),u=ae.pointSub(c,{x:o,y:0});if(n.moveTo(i.x,i.y),0!==e.top)for(let o=0;o=0&&e&&this.explode(e),this.px+=this.vx,this.py+=this.vy}draw(e){e.beginPath(),e.arc(this.px,this.py,this.previousRadius,0,2*Math.PI,!1),this.hasExploded||(e.fillStyle=this.color,e.lineWidth=1,e.fill())}explode(e){this.hasExploded=!0;const t=3+Math.floor(3*Math.random());for(let n=0;n{this.resize()}))}setSpeedParams(){let e=0,t=0;for(;e=0;)t+=1,e+=t;Wn=t/2,Ln=t-Wn;const n=1/4*this.canvas.width/(t/2);jn=-n,Fn=2*n}resize(){this.setSpeedParams()}init(){this.readyBombs=[],this.explodedBombs=[],this.particles=[];for(let e=0;e<1;e++)this.readyBombs.push(new Hn(this.rng))}update(){100*Math.random()<5&&this.readyBombs.push(new Hn(this.rng));const e=[];for(;this.explodedBombs.length>0;){const t=this.explodedBombs.shift();if(!t)break;t.update(),t.alive&&e.push(t)}this.explodedBombs=e;const t=[];for(;this.readyBombs.length>0;){const e=this.readyBombs.shift();if(!e)break;e.update(this.particles),e.hasExploded?this.explodedBombs.push(e):t.push(e)}this.readyBombs=t;const n=[];for(;this.particles.length>0;){const e=this.particles.shift();if(!e)break;e.update(),e.alive&&n.push(e)}this.particles=n}render(){this.ctx.beginPath(),this.ctx.fillStyle="rgba(0, 0, 0, 0.1)",this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height);for(let e=0;e{const t=e.color+" "+e.d;if(!h[t]){const n=e.d?a:s;if(e.color){const o=e.d?r:d;h[t]=await On.colorize(n,o,e.color)}else h[t]=n}return h[t]},m=function(e,t){return t.width=window.innerWidth,t.height=window.innerHeight,e.appendChild(t),window.addEventListener("resize",(()=>{t.width=window.innerWidth,t.height=window.innerHeight,Jn=!0})),t}(l,On.createCanvas()),f={log:[],logIdx:0,speeds:[.5,1,2,5,10,20,50],speedIdx:1,paused:!1,lastRealTs:0,lastGameTs:0,gameStartTs:0};dn.onConnectionStateChange((e=>{i.setConnectionState(e)}));let w=()=>0;const v=async()=>{if("play"===o){const o=await dn.connect(n,e,t),l=Ne.decodeGame(o);wt.setGame(l.id,l),w=()=>P()}else{if("replay"!==o)throw"[ 2020-12-22 MODE invalid, must be play|replay ]";{const o=await dn.connectReplay(n,e,t),l=Ne.decodeGame(o.game);wt.setGame(l.id,l),f.log=o.log,f.lastRealTs=P(),f.gameStartTs=parseInt(f.log[0][f.log[0].length-2],10),f.lastGameTs=f.gameStartTs,w=()=>f.lastGameTs}}Jn=!0};await v();const b=wt.getTileDrawOffset(e),x=wt.getTileDrawSize(e),C=wt.getPuzzleWidth(e),A=wt.getPuzzleHeight(e),k=wt.getTableWidth(e),T=wt.getTableHeight(e),z={x:(k-C)/2,y:(T-A)/2},S={w:C,h:A},I={w:x,h:x},_=await Vn.loadPuzzleBitmaps(wt.getPuzzle(e)),D=new Yn(m,wt.getRng(e));D.init();const B=m.getContext("2d");m.classList.add("loaded");const O=Dn();O.move(-(k-m.width)/2,-(T-m.height)/2);const E=function(e,t,n){let o=[],l=!0,i=!1,a=!1,s=!1,r=!1,d=!1,c=!1,u=!1;const p=(e,t)=>{const o=n.viewportToWorld({x:e,y:t});return[o.x,o.y]},g=e=>p(e.offsetX,e.offsetY),h=()=>p(e.width/2,e.height/2),y=(e,t)=>{l&&("Shift"===t.key?u=e:"ArrowUp"===t.key||"w"===t.key||"W"===t.key?s=e:"ArrowDown"===t.key||"s"===t.key||"S"===t.key?r=e:"ArrowLeft"===t.key||"a"===t.key||"A"===t.key?i=e:"ArrowRight"===t.key||"d"===t.key||"D"===t.key?a=e:"q"===t.key?c=e:"e"===t.key&&(d=e))};e.addEventListener("mousedown",(e=>{0===e.button&&m([fe,...g(e)])})),e.addEventListener("mouseup",(e=>{0===e.button&&m([we,...g(e)])})),e.addEventListener("mousemove",(e=>{m([ve,...g(e)])})),e.addEventListener("wheel",(e=>{if(n.canZoom(e.deltaY<0?"in":"out")){const t=e.deltaY<0?be:xe;m([t,...g(e)])}})),t.addEventListener("keydown",(e=>y(!0,e))),t.addEventListener("keyup",(e=>y(!1,e))),t.addEventListener("keypress",(e=>{l&&(" "===e.key&&m([Te]),"F"!==e.key&&"f"!==e.key||(Zn=!Zn,Jn=!0),"G"!==e.key&&"g"!==e.key||(Kn=!Kn,Jn=!0))}));const m=e=>{o.push(e)};return{addEvent:m,consumeAll:()=>{if(0===o.length)return[];const e=o.slice();return o=[],e},createKeyEvents:()=>{const e=u?20:10,t=(i?e:0)-(a?e:0),o=(s?e:0)-(r?e:0);0===t&&0===o||m([me,t,o]),d&&c||(d?n.canZoom("in")&&m([be,...h()]):c&&n.canZoom("out")&&m([xe,...h()]))},setHotkeys:e=>{l=e}}}(m,window,O),M=wt.getImageUrl(e),N=()=>{const t=wt.getStartTs(e),n=wt.getFinishTs(e),o=w();i.setFinished(!!n),i.setDuration((n||o)-t)};N(),i.setPiecesDone(wt.getFinishedTileCount(e)),i.setPiecesTotal(wt.getTileCount(e));const U=w();i.setActivePlayers(wt.getActivePlayers(e,U)),i.setIdlePlayers(wt.getIdlePlayers(e,U));const G=!!wt.getFinishTs(e);let R=G;const $=()=>R&&!G,V=()=>wt.getPlayerBgColor(e,t)||localStorage.getItem("bg_color")||"#222222",j=()=>{i.setReplaySpeed&&i.setReplaySpeed(f.speeds[f.speedIdx]),i.setReplayPaused&&i.setReplayPaused(f.paused)};if("play"===o?setInterval(N,1e3):"replay"===o&&j(),"play"===o)dn.onServerChange((n=>{n[0],n[1],n[2];const o=n[3];for(const[l,i]of o)switch(l){case Ie:{const n=Ne.decodePlayer(i);n.id!==t&&(wt.setPlayer(e,n.id,n),Jn=!0)}break;case Se:{const t=Ne.decodeTile(i);wt.setTile(e,t.idx,t),Jn=!0}break;case ze:wt.setPuzzleData(e,i),Jn=!0}R=!!wt.getFinishTs(e)}));else if("replay"===o){let t=setInterval((()=>{const n=P();if(f.paused)return void(f.lastRealTs=n);const o=(n-f.lastRealTs)*f.speeds[f.speedIdx],l=f.lastGameTs+o;for(;;){if(f.paused)break;const n=f.logIdx+1;if(n>=f.log.length){clearInterval(t);break}const o=f.log[n],i=f.gameStartTs+o[o.length-1];if(i>l)break;const a=o.slice();if(a[0]===ge){const t=a[1];wt.addPlayer(e,t,i),Jn=!0}else if(a[0]===he){const t=wt.getPlayerIdByIndex(e,a[1]);if(!t)throw"[ 2021-05-17 player not found (update player) ]";wt.addPlayer(e,t,i),Jn=!0}else if(a[0]===ye){const t=wt.getPlayerIdByIndex(e,a[1]);if(!t)throw"[ 2021-05-17 player not found (handle input) ]";const n=a[2];wt.handleInput(e,t,n,i),Jn=!0}f.logIdx=n}f.lastRealTs=n,f.lastGameTs=l,N()}),50)}let F=null;return(e=>{const t=e.fps||60,n=e.slow||1,o=e.update,l=e.render,i=window.requestAnimationFrame,a=1/t,s=n*a;let r,d=0,c=window.performance.now();const u=()=>{for(r=window.performance.now(),d+=Math.min(1,(r-c)/1e3);d>s;)d-=s,o(a);l(d/n),c=r,i(u)};i(u)})({update:()=>{E.createKeyEvents();for(const n of E.consumeAll())if("play"===o){const o=n[0];if(o===me){const e=n[1],t=n[2];Jn=!0,O.move(e,t)}else if(o===ve){if(F&&!wt.getFirstOwnedTile(e,t)){const e={x:n[1],y:n[2]},t=O.worldToViewport(e),o=Math.round(t.x-F.x),l=Math.round(t.y-F.y);Jn=!0,O.move(o,l),F=t}}else if(o===fe){const e={x:n[1],y:n[2]};F=O.worldToViewport(e)}else if(o===we)F=null;else if(o===be){const e={x:n[1],y:n[2]};Jn=!0,O.zoom("in",O.worldToViewport(e))}else if(o===xe){const e={x:n[1],y:n[2]};Jn=!0,O.zoom("out",O.worldToViewport(e))}else o===Te&&i.togglePreview();const l=w();wt.handleInput(e,t,n,l).length>0&&(Jn=!0),dn.sendClientEvent(n)}else if("replay"===o){const e=n[0];if(e===me){const e=n[1],t=n[2];Jn=!0,O.move(e,t)}else if(e===ve){if(F){const e={x:n[1],y:n[2]},t=O.worldToViewport(e),o=Math.round(t.x-F.x),l=Math.round(t.y-F.y);Jn=!0,O.move(o,l),F=t}}else if(e===fe){const e={x:n[1],y:n[2]};F=O.worldToViewport(e)}else if(e===we)F=null;else if(e===be){const e={x:n[1],y:n[2]};Jn=!0,O.zoom("in",O.worldToViewport(e))}else if(e===xe){const e={x:n[1],y:n[2]};Jn=!0,O.zoom("out",O.worldToViewport(e))}else e===Te&&i.togglePreview()}R=!!wt.getFinishTs(e),$()&&(D.update(),Jn=!0)},render:async()=>{if(!Jn)return;const n=w();let l,a,s;window.DEBUG&&Un(0),B.fillStyle=V(),B.fillRect(0,0,m.width,m.height),window.DEBUG&&Gn("clear done"),l=O.worldToViewportRaw(z),a=O.worldDimToViewportRaw(S),B.fillStyle="rgba(255, 255, 255, .3)",B.fillRect(l.x,l.y,a.w,a.h),window.DEBUG&&Gn("board done");const r=wt.getTilesSortedByZIndex(e);window.DEBUG&&Gn("get tiles done"),a=O.worldDimToViewportRaw(I);for(const e of r)(-1===e.owner?Zn:Kn)&&(s=_[e.idx],l=O.worldToViewportRaw({x:b+e.pos.x,y:b+e.pos.y}),B.drawImage(s,0,0,s.width,s.height,l.x,l.y,a.w,a.h));window.DEBUG&&Gn("tiles done");const d=[];for(const i of wt.getActivePlayers(e,n))s=await y(i),l=O.worldToViewport(i),B.drawImage(s,l.x-u,l.y-g),c=i,("replay"===o||c.id!==t)&&d.push([`${i.name} (${i.points})`,l.x,l.y+p]);var c;B.fillStyle="white",B.textAlign="center";for(const[e,t,o]of d)B.fillText(e,t,o);window.DEBUG&&Gn("players done"),i.setActivePlayers(wt.getActivePlayers(e,n)),i.setIdlePlayers(wt.getIdlePlayers(e,n)),i.setPiecesDone(wt.getFinishedTileCount(e)),window.DEBUG&&Gn("HUD done"),$()&&D.render(),Jn=!1}}),{setHotkeys:e=>{E.setHotkeys(e)},onBgChange:e=>{localStorage.setItem("bg_color",e),E.addEvent([Ce,e])},onColorChange:e=>{localStorage.setItem("player_color",e),E.addEvent([Ae,e])},onNameChange:e=>{localStorage.setItem("player_name",e),E.addEvent([ke,e])},replayOnSpeedUp:()=>{f.speedIdx+1{f.speedIdx>=1&&(f.speedIdx--,j())},replayOnPauseToggle:()=>{f.paused=!f.paused,j()},previewImageUrl:M,player:{background:V(),color:wt.getPlayerColor(e,t)||localStorage.getItem("player_color")||"#ffffff",name:wt.getPlayerName(e,t)||localStorage.getItem("player_name")||"anon"},disconnect:dn.disconnect,connect:v}}var eo=e({name:"game",components:{PuzzleStatus:Lt,Scores:$t,SettingsOverlay:Qt,PreviewOverlay:Kt,ConnectionOverlay:cn,HelpOverlay:mn},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,g:{player:{background:"",color:"",name:""},previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},disconnect:()=>{},connect:()=>{}}}),async mounted(){this.$route.params.id&&(this.$watch((()=>this.g.player.background),(e=>{this.g.onBgChange(e)})),this.$watch((()=>this.g.player.color),(e=>{this.g.onColorChange(e)})),this.$watch((()=>this.g.player.name),(e=>{this.g.onNameChange(e)})),this.g=await Xn(`${this.$route.params.id}`,this.$clientId,this.$config.WS_ADDRESS,"play",this.$el,{setActivePlayers:e=>{this.activePlayers=e},setIdlePlayers:e=>{this.idlePlayers=e},setFinished:e=>{this.finished=e},setDuration:e=>{this.duration=e},setPiecesDone:e=>{this.piecesDone=e},setPiecesTotal:e=>{this.piecesTotal=e},setConnectionState:e=>{this.connectionState=e},togglePreview:()=>{this.toggle("preview",!1)}}))},unmounted(){this.g.disconnect()},methods:{reconnect(){this.g.connect()},toggle(e,t){""===this.overlay?(this.overlay=e,t&&this.g.setHotkeys(!1)):(this.overlay="",t&&this.g.setHotkeys(!0))}}});const to={id:"game"},no={class:"menu"},oo={class:"tabs"},lo=s("🧩 Puzzles");eo.render=function(e,l,s,r,d,c){const p=i("settings-overlay"),g=i("preview-overlay"),h=i("help-overlay"),y=i("connection-overlay"),m=i("puzzle-status"),w=i("router-link"),v=i("scores");return a(),t("div",to,[u(n(p,{onBgclick:l[1]||(l[1]=t=>e.toggle("settings",!0)),modelValue:e.g.player,"onUpdate:modelValue":l[2]||(l[2]=t=>e.g.player=t)},null,8,["modelValue"]),[[f,"settings"===e.overlay]]),u(n(g,{onBgclick:l[3]||(l[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[f,"preview"===e.overlay]]),u(n(h,{onBgclick:l[4]||(l[4]=t=>e.toggle("help",!0))},null,512),[[f,"help"===e.overlay]]),n(y,{connectionState:e.connectionState,onReconnect:e.reconnect},null,8,["connectionState","onReconnect"]),n(m,{finished:e.finished,duration:e.duration,piecesDone:e.piecesDone,piecesTotal:e.piecesTotal},null,8,["finished","duration","piecesDone","piecesTotal"]),n("div",no,[n("div",oo,[n(w,{class:"opener",to:{name:"index"},target:"_blank"},{default:o((()=>[lo])),_:1}),n("div",{class:"opener",onClick:l[5]||(l[5]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:l[6]||(l[6]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:l[7]||(l[7]=t=>e.toggle("help",!0))},"ℹ️ Help")])]),n(v,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])};var io=e({name:"replay",components:{PuzzleStatus:Lt,Scores:$t,SettingsOverlay:Qt,PreviewOverlay:Kt,HelpOverlay:mn},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,g:{player:{background:"",color:"",name:""},previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},replayOnSpeedUp:()=>{},replayOnSpeedDown:()=>{},replayOnPauseToggle:()=>{},disconnect:()=>{}},replay:{speed:1,paused:!1}}),async mounted(){this.$route.params.id&&(this.$watch((()=>this.g.player.background),(e=>{this.g.onBgChange(e)})),this.$watch((()=>this.g.player.color),(e=>{this.g.onColorChange(e)})),this.$watch((()=>this.g.player.name),(e=>{this.g.onNameChange(e)})),this.g=await Xn(`${this.$route.params.id}`,this.$clientId,this.$config.WS_ADDRESS,"replay",this.$el,{setActivePlayers:e=>{this.activePlayers=e},setIdlePlayers:e=>{this.idlePlayers=e},setFinished:e=>{this.finished=e},setDuration:e=>{this.duration=e},setPiecesDone:e=>{this.piecesDone=e},setPiecesTotal:e=>{this.piecesTotal=e},togglePreview:()=>{this.toggle("preview",!1)},setConnectionState:e=>{this.connectionState=e},setReplaySpeed:e=>{this.replay.speed=e},setReplayPaused:e=>{this.replay.paused=e}}))},unmounted(){this.g.disconnect()},methods:{toggle(e,t){""===this.overlay?(this.overlay=e,t&&this.g.setHotkeys(!1)):(this.overlay="",t&&this.g.setHotkeys(!0))}},computed:{replayText(){return"Replay-Speed: "+this.replay.speed+"x"+(this.replay.paused?" Paused":"")}}});const ao={id:"replay"},so={class:"menu"},ro={class:"tabs"},co=s("🧩 Puzzles");io.render=function(e,l,s,d,c,p){const g=i("settings-overlay"),h=i("preview-overlay"),y=i("help-overlay"),m=i("puzzle-status"),w=i("router-link"),v=i("scores");return a(),t("div",ao,[u(n(g,{onBgclick:l[1]||(l[1]=t=>e.toggle("settings",!0)),modelValue:e.g.player,"onUpdate:modelValue":l[2]||(l[2]=t=>e.g.player=t)},null,8,["modelValue"]),[[f,"settings"===e.overlay]]),u(n(h,{onBgclick:l[3]||(l[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[f,"preview"===e.overlay]]),u(n(y,{onBgclick:l[4]||(l[4]=t=>e.toggle("help",!0))},null,512),[[f,"help"===e.overlay]]),n(m,{finished:e.finished,duration:e.duration,piecesDone:e.piecesDone,piecesTotal:e.piecesTotal},{default:o((()=>[n("div",null,[n("div",null,r(e.replayText),1),n("button",{class:"btn",onClick:l[5]||(l[5]=t=>e.g.replayOnSpeedUp())},"⏫"),n("button",{class:"btn",onClick:l[6]||(l[6]=t=>e.g.replayOnSpeedDown())},"⏬"),n("button",{class:"btn",onClick:l[7]||(l[7]=t=>e.g.replayOnPauseToggle())},"⏸️")])])),_:1},8,["finished","duration","piecesDone","piecesTotal"]),n("div",so,[n("div",ro,[n(w,{class:"opener",to:{name:"index"},target:"_blank"},{default:o((()=>[co])),_:1}),n("div",{class:"opener",onClick:l[8]||(l[8]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:l[9]||(l[9]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:l[10]||(l[10]=t=>e.toggle("help",!0))},"ℹ️ Help")])]),n(v,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])},(async()=>{const e=await fetch("/api/conf"),t=await e.json();const n=w({history:v(),routes:[{name:"index",path:"/",component:U},{name:"new-game",path:"/new-game",component:Pt},{name:"game",path:"/g/:id",component:eo},{name:"replay",path:"/replay/:id",component:io}]});n.beforeEach(((e,t)=>{t.name&&document.documentElement.classList.remove(`view-${String(t.name)}`),document.documentElement.classList.add(`view-${String(e.name)}`)}));const o=b(x);o.config.globalProperties.$config=t,o.config.globalProperties.$clientId=function(){let e=localStorage.getItem("ID");return e||(e=Ne.uniqId(),localStorage.setItem("ID",e)),e}(),o.use(n),o.mount("#app")})(); diff --git a/build/public/assets/index.b49dadca.js b/build/public/assets/index.b49dadca.js deleted file mode 100644 index c024c33..0000000 --- a/build/public/assets/index.b49dadca.js +++ /dev/null @@ -1 +0,0 @@ -import{d as e,c as t,a as n,w as o,b as l,r as i,o as a,e as s,t as r,F as d,f as c,g as u,v as g,h as p,i as h,j as m,k as y,l as f,m as w,n as v,p as x}from"./vendor.8616a479.js";var b=e({name:"app",computed:{showNav(){return!["game","replay"].includes(String(this.$route.name))}}});const A={id:"app"},C={key:0,class:"nav"},k=s("Index"),S=s("New game");b.render=function(e,s,r,d,c,u){const g=i("router-link"),p=i("router-view");return a(),t("div",A,[e.showNav?(a(),t("ul",C,[n("li",null,[n(g,{class:"btn",to:{name:"index"}},{default:o((()=>[k])),_:1})]),n("li",null,[n(g,{class:"btn",to:{name:"new-game"}},{default:o((()=>[S])),_:1})])])):l("",!0),n(p)])};const T=864e5,z=e=>{const t=Math.floor(e/T);e%=T;const n=Math.floor(e/36e5);e%=36e5;const o=Math.floor(e/6e4);e%=6e4;return`${t}d ${n}h ${o}m ${Math.floor(e/1e3)}s`};var I=1e3,P=()=>{const e=new Date;return Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),e.getUTCMilliseconds())},_=(e,t)=>z(t-e),D=z,B=e({name:"game-teaser",props:{game:{type:Object,required:!0}},computed:{style(){return{"background-image":`url("${this.game.imageUrl.replace("uploads/","uploads/r/")+"-375x210.webp"}")`}}},methods:{time(e,t){const n=t?"🏁":"⏳",o=e,l=t||P();return`${n} ${_(o,l)}`}}});const O={class:"game-info-text"},E=n("br",null,null,-1),M=n("br",null,null,-1),N=n("br",null,null,-1);B.render=function(e,d,c,u,g,p){const h=i("router-link");return a(),t("div",{class:"game-teaser",style:e.style},[n(h,{class:"game-info",to:{name:"game",params:{id:e.game.id}}},{default:o((()=>[n("span",O,[s(" 🧩 "+r(e.game.tilesFinished)+"/"+r(e.game.tilesTotal),1),E,s(" πŸ‘₯ "+r(e.game.players),1),M,s(" "+r(e.time(e.game.started,e.game.finished)),1),N])])),_:1},8,["to"]),l("",!0)],4)};var U=e({components:{GameTeaser:B},data:()=>({gamesRunning:[],gamesFinished:[]}),async created(){const e=await fetch("/api/index-data"),t=await e.json();this.gamesRunning=t.gamesRunning,this.gamesFinished=t.gamesFinished}});const G=n("h1",null,"Running games",-1),R=n("h1",null,"Finished games",-1);U.render=function(e,o,l,s,r,u){const g=i("game-teaser");return a(),t("div",null,[G,(a(!0),t(d,null,c(e.gamesRunning,((e,o)=>(a(),t("div",{class:"game-teaser-wrap",key:o},[n(g,{game:e},null,8,["game"])])))),128)),R,(a(!0),t(d,null,c(e.gamesFinished,((e,o)=>(a(),t("div",{class:"game-teaser-wrap",key:o},[n(g,{game:e},null,8,["game"])])))),128))])};var $=e({name:"image-teaser",props:{image:{type:Object,required:!0}},computed:{style(){return{backgroundImage:`url("${this.image.url.replace("uploads/","uploads/r/")+"-150x100.webp"}")`}}},methods:{onClick(){this.$emit("click")}}});$.render=function(e,n,o,l,i,s){return a(),t("div",{class:"imageteaser",style:e.style,onClick:n[1]||(n[1]=(...t)=>e.onClick&&e.onClick(...t))},null,4)};var V=e({name:"image-library",components:{ImageTeaser:$},props:{images:{type:Array,required:!0}},emits:{imageClicked:null},methods:{imageClicked(e){this.$emit("imageClicked",e)}}});V.render=function(e,n,o,l,s,r){const u=i("image-teaser");return a(),t("div",null,[(a(!0),t(d,null,c(e.images,((n,o)=>(a(),t(u,{image:n,onClick:t=>e.imageClicked(n),key:o},null,8,["image","onClick"])))),128))])};var j=e({name:"upload",props:{accept:String,label:String},methods:{async upload(e){const t=e.target;if(!t.files)return;const n=t.files[0];if(!n)return;const o=new FormData;o.append("file",n,n.name);const l=await fetch("/upload",{method:"post",body:o}),i=await l.json();this.$emit("uploaded",i)}}});const F={class:"btn"};j.render=function(e,o,l,i,s,d){return a(),t("label",null,[n("input",{type:"file",style:{display:"none"},onChange:o[1]||(o[1]=(...t)=>e.upload&&e.upload(...t)),accept:e.accept},null,40,["accept"]),n("span",F,r(e.label||"Upload File"),1)])};const W={name:"responsive-image",props:{src:String,title:{type:String,default:""},height:{type:String,default:"100%"},width:{type:String,default:"100%"}},computed:{style(){return{display:"inline-block",verticalAlign:"text-bottom",backgroundImage:`url('${this.src}')`,backgroundRepeat:"no-repeat",backgroundSize:"contain",backgroundPosition:"center",width:this.width,height:this.height}}}};W.render=function(e,n,o,l,i,s){return a(),t("div",{style:s.style,title:o.title},null,12,["title"])};var L=e({name:"new-image-dialog",components:{Upload:j,ResponsiveImage:W},emits:{bgclick:null,setupGameClick:null},data:()=>({image:{file:"",url:"",title:"",category:""}}),computed:{canPostToGallery(){return!!this.image.url},canSetupGameClick(){return!!this.image.url}},methods:{mediaImgUploaded(e){this.image.file=e.image.file,this.image.url=e.image.url},postToGallery(){this.$emit("postToGallery",this.image)},setupGameClick(){this.$emit("setupGameClick",this.image)}}});const H={key:0,class:"has-image"},Q={key:1},Y={class:"area-settings"},q=n("td",null,[n("label",null,"Title")],-1),Z=n("tr",null,[n("td",{colspan:"2"},[n("div",{class:"hint"},"Feel free to leave a credit to the artist/photographer in the title :)")])],-1),K=n("td",null,[n("label",null,"Category")],-1),J={class:"area-buttons"},X=s("🧩 Post to gallery "),ee=n("br",null,null,-1),te=s(" + set up game");function ne(e,t){const n=e.x-t.x,o=e.y-t.y;return Math.sqrt(n*n+o*o)}function oe(e){return{x:e.x+e.w/2,y:e.y+e.h/2}}L.render=function(e,o,l,s,r,d){const c=i("responsive-image"),h=i("upload");return a(),t("div",{class:"overlay new-image-dialog",onClick:o[7]||(o[7]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:o[6]||(o[6]=p((()=>{}),["stop"]))},[n("div",{class:["area-image",{"has-image":!!e.image.url,"no-image":!e.image.url}]},[e.image.url?(a(),t("div",H,[n("span",{class:"remove btn",onClick:o[1]||(o[1]=t=>e.image.url="")},"X"),n(c,{src:e.image.url},null,8,["src"])])):(a(),t("div",Q,[n(h,{class:"upload",onUploaded:o[2]||(o[2]=t=>e.mediaImgUploaded(t)),accept:"image/*",label:"Upload an image"})]))],2),n("div",Y,[n("table",null,[n("tr",null,[q,n("td",null,[u(n("input",{type:"text","onUpdate:modelValue":o[3]||(o[3]=t=>e.image.title=t),placeholder:"Flower by @artist"},null,512),[[g,e.image.title]])])]),Z,n("tr",null,[K,n("td",null,[u(n("input",{type:"text","onUpdate:modelValue":o[4]||(o[4]=t=>e.image.category=t),placeholder:"Plants"},null,512),[[g,e.image.category]])])])])]),n("div",J,[n("button",{class:"btn",disabled:!e.canSetupGameClick,onClick:o[5]||(o[5]=(...t)=>e.setupGameClick&&e.setupGameClick(...t))},[X,ee,te],8,["disabled"])])])])};var le={pointSub:function(e,t){return{x:e.x-t.x,y:e.y-t.y}},pointAdd:function(e,t){return{x:e.x+t.x,y:e.y+t.y}},pointDistance:ne,pointInBounds:function(e,t){return e.x>=t.x&&e.x<=t.x+t.w&&e.y>=t.y&&e.y<=t.y+t.h},rectCenter:oe,rectMoved:function(e,t,n){return{x:e.x+t,y:e.y+n,w:e.w,h:e.h}},rectCenterDistance:function(e,t){return ne(oe(e),oe(t))},rectsOverlap:function(e,t){return!(t.x>e.x+e.w||e.x>t.x+t.w||t.y>e.y+e.h||e.y>t.y+t.h)}};var ie=1,ae=4,se=5,re=2,de=3,ce=6,ue=2,ge=4,pe=3,he=9,me=1,ye=2,fe=3,we=4,ve=5,xe=6,be=7,Ae=8,Ce=10,ke=1,Se=2,Te=3;class ze{constructor(e){this.rand_high=e||3735929054,this.rand_low=1231121986^e}random(e,t){return this.rand_high=(this.rand_high<<16)+(this.rand_high>>16)+this.rand_low&4294967295,this.rand_low=this.rand_low+this.rand_high&4294967295,e+(this.rand_high>>>0)/4294967295*(t-e+1)|0}choice(e){return e[this.random(0,e.length-1)]}shuffle(e){const t=e.slice();for(let n=0;n<=t.length-2;n++){const e=this.random(n,t.length-1),o=t[n];t[n]=t[e],t[e]=o}return t}static serialize(e){return{rand_high:e.rand_high,rand_low:e.rand_low}}static unserialize(e){const t=new ze(0);return t.rand_high=e.rand_high,t.rand_low=e.rand_low,t}}const Ie=(e,t)=>{const n=`${e}`;return n.length>=t.length?n:t.substr(0,t.length-n.length)+n},Pe=(...e)=>{const t=t=>(...n)=>{const o=new Date,l=Ie(o.getHours(),"00"),i=Ie(o.getMinutes(),"00"),a=Ie(o.getSeconds(),"00");console[t](`${l}:${i}:${a}`,...e,...n)};return{log:t("log"),error:t("error"),info:t("info")}};var _e,De,Be,Oe,Ee={hash:e=>{let t=0;for(let n=0;nDate.now().toString(36)+Math.random().toString(36).substring(2),encodeShape:function(e){return e.top+1<<0|e.right+1<<2|e.bottom+1<<4|e.left+1<<6},decodeShape:function(e){return{top:(e>>0&3)-1,right:(e>>2&3)-1,bottom:(e>>4&3)-1,left:(e>>6&3)-1}},encodeTile:function(e){return[e.idx,e.pos.x,e.pos.y,e.z,e.owner,e.group]},decodeTile:function(e){return{idx:e[0],pos:{x:e[1],y:e[2]},z:e[3],owner:e[4],group:e[5]}},encodePlayer:function(e){return[e.id,e.x,e.y,e.d,e.name,e.color,e.bgcolor,e.points,e.ts]},decodePlayer:function(e){return{id:e[0],x:e[1],y:e[2],d:e[3],name:e[4],color:e[5],bgcolor:e[6],points:e[7],ts:e[8]}},encodeGame:function(e){return Array.isArray(e)?e:[e.id,e.rng.type,ze.serialize(e.rng.obj),e.puzzle,e.players,e.evtInfos,e.scoreMode]},decodeGame:function(e){return Array.isArray(e)?{id:e[0],rng:{type:e[1],obj:ze.unserialize(e[2])},puzzle:e[3],players:e[4],evtInfos:e[5],scoreMode:e[6]}:e},coordByTileIdx:function(e,t){const n=e.width/e.tileSize;return{x:t%n,y:Math.floor(t/n)}},asQueryArgs:function(e){const t=[];for(let n in e){const o=[n,e[n]].map(encodeURIComponent);t.push(o.join("="))}return 0===t.length?"":`?${t.join("&")}`}};(De=_e||(_e={}))[De.Flat=0]="Flat",De[De.Out=1]="Out",De[De.In=-1]="In",(Oe=Be||(Be={}))[Oe.FINAL=0]="FINAL",Oe[Oe.ANY=1]="ANY";const Me={};function Ne(e,t){return{id:e,x:0,y:0,d:0,name:null,color:null,bgcolor:null,points:0,ts:t}}function Ue(e,t){let n=0;for(let o of Me[e].players){if(Ee.decodePlayer(o).id===t)return n;n++}return-1}function Ge(e,t){const n=Ue(e,t);return Ee.decodePlayer(Me[e].players[n])}function Re(e,t,n){const o=Ue(e,t);-1===o?Me[e].players.push(Ee.encodePlayer(n)):Me[e].players[o]=Ee.encodePlayer(n)}function $e(e,t){return-1!==Ue(e,t)}function Ve(e){return Me[e]?Me[e].players.map(Ee.decodePlayer):[]}function je(e){return Me[e].puzzle.tiles.length}function Fe(e){return Me[e].scoreMode||0}function We(e){return Le(e)===je(e)}function Le(e){let t=0;for(let n of Me[e].puzzle.tiles)-1===Ee.decodeTile(n).owner&&t++;return t}function He(e,t,n){const o=Ge(e,t);for(let l of Object.keys(n))o[l]=n[l];Re(e,t,o)}function Qe(e,t){for(let n of Object.keys(t))Me[e].puzzle.data[n]=t[n]}function Ye(e,t,n){for(let o of Object.keys(n)){const l=Ee.decodeTile(Me[e].puzzle.tiles[t]);l[o]=n[o],Me[e].puzzle.tiles[t]=Ee.encodeTile(l)}}const qe=(e,t)=>Ee.decodeTile(Me[e].puzzle.tiles[t]),Ze=(e,t)=>qe(e,t).group,Ke=(e,t)=>{const n=Me[e].puzzle.info,o={x:(n.table.width-n.width)/2,y:(n.table.height-n.height)/2},l=function(e,t){const n=Me[e].puzzle.info,o=Ee.coordByTileIdx(n,t),l=o.x*n.tileSize,i=o.y*n.tileSize;return{x:l,y:i}}(e,t);return le.pointAdd(o,l)},Je=(e,t)=>qe(e,t).pos,Xe=e=>{const t=ht(e),n=mt(e),o=Math.round(t/4),l=Math.round(n/4);return{x:0-o,y:0-l,w:t+2*o,h:n+2*l}},et=(e,t)=>{const n=lt(e),o=qe(e,t);return{x:o.pos.x,y:o.pos.y,w:n,h:n}},tt=(e,t)=>qe(e,t).z,nt=(e,t)=>{for(let n of Me[e].puzzle.tiles){const e=Ee.decodeTile(n);if(e.owner===t)return e.idx}return-1},ot=e=>Me[e].puzzle.info.tileDrawSize,lt=e=>Me[e].puzzle.info.tileSize,it=e=>Me[e].puzzle.data.maxGroup,at=e=>Me[e].puzzle.data.maxZ;function st(e,t){const n=Me[e].puzzle.info,o=Ee.coordByTileIdx(n,t);return[o.y>0?t-n.tilesX:-1,o.x0?t-1:-1]}const rt=(e,t,n)=>{for(let o of t)Ye(e,o,{z:n})},dt=(e,t,n)=>{const o=Je(e,t);Ye(e,t,{pos:le.pointAdd(o,n)})},ct=(e,t,n)=>{const o=ot(e),l=Xe(e),i=n;for(let a of t){const t=qe(e,a);t.pos.x+n.xl.x+l.w&&(i.x=Math.min(l.x+l.w-t.pos.x+o,i.x)),t.pos.y+n.yl.y+l.h&&(i.y=Math.min(l.y+l.h-t.pos.y+o,i.y))}for(let a of t)dt(e,a,i)},ut=(e,t,n)=>{for(let o of t)Ye(e,o,{owner:n})};function gt(e,t){const n=Me[e].puzzle.tiles,o=Ee.decodeTile(n[t]),l=[];if(o.group)for(let i of n){const e=Ee.decodeTile(i);e.group===o.group&&l.push(e.idx)}else l.push(o.idx);return l}const pt=(e,t)=>{const n=Ge(e,t);return n?n.points:0},ht=e=>Me[e].puzzle.info.table.width,mt=e=>Me[e].puzzle.info.table.height;var yt={__createPlayerObject:Ne,setGame:function(e,t){Me[e]=t},exists:function(e){return!!Me[e]||!1},playerExists:$e,getActivePlayers:function(e,t){const n=t-30*I;return Ve(e).filter((e=>e.ts>=n))},getIdlePlayers:function(e,t){const n=t-30*I;return Ve(e).filter((e=>e.ts0))},addPlayer:function(e,t,n){$e(e,t)?He(e,t,{ts:n}):Re(e,t,Ne(t,n))},getFinishedTileCount:Le,getTileCount:je,getImageUrl:function(e){return Me[e].puzzle.info.imageUrl},setImageUrl:function(e,t){Me[e].puzzle.info.imageUrl=t},get:function(e){return Me[e]},getAllGames:function(){return Object.values(Me).sort(((e,t)=>We(e.id)===We(t.id)?t.puzzle.data.started-e.puzzle.data.started:We(e.id)?1:-1))},getPlayerBgColor:(e,t)=>{const n=Ge(e,t);return n?n.bgcolor:null},getPlayerColor:(e,t)=>{const n=Ge(e,t);return n?n.color:null},getPlayerName:(e,t)=>{const n=Ge(e,t);return n?n.name:null},getPlayerIndexById:Ue,getPlayerIdByIndex:function(e,t){return Me[e].players.length>t?Ee.decodePlayer(Me[e].players[t]).id:null},changePlayer:He,setPlayer:Re,setTile:function(e,t,n){Me[e].puzzle.tiles[t]=Ee.encodeTile(n)},setPuzzleData:function(e,t){Me[e].puzzle.data=t},getTableWidth:ht,getTableHeight:mt,getPuzzle:e=>Me[e].puzzle,getRng:e=>Me[e].rng.obj,getPuzzleWidth:e=>Me[e].puzzle.info.width,getPuzzleHeight:e=>Me[e].puzzle.info.height,getTilesSortedByZIndex:function(e){return Me[e].puzzle.tiles.map(Ee.decodeTile).sort(((e,t)=>e.z-t.z))},getFirstOwnedTile:(e,t)=>{const n=nt(e,t);return n<0?null:Me[e].puzzle.tiles[n]},getTileDrawOffset:e=>Me[e].puzzle.info.tileDrawOffset,getTileDrawSize:ot,getFinalTilePos:Ke,getStartTs:e=>Me[e].puzzle.data.started,getFinishTs:e=>Me[e].puzzle.data.finished,handleInput:function(e,t,n,o){const l=Me[e].puzzle,i=function(e,t){return t in Me[e].evtInfos?Me[e].evtInfos[t]:{_last_mouse:null,_last_mouse_down:null}}(e,t),a=[],s=()=>{a.push([ke,l.data])},r=t=>{a.push([Se,Ee.encodeTile(qe(e,t))])},d=e=>{for(const t of e)r(t)},c=()=>{a.push([Te,Ee.encodePlayer(Ge(e,t))])},u=n[0];if(u===xe){const l=n[1];He(e,t,{bgcolor:l,ts:o}),c()}else if(u===be){const l=n[1];He(e,t,{color:l,ts:o}),c()}else if(u===Ae){const l=`${n[1]}`.substr(0,16);He(e,t,{name:l,ts:o}),c()}else if(u===me){const l={x:n[1],y:n[2]};He(e,t,{d:1,ts:o}),c(),i._last_mouse_down=l;const a=((e,t)=>{let n=Me[e].puzzle.info,o=Me[e].puzzle.tiles,l=-1,i=-1;for(let a=0;al)&&(l=e.z,i=a)}return i})(e,l);if(a>=0){let n=at(e)+1;Qe(e,{maxZ:n}),s();const o=gt(e,a);rt(e,o,at(e)),ut(e,o,t),d(o)}i._last_mouse=l}else if(u===fe){const l=n[1],a=n[2],s={x:l,y:a};if(null===i._last_mouse_down)He(e,t,{x:l,y:a,ts:o}),c();else{let n=nt(e,t);if(n>=0){He(e,t,{x:l,y:a,ts:o}),c();const r=gt(e,n);let u=le.pointInBounds(s,Xe(e))&&le.pointInBounds(i._last_mouse_down,Xe(e));for(let t of r){const n=et(e,t);if(le.pointInBounds(s,n)){u=!0;break}}if(u){const t=l-i._last_mouse_down.x,n=a-i._last_mouse_down.y;ct(e,r,{x:t,y:n}),d(r)}}else He(e,t,{ts:o}),c();i._last_mouse_down=s}i._last_mouse=s}else if(u===ye){const a={x:n[1],y:n[2]},u=0;i._last_mouse_down=null;let g=nt(e,t);if(g>=0){let n=gt(e,g);ut(e,n,0),d(n);let i=Je(e,g),a=Ke(e,g);if(le.pointDistance(a,i){for(let n of t)Ye(e,n,{owner:-1,z:1})})(e,n),d(n);let r=pt(e,t);0===Fe(e)?r+=n.length:1===Fe(e)&&(r+=1),He(e,t,{d:u,ts:o,points:r}),c(),Le(e)===je(e)&&(Qe(e,{finished:o}),s())}else{const n=(e,t,n,o)=>{let l=Me[e].puzzle.info;if(n<0)return!1;if(((e,t,n)=>{const o=Ze(e,t),l=Ze(e,n);return o&&o===l})(e,t,n))return!1;const i=Je(e,t),a=le.pointAdd(Je(e,n),{x:o[0]*l.tileSize,y:o[1]*l.tileSize});if(le.pointDistance(i,a){const o=Me[e].puzzle.tiles,l=Ze(e,t),i=Ze(e,n);let a;const d=[];l&&d.push(l),i&&d.push(i),l?a=l:i?a=i:(Qe(e,{maxGroup:it(e)+1}),s(),a=it(e));if(Ye(e,t,{group:a}),r(t),Ye(e,n,{group:a}),r(n),d.length>0)for(const s of o){const t=Ee.decodeTile(s);d.includes(t.group)&&(Ye(e,t.idx,{group:a}),r(t.idx))}})(e,t,n),l=gt(e,t);const c=((e,t)=>{let n=0;for(let o of t){let t=tt(e,o);t>n&&(n=t)}return n})(e,l);return rt(e,l,c),d(l),!0}return!1};let l=!1;for(let t of gt(e,g)){let o=st(e,t);if(n(e,t,o[0],[0,1])||n(e,t,o[1],[-1,0])||n(e,t,o[2],[0,-1])||n(e,t,o[3],[1,0])){l=!0;break}}if(l&&1===Fe(e)){const n=pt(e,t)+1;He(e,t,{d:u,ts:o,points:n}),c()}else He(e,t,{d:u,ts:o}),c()}}else He(e,t,{d:u,ts:o}),c();i._last_mouse=a}else if(u===we){const l=n[1],a=n[2];He(e,t,{x:l,y:a,ts:o}),c(),i._last_mouse={x:l,y:a}}else if(u===ve){const l=n[1],a=n[2];He(e,t,{x:l,y:a,ts:o}),c(),i._last_mouse={x:l,y:a}}else He(e,t,{ts:o}),c();return function(e,t,n){Me[e].evtInfos[t]=n}(e,t,i),a}},ft=e({name:"new-game-dialog",components:{ResponsiveImage:W},props:{image:{type:Object,required:!0}},emits:{newGame:null,bgclick:null},data:()=>({tiles:1e3,scoreMode:Be.ANY}),methods:{onNewGameClick(){this.$emit("newGame",{tiles:this.tilesInt,image:this.image,scoreMode:this.scoreModeInt})}},computed:{canStartNewGame(){return!!(this.tilesInt&&this.image&&this.image.url&&[0,1].includes(this.scoreModeInt))},scoreModeInt(){return parseInt(`${this.scoreMode}`,10)},tilesInt(){return parseInt(`${this.tiles}`,10)}}});const wt={class:"area-image"},vt={class:"has-image"},xt={class:"area-settings"},bt=n("td",null,[n("label",null,"Pieces")],-1),At=n("td",null,[n("label",null,"Scoring: ")],-1),Ct=s(" Any (Score when pieces are connected to each other or on final location)"),kt=n("br",null,null,-1),St=s(" Final (Score when pieces are put to their final location)"),Tt={class:"area-buttons"};ft.render=function(e,o,l,s,r,d){const c=i("responsive-image");return a(),t("div",{class:"overlay new-game-dialog",onClick:o[6]||(o[6]=t=>e.$emit("bgclick"))},[n("div",{class:"overlay-content",onClick:o[5]||(o[5]=p((()=>{}),["stop"]))},[n("div",wt,[n("div",vt,[n(c,{src:e.image.url,title:e.image.title},null,8,["src","title"])])]),n("div",xt,[n("table",null,[n("tr",null,[bt,n("td",null,[u(n("input",{type:"text","onUpdate:modelValue":o[1]||(o[1]=t=>e.tiles=t)},null,512),[[g,e.tiles]])])]),n("tr",null,[At,n("td",null,[n("label",null,[u(n("input",{type:"radio","onUpdate:modelValue":o[2]||(o[2]=t=>e.scoreMode=t),value:"1"},null,512),[[h,e.scoreMode]]),Ct]),kt,n("label",null,[u(n("input",{type:"radio","onUpdate:modelValue":o[3]||(o[3]=t=>e.scoreMode=t),value:"0"},null,512),[[h,e.scoreMode]]),St])])])])]),n("div",Tt,[n("button",{class:"btn",disabled:!e.canStartNewGame,onClick:o[4]||(o[4]=(...t)=>e.onNewGameClick&&e.onNewGameClick(...t))}," 🧩 Generate Puzzle ",8,["disabled"])])])])};var zt=e({components:{ImageLibrary:V,NewImageDialog:L,NewGameDialog:ft},data:()=>({filters:{sort:"date_desc",category:""},images:[],categories:[],image:{url:"",file:"",title:"",category:""},dialog:""}),async created(){await this.loadImages()},methods:{async loadImages(){const e=await fetch(`/api/newgame-data${Ee.asQueryArgs(this.filters)}`),t=await e.json();this.images=t.images,this.categories=t.categories},async filtersChanged(){await this.loadImages()},imageClicked(e){this.image=e,this.dialog="new-game"},setupGameClick(e){this.image=e,this.dialog="new-game"},async onNewGame(e){const t=await fetch("/newgame",{method:"post",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(e)});if(200===t.status){const e=await t.json();this.$router.push({name:"game",params:{id:e.id}})}}}});const It={class:"upload-image-teaser"},Pt=n("div",{class:"hint"},"(The image you upload will be added to the public gallery.)",-1),_t={key:0},Dt=s(" Category: "),Bt=n("option",{value:""},"All",-1),Ot=s(" Sort by: "),Et=n("option",{value:"date_desc"},"Newest first",-1),Mt=n("option",{value:"date_asc"},"Oldest first",-1),Nt=n("option",{value:"alpha_asc"},"A-Z",-1),Ut=n("option",{value:"alpha_desc"},"Z-A",-1);zt.render=function(e,o,s,g,p,h){const y=i("image-library"),f=i("new-image-dialog"),w=i("new-game-dialog");return a(),t("div",null,[n("div",It,[n("div",{class:"btn btn-big",onClick:o[1]||(o[1]=t=>e.dialog="new-image")},"Upload your image"),Pt]),n("div",null,[e.categories.length>0?(a(),t("label",_t,[Dt,u(n("select",{"onUpdate:modelValue":o[2]||(o[2]=t=>e.filters.category=t),onChange:o[3]||(o[3]=(...t)=>e.filtersChanged&&e.filtersChanged(...t))},[Bt,(a(!0),t(d,null,c(e.categories,((e,n)=>(a(),t("option",{key:n,value:e},r(e),9,["value"])))),128))],544),[[m,e.filters.category]])])):l("",!0),n("label",null,[Ot,u(n("select",{"onUpdate:modelValue":o[4]||(o[4]=t=>e.filters.sort=t),onChange:o[5]||(o[5]=(...t)=>e.filtersChanged&&e.filtersChanged(...t))},[Et,Mt,Nt,Ut],544),[[m,e.filters.sort]])])]),n(y,{images:e.images,categories:e.categories,onImageClicked:e.imageClicked},null,8,["images","categories","onImageClicked"]),"new-image"===e.dialog?(a(),t(f,{key:0,onBgclick:o[6]||(o[6]=t=>e.dialog=""),onSetupGameClick:e.setupGameClick},null,8,["onSetupGameClick"])):l("",!0),e.image&&"new-game"===e.dialog?(a(),t(w,{key:1,onBgclick:o[7]||(o[7]=t=>e.dialog=""),onNewGame:e.onNewGame,image:e.image},null,8,["onNewGame","image"])):l("",!0)])};var Gt=e({name:"scores",props:{activePlayers:{type:Array,required:!0},idlePlayers:{type:Array,required:!0}},computed:{actives(){return this.activePlayers.sort(((e,t)=>t.points-e.points)),this.activePlayers},idles(){return this.idlePlayers.sort(((e,t)=>t.points-e.points)),this.idlePlayers}}});const Rt={class:"scores"},$t=n("div",null,"Scores",-1),Vt=n("td",null,"⚑",-1),jt=n("td",null,"πŸ’€",-1);Gt.render=function(e,o,l,i,s,u){return a(),t("div",Rt,[$t,n("table",null,[(a(!0),t(d,null,c(e.actives,((e,o)=>(a(),t("tr",{key:o,style:{color:e.color}},[Vt,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128)),(a(!0),t(d,null,c(e.idles,((e,o)=>(a(),t("tr",{key:o,style:{color:e.color}},[jt,n("td",null,r(e.name),1),n("td",null,r(e.points),1)],4)))),128))])])};var Ft=e({name:"puzzle-status",props:{finished:{type:Boolean,required:!0},duration:{type:Number,required:!0},piecesDone:{type:Number,required:!0},piecesTotal:{type:Number,required:!0}},computed:{icon(){return this.finished?"🏁":"⏳"},durationStr(){return D(this.duration)}}});const Wt={class:"timer"};Ft.render=function(e,o,l,i,s,d){return a(),t("div",Wt,[n("div",null," 🧩 "+r(e.piecesDone)+"/"+r(e.piecesTotal),1),n("div",null,r(e.icon)+" "+r(e.durationStr),1),y(e.$slots,"default")])};var Lt=e({name:"settings-overlay",emits:{bgclick:null,"update:modelValue":null},props:{modelValue:Object},created(){this.$watch("modelValue",(e=>{this.$emit("update:modelValue",e)}),{deep:!0})}});const Ht=n("td",null,[n("label",null,"Background: ")],-1),Qt=n("td",null,[n("label",null,"Color: ")],-1),Yt=n("td",null,[n("label",null,"Name: ")],-1);Lt.render=function(e,o,l,i,s,r){return a(),t("div",{class:"overlay transparent",onClick:o[5]||(o[5]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content settings",onClick:o[4]||(o[4]=p((()=>{}),["stop"]))},[n("tr",null,[Ht,n("td",null,[u(n("input",{type:"color","onUpdate:modelValue":o[1]||(o[1]=t=>e.modelValue.background=t)},null,512),[[g,e.modelValue.background]])])]),n("tr",null,[Qt,n("td",null,[u(n("input",{type:"color","onUpdate:modelValue":o[2]||(o[2]=t=>e.modelValue.color=t)},null,512),[[g,e.modelValue.color]])])]),n("tr",null,[Yt,n("td",null,[u(n("input",{type:"text",maxLength:"16","onUpdate:modelValue":o[3]||(o[3]=t=>e.modelValue.name=t)},null,512),[[g,e.modelValue.name]])])])])])};var qt=e({name:"preview-overlay",props:{img:String},emits:{bgclick:null},computed:{previewStyle(){return{backgroundImage:`url('${this.img}')`}}}});const Zt={class:"preview"};qt.render=function(e,o,l,i,s,r){return a(),t("div",{class:"overlay",onClick:o[1]||(o[1]=t=>e.$emit("bgclick"))},[n("div",Zt,[n("div",{class:"img",style:e.previewStyle},null,4)])])};const Kt=Pe("Communication.js");let Jt,Xt=e=>{},en=e=>{};let tn=0;const nn=e=>{tn!==e&&(tn=e,en(e))};function on(e){if(2===tn)try{Jt.send(JSON.stringify(e))}catch(t){Kt.info("unable to send message.. maybe because ws is invalid?")}}let ln,an;var sn={connect:function(e,t,n){return ln=0,an={},nn(3),new Promise((o=>{Jt=new WebSocket(e,n+"|"+t),Jt.onopen=e=>{nn(2),on([de])},Jt.onmessage=e=>{const t=JSON.parse(e.data),l=t[0];if(l===ae){const e=t[1];o(e)}else{if(l!==ie)throw`[ 2021-05-09 invalid connect msgType ${l} ]`;{const e=t[1],o=t[2];if(e===n&&an[o])return void delete an[o];Xt(t)}}},Jt.onerror=e=>{throw nn(1),"[ 2021-05-15 onerror ]"},Jt.onclose=e=>{4e3===e.code||1001===e.code?nn(4):nn(1)}}))},connectReplay:function(e,t,n){return ln=0,an={},nn(3),new Promise((o=>{Jt=new WebSocket(e,n+"|"+t),Jt.onopen=e=>{nn(2),on([ce])},Jt.onmessage=e=>{const t=JSON.parse(e.data),n=t[0];if(n!==se)throw`[ 2021-05-09 invalid connectReplay msgType ${n} ]`;{const e=t[1],n=t[2];o({game:e,log:n})}},Jt.onerror=e=>{throw nn(1),"[ 2021-05-15 onerror ]"},Jt.onclose=e=>{4e3===e.code||1001===e.code?nn(4):nn(1)}}))},disconnect:function(){Jt&&Jt.close(4e3),ln=0,an={}},sendClientEvent:function(e){ln++,an[ln]=e,on([re,ln,an[ln]])},onServerChange:function(e){Xt=e},onConnectionStateChange:function(e){en=e},CODE_CUSTOM_DISCONNECT:4e3,CONN_STATE_NOT_CONNECTED:0,CONN_STATE_DISCONNECTED:1,CONN_STATE_CLOSED:4,CONN_STATE_CONNECTED:2,CONN_STATE_CONNECTING:3},rn=e({name:"connection-overlay",emits:{reconnect:null},props:{connectionState:Number},computed:{lostConnection(){return this.connectionState===sn.CONN_STATE_DISCONNECTED},connecting(){return this.connectionState===sn.CONN_STATE_CONNECTING},show(){return!(!this.lostConnection&&!this.connecting)}}});const dn={key:0,class:"overlay connection-lost"},cn={key:0,class:"overlay-content"},un=n("div",null,"⁉️ LOST CONNECTION ⁉️",-1),gn={key:1,class:"overlay-content"},pn=n("div",null,"Connecting...",-1);rn.render=function(e,o,i,s,r,d){return e.show?(a(),t("div",dn,[e.lostConnection?(a(),t("div",cn,[un,n("span",{class:"btn",onClick:o[1]||(o[1]=t=>e.$emit("reconnect"))},"Reconnect")])):l("",!0),e.connecting?(a(),t("div",gn,[pn])):l("",!0)])):l("",!0)};var hn=e({name:"help-overlay",emits:{bgclick:null}});const mn=n("tr",null,[n("td",null,"⬆️ Move up:"),n("td",null,[n("div",null,[n("kbd",null,"W"),s("/"),n("kbd",null,"↑"),s("/πŸ–±οΈ")])])],-1),yn=n("tr",null,[n("td",null,"⬇️ Move down:"),n("td",null,[n("div",null,[n("kbd",null,"S"),s("/"),n("kbd",null,"↓"),s("/πŸ–±οΈ")])])],-1),fn=n("tr",null,[n("td",null,"⬅️ Move left:"),n("td",null,[n("div",null,[n("kbd",null,"A"),s("/"),n("kbd",null,"←"),s("/πŸ–±οΈ")])])],-1),wn=n("tr",null,[n("td",null,"➑️ Move right:"),n("td",null,[n("div",null,[n("kbd",null,"D"),s("/"),n("kbd",null,"β†’"),s("/πŸ–±οΈ")])])],-1),vn=n("tr",null,[n("td"),n("td",null,[n("div",null,[s("Move faster by holding "),n("kbd",null,"Shift")])])],-1),xn=n("tr",null,[n("td",null,"πŸ”+ Zoom in:"),n("td",null,[n("div",null,[n("kbd",null,"E"),s("/πŸ–±οΈ-Wheel")])])],-1),bn=n("tr",null,[n("td",null,"πŸ”- Zoom out:"),n("td",null,[n("div",null,[n("kbd",null,"Q"),s("/πŸ–±οΈ-Wheel")])])],-1),An=n("tr",null,[n("td",null,"πŸ–ΌοΈ Toggle preview:"),n("td",null,[n("div",null,[n("kbd",null,"Space")])])],-1),Cn=n("tr",null,[n("td",null,"πŸ§©βœ”οΈ Toggle fixed pieces:"),n("td",null,[n("div",null,[n("kbd",null,"F")])])],-1),kn=n("tr",null,[n("td",null,"πŸ§©β“ Toggle loose pieces:"),n("td",null,[n("div",null,[n("kbd",null,"G")])])],-1);hn.render=function(e,o,l,i,s,r){return a(),t("div",{class:"overlay transparent",onClick:o[2]||(o[2]=t=>e.$emit("bgclick"))},[n("table",{class:"overlay-content help",onClick:o[1]||(o[1]=p((()=>{}),["stop"]))},[mn,yn,fn,wn,vn,xn,bn,An,Cn,kn])])};var Sn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAW0lEQVQ4je1RywrAIAxLxP//5exixRWlVgZelpOKeTQFfnDypgy3eLIkSLLL8mxGPoHsU2hPAgDHBLvRX6hZZw/fwT0BtlLSONqCbWAmEIqMZOCDDlaDR3N03gOyDCn+y4DWmAAAAABJRU5ErkJggg=="}),Tn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAARElEQVQ4jWNgGAU0Af+hmBCbgYGBgYERhwHEAEYGBgYGJtIdiApYyLAZBVDsAqoagC1ACQJyY4ERg0GCISh6KA4DigEAou8LC+LnIJoAAAAASUVORK5CYII="}),zn=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAcUlEQVQ4ja1TQQ7AIAgD///n7jCozA2Hbk00jbG1KIrcARszTugoBs49qioZj7r2kKACptkyAOCJsJuA+GzglwHjvMSSWFVaENWVASxh5eRLiq5fN/ASjI89VcP2K3hHpq1cEXNaMfnrL3TDZP2tDuoOA6MzCCXWr38AAAAASUVORK5CYII="}),In=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAU0lEQVQ4jWNgoAH4D8X42HDARKlt5BoAd82AuQAOGLGIYQQUPv0wF5CiCQUge4EsQ5C9QI4BjMguwBYeBAElscCIy1ZivMKIwSDBEBQ9FCckigEAU3QOD7TGvY4AAAAASUVORK5CYII="});function Pn(){let e=0,t=0,n=1;const o=(o,l)=>{e+=o/n,t+=l/n},l=e=>{const t=n+.05*n*("in"===e?1:-1);return Math.min(Math.max(t,.1),6)},i=o=>({x:o.x/n-e,y:o.y/n-t}),a=o=>({x:(o.x+e)*n,y:(o.y+t)*n}),s=e=>({w:e.w*n,h:e.h*n});return{move:o,canZoom:e=>n!=l(e),zoom:(e,t)=>((e,t)=>{if(n==e)return!1;const l=1-n/e;return o(-t.x*l,-t.y*l),n=e,!0})(l(e),t),worldToViewport:e=>{const{x:t,y:n}=a(e);return{x:Math.round(t),y:Math.round(n)}},worldToViewportRaw:a,worldDimToViewport:e=>{const{w:t,h:n}=s(e);return{w:Math.round(t),h:Math.round(n)}},worldDimToViewportRaw:s,viewportToWorld:e=>{const{x:t,y:n}=i(e);return{x:Math.round(t),y:Math.round(n)}},viewportToWorldRaw:i}}function _n(e=0,t=0){const n=document.createElement("canvas");return n.width=e,n.height=t,n}var Dn={createCanvas:_n,loadImageToBitmap:async function(e){return new Promise((t=>{const n=new Image;n.onload=()=>{createImageBitmap(n).then(t)},n.src=e}))},resizeBitmap:async function(e,t,n){const o=_n(t,n);return o.getContext("2d").drawImage(e,0,0,e.width,e.height,0,0,t,n),await createImageBitmap(o)},colorize:async function(e,t,n){const o=_n(e.width,e.height),l=o.getContext("2d");return l.save(),l.drawImage(t,0,0),l.fillStyle=n,l.globalCompositeOperation="source-in",l.fillRect(0,0,t.width,t.height),l.restore(),l.save(),l.globalCompositeOperation="destination-over",l.drawImage(e,0,0),l.restore(),await createImageBitmap(o)}};const Bn=Pe("Debug.js");let On=0,En=0;var Mn=e=>{On=performance.now(),En=e},Nn=e=>{const t=performance.now(),n=t-On;n>En&&Bn.log(e+": "+n),On=t};const Un=Pe("PuzzleGraphics.js");function Gn(e,t){const n=Ee.coordByTileIdx(e,t);return{x:n.x*e.tileSize,y:n.y*e.tileSize,w:e.tileSize,h:e.tileSize}}var Rn={loadPuzzleBitmaps:async function(e){const t=await Dn.loadImageToBitmap(e.info.imageUrl),n=await Dn.resizeBitmap(t,e.info.width,e.info.height);return await async function(e,t,n){Un.log("start createPuzzleTileBitmaps");var o=n.tileSize,l=n.tileMarginWidth,i=n.tileDrawSize,a=o/100,s=[0,0,40,15,37,5,37,5,40,0,38,-5,38,-5,20,-20,50,-20,50,-20,80,-20,62,-5,62,-5,60,0,63,5,63,5,65,15,100,0];const r=new Array(t.length),d={};function c(e){const t=`${e.top}${e.right}${e.left}${e.bottom}`;if(d[t])return d[t];const n=new Path2D,i={x:l,y:l},r=le.pointAdd(i,{x:o,y:0}),c=le.pointAdd(r,{x:0,y:o}),u=le.pointSub(c,{x:o,y:0});if(n.moveTo(i.x,i.y),0!==e.top)for(let o=0;o=0&&e&&this.explode(e),this.px+=this.vx,this.py+=this.vy}draw(e){e.beginPath(),e.arc(this.px,this.py,this.previousRadius,0,2*Math.PI,!1),this.hasExploded||(e.fillStyle=this.color,e.lineWidth=1,e.fill())}explode(e){this.hasExploded=!0;const t=3+Math.floor(3*Math.random());for(let n=0;n{this.resize()}))}setSpeedParams(){let e=0,t=0;for(;e=0;)t+=1,e+=t;jn=t/2,Fn=t-jn;const n=1/4*this.canvas.width/(t/2);$n=-n,Vn=2*n}resize(){this.setSpeedParams()}init(){this.readyBombs=[],this.explodedBombs=[],this.particles=[];for(let e=0;e<1;e++)this.readyBombs.push(new Wn(this.rng))}update(){100*Math.random()<5&&this.readyBombs.push(new Wn(this.rng));const e=[];for(;this.explodedBombs.length>0;){const t=this.explodedBombs.shift();if(!t)break;t.update(),t.alive&&e.push(t)}this.explodedBombs=e;const t=[];for(;this.readyBombs.length>0;){const e=this.readyBombs.shift();if(!e)break;e.update(this.particles),e.hasExploded?this.explodedBombs.push(e):t.push(e)}this.readyBombs=t;const n=[];for(;this.particles.length>0;){const e=this.particles.shift();if(!e)break;e.update(),e.alive&&n.push(e)}this.particles=n}render(){this.ctx.beginPath(),this.ctx.fillStyle="rgba(0, 0, 0, 0.1)",this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height);for(let e=0;e{const t=e.color+" "+e.d;if(!h[t]){const n=e.d?a:s;if(e.color){const o=e.d?r:d;h[t]=await Dn.colorize(n,o,e.color)}else h[t]=n}return h[t]},y=function(e,t){return t.width=window.innerWidth,t.height=window.innerHeight,e.appendChild(t),window.addEventListener("resize",(()=>{t.width=window.innerWidth,t.height=window.innerHeight,Zn=!0})),t}(l,Dn.createCanvas()),f={log:[],logIdx:0,speeds:[.5,1,2,5,10,20,50],speedIdx:1,paused:!1,lastRealTs:0,lastGameTs:0,gameStartTs:0};sn.onConnectionStateChange((e=>{i.setConnectionState(e)}));let w=()=>0;const v=async()=>{if("play"===o){const o=await sn.connect(n,e,t),l=Ee.decodeGame(o);yt.setGame(l.id,l),w=()=>P()}else{if("replay"!==o)throw"[ 2020-12-22 MODE invalid, must be play|replay ]";{const o=await sn.connectReplay(n,e,t),l=Ee.decodeGame(o.game);yt.setGame(l.id,l),f.log=o.log,f.lastRealTs=P(),f.gameStartTs=parseInt(f.log[0][f.log[0].length-2],10),f.lastGameTs=f.gameStartTs,w=()=>f.lastGameTs}}Zn=!0};await v();const x=yt.getTileDrawOffset(e),b=yt.getTileDrawSize(e),A=yt.getPuzzleWidth(e),C=yt.getPuzzleHeight(e),k=yt.getTableWidth(e),S=yt.getTableHeight(e),T={x:(k-A)/2,y:(S-C)/2},z={w:A,h:C},I={w:b,h:b},_=await Rn.loadPuzzleBitmaps(yt.getPuzzle(e)),D=new Hn(y,yt.getRng(e));D.init();const B=y.getContext("2d");y.classList.add("loaded");const O=Pn();O.move(-(k-y.width)/2,-(S-y.height)/2);const E=function(e,t,n){let o=[],l=!0,i=!1,a=!1,s=!1,r=!1,d=!1,c=!1,u=!1;const g=(e,t)=>{const o=n.viewportToWorld({x:e,y:t});return[o.x,o.y]},p=e=>g(e.offsetX,e.offsetY),h=()=>g(e.width/2,e.height/2),m=(e,t)=>{l&&("Shift"===t.key?u=e:"ArrowUp"===t.key||"w"===t.key||"W"===t.key?s=e:"ArrowDown"===t.key||"s"===t.key||"S"===t.key?r=e:"ArrowLeft"===t.key||"a"===t.key||"A"===t.key?i=e:"ArrowRight"===t.key||"d"===t.key||"D"===t.key?a=e:"q"===t.key?c=e:"e"===t.key&&(d=e))};e.addEventListener("mousedown",(e=>{0===e.button&&y([me,...p(e)])})),e.addEventListener("mouseup",(e=>{0===e.button&&y([ye,...p(e)])})),e.addEventListener("mousemove",(e=>{y([fe,...p(e)])})),e.addEventListener("wheel",(e=>{if(n.canZoom(e.deltaY<0?"in":"out")){const t=e.deltaY<0?we:ve;y([t,...p(e)])}})),t.addEventListener("keydown",(e=>m(!0,e))),t.addEventListener("keyup",(e=>m(!1,e))),t.addEventListener("keypress",(e=>{l&&(" "===e.key&&y([Ce]),"F"!==e.key&&"f"!==e.key||(Yn=!Yn,Zn=!0),"G"!==e.key&&"g"!==e.key||(qn=!qn,Zn=!0))}));const y=e=>{o.push(e)};return{addEvent:y,consumeAll:()=>{if(0===o.length)return[];const e=o.slice();return o=[],e},createKeyEvents:()=>{const e=u?20:10,t=(i?e:0)-(a?e:0),o=(s?e:0)-(r?e:0);0===t&&0===o||y([he,t,o]),d&&c||(d?n.canZoom("in")&&y([we,...h()]):c&&n.canZoom("out")&&y([ve,...h()]))},setHotkeys:e=>{l=e}}}(y,window,O),M=yt.getImageUrl(e),N=()=>{const t=yt.getStartTs(e),n=yt.getFinishTs(e),o=w();i.setFinished(!!n),i.setDuration((n||o)-t)};N(),i.setPiecesDone(yt.getFinishedTileCount(e)),i.setPiecesTotal(yt.getTileCount(e));const U=w();i.setActivePlayers(yt.getActivePlayers(e,U)),i.setIdlePlayers(yt.getIdlePlayers(e,U));const G=!!yt.getFinishTs(e);let R=G;const $=()=>R&&!G,V=()=>yt.getPlayerBgColor(e,t)||localStorage.getItem("bg_color")||"#222222",j=()=>{i.setReplaySpeed&&i.setReplaySpeed(f.speeds[f.speedIdx]),i.setReplayPaused&&i.setReplayPaused(f.paused)};if("play"===o?setInterval(N,1e3):"replay"===o&&j(),"play"===o)sn.onServerChange((n=>{n[0],n[1],n[2];const o=n[3];for(const[l,i]of o)switch(l){case Te:{const n=Ee.decodePlayer(i);n.id!==t&&(yt.setPlayer(e,n.id,n),Zn=!0)}break;case Se:{const t=Ee.decodeTile(i);yt.setTile(e,t.idx,t),Zn=!0}break;case ke:yt.setPuzzleData(e,i),Zn=!0}R=!!yt.getFinishTs(e)}));else if("replay"===o){let t=setInterval((()=>{const n=P();if(f.paused)return void(f.lastRealTs=n);const o=(n-f.lastRealTs)*f.speeds[f.speedIdx],l=f.lastGameTs+o;for(;;){if(f.paused)break;const n=f.logIdx+1;if(n>=f.log.length){clearInterval(t);break}const o=f.log[n],i=f.gameStartTs+o[o.length-1];if(i>l)break;const a=o.slice();if(a[0]===ue){const t=a[1];yt.addPlayer(e,t,i),Zn=!0}else if(a[0]===ge){const t=yt.getPlayerIdByIndex(e,a[1]);if(!t)throw"[ 2021-05-17 player not found (update player) ]";yt.addPlayer(e,t,i),Zn=!0}else if(a[0]===pe){const t=yt.getPlayerIdByIndex(e,a[1]);if(!t)throw"[ 2021-05-17 player not found (handle input) ]";const n=a[2];yt.handleInput(e,t,n,i),Zn=!0}f.logIdx=n}f.lastRealTs=n,f.lastGameTs=l,N()}),50)}let F=null;return(e=>{const t=e.fps||60,n=e.slow||1,o=e.update,l=e.render,i=window.requestAnimationFrame,a=1/t,s=n*a;let r,d=0,c=window.performance.now();const u=()=>{for(r=window.performance.now(),d+=Math.min(1,(r-c)/1e3);d>s;)d-=s,o(a);l(d/n),c=r,i(u)};i(u)})({update:()=>{E.createKeyEvents();for(const n of E.consumeAll())if("play"===o){const o=n[0];if(o===he){const e=n[1],t=n[2];Zn=!0,O.move(e,t)}else if(o===fe){if(F&&!yt.getFirstOwnedTile(e,t)){const e={x:n[1],y:n[2]},t=O.worldToViewport(e),o=Math.round(t.x-F.x),l=Math.round(t.y-F.y);Zn=!0,O.move(o,l),F=t}}else if(o===me){const e={x:n[1],y:n[2]};F=O.worldToViewport(e)}else if(o===ye)F=null;else if(o===we){const e={x:n[1],y:n[2]};Zn=!0,O.zoom("in",O.worldToViewport(e))}else if(o===ve){const e={x:n[1],y:n[2]};Zn=!0,O.zoom("out",O.worldToViewport(e))}else o===Ce&&i.togglePreview();const l=w();yt.handleInput(e,t,n,l).length>0&&(Zn=!0),sn.sendClientEvent(n)}else if("replay"===o){const e=n[0];if(e===he){const e=n[1],t=n[2];Zn=!0,O.move(e,t)}else if(e===fe){if(F){const e={x:n[1],y:n[2]},t=O.worldToViewport(e),o=Math.round(t.x-F.x),l=Math.round(t.y-F.y);Zn=!0,O.move(o,l),F=t}}else if(e===me){const e={x:n[1],y:n[2]};F=O.worldToViewport(e)}else if(e===ye)F=null;else if(e===we){const e={x:n[1],y:n[2]};Zn=!0,O.zoom("in",O.worldToViewport(e))}else if(e===ve){const e={x:n[1],y:n[2]};Zn=!0,O.zoom("out",O.worldToViewport(e))}else e===Ce&&i.togglePreview()}R=!!yt.getFinishTs(e),$()&&(D.update(),Zn=!0)},render:async()=>{if(!Zn)return;const n=w();let l,a,s;window.DEBUG&&Mn(0),B.fillStyle=V(),B.fillRect(0,0,y.width,y.height),window.DEBUG&&Nn("clear done"),l=O.worldToViewportRaw(T),a=O.worldDimToViewportRaw(z),B.fillStyle="rgba(255, 255, 255, .3)",B.fillRect(l.x,l.y,a.w,a.h),window.DEBUG&&Nn("board done");const r=yt.getTilesSortedByZIndex(e);window.DEBUG&&Nn("get tiles done"),a=O.worldDimToViewportRaw(I);for(const e of r)(-1===e.owner?Yn:qn)&&(s=_[e.idx],l=O.worldToViewportRaw({x:x+e.pos.x,y:x+e.pos.y}),B.drawImage(s,0,0,s.width,s.height,l.x,l.y,a.w,a.h));window.DEBUG&&Nn("tiles done");const d=[];for(const i of yt.getActivePlayers(e,n))s=await m(i),l=O.worldToViewport(i),B.drawImage(s,l.x-u,l.y-p),c=i,("replay"===o||c.id!==t)&&d.push([`${i.name} (${i.points})`,l.x,l.y+g]);var c;B.fillStyle="white",B.textAlign="center";for(const[e,t,o]of d)B.fillText(e,t,o);window.DEBUG&&Nn("players done"),i.setActivePlayers(yt.getActivePlayers(e,n)),i.setIdlePlayers(yt.getIdlePlayers(e,n)),i.setPiecesDone(yt.getFinishedTileCount(e)),window.DEBUG&&Nn("HUD done"),$()&&D.render(),Zn=!1}}),{setHotkeys:e=>{E.setHotkeys(e)},onBgChange:e=>{localStorage.setItem("bg_color",e),E.addEvent([xe,e])},onColorChange:e=>{localStorage.setItem("player_color",e),E.addEvent([be,e])},onNameChange:e=>{localStorage.setItem("player_name",e),E.addEvent([Ae,e])},replayOnSpeedUp:()=>{f.speedIdx+1{f.speedIdx>=1&&(f.speedIdx--,j())},replayOnPauseToggle:()=>{f.paused=!f.paused,j()},previewImageUrl:M,player:{background:V(),color:yt.getPlayerColor(e,t)||localStorage.getItem("player_color")||"#ffffff",name:yt.getPlayerName(e,t)||localStorage.getItem("player_name")||"anon"},disconnect:sn.disconnect,connect:v}}var Jn=e({name:"game",components:{PuzzleStatus:Ft,Scores:Gt,SettingsOverlay:Lt,PreviewOverlay:qt,ConnectionOverlay:rn,HelpOverlay:hn},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,g:{player:{background:"",color:"",name:""},previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},disconnect:()=>{},connect:()=>{}}}),async mounted(){this.$route.params.id&&(this.$watch((()=>this.g.player.background),(e=>{this.g.onBgChange(e)})),this.$watch((()=>this.g.player.color),(e=>{this.g.onColorChange(e)})),this.$watch((()=>this.g.player.name),(e=>{this.g.onNameChange(e)})),this.g=await Kn(`${this.$route.params.id}`,this.$clientId,this.$config.WS_ADDRESS,"play",this.$el,{setActivePlayers:e=>{this.activePlayers=e},setIdlePlayers:e=>{this.idlePlayers=e},setFinished:e=>{this.finished=e},setDuration:e=>{this.duration=e},setPiecesDone:e=>{this.piecesDone=e},setPiecesTotal:e=>{this.piecesTotal=e},setConnectionState:e=>{this.connectionState=e},togglePreview:()=>{this.toggle("preview",!1)}}))},unmounted(){this.g.disconnect()},methods:{reconnect(){this.g.connect()},toggle(e,t){""===this.overlay?(this.overlay=e,t&&this.g.setHotkeys(!1)):(this.overlay="",t&&this.g.setHotkeys(!0))}}});const Xn={id:"game"},eo={class:"menu"},to={class:"tabs"},no=s("🧩 Puzzles");Jn.render=function(e,l,s,r,d,c){const g=i("settings-overlay"),p=i("preview-overlay"),h=i("help-overlay"),m=i("connection-overlay"),y=i("puzzle-status"),w=i("router-link"),v=i("scores");return a(),t("div",Xn,[u(n(g,{onBgclick:l[1]||(l[1]=t=>e.toggle("settings",!0)),modelValue:e.g.player,"onUpdate:modelValue":l[2]||(l[2]=t=>e.g.player=t)},null,8,["modelValue"]),[[f,"settings"===e.overlay]]),u(n(p,{onBgclick:l[3]||(l[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[f,"preview"===e.overlay]]),u(n(h,{onBgclick:l[4]||(l[4]=t=>e.toggle("help",!0))},null,512),[[f,"help"===e.overlay]]),n(m,{connectionState:e.connectionState,onReconnect:e.reconnect},null,8,["connectionState","onReconnect"]),n(y,{finished:e.finished,duration:e.duration,piecesDone:e.piecesDone,piecesTotal:e.piecesTotal},null,8,["finished","duration","piecesDone","piecesTotal"]),n("div",eo,[n("div",to,[n(w,{class:"opener",to:{name:"index"},target:"_blank"},{default:o((()=>[no])),_:1}),n("div",{class:"opener",onClick:l[5]||(l[5]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:l[6]||(l[6]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:l[7]||(l[7]=t=>e.toggle("help",!0))},"ℹ️ Help")])]),n(v,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])};var oo=e({name:"replay",components:{PuzzleStatus:Ft,Scores:Gt,SettingsOverlay:Lt,PreviewOverlay:qt,HelpOverlay:hn},data:()=>({activePlayers:[],idlePlayers:[],finished:!1,duration:0,piecesDone:0,piecesTotal:0,overlay:"",connectionState:0,g:{player:{background:"",color:"",name:""},previewImageUrl:"",setHotkeys:e=>{},onBgChange:e=>{},onColorChange:e=>{},onNameChange:e=>{},replayOnSpeedUp:()=>{},replayOnSpeedDown:()=>{},replayOnPauseToggle:()=>{},disconnect:()=>{}},replay:{speed:1,paused:!1}}),async mounted(){this.$route.params.id&&(this.$watch((()=>this.g.player.background),(e=>{this.g.onBgChange(e)})),this.$watch((()=>this.g.player.color),(e=>{this.g.onColorChange(e)})),this.$watch((()=>this.g.player.name),(e=>{this.g.onNameChange(e)})),this.g=await Kn(`${this.$route.params.id}`,this.$clientId,this.$config.WS_ADDRESS,"replay",this.$el,{setActivePlayers:e=>{this.activePlayers=e},setIdlePlayers:e=>{this.idlePlayers=e},setFinished:e=>{this.finished=e},setDuration:e=>{this.duration=e},setPiecesDone:e=>{this.piecesDone=e},setPiecesTotal:e=>{this.piecesTotal=e},togglePreview:()=>{this.toggle("preview",!1)},setConnectionState:e=>{this.connectionState=e},setReplaySpeed:e=>{this.replay.speed=e},setReplayPaused:e=>{this.replay.paused=e}}))},unmounted(){this.g.disconnect()},methods:{toggle(e,t){""===this.overlay?(this.overlay=e,t&&this.g.setHotkeys(!1)):(this.overlay="",t&&this.g.setHotkeys(!0))}},computed:{replayText(){return"Replay-Speed: "+this.replay.speed+"x"+(this.replay.paused?" Paused":"")}}});const lo={id:"replay"},io={class:"menu"},ao={class:"tabs"},so=s("🧩 Puzzles");oo.render=function(e,l,s,d,c,g){const p=i("settings-overlay"),h=i("preview-overlay"),m=i("help-overlay"),y=i("puzzle-status"),w=i("router-link"),v=i("scores");return a(),t("div",lo,[u(n(p,{onBgclick:l[1]||(l[1]=t=>e.toggle("settings",!0)),modelValue:e.g.player,"onUpdate:modelValue":l[2]||(l[2]=t=>e.g.player=t)},null,8,["modelValue"]),[[f,"settings"===e.overlay]]),u(n(h,{onBgclick:l[3]||(l[3]=t=>e.toggle("preview",!1)),img:e.g.previewImageUrl},null,8,["img"]),[[f,"preview"===e.overlay]]),u(n(m,{onBgclick:l[4]||(l[4]=t=>e.toggle("help",!0))},null,512),[[f,"help"===e.overlay]]),n(y,{finished:e.finished,duration:e.duration,piecesDone:e.piecesDone,piecesTotal:e.piecesTotal},{default:o((()=>[n("div",null,[n("div",null,r(e.replayText),1),n("button",{class:"btn",onClick:l[5]||(l[5]=t=>e.g.replayOnSpeedUp())},"⏫"),n("button",{class:"btn",onClick:l[6]||(l[6]=t=>e.g.replayOnSpeedDown())},"⏬"),n("button",{class:"btn",onClick:l[7]||(l[7]=t=>e.g.replayOnPauseToggle())},"⏸️")])])),_:1},8,["finished","duration","piecesDone","piecesTotal"]),n("div",io,[n("div",ao,[n(w,{class:"opener",to:{name:"index"},target:"_blank"},{default:o((()=>[so])),_:1}),n("div",{class:"opener",onClick:l[8]||(l[8]=t=>e.toggle("preview",!1))},"πŸ–ΌοΈ Preview"),n("div",{class:"opener",onClick:l[9]||(l[9]=t=>e.toggle("settings",!0))},"πŸ› οΈ Settings"),n("div",{class:"opener",onClick:l[10]||(l[10]=t=>e.toggle("help",!0))},"ℹ️ Help")])]),n(v,{activePlayers:e.activePlayers,idlePlayers:e.idlePlayers},null,8,["activePlayers","idlePlayers"])])},(async()=>{const e=await fetch("/api/conf"),t=await e.json();const n=w({history:v(),routes:[{name:"index",path:"/",component:U},{name:"new-game",path:"/new-game",component:zt},{name:"game",path:"/g/:id",component:Jn},{name:"replay",path:"/replay/:id",component:oo}]});n.beforeEach(((e,t)=>{t.name&&document.documentElement.classList.remove(`view-${String(t.name)}`),document.documentElement.classList.add(`view-${String(e.name)}`)}));const o=x(b);o.config.globalProperties.$config=t,o.config.globalProperties.$clientId=function(){let e=localStorage.getItem("ID");return e||(e=Ee.uniqId(),localStorage.setItem("ID",e)),e}(),o.use(n),o.mount("#app")})(); diff --git a/build/public/assets/index.6c4f6859.css b/build/public/assets/index.c5b0553c.css similarity index 80% rename from build/public/assets/index.6c4f6859.css rename to build/public/assets/index.c5b0553c.css index 981015b..a3b815d 100644 --- a/build/public/assets/index.6c4f6859.css +++ b/build/public/assets/index.c5b0553c.css @@ -1 +1 @@ -:root{--main-color:#c1b19f;--main-darker-color:#4f4e4c;--link-color:#808db0;--link-hover-color:#c5cfeb;--highlight-color:#dd7e7e;--input-bg-color:#262523;--bg-color:rgba(0,0,0,.7)}body,html{margin:0;background:#2b2b2b;color:var(--main-color);height:100%}*{font-family:monospace;font-size:15px}h1,h2,h3,h4{font-size:20px}a{color:var(--link-color);text-decoration:none}a:hover{color:var(--link-hover-color)}td,th{vertical-align:top}.btn{display:inline-block;background:var(--input-bg-color);color:var(--link-color);border:solid 1px #000;padding:5px 10px;box-shadow:1px 1px 2px rgba(0,0,0,.5),0 0 1px rgba(150,150,150,.4) inset;border-radius:4px;user-select:none}.btn-big{font-size:1.5em;padding:10px 20px}.btn:hover{background:#2f2e2c;color:var(--link-hover-color);border:solid 1px #111;box-shadow:0 0 1px rgba(150,150,150,.4) inset;cursor:pointer}.btn:disabled{background:#2f2e2c;color:#8c4747!important;border:solid 1px #111;box-shadow:0 0 1px rgba(150,150,150,.4) inset;cursor:not-allowed}input{background:#333230;border-radius:4px;color:var(--main-color);padding:6px 10px;border:solid 1px #000;box-shadow:0 0 3px rgba(0,0,0,.3) inset}input:focus{border:solid 1px #686767;background:var(--input-bg-color)}.scores{position:absolute;right:0;top:0;background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7)}.timer{position:absolute;left:0;top:0;background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7)}.menu{position:absolute;top:0;left:50%;transform:translateX(-50%);background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7);z-index:2}.closed{display:none}.overlay{position:absolute;top:0;left:0;right:0;bottom:0;z-index:10;background:var(--bg-color)}.overlay.transparent{background:0 0}.overlay-content{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7);z-index:1}.connection-lost .overlay-content{padding:20px;text-align:center}.preview{position:absolute;top:20px;left:20px;bottom:20px;right:20px}.preview .img{height:100%;width:100%;position:absolute;background-repeat:no-repeat;background-position:center;background-size:contain}.menu .opener{display:inline-block;margin-right:10px;color:var(--link-color)}.menu .opener:last-child{margin-right:0}.menu .opener:hover{color:var(--link-hover-color);cursor:pointer}canvas.loaded{cursor:none}kbd{background-color:#eee;border-radius:3px;border:1px solid #b4b4b4;box-shadow:0 1px 1px rgba(0,0,0,.2),0 2px 0 0 rgba(255,255,255,.7) inset;color:#333;display:inline-block;font-size:.85em;font-weight:700;line-height:1;padding:2px 4px;white-space:nowrap}.hint{color:var(--main-darker-color)}.upload-image-teaser{text-align:center}.upload-image-teaser .btn{margin-bottom:.5em}table label{line-height:32px}.nav{list-style:none;padding:0}.nav li{display:inline-block;margin-right:1em}.image-list{overflow:scroll}.image-list-inner{white-space:nowrap}.imageteaser{width:150px;height:100px;display:inline-block;margin:5px;background-size:contain;background-position:center;background-repeat:no-repeat;background-color:#222;cursor:pointer}.game-teaser-wrap{display:inline-block;width:20%;padding:5px;box-sizing:border-box}.game-teaser{display:block;background-repeat:no-repeat;background-position:center;background-size:contain;position:relative;padding-top:56.25%;width:100%;background-color:#222}.game-info{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%}.game-info-text{position:absolute;top:0;background:var(--bg-color);padding:5px}.game-replay{position:absolute;top:0;right:0}html.view-game{overflow:hidden}html.view-game body{overflow:hidden}html.view-replay{overflow:hidden}html.view-replay body{overflow:hidden}html.view-replay canvas{cursor:grab}.new-image-dialog .overlay-content{display:grid;grid-template-columns:auto 450px;grid-template-rows:auto;grid-template-areas:"image settings" "image buttons";height:90%;width:80%}.new-image-dialog .area-image{grid-area:image}.new-image-dialog .area-image.no-image{align-content:center;display:grid;text-align:center;margin:20px;border:dashed 6px;position:relative}.new-image-dialog .area-image .has-image{position:relative;width:100%;height:100%}.new-image-dialog .area-image .has-image .remove{position:absolute;top:.5em;left:.5em}.new-image-dialog .area-settings{grid-area:settings}.new-image-dialog .area-settings table input[type=text]{width:100%;box-sizing:border-box}.new-image-dialog .area-buttons{align-self:end;grid-area:buttons}.new-image-dialog .area-buttons button{width:100%}.new-image-dialog .upload{position:absolute;top:0;left:0;right:0;bottom:0;cursor:pointer}.new-image-dialog .upload .btn{position:absolute;top:50%;transform:translate(-50%,-50%)}.new-game-dialog .overlay-content{display:grid;grid-template-columns:auto 450px;grid-template-rows:auto;grid-template-areas:"image settings" "image buttons";height:90%;width:80%}.new-game-dialog .area-image{grid-area:image}.new-game-dialog .area-settings{grid-area:settings}.new-game-dialog .area-settings table input[type=text]{width:100%;box-sizing:border-box}.new-game-dialog .area-buttons{align-self:end;grid-area:buttons}.new-game-dialog .area-buttons button{width:100%}.new-game-dialog .has-image{position:relative;width:100%;height:100%}.new-game-dialog .has-image .remove{position:absolute;top:.5em;left:.5em} \ No newline at end of file +:root{--main-color:#c1b19f;--main-darker-color:#4f4e4c;--link-color:#808db0;--link-hover-color:#c5cfeb;--highlight-color:#dd7e7e;--input-bg-color:#262523;--bg-color:rgba(0,0,0,.7)}body,html{margin:0;background:#2b2b2b;color:var(--main-color);height:100%}*{font-family:monospace;font-size:15px}h1,h2,h3,h4{font-size:20px}a{color:var(--link-color);text-decoration:none}a:hover{color:var(--link-hover-color)}td,th{vertical-align:top}.btn{display:inline-block;background:var(--input-bg-color);color:var(--link-color);border:solid 1px #000;padding:5px 10px;box-shadow:1px 1px 2px rgba(0,0,0,.5),0 0 1px rgba(150,150,150,.4) inset;border-radius:4px;user-select:none}.btn-big{font-size:1.5em;padding:10px 20px}.btn:hover{background:#2f2e2c;color:var(--link-hover-color);border:solid 1px #111;box-shadow:0 0 1px rgba(150,150,150,.4) inset;cursor:pointer}.btn:disabled{background:#2f2e2c;color:#8c4747!important;border:solid 1px #111;box-shadow:0 0 1px rgba(150,150,150,.4) inset;cursor:not-allowed}input{background:#333230;border-radius:4px;color:var(--main-color);padding:6px 10px;border:solid 1px #000;box-shadow:0 0 3px rgba(0,0,0,.3) inset}input:focus{border:solid 1px #686767;background:var(--input-bg-color)}.scores{position:absolute;right:0;top:0;background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7)}.timer{position:absolute;left:0;top:0;background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7)}.menu{position:absolute;top:0;left:50%;transform:translateX(-50%);background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7);z-index:2}.closed{display:none}.overlay{position:absolute;top:0;left:0;right:0;bottom:0;z-index:10;background:var(--bg-color)}.overlay.transparent{background:0 0}.overlay-content{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:var(--bg-color);padding:5px;border:solid 1px #000;box-shadow:0 0 10px 0 rgba(0,0,0,.7);z-index:1}.connection-lost .overlay-content{padding:20px;text-align:center}.preview{position:absolute;top:20px;left:20px;bottom:20px;right:20px}.preview .img{height:100%;width:100%;position:absolute;background-repeat:no-repeat;background-position:center;background-size:contain}.menu .opener{display:inline-block;margin-right:10px;color:var(--link-color)}.menu .opener:last-child{margin-right:0}.menu .opener:hover{color:var(--link-hover-color);cursor:pointer}canvas.loaded{cursor:none}kbd{background-color:#eee;border-radius:3px;border:1px solid #b4b4b4;box-shadow:0 1px 1px rgba(0,0,0,.2),0 2px 0 0 rgba(255,255,255,.7) inset;color:#333;display:inline-block;font-size:.85em;font-weight:700;line-height:1;padding:2px 4px;white-space:nowrap}.hint{color:var(--main-darker-color)}.upload-image-teaser{text-align:center}.upload-image-teaser .btn{margin-bottom:.5em}table label{line-height:32px}.nav{list-style:none;padding:0}.nav li{display:inline-block;margin-right:1em}.image-list{overflow:scroll}.image-list-inner{white-space:nowrap}.imageteaser{width:150px;height:100px;display:inline-block;margin:5px;background-size:contain;background-position:center;background-repeat:no-repeat;background-color:#222;cursor:pointer}.game-teaser-wrap{display:inline-block;width:20%;padding:5px;box-sizing:border-box}.game-teaser{display:block;background-repeat:no-repeat;background-position:center;background-size:contain;position:relative;padding-top:56.25%;width:100%;background-color:#222}.game-info{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%}.game-info-text{position:absolute;top:0;background:var(--bg-color);padding:5px}.game-replay{position:absolute;top:0;right:0}html.view-game{overflow:hidden}html.view-game body{overflow:hidden}html.view-replay{overflow:hidden}html.view-replay body{overflow:hidden}html.view-replay canvas{cursor:grab}.new-image-dialog .overlay-content{display:grid;grid-template-columns:auto 450px;grid-template-rows:auto;grid-template-areas:"image settings" "image buttons";height:90%;width:80%}.new-image-dialog .area-image{grid-area:image;margin:20px}.new-image-dialog .area-image.no-image{align-content:center;display:grid;text-align:center;border:dashed 6px;position:relative}.new-image-dialog .area-image .has-image{position:relative;width:100%;height:100%}.new-image-dialog .area-image .has-image .remove{position:absolute;top:.5em;left:.5em}.new-image-dialog .area-settings{grid-area:settings}.new-image-dialog .area-settings table input[type=text]{width:100%;box-sizing:border-box}.new-image-dialog .area-buttons{align-self:end;grid-area:buttons}.new-image-dialog .area-buttons button{width:100%;margin-top:.5em}.new-image-dialog .upload{position:absolute;top:0;left:0;right:0;bottom:0;cursor:pointer}.new-image-dialog .upload .btn{position:absolute;top:50%;transform:translate(-50%,-50%)}.new-game-dialog .overlay-content{display:grid;grid-template-columns:auto 450px;grid-template-rows:auto;grid-template-areas:"image settings" "image buttons";height:90%;width:80%}.new-game-dialog .area-image{grid-area:image;margin:20px}.new-game-dialog .area-settings{grid-area:settings}.new-game-dialog .area-settings table input[type=text]{width:100%;box-sizing:border-box}.new-game-dialog .area-buttons{align-self:end;grid-area:buttons}.new-game-dialog .area-buttons button{width:100%}.new-game-dialog .has-image{position:relative;width:100%;height:100%}.new-game-dialog .has-image .remove{position:absolute;top:.5em;left:.5em} \ No newline at end of file diff --git a/build/public/index.html b/build/public/index.html index 0dfa3c3..64abc0c 100644 --- a/build/public/index.html +++ b/build/public/index.html @@ -4,9 +4,9 @@ 🧩 jigsaw.hyottoko.club - + - +
diff --git a/build/server/main.js b/build/server/main.js index aea4126..d990551 100644 --- a/build/server/main.js +++ b/build/server/main.js @@ -9,6 +9,7 @@ import exif from 'exif'; import sharp from 'sharp'; import bodyParser from 'body-parser'; import v8 from 'v8'; +import bsqlite from 'better-sqlite3'; class Rng { constructor(seed) { @@ -50,6 +51,12 @@ class Rng { } } +const slug = (str) => { + let tmp = str.toLowerCase(); + tmp = tmp.replace(/[^a-z0-9]+/g, '-'); + tmp = tmp.replace(/^-|-$/, ''); + return tmp; +}; const pad = (x, pad) => { const str = `${x}`; if (str.length >= pad.length) { @@ -194,6 +201,7 @@ function asQueryArgs(data) { } var Util = { hash, + slug, uniqId, encodeShape, decodeShape, @@ -207,7 +215,7 @@ var Util = { asQueryArgs, }; -const log$4 = logger('WebSocketServer.js'); +const log$5 = logger('WebSocketServer.js'); /* Example config @@ -245,12 +253,12 @@ class WebSocketServer { this._websocketserver.on('connection', (socket, request) => { const pathname = new URL(this.config.connectstring).pathname; if (request.url.indexOf(pathname) !== 0) { - log$4.log('bad request url: ', request.url); + log$5.log('bad request url: ', request.url); socket.close(); return; } socket.on('message', (data) => { - log$4.log(`ws`, socket.protocol, data); + log$5.log(`ws`, socket.protocol, data); this.evt.dispatch('message', { socket, data }); }); socket.on('close', () => { @@ -1163,9 +1171,11 @@ const BASE_DIR = `${__dirname}/../..`; const DATA_DIR = `${BASE_DIR}/data`; const UPLOAD_DIR = `${BASE_DIR}/data/uploads`; const UPLOAD_URL = `/uploads`; -const PUBLIC_DIR = `${BASE_DIR}/build/public/`; +const PUBLIC_DIR = `${BASE_DIR}/build/public/`; +const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches`; +const DB_FILE = `${BASE_DIR}/data/db.sqlite`; -const log$3 = logger('GameLog.js'); +const log$4 = logger('GameLog.js'); const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`; const create = (gameId) => { const file = filename(gameId); @@ -1196,8 +1206,8 @@ const get = (gameId) => { return JSON.parse(line); } catch (e) { - log$3.log(line); - log$3.log(e); + log$4.log(line); + log$4.log(e); } }); }; @@ -1248,16 +1258,71 @@ async function getExifOrientation(imagePath) { }); }); } -const allImages = (sort) => { +const getCategories = (db, imageId) => { + const query = ` +select * from categories c +inner join image_x_category ixc on c.id = ixc.category_id +where ixc.image_id = ?`; + return db._getMany(query, [imageId]); +}; +const imageFromDb = (db, imageId) => { + const i = db.get('images', { id: imageId }); + return { + id: i.id, + filename: i.filename, + file: `${UPLOAD_DIR}/${i.filename}`, + url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`, + title: i.title, + categories: getCategories(db, i.id), + created: i.created * 1000, + }; +}; +const allImagesFromDb = (db, categorySlug, sort) => { + const sortMap = { + alpha_asc: [{ filename: 1 }], + alpha_desc: [{ filename: -1 }], + date_asc: [{ created: 1 }], + date_desc: [{ created: -1 }], + }; + // TODO: .... clean up + const wheresRaw = {}; + if (categorySlug !== '') { + const c = db.get('categories', { slug: categorySlug }); + if (!c) { + return []; + } + const ids = db._getMany(` +select i.id from image_x_category ixc +inner join images i on i.id = ixc.image_id +where ixc.category_id = ?; +`, [c.id]).map(img => img.id); + if (ids.length === 0) { + return []; + } + wheresRaw['id'] = { '$in': ids }; + } + const images = db.getMany('images', wheresRaw, sortMap[sort]); + return images.map(i => ({ + id: i.id, + filename: i.filename, + file: `${UPLOAD_DIR}/${i.filename}`, + url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`, + title: i.title, + categories: getCategories(db, i.id), + created: i.created * 1000, + })); +}; +const allImagesFromDisk = (category, sort) => { let images = fs.readdirSync(UPLOAD_DIR) .filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/)) .map(f => ({ + id: 0, filename: f, file: `${UPLOAD_DIR}/${f}`, url: `${UPLOAD_URL}/${encodeURIComponent(f)}`, - title: '', - category: '', - ts: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(), + title: f.replace(/\.[a-z]+$/, ''), + categories: [], + created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(), })); switch (sort) { case 'alpha_asc': @@ -1272,13 +1337,13 @@ const allImages = (sort) => { break; case 'date_asc': images = images.sort((a, b) => { - return a.ts > b.ts ? 1 : -1; + return a.created > b.created ? 1 : -1; }); break; case 'date_desc': default: images = images.sort((a, b) => { - return a.ts < b.ts ? 1 : -1; + return a.created < b.created ? 1 : -1; }); break; } @@ -1298,7 +1363,9 @@ async function getDimensions(imagePath) { return dimensions; } var Images = { - allImages, + allImagesFromDisk, + imageFromDb, + allImagesFromDb, resizeImage, getDimensions, }; @@ -1477,7 +1544,7 @@ const determinePuzzleInfo = (w, h, targetTiles) => { }; }; -const log$2 = logger('GameStorage.js'); +const log$3 = logger('GameStorage.js'); const DIRTY_GAMES = {}; function setDirty(gameId) { DIRTY_GAMES[gameId] = true; @@ -1504,7 +1571,7 @@ function loadGame(gameId) { game = JSON.parse(contents); } catch { - log$2.log(`[ERR] unable to load game from file ${file}`); + log$3.log(`[ERR] unable to load game from file ${file}`); } if (typeof game.puzzle.data.started === 'undefined') { game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs); @@ -1549,7 +1616,7 @@ function persistGame(gameId) { players: game.players, scoreMode: game.scoreMode, })); - log$2.info(`[INFO] persisted game ${game.id}`); + log$3.info(`[INFO] persisted game ${game.id}`); } var GameStorage = { loadGames, @@ -1615,7 +1682,7 @@ var Game = { getFinishTs: GameCommon.getFinishTs, }; -const log$1 = logger('GameSocket.js'); +const log$2 = logger('GameSocket.js'); // Map const SOCKETS = {}; function socketExists(gameId, socket) { @@ -1629,8 +1696,8 @@ function removeSocket(gameId, socket) { return; } SOCKETS[gameId] = SOCKETS[gameId].filter((s) => s !== socket); - log$1.log('removed socket: ', gameId, socket.protocol); - log$1.log('socket count: ', Object.keys(SOCKETS[gameId]).length); + log$2.log('removed socket: ', gameId, socket.protocol); + log$2.log('socket count: ', Object.keys(SOCKETS[gameId]).length); } function addSocket(gameId, socket) { if (!(gameId in SOCKETS)) { @@ -1638,8 +1705,8 @@ function addSocket(gameId, socket) { } if (!SOCKETS[gameId].includes(socket)) { SOCKETS[gameId].push(socket); - log$1.log('added socket: ', gameId, socket.protocol); - log$1.log('socket count: ', Object.keys(SOCKETS[gameId]).length); + log$2.log('added socket: ', gameId, socket.protocol); + log$2.log('socket count: ', Object.keys(SOCKETS[gameId]).length); } } function getSockets(gameId) { @@ -1655,6 +1722,155 @@ var GameSockets = { getSockets, }; +const log$1 = logger('Db.ts'); +class Db { + constructor(file, patchesDir) { + this.file = file; + this.patchesDir = patchesDir; + this.dbh = bsqlite(this.file); + } + close() { + this.dbh.close(); + } + patch(verbose = true) { + if (!this.get('sqlite_master', { type: 'table', name: 'db_patches' })) { + this.run('CREATE TABLE db_patches ( id TEXT PRIMARY KEY);', []); + } + const files = fs.readdirSync(this.patchesDir); + const patches = (this.getMany('db_patches')).map(row => row.id); + for (const f of files) { + if (patches.includes(f)) { + if (verbose) { + log$1.info(`➑ skipping already applied db patch: ${f}`); + } + continue; + } + const contents = fs.readFileSync(`${this.patchesDir}/${f}`, 'utf-8'); + const all = contents.split(';').map(s => s.trim()).filter(s => !!s); + try { + this.dbh.transaction((all) => { + for (const q of all) { + if (verbose) { + log$1.info(`Running: ${q}`); + } + this.run(q); + } + this.insert('db_patches', { id: f }); + })(all); + log$1.info(`βœ“ applied db patch: ${f}`); + } + catch (e) { + log$1.error(`βœ– unable to apply patch: ${f} ${e}`); + return; + } + } + } + _buildWhere(where) { + const wheres = []; + const values = []; + for (const k of Object.keys(where)) { + if (where[k] === null) { + wheres.push(k + ' IS NULL'); + continue; + } + if (typeof where[k] === 'object') { + let prop = '$nin'; + if (where[k][prop]) { + if (where[k][prop].length > 0) { + wheres.push(k + ' NOT IN (' + where[k][prop].map((_) => '?') + ')'); + values.push(...where[k][prop]); + } + continue; + } + prop = '$in'; + if (where[k][prop]) { + if (where[k][prop].length > 0) { + wheres.push(k + ' IN (' + where[k][prop].map((_) => '?') + ')'); + values.push(...where[k][prop]); + } + continue; + } + // TODO: implement rest of mongo like query args ($eq, $lte, $in...) + throw new Error('not implemented: ' + JSON.stringify(where[k])); + } + wheres.push(k + ' = ?'); + values.push(where[k]); + } + return { + sql: wheres.length > 0 ? ' WHERE ' + wheres.join(' AND ') : '', + values, + }; + } + _buildOrderBy(orderBy) { + const sorts = []; + for (const s of orderBy) { + const k = Object.keys(s)[0]; + sorts.push(k + ' COLLATE NOCASE ' + (s[k] > 0 ? 'ASC' : 'DESC')); + } + return sorts.length > 0 ? ' ORDER BY ' + sorts.join(', ') : ''; + } + _get(query, params = []) { + return this.dbh.prepare(query).get(...params); + } + run(query, params = []) { + return this.dbh.prepare(query).run(...params); + } + _getMany(query, params = []) { + return this.dbh.prepare(query).all(...params); + } + get(table, whereRaw = {}, orderBy = []) { + const where = this._buildWhere(whereRaw); + const orderBySql = this._buildOrderBy(orderBy); + const sql = 'SELECT * FROM ' + table + where.sql + orderBySql; + return this._get(sql, where.values); + } + getMany(table, whereRaw = {}, orderBy = []) { + const where = this._buildWhere(whereRaw); + const orderBySql = this._buildOrderBy(orderBy); + const sql = 'SELECT * FROM ' + table + where.sql + orderBySql; + return this._getMany(sql, where.values); + } + delete(table, whereRaw = {}) { + const where = this._buildWhere(whereRaw); + const sql = 'DELETE FROM ' + table + where.sql; + return this.run(sql, where.values); + } + exists(table, whereRaw) { + return !!this.get(table, whereRaw); + } + upsert(table, data, check, idcol = null) { + if (!this.exists(table, check)) { + return this.insert(table, data); + } + this.update(table, data, check); + if (idcol === null) { + return 0; // dont care about id + } + return this.get(table, check)[idcol]; // get id manually + } + insert(table, data) { + const keys = Object.keys(data); + const values = keys.map(k => data[k]); + const sql = 'INSERT INTO ' + table + + ' (' + keys.join(',') + ')' + + ' VALUES (' + keys.map(k => '?').join(',') + ')'; + return this.run(sql, values).lastInsertRowid; + } + update(table, data, whereRaw = {}) { + const keys = Object.keys(data); + if (keys.length === 0) { + return; + } + const values = keys.map(k => data[k]); + const setSql = ' SET ' + keys.join(' = ?,') + ' = ?'; + const where = this._buildWhere(whereRaw); + const sql = 'UPDATE ' + table + setSql + where.sql; + this.run(sql, [...values, ...where.values]); + } +} + +const db = new Db(DB_FILE, DB_PATCHES_DIR); +db.patch(); let configFile = ''; let last = ''; for (const val of process.argv) { @@ -1686,8 +1902,8 @@ app.get('/api/conf', (req, res) => { app.get('/api/newgame-data', (req, res) => { const q = req.query; res.send({ - images: Images.allImages(q.sort), - categories: [], + images: Images.allImagesFromDb(db, q.category, q.sort), + categories: db.getMany('categories', {}, [{ title: 1 }]), }); }); app.get('/api/index-data', (req, res) => { @@ -1722,12 +1938,24 @@ app.post('/upload', (req, res) => { log.log(err); res.status(400).send("Something went wrong!"); } - res.send({ - image: { - file: `${UPLOAD_DIR}/${req.file.filename}`, - url: `${UPLOAD_URL}/${req.file.filename}`, - }, + const imageId = db.insert('images', { + filename: req.file.filename, + filename_original: req.file.originalname, + title: req.body.title || '', + created: Time.timestamp(), }); + if (req.body.category) { + const title = req.body.category; + const slug = Util.slug(title); + const id = db.upsert('categories', { slug, title }, { slug }, 'id'); + if (id) { + db.insert('image_x_category', { + image_id: imageId, + category_id: id, + }); + } + } + res.send(Images.imageFromDb(db, imageId)); }); }); app.post('/newgame', bodyParser.json(), async (req, res) => { diff --git a/package-lock.json b/package-lock.json index 9b2efea..c1aaffc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "better-sqlite3": "^7.4.0", "body-parser": "^1.19.0", "exif": "^0.6.0", "express": "^4.17.1", @@ -16,6 +17,7 @@ "ws": "^7.3.1" }, "devDependencies": { + "@types/better-sqlite3": "^5.4.1", "@types/exif": "^0.6.2", "@types/express": "^4.17.11", "@types/multer": "^1.4.5", @@ -1482,6 +1484,15 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/better-sqlite3": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-5.4.1.tgz", + "integrity": "sha512-8hje3Rhsg/9veTkALfCwiWn7VMrP1QDwHhBSgerttYPABEvrHsMQnU9dlqoM6QX3x4uw3Y06dDVz8uDQo1J4Ng==", + "dev": true, + "dependencies": { + "@types/integer": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -1548,6 +1559,12 @@ "@types/node": "*" } }, + "node_modules/@types/integer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/integer/-/integer-4.0.0.tgz", + "integrity": "sha512-2U1i6bIRiqizl6O+ETkp2HhUZIxg7g+burUabh9tzGd0qcszfNaFRaY9bGNlQKgEU7DCsH5qMajRDW5QamWQbw==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -2509,6 +2526,17 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/better-sqlite3": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.4.0.tgz", + "integrity": "sha512-hXwwaFvtYwRfjBSGP6+woB95qbwSnfpXyy/kDFzgOMoDttzyaWsBGcU3FGuRbzhbRv0qpKRCJQ6Hru2pQ8adxg==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^6.0.1", + "tar": "^6.1.0" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -2519,6 +2547,14 @@ "node": "*" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -4180,6 +4216,11 @@ "bser": "2.1.1" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4331,6 +4372,17 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7372,6 +7424,39 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, + "node_modules/minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -9982,6 +10067,22 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tar": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", + "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -10048,6 +10149,30 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -12107,6 +12232,15 @@ "@babel/types": "^7.3.0" } }, + "@types/better-sqlite3": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-5.4.1.tgz", + "integrity": "sha512-8hje3Rhsg/9veTkALfCwiWn7VMrP1QDwHhBSgerttYPABEvrHsMQnU9dlqoM6QX3x4uw3Y06dDVz8uDQo1J4Ng==", + "dev": true, + "requires": { + "@types/integer": "*" + } + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -12173,6 +12307,12 @@ "@types/node": "*" } }, + "@types/integer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/integer/-/integer-4.0.0.tgz", + "integrity": "sha512-2U1i6bIRiqizl6O+ETkp2HhUZIxg7g+burUabh9tzGd0qcszfNaFRaY9bGNlQKgEU7DCsH5qMajRDW5QamWQbw==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -12971,6 +13111,16 @@ "tweetnacl": "^0.14.3" } }, + "better-sqlite3": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.4.0.tgz", + "integrity": "sha512-hXwwaFvtYwRfjBSGP6+woB95qbwSnfpXyy/kDFzgOMoDttzyaWsBGcU3FGuRbzhbRv0qpKRCJQ6Hru2pQ8adxg==", + "requires": { + "bindings": "^1.5.0", + "prebuild-install": "^6.0.1", + "tar": "^6.1.0" + } + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -12978,6 +13128,14 @@ "dev": true, "peer": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -14304,6 +14462,11 @@ "bser": "2.1.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -14421,6 +14584,14 @@ "universalify": "^0.1.0" } }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -16743,6 +16914,37 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -18801,6 +19003,36 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "tar": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", + "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", diff --git a/package.json b/package.json index 6f2c7aa..0a8f0d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "type": "module", "dependencies": { + "better-sqlite3": "^7.4.0", "body-parser": "^1.19.0", "exif": "^0.6.0", "express": "^4.17.1", @@ -12,6 +13,7 @@ "ws": "^7.3.1" }, "devDependencies": { + "@types/better-sqlite3": "^5.4.1", "@types/exif": "^0.6.2", "@types/express": "^4.17.11", "@types/multer": "^1.4.5", @@ -29,6 +31,7 @@ }, "scripts": { "rollup": "rollup", - "vite": "vite" + "vite": "vite", + "ts-node": "node --experimental-specifier-resolution=node --loader ts-node/esm" } } diff --git a/rollup.server.config.js b/rollup.server.config.js index 3aac259..117fee1 100644 --- a/rollup.server.config.js +++ b/rollup.server.config.js @@ -20,7 +20,5 @@ export default { "url", "path", ], - plugins: [typescript({ - "tsconfig": "tsconfig.server.json" - })], + plugins: [typescript()], }; diff --git a/scripts/import_images.ts b/scripts/import_images.ts new file mode 100644 index 0000000..bcc071b --- /dev/null +++ b/scripts/import_images.ts @@ -0,0 +1,23 @@ +import { DB_FILE, DB_PATCHES_DIR } from '../src/server/Dirs' +import Db from '../src/server/Db' +import Images from '../src/server/Images' + +const db = new Db(DB_FILE, DB_PATCHES_DIR) +db.patch(true) + +const cat = '' +const sort = 'date_desc' +let images = Images.allImagesFromDisk(cat, sort) +images.forEach((image: any) => { + db.upsert('images', { + filename: image.filename, + filename_original: image.filename, + title: image.title, + created: image.created / 1000, + }, { + filename: image.filename + }) +}) + +images = Images.allImagesFromDb(db, cat, sort) +console.log(images) diff --git a/scripts/server b/scripts/server index 2487009..f3e2fdf 100755 --- a/scripts/server +++ b/scripts/server @@ -1,5 +1,11 @@ #!/bin/sh +# TODO: add switch via param + +# server for built files cd "$RUN_DIR/build/server" nodemon --max-old-space-size=64 main.js -c ../../config.json + +# dev server +# npm run ts-node src/server/main.ts -- -c config.json diff --git a/src/common/GameCommon.ts b/src/common/GameCommon.ts index 46d3189..dfb7801 100644 --- a/src/common/GameCommon.ts +++ b/src/common/GameCommon.ts @@ -8,8 +8,11 @@ export type EncodedPlayer = Array export type EncodedPiece = Array export type EncodedPieceShape = number -// TODO: maybe something other than string in the future -export type Category = string +export interface Category { + id: number + slug: string + title: string +} interface GameRng { obj: Rng @@ -26,10 +29,13 @@ interface Game { } export interface Image { + id: number + filename: string file: string url: string - category: Category title: string + categories: Array + created: number } export interface GameSettings { diff --git a/src/common/Util.ts b/src/common/Util.ts index 69248fa..f691b75 100644 --- a/src/common/Util.ts +++ b/src/common/Util.ts @@ -2,6 +2,12 @@ import { EncodedPiece, EncodedPieceShape, EncodedPlayer, Piece, PieceShape, Play import { Point } from './Geometry' import { Rng } from './Rng' +const slug = (str: string) => { + let tmp = str.toLowerCase() + tmp = tmp.replace(/[^a-z0-9]+/g, '-') + tmp = tmp.replace(/^-|-$/, '') + return tmp +} const pad = (x: any, pad: string) => { const str = `${x}` @@ -162,6 +168,7 @@ function asQueryArgs(data: any) { export default { hash, + slug, uniqId, encodeShape, diff --git a/src/dbpatches/01_initial.sqlite b/src/dbpatches/01_initial.sqlite new file mode 100644 index 0000000..f371746 --- /dev/null +++ b/src/dbpatches/01_initial.sqlite @@ -0,0 +1,24 @@ +CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + slug TEXT UNIQUE, + title TEXT UNIQUE +); + +CREATE TABLE images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + created TIMESTAMP NOT NULL, + + filename TEXT NOT NULL UNIQUE, + filename_original TEXT NOT NULL, + title TEXT NOT NULL +); + +CREATE TABLE image_x_category ( + image_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + + FOREIGN KEY(image_id) REFERENCES images(id) ON DELETE CASCADE, + FOREIGN KEY(category_id) REFERENCES categories(id) ON DELETE CASCADE +); diff --git a/src/frontend/components/NewGameDialog.vue b/src/frontend/components/NewGameDialog.vue index ba6a5ec..0eff20a 100644 --- a/src/frontend/components/NewGameDialog.vue +++ b/src/frontend/components/NewGameDialog.vue @@ -106,6 +106,7 @@ export default defineComponent({ } .new-game-dialog .area-image { grid-area: image; + margin: 20px; } .new-game-dialog .area-settings { grid-area: settings; diff --git a/src/frontend/components/NewImageDialog.vue b/src/frontend/components/NewImageDialog.vue index c130d36..ac0ed7e 100644 --- a/src/frontend/components/NewImageDialog.vue +++ b/src/frontend/components/NewImageDialog.vue @@ -7,15 +7,21 @@ gallery", if possible!
-
+
-
- X - +
+ X +
+ + + - +
@@ -23,7 +29,7 @@ gallery", if possible! - + - +
@@ -33,13 +39,13 @@ gallery", if possible!
- +
@@ -51,7 +57,6 @@ import { defineComponent } from 'vue' import Upload from './Upload.vue' import ResponsiveImage from './ResponsiveImage.vue' -import { Image } from '../../common/GameCommon' export default defineComponent({ name: 'new-image-dialog', @@ -62,35 +67,51 @@ export default defineComponent({ emits: { bgclick: null, setupGameClick: null, + postToGalleryClick: null, }, data () { return { - image: { - file: '', - url: '', - title: '', - category: '', - } as Image, + previewUrl: '', + file: null as File|null, + title: '', + category: '', } }, computed: { - canPostToGallery () { - return !!this.image.url + canPostToGallery (): boolean { + return !!(this.previewUrl && this.file) }, - canSetupGameClick () { - return !!this.image.url + canSetupGameClick (): boolean { + return !!(this.previewUrl && this.file) }, }, methods: { - mediaImgUploaded(data: any) { - this.image.file = data.image.file - this.image.url = data.image.url + preview (evt: Event) { + const target = (evt.target as HTMLInputElement) + if (!target.files) return; + const file = target.files[0] + if (!file) return; + + const r = new FileReader() + r.readAsDataURL(file) + r.onload = (ev: any) => { + this.previewUrl = ev.target.result + this.file = file + } }, postToGallery () { - this.$emit('postToGallery', this.image) + this.$emit('postToGalleryClick', { + file: this.file, + title: this.title, + category: this.category, + }) }, setupGameClick () { - this.$emit('setupGameClick', this.image) + this.$emit('setupGameClick', { + file: this.file, + title: this.title, + category: this.category, + }) }, }, }) @@ -111,12 +132,12 @@ export default defineComponent({ .new-image-dialog .area-image { grid-area: image; + margin: 20px; } .new-image-dialog .area-image.no-image { align-content: center; display: grid; text-align: center; - margin: 20px; border: dashed 6px; position: relative; } @@ -146,6 +167,7 @@ export default defineComponent({ } .new-image-dialog .area-buttons button { width: 100%; + margin-top: .5em; } .new-image-dialog .upload { position: absolute; diff --git a/src/frontend/views/NewGame.vue b/src/frontend/views/NewGame.vue index bc478eb..b2332a6 100644 --- a/src/frontend/views/NewGame.vue +++ b/src/frontend/views/NewGame.vue @@ -17,7 +17,7 @@ in jigsawpuzzles.io Category:
- - + +
@@ -61,10 +61,13 @@ export default defineComponent({ categories: [], image: { - url: '', + id: 0, + filename: '', file: '', + url: '', title: '', - category: '', + categories: [], + created: 0, } as Image, dialog: '', @@ -87,7 +90,26 @@ export default defineComponent({ this.image = image this.dialog = 'new-game' }, - setupGameClick (image: Image) { + async uploadImage (data: any) { + const formData = new FormData(); + formData.append('file', data.file, data.file.name); + formData.append('title', data.title) + formData.append('category', data.category) + + const res = await fetch('/upload', { + method: 'post', + body: formData, + }) + return await res.json() + }, + async postToGalleryClick(data: any) { + await this.uploadImage(data) + this.dialog = '' + await this.loadImages() + }, + async setupGameClick (data: any) { + const image = await this.uploadImage(data) + this.loadImages() // load images in background this.image = image this.dialog = 'new-game' }, diff --git a/src/server/Db.ts b/src/server/Db.ts new file mode 100644 index 0000000..a3277f0 --- /dev/null +++ b/src/server/Db.ts @@ -0,0 +1,212 @@ +import fs from 'fs' +import bsqlite from 'better-sqlite3' +import Integer from 'integer' +import { logger } from '../common/Util' + +const log = logger('Db.ts') + +/** + * TODO: create a more specific type for OrderBy. + * It looks like this (example): + * [ + * {id: -1}, // by id descending + * {name: 1}, // then by name ascending + * ] + */ +type OrderBy = Array +type Data = Record +type WhereRaw = Record +type Params = Array + +interface Where { + sql: string + values: Array +} + +class Db { + file: string + patchesDir: string + dbh: bsqlite.Database + + constructor(file: string, patchesDir: string) { + this.file = file + this.patchesDir = patchesDir + this.dbh = bsqlite(this.file) + } + + close(): void { + this.dbh.close() + } + + patch (verbose: boolean =true): void { + if (!this.get('sqlite_master', {type: 'table', name: 'db_patches'})) { + this.run('CREATE TABLE db_patches ( id TEXT PRIMARY KEY);', []) + } + + const files = fs.readdirSync(this.patchesDir) + const patches = (this.getMany('db_patches')).map(row => row.id) + + for (const f of files) { + if (patches.includes(f)) { + if (verbose) { + log.info(`➑ skipping already applied db patch: ${f}`) + } + continue + } + const contents = fs.readFileSync(`${this.patchesDir}/${f}`, 'utf-8') + + const all = contents.split(';').map(s => s.trim()).filter(s => !!s) + try { + this.dbh.transaction((all) => { + for (const q of all) { + if (verbose) { + log.info(`Running: ${q}`) + } + this.run(q) + } + this.insert('db_patches', {id: f}) + })(all) + + log.info(`βœ“ applied db patch: ${f}`) + } catch (e) { + log.error(`βœ– unable to apply patch: ${f} ${e}`) + return + } + } + } + + _buildWhere (where: WhereRaw): Where { + const wheres = [] + const values = [] + for (const k of Object.keys(where)) { + if (where[k] === null) { + wheres.push(k + ' IS NULL') + continue + } + + if (typeof where[k] === 'object') { + let prop = '$nin' + if (where[k][prop]) { + if (where[k][prop].length > 0) { + wheres.push(k + ' NOT IN (' + where[k][prop].map((_: any) => '?') + ')') + values.push(...where[k][prop]) + } + continue + } + prop = '$in' + if (where[k][prop]) { + if (where[k][prop].length > 0) { + wheres.push(k + ' IN (' + where[k][prop].map((_: any) => '?') + ')') + values.push(...where[k][prop]) + } + continue + } + + // TODO: implement rest of mongo like query args ($eq, $lte, $in...) + throw new Error('not implemented: ' + JSON.stringify(where[k])) + } + + wheres.push(k + ' = ?') + values.push(where[k]) + } + + return { + sql: wheres.length > 0 ? ' WHERE ' + wheres.join(' AND ') : '', + values, + } + } + + _buildOrderBy (orderBy: OrderBy): string { + const sorts = [] + for (const s of orderBy) { + const k = Object.keys(s)[0] + sorts.push(k + ' COLLATE NOCASE ' + (s[k] > 0 ? 'ASC' : 'DESC')) + } + return sorts.length > 0 ? ' ORDER BY ' + sorts.join(', ') : '' + } + + _get (query: string, params: Params = []): any { + return this.dbh.prepare(query).get(...params) + } + + run (query: string, params: Params = []): bsqlite.RunResult { + return this.dbh.prepare(query).run(...params) + } + + _getMany (query: string, params: Params = []): Array { + return this.dbh.prepare(query).all(...params) + } + + get ( + table: string, + whereRaw: WhereRaw = {}, + orderBy: OrderBy = [] + ): any { + const where = this._buildWhere(whereRaw) + const orderBySql = this._buildOrderBy(orderBy) + const sql = 'SELECT * FROM ' + table + where.sql + orderBySql + return this._get(sql, where.values) + } + + getMany ( + table: string, + whereRaw: WhereRaw = {}, + orderBy: OrderBy = [] + ): Array { + const where = this._buildWhere(whereRaw) + const orderBySql = this._buildOrderBy(orderBy) + const sql = 'SELECT * FROM ' + table + where.sql + orderBySql + return this._getMany(sql, where.values) + } + + delete (table: string, whereRaw: WhereRaw = {}): bsqlite.RunResult { + const where = this._buildWhere(whereRaw) + const sql = 'DELETE FROM ' + table + where.sql + return this.run(sql, where.values) + } + + exists (table: string, whereRaw: WhereRaw): boolean { + return !!this.get(table, whereRaw) + } + + upsert ( + table: string, + data: Data, + check: WhereRaw, + idcol: string|null = null + ): any { + if (!this.exists(table, check)) { + return this.insert(table, data) + } + this.update(table, data, check) + if (idcol === null) { + return 0 // dont care about id + } + + return this.get(table, check)[idcol] // get id manually + } + + insert (table: string, data: Data): Integer.IntLike { + const keys = Object.keys(data) + const values = keys.map(k => data[k]) + const sql = 'INSERT INTO '+ table + + ' (' + keys.join(',') + ')' + + ' VALUES (' + keys.map(k => '?').join(',') + ')' + return this.run(sql, values).lastInsertRowid + } + + update (table: string, data: Data, whereRaw: WhereRaw = {}): void { + const keys = Object.keys(data) + if (keys.length === 0) { + return + } + const values = keys.map(k => data[k]) + const setSql = ' SET ' + keys.join(' = ?,') + ' = ?' + const where = this._buildWhere(whereRaw) + + const sql = 'UPDATE ' + table + setSql + where.sql + this.run(sql, [...values, ...where.values]) + } +} + +export default Db diff --git a/src/server/Dirs.ts b/src/server/Dirs.ts index 9a5dde8..77bc63c 100644 --- a/src/server/Dirs.ts +++ b/src/server/Dirs.ts @@ -10,3 +10,6 @@ export const DATA_DIR = `${BASE_DIR}/data` export const UPLOAD_DIR = `${BASE_DIR}/data/uploads` export const UPLOAD_URL = `/uploads` export const PUBLIC_DIR = `${BASE_DIR}/build/public/` + +export const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches` +export const DB_FILE = `${BASE_DIR}/data/db.sqlite` diff --git a/src/server/Images.ts b/src/server/Images.ts index 6d1a536..9e231e8 100644 --- a/src/server/Images.ts +++ b/src/server/Images.ts @@ -4,6 +4,7 @@ import exif from 'exif' import sharp from 'sharp' import {UPLOAD_DIR, UPLOAD_URL} from './Dirs' +import Db from './Db' const resizeImage = async (filename: string) => { if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) { @@ -46,16 +47,76 @@ async function getExifOrientation(imagePath: string) { }) } -const allImages = (sort: string) => { +const getCategories = (db: Db, imageId: number) => { + const query = ` +select * from categories c +inner join image_x_category ixc on c.id = ixc.category_id +where ixc.image_id = ?` + return db._getMany(query, [imageId]) +} + +const imageFromDb = (db: Db, imageId: number) => { + const i = db.get('images', { id: imageId }) + return { + id: i.id, + filename: i.filename, + file: `${UPLOAD_DIR}/${i.filename}`, + url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`, + title: i.title, + categories: getCategories(db, i.id) as any[], + created: i.created * 1000, + } +} + +const allImagesFromDb = (db: Db, categorySlug: string, sort: string) => { + const sortMap = { + alpha_asc: [{filename: 1}], + alpha_desc: [{filename: -1}], + date_asc: [{created: 1}], + date_desc: [{created: -1}], + } as Record + + // TODO: .... clean up + const wheresRaw: Record = {} + if (categorySlug !== '') { + const c = db.get('categories', {slug: categorySlug}) + if (!c) { + return [] + } + const ids = db._getMany(` +select i.id from image_x_category ixc +inner join images i on i.id = ixc.image_id +where ixc.category_id = ?; +`, [c.id]).map(img => img.id) + if (ids.length === 0) { + return [] + } + wheresRaw['id'] = {'$in': ids} + } + const images = db.getMany('images', wheresRaw, sortMap[sort]) + + return images.map(i => ({ + id: i.id as number, + filename: i.filename, + file: `${UPLOAD_DIR}/${i.filename}`, + url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`, + title: i.title, + categories: getCategories(db, i.id) as any[], + created: i.created * 1000, + })) +} + +const allImagesFromDisk = (category: string, sort: string) => { let images = fs.readdirSync(UPLOAD_DIR) .filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/)) .map(f => ({ + id: 0, filename: f, file: `${UPLOAD_DIR}/${f}`, url: `${UPLOAD_URL}/${encodeURIComponent(f)}`, - title: '', - category: '', - ts: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(), + title: f.replace(/\.[a-z]+$/, ''), + categories: [] as any[], + created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(), })) switch (sort) { @@ -73,14 +134,14 @@ const allImages = (sort: string) => { case 'date_asc': images = images.sort((a, b) => { - return a.ts > b.ts ? 1 : -1 + return a.created > b.created ? 1 : -1 }) break; case 'date_desc': default: images = images.sort((a, b) => { - return a.ts < b.ts ? 1 : -1 + return a.created < b.created ? 1 : -1 }) break; } @@ -102,7 +163,9 @@ async function getDimensions(imagePath: string) { } export default { - allImages, + allImagesFromDisk, + imageFromDb, + allImagesFromDb, resizeImage, getDimensions, } diff --git a/src/server/main.ts b/src/server/main.ts index ebc0912..6bc3903 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -12,9 +12,19 @@ import GameLog from './GameLog' import GameSockets from './GameSockets' import Time from './../common/Time' import Images from './Images' -import { UPLOAD_DIR, UPLOAD_URL, PUBLIC_DIR } from './Dirs' +import { + DB_FILE, + DB_PATCHES_DIR, + PUBLIC_DIR, + UPLOAD_DIR, + UPLOAD_URL +} from './Dirs' import { GameSettings, ScoreMode } from '../common/GameCommon' import GameStorage from './GameStorage' +import Db from './Db' + +const db = new Db(DB_FILE, DB_PATCHES_DIR) +db.patch() let configFile = '' let last = '' @@ -54,8 +64,8 @@ app.get('/api/conf', (req, res) => { app.get('/api/newgame-data', (req, res) => { const q = req.query as any res.send({ - images: Images.allImages(q.sort), - categories: [], + images: Images.allImagesFromDb(db, q.category, q.sort), + categories: db.getMany('categories', {}, [{ title: 1 }]), }) }) @@ -94,12 +104,26 @@ app.post('/upload', (req, res) => { res.status(400).send("Something went wrong!"); } - res.send({ - image: { - file: `${UPLOAD_DIR}/${req.file.filename}`, - url: `${UPLOAD_URL}/${req.file.filename}`, - }, + const imageId = db.insert('images', { + filename: req.file.filename, + filename_original: req.file.originalname, + title: req.body.title || '', + created: Time.timestamp(), }) + + if (req.body.category) { + const title = req.body.category + const slug = Util.slug(title) + const id = db.upsert('categories', { slug, title }, { slug }, 'id') + if (id) { + db.insert('image_x_category', { + image_id: imageId, + category_id: id, + }) + } + } + + res.send(Images.imageFromDb(db, imageId as number)) }) }) diff --git a/tsconfig.server.json b/tsconfig.json similarity index 100% rename from tsconfig.server.json rename to tsconfig.json diff --git a/vite.config.js b/vite.config.js index 7d63d4d..01fa870 100644 --- a/vite.config.js +++ b/vite.config.js @@ -14,6 +14,10 @@ export default vite.defineConfig({ target: 'http://localhost:1337', secure: false, }, + '^/upload': { + target: 'http://localhost:1337', + secure: false, + }, }, }, })