Commit b2ff4e29 authored by Matthieu Le Corre's avatar Matthieu Le Corre

backend almost done

Signed-off-by: Matthieu Le Corre's avatarMatthieu Le Corre <matthieu.lecorre@univ-nantes.fr>
parent 84f6456b
......@@ -24,11 +24,16 @@
[ 'name' => 'file#save', 'url' => '/file/save', 'verb' => 'POST' ],
[ 'name' => 'file#load', 'url' => '/file/load', 'verb' => 'GET' ],
[ 'name'=> 'collaboration#createSession', 'url' => '/collaboration/createsession','verb' => 'POST'],
[ 'name'=> 'collaboration#startSession', 'url' => '/collaboration/startsession','verb' => 'POST'],
[ 'name'=> 'collaboration#addStep', 'url' => '/collaboration/addstep','verb' => 'POST'],
[ 'name'=> 'collaboration#getStep', 'url' => '/collaboration/getstep','verb' => 'GET'],
[ 'name'=> 'collaboration#getSteps', 'url' => '/collaboration/getsteps','verb' => 'GET'],
[ 'name'=> 'collaboration#addUser', 'url' => '/collaboration/adduser','verb' => 'POST'],
[ 'name'=> 'collaboration#removeUser', 'url' => '/collaboration/removeuser','verb' => 'POST']
[ 'name'=> 'collaboration#removeUser', 'url' => '/collaboration/removeuser','verb' => 'POST'],
[ 'name'=> 'collaboration#getUserList', 'url' => '/collaboration/getuserlist','verb' => 'GET'],
[ 'name'=> 'collaboration#pushStep', 'url'=> '/collaboration/pushstep', 'verb' => 'GET']
]
......
......@@ -14,7 +14,26 @@
background-image: url("../img/whiteboard.svg");
}
.icon-save {
background-image: url("../img/save.svg");
background-size: 16px;
}
/*button*/
#whiteboard-sharebtn {
position: absolute;
top:0 ;
right: 60px ;
width: 30px ;
height: 30Px ;
z-index: inherit;
}
#whiteboard-sharebtn:hover {
border: 1px solid silver ;
border-radius: 2px ;
}
#whiteboard-savebtn {
position: absolute;
top:0 ;
......@@ -22,7 +41,6 @@
width: 30px ;
height: 30Px ;
z-index: inherit;
background-color: lawngreen;
}
#whiteboard-savebtn:hover {
......@@ -45,6 +63,8 @@
}
/*Editor stuff*/
.literally {
background-color: white !important;
......@@ -63,8 +83,4 @@
.square-toolbar-button {
border: 0px solid silver ! important;
}
.square-toolbar-button svg circle{
fill: var(--color-primary) ;
}
\ No newline at end of file
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<path d="M493.254,77.255l-58.508-58.51C422.742,6.742,406.465,0,389.49,0H352v112c0,8.836-7.164,16-16,16H80
c-8.836,0-16-7.164-16-16V0H32C14.328,0,0,14.326,0,32v448c0,17.673,14.328,32,32,32h448c17.672,0,32-14.327,32-32V122.51
C512,105.535,505.258,89.257,493.254,77.255z M448,448H64V256h384V448z"/>
<rect x="224" width="64" height="96"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
......@@ -28,7 +28,7 @@ interface ICollaborationEngine {
public function createSession(string $fileId): int ;
public function addUser(User $user): int ;
public function removeUser(User $user): int ;
public function addStep(string $step): int ;
public function addStep(User $user, string $type,string $step): int ;
public function getStep(): array ;
}
\ No newline at end of file
......@@ -21,48 +21,124 @@
namespace OCA\whiteboard\Collaboration ;
//use OCP\User ;
use OCP\ICache ;
use OCP\ICacheFactory;
//class SimpleFileCollaborationEngine implements ICollaborationEngine {
class SimpleFileCollaborationEngine {
public function __construct(string $fileId) {
public function __construct(string $fileId, ICacheFactory $cacheFactory) {
$this->file = $fileId ;
$this->Fsession =Array() ;
$this->Fsession = json_decode(file_get_contents("/tmp/session-".$this->file),true) ;
$this->cache = $cacheFactory->createDistributed('WhiteboardSession::'.$this->file);
try {
$this->Fsession = json_decode(file_get_contents("/tmp/session-".$this->file),true) ;
} catch (Exception $e) {
$this->Fsession =Array() ;
}
}
public function createSession(string $file) {
$this->Fsession["session"] = $file ;
return $this-> updateStorage() ;
public function startSession(string $file) {
if (! (in_array($file,$this->Fsession["session"]))) {
$this->Fsession["session"] = $file ;
return $this-> updateStorage() ;
} ;
}
public function addUSer(string $user) {
$this->Fsession["user"][]=$user ;
return $this-> updateStorage() ;
public function addUser(string $user) {
if (! (in_array($user,$this->Fsession["user"]))) {
$this->Fsession["user"][]=$user ;
return $this-> updateStorage() ;
} ;
}
}
public function removeUSer(string $user) {
$index = array_search($user, $this->Fsession["user"]) ;
if ($index !== FALSE ) {
unset($this->Fsession["user"][$index]) ;
if ( $this->getUserNumber() == 0 ) {
return $this->sessionDestroy() ;
} else {
return $this-> updateStorage() ;
} ;
} ;
}
public function addStep($datas) {
public function addStep($user,$type,$datas) {
$step = array (
"user" => $user,
"type" => $type,
"data" => $datas,
"stepid" => "1"
"stepid" => time()
) ;
$this->Fsession["steps"][]=$step ;
//send last step to the cache to be pushed to client should be faster then anything else !
//$this->cache->set('WhiteboardSession::'.$this->file,json_encode($step)) ;
$this->setLastStep($step) ;
return $this-> updateStorage() ;
}
public function getStep() {
return $this->Fsession["steps"] ;
public function getSteps($fromStep,$handleCheckpoint = FALSE ) {
$result = Array() ;
foreach ($this->Fsession["steps"] as $step) {
// if save step then dont send step before this one
if ($step["type"] == "save" && $handleCheckpoint == TRUE ) {
$result = [] ;
} else {
if ($step["stepid"] > $fromstep) {
$result[] = $step ;
}
}
}
return $result ;
}
public function getUserList() {
return $this->Fsession["user"] ;
}
public function waitForNewSteps(){
$timeout = 20 ;
$elapsedTime = 0;
while (empty($step) && $elapsedTime < $timeout) {
sleep(1) ;
//$step = json_decode($this->cache->get('WhiteboardSession::'.$this->file)) ;
$step = $this->getLastStep() ;
$elapsedTime++ ;
} ;
//remove key from cache
//$this->cache.remove('WhiteboardSession::'.$this->file) ;
return $step ;
}
/* Private methods */
private function updateStorage() {
return file_put_contents("/tmp/session-".$this->file,json_encode($this->Fsession)) ;
return file_put_contents("/tmp/session-".$this->file,json_encode($this->Fsession), LOCK_EX) ;
}
private function getUserNumber() {
return count($this->Fsession["user"]) ;
}
private function sessionDestroy() {
return unlink("/tmp/session-".$this->file) ;
}
private function setLastStep($step){
file_put_contents("/tmp/session-queue-".$this->file,json_encode($step),FILE_APPEND | LOCK_EX) ;
}
private function getLastStep(){
$steps = file_get_contents("/tmp/session-queue".$this->file) ;
return json_decode($steps,true) ;
}
......
<?php
/**
* @author 2020 Matthieu Le Corre <matthieu.lecorre@univ-nantes.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
use OCP\EventDispatcher\Event;
namespace OCA\whiteboard\Collaboration ;
class stepEvent extends Event {
private $step ;
public function __construct(Array $step) {
parent::__construct() ;
$this->step = $step ;
}
public function getStep(): Array {
return $this->step ;
}
}
......@@ -26,25 +26,77 @@ use OCP\IRequest;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\User ;
use OCP\ICache ;
use OCP\ICacheFactory;
use OCA\whiteboard\Collaboration\SimpleFileCollaborationEngine ;
class CollaborationController extends Controller {
public function __construct($AppName, IRequest $request, Folder $userFolder ) {
/**
* @NoAdminRequired
*
**/
public function __construct($AppName, IRequest $request, Folder $userFolder ,ICacheFactory $cacheFactory ) {
parent::__construct($AppName, $request);
$this->id = $request->getParam("id") ;
$this->engine = new SimpleFileCollaborationEngine($this->id) ;
$this->engine = new SimpleFileCollaborationEngine($this->id,$cacheFactory) ;
}
public function createSession($id) {
return $this->engine->createSession($id) ;
/**
* @NoAdminRequired
*
**/
public function startSession($id) {
return $this->engine->startSession($id) ;
}
/**
* @NoAdminRequired
*
**/
public function addUser($id,$user) {
return $this->engine->addUser($user) ;
}
/**
* @NoAdminRequired
*
**/
public function removeUser($id,$user) {
return $this->engine->removeUser($user) ;
}
/**
* @NoAdminRequired
*
**/
public function getUserList($id) {
return $this->engine->getUserList() ;
}
/**
* @NoAdminRequired
*
**/
public function addStep($id,$user,$type,$step) {
return $this->engine->addStep($user,$type,$step) ;
}
/**
* @NoAdminRequired
*
**/
public function getSteps($id,$from,$handlecheckpoint) {
return $this->engine->getSteps($from,$handlecheckpoint) ;
}
/**
* @NoAdminRequired
*
**/
public function pushStep($id) {
return $this->engine->waitForNewSteps() ;
}
}
\ No newline at end of file
......@@ -18,37 +18,150 @@
*
*/
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' ;
/**
* @namespace collaborationEngine
*/
collaborationEngine = {
export default {
startSession: function(filename) {
var url = OC.generateUrl('apps/whiteboard/collaboration/createsession');
var id = window.FileList.findFile(filename).id
console.log("Create session for id #"+id) ;
ajx = $.ajax({
name: 'collaborationEngine',
start: function(app_name,filename,context) {
var self = this ;
this.app_name = app_name ;
this.filename = filename ;
this.context = context ;
this.SSE_URL = OC.generateUrl('apps/'+this.app_name+'/collaboration/event');
this.SSE_OPT = {withCredentials:true} ;
this.init().then(function(data) {
console.log("Collaboration started for "+ self.app_name) ;
self.addUser() ;
// because we may arrive in an allready running session
// get all steps from last last save
self.getSteps(0,true).then(function(steps){
steps.forEach(step => emit(self.app_name+"::externalAddStep",step)) ;
})
// start pulling for change
self.startCommunication() ;
});
},
stop: function() {
this.removeUser() ;
},
init: function() {
var url = OC.generateUrl('apps/'+this.app_name+'/collaboration/startsession');
this.id = window.FileList.findFile(this.filename).id
var ajx = $.ajax({
type: 'POST',
url: url,
data: {id: id }
data: {id: this.id }
}) ;
return ajx.promise() ;
},
addUser: function(filename) {
var url = OC.generateUrl('apps/whiteboard/collaboration/adduser');
var id = window.FileList.findFile(filename).id
addUser: function() {
var url = OC.generateUrl('apps/'+this.app_name+'/collaboration/adduser');
console.log("Adding user " + OC.currentUser ) ;
ajx = $.ajax({
var ajx = $.ajax({
type: 'POST',
url: url,
data: {id: this.id,
user: OC.currentUser
}
}) ;
return ajx.promise() ;
},
removeUser: function() {
var url = OC.generateUrl('apps/'+this.app_name+'/collaboration/removeuser');
console.log("Removing user " + OC.currentUser ) ;
var ajx = $.ajax({
type: 'POST',
url: url,
data: {id: id,
data: {id: this.id,
user: OC.currentUser
}
}) ;
return ajx.promise() ;
},
sendStep: function(payload) {
var url = OC.generateUrl('apps/'+this.app_name+'/collaboration/addstep');
var ajx = $.ajax({
type: 'POST',
contentType: "application/json",
dataType: "json",
url: url,
data: JSON.stringify({id: this.id,
user: OC.currentUser,
type: payload.type,
step: payload.step
})
}) ;
return ajx.promise() ;
},
getSteps: function(from,handlecheckpoint) {
var url = OC.generateUrl('apps/'+this.app_name+'/collaboration/getsteps');
console.log("get initial steps") ;
var ajx = $.ajax({
type: 'GET',
url: url,
data: {
id: this.id,
fromStep: from,
handlecheckpoint: handlecheckpoint
}
}) ;
return ajx.promise() ;
},
startCommunication: function() {
this.communicationStarted = true ;
while (this.communicationStarted = true) {
this.longPull().done( function(data){
console.log(data) ;
})
}
},
longPull: function() {
var url = OC.generateUrl('apps/'+this.app_name+'/collaboration/pushstep');
console.log("Starting polling ...") ;
var ajx = $.ajax({
type: 'GET',
url: url,
data: {
id: this.id,
}
}) ;
return ajx.promise() ;
},
stopCommunication: function(){
this.communicationStarted = false ;
// this.source.close() ;
}
}
......@@ -17,6 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
export default {
name: "editor",
......@@ -31,14 +32,14 @@ export default {
this.init().then(function(){
self.loadContent() ;
self.setupCallback() ;
self.setupCallbacks() ;
}) ;
},
init: function () {
return import(/* webpackChunkName: "literallycanvas" */ "literallycanvas").then(LC => {
this.LC = LC ;
this.whiteboard = LC.init(
document.getElementById(this.app_name+'-editor'),
{
......@@ -67,6 +68,9 @@ export default {
//save whiteboard
saveContent: function() {
var self = this ;
var url = OC.generateUrl('apps/'+this.app_name+'/file/save');
var postObject = {
......@@ -79,50 +83,53 @@ export default {
url: url,
data: postObject
}).done(function(content){
console.log("Save whiteboard ...") ;
console.log("Whiteboard Saved") ;
var payload = {
'type' : 'save',
'step' : 'NA'
} ;
emit(self.app_name+"::editorAddStep",payload) ;
})
},
//setup callback
setupCallback: function (){
setupCallbacks: function (){
var self = this ;
// set save callback
this.whiteboard.on('drawingChange', function() {
self.saveContent() ;
//this.whiteboard.on('drawingChange', function(data) {
//emit(self.app_name+"::editorContentChange",data) ,
//self.saveContent() ;
//});
this.whiteboard.on('shapeSave', function(data) {
var payload = {
'type' : 'shapeSave',
'step' : self.LC.shapeToJSON(data.shape)
} ;
emit(self.app_name+"::editorAddStep",payload) ;
console.log("ED: Creating NewShape ") ;
return data ;
});
},
//destroy editor
close: function() {
stop: function() {