Spaces:
Sleeping
Sleeping
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/services.dart'; // for going in fullscreen mode | |
| import 'package:flutter/scheduler.dart'; // to add scheduler for touch collision detection | |
| import 'package:shared_preferences/shared_preferences.dart'; | |
| // import 'package:web_socket_channel/status.dart' as status; | |
| import 'ws_server_connection_handler.dart'; | |
| import 'package:flutter_colorpicker/flutter_colorpicker.dart'; | |
| import 'package:flutter/foundation.dart' show kIsWeb; // to check of platform is web | |
| import 'vibration_exporter.dart'; | |
| import "dart:convert"; | |
| import "dart:async"; | |
| void enterFullScreen(){ | |
| SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive,overlays: []); | |
| } | |
| void exitFullScreen(){ | |
| SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,overlays:SystemUiOverlay.values); | |
| } | |
| class HomeScreen extends StatefulWidget{ | |
| const HomeScreen({super.key}); | |
| State<HomeScreen> createState(){ | |
| return HomeScreenState(); | |
| } | |
| } | |
| class HomeScreenState extends State<HomeScreen>{ | |
| late CustomWSChannel channel; | |
| late double width,height; | |
| late SharedPreferences prefs; | |
| double containerHeight = 0,containerWidth=0; | |
| late Timer sendTouchScheduler; | |
| double touchsize=100; | |
| double touchX = 0,touchY=0; | |
| bool isTouching=false; | |
| bool hasCollided=false; | |
| late List<String> roomidList,roomnameList; | |
| String currRoomId="",currRoomName=""; | |
| String username=""; | |
| String userid=""; | |
| String profileColor="ff000000"; | |
| Map<String,Map> othersTouchPoints={}; | |
| final double outsidepadding=15; | |
| final int touchRefreshRate = 1000~/30; // 30 fps | |
| // final int touchRefreshRate = 1000~/60; // 60 fps | |
| GlobalKey headerKey = GlobalKey(); // to calculate the header's height | |
| void initState(){ | |
| super.initState(); | |
| enterFullScreen(); | |
| roomidList=[]; | |
| roomnameList=[]; | |
| // roomidList=["1","2"]; | |
| // roomnameList=["room 1","room 2"]; | |
| Map data={ | |
| "user": { | |
| "username": "enemy", | |
| "id": "string", | |
| }, | |
| "position": { | |
| "x": 0.0+touchsize/2, // top left corner | |
| "y": 0.0+touchsize/2, // top left corner | |
| }, | |
| "color": "ff2244bb", // Hex value. | |
| "intensity": 1, // Vibration intensity. | |
| }; | |
| othersTouchPoints[data['user']['id']]=data; | |
| // just for debugging | |
| WidgetsBinding.instance.addPostFrameCallback((_){ | |
| getPreferences(); | |
| // setPreferences(); | |
| connectWebsocket(); | |
| setUsername(); | |
| }); | |
| // WidgetsBinding.instance.addPostFrameCallback((_){ | |
| // ScaffoldMessenger.of(context).hideCurrentSnackBar(); | |
| // ScaffoldMessenger.of(context).showSnackBar( | |
| // SnackBar( | |
| // content:Text(getHeaderHeight().toString()) | |
| // ) | |
| // ); | |
| // }); | |
| // vibration_web.test(); | |
| // vibration_web.test(); | |
| // vibration_web.vibrate(pattern:[200,100]); | |
| // vibration_web.vibrate(pattern:[100]); | |
| // debugdebugPrint("asdasdas"); | |
| } | |
| void dispose(){ | |
| exitFullScreen(); | |
| channel.sink.close(); | |
| super.dispose(); | |
| } | |
| void showSnackBar(String text){ | |
| ScaffoldMessenger.of(context).hideCurrentSnackBar(); | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar( | |
| content:Text(text) | |
| ) | |
| ); | |
| } | |
| double? getHeaderHeight(){ | |
| var size=headerKey.currentContext?.size; | |
| // debugPrint(size?.height); | |
| return size?.height; | |
| } | |
| void getPreferences() async{ | |
| prefs = await SharedPreferences.getInstance(); | |
| // initialize empty room list if no preferences found | |
| if(!prefs.containsKey("roomid_list")){ | |
| prefs.setStringList("roomid_list",[]); | |
| prefs.setStringList("roomname_list",[]); | |
| } | |
| // intialize from preferences | |
| setState(() { | |
| roomidList=prefs.getStringList("roomid_list")!; | |
| roomnameList=prefs.getStringList("roomname_list")!; | |
| username=prefs.getString("username")!; | |
| profileColor=prefs.getString("profile_color")!; | |
| }); | |
| } | |
| void connectWebsocket() async{ | |
| channel=CustomWSChannel( | |
| const String.fromEnvironment( | |
| "WS_SERVER_URL", | |
| defaultValue: "ws://localhost:3000" | |
| ), | |
| onMessage: responseHandler, | |
| // onMessage: (value){}, | |
| onErrorShowMessage:(msg,e){showSnackBar(msg);}, | |
| ); | |
| if(!await channel.isConnected()){ | |
| return; | |
| } | |
| Object data = { | |
| "route":"test_connection" | |
| }; | |
| channel.sink.add(json.encode(data)); | |
| } | |
| void responseHandler(message) { | |
| // message.get | |
| Map map; | |
| final String type; | |
| try{ | |
| map = jsonDecode(message); | |
| } | |
| catch(e){ | |
| debugPrint("can't decode to json:\t$message"); | |
| return; | |
| } | |
| // if any error found do nothing | |
| if(map.containsKey("error")){ | |
| debugPrint(map.toString()); | |
| return; | |
| } | |
| type = map["message"]; | |
| if(type=="test_connection_response"){ | |
| // testing connection | |
| debugPrint("test_connection successful"); | |
| } | |
| else if(type=="join_room_response"){ | |
| debugPrint(map.toString()); | |
| } | |
| else if(type=="set_username_response"){ | |
| debugPrint(map.toString()); | |
| setState(() { | |
| userid=map['user']['id']; | |
| }); | |
| } | |
| else if(type=="get_rooms_response"){ | |
| debugPrint(map.toString()); | |
| } | |
| else if(type=="room_update"){ | |
| debugPrint(map.toString()); | |
| enterRoom(map); | |
| } | |
| else if(type=="send_vibration_response"){ // handles when we send_touch without being in a room | |
| debugPrint(map.toString()); | |
| showSnackBar(map['status']); | |
| setState(() { | |
| currRoomId=""; | |
| currRoomName=""; | |
| }); | |
| } | |
| else if(type=="receive_touch"){ | |
| if(map['user']['id']==userid){ | |
| // debugPrint("self touch received. Ignore this"); | |
| return; | |
| } | |
| // touch received to update othersTouchPoints | |
| // debugPrint(map); | |
| if(map['type']=='enabled'){ // add touch point | |
| // static width and height | |
| double staticHeight=getHeaderHeight()!+outsidepadding*2; // this is the extra static height which change with resize | |
| double staticWidth=outsidepadding*2; // this is the extra static height which change with resize | |
| // rescale point according to the screen | |
| map['position']['x']*=width-staticWidth; | |
| map['position']['y']*=height-staticHeight; | |
| // debugPrint(map['position']['x'].toString()); | |
| // debugPrint(map['position']['y'].toString()); | |
| setState(() { | |
| othersTouchPoints[map['id']]=map; | |
| }); | |
| } | |
| else{// delete the touch point | |
| setState(() { | |
| othersTouchPoints.remove(map['id']); | |
| }); | |
| } | |
| } | |
| } | |
| void setUsername() async{ | |
| prefs = await SharedPreferences.getInstance(); | |
| if(!await channel.isConnected()){ | |
| return; | |
| } | |
| // set username | |
| Map data = { | |
| "route": "set_username", | |
| "username": prefs.getString("username"), | |
| }; | |
| channel.sink.add(json.encode(data)); | |
| } | |
| void enterRoom(Map roomdata){ | |
| setState((){ | |
| currRoomId=roomdata['roomId']; | |
| currRoomName=roomdata['roomId']; | |
| }); | |
| } | |
| void toggleContainer(){ | |
| if(containerHeight==0){ | |
| setState(() { | |
| containerHeight=200; | |
| containerWidth=width; | |
| }); | |
| } | |
| else{ | |
| setState(() { | |
| containerHeight=0; | |
| containerWidth=0; | |
| }); | |
| } | |
| } | |
| void createRoom({String roomId=""}) async{ | |
| // it is used to create a room in server | |
| Map data; | |
| if(!await channel.isConnected()){ | |
| return; | |
| } | |
| // join room | |
| data = { | |
| "route": "join_room", | |
| "username": username, | |
| }; | |
| // to join existing room | |
| if(roomId.isNotEmpty){ | |
| data["roomId"]=roomId; | |
| } | |
| channel.sink.add(json.encode(data)); | |
| // we will move to the room screen with the server data | |
| // from the room_update callback | |
| } | |
| void joinRoomForm(){ | |
| // close old dialog | |
| showDialog( | |
| context: context, | |
| builder:(context) { | |
| // return SingleChildScrollView( | |
| // child: Text("Enter Room ID"), | |
| // ); | |
| return AlertDialog( | |
| content:SingleChildScrollView( | |
| child:Column( | |
| children: [ | |
| TextField( | |
| decoration:const InputDecoration( | |
| hintText: "Enter Room ID", | |
| ), | |
| onSubmitted: (value){ | |
| Navigator.pop(context); // close the dialog | |
| createRoom(roomId:value); // join room | |
| }, | |
| ), | |
| ]) | |
| ), | |
| ); | |
| }, | |
| ); | |
| } | |
| Widget header(){ | |
| Color buttoncolor=const Color.fromARGB(255, 70, 70, 70); | |
| return Padding( | |
| key:headerKey, | |
| padding:const EdgeInsets.symmetric(vertical:20), | |
| child:Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
| children: [ | |
| // create/join room popup | |
| CircleAvatar( | |
| backgroundColor: buttoncolor, | |
| child: PopupMenuButton( | |
| onSelected: (value){ | |
| switch(value){ | |
| case 'createRoom': | |
| return createRoom(); | |
| case 'joinRoom': | |
| return joinRoomForm(); | |
| default: | |
| throw UnimplementedError(); | |
| } | |
| }, | |
| icon:const Icon(Icons.add,color:Colors.white), | |
| tooltip: "Create/Join Room", | |
| itemBuilder: (context)=>const[ | |
| PopupMenuItem( | |
| value:"createRoom", | |
| child: Text("Create Room"), | |
| ), | |
| PopupMenuItem( | |
| value:"joinRoom", | |
| child: Text("Join Room"), | |
| ), | |
| ], | |
| ), | |
| ), | |
| GestureDetector( | |
| onTap:toggleContainer, | |
| child:Row( | |
| children:[ | |
| Text( | |
| currRoomName.isEmpty?"Lobby":currRoomName, | |
| style:const TextStyle( | |
| fontSize: 20, | |
| fontWeight: FontWeight.w500, | |
| overflow:TextOverflow.ellipsis, | |
| ), | |
| ), | |
| if(containerHeight==0)...[ | |
| const Icon(Icons.arrow_drop_down), | |
| ] | |
| else...[ | |
| const Icon(Icons.arrow_drop_up), | |
| ] | |
| ]), | |
| ), | |
| // setting button | |
| CircleAvatar( | |
| backgroundColor: buttoncolor, | |
| child:IconButton( | |
| // iconSize:30, | |
| tooltip: "setings", | |
| onPressed: (){}, | |
| icon:const Icon( | |
| Icons.settings, | |
| color:Colors.white, | |
| ), | |
| ), | |
| ), | |
| ]), | |
| ); | |
| } | |
| // runs every 1 second | |
| void startSendTouch(){ | |
| sendTouchScheduler=Timer.periodic( | |
| Duration(milliseconds: touchRefreshRate), (timer) async { | |
| // send touch to server | |
| Map data; | |
| double staticHeight=getHeaderHeight()!+outsidepadding*2; // this is the extra static height which change with resize | |
| double staticWidth=outsidepadding*2; // this is the extra static height which change with resize | |
| if(!await channel.isConnected()){ | |
| return; | |
| } | |
| data={ | |
| "route": "send_touch", | |
| "id": 1, // Used to indentify vibrations for updating or disabling them | |
| "type": "enabled", // Whether the vibration is active or not. | |
| "position": { | |
| // "x":20, | |
| // "y":20, | |
| "x": touchX/(width-staticWidth), // send ratio x e.g. : x:700, width:800, then send 0.9 | |
| "y": touchY/(height-staticHeight), // send ratio y e.g. : y:400, height:800 then send 0.5 | |
| }, | |
| "color": profileColor, // Hex value. Default: random | |
| // "intensity"?: 1 // Vibration intensity. Default: 1 | |
| }; | |
| // debugPrint(data['position']['x']); | |
| // debugPrint(data['position']['y']); | |
| channel.sink.add(jsonEncode(data)); | |
| } | |
| ); | |
| } | |
| void stopSendTouch() async{ | |
| sendTouchScheduler.cancel(); | |
| // send one last touch to tell touch is ended | |
| Map data; | |
| if(!await channel.isConnected()){ | |
| return; | |
| } | |
| data={ | |
| "route": "send_touch", | |
| "id": 1, // Used to indentify vibrations for updating or disabling them | |
| "type": "disabled", // Whether the vibration is active or not. | |
| "position": { | |
| // "x":20, | |
| // "y":20, | |
| "x": 0, // send ratio x e.g. : x:700, width:800, then send 0.9 | |
| "y": 0, // send ratio y e.g. : y:400, height:800 then send 0.5 | |
| }, | |
| "color": profileColor, // Hex value. Default: random | |
| // "intensity"?: 1 // Vibration intensity. Default: 1 | |
| }; | |
| channel.sink.add(jsonEncode(data)); | |
| } | |
| Widget buildTouchPoint({ | |
| required bool visible, | |
| required double x, | |
| required double y, | |
| required double size, | |
| required Color color | |
| }){ | |
| color=color.withOpacity(0.5); | |
| return Visibility( | |
| visible:visible, | |
| child: AnimatedPositioned( | |
| duration: Duration(milliseconds: touchRefreshRate), | |
| left:x-size/2, | |
| top:y-size/2, | |
| child: Container( | |
| width:size, | |
| height:size, | |
| decoration: BoxDecoration( | |
| color:color, | |
| borderRadius: BorderRadius.circular(50), | |
| boxShadow: [ | |
| BoxShadow( | |
| color:color, | |
| blurRadius: 10, | |
| spreadRadius: 5, | |
| ), | |
| ]), | |
| ), | |
| ), | |
| ); | |
| } | |
| void detectTouchCollision({ | |
| required double x, | |
| required double y, | |
| }){ | |
| // detect our touch point collision with any othersTouchPoints | |
| for(Map map in othersTouchPoints.values){ | |
| double dx = (map["position"]["x"]-x).abs(); | |
| double dy = (map["position"]["y"]-y).abs(); | |
| // debugPrint("x:"+x.toString()+"\ty:"+y.toString()); | |
| // debugPrint("dx:"+dx.toString()+"\tdy:"+dy.toString()); | |
| if (dx<=touchsize && dy<=touchsize){ | |
| if(hasCollided==false){ | |
| // Collision Started | |
| debugPrint("Touch Collision started"); | |
| startHapticFeedback(); | |
| setState(() { | |
| hasCollided=true; | |
| }); | |
| } | |
| } | |
| else{ | |
| if(hasCollided==true){ | |
| // collision ended | |
| debugPrint("Touch Collision ended"); | |
| endHapticFeedback(); | |
| setState(() { | |
| hasCollided=false; | |
| }); | |
| } | |
| // debugPrint("No Touch Collision."); | |
| } | |
| } | |
| } | |
| void startHapticFeedback() async{ | |
| if(kIsWeb){ | |
| // different handler for web | |
| // showSnackBar("web"); | |
| // Vibration.vibrate(pattern:[100,100,20]); | |
| // Vibration.vibrate(pattern:[1000,1000,200]); | |
| // off,on,off | |
| Vibration.vibrate(pattern:[20,100], repeat: 0); // short fast | |
| // Vibration.vibrate(pattern:[200,2000,200], repeat: 0); // short fast | |
| // HapticFeedback.heavyImpact(); | |
| } | |
| else{ | |
| // showSnackBar("mobile"); | |
| if (await Vibration.hasVibrator()==true) { | |
| // off,on,off | |
| Vibration.vibrate(pattern:[20,100], repeat: 0); // short fast | |
| // Vibration.vibrate(pattern:[100, 200, 400],repeat: 0); // slow | |
| } | |
| } | |
| } | |
| void endHapticFeedback() async{ | |
| if(kIsWeb){ | |
| // different handler for web | |
| Vibration.cancel(); | |
| } | |
| else{ | |
| if (await Vibration.hasVibrator()==true) { | |
| Vibration.cancel(); | |
| } | |
| } | |
| } | |
| void detectTouchStart(DragStartDetails details){ | |
| // we have to stop see touch out of touch area | |
| // debugPrint(details.localPosition); | |
| // detect Collision | |
| detectTouchCollision( | |
| x:details.localPosition.dx, | |
| y:details.localPosition.dy, | |
| ); | |
| setState(() { | |
| touchX = details.localPosition.dx; | |
| touchY = details.localPosition.dy; | |
| isTouching=true; // show it | |
| }); | |
| // start send Touch Scheduler | |
| if(currRoomId.isNotEmpty){ | |
| SchedulerBinding.instance.addPostFrameCallback((_) { | |
| startSendTouch(); | |
| }); | |
| } | |
| else{ | |
| // we are not joined in a room | |
| // so can't send_touch | |
| } | |
| } | |
| void detectTouchUpdate(DragUpdateDetails details){ | |
| // we have to stop see touch out of touch area | |
| // detect Collision | |
| detectTouchCollision( | |
| x:details.localPosition.dx, | |
| y:details.localPosition.dy, | |
| ); | |
| // debugPrint(details.localPosition); | |
| setState(() { | |
| touchX = details.localPosition.dx; | |
| touchY = details.localPosition.dy; | |
| }); | |
| // debugPrint(profileColor); | |
| // debugPrint(details.localPosition); | |
| } | |
| void detectTouchEnd(DragEndDetails details){ | |
| // detect Collision end | |
| detectTouchCollision( | |
| x:-1000, | |
| y:-1000, | |
| ); | |
| setState(() { | |
| touchX = 0; | |
| touchY = 0; | |
| isTouching=false; // hide it | |
| }); | |
| // stop send Touch Scheduler and send stop touch message | |
| if(currRoomId.isNotEmpty){ | |
| SchedulerBinding.instance.addPostFrameCallback((_) { | |
| stopSendTouch(); | |
| }); | |
| } | |
| else{ | |
| // can't send_touch end to server cuz we are not in a room | |
| } | |
| } | |
| Widget mainSection(){ | |
| return Expanded( | |
| child: GestureDetector( | |
| onPanStart:(details){ | |
| detectTouchStart(details); | |
| }, | |
| onPanUpdate:(details){ | |
| detectTouchUpdate(details); | |
| }, | |
| onPanEnd:(details){ | |
| detectTouchEnd(details); | |
| }, | |
| child:Container( | |
| decoration: BoxDecoration( | |
| // color:Colors.grey.shade900, | |
| color:const Color.fromARGB(255, 30, 30, 30), | |
| borderRadius: BorderRadius.circular(50), | |
| ), | |
| clipBehavior: Clip.hardEdge, | |
| child:Stack( | |
| clipBehavior: Clip.hardEdge, | |
| children: [ | |
| // render received touch points | |
| ...othersTouchPoints.values.map<Widget>((map){ | |
| return buildTouchPoint( | |
| visible:true, | |
| x:map['position']['x'], | |
| y:map['position']['y'], | |
| size:touchsize, | |
| color:colorFromHex(map['color'])!, | |
| ); | |
| }).toList(), | |
| // our touch point | |
| buildTouchPoint( | |
| visible:isTouching, | |
| x:touchX, | |
| y:touchY, | |
| size:touchsize, | |
| color:colorFromHex(profileColor)!, | |
| // color:Colors.red, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget build(BuildContext context){ | |
| width=MediaQuery.of(context).size.width; | |
| height=MediaQuery.of(context).size.height; | |
| return Scaffold( | |
| // backgroundColor: Color(0xff0e0e0e), | |
| // backgroundColor: Colors.white, | |
| body:Stack( | |
| alignment: Alignment.center, | |
| children: [ | |
| // background | |
| Container( | |
| color:const Color.fromARGB(255, 14, 14, 14), | |
| ), | |
| //overlay | |
| Padding( | |
| padding: EdgeInsets.all(outsidepadding), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.stretch, | |
| children: [ | |
| header(), | |
| mainSection(), | |
| ], | |
| ), | |
| ), | |
| //floating Room section | |
| Positioned( | |
| top:100, | |
| child: AnimatedContainer( | |
| duration:const Duration(milliseconds: 100), | |
| height: containerHeight, | |
| width: containerHeight, | |
| clipBehavior: Clip.hardEdge, | |
| decoration:const BoxDecoration( | |
| color:Color.fromARGB(255, 67, 67, 67), | |
| // borderRadius: BorderRadius.circular(40), | |
| ), | |
| child:TextFormField( | |
| initialValue:currRoomId, | |
| // decoration: InputDecoration( | |
| // val | |
| // ), | |
| ), | |
| // child: ListView.builder( | |
| // itemCount: roomidList.length, | |
| // itemBuilder: ((context, index){ | |
| // return Row( | |
| // mainAxisAlignment: MainAxisAlignment.center, | |
| // // crossAxisAlignment: CrossAxisAlignment.stretch, | |
| // children: [ | |
| // Expanded( | |
| // child: Container( | |
| // padding:EdgeInsets.all(8), | |
| // decoration: BoxDecoration( | |
| // // border:Border( | |
| // // bottom:BorderSide( | |
| // // color:Color.fromRGBO(200, 200, 200, 1), | |
| // // width: 4 | |
| // // ), | |
| // // ), | |
| // ), | |
| // child: Text( | |
| // roomnameList[index], | |
| // textAlign:TextAlign.center, | |
| // maxLines: 1, | |
| // style:TextStyle( | |
| // fontSize:20, | |
| // overflow: TextOverflow.clip, | |
| // ), | |
| // ) | |
| // ), | |
| // ), | |
| // ], | |
| // ); | |
| // })), | |
| curve: Curves.easeIn, | |
| ), | |
| ), | |
| ]), | |
| ); | |
| } | |
| } |