hapticlink-2 / client /lib /home_screen.dart
Anuj-Panthri's picture
renamed everything to make everything consistent
f21ac8b
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});
@override
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
@override
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");
}
@override
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,
),
],
),
),
),
);
}
@override
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,
),
),
]),
);
}
}