Anuj-Panthri commited on
Commit
0409bf8
·
1 Parent(s): 58fc009

pingInterval now working on android

Browse files
client/lib/home_screen.dart CHANGED
@@ -1,178 +1,201 @@
1
  import 'package:flutter/material.dart';
2
  import 'package:flutter/services.dart'; // for going in fullscreen mode
3
- import 'package:flutter/scheduler.dart'; // to add scheduler start sending touch on touch start
4
 
5
  import 'package:shared_preferences/shared_preferences.dart';
6
 
7
  // import 'package:web_socket_channel/status.dart' as status;
8
 
9
- import 'ws_server_connection_handler.dart';
 
10
 
11
  import 'package:flutter_colorpicker/flutter_colorpicker.dart';
12
 
13
- import 'package:flutter/foundation.dart' show kIsWeb; // to check of platform is web
 
14
  import 'vibration/vibration_exporter.dart';
15
 
16
  import "dart:convert";
17
  import "dart:async";
18
 
19
- void enterFullScreen(){
20
- SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive,overlays: []);
21
  }
22
- void exitFullScreen(){
23
- SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,overlays:SystemUiOverlay.values);
 
 
24
  }
25
 
26
- class HomeScreen extends StatefulWidget{
27
  const HomeScreen({super.key});
28
-
29
 
30
  @override
31
- State<HomeScreen> createState(){
32
  return HomeScreenState();
33
  }
34
  }
35
 
