diff --git a/README.md b/README.md new file mode 100644 index 0000000..bfc5f91 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +

+

Family Tree app for nextcloud

+

A Nextcloud app that lets you design freeform family trees.

+

diff --git a/appinfo/info.xml b/appinfo/info.xml new file mode 100644 index 0000000..de8da44 --- /dev/null +++ b/appinfo/info.xml @@ -0,0 +1,28 @@ + + + familytree + Family Tree + Family Tree + + 1.2.1 + agpl + DerBen + familytree + tools + https://github.com/derbenx/nextcloud-familytree + https://github.com/derbenx/nextcloud-familytree/issues + https://raw.githubusercontent.com/derbenx/nextcloud-familytree/familytree.png + + + + + + Family Tree + familytree.page.index + + + diff --git a/appinfo/routes.php b/appinfo/routes.php new file mode 100644 index 0000000..4529eea --- /dev/null +++ b/appinfo/routes.php @@ -0,0 +1,12 @@ + [ + ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], + ] +]; diff --git a/css/css.css b/css/css.css new file mode 100644 index 0000000..fe58d4e --- /dev/null +++ b/css/css.css @@ -0,0 +1,11 @@ + #can{top:0px;left:0px;position:fixed;} + table,.inp { width:95%; } + tr,td,input,#r,#s { min-height:25px !important; height:25px !important; } + #addPer,#updPer,#delPer,#cent,#nt,#st,#lt,#fu,#sh { padding:0px 7px 0px 7px !important; font-size: 13px !important; min-height:23px !important; height:23px !important; } + #bg {min-height:23px !important; height:23px !important;} + td { border:1px dotted green; color:black; text-align:center; } + tr:hover {background:none !important; } + #ln,#hp { width:49%; height:150px; } + #inp { z-index:99;position:relative; background:grey; color:black; } + #sh,#fu { z-index:99;position:relative; } + diff --git a/familytree.png b/familytree.png new file mode 100644 index 0000000..d1caa86 Binary files /dev/null and b/familytree.png differ diff --git a/familytreess.png b/familytreess.png new file mode 100644 index 0000000..646abb4 Binary files /dev/null and b/familytreess.png differ diff --git a/img/Solitaire.png b/img/Solitaire.png new file mode 100644 index 0000000..90431c1 Binary files /dev/null and b/img/Solitaire.png differ diff --git a/img/app.svg b/img/app.svg new file mode 100644 index 0000000..ef04c23 --- /dev/null +++ b/img/app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/familytree.png b/img/familytree.png new file mode 100644 index 0000000..d1caa86 Binary files /dev/null and b/img/familytree.png differ diff --git a/js/js.js b/js/js.js new file mode 100644 index 0000000..5b59a74 --- /dev/null +++ b/js/js.js @@ -0,0 +1,512 @@ + +/* + TO DO + + load GED files (partial) + zoom + manually add lines to merge branches + photos/sounds? + +*/ + +//Global Variables *** +//define json structure + +const box={w:200,h:100}; +const cent = document.getElementById('cent'); +const aPer = document.getElementById('addPer'); +const uPer = document.getElementById('updPer'); +const dPer = document.getElementById('delPer'); +const fu = document.getElementById('fu'); +const hd = document.getElementById('sh'); +const bg = document.getElementById('bg'); +const mu = document.getElementById('mu'); +const ln = document.getElementById('ln'); +const hp = document.getElementById('hp'); +const st = document.getElementById('st'); +const lt = document.getElementById('lt'); +const tr = document.getElementById('t'); +const gn = document.getElementById('g'); +const fn = document.getElementById('f'); +const sx = document.getElementById('s'); +const br = document.getElementById('b'); +const oi = document.getElementById('o'); +const dt = document.getElementById('d'); +const rl = document.getElementById('r'); +const can=document.getElementById('can'); +const ctx = can.getContext("2d"); +var data='{ "pan":[ { "x":0,"y":0 } ], "tree":"My Family", "select":0, "people":[ ], "lines":[ ] }'; +var sex=['pink','cyan','#552255','grey']; +var rel=['white','red','green','blue','orange','yellow','grey']; +var rex=['undecided','marriage','parental','siblings','adopted','divorced','unwed parents']; +var aw=window.innerWidth; +var ah=window.innerHeight; +var hsChld=[]; //delete helper +var fs=1,hole,json,drag=0,selc=[-1],rstim; //scrn resize timer; +var p={x:10,y:15}; //drag pan previous +bg.selectedIndex=0; +document.getElementById('updPer').disabled=true; +document.getElementById('delPer').disabled=true; + +//Functions *** +function draw(cl=0){ + //draw data from JSON + if (cl==1){ ctx.clearRect(0, 0, can.width, can.height); } + + var aw=window.innerWidth+box.w,ah=window.innerHeight+box.h; + lx=-box.w;ly=-box.h; //lower draw limit ^^ upper draw limit + var yyy=parseInt(can.style.top); //can offset + + //lines + for(let i=0;ilx && sy>ly && ex>lx && ey>ly && sxlx && sy>ly && ex>lx && ey>ly && sx-1 && selc.includes(i)) { + ctx.strokeStyle="orange"; + ctx.lineWidth = 8; + ctx.stroke(); + } + font=18; + ctx.fillStyle="#000"; + ctx.font = font+"px times"; + ctx.fillText('id:'+json.people[i].id, sx+10, sy+(font)); + ctx.fillText(json.people[i].gn, sx+10,sy+(font*2)); + ctx.fillText(json.people[i].fn, sx+10,sy+(font*3)); + if (json.people[i].br){ + ctx.fillText('Birth: '+json.people[i].br, sx+10,sy+(font*4)); + } + if (json.people[i].dt){ + ctx.fillText('Death: '+json.people[i].dt, sx+10,sy+(font*5)); + } + } + } + } +} + +function pan(xx,yy,cl=1,dd=0){ + if (cl==1){ ctx.clearRect(0, 0, can.width, can.height); } + //console.log(xx,xx.currentTarget.cen); + //for(let i=0;i0) { + //only enable if no sublinks + document.getElementById('delPer').disabled=true; + } else { + document.getElementById('delPer').disabled=false; + } + + if (selc[0]>-1){ + document.getElementById('updPer').disabled=false; + + gn.value=json.people[selc[0]].gn; + fn.value=json.people[selc[0]].fn; + sx.selectedIndex=json.people[selc[0]].sx; + br.value=json.people[selc[0]].br; + dt.value=json.people[selc[0]].dt; + oi.value=json.people[selc[0]].oi || ""; + tr.value=json.tree; + } else { + //document.getElementById('delPer').disabled=true; + document.getElementById('updPer').disabled=true; + } + draw(1); + //console.log('down',evn); +} + +function movr(evn){ + if (drag) { + + //console.log('drag',evn,selc); + //console.log('drag',evn.clientX,evn.clientY); + if (selc[0]==-1) { + //pan canvas + pan(evn.clientX-p.x,evn.clientY-p.y); + } else if (json.people[selc[0]]!=null) { + //or move person + //console.log('poo'); + //var ro=10; + //xy=[Math.round((evn.clientX-p.x)/ro)*ro,Math.round((evn.clientY-p.y)/ro)*ro]; + for (var i=0;i=0;i--){ + if (json.people[i]!=null){ + var tx=json.people[i].x+json.pan[0].x; + var ty=json.people[i].y+json.pan[0].y; + var tw=tx+json.people[i].w; + var th=ty+json.people[i].h; + //console.log('xywh',tx,ty,tw,th); + if (xx>tx && xxty && yy0 && a==-1){ + alert("Can't add person without relation, select someone first!"); return; + } + var x,y; + var b=hole.length>0 ? hole[0] : json.people.length; + //console.log(a); + if (a!=-1) { + x=json.people[a].x+250; + y=json.people[a].y; + } else { + x=100;y=300; + } + var tmp={"id":b,"x":x,"y":y,"w":box.w,"h":box.h,"gn":gn.value,"fn":fn.value,"sx":sx.selectedIndex,"br":br.value,"dt":dt.value,"oi":oi.value}; + if (hole.length>0){ + //check for hole + //console.log(hole); + json.people[hole[0]]=tmp; + hole.shift(); + } else { + //add to end + json.people.push(tmp); + } + + if (a!=-1) { + json.lines.push({"id":a+"-"+b,"rl":rl.selectedIndex}); + } + //hsChld[a].push(b); + bldHsCh(); + poplst(); + draw(1); +} +function updPer(){ + //console.log(selc); + if (selc[0]!=-1) { + json.people[selc[0]].gn=gn.value; + json.people[selc[0]].fn=fn.value; + json.people[selc[0]].br=br.value; + json.people[selc[0]].dt=dt.value; + json.people[selc[0]].oi=oi.value; + json.people[selc[0]].sx=sx.selectedIndex; + //console.log(); + draw(1); + } +} +function delPer(){ + //console.log(selc,hsChld[selc]); + if (selc[0]==0 || hsChld[selc[0]] && hsChld[selc[0]].length>0) { + alert("Can't delete person with sub branches or id:0"); + return; + } else { + //delete + json.people[selc[0]]=null; + //mark as hole for reuse; + hole.push(selc[0]); + } + for (a in json.lines) { ln.options.remove(0); } //clear + + for(let i=0;i { + var inp=document.getElementById("inp").getBoundingClientRect(); + var tmp=inp.top + window.scrollY; + //console.log(tmp,inp.top, window.scrollY); + can.style.top=tmp+"px"; + draw(1); + }, "500"); +} + +//Listeners *** +window.addEventListener('resize', function(event) { rstim=setTimeout(scale,150); }, true); +can.onmousedown = clkd; +can.onmouseout= clku; +can.onmouseup = clku; +can.onmousemove = movr; +can.addEventListener("touchstart", clkd, {passive: true}); +can.addEventListener("touchend", clku, false); +//spr.addEventListener("touchcancel", handleCancel, false); +can.addEventListener("touchmove", movr, {passive: true}); +aPer.addEventListener('click', addPer); +uPer.addEventListener('click', updPer); +dPer.addEventListener('click', delPer); +cent.addEventListener('click', pan); +lt.onclick = function(){ openFD('.json',load) } +st.addEventListener('click', save); +nt.addEventListener('click', newt); +ln.addEventListener('click', mselc); +ln.addEventListener('keyup', mselc); +rl.addEventListener('change', updln); +bg.addEventListener('change', chbg); +hd.addEventListener('click', hide); +fu.addEventListener('click', fullscreen); +//ln.addEventListener('dblclick', updln); +json = JSON.parse(data); +cent.cen = 1; +pan(0,150); //move canvas down +fullscreen(); // Set up offset +start(); \ No newline at end of file diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php new file mode 100644 index 0000000..ffc60f2 --- /dev/null +++ b/lib/Controller/PageController.php @@ -0,0 +1,37 @@ +userId = $UserId; + } + + /** + * CAUTION: the @Stuff turns off security checks; for this page no admin is + * required and no CSRF check. If you don't know what CSRF is, read + * it up in the docs or you might create a security hole. This is + * basically the only required method to add this exemption, don't + * add it to any other method if you don't exactly know what it does + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function index() { + $response = new TemplateResponse('familytree', 'index'); // templates/index.php + $csp = new ContentSecurityPolicy(); + $csp->allowEvalScript(true); + $response->setContentSecurityPolicy($csp); + + return $response; + } + +} diff --git a/templates/content/index.php b/templates/content/index.php new file mode 100644 index 0000000..081bc46 --- /dev/null +++ b/templates/content/index.php @@ -0,0 +1,48 @@ +
+ + +
+ + + + + + + + + + +
Tree name:
ID Lines: select id pair and set relation on left.
Given names:
Family name:
Gender: + +
Birth:
Death:
Other info:
Relation: + +
Buttons:Version 1.2.1
+ + + + + +Background colour: + Select multiple +
+ +
\ No newline at end of file diff --git a/templates/index.php b/templates/index.php new file mode 100644 index 0000000..5b5e189 --- /dev/null +++ b/templates/index.php @@ -0,0 +1,6 @@ + +inc('content/index')); ?> +