36
- class HomeScreenState extends State<HomeScreen>{
37
- late CustomWSChannel channel;
38
- late double width,height;
39
  late SharedPreferences prefs;
40
- double containerHeight = 0,containerWidth=0;
41
 
42
  late Timer sendTouchScheduler;
 
43
 
44
- double touchsize=100;
45
- double touchX = 0,touchY=0;
46
- bool isTouching=false;
47
 
48
- bool hasCollided=false; // hascollided can be useful to show any type special effect later on
 
49
 
50
  late List<dynamic> userList;
51
- late List<String> roomidList,roomnameList;
52
- String currRoomId="",currRoomName="";
53
- String username="";
54
- String userid="";
55
- String profileColor="ff000000";
56
- Map<String,Map> othersTouchPoints={};
57
- final double outsidepadding=15;
58
- final int touchRefreshRate = 1000~/30; // 30 fps
59
  // final int touchRefreshRate = 1000~/60; // 60 fps
 
60
 
61
- GlobalKey headerKey = GlobalKey(); // to calculate the header's height
62
 
63
  @override
64
- void initState(){
65
  super.initState();
66
  enterFullScreen();
67
- roomidList=[];
68
- roomnameList=[];
69
- userList=[];
70
 
71
  // roomidList=["1","2"];
72
  // roomnameList=["room 1","room 2"];
73
 
74
- Map data={
75
  "user": {
76
- "username": "enemy",
77
- "id": "string",
78
  },
79
  "position": {
80
- "x": 0.0+touchsize/2, // top left corner
81
- "y": 0.0+touchsize/2, // top left corner
82
  },
83
  "color": "ff2244bb", // Hex value.
84
  "intensity": 1, // Vibration intensity.
85
- };
86
 
87
- othersTouchPoints[data['user']['id']]=data;
88
-
89
 
90
- WidgetsBinding.instance.addPostFrameCallback((_){ // this function is called after the build method is done executing
 
91
  getPreferences();
92
  connectWebsocket();
93
  });
94
-
95
- // just for debugging
96
  // WidgetsBinding.instance.addPostFrameCallback((_){
97
  // showSnackBar(getHeaderHeight().toString());
98
  // });
99
-
100
  // just for debugging vibrations
101
  // Vibration.vibrate(pattern:[100]);
102
  // Vibration.vibrate(pattern:[200,100]);
103
-
104
  }
105
 
106
  @override
107
- void dispose(){
108
  exitFullScreen();
109
- channel.sink.close();
110
  super.dispose();
111
  }
112
 
113
- void showSnackBar(String text){
114
  ScaffoldMessenger.of(context).hideCurrentSnackBar();
115
- ScaffoldMessenger.of(context).showSnackBar(
116
- SnackBar(
117
- content:Text(text)
118
- )
119
- );
120
  }
121
 
122
- double? getHeaderHeight(){
123
- var size=headerKey.currentContext?.size;
124
  // debugPrint(size?.height);
125
  return size?.height;
126
  }
127
 
128
- void getPreferences() async{
129
  prefs = await SharedPreferences.getInstance();
130
 
131
  // initialize empty room list if no preferences found
132
- if(!prefs.containsKey("roomid_list")){
133
- prefs.setStringList("roomid_list",[]);
134
- prefs.setStringList("roomname_list",[]);
135
  }
136
  // intialize from preferences
137
  setState(() {
138
- roomidList=prefs.getStringList("roomid_list")!;
139
- roomnameList=prefs.getStringList("roomname_list")!;
140
- username=prefs.getString("username")!; // to get the username from device's local storage
141
- profileColor=prefs.getString("profile_color")!; // to get the profilecolor from device's local storage
 
 
142
  });
143
  }
144
 
 
 
 
 
145
 
146
- void connectWebsocket() async{
 
147
 
148
- channel=CustomWSChannel(
149
-
150
- const String.fromEnvironment(
151
- "WS_SERVER_URL",
152
- defaultValue: "ws://localhost:3000"
153
- ),
154
-
155
- onMessage: responseHandler,
156
- onErrorShowMessage:(msg,e){
157
- showSnackBar(msg);
158
- },
159
- onConnect:(){
160
- showSnackBar("connected");
161
- setUsername();
162
-
163
- // on connected test connection
164
- Object data = {
165
- "route":"test_connection"
166
- };
167
- channel.sink.add(json.encode(data));
168
- }
169
  );
170
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  }
172
 
173
- void setUsername() async{
174
  prefs = await SharedPreferences.getInstance();
175
- if(!await channel.isConnected()){
176
  return;
177
  }
178
  // set username
@@ -180,129 +203,127 @@ class HomeScreenState extends State<HomeScreen>{
180
  "route": "set_username",
181
  "username": prefs.getString("username"),
182
  };
183
-
184
- channel.sink.add(json.encode(data));
185
 
 
186
  }
187
-
188
  void responseHandler(message) {
189
  // message.get
190
  Map map;
191
  final String type;
192
-
193
- try{
194
- map = jsonDecode(message); // try to decode the message into json format
195
- }
196
- catch(e){
197
  debugPrint("can't decode to json:\t$message");
198
  return;
199
  }
200
-
201
  // if any error found do nothing
202
- if(map.containsKey("error")){
203
  debugPrint(map.toString());
204
  return;
205
  }
206
-
207
- type = map["message"]; // get the type of the message
208
 
209
- if(type=="test_connection_response"){
 
 
 
210
  // testing connection
211
  debugPrint("test_connection successful");
212
-
213
- }
214
- else if(type=="set_username_response"){
215
  debugPrint(map.toString());
216
  setState(() {
217
- userid=map['user']['id']; // set our userid
218
  });
219
- }
220
- else if(type=="join_room_response"){ // on error
221
  // debugPrint(map.toString());
222
  showSnackBar(map['status']);
223
- }
224
- else if(type=="room_update"){
225
  debugPrint(map.toString());
226
- if(currRoomId!=map['roomId']){ // only update on room change
227
- setState((){
228
- currRoomId=map['roomId'];
229
- currRoomName=map['roomId'];
 
230
  });
231
  }
232
-
233
  // update users list
234
  setState(() {
235
- userList=map['users'];
236
  });
237
-
238
- }
239
- else if(type=="send_vibration_response"){ // handles when we send_touch without being in a room
240
  debugPrint(map.toString());
241
- showSnackBar(map['status']); // shows error that we are not part of the room
 
242
  setState(() {
243
- currRoomId=""; // clears the current roomid
244
- currRoomName="";
 
245
  });
246
- }
247
- else if(type=="receive_touch"){
248
- if(map['user']['id']==userid){ // this is touch which we originated so we ignore it
249
  // debugPrint("self touch received. Ignore this");
250
  return;
251
  }
252
-
253
  // touch received to update othersTouchPoints
254
  // debugPrint(map);
255
- if(map['type']=='enabled'){ // add touch point
256
-
257
- // static height and width
258
- double staticHeight=getHeaderHeight()!+outsidepadding*2; // this is the extra static height
259
- double staticWidth=outsidepadding*2; // this is the extra static width
260
-
 
 
 
261
  // rescale point according to the screen
262
- map['position']['x']*=width-staticWidth;
263
- map['position']['y']*=height-staticHeight;
264
-
265
  // debugPrint(map['position']['x'].toString());
266
  // debugPrint(map['position']['y'].toString());
267
  setState(() {
268
- othersTouchPoints[map['id']]=map; // updating other's touch points
269
  });
270
- }
271
- else{ // map['type']=='disabled'
272
  setState(() {
273
- othersTouchPoints.remove(map['id']); // delete the touch point
274
  });
275
  }
276
-
277
  }
278
-
279
  }
280
 
281
-
282
-
283
-
284
- void toggleContainer(){
285
  // to toggle the visibility of the center popup container
286
- if(containerHeight==0){
287
  setState(() {
288
- containerHeight=200;
289
- containerWidth=220;
290
  });
291
- }
292
- else{
293
  setState(() {
294
- containerHeight=0;
295
- containerWidth=0;
296
  });
297
  }
298
  }
299
 
300
- void createRoom({String roomId=""}) async{
301
  // it is used to create a room in server
302
  // or to connect to an existing room
303
  Map data;
 
304
 
305
- if(!await channel.isConnected()){
306
  return;
307
  }
308
 
@@ -312,217 +333,213 @@ class HomeScreenState extends State<HomeScreen>{
312
  "username": username,
313
  };
314
 
315
- // to join existing room
316
- if(roomId.isNotEmpty){
317
- data["roomId"]=roomId;
318
  }
319
- channel.sink.add(json.encode(data));
320
 
321
  // we will get in the room with the server data
322
  // from the room_update response
323
  }
324
 
325
- void joinRoomForm(){
326
  // popup for entering the room id to join an existing room
327
-
328
  showDialog(
329
- context: context,
330
- builder:(context) {
331
-
332
  return AlertDialog(
333
- content:SingleChildScrollView(
334
- child:Column(
335
- children: [
336
-
337
- TextField(
338
- decoration:const InputDecoration(
339
- hintText: "Enter Room ID",
340
- ),
341
-
342
- onSubmitted: (value){
343
- Navigator.pop(context); // close the dialog
344
- createRoom(roomId:value); // join room
345
- },
346
- ),
347
- ])
348
- ),
349
  );
350
  },
351
  );
352
  }
353
-
354
- Widget header(){
355
 
356
- Color buttoncolor=const Color.fromARGB(255, 70, 70, 70);
357
-
 
358
  return Padding(
359
- key:headerKey,
360
- padding:const EdgeInsets.symmetric(vertical:20),
361
- child:Row(
362
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
363
- children: [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
- // sub row to group create/join room button and roomuserslist
366
- Row(
367
- children: [
368
-
369
- // create/join room popup
370
- CircleAvatar(
371
- backgroundColor: buttoncolor,
372
- child: PopupMenuButton(
373
- onSelected: (value){
374
- switch(value){
375
- case 'createRoom':
376
- return createRoom();
377
- case 'joinRoom':
378
- return joinRoomForm();
379
- default:
380
- throw UnimplementedError();
381
- }
382
- },
383
- icon:const Icon(Icons.add,color:Colors.white), // + icon
384
- tooltip: "Create/Join Room",
385
- itemBuilder: (context)=>const[
386
-
387
- PopupMenuItem(
388
- value:"createRoom",
389
- child: Text("Create Room"),
390
- ),
391
-
392
- PopupMenuItem(
393
- value:"joinRoom",
394
- child: Text("Join Room"),
395
- ),
396
-
397
- ],
398
  ),
399
- ),
400
-
401
- // users list
402
- Container(
403
- margin:const EdgeInsets.only(left:10),
404
- child: Row(children: [
405
- ...userList.map((user) {
406
-
407
- return Container( // each person's name who is in a room
408
- height: 50,
409
- width:50,
410
- margin:const EdgeInsets.only(left:4),
411
- alignment:Alignment.center,
412
- decoration: BoxDecoration(
413
- color: Colors.white, // white background // we can also use their profilecolor here
414
- borderRadius: BorderRadius.circular(50),
415
- ),
416
- child: Text(user["username"], // their username
417
- style:const TextStyle(
418
- color: Colors.black,
419
- ),
420
- ),
421
- );
422
- },).toList(),
423
-
424
- ]),
425
- )
426
-
427
- ]),
428
-
429
- // centered Lobby thing
430
- GestureDetector(
431
- onTap:toggleContainer,
432
- onLongPress:() async{ // long press to copy roomid to clipboard
433
- // copy to clipboard
434
- await Clipboard.setData(ClipboardData(text:currRoomId));
435
- showSnackBar("copied!");
436
- },
437
- child:Row( // roomid+dropdown_icon
438
- children:[
439
-
440
- // roomid
441
- Text(
442
- currRoomName.isEmpty?"Lobby":currRoomName,
443
- style:const TextStyle(
444
- fontSize: 20,
445
- fontWeight: FontWeight.w500,
446
- overflow:TextOverflow.ellipsis,
447
  ),
448
  ),
449
-
450
- // icon based on if the container is visible or not
451
- if(containerHeight==0)...[
452
- const Icon(Icons.arrow_drop_down),
453
- ]
454
- else...[
455
- const Icon(Icons.arrow_drop_up),
456
- ]
457
-
458
- ]),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  ),
 
460
 
461
- // settings button
462
- CircleAvatar(
463
- backgroundColor: buttoncolor,
464
- child:IconButton(
465
-
466
- tooltip: "setings", // text which is showed when long pressing or hovering
467
- onPressed: (){}, // does nothing till now
468
- icon:const Icon(
469
- Icons.settings,
470
- color:Colors.white,
471
- ),
472
- ),
473
- ),
474
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  ]),
476
-
477
  );
478
  }
479
 
480
-
481
- void startSendTouch(){
482
  // to start the send_touch scheduler
483
 
484
- sendTouchScheduler=Timer.periodic(
485
- Duration(milliseconds: touchRefreshRate), (timer) async {
486
- // send touch to server
487
- Map data;
488
- double staticHeight=getHeaderHeight()!+outsidepadding*2; // this is the extra static height which change with resize
489
- double staticWidth=outsidepadding*2; // this is the extra static height which change with resize
490
-
491
- if(!await channel.isConnected()){
492
- return;
493
- }
494
- data={
495
- "route": "send_touch",
496
- "id": 1,
497
- "type": "enabled", // Whether the vibration is active or not.
498
- "position": {
499
- "x": touchX/(width-staticWidth), // send ratio x e.g. : x:700, width:800, then send 0.875
500
- "y": touchY/(height-staticHeight), // send ratio y e.g. : y:400, height:800 then send 0.5
501
- },
502
- "color": profileColor, // Hex value. Default: random
503
- // "intensity"?: 1 // Vibration intensity. Default: 1
504
- };
505
- // debugPrint(data['position']['x']);
506
- // debugPrint(data['position']['y']);
507
- channel.sink.add(jsonEncode(data));
508
  }
509
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  }
511
 
512
- void stopSendTouch() async{
513
  // to stop the send_touch scheduler
514
 
515
  sendTouchScheduler.cancel();
516
 
517
  // send one last touch to tell touch is ended
518
  Map data;
519
- if(!await channel.isConnected()){
520
  return;
521
  }
522
- data={
523
  "route": "send_touch",
524
  "id": 1,
525
- "type": "disabled", // Whether the vibration is active or not.
526
  "position": {
527
  "x": 0,
528
  "y": 0,
@@ -531,17 +548,15 @@ class HomeScreenState extends State<HomeScreen>{
531
  // "intensity"?: 1 // Vibration intensity. Default: 1
532
  };
533
 
534
- channel.sink.add(jsonEncode(data));
535
-
536
  }
537
 
538
- Widget buildTouchPoint({
539
- required bool visible,
540
- required double x,
541
- required double y,
542
- required double size,
543
- required Color color
544
- }){
545
  /*
546
  bool visible: if the touchpoint is visible
547
  double x: touchpoint's center x position
@@ -550,28 +565,28 @@ class HomeScreenState extends State<HomeScreen>{
550
  double size: touchpoint's size
551
  Color color: touchpoint's color
552
  */
553
-
554
- color=color.withOpacity(0.5); // make the color a little translucent
555
 
556
  return Visibility(
557
- visible:visible,
558
  child: AnimatedPositioned(
559
  duration: Duration(milliseconds: touchRefreshRate),
560
- left:x-size/2,
561
- top:y-size/2,
562
  child: Container(
563
- width:size,
564
- height:size,
565
  decoration: BoxDecoration(
566
- color:color,
567
- borderRadius: BorderRadius.circular(50),
568
- boxShadow: [
569
- BoxShadow(
570
- color:color,
571
- blurRadius: 10,
572
- spreadRadius: 5,
573
- ),
574
- ]),
575
  ),
576
  ),
577
  );
@@ -580,38 +595,34 @@ class HomeScreenState extends State<HomeScreen>{
580
  void detectTouchCollision({
581
  required double x,
582
  required double y,
583
- }){
584
  // detect our touch point collision with any other's TouchPoints
585
 
586
- for(Map map in othersTouchPoints.values){
587
-
588
- double dx = (map["position"]["x"]-x).abs();
589
- double dy = (map["position"]["y"]-y).abs();
590
  // debugPrint("x:"+x.toString()+"\ty:"+y.toString());
591
  // debugPrint("dx:"+dx.toString()+"\tdy:"+dy.toString());
592
- if (dx<=touchsize && dy<=touchsize){
593
-
594
- if(hasCollided==false){
595
  // Collision Started
596
  debugPrint("Touch Collision started");
597
 
598
  startHapticFeedback();
599
-
600
  setState(() {
601
- hasCollided=true;
602
  });
603
  }
604
- }
605
- else{
606
-
607
- if(hasCollided==true){
608
  // collision ended
609
  debugPrint("Touch Collision ended");
610
-
611
  endHapticFeedback();
612
 
613
  setState(() {
614
- hasCollided=false;
615
  });
616
  }
617
  // debugPrint("No Touch Collision.");
@@ -619,71 +630,65 @@ class HomeScreenState extends State<HomeScreen>{
619
  }
620
  }
621
 
622
- void startHapticFeedback() async{
623
-
624
- if(kIsWeb){
625
  // different handler for web
626
  // showSnackBar("web");
627
  // off,on,off
628
- Vibration.vibrate(pattern:[20,100], repeat: 0); // short fast
629
  // Vibration.vibrate(pattern:[100, 200, 400],repeat: 0); // slow
630
- }
631
- else{
632
  // showSnackBar("mobile");
633
- if (await Vibration.hasVibrator()==true) {
634
  // off,on,off
635
- Vibration.vibrate(pattern:[20,100], repeat: 0); // short fast
636
- // Vibration.vibrate(pattern:[100, 200, 400],repeat: 0); // slow
637
  }
638
  }
639
-
640
  }
641
 
642
- void endHapticFeedback() async{
643
- if(kIsWeb){
644
  // different handler for web
645
  Vibration.cancel();
646
- }
647
- else{
648
- if (await Vibration.hasVibrator()==true) {
649
  Vibration.cancel();
650
  }
651
  }
652
  }
653
 
654
- void detectTouchStart(DragStartDetails details){
655
  // to detect touch start on screen to start the send_touch scheduler
656
-
657
  // debugPrint(details.localPosition);
658
 
659
  // update our touch point on our screen
660
  setState(() {
661
  touchX = details.localPosition.dx;
662
  touchY = details.localPosition.dy;
663
- isTouching=true; // show it
664
  });
665
 
666
-
667
  // detect Collision for haptic feedback
668
  detectTouchCollision(
669
- x:details.localPosition.dx,
670
- y:details.localPosition.dy,
671
  );
672
-
673
-
674
  // start send Touch Scheduler
675
- if(currRoomId.isNotEmpty){ // only send touch if we are part of a room
676
- SchedulerBinding.instance.addPostFrameCallback((_) {
 
677
  startSendTouch();
678
  });
679
- }
680
- else{
681
  // we are not joined in a room
682
- // so can't send_touch
683
  }
684
-
685
  }
686
- void detectTouchUpdate(DragUpdateDetails details){
 
687
  // to update touchpoint
688
 
689
  // debugPrint(details.localPosition);
@@ -696,163 +701,143 @@ class HomeScreenState extends State<HomeScreen>{
696
 
697
  // detect Collision
698
  detectTouchCollision(
699
- x:details.localPosition.dx,
700
- y:details.localPosition.dy,
701
  );
702
-
703
  }
704
 
705
-
706
-
707
- void detectTouchEnd(DragEndDetails details){
708
  // triggered on touch end
709
 
710
  // update our touch point on our screen
711
  setState(() {
712
  touchX = 0;
713
  touchY = 0;
714
- isTouching=false; // hide it
715
  });
716
-
717
  // detect Collision end
718
  detectTouchCollision(
719
- x:-1000,
720
- y:-1000,
721
  );
722
 
723
-
724
  // stop send Touch Scheduler and send stop touch message
725
- if(currRoomId.isNotEmpty){
726
- SchedulerBinding.instance.addPostFrameCallback((_) {
727
  stopSendTouch();
728
  });
729
- }
730
- else{
731
  // can't send_touch end to server cuz we are not in a room
732
  }
733
  }
734
 
735
- Widget mainSection(){
736
  // this is the main touch area
737
 
738
-
739
- return Expanded( // expanded for it to take up all the available space
740
 
741
  child: GestureDetector(
742
- onPanStart:(details){
743
- detectTouchStart(details); // on touch start
744
  },
745
- onPanUpdate:(details){
746
- detectTouchUpdate(details); // on touch update
747
  },
748
- onPanEnd:(details){
749
- detectTouchEnd(details); // on touch end
750
  },
751
- child:Container(
752
  decoration: BoxDecoration(
753
- color:Colors.grey.shade900,
754
  borderRadius: BorderRadius.circular(50),
755
  ),
756
  clipBehavior: Clip.hardEdge,
757
- child:Stack(
758
- clipBehavior: Clip.hardEdge,
759
- children: [
760
-
761
- // render received touch points
762
- ...othersTouchPoints.values.map<Widget>((map){
763
- return buildTouchPoint(
764
- visible:true,
765
- x:map['position']['x'],
766
- y:map['position']['y'],
767
- size:touchsize,
768
- color:colorFromHex(map['color'])!,
769
  );
770
- }).toList(),
771
-
772
-
773
- // our touch point
774
- buildTouchPoint(
775
- visible:isTouching,
776
- x:touchX,
777
- y:touchY,
778
- size:touchsize,
779
- color:colorFromHex(profileColor)!,
780
- ),
781
-
782
  ]),
783
  ),
784
  ),
785
  );
786
-
787
  }
788
 
789
-
790
  @override
791
- Widget build(BuildContext context){
792
- width = MediaQuery.of(context).size.width; // to get screen width
793
- height = MediaQuery.of(context).size.height; // to get screen height
794
 
795
  return Scaffold(
796
-
797
- body:Stack(
798
- alignment: Alignment.center,
799
- children: [
 
800
 
801
- //outermost background
802
- Container(
803
- color:const Color.fromARGB(255, 14, 14, 14),
 
 
 
 
 
 
 
804
  ),
 
805
 
806
- // header and main touch area
807
- Padding(
808
- padding: EdgeInsets.all(outsidepadding),
809
- child: Column(
810
- crossAxisAlignment: CrossAxisAlignment.stretch, // mainSection expands in height because of this
811
- children: [
812
- header(),
813
- mainSection(),
814
- ],
 
 
 
815
  ),
816
- ),
817
-
818
- //floating Room menu
819
- Positioned(
820
- top:100,
821
- child: AnimatedContainer(
822
- duration:const Duration(milliseconds: 100),
823
- height: containerHeight,
824
- width: containerWidth,
825
- clipBehavior: Clip.hardEdge,
826
- curve: Curves.easeIn,
827
- decoration:BoxDecoration(
828
- color:const Color.fromARGB(255, 67, 67, 67),
829
- borderRadius: BorderRadius.circular(20),
830
- ),
831
- child:ListView(
832
- children: [
833
-
834
- Container(
835
- margin:const EdgeInsets.all(20)+const EdgeInsets.symmetric(horizontal: 40),
836
- child: TextButton( // exit room button
837
- onPressed: (){},
838
- style:TextButton.styleFrom(
839
- backgroundColor:const Color.fromARGB(255, 239, 83, 80),
840
- foregroundColor:const Color.fromARGB(255, 255, 255, 255),
841
- shape:RoundedRectangleBorder(
842
- borderRadius: BorderRadius.circular(20)
843
- ),
844
- ),
845
- child:const Text("Exit Room"),
846
- ),
847
  ),
848
-
849
- ]),
850
-
851
- ),
852
  ),
853
-
854
  ]),
855
-
856
  );
857
  }
858
- }
 
1
  import 'package:flutter/material.dart';
2
  import 'package:flutter/services.dart'; // for going in fullscreen mode
3
+ import 'package:flutter/scheduler.dart'; // to add scheduler start sending touch on touch start
4
 
5
  import 'package:shared_preferences/shared_preferences.dart';
6
 
7
  // import 'package:web_socket_channel/status.dart' as status;
8
 
9
+ // import 'ws_server_connection_handler.dart';
10
+ import 'package:web_socket_client/web_socket_client.dart';
11
 
12
  import 'package:flutter_colorpicker/flutter_colorpicker.dart';
13
 
14
+ import 'package:flutter/foundation.dart'
15
+ show kIsWeb; // to check of platform is web
16
  import 'vibration/vibration_exporter.dart';
17
 
18
  import "dart:convert";
19
  import "dart:async";
20
 
21
+ void enterFullScreen() {
22
+ SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive, overlays: []);
23
  }
24
+
25
+ void exitFullScreen() {
26
+ SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
27
+ overlays: SystemUiOverlay.values);
28
  }
29
 
30
+ class HomeScreen extends StatefulWidget {
31
  const HomeScreen({super.key});
 
32
 
33
  @override
34
+ State<HomeScreen> createState() {
35
  return HomeScreenState();
36
  }
37
  }
38
 
39
+ class HomeScreenState extends State<HomeScreen> {
40
+ late WebSocket socket;
41
+ late double width, height;
42
  late SharedPreferences prefs;
43
+ double containerHeight = 0, containerWidth = 0;
44
 
45
  late Timer sendTouchScheduler;
46
+ Timer endSendTouchTimer = Timer(Duration.zero, () {});
47
 
48
+ double touchsize = 100;
49
+ double touchX = 0, touchY = 0;
50
+ bool isTouching = false;
51
 
52
+ bool hasCollided =
53
+ false; // hascollided can be useful to show any type special effect later on
54
 
55
  late List<dynamic> userList;
56
+ late List<String> roomidList, roomnameList;
57
+ String currRoomId = "", currRoomName = "";
58
+ String username = "";
59
+ String userid = "";
60
+ String profileColor = "ff000000";
61
+ Map<String, Map> othersTouchPoints = {};
62
+ final double outsidepadding = 15;
63
+ final int touchRefreshRate = 1000 ~/ 30; // 30 fps
64
  // final int touchRefreshRate = 1000~/60; // 60 fps
65
+ // bool isConnected = false;
66
 
67
+ GlobalKey headerKey = GlobalKey(); // to calculate the header's height
68
 
69
  @override
70
+ void initState() {
71
  super.initState();
72
  enterFullScreen();
73
+ roomidList = [];
74
+ roomnameList = [];
75
+ userList = [];
76
 
77
  // roomidList=["1","2"];
78
  // roomnameList=["room 1","room 2"];
79
 
80
+ Map data = {
81
  "user": {
82
+ "username": "enemy",
83
+ "id": "string",
84
  },
85
  "position": {
86
+ "x": 0.0 + touchsize / 2, // top left corner
87
+ "y": 0.0 + touchsize / 2, // top left corner
88
  },
89
  "color": "ff2244bb", // Hex value.
90
  "intensity": 1, // Vibration intensity.
91
+ };
92
 
93
+ othersTouchPoints[data['user']['id']] = data;
 
94
 
95
+ WidgetsBinding.instance.addPostFrameCallback((_) {
96
+ // this function is called after the build method is done executing
97
  getPreferences();
98
  connectWebsocket();
99
  });
100
+
101
+ // just for debugging
102
  // WidgetsBinding.instance.addPostFrameCallback((_){
103
  // showSnackBar(getHeaderHeight().toString());
104
  // });
105
+
106
  // just for debugging vibrations
107
  // Vibration.vibrate(pattern:[100]);
108
  // Vibration.vibrate(pattern:[200,100]);
 
109
  }
110
 
111
  @override
112
+ void dispose() {
113
  exitFullScreen();
114
+ socket.close();
115
  super.dispose();
116
  }
117
 
118
+ void showSnackBar(String text) {
119
  ScaffoldMessenger.of(context).hideCurrentSnackBar();
120
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
 
 
 
 
121
  }
122
 
123
+ double? getHeaderHeight() {
124
+ var size = headerKey.currentContext?.size;
125
  // debugPrint(size?.height);
126
  return size?.height;
127
  }
128
 
129
+ void getPreferences() async {
130
  prefs = await SharedPreferences.getInstance();
131
 
132
  // initialize empty room list if no preferences found
133
+ if (!prefs.containsKey("roomid_list")) {
134
+ prefs.setStringList("roomid_list", []);
135
+ prefs.setStringList("roomname_list", []);
136
  }
137
  // intialize from preferences
138
  setState(() {
139
+ roomidList = prefs.getStringList("roomid_list")!;
140
+ roomnameList = prefs.getStringList("roomname_list")!;
141
+ username = prefs.getString(
142
+ "username")!; // to get the username from device's local storage
143
+ profileColor = prefs.getString(
144
+ "profile_color")!; // to get the profilecolor from device's local storage
145
  });
146
  }
147
 
148
+ void connectWebsocket() async {
149
+ const timeout = Duration(seconds: 10);
150
+ const backoff = ConstantBackoff(Duration(seconds: 1));
151
+ const pingInterval = Duration(seconds: 3);
152
 
153
+ final socketUri = Uri.parse(const String.fromEnvironment("WS_SERVER_URL",
154
+ defaultValue: "ws://localhost:3000"));
155
 
156
+ socket = WebSocket(
157
+ socketUri,
158
+ timeout: timeout,
159
+ backoff: backoff,
160
+ pingInterval: pingInterval,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  );
162
+
163
+ socket.connection.listen((state) {
164
+ debugPrint("state:\t $state");
165
+ if (state is Connected || state is Reconnected) {
166
+ // isConnected = true;
167
+ onConnected();
168
+ }
169
+
170
+ // isConnected = false;
171
+
172
+ if (state is Connecting || state is Reconnecting) {
173
+ showSnackBar("Connecting");
174
+ }
175
+
176
+ if (state is Disconnected) {
177
+ showSnackBar("Disconnected");
178
+ }
179
+ });
180
+
181
+ socket.messages.listen(responseHandler);
182
+ }
183
+
184
+ bool isConnected(){
185
+ return socket.connection.state is Connected || socket.connection.state is Reconnected;
186
+ }
187
+ void onConnected() {
188
+ showSnackBar("connected");
189
+ setUsername();
190
+
191
+ // on connected test connection
192
+ Object data = {"route": "test_connection"};
193
+ socket.send(json.encode(data));
194
  }
195
 
196
+ void setUsername() async {
197
  prefs = await SharedPreferences.getInstance();
198
+ if (!isConnected()) {
199
  return;
200
  }
201
  // set username
 
203
  "route": "set_username",
204
  "username": prefs.getString("username"),
205
  };
 
 
206
 
207
+ socket.send(json.encode(data));
208
  }
209
+
210
  void responseHandler(message) {
211
  // message.get
212
  Map map;
213
  final String type;
214
+
215
+ try {
216
+ map = jsonDecode(message); // try to decode the message into json format
217
+ } catch (e) {
 
218
  debugPrint("can't decode to json:\t$message");
219
  return;
220
  }
221
+
222
  // if any error found do nothing
223
+ if (map.containsKey("error")) {
224
  debugPrint(map.toString());
225
  return;
226
  }
 
 
227
 
228
+ type = map["message"]; // get the type of the message
229
+
230
+
231
+ if (type == "test_connection_response") {
232
  // testing connection
233
  debugPrint("test_connection successful");
234
+ } else if (type == "pong") {
235
+ debugPrint("pong"); // handling pongs
236
+ } else if (type == "set_username_response") {
237
  debugPrint(map.toString());
238
  setState(() {
239
+ userid = map['user']['id']; // set our userid
240
  });
241
+ } else if (type == "join_room_response") {
242
+ // on error
243
  // debugPrint(map.toString());
244
  showSnackBar(map['status']);
245
+ } else if (type == "room_update") {
 
246
  debugPrint(map.toString());
247
+ if (currRoomId != map['roomId']) {
248
+ // only update on room change
249
+ setState(() {
250
+ currRoomId = map['roomId'];
251
+ currRoomName = map['roomId'];
252
  });
253
  }
254
+
255
  // update users list
256
  setState(() {
257
+ userList = map['users'];
258
  });
259
+ } else if (type == "send_vibration_response") {
260
+ // handles when we send_touch without being in a room
 
261
  debugPrint(map.toString());
262
+ showSnackBar(
263
+ map['status']); // shows error that we are not part of the room
264
  setState(() {
265
+ currRoomId = ""; // clears the current roomid
266
+ currRoomName = "";
267
+ userList = [];
268
  });
269
+ } else if (type == "receive_touch") {
270
+ if (map['user']['id'] == userid) {
271
+ // this is touch which we originated so we ignore it
272
  // debugPrint("self touch received. Ignore this");
273
  return;
274
  }
275
+
276
  // touch received to update othersTouchPoints
277
  // debugPrint(map);
278
+ if (map['type'] == 'enabled') {
279
+ // add touch point
280
+
281
+ // static height and width
282
+ double staticHeight = getHeaderHeight()! +
283
+ outsidepadding * 2; // this is the extra static height
284
+ double staticWidth =
285
+ outsidepadding * 2; // this is the extra static width
286
+
287
  // rescale point according to the screen
288
+ map['position']['x'] *= width - staticWidth;
289
+ map['position']['y'] *= height - staticHeight;
290
+
291
  // debugPrint(map['position']['x'].toString());
292
  // debugPrint(map['position']['y'].toString());
293
  setState(() {
294
+ othersTouchPoints[map['id']] = map; // updating other's touch points
295
  });
296
+ } else {
297
+ // map['type']=='disabled'
298
  setState(() {
299
+ othersTouchPoints.remove(map['id']); // delete the touch point
300
  });
301
  }
 
302
  }
 
303
  }
304
 
305
+ void toggleContainer() {
 
 
 
306
  // to toggle the visibility of the center popup container
307
+ if (containerHeight == 0) {
308
  setState(() {
309
+ containerHeight = 200;
310
+ containerWidth = 220;
311
  });
312
+ } else {
 
313
  setState(() {
314
+ containerHeight = 0;
315
+ containerWidth = 0;
316
  });
317
  }
318
  }
319
 
320
+ void createRoom({String roomId = ""}) async {
321
  // it is used to create a room in server
322
  // or to connect to an existing room
323
  Map data;
324
+ print(isConnected());
325
 
326
+ if (!isConnected()) {
327
  return;
328
  }
329
 
 
333
  "username": username,
334
  };
335
 
336
+ // to join existing room
337
+ if (roomId.isNotEmpty) {
338
+ data["roomId"] = roomId;
339
  }
340
+ socket.send(json.encode(data));
341
 
342
  // we will get in the room with the server data
343
  // from the room_update response
344
  }
345
 
346
+ void joinRoomForm() {
347
  // popup for entering the room id to join an existing room
348
+
349
  showDialog(
350
+ context: context,
351
+ builder: (context) {
 
352
  return AlertDialog(
353
+ content: SingleChildScrollView(
354
+ child: Column(children: [
355
+ TextField(
356
+ decoration: const InputDecoration(
357
+ hintText: "Enter Room ID",
358
+ ),
359
+ onSubmitted: (value) {
360
+ Navigator.pop(context); // close the dialog
361
+ createRoom(roomId: value); // join room
362
+ },
363
+ ),
364
+ ])),
 
 
 
 
365
  );
366
  },
367
  );
368
  }
 
 
369
 
370
+ Widget header() {
371
+ Color buttoncolor = const Color.fromARGB(255, 70, 70, 70);
372
+
373
  return Padding(
374
+ key: headerKey,
375
+ padding: const EdgeInsets.symmetric(vertical: 20),
376
+ child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
377
+ // sub row to group create/join room button and roomuserslist
378
+ Row(children: [
379
+ // create/join room popup
380
+ CircleAvatar(
381
+ backgroundColor: buttoncolor,
382
+ child: PopupMenuButton(
383
+ onSelected: (value) {
384
+ switch (value) {
385
+ case 'createRoom':
386
+ return createRoom();
387
+ case 'joinRoom':
388
+ return joinRoomForm();
389
+ default:
390
+ throw UnimplementedError();
391
+ }
392
+ },
393
+ icon: const Icon(Icons.add, color: Colors.white), // + icon
394
+ tooltip: "Create/Join Room",
395
+ itemBuilder: (context) => const [
396
+ PopupMenuItem(
397
+ value: "createRoom",
398
+ child: Text("Create Room"),
399
+ ),
400
+ PopupMenuItem(
401
+ value: "joinRoom",
402
+ child: Text("Join Room"),
403
+ ),
404
+ ],
405
+ ),
406
+ ),
407
 
408
+ // users list
409
+ Container(
410
+ margin: const EdgeInsets.only(left: 10),
411
+ child: Row(children: [
412
+ ...userList.map(
413
+ (user) {
414
+ return Container(
415
+ // each person's name who is in a room
416
+ height: 50,
417
+ width: 50,
418
+ margin: const EdgeInsets.only(left: 4),
419
+ alignment: Alignment.center,
420
+ decoration: BoxDecoration(
421
+ color: Colors
422
+ .white, // white background // we can also use their profilecolor here
423
+ borderRadius: BorderRadius.circular(50),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  ),
425
+ child: Text(
426
+ user["username"], // their username
427
+ style: const TextStyle(
428
+ color: Colors.black,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  ),
430
  ),
431
+ );
432
+ },
433
+ ).toList(),
434
+ ]),
435
+ )
436
+ ]),
437
+
438
+ // centered Lobby thing
439
+ GestureDetector(
440
+ onTap: toggleContainer,
441
+ onLongPress: () async {
442
+ // long press to copy roomid to clipboard
443
+ // copy to clipboard
444
+ await Clipboard.setData(ClipboardData(text: currRoomId));
445
+ showSnackBar("copied!");
446
+ },
447
+ child: Row(// roomid+dropdown_icon
448
+ children: [
449
+ // roomid
450
+ Text(
451
+ currRoomName.isEmpty ? "Lobby" : currRoomName,
452
+ style: const TextStyle(
453
+ fontSize: 20,
454
+ fontWeight: FontWeight.w500,
455
+ overflow: TextOverflow.ellipsis,
456
  ),
457
+ ),
458
 
459
+ // icon based on if the container is visible or not
460
+ if (containerHeight == 0) ...[
461
+ const Icon(Icons.arrow_drop_down),
462
+ ] else ...[
463
+ const Icon(Icons.arrow_drop_up),
464
+ ]
465
+ ]),
466
+ ),
 
 
 
 
 
467
 
468
+ // settings button
469
+ CircleAvatar(
470
+ backgroundColor: buttoncolor,
471
+ child: IconButton(
472
+ tooltip:
473
+ "setings", // text which is showed when long pressing or hovering
474
+ onPressed: () {}, // does nothing till now
475
+ icon: const Icon(
476
+ Icons.settings,
477
+ color: Colors.white,
478
+ ),
479
+ ),
480
+ ),
481
  ]),
 
482
  );
483
  }
484
 
485
+ void startSendTouch() {
 
486
  // to start the send_touch scheduler
487
 
488
+ // endSendTouchTimer.cancel();
489
+
490
+ sendTouchScheduler =
491
+ Timer.periodic(Duration(milliseconds: touchRefreshRate), (timer) async {
492
+ // send touch to server
493
+ Map data;
494
+ double staticHeight = getHeaderHeight()! +
495
+ outsidepadding *
496
+ 2; // this is the extra static height which change with resize
497
+ double staticWidth = outsidepadding *
498
+ 2; // this is the extra static height which change with resize
499
+
500
+ // debugPrint("isaAlive:"+channel.isAliave.toString());
501
+ debugPrint("isConnected():" + isConnected().toString());
502
+ // debugPrint("see:"+channel.sink.toString());
503
+ if (!isConnected()) {
504
+ return;
 
 
 
 
 
 
 
505
  }
506
+ data = {
507
+ "route": "send_touch",
508
+ "id": 1,
509
+ "type": "enabled", // Whether the vibration is active or not.
510
+ "position": {
511
+ "x": touchX /
512
+ (width -
513
+ staticWidth), // send ratio x e.g. : x:700, width:800, then send 0.875
514
+ "y": touchY /
515
+ (height -
516
+ staticHeight), // send ratio y e.g. : y:400, height:800 then send 0.5
517
+ },
518
+ "color": profileColor, // Hex value. Default: random
519
+ // "intensity"?: 1 // Vibration intensity. Default: 1
520
+ };
521
+ // debugPrint(data['position']['x']);
522
+ // debugPrint(data['position']['y']);
523
+ socket.send(jsonEncode(data));
524
+ });
525
+
526
+ // endSendTouchTimer = Timer(const Duration(milliseconds: 1000),() {sendTouchScheduler.cancel(); });
527
  }
528
 
529
+ void stopSendTouch() async {
530
  // to stop the send_touch scheduler
531
 
532
  sendTouchScheduler.cancel();
533
 
534
  // send one last touch to tell touch is ended
535
  Map data;
536
+ if (!isConnected()) {
537
  return;
538
  }
539
+ data = {
540
  "route": "send_touch",
541
  "id": 1,
542
+ "type": "disabled", // Whether the vibration is active or not.
543
  "position": {
544
  "x": 0,
545
  "y": 0,
 
548
  // "intensity"?: 1 // Vibration intensity. Default: 1
549
  };
550
 
551
+ socket.send(jsonEncode(data));
 
552
  }
553
 
554
+ Widget buildTouchPoint(
555
+ {required bool visible,
556
+ required double x,
557
+ required double y,
558
+ required double size,
559
+ required Color color}) {
 
560
  /*
561
  bool visible: if the touchpoint is visible
562
  double x: touchpoint's center x position
 
565
  double size: touchpoint's size
566
  Color color: touchpoint's color
567
  */
568
+
569
+ color = color.withOpacity(0.5); // make the color a little translucent
570
 
571
  return Visibility(
572
+ visible: visible,
573
  child: AnimatedPositioned(
574
  duration: Duration(milliseconds: touchRefreshRate),
575
+ left: x - size / 2,
576
+ top: y - size / 2,
577
  child: Container(
578
+ width: size,
579
+ height: size,
580
  decoration: BoxDecoration(
581
+ color: color,
582
+ borderRadius: BorderRadius.circular(50),
583
+ boxShadow: [
584
+ BoxShadow(
585
+ color: color,
586
+ blurRadius: 10,
587
+ spreadRadius: 5,
588
+ ),
589
+ ]),
590
  ),
591
  ),
592
  );
 
595
  void detectTouchCollision({
596
  required double x,
597
  required double y,
598
+ }) {
599
  // detect our touch point collision with any other's TouchPoints
600
 
601
+ for (Map map in othersTouchPoints.values) {
602
+ double dx = (map["position"]["x"] - x).abs();
603
+ double dy = (map["position"]["y"] - y).abs();
 
604
  // debugPrint("x:"+x.toString()+"\ty:"+y.toString());
605
  // debugPrint("dx:"+dx.toString()+"\tdy:"+dy.toString());
606
+ if (dx <= touchsize && dy <= touchsize) {
607
+ if (hasCollided == false) {
 
608
  // Collision Started
609
  debugPrint("Touch Collision started");
610
 
611
  startHapticFeedback();
612
+
613
  setState(() {
614
+ hasCollided = true;
615
  });
616
  }
617
+ } else {
618
+ if (hasCollided == true) {
 
 
619
  // collision ended
620
  debugPrint("Touch Collision ended");
621
+
622
  endHapticFeedback();
623
 
624
  setState(() {
625
+ hasCollided = false;
626
  });
627
  }
628
  // debugPrint("No Touch Collision.");
 
630
  }
631
  }
632
 
633
+ void startHapticFeedback() async {
634
+ if (kIsWeb) {
 
635
  // different handler for web
636
  // showSnackBar("web");
637
  // off,on,off
638
+ Vibration.vibrate(pattern: [20, 100], repeat: 0); // short fast
639
  // Vibration.vibrate(pattern:[100, 200, 400],repeat: 0); // slow
640
+ } else {
 
641
  // showSnackBar("mobile");
642
+ if (await Vibration.hasVibrator() == true) {
643
  // off,on,off
644
+ Vibration.vibrate(pattern: [20, 100], repeat: 0); // short fast
645
+ // Vibration.vibrate(pattern:[100, 200, 400],repeat: 0); // slow
646
  }
647
  }
 
648
  }
649
 
650
+ void endHapticFeedback() async {
651
+ if (kIsWeb) {
652
  // different handler for web
653
  Vibration.cancel();
654
+ } else {
655
+ if (await Vibration.hasVibrator() == true) {
 
656
  Vibration.cancel();
657
  }
658
  }
659
  }
660
 
661
+ void detectTouchStart(DragStartDetails details) {
662
  // to detect touch start on screen to start the send_touch scheduler
663
+
664
  // debugPrint(details.localPosition);
665
 
666
  // update our touch point on our screen
667
  setState(() {
668
  touchX = details.localPosition.dx;
669
  touchY = details.localPosition.dy;
670
+ isTouching = true; // show it
671
  });
672
 
 
673
  // detect Collision for haptic feedback
674
  detectTouchCollision(
675
+ x: details.localPosition.dx,
676
+ y: details.localPosition.dy,
677
  );
678
+
 
679
  // start send Touch Scheduler
680
+ if (currRoomId.isNotEmpty) {
681
+ // only send touch if we are part of a room
682
+ SchedulerBinding.instance.addPostFrameCallback((_) {
683
  startSendTouch();
684
  });
685
+ } else {
 
686
  // we are not joined in a room
687
+ // so can't send_touch
688
  }
 
689
  }
690
+
691
+ void detectTouchUpdate(DragUpdateDetails details) {
692
  // to update touchpoint
693
 
694
  // debugPrint(details.localPosition);
 
701
 
702
  // detect Collision
703
  detectTouchCollision(
704
+ x: details.localPosition.dx,
705
+ y: details.localPosition.dy,
706
  );
 
707
  }
708
 
709
+ void detectTouchEnd(DragEndDetails details) {
 
 
710
  // triggered on touch end
711
 
712
  // update our touch point on our screen
713
  setState(() {
714
  touchX = 0;
715
  touchY = 0;
716
+ isTouching = false; // hide it
717
  });
718
+
719
  // detect Collision end
720
  detectTouchCollision(
721
+ x: -1000,
722
+ y: -1000,
723
  );
724
 
 
725
  // stop send Touch Scheduler and send stop touch message
726
+ if (currRoomId.isNotEmpty) {
727
+ SchedulerBinding.instance.addPostFrameCallback((_) {
728
  stopSendTouch();
729
  });
730
+ } else {
 
731
  // can't send_touch end to server cuz we are not in a room
732
  }
733
  }
734
 
735
+ Widget mainSection() {
736
  // this is the main touch area
737
 
738
+ return Expanded(
739
+ // expanded for it to take up all the available space
740
 
741
  child: GestureDetector(
742
+ onPanStart: (details) {
743
+ detectTouchStart(details); // on touch start
744
  },
745
+ onPanUpdate: (details) {
746
+ detectTouchUpdate(details); // on touch update
747
  },
748
+ onPanEnd: (details) {
749
+ detectTouchEnd(details); // on touch end
750
  },
751
+ child: Container(
752
  decoration: BoxDecoration(
753
+ color: Colors.grey.shade900,
754
  borderRadius: BorderRadius.circular(50),
755
  ),
756
  clipBehavior: Clip.hardEdge,
757
+ child: Stack(clipBehavior: Clip.hardEdge, children: [
758
+ // render received touch points
759
+ ...othersTouchPoints.values.map<Widget>((map) {
760
+ return buildTouchPoint(
761
+ visible: true,
762
+ x: map['position']['x'],
763
+ y: map['position']['y'],
764
+ size: touchsize,
765
+ color: colorFromHex(map['color'])!,
 
 
 
766
  );
767
+ }).toList(),
768
+
769
+ // our touch point
770
+ buildTouchPoint(
771
+ visible: isTouching,
772
+ x: touchX,
773
+ y: touchY,
774
+ size: touchsize,
775
+ color: colorFromHex(profileColor)!,
776
+ ),
 
 
777
  ]),
778
  ),
779
  ),
780
  );
 
781
  }
782
 
 
783
  @override
784
+ Widget build(BuildContext context) {
785
+ width = MediaQuery.of(context).size.width; // to get screen width
786
+ height = MediaQuery.of(context).size.height; // to get screen height
787
 
788
  return Scaffold(
789
+ body: Stack(alignment: Alignment.center, children: [
790
+ //outermost background
791
+ Container(
792
+ color: const Color.fromARGB(255, 14, 14, 14),
793
+ ),
794
 
795
+ // header and main touch area
796
+ Padding(
797
+ padding: EdgeInsets.all(outsidepadding),
798
+ child: Column(
799
+ crossAxisAlignment: CrossAxisAlignment
800
+ .stretch, // mainSection expands in height because of this
801
+ children: [
802
+ header(),
803
+ mainSection(),
804
+ ],
805
  ),
806
+ ),
807
 
808
+ //floating Room menu
809
+ Positioned(
810
+ top: 100,
811
+ child: AnimatedContainer(
812
+ duration: const Duration(milliseconds: 100),
813
+ height: containerHeight,
814
+ width: containerWidth,
815
+ clipBehavior: Clip.hardEdge,
816
+ curve: Curves.easeIn,
817
+ decoration: BoxDecoration(
818
+ color: const Color.fromARGB(255, 67, 67, 67),
819
+ borderRadius: BorderRadius.circular(20),
820
  ),
821
+ child: ListView(children: [
822
+ Container(
823
+ margin: const EdgeInsets.all(20) +
824
+ const EdgeInsets.symmetric(horizontal: 40),
825
+ child: TextButton(
826
+ // exit room button
827
+ onPressed: () {},
828
+ style: TextButton.styleFrom(
829
+ backgroundColor: const Color.fromARGB(255, 239, 83, 80),
830
+ foregroundColor: const Color.fromARGB(255, 255, 255, 255),
831
+ shape: RoundedRectangleBorder(
832
+ borderRadius: BorderRadius.circular(20)),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
833
  ),
834
+ child: const Text("Exit Room"),
835
+ ),
836
+ ),
837
+ ]),
838
  ),
839
+ ),
840
  ]),
 
841
  );
842
  }
843
+ }
client/lib/ws_server_connection_handler.dart CHANGED
@@ -1,79 +1,145 @@
 
 
 
1
  import 'package:web_socket_channel/web_socket_channel.dart';
2
  import 'package:web_socket_channel/status.dart' as status;
 
 
 
 
3
 
4
- class CustomWSChannel{
 
5
  /*
6
  created custom class for web socket handler
7
  with support for reconnect
8
  */
9
- late WebSocketChannel channel; // to store the websocketchannel
10
- bool webSocketConnected=false; // boolean flag to check if we are connected to the websocket
11
- int reconnectAttempts=0; // to keep track of how many times we have tried to reconnect
 
 
 
12
  late Uri serverUri;
13
- int delay; // to set amount of delay between reconnects
14
- void Function(dynamic) onMessage; // function which is called everytime we receive an message from the server
15
- void Function(String,dynamic) onErrorShowMessage; // triggered everytime an error occur
16
- void Function() onConnect; // function which is called everytime we are connected to the server
17
-
18
-
19
- CustomWSChannel(serverUrl,{this.delay=5,required this.onConnect,required this.onMessage,required this.onErrorShowMessage}){
20
- serverUri=Uri.parse(serverUrl);
 
 
 
 
 
 
 
 
 
21
  connect();
22
  }
23
 
 
24
  get sink => channel.sink;
25
- get ready =>channel.ready;
26
-
27
- Future<bool> isConnected() async{
28
- try{
29
- await channel.ready;
30
- return true;
31
- }
32
- catch(e){
33
- debugFn("we are not connected");
34
- return false;
35
- }
 
 
 
 
 
36
  }
37
 
38
- void onDone() async{
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  debugFn("Reconnecting in $delay seconds, attempt $reconnectAttempts ");
40
- webSocketConnected=false;
41
  // channel = null;
42
- channel.sink.close(status.goingAway); // close old connection
43
  await Future.delayed(Duration(seconds: delay));
44
  connect();
45
  }
46
- void onError(error){
47
- debugFn("Error while connecting.",e:error);
 
48
  reconnectAttempts += 1;
49
  }
50
 
51
- void connect() async{
52
-
53
- try{
54
- channel=WebSocketChannel.connect(
55
- serverUri
56
- );
 
 
 
 
 
 
 
 
 
57
 
58
- channel.stream.listen(onMessage, onDone:onDone, onError:onError);
59
- // channel.stream.listen(onMessage, onError:onError);
60
  await channel.ready;
61
- onConnect();
62
- }
63
- catch(e){
64
- debugFn("can't connect to the server",e:e);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
66
 
67
-
68
  }
69
 
70
-
71
- void debugFn(String s,{e=""}){
72
  // print("space");
73
  // print("Custom Message:"+s);
74
  // print("Custom Error:"+e.toString());
75
 
76
- onErrorShowMessage(s,e);
77
-
78
- }
79
- }
 
1
+ import 'dart:convert';
2
+ import 'dart:io';
3
+
4
  import 'package:web_socket_channel/web_socket_channel.dart';
5
  import 'package:web_socket_channel/status.dart' as status;
6
+ import 'package:flutter/scheduler.dart';
7
+ import "dart:async";
8
+ import 'package:flutter/src/foundation/print.dart';
9
+
10
 
11
+
12
+ class CustomWSChannel {
13
  /*
14
  created custom class for web socket handler
15
  with support for reconnect
16
  */
17
+ late WebSocketChannel channel; // to store the websocketchannel
18
+ bool isAlive =
19
+ false; // boolean flag to check if we are connected to the websocket
20
+ bool isConnected = false;
21
+ int reconnectAttempts =
22
+ 0; // to keep track of how many times we have tried to reconnect
23
  late Uri serverUri;
24
+ int delay; // to set amount of delay between reconnects
25
+ int pingInterval; // pings to the server after every pingInterval time and if pong is not received close the socket
26
+ Timer pingScheduler=Timer(Duration.zero, () { });
27
+ void Function(dynamic)
28
+ onMessage; // function which is called everytime we receive an message from the server
29
+ void Function(String, dynamic)
30
+ onErrorShowMessage; // triggered everytime an error occur
31
+ void Function()
32
+ onConnect; // function which is called everytime we are connected to the server
33
+
34
+ CustomWSChannel(serverUrl,
35
+ {this.delay = 5,
36
+ this.pingInterval = 5000,
37
+ required this.onConnect,
38
+ required this.onMessage,
39
+ required this.onErrorShowMessage}) {
40
+ serverUri = Uri.parse(serverUrl);
41
  connect();
42
  }
43
 
44
+
45
  get sink => channel.sink;
46
+ get ready => channel.ready;
47
+
48
+ // Future<bool> isConnected() async{
49
+ // try{
50
+ // await channel.ready;
51
+ // return true;
52
+ // }
53
+ // catch(e){
54
+ // debugFn("we are not connected");
55
+ // return false;
56
+ // }
57
+ // }
58
+ void ping() {
59
+ // send ping
60
+ // channel.
61
+ channel.sink.add(jsonEncode({"route": "get_pong"}));
62
  }
63
 
64
+ void handlepong() {
65
+ //
66
+ isAlive = true;
67
+ isConnected = true;
68
+
69
+ }
70
+
71
+ void onDone() async {
72
+ isConnected = false;
73
+ isAlive = false;
74
+
75
+ debugPrint("onDone called");
76
+ pingScheduler.cancel();
77
+ await channel.sink.close();
78
  debugFn("Reconnecting in $delay seconds, attempt $reconnectAttempts ");
79
+
80
  // channel = null;
81
+
82
  await Future.delayed(Duration(seconds: delay));
83
  connect();
84
  }
85
+
86
+ void onError(error) {
87
+ debugFn("Error while connecting.", e: error);
88
  reconnectAttempts += 1;
89
  }
90
 
91
+ void connect() async {
92
+ final Timer connectTimeout;
93
+ try {
94
+ channel = WebSocketChannel.connect(serverUri);
95
+
96
+ channel.stream.listen(onMessage, onDone: onDone, onError: onError);
97
+ // channel.stream.listen(onMessage, onError:onError);
98
+
99
+ connectTimeout = Timer(const Duration(seconds: 5),(){
100
+
101
+ debugPrint('connectTimeout');
102
+ channel.sink.close();
103
+ // channel.
104
+ // onDone();
105
+ });
106
 
107
+ debugPrint('before channel.ready');
 
108
  await channel.ready;
109
+ debugPrint('after channel.ready');
110
+
111
+ connectTimeout.cancel();
112
+
113
+ // await channel.sink.done;
114
+ // await channel.stream.drain();
115
+ // channel.stream.
116
+
117
+ debugPrint('Reconnected successfully');
118
+
119
+ // isAlive = true;
120
+ // isConnected = true;
121
+
122
+ // onConnect();
123
+ // pingScheduler =
124
+ // Timer.periodic(Duration(milliseconds: pingInterval), (timer) async {
125
+ // if(isAlive==false) return onDone();
126
+
127
+ // ping();
128
+ // // set isAlive False
129
+ // isAlive = false;
130
+
131
+ // });
132
+ } catch (e) {
133
+ debugFn("can't connect to the server", e: e);
134
  }
135
 
 
136
  }
137
 
138
+ void debugFn(String s, {e = ""}) {
 
139
  // print("space");
140
  // print("Custom Message:"+s);
141
  // print("Custom Error:"+e.toString());
142
 
143
+ onErrorShowMessage(s, e);
144
+ }
145
+ }
 
client/pubspec.yaml CHANGED
@@ -15,6 +15,7 @@ dependencies:
15
  shared_preferences: ^2.2.2
16
  vibration: ^1.8.3
17
  web_socket_channel: ^2.4.0
 
18
 
19
  dev_dependencies:
20
  flutter_test:
 
15
  shared_preferences: ^2.2.2
16
  vibration: ^1.8.3
17
  web_socket_channel: ^2.4.0
18
+ web_socket_client: ^0.1.0
19
 
20
  dev_dependencies:
21
  flutter_test: